@lexical/table 0.44.1-nightly.20260519.0 → 0.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/{LexicalTable.dev.js → dist/LexicalTable.dev.js} +243 -33
- package/{LexicalTable.dev.mjs → dist/LexicalTable.dev.mjs} +239 -33
- package/{LexicalTable.mjs → dist/LexicalTable.mjs} +4 -0
- package/{LexicalTable.node.mjs → dist/LexicalTable.node.mjs} +4 -0
- package/dist/LexicalTable.prod.js +9 -0
- package/dist/LexicalTable.prod.mjs +9 -0
- package/dist/TableImportExtension.d.ts +38 -0
- package/{index.d.ts → dist/index.d.ts} +1 -0
- package/package.json +34 -18
- package/src/LexicalTableCellNode.ts +479 -0
- package/src/LexicalTableCommands.ts +27 -0
- package/src/LexicalTableExtension.ts +104 -0
- package/src/LexicalTableNode.ts +678 -0
- package/src/LexicalTableObserver.ts +575 -0
- package/src/LexicalTablePluginHelpers.ts +694 -0
- package/src/LexicalTableRowNode.ts +154 -0
- package/src/LexicalTableSelection.ts +460 -0
- package/src/LexicalTableSelectionHelpers.ts +2409 -0
- package/src/LexicalTableUtils.ts +1386 -0
- package/src/TableImportExtension.ts +302 -0
- package/src/constants.ts +13 -0
- package/src/index.ts +97 -0
- package/LexicalTable.prod.js +0 -9
- package/LexicalTable.prod.mjs +0 -9
- /package/{LexicalTable.js → dist/LexicalTable.js} +0 -0
- /package/{LexicalTable.js.flow → dist/LexicalTable.js.flow} +0 -0
- /package/{LexicalTableCellNode.d.ts → dist/LexicalTableCellNode.d.ts} +0 -0
- /package/{LexicalTableCommands.d.ts → dist/LexicalTableCommands.d.ts} +0 -0
- /package/{LexicalTableExtension.d.ts → dist/LexicalTableExtension.d.ts} +0 -0
- /package/{LexicalTableNode.d.ts → dist/LexicalTableNode.d.ts} +0 -0
- /package/{LexicalTableObserver.d.ts → dist/LexicalTableObserver.d.ts} +0 -0
- /package/{LexicalTablePluginHelpers.d.ts → dist/LexicalTablePluginHelpers.d.ts} +0 -0
- /package/{LexicalTableRowNode.d.ts → dist/LexicalTableRowNode.d.ts} +0 -0
- /package/{LexicalTableSelection.d.ts → dist/LexicalTableSelection.d.ts} +0 -0
- /package/{LexicalTableSelectionHelpers.d.ts → dist/LexicalTableSelectionHelpers.d.ts} +0 -0
- /package/{LexicalTableUtils.d.ts → dist/LexicalTableUtils.d.ts} +0 -0
- /package/{constants.d.ts → dist/constants.d.ts} +0 -0
|
@@ -0,0 +1,2409 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type {TableCellNode} from './LexicalTableCellNode';
|
|
10
|
+
import type {
|
|
11
|
+
TableDOMCell,
|
|
12
|
+
TableDOMRows,
|
|
13
|
+
TableObservers,
|
|
14
|
+
} from './LexicalTableObserver';
|
|
15
|
+
import type {
|
|
16
|
+
TableMapType,
|
|
17
|
+
TableMapValueType,
|
|
18
|
+
TableSelection,
|
|
19
|
+
} from './LexicalTableSelection';
|
|
20
|
+
import type {
|
|
21
|
+
BaseSelection,
|
|
22
|
+
CaretDirection,
|
|
23
|
+
ChildCaret,
|
|
24
|
+
EditorState,
|
|
25
|
+
ElementNode,
|
|
26
|
+
LexicalCommand,
|
|
27
|
+
LexicalEditor,
|
|
28
|
+
LexicalNode,
|
|
29
|
+
PointCaret,
|
|
30
|
+
RangeSelection,
|
|
31
|
+
SiblingCaret,
|
|
32
|
+
} from 'lexical';
|
|
33
|
+
|
|
34
|
+
import {
|
|
35
|
+
$getClipboardDataFromSelection,
|
|
36
|
+
copyToClipboard,
|
|
37
|
+
} from '@lexical/clipboard';
|
|
38
|
+
import invariant from '@lexical/internal/invariant';
|
|
39
|
+
import {
|
|
40
|
+
$findMatchingParent,
|
|
41
|
+
addClassNamesToElement,
|
|
42
|
+
objectKlassEquals,
|
|
43
|
+
removeClassNamesFromElement,
|
|
44
|
+
} from '@lexical/utils';
|
|
45
|
+
import {
|
|
46
|
+
$caretFromPoint,
|
|
47
|
+
$createParagraphNode,
|
|
48
|
+
$createRangeSelectionFromDom,
|
|
49
|
+
$createTextNode,
|
|
50
|
+
$extendCaretToRange,
|
|
51
|
+
$getAdjacentChildCaret,
|
|
52
|
+
$getChildCaret,
|
|
53
|
+
$getNearestNodeFromDOMNode,
|
|
54
|
+
$getNodeByKeyOrThrow,
|
|
55
|
+
$getPreviousSelection,
|
|
56
|
+
$getSelection,
|
|
57
|
+
$getSiblingCaret,
|
|
58
|
+
$isChildCaret,
|
|
59
|
+
$isElementNode,
|
|
60
|
+
$isExtendableTextPointCaret,
|
|
61
|
+
$isRangeSelection,
|
|
62
|
+
$isRootOrShadowRoot,
|
|
63
|
+
$isSiblingCaret,
|
|
64
|
+
$isTextNode,
|
|
65
|
+
$normalizeCaret,
|
|
66
|
+
$setPointFromCaret,
|
|
67
|
+
$setSelection,
|
|
68
|
+
COMMAND_PRIORITY_HIGH,
|
|
69
|
+
CONTROLLED_TEXT_INSERTION_COMMAND,
|
|
70
|
+
CUT_COMMAND,
|
|
71
|
+
DELETE_CHARACTER_COMMAND,
|
|
72
|
+
DELETE_LINE_COMMAND,
|
|
73
|
+
DELETE_WORD_COMMAND,
|
|
74
|
+
FOCUS_COMMAND,
|
|
75
|
+
FORMAT_ELEMENT_COMMAND,
|
|
76
|
+
FORMAT_TEXT_COMMAND,
|
|
77
|
+
getDOMSelection,
|
|
78
|
+
INSERT_PARAGRAPH_COMMAND,
|
|
79
|
+
IS_FIREFOX,
|
|
80
|
+
isDOMNode,
|
|
81
|
+
isHTMLElement,
|
|
82
|
+
KEY_ARROW_DOWN_COMMAND,
|
|
83
|
+
KEY_ARROW_LEFT_COMMAND,
|
|
84
|
+
KEY_ARROW_RIGHT_COMMAND,
|
|
85
|
+
KEY_ARROW_UP_COMMAND,
|
|
86
|
+
KEY_BACKSPACE_COMMAND,
|
|
87
|
+
KEY_DELETE_COMMAND,
|
|
88
|
+
KEY_ESCAPE_COMMAND,
|
|
89
|
+
KEY_TAB_COMMAND,
|
|
90
|
+
SELECTION_CHANGE_COMMAND,
|
|
91
|
+
} from 'lexical';
|
|
92
|
+
|
|
93
|
+
import {$isTableCellNode} from './LexicalTableCellNode';
|
|
94
|
+
import {
|
|
95
|
+
$getElementForTableNode,
|
|
96
|
+
$isScrollableTablesActive,
|
|
97
|
+
$isTableNode,
|
|
98
|
+
TableNode,
|
|
99
|
+
} from './LexicalTableNode';
|
|
100
|
+
import {TableDOMTable, TableObserver} from './LexicalTableObserver';
|
|
101
|
+
import {$isTableRowNode} from './LexicalTableRowNode';
|
|
102
|
+
import {$isTableSelection} from './LexicalTableSelection';
|
|
103
|
+
import {
|
|
104
|
+
$computeTableCellRectBoundary,
|
|
105
|
+
$computeTableCellRectSpans,
|
|
106
|
+
$computeTableMap,
|
|
107
|
+
$getNodeTriplet,
|
|
108
|
+
TableCellRectBoundary,
|
|
109
|
+
} from './LexicalTableUtils';
|
|
110
|
+
|
|
111
|
+
const LEXICAL_ELEMENT_KEY = '__lexicalTableSelection';
|
|
112
|
+
|
|
113
|
+
const isPointerDownOnEvent = (event: PointerEvent) => {
|
|
114
|
+
return (event.buttons & 1) === 1;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export function isHTMLTableElement(el: unknown): el is HTMLTableElement {
|
|
118
|
+
return isHTMLElement(el) && el.nodeName === 'TABLE';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function getTableElement<T extends HTMLElement | null>(
|
|
122
|
+
tableNode: TableNode,
|
|
123
|
+
dom: T,
|
|
124
|
+
): HTMLTableElementWithWithTableSelectionState | (T & null) {
|
|
125
|
+
if (!dom) {
|
|
126
|
+
return dom as T & null;
|
|
127
|
+
}
|
|
128
|
+
const element: null | HTMLTableElementWithWithTableSelectionState =
|
|
129
|
+
isHTMLTableElement(dom) ? dom : dom.querySelector('table');
|
|
130
|
+
invariant(
|
|
131
|
+
isHTMLTableElement(element),
|
|
132
|
+
'getTableElement: Expecting table in DOM node for %s of type %s with key %s, not %s',
|
|
133
|
+
tableNode.constructor.name,
|
|
134
|
+
tableNode.getType(),
|
|
135
|
+
tableNode.getKey(),
|
|
136
|
+
dom.nodeName,
|
|
137
|
+
);
|
|
138
|
+
return element;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function getEditorWindow(editor: LexicalEditor): Window | null {
|
|
142
|
+
return editor._window;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function $findParentTableCellNodeInTable(
|
|
146
|
+
tableNode: LexicalNode,
|
|
147
|
+
node: LexicalNode | null,
|
|
148
|
+
): TableCellNode | null {
|
|
149
|
+
for (
|
|
150
|
+
let currentNode = node, lastTableCellNode: TableCellNode | null = null;
|
|
151
|
+
currentNode !== null;
|
|
152
|
+
currentNode = currentNode.getParent()
|
|
153
|
+
) {
|
|
154
|
+
if (tableNode.is(currentNode)) {
|
|
155
|
+
return lastTableCellNode;
|
|
156
|
+
} else if ($isTableCellNode(currentNode)) {
|
|
157
|
+
lastTableCellNode = currentNode;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const ARROW_KEY_COMMANDS_WITH_DIRECTION = [
|
|
164
|
+
[KEY_ARROW_DOWN_COMMAND, 'down'],
|
|
165
|
+
[KEY_ARROW_UP_COMMAND, 'up'],
|
|
166
|
+
[KEY_ARROW_LEFT_COMMAND, 'backward'],
|
|
167
|
+
[KEY_ARROW_RIGHT_COMMAND, 'forward'],
|
|
168
|
+
] as const;
|
|
169
|
+
const DELETE_TEXT_COMMANDS = [
|
|
170
|
+
DELETE_WORD_COMMAND,
|
|
171
|
+
DELETE_LINE_COMMAND,
|
|
172
|
+
DELETE_CHARACTER_COMMAND,
|
|
173
|
+
] as const;
|
|
174
|
+
const DELETE_KEY_COMMANDS = [
|
|
175
|
+
KEY_BACKSPACE_COMMAND,
|
|
176
|
+
KEY_DELETE_COMMAND,
|
|
177
|
+
] as const;
|
|
178
|
+
|
|
179
|
+
export function registerTableWindowHandlers(
|
|
180
|
+
editor: LexicalEditor,
|
|
181
|
+
tableObservers: TableObservers,
|
|
182
|
+
) {
|
|
183
|
+
// Use registerRootListener so the pointerdown handler is (re)attached
|
|
184
|
+
// whenever the root element is set. This is required for the Extension API,
|
|
185
|
+
// where register() runs before the ContentEditable mounts and getRootElement()
|
|
186
|
+
// is still null.
|
|
187
|
+
return editor.registerRootListener(rootElement => {
|
|
188
|
+
if (rootElement === null) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const editorWindow = editor._window;
|
|
192
|
+
if (editorWindow === null) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const pointerDownCallback = (event: PointerEvent) => {
|
|
197
|
+
const target = event.target;
|
|
198
|
+
if (
|
|
199
|
+
event.button !== 0 ||
|
|
200
|
+
!isDOMNode(target) ||
|
|
201
|
+
!rootElement.contains(target)
|
|
202
|
+
) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const selectionInfo = getTableObserverFromCellNode(target);
|
|
206
|
+
|
|
207
|
+
editor.update(() => {
|
|
208
|
+
// Clear highlights from all tables (even one we're actively clicking on)
|
|
209
|
+
const selection = $getSelection();
|
|
210
|
+
if ($isTableSelection(selection)) {
|
|
211
|
+
for (const [observer] of tableObservers.observers.values()) {
|
|
212
|
+
observer.$clearHighlight(false);
|
|
213
|
+
}
|
|
214
|
+
$setSelection(null);
|
|
215
|
+
editor.dispatchCommand(SELECTION_CHANGE_COMMAND, undefined);
|
|
216
|
+
}
|
|
217
|
+
if (!selectionInfo) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const {tableObserver, tableElement, cellElement} = selectionInfo;
|
|
221
|
+
$handleTableClick(
|
|
222
|
+
editor,
|
|
223
|
+
event,
|
|
224
|
+
cellElement,
|
|
225
|
+
tableElement,
|
|
226
|
+
tableObserver,
|
|
227
|
+
tableObservers,
|
|
228
|
+
);
|
|
229
|
+
});
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
editorWindow.addEventListener('pointerdown', pointerDownCallback);
|
|
233
|
+
return () => {
|
|
234
|
+
editorWindow.removeEventListener('pointerdown', pointerDownCallback);
|
|
235
|
+
};
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function $handleTableClick(
|
|
240
|
+
editor: LexicalEditor,
|
|
241
|
+
event: PointerEvent,
|
|
242
|
+
selectedDOMCell: TableDOMCell,
|
|
243
|
+
tableElement: HTMLTableElementWithWithTableSelectionState,
|
|
244
|
+
tableObserver: TableObserver,
|
|
245
|
+
tableObservers: TableObservers,
|
|
246
|
+
) {
|
|
247
|
+
const editorWindow = editor._window;
|
|
248
|
+
if (!editorWindow) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const createPointerHandlers = (startingCell: TableDOMCell | null) => {
|
|
252
|
+
if (tableObserver.isSelecting) {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
tableObserver.isSelecting = true;
|
|
256
|
+
|
|
257
|
+
// Set anchor immediately if starting cell provided (handles direct drag without click)
|
|
258
|
+
if (startingCell !== null && tableObserver.anchorCell === null) {
|
|
259
|
+
editor.update(() => {
|
|
260
|
+
tableObserver.$setAnchorCellForSelection(startingCell);
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const onPointerUp = () => {
|
|
265
|
+
tableObserver.isSelecting = false;
|
|
266
|
+
editorWindow.removeEventListener('pointerup', onPointerUp);
|
|
267
|
+
editorWindow.removeEventListener('pointermove', onPointerMove);
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
const onPointerMove = (moveEvent: PointerEvent) => {
|
|
271
|
+
if (!isPointerDownOnEvent(moveEvent) && tableObserver.isSelecting) {
|
|
272
|
+
tableObserver.isSelecting = false;
|
|
273
|
+
editorWindow.removeEventListener('pointerup', onPointerUp);
|
|
274
|
+
editorWindow.removeEventListener('pointermove', onPointerMove);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
if (!isDOMNode(moveEvent.target)) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
let focusCell: null | TableDOMCell = null;
|
|
281
|
+
// In firefox the moveEvent.target may be captured so we must always
|
|
282
|
+
// consult the coordinates #7245
|
|
283
|
+
const override = !(IS_FIREFOX || tableElement.contains(moveEvent.target));
|
|
284
|
+
if (override) {
|
|
285
|
+
focusCell = getDOMCellInTableFromTarget(tableElement, moveEvent.target);
|
|
286
|
+
} else {
|
|
287
|
+
for (const el of document.elementsFromPoint(
|
|
288
|
+
moveEvent.clientX,
|
|
289
|
+
moveEvent.clientY,
|
|
290
|
+
)) {
|
|
291
|
+
focusCell = getDOMCellInTableFromTarget(tableElement, el);
|
|
292
|
+
if (focusCell) {
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (focusCell) {
|
|
298
|
+
const anchorCell = focusCell;
|
|
299
|
+
// Fallback: set anchor if still missing (handles race conditions)
|
|
300
|
+
if (tableObserver.anchorCell === null) {
|
|
301
|
+
editor.update(() => {
|
|
302
|
+
tableObserver.$setAnchorCellForSelection(anchorCell);
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
if (
|
|
306
|
+
tableObserver.focusCell === null ||
|
|
307
|
+
focusCell.elem !== tableObserver.focusCell.elem
|
|
308
|
+
) {
|
|
309
|
+
tableObservers.setNextFocus({
|
|
310
|
+
focusCell,
|
|
311
|
+
override,
|
|
312
|
+
tableKey: tableObserver.tableNodeKey,
|
|
313
|
+
});
|
|
314
|
+
editor.dispatchCommand(SELECTION_CHANGE_COMMAND, undefined);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
editorWindow.addEventListener(
|
|
319
|
+
'pointerup',
|
|
320
|
+
onPointerUp,
|
|
321
|
+
tableObserver.listenerOptions,
|
|
322
|
+
);
|
|
323
|
+
editorWindow.addEventListener(
|
|
324
|
+
'pointermove',
|
|
325
|
+
onPointerMove,
|
|
326
|
+
tableObserver.listenerOptions,
|
|
327
|
+
);
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
tableObserver.pointerType = event.pointerType;
|
|
331
|
+
const tableNode = $getNodeByKeyOrThrow<TableNode>(tableObserver.tableNodeKey);
|
|
332
|
+
const prevSelection = $getPreviousSelection();
|
|
333
|
+
// We can't trust Firefox to do the right thing with the selection and
|
|
334
|
+
// we don't have a proper state machine to do this "correctly" but
|
|
335
|
+
// if we go ahead and make the table selection now it will work
|
|
336
|
+
if (
|
|
337
|
+
IS_FIREFOX &&
|
|
338
|
+
event.shiftKey &&
|
|
339
|
+
$isSelectionInTable(prevSelection, tableNode) &&
|
|
340
|
+
($isRangeSelection(prevSelection) || $isTableSelection(prevSelection))
|
|
341
|
+
) {
|
|
342
|
+
const prevAnchorNode = prevSelection.anchor.getNode();
|
|
343
|
+
const prevAnchorCell = $findParentTableCellNodeInTable(
|
|
344
|
+
tableNode,
|
|
345
|
+
prevSelection.anchor.getNode(),
|
|
346
|
+
);
|
|
347
|
+
if (prevAnchorCell) {
|
|
348
|
+
tableObserver.$setAnchorCellForSelection(
|
|
349
|
+
$getObserverCellFromCellNodeOrThrow(tableObserver, prevAnchorCell),
|
|
350
|
+
);
|
|
351
|
+
tableObserver.$setFocusCellForSelection(selectedDOMCell);
|
|
352
|
+
stopEvent(event);
|
|
353
|
+
} else {
|
|
354
|
+
const newSelection = tableNode.isBefore(prevAnchorNode)
|
|
355
|
+
? tableNode.selectStart()
|
|
356
|
+
: tableNode.selectEnd();
|
|
357
|
+
newSelection.anchor.set(
|
|
358
|
+
prevSelection.anchor.key,
|
|
359
|
+
prevSelection.anchor.offset,
|
|
360
|
+
prevSelection.anchor.type,
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
} else {
|
|
364
|
+
// Only set anchor cell for selection if this is not a simple touch tap
|
|
365
|
+
// Touch taps should not initiate table selection mode
|
|
366
|
+
if (event.pointerType !== 'touch') {
|
|
367
|
+
tableObserver.$setAnchorCellForSelection(selectedDOMCell);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Pass the target cell to createPointerHandlers so it can be used as anchor
|
|
372
|
+
// if user drags directly without clicking first
|
|
373
|
+
createPointerHandlers(selectedDOMCell);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export function applyTableHandlers(
|
|
377
|
+
tableNode: TableNode,
|
|
378
|
+
element: HTMLElement,
|
|
379
|
+
editor: LexicalEditor,
|
|
380
|
+
hasTabHandler: boolean,
|
|
381
|
+
tableObservers: TableObservers,
|
|
382
|
+
): TableObserver {
|
|
383
|
+
const rootElement = editor.getRootElement();
|
|
384
|
+
const editorWindow = getEditorWindow(editor);
|
|
385
|
+
invariant(
|
|
386
|
+
rootElement !== null && editorWindow !== null,
|
|
387
|
+
'applyTableHandlers: editor has no root element set',
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
const tableObserver = new TableObserver(editor, tableNode.getKey());
|
|
391
|
+
|
|
392
|
+
const tableElement = getTableElement(tableNode, element);
|
|
393
|
+
attachTableObserverToTableElement(tableElement, tableObserver);
|
|
394
|
+
tableObserver.listenersToRemove.add(() =>
|
|
395
|
+
detachTableObserverFromTableElement(tableElement, tableObserver),
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
const onTripleClick = (event: MouseEvent) => {
|
|
399
|
+
if (event.detail >= 3 && isDOMNode(event.target)) {
|
|
400
|
+
const targetCell = getDOMCellFromTarget(event.target);
|
|
401
|
+
if (targetCell !== null) {
|
|
402
|
+
event.preventDefault();
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
tableElement.addEventListener(
|
|
407
|
+
'mousedown',
|
|
408
|
+
onTripleClick,
|
|
409
|
+
tableObserver.listenerOptions,
|
|
410
|
+
);
|
|
411
|
+
tableObserver.listenersToRemove.add(() => {
|
|
412
|
+
tableElement.removeEventListener('mousedown', onTripleClick);
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
for (const [command, direction] of ARROW_KEY_COMMANDS_WITH_DIRECTION) {
|
|
416
|
+
tableObserver.listenersToRemove.add(
|
|
417
|
+
editor.registerCommand(
|
|
418
|
+
command,
|
|
419
|
+
event =>
|
|
420
|
+
$handleArrowKey(
|
|
421
|
+
editor,
|
|
422
|
+
event,
|
|
423
|
+
direction,
|
|
424
|
+
tableNode,
|
|
425
|
+
tableObserver,
|
|
426
|
+
tableObservers,
|
|
427
|
+
),
|
|
428
|
+
COMMAND_PRIORITY_HIGH,
|
|
429
|
+
),
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
tableObserver.listenersToRemove.add(
|
|
434
|
+
editor.registerCommand(
|
|
435
|
+
KEY_ESCAPE_COMMAND,
|
|
436
|
+
event => {
|
|
437
|
+
const selection = $getSelection();
|
|
438
|
+
if ($isTableSelection(selection)) {
|
|
439
|
+
const focusCellNode = $findParentTableCellNodeInTable(
|
|
440
|
+
tableNode,
|
|
441
|
+
selection.focus.getNode(),
|
|
442
|
+
);
|
|
443
|
+
if (focusCellNode !== null) {
|
|
444
|
+
stopEvent(event);
|
|
445
|
+
focusCellNode.selectEnd();
|
|
446
|
+
return true;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return false;
|
|
451
|
+
},
|
|
452
|
+
COMMAND_PRIORITY_HIGH,
|
|
453
|
+
),
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
const deleteTextHandler = (command: LexicalCommand<boolean>) => () => {
|
|
457
|
+
const selection = $getSelection();
|
|
458
|
+
|
|
459
|
+
if (!$isSelectionInTable(selection, tableNode)) {
|
|
460
|
+
return false;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if ($isTableSelection(selection)) {
|
|
464
|
+
tableObserver.$clearText();
|
|
465
|
+
|
|
466
|
+
return true;
|
|
467
|
+
} else if ($isRangeSelection(selection)) {
|
|
468
|
+
const tableCellNode = $findParentTableCellNodeInTable(
|
|
469
|
+
tableNode,
|
|
470
|
+
selection.anchor.getNode(),
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
if (!$isTableCellNode(tableCellNode)) {
|
|
474
|
+
return false;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const anchorNode = selection.anchor.getNode();
|
|
478
|
+
const focusNode = selection.focus.getNode();
|
|
479
|
+
const isAnchorInside = tableNode.isParentOf(anchorNode);
|
|
480
|
+
const isFocusInside = tableNode.isParentOf(focusNode);
|
|
481
|
+
|
|
482
|
+
const selectionContainsPartialTable =
|
|
483
|
+
(isAnchorInside && !isFocusInside) ||
|
|
484
|
+
(isFocusInside && !isAnchorInside);
|
|
485
|
+
|
|
486
|
+
if (selectionContainsPartialTable) {
|
|
487
|
+
tableObserver.$clearText();
|
|
488
|
+
return true;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const nearestElementNode = $findMatchingParent(
|
|
492
|
+
selection.anchor.getNode(),
|
|
493
|
+
n => $isElementNode(n),
|
|
494
|
+
);
|
|
495
|
+
|
|
496
|
+
const topLevelCellElementNode =
|
|
497
|
+
nearestElementNode &&
|
|
498
|
+
$findMatchingParent(
|
|
499
|
+
nearestElementNode,
|
|
500
|
+
n => $isElementNode(n) && $isTableCellNode(n.getParent()),
|
|
501
|
+
);
|
|
502
|
+
|
|
503
|
+
if (
|
|
504
|
+
!$isElementNode(topLevelCellElementNode) ||
|
|
505
|
+
!$isElementNode(nearestElementNode)
|
|
506
|
+
) {
|
|
507
|
+
return false;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (
|
|
511
|
+
command === DELETE_LINE_COMMAND &&
|
|
512
|
+
topLevelCellElementNode.getPreviousSibling() === null
|
|
513
|
+
) {
|
|
514
|
+
// TODO: Fix Delete Line in Table Cells.
|
|
515
|
+
return true;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
return false;
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
for (const command of DELETE_TEXT_COMMANDS) {
|
|
523
|
+
tableObserver.listenersToRemove.add(
|
|
524
|
+
editor.registerCommand(
|
|
525
|
+
command,
|
|
526
|
+
deleteTextHandler(command),
|
|
527
|
+
COMMAND_PRIORITY_HIGH,
|
|
528
|
+
),
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const $deleteCellHandler = (
|
|
533
|
+
event: KeyboardEvent | ClipboardEvent | null,
|
|
534
|
+
): boolean => {
|
|
535
|
+
const selection = $getSelection();
|
|
536
|
+
if (!($isTableSelection(selection) || $isRangeSelection(selection))) {
|
|
537
|
+
return false;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// If the selection is inside the table but should remove the whole table
|
|
541
|
+
// we expand the selection so that both the anchor and focus are outside
|
|
542
|
+
// the table and the editor's command listener will handle the delete
|
|
543
|
+
const isAnchorInside = tableNode.isParentOf(selection.anchor.getNode());
|
|
544
|
+
const isFocusInside = tableNode.isParentOf(selection.focus.getNode());
|
|
545
|
+
if (isAnchorInside !== isFocusInside) {
|
|
546
|
+
const tablePoint = isAnchorInside ? 'anchor' : 'focus';
|
|
547
|
+
const outerPoint = isAnchorInside ? 'focus' : 'anchor';
|
|
548
|
+
// Preserve the outer point
|
|
549
|
+
const {key, offset, type} = selection[outerPoint];
|
|
550
|
+
// Expand the selection around the table
|
|
551
|
+
const newSelection =
|
|
552
|
+
tableNode[
|
|
553
|
+
selection[tablePoint].isBefore(selection[outerPoint])
|
|
554
|
+
? 'selectPrevious'
|
|
555
|
+
: 'selectNext'
|
|
556
|
+
]();
|
|
557
|
+
// Restore the outer point of the selection
|
|
558
|
+
newSelection[outerPoint].set(key, offset, type);
|
|
559
|
+
// Let the base implementation handle the rest
|
|
560
|
+
return false;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
if (!$isSelectionInTable(selection, tableNode)) {
|
|
564
|
+
return false;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if ($isTableSelection(selection)) {
|
|
568
|
+
if (event) {
|
|
569
|
+
event.preventDefault();
|
|
570
|
+
event.stopPropagation();
|
|
571
|
+
}
|
|
572
|
+
tableObserver.$clearText();
|
|
573
|
+
|
|
574
|
+
return true;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
return false;
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
for (const command of DELETE_KEY_COMMANDS) {
|
|
581
|
+
tableObserver.listenersToRemove.add(
|
|
582
|
+
editor.registerCommand(
|
|
583
|
+
command,
|
|
584
|
+
$deleteCellHandler,
|
|
585
|
+
COMMAND_PRIORITY_HIGH,
|
|
586
|
+
),
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
tableObserver.listenersToRemove.add(
|
|
591
|
+
editor.registerCommand(
|
|
592
|
+
CUT_COMMAND,
|
|
593
|
+
event => {
|
|
594
|
+
const selection = $getSelection();
|
|
595
|
+
if (selection) {
|
|
596
|
+
if (!($isTableSelection(selection) || $isRangeSelection(selection))) {
|
|
597
|
+
return false;
|
|
598
|
+
}
|
|
599
|
+
// Copying to the clipboard is async so we must capture the data
|
|
600
|
+
// before we delete it
|
|
601
|
+
void copyToClipboard(
|
|
602
|
+
editor,
|
|
603
|
+
objectKlassEquals(event, ClipboardEvent) ? event : null,
|
|
604
|
+
$getClipboardDataFromSelection(selection),
|
|
605
|
+
);
|
|
606
|
+
const intercepted = $deleteCellHandler(event);
|
|
607
|
+
if ($isRangeSelection(selection)) {
|
|
608
|
+
selection.removeText();
|
|
609
|
+
return true;
|
|
610
|
+
}
|
|
611
|
+
return intercepted;
|
|
612
|
+
}
|
|
613
|
+
return false;
|
|
614
|
+
},
|
|
615
|
+
COMMAND_PRIORITY_HIGH,
|
|
616
|
+
),
|
|
617
|
+
);
|
|
618
|
+
|
|
619
|
+
tableObserver.listenersToRemove.add(
|
|
620
|
+
editor.registerCommand(
|
|
621
|
+
FORMAT_TEXT_COMMAND,
|
|
622
|
+
payload => {
|
|
623
|
+
const selection = $getSelection();
|
|
624
|
+
|
|
625
|
+
if (!$isSelectionInTable(selection, tableNode)) {
|
|
626
|
+
return false;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
if ($isTableSelection(selection)) {
|
|
630
|
+
tableObserver.$formatCells(payload);
|
|
631
|
+
|
|
632
|
+
return true;
|
|
633
|
+
} else if ($isRangeSelection(selection)) {
|
|
634
|
+
const tableCellNode = $findMatchingParent(
|
|
635
|
+
selection.anchor.getNode(),
|
|
636
|
+
n => $isTableCellNode(n),
|
|
637
|
+
);
|
|
638
|
+
|
|
639
|
+
if (!$isTableCellNode(tableCellNode)) {
|
|
640
|
+
return false;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
return false;
|
|
645
|
+
},
|
|
646
|
+
COMMAND_PRIORITY_HIGH,
|
|
647
|
+
),
|
|
648
|
+
);
|
|
649
|
+
|
|
650
|
+
tableObserver.listenersToRemove.add(
|
|
651
|
+
editor.registerCommand(
|
|
652
|
+
FORMAT_ELEMENT_COMMAND,
|
|
653
|
+
formatType => {
|
|
654
|
+
const selection = $getSelection();
|
|
655
|
+
if (
|
|
656
|
+
!$isTableSelection(selection) ||
|
|
657
|
+
!$isSelectionInTable(selection, tableNode)
|
|
658
|
+
) {
|
|
659
|
+
return false;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
const anchorNode = selection.anchor.getNode();
|
|
663
|
+
const focusNode = selection.focus.getNode();
|
|
664
|
+
if (!$isTableCellNode(anchorNode) || !$isTableCellNode(focusNode)) {
|
|
665
|
+
return false;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// Align the table if the entire table is selected
|
|
669
|
+
if ($isFullTableSelection(selection, tableNode)) {
|
|
670
|
+
tableNode.setFormat(formatType);
|
|
671
|
+
return true;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
const [tableMap, anchorCell, focusCell] = $computeTableMap(
|
|
675
|
+
tableNode,
|
|
676
|
+
anchorNode,
|
|
677
|
+
focusNode,
|
|
678
|
+
);
|
|
679
|
+
const maxRow = Math.max(
|
|
680
|
+
anchorCell.startRow + anchorCell.cell.__rowSpan - 1,
|
|
681
|
+
focusCell.startRow + focusCell.cell.__rowSpan - 1,
|
|
682
|
+
);
|
|
683
|
+
const maxColumn = Math.max(
|
|
684
|
+
anchorCell.startColumn + anchorCell.cell.__colSpan - 1,
|
|
685
|
+
focusCell.startColumn + focusCell.cell.__colSpan - 1,
|
|
686
|
+
);
|
|
687
|
+
const minRow = Math.min(anchorCell.startRow, focusCell.startRow);
|
|
688
|
+
const minColumn = Math.min(
|
|
689
|
+
anchorCell.startColumn,
|
|
690
|
+
focusCell.startColumn,
|
|
691
|
+
);
|
|
692
|
+
const visited = new Set<TableCellNode>();
|
|
693
|
+
for (let i = minRow; i <= maxRow; i++) {
|
|
694
|
+
for (let j = minColumn; j <= maxColumn; j++) {
|
|
695
|
+
const cell = tableMap[i][j].cell;
|
|
696
|
+
if (visited.has(cell)) {
|
|
697
|
+
continue;
|
|
698
|
+
}
|
|
699
|
+
visited.add(cell);
|
|
700
|
+
cell.setFormat(formatType);
|
|
701
|
+
|
|
702
|
+
const cellChildren = cell.getChildren();
|
|
703
|
+
for (let k = 0; k < cellChildren.length; k++) {
|
|
704
|
+
const child = cellChildren[k];
|
|
705
|
+
if ($isElementNode(child) && !child.isInline()) {
|
|
706
|
+
child.setFormat(formatType);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
return true;
|
|
712
|
+
},
|
|
713
|
+
COMMAND_PRIORITY_HIGH,
|
|
714
|
+
),
|
|
715
|
+
);
|
|
716
|
+
|
|
717
|
+
tableObserver.listenersToRemove.add(
|
|
718
|
+
editor.registerCommand(
|
|
719
|
+
CONTROLLED_TEXT_INSERTION_COMMAND,
|
|
720
|
+
payload => {
|
|
721
|
+
const selection = $getSelection();
|
|
722
|
+
|
|
723
|
+
if (!$isSelectionInTable(selection, tableNode)) {
|
|
724
|
+
return false;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
if ($isTableSelection(selection)) {
|
|
728
|
+
tableObserver.$clearHighlight();
|
|
729
|
+
|
|
730
|
+
return false;
|
|
731
|
+
} else if ($isRangeSelection(selection)) {
|
|
732
|
+
const tableCellNode = $findMatchingParent(
|
|
733
|
+
selection.anchor.getNode(),
|
|
734
|
+
n => $isTableCellNode(n),
|
|
735
|
+
);
|
|
736
|
+
|
|
737
|
+
if (!$isTableCellNode(tableCellNode)) {
|
|
738
|
+
return false;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
if (typeof payload === 'string') {
|
|
742
|
+
const edgePosition = $getTableEdgeCursorPosition(
|
|
743
|
+
editor,
|
|
744
|
+
selection,
|
|
745
|
+
tableNode,
|
|
746
|
+
);
|
|
747
|
+
if (edgePosition) {
|
|
748
|
+
$insertParagraphAtTableEdge(edgePosition, tableNode, [
|
|
749
|
+
$createTextNode(payload),
|
|
750
|
+
]);
|
|
751
|
+
return true;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
return false;
|
|
757
|
+
},
|
|
758
|
+
COMMAND_PRIORITY_HIGH,
|
|
759
|
+
),
|
|
760
|
+
);
|
|
761
|
+
|
|
762
|
+
if (hasTabHandler) {
|
|
763
|
+
tableObserver.listenersToRemove.add(
|
|
764
|
+
editor.registerCommand(
|
|
765
|
+
KEY_TAB_COMMAND,
|
|
766
|
+
event => {
|
|
767
|
+
const selection = $getSelection();
|
|
768
|
+
if (
|
|
769
|
+
!$isRangeSelection(selection) ||
|
|
770
|
+
!selection.isCollapsed() ||
|
|
771
|
+
!$isSelectionInTable(selection, tableNode)
|
|
772
|
+
) {
|
|
773
|
+
return false;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
const tableCellNode = $findCellNode(selection.anchor.getNode());
|
|
777
|
+
if (
|
|
778
|
+
tableCellNode === null ||
|
|
779
|
+
!tableNode.is($findTableNode(tableCellNode))
|
|
780
|
+
) {
|
|
781
|
+
return false;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
stopEvent(event);
|
|
785
|
+
$selectAdjacentCell(
|
|
786
|
+
tableCellNode,
|
|
787
|
+
event.shiftKey ? 'previous' : 'next',
|
|
788
|
+
);
|
|
789
|
+
|
|
790
|
+
return true;
|
|
791
|
+
},
|
|
792
|
+
COMMAND_PRIORITY_HIGH,
|
|
793
|
+
),
|
|
794
|
+
);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
tableObserver.listenersToRemove.add(
|
|
798
|
+
editor.registerCommand(
|
|
799
|
+
FOCUS_COMMAND,
|
|
800
|
+
payload => {
|
|
801
|
+
return tableNode.isSelected();
|
|
802
|
+
},
|
|
803
|
+
COMMAND_PRIORITY_HIGH,
|
|
804
|
+
),
|
|
805
|
+
);
|
|
806
|
+
|
|
807
|
+
tableObserver.listenersToRemove.add(
|
|
808
|
+
editor.registerCommand(
|
|
809
|
+
INSERT_PARAGRAPH_COMMAND,
|
|
810
|
+
() => {
|
|
811
|
+
const selection = $getSelection();
|
|
812
|
+
if (
|
|
813
|
+
!$isRangeSelection(selection) ||
|
|
814
|
+
!selection.isCollapsed() ||
|
|
815
|
+
!$isSelectionInTable(selection, tableNode)
|
|
816
|
+
) {
|
|
817
|
+
return false;
|
|
818
|
+
}
|
|
819
|
+
const edgePosition = $getTableEdgeCursorPosition(
|
|
820
|
+
editor,
|
|
821
|
+
selection,
|
|
822
|
+
tableNode,
|
|
823
|
+
);
|
|
824
|
+
if (edgePosition) {
|
|
825
|
+
$insertParagraphAtTableEdge(edgePosition, tableNode);
|
|
826
|
+
return true;
|
|
827
|
+
}
|
|
828
|
+
return false;
|
|
829
|
+
},
|
|
830
|
+
COMMAND_PRIORITY_HIGH,
|
|
831
|
+
),
|
|
832
|
+
);
|
|
833
|
+
|
|
834
|
+
return tableObserver;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/** @internal */
|
|
838
|
+
export function $handleTableSelectionChangeCommand(
|
|
839
|
+
tableObservers: TableObservers,
|
|
840
|
+
editor: LexicalEditor,
|
|
841
|
+
) {
|
|
842
|
+
const selection = $getSelection();
|
|
843
|
+
const prevSelection = $getPreviousSelection();
|
|
844
|
+
|
|
845
|
+
const nextFocus = tableObservers.getAndClearNextFocus();
|
|
846
|
+
if (nextFocus !== null) {
|
|
847
|
+
const {tableKey, focusCell} = nextFocus;
|
|
848
|
+
const observerAndTable = tableObservers.observers.get(tableKey);
|
|
849
|
+
invariant(
|
|
850
|
+
!!observerAndTable,
|
|
851
|
+
'tableObserver not found for tableKey: %s',
|
|
852
|
+
tableKey,
|
|
853
|
+
);
|
|
854
|
+
const [tableObserver] = observerAndTable;
|
|
855
|
+
if (
|
|
856
|
+
$isTableSelection(selection) &&
|
|
857
|
+
selection.tableKey === tableObserver.tableNodeKey
|
|
858
|
+
) {
|
|
859
|
+
if (
|
|
860
|
+
focusCell.x === tableObserver.focusX &&
|
|
861
|
+
focusCell.y === tableObserver.focusY
|
|
862
|
+
) {
|
|
863
|
+
// The selection is already the correct table selection
|
|
864
|
+
return false;
|
|
865
|
+
} else {
|
|
866
|
+
tableObserver.$setFocusCellForSelection(focusCell);
|
|
867
|
+
return true;
|
|
868
|
+
}
|
|
869
|
+
} else if (
|
|
870
|
+
tableObserver.anchorCell !== null &&
|
|
871
|
+
tableObserver.anchorCellNodeKey !== null &&
|
|
872
|
+
focusCell.elem !== tableObserver.anchorCell.elem &&
|
|
873
|
+
tableObserver.tableSelection !== null
|
|
874
|
+
) {
|
|
875
|
+
// The selection has crossed cells
|
|
876
|
+
// If we have an anchor cell set and tableSelection initialized,
|
|
877
|
+
// we have all the necessary state to create the selection.
|
|
878
|
+
// The presence of nextFocus means we're dragging, so process it.
|
|
879
|
+
// Use ignoreStart=true to ensure isHighlightingCells is set correctly
|
|
880
|
+
// on the first drag attempt, especially when switching columns.
|
|
881
|
+
tableObserver.$setFocusCellForSelection(focusCell, true);
|
|
882
|
+
return true;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
const shouldCheckSelectionForTable =
|
|
886
|
+
tableObservers.getAndClearShouldCheckSelectionForTable();
|
|
887
|
+
// If they pressed the down arrow with the selection outside of the
|
|
888
|
+
// table, and then the selection ends up in the table but not in the
|
|
889
|
+
// first cell, then move the selection to the first cell.
|
|
890
|
+
if (
|
|
891
|
+
!!shouldCheckSelectionForTable &&
|
|
892
|
+
$isRangeSelection(prevSelection) &&
|
|
893
|
+
$isRangeSelection(selection) &&
|
|
894
|
+
selection.isCollapsed()
|
|
895
|
+
) {
|
|
896
|
+
const tableNode = $getNodeByKeyOrThrow<TableNode>(
|
|
897
|
+
shouldCheckSelectionForTable,
|
|
898
|
+
);
|
|
899
|
+
const anchor = selection.anchor.getNode();
|
|
900
|
+
const firstRow = tableNode.getFirstChild();
|
|
901
|
+
const anchorCell = $findCellNode(anchor);
|
|
902
|
+
if (anchorCell !== null && $isTableRowNode(firstRow)) {
|
|
903
|
+
const firstCell = firstRow.getFirstChild();
|
|
904
|
+
if (
|
|
905
|
+
$isTableCellNode(firstCell) &&
|
|
906
|
+
tableNode.is(
|
|
907
|
+
$findMatchingParent(
|
|
908
|
+
anchorCell,
|
|
909
|
+
node => node.is(tableNode) || node.is(firstCell),
|
|
910
|
+
),
|
|
911
|
+
)
|
|
912
|
+
) {
|
|
913
|
+
// The selection moved to the table, but not in the first cell
|
|
914
|
+
firstCell.selectStart();
|
|
915
|
+
return true;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
if ($isTableSelection(selection)) {
|
|
921
|
+
$fixTableSelectionForSelectedTable(editor, selection);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
if ($isRangeSelection(selection)) {
|
|
925
|
+
$fixRangeSelectionForSelectedTable(selection, tableObservers);
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
// Generic selection logic that runs across every table observer when the selection changes.
|
|
929
|
+
// Note: the selection might have changed in the code above, which re-dispatches the selection change command
|
|
930
|
+
// and gets handled here on the second pass. This should be refactored.
|
|
931
|
+
const tableNodesAndObservers = Array.from(
|
|
932
|
+
tableObservers.observers.entries(),
|
|
933
|
+
).map(([tableKey, [tableObserver]]) => ({
|
|
934
|
+
tableNode: $getNodeByKeyOrThrow<TableNode>(tableKey),
|
|
935
|
+
tableObserver,
|
|
936
|
+
}));
|
|
937
|
+
for (const {tableNode, tableObserver} of tableNodesAndObservers) {
|
|
938
|
+
$syncTableSelectionState(editor, tableNode, tableObserver);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
return false;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
/**
|
|
945
|
+
* Handles cases where range selections cross into, out of, or within tables.
|
|
946
|
+
*/
|
|
947
|
+
function $fixRangeSelectionForSelectedTable(
|
|
948
|
+
selection: RangeSelection,
|
|
949
|
+
tableObservers: TableObservers,
|
|
950
|
+
) {
|
|
951
|
+
const prevSelection = $getPreviousSelection();
|
|
952
|
+
const {anchor, focus} = selection;
|
|
953
|
+
const anchorNode = anchor.getNode();
|
|
954
|
+
const focusNode = focus.getNode();
|
|
955
|
+
// Using explicit comparison with table node to ensure it's not a nested table
|
|
956
|
+
// as in that case we'll leave selection resolving to that table
|
|
957
|
+
const anchorCellNode = $findCellNode(anchorNode);
|
|
958
|
+
const focusCellNode = $findCellNode(focusNode);
|
|
959
|
+
const anchorCellTable = anchorCellNode
|
|
960
|
+
? $findTableNode(anchorCellNode)
|
|
961
|
+
: null;
|
|
962
|
+
const focusCellTable = focusCellNode ? $findTableNode(focusCellNode) : null;
|
|
963
|
+
const isBackward = selection.isBackward();
|
|
964
|
+
|
|
965
|
+
const isSameTable =
|
|
966
|
+
anchorCellNode &&
|
|
967
|
+
focusCellNode &&
|
|
968
|
+
anchorCellTable &&
|
|
969
|
+
focusCellTable &&
|
|
970
|
+
anchorCellTable.is(focusCellTable);
|
|
971
|
+
|
|
972
|
+
// The focus should be moved (to cover the whole focus table) if it is moved outside of the anchor's table.
|
|
973
|
+
// For example, when dragging from outside a table into it.
|
|
974
|
+
const shouldMoveFocus =
|
|
975
|
+
focusCellTable &&
|
|
976
|
+
(!anchorCellTable || anchorCellTable.isParentOf(focusCellTable));
|
|
977
|
+
// The anchor should be moved (to cover the whole anchor table) if the focus is moved outside of the anchor table.
|
|
978
|
+
// For example, when dragging from inside a table out of it.
|
|
979
|
+
const shouldMoveAnchor =
|
|
980
|
+
anchorCellTable &&
|
|
981
|
+
(!focusCellTable || focusCellTable.isParentOf(anchorCellTable));
|
|
982
|
+
|
|
983
|
+
if (shouldMoveFocus) {
|
|
984
|
+
// Select the whole focus table.
|
|
985
|
+
const newSelection = selection.clone();
|
|
986
|
+
const [tableMap] = $computeTableMap(
|
|
987
|
+
focusCellTable,
|
|
988
|
+
focusCellNode!,
|
|
989
|
+
focusCellNode!,
|
|
990
|
+
);
|
|
991
|
+
const firstCell = tableMap[0][0].cell;
|
|
992
|
+
const lastCell = tableMap[tableMap.length - 1].at(-1)!.cell;
|
|
993
|
+
newSelection.focus.set(
|
|
994
|
+
isBackward ? firstCell.getKey() : lastCell.getKey(),
|
|
995
|
+
isBackward ? 0 : lastCell.getChildrenSize(),
|
|
996
|
+
'element',
|
|
997
|
+
);
|
|
998
|
+
$setSelection(newSelection);
|
|
999
|
+
} else if (shouldMoveAnchor) {
|
|
1000
|
+
// Select the whole anchor table.
|
|
1001
|
+
const newSelection = selection.clone();
|
|
1002
|
+
const [tableMap] = $computeTableMap(
|
|
1003
|
+
anchorCellTable,
|
|
1004
|
+
anchorCellNode!,
|
|
1005
|
+
anchorCellNode!,
|
|
1006
|
+
);
|
|
1007
|
+
const firstCell = tableMap[0][0].cell;
|
|
1008
|
+
const lastCell = tableMap[tableMap.length - 1].at(-1)!.cell;
|
|
1009
|
+
newSelection.anchor.set(
|
|
1010
|
+
isBackward ? lastCell.getKey() : firstCell.getKey(),
|
|
1011
|
+
isBackward ? lastCell.getChildrenSize() : 0,
|
|
1012
|
+
'element',
|
|
1013
|
+
);
|
|
1014
|
+
$setSelection(newSelection);
|
|
1015
|
+
} else if (isSameTable) {
|
|
1016
|
+
// Handle case when selection spans across multiple cells but still
|
|
1017
|
+
// has range selection, then we convert it into table selection
|
|
1018
|
+
// For example, this fires when dragging up from first cell, outside of the table, or when clicking a cell
|
|
1019
|
+
// then shift-clicking another cell.
|
|
1020
|
+
const observerInfo = tableObservers.observers.get(anchorCellTable.getKey());
|
|
1021
|
+
invariant(
|
|
1022
|
+
!!observerInfo,
|
|
1023
|
+
'tableObserver not found for tableKey: %s',
|
|
1024
|
+
anchorCellTable.getKey(),
|
|
1025
|
+
);
|
|
1026
|
+
const [tableObserver] = observerInfo;
|
|
1027
|
+
if (!anchorCellNode.is(focusCellNode)) {
|
|
1028
|
+
tableObserver.$setAnchorCellForSelection(
|
|
1029
|
+
$getObserverCellFromCellNodeOrThrow(tableObserver, anchorCellNode),
|
|
1030
|
+
);
|
|
1031
|
+
tableObserver.$setFocusCellForSelection(
|
|
1032
|
+
$getObserverCellFromCellNodeOrThrow(tableObserver, focusCellNode),
|
|
1033
|
+
true,
|
|
1034
|
+
);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
// Handle case when the pointer type is touch and the current and
|
|
1038
|
+
// previous selection are collapsed, and the previous anchor and current
|
|
1039
|
+
// focus cell nodes are different, then we convert it into table selection
|
|
1040
|
+
// However, only do this if the table observer is actively selecting (user dragging)
|
|
1041
|
+
// to prevent unwanted selections when simply tapping between cells on mobile
|
|
1042
|
+
if (
|
|
1043
|
+
tableObserver.pointerType === 'touch' &&
|
|
1044
|
+
tableObserver.isSelecting &&
|
|
1045
|
+
selection.isCollapsed() &&
|
|
1046
|
+
$isRangeSelection(prevSelection) &&
|
|
1047
|
+
prevSelection.isCollapsed()
|
|
1048
|
+
) {
|
|
1049
|
+
const prevAnchorCellNode = $findCellNode(prevSelection.anchor.getNode());
|
|
1050
|
+
if (prevAnchorCellNode && !prevAnchorCellNode.is(focusCellNode)) {
|
|
1051
|
+
tableObserver.$setAnchorCellForSelection(
|
|
1052
|
+
$getObserverCellFromCellNodeOrThrow(
|
|
1053
|
+
tableObserver,
|
|
1054
|
+
prevAnchorCellNode,
|
|
1055
|
+
),
|
|
1056
|
+
);
|
|
1057
|
+
tableObserver.$setFocusCellForSelection(
|
|
1058
|
+
$getObserverCellFromCellNodeOrThrow(tableObserver, focusCellNode),
|
|
1059
|
+
true,
|
|
1060
|
+
);
|
|
1061
|
+
tableObserver.pointerType = null;
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
/**
|
|
1068
|
+
* Ensures that a TableSelection is automatically changed to a RangeSelection when the selection goes outside of the table.
|
|
1069
|
+
*/
|
|
1070
|
+
function $fixTableSelectionForSelectedTable(
|
|
1071
|
+
editor: LexicalEditor,
|
|
1072
|
+
selection: TableSelection,
|
|
1073
|
+
) {
|
|
1074
|
+
const editorWindow = getEditorWindow(editor);
|
|
1075
|
+
const prevSelection = $getPreviousSelection();
|
|
1076
|
+
if (!selection.is(prevSelection)) {
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
const tableNode = $getNodeByKeyOrThrow<TableNode>(selection.tableKey);
|
|
1080
|
+
// if selection goes outside of the table we need to change it to Range selection
|
|
1081
|
+
const domSelection = getDOMSelection(editorWindow);
|
|
1082
|
+
if (domSelection && domSelection.anchorNode && domSelection.focusNode) {
|
|
1083
|
+
const focusNode = $getNearestNodeFromDOMNode(domSelection.focusNode);
|
|
1084
|
+
const isFocusOutside = focusNode && !tableNode.isParentOf(focusNode);
|
|
1085
|
+
|
|
1086
|
+
const anchorNode = $getNearestNodeFromDOMNode(domSelection.anchorNode);
|
|
1087
|
+
const isAnchorInside = anchorNode && tableNode.isParentOf(anchorNode);
|
|
1088
|
+
|
|
1089
|
+
if (isFocusOutside && isAnchorInside && domSelection.rangeCount > 0) {
|
|
1090
|
+
const newSelection = $createRangeSelectionFromDom(domSelection, editor);
|
|
1091
|
+
if (newSelection) {
|
|
1092
|
+
newSelection.anchor.set(
|
|
1093
|
+
tableNode.getKey(),
|
|
1094
|
+
selection.isBackward() ? tableNode.getChildrenSize() : 0,
|
|
1095
|
+
'element',
|
|
1096
|
+
);
|
|
1097
|
+
domSelection.removeAllRanges();
|
|
1098
|
+
$setSelection(newSelection);
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
// Handle keeping the table observer/DOM in sync with the selection.
|
|
1105
|
+
function $syncTableSelectionState(
|
|
1106
|
+
editor: LexicalEditor,
|
|
1107
|
+
tableNode: TableNode,
|
|
1108
|
+
tableObserver: TableObserver,
|
|
1109
|
+
) {
|
|
1110
|
+
const selection = $getSelection();
|
|
1111
|
+
const prevSelection = $getPreviousSelection();
|
|
1112
|
+
if (
|
|
1113
|
+
selection &&
|
|
1114
|
+
!selection.is(prevSelection) &&
|
|
1115
|
+
($isTableSelection(selection) || $isTableSelection(prevSelection)) &&
|
|
1116
|
+
tableObserver.tableSelection &&
|
|
1117
|
+
!tableObserver.tableSelection.is(prevSelection)
|
|
1118
|
+
) {
|
|
1119
|
+
if (
|
|
1120
|
+
$isTableSelection(selection) &&
|
|
1121
|
+
selection.tableKey === tableObserver.tableNodeKey
|
|
1122
|
+
) {
|
|
1123
|
+
tableObserver.$updateTableTableSelection(selection);
|
|
1124
|
+
} else if (
|
|
1125
|
+
!$isTableSelection(selection) &&
|
|
1126
|
+
$isTableSelection(prevSelection) &&
|
|
1127
|
+
prevSelection.tableKey === tableObserver.tableNodeKey
|
|
1128
|
+
) {
|
|
1129
|
+
tableObserver.$updateTableTableSelection(null);
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
if (tableObserver.hasHijackedSelectionStyles && !tableNode.isSelected()) {
|
|
1134
|
+
$removeHighlightStyleToTable(editor, tableObserver);
|
|
1135
|
+
} else if (
|
|
1136
|
+
!tableObserver.hasHijackedSelectionStyles &&
|
|
1137
|
+
tableNode.isSelected()
|
|
1138
|
+
) {
|
|
1139
|
+
$addHighlightStyleToTable(editor, tableObserver);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
export type HTMLTableElementWithWithTableSelectionState = HTMLTableElement & {
|
|
1144
|
+
[LEXICAL_ELEMENT_KEY]?: TableObserver | undefined;
|
|
1145
|
+
};
|
|
1146
|
+
|
|
1147
|
+
export function detachTableObserverFromTableElement(
|
|
1148
|
+
tableElement: HTMLTableElementWithWithTableSelectionState,
|
|
1149
|
+
tableObserver: TableObserver,
|
|
1150
|
+
) {
|
|
1151
|
+
if (getTableObserverFromTableElement(tableElement) === tableObserver) {
|
|
1152
|
+
delete tableElement[LEXICAL_ELEMENT_KEY];
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
export function attachTableObserverToTableElement(
|
|
1157
|
+
tableElement: HTMLTableElementWithWithTableSelectionState,
|
|
1158
|
+
tableObserver: TableObserver,
|
|
1159
|
+
) {
|
|
1160
|
+
invariant(
|
|
1161
|
+
getTableObserverFromTableElement(tableElement) === null,
|
|
1162
|
+
'tableElement already has an attached TableObserver',
|
|
1163
|
+
);
|
|
1164
|
+
tableElement[LEXICAL_ELEMENT_KEY] = tableObserver;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
export function getTableObserverFromTableElement(
|
|
1168
|
+
tableElement: HTMLTableElementWithWithTableSelectionState,
|
|
1169
|
+
): TableObserver | null {
|
|
1170
|
+
return tableElement[LEXICAL_ELEMENT_KEY] || null;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
function getTableObserverFromCellNode(node: null | Node): {
|
|
1174
|
+
tableObserver: TableObserver;
|
|
1175
|
+
tableElement: HTMLTableElementWithWithTableSelectionState;
|
|
1176
|
+
cellElement: TableDOMCell;
|
|
1177
|
+
} | null {
|
|
1178
|
+
const cellNode = getDOMCellFromTarget(node);
|
|
1179
|
+
if (cellNode === null) {
|
|
1180
|
+
return null;
|
|
1181
|
+
}
|
|
1182
|
+
let currentNode: ParentNode | Node | null = cellNode.elem;
|
|
1183
|
+
while (currentNode != null) {
|
|
1184
|
+
const nodeName = currentNode.nodeName;
|
|
1185
|
+
if (
|
|
1186
|
+
nodeName === 'TABLE' &&
|
|
1187
|
+
LEXICAL_ELEMENT_KEY in currentNode &&
|
|
1188
|
+
!!currentNode[LEXICAL_ELEMENT_KEY]
|
|
1189
|
+
) {
|
|
1190
|
+
return {
|
|
1191
|
+
cellElement: cellNode,
|
|
1192
|
+
tableElement:
|
|
1193
|
+
currentNode as HTMLTableElementWithWithTableSelectionState,
|
|
1194
|
+
tableObserver: currentNode[LEXICAL_ELEMENT_KEY] as TableObserver,
|
|
1195
|
+
};
|
|
1196
|
+
}
|
|
1197
|
+
currentNode = currentNode.parentNode;
|
|
1198
|
+
}
|
|
1199
|
+
return null;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
export function getDOMCellFromTarget(node: null | Node): TableDOMCell | null {
|
|
1203
|
+
let currentNode: ParentNode | Node | null = node;
|
|
1204
|
+
|
|
1205
|
+
while (currentNode != null) {
|
|
1206
|
+
const nodeName = currentNode.nodeName;
|
|
1207
|
+
|
|
1208
|
+
if (nodeName === 'TD' || nodeName === 'TH') {
|
|
1209
|
+
// @ts-expect-error: internal field
|
|
1210
|
+
const cell = currentNode._cell;
|
|
1211
|
+
|
|
1212
|
+
if (cell === undefined) {
|
|
1213
|
+
return null;
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
return cell;
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
currentNode = currentNode.parentNode;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
return null;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
export function getDOMCellInTableFromTarget(
|
|
1226
|
+
table: HTMLTableElementWithWithTableSelectionState,
|
|
1227
|
+
node: null | Node,
|
|
1228
|
+
): TableDOMCell | null {
|
|
1229
|
+
if (!table.contains(node)) {
|
|
1230
|
+
return null;
|
|
1231
|
+
}
|
|
1232
|
+
let cell: null | TableDOMCell = null;
|
|
1233
|
+
for (
|
|
1234
|
+
let currentNode: ParentNode | Node | null = node;
|
|
1235
|
+
currentNode != null;
|
|
1236
|
+
currentNode = currentNode.parentNode
|
|
1237
|
+
) {
|
|
1238
|
+
if (currentNode === table) {
|
|
1239
|
+
return cell;
|
|
1240
|
+
}
|
|
1241
|
+
const nodeName = currentNode.nodeName;
|
|
1242
|
+
if (nodeName === 'TD' || nodeName === 'TH') {
|
|
1243
|
+
// @ts-expect-error: internal field
|
|
1244
|
+
cell = currentNode._cell || null;
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
return null;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
export function doesTargetContainText(node: Node): boolean {
|
|
1251
|
+
const currentNode: ParentNode | Node | null = node;
|
|
1252
|
+
|
|
1253
|
+
if (currentNode !== null) {
|
|
1254
|
+
const nodeName = currentNode.nodeName;
|
|
1255
|
+
|
|
1256
|
+
if (nodeName === 'SPAN') {
|
|
1257
|
+
return true;
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
return false;
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
export function getTable(
|
|
1264
|
+
tableNode: TableNode,
|
|
1265
|
+
dom: HTMLElement,
|
|
1266
|
+
): TableDOMTable {
|
|
1267
|
+
const tableElement = getTableElement(tableNode, dom);
|
|
1268
|
+
const domRows: TableDOMRows = [];
|
|
1269
|
+
const grid = {
|
|
1270
|
+
columns: 0,
|
|
1271
|
+
domRows,
|
|
1272
|
+
rows: 0,
|
|
1273
|
+
};
|
|
1274
|
+
let currentNode = tableElement.querySelector('tr') as ChildNode | null;
|
|
1275
|
+
let x = 0;
|
|
1276
|
+
let y = 0;
|
|
1277
|
+
domRows.length = 0;
|
|
1278
|
+
|
|
1279
|
+
while (currentNode != null) {
|
|
1280
|
+
const nodeMame = currentNode.nodeName;
|
|
1281
|
+
|
|
1282
|
+
if (nodeMame === 'TD' || nodeMame === 'TH') {
|
|
1283
|
+
const elem = currentNode as HTMLElement;
|
|
1284
|
+
const cell = {
|
|
1285
|
+
elem,
|
|
1286
|
+
hasBackgroundColor: elem.style.backgroundColor !== '',
|
|
1287
|
+
highlighted: false,
|
|
1288
|
+
x,
|
|
1289
|
+
y,
|
|
1290
|
+
};
|
|
1291
|
+
|
|
1292
|
+
// @ts-expect-error: internal field
|
|
1293
|
+
currentNode._cell = cell;
|
|
1294
|
+
|
|
1295
|
+
let row = domRows[y];
|
|
1296
|
+
if (row === undefined) {
|
|
1297
|
+
row = domRows[y] = [];
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
row[x] = cell;
|
|
1301
|
+
} else {
|
|
1302
|
+
const child = currentNode.firstChild;
|
|
1303
|
+
|
|
1304
|
+
if (child != null) {
|
|
1305
|
+
currentNode = child;
|
|
1306
|
+
continue;
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
const sibling = currentNode.nextSibling;
|
|
1311
|
+
|
|
1312
|
+
if (sibling != null) {
|
|
1313
|
+
x++;
|
|
1314
|
+
currentNode = sibling;
|
|
1315
|
+
continue;
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
const parent = currentNode.parentNode;
|
|
1319
|
+
|
|
1320
|
+
if (parent != null) {
|
|
1321
|
+
const parentSibling = parent.nextSibling;
|
|
1322
|
+
|
|
1323
|
+
if (parentSibling == null) {
|
|
1324
|
+
break;
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
y++;
|
|
1328
|
+
x = 0;
|
|
1329
|
+
currentNode = parentSibling;
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
grid.columns = x + 1;
|
|
1334
|
+
grid.rows = y + 1;
|
|
1335
|
+
|
|
1336
|
+
return grid;
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
export function $updateDOMForSelection(
|
|
1340
|
+
editor: LexicalEditor,
|
|
1341
|
+
table: TableDOMTable,
|
|
1342
|
+
selection: TableSelection | RangeSelection | null,
|
|
1343
|
+
) {
|
|
1344
|
+
const selectedCellNodes = new Set(selection ? selection.getNodes() : []);
|
|
1345
|
+
$forEachTableCell(table, (cell, lexicalNode) => {
|
|
1346
|
+
const elem = cell.elem;
|
|
1347
|
+
|
|
1348
|
+
if (selectedCellNodes.has(lexicalNode)) {
|
|
1349
|
+
cell.highlighted = true;
|
|
1350
|
+
$addHighlightToDOM(editor, cell);
|
|
1351
|
+
} else {
|
|
1352
|
+
cell.highlighted = false;
|
|
1353
|
+
$removeHighlightFromDOM(editor, cell);
|
|
1354
|
+
if (!elem.getAttribute('style')) {
|
|
1355
|
+
elem.removeAttribute('style');
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
export function $forEachTableCell(
|
|
1362
|
+
grid: TableDOMTable,
|
|
1363
|
+
cb: (
|
|
1364
|
+
cell: TableDOMCell,
|
|
1365
|
+
lexicalNode: LexicalNode,
|
|
1366
|
+
cords: {
|
|
1367
|
+
x: number;
|
|
1368
|
+
y: number;
|
|
1369
|
+
},
|
|
1370
|
+
) => void,
|
|
1371
|
+
) {
|
|
1372
|
+
const {domRows} = grid;
|
|
1373
|
+
|
|
1374
|
+
for (let y = 0; y < domRows.length; y++) {
|
|
1375
|
+
const row = domRows[y];
|
|
1376
|
+
if (!row) {
|
|
1377
|
+
continue;
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
for (let x = 0; x < row.length; x++) {
|
|
1381
|
+
const cell = row[x];
|
|
1382
|
+
if (!cell) {
|
|
1383
|
+
continue;
|
|
1384
|
+
}
|
|
1385
|
+
const lexicalNode = $getNearestNodeFromDOMNode(cell.elem);
|
|
1386
|
+
|
|
1387
|
+
if (lexicalNode !== null) {
|
|
1388
|
+
cb(cell, lexicalNode, {
|
|
1389
|
+
x,
|
|
1390
|
+
y,
|
|
1391
|
+
});
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
export function $addHighlightStyleToTable(
|
|
1398
|
+
editor: LexicalEditor,
|
|
1399
|
+
tableSelection: TableObserver,
|
|
1400
|
+
) {
|
|
1401
|
+
tableSelection.$disableHighlightStyle();
|
|
1402
|
+
$forEachTableCell(tableSelection.table, cell => {
|
|
1403
|
+
cell.highlighted = true;
|
|
1404
|
+
$addHighlightToDOM(editor, cell);
|
|
1405
|
+
});
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
export function $removeHighlightStyleToTable(
|
|
1409
|
+
editor: LexicalEditor,
|
|
1410
|
+
tableObserver: TableObserver,
|
|
1411
|
+
) {
|
|
1412
|
+
tableObserver.$enableHighlightStyle();
|
|
1413
|
+
$forEachTableCell(tableObserver.table, cell => {
|
|
1414
|
+
const elem = cell.elem;
|
|
1415
|
+
cell.highlighted = false;
|
|
1416
|
+
$removeHighlightFromDOM(editor, cell);
|
|
1417
|
+
|
|
1418
|
+
if (!elem.getAttribute('style')) {
|
|
1419
|
+
elem.removeAttribute('style');
|
|
1420
|
+
}
|
|
1421
|
+
});
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
function $selectAdjacentCell(
|
|
1425
|
+
tableCellNode: TableCellNode,
|
|
1426
|
+
direction: 'next' | 'previous',
|
|
1427
|
+
) {
|
|
1428
|
+
const siblingMethod =
|
|
1429
|
+
direction === 'next' ? 'getNextSibling' : 'getPreviousSibling';
|
|
1430
|
+
const childMethod = direction === 'next' ? 'getFirstChild' : 'getLastChild';
|
|
1431
|
+
const sibling = tableCellNode[siblingMethod]();
|
|
1432
|
+
if ($isElementNode(sibling)) {
|
|
1433
|
+
return sibling.selectEnd();
|
|
1434
|
+
}
|
|
1435
|
+
const parentRow = $findMatchingParent(tableCellNode, $isTableRowNode);
|
|
1436
|
+
invariant(parentRow !== null, 'selectAdjacentCell: Cell not in table row');
|
|
1437
|
+
for (
|
|
1438
|
+
let nextRow = parentRow[siblingMethod]();
|
|
1439
|
+
$isTableRowNode(nextRow);
|
|
1440
|
+
nextRow = nextRow[siblingMethod]()
|
|
1441
|
+
) {
|
|
1442
|
+
const child = nextRow[childMethod]();
|
|
1443
|
+
if ($isElementNode(child)) {
|
|
1444
|
+
return child.selectEnd();
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
const parentTable = $findMatchingParent(parentRow, $isTableNode);
|
|
1448
|
+
invariant(parentTable !== null, 'selectAdjacentCell: Row not in table');
|
|
1449
|
+
return direction === 'next'
|
|
1450
|
+
? parentTable.selectNext()
|
|
1451
|
+
: parentTable.selectPrevious();
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
type Direction = 'backward' | 'forward' | 'up' | 'down';
|
|
1455
|
+
|
|
1456
|
+
const selectTableNodeInDirection = (
|
|
1457
|
+
tableObserver: TableObserver,
|
|
1458
|
+
tableNode: TableNode,
|
|
1459
|
+
x: number,
|
|
1460
|
+
y: number,
|
|
1461
|
+
direction: Direction,
|
|
1462
|
+
): boolean => {
|
|
1463
|
+
const isForward = direction === 'forward';
|
|
1464
|
+
|
|
1465
|
+
switch (direction) {
|
|
1466
|
+
case 'backward':
|
|
1467
|
+
case 'forward':
|
|
1468
|
+
if (x !== (isForward ? tableObserver.table.columns - 1 : 0)) {
|
|
1469
|
+
selectTableCellNode(
|
|
1470
|
+
tableNode.getCellNodeFromCordsOrThrow(
|
|
1471
|
+
x + (isForward ? 1 : -1),
|
|
1472
|
+
y,
|
|
1473
|
+
tableObserver.table,
|
|
1474
|
+
),
|
|
1475
|
+
isForward,
|
|
1476
|
+
);
|
|
1477
|
+
} else {
|
|
1478
|
+
if (y !== (isForward ? tableObserver.table.rows - 1 : 0)) {
|
|
1479
|
+
selectTableCellNode(
|
|
1480
|
+
tableNode.getCellNodeFromCordsOrThrow(
|
|
1481
|
+
isForward ? 0 : tableObserver.table.columns - 1,
|
|
1482
|
+
y + (isForward ? 1 : -1),
|
|
1483
|
+
tableObserver.table,
|
|
1484
|
+
),
|
|
1485
|
+
isForward,
|
|
1486
|
+
);
|
|
1487
|
+
} else if (!isForward) {
|
|
1488
|
+
tableNode.selectPrevious();
|
|
1489
|
+
} else {
|
|
1490
|
+
tableNode.selectNext();
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
return true;
|
|
1495
|
+
|
|
1496
|
+
case 'up':
|
|
1497
|
+
if (y !== 0) {
|
|
1498
|
+
selectTableCellNode(
|
|
1499
|
+
tableNode.getCellNodeFromCordsOrThrow(x, y - 1, tableObserver.table),
|
|
1500
|
+
false,
|
|
1501
|
+
);
|
|
1502
|
+
} else {
|
|
1503
|
+
tableNode.selectPrevious();
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
return true;
|
|
1507
|
+
|
|
1508
|
+
case 'down':
|
|
1509
|
+
if (y !== tableObserver.table.rows - 1) {
|
|
1510
|
+
selectTableCellNode(
|
|
1511
|
+
tableNode.getCellNodeFromCordsOrThrow(x, y + 1, tableObserver.table),
|
|
1512
|
+
true,
|
|
1513
|
+
);
|
|
1514
|
+
} else {
|
|
1515
|
+
tableNode.selectNext();
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
return true;
|
|
1519
|
+
default:
|
|
1520
|
+
return false;
|
|
1521
|
+
}
|
|
1522
|
+
};
|
|
1523
|
+
|
|
1524
|
+
type Corner = ['minColumn' | 'maxColumn', 'minRow' | 'maxRow'];
|
|
1525
|
+
function getCorner(
|
|
1526
|
+
rect: TableCellRectBoundary,
|
|
1527
|
+
cellValue: TableMapValueType,
|
|
1528
|
+
): Corner | null {
|
|
1529
|
+
let colName: 'minColumn' | 'maxColumn';
|
|
1530
|
+
let rowName: 'minRow' | 'maxRow';
|
|
1531
|
+
if (cellValue.startColumn === rect.minColumn) {
|
|
1532
|
+
colName = 'minColumn';
|
|
1533
|
+
} else if (
|
|
1534
|
+
cellValue.startColumn + cellValue.cell.__colSpan - 1 ===
|
|
1535
|
+
rect.maxColumn
|
|
1536
|
+
) {
|
|
1537
|
+
colName = 'maxColumn';
|
|
1538
|
+
} else {
|
|
1539
|
+
return null;
|
|
1540
|
+
}
|
|
1541
|
+
if (cellValue.startRow === rect.minRow) {
|
|
1542
|
+
rowName = 'minRow';
|
|
1543
|
+
} else if (
|
|
1544
|
+
cellValue.startRow + cellValue.cell.__rowSpan - 1 ===
|
|
1545
|
+
rect.maxRow
|
|
1546
|
+
) {
|
|
1547
|
+
rowName = 'maxRow';
|
|
1548
|
+
} else {
|
|
1549
|
+
return null;
|
|
1550
|
+
}
|
|
1551
|
+
return [colName, rowName];
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
function getCornerOrThrow(
|
|
1555
|
+
rect: TableCellRectBoundary,
|
|
1556
|
+
cellValue: TableMapValueType,
|
|
1557
|
+
): Corner {
|
|
1558
|
+
const corner = getCorner(rect, cellValue);
|
|
1559
|
+
invariant(
|
|
1560
|
+
corner !== null,
|
|
1561
|
+
'getCornerOrThrow: cell %s is not at a corner of rect',
|
|
1562
|
+
cellValue.cell.getKey(),
|
|
1563
|
+
);
|
|
1564
|
+
return corner;
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
function oppositeCorner([colName, rowName]: Corner): Corner {
|
|
1568
|
+
return [
|
|
1569
|
+
colName === 'minColumn' ? 'maxColumn' : 'minColumn',
|
|
1570
|
+
rowName === 'minRow' ? 'maxRow' : 'minRow',
|
|
1571
|
+
];
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
function cellAtCornerOrThrow(
|
|
1575
|
+
tableMap: TableMapType,
|
|
1576
|
+
rect: TableCellRectBoundary,
|
|
1577
|
+
[colName, rowName]: Corner,
|
|
1578
|
+
): TableMapValueType {
|
|
1579
|
+
const rowNum = rect[rowName];
|
|
1580
|
+
const rowMap = tableMap[rowNum];
|
|
1581
|
+
invariant(
|
|
1582
|
+
rowMap !== undefined,
|
|
1583
|
+
'cellAtCornerOrThrow: %s = %s missing in tableMap',
|
|
1584
|
+
rowName,
|
|
1585
|
+
String(rowNum),
|
|
1586
|
+
);
|
|
1587
|
+
const colNum = rect[colName];
|
|
1588
|
+
const cell = rowMap[colNum];
|
|
1589
|
+
invariant(
|
|
1590
|
+
cell !== undefined,
|
|
1591
|
+
'cellAtCornerOrThrow: %s = %s missing in tableMap',
|
|
1592
|
+
colName,
|
|
1593
|
+
String(colNum),
|
|
1594
|
+
);
|
|
1595
|
+
return cell;
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
function $extractRectCorners(
|
|
1599
|
+
tableMap: TableMapType,
|
|
1600
|
+
anchorCellValue: TableMapValueType,
|
|
1601
|
+
newFocusCellValue: TableMapValueType,
|
|
1602
|
+
) {
|
|
1603
|
+
// We are sure that the focus now either contracts or expands the rect
|
|
1604
|
+
// but both the anchor and focus might be moved to ensure a rectangle
|
|
1605
|
+
// given a potentially ragged merge shape
|
|
1606
|
+
const rect = $computeTableCellRectBoundary(
|
|
1607
|
+
tableMap,
|
|
1608
|
+
anchorCellValue,
|
|
1609
|
+
newFocusCellValue,
|
|
1610
|
+
);
|
|
1611
|
+
const anchorCorner = getCorner(rect, anchorCellValue);
|
|
1612
|
+
if (anchorCorner) {
|
|
1613
|
+
return [
|
|
1614
|
+
cellAtCornerOrThrow(tableMap, rect, anchorCorner),
|
|
1615
|
+
cellAtCornerOrThrow(tableMap, rect, oppositeCorner(anchorCorner)),
|
|
1616
|
+
];
|
|
1617
|
+
}
|
|
1618
|
+
const newFocusCorner = getCorner(rect, newFocusCellValue);
|
|
1619
|
+
if (newFocusCorner) {
|
|
1620
|
+
return [
|
|
1621
|
+
cellAtCornerOrThrow(tableMap, rect, oppositeCorner(newFocusCorner)),
|
|
1622
|
+
cellAtCornerOrThrow(tableMap, rect, newFocusCorner),
|
|
1623
|
+
];
|
|
1624
|
+
}
|
|
1625
|
+
// TODO this doesn't have to be arbitrary, use the closest corner instead
|
|
1626
|
+
const newAnchorCorner: Corner = ['minColumn', 'minRow'];
|
|
1627
|
+
return [
|
|
1628
|
+
cellAtCornerOrThrow(tableMap, rect, newAnchorCorner),
|
|
1629
|
+
cellAtCornerOrThrow(tableMap, rect, oppositeCorner(newAnchorCorner)),
|
|
1630
|
+
];
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
function $adjustFocusInDirection(
|
|
1634
|
+
tableObserver: TableObserver,
|
|
1635
|
+
tableMap: TableMapType,
|
|
1636
|
+
anchorCellValue: TableMapValueType,
|
|
1637
|
+
focusCellValue: TableMapValueType,
|
|
1638
|
+
direction: Direction,
|
|
1639
|
+
): boolean {
|
|
1640
|
+
const rect = $computeTableCellRectBoundary(
|
|
1641
|
+
tableMap,
|
|
1642
|
+
anchorCellValue,
|
|
1643
|
+
focusCellValue,
|
|
1644
|
+
);
|
|
1645
|
+
const spans = $computeTableCellRectSpans(tableMap, rect);
|
|
1646
|
+
const {topSpan, leftSpan, bottomSpan, rightSpan} = spans;
|
|
1647
|
+
const anchorCorner = getCornerOrThrow(rect, anchorCellValue);
|
|
1648
|
+
const [focusColumn, focusRow] = oppositeCorner(anchorCorner);
|
|
1649
|
+
let fCol = rect[focusColumn];
|
|
1650
|
+
let fRow = rect[focusRow];
|
|
1651
|
+
if (direction === 'forward') {
|
|
1652
|
+
fCol += focusColumn === 'maxColumn' ? 1 : leftSpan;
|
|
1653
|
+
} else if (direction === 'backward') {
|
|
1654
|
+
fCol -= focusColumn === 'minColumn' ? 1 : rightSpan;
|
|
1655
|
+
} else if (direction === 'down') {
|
|
1656
|
+
fRow += focusRow === 'maxRow' ? 1 : topSpan;
|
|
1657
|
+
} else if (direction === 'up') {
|
|
1658
|
+
fRow -= focusRow === 'minRow' ? 1 : bottomSpan;
|
|
1659
|
+
}
|
|
1660
|
+
const targetRowMap = tableMap[fRow];
|
|
1661
|
+
if (targetRowMap === undefined) {
|
|
1662
|
+
return false;
|
|
1663
|
+
}
|
|
1664
|
+
const newFocusCellValue = targetRowMap[fCol];
|
|
1665
|
+
if (newFocusCellValue === undefined) {
|
|
1666
|
+
return false;
|
|
1667
|
+
}
|
|
1668
|
+
// We can be certain that anchorCellValue and newFocusCellValue are
|
|
1669
|
+
// contained within the desired selection, but we are not certain if
|
|
1670
|
+
// they need to be expanded or not to maintain a rectangular shape
|
|
1671
|
+
const [finalAnchorCell, finalFocusCell] = $extractRectCorners(
|
|
1672
|
+
tableMap,
|
|
1673
|
+
anchorCellValue,
|
|
1674
|
+
newFocusCellValue,
|
|
1675
|
+
);
|
|
1676
|
+
const anchorDOM = $getObserverCellFromCellNodeOrThrow(
|
|
1677
|
+
tableObserver,
|
|
1678
|
+
finalAnchorCell.cell,
|
|
1679
|
+
)!;
|
|
1680
|
+
const focusDOM = $getObserverCellFromCellNodeOrThrow(
|
|
1681
|
+
tableObserver,
|
|
1682
|
+
finalFocusCell.cell,
|
|
1683
|
+
);
|
|
1684
|
+
tableObserver.$setAnchorCellForSelection(anchorDOM);
|
|
1685
|
+
tableObserver.$setFocusCellForSelection(focusDOM, true);
|
|
1686
|
+
return true;
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
function $isSelectionInTable(
|
|
1690
|
+
selection: null | BaseSelection,
|
|
1691
|
+
tableNode: TableNode,
|
|
1692
|
+
): boolean {
|
|
1693
|
+
if ($isRangeSelection(selection) || $isTableSelection(selection)) {
|
|
1694
|
+
// TODO this should probably return false if there's an unrelated
|
|
1695
|
+
// shadow root between the node and the table (e.g. another table,
|
|
1696
|
+
// collapsible, etc.)
|
|
1697
|
+
const isAnchorInside = tableNode.isParentOf(selection.anchor.getNode());
|
|
1698
|
+
const isFocusInside = tableNode.isParentOf(selection.focus.getNode());
|
|
1699
|
+
|
|
1700
|
+
return isAnchorInside && isFocusInside;
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
return false;
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
function $isFullTableSelection(
|
|
1707
|
+
selection: null | BaseSelection,
|
|
1708
|
+
tableNode: TableNode,
|
|
1709
|
+
): boolean {
|
|
1710
|
+
if ($isTableSelection(selection)) {
|
|
1711
|
+
const anchorNode = selection.anchor.getNode() as TableCellNode;
|
|
1712
|
+
const focusNode = selection.focus.getNode() as TableCellNode;
|
|
1713
|
+
if (tableNode && anchorNode && focusNode) {
|
|
1714
|
+
const [map] = $computeTableMap(tableNode, anchorNode, focusNode);
|
|
1715
|
+
return (
|
|
1716
|
+
anchorNode.getKey() === map[0][0].cell.getKey() &&
|
|
1717
|
+
focusNode.getKey() === map[map.length - 1].at(-1)!.cell.getKey()
|
|
1718
|
+
);
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
return false;
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
function selectTableCellNode(tableCell: TableCellNode, fromStart: boolean) {
|
|
1725
|
+
if (fromStart) {
|
|
1726
|
+
tableCell.selectStart();
|
|
1727
|
+
} else {
|
|
1728
|
+
tableCell.selectEnd();
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
function $addHighlightToDOM(editor: LexicalEditor, cell: TableDOMCell): void {
|
|
1733
|
+
const element = cell.elem;
|
|
1734
|
+
const editorThemeClasses = editor._config.theme;
|
|
1735
|
+
const node = $getNearestNodeFromDOMNode(element);
|
|
1736
|
+
invariant(
|
|
1737
|
+
$isTableCellNode(node),
|
|
1738
|
+
'Expected to find LexicalNode from Table Cell DOMNode',
|
|
1739
|
+
);
|
|
1740
|
+
addClassNamesToElement(element, editorThemeClasses.tableCellSelected);
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
function $removeHighlightFromDOM(
|
|
1744
|
+
editor: LexicalEditor,
|
|
1745
|
+
cell: TableDOMCell,
|
|
1746
|
+
): void {
|
|
1747
|
+
const element = cell.elem;
|
|
1748
|
+
const node = $getNearestNodeFromDOMNode(element);
|
|
1749
|
+
invariant(
|
|
1750
|
+
$isTableCellNode(node),
|
|
1751
|
+
'Expected to find LexicalNode from Table Cell DOMNode',
|
|
1752
|
+
);
|
|
1753
|
+
const editorThemeClasses = editor._config.theme;
|
|
1754
|
+
removeClassNamesFromElement(element, editorThemeClasses.tableCellSelected);
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
export function $findCellNode(node: LexicalNode): null | TableCellNode {
|
|
1758
|
+
const cellNode = $findMatchingParent(node, $isTableCellNode);
|
|
1759
|
+
return $isTableCellNode(cellNode) ? cellNode : null;
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
export function $findTableNode(node: LexicalNode): null | TableNode {
|
|
1763
|
+
const tableNode = $findMatchingParent(node, $isTableNode);
|
|
1764
|
+
return $isTableNode(tableNode) ? tableNode : null;
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
function $getBlockParentIfFirstNode(node: LexicalNode): ElementNode | null {
|
|
1768
|
+
for (
|
|
1769
|
+
let prevNode = node, currentNode: LexicalNode | null = node;
|
|
1770
|
+
currentNode !== null;
|
|
1771
|
+
prevNode = currentNode, currentNode = currentNode.getParent()
|
|
1772
|
+
) {
|
|
1773
|
+
if ($isElementNode(currentNode)) {
|
|
1774
|
+
if (
|
|
1775
|
+
currentNode !== prevNode &&
|
|
1776
|
+
currentNode.getFirstChild() !== prevNode
|
|
1777
|
+
) {
|
|
1778
|
+
// Not the first child or the initial node
|
|
1779
|
+
return null;
|
|
1780
|
+
} else if (!currentNode.isInline()) {
|
|
1781
|
+
return currentNode;
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
return null;
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
function $handleHorizontalArrowKeyRangeSelection(
|
|
1789
|
+
editor: LexicalEditor,
|
|
1790
|
+
event: KeyboardEvent,
|
|
1791
|
+
selection: RangeSelection,
|
|
1792
|
+
alter: 'extend' | 'move',
|
|
1793
|
+
isBackward: boolean,
|
|
1794
|
+
tableNode: TableNode,
|
|
1795
|
+
tableObserver: TableObserver,
|
|
1796
|
+
): boolean {
|
|
1797
|
+
const initialFocus = $caretFromPoint(
|
|
1798
|
+
selection.focus,
|
|
1799
|
+
isBackward ? 'previous' : 'next',
|
|
1800
|
+
);
|
|
1801
|
+
if ($isExtendableTextPointCaret(initialFocus)) {
|
|
1802
|
+
return false;
|
|
1803
|
+
}
|
|
1804
|
+
let lastCaret = initialFocus;
|
|
1805
|
+
// TableCellNode is the only shadow root we are interested in piercing so
|
|
1806
|
+
// we find the last internal caret and then check its parent
|
|
1807
|
+
for (const nextCaret of $extendCaretToRange(initialFocus).iterNodeCarets(
|
|
1808
|
+
'shadowRoot',
|
|
1809
|
+
)) {
|
|
1810
|
+
if (!($isSiblingCaret(nextCaret) && $isElementNode(nextCaret.origin))) {
|
|
1811
|
+
return false;
|
|
1812
|
+
}
|
|
1813
|
+
lastCaret = nextCaret;
|
|
1814
|
+
}
|
|
1815
|
+
const lastCaretParent = lastCaret.getParentAtCaret();
|
|
1816
|
+
if (!$isTableCellNode(lastCaretParent)) {
|
|
1817
|
+
return false;
|
|
1818
|
+
}
|
|
1819
|
+
const anchorCell = lastCaretParent;
|
|
1820
|
+
const focusCaret = $findNextTableCell(
|
|
1821
|
+
$getSiblingCaret(anchorCell, lastCaret.direction),
|
|
1822
|
+
);
|
|
1823
|
+
const anchorCellTable = $findMatchingParent(anchorCell, $isTableNode);
|
|
1824
|
+
if (!(anchorCellTable && anchorCellTable.is(tableNode))) {
|
|
1825
|
+
return false;
|
|
1826
|
+
}
|
|
1827
|
+
const anchorCellDOM = editor.getElementByKey(anchorCell.getKey());
|
|
1828
|
+
const anchorDOMCell = getDOMCellFromTarget(anchorCellDOM);
|
|
1829
|
+
if (!anchorCellDOM || !anchorDOMCell) {
|
|
1830
|
+
return false;
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
const anchorCellTableElement = $getElementForTableNode(
|
|
1834
|
+
editor,
|
|
1835
|
+
anchorCellTable,
|
|
1836
|
+
);
|
|
1837
|
+
tableObserver.table = anchorCellTableElement;
|
|
1838
|
+
if (!focusCaret) {
|
|
1839
|
+
if (alter === 'extend') {
|
|
1840
|
+
// extend the selection from a range inside the cell to a table selection of the cell
|
|
1841
|
+
tableObserver.$setAnchorCellForSelection(anchorDOMCell);
|
|
1842
|
+
tableObserver.$setFocusCellForSelection(anchorDOMCell, true);
|
|
1843
|
+
} else {
|
|
1844
|
+
// exit the table
|
|
1845
|
+
const outerFocusCaret = $getTableExitCaret(
|
|
1846
|
+
$getSiblingCaret(anchorCellTable, initialFocus.direction),
|
|
1847
|
+
);
|
|
1848
|
+
$setPointFromCaret(selection.anchor, outerFocusCaret);
|
|
1849
|
+
$setPointFromCaret(selection.focus, outerFocusCaret);
|
|
1850
|
+
}
|
|
1851
|
+
} else if (alter === 'extend') {
|
|
1852
|
+
const focusDOMCell = getDOMCellFromTarget(
|
|
1853
|
+
editor.getElementByKey(focusCaret.origin.getKey()),
|
|
1854
|
+
);
|
|
1855
|
+
if (!focusDOMCell) {
|
|
1856
|
+
return false;
|
|
1857
|
+
}
|
|
1858
|
+
tableObserver.$setAnchorCellForSelection(anchorDOMCell);
|
|
1859
|
+
tableObserver.$setFocusCellForSelection(focusDOMCell, true);
|
|
1860
|
+
} else {
|
|
1861
|
+
// alter === 'move'
|
|
1862
|
+
const innerFocusCaret = $normalizeCaret(focusCaret);
|
|
1863
|
+
$setPointFromCaret(selection.anchor, innerFocusCaret);
|
|
1864
|
+
$setPointFromCaret(selection.focus, innerFocusCaret);
|
|
1865
|
+
}
|
|
1866
|
+
stopEvent(event);
|
|
1867
|
+
return true;
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
function $getTableExitCaret<D extends CaretDirection>(
|
|
1871
|
+
initialCaret: SiblingCaret<TableNode, D>,
|
|
1872
|
+
): PointCaret<D> {
|
|
1873
|
+
const adjacent = $getAdjacentChildCaret(initialCaret);
|
|
1874
|
+
return $isChildCaret(adjacent) ? $normalizeCaret(adjacent) : initialCaret;
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
function $findNextTableCell<D extends CaretDirection>(
|
|
1878
|
+
initialCaret: SiblingCaret<TableCellNode, D>,
|
|
1879
|
+
): null | ChildCaret<TableCellNode, D> {
|
|
1880
|
+
for (const nextCaret of $extendCaretToRange(initialCaret).iterNodeCarets(
|
|
1881
|
+
'root',
|
|
1882
|
+
)) {
|
|
1883
|
+
const {origin} = nextCaret;
|
|
1884
|
+
if ($isTableCellNode(origin)) {
|
|
1885
|
+
// not sure why ts isn't narrowing here (even if the guard is on nextCaret.origin)
|
|
1886
|
+
// but returning a new caret is fine
|
|
1887
|
+
if ($isChildCaret(nextCaret)) {
|
|
1888
|
+
return $getChildCaret(origin, initialCaret.direction);
|
|
1889
|
+
}
|
|
1890
|
+
} else if (!$isTableRowNode(origin)) {
|
|
1891
|
+
break;
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
return null;
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
function $handleArrowKey(
|
|
1898
|
+
editor: LexicalEditor,
|
|
1899
|
+
event: KeyboardEvent,
|
|
1900
|
+
direction: Direction,
|
|
1901
|
+
tableNode: TableNode,
|
|
1902
|
+
tableObserver: TableObserver,
|
|
1903
|
+
tableObservers: TableObservers,
|
|
1904
|
+
): boolean {
|
|
1905
|
+
if (
|
|
1906
|
+
(direction === 'up' || direction === 'down') &&
|
|
1907
|
+
isTypeaheadMenuInView(editor)
|
|
1908
|
+
) {
|
|
1909
|
+
return false;
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
const selection = $getSelection();
|
|
1913
|
+
|
|
1914
|
+
// Handle arrow key into a table (including from a table into a nested table)
|
|
1915
|
+
if (!$isSelectionInTable(selection, tableNode)) {
|
|
1916
|
+
if ($isRangeSelection(selection)) {
|
|
1917
|
+
if (direction === 'backward') {
|
|
1918
|
+
if (selection.focus.offset > 0) {
|
|
1919
|
+
return false;
|
|
1920
|
+
}
|
|
1921
|
+
const parentNode = $getBlockParentIfFirstNode(
|
|
1922
|
+
selection.focus.getNode(),
|
|
1923
|
+
);
|
|
1924
|
+
if (!parentNode) {
|
|
1925
|
+
return false;
|
|
1926
|
+
}
|
|
1927
|
+
const siblingNode = parentNode.getPreviousSibling();
|
|
1928
|
+
if (!$isTableNode(siblingNode)) {
|
|
1929
|
+
return false;
|
|
1930
|
+
}
|
|
1931
|
+
stopEvent(event);
|
|
1932
|
+
if (event.shiftKey) {
|
|
1933
|
+
selection.focus.set(
|
|
1934
|
+
siblingNode.getParentOrThrow().getKey(),
|
|
1935
|
+
siblingNode.getIndexWithinParent(),
|
|
1936
|
+
'element',
|
|
1937
|
+
);
|
|
1938
|
+
} else {
|
|
1939
|
+
siblingNode.selectEnd();
|
|
1940
|
+
}
|
|
1941
|
+
return true;
|
|
1942
|
+
} else if (
|
|
1943
|
+
event.shiftKey &&
|
|
1944
|
+
(direction === 'up' || direction === 'down')
|
|
1945
|
+
) {
|
|
1946
|
+
const focusNode = selection.focus.getNode();
|
|
1947
|
+
const isTableUnselect =
|
|
1948
|
+
!selection.isCollapsed() &&
|
|
1949
|
+
((direction === 'up' && !selection.isBackward()) ||
|
|
1950
|
+
(direction === 'down' && selection.isBackward()));
|
|
1951
|
+
if (isTableUnselect) {
|
|
1952
|
+
let focusParentNode = $findMatchingParent(focusNode, n =>
|
|
1953
|
+
$isTableNode(n),
|
|
1954
|
+
);
|
|
1955
|
+
if ($isTableCellNode(focusParentNode)) {
|
|
1956
|
+
focusParentNode = $findMatchingParent(
|
|
1957
|
+
focusParentNode,
|
|
1958
|
+
$isTableNode,
|
|
1959
|
+
);
|
|
1960
|
+
}
|
|
1961
|
+
if (focusParentNode !== tableNode) {
|
|
1962
|
+
return false;
|
|
1963
|
+
}
|
|
1964
|
+
if (!focusParentNode) {
|
|
1965
|
+
return false;
|
|
1966
|
+
}
|
|
1967
|
+
const sibling =
|
|
1968
|
+
direction === 'down'
|
|
1969
|
+
? focusParentNode.getNextSibling()
|
|
1970
|
+
: focusParentNode.getPreviousSibling();
|
|
1971
|
+
if (!sibling) {
|
|
1972
|
+
return false;
|
|
1973
|
+
}
|
|
1974
|
+
let newOffset = 0;
|
|
1975
|
+
if (direction === 'up') {
|
|
1976
|
+
if ($isElementNode(sibling)) {
|
|
1977
|
+
newOffset = sibling.getChildrenSize();
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
let newFocusNode = sibling;
|
|
1981
|
+
if (direction === 'up') {
|
|
1982
|
+
if ($isElementNode(sibling)) {
|
|
1983
|
+
const lastCell = sibling.getLastChild();
|
|
1984
|
+
newFocusNode = lastCell ? lastCell : sibling;
|
|
1985
|
+
newOffset = $isTextNode(newFocusNode)
|
|
1986
|
+
? newFocusNode.getTextContentSize()
|
|
1987
|
+
: 0;
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
const newSelection = selection.clone();
|
|
1991
|
+
|
|
1992
|
+
newSelection.focus.set(
|
|
1993
|
+
newFocusNode.getKey(),
|
|
1994
|
+
newOffset,
|
|
1995
|
+
$isTextNode(newFocusNode) ? 'text' : 'element',
|
|
1996
|
+
);
|
|
1997
|
+
$setSelection(newSelection);
|
|
1998
|
+
stopEvent(event);
|
|
1999
|
+
return true;
|
|
2000
|
+
} else if ($isRootOrShadowRoot(focusNode)) {
|
|
2001
|
+
const selectedNode =
|
|
2002
|
+
direction === 'up'
|
|
2003
|
+
? selection.getNodes()[selection.getNodes().length - 1]
|
|
2004
|
+
: selection.getNodes()[0];
|
|
2005
|
+
if (selectedNode) {
|
|
2006
|
+
const tableCellNode = $findParentTableCellNodeInTable(
|
|
2007
|
+
tableNode,
|
|
2008
|
+
selectedNode,
|
|
2009
|
+
);
|
|
2010
|
+
if (tableCellNode !== null) {
|
|
2011
|
+
const firstDescendant = tableNode.getFirstDescendant();
|
|
2012
|
+
const lastDescendant = tableNode.getLastDescendant();
|
|
2013
|
+
if (!firstDescendant || !lastDescendant) {
|
|
2014
|
+
return false;
|
|
2015
|
+
}
|
|
2016
|
+
const [firstCellNode] = $getNodeTriplet(firstDescendant);
|
|
2017
|
+
const [lastCellNode] = $getNodeTriplet(lastDescendant);
|
|
2018
|
+
const firstCellCoords = tableNode.getCordsFromCellNode(
|
|
2019
|
+
firstCellNode,
|
|
2020
|
+
tableObserver.table,
|
|
2021
|
+
);
|
|
2022
|
+
const lastCellCoords = tableNode.getCordsFromCellNode(
|
|
2023
|
+
lastCellNode,
|
|
2024
|
+
tableObserver.table,
|
|
2025
|
+
);
|
|
2026
|
+
const firstCellDOM = tableNode.getDOMCellFromCordsOrThrow(
|
|
2027
|
+
firstCellCoords.x,
|
|
2028
|
+
firstCellCoords.y,
|
|
2029
|
+
tableObserver.table,
|
|
2030
|
+
);
|
|
2031
|
+
const lastCellDOM = tableNode.getDOMCellFromCordsOrThrow(
|
|
2032
|
+
lastCellCoords.x,
|
|
2033
|
+
lastCellCoords.y,
|
|
2034
|
+
tableObserver.table,
|
|
2035
|
+
);
|
|
2036
|
+
tableObserver.$setAnchorCellForSelection(firstCellDOM);
|
|
2037
|
+
tableObserver.$setFocusCellForSelection(lastCellDOM, true);
|
|
2038
|
+
return true;
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
return false;
|
|
2042
|
+
} else {
|
|
2043
|
+
let focusParentNode = $findMatchingParent(
|
|
2044
|
+
focusNode,
|
|
2045
|
+
n => $isElementNode(n) && !n.isInline(),
|
|
2046
|
+
);
|
|
2047
|
+
if ($isTableCellNode(focusParentNode)) {
|
|
2048
|
+
focusParentNode = $findMatchingParent(
|
|
2049
|
+
focusParentNode,
|
|
2050
|
+
$isTableNode,
|
|
2051
|
+
);
|
|
2052
|
+
}
|
|
2053
|
+
if (!focusParentNode) {
|
|
2054
|
+
return false;
|
|
2055
|
+
}
|
|
2056
|
+
const sibling =
|
|
2057
|
+
direction === 'down'
|
|
2058
|
+
? focusParentNode.getNextSibling()
|
|
2059
|
+
: focusParentNode.getPreviousSibling();
|
|
2060
|
+
if (
|
|
2061
|
+
$isTableNode(sibling) &&
|
|
2062
|
+
tableObserver.tableNodeKey === sibling.getKey()
|
|
2063
|
+
) {
|
|
2064
|
+
const firstDescendant = sibling.getFirstDescendant();
|
|
2065
|
+
const lastDescendant = sibling.getLastDescendant();
|
|
2066
|
+
if (!firstDescendant || !lastDescendant) {
|
|
2067
|
+
return false;
|
|
2068
|
+
}
|
|
2069
|
+
const [firstCellNode] = $getNodeTriplet(firstDescendant);
|
|
2070
|
+
const [lastCellNode] = $getNodeTriplet(lastDescendant);
|
|
2071
|
+
const newSelection = selection.clone();
|
|
2072
|
+
newSelection.focus.set(
|
|
2073
|
+
(direction === 'up' ? firstCellNode : lastCellNode).getKey(),
|
|
2074
|
+
direction === 'up' ? 0 : lastCellNode.getChildrenSize(),
|
|
2075
|
+
'element',
|
|
2076
|
+
);
|
|
2077
|
+
stopEvent(event);
|
|
2078
|
+
$setSelection(newSelection);
|
|
2079
|
+
return true;
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
if (direction === 'down' && $isScrollableTablesActive(editor)) {
|
|
2085
|
+
// Enable Firefox workaround
|
|
2086
|
+
tableObservers.setShouldCheckSelectionForTable(tableNode.getKey());
|
|
2087
|
+
}
|
|
2088
|
+
return false;
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
if ($isRangeSelection(selection)) {
|
|
2092
|
+
if (direction === 'backward' || direction === 'forward') {
|
|
2093
|
+
const alter = event.shiftKey ? 'extend' : 'move';
|
|
2094
|
+
return $handleHorizontalArrowKeyRangeSelection(
|
|
2095
|
+
editor,
|
|
2096
|
+
event,
|
|
2097
|
+
selection,
|
|
2098
|
+
alter,
|
|
2099
|
+
direction === 'backward',
|
|
2100
|
+
tableNode,
|
|
2101
|
+
tableObserver,
|
|
2102
|
+
);
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
if (selection.isCollapsed()) {
|
|
2106
|
+
const {anchor, focus} = selection;
|
|
2107
|
+
const anchorCellNode = $findMatchingParent(
|
|
2108
|
+
anchor.getNode(),
|
|
2109
|
+
$isTableCellNode,
|
|
2110
|
+
);
|
|
2111
|
+
const focusCellNode = $findMatchingParent(
|
|
2112
|
+
focus.getNode(),
|
|
2113
|
+
$isTableCellNode,
|
|
2114
|
+
);
|
|
2115
|
+
if (
|
|
2116
|
+
!$isTableCellNode(anchorCellNode) ||
|
|
2117
|
+
!anchorCellNode.is(focusCellNode)
|
|
2118
|
+
) {
|
|
2119
|
+
return false;
|
|
2120
|
+
}
|
|
2121
|
+
const anchorCellTable = $findTableNode(anchorCellNode);
|
|
2122
|
+
if (anchorCellTable !== tableNode && anchorCellTable != null) {
|
|
2123
|
+
const anchorCellTableElement = getTableElement(
|
|
2124
|
+
anchorCellTable,
|
|
2125
|
+
editor.getElementByKey(anchorCellTable.getKey()),
|
|
2126
|
+
);
|
|
2127
|
+
if (anchorCellTableElement != null) {
|
|
2128
|
+
tableObserver.table = getTable(
|
|
2129
|
+
anchorCellTable,
|
|
2130
|
+
anchorCellTableElement,
|
|
2131
|
+
);
|
|
2132
|
+
return $handleArrowKey(
|
|
2133
|
+
editor,
|
|
2134
|
+
event,
|
|
2135
|
+
direction,
|
|
2136
|
+
anchorCellTable,
|
|
2137
|
+
tableObserver,
|
|
2138
|
+
tableObservers,
|
|
2139
|
+
);
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
const anchorCellDom = editor.getElementByKey(anchorCellNode.__key);
|
|
2144
|
+
const anchorDOM = editor.getElementByKey(anchor.key);
|
|
2145
|
+
if (anchorDOM == null || anchorCellDom == null) {
|
|
2146
|
+
return false;
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
let edgeSelectionRect;
|
|
2150
|
+
if (anchor.type === 'element') {
|
|
2151
|
+
edgeSelectionRect = anchorDOM.getBoundingClientRect();
|
|
2152
|
+
} else {
|
|
2153
|
+
const domSelection = getDOMSelection(getEditorWindow(editor));
|
|
2154
|
+
if (domSelection === null || domSelection.rangeCount === 0) {
|
|
2155
|
+
return false;
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
const range = domSelection.getRangeAt(0);
|
|
2159
|
+
edgeSelectionRect = range.getBoundingClientRect();
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
const edgeChild =
|
|
2163
|
+
direction === 'up'
|
|
2164
|
+
? anchorCellNode.getFirstChild()
|
|
2165
|
+
: anchorCellNode.getLastChild();
|
|
2166
|
+
if (edgeChild == null) {
|
|
2167
|
+
return false;
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
const edgeChildDOM = editor.getElementByKey(edgeChild.__key);
|
|
2171
|
+
|
|
2172
|
+
if (edgeChildDOM == null) {
|
|
2173
|
+
return false;
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
const edgeRect = edgeChildDOM.getBoundingClientRect();
|
|
2177
|
+
const isExiting =
|
|
2178
|
+
direction === 'up'
|
|
2179
|
+
? edgeRect.top > edgeSelectionRect.top - edgeSelectionRect.height
|
|
2180
|
+
: edgeSelectionRect.bottom + edgeSelectionRect.height >
|
|
2181
|
+
edgeRect.bottom;
|
|
2182
|
+
|
|
2183
|
+
if (isExiting) {
|
|
2184
|
+
stopEvent(event);
|
|
2185
|
+
|
|
2186
|
+
const cords = tableNode.getCordsFromCellNode(
|
|
2187
|
+
anchorCellNode,
|
|
2188
|
+
tableObserver.table,
|
|
2189
|
+
);
|
|
2190
|
+
|
|
2191
|
+
if (event.shiftKey) {
|
|
2192
|
+
const cell = tableNode.getDOMCellFromCordsOrThrow(
|
|
2193
|
+
cords.x,
|
|
2194
|
+
cords.y,
|
|
2195
|
+
tableObserver.table,
|
|
2196
|
+
);
|
|
2197
|
+
tableObserver.$setAnchorCellForSelection(cell);
|
|
2198
|
+
tableObserver.$setFocusCellForSelection(cell, true);
|
|
2199
|
+
} else {
|
|
2200
|
+
return selectTableNodeInDirection(
|
|
2201
|
+
tableObserver,
|
|
2202
|
+
tableNode,
|
|
2203
|
+
cords.x,
|
|
2204
|
+
cords.y,
|
|
2205
|
+
direction,
|
|
2206
|
+
);
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
return true;
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
} else if ($isTableSelection(selection)) {
|
|
2213
|
+
const {anchor, focus, tableKey} = selection;
|
|
2214
|
+
if (tableKey !== tableNode.getKey()) {
|
|
2215
|
+
return false;
|
|
2216
|
+
}
|
|
2217
|
+
const anchorCellNode = $findMatchingParent(
|
|
2218
|
+
anchor.getNode(),
|
|
2219
|
+
$isTableCellNode,
|
|
2220
|
+
);
|
|
2221
|
+
const focusCellNode = $findMatchingParent(
|
|
2222
|
+
focus.getNode(),
|
|
2223
|
+
$isTableCellNode,
|
|
2224
|
+
);
|
|
2225
|
+
|
|
2226
|
+
const [tableNodeFromSelection] = selection.getNodes();
|
|
2227
|
+
invariant(
|
|
2228
|
+
$isTableNode(tableNodeFromSelection),
|
|
2229
|
+
'$handleArrowKey: TableSelection.getNodes()[0] expected to be TableNode',
|
|
2230
|
+
);
|
|
2231
|
+
const tableElement = getTableElement(
|
|
2232
|
+
tableNodeFromSelection,
|
|
2233
|
+
editor.getElementByKey(tableNodeFromSelection.getKey()),
|
|
2234
|
+
);
|
|
2235
|
+
if (
|
|
2236
|
+
!$isTableCellNode(anchorCellNode) ||
|
|
2237
|
+
!$isTableCellNode(focusCellNode) ||
|
|
2238
|
+
!$isTableNode(tableNodeFromSelection) ||
|
|
2239
|
+
tableElement == null
|
|
2240
|
+
) {
|
|
2241
|
+
return false;
|
|
2242
|
+
}
|
|
2243
|
+
tableObserver.$updateTableTableSelection(selection);
|
|
2244
|
+
|
|
2245
|
+
const grid = getTable(tableNodeFromSelection, tableElement);
|
|
2246
|
+
const cordsAnchor = tableNode.getCordsFromCellNode(anchorCellNode, grid);
|
|
2247
|
+
const anchorCell = tableNode.getDOMCellFromCordsOrThrow(
|
|
2248
|
+
cordsAnchor.x,
|
|
2249
|
+
cordsAnchor.y,
|
|
2250
|
+
grid,
|
|
2251
|
+
);
|
|
2252
|
+
tableObserver.$setAnchorCellForSelection(anchorCell);
|
|
2253
|
+
|
|
2254
|
+
stopEvent(event);
|
|
2255
|
+
|
|
2256
|
+
if (event.shiftKey) {
|
|
2257
|
+
const [tableMap, anchorValue, focusValue] = $computeTableMap(
|
|
2258
|
+
tableNode,
|
|
2259
|
+
anchorCellNode,
|
|
2260
|
+
focusCellNode,
|
|
2261
|
+
);
|
|
2262
|
+
return $adjustFocusInDirection(
|
|
2263
|
+
tableObserver,
|
|
2264
|
+
tableMap,
|
|
2265
|
+
anchorValue,
|
|
2266
|
+
focusValue,
|
|
2267
|
+
direction,
|
|
2268
|
+
);
|
|
2269
|
+
} else {
|
|
2270
|
+
focusCellNode.selectEnd();
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2273
|
+
return true;
|
|
2274
|
+
}
|
|
2275
|
+
|
|
2276
|
+
return false;
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
function stopEvent(event: Event) {
|
|
2280
|
+
event.preventDefault();
|
|
2281
|
+
event.stopImmediatePropagation();
|
|
2282
|
+
event.stopPropagation();
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2285
|
+
function isTypeaheadMenuInView(editor: LexicalEditor) {
|
|
2286
|
+
// There is no inbuilt way to check if the component picker is in view
|
|
2287
|
+
// but we can check if the root DOM element has the aria-controls attribute "typeahead-menu".
|
|
2288
|
+
const root = editor.getRootElement();
|
|
2289
|
+
if (!root) {
|
|
2290
|
+
return false;
|
|
2291
|
+
}
|
|
2292
|
+
return (
|
|
2293
|
+
root.hasAttribute('aria-controls') &&
|
|
2294
|
+
root.getAttribute('aria-controls') === 'typeahead-menu'
|
|
2295
|
+
);
|
|
2296
|
+
}
|
|
2297
|
+
|
|
2298
|
+
function $insertParagraphAtTableEdge(
|
|
2299
|
+
edgePosition: 'first' | 'last',
|
|
2300
|
+
tableNode: TableNode,
|
|
2301
|
+
children?: LexicalNode[],
|
|
2302
|
+
) {
|
|
2303
|
+
const paragraphNode = $createParagraphNode();
|
|
2304
|
+
if (edgePosition === 'first') {
|
|
2305
|
+
tableNode.insertBefore(paragraphNode);
|
|
2306
|
+
} else {
|
|
2307
|
+
tableNode.insertAfter(paragraphNode);
|
|
2308
|
+
}
|
|
2309
|
+
paragraphNode.append(...(children || []));
|
|
2310
|
+
paragraphNode.selectEnd();
|
|
2311
|
+
}
|
|
2312
|
+
|
|
2313
|
+
function $getTableEdgeCursorPosition(
|
|
2314
|
+
editor: LexicalEditor,
|
|
2315
|
+
selection: RangeSelection,
|
|
2316
|
+
tableNode: TableNode,
|
|
2317
|
+
) {
|
|
2318
|
+
const tableNodeParent = tableNode.getParent();
|
|
2319
|
+
if (!tableNodeParent) {
|
|
2320
|
+
return undefined;
|
|
2321
|
+
}
|
|
2322
|
+
|
|
2323
|
+
// TODO: Add support for nested tables
|
|
2324
|
+
const domSelection = getDOMSelection(getEditorWindow(editor));
|
|
2325
|
+
if (!domSelection) {
|
|
2326
|
+
return undefined;
|
|
2327
|
+
}
|
|
2328
|
+
const domAnchorNode = domSelection.anchorNode;
|
|
2329
|
+
const tableNodeParentDOM = editor.getElementByKey(tableNodeParent.getKey());
|
|
2330
|
+
const tableElement = getTableElement(
|
|
2331
|
+
tableNode,
|
|
2332
|
+
editor.getElementByKey(tableNode.getKey()),
|
|
2333
|
+
);
|
|
2334
|
+
// We are only interested in the scenario where the
|
|
2335
|
+
// native selection anchor is:
|
|
2336
|
+
// - at or inside the table's parent DOM
|
|
2337
|
+
// - and NOT at or inside the table DOM
|
|
2338
|
+
// It may be adjacent to the table DOM (e.g. in a wrapper)
|
|
2339
|
+
if (
|
|
2340
|
+
!domAnchorNode ||
|
|
2341
|
+
!tableNodeParentDOM ||
|
|
2342
|
+
!tableElement ||
|
|
2343
|
+
!tableNodeParentDOM.contains(domAnchorNode) ||
|
|
2344
|
+
tableElement.contains(domAnchorNode)
|
|
2345
|
+
) {
|
|
2346
|
+
return undefined;
|
|
2347
|
+
}
|
|
2348
|
+
|
|
2349
|
+
const anchorCellNode = $findMatchingParent(selection.anchor.getNode(), n =>
|
|
2350
|
+
$isTableCellNode(n),
|
|
2351
|
+
) as TableCellNode | null;
|
|
2352
|
+
if (!anchorCellNode) {
|
|
2353
|
+
return undefined;
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2356
|
+
const parentTable = $findMatchingParent(anchorCellNode, n => $isTableNode(n));
|
|
2357
|
+
if (!$isTableNode(parentTable) || !parentTable.is(tableNode)) {
|
|
2358
|
+
return undefined;
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
const [tableMap, cellValue] = $computeTableMap(
|
|
2362
|
+
tableNode,
|
|
2363
|
+
anchorCellNode,
|
|
2364
|
+
anchorCellNode,
|
|
2365
|
+
);
|
|
2366
|
+
const firstCell = tableMap[0][0];
|
|
2367
|
+
const lastCell = tableMap[tableMap.length - 1][tableMap[0].length - 1];
|
|
2368
|
+
const {startRow, startColumn} = cellValue;
|
|
2369
|
+
|
|
2370
|
+
const isAtFirstCell =
|
|
2371
|
+
startRow === firstCell.startRow && startColumn === firstCell.startColumn;
|
|
2372
|
+
const isAtLastCell =
|
|
2373
|
+
startRow === lastCell.startRow && startColumn === lastCell.startColumn;
|
|
2374
|
+
|
|
2375
|
+
if (isAtFirstCell) {
|
|
2376
|
+
return 'first';
|
|
2377
|
+
} else if (isAtLastCell) {
|
|
2378
|
+
return 'last';
|
|
2379
|
+
} else {
|
|
2380
|
+
return undefined;
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
export function $getObserverCellFromCellNodeOrThrow(
|
|
2385
|
+
tableObserver: TableObserver,
|
|
2386
|
+
tableCellNode: TableCellNode,
|
|
2387
|
+
): TableDOMCell {
|
|
2388
|
+
const {tableNode} = tableObserver.$lookup();
|
|
2389
|
+
const currentCords = tableNode.getCordsFromCellNode(
|
|
2390
|
+
tableCellNode,
|
|
2391
|
+
tableObserver.table,
|
|
2392
|
+
);
|
|
2393
|
+
return tableNode.getDOMCellFromCordsOrThrow(
|
|
2394
|
+
currentCords.x,
|
|
2395
|
+
currentCords.y,
|
|
2396
|
+
tableObserver.table,
|
|
2397
|
+
);
|
|
2398
|
+
}
|
|
2399
|
+
|
|
2400
|
+
export function $getNearestTableCellInTableFromDOMNode(
|
|
2401
|
+
tableNode: TableNode,
|
|
2402
|
+
startingDOM: Node,
|
|
2403
|
+
editorState?: EditorState,
|
|
2404
|
+
) {
|
|
2405
|
+
return $findParentTableCellNodeInTable(
|
|
2406
|
+
tableNode,
|
|
2407
|
+
$getNearestNodeFromDOMNode(startingDOM, editorState),
|
|
2408
|
+
);
|
|
2409
|
+
}
|