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)
116 changes: 116 additions & 0 deletions src/nodes/Table/Table.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,31 @@ function findSameCellInNextRow($cell) {
}
}

const getSortableCellText = (cell) => cell.textContent.trim()

const isTableCellType = (node) =>
node?.type?.name === 'tableCell' || node?.type?.name === 'tableHeader'

const resolveTableCellFromPosition = (doc, position) => {
const tryResolve = (pos) => {
if (typeof pos !== 'number' || pos < 0 || pos > doc.content.size) {
return null
}
const $pos = doc.resolve(pos)
for (let depth = $pos.depth; depth > 0; depth -= 1) {
if (isTableCellType($pos.node(depth))) {
return { $cell: $pos, cellDepth: depth }
}
}
return null
}

return (
tryResolve(position)
|| tryResolve(typeof position === 'number' ? position + 1 : position)
)
}

Comment on lines +75 to +99
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this needed?

export default Table.extend({
content: 'tableCaption? tableHeadRow tableRow*',

Expand Down Expand Up @@ -181,6 +206,97 @@ export default Table.extend({
)
dispatch(tr.setSelection(selection).scrollIntoView())
}
return true
},
sortColumn:
(direction = 'asc', cell = null) =>
({ state, tr, dispatch }) => {
const resolvedCell = resolveTableCellFromPosition(
state.doc,
cell,
)
if (!resolvedCell) return false

const { $cell, cellDepth } = resolvedCell
const columnIndex = $cell.index(cellDepth - 1)

// find the table node
let tableDepth = $cell.depth
while (
tableDepth > 0
&& $cell.node(tableDepth).type.name !== 'table'
) {
tableDepth -= 1
}
if (tableDepth === 0) return false

const table = $cell.node(tableDepth)
const tablePos = $cell.before(tableDepth)
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: getSortableCellText(row.child(columnIndex)),
}))
.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 @@ -52,6 +52,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 @@ -94,6 +112,8 @@ import {
AlignHorizontalCenter,
AlignHorizontalLeft,
AlignHorizontalRight,
SortAscending,
SortDescending,
TableAddColumnAfter,
TableAddColumnBefore,
TrashCan,
Expand All @@ -113,6 +133,8 @@ export default {
NodeViewContent,
TableAddColumnBefore,
TableAddColumnAfter,
SortAscending,
SortDescending,
},
props: {
editor: {
Expand Down Expand Up @@ -195,6 +217,15 @@ export default {
.addColumnAfter()
.run()
},
sortColumnAsc() {
this.sortColumn('asc')
},
sortColumnDesc() {
this.sortColumn('desc')
},
sortColumn(direction) {
this.editor.chain().focus().sortColumn(direction, this.getPos()).run()
},
t,
},
}
Expand Down
79 changes: 79 additions & 0 deletions src/tests/nodes/Table.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -236,8 +236,87 @@ 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 cellPos
cellPos = getHeaderCellPos(editor, 0)
expect(editor.commands.sortColumn('asc', cellPos)).toBe(true)

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

cellPos = getHeaderCellPos(editor, 1)
expect(editor.commands.sortColumn('asc', cellPos)).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 cellPos
cellPos = getHeaderCellPos(editor, 0)
expect(editor.commands.sortColumn('desc', cellPos)).toBe(true)

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

cellPos = getHeaderCellPos(editor, 1)
expect(editor.commands.sortColumn('desc', cellPos)).toBe(true)

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

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

cellPos = pos + 1
for (let index = 0; index < targetIndex; index += 1) {
cellPos += node.child(index).nodeSize
}
return false
})
return cellPos
}

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