@ekz/lexical-table 0.40.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.
@@ -0,0 +1,4261 @@
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 { addClassNamesToElement, $descendantsMatching, $findMatchingParent, removeClassNamesFromElement, objectKlassEquals, isHTMLElement as isHTMLElement$1, $insertFirst as $insertFirst$1, mergeRegister, $insertNodeToNearestRoot, $unwrapAndFilterDescendants } from '@ekz/lexical-utils';
10
+ import { ElementNode, isHTMLElement, $isInlineElementOrDecoratorNode, $isTextNode, $isLineBreakNode, $createParagraphNode, $applyNodeReplacement, createCommand, $createTextNode, $getSelection, $isRangeSelection, $isParagraphNode, $createPoint, $getNodeByKey, $isElementNode, $normalizeSelection__EXPERIMENTAL, isCurrentlyReadOnlyMode, TEXT_TYPE_TO_FORMAT, $getEditor, $setSelection, SELECTION_CHANGE_COMMAND, getDOMSelectionForEditor, $createRangeSelection, $isRootNode, INSERT_PARAGRAPH_COMMAND, KEY_ARROW_DOWN_COMMAND, KEY_ARROW_UP_COMMAND, KEY_ARROW_LEFT_COMMAND, KEY_ARROW_RIGHT_COMMAND, COMMAND_PRIORITY_HIGH, KEY_ESCAPE_COMMAND, DELETE_WORD_COMMAND, DELETE_LINE_COMMAND, DELETE_CHARACTER_COMMAND, KEY_BACKSPACE_COMMAND, KEY_DELETE_COMMAND, CUT_COMMAND, FORMAT_TEXT_COMMAND, FORMAT_ELEMENT_COMMAND, CONTROLLED_TEXT_INSERTION_COMMAND, KEY_TAB_COMMAND, FOCUS_COMMAND, SELECTION_INSERT_CLIPBOARD_NODES_COMMAND, $getPreviousSelection, $getNearestNodeFromDOMNode, $createRangeSelectionFromDom, isDOMNode, $isRootOrShadowRoot, $caretFromPoint, $isExtendableTextPointCaret, $extendCaretToRange, $isSiblingCaret, $getSiblingCaret, $setPointFromCaret, $normalizeCaret, $isChildCaret, $getChildCaret, $getAdjacentChildCaret, setDOMUnmanaged, COMMAND_PRIORITY_EDITOR, CLICK_COMMAND, defineExtension, safeCast } from '@ekz/lexical';
11
+ import { signal, effect, namedSignals } from '@ekz/lexical-extension';
12
+ import { copyToClipboard, $getClipboardDataFromSelection } from '@ekz/lexical-clipboard';
13
+
14
+ /**
15
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
16
+ *
17
+ * This source code is licensed under the MIT license found in the
18
+ * LICENSE file in the root directory of this source tree.
19
+ *
20
+ */
21
+
22
+ const PIXEL_VALUE_REG_EXP = /^(\d+(?:\.\d+)?)px$/;
23
+
24
+ // .PlaygroundEditorTheme__tableCell width value from
25
+ // packages/lexical-playground/src/themes/PlaygroundEditorTheme.css
26
+ const COLUMN_WIDTH = 75;
27
+
28
+ /**
29
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
30
+ *
31
+ * This source code is licensed under the MIT license found in the
32
+ * LICENSE file in the root directory of this source tree.
33
+ *
34
+ */
35
+
36
+ const TableCellHeaderStates = {
37
+ BOTH: 3,
38
+ COLUMN: 2,
39
+ NO_STATUS: 0,
40
+ ROW: 1
41
+ };
42
+ /** @noInheritDoc */
43
+ class TableCellNode extends ElementNode {
44
+ /** @internal */
45
+ __colSpan;
46
+ /** @internal */
47
+ __rowSpan;
48
+ /** @internal */
49
+ __headerState;
50
+ /** @internal */
51
+ __width;
52
+ /** @internal */
53
+ __backgroundColor;
54
+ /** @internal */
55
+ __verticalAlign;
56
+ static getType() {
57
+ return 'tablecell';
58
+ }
59
+ static clone(node) {
60
+ return new TableCellNode(node.__headerState, node.__colSpan, node.__width, node.__key);
61
+ }
62
+ afterCloneFrom(node) {
63
+ super.afterCloneFrom(node);
64
+ this.__rowSpan = node.__rowSpan;
65
+ this.__backgroundColor = node.__backgroundColor;
66
+ this.__verticalAlign = node.__verticalAlign;
67
+ }
68
+ static importDOM() {
69
+ return {
70
+ td: node => ({
71
+ conversion: $convertTableCellNodeElement,
72
+ priority: 0
73
+ }),
74
+ th: node => ({
75
+ conversion: $convertTableCellNodeElement,
76
+ priority: 0
77
+ })
78
+ };
79
+ }
80
+ static importJSON(serializedNode) {
81
+ return $createTableCellNode().updateFromJSON(serializedNode);
82
+ }
83
+ updateFromJSON(serializedNode) {
84
+ return super.updateFromJSON(serializedNode).setHeaderStyles(serializedNode.headerState).setColSpan(serializedNode.colSpan || 1).setRowSpan(serializedNode.rowSpan || 1).setWidth(serializedNode.width || undefined).setBackgroundColor(serializedNode.backgroundColor || null).setVerticalAlign(serializedNode.verticalAlign || undefined);
85
+ }
86
+ constructor(headerState = TableCellHeaderStates.NO_STATUS, colSpan = 1, width, key) {
87
+ super(key);
88
+ this.__colSpan = colSpan;
89
+ this.__rowSpan = 1;
90
+ this.__headerState = headerState;
91
+ this.__width = width;
92
+ this.__backgroundColor = null;
93
+ this.__verticalAlign = undefined;
94
+ }
95
+ createDOM(config) {
96
+ const element = document.createElement(this.getTag());
97
+ if (this.__width) {
98
+ element.style.width = `${this.__width}px`;
99
+ }
100
+ if (this.__colSpan > 1) {
101
+ element.colSpan = this.__colSpan;
102
+ }
103
+ if (this.__rowSpan > 1) {
104
+ element.rowSpan = this.__rowSpan;
105
+ }
106
+ if (this.__backgroundColor !== null) {
107
+ element.style.backgroundColor = this.__backgroundColor;
108
+ }
109
+ if (isValidVerticalAlign(this.__verticalAlign)) {
110
+ element.style.verticalAlign = this.__verticalAlign;
111
+ }
112
+ addClassNamesToElement(element, config.theme.tableCell, this.hasHeader() && config.theme.tableCellHeader);
113
+ return element;
114
+ }
115
+ exportDOM(editor) {
116
+ const output = super.exportDOM(editor);
117
+ if (isHTMLElement(output.element)) {
118
+ const element = output.element;
119
+ element.setAttribute('data-temporary-table-cell-lexical-key', this.getKey());
120
+ element.style.border = '1px solid black';
121
+ if (this.__colSpan > 1) {
122
+ element.colSpan = this.__colSpan;
123
+ }
124
+ if (this.__rowSpan > 1) {
125
+ element.rowSpan = this.__rowSpan;
126
+ }
127
+ element.style.width = `${this.getWidth() || COLUMN_WIDTH}px`;
128
+ element.style.verticalAlign = this.getVerticalAlign() || 'top';
129
+ element.style.textAlign = 'start';
130
+ if (this.__backgroundColor === null && this.hasHeader()) {
131
+ element.style.backgroundColor = '#f2f3f5';
132
+ }
133
+ }
134
+ return output;
135
+ }
136
+ exportJSON() {
137
+ return {
138
+ ...super.exportJSON(),
139
+ ...(isValidVerticalAlign(this.__verticalAlign) && {
140
+ verticalAlign: this.__verticalAlign
141
+ }),
142
+ backgroundColor: this.getBackgroundColor(),
143
+ colSpan: this.__colSpan,
144
+ headerState: this.__headerState,
145
+ rowSpan: this.__rowSpan,
146
+ width: this.getWidth()
147
+ };
148
+ }
149
+ getColSpan() {
150
+ return this.getLatest().__colSpan;
151
+ }
152
+ setColSpan(colSpan) {
153
+ const self = this.getWritable();
154
+ self.__colSpan = colSpan;
155
+ return self;
156
+ }
157
+ getRowSpan() {
158
+ return this.getLatest().__rowSpan;
159
+ }
160
+ setRowSpan(rowSpan) {
161
+ const self = this.getWritable();
162
+ self.__rowSpan = rowSpan;
163
+ return self;
164
+ }
165
+ getTag() {
166
+ return this.hasHeader() ? 'th' : 'td';
167
+ }
168
+ setHeaderStyles(headerState, mask = TableCellHeaderStates.BOTH) {
169
+ const self = this.getWritable();
170
+ self.__headerState = headerState & mask | self.__headerState & ~mask;
171
+ return self;
172
+ }
173
+ getHeaderStyles() {
174
+ return this.getLatest().__headerState;
175
+ }
176
+ setWidth(width) {
177
+ const self = this.getWritable();
178
+ self.__width = width;
179
+ return self;
180
+ }
181
+ getWidth() {
182
+ return this.getLatest().__width;
183
+ }
184
+ getBackgroundColor() {
185
+ return this.getLatest().__backgroundColor;
186
+ }
187
+ setBackgroundColor(newBackgroundColor) {
188
+ const self = this.getWritable();
189
+ self.__backgroundColor = newBackgroundColor;
190
+ return self;
191
+ }
192
+ getVerticalAlign() {
193
+ return this.getLatest().__verticalAlign;
194
+ }
195
+ setVerticalAlign(newVerticalAlign) {
196
+ const self = this.getWritable();
197
+ self.__verticalAlign = newVerticalAlign || undefined;
198
+ return self;
199
+ }
200
+ toggleHeaderStyle(headerStateToToggle) {
201
+ const self = this.getWritable();
202
+ if ((self.__headerState & headerStateToToggle) === headerStateToToggle) {
203
+ self.__headerState -= headerStateToToggle;
204
+ } else {
205
+ self.__headerState += headerStateToToggle;
206
+ }
207
+ return self;
208
+ }
209
+ hasHeaderState(headerState) {
210
+ return (this.getHeaderStyles() & headerState) === headerState;
211
+ }
212
+ hasHeader() {
213
+ return this.getLatest().__headerState !== TableCellHeaderStates.NO_STATUS;
214
+ }
215
+ updateDOM(prevNode) {
216
+ return prevNode.__headerState !== this.__headerState || prevNode.__width !== this.__width || prevNode.__colSpan !== this.__colSpan || prevNode.__rowSpan !== this.__rowSpan || prevNode.__backgroundColor !== this.__backgroundColor || prevNode.__verticalAlign !== this.__verticalAlign;
217
+ }
218
+ isShadowRoot() {
219
+ return true;
220
+ }
221
+ collapseAtStart() {
222
+ return true;
223
+ }
224
+ canBeEmpty() {
225
+ return false;
226
+ }
227
+ canIndent() {
228
+ return false;
229
+ }
230
+ }
231
+ function isValidVerticalAlign(verticalAlign) {
232
+ return verticalAlign === 'middle' || verticalAlign === 'bottom';
233
+ }
234
+ function $convertTableCellNodeElement(domNode) {
235
+ const domNode_ = domNode;
236
+ const nodeName = domNode.nodeName.toLowerCase();
237
+ let width = undefined;
238
+ if (PIXEL_VALUE_REG_EXP.test(domNode_.style.width)) {
239
+ width = parseFloat(domNode_.style.width);
240
+ }
241
+ const tableCellNode = $createTableCellNode(nodeName === 'th' ? TableCellHeaderStates.ROW : TableCellHeaderStates.NO_STATUS, domNode_.colSpan, width);
242
+ tableCellNode.__rowSpan = domNode_.rowSpan;
243
+ const backgroundColor = domNode_.style.backgroundColor;
244
+ if (backgroundColor !== '') {
245
+ tableCellNode.__backgroundColor = backgroundColor;
246
+ }
247
+ const verticalAlign = domNode_.style.verticalAlign;
248
+ if (isValidVerticalAlign(verticalAlign)) {
249
+ tableCellNode.__verticalAlign = verticalAlign;
250
+ }
251
+ const style = domNode_.style;
252
+ const textDecoration = (style && style.textDecoration || '').split(' ');
253
+ const hasBoldFontWeight = style.fontWeight === '700' || style.fontWeight === 'bold';
254
+ const hasLinethroughTextDecoration = textDecoration.includes('line-through');
255
+ const hasItalicFontStyle = style.fontStyle === 'italic';
256
+ const hasUnderlineTextDecoration = textDecoration.includes('underline');
257
+ return {
258
+ after: childLexicalNodes => {
259
+ const result = [];
260
+ let paragraphNode = null;
261
+ const removeSingleLineBreakNode = () => {
262
+ if (paragraphNode) {
263
+ const firstChild = paragraphNode.getFirstChild();
264
+ if ($isLineBreakNode(firstChild) && paragraphNode.getChildrenSize() === 1) {
265
+ firstChild.remove();
266
+ }
267
+ }
268
+ };
269
+ for (const child of childLexicalNodes) {
270
+ if ($isInlineElementOrDecoratorNode(child) || $isTextNode(child) || $isLineBreakNode(child)) {
271
+ if ($isTextNode(child)) {
272
+ if (hasBoldFontWeight) {
273
+ child.toggleFormat('bold');
274
+ }
275
+ if (hasLinethroughTextDecoration) {
276
+ child.toggleFormat('strikethrough');
277
+ }
278
+ if (hasItalicFontStyle) {
279
+ child.toggleFormat('italic');
280
+ }
281
+ if (hasUnderlineTextDecoration) {
282
+ child.toggleFormat('underline');
283
+ }
284
+ }
285
+ if (paragraphNode) {
286
+ paragraphNode.append(child);
287
+ } else {
288
+ paragraphNode = $createParagraphNode().append(child);
289
+ result.push(paragraphNode);
290
+ }
291
+ } else {
292
+ result.push(child);
293
+ removeSingleLineBreakNode();
294
+ paragraphNode = null;
295
+ }
296
+ }
297
+ removeSingleLineBreakNode();
298
+ if (result.length === 0) {
299
+ result.push($createParagraphNode());
300
+ }
301
+ return result;
302
+ },
303
+ node: tableCellNode
304
+ };
305
+ }
306
+ function $createTableCellNode(headerState = TableCellHeaderStates.NO_STATUS, colSpan = 1, width) {
307
+ return $applyNodeReplacement(new TableCellNode(headerState, colSpan, width));
308
+ }
309
+ function $isTableCellNode(node) {
310
+ return node instanceof TableCellNode;
311
+ }
312
+
313
+ /**
314
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
315
+ *
316
+ * This source code is licensed under the MIT license found in the
317
+ * LICENSE file in the root directory of this source tree.
318
+ *
319
+ */
320
+
321
+ const INSERT_TABLE_COMMAND = createCommand('INSERT_TABLE_COMMAND');
322
+
323
+ /**
324
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
325
+ *
326
+ * This source code is licensed under the MIT license found in the
327
+ * LICENSE file in the root directory of this source tree.
328
+ *
329
+ */
330
+
331
+ // Do not require this module directly! Use normal `invariant` calls.
332
+
333
+ function formatDevErrorMessage(message) {
334
+ throw new Error(message);
335
+ }
336
+
337
+ /**
338
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
339
+ *
340
+ * This source code is licensed under the MIT license found in the
341
+ * LICENSE file in the root directory of this source tree.
342
+ *
343
+ */
344
+
345
+ /** @noInheritDoc */
346
+ class TableRowNode extends ElementNode {
347
+ /** @internal */
348
+ __height;
349
+ static getType() {
350
+ return 'tablerow';
351
+ }
352
+ static clone(node) {
353
+ return new TableRowNode(node.__height, node.__key);
354
+ }
355
+ static importDOM() {
356
+ return {
357
+ tr: node => ({
358
+ conversion: $convertTableRowElement,
359
+ priority: 0
360
+ })
361
+ };
362
+ }
363
+ static importJSON(serializedNode) {
364
+ return $createTableRowNode().updateFromJSON(serializedNode);
365
+ }
366
+ updateFromJSON(serializedNode) {
367
+ return super.updateFromJSON(serializedNode).setHeight(serializedNode.height);
368
+ }
369
+ constructor(height, key) {
370
+ super(key);
371
+ this.__height = height;
372
+ }
373
+ exportJSON() {
374
+ const height = this.getHeight();
375
+ return {
376
+ ...super.exportJSON(),
377
+ ...(height === undefined ? undefined : {
378
+ height
379
+ })
380
+ };
381
+ }
382
+ createDOM(config) {
383
+ const element = document.createElement('tr');
384
+ if (this.__height) {
385
+ element.style.height = `${this.__height}px`;
386
+ }
387
+ addClassNamesToElement(element, config.theme.tableRow);
388
+ return element;
389
+ }
390
+ extractWithChild(child, selection, destination) {
391
+ return destination === 'html';
392
+ }
393
+ isShadowRoot() {
394
+ return true;
395
+ }
396
+ setHeight(height) {
397
+ const self = this.getWritable();
398
+ self.__height = height;
399
+ return self;
400
+ }
401
+ getHeight() {
402
+ return this.getLatest().__height;
403
+ }
404
+ updateDOM(prevNode) {
405
+ return prevNode.__height !== this.__height;
406
+ }
407
+ canBeEmpty() {
408
+ return false;
409
+ }
410
+ canIndent() {
411
+ return false;
412
+ }
413
+ }
414
+ function $convertTableRowElement(domNode) {
415
+ const domNode_ = domNode;
416
+ let height = undefined;
417
+ if (PIXEL_VALUE_REG_EXP.test(domNode_.style.height)) {
418
+ height = parseFloat(domNode_.style.height);
419
+ }
420
+ return {
421
+ after: children => $descendantsMatching(children, $isTableCellNode),
422
+ node: $createTableRowNode(height)
423
+ };
424
+ }
425
+ function $createTableRowNode(height) {
426
+ return $applyNodeReplacement(new TableRowNode(height));
427
+ }
428
+ function $isTableRowNode(node) {
429
+ return node instanceof TableRowNode;
430
+ }
431
+
432
+ /**
433
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
434
+ *
435
+ * This source code is licensed under the MIT license found in the
436
+ * LICENSE file in the root directory of this source tree.
437
+ *
438
+ */
439
+
440
+ const CAN_USE_DOM = typeof window !== 'undefined' && typeof window.document !== 'undefined' && typeof window.document.createElement !== 'undefined';
441
+
442
+ /**
443
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
444
+ *
445
+ * This source code is licensed under the MIT license found in the
446
+ * LICENSE file in the root directory of this source tree.
447
+ *
448
+ */
449
+
450
+ const documentMode = CAN_USE_DOM && 'documentMode' in document ? document.documentMode : null;
451
+ const IS_FIREFOX = CAN_USE_DOM && /^(?!.*Seamonkey)(?=.*Firefox).*/i.test(navigator.userAgent);
452
+ CAN_USE_DOM && 'InputEvent' in window && !documentMode ? 'getTargetRanges' in new window.InputEvent('input') : false;
453
+
454
+ function $createTableNodeWithDimensions(rowCount, columnCount, includeHeaders = true) {
455
+ const tableNode = $createTableNode();
456
+ for (let iRow = 0; iRow < rowCount; iRow++) {
457
+ const tableRowNode = $createTableRowNode();
458
+ for (let iColumn = 0; iColumn < columnCount; iColumn++) {
459
+ let headerState = TableCellHeaderStates.NO_STATUS;
460
+ if (typeof includeHeaders === 'object') {
461
+ if (iRow === 0 && includeHeaders.rows) {
462
+ headerState |= TableCellHeaderStates.ROW;
463
+ }
464
+ if (iColumn === 0 && includeHeaders.columns) {
465
+ headerState |= TableCellHeaderStates.COLUMN;
466
+ }
467
+ } else if (includeHeaders) {
468
+ if (iRow === 0) {
469
+ headerState |= TableCellHeaderStates.ROW;
470
+ }
471
+ if (iColumn === 0) {
472
+ headerState |= TableCellHeaderStates.COLUMN;
473
+ }
474
+ }
475
+ const tableCellNode = $createTableCellNode(headerState);
476
+ const paragraphNode = $createParagraphNode();
477
+ paragraphNode.append($createTextNode());
478
+ tableCellNode.append(paragraphNode);
479
+ tableRowNode.append(tableCellNode);
480
+ }
481
+ tableNode.append(tableRowNode);
482
+ }
483
+ return tableNode;
484
+ }
485
+ function $getTableCellNodeFromLexicalNode(startingNode) {
486
+ const node = $findMatchingParent(startingNode, n => $isTableCellNode(n));
487
+ if ($isTableCellNode(node)) {
488
+ return node;
489
+ }
490
+ return null;
491
+ }
492
+ function $getTableRowNodeFromTableCellNodeOrThrow(startingNode) {
493
+ const node = $findMatchingParent(startingNode, n => $isTableRowNode(n));
494
+ if ($isTableRowNode(node)) {
495
+ return node;
496
+ }
497
+ throw new Error('Expected table cell to be inside of table row.');
498
+ }
499
+ function $getTableNodeFromLexicalNodeOrThrow(startingNode) {
500
+ const node = $findMatchingParent(startingNode, n => $isTableNode(n));
501
+ if ($isTableNode(node)) {
502
+ return node;
503
+ }
504
+ throw new Error('Expected table cell to be inside of table.');
505
+ }
506
+ function $getTableRowIndexFromTableCellNode(tableCellNode) {
507
+ const tableRowNode = $getTableRowNodeFromTableCellNodeOrThrow(tableCellNode);
508
+ const tableNode = $getTableNodeFromLexicalNodeOrThrow(tableRowNode);
509
+ return tableNode.getChildren().findIndex(n => n.is(tableRowNode));
510
+ }
511
+ function $getTableColumnIndexFromTableCellNode(tableCellNode) {
512
+ const tableRowNode = $getTableRowNodeFromTableCellNodeOrThrow(tableCellNode);
513
+ return tableRowNode.getChildren().findIndex(n => n.is(tableCellNode));
514
+ }
515
+ function $getTableCellSiblingsFromTableCellNode(tableCellNode, table) {
516
+ const tableNode = $getTableNodeFromLexicalNodeOrThrow(tableCellNode);
517
+ const {
518
+ x,
519
+ y
520
+ } = tableNode.getCordsFromCellNode(tableCellNode, table);
521
+ return {
522
+ above: tableNode.getCellNodeFromCords(x, y - 1, table),
523
+ below: tableNode.getCellNodeFromCords(x, y + 1, table),
524
+ left: tableNode.getCellNodeFromCords(x - 1, y, table),
525
+ right: tableNode.getCellNodeFromCords(x + 1, y, table)
526
+ };
527
+ }
528
+ function $removeTableRowAtIndex(tableNode, indexToDelete) {
529
+ const tableRows = tableNode.getChildren();
530
+ if (indexToDelete >= tableRows.length || indexToDelete < 0) {
531
+ throw new Error('Expected table cell to be inside of table row.');
532
+ }
533
+ const targetRowNode = tableRows[indexToDelete];
534
+ targetRowNode.remove();
535
+ return tableNode;
536
+ }
537
+
538
+ /**
539
+ * @deprecated This function does not support merged cells. Use {@link $insertTableRowAtSelection} or {@link $insertTableRowAtNode} instead.
540
+ */
541
+ function $insertTableRow(tableNode, targetIndex, shouldInsertAfter = true, rowCount, table) {
542
+ const tableRows = tableNode.getChildren();
543
+ if (targetIndex >= tableRows.length || targetIndex < 0) {
544
+ throw new Error('Table row target index out of range');
545
+ }
546
+ const targetRowNode = tableRows[targetIndex];
547
+ if ($isTableRowNode(targetRowNode)) {
548
+ for (let r = 0; r < rowCount; r++) {
549
+ const tableRowCells = targetRowNode.getChildren();
550
+ const tableColumnCount = tableRowCells.length;
551
+ const newTableRowNode = $createTableRowNode();
552
+ for (let c = 0; c < tableColumnCount; c++) {
553
+ const tableCellFromTargetRow = tableRowCells[c];
554
+ if (!$isTableCellNode(tableCellFromTargetRow)) {
555
+ formatDevErrorMessage(`Expected table cell`);
556
+ }
557
+ const {
558
+ above,
559
+ below
560
+ } = $getTableCellSiblingsFromTableCellNode(tableCellFromTargetRow, table);
561
+ let headerState = TableCellHeaderStates.NO_STATUS;
562
+ const width = above && above.getWidth() || below && below.getWidth() || undefined;
563
+ if (above && above.hasHeaderState(TableCellHeaderStates.COLUMN) || below && below.hasHeaderState(TableCellHeaderStates.COLUMN)) {
564
+ headerState |= TableCellHeaderStates.COLUMN;
565
+ }
566
+ const tableCellNode = $createTableCellNode(headerState, 1, width);
567
+ tableCellNode.append($createParagraphNode());
568
+ newTableRowNode.append(tableCellNode);
569
+ }
570
+ if (shouldInsertAfter) {
571
+ targetRowNode.insertAfter(newTableRowNode);
572
+ } else {
573
+ targetRowNode.insertBefore(newTableRowNode);
574
+ }
575
+ }
576
+ } else {
577
+ throw new Error('Row before insertion index does not exist.');
578
+ }
579
+ return tableNode;
580
+ }
581
+ const getHeaderState = (currentState, possibleState) => {
582
+ if (currentState === TableCellHeaderStates.BOTH || currentState === possibleState) {
583
+ return possibleState;
584
+ }
585
+ return TableCellHeaderStates.NO_STATUS;
586
+ };
587
+
588
+ /**
589
+ * Inserts a table row before or after the current focus cell node,
590
+ * taking into account any spans. If successful, returns the
591
+ * inserted table row node.
592
+ */
593
+ function $insertTableRowAtSelection(insertAfter = true) {
594
+ const selection = $getSelection();
595
+ if (!($isRangeSelection(selection) || $isTableSelection(selection))) {
596
+ formatDevErrorMessage(`Expected a RangeSelection or TableSelection`);
597
+ }
598
+ const anchor = selection.anchor.getNode();
599
+ const focus = selection.focus.getNode();
600
+ const [anchorCell] = $getNodeTriplet(anchor);
601
+ const [focusCell,, grid] = $getNodeTriplet(focus);
602
+ const [, focusCellMap, anchorCellMap] = $computeTableMap(grid, focusCell, anchorCell);
603
+ const {
604
+ startRow: anchorStartRow
605
+ } = anchorCellMap;
606
+ const {
607
+ startRow: focusStartRow
608
+ } = focusCellMap;
609
+ if (insertAfter) {
610
+ return $insertTableRowAtNode(anchorStartRow + anchorCell.__rowSpan > focusStartRow + focusCell.__rowSpan ? anchorCell : focusCell, true);
611
+ } else {
612
+ return $insertTableRowAtNode(focusStartRow < anchorStartRow ? focusCell : anchorCell, false);
613
+ }
614
+ }
615
+
616
+ /**
617
+ * @deprecated renamed to {@link $insertTableRowAtSelection}
618
+ */
619
+ const $insertTableRow__EXPERIMENTAL = $insertTableRowAtSelection;
620
+
621
+ /**
622
+ * Inserts a table row before or after the given cell node,
623
+ * taking into account any spans. If successful, returns the
624
+ * inserted table row node.
625
+ */
626
+ function $insertTableRowAtNode(cellNode, insertAfter = true) {
627
+ const [,, grid] = $getNodeTriplet(cellNode);
628
+ const [gridMap, cellMap] = $computeTableMap(grid, cellNode, cellNode);
629
+ const columnCount = gridMap[0].length;
630
+ const {
631
+ startRow: cellStartRow
632
+ } = cellMap;
633
+ let insertedRow = null;
634
+ if (insertAfter) {
635
+ const insertAfterEndRow = cellStartRow + cellNode.__rowSpan - 1;
636
+ const insertAfterEndRowMap = gridMap[insertAfterEndRow];
637
+ const newRow = $createTableRowNode();
638
+ for (let i = 0; i < columnCount; i++) {
639
+ const {
640
+ cell,
641
+ startRow
642
+ } = insertAfterEndRowMap[i];
643
+ if (startRow + cell.__rowSpan - 1 <= insertAfterEndRow) {
644
+ const currentCell = insertAfterEndRowMap[i].cell;
645
+ const currentCellHeaderState = currentCell.__headerState;
646
+ const headerState = getHeaderState(currentCellHeaderState, TableCellHeaderStates.COLUMN);
647
+ newRow.append($createTableCellNode(headerState).append($createParagraphNode()));
648
+ } else {
649
+ cell.setRowSpan(cell.__rowSpan + 1);
650
+ }
651
+ }
652
+ const insertAfterEndRowNode = grid.getChildAtIndex(insertAfterEndRow);
653
+ if (!$isTableRowNode(insertAfterEndRowNode)) {
654
+ formatDevErrorMessage(`insertAfterEndRow is not a TableRowNode`);
655
+ }
656
+ insertAfterEndRowNode.insertAfter(newRow);
657
+ insertedRow = newRow;
658
+ } else {
659
+ const insertBeforeStartRow = cellStartRow;
660
+ const insertBeforeStartRowMap = gridMap[insertBeforeStartRow];
661
+ const newRow = $createTableRowNode();
662
+ for (let i = 0; i < columnCount; i++) {
663
+ const {
664
+ cell,
665
+ startRow
666
+ } = insertBeforeStartRowMap[i];
667
+ if (startRow === insertBeforeStartRow) {
668
+ const currentCell = insertBeforeStartRowMap[i].cell;
669
+ const currentCellHeaderState = currentCell.__headerState;
670
+ const headerState = getHeaderState(currentCellHeaderState, TableCellHeaderStates.COLUMN);
671
+ newRow.append($createTableCellNode(headerState).append($createParagraphNode()));
672
+ } else {
673
+ cell.setRowSpan(cell.__rowSpan + 1);
674
+ }
675
+ }
676
+ const insertBeforeStartRowNode = grid.getChildAtIndex(insertBeforeStartRow);
677
+ if (!$isTableRowNode(insertBeforeStartRowNode)) {
678
+ formatDevErrorMessage(`insertBeforeStartRow is not a TableRowNode`);
679
+ }
680
+ insertBeforeStartRowNode.insertBefore(newRow);
681
+ insertedRow = newRow;
682
+ }
683
+ return insertedRow;
684
+ }
685
+
686
+ /**
687
+ * @deprecated This function does not support merged cells. Use {@link $insertTableColumnAtSelection} or {@link $insertTableColumnAtNode} instead.
688
+ */
689
+ function $insertTableColumn(tableNode, targetIndex, shouldInsertAfter = true, columnCount, table) {
690
+ const tableRows = tableNode.getChildren();
691
+ const tableCellsToBeInserted = [];
692
+ for (let r = 0; r < tableRows.length; r++) {
693
+ const currentTableRowNode = tableRows[r];
694
+ if ($isTableRowNode(currentTableRowNode)) {
695
+ for (let c = 0; c < columnCount; c++) {
696
+ const tableRowChildren = currentTableRowNode.getChildren();
697
+ if (targetIndex >= tableRowChildren.length || targetIndex < 0) {
698
+ throw new Error('Table column target index out of range');
699
+ }
700
+ const targetCell = tableRowChildren[targetIndex];
701
+ if (!$isTableCellNode(targetCell)) {
702
+ formatDevErrorMessage(`Expected table cell`);
703
+ }
704
+ const {
705
+ left,
706
+ right
707
+ } = $getTableCellSiblingsFromTableCellNode(targetCell, table);
708
+ let headerState = TableCellHeaderStates.NO_STATUS;
709
+ if (left && left.hasHeaderState(TableCellHeaderStates.ROW) || right && right.hasHeaderState(TableCellHeaderStates.ROW)) {
710
+ headerState |= TableCellHeaderStates.ROW;
711
+ }
712
+ const newTableCell = $createTableCellNode(headerState);
713
+ newTableCell.append($createParagraphNode());
714
+ tableCellsToBeInserted.push({
715
+ newTableCell,
716
+ targetCell
717
+ });
718
+ }
719
+ }
720
+ }
721
+ tableCellsToBeInserted.forEach(({
722
+ newTableCell,
723
+ targetCell
724
+ }) => {
725
+ if (shouldInsertAfter) {
726
+ targetCell.insertAfter(newTableCell);
727
+ } else {
728
+ targetCell.insertBefore(newTableCell);
729
+ }
730
+ });
731
+ return tableNode;
732
+ }
733
+
734
+ /**
735
+ * Inserts a column before or after the current focus cell node,
736
+ * taking into account any spans. If successful, returns the
737
+ * first inserted cell node.
738
+ */
739
+ function $insertTableColumnAtSelection(insertAfter = true) {
740
+ const selection = $getSelection();
741
+ if (!($isRangeSelection(selection) || $isTableSelection(selection))) {
742
+ formatDevErrorMessage(`Expected a RangeSelection or TableSelection`);
743
+ }
744
+ const anchor = selection.anchor.getNode();
745
+ const focus = selection.focus.getNode();
746
+ const [anchorCell] = $getNodeTriplet(anchor);
747
+ const [focusCell,, grid] = $getNodeTriplet(focus);
748
+ const [, focusCellMap, anchorCellMap] = $computeTableMap(grid, focusCell, anchorCell);
749
+ const {
750
+ startColumn: anchorStartColumn
751
+ } = anchorCellMap;
752
+ const {
753
+ startColumn: focusStartColumn
754
+ } = focusCellMap;
755
+ if (insertAfter) {
756
+ return $insertTableColumnAtNode(anchorStartColumn + anchorCell.__colSpan > focusStartColumn + focusCell.__colSpan ? anchorCell : focusCell, true);
757
+ } else {
758
+ return $insertTableColumnAtNode(focusStartColumn < anchorStartColumn ? focusCell : anchorCell, false);
759
+ }
760
+ }
761
+
762
+ /**
763
+ * @deprecated renamed to {@link $insertTableColumnAtSelection}
764
+ */
765
+ const $insertTableColumn__EXPERIMENTAL = $insertTableColumnAtSelection;
766
+
767
+ /**
768
+ * Inserts a column before or after the given cell node,
769
+ * taking into account any spans. If successful, returns the
770
+ * first inserted cell node.
771
+ */
772
+ function $insertTableColumnAtNode(cellNode, insertAfter = true, shouldSetSelection = true) {
773
+ const [,, grid] = $getNodeTriplet(cellNode);
774
+ const [gridMap, cellMap] = $computeTableMap(grid, cellNode, cellNode);
775
+ const rowCount = gridMap.length;
776
+ const {
777
+ startColumn
778
+ } = cellMap;
779
+ const insertAfterColumn = insertAfter ? startColumn + cellNode.__colSpan - 1 : startColumn - 1;
780
+ const gridFirstChild = grid.getFirstChild();
781
+ if (!$isTableRowNode(gridFirstChild)) {
782
+ formatDevErrorMessage(`Expected firstTable child to be a row`);
783
+ }
784
+ let firstInsertedCell = null;
785
+ function $createTableCellNodeForInsertTableColumn(headerState = TableCellHeaderStates.NO_STATUS) {
786
+ const cell = $createTableCellNode(headerState).append($createParagraphNode());
787
+ if (firstInsertedCell === null) {
788
+ firstInsertedCell = cell;
789
+ }
790
+ return cell;
791
+ }
792
+ let loopRow = gridFirstChild;
793
+ rowLoop: for (let i = 0; i < rowCount; i++) {
794
+ if (i !== 0) {
795
+ const currentRow = loopRow.getNextSibling();
796
+ if (!$isTableRowNode(currentRow)) {
797
+ formatDevErrorMessage(`Expected row nextSibling to be a row`);
798
+ }
799
+ loopRow = currentRow;
800
+ }
801
+ const rowMap = gridMap[i];
802
+ const currentCellHeaderState = rowMap[insertAfterColumn < 0 ? 0 : insertAfterColumn].cell.__headerState;
803
+ const headerState = getHeaderState(currentCellHeaderState, TableCellHeaderStates.ROW);
804
+ if (insertAfterColumn < 0) {
805
+ $insertFirst(loopRow, $createTableCellNodeForInsertTableColumn(headerState));
806
+ continue;
807
+ }
808
+ const {
809
+ cell: currentCell,
810
+ startColumn: currentStartColumn,
811
+ startRow: currentStartRow
812
+ } = rowMap[insertAfterColumn];
813
+ if (currentStartColumn + currentCell.__colSpan - 1 <= insertAfterColumn) {
814
+ let insertAfterCell = currentCell;
815
+ let insertAfterCellRowStart = currentStartRow;
816
+ let prevCellIndex = insertAfterColumn;
817
+ while (insertAfterCellRowStart !== i && insertAfterCell.__rowSpan > 1) {
818
+ prevCellIndex -= currentCell.__colSpan;
819
+ if (prevCellIndex >= 0) {
820
+ const {
821
+ cell: cell_,
822
+ startRow: startRow_
823
+ } = rowMap[prevCellIndex];
824
+ insertAfterCell = cell_;
825
+ insertAfterCellRowStart = startRow_;
826
+ } else {
827
+ loopRow.append($createTableCellNodeForInsertTableColumn(headerState));
828
+ continue rowLoop;
829
+ }
830
+ }
831
+ insertAfterCell.insertAfter($createTableCellNodeForInsertTableColumn(headerState));
832
+ } else {
833
+ currentCell.setColSpan(currentCell.__colSpan + 1);
834
+ }
835
+ }
836
+ if (firstInsertedCell !== null && shouldSetSelection) {
837
+ $moveSelectionToCell(firstInsertedCell);
838
+ }
839
+ const colWidths = grid.getColWidths();
840
+ if (colWidths) {
841
+ const newColWidths = [...colWidths];
842
+ const columnIndex = insertAfterColumn < 0 ? 0 : insertAfterColumn;
843
+ const newWidth = newColWidths[columnIndex];
844
+ newColWidths.splice(columnIndex, 0, newWidth);
845
+ grid.setColWidths(newColWidths);
846
+ }
847
+ return firstInsertedCell;
848
+ }
849
+
850
+ /**
851
+ * @deprecated This function does not support merged cells. Use {@link $deleteTableColumnAtSelection} instead.
852
+ */
853
+ function $deleteTableColumn(tableNode, targetIndex) {
854
+ const tableRows = tableNode.getChildren();
855
+ for (let i = 0; i < tableRows.length; i++) {
856
+ const currentTableRowNode = tableRows[i];
857
+ if ($isTableRowNode(currentTableRowNode)) {
858
+ const tableRowChildren = currentTableRowNode.getChildren();
859
+ if (targetIndex >= tableRowChildren.length || targetIndex < 0) {
860
+ throw new Error('Table column target index out of range');
861
+ }
862
+ tableRowChildren[targetIndex].remove();
863
+ }
864
+ }
865
+ return tableNode;
866
+ }
867
+ function $deleteTableRowAtSelection() {
868
+ const selection = $getSelection();
869
+ if (!($isRangeSelection(selection) || $isTableSelection(selection))) {
870
+ formatDevErrorMessage(`Expected a RangeSelection or TableSelection`);
871
+ }
872
+ const [anchor, focus] = selection.isBackward() ? [selection.focus.getNode(), selection.anchor.getNode()] : [selection.anchor.getNode(), selection.focus.getNode()];
873
+ const [anchorCell,, grid] = $getNodeTriplet(anchor);
874
+ const [focusCell] = $getNodeTriplet(focus);
875
+ const [gridMap, anchorCellMap, focusCellMap] = $computeTableMap(grid, anchorCell, focusCell);
876
+ const {
877
+ startRow: anchorStartRow
878
+ } = anchorCellMap;
879
+ const {
880
+ startRow: focusStartRow
881
+ } = focusCellMap;
882
+ const focusEndRow = focusStartRow + focusCell.__rowSpan - 1;
883
+ if (gridMap.length === focusEndRow - anchorStartRow + 1) {
884
+ // Empty grid
885
+ grid.remove();
886
+ return;
887
+ }
888
+ const columnCount = gridMap[0].length;
889
+ const nextRow = gridMap[focusEndRow + 1];
890
+ const nextRowNode = grid.getChildAtIndex(focusEndRow + 1);
891
+ for (let row = focusEndRow; row >= anchorStartRow; row--) {
892
+ for (let column = columnCount - 1; column >= 0; column--) {
893
+ const {
894
+ cell,
895
+ startRow: cellStartRow,
896
+ startColumn: cellStartColumn
897
+ } = gridMap[row][column];
898
+ if (cellStartColumn !== column) {
899
+ // Don't repeat work for the same Cell
900
+ continue;
901
+ }
902
+ // Rows overflowing top or bottom have to be trimmed
903
+ if (cellStartRow < anchorStartRow || cellStartRow + cell.__rowSpan - 1 > focusEndRow) {
904
+ const intersectionStart = Math.max(cellStartRow, anchorStartRow);
905
+ const intersectionEnd = Math.min(cell.__rowSpan + cellStartRow - 1, focusEndRow);
906
+ const overflowRowsCount = intersectionStart <= intersectionEnd ? intersectionEnd - intersectionStart + 1 : 0;
907
+ cell.setRowSpan(cell.__rowSpan - overflowRowsCount);
908
+ }
909
+ // Rows overflowing bottom have to be moved to the next row
910
+ if (cellStartRow >= anchorStartRow && cellStartRow + cell.__rowSpan - 1 > focusEndRow &&
911
+ // Handle overflow only once
912
+ row === focusEndRow) {
913
+ if (!(nextRowNode !== null)) {
914
+ formatDevErrorMessage(`Expected nextRowNode not to be null`);
915
+ }
916
+ let insertAfterCell = null;
917
+ for (let columnIndex = 0; columnIndex < column; columnIndex++) {
918
+ const currentCellMap = nextRow[columnIndex];
919
+ const currentCell = currentCellMap.cell;
920
+ // Checking the cell having startRow as same as nextRow
921
+ if (currentCellMap.startRow === row + 1) {
922
+ insertAfterCell = currentCell;
923
+ }
924
+ if (currentCell.__colSpan > 1) {
925
+ columnIndex += currentCell.__colSpan - 1;
926
+ }
927
+ }
928
+ if (insertAfterCell === null) {
929
+ $insertFirst(nextRowNode, cell);
930
+ } else {
931
+ insertAfterCell.insertAfter(cell);
932
+ }
933
+ }
934
+ }
935
+ const rowNode = grid.getChildAtIndex(row);
936
+ if (!$isTableRowNode(rowNode)) {
937
+ formatDevErrorMessage(`Expected TableNode childAtIndex(${String(row)}) to be RowNode`);
938
+ }
939
+ rowNode.remove();
940
+ }
941
+ if (nextRow !== undefined) {
942
+ const {
943
+ cell
944
+ } = nextRow[0];
945
+ $moveSelectionToCell(cell);
946
+ } else {
947
+ const previousRow = gridMap[anchorStartRow - 1];
948
+ const {
949
+ cell
950
+ } = previousRow[0];
951
+ $moveSelectionToCell(cell);
952
+ }
953
+ }
954
+
955
+ /**
956
+ * @deprecated renamed to {@link $deleteTableRowAtSelection}
957
+ */
958
+ const $deleteTableRow__EXPERIMENTAL = $deleteTableRowAtSelection;
959
+ function $deleteTableColumnAtSelection() {
960
+ const selection = $getSelection();
961
+ if (!($isRangeSelection(selection) || $isTableSelection(selection))) {
962
+ formatDevErrorMessage(`Expected a RangeSelection or TableSelection`);
963
+ }
964
+ const anchor = selection.anchor.getNode();
965
+ const focus = selection.focus.getNode();
966
+ const [anchorCell,, grid] = $getNodeTriplet(anchor);
967
+ const [focusCell] = $getNodeTriplet(focus);
968
+ const [gridMap, anchorCellMap, focusCellMap] = $computeTableMap(grid, anchorCell, focusCell);
969
+ const {
970
+ startColumn: anchorStartColumn
971
+ } = anchorCellMap;
972
+ const {
973
+ startRow: focusStartRow,
974
+ startColumn: focusStartColumn
975
+ } = focusCellMap;
976
+ const startColumn = Math.min(anchorStartColumn, focusStartColumn);
977
+ const endColumn = Math.max(anchorStartColumn + anchorCell.__colSpan - 1, focusStartColumn + focusCell.__colSpan - 1);
978
+ const selectedColumnCount = endColumn - startColumn + 1;
979
+ const columnCount = gridMap[0].length;
980
+ if (columnCount === endColumn - startColumn + 1) {
981
+ // Empty grid
982
+ grid.selectPrevious();
983
+ grid.remove();
984
+ return;
985
+ }
986
+ const rowCount = gridMap.length;
987
+ for (let row = 0; row < rowCount; row++) {
988
+ for (let column = startColumn; column <= endColumn; column++) {
989
+ const {
990
+ cell,
991
+ startColumn: cellStartColumn
992
+ } = gridMap[row][column];
993
+ if (cellStartColumn < startColumn) {
994
+ if (column === startColumn) {
995
+ const overflowLeft = startColumn - cellStartColumn;
996
+ // Overflowing left
997
+ cell.setColSpan(cell.__colSpan -
998
+ // Possible overflow right too
999
+ Math.min(selectedColumnCount, cell.__colSpan - overflowLeft));
1000
+ }
1001
+ } else if (cellStartColumn + cell.__colSpan - 1 > endColumn) {
1002
+ if (column === endColumn) {
1003
+ // Overflowing right
1004
+ const inSelectedArea = endColumn - cellStartColumn + 1;
1005
+ cell.setColSpan(cell.__colSpan - inSelectedArea);
1006
+ }
1007
+ } else {
1008
+ cell.remove();
1009
+ }
1010
+ }
1011
+ }
1012
+ const focusRowMap = gridMap[focusStartRow];
1013
+ const nextColumn = anchorStartColumn > focusStartColumn ? focusRowMap[anchorStartColumn + anchorCell.__colSpan] : focusRowMap[focusStartColumn + focusCell.__colSpan];
1014
+ if (nextColumn !== undefined) {
1015
+ const {
1016
+ cell
1017
+ } = nextColumn;
1018
+ $moveSelectionToCell(cell);
1019
+ } else {
1020
+ const previousRow = focusStartColumn < anchorStartColumn ? focusRowMap[focusStartColumn - 1] : focusRowMap[anchorStartColumn - 1];
1021
+ const {
1022
+ cell
1023
+ } = previousRow;
1024
+ $moveSelectionToCell(cell);
1025
+ }
1026
+ const colWidths = grid.getColWidths();
1027
+ if (colWidths) {
1028
+ const newColWidths = [...colWidths];
1029
+ newColWidths.splice(startColumn, selectedColumnCount);
1030
+ grid.setColWidths(newColWidths);
1031
+ }
1032
+ }
1033
+
1034
+ /**
1035
+ * @deprecated renamed to {@link $deleteTableColumnAtSelection}
1036
+ */
1037
+ const $deleteTableColumn__EXPERIMENTAL = $deleteTableColumnAtSelection;
1038
+ function $moveSelectionToCell(cell) {
1039
+ const firstDescendant = cell.getFirstDescendant();
1040
+ if (firstDescendant == null) {
1041
+ cell.selectStart();
1042
+ } else {
1043
+ firstDescendant.getParentOrThrow().selectStart();
1044
+ }
1045
+ }
1046
+ function $insertFirst(parent, node) {
1047
+ const firstChild = parent.getFirstChild();
1048
+ if (firstChild !== null) {
1049
+ firstChild.insertBefore(node);
1050
+ } else {
1051
+ parent.append(node);
1052
+ }
1053
+ }
1054
+ function $mergeCells(cellNodes) {
1055
+ if (cellNodes.length === 0) {
1056
+ return null;
1057
+ }
1058
+
1059
+ // Find the table node
1060
+ const tableNode = $getTableNodeFromLexicalNodeOrThrow(cellNodes[0]);
1061
+ const [gridMap] = $computeTableMapSkipCellCheck(tableNode, null, null);
1062
+
1063
+ // Find the boundaries of the selection including merged cells
1064
+ let minRow = Infinity;
1065
+ let maxRow = -Infinity;
1066
+ let minCol = Infinity;
1067
+ let maxCol = -Infinity;
1068
+
1069
+ // First pass: find the actual boundaries considering merged cells
1070
+ const processedCells = new Set();
1071
+ for (const row of gridMap) {
1072
+ for (const mapCell of row) {
1073
+ if (!mapCell || !mapCell.cell) {
1074
+ continue;
1075
+ }
1076
+ const cellKey = mapCell.cell.getKey();
1077
+ if (processedCells.has(cellKey)) {
1078
+ continue;
1079
+ }
1080
+ if (cellNodes.some(cell => cell.is(mapCell.cell))) {
1081
+ processedCells.add(cellKey);
1082
+ // Get the actual position of this cell in the grid
1083
+ const cellStartRow = mapCell.startRow;
1084
+ const cellStartCol = mapCell.startColumn;
1085
+ const cellRowSpan = mapCell.cell.__rowSpan || 1;
1086
+ const cellColSpan = mapCell.cell.__colSpan || 1;
1087
+
1088
+ // Update boundaries considering the cell's actual position and span
1089
+ minRow = Math.min(minRow, cellStartRow);
1090
+ maxRow = Math.max(maxRow, cellStartRow + cellRowSpan - 1);
1091
+ minCol = Math.min(minCol, cellStartCol);
1092
+ maxCol = Math.max(maxCol, cellStartCol + cellColSpan - 1);
1093
+ }
1094
+ }
1095
+ }
1096
+
1097
+ // Validate boundaries
1098
+ if (minRow === Infinity || minCol === Infinity) {
1099
+ return null;
1100
+ }
1101
+
1102
+ // The total span of the merged cell
1103
+ const totalRowSpan = maxRow - minRow + 1;
1104
+ const totalColSpan = maxCol - minCol + 1;
1105
+
1106
+ // Use the top-left cell as the target cell
1107
+ const targetCellMap = gridMap[minRow][minCol];
1108
+ if (!targetCellMap.cell) {
1109
+ return null;
1110
+ }
1111
+ const targetCell = targetCellMap.cell;
1112
+
1113
+ // Set the spans for the target cell
1114
+ targetCell.setColSpan(totalColSpan);
1115
+ targetCell.setRowSpan(totalRowSpan);
1116
+
1117
+ // Move content from other cells to the target cell
1118
+ const seenCells = new Set([targetCell.getKey()]);
1119
+
1120
+ // Second pass: merge content and remove other cells
1121
+ for (let row = minRow; row <= maxRow; row++) {
1122
+ for (let col = minCol; col <= maxCol; col++) {
1123
+ const mapCell = gridMap[row][col];
1124
+ if (!mapCell.cell) {
1125
+ continue;
1126
+ }
1127
+ const currentCell = mapCell.cell;
1128
+ const key = currentCell.getKey();
1129
+ if (!seenCells.has(key)) {
1130
+ seenCells.add(key);
1131
+ const isEmpty = $cellContainsEmptyParagraph(currentCell);
1132
+ if (!isEmpty) {
1133
+ targetCell.append(...currentCell.getChildren());
1134
+ }
1135
+ currentCell.remove();
1136
+ }
1137
+ }
1138
+ }
1139
+
1140
+ // Ensure target cell has content
1141
+ if (targetCell.getChildrenSize() === 0) {
1142
+ targetCell.append($createParagraphNode());
1143
+ }
1144
+ return targetCell;
1145
+ }
1146
+ function $cellContainsEmptyParagraph(cell) {
1147
+ if (cell.getChildrenSize() !== 1) {
1148
+ return false;
1149
+ }
1150
+ const firstChild = cell.getFirstChildOrThrow();
1151
+ if (!$isParagraphNode(firstChild) || !firstChild.isEmpty()) {
1152
+ return false;
1153
+ }
1154
+ return true;
1155
+ }
1156
+ function $unmergeCell() {
1157
+ const selection = $getSelection();
1158
+ if (!($isRangeSelection(selection) || $isTableSelection(selection))) {
1159
+ formatDevErrorMessage(`Expected a RangeSelection or TableSelection`);
1160
+ }
1161
+ const anchor = selection.anchor.getNode();
1162
+ const cellNode = $findMatchingParent(anchor, $isTableCellNode);
1163
+ if (!$isTableCellNode(cellNode)) {
1164
+ formatDevErrorMessage(`Expected to find a parent TableCellNode`);
1165
+ }
1166
+ return $unmergeCellNode(cellNode);
1167
+ }
1168
+ function $unmergeCellNode(cellNode) {
1169
+ const [cell, row, grid] = $getNodeTriplet(cellNode);
1170
+ const colSpan = cell.__colSpan;
1171
+ const rowSpan = cell.__rowSpan;
1172
+ if (colSpan === 1 && rowSpan === 1) {
1173
+ return;
1174
+ }
1175
+ const [map, cellMap] = $computeTableMap(grid, cell, cell);
1176
+ const {
1177
+ startColumn,
1178
+ startRow
1179
+ } = cellMap;
1180
+ // Create a heuristic for what the style of the unmerged cells should be
1181
+ // based on whether every row or column already had that state before the
1182
+ // unmerge.
1183
+ const baseColStyle = cell.__headerState & TableCellHeaderStates.COLUMN;
1184
+ const colStyles = Array.from({
1185
+ length: colSpan
1186
+ }, (_v, i) => {
1187
+ let colStyle = baseColStyle;
1188
+ for (let rowIdx = 0; colStyle !== 0 && rowIdx < map.length; rowIdx++) {
1189
+ colStyle &= map[rowIdx][i + startColumn].cell.__headerState;
1190
+ }
1191
+ return colStyle;
1192
+ });
1193
+ const baseRowStyle = cell.__headerState & TableCellHeaderStates.ROW;
1194
+ const rowStyles = Array.from({
1195
+ length: rowSpan
1196
+ }, (_v, i) => {
1197
+ let rowStyle = baseRowStyle;
1198
+ for (let colIdx = 0; rowStyle !== 0 && colIdx < map[0].length; colIdx++) {
1199
+ rowStyle &= map[i + startRow][colIdx].cell.__headerState;
1200
+ }
1201
+ return rowStyle;
1202
+ });
1203
+ if (colSpan > 1) {
1204
+ for (let i = 1; i < colSpan; i++) {
1205
+ cell.insertAfter($createTableCellNode(colStyles[i] | rowStyles[0]).append($createParagraphNode()));
1206
+ }
1207
+ cell.setColSpan(1);
1208
+ }
1209
+ if (rowSpan > 1) {
1210
+ let currentRowNode;
1211
+ for (let i = 1; i < rowSpan; i++) {
1212
+ const currentRow = startRow + i;
1213
+ const currentRowMap = map[currentRow];
1214
+ currentRowNode = (currentRowNode || row).getNextSibling();
1215
+ if (!$isTableRowNode(currentRowNode)) {
1216
+ formatDevErrorMessage(`Expected row next sibling to be a row`);
1217
+ }
1218
+ let insertAfterCell = null;
1219
+ for (let column = 0; column < startColumn; column++) {
1220
+ const currentCellMap = currentRowMap[column];
1221
+ const currentCell = currentCellMap.cell;
1222
+ if (currentCellMap.startRow === currentRow) {
1223
+ insertAfterCell = currentCell;
1224
+ }
1225
+ if (currentCell.__colSpan > 1) {
1226
+ column += currentCell.__colSpan - 1;
1227
+ }
1228
+ }
1229
+ if (insertAfterCell === null) {
1230
+ for (let j = colSpan - 1; j >= 0; j--) {
1231
+ $insertFirst(currentRowNode, $createTableCellNode(colStyles[j] | rowStyles[i]).append($createParagraphNode()));
1232
+ }
1233
+ } else {
1234
+ for (let j = colSpan - 1; j >= 0; j--) {
1235
+ insertAfterCell.insertAfter($createTableCellNode(colStyles[j] | rowStyles[i]).append($createParagraphNode()));
1236
+ }
1237
+ }
1238
+ }
1239
+ cell.setRowSpan(1);
1240
+ }
1241
+ }
1242
+ function $computeTableMap(tableNode, cellA, cellB) {
1243
+ const [tableMap, cellAValue, cellBValue] = $computeTableMapSkipCellCheck(tableNode, cellA, cellB);
1244
+ if (!(cellAValue !== null)) {
1245
+ formatDevErrorMessage(`Anchor not found in Table`);
1246
+ }
1247
+ if (!(cellBValue !== null)) {
1248
+ formatDevErrorMessage(`Focus not found in Table`);
1249
+ }
1250
+ return [tableMap, cellAValue, cellBValue];
1251
+ }
1252
+ function $computeTableMapSkipCellCheck(tableNode, cellA, cellB) {
1253
+ const tableMap = [];
1254
+ let cellAValue = null;
1255
+ let cellBValue = null;
1256
+ function getMapRow(i) {
1257
+ let row = tableMap[i];
1258
+ if (row === undefined) {
1259
+ tableMap[i] = row = [];
1260
+ }
1261
+ return row;
1262
+ }
1263
+ const gridChildren = tableNode.getChildren();
1264
+ for (let rowIdx = 0; rowIdx < gridChildren.length; rowIdx++) {
1265
+ const row = gridChildren[rowIdx];
1266
+ if (!$isTableRowNode(row)) {
1267
+ formatDevErrorMessage(`Expected TableNode children to be TableRowNode`);
1268
+ }
1269
+ const startMapRow = getMapRow(rowIdx);
1270
+ for (let cell = row.getFirstChild(), colIdx = 0; cell != null; cell = cell.getNextSibling()) {
1271
+ if (!$isTableCellNode(cell)) {
1272
+ formatDevErrorMessage(`Expected TableRowNode children to be TableCellNode`);
1273
+ } // Skip past any columns that were merged from a higher row
1274
+ while (startMapRow[colIdx] !== undefined) {
1275
+ colIdx++;
1276
+ }
1277
+ const value = {
1278
+ cell,
1279
+ startColumn: colIdx,
1280
+ startRow: rowIdx
1281
+ };
1282
+ const {
1283
+ __rowSpan: rowSpan,
1284
+ __colSpan: colSpan
1285
+ } = cell;
1286
+ for (let j = 0; j < rowSpan; j++) {
1287
+ if (rowIdx + j >= gridChildren.length) {
1288
+ // The table is non-rectangular with a rowSpan
1289
+ // below the last <tr> in the table.
1290
+ // We should probably handle this with a node transform
1291
+ // to ensure that tables are always rectangular but this
1292
+ // will avoid crashes such as #6584
1293
+ // Note that there are probably still latent bugs
1294
+ // regarding colSpan or general cell count mismatches.
1295
+ break;
1296
+ }
1297
+ const mapRow = getMapRow(rowIdx + j);
1298
+ for (let i = 0; i < colSpan; i++) {
1299
+ mapRow[colIdx + i] = value;
1300
+ }
1301
+ }
1302
+ if (cellA !== null && cellAValue === null && cellA.is(cell)) {
1303
+ cellAValue = value;
1304
+ }
1305
+ if (cellB !== null && cellBValue === null && cellB.is(cell)) {
1306
+ cellBValue = value;
1307
+ }
1308
+ }
1309
+ }
1310
+ return [tableMap, cellAValue, cellBValue];
1311
+ }
1312
+ function $getNodeTriplet(source) {
1313
+ let cell;
1314
+ if (source instanceof TableCellNode) {
1315
+ cell = source;
1316
+ } else if ('__type' in source) {
1317
+ const cell_ = $findMatchingParent(source, $isTableCellNode);
1318
+ if (!$isTableCellNode(cell_)) {
1319
+ formatDevErrorMessage(`Expected to find a parent TableCellNode`);
1320
+ }
1321
+ cell = cell_;
1322
+ } else {
1323
+ const cell_ = $findMatchingParent(source.getNode(), $isTableCellNode);
1324
+ if (!$isTableCellNode(cell_)) {
1325
+ formatDevErrorMessage(`Expected to find a parent TableCellNode`);
1326
+ }
1327
+ cell = cell_;
1328
+ }
1329
+ const row = cell.getParent();
1330
+ if (!$isTableRowNode(row)) {
1331
+ formatDevErrorMessage(`Expected TableCellNode to have a parent TableRowNode`);
1332
+ }
1333
+ const grid = row.getParent();
1334
+ if (!$isTableNode(grid)) {
1335
+ formatDevErrorMessage(`Expected TableRowNode to have a parent TableNode`);
1336
+ }
1337
+ return [cell, row, grid];
1338
+ }
1339
+ function $computeTableCellRectSpans(map, boundary) {
1340
+ const {
1341
+ minColumn,
1342
+ maxColumn,
1343
+ minRow,
1344
+ maxRow
1345
+ } = boundary;
1346
+ let topSpan = 1;
1347
+ let leftSpan = 1;
1348
+ let rightSpan = 1;
1349
+ let bottomSpan = 1;
1350
+ const topRow = map[minRow];
1351
+ const bottomRow = map[maxRow];
1352
+ for (let col = minColumn; col <= maxColumn; col++) {
1353
+ topSpan = Math.max(topSpan, topRow[col].cell.__rowSpan);
1354
+ bottomSpan = Math.max(bottomSpan, bottomRow[col].cell.__rowSpan);
1355
+ }
1356
+ for (let row = minRow; row <= maxRow; row++) {
1357
+ leftSpan = Math.max(leftSpan, map[row][minColumn].cell.__colSpan);
1358
+ rightSpan = Math.max(rightSpan, map[row][maxColumn].cell.__colSpan);
1359
+ }
1360
+ return {
1361
+ bottomSpan,
1362
+ leftSpan,
1363
+ rightSpan,
1364
+ topSpan
1365
+ };
1366
+ }
1367
+ function $computeTableCellRectBoundary(map, cellAMap, cellBMap) {
1368
+ // Initial boundaries based on the anchor and focus cells
1369
+ let minColumn = Math.min(cellAMap.startColumn, cellBMap.startColumn);
1370
+ let minRow = Math.min(cellAMap.startRow, cellBMap.startRow);
1371
+ let maxColumn = Math.max(cellAMap.startColumn + cellAMap.cell.__colSpan - 1, cellBMap.startColumn + cellBMap.cell.__colSpan - 1);
1372
+ let maxRow = Math.max(cellAMap.startRow + cellAMap.cell.__rowSpan - 1, cellBMap.startRow + cellBMap.cell.__rowSpan - 1);
1373
+
1374
+ // Keep expanding until we have a complete rectangle
1375
+ let hasChanges;
1376
+ do {
1377
+ hasChanges = false;
1378
+
1379
+ // Check all cells in the table
1380
+ for (let row = 0; row < map.length; row++) {
1381
+ for (let col = 0; col < map[0].length; col++) {
1382
+ const cell = map[row][col];
1383
+ if (!cell) {
1384
+ continue;
1385
+ }
1386
+ const cellEndCol = cell.startColumn + cell.cell.__colSpan - 1;
1387
+ const cellEndRow = cell.startRow + cell.cell.__rowSpan - 1;
1388
+
1389
+ // Check if this cell intersects with our current selection rectangle
1390
+ const intersectsHorizontally = cell.startColumn <= maxColumn && cellEndCol >= minColumn;
1391
+ const intersectsVertically = cell.startRow <= maxRow && cellEndRow >= minRow;
1392
+
1393
+ // If the cell intersects either horizontally or vertically
1394
+ if (intersectsHorizontally && intersectsVertically) {
1395
+ // Expand boundaries to include this cell completely
1396
+ const newMinColumn = Math.min(minColumn, cell.startColumn);
1397
+ const newMaxColumn = Math.max(maxColumn, cellEndCol);
1398
+ const newMinRow = Math.min(minRow, cell.startRow);
1399
+ const newMaxRow = Math.max(maxRow, cellEndRow);
1400
+
1401
+ // Check if boundaries changed
1402
+ if (newMinColumn !== minColumn || newMaxColumn !== maxColumn || newMinRow !== minRow || newMaxRow !== maxRow) {
1403
+ minColumn = newMinColumn;
1404
+ maxColumn = newMaxColumn;
1405
+ minRow = newMinRow;
1406
+ maxRow = newMaxRow;
1407
+ hasChanges = true;
1408
+ }
1409
+ }
1410
+ }
1411
+ }
1412
+ } while (hasChanges);
1413
+ return {
1414
+ maxColumn,
1415
+ maxRow,
1416
+ minColumn,
1417
+ minRow
1418
+ };
1419
+ }
1420
+ function $getTableCellNodeRect(tableCellNode) {
1421
+ const [cellNode,, gridNode] = $getNodeTriplet(tableCellNode);
1422
+ const rows = gridNode.getChildren();
1423
+ const rowCount = rows.length;
1424
+ const columnCount = rows[0].getChildren().length;
1425
+
1426
+ // Create a matrix of the same size as the table to track the position of each cell
1427
+ const cellMatrix = new Array(rowCount);
1428
+ for (let i = 0; i < rowCount; i++) {
1429
+ cellMatrix[i] = new Array(columnCount);
1430
+ }
1431
+ for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {
1432
+ const row = rows[rowIndex];
1433
+ const cells = row.getChildren();
1434
+ let columnIndex = 0;
1435
+ for (let cellIndex = 0; cellIndex < cells.length; cellIndex++) {
1436
+ // Find the next available position in the matrix, skip the position of merged cells
1437
+ while (cellMatrix[rowIndex][columnIndex]) {
1438
+ columnIndex++;
1439
+ }
1440
+ const cell = cells[cellIndex];
1441
+ const rowSpan = cell.__rowSpan || 1;
1442
+ const colSpan = cell.__colSpan || 1;
1443
+
1444
+ // Put the cell into the corresponding position in the matrix
1445
+ for (let i = 0; i < rowSpan; i++) {
1446
+ for (let j = 0; j < colSpan; j++) {
1447
+ cellMatrix[rowIndex + i][columnIndex + j] = cell;
1448
+ }
1449
+ }
1450
+
1451
+ // Return to the original index, row span and column span of the cell.
1452
+ if (cellNode === cell) {
1453
+ return {
1454
+ colSpan,
1455
+ columnIndex,
1456
+ rowIndex,
1457
+ rowSpan
1458
+ };
1459
+ }
1460
+ columnIndex += colSpan;
1461
+ }
1462
+ }
1463
+ return null;
1464
+ }
1465
+
1466
+ function $getCellNodes(tableSelection) {
1467
+ const [[anchorNode, anchorCell, anchorRow, anchorTable], [focusNode, focusCell, focusRow, focusTable]] = ['anchor', 'focus'].map(k => {
1468
+ const node = tableSelection[k].getNode();
1469
+ const cellNode = $findMatchingParent(node, $isTableCellNode);
1470
+ if (!$isTableCellNode(cellNode)) {
1471
+ formatDevErrorMessage(`Expected TableSelection ${k} to be (or a child of) TableCellNode, got key ${node.getKey()} of type ${node.getType()}`);
1472
+ }
1473
+ const rowNode = cellNode.getParent();
1474
+ if (!$isTableRowNode(rowNode)) {
1475
+ formatDevErrorMessage(`Expected TableSelection ${k} cell parent to be a TableRowNode`);
1476
+ }
1477
+ const tableNode = rowNode.getParent();
1478
+ if (!$isTableNode(tableNode)) {
1479
+ formatDevErrorMessage(`Expected TableSelection ${k} row parent to be a TableNode`);
1480
+ }
1481
+ return [node, cellNode, rowNode, tableNode];
1482
+ });
1483
+ // TODO: nested tables may violate this
1484
+ if (!anchorTable.is(focusTable)) {
1485
+ formatDevErrorMessage(`Expected TableSelection anchor and focus to be in the same table`);
1486
+ }
1487
+ return {
1488
+ anchorCell,
1489
+ anchorNode,
1490
+ anchorRow,
1491
+ anchorTable,
1492
+ focusCell,
1493
+ focusNode,
1494
+ focusRow,
1495
+ focusTable
1496
+ };
1497
+ }
1498
+ class TableSelection {
1499
+ tableKey;
1500
+ anchor;
1501
+ focus;
1502
+ _cachedNodes;
1503
+ dirty;
1504
+ constructor(tableKey, anchor, focus) {
1505
+ this.anchor = anchor;
1506
+ this.focus = focus;
1507
+ anchor._selection = this;
1508
+ focus._selection = this;
1509
+ this._cachedNodes = null;
1510
+ this.dirty = false;
1511
+ this.tableKey = tableKey;
1512
+ }
1513
+ getStartEndPoints() {
1514
+ return [this.anchor, this.focus];
1515
+ }
1516
+
1517
+ /**
1518
+ * {@link $createTableSelection} unfortunately makes it very easy to create
1519
+ * nonsense selections, so we have a method to see if the selection probably
1520
+ * makes sense.
1521
+ *
1522
+ * @returns true if the TableSelection is (probably) valid
1523
+ */
1524
+ isValid() {
1525
+ if (this.tableKey === 'root' || this.anchor.key === 'root' || this.anchor.type !== 'element' || this.focus.key === 'root' || this.focus.type !== 'element') {
1526
+ return false;
1527
+ }
1528
+
1529
+ // Check if the referenced nodes still exist in the editor
1530
+ const tableNode = $getNodeByKey(this.tableKey);
1531
+ const anchorNode = $getNodeByKey(this.anchor.key);
1532
+ const focusNode = $getNodeByKey(this.focus.key);
1533
+ return tableNode !== null && anchorNode !== null && focusNode !== null;
1534
+ }
1535
+
1536
+ /**
1537
+ * Returns whether the Selection is "backwards", meaning the focus
1538
+ * logically precedes the anchor in the EditorState.
1539
+ * @returns true if the Selection is backwards, false otherwise.
1540
+ */
1541
+ isBackward() {
1542
+ return this.focus.isBefore(this.anchor);
1543
+ }
1544
+ getCachedNodes() {
1545
+ return this._cachedNodes;
1546
+ }
1547
+ setCachedNodes(nodes) {
1548
+ this._cachedNodes = nodes;
1549
+ }
1550
+ is(selection) {
1551
+ return $isTableSelection(selection) && this.tableKey === selection.tableKey && this.anchor.is(selection.anchor) && this.focus.is(selection.focus);
1552
+ }
1553
+ set(tableKey, anchorCellKey, focusCellKey) {
1554
+ // note: closure compiler's acorn does not support ||=
1555
+ this.dirty = this.dirty || tableKey !== this.tableKey || anchorCellKey !== this.anchor.key || focusCellKey !== this.focus.key;
1556
+ this.tableKey = tableKey;
1557
+ this.anchor.key = anchorCellKey;
1558
+ this.focus.key = focusCellKey;
1559
+ this._cachedNodes = null;
1560
+ }
1561
+ clone() {
1562
+ return new TableSelection(this.tableKey, $createPoint(this.anchor.key, this.anchor.offset, this.anchor.type), $createPoint(this.focus.key, this.focus.offset, this.focus.type));
1563
+ }
1564
+ isCollapsed() {
1565
+ return false;
1566
+ }
1567
+ extract() {
1568
+ return this.getNodes();
1569
+ }
1570
+ insertRawText(text) {
1571
+ // Do nothing?
1572
+ }
1573
+ insertText() {
1574
+ // Do nothing?
1575
+ }
1576
+
1577
+ /**
1578
+ * Returns whether the provided TextFormatType is present on the Selection.
1579
+ * This will be true if any paragraph in table cells has the specified format.
1580
+ *
1581
+ * @param type the TextFormatType to check for.
1582
+ * @returns true if the provided format is currently toggled on on the Selection, false otherwise.
1583
+ */
1584
+ hasFormat(type) {
1585
+ let format = 0;
1586
+ const cellNodes = this.getNodes().filter($isTableCellNode);
1587
+ cellNodes.forEach(cellNode => {
1588
+ const paragraph = cellNode.getFirstChild();
1589
+ if ($isParagraphNode(paragraph)) {
1590
+ format |= paragraph.getTextFormat();
1591
+ }
1592
+ });
1593
+ const formatFlag = TEXT_TYPE_TO_FORMAT[type];
1594
+ return (format & formatFlag) !== 0;
1595
+ }
1596
+ insertNodes(nodes) {
1597
+ const focusNode = this.focus.getNode();
1598
+ if (!$isElementNode(focusNode)) {
1599
+ formatDevErrorMessage(`Expected TableSelection focus to be an ElementNode`);
1600
+ }
1601
+ const selection = $normalizeSelection__EXPERIMENTAL(focusNode.select(0, focusNode.getChildrenSize()));
1602
+ selection.insertNodes(nodes);
1603
+ }
1604
+
1605
+ // TODO Deprecate this method. It's confusing when used with colspan|rowspan
1606
+ getShape() {
1607
+ const {
1608
+ anchorCell,
1609
+ focusCell
1610
+ } = $getCellNodes(this);
1611
+ const anchorCellNodeRect = $getTableCellNodeRect(anchorCell);
1612
+ if (!(anchorCellNodeRect !== null)) {
1613
+ formatDevErrorMessage(`getCellRect: expected to find AnchorNode`);
1614
+ }
1615
+ const focusCellNodeRect = $getTableCellNodeRect(focusCell);
1616
+ if (!(focusCellNodeRect !== null)) {
1617
+ formatDevErrorMessage(`getCellRect: expected to find focusCellNode`);
1618
+ }
1619
+ const startX = Math.min(anchorCellNodeRect.columnIndex, focusCellNodeRect.columnIndex);
1620
+ const stopX = Math.max(anchorCellNodeRect.columnIndex + anchorCellNodeRect.colSpan - 1, focusCellNodeRect.columnIndex + focusCellNodeRect.colSpan - 1);
1621
+ const startY = Math.min(anchorCellNodeRect.rowIndex, focusCellNodeRect.rowIndex);
1622
+ const stopY = Math.max(anchorCellNodeRect.rowIndex + anchorCellNodeRect.rowSpan - 1, focusCellNodeRect.rowIndex + focusCellNodeRect.rowSpan - 1);
1623
+ return {
1624
+ fromX: Math.min(startX, stopX),
1625
+ fromY: Math.min(startY, stopY),
1626
+ toX: Math.max(startX, stopX),
1627
+ toY: Math.max(startY, stopY)
1628
+ };
1629
+ }
1630
+ getNodes() {
1631
+ if (!this.isValid()) {
1632
+ return [];
1633
+ }
1634
+ const cachedNodes = this._cachedNodes;
1635
+ if (cachedNodes !== null) {
1636
+ return cachedNodes;
1637
+ }
1638
+ const {
1639
+ anchorTable: tableNode,
1640
+ anchorCell,
1641
+ focusCell
1642
+ } = $getCellNodes(this);
1643
+ const focusCellGrid = focusCell.getParents()[1];
1644
+ if (focusCellGrid !== tableNode) {
1645
+ if (!tableNode.isParentOf(focusCell)) {
1646
+ // focus is on higher Grid level than anchor
1647
+ const gridParent = tableNode.getParent();
1648
+ if (!(gridParent != null)) {
1649
+ formatDevErrorMessage(`Expected gridParent to have a parent`);
1650
+ }
1651
+ this.set(this.tableKey, gridParent.getKey(), focusCell.getKey());
1652
+ } else {
1653
+ // anchor is on higher Grid level than focus
1654
+ const focusCellParent = focusCellGrid.getParent();
1655
+ if (!(focusCellParent != null)) {
1656
+ formatDevErrorMessage(`Expected focusCellParent to have a parent`);
1657
+ }
1658
+ this.set(this.tableKey, focusCell.getKey(), focusCellParent.getKey());
1659
+ }
1660
+ return this.getNodes();
1661
+ }
1662
+
1663
+ // TODO Mapping the whole Grid every time not efficient. We need to compute the entire state only
1664
+ // once (on load) and iterate on it as updates occur. However, to do this we need to have the
1665
+ // ability to store a state. Killing TableSelection and moving the logic to the plugin would make
1666
+ // this possible.
1667
+ const [map, cellAMap, cellBMap] = $computeTableMap(tableNode, anchorCell, focusCell);
1668
+ const {
1669
+ minColumn,
1670
+ maxColumn,
1671
+ minRow,
1672
+ maxRow
1673
+ } = $computeTableCellRectBoundary(map, cellAMap, cellBMap);
1674
+
1675
+ // We use a Map here because merged cells in the grid would otherwise
1676
+ // show up multiple times in the nodes array
1677
+ const nodeMap = new Map([[tableNode.getKey(), tableNode]]);
1678
+ let lastRow = null;
1679
+ for (let i = minRow; i <= maxRow; i++) {
1680
+ for (let j = minColumn; j <= maxColumn; j++) {
1681
+ const {
1682
+ cell
1683
+ } = map[i][j];
1684
+ const currentRow = cell.getParent();
1685
+ if (!$isTableRowNode(currentRow)) {
1686
+ formatDevErrorMessage(`Expected TableCellNode parent to be a TableRowNode`);
1687
+ }
1688
+ if (currentRow !== lastRow) {
1689
+ nodeMap.set(currentRow.getKey(), currentRow);
1690
+ lastRow = currentRow;
1691
+ }
1692
+ if (!nodeMap.has(cell.getKey())) {
1693
+ $visitRecursively(cell, childNode => {
1694
+ nodeMap.set(childNode.getKey(), childNode);
1695
+ });
1696
+ }
1697
+ }
1698
+ }
1699
+ const nodes = Array.from(nodeMap.values());
1700
+ if (!isCurrentlyReadOnlyMode()) {
1701
+ this._cachedNodes = nodes;
1702
+ }
1703
+ return nodes;
1704
+ }
1705
+ getTextContent() {
1706
+ const nodes = this.getNodes().filter(node => $isTableCellNode(node));
1707
+ let textContent = '';
1708
+ for (let i = 0; i < nodes.length; i++) {
1709
+ const node = nodes[i];
1710
+ const row = node.__parent;
1711
+ const nextRow = (nodes[i + 1] || {}).__parent;
1712
+ textContent += node.getTextContent() + (nextRow !== row ? '\n' : '\t');
1713
+ }
1714
+ return textContent;
1715
+ }
1716
+ }
1717
+ function $isTableSelection(x) {
1718
+ return x instanceof TableSelection;
1719
+ }
1720
+ function $createTableSelection() {
1721
+ // TODO this is a suboptimal design, it doesn't make sense to have
1722
+ // a table selection that isn't associated with a table. This
1723
+ // constructor should have required arguments and in true we
1724
+ // should check that they point to a table and are element points to
1725
+ // cell nodes of that table.
1726
+ const anchor = $createPoint('root', 0, 'element');
1727
+ const focus = $createPoint('root', 0, 'element');
1728
+ return new TableSelection('root', anchor, focus);
1729
+ }
1730
+ function $createTableSelectionFrom(tableNode, anchorCell, focusCell) {
1731
+ const tableNodeKey = tableNode.getKey();
1732
+ const anchorCellKey = anchorCell.getKey();
1733
+ const focusCellKey = focusCell.getKey();
1734
+ {
1735
+ if (!tableNode.isAttached()) {
1736
+ formatDevErrorMessage(`$createTableSelectionFrom: tableNode ${tableNodeKey} is not attached`);
1737
+ }
1738
+ if (!tableNode.is($findTableNode(anchorCell))) {
1739
+ formatDevErrorMessage(`$createTableSelectionFrom: anchorCell ${anchorCellKey} is not in table ${tableNodeKey}`);
1740
+ }
1741
+ if (!tableNode.is($findTableNode(focusCell))) {
1742
+ formatDevErrorMessage(`$createTableSelectionFrom: focusCell ${focusCellKey} is not in table ${tableNodeKey}`);
1743
+ } // TODO: Check for rectangular grid
1744
+ }
1745
+ const prevSelection = $getSelection();
1746
+ const nextSelection = $isTableSelection(prevSelection) ? prevSelection.clone() : $createTableSelection();
1747
+ nextSelection.set(tableNode.getKey(), anchorCell.getKey(), focusCell.getKey());
1748
+ return nextSelection;
1749
+ }
1750
+
1751
+ /**
1752
+ * Depth first visitor
1753
+ * @param node The starting node
1754
+ * @param $visit The function to call for each node. If the function returns false, then children of this node will not be explored
1755
+ */
1756
+ function $visitRecursively(node, $visit) {
1757
+ const stack = [[node]];
1758
+ for (let currentArray = stack.at(-1); currentArray !== undefined && stack.length > 0; currentArray = stack.at(-1)) {
1759
+ const currentNode = currentArray.pop();
1760
+ if (currentNode === undefined) {
1761
+ stack.pop();
1762
+ } else if ($visit(currentNode) !== false && $isElementNode(currentNode)) {
1763
+ stack.push(currentNode.getChildren());
1764
+ }
1765
+ }
1766
+ }
1767
+
1768
+ function $getTableAndElementByKey(tableNodeKey, editor = $getEditor()) {
1769
+ const tableNode = $getNodeByKey(tableNodeKey);
1770
+ if (!$isTableNode(tableNode)) {
1771
+ formatDevErrorMessage(`TableObserver: Expected tableNodeKey ${tableNodeKey} to be a TableNode`);
1772
+ }
1773
+ const tableElement = getTableElement(tableNode, editor.getElementByKey(tableNodeKey));
1774
+ if (!(tableElement !== null)) {
1775
+ formatDevErrorMessage(`TableObserver: Expected to find TableElement in DOM for key ${tableNodeKey}`);
1776
+ }
1777
+ return {
1778
+ tableElement,
1779
+ tableNode
1780
+ };
1781
+ }
1782
+ class TableObserver {
1783
+ focusX;
1784
+ focusY;
1785
+ listenersToRemove;
1786
+ table;
1787
+ isHighlightingCells;
1788
+ anchorX;
1789
+ anchorY;
1790
+ tableNodeKey;
1791
+ anchorCell;
1792
+ focusCell;
1793
+ anchorCellNodeKey;
1794
+ focusCellNodeKey;
1795
+ editor;
1796
+ tableSelection;
1797
+ hasHijackedSelectionStyles;
1798
+ isSelecting;
1799
+ pointerType;
1800
+ shouldCheckSelection;
1801
+ abortController;
1802
+ listenerOptions;
1803
+ nextFocus;
1804
+ constructor(editor, tableNodeKey) {
1805
+ this.isHighlightingCells = false;
1806
+ this.anchorX = -1;
1807
+ this.anchorY = -1;
1808
+ this.focusX = -1;
1809
+ this.focusY = -1;
1810
+ this.listenersToRemove = new Set();
1811
+ this.tableNodeKey = tableNodeKey;
1812
+ this.editor = editor;
1813
+ this.table = {
1814
+ columns: 0,
1815
+ domRows: [],
1816
+ rows: 0
1817
+ };
1818
+ this.tableSelection = null;
1819
+ this.anchorCellNodeKey = null;
1820
+ this.focusCellNodeKey = null;
1821
+ this.anchorCell = null;
1822
+ this.focusCell = null;
1823
+ this.hasHijackedSelectionStyles = false;
1824
+ this.isSelecting = false;
1825
+ this.pointerType = null;
1826
+ this.shouldCheckSelection = false;
1827
+ this.abortController = new AbortController();
1828
+ this.listenerOptions = {
1829
+ signal: this.abortController.signal
1830
+ };
1831
+ this.nextFocus = null;
1832
+ this.trackTable();
1833
+ }
1834
+ getTable() {
1835
+ return this.table;
1836
+ }
1837
+ removeListeners() {
1838
+ this.abortController.abort('removeListeners');
1839
+ Array.from(this.listenersToRemove).forEach(removeListener => removeListener());
1840
+ this.listenersToRemove.clear();
1841
+ }
1842
+ $lookup() {
1843
+ return $getTableAndElementByKey(this.tableNodeKey, this.editor);
1844
+ }
1845
+ trackTable() {
1846
+ const observer = new MutationObserver(records => {
1847
+ this.editor.getEditorState().read(() => {
1848
+ let gridNeedsRedraw = false;
1849
+ for (let i = 0; i < records.length; i++) {
1850
+ const record = records[i];
1851
+ const target = record.target;
1852
+ const nodeName = target.nodeName;
1853
+ if (nodeName === 'TABLE' || nodeName === 'TBODY' || nodeName === 'THEAD' || nodeName === 'TR') {
1854
+ gridNeedsRedraw = true;
1855
+ break;
1856
+ }
1857
+ }
1858
+ if (!gridNeedsRedraw) {
1859
+ return;
1860
+ }
1861
+ const {
1862
+ tableNode,
1863
+ tableElement
1864
+ } = this.$lookup();
1865
+ this.table = getTable(tableNode, tableElement);
1866
+ }, {
1867
+ editor: this.editor
1868
+ });
1869
+ });
1870
+ this.editor.getEditorState().read(() => {
1871
+ const {
1872
+ tableNode,
1873
+ tableElement
1874
+ } = this.$lookup();
1875
+ this.table = getTable(tableNode, tableElement);
1876
+ observer.observe(tableElement, {
1877
+ attributes: true,
1878
+ childList: true,
1879
+ subtree: true
1880
+ });
1881
+ }, {
1882
+ editor: this.editor
1883
+ });
1884
+ }
1885
+ $clearHighlight() {
1886
+ const editor = this.editor;
1887
+ this.isHighlightingCells = false;
1888
+ this.anchorX = -1;
1889
+ this.anchorY = -1;
1890
+ this.focusX = -1;
1891
+ this.focusY = -1;
1892
+ this.tableSelection = null;
1893
+ this.anchorCellNodeKey = null;
1894
+ this.focusCellNodeKey = null;
1895
+ this.anchorCell = null;
1896
+ this.focusCell = null;
1897
+ this.hasHijackedSelectionStyles = false;
1898
+ this.$enableHighlightStyle();
1899
+ const {
1900
+ tableNode,
1901
+ tableElement
1902
+ } = this.$lookup();
1903
+ const grid = getTable(tableNode, tableElement);
1904
+ $updateDOMForSelection(editor, grid, null);
1905
+ if ($getSelection() !== null) {
1906
+ $setSelection(null);
1907
+ editor.dispatchCommand(SELECTION_CHANGE_COMMAND, undefined);
1908
+ }
1909
+ }
1910
+ $enableHighlightStyle() {
1911
+ const editor = this.editor;
1912
+ const {
1913
+ tableElement
1914
+ } = this.$lookup();
1915
+ removeClassNamesFromElement(tableElement, editor._config.theme.tableSelection);
1916
+ tableElement.classList.remove('disable-selection');
1917
+ this.hasHijackedSelectionStyles = false;
1918
+ }
1919
+ $disableHighlightStyle() {
1920
+ const {
1921
+ tableElement
1922
+ } = this.$lookup();
1923
+ addClassNamesToElement(tableElement, this.editor._config.theme.tableSelection);
1924
+ this.hasHijackedSelectionStyles = true;
1925
+ }
1926
+ $updateTableTableSelection(selection) {
1927
+ if (selection !== null) {
1928
+ if (!(selection.tableKey === this.tableNodeKey)) {
1929
+ formatDevErrorMessage(`TableObserver.$updateTableTableSelection: selection.tableKey !== this.tableNodeKey ('${selection.tableKey}' !== '${this.tableNodeKey}')`);
1930
+ }
1931
+ const editor = this.editor;
1932
+ this.tableSelection = selection;
1933
+ this.isHighlightingCells = true;
1934
+ this.$disableHighlightStyle();
1935
+ this.updateDOMSelection();
1936
+ $updateDOMForSelection(editor, this.table, this.tableSelection);
1937
+ } else {
1938
+ this.$clearHighlight();
1939
+ }
1940
+ }
1941
+
1942
+ /**
1943
+ * @internal
1944
+ * Firefox has a strange behavior where pressing the down arrow key from
1945
+ * above the table will move the caret after the table and then lexical
1946
+ * will select the last cell instead of the first.
1947
+ * We do still want to let the browser handle caret movement but we will
1948
+ * use this property to "tag" the update so that we can recheck the
1949
+ * selection after the event is processed.
1950
+ */
1951
+ setShouldCheckSelection() {
1952
+ this.shouldCheckSelection = true;
1953
+ }
1954
+ /**
1955
+ * @internal
1956
+ */
1957
+ getAndClearShouldCheckSelection() {
1958
+ if (this.shouldCheckSelection) {
1959
+ this.shouldCheckSelection = false;
1960
+ return true;
1961
+ }
1962
+ return false;
1963
+ }
1964
+
1965
+ /**
1966
+ * @internal
1967
+ * When handling mousemove events we track what the focus cell should be, but
1968
+ * the DOM selection may end up somewhere else entirely. We don't have an elegant
1969
+ * way to handle this after the DOM selection has been resolved in a
1970
+ * SELECTION_CHANGE_COMMAND callback.
1971
+ */
1972
+ setNextFocus(nextFocus) {
1973
+ this.nextFocus = nextFocus;
1974
+ }
1975
+
1976
+ /** @internal */
1977
+ getAndClearNextFocus() {
1978
+ const {
1979
+ nextFocus
1980
+ } = this;
1981
+ if (nextFocus !== null) {
1982
+ this.nextFocus = null;
1983
+ }
1984
+ return nextFocus;
1985
+ }
1986
+
1987
+ /** @internal */
1988
+ updateDOMSelection() {
1989
+ if (this.anchorCell !== null && this.focusCell !== null) {
1990
+ const domSelection = getDOMSelectionForEditor(this.editor);
1991
+ // We are not using a native selection for tables, and if we
1992
+ // set one then the reconciler will undo it.
1993
+ // TODO - it would make sense to have one so that native
1994
+ // copy/paste worked. Right now we have to emulate with
1995
+ // keyboard events but it won't fire if triggered from the menu
1996
+ if (domSelection && domSelection.rangeCount > 0) {
1997
+ domSelection.removeAllRanges();
1998
+ }
1999
+ }
2000
+ }
2001
+ $setFocusCellForSelection(cell, ignoreStart = false) {
2002
+ const editor = this.editor;
2003
+ const {
2004
+ tableNode
2005
+ } = this.$lookup();
2006
+ const cellX = cell.x;
2007
+ const cellY = cell.y;
2008
+ this.focusCell = cell;
2009
+ if (!this.isHighlightingCells && (this.anchorX !== cellX || this.anchorY !== cellY || ignoreStart)) {
2010
+ this.isHighlightingCells = true;
2011
+ this.$disableHighlightStyle();
2012
+ } else if (cellX === this.focusX && cellY === this.focusY) {
2013
+ return false;
2014
+ }
2015
+ this.focusX = cellX;
2016
+ this.focusY = cellY;
2017
+ if (this.isHighlightingCells) {
2018
+ const focusTableCellNode = $getNearestTableCellInTableFromDOMNode(tableNode, cell.elem);
2019
+ if (this.tableSelection != null && this.anchorCellNodeKey != null && focusTableCellNode !== null) {
2020
+ this.focusCellNodeKey = focusTableCellNode.getKey();
2021
+ this.tableSelection = $createTableSelectionFrom(tableNode, this.$getAnchorTableCellOrThrow(), focusTableCellNode);
2022
+ $setSelection(this.tableSelection);
2023
+ editor.dispatchCommand(SELECTION_CHANGE_COMMAND, undefined);
2024
+ $updateDOMForSelection(editor, this.table, this.tableSelection);
2025
+ return true;
2026
+ }
2027
+ }
2028
+ return false;
2029
+ }
2030
+ $getAnchorTableCell() {
2031
+ return this.anchorCellNodeKey ? $getNodeByKey(this.anchorCellNodeKey) : null;
2032
+ }
2033
+ $getAnchorTableCellOrThrow() {
2034
+ const anchorTableCell = this.$getAnchorTableCell();
2035
+ if (!(anchorTableCell !== null)) {
2036
+ formatDevErrorMessage(`TableObserver anchorTableCell is null`);
2037
+ }
2038
+ return anchorTableCell;
2039
+ }
2040
+ $getFocusTableCell() {
2041
+ return this.focusCellNodeKey ? $getNodeByKey(this.focusCellNodeKey) : null;
2042
+ }
2043
+ $getFocusTableCellOrThrow() {
2044
+ const focusTableCell = this.$getFocusTableCell();
2045
+ if (!(focusTableCell !== null)) {
2046
+ formatDevErrorMessage(`TableObserver focusTableCell is null`);
2047
+ }
2048
+ return focusTableCell;
2049
+ }
2050
+ $setAnchorCellForSelection(cell) {
2051
+ this.isHighlightingCells = false;
2052
+ this.anchorCell = cell;
2053
+ this.anchorX = cell.x;
2054
+ this.anchorY = cell.y;
2055
+ const {
2056
+ tableNode
2057
+ } = this.$lookup();
2058
+ const anchorTableCellNode = $getNearestTableCellInTableFromDOMNode(tableNode, cell.elem);
2059
+ if (anchorTableCellNode !== null) {
2060
+ const anchorNodeKey = anchorTableCellNode.getKey();
2061
+ this.tableSelection = this.tableSelection != null ? this.tableSelection.clone() : $createTableSelection();
2062
+ this.anchorCellNodeKey = anchorNodeKey;
2063
+ }
2064
+ }
2065
+ $formatCells(type) {
2066
+ const selection = $getSelection();
2067
+ if (!$isTableSelection(selection)) {
2068
+ formatDevErrorMessage(`Expected Table selection`);
2069
+ }
2070
+ const formatSelection = $createRangeSelection();
2071
+ const anchor = formatSelection.anchor;
2072
+ const focus = formatSelection.focus;
2073
+ const cellNodes = selection.getNodes().filter($isTableCellNode);
2074
+ if (!(cellNodes.length > 0)) {
2075
+ formatDevErrorMessage(`No table cells present`);
2076
+ }
2077
+ const paragraph = cellNodes[0].getFirstChild();
2078
+ const alignFormatWith = $isParagraphNode(paragraph) ? paragraph.getFormatFlags(type, null) : null;
2079
+ cellNodes.forEach(cellNode => {
2080
+ anchor.set(cellNode.getKey(), 0, 'element');
2081
+ focus.set(cellNode.getKey(), cellNode.getChildrenSize(), 'element');
2082
+ formatSelection.formatText(type, alignFormatWith);
2083
+ });
2084
+ $setSelection(selection);
2085
+ this.editor.dispatchCommand(SELECTION_CHANGE_COMMAND, undefined);
2086
+ }
2087
+ $clearText() {
2088
+ const {
2089
+ editor
2090
+ } = this;
2091
+ const tableNode = $getNodeByKey(this.tableNodeKey);
2092
+ if (!$isTableNode(tableNode)) {
2093
+ throw new Error('Expected TableNode.');
2094
+ }
2095
+ const selection = $getSelection();
2096
+ if (!$isTableSelection(selection)) {
2097
+ formatDevErrorMessage(`Expected TableSelection`);
2098
+ }
2099
+ const selectedNodes = selection.getNodes().filter($isTableCellNode);
2100
+
2101
+ // Check if the entire table is selected by verifying first and last cells
2102
+ const firstRow = tableNode.getFirstChild();
2103
+ const lastRow = tableNode.getLastChild();
2104
+ const isEntireTableSelected = selectedNodes.length > 0 && firstRow !== null && lastRow !== null && $isTableRowNode(firstRow) && $isTableRowNode(lastRow) && selectedNodes[0] === firstRow.getFirstChild() && selectedNodes[selectedNodes.length - 1] === lastRow.getLastChild();
2105
+ if (isEntireTableSelected) {
2106
+ tableNode.selectPrevious();
2107
+ const parent = tableNode.getParent();
2108
+ // Delete entire table
2109
+ tableNode.remove();
2110
+ // Handle case when table was the only node
2111
+ if ($isRootNode(parent) && parent.isEmpty()) {
2112
+ editor.dispatchCommand(INSERT_PARAGRAPH_COMMAND, undefined);
2113
+ }
2114
+ return;
2115
+ }
2116
+ selectedNodes.forEach(cellNode => {
2117
+ if ($isElementNode(cellNode)) {
2118
+ const paragraphNode = $createParagraphNode();
2119
+ const textNode = $createTextNode();
2120
+ paragraphNode.append(textNode);
2121
+ cellNode.append(paragraphNode);
2122
+ cellNode.getChildren().forEach(child => {
2123
+ if (child !== paragraphNode) {
2124
+ child.remove();
2125
+ }
2126
+ });
2127
+ }
2128
+ });
2129
+ $updateDOMForSelection(editor, this.table, null);
2130
+ $setSelection(null);
2131
+ editor.dispatchCommand(SELECTION_CHANGE_COMMAND, undefined);
2132
+ }
2133
+ }
2134
+
2135
+ const LEXICAL_ELEMENT_KEY = '__lexicalTableSelection';
2136
+ const isPointerDownOnEvent = event => {
2137
+ return (event.buttons & 1) === 1;
2138
+ };
2139
+ function isHTMLTableElement(el) {
2140
+ return isHTMLElement(el) && el.nodeName === 'TABLE';
2141
+ }
2142
+ function getTableElement(tableNode, dom) {
2143
+ if (!dom) {
2144
+ return dom;
2145
+ }
2146
+ const element = isHTMLTableElement(dom) ? dom : tableNode.getDOMSlot(dom).element;
2147
+ if (!(element.nodeName === 'TABLE')) {
2148
+ formatDevErrorMessage(`getTableElement: Expecting table in as DOM node for TableNode, not ${dom.nodeName}`);
2149
+ }
2150
+ return element;
2151
+ }
2152
+ function getEditorWindow(editor) {
2153
+ return editor._window;
2154
+ }
2155
+ function $findParentTableCellNodeInTable(tableNode, node) {
2156
+ for (let currentNode = node, lastTableCellNode = null; currentNode !== null; currentNode = currentNode.getParent()) {
2157
+ if (tableNode.is(currentNode)) {
2158
+ return lastTableCellNode;
2159
+ } else if ($isTableCellNode(currentNode)) {
2160
+ lastTableCellNode = currentNode;
2161
+ }
2162
+ }
2163
+ return null;
2164
+ }
2165
+ const ARROW_KEY_COMMANDS_WITH_DIRECTION = [[KEY_ARROW_DOWN_COMMAND, 'down'], [KEY_ARROW_UP_COMMAND, 'up'], [KEY_ARROW_LEFT_COMMAND, 'backward'], [KEY_ARROW_RIGHT_COMMAND, 'forward']];
2166
+ const DELETE_TEXT_COMMANDS = [DELETE_WORD_COMMAND, DELETE_LINE_COMMAND, DELETE_CHARACTER_COMMAND];
2167
+ const DELETE_KEY_COMMANDS = [KEY_BACKSPACE_COMMAND, KEY_DELETE_COMMAND];
2168
+ function applyTableHandlers(tableNode, element, editor, hasTabHandler) {
2169
+ const rootElement = editor.getRootElement();
2170
+ const editorWindow = getEditorWindow(editor);
2171
+ if (!(rootElement !== null && editorWindow !== null)) {
2172
+ formatDevErrorMessage(`applyTableHandlers: editor has no root element set`);
2173
+ }
2174
+ const tableObserver = new TableObserver(editor, tableNode.getKey());
2175
+ const tableElement = getTableElement(tableNode, element);
2176
+ attachTableObserverToTableElement(tableElement, tableObserver);
2177
+ tableObserver.listenersToRemove.add(() => detachTableObserverFromTableElement(tableElement, tableObserver));
2178
+ const createPointerHandlers = () => {
2179
+ if (tableObserver.isSelecting) {
2180
+ return;
2181
+ }
2182
+ const onPointerUp = () => {
2183
+ tableObserver.isSelecting = false;
2184
+ editorWindow.removeEventListener('pointerup', onPointerUp);
2185
+ editorWindow.removeEventListener('pointermove', onPointerMove);
2186
+ };
2187
+ const onPointerMove = moveEvent => {
2188
+ if (!isPointerDownOnEvent(moveEvent) && tableObserver.isSelecting) {
2189
+ tableObserver.isSelecting = false;
2190
+ editorWindow.removeEventListener('pointerup', onPointerUp);
2191
+ editorWindow.removeEventListener('pointermove', onPointerMove);
2192
+ return;
2193
+ }
2194
+ if (!isDOMNode(moveEvent.target)) {
2195
+ return;
2196
+ }
2197
+ let focusCell = null;
2198
+ // In firefox the moveEvent.target may be captured so we must always
2199
+ // consult the coordinates #7245
2200
+ const override = !(IS_FIREFOX || tableElement.contains(moveEvent.target));
2201
+ if (override) {
2202
+ focusCell = getDOMCellInTableFromTarget(tableElement, moveEvent.target);
2203
+ } else {
2204
+ for (const el of document.elementsFromPoint(moveEvent.clientX, moveEvent.clientY)) {
2205
+ focusCell = getDOMCellInTableFromTarget(tableElement, el);
2206
+ if (focusCell) {
2207
+ break;
2208
+ }
2209
+ }
2210
+ }
2211
+ if (focusCell && (tableObserver.focusCell === null || focusCell.elem !== tableObserver.focusCell.elem)) {
2212
+ tableObserver.setNextFocus({
2213
+ focusCell,
2214
+ override
2215
+ });
2216
+ editor.dispatchCommand(SELECTION_CHANGE_COMMAND, undefined);
2217
+ }
2218
+ };
2219
+ tableObserver.isSelecting = true;
2220
+ editorWindow.addEventListener('pointerup', onPointerUp, tableObserver.listenerOptions);
2221
+ editorWindow.addEventListener('pointermove', onPointerMove, tableObserver.listenerOptions);
2222
+ };
2223
+ const onPointerDown = event => {
2224
+ tableObserver.pointerType = event.pointerType;
2225
+ if (event.button !== 0 || !isDOMNode(event.target) || !editorWindow) {
2226
+ return;
2227
+ }
2228
+ const targetCell = getDOMCellFromTarget(event.target);
2229
+ if (targetCell !== null) {
2230
+ editor.update(() => {
2231
+ const prevSelection = $getPreviousSelection();
2232
+ // We can't trust Firefox to do the right thing with the selection and
2233
+ // we don't have a proper state machine to do this "correctly" but
2234
+ // if we go ahead and make the table selection now it will work
2235
+ if (IS_FIREFOX && event.shiftKey && $isSelectionInTable(prevSelection, tableNode) && ($isRangeSelection(prevSelection) || $isTableSelection(prevSelection))) {
2236
+ const prevAnchorNode = prevSelection.anchor.getNode();
2237
+ const prevAnchorCell = $findParentTableCellNodeInTable(tableNode, prevSelection.anchor.getNode());
2238
+ if (prevAnchorCell) {
2239
+ tableObserver.$setAnchorCellForSelection($getObserverCellFromCellNodeOrThrow(tableObserver, prevAnchorCell));
2240
+ tableObserver.$setFocusCellForSelection(targetCell);
2241
+ stopEvent(event);
2242
+ } else {
2243
+ const newSelection = tableNode.isBefore(prevAnchorNode) ? tableNode.selectStart() : tableNode.selectEnd();
2244
+ newSelection.anchor.set(prevSelection.anchor.key, prevSelection.anchor.offset, prevSelection.anchor.type);
2245
+ }
2246
+ } else {
2247
+ // Only set anchor cell for selection if this is not a simple touch tap
2248
+ // Touch taps should not initiate table selection mode
2249
+ if (event.pointerType !== 'touch') {
2250
+ tableObserver.$setAnchorCellForSelection(targetCell);
2251
+ }
2252
+ }
2253
+ });
2254
+ }
2255
+ createPointerHandlers();
2256
+ };
2257
+ tableElement.addEventListener('pointerdown', onPointerDown, tableObserver.listenerOptions);
2258
+ tableObserver.listenersToRemove.add(() => {
2259
+ tableElement.removeEventListener('pointerdown', onPointerDown);
2260
+ });
2261
+ const onTripleClick = event => {
2262
+ if (event.detail >= 3 && isDOMNode(event.target)) {
2263
+ const targetCell = getDOMCellFromTarget(event.target);
2264
+ if (targetCell !== null) {
2265
+ event.preventDefault();
2266
+ }
2267
+ }
2268
+ };
2269
+ tableElement.addEventListener('mousedown', onTripleClick, tableObserver.listenerOptions);
2270
+ tableObserver.listenersToRemove.add(() => {
2271
+ tableElement.removeEventListener('mousedown', onTripleClick);
2272
+ });
2273
+
2274
+ // Clear selection when clicking outside of dom.
2275
+ const pointerDownCallback = event => {
2276
+ const target = event.target;
2277
+ if (event.button !== 0 || !isDOMNode(target)) {
2278
+ return;
2279
+ }
2280
+ editor.update(() => {
2281
+ const selection = $getSelection();
2282
+ if ($isTableSelection(selection) && selection.tableKey === tableObserver.tableNodeKey && rootElement.contains(target)) {
2283
+ tableObserver.$clearHighlight();
2284
+ }
2285
+ });
2286
+ };
2287
+ editorWindow.addEventListener('pointerdown', pointerDownCallback, tableObserver.listenerOptions);
2288
+ tableObserver.listenersToRemove.add(() => {
2289
+ editorWindow.removeEventListener('pointerdown', pointerDownCallback);
2290
+ });
2291
+ for (const [command, direction] of ARROW_KEY_COMMANDS_WITH_DIRECTION) {
2292
+ tableObserver.listenersToRemove.add(editor.registerCommand(command, event => $handleArrowKey(editor, event, direction, tableNode, tableObserver), COMMAND_PRIORITY_HIGH));
2293
+ }
2294
+ tableObserver.listenersToRemove.add(editor.registerCommand(KEY_ESCAPE_COMMAND, event => {
2295
+ const selection = $getSelection();
2296
+ if ($isTableSelection(selection)) {
2297
+ const focusCellNode = $findParentTableCellNodeInTable(tableNode, selection.focus.getNode());
2298
+ if (focusCellNode !== null) {
2299
+ stopEvent(event);
2300
+ focusCellNode.selectEnd();
2301
+ return true;
2302
+ }
2303
+ }
2304
+ return false;
2305
+ }, COMMAND_PRIORITY_HIGH));
2306
+ const deleteTextHandler = command => () => {
2307
+ const selection = $getSelection();
2308
+ if (!$isSelectionInTable(selection, tableNode)) {
2309
+ return false;
2310
+ }
2311
+ if ($isTableSelection(selection)) {
2312
+ tableObserver.$clearText();
2313
+ return true;
2314
+ } else if ($isRangeSelection(selection)) {
2315
+ const tableCellNode = $findParentTableCellNodeInTable(tableNode, selection.anchor.getNode());
2316
+ if (!$isTableCellNode(tableCellNode)) {
2317
+ return false;
2318
+ }
2319
+ const anchorNode = selection.anchor.getNode();
2320
+ const focusNode = selection.focus.getNode();
2321
+ const isAnchorInside = tableNode.isParentOf(anchorNode);
2322
+ const isFocusInside = tableNode.isParentOf(focusNode);
2323
+ const selectionContainsPartialTable = isAnchorInside && !isFocusInside || isFocusInside && !isAnchorInside;
2324
+ if (selectionContainsPartialTable) {
2325
+ tableObserver.$clearText();
2326
+ return true;
2327
+ }
2328
+ const nearestElementNode = $findMatchingParent(selection.anchor.getNode(), n => $isElementNode(n));
2329
+ const topLevelCellElementNode = nearestElementNode && $findMatchingParent(nearestElementNode, n => $isElementNode(n) && $isTableCellNode(n.getParent()));
2330
+ if (!$isElementNode(topLevelCellElementNode) || !$isElementNode(nearestElementNode)) {
2331
+ return false;
2332
+ }
2333
+ if (command === DELETE_LINE_COMMAND && topLevelCellElementNode.getPreviousSibling() === null) {
2334
+ // TODO: Fix Delete Line in Table Cells.
2335
+ return true;
2336
+ }
2337
+ }
2338
+ return false;
2339
+ };
2340
+ for (const command of DELETE_TEXT_COMMANDS) {
2341
+ tableObserver.listenersToRemove.add(editor.registerCommand(command, deleteTextHandler(command), COMMAND_PRIORITY_HIGH));
2342
+ }
2343
+ const $deleteCellHandler = event => {
2344
+ const selection = $getSelection();
2345
+ if (!($isTableSelection(selection) || $isRangeSelection(selection))) {
2346
+ return false;
2347
+ }
2348
+
2349
+ // If the selection is inside the table but should remove the whole table
2350
+ // we expand the selection so that both the anchor and focus are outside
2351
+ // the table and the editor's command listener will handle the delete
2352
+ const isAnchorInside = tableNode.isParentOf(selection.anchor.getNode());
2353
+ const isFocusInside = tableNode.isParentOf(selection.focus.getNode());
2354
+ if (isAnchorInside !== isFocusInside) {
2355
+ const tablePoint = isAnchorInside ? 'anchor' : 'focus';
2356
+ const outerPoint = isAnchorInside ? 'focus' : 'anchor';
2357
+ // Preserve the outer point
2358
+ const {
2359
+ key,
2360
+ offset,
2361
+ type
2362
+ } = selection[outerPoint];
2363
+ // Expand the selection around the table
2364
+ const newSelection = tableNode[selection[tablePoint].isBefore(selection[outerPoint]) ? 'selectPrevious' : 'selectNext']();
2365
+ // Restore the outer point of the selection
2366
+ newSelection[outerPoint].set(key, offset, type);
2367
+ // Let the base implementation handle the rest
2368
+ return false;
2369
+ }
2370
+ if (!$isSelectionInTable(selection, tableNode)) {
2371
+ return false;
2372
+ }
2373
+ if ($isTableSelection(selection)) {
2374
+ if (event) {
2375
+ event.preventDefault();
2376
+ event.stopPropagation();
2377
+ }
2378
+ tableObserver.$clearText();
2379
+ return true;
2380
+ }
2381
+ return false;
2382
+ };
2383
+ for (const command of DELETE_KEY_COMMANDS) {
2384
+ tableObserver.listenersToRemove.add(editor.registerCommand(command, $deleteCellHandler, COMMAND_PRIORITY_HIGH));
2385
+ }
2386
+ tableObserver.listenersToRemove.add(editor.registerCommand(CUT_COMMAND, event => {
2387
+ const selection = $getSelection();
2388
+ if (selection) {
2389
+ if (!($isTableSelection(selection) || $isRangeSelection(selection))) {
2390
+ return false;
2391
+ }
2392
+ // Copying to the clipboard is async so we must capture the data
2393
+ // before we delete it
2394
+ void copyToClipboard(editor, objectKlassEquals(event, ClipboardEvent) ? event : null, $getClipboardDataFromSelection(selection));
2395
+ const intercepted = $deleteCellHandler(event);
2396
+ if ($isRangeSelection(selection)) {
2397
+ selection.removeText();
2398
+ return true;
2399
+ }
2400
+ return intercepted;
2401
+ }
2402
+ return false;
2403
+ }, COMMAND_PRIORITY_HIGH));
2404
+ tableObserver.listenersToRemove.add(editor.registerCommand(FORMAT_TEXT_COMMAND, payload => {
2405
+ const selection = $getSelection();
2406
+ if (!$isSelectionInTable(selection, tableNode)) {
2407
+ return false;
2408
+ }
2409
+ if ($isTableSelection(selection)) {
2410
+ tableObserver.$formatCells(payload);
2411
+ return true;
2412
+ } else if ($isRangeSelection(selection)) {
2413
+ const tableCellNode = $findMatchingParent(selection.anchor.getNode(), n => $isTableCellNode(n));
2414
+ if (!$isTableCellNode(tableCellNode)) {
2415
+ return false;
2416
+ }
2417
+ }
2418
+ return false;
2419
+ }, COMMAND_PRIORITY_HIGH));
2420
+ tableObserver.listenersToRemove.add(editor.registerCommand(FORMAT_ELEMENT_COMMAND, formatType => {
2421
+ const selection = $getSelection();
2422
+ if (!$isTableSelection(selection) || !$isSelectionInTable(selection, tableNode)) {
2423
+ return false;
2424
+ }
2425
+ const anchorNode = selection.anchor.getNode();
2426
+ const focusNode = selection.focus.getNode();
2427
+ if (!$isTableCellNode(anchorNode) || !$isTableCellNode(focusNode)) {
2428
+ return false;
2429
+ }
2430
+
2431
+ // Align the table if the entire table is selected
2432
+ if ($isFullTableSelection(selection, tableNode)) {
2433
+ tableNode.setFormat(formatType);
2434
+ return true;
2435
+ }
2436
+ const [tableMap, anchorCell, focusCell] = $computeTableMap(tableNode, anchorNode, focusNode);
2437
+ const maxRow = Math.max(anchorCell.startRow + anchorCell.cell.__rowSpan - 1, focusCell.startRow + focusCell.cell.__rowSpan - 1);
2438
+ const maxColumn = Math.max(anchorCell.startColumn + anchorCell.cell.__colSpan - 1, focusCell.startColumn + focusCell.cell.__colSpan - 1);
2439
+ const minRow = Math.min(anchorCell.startRow, focusCell.startRow);
2440
+ const minColumn = Math.min(anchorCell.startColumn, focusCell.startColumn);
2441
+ const visited = new Set();
2442
+ for (let i = minRow; i <= maxRow; i++) {
2443
+ for (let j = minColumn; j <= maxColumn; j++) {
2444
+ const cell = tableMap[i][j].cell;
2445
+ if (visited.has(cell)) {
2446
+ continue;
2447
+ }
2448
+ visited.add(cell);
2449
+ cell.setFormat(formatType);
2450
+ const cellChildren = cell.getChildren();
2451
+ for (let k = 0; k < cellChildren.length; k++) {
2452
+ const child = cellChildren[k];
2453
+ if ($isElementNode(child) && !child.isInline()) {
2454
+ child.setFormat(formatType);
2455
+ }
2456
+ }
2457
+ }
2458
+ }
2459
+ return true;
2460
+ }, COMMAND_PRIORITY_HIGH));
2461
+ tableObserver.listenersToRemove.add(editor.registerCommand(CONTROLLED_TEXT_INSERTION_COMMAND, payload => {
2462
+ const selection = $getSelection();
2463
+ if (!$isSelectionInTable(selection, tableNode)) {
2464
+ return false;
2465
+ }
2466
+ if ($isTableSelection(selection)) {
2467
+ tableObserver.$clearHighlight();
2468
+ return false;
2469
+ } else if ($isRangeSelection(selection)) {
2470
+ const tableCellNode = $findMatchingParent(selection.anchor.getNode(), n => $isTableCellNode(n));
2471
+ if (!$isTableCellNode(tableCellNode)) {
2472
+ return false;
2473
+ }
2474
+ if (typeof payload === 'string') {
2475
+ const edgePosition = $getTableEdgeCursorPosition(editor, selection, tableNode);
2476
+ if (edgePosition) {
2477
+ $insertParagraphAtTableEdge(edgePosition, tableNode, [$createTextNode(payload)]);
2478
+ return true;
2479
+ }
2480
+ }
2481
+ }
2482
+ return false;
2483
+ }, COMMAND_PRIORITY_HIGH));
2484
+ if (hasTabHandler) {
2485
+ tableObserver.listenersToRemove.add(editor.registerCommand(KEY_TAB_COMMAND, event => {
2486
+ const selection = $getSelection();
2487
+ if (!$isRangeSelection(selection) || !selection.isCollapsed() || !$isSelectionInTable(selection, tableNode)) {
2488
+ return false;
2489
+ }
2490
+ const tableCellNode = $findCellNode(selection.anchor.getNode());
2491
+ if (tableCellNode === null || !tableNode.is($findTableNode(tableCellNode))) {
2492
+ return false;
2493
+ }
2494
+ stopEvent(event);
2495
+ $selectAdjacentCell(tableCellNode, event.shiftKey ? 'previous' : 'next');
2496
+ return true;
2497
+ }, COMMAND_PRIORITY_HIGH));
2498
+ }
2499
+ tableObserver.listenersToRemove.add(editor.registerCommand(FOCUS_COMMAND, payload => {
2500
+ return tableNode.isSelected();
2501
+ }, COMMAND_PRIORITY_HIGH));
2502
+ tableObserver.listenersToRemove.add(editor.registerCommand(SELECTION_INSERT_CLIPBOARD_NODES_COMMAND, (selectionPayload, dispatchEditor) => {
2503
+ if (editor !== dispatchEditor) {
2504
+ return false;
2505
+ }
2506
+ const {
2507
+ nodes,
2508
+ selection
2509
+ } = selectionPayload;
2510
+ const anchorAndFocus = selection.getStartEndPoints();
2511
+ const isTableSelection = $isTableSelection(selection);
2512
+ const isRangeSelection = $isRangeSelection(selection);
2513
+ const isSelectionInsideOfGrid = isRangeSelection && $findMatchingParent(selection.anchor.getNode(), n => $isTableCellNode(n)) !== null && $findMatchingParent(selection.focus.getNode(), n => $isTableCellNode(n)) !== null || isTableSelection;
2514
+ if (nodes.length !== 1 || !$isTableNode(nodes[0]) || !isSelectionInsideOfGrid || anchorAndFocus === null) {
2515
+ return false;
2516
+ }
2517
+ const [anchor, focus] = anchorAndFocus;
2518
+ const [anchorCellNode, anchorRowNode, gridNode] = $getNodeTriplet(anchor);
2519
+ const focusCellNode = $findMatchingParent(focus.getNode(), n => $isTableCellNode(n));
2520
+ if (!$isTableCellNode(anchorCellNode) || !$isTableCellNode(focusCellNode) || !$isTableRowNode(anchorRowNode) || !$isTableNode(gridNode)) {
2521
+ return false;
2522
+ }
2523
+ const templateGrid = nodes[0];
2524
+ const [initialGridMap, anchorCellMap, focusCellMap] = $computeTableMap(gridNode, anchorCellNode, focusCellNode);
2525
+ const [templateGridMap] = $computeTableMapSkipCellCheck(templateGrid, null, null);
2526
+ const initialRowCount = initialGridMap.length;
2527
+ const initialColCount = initialRowCount > 0 ? initialGridMap[0].length : 0;
2528
+
2529
+ // If we have a range selection, we'll fit the template grid into the
2530
+ // table, growing the table if necessary.
2531
+ let startRow = anchorCellMap.startRow;
2532
+ let startCol = anchorCellMap.startColumn;
2533
+ let affectedRowCount = templateGridMap.length;
2534
+ let affectedColCount = affectedRowCount > 0 ? templateGridMap[0].length : 0;
2535
+ if (isTableSelection) {
2536
+ // If we have a table selection, we'll only modify the cells within
2537
+ // the selection boundary.
2538
+ const selectionBoundary = $computeTableCellRectBoundary(initialGridMap, anchorCellMap, focusCellMap);
2539
+ const selectionRowCount = selectionBoundary.maxRow - selectionBoundary.minRow + 1;
2540
+ const selectionColCount = selectionBoundary.maxColumn - selectionBoundary.minColumn + 1;
2541
+ startRow = selectionBoundary.minRow;
2542
+ startCol = selectionBoundary.minColumn;
2543
+ affectedRowCount = Math.min(affectedRowCount, selectionRowCount);
2544
+ affectedColCount = Math.min(affectedColCount, selectionColCount);
2545
+ }
2546
+
2547
+ // Step 1: Unmerge all merged cells within the affected area
2548
+ let didPerformMergeOperations = false;
2549
+ const lastRowForUnmerge = Math.min(initialRowCount, startRow + affectedRowCount) - 1;
2550
+ const lastColForUnmerge = Math.min(initialColCount, startCol + affectedColCount) - 1;
2551
+ const unmergedKeys = new Set();
2552
+ for (let row = startRow; row <= lastRowForUnmerge; row++) {
2553
+ for (let col = startCol; col <= lastColForUnmerge; col++) {
2554
+ const cellMap = initialGridMap[row][col];
2555
+ if (unmergedKeys.has(cellMap.cell.getKey())) {
2556
+ continue; // cell was a merged cell that was already handled
2557
+ }
2558
+ if (cellMap.cell.__rowSpan === 1 && cellMap.cell.__colSpan === 1) {
2559
+ continue; // cell is not a merged cell
2560
+ }
2561
+ $unmergeCellNode(cellMap.cell);
2562
+ unmergedKeys.add(cellMap.cell.getKey());
2563
+ didPerformMergeOperations = true;
2564
+ }
2565
+ }
2566
+ let [interimGridMap] = $computeTableMapSkipCellCheck(gridNode.getWritable(), null, null);
2567
+
2568
+ // Step 2: Expand current table (if needed)
2569
+ const rowsToInsert = affectedRowCount - initialRowCount + startRow;
2570
+ for (let i = 0; i < rowsToInsert; i++) {
2571
+ const cellMap = interimGridMap[initialRowCount - 1][0];
2572
+ $insertTableRowAtNode(cellMap.cell);
2573
+ }
2574
+ const colsToInsert = affectedColCount - initialColCount + startCol;
2575
+ for (let i = 0; i < colsToInsert; i++) {
2576
+ const cellMap = interimGridMap[0][initialColCount - 1];
2577
+ $insertTableColumnAtNode(cellMap.cell, true, false);
2578
+ }
2579
+ [interimGridMap] = $computeTableMapSkipCellCheck(gridNode.getWritable(), null, null);
2580
+
2581
+ // Step 3: Merge cells and set cell content, to match template grid
2582
+ for (let row = startRow; row < startRow + affectedRowCount; row++) {
2583
+ for (let col = startCol; col < startCol + affectedColCount; col++) {
2584
+ const templateRow = row - startRow;
2585
+ const templateCol = col - startCol;
2586
+ const templateCellMap = templateGridMap[templateRow][templateCol];
2587
+ if (templateCellMap.startRow !== templateRow || templateCellMap.startColumn !== templateCol) {
2588
+ continue; // cell is a merged cell that was already handled
2589
+ }
2590
+ const templateCell = templateCellMap.cell;
2591
+ if (templateCell.__rowSpan !== 1 || templateCell.__colSpan !== 1) {
2592
+ const cellsToMerge = [];
2593
+ const lastRowForMerge = Math.min(row + templateCell.__rowSpan, startRow + affectedRowCount) - 1;
2594
+ const lastColForMerge = Math.min(col + templateCell.__colSpan, startCol + affectedColCount) - 1;
2595
+ for (let r = row; r <= lastRowForMerge; r++) {
2596
+ for (let c = col; c <= lastColForMerge; c++) {
2597
+ const cellMap = interimGridMap[r][c];
2598
+ cellsToMerge.push(cellMap.cell);
2599
+ }
2600
+ }
2601
+ $mergeCells(cellsToMerge);
2602
+ didPerformMergeOperations = true;
2603
+ }
2604
+ const {
2605
+ cell
2606
+ } = interimGridMap[row][col];
2607
+ const originalChildren = cell.getChildren();
2608
+ templateCell.getChildren().forEach(child => {
2609
+ if ($isTextNode(child)) {
2610
+ const paragraphNode = $createParagraphNode();
2611
+ paragraphNode.append(child);
2612
+ cell.append(child);
2613
+ } else {
2614
+ cell.append(child);
2615
+ }
2616
+ });
2617
+ originalChildren.forEach(n => n.remove());
2618
+ }
2619
+ }
2620
+ if (isTableSelection && didPerformMergeOperations) {
2621
+ // reset the table selection in case the anchor or focus cell was
2622
+ // removed via merge operations
2623
+ const [finalGridMap] = $computeTableMapSkipCellCheck(gridNode.getWritable(), null, null);
2624
+ const newAnchorCellMap = finalGridMap[anchorCellMap.startRow][anchorCellMap.startColumn];
2625
+ newAnchorCellMap.cell.selectEnd();
2626
+ }
2627
+ return true;
2628
+ }, COMMAND_PRIORITY_HIGH));
2629
+ tableObserver.listenersToRemove.add(editor.registerCommand(SELECTION_CHANGE_COMMAND, () => {
2630
+ const selection = $getSelection();
2631
+ const prevSelection = $getPreviousSelection();
2632
+ const nextFocus = tableObserver.getAndClearNextFocus();
2633
+ if (nextFocus !== null) {
2634
+ const {
2635
+ focusCell
2636
+ } = nextFocus;
2637
+ if ($isTableSelection(selection) && selection.tableKey === tableObserver.tableNodeKey) {
2638
+ if (focusCell.x === tableObserver.focusX && focusCell.y === tableObserver.focusY) {
2639
+ // The selection is already the correct table selection
2640
+ return false;
2641
+ } else {
2642
+ tableObserver.$setFocusCellForSelection(focusCell);
2643
+ return true;
2644
+ }
2645
+ } else if (focusCell !== tableObserver.anchorCell && $isSelectionInTable(selection, tableNode)) {
2646
+ // The selection has crossed cells
2647
+ tableObserver.$setFocusCellForSelection(focusCell);
2648
+ return true;
2649
+ }
2650
+ }
2651
+ const shouldCheckSelection = tableObserver.getAndClearShouldCheckSelection();
2652
+ // If they pressed the down arrow with the selection outside of the
2653
+ // table, and then the selection ends up in the table but not in the
2654
+ // first cell, then move the selection to the first cell.
2655
+ if (shouldCheckSelection && $isRangeSelection(prevSelection) && $isRangeSelection(selection) && selection.isCollapsed()) {
2656
+ const anchor = selection.anchor.getNode();
2657
+ const firstRow = tableNode.getFirstChild();
2658
+ const anchorCell = $findCellNode(anchor);
2659
+ if (anchorCell !== null && $isTableRowNode(firstRow)) {
2660
+ const firstCell = firstRow.getFirstChild();
2661
+ if ($isTableCellNode(firstCell) && tableNode.is($findMatchingParent(anchorCell, node => node.is(tableNode) || node.is(firstCell)))) {
2662
+ // The selection moved to the table, but not in the first cell
2663
+ firstCell.selectStart();
2664
+ return true;
2665
+ }
2666
+ }
2667
+ }
2668
+ if ($isRangeSelection(selection)) {
2669
+ const {
2670
+ anchor,
2671
+ focus
2672
+ } = selection;
2673
+ const anchorNode = anchor.getNode();
2674
+ const focusNode = focus.getNode();
2675
+ // Using explicit comparison with table node to ensure it's not a nested table
2676
+ // as in that case we'll leave selection resolving to that table
2677
+ const anchorCellNode = $findCellNode(anchorNode);
2678
+ const focusCellNode = $findCellNode(focusNode);
2679
+ const isAnchorInside = !!(anchorCellNode && tableNode.is($findTableNode(anchorCellNode)));
2680
+ const isFocusInside = !!(focusCellNode && tableNode.is($findTableNode(focusCellNode)));
2681
+ const isPartiallyWithinTable = isAnchorInside !== isFocusInside;
2682
+ const isWithinTable = isAnchorInside && isFocusInside;
2683
+ const isBackward = selection.isBackward();
2684
+ if (isPartiallyWithinTable) {
2685
+ const newSelection = selection.clone();
2686
+ if (isFocusInside) {
2687
+ const [tableMap] = $computeTableMap(tableNode, focusCellNode, focusCellNode);
2688
+ const firstCell = tableMap[0][0].cell;
2689
+ const lastCell = tableMap[tableMap.length - 1].at(-1).cell;
2690
+ // When backward, focus should be at START of first cell (0)
2691
+ // When forward, focus should be at END of last cell (getChildrenSize)
2692
+ newSelection.focus.set(isBackward ? firstCell.getKey() : lastCell.getKey(), isBackward ? 0 : lastCell.getChildrenSize(), 'element');
2693
+ } else if (isAnchorInside) {
2694
+ const [tableMap] = $computeTableMap(tableNode, anchorCellNode, anchorCellNode);
2695
+ const firstCell = tableMap[0][0].cell;
2696
+ const lastCell = tableMap[tableMap.length - 1].at(-1).cell;
2697
+ /**
2698
+ * If isBackward, set the anchor to be at the end of the table so that when the cursor moves outside of
2699
+ * the table in the backward direction, the entire table will be selected from its end.
2700
+ * Otherwise, if forward, set the anchor to be at the start of the table so that when the focus is dragged
2701
+ * outside th end of the table, it will start from the beginning of the table.
2702
+ */
2703
+ newSelection.anchor.set(isBackward ? lastCell.getKey() : firstCell.getKey(), isBackward ? lastCell.getChildrenSize() : 0, 'element');
2704
+ }
2705
+ $setSelection(newSelection);
2706
+ $addHighlightStyleToTable(editor, tableObserver);
2707
+ } else if (isWithinTable) {
2708
+ // Handle case when selection spans across multiple cells but still
2709
+ // has range selection, then we convert it into table selection
2710
+ if (!anchorCellNode.is(focusCellNode)) {
2711
+ tableObserver.$setAnchorCellForSelection($getObserverCellFromCellNodeOrThrow(tableObserver, anchorCellNode));
2712
+ tableObserver.$setFocusCellForSelection($getObserverCellFromCellNodeOrThrow(tableObserver, focusCellNode), true);
2713
+ }
2714
+
2715
+ // Handle case when the pointer type is touch and the current and
2716
+ // previous selection are collapsed, and the previous anchor and current
2717
+ // focus cell nodes are different, then we convert it into table selection
2718
+ // However, only do this if the table observer is actively selecting (user dragging)
2719
+ // to prevent unwanted selections when simply tapping between cells on mobile
2720
+ if (tableObserver.pointerType === 'touch' && tableObserver.isSelecting && selection.isCollapsed() && $isRangeSelection(prevSelection) && prevSelection.isCollapsed()) {
2721
+ const prevAnchorCellNode = $findCellNode(prevSelection.anchor.getNode());
2722
+ if (prevAnchorCellNode && !prevAnchorCellNode.is(focusCellNode)) {
2723
+ tableObserver.$setAnchorCellForSelection($getObserverCellFromCellNodeOrThrow(tableObserver, prevAnchorCellNode));
2724
+ tableObserver.$setFocusCellForSelection($getObserverCellFromCellNodeOrThrow(tableObserver, focusCellNode), true);
2725
+ tableObserver.pointerType = null;
2726
+ }
2727
+ }
2728
+ }
2729
+ } else if (selection && $isTableSelection(selection) && selection.is(prevSelection) && selection.tableKey === tableNode.getKey()) {
2730
+ // if selection goes outside of the table we need to change it to Range selection
2731
+ const domSelection = getDOMSelectionForEditor(editor);
2732
+ if (domSelection && domSelection.anchorNode && domSelection.focusNode) {
2733
+ const focusNode = $getNearestNodeFromDOMNode(domSelection.focusNode);
2734
+ const isFocusOutside = focusNode && !tableNode.isParentOf(focusNode);
2735
+ const anchorNode = $getNearestNodeFromDOMNode(domSelection.anchorNode);
2736
+ const isAnchorInside = anchorNode && tableNode.isParentOf(anchorNode);
2737
+ if (isFocusOutside && isAnchorInside && domSelection.rangeCount > 0) {
2738
+ const newSelection = $createRangeSelectionFromDom(domSelection, editor);
2739
+ if (newSelection) {
2740
+ newSelection.anchor.set(tableNode.getKey(), selection.isBackward() ? tableNode.getChildrenSize() : 0, 'element');
2741
+ domSelection.removeAllRanges();
2742
+ $setSelection(newSelection);
2743
+ }
2744
+ }
2745
+ }
2746
+ }
2747
+ if (selection && !selection.is(prevSelection) && ($isTableSelection(selection) || $isTableSelection(prevSelection)) && tableObserver.tableSelection && !tableObserver.tableSelection.is(prevSelection)) {
2748
+ if ($isTableSelection(selection) && selection.tableKey === tableObserver.tableNodeKey) {
2749
+ tableObserver.$updateTableTableSelection(selection);
2750
+ } else if (!$isTableSelection(selection) && $isTableSelection(prevSelection) && prevSelection.tableKey === tableObserver.tableNodeKey) {
2751
+ tableObserver.$updateTableTableSelection(null);
2752
+ }
2753
+ return false;
2754
+ }
2755
+ if (tableObserver.hasHijackedSelectionStyles && !tableNode.isSelected()) {
2756
+ $removeHighlightStyleToTable(editor, tableObserver);
2757
+ } else if (!tableObserver.hasHijackedSelectionStyles && tableNode.isSelected()) {
2758
+ $addHighlightStyleToTable(editor, tableObserver);
2759
+ }
2760
+ return false;
2761
+ }, COMMAND_PRIORITY_HIGH));
2762
+ tableObserver.listenersToRemove.add(editor.registerCommand(INSERT_PARAGRAPH_COMMAND, () => {
2763
+ const selection = $getSelection();
2764
+ if (!$isRangeSelection(selection) || !selection.isCollapsed() || !$isSelectionInTable(selection, tableNode)) {
2765
+ return false;
2766
+ }
2767
+ const edgePosition = $getTableEdgeCursorPosition(editor, selection, tableNode);
2768
+ if (edgePosition) {
2769
+ $insertParagraphAtTableEdge(edgePosition, tableNode);
2770
+ return true;
2771
+ }
2772
+ return false;
2773
+ }, COMMAND_PRIORITY_HIGH));
2774
+ return tableObserver;
2775
+ }
2776
+ function detachTableObserverFromTableElement(tableElement, tableObserver) {
2777
+ if (getTableObserverFromTableElement(tableElement) === tableObserver) {
2778
+ delete tableElement[LEXICAL_ELEMENT_KEY];
2779
+ }
2780
+ }
2781
+ function attachTableObserverToTableElement(tableElement, tableObserver) {
2782
+ if (!(getTableObserverFromTableElement(tableElement) === null)) {
2783
+ formatDevErrorMessage(`tableElement already has an attached TableObserver`);
2784
+ }
2785
+ tableElement[LEXICAL_ELEMENT_KEY] = tableObserver;
2786
+ }
2787
+ function getTableObserverFromTableElement(tableElement) {
2788
+ return tableElement[LEXICAL_ELEMENT_KEY] || null;
2789
+ }
2790
+ function getDOMCellFromTarget(node) {
2791
+ let currentNode = node;
2792
+ while (currentNode != null) {
2793
+ const nodeName = currentNode.nodeName;
2794
+ if (nodeName === 'TD' || nodeName === 'TH') {
2795
+ // @ts-expect-error: internal field
2796
+ const cell = currentNode._cell;
2797
+ if (cell === undefined) {
2798
+ return null;
2799
+ }
2800
+ return cell;
2801
+ }
2802
+ currentNode = currentNode.parentNode;
2803
+ }
2804
+ return null;
2805
+ }
2806
+ function getDOMCellInTableFromTarget(table, node) {
2807
+ if (!table.contains(node)) {
2808
+ return null;
2809
+ }
2810
+ let cell = null;
2811
+ for (let currentNode = node; currentNode != null; currentNode = currentNode.parentNode) {
2812
+ if (currentNode === table) {
2813
+ return cell;
2814
+ }
2815
+ const nodeName = currentNode.nodeName;
2816
+ if (nodeName === 'TD' || nodeName === 'TH') {
2817
+ // @ts-expect-error: internal field
2818
+ cell = currentNode._cell || null;
2819
+ }
2820
+ }
2821
+ return null;
2822
+ }
2823
+ function getTable(tableNode, dom) {
2824
+ const tableElement = getTableElement(tableNode, dom);
2825
+ const domRows = [];
2826
+ const grid = {
2827
+ columns: 0,
2828
+ domRows,
2829
+ rows: 0
2830
+ };
2831
+ let currentNode = tableElement.querySelector('tr');
2832
+ let x = 0;
2833
+ let y = 0;
2834
+ domRows.length = 0;
2835
+ while (currentNode != null) {
2836
+ const nodeMame = currentNode.nodeName;
2837
+ if (nodeMame === 'TD' || nodeMame === 'TH') {
2838
+ const elem = currentNode;
2839
+ const cell = {
2840
+ elem,
2841
+ hasBackgroundColor: elem.style.backgroundColor !== '',
2842
+ highlighted: false,
2843
+ x,
2844
+ y
2845
+ };
2846
+
2847
+ // @ts-expect-error: internal field
2848
+ currentNode._cell = cell;
2849
+ let row = domRows[y];
2850
+ if (row === undefined) {
2851
+ row = domRows[y] = [];
2852
+ }
2853
+ row[x] = cell;
2854
+ } else {
2855
+ const child = currentNode.firstChild;
2856
+ if (child != null) {
2857
+ currentNode = child;
2858
+ continue;
2859
+ }
2860
+ }
2861
+ const sibling = currentNode.nextSibling;
2862
+ if (sibling != null) {
2863
+ x++;
2864
+ currentNode = sibling;
2865
+ continue;
2866
+ }
2867
+ const parent = currentNode.parentNode;
2868
+ if (parent != null) {
2869
+ const parentSibling = parent.nextSibling;
2870
+ if (parentSibling == null) {
2871
+ break;
2872
+ }
2873
+ y++;
2874
+ x = 0;
2875
+ currentNode = parentSibling;
2876
+ }
2877
+ }
2878
+ grid.columns = x + 1;
2879
+ grid.rows = y + 1;
2880
+ return grid;
2881
+ }
2882
+ function $updateDOMForSelection(editor, table, selection) {
2883
+ const selectedCellNodes = new Set(selection ? selection.getNodes() : []);
2884
+ $forEachTableCell(table, (cell, lexicalNode) => {
2885
+ const elem = cell.elem;
2886
+ if (selectedCellNodes.has(lexicalNode)) {
2887
+ cell.highlighted = true;
2888
+ $addHighlightToDOM(editor, cell);
2889
+ } else {
2890
+ cell.highlighted = false;
2891
+ $removeHighlightFromDOM(editor, cell);
2892
+ if (!elem.getAttribute('style')) {
2893
+ elem.removeAttribute('style');
2894
+ }
2895
+ }
2896
+ });
2897
+ }
2898
+ function $forEachTableCell(grid, cb) {
2899
+ const {
2900
+ domRows
2901
+ } = grid;
2902
+ for (let y = 0; y < domRows.length; y++) {
2903
+ const row = domRows[y];
2904
+ if (!row) {
2905
+ continue;
2906
+ }
2907
+ for (let x = 0; x < row.length; x++) {
2908
+ const cell = row[x];
2909
+ if (!cell) {
2910
+ continue;
2911
+ }
2912
+ const lexicalNode = $getNearestNodeFromDOMNode(cell.elem);
2913
+ if (lexicalNode !== null) {
2914
+ cb(cell, lexicalNode, {
2915
+ x,
2916
+ y
2917
+ });
2918
+ }
2919
+ }
2920
+ }
2921
+ }
2922
+ function $addHighlightStyleToTable(editor, tableSelection) {
2923
+ tableSelection.$disableHighlightStyle();
2924
+ $forEachTableCell(tableSelection.table, cell => {
2925
+ cell.highlighted = true;
2926
+ $addHighlightToDOM(editor, cell);
2927
+ });
2928
+ }
2929
+ function $removeHighlightStyleToTable(editor, tableObserver) {
2930
+ tableObserver.$enableHighlightStyle();
2931
+ $forEachTableCell(tableObserver.table, cell => {
2932
+ const elem = cell.elem;
2933
+ cell.highlighted = false;
2934
+ $removeHighlightFromDOM(editor, cell);
2935
+ if (!elem.getAttribute('style')) {
2936
+ elem.removeAttribute('style');
2937
+ }
2938
+ });
2939
+ }
2940
+ function $selectAdjacentCell(tableCellNode, direction) {
2941
+ const siblingMethod = direction === 'next' ? 'getNextSibling' : 'getPreviousSibling';
2942
+ const childMethod = direction === 'next' ? 'getFirstChild' : 'getLastChild';
2943
+ const sibling = tableCellNode[siblingMethod]();
2944
+ if ($isElementNode(sibling)) {
2945
+ return sibling.selectEnd();
2946
+ }
2947
+ const parentRow = $findMatchingParent(tableCellNode, $isTableRowNode);
2948
+ if (!(parentRow !== null)) {
2949
+ formatDevErrorMessage(`selectAdjacentCell: Cell not in table row`);
2950
+ }
2951
+ for (let nextRow = parentRow[siblingMethod](); $isTableRowNode(nextRow); nextRow = nextRow[siblingMethod]()) {
2952
+ const child = nextRow[childMethod]();
2953
+ if ($isElementNode(child)) {
2954
+ return child.selectEnd();
2955
+ }
2956
+ }
2957
+ const parentTable = $findMatchingParent(parentRow, $isTableNode);
2958
+ if (!(parentTable !== null)) {
2959
+ formatDevErrorMessage(`selectAdjacentCell: Row not in table`);
2960
+ }
2961
+ return direction === 'next' ? parentTable.selectNext() : parentTable.selectPrevious();
2962
+ }
2963
+ const selectTableNodeInDirection = (tableObserver, tableNode, x, y, direction) => {
2964
+ const isForward = direction === 'forward';
2965
+ switch (direction) {
2966
+ case 'backward':
2967
+ case 'forward':
2968
+ if (x !== (isForward ? tableObserver.table.columns - 1 : 0)) {
2969
+ selectTableCellNode(tableNode.getCellNodeFromCordsOrThrow(x + (isForward ? 1 : -1), y, tableObserver.table), isForward);
2970
+ } else {
2971
+ if (y !== (isForward ? tableObserver.table.rows - 1 : 0)) {
2972
+ selectTableCellNode(tableNode.getCellNodeFromCordsOrThrow(isForward ? 0 : tableObserver.table.columns - 1, y + (isForward ? 1 : -1), tableObserver.table), isForward);
2973
+ } else if (!isForward) {
2974
+ tableNode.selectPrevious();
2975
+ } else {
2976
+ tableNode.selectNext();
2977
+ }
2978
+ }
2979
+ return true;
2980
+ case 'up':
2981
+ if (y !== 0) {
2982
+ selectTableCellNode(tableNode.getCellNodeFromCordsOrThrow(x, y - 1, tableObserver.table), false);
2983
+ } else {
2984
+ tableNode.selectPrevious();
2985
+ }
2986
+ return true;
2987
+ case 'down':
2988
+ if (y !== tableObserver.table.rows - 1) {
2989
+ selectTableCellNode(tableNode.getCellNodeFromCordsOrThrow(x, y + 1, tableObserver.table), true);
2990
+ } else {
2991
+ tableNode.selectNext();
2992
+ }
2993
+ return true;
2994
+ default:
2995
+ return false;
2996
+ }
2997
+ };
2998
+ function getCorner(rect, cellValue) {
2999
+ let colName;
3000
+ let rowName;
3001
+ if (cellValue.startColumn === rect.minColumn) {
3002
+ colName = 'minColumn';
3003
+ } else if (cellValue.startColumn + cellValue.cell.__colSpan - 1 === rect.maxColumn) {
3004
+ colName = 'maxColumn';
3005
+ } else {
3006
+ return null;
3007
+ }
3008
+ if (cellValue.startRow === rect.minRow) {
3009
+ rowName = 'minRow';
3010
+ } else if (cellValue.startRow + cellValue.cell.__rowSpan - 1 === rect.maxRow) {
3011
+ rowName = 'maxRow';
3012
+ } else {
3013
+ return null;
3014
+ }
3015
+ return [colName, rowName];
3016
+ }
3017
+ function getCornerOrThrow(rect, cellValue) {
3018
+ const corner = getCorner(rect, cellValue);
3019
+ if (!(corner !== null)) {
3020
+ formatDevErrorMessage(`getCornerOrThrow: cell ${cellValue.cell.getKey()} is not at a corner of rect`);
3021
+ }
3022
+ return corner;
3023
+ }
3024
+ function oppositeCorner([colName, rowName]) {
3025
+ return [colName === 'minColumn' ? 'maxColumn' : 'minColumn', rowName === 'minRow' ? 'maxRow' : 'minRow'];
3026
+ }
3027
+ function cellAtCornerOrThrow(tableMap, rect, [colName, rowName]) {
3028
+ const rowNum = rect[rowName];
3029
+ const rowMap = tableMap[rowNum];
3030
+ if (!(rowMap !== undefined)) {
3031
+ formatDevErrorMessage(`cellAtCornerOrThrow: ${rowName} = ${String(rowNum)} missing in tableMap`);
3032
+ }
3033
+ const colNum = rect[colName];
3034
+ const cell = rowMap[colNum];
3035
+ if (!(cell !== undefined)) {
3036
+ formatDevErrorMessage(`cellAtCornerOrThrow: ${colName} = ${String(colNum)} missing in tableMap`);
3037
+ }
3038
+ return cell;
3039
+ }
3040
+ function $extractRectCorners(tableMap, anchorCellValue, newFocusCellValue) {
3041
+ // We are sure that the focus now either contracts or expands the rect
3042
+ // but both the anchor and focus might be moved to ensure a rectangle
3043
+ // given a potentially ragged merge shape
3044
+ const rect = $computeTableCellRectBoundary(tableMap, anchorCellValue, newFocusCellValue);
3045
+ const anchorCorner = getCorner(rect, anchorCellValue);
3046
+ if (anchorCorner) {
3047
+ return [cellAtCornerOrThrow(tableMap, rect, anchorCorner), cellAtCornerOrThrow(tableMap, rect, oppositeCorner(anchorCorner))];
3048
+ }
3049
+ const newFocusCorner = getCorner(rect, newFocusCellValue);
3050
+ if (newFocusCorner) {
3051
+ return [cellAtCornerOrThrow(tableMap, rect, oppositeCorner(newFocusCorner)), cellAtCornerOrThrow(tableMap, rect, newFocusCorner)];
3052
+ }
3053
+ // TODO this doesn't have to be arbitrary, use the closest corner instead
3054
+ const newAnchorCorner = ['minColumn', 'minRow'];
3055
+ return [cellAtCornerOrThrow(tableMap, rect, newAnchorCorner), cellAtCornerOrThrow(tableMap, rect, oppositeCorner(newAnchorCorner))];
3056
+ }
3057
+ function $adjustFocusInDirection(tableObserver, tableMap, anchorCellValue, focusCellValue, direction) {
3058
+ const rect = $computeTableCellRectBoundary(tableMap, anchorCellValue, focusCellValue);
3059
+ const spans = $computeTableCellRectSpans(tableMap, rect);
3060
+ const {
3061
+ topSpan,
3062
+ leftSpan,
3063
+ bottomSpan,
3064
+ rightSpan
3065
+ } = spans;
3066
+ const anchorCorner = getCornerOrThrow(rect, anchorCellValue);
3067
+ const [focusColumn, focusRow] = oppositeCorner(anchorCorner);
3068
+ let fCol = rect[focusColumn];
3069
+ let fRow = rect[focusRow];
3070
+ if (direction === 'forward') {
3071
+ fCol += focusColumn === 'maxColumn' ? 1 : leftSpan;
3072
+ } else if (direction === 'backward') {
3073
+ fCol -= focusColumn === 'minColumn' ? 1 : rightSpan;
3074
+ } else if (direction === 'down') {
3075
+ fRow += focusRow === 'maxRow' ? 1 : topSpan;
3076
+ } else if (direction === 'up') {
3077
+ fRow -= focusRow === 'minRow' ? 1 : bottomSpan;
3078
+ }
3079
+ const targetRowMap = tableMap[fRow];
3080
+ if (targetRowMap === undefined) {
3081
+ return false;
3082
+ }
3083
+ const newFocusCellValue = targetRowMap[fCol];
3084
+ if (newFocusCellValue === undefined) {
3085
+ return false;
3086
+ }
3087
+ // We can be certain that anchorCellValue and newFocusCellValue are
3088
+ // contained within the desired selection, but we are not certain if
3089
+ // they need to be expanded or not to maintain a rectangular shape
3090
+ const [finalAnchorCell, finalFocusCell] = $extractRectCorners(tableMap, anchorCellValue, newFocusCellValue);
3091
+ const anchorDOM = $getObserverCellFromCellNodeOrThrow(tableObserver, finalAnchorCell.cell);
3092
+ const focusDOM = $getObserverCellFromCellNodeOrThrow(tableObserver, finalFocusCell.cell);
3093
+ tableObserver.$setAnchorCellForSelection(anchorDOM);
3094
+ tableObserver.$setFocusCellForSelection(focusDOM, true);
3095
+ return true;
3096
+ }
3097
+ function $isSelectionInTable(selection, tableNode) {
3098
+ if ($isRangeSelection(selection) || $isTableSelection(selection)) {
3099
+ // TODO this should probably return false if there's an unrelated
3100
+ // shadow root between the node and the table (e.g. another table,
3101
+ // collapsible, etc.)
3102
+ const isAnchorInside = tableNode.isParentOf(selection.anchor.getNode());
3103
+ const isFocusInside = tableNode.isParentOf(selection.focus.getNode());
3104
+ return isAnchorInside && isFocusInside;
3105
+ }
3106
+ return false;
3107
+ }
3108
+ function $isFullTableSelection(selection, tableNode) {
3109
+ if ($isTableSelection(selection)) {
3110
+ const anchorNode = selection.anchor.getNode();
3111
+ const focusNode = selection.focus.getNode();
3112
+ if (tableNode && anchorNode && focusNode) {
3113
+ const [map] = $computeTableMap(tableNode, anchorNode, focusNode);
3114
+ return anchorNode.getKey() === map[0][0].cell.getKey() && focusNode.getKey() === map[map.length - 1].at(-1).cell.getKey();
3115
+ }
3116
+ }
3117
+ return false;
3118
+ }
3119
+ function selectTableCellNode(tableCell, fromStart) {
3120
+ if (fromStart) {
3121
+ tableCell.selectStart();
3122
+ } else {
3123
+ tableCell.selectEnd();
3124
+ }
3125
+ }
3126
+ function $addHighlightToDOM(editor, cell) {
3127
+ const element = cell.elem;
3128
+ const editorThemeClasses = editor._config.theme;
3129
+ const node = $getNearestNodeFromDOMNode(element);
3130
+ if (!$isTableCellNode(node)) {
3131
+ formatDevErrorMessage(`Expected to find LexicalNode from Table Cell DOMNode`);
3132
+ }
3133
+ addClassNamesToElement(element, editorThemeClasses.tableCellSelected);
3134
+ }
3135
+ function $removeHighlightFromDOM(editor, cell) {
3136
+ const element = cell.elem;
3137
+ const node = $getNearestNodeFromDOMNode(element);
3138
+ if (!$isTableCellNode(node)) {
3139
+ formatDevErrorMessage(`Expected to find LexicalNode from Table Cell DOMNode`);
3140
+ }
3141
+ const editorThemeClasses = editor._config.theme;
3142
+ removeClassNamesFromElement(element, editorThemeClasses.tableCellSelected);
3143
+ }
3144
+ function $findCellNode(node) {
3145
+ const cellNode = $findMatchingParent(node, $isTableCellNode);
3146
+ return $isTableCellNode(cellNode) ? cellNode : null;
3147
+ }
3148
+ function $findTableNode(node) {
3149
+ const tableNode = $findMatchingParent(node, $isTableNode);
3150
+ return $isTableNode(tableNode) ? tableNode : null;
3151
+ }
3152
+ function $getBlockParentIfFirstNode(node) {
3153
+ for (let prevNode = node, currentNode = node; currentNode !== null; prevNode = currentNode, currentNode = currentNode.getParent()) {
3154
+ if ($isElementNode(currentNode)) {
3155
+ if (currentNode !== prevNode && currentNode.getFirstChild() !== prevNode) {
3156
+ // Not the first child or the initial node
3157
+ return null;
3158
+ } else if (!currentNode.isInline()) {
3159
+ return currentNode;
3160
+ }
3161
+ }
3162
+ }
3163
+ return null;
3164
+ }
3165
+ function $handleHorizontalArrowKeyRangeSelection(editor, event, selection, alter, isBackward, tableNode, tableObserver) {
3166
+ const initialFocus = $caretFromPoint(selection.focus, isBackward ? 'previous' : 'next');
3167
+ if ($isExtendableTextPointCaret(initialFocus)) {
3168
+ return false;
3169
+ }
3170
+ let lastCaret = initialFocus;
3171
+ // TableCellNode is the only shadow root we are interested in piercing so
3172
+ // we find the last internal caret and then check its parent
3173
+ for (const nextCaret of $extendCaretToRange(initialFocus).iterNodeCarets('shadowRoot')) {
3174
+ if (!($isSiblingCaret(nextCaret) && $isElementNode(nextCaret.origin))) {
3175
+ return false;
3176
+ }
3177
+ lastCaret = nextCaret;
3178
+ }
3179
+ const lastCaretParent = lastCaret.getParentAtCaret();
3180
+ if (!$isTableCellNode(lastCaretParent)) {
3181
+ return false;
3182
+ }
3183
+ const anchorCell = lastCaretParent;
3184
+ const focusCaret = $findNextTableCell($getSiblingCaret(anchorCell, lastCaret.direction));
3185
+ const anchorCellTable = $findMatchingParent(anchorCell, $isTableNode);
3186
+ if (!(anchorCellTable && anchorCellTable.is(tableNode))) {
3187
+ return false;
3188
+ }
3189
+ const anchorCellDOM = editor.getElementByKey(anchorCell.getKey());
3190
+ const anchorDOMCell = getDOMCellFromTarget(anchorCellDOM);
3191
+ if (!anchorCellDOM || !anchorDOMCell) {
3192
+ return false;
3193
+ }
3194
+ const anchorCellTableElement = $getElementForTableNode(editor, anchorCellTable);
3195
+ tableObserver.table = anchorCellTableElement;
3196
+ if (!focusCaret) {
3197
+ if (alter === 'extend') {
3198
+ // extend the selection from a range inside the cell to a table selection of the cell
3199
+ tableObserver.$setAnchorCellForSelection(anchorDOMCell);
3200
+ tableObserver.$setFocusCellForSelection(anchorDOMCell, true);
3201
+ } else {
3202
+ // exit the table
3203
+ const outerFocusCaret = $getTableExitCaret($getSiblingCaret(anchorCellTable, initialFocus.direction));
3204
+ $setPointFromCaret(selection.anchor, outerFocusCaret);
3205
+ $setPointFromCaret(selection.focus, outerFocusCaret);
3206
+ }
3207
+ } else if (alter === 'extend') {
3208
+ const focusDOMCell = getDOMCellFromTarget(editor.getElementByKey(focusCaret.origin.getKey()));
3209
+ if (!focusDOMCell) {
3210
+ return false;
3211
+ }
3212
+ tableObserver.$setAnchorCellForSelection(anchorDOMCell);
3213
+ tableObserver.$setFocusCellForSelection(focusDOMCell, true);
3214
+ } else {
3215
+ // alter === 'move'
3216
+ const innerFocusCaret = $normalizeCaret(focusCaret);
3217
+ $setPointFromCaret(selection.anchor, innerFocusCaret);
3218
+ $setPointFromCaret(selection.focus, innerFocusCaret);
3219
+ }
3220
+ stopEvent(event);
3221
+ return true;
3222
+ }
3223
+ function $getTableExitCaret(initialCaret) {
3224
+ const adjacent = $getAdjacentChildCaret(initialCaret);
3225
+ return $isChildCaret(adjacent) ? $normalizeCaret(adjacent) : initialCaret;
3226
+ }
3227
+ function $findNextTableCell(initialCaret) {
3228
+ for (const nextCaret of $extendCaretToRange(initialCaret).iterNodeCarets('root')) {
3229
+ const {
3230
+ origin
3231
+ } = nextCaret;
3232
+ if ($isTableCellNode(origin)) {
3233
+ // not sure why ts isn't narrowing here (even if the guard is on nextCaret.origin)
3234
+ // but returning a new caret is fine
3235
+ if ($isChildCaret(nextCaret)) {
3236
+ return $getChildCaret(origin, initialCaret.direction);
3237
+ }
3238
+ } else if (!$isTableRowNode(origin)) {
3239
+ break;
3240
+ }
3241
+ }
3242
+ return null;
3243
+ }
3244
+ function $handleArrowKey(editor, event, direction, tableNode, tableObserver) {
3245
+ if ((direction === 'up' || direction === 'down') && isTypeaheadMenuInView(editor)) {
3246
+ return false;
3247
+ }
3248
+ const selection = $getSelection();
3249
+ if (!$isSelectionInTable(selection, tableNode)) {
3250
+ if ($isRangeSelection(selection)) {
3251
+ if (direction === 'backward') {
3252
+ if (selection.focus.offset > 0) {
3253
+ return false;
3254
+ }
3255
+ const parentNode = $getBlockParentIfFirstNode(selection.focus.getNode());
3256
+ if (!parentNode) {
3257
+ return false;
3258
+ }
3259
+ const siblingNode = parentNode.getPreviousSibling();
3260
+ if (!$isTableNode(siblingNode)) {
3261
+ return false;
3262
+ }
3263
+ stopEvent(event);
3264
+ if (event.shiftKey) {
3265
+ selection.focus.set(siblingNode.getParentOrThrow().getKey(), siblingNode.getIndexWithinParent(), 'element');
3266
+ } else {
3267
+ siblingNode.selectEnd();
3268
+ }
3269
+ return true;
3270
+ } else if (event.shiftKey && (direction === 'up' || direction === 'down')) {
3271
+ const focusNode = selection.focus.getNode();
3272
+ const isTableUnselect = !selection.isCollapsed() && (direction === 'up' && !selection.isBackward() || direction === 'down' && selection.isBackward());
3273
+ if (isTableUnselect) {
3274
+ let focusParentNode = $findMatchingParent(focusNode, n => $isTableNode(n));
3275
+ if ($isTableCellNode(focusParentNode)) {
3276
+ focusParentNode = $findMatchingParent(focusParentNode, $isTableNode);
3277
+ }
3278
+ if (focusParentNode !== tableNode) {
3279
+ return false;
3280
+ }
3281
+ if (!focusParentNode) {
3282
+ return false;
3283
+ }
3284
+ const sibling = direction === 'down' ? focusParentNode.getNextSibling() : focusParentNode.getPreviousSibling();
3285
+ if (!sibling) {
3286
+ return false;
3287
+ }
3288
+ let newOffset = 0;
3289
+ if (direction === 'up') {
3290
+ if ($isElementNode(sibling)) {
3291
+ newOffset = sibling.getChildrenSize();
3292
+ }
3293
+ }
3294
+ let newFocusNode = sibling;
3295
+ if (direction === 'up') {
3296
+ if ($isElementNode(sibling)) {
3297
+ const lastCell = sibling.getLastChild();
3298
+ newFocusNode = lastCell ? lastCell : sibling;
3299
+ newOffset = $isTextNode(newFocusNode) ? newFocusNode.getTextContentSize() : 0;
3300
+ }
3301
+ }
3302
+ const newSelection = selection.clone();
3303
+ newSelection.focus.set(newFocusNode.getKey(), newOffset, $isTextNode(newFocusNode) ? 'text' : 'element');
3304
+ $setSelection(newSelection);
3305
+ stopEvent(event);
3306
+ return true;
3307
+ } else if ($isRootOrShadowRoot(focusNode)) {
3308
+ const selectedNode = direction === 'up' ? selection.getNodes()[selection.getNodes().length - 1] : selection.getNodes()[0];
3309
+ if (selectedNode) {
3310
+ const tableCellNode = $findParentTableCellNodeInTable(tableNode, selectedNode);
3311
+ if (tableCellNode !== null) {
3312
+ const firstDescendant = tableNode.getFirstDescendant();
3313
+ const lastDescendant = tableNode.getLastDescendant();
3314
+ if (!firstDescendant || !lastDescendant) {
3315
+ return false;
3316
+ }
3317
+ const [firstCellNode] = $getNodeTriplet(firstDescendant);
3318
+ const [lastCellNode] = $getNodeTriplet(lastDescendant);
3319
+ const firstCellCoords = tableNode.getCordsFromCellNode(firstCellNode, tableObserver.table);
3320
+ const lastCellCoords = tableNode.getCordsFromCellNode(lastCellNode, tableObserver.table);
3321
+ const firstCellDOM = tableNode.getDOMCellFromCordsOrThrow(firstCellCoords.x, firstCellCoords.y, tableObserver.table);
3322
+ const lastCellDOM = tableNode.getDOMCellFromCordsOrThrow(lastCellCoords.x, lastCellCoords.y, tableObserver.table);
3323
+ tableObserver.$setAnchorCellForSelection(firstCellDOM);
3324
+ tableObserver.$setFocusCellForSelection(lastCellDOM, true);
3325
+ return true;
3326
+ }
3327
+ }
3328
+ return false;
3329
+ } else {
3330
+ let focusParentNode = $findMatchingParent(focusNode, n => $isElementNode(n) && !n.isInline());
3331
+ if ($isTableCellNode(focusParentNode)) {
3332
+ focusParentNode = $findMatchingParent(focusParentNode, $isTableNode);
3333
+ }
3334
+ if (!focusParentNode) {
3335
+ return false;
3336
+ }
3337
+ const sibling = direction === 'down' ? focusParentNode.getNextSibling() : focusParentNode.getPreviousSibling();
3338
+ if ($isTableNode(sibling) && tableObserver.tableNodeKey === sibling.getKey()) {
3339
+ const firstDescendant = sibling.getFirstDescendant();
3340
+ const lastDescendant = sibling.getLastDescendant();
3341
+ if (!firstDescendant || !lastDescendant) {
3342
+ return false;
3343
+ }
3344
+ const [firstCellNode] = $getNodeTriplet(firstDescendant);
3345
+ const [lastCellNode] = $getNodeTriplet(lastDescendant);
3346
+ const newSelection = selection.clone();
3347
+ newSelection.focus.set((direction === 'up' ? firstCellNode : lastCellNode).getKey(), direction === 'up' ? 0 : lastCellNode.getChildrenSize(), 'element');
3348
+ stopEvent(event);
3349
+ $setSelection(newSelection);
3350
+ return true;
3351
+ }
3352
+ }
3353
+ }
3354
+ }
3355
+ if (direction === 'down' && $isScrollableTablesActive(editor)) {
3356
+ // Enable Firefox workaround
3357
+ tableObserver.setShouldCheckSelection();
3358
+ }
3359
+ return false;
3360
+ }
3361
+ if ($isRangeSelection(selection)) {
3362
+ if (direction === 'backward' || direction === 'forward') {
3363
+ const alter = event.shiftKey ? 'extend' : 'move';
3364
+ return $handleHorizontalArrowKeyRangeSelection(editor, event, selection, alter, direction === 'backward', tableNode, tableObserver);
3365
+ }
3366
+ if (selection.isCollapsed()) {
3367
+ const {
3368
+ anchor,
3369
+ focus
3370
+ } = selection;
3371
+ const anchorCellNode = $findMatchingParent(anchor.getNode(), $isTableCellNode);
3372
+ const focusCellNode = $findMatchingParent(focus.getNode(), $isTableCellNode);
3373
+ if (!$isTableCellNode(anchorCellNode) || !anchorCellNode.is(focusCellNode)) {
3374
+ return false;
3375
+ }
3376
+ const anchorCellTable = $findTableNode(anchorCellNode);
3377
+ if (anchorCellTable !== tableNode && anchorCellTable != null) {
3378
+ const anchorCellTableElement = getTableElement(anchorCellTable, editor.getElementByKey(anchorCellTable.getKey()));
3379
+ if (anchorCellTableElement != null) {
3380
+ tableObserver.table = getTable(anchorCellTable, anchorCellTableElement);
3381
+ return $handleArrowKey(editor, event, direction, anchorCellTable, tableObserver);
3382
+ }
3383
+ }
3384
+ const anchorCellDom = editor.getElementByKey(anchorCellNode.__key);
3385
+ const anchorDOM = editor.getElementByKey(anchor.key);
3386
+ if (anchorDOM == null || anchorCellDom == null) {
3387
+ return false;
3388
+ }
3389
+ let edgeSelectionRect;
3390
+ if (anchor.type === 'element') {
3391
+ edgeSelectionRect = anchorDOM.getBoundingClientRect();
3392
+ } else {
3393
+ const domSelection = getDOMSelectionForEditor(editor);
3394
+ if (domSelection === null || domSelection.rangeCount === 0) {
3395
+ return false;
3396
+ }
3397
+ const range = domSelection.getRangeAt(0);
3398
+ edgeSelectionRect = range.getBoundingClientRect();
3399
+ }
3400
+ const edgeChild = direction === 'up' ? anchorCellNode.getFirstChild() : anchorCellNode.getLastChild();
3401
+ if (edgeChild == null) {
3402
+ return false;
3403
+ }
3404
+ const edgeChildDOM = editor.getElementByKey(edgeChild.__key);
3405
+ if (edgeChildDOM == null) {
3406
+ return false;
3407
+ }
3408
+ const edgeRect = edgeChildDOM.getBoundingClientRect();
3409
+ const isExiting = direction === 'up' ? edgeRect.top > edgeSelectionRect.top - edgeSelectionRect.height : edgeSelectionRect.bottom + edgeSelectionRect.height > edgeRect.bottom;
3410
+ if (isExiting) {
3411
+ stopEvent(event);
3412
+ const cords = tableNode.getCordsFromCellNode(anchorCellNode, tableObserver.table);
3413
+ if (event.shiftKey) {
3414
+ const cell = tableNode.getDOMCellFromCordsOrThrow(cords.x, cords.y, tableObserver.table);
3415
+ tableObserver.$setAnchorCellForSelection(cell);
3416
+ tableObserver.$setFocusCellForSelection(cell, true);
3417
+ } else {
3418
+ return selectTableNodeInDirection(tableObserver, tableNode, cords.x, cords.y, direction);
3419
+ }
3420
+ return true;
3421
+ }
3422
+ }
3423
+ } else if ($isTableSelection(selection)) {
3424
+ const {
3425
+ anchor,
3426
+ focus
3427
+ } = selection;
3428
+ const anchorCellNode = $findMatchingParent(anchor.getNode(), $isTableCellNode);
3429
+ const focusCellNode = $findMatchingParent(focus.getNode(), $isTableCellNode);
3430
+ const [tableNodeFromSelection] = selection.getNodes();
3431
+ if (!$isTableNode(tableNodeFromSelection)) {
3432
+ formatDevErrorMessage(`$handleArrowKey: TableSelection.getNodes()[0] expected to be TableNode`);
3433
+ }
3434
+ const tableElement = getTableElement(tableNodeFromSelection, editor.getElementByKey(tableNodeFromSelection.getKey()));
3435
+ if (!$isTableCellNode(anchorCellNode) || !$isTableCellNode(focusCellNode) || !$isTableNode(tableNodeFromSelection) || tableElement == null) {
3436
+ return false;
3437
+ }
3438
+ tableObserver.$updateTableTableSelection(selection);
3439
+ const grid = getTable(tableNodeFromSelection, tableElement);
3440
+ const cordsAnchor = tableNode.getCordsFromCellNode(anchorCellNode, grid);
3441
+ const anchorCell = tableNode.getDOMCellFromCordsOrThrow(cordsAnchor.x, cordsAnchor.y, grid);
3442
+ tableObserver.$setAnchorCellForSelection(anchorCell);
3443
+ stopEvent(event);
3444
+ if (event.shiftKey) {
3445
+ const [tableMap, anchorValue, focusValue] = $computeTableMap(tableNode, anchorCellNode, focusCellNode);
3446
+ return $adjustFocusInDirection(tableObserver, tableMap, anchorValue, focusValue, direction);
3447
+ } else {
3448
+ focusCellNode.selectEnd();
3449
+ }
3450
+ return true;
3451
+ }
3452
+ return false;
3453
+ }
3454
+ function stopEvent(event) {
3455
+ event.preventDefault();
3456
+ event.stopImmediatePropagation();
3457
+ event.stopPropagation();
3458
+ }
3459
+ function isTypeaheadMenuInView(editor) {
3460
+ // There is no inbuilt way to check if the component picker is in view
3461
+ // but we can check if the root DOM element has the aria-controls attribute "typeahead-menu".
3462
+ const root = editor.getRootElement();
3463
+ if (!root) {
3464
+ return false;
3465
+ }
3466
+ return root.hasAttribute('aria-controls') && root.getAttribute('aria-controls') === 'typeahead-menu';
3467
+ }
3468
+ function $insertParagraphAtTableEdge(edgePosition, tableNode, children) {
3469
+ const paragraphNode = $createParagraphNode();
3470
+ if (edgePosition === 'first') {
3471
+ tableNode.insertBefore(paragraphNode);
3472
+ } else {
3473
+ tableNode.insertAfter(paragraphNode);
3474
+ }
3475
+ paragraphNode.append(...(children || []));
3476
+ paragraphNode.selectEnd();
3477
+ }
3478
+ function $getTableEdgeCursorPosition(editor, selection, tableNode) {
3479
+ const tableNodeParent = tableNode.getParent();
3480
+ if (!tableNodeParent) {
3481
+ return undefined;
3482
+ }
3483
+
3484
+ // TODO: Add support for nested tables
3485
+ const domSelection = getDOMSelectionForEditor(editor);
3486
+ if (!domSelection) {
3487
+ return undefined;
3488
+ }
3489
+ const domAnchorNode = domSelection.anchorNode;
3490
+ const tableNodeParentDOM = editor.getElementByKey(tableNodeParent.getKey());
3491
+ const tableElement = getTableElement(tableNode, editor.getElementByKey(tableNode.getKey()));
3492
+ // We are only interested in the scenario where the
3493
+ // native selection anchor is:
3494
+ // - at or inside the table's parent DOM
3495
+ // - and NOT at or inside the table DOM
3496
+ // It may be adjacent to the table DOM (e.g. in a wrapper)
3497
+ if (!domAnchorNode || !tableNodeParentDOM || !tableElement || !tableNodeParentDOM.contains(domAnchorNode) || tableElement.contains(domAnchorNode)) {
3498
+ return undefined;
3499
+ }
3500
+ const anchorCellNode = $findMatchingParent(selection.anchor.getNode(), n => $isTableCellNode(n));
3501
+ if (!anchorCellNode) {
3502
+ return undefined;
3503
+ }
3504
+ const parentTable = $findMatchingParent(anchorCellNode, n => $isTableNode(n));
3505
+ if (!$isTableNode(parentTable) || !parentTable.is(tableNode)) {
3506
+ return undefined;
3507
+ }
3508
+ const [tableMap, cellValue] = $computeTableMap(tableNode, anchorCellNode, anchorCellNode);
3509
+ const firstCell = tableMap[0][0];
3510
+ const lastCell = tableMap[tableMap.length - 1][tableMap[0].length - 1];
3511
+ const {
3512
+ startRow,
3513
+ startColumn
3514
+ } = cellValue;
3515
+ const isAtFirstCell = startRow === firstCell.startRow && startColumn === firstCell.startColumn;
3516
+ const isAtLastCell = startRow === lastCell.startRow && startColumn === lastCell.startColumn;
3517
+ if (isAtFirstCell) {
3518
+ return 'first';
3519
+ } else if (isAtLastCell) {
3520
+ return 'last';
3521
+ } else {
3522
+ return undefined;
3523
+ }
3524
+ }
3525
+ function $getObserverCellFromCellNodeOrThrow(tableObserver, tableCellNode) {
3526
+ const {
3527
+ tableNode
3528
+ } = tableObserver.$lookup();
3529
+ const currentCords = tableNode.getCordsFromCellNode(tableCellNode, tableObserver.table);
3530
+ return tableNode.getDOMCellFromCordsOrThrow(currentCords.x, currentCords.y, tableObserver.table);
3531
+ }
3532
+ function $getNearestTableCellInTableFromDOMNode(tableNode, startingDOM, editorState) {
3533
+ return $findParentTableCellNodeInTable(tableNode, $getNearestNodeFromDOMNode(startingDOM, editorState));
3534
+ }
3535
+
3536
+ function isHTMLDivElement(element) {
3537
+ return isHTMLElement$1(element) && element.nodeName === 'DIV';
3538
+ }
3539
+ function updateColgroup(dom, config, colCount, colWidths) {
3540
+ const colGroup = dom.querySelector('colgroup');
3541
+ if (!colGroup) {
3542
+ return;
3543
+ }
3544
+ const cols = [];
3545
+ for (let i = 0; i < colCount; i++) {
3546
+ const col = document.createElement('col');
3547
+ const width = colWidths && colWidths[i];
3548
+ if (width) {
3549
+ col.style.width = `${width}px`;
3550
+ }
3551
+ cols.push(col);
3552
+ }
3553
+ colGroup.replaceChildren(...cols);
3554
+ }
3555
+ function setRowStriping(dom, config, rowStriping) {
3556
+ if (rowStriping) {
3557
+ addClassNamesToElement(dom, config.theme.tableRowStriping);
3558
+ dom.setAttribute('data-lexical-row-striping', 'true');
3559
+ } else {
3560
+ removeClassNamesFromElement(dom, config.theme.tableRowStriping);
3561
+ dom.removeAttribute('data-lexical-row-striping');
3562
+ }
3563
+ }
3564
+ function setFrozenColumns(dom, tableElement, config, frozenColumnCount) {
3565
+ if (frozenColumnCount > 0) {
3566
+ addClassNamesToElement(dom, config.theme.tableFrozenColumn);
3567
+ tableElement.setAttribute('data-lexical-frozen-column', 'true');
3568
+ } else {
3569
+ removeClassNamesFromElement(dom, config.theme.tableFrozenColumn);
3570
+ tableElement.removeAttribute('data-lexical-frozen-column');
3571
+ }
3572
+ }
3573
+ function setFrozenRows(dom, tableElement, config, frozenRowCount) {
3574
+ if (frozenRowCount > 0) {
3575
+ addClassNamesToElement(dom, config.theme.tableFrozenRow);
3576
+ tableElement.setAttribute('data-lexical-frozen-row', 'true');
3577
+ } else {
3578
+ removeClassNamesFromElement(dom, config.theme.tableFrozenRow);
3579
+ tableElement.removeAttribute('data-lexical-frozen-row');
3580
+ }
3581
+ }
3582
+ function alignTableElement(dom, config, formatType) {
3583
+ if (!config.theme.tableAlignment) {
3584
+ return;
3585
+ }
3586
+ const removeClasses = [];
3587
+ const addClasses = [];
3588
+ for (const format of ['center', 'right']) {
3589
+ const classes = config.theme.tableAlignment[format];
3590
+ if (!classes) {
3591
+ continue;
3592
+ }
3593
+ (format === formatType ? addClasses : removeClasses).push(classes);
3594
+ }
3595
+ removeClassNamesFromElement(dom, ...removeClasses);
3596
+ addClassNamesToElement(dom, ...addClasses);
3597
+ }
3598
+ const scrollableEditors = new WeakSet();
3599
+ function $isScrollableTablesActive(editor = $getEditor()) {
3600
+ return scrollableEditors.has(editor);
3601
+ }
3602
+ function setScrollableTablesActive(editor, active) {
3603
+ if (active) {
3604
+ if (!editor._config.theme.tableScrollableWrapper) {
3605
+ console.warn('TableNode: hasHorizontalScroll is active but theme.tableScrollableWrapper is not defined.');
3606
+ }
3607
+ scrollableEditors.add(editor);
3608
+ } else {
3609
+ scrollableEditors.delete(editor);
3610
+ }
3611
+ }
3612
+
3613
+ /** @noInheritDoc */
3614
+ class TableNode extends ElementNode {
3615
+ /** @internal */
3616
+ __rowStriping;
3617
+ __frozenColumnCount;
3618
+ __frozenRowCount;
3619
+ __colWidths;
3620
+ static getType() {
3621
+ return 'table';
3622
+ }
3623
+ getColWidths() {
3624
+ const self = this.getLatest();
3625
+ return self.__colWidths;
3626
+ }
3627
+ setColWidths(colWidths) {
3628
+ const self = this.getWritable();
3629
+ // NOTE: Node properties should be immutable. Freeze to prevent accidental mutation.
3630
+ self.__colWidths = colWidths !== undefined && true ? Object.freeze(colWidths) : colWidths;
3631
+ return self;
3632
+ }
3633
+ static clone(node) {
3634
+ return new TableNode(node.__key);
3635
+ }
3636
+ afterCloneFrom(prevNode) {
3637
+ super.afterCloneFrom(prevNode);
3638
+ this.__colWidths = prevNode.__colWidths;
3639
+ this.__rowStriping = prevNode.__rowStriping;
3640
+ this.__frozenColumnCount = prevNode.__frozenColumnCount;
3641
+ this.__frozenRowCount = prevNode.__frozenRowCount;
3642
+ }
3643
+ static importDOM() {
3644
+ return {
3645
+ table: _node => ({
3646
+ conversion: $convertTableElement,
3647
+ priority: 1
3648
+ })
3649
+ };
3650
+ }
3651
+ static importJSON(serializedNode) {
3652
+ return $createTableNode().updateFromJSON(serializedNode);
3653
+ }
3654
+ updateFromJSON(serializedNode) {
3655
+ return super.updateFromJSON(serializedNode).setRowStriping(serializedNode.rowStriping || false).setFrozenColumns(serializedNode.frozenColumnCount || 0).setFrozenRows(serializedNode.frozenRowCount || 0).setColWidths(serializedNode.colWidths);
3656
+ }
3657
+ constructor(key) {
3658
+ super(key);
3659
+ this.__rowStriping = false;
3660
+ this.__frozenColumnCount = 0;
3661
+ this.__frozenRowCount = 0;
3662
+ this.__colWidths = undefined;
3663
+ }
3664
+ exportJSON() {
3665
+ return {
3666
+ ...super.exportJSON(),
3667
+ colWidths: this.getColWidths(),
3668
+ frozenColumnCount: this.__frozenColumnCount ? this.__frozenColumnCount : undefined,
3669
+ frozenRowCount: this.__frozenRowCount ? this.__frozenRowCount : undefined,
3670
+ rowStriping: this.__rowStriping ? this.__rowStriping : undefined
3671
+ };
3672
+ }
3673
+ extractWithChild(child, selection, destination) {
3674
+ return destination === 'html';
3675
+ }
3676
+ getDOMSlot(element) {
3677
+ const tableElement = !isHTMLTableElement(element) ? element.querySelector('table') : element;
3678
+ if (!isHTMLTableElement(tableElement)) {
3679
+ formatDevErrorMessage(`TableNode.getDOMSlot: createDOM() did not return a table`);
3680
+ }
3681
+ return super.getDOMSlot(element).withElement(tableElement).withAfter(tableElement.querySelector('colgroup'));
3682
+ }
3683
+ createDOM(config, editor) {
3684
+ const tableElement = document.createElement('table');
3685
+ if (this.__style) {
3686
+ tableElement.style.cssText = this.__style;
3687
+ }
3688
+ const colGroup = document.createElement('colgroup');
3689
+ tableElement.appendChild(colGroup);
3690
+ setDOMUnmanaged(colGroup);
3691
+ addClassNamesToElement(tableElement, config.theme.table);
3692
+ this.updateTableElement(null, tableElement, config);
3693
+ if ($isScrollableTablesActive(editor)) {
3694
+ const wrapperElement = document.createElement('div');
3695
+ const classes = config.theme.tableScrollableWrapper;
3696
+ if (classes) {
3697
+ addClassNamesToElement(wrapperElement, classes);
3698
+ } else {
3699
+ wrapperElement.style.cssText = 'overflow-x: auto;';
3700
+ }
3701
+ wrapperElement.appendChild(tableElement);
3702
+ this.updateTableWrapper(null, wrapperElement, tableElement, config);
3703
+ return wrapperElement;
3704
+ }
3705
+ return tableElement;
3706
+ }
3707
+ updateTableWrapper(prevNode, tableWrapper, tableElement, config) {
3708
+ if (this.__frozenColumnCount !== (prevNode ? prevNode.__frozenColumnCount : 0)) {
3709
+ setFrozenColumns(tableWrapper, tableElement, config, this.__frozenColumnCount);
3710
+ }
3711
+ if (this.__frozenRowCount !== (prevNode ? prevNode.__frozenRowCount : 0)) {
3712
+ setFrozenRows(tableWrapper, tableElement, config, this.__frozenRowCount);
3713
+ }
3714
+ }
3715
+ updateTableElement(prevNode, tableElement, config) {
3716
+ if (this.__style !== (prevNode ? prevNode.__style : '')) {
3717
+ tableElement.style.cssText = this.__style;
3718
+ }
3719
+ if (this.__rowStriping !== (prevNode ? prevNode.__rowStriping : false)) {
3720
+ setRowStriping(tableElement, config, this.__rowStriping);
3721
+ }
3722
+ updateColgroup(tableElement, config, this.getColumnCount(), this.getColWidths());
3723
+ alignTableElement(tableElement, config, this.getFormatType());
3724
+ }
3725
+ updateDOM(prevNode, dom, config) {
3726
+ const slot = this.getDOMSlot(dom);
3727
+ const tableElement = slot.element;
3728
+ if (dom === tableElement === $isScrollableTablesActive()) {
3729
+ return true;
3730
+ }
3731
+ if (isHTMLDivElement(dom)) {
3732
+ this.updateTableWrapper(prevNode, dom, tableElement, config);
3733
+ }
3734
+ this.updateTableElement(prevNode, tableElement, config);
3735
+ return false;
3736
+ }
3737
+ exportDOM(editor) {
3738
+ const superExport = super.exportDOM(editor);
3739
+ const {
3740
+ element
3741
+ } = superExport;
3742
+ return {
3743
+ after: tableElement => {
3744
+ if (superExport.after) {
3745
+ tableElement = superExport.after(tableElement);
3746
+ }
3747
+ if (!isHTMLTableElement(tableElement) && isHTMLElement$1(tableElement)) {
3748
+ tableElement = tableElement.querySelector('table');
3749
+ }
3750
+ if (!isHTMLTableElement(tableElement)) {
3751
+ return null;
3752
+ }
3753
+ alignTableElement(tableElement, editor._config, this.getFormatType());
3754
+
3755
+ // Scan the table map to build a map of table cell key to the columns it needs
3756
+ const [tableMap] = $computeTableMapSkipCellCheck(this, null, null);
3757
+ const cellValues = new Map();
3758
+ for (const mapRow of tableMap) {
3759
+ for (const mapValue of mapRow) {
3760
+ const key = mapValue.cell.getKey();
3761
+ if (!cellValues.has(key)) {
3762
+ cellValues.set(key, {
3763
+ colSpan: mapValue.cell.getColSpan(),
3764
+ startColumn: mapValue.startColumn
3765
+ });
3766
+ }
3767
+ }
3768
+ }
3769
+
3770
+ // scan the DOM to find the table cell keys that were used and mark those columns
3771
+ const knownColumns = new Set();
3772
+ for (const cellDOM of tableElement.querySelectorAll(':scope > tr > [data-temporary-table-cell-lexical-key]')) {
3773
+ const key = cellDOM.getAttribute('data-temporary-table-cell-lexical-key');
3774
+ if (key) {
3775
+ const cellSpan = cellValues.get(key);
3776
+ cellDOM.removeAttribute('data-temporary-table-cell-lexical-key');
3777
+ if (cellSpan) {
3778
+ cellValues.delete(key);
3779
+ for (let i = 0; i < cellSpan.colSpan; i++) {
3780
+ knownColumns.add(i + cellSpan.startColumn);
3781
+ }
3782
+ }
3783
+ }
3784
+ }
3785
+
3786
+ // Compute the colgroup and columns in the export
3787
+ const colGroup = tableElement.querySelector(':scope > colgroup');
3788
+ if (colGroup) {
3789
+ // Only include the <col /> for rows that are in the output
3790
+ const cols = Array.from(tableElement.querySelectorAll(':scope > colgroup > col')).filter((dom, i) => knownColumns.has(i));
3791
+ colGroup.replaceChildren(...cols);
3792
+ }
3793
+
3794
+ // Wrap direct descendant rows in a tbody for export
3795
+ const rows = tableElement.querySelectorAll(':scope > tr');
3796
+ if (rows.length > 0) {
3797
+ const tBody = document.createElement('tbody');
3798
+ for (const row of rows) {
3799
+ tBody.appendChild(row);
3800
+ }
3801
+ tableElement.append(tBody);
3802
+ }
3803
+ return tableElement;
3804
+ },
3805
+ element: !isHTMLTableElement(element) && isHTMLElement$1(element) ? element.querySelector('table') : element
3806
+ };
3807
+ }
3808
+ canBeEmpty() {
3809
+ return false;
3810
+ }
3811
+ isShadowRoot() {
3812
+ return true;
3813
+ }
3814
+ getCordsFromCellNode(tableCellNode, table) {
3815
+ const {
3816
+ rows,
3817
+ domRows
3818
+ } = table;
3819
+ for (let y = 0; y < rows; y++) {
3820
+ const row = domRows[y];
3821
+ if (row == null) {
3822
+ continue;
3823
+ }
3824
+ for (let x = 0; x < row.length; x++) {
3825
+ const cell = row[x];
3826
+ if (cell == null) {
3827
+ continue;
3828
+ }
3829
+ const {
3830
+ elem
3831
+ } = cell;
3832
+ const cellNode = $getNearestTableCellInTableFromDOMNode(this, elem);
3833
+ if (cellNode !== null && tableCellNode.is(cellNode)) {
3834
+ return {
3835
+ x,
3836
+ y
3837
+ };
3838
+ }
3839
+ }
3840
+ }
3841
+ throw new Error('Cell not found in table.');
3842
+ }
3843
+ getDOMCellFromCords(x, y, table) {
3844
+ const {
3845
+ domRows
3846
+ } = table;
3847
+ const row = domRows[y];
3848
+ if (row == null) {
3849
+ return null;
3850
+ }
3851
+ const index = x < row.length ? x : row.length - 1;
3852
+ const cell = row[index];
3853
+ if (cell == null) {
3854
+ return null;
3855
+ }
3856
+ return cell;
3857
+ }
3858
+ getDOMCellFromCordsOrThrow(x, y, table) {
3859
+ const cell = this.getDOMCellFromCords(x, y, table);
3860
+ if (!cell) {
3861
+ throw new Error('Cell not found at cords.');
3862
+ }
3863
+ return cell;
3864
+ }
3865
+ getCellNodeFromCords(x, y, table) {
3866
+ const cell = this.getDOMCellFromCords(x, y, table);
3867
+ if (cell == null) {
3868
+ return null;
3869
+ }
3870
+ const node = $getNearestNodeFromDOMNode(cell.elem);
3871
+ if ($isTableCellNode(node)) {
3872
+ return node;
3873
+ }
3874
+ return null;
3875
+ }
3876
+ getCellNodeFromCordsOrThrow(x, y, table) {
3877
+ const node = this.getCellNodeFromCords(x, y, table);
3878
+ if (!node) {
3879
+ throw new Error('Node at cords not TableCellNode.');
3880
+ }
3881
+ return node;
3882
+ }
3883
+ getRowStriping() {
3884
+ return Boolean(this.getLatest().__rowStriping);
3885
+ }
3886
+ setRowStriping(newRowStriping) {
3887
+ const self = this.getWritable();
3888
+ self.__rowStriping = newRowStriping;
3889
+ return self;
3890
+ }
3891
+ setFrozenColumns(columnCount) {
3892
+ const self = this.getWritable();
3893
+ self.__frozenColumnCount = columnCount;
3894
+ return self;
3895
+ }
3896
+ getFrozenColumns() {
3897
+ return this.getLatest().__frozenColumnCount;
3898
+ }
3899
+ setFrozenRows(rowCount) {
3900
+ const self = this.getWritable();
3901
+ self.__frozenRowCount = rowCount;
3902
+ return self;
3903
+ }
3904
+ getFrozenRows() {
3905
+ return this.getLatest().__frozenRowCount;
3906
+ }
3907
+ canSelectBefore() {
3908
+ return true;
3909
+ }
3910
+ canIndent() {
3911
+ return false;
3912
+ }
3913
+ getColumnCount() {
3914
+ const firstRow = this.getFirstChild();
3915
+ if (!firstRow) {
3916
+ return 0;
3917
+ }
3918
+ let columnCount = 0;
3919
+ firstRow.getChildren().forEach(cell => {
3920
+ if ($isTableCellNode(cell)) {
3921
+ columnCount += cell.getColSpan();
3922
+ }
3923
+ });
3924
+ return columnCount;
3925
+ }
3926
+ }
3927
+ function $getElementForTableNode(editor, tableNode) {
3928
+ const tableElement = editor.getElementByKey(tableNode.getKey());
3929
+ if (!(tableElement !== null)) {
3930
+ formatDevErrorMessage(`$getElementForTableNode: Table Element Not Found`);
3931
+ }
3932
+ return getTable(tableNode, tableElement);
3933
+ }
3934
+ function $convertTableElement(domNode) {
3935
+ const tableNode = $createTableNode();
3936
+ if (domNode.hasAttribute('data-lexical-row-striping')) {
3937
+ tableNode.setRowStriping(true);
3938
+ }
3939
+ if (domNode.hasAttribute('data-lexical-frozen-column')) {
3940
+ tableNode.setFrozenColumns(1);
3941
+ }
3942
+ if (domNode.hasAttribute('data-lexical-frozen-row')) {
3943
+ tableNode.setFrozenRows(1);
3944
+ }
3945
+ const colGroup = domNode.querySelector(':scope > colgroup');
3946
+ if (colGroup) {
3947
+ let columns = [];
3948
+ for (const col of colGroup.querySelectorAll(':scope > col')) {
3949
+ let width = col.style.width || '';
3950
+ if (!PIXEL_VALUE_REG_EXP.test(width)) {
3951
+ // Also support deprecated width attribute for google docs
3952
+ width = col.getAttribute('width') || '';
3953
+ if (!/^\d+$/.test(width)) {
3954
+ columns = undefined;
3955
+ break;
3956
+ }
3957
+ }
3958
+ columns.push(parseFloat(width));
3959
+ }
3960
+ if (columns) {
3961
+ tableNode.setColWidths(columns);
3962
+ }
3963
+ }
3964
+ return {
3965
+ after: children => $descendantsMatching(children, $isTableRowNode),
3966
+ node: tableNode
3967
+ };
3968
+ }
3969
+ function $createTableNode() {
3970
+ return $applyNodeReplacement(new TableNode());
3971
+ }
3972
+ function $isTableNode(node) {
3973
+ return node instanceof TableNode;
3974
+ }
3975
+
3976
+ function $insertTable({
3977
+ rows,
3978
+ columns,
3979
+ includeHeaders
3980
+ }, hasNestedTables) {
3981
+ const selection = $getSelection() || $getPreviousSelection();
3982
+ if (!selection || !$isRangeSelection(selection)) {
3983
+ return false;
3984
+ }
3985
+
3986
+ // Prevent nested tables by checking if we're already inside a table
3987
+ if (!hasNestedTables && $findTableNode(selection.anchor.getNode())) {
3988
+ return false;
3989
+ }
3990
+ const tableNode = $createTableNodeWithDimensions(Number(rows), Number(columns), includeHeaders);
3991
+ $insertNodeToNearestRoot(tableNode);
3992
+ const firstDescendant = tableNode.getFirstDescendant();
3993
+ if ($isTextNode(firstDescendant)) {
3994
+ firstDescendant.select();
3995
+ }
3996
+ return true;
3997
+ }
3998
+ function $tableCellTransform(node) {
3999
+ if (!$isTableRowNode(node.getParent())) {
4000
+ // TableCellNode must be a child of TableRowNode.
4001
+ node.remove();
4002
+ } else if (node.isEmpty()) {
4003
+ // TableCellNode should never be empty
4004
+ node.append($createParagraphNode());
4005
+ }
4006
+ }
4007
+ function $tableRowTransform(node) {
4008
+ if (!$isTableNode(node.getParent())) {
4009
+ // TableRowNode must be a child of TableNode.
4010
+ // TODO: Future support of tbody/thead/tfoot may change this
4011
+ node.remove();
4012
+ } else {
4013
+ $unwrapAndFilterDescendants(node, $isTableCellNode);
4014
+ }
4015
+ }
4016
+ function $tableTransform(node) {
4017
+ // TableRowNode is the only valid child for TableNode
4018
+ // TODO: Future support of tbody/thead/tfoot/caption may change this
4019
+ $unwrapAndFilterDescendants(node, $isTableRowNode);
4020
+ const [gridMap] = $computeTableMapSkipCellCheck(node, null, null);
4021
+ const maxRowLength = gridMap.reduce((curLength, row) => {
4022
+ return Math.max(curLength, row.length);
4023
+ }, 0);
4024
+ const rowNodes = node.getChildren();
4025
+ for (let i = 0; i < gridMap.length; ++i) {
4026
+ const rowNode = rowNodes[i];
4027
+ if (!rowNode) {
4028
+ continue;
4029
+ }
4030
+ if (!$isTableRowNode(rowNode)) {
4031
+ formatDevErrorMessage(`TablePlugin: Expecting all children of TableNode to be TableRowNode, found ${rowNode.constructor.name} (type ${rowNode.getType()})`);
4032
+ }
4033
+ const rowLength = gridMap[i].reduce((acc, cell) => cell ? 1 + acc : acc, 0);
4034
+ if (rowLength === maxRowLength) {
4035
+ continue;
4036
+ }
4037
+ for (let j = rowLength; j < maxRowLength; ++j) {
4038
+ // TODO: inherit header state from another header or body
4039
+ const newCell = $createTableCellNode();
4040
+ newCell.append($createParagraphNode());
4041
+ rowNode.append(newCell);
4042
+ }
4043
+ }
4044
+ const colWidths = node.getColWidths();
4045
+ const columnCount = node.getColumnCount();
4046
+ if (colWidths && colWidths.length !== columnCount) {
4047
+ let newColWidths = undefined;
4048
+ if (columnCount < colWidths.length) {
4049
+ newColWidths = colWidths.slice(0, columnCount);
4050
+ } else if (colWidths.length > 0) {
4051
+ // Repeat the last column width.
4052
+ const fillWidth = colWidths[colWidths.length - 1];
4053
+ newColWidths = [...colWidths, ...Array(columnCount - colWidths.length).fill(fillWidth)];
4054
+ }
4055
+ node.setColWidths(newColWidths);
4056
+ }
4057
+ }
4058
+ function $tableClickCommand(event) {
4059
+ if (event.detail < 3 || !isDOMNode(event.target)) {
4060
+ return false;
4061
+ }
4062
+ const startNode = $getNearestNodeFromDOMNode(event.target);
4063
+ if (startNode === null) {
4064
+ return false;
4065
+ }
4066
+ const blockNode = $findMatchingParent(startNode, node => $isElementNode(node) && !node.isInline());
4067
+ if (blockNode === null) {
4068
+ return false;
4069
+ }
4070
+ const rootNode = blockNode.getParent();
4071
+ if (!$isTableCellNode(rootNode)) {
4072
+ return false;
4073
+ }
4074
+ blockNode.select(0);
4075
+ return true;
4076
+ }
4077
+
4078
+ /**
4079
+ * Register a transform to ensure that all TableCellNode have a colSpan and rowSpan of 1.
4080
+ * This should only be registered when you do not want to support merged cells.
4081
+ *
4082
+ * @param editor The editor
4083
+ * @returns An unregister callback
4084
+ */
4085
+ function registerTableCellUnmergeTransform(editor) {
4086
+ return editor.registerNodeTransform(TableCellNode, node => {
4087
+ if (node.getColSpan() > 1 || node.getRowSpan() > 1) {
4088
+ // When we have rowSpan we have to map the entire Table to understand where the new Cells
4089
+ // fit best; let's analyze all Cells at once to save us from further transform iterations
4090
+ const [,, gridNode] = $getNodeTriplet(node);
4091
+ const [gridMap] = $computeTableMap(gridNode, node, node);
4092
+ // TODO this function expects Tables to be normalized. Look into this once it exists
4093
+ const rowsCount = gridMap.length;
4094
+ const columnsCount = gridMap[0].length;
4095
+ let row = gridNode.getFirstChild();
4096
+ if (!$isTableRowNode(row)) {
4097
+ formatDevErrorMessage(`Expected TableNode first child to be a RowNode`);
4098
+ }
4099
+ const unmerged = [];
4100
+ for (let i = 0; i < rowsCount; i++) {
4101
+ if (i !== 0) {
4102
+ row = row.getNextSibling();
4103
+ if (!$isTableRowNode(row)) {
4104
+ formatDevErrorMessage(`Expected TableNode first child to be a RowNode`);
4105
+ }
4106
+ }
4107
+ let lastRowCell = null;
4108
+ for (let j = 0; j < columnsCount; j++) {
4109
+ const cellMap = gridMap[i][j];
4110
+ const cell = cellMap.cell;
4111
+ if (cellMap.startRow === i && cellMap.startColumn === j) {
4112
+ lastRowCell = cell;
4113
+ unmerged.push(cell);
4114
+ } else if (cell.getColSpan() > 1 || cell.getRowSpan() > 1) {
4115
+ if (!$isTableCellNode(cell)) {
4116
+ formatDevErrorMessage(`Expected TableNode cell to be a TableCellNode`);
4117
+ }
4118
+ const newCell = $createTableCellNode(cell.__headerState);
4119
+ if (lastRowCell !== null) {
4120
+ lastRowCell.insertAfter(newCell);
4121
+ } else {
4122
+ $insertFirst$1(row, newCell);
4123
+ }
4124
+ }
4125
+ }
4126
+ }
4127
+ for (const cell of unmerged) {
4128
+ cell.setColSpan(1);
4129
+ cell.setRowSpan(1);
4130
+ }
4131
+ }
4132
+ });
4133
+ }
4134
+ function registerTableSelectionObserver(editor, hasTabHandler = true) {
4135
+ const tableSelections = new Map();
4136
+ const initializeTableNode = (tableNode, nodeKey, dom) => {
4137
+ const tableElement = getTableElement(tableNode, dom);
4138
+ const tableSelection = applyTableHandlers(tableNode, tableElement, editor, hasTabHandler);
4139
+ tableSelections.set(nodeKey, [tableSelection, tableElement]);
4140
+ };
4141
+ const unregisterMutationListener = editor.registerMutationListener(TableNode, nodeMutations => {
4142
+ editor.getEditorState().read(() => {
4143
+ for (const [nodeKey, mutation] of nodeMutations) {
4144
+ const tableSelection = tableSelections.get(nodeKey);
4145
+ if (mutation === 'created' || mutation === 'updated') {
4146
+ const {
4147
+ tableNode,
4148
+ tableElement
4149
+ } = $getTableAndElementByKey(nodeKey);
4150
+ if (tableSelection === undefined) {
4151
+ initializeTableNode(tableNode, nodeKey, tableElement);
4152
+ } else if (tableElement !== tableSelection[1]) {
4153
+ // The update created a new DOM node, destroy the existing TableObserver
4154
+ tableSelection[0].removeListeners();
4155
+ tableSelections.delete(nodeKey);
4156
+ initializeTableNode(tableNode, nodeKey, tableElement);
4157
+ }
4158
+ } else if (mutation === 'destroyed') {
4159
+ if (tableSelection !== undefined) {
4160
+ tableSelection[0].removeListeners();
4161
+ tableSelections.delete(nodeKey);
4162
+ }
4163
+ }
4164
+ }
4165
+ }, {
4166
+ editor
4167
+ });
4168
+ }, {
4169
+ skipInitialization: false
4170
+ });
4171
+ return () => {
4172
+ unregisterMutationListener();
4173
+ // Hook might be called multiple times so cleaning up tables listeners as well,
4174
+ // as it'll be reinitialized during recurring call
4175
+ for (const [, [tableSelection]] of tableSelections) {
4176
+ tableSelection.removeListeners();
4177
+ }
4178
+ };
4179
+ }
4180
+
4181
+ /**
4182
+ * Register the INSERT_TABLE_COMMAND listener and the table integrity transforms. The
4183
+ * table selection observer should be registered separately after this with
4184
+ * {@link registerTableSelectionObserver}.
4185
+ *
4186
+ * @param editor The editor
4187
+ * @returns An unregister callback
4188
+ */
4189
+ function registerTablePlugin(editor, options) {
4190
+ if (!editor.hasNodes([TableNode])) {
4191
+ {
4192
+ formatDevErrorMessage(`TablePlugin: TableNode is not registered on editor`);
4193
+ }
4194
+ }
4195
+ const {
4196
+ hasNestedTables = signal(false)
4197
+ } = options ?? {};
4198
+ return mergeRegister(editor.registerCommand(INSERT_TABLE_COMMAND, payload => {
4199
+ return $insertTable(payload, hasNestedTables.peek());
4200
+ }, COMMAND_PRIORITY_EDITOR), editor.registerCommand(SELECTION_INSERT_CLIPBOARD_NODES_COMMAND, ({
4201
+ nodes,
4202
+ selection
4203
+ }, dispatchEditor) => {
4204
+ if (hasNestedTables.peek() || editor !== dispatchEditor || !$isRangeSelection(selection)) {
4205
+ return false;
4206
+ }
4207
+ const isInsideTableCell = $findTableNode(selection.anchor.getNode()) !== null;
4208
+ return isInsideTableCell && nodes.some($isTableNode);
4209
+ }, COMMAND_PRIORITY_EDITOR), editor.registerCommand(CLICK_COMMAND, $tableClickCommand, COMMAND_PRIORITY_EDITOR), editor.registerNodeTransform(TableNode, $tableTransform), editor.registerNodeTransform(TableRowNode, $tableRowTransform), editor.registerNodeTransform(TableCellNode, $tableCellTransform));
4210
+ }
4211
+
4212
+ /**
4213
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
4214
+ *
4215
+ * This source code is licensed under the MIT license found in the
4216
+ * LICENSE file in the root directory of this source tree.
4217
+ *
4218
+ */
4219
+
4220
+ /**
4221
+ * Configures {@link TableNode}, {@link TableRowNode}, {@link TableCellNode} and
4222
+ * registers table behaviors (see {@link TableConfig})
4223
+ */
4224
+ const TableExtension = defineExtension({
4225
+ build(editor, config, state) {
4226
+ return namedSignals(config);
4227
+ },
4228
+ config: safeCast({
4229
+ hasCellBackgroundColor: true,
4230
+ hasCellMerge: true,
4231
+ hasHorizontalScroll: true,
4232
+ hasNestedTables: false,
4233
+ hasTabHandler: true
4234
+ }),
4235
+ name: '@ekz/lexical-table/Table',
4236
+ nodes: () => [TableNode, TableRowNode, TableCellNode],
4237
+ register(editor, config, state) {
4238
+ const stores = state.getOutput();
4239
+ const {
4240
+ hasNestedTables
4241
+ } = stores;
4242
+ return mergeRegister(effect(() => {
4243
+ const hasHorizontalScroll = stores.hasHorizontalScroll.value;
4244
+ const hadHorizontalScroll = $isScrollableTablesActive(editor);
4245
+ if (hadHorizontalScroll !== hasHorizontalScroll) {
4246
+ setScrollableTablesActive(editor, hasHorizontalScroll);
4247
+ // Registering the transform has the side-effect of marking all existing
4248
+ // TableNodes as dirty. The handler is immediately unregistered.
4249
+ editor.registerNodeTransform(TableNode, () => {})();
4250
+ }
4251
+ }), registerTablePlugin(editor, {
4252
+ hasNestedTables
4253
+ }), effect(() => registerTableSelectionObserver(editor, stores.hasTabHandler.value)), effect(() => stores.hasCellMerge.value ? undefined : registerTableCellUnmergeTransform(editor)), effect(() => stores.hasCellBackgroundColor.value ? undefined : editor.registerNodeTransform(TableCellNode, node => {
4254
+ if (node.getBackgroundColor() !== null) {
4255
+ node.setBackgroundColor(null);
4256
+ }
4257
+ })));
4258
+ }
4259
+ });
4260
+
4261
+ export { $computeTableMap, $computeTableMapSkipCellCheck, $createTableCellNode, $createTableNode, $createTableNodeWithDimensions, $createTableRowNode, $createTableSelection, $createTableSelectionFrom, $deleteTableColumn, $deleteTableColumnAtSelection, $deleteTableColumn__EXPERIMENTAL, $deleteTableRowAtSelection, $deleteTableRow__EXPERIMENTAL, $findCellNode, $findTableNode, $getElementForTableNode, $getNodeTriplet, $getTableAndElementByKey, $getTableCellNodeFromLexicalNode, $getTableCellNodeRect, $getTableColumnIndexFromTableCellNode, $getTableNodeFromLexicalNodeOrThrow, $getTableRowIndexFromTableCellNode, $getTableRowNodeFromTableCellNodeOrThrow, $insertTableColumn, $insertTableColumnAtSelection, $insertTableColumn__EXPERIMENTAL, $insertTableRow, $insertTableRowAtSelection, $insertTableRow__EXPERIMENTAL, $isScrollableTablesActive, $isTableCellNode, $isTableNode, $isTableRowNode, $isTableSelection, $mergeCells, $removeTableRowAtIndex, $unmergeCell, INSERT_TABLE_COMMAND, TableCellHeaderStates, TableCellNode, TableExtension, TableNode, TableObserver, TableRowNode, applyTableHandlers, getDOMCellFromTarget, getTableElement, getTableObserverFromTableElement, registerTableCellUnmergeTransform, registerTablePlugin, registerTableSelectionObserver, setScrollableTablesActive };