|
| 1 | +import type { Editor } from "@tiptap/core"; |
| 2 | +import { Fragment, type Node, type Node as ProseMirrorNode } from "@tiptap/pm/model"; |
| 3 | +import type { Transaction } from "@tiptap/pm/state"; |
| 4 | +import { type CellSelection, TableMap } from "@tiptap/pm/tables"; |
| 5 | +// extensions |
| 6 | +import { TableNodeLocation } from "@/extensions/table/table/utilities/helpers"; |
| 7 | + |
| 8 | +type TableRow = (ProseMirrorNode | null)[]; |
| 9 | +type TableRows = TableRow[]; |
| 10 | + |
| 11 | +/** |
| 12 | + * Move the selected columns to the specified index. |
| 13 | + * @param {Editor} editor - The editor instance. |
| 14 | + * @param {TableNodeLocation} table - The table node location. |
| 15 | + * @param {CellSelection} selection - The cell selection. |
| 16 | + * @param {number} to - The index to move the columns to. |
| 17 | + * @param {Transaction} tr - The transaction. |
| 18 | + * @returns {Transaction} The updated transaction. |
| 19 | + */ |
| 20 | +export const moveSelectedColumns = ( |
| 21 | + editor: Editor, |
| 22 | + table: TableNodeLocation, |
| 23 | + selection: CellSelection, |
| 24 | + to: number, |
| 25 | + tr: Transaction |
| 26 | +): Transaction => { |
| 27 | + const tableMap = TableMap.get(table.node); |
| 28 | + |
| 29 | + let columnStart = -1; |
| 30 | + let columnEnd = -1; |
| 31 | + |
| 32 | + selection.forEachCell((_node, pos) => { |
| 33 | + const cell = tableMap.findCell(pos - table.pos - 1); |
| 34 | + for (let i = cell.left; i < cell.right; i++) { |
| 35 | + columnStart = columnStart >= 0 ? Math.min(cell.left, columnStart) : cell.left; |
| 36 | + columnEnd = columnEnd >= 0 ? Math.max(cell.right, columnEnd) : cell.right; |
| 37 | + } |
| 38 | + }); |
| 39 | + |
| 40 | + if (columnStart === -1 || columnEnd === -1) { |
| 41 | + console.warn("Invalid column selection"); |
| 42 | + return tr; |
| 43 | + } |
| 44 | + |
| 45 | + if (to < 0 || to > tableMap.width || (to >= columnStart && to < columnEnd)) return tr; |
| 46 | + |
| 47 | + const rows = tableToCells(table); |
| 48 | + for (const row of rows) { |
| 49 | + const range = row.splice(columnStart, columnEnd - columnStart); |
| 50 | + const offset = to > columnStart ? to - (columnEnd - columnStart - 1) : to; |
| 51 | + row.splice(offset, 0, ...range); |
| 52 | + } |
| 53 | + |
| 54 | + tableFromCells(editor, table, rows, tr); |
| 55 | + return tr; |
| 56 | +}; |
| 57 | + |
| 58 | +/** |
| 59 | + * Move the selected rows to the specified index. |
| 60 | + * @param {Editor} editor - The editor instance. |
| 61 | + * @param {TableNodeLocation} table - The table node location. |
| 62 | + * @param {CellSelection} selection - The cell selection. |
| 63 | + * @param {number} to - The index to move the rows to. |
| 64 | + * @param {Transaction} tr - The transaction. |
| 65 | + * @returns {Transaction} The updated transaction. |
| 66 | + */ |
| 67 | +export const moveSelectedRows = ( |
| 68 | + editor: Editor, |
| 69 | + table: TableNodeLocation, |
| 70 | + selection: CellSelection, |
| 71 | + to: number, |
| 72 | + tr: Transaction |
| 73 | +): Transaction => { |
| 74 | + const tableMap = TableMap.get(table.node); |
| 75 | + |
| 76 | + let rowStart = -1; |
| 77 | + let rowEnd = -1; |
| 78 | + |
| 79 | + selection.forEachCell((_node, pos) => { |
| 80 | + const cell = tableMap.findCell(pos - table.pos - 1); |
| 81 | + for (let i = cell.top; i < cell.bottom; i++) { |
| 82 | + rowStart = rowStart >= 0 ? Math.min(cell.top, rowStart) : cell.top; |
| 83 | + rowEnd = rowEnd >= 0 ? Math.max(cell.bottom, rowEnd) : cell.bottom; |
| 84 | + } |
| 85 | + }); |
| 86 | + |
| 87 | + if (rowStart === -1 || rowEnd === -1) { |
| 88 | + console.warn("Invalid row selection"); |
| 89 | + return tr; |
| 90 | + } |
| 91 | + |
| 92 | + if (to < 0 || to > tableMap.height || (to >= rowStart && to < rowEnd)) return tr; |
| 93 | + |
| 94 | + const rows = tableToCells(table); |
| 95 | + const range = rows.splice(rowStart, rowEnd - rowStart); |
| 96 | + const offset = to > rowStart ? to - (rowEnd - rowStart - 1) : to; |
| 97 | + rows.splice(offset, 0, ...range); |
| 98 | + |
| 99 | + tableFromCells(editor, table, rows, tr); |
| 100 | + return tr; |
| 101 | +}; |
| 102 | + |
| 103 | +/** |
| 104 | + * @description Duplicate the selected rows. |
| 105 | + * @param {TableNodeLocation} table - The table node location. |
| 106 | + * @param {number[]} rowIndices - The indices of the rows to duplicate. |
| 107 | + * @param {Transaction} tr - The transaction. |
| 108 | + * @returns {Transaction} The updated transaction. |
| 109 | + */ |
| 110 | +export const duplicateRows = (table: TableNodeLocation, rowIndices: number[], tr: Transaction): Transaction => { |
| 111 | + const rows = tableToCells(table); |
| 112 | + |
| 113 | + const { map, width } = TableMap.get(table.node); |
| 114 | + |
| 115 | + // Validate row indices |
| 116 | + const maxRow = rows.length - 1; |
| 117 | + if (rowIndices.some((idx) => idx < 0 || idx > maxRow)) { |
| 118 | + console.warn("Invalid row indices for duplication"); |
| 119 | + return tr; |
| 120 | + } |
| 121 | + |
| 122 | + const mapStart = tr.mapping.maps.length; |
| 123 | + |
| 124 | + const lastRowPos = map[rowIndices[rowIndices.length - 1] * width + width - 1]; |
| 125 | + const nextRowStart = lastRowPos + (table.node.nodeAt(lastRowPos)?.nodeSize ?? 0) + 1; |
| 126 | + const insertPos = tr.mapping.slice(mapStart).map(table.start + nextRowStart); |
| 127 | + |
| 128 | + for (let i = rowIndices.length - 1; i >= 0; i--) { |
| 129 | + tr.insert( |
| 130 | + insertPos, |
| 131 | + rows[rowIndices[i]].filter((r) => r !== null) |
| 132 | + ); |
| 133 | + } |
| 134 | + |
| 135 | + return tr; |
| 136 | +}; |
| 137 | + |
| 138 | +/** |
| 139 | + * @description Duplicate the selected columns. |
| 140 | + * @param {TableNodeLocation} table - The table node location. |
| 141 | + * @param {number[]} columnIndices - The indices of the columns to duplicate. |
| 142 | + * @param {Transaction} tr - The transaction. |
| 143 | + * @returns {Transaction} The updated transaction. |
| 144 | + */ |
| 145 | +export const duplicateColumns = (table: TableNodeLocation, columnIndices: number[], tr: Transaction): Transaction => { |
| 146 | + const rows = tableToCells(table); |
| 147 | + |
| 148 | + const { map, width, height } = TableMap.get(table.node); |
| 149 | + |
| 150 | + // Validate column indices |
| 151 | + if (columnIndices.some((idx) => idx < 0 || idx >= width)) { |
| 152 | + console.warn("Invalid column indices for duplication"); |
| 153 | + return tr; |
| 154 | + } |
| 155 | + |
| 156 | + const mapStart = tr.mapping.maps.length; |
| 157 | + |
| 158 | + for (let row = 0; row < height; row++) { |
| 159 | + const lastColumnPos = map[row * width + columnIndices[columnIndices.length - 1]]; |
| 160 | + const nextColumnStart = lastColumnPos + (table.node.nodeAt(lastColumnPos)?.nodeSize ?? 0); |
| 161 | + const insertPos = tr.mapping.slice(mapStart).map(table.start + nextColumnStart); |
| 162 | + |
| 163 | + for (let i = columnIndices.length - 1; i >= 0; i--) { |
| 164 | + const copiedNode = rows[row][columnIndices[i]]; |
| 165 | + if (copiedNode !== null) { |
| 166 | + tr.insert(insertPos, copiedNode); |
| 167 | + } |
| 168 | + } |
| 169 | + } |
| 170 | + |
| 171 | + return tr; |
| 172 | +}; |
| 173 | + |
| 174 | +/** |
| 175 | + * @description Convert the table to cells. |
| 176 | + * @param {TableNodeLocation} table - The table node location. |
| 177 | + * @returns {TableRows} The table rows. |
| 178 | + */ |
| 179 | +const tableToCells = (table: TableNodeLocation): TableRows => { |
| 180 | + const { map, width, height } = TableMap.get(table.node); |
| 181 | + |
| 182 | + const visitedCells = new Set<number>(); |
| 183 | + const rows: TableRows = []; |
| 184 | + for (let row = 0; row < height; row++) { |
| 185 | + const cells: (ProseMirrorNode | null)[] = []; |
| 186 | + for (let col = 0; col < width; col++) { |
| 187 | + const pos = map[row * width + col]; |
| 188 | + cells.push(!visitedCells.has(pos) ? table.node.nodeAt(pos) : null); |
| 189 | + visitedCells.add(pos); |
| 190 | + } |
| 191 | + rows.push(cells); |
| 192 | + } |
| 193 | + |
| 194 | + return rows; |
| 195 | +}; |
| 196 | + |
| 197 | +/** |
| 198 | + * @description Convert the cells to a table. |
| 199 | + * @param {Editor} editor - The editor instance. |
| 200 | + * @param {TableNodeLocation} table - The table node location. |
| 201 | + * @param {TableRows} rows - The table rows. |
| 202 | + * @param {Transaction} tr - The transaction. |
| 203 | + */ |
| 204 | +const tableFromCells = (editor: Editor, table: TableNodeLocation, rows: TableRows, tr: Transaction): void => { |
| 205 | + const schema = editor.schema.nodes; |
| 206 | + const newRowNodes = rows.map((row) => |
| 207 | + schema.tableRow.create(null, row.filter((cell) => cell !== null) as readonly Node[]) |
| 208 | + ); |
| 209 | + const newTableNode = table.node.copy(Fragment.from(newRowNodes)); |
| 210 | + tr.replaceWith(table.pos, table.pos + table.node.nodeSize, newTableNode); |
| 211 | +}; |
0 commit comments