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