Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/components/icons.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ import MDI_Pencil from 'vue-material-design-icons/PencilOutline.vue'
import MDI_Plus from 'vue-material-design-icons/Plus.vue'
import MDI_Shape from 'vue-material-design-icons/ShapeOutline.vue'
import MDI_Sigma from 'vue-material-design-icons/Sigma.vue'
import MDI_SortAscending from 'vue-material-design-icons/SortAscending.vue'
import MDI_SortDescending from 'vue-material-design-icons/SortDescending.vue'
import MDI_Table from 'vue-material-design-icons/Table.vue'
import MDI_TableSettings from 'vue-material-design-icons/TableCog.vue'
import MDI_TableAddColumnAfter from 'vue-material-design-icons/TableColumnPlusAfter.vue'
Expand Down Expand Up @@ -152,3 +154,5 @@ export const Warn = makeIcon(MDI_Warn)
export const Web = makeIcon(MDI_Web)
export const Plus = makeIcon(MDI_Plus)
export const Sigma = makeIcon(MDI_Sigma)
export const SortAscending = makeIcon(MDI_SortAscending)
export const SortDescending = makeIcon(MDI_SortDescending)
112 changes: 112 additions & 0 deletions src/nodes/Table/Table.js
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,118 @@ export default Table.extend({
)
dispatch(tr.setSelection(selection).scrollIntoView())
}
return true
},
sortColumn:
(direction = 'asc', cell = null) =>
({ state, tr, dispatch }) => {
if (
cell?.type?.name !== 'tableCell'
&& cell?.type?.name !== 'tableHeader'
) {
return false
}

// find the table, its position and the column index of the cell
let table = null
let tablePos = null
let columnIndex = -1

state.doc.descendants((node, pos) => {
if (node.type.name !== 'table') {
return true
}

for (
let rowIndex = 0;
rowIndex < node.childCount;
rowIndex += 1
) {
const row = node.child(rowIndex)
for (
let currentColumnIndex = 0;
currentColumnIndex < row.childCount;
currentColumnIndex += 1
) {
if (row.child(currentColumnIndex) === cell) {
table = node
tablePos = pos
columnIndex = currentColumnIndex
return false
}
}
}

return true
})

if (!table || tablePos === null || columnIndex < 0) return false

const bodyRows = []
const nonBodyChildren = []
table.forEach((child) => {
if (child.type.name === 'tableRow') {
bodyRows.push(child)
return
}
nonBodyChildren.push(child)
})
if (bodyRows.length < 2) return true

// check if all rows have a cell at the column index and that the cell doesn't have colspan or rowspan
const canSortRows = bodyRows.every((row) => {
if (columnIndex >= row.childCount) {
return false
}
const targetCell = row.child(columnIndex)
return (
(targetCell.attrs.colspan ?? 1) === 1
&& (targetCell.attrs.rowspan ?? 1) === 1
)
})
if (!canSortRows) return false

// sort the rows based on the content of the cell at the column index
const collator = new Intl.Collator(undefined, {
numeric: true,
sensitivity: 'base',
})
const sortDirection = direction === 'desc' ? -1 : 1
const sortedRows = bodyRows
.map((row, index) => ({
index,
row,
key: row.child(columnIndex).textContent.trim(),
}))
.sort((a, b) => {
const keyCompare =
collator.compare(a.key, b.key) * sortDirection
if (keyCompare !== 0) {
return keyCompare
}
return a.index - b.index
})

const hasChangedOrder = sortedRows.some(
({ index }, sortedIndex) => index !== sortedIndex,
)
if (!hasChangedOrder) return true

const sortedTable = table.type.createChecked(
table.attrs,
[...nonBodyChildren, ...sortedRows.map(({ row }) => row)],
table.marks,
)

if (dispatch) {
tr.replaceWith(
tablePos,
tablePos + table.nodeSize,
sortedTable,
)
dispatch(tr.scrollIntoView())
}

return true
},
}
Expand Down
35 changes: 33 additions & 2 deletions src/nodes/Table/TableHeaderView.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<!--
- SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
- SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->

