@lexical/table 0.44.1-nightly.20260519.0 → 0.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/{LexicalTable.dev.js → dist/LexicalTable.dev.js} +243 -33
- package/{LexicalTable.dev.mjs → dist/LexicalTable.dev.mjs} +239 -33
- package/{LexicalTable.mjs → dist/LexicalTable.mjs} +4 -0
- package/{LexicalTable.node.mjs → dist/LexicalTable.node.mjs} +4 -0
- package/dist/LexicalTable.prod.js +9 -0
- package/dist/LexicalTable.prod.mjs +9 -0
- package/dist/TableImportExtension.d.ts +38 -0
- package/{index.d.ts → dist/index.d.ts} +1 -0
- package/package.json +34 -18
- package/src/LexicalTableCellNode.ts +479 -0
- package/src/LexicalTableCommands.ts +27 -0
- package/src/LexicalTableExtension.ts +104 -0
- package/src/LexicalTableNode.ts +678 -0
- package/src/LexicalTableObserver.ts +575 -0
- package/src/LexicalTablePluginHelpers.ts +694 -0
- package/src/LexicalTableRowNode.ts +154 -0
- package/src/LexicalTableSelection.ts +460 -0
- package/src/LexicalTableSelectionHelpers.ts +2409 -0
- package/src/LexicalTableUtils.ts +1386 -0
- package/src/TableImportExtension.ts +302 -0
- package/src/constants.ts +13 -0
- package/src/index.ts +97 -0
- package/LexicalTable.prod.js +0 -9
- package/LexicalTable.prod.mjs +0 -9
- /package/{LexicalTable.js → dist/LexicalTable.js} +0 -0
- /package/{LexicalTable.js.flow → dist/LexicalTable.js.flow} +0 -0
- /package/{LexicalTableCellNode.d.ts → dist/LexicalTableCellNode.d.ts} +0 -0
- /package/{LexicalTableCommands.d.ts → dist/LexicalTableCommands.d.ts} +0 -0
- /package/{LexicalTableExtension.d.ts → dist/LexicalTableExtension.d.ts} +0 -0
- /package/{LexicalTableNode.d.ts → dist/LexicalTableNode.d.ts} +0 -0
- /package/{LexicalTableObserver.d.ts → dist/LexicalTableObserver.d.ts} +0 -0
- /package/{LexicalTablePluginHelpers.d.ts → dist/LexicalTablePluginHelpers.d.ts} +0 -0
- /package/{LexicalTableRowNode.d.ts → dist/LexicalTableRowNode.d.ts} +0 -0
- /package/{LexicalTableSelection.d.ts → dist/LexicalTableSelection.d.ts} +0 -0
- /package/{LexicalTableSelectionHelpers.d.ts → dist/LexicalTableSelectionHelpers.d.ts} +0 -0
- /package/{LexicalTableUtils.d.ts → dist/LexicalTableUtils.d.ts} +0 -0
- /package/{constants.d.ts → dist/constants.d.ts} +0 -0
|
@@ -0,0 +1,694 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {NamedSignalsOutput, Signal, signal} from '@lexical/extension';
|
|
10
|
+
import invariant from '@lexical/internal/invariant';
|
|
11
|
+
import {
|
|
12
|
+
$dfs,
|
|
13
|
+
$findMatchingParent,
|
|
14
|
+
$insertFirst,
|
|
15
|
+
$insertNodeToNearestRoot,
|
|
16
|
+
$unwrapAndFilterDescendants,
|
|
17
|
+
mergeRegister,
|
|
18
|
+
} from '@lexical/utils';
|
|
19
|
+
import {
|
|
20
|
+
$createParagraphNode,
|
|
21
|
+
$getNearestNodeFromDOMNode,
|
|
22
|
+
$getPreviousSelection,
|
|
23
|
+
$getRoot,
|
|
24
|
+
$getSelection,
|
|
25
|
+
$isElementNode,
|
|
26
|
+
$isRangeSelection,
|
|
27
|
+
$isTextNode,
|
|
28
|
+
$setSelection,
|
|
29
|
+
CLICK_COMMAND,
|
|
30
|
+
COMMAND_PRIORITY_EDITOR,
|
|
31
|
+
COMMAND_PRIORITY_HIGH,
|
|
32
|
+
COMMAND_PRIORITY_LOW,
|
|
33
|
+
CommandPayloadType,
|
|
34
|
+
ElementNode,
|
|
35
|
+
isDOMNode,
|
|
36
|
+
LexicalEditor,
|
|
37
|
+
NodeKey,
|
|
38
|
+
RangeSelection,
|
|
39
|
+
SELECT_ALL_COMMAND,
|
|
40
|
+
SELECTION_CHANGE_COMMAND,
|
|
41
|
+
SELECTION_INSERT_CLIPBOARD_NODES_COMMAND,
|
|
42
|
+
} from 'lexical';
|
|
43
|
+
|
|
44
|
+
import {
|
|
45
|
+
$createTableCellNode,
|
|
46
|
+
$isTableCellNode,
|
|
47
|
+
TableCellNode,
|
|
48
|
+
} from './LexicalTableCellNode';
|
|
49
|
+
import {
|
|
50
|
+
INSERT_TABLE_COMMAND,
|
|
51
|
+
InsertTableCommandPayload,
|
|
52
|
+
} from './LexicalTableCommands';
|
|
53
|
+
import {TableConfig} from './LexicalTableExtension';
|
|
54
|
+
import {$isTableNode, TableNode} from './LexicalTableNode';
|
|
55
|
+
import {$getTableAndElementByKey, TableObservers} from './LexicalTableObserver';
|
|
56
|
+
import {$isTableRowNode, TableRowNode} from './LexicalTableRowNode';
|
|
57
|
+
import {
|
|
58
|
+
$createTableSelectionFrom,
|
|
59
|
+
$isTableSelection,
|
|
60
|
+
TableSelection,
|
|
61
|
+
} from './LexicalTableSelection';
|
|
62
|
+
import {
|
|
63
|
+
$findTableNode,
|
|
64
|
+
$handleTableSelectionChangeCommand,
|
|
65
|
+
applyTableHandlers,
|
|
66
|
+
getTableElement,
|
|
67
|
+
registerTableWindowHandlers,
|
|
68
|
+
} from './LexicalTableSelectionHelpers';
|
|
69
|
+
import {
|
|
70
|
+
$computeTableCellRectBoundary,
|
|
71
|
+
$computeTableMap,
|
|
72
|
+
$computeTableMapSkipCellCheck,
|
|
73
|
+
$createTableNodeWithDimensions,
|
|
74
|
+
$getNodeTriplet,
|
|
75
|
+
$insertTableColumnAtNode,
|
|
76
|
+
$insertTableRowAtNode,
|
|
77
|
+
$mergeCells,
|
|
78
|
+
$unmergeCellNode,
|
|
79
|
+
} from './LexicalTableUtils';
|
|
80
|
+
|
|
81
|
+
function $insertTable(
|
|
82
|
+
{rows, columns, includeHeaders}: InsertTableCommandPayload,
|
|
83
|
+
hasNestedTables: boolean,
|
|
84
|
+
): boolean {
|
|
85
|
+
const selection = $getSelection() || $getPreviousSelection();
|
|
86
|
+
if (!selection || !$isRangeSelection(selection)) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Prevent nested tables by checking if we're already inside a table
|
|
91
|
+
if (!hasNestedTables && $findTableNode(selection.anchor.getNode())) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const tableNode = $createTableNodeWithDimensions(
|
|
96
|
+
Number(rows),
|
|
97
|
+
Number(columns),
|
|
98
|
+
includeHeaders,
|
|
99
|
+
);
|
|
100
|
+
$insertNodeToNearestRoot(tableNode);
|
|
101
|
+
|
|
102
|
+
const firstDescendant = tableNode.getFirstDescendant();
|
|
103
|
+
if ($isTextNode(firstDescendant)) {
|
|
104
|
+
firstDescendant.select();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function $tableCellTransform(node: TableCellNode) {
|
|
111
|
+
if (!$isTableRowNode(node.getParent())) {
|
|
112
|
+
// TableCellNode must be a child of TableRowNode.
|
|
113
|
+
node.remove();
|
|
114
|
+
} else if (node.isEmpty()) {
|
|
115
|
+
// TableCellNode should never be empty
|
|
116
|
+
node.append($createParagraphNode());
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function $tableRowTransform(node: TableRowNode) {
|
|
121
|
+
if (!$isTableNode(node.getParent())) {
|
|
122
|
+
// TableRowNode must be a child of TableNode.
|
|
123
|
+
// TODO: Future support of tbody/thead/tfoot may change this
|
|
124
|
+
node.remove();
|
|
125
|
+
} else {
|
|
126
|
+
$unwrapAndFilterDescendants(node, $isTableCellNode);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function $tableTransform(node: TableNode) {
|
|
131
|
+
// TableRowNode is the only valid child for TableNode
|
|
132
|
+
// TODO: Future support of tbody/thead/tfoot/caption may change this
|
|
133
|
+
$unwrapAndFilterDescendants(node, $isTableRowNode);
|
|
134
|
+
|
|
135
|
+
const [gridMap] = $computeTableMapSkipCellCheck(node, null, null);
|
|
136
|
+
const maxRowLength = gridMap.reduce((curLength, row) => {
|
|
137
|
+
return Math.max(curLength, row.length);
|
|
138
|
+
}, 0);
|
|
139
|
+
const rowNodes = node.getChildren();
|
|
140
|
+
for (let i = 0; i < gridMap.length; ++i) {
|
|
141
|
+
const rowNode = rowNodes[i];
|
|
142
|
+
if (!rowNode) {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
invariant(
|
|
146
|
+
$isTableRowNode(rowNode),
|
|
147
|
+
'TablePlugin: Expecting all children of TableNode to be TableRowNode, found %s (type %s)',
|
|
148
|
+
rowNode.constructor.name,
|
|
149
|
+
rowNode.getType(),
|
|
150
|
+
);
|
|
151
|
+
const rowLength = gridMap[i].reduce(
|
|
152
|
+
(acc, cell) => (cell ? 1 + acc : acc),
|
|
153
|
+
0,
|
|
154
|
+
);
|
|
155
|
+
if (rowLength === maxRowLength) {
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
for (let j = rowLength; j < maxRowLength; ++j) {
|
|
159
|
+
// TODO: inherit header state from another header or body
|
|
160
|
+
const newCell = $createTableCellNode();
|
|
161
|
+
newCell.append($createParagraphNode());
|
|
162
|
+
rowNode.append(newCell);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const colWidths = node.getColWidths();
|
|
166
|
+
const columnCount = node.getColumnCount();
|
|
167
|
+
if (colWidths && colWidths.length !== columnCount) {
|
|
168
|
+
let newColWidths: number[] | undefined = undefined;
|
|
169
|
+
if (columnCount < colWidths.length) {
|
|
170
|
+
newColWidths = colWidths.slice(0, columnCount);
|
|
171
|
+
} else if (colWidths.length > 0) {
|
|
172
|
+
// Repeat the last column width.
|
|
173
|
+
const fillWidth = colWidths[colWidths.length - 1];
|
|
174
|
+
newColWidths = [
|
|
175
|
+
...colWidths,
|
|
176
|
+
...Array(columnCount - colWidths.length).fill(fillWidth),
|
|
177
|
+
];
|
|
178
|
+
}
|
|
179
|
+
node.setColWidths(newColWidths);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function $tableClickCommand(event: MouseEvent): boolean {
|
|
184
|
+
if (event.detail < 3 || !isDOMNode(event.target)) {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
const startNode = $getNearestNodeFromDOMNode(event.target);
|
|
188
|
+
if (startNode === null) {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
const blockNode = $findMatchingParent(
|
|
192
|
+
startNode,
|
|
193
|
+
(node): node is ElementNode => $isElementNode(node) && !node.isInline(),
|
|
194
|
+
);
|
|
195
|
+
if (blockNode === null) {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
const rootNode = blockNode.getParent();
|
|
199
|
+
if (!$isTableCellNode(rootNode)) {
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
blockNode.select(0);
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function $tableSelectAllCommand(): boolean {
|
|
207
|
+
const selection = $getSelection();
|
|
208
|
+
if (!$isRangeSelection(selection)) {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Check if the selection is inside a table
|
|
213
|
+
const anchorNode = selection.anchor.getNode();
|
|
214
|
+
const tableNode = $findTableNode(anchorNode);
|
|
215
|
+
if (tableNode === null) {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// CRITICAL: Only intercept if table is the ONLY child of root
|
|
220
|
+
// This is required to reproduce the bug: table must be the only content, no empty paragraphs
|
|
221
|
+
// This prevents breaking other tests that expect RangeSelection when there's content outside table
|
|
222
|
+
const root = $getRoot();
|
|
223
|
+
if (!root.is(tableNode.getParent()) || root.getChildrenSize() !== 1) {
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// At this point, table is the only child
|
|
228
|
+
// This is the exact scenario from issue #8074: table is the only content in editor
|
|
229
|
+
|
|
230
|
+
// Get the table map to find first and last cells (handles merged cells correctly)
|
|
231
|
+
const [tableMap] = $computeTableMapSkipCellCheck(tableNode, null, null);
|
|
232
|
+
if (tableMap.length === 0 || tableMap[0].length === 0) {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Get the first cell (top-left)
|
|
237
|
+
const firstCellMap = tableMap[0][0];
|
|
238
|
+
if (!firstCellMap || !firstCellMap.cell) {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Get the last cell (bottom-right)
|
|
243
|
+
const lastRow = tableMap[tableMap.length - 1];
|
|
244
|
+
const lastCellMap = lastRow[lastRow.length - 1];
|
|
245
|
+
if (!lastCellMap || !lastCellMap.cell) {
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Create a TableSelection that selects all cells
|
|
250
|
+
const tableSelection = $createTableSelectionFrom(
|
|
251
|
+
tableNode,
|
|
252
|
+
firstCellMap.cell,
|
|
253
|
+
lastCellMap.cell,
|
|
254
|
+
);
|
|
255
|
+
$setSelection(tableSelection);
|
|
256
|
+
|
|
257
|
+
return true;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Register a transform to ensure that all TableCellNode have a colSpan and rowSpan of 1.
|
|
262
|
+
* This should only be registered when you do not want to support merged cells.
|
|
263
|
+
*
|
|
264
|
+
* @param editor The editor
|
|
265
|
+
* @returns An unregister callback
|
|
266
|
+
*/
|
|
267
|
+
export function registerTableCellUnmergeTransform(
|
|
268
|
+
editor: LexicalEditor,
|
|
269
|
+
): () => void {
|
|
270
|
+
return editor.registerNodeTransform(TableCellNode, node => {
|
|
271
|
+
if (node.getColSpan() > 1 || node.getRowSpan() > 1) {
|
|
272
|
+
// When we have rowSpan we have to map the entire Table to understand where the new Cells
|
|
273
|
+
// fit best; let's analyze all Cells at once to save us from further transform iterations
|
|
274
|
+
const [, , gridNode] = $getNodeTriplet(node);
|
|
275
|
+
const [gridMap] = $computeTableMap(gridNode, node, node);
|
|
276
|
+
// TODO this function expects Tables to be normalized. Look into this once it exists
|
|
277
|
+
const rowsCount = gridMap.length;
|
|
278
|
+
const columnsCount = gridMap[0].length;
|
|
279
|
+
let row = gridNode.getFirstChild();
|
|
280
|
+
invariant(
|
|
281
|
+
$isTableRowNode(row),
|
|
282
|
+
'Expected TableNode first child to be a RowNode',
|
|
283
|
+
);
|
|
284
|
+
const unmerged = [];
|
|
285
|
+
for (let i = 0; i < rowsCount; i++) {
|
|
286
|
+
if (i !== 0) {
|
|
287
|
+
row = row.getNextSibling();
|
|
288
|
+
invariant(
|
|
289
|
+
$isTableRowNode(row),
|
|
290
|
+
'Expected TableNode first child to be a RowNode',
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
let lastRowCell: null | TableCellNode = null;
|
|
294
|
+
for (let j = 0; j < columnsCount; j++) {
|
|
295
|
+
const cellMap = gridMap[i][j];
|
|
296
|
+
const cell = cellMap.cell;
|
|
297
|
+
if (cellMap.startRow === i && cellMap.startColumn === j) {
|
|
298
|
+
lastRowCell = cell;
|
|
299
|
+
unmerged.push(cell);
|
|
300
|
+
} else if (cell.getColSpan() > 1 || cell.getRowSpan() > 1) {
|
|
301
|
+
invariant(
|
|
302
|
+
$isTableCellNode(cell),
|
|
303
|
+
'Expected TableNode cell to be a TableCellNode',
|
|
304
|
+
);
|
|
305
|
+
const newCell = $createTableCellNode(cell.__headerState);
|
|
306
|
+
if (lastRowCell !== null) {
|
|
307
|
+
lastRowCell.insertAfter(newCell);
|
|
308
|
+
} else {
|
|
309
|
+
$insertFirst(row, newCell);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
for (const cell of unmerged) {
|
|
315
|
+
cell.setColSpan(1);
|
|
316
|
+
cell.setRowSpan(1);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export function registerTableSelectionObserver(
|
|
323
|
+
editor: LexicalEditor,
|
|
324
|
+
hasTabHandler: boolean = true,
|
|
325
|
+
): () => void {
|
|
326
|
+
const tableObservers = new TableObservers();
|
|
327
|
+
|
|
328
|
+
const initializeTableNode = (
|
|
329
|
+
tableNode: TableNode,
|
|
330
|
+
nodeKey: NodeKey,
|
|
331
|
+
dom: HTMLElement,
|
|
332
|
+
) => {
|
|
333
|
+
const tableElement = getTableElement(tableNode, dom);
|
|
334
|
+
const tableSelection = applyTableHandlers(
|
|
335
|
+
tableNode,
|
|
336
|
+
tableElement,
|
|
337
|
+
editor,
|
|
338
|
+
hasTabHandler,
|
|
339
|
+
tableObservers,
|
|
340
|
+
);
|
|
341
|
+
tableObservers.observers.set(nodeKey, [tableSelection, tableElement]);
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
return mergeRegister(
|
|
345
|
+
registerTableWindowHandlers(editor, tableObservers),
|
|
346
|
+
editor.registerCommand(
|
|
347
|
+
SELECTION_CHANGE_COMMAND,
|
|
348
|
+
() => {
|
|
349
|
+
return $handleTableSelectionChangeCommand(tableObservers, editor);
|
|
350
|
+
},
|
|
351
|
+
COMMAND_PRIORITY_HIGH,
|
|
352
|
+
),
|
|
353
|
+
editor.registerMutationListener(
|
|
354
|
+
TableNode,
|
|
355
|
+
nodeMutations => {
|
|
356
|
+
editor.getEditorState().read(
|
|
357
|
+
() => {
|
|
358
|
+
for (const [nodeKey, mutation] of nodeMutations) {
|
|
359
|
+
const tableSelection = tableObservers.observers.get(nodeKey);
|
|
360
|
+
if (mutation === 'created' || mutation === 'updated') {
|
|
361
|
+
const {tableNode, tableElement} =
|
|
362
|
+
$getTableAndElementByKey(nodeKey);
|
|
363
|
+
if (tableSelection === undefined) {
|
|
364
|
+
initializeTableNode(tableNode, nodeKey, tableElement);
|
|
365
|
+
} else if (tableElement !== tableSelection[1]) {
|
|
366
|
+
// The update created a new DOM node, destroy the existing TableObserver
|
|
367
|
+
tableSelection[0].removeListeners();
|
|
368
|
+
tableObservers.observers.delete(nodeKey);
|
|
369
|
+
initializeTableNode(tableNode, nodeKey, tableElement);
|
|
370
|
+
}
|
|
371
|
+
} else if (mutation === 'destroyed') {
|
|
372
|
+
if (tableSelection !== undefined) {
|
|
373
|
+
tableSelection[0].removeListeners();
|
|
374
|
+
tableObservers.observers.delete(nodeKey);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
},
|
|
379
|
+
{editor},
|
|
380
|
+
);
|
|
381
|
+
},
|
|
382
|
+
{skipInitialization: false},
|
|
383
|
+
),
|
|
384
|
+
() => {
|
|
385
|
+
// Hook might be called multiple times so cleaning up tables listeners as well,
|
|
386
|
+
// as it'll be reinitialized during recurring call
|
|
387
|
+
for (const [, [tableSelection]] of tableObservers.observers) {
|
|
388
|
+
tableSelection.removeListeners();
|
|
389
|
+
}
|
|
390
|
+
},
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Register table command listeners and the table integrity transforms. The
|
|
396
|
+
* table selection observer should be registered separately after this with
|
|
397
|
+
* {@link registerTableSelectionObserver}.
|
|
398
|
+
*
|
|
399
|
+
* @param editor The editor
|
|
400
|
+
* @returns An unregister callback
|
|
401
|
+
*/
|
|
402
|
+
export function registerTablePlugin(
|
|
403
|
+
editor: LexicalEditor,
|
|
404
|
+
options?: Pick<NamedSignalsOutput<TableConfig>, 'hasNestedTables'>,
|
|
405
|
+
): () => void {
|
|
406
|
+
if (!editor.hasNodes([TableNode])) {
|
|
407
|
+
invariant(false, 'TablePlugin: TableNode is not registered on editor');
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const {hasNestedTables = signal(false)} = options ?? {};
|
|
411
|
+
|
|
412
|
+
return mergeRegister(
|
|
413
|
+
editor.registerCommand(
|
|
414
|
+
INSERT_TABLE_COMMAND,
|
|
415
|
+
payload => {
|
|
416
|
+
return $insertTable(payload, hasNestedTables.peek());
|
|
417
|
+
},
|
|
418
|
+
COMMAND_PRIORITY_EDITOR,
|
|
419
|
+
),
|
|
420
|
+
editor.registerCommand(
|
|
421
|
+
SELECTION_INSERT_CLIPBOARD_NODES_COMMAND,
|
|
422
|
+
(payload, dispatchEditor) => {
|
|
423
|
+
if (editor !== dispatchEditor) {
|
|
424
|
+
return false;
|
|
425
|
+
}
|
|
426
|
+
return $tableSelectionInsertClipboardNodesCommand(
|
|
427
|
+
payload,
|
|
428
|
+
hasNestedTables,
|
|
429
|
+
);
|
|
430
|
+
},
|
|
431
|
+
COMMAND_PRIORITY_EDITOR,
|
|
432
|
+
),
|
|
433
|
+
editor.registerCommand(
|
|
434
|
+
SELECT_ALL_COMMAND,
|
|
435
|
+
$tableSelectAllCommand,
|
|
436
|
+
COMMAND_PRIORITY_LOW,
|
|
437
|
+
),
|
|
438
|
+
editor.registerCommand(
|
|
439
|
+
CLICK_COMMAND,
|
|
440
|
+
$tableClickCommand,
|
|
441
|
+
COMMAND_PRIORITY_EDITOR,
|
|
442
|
+
),
|
|
443
|
+
editor.registerNodeTransform(TableNode, $tableTransform),
|
|
444
|
+
editor.registerNodeTransform(TableRowNode, $tableRowTransform),
|
|
445
|
+
editor.registerNodeTransform(TableCellNode, $tableCellTransform),
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function $tableSelectionInsertClipboardNodesCommand(
|
|
450
|
+
selectionPayload: CommandPayloadType<
|
|
451
|
+
typeof SELECTION_INSERT_CLIPBOARD_NODES_COMMAND
|
|
452
|
+
>,
|
|
453
|
+
hasNestedTables: Signal<boolean>,
|
|
454
|
+
) {
|
|
455
|
+
const {nodes, selection} = selectionPayload;
|
|
456
|
+
|
|
457
|
+
const hasTables = nodes.some(
|
|
458
|
+
n => $isTableNode(n) || $dfs(n).some(d => $isTableNode(d.node)),
|
|
459
|
+
);
|
|
460
|
+
if (!hasTables) {
|
|
461
|
+
// Not pasting a table - no special handling required.
|
|
462
|
+
return false;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const isTableSelection = $isTableSelection(selection);
|
|
466
|
+
const isRangeSelection = $isRangeSelection(selection);
|
|
467
|
+
const isSelectionInsideOfGrid =
|
|
468
|
+
(isRangeSelection &&
|
|
469
|
+
$findMatchingParent(selection.anchor.getNode(), n =>
|
|
470
|
+
$isTableCellNode(n),
|
|
471
|
+
) !== null &&
|
|
472
|
+
$findMatchingParent(selection.focus.getNode(), n =>
|
|
473
|
+
$isTableCellNode(n),
|
|
474
|
+
) !== null) ||
|
|
475
|
+
isTableSelection;
|
|
476
|
+
|
|
477
|
+
if (!isSelectionInsideOfGrid) {
|
|
478
|
+
// Not pasting in a grid - no special handling required.
|
|
479
|
+
return false;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// When pasting just a table, flatten the table on the destination table, even when nested tables are allowed.
|
|
483
|
+
if (nodes.length === 1 && $isTableNode(nodes[0])) {
|
|
484
|
+
return $insertTableIntoGrid(nodes[0], selection);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// If nested tables are enabled, allow pasting a table into a single cell.
|
|
488
|
+
if (
|
|
489
|
+
isRangeSelection &&
|
|
490
|
+
hasNestedTables.peek() &&
|
|
491
|
+
!$isMultiCellTableSelection(selection)
|
|
492
|
+
) {
|
|
493
|
+
return false;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// If we reached this point, there's a table in the selection and nested tables are not allowed - reject the paste.
|
|
497
|
+
return true;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function $insertTableIntoGrid(
|
|
501
|
+
tableNode: TableNode,
|
|
502
|
+
selection: RangeSelection | TableSelection,
|
|
503
|
+
) {
|
|
504
|
+
const anchorAndFocus = selection.getStartEndPoints();
|
|
505
|
+
const isTableSelection = $isTableSelection(selection);
|
|
506
|
+
|
|
507
|
+
if (anchorAndFocus === null) {
|
|
508
|
+
return false;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const [anchor, focus] = anchorAndFocus;
|
|
512
|
+
const [anchorCellNode, anchorRowNode, gridNode] = $getNodeTriplet(anchor);
|
|
513
|
+
const focusCellNode = $findMatchingParent(focus.getNode(), n =>
|
|
514
|
+
$isTableCellNode(n),
|
|
515
|
+
);
|
|
516
|
+
|
|
517
|
+
if (
|
|
518
|
+
!$isTableCellNode(anchorCellNode) ||
|
|
519
|
+
!$isTableCellNode(focusCellNode) ||
|
|
520
|
+
!$isTableRowNode(anchorRowNode) ||
|
|
521
|
+
!$isTableNode(gridNode)
|
|
522
|
+
) {
|
|
523
|
+
return false;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const [initialGridMap, anchorCellMap, focusCellMap] = $computeTableMap(
|
|
527
|
+
gridNode,
|
|
528
|
+
anchorCellNode,
|
|
529
|
+
focusCellNode,
|
|
530
|
+
);
|
|
531
|
+
const [templateGridMap] = $computeTableMapSkipCellCheck(
|
|
532
|
+
tableNode,
|
|
533
|
+
null,
|
|
534
|
+
null,
|
|
535
|
+
);
|
|
536
|
+
const initialRowCount = initialGridMap.length;
|
|
537
|
+
const initialColCount = initialRowCount > 0 ? initialGridMap[0].length : 0;
|
|
538
|
+
|
|
539
|
+
// If we have a range selection, we'll fit the template grid into the
|
|
540
|
+
// table, growing the table if necessary.
|
|
541
|
+
let startRow = anchorCellMap.startRow;
|
|
542
|
+
let startCol = anchorCellMap.startColumn;
|
|
543
|
+
let affectedRowCount = templateGridMap.length;
|
|
544
|
+
let affectedColCount = affectedRowCount > 0 ? templateGridMap[0].length : 0;
|
|
545
|
+
|
|
546
|
+
if (isTableSelection) {
|
|
547
|
+
// If we have a table selection, we'll only modify the cells within
|
|
548
|
+
// the selection boundary.
|
|
549
|
+
const selectionBoundary = $computeTableCellRectBoundary(
|
|
550
|
+
initialGridMap,
|
|
551
|
+
anchorCellMap,
|
|
552
|
+
focusCellMap,
|
|
553
|
+
);
|
|
554
|
+
const selectionRowCount =
|
|
555
|
+
selectionBoundary.maxRow - selectionBoundary.minRow + 1;
|
|
556
|
+
const selectionColCount =
|
|
557
|
+
selectionBoundary.maxColumn - selectionBoundary.minColumn + 1;
|
|
558
|
+
startRow = selectionBoundary.minRow;
|
|
559
|
+
startCol = selectionBoundary.minColumn;
|
|
560
|
+
affectedRowCount = Math.min(affectedRowCount, selectionRowCount);
|
|
561
|
+
affectedColCount = Math.min(affectedColCount, selectionColCount);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Step 1: Unmerge all merged cells within the affected area
|
|
565
|
+
let didPerformMergeOperations = false;
|
|
566
|
+
const lastRowForUnmerge =
|
|
567
|
+
Math.min(initialRowCount, startRow + affectedRowCount) - 1;
|
|
568
|
+
const lastColForUnmerge =
|
|
569
|
+
Math.min(initialColCount, startCol + affectedColCount) - 1;
|
|
570
|
+
const unmergedKeys = new Set<NodeKey>();
|
|
571
|
+
for (let row = startRow; row <= lastRowForUnmerge; row++) {
|
|
572
|
+
for (let col = startCol; col <= lastColForUnmerge; col++) {
|
|
573
|
+
const cellMap = initialGridMap[row][col];
|
|
574
|
+
if (unmergedKeys.has(cellMap.cell.getKey())) {
|
|
575
|
+
continue; // cell was a merged cell that was already handled
|
|
576
|
+
}
|
|
577
|
+
if (cellMap.cell.__rowSpan === 1 && cellMap.cell.__colSpan === 1) {
|
|
578
|
+
continue; // cell is not a merged cell
|
|
579
|
+
}
|
|
580
|
+
$unmergeCellNode(cellMap.cell);
|
|
581
|
+
unmergedKeys.add(cellMap.cell.getKey());
|
|
582
|
+
didPerformMergeOperations = true;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
let [interimGridMap] = $computeTableMapSkipCellCheck(
|
|
587
|
+
gridNode.getWritable(),
|
|
588
|
+
null,
|
|
589
|
+
null,
|
|
590
|
+
);
|
|
591
|
+
|
|
592
|
+
// Step 2: Expand current table (if needed)
|
|
593
|
+
const rowsToInsert = affectedRowCount - initialRowCount + startRow;
|
|
594
|
+
for (let i = 0; i < rowsToInsert; i++) {
|
|
595
|
+
const cellMap = interimGridMap[initialRowCount - 1][0];
|
|
596
|
+
$insertTableRowAtNode(cellMap.cell);
|
|
597
|
+
}
|
|
598
|
+
const colsToInsert = affectedColCount - initialColCount + startCol;
|
|
599
|
+
for (let i = 0; i < colsToInsert; i++) {
|
|
600
|
+
const cellMap = interimGridMap[0][initialColCount - 1];
|
|
601
|
+
$insertTableColumnAtNode(cellMap.cell, true, false);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
[interimGridMap] = $computeTableMapSkipCellCheck(
|
|
605
|
+
gridNode.getWritable(),
|
|
606
|
+
null,
|
|
607
|
+
null,
|
|
608
|
+
);
|
|
609
|
+
|
|
610
|
+
// Step 3: Merge cells and set cell content, to match template grid
|
|
611
|
+
for (let row = startRow; row < startRow + affectedRowCount; row++) {
|
|
612
|
+
for (let col = startCol; col < startCol + affectedColCount; col++) {
|
|
613
|
+
const templateRow = row - startRow;
|
|
614
|
+
const templateCol = col - startCol;
|
|
615
|
+
const templateCellMap = templateGridMap[templateRow][templateCol];
|
|
616
|
+
if (
|
|
617
|
+
templateCellMap.startRow !== templateRow ||
|
|
618
|
+
templateCellMap.startColumn !== templateCol
|
|
619
|
+
) {
|
|
620
|
+
continue; // cell is a merged cell that was already handled
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const templateCell = templateCellMap.cell;
|
|
624
|
+
if (templateCell.__rowSpan !== 1 || templateCell.__colSpan !== 1) {
|
|
625
|
+
const cellsToMerge = [];
|
|
626
|
+
const lastRowForMerge =
|
|
627
|
+
Math.min(row + templateCell.__rowSpan, startRow + affectedRowCount) -
|
|
628
|
+
1;
|
|
629
|
+
const lastColForMerge =
|
|
630
|
+
Math.min(col + templateCell.__colSpan, startCol + affectedColCount) -
|
|
631
|
+
1;
|
|
632
|
+
for (let r = row; r <= lastRowForMerge; r++) {
|
|
633
|
+
for (let c = col; c <= lastColForMerge; c++) {
|
|
634
|
+
const cellMap = interimGridMap[r][c];
|
|
635
|
+
cellsToMerge.push(cellMap.cell);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
$mergeCells(cellsToMerge);
|
|
639
|
+
didPerformMergeOperations = true;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
const {cell} = interimGridMap[row][col];
|
|
643
|
+
const backgroundColor = templateCell.getBackgroundColor();
|
|
644
|
+
if (backgroundColor !== null && backgroundColor !== undefined) {
|
|
645
|
+
cell.setBackgroundColor(backgroundColor);
|
|
646
|
+
}
|
|
647
|
+
const originalChildren = cell.getChildren();
|
|
648
|
+
templateCell.getChildren().forEach(child => {
|
|
649
|
+
if ($isTextNode(child)) {
|
|
650
|
+
const paragraphNode = $createParagraphNode();
|
|
651
|
+
paragraphNode.append(child);
|
|
652
|
+
cell.append(child);
|
|
653
|
+
} else {
|
|
654
|
+
cell.append(child);
|
|
655
|
+
}
|
|
656
|
+
});
|
|
657
|
+
originalChildren.forEach(n => n.remove());
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
if (isTableSelection && didPerformMergeOperations) {
|
|
662
|
+
// reset the table selection in case the anchor or focus cell was
|
|
663
|
+
// removed via merge operations
|
|
664
|
+
const [finalGridMap] = $computeTableMapSkipCellCheck(
|
|
665
|
+
gridNode.getWritable(),
|
|
666
|
+
null,
|
|
667
|
+
null,
|
|
668
|
+
);
|
|
669
|
+
const newAnchorCellMap =
|
|
670
|
+
finalGridMap[anchorCellMap.startRow][anchorCellMap.startColumn];
|
|
671
|
+
newAnchorCellMap.cell.selectEnd();
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
return true;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function $isMultiCellTableSelection(
|
|
678
|
+
selection: TableSelection | RangeSelection,
|
|
679
|
+
) {
|
|
680
|
+
if (
|
|
681
|
+
$isTableSelection(selection) &&
|
|
682
|
+
!selection.focus.getNode().is(selection.anchor.getNode())
|
|
683
|
+
) {
|
|
684
|
+
return true;
|
|
685
|
+
}
|
|
686
|
+
if (
|
|
687
|
+
$isRangeSelection(selection) &&
|
|
688
|
+
$isTableCellNode(selection.anchor.getNode()) &&
|
|
689
|
+
!selection.anchor.getNode().is(selection.focus.getNode())
|
|
690
|
+
) {
|
|
691
|
+
return true;
|
|
692
|
+
}
|
|
693
|
+
return false;
|
|
694
|
+
}
|