diff --git a/plugins/text-editor-assets/assets/icons.svg b/plugins/text-editor-assets/assets/icons.svg
index 0de43e2d89..56514789e4 100644
--- a/plugins/text-editor-assets/assets/icons.svg
+++ b/plugins/text-editor-assets/assets/icons.svg
@@ -207,4 +207,11 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/plugins/text-editor-assets/lang/en.json b/plugins/text-editor-assets/lang/en.json
index cceeb8ebd7..8677c3e156 100644
--- a/plugins/text-editor-assets/lang/en.json
+++ b/plugins/text-editor-assets/lang/en.json
@@ -46,9 +46,12 @@
"AddRowAfter": "Add after",
"DeleteRow": "Delete",
"DeleteTable": "Delete",
+ "MergeCells": "Merge cells",
+ "SplitCells": "Split cells",
"Duplicate": "Duplicate",
"CategoryRow": "Rows",
"CategoryColumn": "Columns",
+ "CategoryCell": "Cells",
"Table": "Table",
"InsertTable": "Insert table",
"TableOptions": "Customize table",
diff --git a/plugins/text-editor-assets/lang/ru.json b/plugins/text-editor-assets/lang/ru.json
index e1cea576aa..704ab81366 100644
--- a/plugins/text-editor-assets/lang/ru.json
+++ b/plugins/text-editor-assets/lang/ru.json
@@ -46,9 +46,12 @@
"AddRowAfter": "Добавить после",
"DeleteRow": "Удалить",
"DeleteTable": "Удалить",
+ "MergeCells": "Объединить ячейки",
+ "SplitCells": "Разделить ячейки",
"Duplicate": "Дублировать",
"CategoryRow": "Строки",
"CategoryColumn": "Колонки",
+ "CategoryCell": "Ячейки",
"Table": "Таблица",
"InsertTable": "Добавить таблицу",
"TableOptions": "Настроить таблицу",
diff --git a/plugins/text-editor-assets/src/index.ts b/plugins/text-editor-assets/src/index.ts
index 43a450ce20..0a4173dbd5 100644
--- a/plugins/text-editor-assets/src/index.ts
+++ b/plugins/text-editor-assets/src/index.ts
@@ -41,5 +41,7 @@ loadMetadata(textEditor.icon, {
Download: `${icons}#download`,
Note: `${icons}#note`,
Comment: `${icons}#comment`,
- SelectTable: `${icons}#move`
+ SelectTable: `${icons}#move`,
+ MergeCells: `${icons}#union`,
+ SplitCells: `${icons}#divide`
})
diff --git a/plugins/text-editor-resources/src/components/extension/table/TableNodeView.svelte b/plugins/text-editor-resources/src/components/extension/table/TableNodeView.svelte
index 48896c0afc..fb164977d2 100644
--- a/plugins/text-editor-resources/src/components/extension/table/TableNodeView.svelte
+++ b/plugins/text-editor-resources/src/components/extension/table/TableNodeView.svelte
@@ -175,7 +175,7 @@
}
&__col {
- right: -1.5rem;
+ right: calc(var(--table-offscreen-spacing) - 1.5rem);
top: 0;
bottom: 0;
margin: 1.25rem 0;
@@ -188,7 +188,7 @@
&__row {
bottom: -0.25rem;
left: var(--table-offscreen-spacing);
- right: 0;
+ right: var(--table-offscreen-spacing);
.table-button {
height: 1.25rem;
diff --git a/plugins/text-editor-resources/src/components/extension/table/decorations/actions.ts b/plugins/text-editor-resources/src/components/extension/table/decorations/actions.ts
index a7f022b089..81710e85f8 100644
--- a/plugins/text-editor-resources/src/components/extension/table/decorations/actions.ts
+++ b/plugins/text-editor-resources/src/components/extension/table/decorations/actions.ts
@@ -13,25 +13,76 @@
// limitations under the License.
//
-import type { Node as ProseMirrorNode } from '@tiptap/pm/model'
+import { Fragment, type Node, type Node as ProseMirrorNode } from '@tiptap/pm/model'
import type { Transaction } from '@tiptap/pm/state'
-import { TableMap } from '@tiptap/pm/tables'
+import { type CellSelection, TableMap } from '@tiptap/pm/tables'
import type { TableNodeLocation } from '../types'
+import { type Editor } from '@tiptap/core'
type TableRow = Array
type TableRows = TableRow[]
-export function moveColumn (table: TableNodeLocation, from: number, to: number, tr: Transaction): Transaction {
- const cols = transpose(tableToCells(table))
- moveRowInplace(cols, from, to)
- tableFromCells(table, transpose(cols), tr)
+export function moveSelectedColumns (
+ editor: Editor,
+ table: TableNodeLocation,
+ selection: CellSelection,
+ to: number,
+ tr: Transaction
+): Transaction {
+ const tableMap = TableMap.get(table.node)
+
+ let columnStart = -1
+ let columnEnd = -1
+
+ selection.forEachCell((node, pos) => {
+ const cell = tableMap.findCell(pos - table.pos - 1)
+ for (let i = cell.left; i < cell.right; i++) {
+ columnStart = columnStart >= 0 ? Math.min(cell.left, columnStart) : cell.left
+ columnEnd = columnEnd >= 0 ? Math.max(cell.right, columnEnd) : cell.right
+ }
+ })
+
+ if (to < 0 || to > tableMap.width || (to >= columnStart && to < columnEnd)) return tr
+
+ const rows = tableToCells(table)
+ for (const row of rows) {
+ const range = row.splice(columnStart, columnEnd - columnStart)
+ const offset = to > columnStart ? to - (columnEnd - columnStart - 1) : to
+ row.splice(offset, 0, ...range)
+ }
+
+ tableFromCells(editor, table, rows, tr)
return tr
}
-export function moveRow (table: TableNodeLocation, from: number, to: number, tr: Transaction): Transaction {
+export function moveSelectedRows (
+ editor: Editor,
+ table: TableNodeLocation,
+ selection: CellSelection,
+ to: number,
+ tr: Transaction
+): Transaction {
+ const tableMap = TableMap.get(table.node)
+
+ let rowStart = -1
+ let rowEnd = -1
+
+ selection.forEachCell((node, pos) => {
+ const cell = tableMap.findCell(pos - table.pos - 1)
+ for (let i = cell.top; i < cell.bottom; i++) {
+ rowStart = rowStart >= 0 ? Math.min(cell.top, rowStart) : cell.top
+ rowEnd = rowEnd >= 0 ? Math.max(cell.bottom, rowEnd) : cell.bottom
+ }
+ })
+
+ if (to < 0 || to > tableMap.height || (to >= rowStart && to < rowEnd)) return tr
+
const rows = tableToCells(table)
- moveRowInplace(rows, from, to)
- tableFromCells(table, rows, tr)
+ const range = rows.splice(rowStart, rowEnd - rowStart)
+ const offset = to > rowStart ? to - (rowEnd - rowStart - 1) : to
+ rows.splice(offset, 0, ...range)
+
+ tableFromCells(editor, table, rows, tr)
return tr
}
@@ -78,23 +129,17 @@ export function duplicateColumns (table: TableNodeLocation, columnIndices: numbe
return tr
}
-function moveRowInplace (rows: TableRows, from: number, to: number): void {
- rows.splice(to, 0, rows.splice(from, 1)[0])
-}
-
-function transpose (rows: TableRows): TableRows {
- return rows[0].map((_, colIdx) => rows.map((row) => row[colIdx]))
-}
-
function tableToCells (table: TableNodeLocation): TableRows {
const { map, width, height } = TableMap.get(table.node)
+ const visitedCells = new Set()
const rows = []
for (let row = 0; row < height; row++) {
const cells = []
for (let col = 0; col < width; col++) {
const pos = map[row * width + col]
- cells.push(table.node.nodeAt(pos))
+ cells.push(!visitedCells.has(pos) ? table.node.nodeAt(pos) : null)
+ visitedCells.add(pos)
}
rows.push(cells)
}
@@ -102,23 +147,11 @@ function tableToCells (table: TableNodeLocation): TableRows {
return rows
}
-function tableFromCells (table: TableNodeLocation, rows: TableRows, tr: Transaction): void {
- const { map, width, height } = TableMap.get(table.node)
- const mapStart = tr.mapping.maps.length
-
- for (let row = 0; row < height; row++) {
- for (let col = 0; col < width; col++) {
- const pos = map[row * width + col]
-
- const oldCell = table.node.nodeAt(pos)
- const newCell = rows[row][col]
-
- if (oldCell !== null && newCell !== null && oldCell !== newCell) {
- const start = tr.mapping.slice(mapStart).map(table.start + pos)
- const end = start + oldCell.nodeSize
-
- tr.replaceWith(start, end, newCell)
- }
- }
- }
+function tableFromCells (editor: Editor, table: TableNodeLocation, rows: TableRows, tr: Transaction): void {
+ const schema = editor.schema.nodes
+ const newRowNodes = rows.map((row) =>
+ schema.tableRow.create(null, row.filter((cell) => cell !== null) as readonly Node[])
+ )
+ const newTableNode = table.node.copy(Fragment.from(newRowNodes))
+ tr.replaceWith(table.pos, table.pos + table.node.nodeSize, newTableNode)
}
diff --git a/plugins/text-editor-resources/src/components/extension/table/decorations/columnHandlerDecoration.ts b/plugins/text-editor-resources/src/components/extension/table/decorations/columnHandlerDecoration.ts
index e3cf2ae58c..762c4a3dad 100644
--- a/plugins/text-editor-resources/src/components/extension/table/decorations/columnHandlerDecoration.ts
+++ b/plugins/text-editor-resources/src/components/extension/table/decorations/columnHandlerDecoration.ts
@@ -15,14 +15,14 @@
import { type Editor } from '@tiptap/core'
import { type EditorState } from '@tiptap/pm/state'
-import { TableMap } from '@tiptap/pm/tables'
+import { CellSelection, TableMap } from '@tiptap/pm/tables'
import { Decoration } from '@tiptap/pm/view'
import textEditor from '@hcengineering/text-editor'
import { type TableNodeLocation } from '../types'
import { findTable, getSelectedColumns, isColumnSelected, selectColumn } from '../utils'
-import { duplicateColumns, moveColumn } from './actions'
+import { duplicateColumns, moveSelectedColumns } from './actions'
import DeleteCol from '../../../icons/table/DeleteCol.svelte'
import Duplicate from '../../../icons/table/Duplicate.svelte'
import { createCellsHandle, type OptionItem } from './cellsHandle'
@@ -120,8 +120,10 @@ const handleMouseDown = (
if (col !== dropIndex) {
let tr = editor.state.tr
- tr = selectColumn(table, dropIndex, tr)
- tr = moveColumn(table, col, dropIndex, tr)
+ const selection = editor.state.selection
+ if (selection instanceof CellSelection) {
+ tr = moveSelectedColumns(editor, table, selection, dropIndex, tr)
+ }
editor.view.dispatch(tr)
}
window.removeEventListener('mouseup', handleFinish)
diff --git a/plugins/text-editor-resources/src/components/extension/table/decorations/rowHandlerDecoration.ts b/plugins/text-editor-resources/src/components/extension/table/decorations/rowHandlerDecoration.ts
index 1f8b11a1a3..4ff95067f7 100644
--- a/plugins/text-editor-resources/src/components/extension/table/decorations/rowHandlerDecoration.ts
+++ b/plugins/text-editor-resources/src/components/extension/table/decorations/rowHandlerDecoration.ts
@@ -15,14 +15,14 @@
import { type Editor } from '@tiptap/core'
import { type EditorState } from '@tiptap/pm/state'
-import { TableMap } from '@tiptap/pm/tables'
+import { CellSelection, TableMap } from '@tiptap/pm/tables'
import { Decoration } from '@tiptap/pm/view'
import textEditor from '@hcengineering/text-editor'
import { type TableNodeLocation } from '../types'
import { findTable, getSelectedRows, isRowSelected, selectRow } from '../utils'
-import { duplicateRows, moveRow } from './actions'
+import { duplicateRows, moveSelectedRows } from './actions'
import DeleteRow from '../../../icons/table/DeleteRow.svelte'
import Duplicate from '../../../icons/table/Duplicate.svelte'
import { createCellsHandle, type OptionItem } from './cellsHandle'
@@ -120,8 +120,10 @@ const handleMouseDown = (
if (row !== dropIndex) {
let tr = editor.state.tr
- tr = selectRow(table, dropIndex, tr)
- tr = moveRow(table, row, dropIndex, tr)
+ const selection = editor.state.selection
+ if (selection instanceof CellSelection) {
+ tr = moveSelectedRows(editor, table, selection, dropIndex, tr)
+ }
editor.view.dispatch(tr)
}
window.removeEventListener('mouseup', handleFinish)
diff --git a/plugins/text-editor-resources/src/components/extension/table/decorations/tableSelectionDecoration.ts b/plugins/text-editor-resources/src/components/extension/table/decorations/tableSelectionDecoration.ts
index c6197e511c..6962b79414 100644
--- a/plugins/text-editor-resources/src/components/extension/table/decorations/tableSelectionDecoration.ts
+++ b/plugins/text-editor-resources/src/components/extension/table/decorations/tableSelectionDecoration.ts
@@ -60,10 +60,17 @@ function getTableCellBorders (
const { width, height } = tableMap
const cellIndex = tableMap.map.indexOf(cell)
+ const rect = tableMap.findCell(cell)
+ const cellW = rect.right - rect.left
+ const cellH = rect.bottom - rect.top
+
+ const testRight = cellW
+ const testBottom = width * cellH
+
const topCell = cellIndex >= width ? tableMap.map[cellIndex - width] : undefined
- const bottomCell = cellIndex < width * height - width ? tableMap.map[cellIndex + width] : undefined
- const leftCell = cellIndex % width !== 0 ? tableMap.map[cellIndex - 1] : undefined
- const rightCell = cellIndex % width !== width - 1 ? tableMap.map[cellIndex + 1] : undefined
+ const bottomCell = cellIndex < width * height - testBottom ? tableMap.map[cellIndex + testBottom] : undefined
+ const leftCell = cellIndex % width > 0 ? tableMap.map[cellIndex - 1] : undefined
+ const rightCell = cellIndex % width < width - testRight ? tableMap.map[cellIndex + testRight] : undefined
return {
top: topCell === undefined || !selection.includes(topCell),
diff --git a/plugins/text-editor-resources/src/components/extension/table/table.ts b/plugins/text-editor-resources/src/components/extension/table/table.ts
index c130cdf7bd..0ce0cc0c54 100644
--- a/plugins/text-editor-resources/src/components/extension/table/table.ts
+++ b/plugins/text-editor-resources/src/components/extension/table/table.ts
@@ -116,6 +116,24 @@ export async function openTableOptions (editor: Editor, event: MouseEvent): Prom
label: textEditor.string.CategoryRow
}
},
+ {
+ id: '#mergeCells',
+ icon: textEditor.icon.MergeCells,
+ label: textEditor.string.MergeCells,
+ action: () => editor.commands.mergeCells(),
+ category: {
+ label: textEditor.string.CategoryCell
+ }
+ },
+ {
+ id: '#splitCell',
+ icon: textEditor.icon.SplitCells,
+ label: textEditor.string.SplitCells,
+ action: () => editor.commands.splitCell(),
+ category: {
+ label: textEditor.string.CategoryCell
+ }
+ },
{
id: '#deleteTable',
icon: DeleteTable,
diff --git a/plugins/text-editor-resources/src/components/extension/table/tableCell.ts b/plugins/text-editor-resources/src/components/extension/table/tableCell.ts
index f34e8c603f..0bbf96dcce 100644
--- a/plugins/text-editor-resources/src/components/extension/table/tableCell.ts
+++ b/plugins/text-editor-resources/src/components/extension/table/tableCell.ts
@@ -18,17 +18,19 @@ import TiptapTableCell from '@tiptap/extension-table-cell'
import { Plugin, PluginKey, type Selection } from '@tiptap/pm/state'
import { DecorationSet } from '@tiptap/pm/view'
-import { findTable } from './utils'
+import { CellSelection, type Rect, TableMap } from '@tiptap/pm/tables'
import { columnHandlerDecoration } from './decorations/columnHandlerDecoration'
import { columnInsertDecoration } from './decorations/columnInsertDecoration'
+import { rowHandlerDecoration } from './decorations/rowHandlerDecoration'
import { rowInsertDecoration } from './decorations/rowInsertDecoration'
import { tableDragMarkerDecoration } from './decorations/tableDragMarkerDecoration'
import { tableSelectionDecoration } from './decorations/tableSelectionDecoration'
-import { rowHandlerDecoration } from './decorations/rowHandlerDecoration'
+import { findTable } from './utils'
+import { type Node } from '@tiptap/pm/model'
export const TableCell = TiptapTableCell.extend({
addProseMirrorPlugins () {
- return [tableCellDecorationPlugin(this.editor)]
+ return [tableCellDecorationPlugin(this.editor), tableSelectionNormalizer()]
}
})
@@ -78,3 +80,80 @@ const tableCellDecorationPlugin = (editor: Editor): Plugin => {
+ return new Plugin({
+ appendTransaction: (transactions, oldState, newState) => {
+ const selection = newState.selection
+ if (selection.eq(oldState.selection) || !(selection instanceof CellSelection)) return
+
+ const table = findTable(newState.selection)
+ if (table === undefined) return
+
+ const tableMap = TableMap.get(table.node)
+
+ let rect: Rect | undefined
+
+ const walkCell = (pos: number): void => {
+ const cell = tableMap.findCell(pos)
+ if (cell === undefined) return
+
+ if (rect === undefined) {
+ rect = { ...cell }
+ } else {
+ rect.left = Math.min(rect.left, cell.left)
+ rect.top = Math.min(rect.top, cell.top)
+
+ rect.right = Math.max(rect.right, cell.right)
+ rect.bottom = Math.max(rect.bottom, cell.bottom)
+ }
+ }
+
+ selection.forEachCell((_node, pos) => {
+ walkCell(pos - table.pos - 1)
+ })
+ if (rect === undefined) return
+
+ const rectSelection: number[] = []
+ for (let row = rect.top; row < rect.bottom; row++) {
+ for (let col = rect.left; col < rect.right; col++) {
+ rectSelection.push(tableMap.map[row * tableMap.width + col])
+ }
+ }
+ rectSelection.forEach((pos) => {
+ walkCell(pos)
+ })
+
+ if (rect === undefined) return
+
+ // Original promemirror implementation of TableMap.positionAt skips rowspawn cells, which leads to unpredictable selection behaviour
+ const firstCellOffset = cellPositionAt(tableMap, rect.bottom - 1, rect.right - 1, table.node)
+ const lastCellOffset = cellPositionAt(tableMap, rect.top, rect.left, table.node)
+
+ const firstCellPos = newState.doc.resolve(table.start + firstCellOffset)
+ const lastCellPos = newState.doc.resolve(table.start + lastCellOffset)
+
+ const reverseOrder = selection.$anchorCell.pos > selection.$headCell.pos
+ const $head = reverseOrder ? lastCellPos : firstCellPos
+ const $anchor = reverseOrder ? firstCellPos : lastCellPos
+
+ const newSelection = new CellSelection($anchor, $head)
+
+ if (newSelection.eq(newState.selection)) return
+
+ return newState.tr.setSelection(new CellSelection($anchor, $head))
+ }
+ })
+}
+
+function cellPositionAt (tableMap: TableMap, row: number, col: number, table: Node): number {
+ for (let i = 0, rowStart = 0; ; i++) {
+ const rowEnd = rowStart + table.child(i).nodeSize
+ if (i === row) {
+ const index = col + row * tableMap.width
+ const rowEndIndex = (row + 1) * tableMap.width
+ return index === rowEndIndex ? rowEnd - 1 : tableMap.map[index]
+ }
+ rowStart = rowEnd
+ }
+}
diff --git a/plugins/text-editor/src/plugin.ts b/plugins/text-editor/src/plugin.ts
index c3e95c1692..408caec562 100644
--- a/plugins/text-editor/src/plugin.ts
+++ b/plugins/text-editor/src/plugin.ts
@@ -84,11 +84,14 @@ export default plugin(textEditorId, {
DeleteColumn: '' as IntlString,
AddRowBefore: '' as IntlString,
AddRowAfter: '' as IntlString,
+ MergeCells: '' as IntlString,
+ SplitCells: '' as IntlString,
DeleteRow: '' as IntlString,
DeleteTable: '' as IntlString,
Duplicate: '' as IntlString,
CategoryRow: '' as IntlString,
CategoryColumn: '' as IntlString,
+ CategoryCell: '' as IntlString,
Table: '' as IntlString,
TableOptions: '' as IntlString,
SelectTable: '' as IntlString,
@@ -125,6 +128,8 @@ export default plugin(textEditorId, {
Download: '' as Asset,
Note: '' as Asset,
Comment: '' as Asset,
- SelectTable: '' as Asset
+ SelectTable: '' as Asset,
+ MergeCells: '' as Asset,
+ SplitCells: '' as Asset
}
})