<template>
Expand Down Expand Up @@ -48,6 +48,24 @@
</template>
</NcActionButton>
</NcActionButtonGroup>
<NcActionButton
data-text-table-action="sort-column-asc"
close-after-click
@click="sortColumnAsc">
<template #icon>
<SortAscending />
</template>
{{ t('text', 'Sort ascending') }}
</NcActionButton>
<NcActionButton
data-text-table-action="sort-column-desc"
close-after-click
@click="sortColumnDesc">
<template #icon>
<SortDescending />
</template>
{{ t('text', 'Sort descending') }}
</NcActionButton>
<NcActionButton
data-text-table-action="add-column-before"
close-after-click
Expand Down Expand Up @@ -90,6 +108,8 @@ import {
AlignHorizontalCenter,
AlignHorizontalLeft,
AlignHorizontalRight,
SortAscending,
SortDescending,
TableAddColumnAfter,
TableAddColumnBefore,
TrashCan,
Expand All @@ -109,6 +129,8 @@ export default {
NodeViewContent,
TableAddColumnBefore,
TableAddColumnAfter,
SortAscending,
SortDescending,
},
props: {
editor: {
Expand Down Expand Up @@ -191,6 +213,15 @@ export default {
.addColumnAfter()
.run()
},
sortColumnAsc() {
this.sortColumn('asc')
},
sortColumnDesc() {
this.sortColumn('desc')
},
sortColumn(direction) {
this.editor.commands.sortColumn(direction, this.node)
},
t,
},
}
Expand Down
76 changes: 76 additions & 0 deletions src/tests/nodes/Table.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -224,8 +224,84 @@ describe('Table extension', () => {
expect(editor.getHTML()).toBe(editorHtml)
}
})

test('sorts table body rows in ascending order by selected column', ({
editor,
}) => {
editor.commands.setContent(
markdownit.render(
'| col0 | col1 |\n|---|---|\n| 2 | b |\n| 10 | a |\n| 1 | c |\n',
),
)

let cell
cell = getHeaderCell(editor, 0)
expect(editor.commands.sortColumn('asc', cell)).toBe(true)

expect(getBodyColumnValues(editor, 0)).toEqual(['1', '2', '10'])
expect(getBodyColumnValues(editor, 1)).toEqual(['c', 'b', 'a'])

cell = getHeaderCell(editor, 1)
expect(editor.commands.sortColumn('asc', cell)).toBe(true)

expect(getBodyColumnValues(editor, 0)).toEqual(['10', '2', '1'])
expect(getBodyColumnValues(editor, 1)).toEqual(['a', 'b', 'c'])
})

test('sorts table body rows in descending order by selected column', ({
editor,
}) => {
editor.commands.setContent(
markdownit.render(
'| col0 | col1 |\n|---|---|\n| 2 | b |\n| 10 | a |\n| 1 | c |\n',
),
)

let cell
cell = getHeaderCell(editor, 0)
expect(editor.commands.sortColumn('desc', cell)).toBe(true)

expect(getBodyColumnValues(editor, 0)).toEqual(['10', '2', '1'])
expect(getBodyColumnValues(editor, 1)).toEqual(['a', 'b', 'c'])

cell = getHeaderCell(editor, 1)
expect(editor.commands.sortColumn('desc', cell)).toBe(true)

expect(getBodyColumnValues(editor, 0)).toEqual(['1', '2', '10'])
expect(getBodyColumnValues(editor, 1)).toEqual(['c', 'b', 'a'])
})
})

const getHeaderCell = (editor, targetIndex = 0) => {
let cell
editor.state.doc.descendants((node, pos) => {
if (!['tableHeadRow', 'tableRow'].includes(node.type.name)) {
return true
}
if (targetIndex >= node.childCount) {
return false
}

cell = node.child(targetIndex)
return false
})
return cell
}

const getBodyColumnValues = (editor, columnIndex) => {
const values = []
editor.state.doc.descendants((node) => {
if (node.type.name !== 'tableRow') {
return true
}
if (columnIndex < node.childCount) {
values.push(node.child(columnIndex).textContent.trim())
}
return true
})
return values
}

const formatHTML = (html) => {
return html.replaceAll('><', '>\n<').replace(/\n$/, '')
}
Loading