@lexical/table 0.44.1-nightly.20260519.0 → 0.45.1-dev.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} +268 -33
- package/{LexicalTable.dev.mjs → dist/LexicalTable.dev.mjs} +264 -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 +41 -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 +324 -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,1386 @@
|
|
|
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 type {TableMapType, TableMapValueType} from './LexicalTableSelection';
|
|
10
|
+
import type {ElementNode, PointType} from 'lexical';
|
|
11
|
+
|
|
12
|
+
import invariant from '@lexical/internal/invariant';
|
|
13
|
+
import {$findMatchingParent} from '@lexical/utils';
|
|
14
|
+
import {
|
|
15
|
+
$createParagraphNode,
|
|
16
|
+
$createTextNode,
|
|
17
|
+
$getSelection,
|
|
18
|
+
$isParagraphNode,
|
|
19
|
+
$isRangeSelection,
|
|
20
|
+
LexicalNode,
|
|
21
|
+
} from 'lexical';
|
|
22
|
+
|
|
23
|
+
import {InsertTableCommandPayloadHeaders} from '.';
|
|
24
|
+
import {
|
|
25
|
+
$createTableCellNode,
|
|
26
|
+
$isTableCellNode,
|
|
27
|
+
TableCellHeaderState,
|
|
28
|
+
TableCellHeaderStates,
|
|
29
|
+
TableCellNode,
|
|
30
|
+
} from './LexicalTableCellNode';
|
|
31
|
+
import {$createTableNode, $isTableNode, TableNode} from './LexicalTableNode';
|
|
32
|
+
import {TableDOMTable} from './LexicalTableObserver';
|
|
33
|
+
import {
|
|
34
|
+
$createTableRowNode,
|
|
35
|
+
$isTableRowNode,
|
|
36
|
+
TableRowNode,
|
|
37
|
+
} from './LexicalTableRowNode';
|
|
38
|
+
import {$isTableSelection} from './LexicalTableSelection';
|
|
39
|
+
|
|
40
|
+
export function $createTableNodeWithDimensions(
|
|
41
|
+
rowCount: number,
|
|
42
|
+
columnCount: number,
|
|
43
|
+
includeHeaders: InsertTableCommandPayloadHeaders = true,
|
|
44
|
+
): TableNode {
|
|
45
|
+
const tableNode = $createTableNode();
|
|
46
|
+
|
|
47
|
+
for (let iRow = 0; iRow < rowCount; iRow++) {
|
|
48
|
+
const tableRowNode = $createTableRowNode();
|
|
49
|
+
|
|
50
|
+
for (let iColumn = 0; iColumn < columnCount; iColumn++) {
|
|
51
|
+
let headerState = TableCellHeaderStates.NO_STATUS;
|
|
52
|
+
|
|
53
|
+
if (typeof includeHeaders === 'object') {
|
|
54
|
+
if (iRow === 0 && includeHeaders.rows) {
|
|
55
|
+
headerState |= TableCellHeaderStates.ROW;
|
|
56
|
+
}
|
|
57
|
+
if (iColumn === 0 && includeHeaders.columns) {
|
|
58
|
+
headerState |= TableCellHeaderStates.COLUMN;
|
|
59
|
+
}
|
|
60
|
+
} else if (includeHeaders) {
|
|
61
|
+
if (iRow === 0) {
|
|
62
|
+
headerState |= TableCellHeaderStates.ROW;
|
|
63
|
+
}
|
|
64
|
+
if (iColumn === 0) {
|
|
65
|
+
headerState |= TableCellHeaderStates.COLUMN;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const tableCellNode = $createTableCellNode(headerState);
|
|
70
|
+
const paragraphNode = $createParagraphNode();
|
|
71
|
+
paragraphNode.append($createTextNode());
|
|
72
|
+
tableCellNode.append(paragraphNode);
|
|
73
|
+
tableRowNode.append(tableCellNode);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
tableNode.append(tableRowNode);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return tableNode;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function $getTableCellNodeFromLexicalNode(
|
|
83
|
+
startingNode: LexicalNode,
|
|
84
|
+
): TableCellNode | null {
|
|
85
|
+
const node = $findMatchingParent(startingNode, n => $isTableCellNode(n));
|
|
86
|
+
|
|
87
|
+
if ($isTableCellNode(node)) {
|
|
88
|
+
return node;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function $getTableRowNodeFromTableCellNodeOrThrow(
|
|
95
|
+
startingNode: LexicalNode,
|
|
96
|
+
): TableRowNode {
|
|
97
|
+
const node = $findMatchingParent(startingNode, n => $isTableRowNode(n));
|
|
98
|
+
|
|
99
|
+
if ($isTableRowNode(node)) {
|
|
100
|
+
return node;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
throw new Error('Expected table cell to be inside of table row.');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function $getTableNodeFromLexicalNodeOrThrow(
|
|
107
|
+
startingNode: LexicalNode,
|
|
108
|
+
): TableNode {
|
|
109
|
+
const node = $findMatchingParent(startingNode, n => $isTableNode(n));
|
|
110
|
+
|
|
111
|
+
if ($isTableNode(node)) {
|
|
112
|
+
return node;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
throw new Error('Expected table cell to be inside of table.');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function $getTableRowIndexFromTableCellNode(
|
|
119
|
+
tableCellNode: TableCellNode,
|
|
120
|
+
): number {
|
|
121
|
+
const tableRowNode = $getTableRowNodeFromTableCellNodeOrThrow(tableCellNode);
|
|
122
|
+
const tableNode = $getTableNodeFromLexicalNodeOrThrow(tableRowNode);
|
|
123
|
+
return tableNode.getChildren().findIndex(n => n.is(tableRowNode));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function $getTableColumnIndexFromTableCellNode(
|
|
127
|
+
tableCellNode: TableCellNode,
|
|
128
|
+
): number {
|
|
129
|
+
const tableRowNode = $getTableRowNodeFromTableCellNodeOrThrow(tableCellNode);
|
|
130
|
+
return tableRowNode.getChildren().findIndex(n => n.is(tableCellNode));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export type TableCellSiblings = {
|
|
134
|
+
above: TableCellNode | null | undefined;
|
|
135
|
+
below: TableCellNode | null | undefined;
|
|
136
|
+
left: TableCellNode | null | undefined;
|
|
137
|
+
right: TableCellNode | null | undefined;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
export function $getTableCellSiblingsFromTableCellNode(
|
|
141
|
+
tableCellNode: TableCellNode,
|
|
142
|
+
table: TableDOMTable,
|
|
143
|
+
): TableCellSiblings {
|
|
144
|
+
const tableNode = $getTableNodeFromLexicalNodeOrThrow(tableCellNode);
|
|
145
|
+
const {x, y} = tableNode.getCordsFromCellNode(tableCellNode, table);
|
|
146
|
+
return {
|
|
147
|
+
above: tableNode.getCellNodeFromCords(x, y - 1, table),
|
|
148
|
+
below: tableNode.getCellNodeFromCords(x, y + 1, table),
|
|
149
|
+
left: tableNode.getCellNodeFromCords(x - 1, y, table),
|
|
150
|
+
right: tableNode.getCellNodeFromCords(x + 1, y, table),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function $removeTableRowAtIndex(
|
|
155
|
+
tableNode: TableNode,
|
|
156
|
+
indexToDelete: number,
|
|
157
|
+
): TableNode {
|
|
158
|
+
const tableRows = tableNode.getChildren();
|
|
159
|
+
|
|
160
|
+
if (indexToDelete >= tableRows.length || indexToDelete < 0) {
|
|
161
|
+
throw new Error('Expected table cell to be inside of table row.');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const targetRowNode = tableRows[indexToDelete];
|
|
165
|
+
targetRowNode.remove();
|
|
166
|
+
return tableNode;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* @deprecated This function does not support merged cells. Use {@link $insertTableRowAtSelection} or {@link $insertTableRowAtNode} instead.
|
|
171
|
+
*/
|
|
172
|
+
export function $insertTableRow(
|
|
173
|
+
tableNode: TableNode,
|
|
174
|
+
targetIndex: number,
|
|
175
|
+
shouldInsertAfter = true,
|
|
176
|
+
rowCount: number,
|
|
177
|
+
table: TableDOMTable,
|
|
178
|
+
): TableNode {
|
|
179
|
+
const tableRows = tableNode.getChildren();
|
|
180
|
+
|
|
181
|
+
if (targetIndex >= tableRows.length || targetIndex < 0) {
|
|
182
|
+
throw new Error('Table row target index out of range');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const targetRowNode = tableRows[targetIndex];
|
|
186
|
+
|
|
187
|
+
if ($isTableRowNode(targetRowNode)) {
|
|
188
|
+
for (let r = 0; r < rowCount; r++) {
|
|
189
|
+
const tableRowCells = targetRowNode.getChildren<TableCellNode>();
|
|
190
|
+
const tableColumnCount = tableRowCells.length;
|
|
191
|
+
const newTableRowNode = $createTableRowNode();
|
|
192
|
+
|
|
193
|
+
for (let c = 0; c < tableColumnCount; c++) {
|
|
194
|
+
const tableCellFromTargetRow = tableRowCells[c];
|
|
195
|
+
|
|
196
|
+
invariant(
|
|
197
|
+
$isTableCellNode(tableCellFromTargetRow),
|
|
198
|
+
'Expected table cell',
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
const {above, below} = $getTableCellSiblingsFromTableCellNode(
|
|
202
|
+
tableCellFromTargetRow,
|
|
203
|
+
table,
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
let headerState = TableCellHeaderStates.NO_STATUS;
|
|
207
|
+
const width =
|
|
208
|
+
(above && above.getWidth()) ||
|
|
209
|
+
(below && below.getWidth()) ||
|
|
210
|
+
undefined;
|
|
211
|
+
|
|
212
|
+
if (
|
|
213
|
+
(above && above.hasHeaderState(TableCellHeaderStates.COLUMN)) ||
|
|
214
|
+
(below && below.hasHeaderState(TableCellHeaderStates.COLUMN))
|
|
215
|
+
) {
|
|
216
|
+
headerState |= TableCellHeaderStates.COLUMN;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const tableCellNode = $createTableCellNode(headerState, 1, width);
|
|
220
|
+
|
|
221
|
+
tableCellNode.append($createParagraphNode());
|
|
222
|
+
|
|
223
|
+
newTableRowNode.append(tableCellNode);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (shouldInsertAfter) {
|
|
227
|
+
targetRowNode.insertAfter(newTableRowNode);
|
|
228
|
+
} else {
|
|
229
|
+
targetRowNode.insertBefore(newTableRowNode);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
} else {
|
|
233
|
+
throw new Error('Row before insertion index does not exist.');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return tableNode;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const getHeaderState = (
|
|
240
|
+
currentState: TableCellHeaderState,
|
|
241
|
+
possibleState: TableCellHeaderState,
|
|
242
|
+
): TableCellHeaderState => {
|
|
243
|
+
if (
|
|
244
|
+
currentState === TableCellHeaderStates.BOTH ||
|
|
245
|
+
currentState === possibleState
|
|
246
|
+
) {
|
|
247
|
+
return possibleState;
|
|
248
|
+
}
|
|
249
|
+
return TableCellHeaderStates.NO_STATUS;
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Inserts a table row before or after the current focus cell node,
|
|
254
|
+
* taking into account any spans. If successful, returns the
|
|
255
|
+
* inserted table row node.
|
|
256
|
+
*/
|
|
257
|
+
export function $insertTableRowAtSelection(
|
|
258
|
+
insertAfter = true,
|
|
259
|
+
): TableRowNode | null {
|
|
260
|
+
const selection = $getSelection();
|
|
261
|
+
invariant(
|
|
262
|
+
$isRangeSelection(selection) || $isTableSelection(selection),
|
|
263
|
+
'Expected a RangeSelection or TableSelection',
|
|
264
|
+
);
|
|
265
|
+
const anchor = selection.anchor.getNode();
|
|
266
|
+
const focus = selection.focus.getNode();
|
|
267
|
+
const [anchorCell] = $getNodeTriplet(anchor);
|
|
268
|
+
const [focusCell, , grid] = $getNodeTriplet(focus);
|
|
269
|
+
const [, focusCellMap, anchorCellMap] = $computeTableMap(
|
|
270
|
+
grid,
|
|
271
|
+
focusCell,
|
|
272
|
+
anchorCell,
|
|
273
|
+
);
|
|
274
|
+
const {startRow: anchorStartRow} = anchorCellMap;
|
|
275
|
+
const {startRow: focusStartRow} = focusCellMap;
|
|
276
|
+
if (insertAfter) {
|
|
277
|
+
return $insertTableRowAtNode(
|
|
278
|
+
anchorStartRow + anchorCell.__rowSpan >
|
|
279
|
+
focusStartRow + focusCell.__rowSpan
|
|
280
|
+
? anchorCell
|
|
281
|
+
: focusCell,
|
|
282
|
+
true,
|
|
283
|
+
);
|
|
284
|
+
} else {
|
|
285
|
+
return $insertTableRowAtNode(
|
|
286
|
+
focusStartRow < anchorStartRow ? focusCell : anchorCell,
|
|
287
|
+
false,
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* @deprecated renamed to {@link $insertTableRowAtSelection}
|
|
294
|
+
*/
|
|
295
|
+
export const $insertTableRow__EXPERIMENTAL = $insertTableRowAtSelection;
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Inserts a table row before or after the given cell node,
|
|
299
|
+
* taking into account any spans. If successful, returns the
|
|
300
|
+
* inserted table row node.
|
|
301
|
+
*/
|
|
302
|
+
export function $insertTableRowAtNode(
|
|
303
|
+
cellNode: TableCellNode,
|
|
304
|
+
insertAfter = true,
|
|
305
|
+
): TableRowNode | null {
|
|
306
|
+
const [, , grid] = $getNodeTriplet(cellNode);
|
|
307
|
+
const [gridMap, cellMap] = $computeTableMap(grid, cellNode, cellNode);
|
|
308
|
+
const columnCount = gridMap[0].length;
|
|
309
|
+
const {startRow: cellStartRow} = cellMap;
|
|
310
|
+
let insertedRow: TableRowNode | null = null;
|
|
311
|
+
if (insertAfter) {
|
|
312
|
+
const insertAfterEndRow = cellStartRow + cellNode.__rowSpan - 1;
|
|
313
|
+
const insertAfterEndRowMap = gridMap[insertAfterEndRow];
|
|
314
|
+
const newRow = $createTableRowNode();
|
|
315
|
+
for (let i = 0; i < columnCount; i++) {
|
|
316
|
+
const {cell, startRow} = insertAfterEndRowMap[i];
|
|
317
|
+
if (startRow + cell.__rowSpan - 1 <= insertAfterEndRow) {
|
|
318
|
+
const currentCell = insertAfterEndRowMap[i].cell;
|
|
319
|
+
const currentCellHeaderState = currentCell.__headerState;
|
|
320
|
+
|
|
321
|
+
const headerState = getHeaderState(
|
|
322
|
+
currentCellHeaderState,
|
|
323
|
+
TableCellHeaderStates.COLUMN,
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
newRow.append(
|
|
327
|
+
$createTableCellNode(headerState).append($createParagraphNode()),
|
|
328
|
+
);
|
|
329
|
+
} else {
|
|
330
|
+
cell.setRowSpan(cell.__rowSpan + 1);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
const insertAfterEndRowNode = grid.getChildAtIndex(insertAfterEndRow);
|
|
334
|
+
invariant(
|
|
335
|
+
$isTableRowNode(insertAfterEndRowNode),
|
|
336
|
+
'insertAfterEndRow is not a TableRowNode',
|
|
337
|
+
);
|
|
338
|
+
insertAfterEndRowNode.insertAfter(newRow);
|
|
339
|
+
insertedRow = newRow;
|
|
340
|
+
} else {
|
|
341
|
+
const insertBeforeStartRow = cellStartRow;
|
|
342
|
+
const insertBeforeStartRowMap = gridMap[insertBeforeStartRow];
|
|
343
|
+
const newRow = $createTableRowNode();
|
|
344
|
+
for (let i = 0; i < columnCount; i++) {
|
|
345
|
+
const {cell, startRow} = insertBeforeStartRowMap[i];
|
|
346
|
+
if (startRow === insertBeforeStartRow) {
|
|
347
|
+
const currentCell = insertBeforeStartRowMap[i].cell;
|
|
348
|
+
const currentCellHeaderState = currentCell.__headerState;
|
|
349
|
+
|
|
350
|
+
const headerState = getHeaderState(
|
|
351
|
+
currentCellHeaderState,
|
|
352
|
+
TableCellHeaderStates.COLUMN,
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
newRow.append(
|
|
356
|
+
$createTableCellNode(headerState).append($createParagraphNode()),
|
|
357
|
+
);
|
|
358
|
+
} else {
|
|
359
|
+
cell.setRowSpan(cell.__rowSpan + 1);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
const insertBeforeStartRowNode = grid.getChildAtIndex(insertBeforeStartRow);
|
|
363
|
+
invariant(
|
|
364
|
+
$isTableRowNode(insertBeforeStartRowNode),
|
|
365
|
+
'insertBeforeStartRow is not a TableRowNode',
|
|
366
|
+
);
|
|
367
|
+
insertBeforeStartRowNode.insertBefore(newRow);
|
|
368
|
+
insertedRow = newRow;
|
|
369
|
+
}
|
|
370
|
+
return insertedRow;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* @deprecated This function does not support merged cells. Use {@link $insertTableColumnAtSelection} or {@link $insertTableColumnAtNode} instead.
|
|
375
|
+
*/
|
|
376
|
+
export function $insertTableColumn(
|
|
377
|
+
tableNode: TableNode,
|
|
378
|
+
targetIndex: number,
|
|
379
|
+
shouldInsertAfter = true,
|
|
380
|
+
columnCount: number,
|
|
381
|
+
table: TableDOMTable,
|
|
382
|
+
): TableNode {
|
|
383
|
+
const tableRows = tableNode.getChildren();
|
|
384
|
+
|
|
385
|
+
const tableCellsToBeInserted = [];
|
|
386
|
+
for (let r = 0; r < tableRows.length; r++) {
|
|
387
|
+
const currentTableRowNode = tableRows[r];
|
|
388
|
+
|
|
389
|
+
if ($isTableRowNode(currentTableRowNode)) {
|
|
390
|
+
for (let c = 0; c < columnCount; c++) {
|
|
391
|
+
const tableRowChildren = currentTableRowNode.getChildren();
|
|
392
|
+
if (targetIndex >= tableRowChildren.length || targetIndex < 0) {
|
|
393
|
+
throw new Error('Table column target index out of range');
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const targetCell = tableRowChildren[targetIndex];
|
|
397
|
+
|
|
398
|
+
invariant($isTableCellNode(targetCell), 'Expected table cell');
|
|
399
|
+
|
|
400
|
+
const {left, right} = $getTableCellSiblingsFromTableCellNode(
|
|
401
|
+
targetCell,
|
|
402
|
+
table,
|
|
403
|
+
);
|
|
404
|
+
|
|
405
|
+
let headerState = TableCellHeaderStates.NO_STATUS;
|
|
406
|
+
|
|
407
|
+
if (
|
|
408
|
+
(left && left.hasHeaderState(TableCellHeaderStates.ROW)) ||
|
|
409
|
+
(right && right.hasHeaderState(TableCellHeaderStates.ROW))
|
|
410
|
+
) {
|
|
411
|
+
headerState |= TableCellHeaderStates.ROW;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const newTableCell = $createTableCellNode(headerState);
|
|
415
|
+
|
|
416
|
+
newTableCell.append($createParagraphNode());
|
|
417
|
+
tableCellsToBeInserted.push({
|
|
418
|
+
newTableCell,
|
|
419
|
+
targetCell,
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
tableCellsToBeInserted.forEach(({newTableCell, targetCell}) => {
|
|
425
|
+
if (shouldInsertAfter) {
|
|
426
|
+
targetCell.insertAfter(newTableCell);
|
|
427
|
+
} else {
|
|
428
|
+
targetCell.insertBefore(newTableCell);
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
return tableNode;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Inserts a column before or after the current focus cell node,
|
|
437
|
+
* taking into account any spans. If successful, returns the
|
|
438
|
+
* first inserted cell node.
|
|
439
|
+
*/
|
|
440
|
+
export function $insertTableColumnAtSelection(
|
|
441
|
+
insertAfter = true,
|
|
442
|
+
): TableCellNode | null {
|
|
443
|
+
const selection = $getSelection();
|
|
444
|
+
invariant(
|
|
445
|
+
$isRangeSelection(selection) || $isTableSelection(selection),
|
|
446
|
+
'Expected a RangeSelection or TableSelection',
|
|
447
|
+
);
|
|
448
|
+
const anchor = selection.anchor.getNode();
|
|
449
|
+
const focus = selection.focus.getNode();
|
|
450
|
+
const [anchorCell] = $getNodeTriplet(anchor);
|
|
451
|
+
const [focusCell, , grid] = $getNodeTriplet(focus);
|
|
452
|
+
const [, focusCellMap, anchorCellMap] = $computeTableMap(
|
|
453
|
+
grid,
|
|
454
|
+
focusCell,
|
|
455
|
+
anchorCell,
|
|
456
|
+
);
|
|
457
|
+
const {startColumn: anchorStartColumn} = anchorCellMap;
|
|
458
|
+
const {startColumn: focusStartColumn} = focusCellMap;
|
|
459
|
+
if (insertAfter) {
|
|
460
|
+
return $insertTableColumnAtNode(
|
|
461
|
+
anchorStartColumn + anchorCell.__colSpan >
|
|
462
|
+
focusStartColumn + focusCell.__colSpan
|
|
463
|
+
? anchorCell
|
|
464
|
+
: focusCell,
|
|
465
|
+
true,
|
|
466
|
+
);
|
|
467
|
+
} else {
|
|
468
|
+
return $insertTableColumnAtNode(
|
|
469
|
+
focusStartColumn < anchorStartColumn ? focusCell : anchorCell,
|
|
470
|
+
false,
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* @deprecated renamed to {@link $insertTableColumnAtSelection}
|
|
477
|
+
*/
|
|
478
|
+
export const $insertTableColumn__EXPERIMENTAL = $insertTableColumnAtSelection;
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Inserts a column before or after the given cell node,
|
|
482
|
+
* taking into account any spans. If successful, returns the
|
|
483
|
+
* first inserted cell node.
|
|
484
|
+
*/
|
|
485
|
+
export function $insertTableColumnAtNode(
|
|
486
|
+
cellNode: TableCellNode,
|
|
487
|
+
insertAfter = true,
|
|
488
|
+
shouldSetSelection = true,
|
|
489
|
+
): TableCellNode | null {
|
|
490
|
+
const [, , grid] = $getNodeTriplet(cellNode);
|
|
491
|
+
const [gridMap, cellMap] = $computeTableMap(grid, cellNode, cellNode);
|
|
492
|
+
const rowCount = gridMap.length;
|
|
493
|
+
const {startColumn} = cellMap;
|
|
494
|
+
const insertAfterColumn = insertAfter
|
|
495
|
+
? startColumn + cellNode.__colSpan - 1
|
|
496
|
+
: startColumn - 1;
|
|
497
|
+
const gridFirstChild = grid.getFirstChild();
|
|
498
|
+
invariant(
|
|
499
|
+
$isTableRowNode(gridFirstChild),
|
|
500
|
+
'Expected firstTable child to be a row',
|
|
501
|
+
);
|
|
502
|
+
let firstInsertedCell: null | TableCellNode = null;
|
|
503
|
+
function $createTableCellNodeForInsertTableColumn(
|
|
504
|
+
headerState: TableCellHeaderState = TableCellHeaderStates.NO_STATUS,
|
|
505
|
+
) {
|
|
506
|
+
const cell = $createTableCellNode(headerState).append(
|
|
507
|
+
$createParagraphNode(),
|
|
508
|
+
);
|
|
509
|
+
if (firstInsertedCell === null) {
|
|
510
|
+
firstInsertedCell = cell;
|
|
511
|
+
}
|
|
512
|
+
return cell;
|
|
513
|
+
}
|
|
514
|
+
let loopRow: TableRowNode = gridFirstChild;
|
|
515
|
+
rowLoop: for (let i = 0; i < rowCount; i++) {
|
|
516
|
+
if (i !== 0) {
|
|
517
|
+
const currentRow = loopRow.getNextSibling();
|
|
518
|
+
invariant(
|
|
519
|
+
$isTableRowNode(currentRow),
|
|
520
|
+
'Expected row nextSibling to be a row',
|
|
521
|
+
);
|
|
522
|
+
loopRow = currentRow;
|
|
523
|
+
}
|
|
524
|
+
const rowMap = gridMap[i];
|
|
525
|
+
|
|
526
|
+
const currentCellHeaderState =
|
|
527
|
+
rowMap[insertAfterColumn < 0 ? 0 : insertAfterColumn].cell.__headerState;
|
|
528
|
+
|
|
529
|
+
const headerState = getHeaderState(
|
|
530
|
+
currentCellHeaderState,
|
|
531
|
+
TableCellHeaderStates.ROW,
|
|
532
|
+
);
|
|
533
|
+
|
|
534
|
+
if (insertAfterColumn < 0) {
|
|
535
|
+
$insertFirst(
|
|
536
|
+
loopRow,
|
|
537
|
+
$createTableCellNodeForInsertTableColumn(headerState),
|
|
538
|
+
);
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
const {
|
|
542
|
+
cell: currentCell,
|
|
543
|
+
startColumn: currentStartColumn,
|
|
544
|
+
startRow: currentStartRow,
|
|
545
|
+
} = rowMap[insertAfterColumn];
|
|
546
|
+
if (currentStartColumn + currentCell.__colSpan - 1 <= insertAfterColumn) {
|
|
547
|
+
let insertAfterCell: TableCellNode = currentCell;
|
|
548
|
+
let insertAfterCellRowStart = currentStartRow;
|
|
549
|
+
let prevCellIndex = insertAfterColumn;
|
|
550
|
+
while (insertAfterCellRowStart !== i && insertAfterCell.__rowSpan > 1) {
|
|
551
|
+
prevCellIndex -= currentCell.__colSpan;
|
|
552
|
+
if (prevCellIndex >= 0) {
|
|
553
|
+
const {cell: cell_, startRow: startRow_} = rowMap[prevCellIndex];
|
|
554
|
+
insertAfterCell = cell_;
|
|
555
|
+
insertAfterCellRowStart = startRow_;
|
|
556
|
+
} else {
|
|
557
|
+
loopRow.append($createTableCellNodeForInsertTableColumn(headerState));
|
|
558
|
+
continue rowLoop;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
insertAfterCell.insertAfter(
|
|
562
|
+
$createTableCellNodeForInsertTableColumn(headerState),
|
|
563
|
+
);
|
|
564
|
+
} else {
|
|
565
|
+
currentCell.setColSpan(currentCell.__colSpan + 1);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
if (firstInsertedCell !== null && shouldSetSelection) {
|
|
569
|
+
$moveSelectionToCell(firstInsertedCell);
|
|
570
|
+
}
|
|
571
|
+
const colWidths = grid.getColWidths();
|
|
572
|
+
if (colWidths) {
|
|
573
|
+
const newColWidths = [...colWidths];
|
|
574
|
+
const columnIndex = insertAfterColumn < 0 ? 0 : insertAfterColumn;
|
|
575
|
+
const newWidth = newColWidths[columnIndex];
|
|
576
|
+
newColWidths.splice(columnIndex, 0, newWidth);
|
|
577
|
+
grid.setColWidths(newColWidths);
|
|
578
|
+
}
|
|
579
|
+
return firstInsertedCell;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* @deprecated This function does not support merged cells. Use {@link $deleteTableColumnAtSelection} instead.
|
|
584
|
+
*/
|
|
585
|
+
export function $deleteTableColumn(
|
|
586
|
+
tableNode: TableNode,
|
|
587
|
+
targetIndex: number,
|
|
588
|
+
): TableNode {
|
|
589
|
+
const tableRows = tableNode.getChildren();
|
|
590
|
+
|
|
591
|
+
for (let i = 0; i < tableRows.length; i++) {
|
|
592
|
+
const currentTableRowNode = tableRows[i];
|
|
593
|
+
|
|
594
|
+
if ($isTableRowNode(currentTableRowNode)) {
|
|
595
|
+
const tableRowChildren = currentTableRowNode.getChildren();
|
|
596
|
+
|
|
597
|
+
if (targetIndex >= tableRowChildren.length || targetIndex < 0) {
|
|
598
|
+
throw new Error('Table column target index out of range');
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
tableRowChildren[targetIndex].remove();
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
return tableNode;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
export function $deleteTableRowAtSelection(): void {
|
|
609
|
+
const selection = $getSelection();
|
|
610
|
+
invariant(
|
|
611
|
+
$isRangeSelection(selection) || $isTableSelection(selection),
|
|
612
|
+
'Expected a RangeSelection or TableSelection',
|
|
613
|
+
);
|
|
614
|
+
const [anchor, focus] = selection.isBackward()
|
|
615
|
+
? [selection.focus.getNode(), selection.anchor.getNode()]
|
|
616
|
+
: [selection.anchor.getNode(), selection.focus.getNode()];
|
|
617
|
+
const [anchorCell, , grid] = $getNodeTriplet(anchor);
|
|
618
|
+
const [focusCell] = $getNodeTriplet(focus);
|
|
619
|
+
const [gridMap, anchorCellMap, focusCellMap] = $computeTableMap(
|
|
620
|
+
grid,
|
|
621
|
+
anchorCell,
|
|
622
|
+
focusCell,
|
|
623
|
+
);
|
|
624
|
+
const {startRow: anchorStartRow} = anchorCellMap;
|
|
625
|
+
const {startRow: focusStartRow} = focusCellMap;
|
|
626
|
+
const focusEndRow = focusStartRow + focusCell.__rowSpan - 1;
|
|
627
|
+
if (gridMap.length === focusEndRow - anchorStartRow + 1) {
|
|
628
|
+
// Empty grid
|
|
629
|
+
grid.remove();
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
const columnCount = gridMap[0].length;
|
|
633
|
+
const nextRow = gridMap[focusEndRow + 1];
|
|
634
|
+
const nextRowNode: null | TableRowNode = grid.getChildAtIndex(
|
|
635
|
+
focusEndRow + 1,
|
|
636
|
+
);
|
|
637
|
+
for (let row = focusEndRow; row >= anchorStartRow; row--) {
|
|
638
|
+
for (let column = columnCount - 1; column >= 0; column--) {
|
|
639
|
+
const {
|
|
640
|
+
cell,
|
|
641
|
+
startRow: cellStartRow,
|
|
642
|
+
startColumn: cellStartColumn,
|
|
643
|
+
} = gridMap[row][column];
|
|
644
|
+
if (cellStartColumn !== column) {
|
|
645
|
+
// Don't repeat work for the same Cell
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
648
|
+
// Rows overflowing top or bottom have to be trimmed
|
|
649
|
+
if (
|
|
650
|
+
cellStartRow < anchorStartRow ||
|
|
651
|
+
cellStartRow + cell.__rowSpan - 1 > focusEndRow
|
|
652
|
+
) {
|
|
653
|
+
const intersectionStart = Math.max(cellStartRow, anchorStartRow);
|
|
654
|
+
const intersectionEnd = Math.min(
|
|
655
|
+
cell.__rowSpan + cellStartRow - 1,
|
|
656
|
+
focusEndRow,
|
|
657
|
+
);
|
|
658
|
+
|
|
659
|
+
const overflowRowsCount =
|
|
660
|
+
intersectionStart <= intersectionEnd
|
|
661
|
+
? intersectionEnd - intersectionStart + 1
|
|
662
|
+
: 0;
|
|
663
|
+
cell.setRowSpan(cell.__rowSpan - overflowRowsCount);
|
|
664
|
+
}
|
|
665
|
+
// Rows overflowing bottom have to be moved to the next row
|
|
666
|
+
if (
|
|
667
|
+
cellStartRow >= anchorStartRow &&
|
|
668
|
+
cellStartRow + cell.__rowSpan - 1 > focusEndRow &&
|
|
669
|
+
// Handle overflow only once
|
|
670
|
+
row === focusEndRow
|
|
671
|
+
) {
|
|
672
|
+
invariant(nextRowNode !== null, 'Expected nextRowNode not to be null');
|
|
673
|
+
let insertAfterCell: null | TableCellNode = null;
|
|
674
|
+
for (let columnIndex = 0; columnIndex < column; columnIndex++) {
|
|
675
|
+
const currentCellMap = nextRow[columnIndex];
|
|
676
|
+
const currentCell = currentCellMap.cell;
|
|
677
|
+
// Checking the cell having startRow as same as nextRow
|
|
678
|
+
if (currentCellMap.startRow === row + 1) {
|
|
679
|
+
insertAfterCell = currentCell;
|
|
680
|
+
}
|
|
681
|
+
if (currentCell.__colSpan > 1) {
|
|
682
|
+
columnIndex += currentCell.__colSpan - 1;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
if (insertAfterCell === null) {
|
|
686
|
+
$insertFirst(nextRowNode, cell);
|
|
687
|
+
} else {
|
|
688
|
+
insertAfterCell.insertAfter(cell);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
const rowNode = grid.getChildAtIndex(row);
|
|
693
|
+
invariant(
|
|
694
|
+
$isTableRowNode(rowNode),
|
|
695
|
+
'Expected TableNode childAtIndex(%s) to be RowNode',
|
|
696
|
+
String(row),
|
|
697
|
+
);
|
|
698
|
+
rowNode.remove();
|
|
699
|
+
}
|
|
700
|
+
if (nextRow !== undefined) {
|
|
701
|
+
const {cell} = nextRow[0];
|
|
702
|
+
$moveSelectionToCell(cell);
|
|
703
|
+
} else {
|
|
704
|
+
const previousRow = gridMap[anchorStartRow - 1];
|
|
705
|
+
const {cell} = previousRow[0];
|
|
706
|
+
$moveSelectionToCell(cell);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* @deprecated renamed to {@link $deleteTableRowAtSelection}
|
|
712
|
+
*/
|
|
713
|
+
export const $deleteTableRow__EXPERIMENTAL = $deleteTableRowAtSelection;
|
|
714
|
+
|
|
715
|
+
export function $deleteTableColumnAtSelection(): void {
|
|
716
|
+
const selection = $getSelection();
|
|
717
|
+
invariant(
|
|
718
|
+
$isRangeSelection(selection) || $isTableSelection(selection),
|
|
719
|
+
'Expected a RangeSelection or TableSelection',
|
|
720
|
+
);
|
|
721
|
+
const anchor = selection.anchor.getNode();
|
|
722
|
+
const focus = selection.focus.getNode();
|
|
723
|
+
const [anchorCell, , grid] = $getNodeTriplet(anchor);
|
|
724
|
+
const [focusCell] = $getNodeTriplet(focus);
|
|
725
|
+
const [gridMap, anchorCellMap, focusCellMap] = $computeTableMap(
|
|
726
|
+
grid,
|
|
727
|
+
anchorCell,
|
|
728
|
+
focusCell,
|
|
729
|
+
);
|
|
730
|
+
const {startColumn: anchorStartColumn} = anchorCellMap;
|
|
731
|
+
const {startRow: focusStartRow, startColumn: focusStartColumn} = focusCellMap;
|
|
732
|
+
const startColumn = Math.min(anchorStartColumn, focusStartColumn);
|
|
733
|
+
const endColumn = Math.max(
|
|
734
|
+
anchorStartColumn + anchorCell.__colSpan - 1,
|
|
735
|
+
focusStartColumn + focusCell.__colSpan - 1,
|
|
736
|
+
);
|
|
737
|
+
const selectedColumnCount = endColumn - startColumn + 1;
|
|
738
|
+
const columnCount = gridMap[0].length;
|
|
739
|
+
if (columnCount === endColumn - startColumn + 1) {
|
|
740
|
+
// Empty grid
|
|
741
|
+
grid.selectPrevious();
|
|
742
|
+
grid.remove();
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
const rowCount = gridMap.length;
|
|
746
|
+
for (let row = 0; row < rowCount; row++) {
|
|
747
|
+
for (let column = startColumn; column <= endColumn; column++) {
|
|
748
|
+
const {cell, startColumn: cellStartColumn} = gridMap[row][column];
|
|
749
|
+
if (cellStartColumn < startColumn) {
|
|
750
|
+
if (column === startColumn) {
|
|
751
|
+
const overflowLeft = startColumn - cellStartColumn;
|
|
752
|
+
// Overflowing left
|
|
753
|
+
cell.setColSpan(
|
|
754
|
+
cell.__colSpan -
|
|
755
|
+
// Possible overflow right too
|
|
756
|
+
Math.min(selectedColumnCount, cell.__colSpan - overflowLeft),
|
|
757
|
+
);
|
|
758
|
+
}
|
|
759
|
+
} else if (cellStartColumn + cell.__colSpan - 1 > endColumn) {
|
|
760
|
+
if (column === endColumn) {
|
|
761
|
+
// Overflowing right
|
|
762
|
+
const inSelectedArea = endColumn - cellStartColumn + 1;
|
|
763
|
+
cell.setColSpan(cell.__colSpan - inSelectedArea);
|
|
764
|
+
}
|
|
765
|
+
} else {
|
|
766
|
+
cell.remove();
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
const focusRowMap = gridMap[focusStartRow];
|
|
771
|
+
const nextColumn =
|
|
772
|
+
anchorStartColumn > focusStartColumn
|
|
773
|
+
? focusRowMap[anchorStartColumn + anchorCell.__colSpan]
|
|
774
|
+
: focusRowMap[focusStartColumn + focusCell.__colSpan];
|
|
775
|
+
if (nextColumn !== undefined) {
|
|
776
|
+
const {cell} = nextColumn;
|
|
777
|
+
$moveSelectionToCell(cell);
|
|
778
|
+
} else {
|
|
779
|
+
const previousRow =
|
|
780
|
+
focusStartColumn < anchorStartColumn
|
|
781
|
+
? focusRowMap[focusStartColumn - 1]
|
|
782
|
+
: focusRowMap[anchorStartColumn - 1];
|
|
783
|
+
const {cell} = previousRow;
|
|
784
|
+
$moveSelectionToCell(cell);
|
|
785
|
+
}
|
|
786
|
+
const colWidths = grid.getColWidths();
|
|
787
|
+
if (colWidths) {
|
|
788
|
+
const newColWidths = [...colWidths];
|
|
789
|
+
newColWidths.splice(startColumn, selectedColumnCount);
|
|
790
|
+
grid.setColWidths(newColWidths);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* @deprecated renamed to {@link $deleteTableColumnAtSelection}
|
|
796
|
+
*/
|
|
797
|
+
export const $deleteTableColumn__EXPERIMENTAL = $deleteTableColumnAtSelection;
|
|
798
|
+
|
|
799
|
+
function $moveSelectionToCell(cell: TableCellNode): void {
|
|
800
|
+
const firstDescendant = cell.getFirstDescendant();
|
|
801
|
+
if (firstDescendant == null) {
|
|
802
|
+
cell.selectStart();
|
|
803
|
+
} else {
|
|
804
|
+
firstDescendant.getParentOrThrow().selectStart();
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
function $insertFirst(parent: ElementNode, node: LexicalNode): void {
|
|
809
|
+
const firstChild = parent.getFirstChild();
|
|
810
|
+
if (firstChild !== null) {
|
|
811
|
+
firstChild.insertBefore(node);
|
|
812
|
+
} else {
|
|
813
|
+
parent.append(node);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
export function $mergeCells(cellNodes: TableCellNode[]): TableCellNode | null {
|
|
818
|
+
if (cellNodes.length === 0) {
|
|
819
|
+
return null;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// Find the table node
|
|
823
|
+
const tableNode = $getTableNodeFromLexicalNodeOrThrow(cellNodes[0]);
|
|
824
|
+
const [gridMap] = $computeTableMapSkipCellCheck(tableNode, null, null);
|
|
825
|
+
|
|
826
|
+
// Find the boundaries of the selection including merged cells
|
|
827
|
+
let minRow = Infinity;
|
|
828
|
+
let maxRow = -Infinity;
|
|
829
|
+
let minCol = Infinity;
|
|
830
|
+
let maxCol = -Infinity;
|
|
831
|
+
|
|
832
|
+
// First pass: find the actual boundaries considering merged cells
|
|
833
|
+
const processedCells = new Set();
|
|
834
|
+
for (const row of gridMap) {
|
|
835
|
+
for (const mapCell of row) {
|
|
836
|
+
if (!mapCell || !mapCell.cell) {
|
|
837
|
+
continue;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
const cellKey = mapCell.cell.getKey();
|
|
841
|
+
if (processedCells.has(cellKey)) {
|
|
842
|
+
continue;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
if (cellNodes.some(cell => cell.is(mapCell.cell))) {
|
|
846
|
+
processedCells.add(cellKey);
|
|
847
|
+
// Get the actual position of this cell in the grid
|
|
848
|
+
const cellStartRow = mapCell.startRow;
|
|
849
|
+
const cellStartCol = mapCell.startColumn;
|
|
850
|
+
const cellRowSpan = mapCell.cell.__rowSpan || 1;
|
|
851
|
+
const cellColSpan = mapCell.cell.__colSpan || 1;
|
|
852
|
+
|
|
853
|
+
// Update boundaries considering the cell's actual position and span
|
|
854
|
+
minRow = Math.min(minRow, cellStartRow);
|
|
855
|
+
maxRow = Math.max(maxRow, cellStartRow + cellRowSpan - 1);
|
|
856
|
+
minCol = Math.min(minCol, cellStartCol);
|
|
857
|
+
maxCol = Math.max(maxCol, cellStartCol + cellColSpan - 1);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
// Validate boundaries
|
|
863
|
+
if (minRow === Infinity || minCol === Infinity) {
|
|
864
|
+
return null;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// The total span of the merged cell
|
|
868
|
+
const totalRowSpan = maxRow - minRow + 1;
|
|
869
|
+
const totalColSpan = maxCol - minCol + 1;
|
|
870
|
+
|
|
871
|
+
// Use the top-left cell as the target cell
|
|
872
|
+
const targetCellMap = gridMap[minRow][minCol];
|
|
873
|
+
if (!targetCellMap.cell) {
|
|
874
|
+
return null;
|
|
875
|
+
}
|
|
876
|
+
const targetCell = targetCellMap.cell;
|
|
877
|
+
|
|
878
|
+
// Set the spans for the target cell
|
|
879
|
+
targetCell.setColSpan(totalColSpan);
|
|
880
|
+
targetCell.setRowSpan(totalRowSpan);
|
|
881
|
+
|
|
882
|
+
// Move content from other cells to the target cell
|
|
883
|
+
const seenCells = new Set([targetCell.getKey()]);
|
|
884
|
+
|
|
885
|
+
// Second pass: merge content and remove other cells
|
|
886
|
+
for (let row = minRow; row <= maxRow; row++) {
|
|
887
|
+
for (let col = minCol; col <= maxCol; col++) {
|
|
888
|
+
const mapCell = gridMap[row][col];
|
|
889
|
+
if (!mapCell.cell) {
|
|
890
|
+
continue;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
const currentCell = mapCell.cell;
|
|
894
|
+
const key = currentCell.getKey();
|
|
895
|
+
|
|
896
|
+
if (!seenCells.has(key)) {
|
|
897
|
+
seenCells.add(key);
|
|
898
|
+
const isEmpty = $cellContainsEmptyParagraph(currentCell);
|
|
899
|
+
if (!isEmpty) {
|
|
900
|
+
targetCell.append(...currentCell.getChildren());
|
|
901
|
+
}
|
|
902
|
+
currentCell.remove();
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// Ensure target cell has content
|
|
908
|
+
if (targetCell.getChildrenSize() === 0) {
|
|
909
|
+
targetCell.append($createParagraphNode());
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
return targetCell;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
function $cellContainsEmptyParagraph(cell: TableCellNode): boolean {
|
|
916
|
+
if (cell.getChildrenSize() !== 1) {
|
|
917
|
+
return false;
|
|
918
|
+
}
|
|
919
|
+
const firstChild = cell.getFirstChildOrThrow();
|
|
920
|
+
if (!$isParagraphNode(firstChild) || !firstChild.isEmpty()) {
|
|
921
|
+
return false;
|
|
922
|
+
}
|
|
923
|
+
return true;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
export function $unmergeCell(): void {
|
|
927
|
+
const selection = $getSelection();
|
|
928
|
+
invariant(
|
|
929
|
+
$isRangeSelection(selection) || $isTableSelection(selection),
|
|
930
|
+
'Expected a RangeSelection or TableSelection',
|
|
931
|
+
);
|
|
932
|
+
const anchor = selection.anchor.getNode();
|
|
933
|
+
const cellNode = $findMatchingParent(anchor, $isTableCellNode);
|
|
934
|
+
invariant(
|
|
935
|
+
$isTableCellNode(cellNode),
|
|
936
|
+
'Expected to find a parent TableCellNode',
|
|
937
|
+
);
|
|
938
|
+
return $unmergeCellNode(cellNode);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
export function $unmergeCellNode(cellNode: TableCellNode): void {
|
|
942
|
+
const [cell, row, grid] = $getNodeTriplet(cellNode);
|
|
943
|
+
const colSpan = cell.__colSpan;
|
|
944
|
+
const rowSpan = cell.__rowSpan;
|
|
945
|
+
if (colSpan === 1 && rowSpan === 1) {
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
const [map, cellMap] = $computeTableMap(grid, cell, cell);
|
|
949
|
+
const {startColumn, startRow} = cellMap;
|
|
950
|
+
// Create a heuristic for what the style of the unmerged cells should be
|
|
951
|
+
// based on whether every row or column already had that state before the
|
|
952
|
+
// unmerge.
|
|
953
|
+
const baseColStyle = cell.__headerState & TableCellHeaderStates.COLUMN;
|
|
954
|
+
const colStyles = Array.from({length: colSpan}, (_v, i) => {
|
|
955
|
+
let colStyle = baseColStyle;
|
|
956
|
+
for (let rowIdx = 0; colStyle !== 0 && rowIdx < map.length; rowIdx++) {
|
|
957
|
+
colStyle &= map[rowIdx][i + startColumn].cell.__headerState;
|
|
958
|
+
}
|
|
959
|
+
return colStyle;
|
|
960
|
+
});
|
|
961
|
+
const baseRowStyle = cell.__headerState & TableCellHeaderStates.ROW;
|
|
962
|
+
const rowStyles = Array.from({length: rowSpan}, (_v, i) => {
|
|
963
|
+
let rowStyle = baseRowStyle;
|
|
964
|
+
for (let colIdx = 0; rowStyle !== 0 && colIdx < map[0].length; colIdx++) {
|
|
965
|
+
rowStyle &= map[i + startRow][colIdx].cell.__headerState;
|
|
966
|
+
}
|
|
967
|
+
return rowStyle;
|
|
968
|
+
});
|
|
969
|
+
|
|
970
|
+
if (colSpan > 1) {
|
|
971
|
+
for (let i = 1; i < colSpan; i++) {
|
|
972
|
+
cell.insertAfter(
|
|
973
|
+
$createTableCellNode(colStyles[i] | rowStyles[0]).append(
|
|
974
|
+
$createParagraphNode(),
|
|
975
|
+
),
|
|
976
|
+
);
|
|
977
|
+
}
|
|
978
|
+
cell.setColSpan(1);
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
if (rowSpan > 1) {
|
|
982
|
+
let currentRowNode;
|
|
983
|
+
for (let i = 1; i < rowSpan; i++) {
|
|
984
|
+
const currentRow = startRow + i;
|
|
985
|
+
const currentRowMap = map[currentRow];
|
|
986
|
+
currentRowNode = (currentRowNode || row).getNextSibling();
|
|
987
|
+
invariant(
|
|
988
|
+
$isTableRowNode(currentRowNode),
|
|
989
|
+
'Expected row next sibling to be a row',
|
|
990
|
+
);
|
|
991
|
+
let insertAfterCell: null | TableCellNode = null;
|
|
992
|
+
for (let column = 0; column < startColumn; column++) {
|
|
993
|
+
const currentCellMap = currentRowMap[column];
|
|
994
|
+
const currentCell = currentCellMap.cell;
|
|
995
|
+
if (currentCellMap.startRow === currentRow) {
|
|
996
|
+
insertAfterCell = currentCell;
|
|
997
|
+
}
|
|
998
|
+
if (currentCell.__colSpan > 1) {
|
|
999
|
+
column += currentCell.__colSpan - 1;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
if (insertAfterCell === null) {
|
|
1003
|
+
for (let j = colSpan - 1; j >= 0; j--) {
|
|
1004
|
+
$insertFirst(
|
|
1005
|
+
currentRowNode,
|
|
1006
|
+
$createTableCellNode(colStyles[j] | rowStyles[i]).append(
|
|
1007
|
+
$createParagraphNode(),
|
|
1008
|
+
),
|
|
1009
|
+
);
|
|
1010
|
+
}
|
|
1011
|
+
} else {
|
|
1012
|
+
for (let j = colSpan - 1; j >= 0; j--) {
|
|
1013
|
+
insertAfterCell.insertAfter(
|
|
1014
|
+
$createTableCellNode(colStyles[j] | rowStyles[i]).append(
|
|
1015
|
+
$createParagraphNode(),
|
|
1016
|
+
),
|
|
1017
|
+
);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
cell.setRowSpan(1);
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
export function $computeTableMap(
|
|
1026
|
+
tableNode: TableNode,
|
|
1027
|
+
cellA: TableCellNode,
|
|
1028
|
+
cellB: TableCellNode,
|
|
1029
|
+
): [TableMapType, TableMapValueType, TableMapValueType] {
|
|
1030
|
+
const [tableMap, cellAValue, cellBValue] = $computeTableMapSkipCellCheck(
|
|
1031
|
+
tableNode,
|
|
1032
|
+
cellA,
|
|
1033
|
+
cellB,
|
|
1034
|
+
);
|
|
1035
|
+
invariant(cellAValue !== null, 'Anchor not found in Table');
|
|
1036
|
+
invariant(cellBValue !== null, 'Focus not found in Table');
|
|
1037
|
+
return [tableMap, cellAValue, cellBValue];
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
export function $computeTableMapSkipCellCheck(
|
|
1041
|
+
tableNode: TableNode,
|
|
1042
|
+
cellA: null | TableCellNode,
|
|
1043
|
+
cellB: null | TableCellNode,
|
|
1044
|
+
): [
|
|
1045
|
+
tableMap: TableMapType,
|
|
1046
|
+
cellAValue: TableMapValueType | null,
|
|
1047
|
+
cellBValue: TableMapValueType | null,
|
|
1048
|
+
] {
|
|
1049
|
+
const tableMap: TableMapType = [];
|
|
1050
|
+
let cellAValue: null | TableMapValueType = null;
|
|
1051
|
+
let cellBValue: null | TableMapValueType = null;
|
|
1052
|
+
function getMapRow(i: number) {
|
|
1053
|
+
let row = tableMap[i];
|
|
1054
|
+
if (row === undefined) {
|
|
1055
|
+
tableMap[i] = row = [];
|
|
1056
|
+
}
|
|
1057
|
+
return row;
|
|
1058
|
+
}
|
|
1059
|
+
const gridChildren = tableNode.getChildren();
|
|
1060
|
+
for (let rowIdx = 0; rowIdx < gridChildren.length; rowIdx++) {
|
|
1061
|
+
const row = gridChildren[rowIdx];
|
|
1062
|
+
invariant(
|
|
1063
|
+
$isTableRowNode(row),
|
|
1064
|
+
'Expected TableNode children to be TableRowNode',
|
|
1065
|
+
);
|
|
1066
|
+
const startMapRow = getMapRow(rowIdx);
|
|
1067
|
+
for (
|
|
1068
|
+
let cell = row.getFirstChild(), colIdx = 0;
|
|
1069
|
+
cell != null;
|
|
1070
|
+
cell = cell.getNextSibling()
|
|
1071
|
+
) {
|
|
1072
|
+
invariant(
|
|
1073
|
+
$isTableCellNode(cell),
|
|
1074
|
+
'Expected TableRowNode children to be TableCellNode',
|
|
1075
|
+
);
|
|
1076
|
+
// Skip past any columns that were merged from a higher row
|
|
1077
|
+
while (startMapRow[colIdx] !== undefined) {
|
|
1078
|
+
colIdx++;
|
|
1079
|
+
}
|
|
1080
|
+
const value: TableMapValueType = {
|
|
1081
|
+
cell,
|
|
1082
|
+
startColumn: colIdx,
|
|
1083
|
+
startRow: rowIdx,
|
|
1084
|
+
};
|
|
1085
|
+
const {__rowSpan: rowSpan, __colSpan: colSpan} = cell;
|
|
1086
|
+
for (let j = 0; j < rowSpan; j++) {
|
|
1087
|
+
if (rowIdx + j >= gridChildren.length) {
|
|
1088
|
+
// The table is non-rectangular with a rowSpan
|
|
1089
|
+
// below the last <tr> in the table.
|
|
1090
|
+
// We should probably handle this with a node transform
|
|
1091
|
+
// to ensure that tables are always rectangular but this
|
|
1092
|
+
// will avoid crashes such as #6584
|
|
1093
|
+
// Note that there are probably still latent bugs
|
|
1094
|
+
// regarding colSpan or general cell count mismatches.
|
|
1095
|
+
break;
|
|
1096
|
+
}
|
|
1097
|
+
const mapRow = getMapRow(rowIdx + j);
|
|
1098
|
+
for (let i = 0; i < colSpan; i++) {
|
|
1099
|
+
mapRow[colIdx + i] = value;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
if (cellA !== null && cellAValue === null && cellA.is(cell)) {
|
|
1103
|
+
cellAValue = value;
|
|
1104
|
+
}
|
|
1105
|
+
if (cellB !== null && cellBValue === null && cellB.is(cell)) {
|
|
1106
|
+
cellBValue = value;
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
return [tableMap, cellAValue, cellBValue];
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
export function $getNodeTriplet(
|
|
1114
|
+
source: PointType | LexicalNode | TableCellNode,
|
|
1115
|
+
): [TableCellNode, TableRowNode, TableNode] {
|
|
1116
|
+
let cell: TableCellNode;
|
|
1117
|
+
if (source instanceof TableCellNode) {
|
|
1118
|
+
cell = source;
|
|
1119
|
+
} else if ('__type' in source) {
|
|
1120
|
+
const cell_ = $findMatchingParent(source, $isTableCellNode);
|
|
1121
|
+
invariant(
|
|
1122
|
+
$isTableCellNode(cell_),
|
|
1123
|
+
'Expected to find a parent TableCellNode',
|
|
1124
|
+
);
|
|
1125
|
+
cell = cell_;
|
|
1126
|
+
} else {
|
|
1127
|
+
const cell_ = $findMatchingParent(source.getNode(), $isTableCellNode);
|
|
1128
|
+
invariant(
|
|
1129
|
+
$isTableCellNode(cell_),
|
|
1130
|
+
'Expected to find a parent TableCellNode',
|
|
1131
|
+
);
|
|
1132
|
+
cell = cell_;
|
|
1133
|
+
}
|
|
1134
|
+
const row = cell.getParent();
|
|
1135
|
+
invariant(
|
|
1136
|
+
$isTableRowNode(row),
|
|
1137
|
+
'Expected TableCellNode to have a parent TableRowNode',
|
|
1138
|
+
);
|
|
1139
|
+
const grid = row.getParent();
|
|
1140
|
+
invariant(
|
|
1141
|
+
$isTableNode(grid),
|
|
1142
|
+
'Expected TableRowNode to have a parent TableNode',
|
|
1143
|
+
);
|
|
1144
|
+
return [cell, row, grid];
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
export interface TableCellRectBoundary {
|
|
1148
|
+
minColumn: number;
|
|
1149
|
+
minRow: number;
|
|
1150
|
+
maxColumn: number;
|
|
1151
|
+
maxRow: number;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
export interface TableCellRectSpans {
|
|
1155
|
+
topSpan: number;
|
|
1156
|
+
leftSpan: number;
|
|
1157
|
+
rightSpan: number;
|
|
1158
|
+
bottomSpan: number;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
export function $computeTableCellRectSpans(
|
|
1162
|
+
map: TableMapType,
|
|
1163
|
+
boundary: TableCellRectBoundary,
|
|
1164
|
+
): TableCellRectSpans {
|
|
1165
|
+
const {minColumn, maxColumn, minRow, maxRow} = boundary;
|
|
1166
|
+
let topSpan = 1;
|
|
1167
|
+
let leftSpan = 1;
|
|
1168
|
+
let rightSpan = 1;
|
|
1169
|
+
let bottomSpan = 1;
|
|
1170
|
+
const topRow = map[minRow];
|
|
1171
|
+
const bottomRow = map[maxRow];
|
|
1172
|
+
for (let col = minColumn; col <= maxColumn; col++) {
|
|
1173
|
+
topSpan = Math.max(topSpan, topRow[col].cell.__rowSpan);
|
|
1174
|
+
bottomSpan = Math.max(bottomSpan, bottomRow[col].cell.__rowSpan);
|
|
1175
|
+
}
|
|
1176
|
+
for (let row = minRow; row <= maxRow; row++) {
|
|
1177
|
+
leftSpan = Math.max(leftSpan, map[row][minColumn].cell.__colSpan);
|
|
1178
|
+
rightSpan = Math.max(rightSpan, map[row][maxColumn].cell.__colSpan);
|
|
1179
|
+
}
|
|
1180
|
+
return {bottomSpan, leftSpan, rightSpan, topSpan};
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
export function $computeTableCellRectBoundary(
|
|
1184
|
+
map: TableMapType,
|
|
1185
|
+
cellAMap: TableMapValueType,
|
|
1186
|
+
cellBMap: TableMapValueType,
|
|
1187
|
+
): TableCellRectBoundary {
|
|
1188
|
+
// Initial boundaries based on the anchor and focus cells
|
|
1189
|
+
let minColumn = Math.min(cellAMap.startColumn, cellBMap.startColumn);
|
|
1190
|
+
let minRow = Math.min(cellAMap.startRow, cellBMap.startRow);
|
|
1191
|
+
let maxColumn = Math.max(
|
|
1192
|
+
cellAMap.startColumn + cellAMap.cell.__colSpan - 1,
|
|
1193
|
+
cellBMap.startColumn + cellBMap.cell.__colSpan - 1,
|
|
1194
|
+
);
|
|
1195
|
+
let maxRow = Math.max(
|
|
1196
|
+
cellAMap.startRow + cellAMap.cell.__rowSpan - 1,
|
|
1197
|
+
cellBMap.startRow + cellBMap.cell.__rowSpan - 1,
|
|
1198
|
+
);
|
|
1199
|
+
|
|
1200
|
+
// Keep expanding until we have a complete rectangle
|
|
1201
|
+
let hasChanges;
|
|
1202
|
+
do {
|
|
1203
|
+
hasChanges = false;
|
|
1204
|
+
|
|
1205
|
+
// Check all cells in the table
|
|
1206
|
+
for (let row = 0; row < map.length; row++) {
|
|
1207
|
+
for (let col = 0; col < map[0].length; col++) {
|
|
1208
|
+
const cell = map[row][col];
|
|
1209
|
+
if (!cell) {
|
|
1210
|
+
continue;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
const cellEndCol = cell.startColumn + cell.cell.__colSpan - 1;
|
|
1214
|
+
const cellEndRow = cell.startRow + cell.cell.__rowSpan - 1;
|
|
1215
|
+
|
|
1216
|
+
// Check if this cell intersects with our current selection rectangle
|
|
1217
|
+
const intersectsHorizontally =
|
|
1218
|
+
cell.startColumn <= maxColumn && cellEndCol >= minColumn;
|
|
1219
|
+
const intersectsVertically =
|
|
1220
|
+
cell.startRow <= maxRow && cellEndRow >= minRow;
|
|
1221
|
+
|
|
1222
|
+
// If the cell intersects either horizontally or vertically
|
|
1223
|
+
if (intersectsHorizontally && intersectsVertically) {
|
|
1224
|
+
// Expand boundaries to include this cell completely
|
|
1225
|
+
const newMinColumn = Math.min(minColumn, cell.startColumn);
|
|
1226
|
+
const newMaxColumn = Math.max(maxColumn, cellEndCol);
|
|
1227
|
+
const newMinRow = Math.min(minRow, cell.startRow);
|
|
1228
|
+
const newMaxRow = Math.max(maxRow, cellEndRow);
|
|
1229
|
+
|
|
1230
|
+
// Check if boundaries changed
|
|
1231
|
+
if (
|
|
1232
|
+
newMinColumn !== minColumn ||
|
|
1233
|
+
newMaxColumn !== maxColumn ||
|
|
1234
|
+
newMinRow !== minRow ||
|
|
1235
|
+
newMaxRow !== maxRow
|
|
1236
|
+
) {
|
|
1237
|
+
minColumn = newMinColumn;
|
|
1238
|
+
maxColumn = newMaxColumn;
|
|
1239
|
+
minRow = newMinRow;
|
|
1240
|
+
maxRow = newMaxRow;
|
|
1241
|
+
hasChanges = true;
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
} while (hasChanges);
|
|
1247
|
+
|
|
1248
|
+
return {
|
|
1249
|
+
maxColumn,
|
|
1250
|
+
maxRow,
|
|
1251
|
+
minColumn,
|
|
1252
|
+
minRow,
|
|
1253
|
+
};
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
/**
|
|
1257
|
+
* Checks if the table does not have any merged cells.
|
|
1258
|
+
*
|
|
1259
|
+
* @param table Table to check for if it has any merged cells.
|
|
1260
|
+
* @returns True if the table does not have any merged cells, false otherwise.
|
|
1261
|
+
*/
|
|
1262
|
+
export function $isSimpleTable(table: TableNode): boolean {
|
|
1263
|
+
const rows = table.getChildren();
|
|
1264
|
+
let columns: null | number = null;
|
|
1265
|
+
for (const row of rows) {
|
|
1266
|
+
if (!$isTableRowNode(row)) {
|
|
1267
|
+
return false;
|
|
1268
|
+
}
|
|
1269
|
+
if (columns === null) {
|
|
1270
|
+
columns = row.getChildrenSize();
|
|
1271
|
+
}
|
|
1272
|
+
if (row.getChildrenSize() !== columns) {
|
|
1273
|
+
return false;
|
|
1274
|
+
}
|
|
1275
|
+
const cells = row.getChildren();
|
|
1276
|
+
for (const cell of cells) {
|
|
1277
|
+
if (
|
|
1278
|
+
!$isTableCellNode(cell) ||
|
|
1279
|
+
cell.getRowSpan() !== 1 ||
|
|
1280
|
+
cell.getColSpan() !== 1
|
|
1281
|
+
) {
|
|
1282
|
+
return false;
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
return (columns || 0) > 0;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
/**
|
|
1290
|
+
* Moves a column from one position to another within a simple (non-merged) table.
|
|
1291
|
+
*
|
|
1292
|
+
* @param tableNode The table node to modify.
|
|
1293
|
+
* @param originColumn The index of the column to move.
|
|
1294
|
+
* @param targetColumn The index to move the column to.
|
|
1295
|
+
*/
|
|
1296
|
+
export function $moveTableColumn(
|
|
1297
|
+
tableNode: TableNode,
|
|
1298
|
+
originColumn: number,
|
|
1299
|
+
targetColumn: number,
|
|
1300
|
+
): void {
|
|
1301
|
+
if (originColumn === targetColumn) {
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
1304
|
+
const columnCount = tableNode.getColumnCount();
|
|
1305
|
+
if (
|
|
1306
|
+
originColumn < 0 ||
|
|
1307
|
+
originColumn >= columnCount ||
|
|
1308
|
+
targetColumn < 0 ||
|
|
1309
|
+
targetColumn >= columnCount
|
|
1310
|
+
) {
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
if (!$isSimpleTable(tableNode)) {
|
|
1314
|
+
return;
|
|
1315
|
+
}
|
|
1316
|
+
const rows = tableNode.getChildren().filter($isTableRowNode);
|
|
1317
|
+
rows.forEach(row => {
|
|
1318
|
+
const cells = row.getChildren();
|
|
1319
|
+
const [moved] = cells.splice(originColumn, 1);
|
|
1320
|
+
cells.splice(targetColumn, 0, moved);
|
|
1321
|
+
row.splice(0, cells.length, cells);
|
|
1322
|
+
});
|
|
1323
|
+
const colWidths = tableNode.getColWidths();
|
|
1324
|
+
if (colWidths && colWidths.length === columnCount) {
|
|
1325
|
+
const newWidths = [...colWidths];
|
|
1326
|
+
const [movedWidth] = newWidths.splice(originColumn, 1);
|
|
1327
|
+
newWidths.splice(targetColumn, 0, movedWidth);
|
|
1328
|
+
tableNode.setColWidths(newWidths);
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
export function $getTableCellNodeRect(tableCellNode: TableCellNode): {
|
|
1333
|
+
rowIndex: number;
|
|
1334
|
+
columnIndex: number;
|
|
1335
|
+
rowSpan: number;
|
|
1336
|
+
colSpan: number;
|
|
1337
|
+
} | null {
|
|
1338
|
+
const [cellNode, , gridNode] = $getNodeTriplet(tableCellNode);
|
|
1339
|
+
const rows = gridNode.getChildren<TableRowNode>();
|
|
1340
|
+
const rowCount = rows.length;
|
|
1341
|
+
const columnCount = rows[0].getChildren().length;
|
|
1342
|
+
|
|
1343
|
+
// Create a matrix of the same size as the table to track the position of each cell
|
|
1344
|
+
const cellMatrix = new Array(rowCount);
|
|
1345
|
+
for (let i = 0; i < rowCount; i++) {
|
|
1346
|
+
cellMatrix[i] = new Array(columnCount);
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {
|
|
1350
|
+
const row = rows[rowIndex];
|
|
1351
|
+
const cells = row.getChildren<TableCellNode>();
|
|
1352
|
+
let columnIndex = 0;
|
|
1353
|
+
|
|
1354
|
+
for (let cellIndex = 0; cellIndex < cells.length; cellIndex++) {
|
|
1355
|
+
// Find the next available position in the matrix, skip the position of merged cells
|
|
1356
|
+
while (cellMatrix[rowIndex][columnIndex]) {
|
|
1357
|
+
columnIndex++;
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
const cell = cells[cellIndex];
|
|
1361
|
+
const rowSpan = cell.__rowSpan || 1;
|
|
1362
|
+
const colSpan = cell.__colSpan || 1;
|
|
1363
|
+
|
|
1364
|
+
// Put the cell into the corresponding position in the matrix
|
|
1365
|
+
for (let i = 0; i < rowSpan; i++) {
|
|
1366
|
+
for (let j = 0; j < colSpan; j++) {
|
|
1367
|
+
cellMatrix[rowIndex + i][columnIndex + j] = cell;
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
// Return to the original index, row span and column span of the cell.
|
|
1372
|
+
if (cellNode === cell) {
|
|
1373
|
+
return {
|
|
1374
|
+
colSpan,
|
|
1375
|
+
columnIndex,
|
|
1376
|
+
rowIndex,
|
|
1377
|
+
rowSpan,
|
|
1378
|
+
};
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
columnIndex += colSpan;
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
return null;
|
|
1386
|
+
}
|