@lexical/table 0.44.1-nightly.20260518.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,154 @@
|
|
|
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 {BaseSelection, LexicalUpdateJSON, Spread} from 'lexical';
|
|
10
|
+
|
|
11
|
+
import {$descendantsMatching, addClassNamesToElement} from '@lexical/utils';
|
|
12
|
+
import {
|
|
13
|
+
$applyNodeReplacement,
|
|
14
|
+
DOMConversionMap,
|
|
15
|
+
DOMConversionOutput,
|
|
16
|
+
EditorConfig,
|
|
17
|
+
ElementNode,
|
|
18
|
+
LexicalNode,
|
|
19
|
+
NodeKey,
|
|
20
|
+
SerializedElementNode,
|
|
21
|
+
} from 'lexical';
|
|
22
|
+
|
|
23
|
+
import {PIXEL_VALUE_REG_EXP} from './constants';
|
|
24
|
+
import {$isTableCellNode} from './LexicalTableCellNode';
|
|
25
|
+
|
|
26
|
+
export type SerializedTableRowNode = Spread<
|
|
27
|
+
{
|
|
28
|
+
height?: number;
|
|
29
|
+
},
|
|
30
|
+
SerializedElementNode
|
|
31
|
+
>;
|
|
32
|
+
|
|
33
|
+
/** @noInheritDoc */
|
|
34
|
+
export class TableRowNode extends ElementNode {
|
|
35
|
+
/** @internal */
|
|
36
|
+
__height?: number;
|
|
37
|
+
|
|
38
|
+
static getType(): string {
|
|
39
|
+
return 'tablerow';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
static clone(node: TableRowNode): TableRowNode {
|
|
43
|
+
return new TableRowNode(node.__height, node.__key);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
afterCloneFrom(prevNode: this): void {
|
|
47
|
+
super.afterCloneFrom(prevNode);
|
|
48
|
+
this.__height = prevNode.__height;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
static importDOM(): DOMConversionMap | null {
|
|
52
|
+
return {
|
|
53
|
+
tr: (node: Node) => ({
|
|
54
|
+
conversion: $convertTableRowElement,
|
|
55
|
+
priority: 0,
|
|
56
|
+
}),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
static importJSON(serializedNode: SerializedTableRowNode): TableRowNode {
|
|
61
|
+
return $createTableRowNode().updateFromJSON(serializedNode);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
updateFromJSON(
|
|
65
|
+
serializedNode: LexicalUpdateJSON<SerializedTableRowNode>,
|
|
66
|
+
): this {
|
|
67
|
+
return super
|
|
68
|
+
.updateFromJSON(serializedNode)
|
|
69
|
+
.setHeight(serializedNode.height);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
constructor(height?: number, key?: NodeKey) {
|
|
73
|
+
super(key);
|
|
74
|
+
this.__height = height;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
exportJSON(): SerializedTableRowNode {
|
|
78
|
+
const height = this.getHeight();
|
|
79
|
+
return {
|
|
80
|
+
...super.exportJSON(),
|
|
81
|
+
...(height === undefined ? undefined : {height}),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
createDOM(config: EditorConfig): HTMLElement {
|
|
86
|
+
const element = document.createElement('tr');
|
|
87
|
+
|
|
88
|
+
if (this.__height) {
|
|
89
|
+
element.style.height = `${this.__height}px`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
addClassNamesToElement(element, config.theme.tableRow);
|
|
93
|
+
|
|
94
|
+
return element;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
extractWithChild(
|
|
98
|
+
child: LexicalNode,
|
|
99
|
+
selection: BaseSelection | null,
|
|
100
|
+
destination: 'clone' | 'html',
|
|
101
|
+
): boolean {
|
|
102
|
+
return destination === 'html';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
isShadowRoot(): boolean {
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
setHeight(height?: number | undefined): this {
|
|
110
|
+
const self = this.getWritable();
|
|
111
|
+
self.__height = height;
|
|
112
|
+
return self;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
getHeight(): number | undefined {
|
|
116
|
+
return this.getLatest().__height;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
updateDOM(prevNode: this): boolean {
|
|
120
|
+
return prevNode.__height !== this.__height;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
canBeEmpty(): false {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
canIndent(): false {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function $convertTableRowElement(domNode: Node): DOMConversionOutput {
|
|
133
|
+
const domNode_ = domNode as HTMLTableCellElement;
|
|
134
|
+
let height: number | undefined = undefined;
|
|
135
|
+
|
|
136
|
+
if (PIXEL_VALUE_REG_EXP.test(domNode_.style.height)) {
|
|
137
|
+
height = parseFloat(domNode_.style.height);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
after: children => $descendantsMatching(children, $isTableCellNode),
|
|
142
|
+
node: $createTableRowNode(height),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function $createTableRowNode(height?: number): TableRowNode {
|
|
147
|
+
return $applyNodeReplacement(new TableRowNode(height));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function $isTableRowNode(
|
|
151
|
+
node: LexicalNode | null | undefined,
|
|
152
|
+
): node is TableRowNode {
|
|
153
|
+
return node instanceof TableRowNode;
|
|
154
|
+
}
|
|
@@ -0,0 +1,460 @@
|
|
|
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 invariant from '@lexical/internal/invariant';
|
|
10
|
+
import {$findMatchingParent} from '@lexical/utils';
|
|
11
|
+
import {
|
|
12
|
+
$createPoint,
|
|
13
|
+
$getNodeByKey,
|
|
14
|
+
$getSelection,
|
|
15
|
+
$isElementNode,
|
|
16
|
+
$isParagraphNode,
|
|
17
|
+
$normalizeSelection__EXPERIMENTAL,
|
|
18
|
+
BaseSelection,
|
|
19
|
+
ElementNode,
|
|
20
|
+
isCurrentlyReadOnlyMode,
|
|
21
|
+
LexicalNode,
|
|
22
|
+
NodeKey,
|
|
23
|
+
PointType,
|
|
24
|
+
TEXT_TYPE_TO_FORMAT,
|
|
25
|
+
TextFormatType,
|
|
26
|
+
TextNode,
|
|
27
|
+
} from 'lexical';
|
|
28
|
+
|
|
29
|
+
import {$isTableCellNode, TableCellNode} from './LexicalTableCellNode';
|
|
30
|
+
import {$isTableNode, TableNode} from './LexicalTableNode';
|
|
31
|
+
import {$isTableRowNode, TableRowNode} from './LexicalTableRowNode';
|
|
32
|
+
import {$findTableNode} from './LexicalTableSelectionHelpers';
|
|
33
|
+
import {
|
|
34
|
+
$computeTableCellRectBoundary,
|
|
35
|
+
$computeTableMap,
|
|
36
|
+
$getTableCellNodeRect,
|
|
37
|
+
} from './LexicalTableUtils';
|
|
38
|
+
|
|
39
|
+
const __DEV__ = process.env.NODE_ENV !== 'production';
|
|
40
|
+
|
|
41
|
+
export type TableSelectionShape = {
|
|
42
|
+
fromX: number;
|
|
43
|
+
fromY: number;
|
|
44
|
+
toX: number;
|
|
45
|
+
toY: number;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export type TableMapValueType = {
|
|
49
|
+
cell: TableCellNode;
|
|
50
|
+
startRow: number;
|
|
51
|
+
startColumn: number;
|
|
52
|
+
};
|
|
53
|
+
export type TableMapType = Array<Array<TableMapValueType>>;
|
|
54
|
+
|
|
55
|
+
function $getCellNodes(tableSelection: TableSelection): {
|
|
56
|
+
anchorCell: TableCellNode;
|
|
57
|
+
anchorNode: TextNode | ElementNode;
|
|
58
|
+
anchorRow: TableRowNode;
|
|
59
|
+
anchorTable: TableNode;
|
|
60
|
+
focusCell: TableCellNode;
|
|
61
|
+
focusNode: TextNode | ElementNode;
|
|
62
|
+
focusRow: TableRowNode;
|
|
63
|
+
focusTable: TableNode;
|
|
64
|
+
} {
|
|
65
|
+
const [
|
|
66
|
+
[anchorNode, anchorCell, anchorRow, anchorTable],
|
|
67
|
+
[focusNode, focusCell, focusRow, focusTable],
|
|
68
|
+
] = (['anchor', 'focus'] as const).map(
|
|
69
|
+
(k): [ElementNode | TextNode, TableCellNode, TableRowNode, TableNode] => {
|
|
70
|
+
const node = tableSelection[k].getNode();
|
|
71
|
+
const cellNode = $findMatchingParent(node, $isTableCellNode);
|
|
72
|
+
invariant(
|
|
73
|
+
$isTableCellNode(cellNode),
|
|
74
|
+
'Expected TableSelection %s to be (or a child of) TableCellNode, got key %s of type %s',
|
|
75
|
+
k,
|
|
76
|
+
node.getKey(),
|
|
77
|
+
node.getType(),
|
|
78
|
+
);
|
|
79
|
+
const rowNode = cellNode.getParent();
|
|
80
|
+
invariant(
|
|
81
|
+
$isTableRowNode(rowNode),
|
|
82
|
+
'Expected TableSelection %s cell parent to be a TableRowNode',
|
|
83
|
+
k,
|
|
84
|
+
);
|
|
85
|
+
const tableNode = rowNode.getParent();
|
|
86
|
+
invariant(
|
|
87
|
+
$isTableNode(tableNode),
|
|
88
|
+
'Expected TableSelection %s row parent to be a TableNode',
|
|
89
|
+
k,
|
|
90
|
+
);
|
|
91
|
+
return [node, cellNode, rowNode, tableNode];
|
|
92
|
+
},
|
|
93
|
+
);
|
|
94
|
+
// TODO: nested tables may violate this
|
|
95
|
+
invariant(
|
|
96
|
+
anchorTable.is(focusTable),
|
|
97
|
+
'Expected TableSelection anchor and focus to be in the same table',
|
|
98
|
+
);
|
|
99
|
+
return {
|
|
100
|
+
anchorCell,
|
|
101
|
+
anchorNode,
|
|
102
|
+
anchorRow,
|
|
103
|
+
anchorTable,
|
|
104
|
+
focusCell,
|
|
105
|
+
focusNode,
|
|
106
|
+
focusRow,
|
|
107
|
+
focusTable,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export class TableSelection implements BaseSelection {
|
|
112
|
+
tableKey: NodeKey;
|
|
113
|
+
anchor: PointType;
|
|
114
|
+
focus: PointType;
|
|
115
|
+
_cachedNodes: Array<LexicalNode> | null;
|
|
116
|
+
dirty: boolean;
|
|
117
|
+
|
|
118
|
+
constructor(tableKey: NodeKey, anchor: PointType, focus: PointType) {
|
|
119
|
+
this.anchor = anchor;
|
|
120
|
+
this.focus = focus;
|
|
121
|
+
anchor._selection = this;
|
|
122
|
+
focus._selection = this;
|
|
123
|
+
this._cachedNodes = null;
|
|
124
|
+
this.dirty = false;
|
|
125
|
+
this.tableKey = tableKey;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
getStartEndPoints(): [PointType, PointType] {
|
|
129
|
+
return [this.anchor, this.focus];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* {@link $createTableSelection} unfortunately makes it very easy to create
|
|
134
|
+
* nonsense selections, so we have a method to see if the selection probably
|
|
135
|
+
* makes sense.
|
|
136
|
+
*
|
|
137
|
+
* @returns true if the TableSelection is (probably) valid
|
|
138
|
+
*/
|
|
139
|
+
isValid(): boolean {
|
|
140
|
+
if (
|
|
141
|
+
this.tableKey === 'root' ||
|
|
142
|
+
this.anchor.key === 'root' ||
|
|
143
|
+
this.anchor.type !== 'element' ||
|
|
144
|
+
this.focus.key === 'root' ||
|
|
145
|
+
this.focus.type !== 'element'
|
|
146
|
+
) {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Check if the referenced nodes still exist in the editor
|
|
151
|
+
const tableNode = $getNodeByKey(this.tableKey);
|
|
152
|
+
const anchorNode = $getNodeByKey(this.anchor.key);
|
|
153
|
+
const focusNode = $getNodeByKey(this.focus.key);
|
|
154
|
+
|
|
155
|
+
return tableNode !== null && anchorNode !== null && focusNode !== null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Returns whether the Selection is "backwards", meaning the focus
|
|
160
|
+
* logically precedes the anchor in the EditorState.
|
|
161
|
+
* @returns true if the Selection is backwards, false otherwise.
|
|
162
|
+
*/
|
|
163
|
+
isBackward(): boolean {
|
|
164
|
+
return this.focus.isBefore(this.anchor);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
getCachedNodes(): LexicalNode[] | null {
|
|
168
|
+
return this._cachedNodes;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
setCachedNodes(nodes: LexicalNode[] | null): void {
|
|
172
|
+
this._cachedNodes = nodes;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
is(selection: null | BaseSelection): boolean {
|
|
176
|
+
return (
|
|
177
|
+
$isTableSelection(selection) &&
|
|
178
|
+
this.tableKey === selection.tableKey &&
|
|
179
|
+
this.anchor.is(selection.anchor) &&
|
|
180
|
+
this.focus.is(selection.focus)
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
set(tableKey: NodeKey, anchorCellKey: NodeKey, focusCellKey: NodeKey): void {
|
|
185
|
+
// note: closure compiler's acorn does not support ||=
|
|
186
|
+
this.dirty =
|
|
187
|
+
this.dirty ||
|
|
188
|
+
tableKey !== this.tableKey ||
|
|
189
|
+
anchorCellKey !== this.anchor.key ||
|
|
190
|
+
focusCellKey !== this.focus.key;
|
|
191
|
+
this.tableKey = tableKey;
|
|
192
|
+
this.anchor.key = anchorCellKey;
|
|
193
|
+
this.focus.key = focusCellKey;
|
|
194
|
+
this._cachedNodes = null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
clone(): TableSelection {
|
|
198
|
+
return new TableSelection(
|
|
199
|
+
this.tableKey,
|
|
200
|
+
$createPoint(this.anchor.key, this.anchor.offset, this.anchor.type),
|
|
201
|
+
$createPoint(this.focus.key, this.focus.offset, this.focus.type),
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
isCollapsed(): boolean {
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
extract(): Array<LexicalNode> {
|
|
210
|
+
return this.getNodes();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
insertRawText(text: string): void {
|
|
214
|
+
// Do nothing?
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
insertText(): void {
|
|
218
|
+
// Do nothing?
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Returns whether the provided TextFormatType is present on the Selection.
|
|
223
|
+
* This will be true if any paragraph in table cells has the specified format.
|
|
224
|
+
*
|
|
225
|
+
* @param type the TextFormatType to check for.
|
|
226
|
+
* @returns true if the provided format is currently toggled on on the Selection, false otherwise.
|
|
227
|
+
*/
|
|
228
|
+
hasFormat(type: TextFormatType): boolean {
|
|
229
|
+
let format = 0;
|
|
230
|
+
|
|
231
|
+
const cellNodes = this.getNodes().filter($isTableCellNode);
|
|
232
|
+
cellNodes.forEach((cellNode: TableCellNode) => {
|
|
233
|
+
const paragraph = cellNode.getFirstChild();
|
|
234
|
+
if ($isParagraphNode(paragraph)) {
|
|
235
|
+
format |= paragraph.getTextFormat();
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
const formatFlag = TEXT_TYPE_TO_FORMAT[type];
|
|
240
|
+
return (format & formatFlag) !== 0;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
insertNodes(nodes: Array<LexicalNode>) {
|
|
244
|
+
const focusNode = this.focus.getNode();
|
|
245
|
+
invariant(
|
|
246
|
+
$isElementNode(focusNode),
|
|
247
|
+
'Expected TableSelection focus to be an ElementNode',
|
|
248
|
+
);
|
|
249
|
+
const selection = $normalizeSelection__EXPERIMENTAL(
|
|
250
|
+
focusNode.select(0, focusNode.getChildrenSize()),
|
|
251
|
+
);
|
|
252
|
+
selection.insertNodes(nodes);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// TODO Deprecate this method. It's confusing when used with colspan|rowspan
|
|
256
|
+
getShape(): TableSelectionShape {
|
|
257
|
+
const {anchorCell, focusCell} = $getCellNodes(this);
|
|
258
|
+
const anchorCellNodeRect = $getTableCellNodeRect(anchorCell);
|
|
259
|
+
invariant(
|
|
260
|
+
anchorCellNodeRect !== null,
|
|
261
|
+
'getCellRect: expected to find AnchorNode',
|
|
262
|
+
);
|
|
263
|
+
const focusCellNodeRect = $getTableCellNodeRect(focusCell);
|
|
264
|
+
invariant(
|
|
265
|
+
focusCellNodeRect !== null,
|
|
266
|
+
'getCellRect: expected to find focusCellNode',
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
const startX = Math.min(
|
|
270
|
+
anchorCellNodeRect.columnIndex,
|
|
271
|
+
focusCellNodeRect.columnIndex,
|
|
272
|
+
);
|
|
273
|
+
const stopX = Math.max(
|
|
274
|
+
anchorCellNodeRect.columnIndex + anchorCellNodeRect.colSpan - 1,
|
|
275
|
+
focusCellNodeRect.columnIndex + focusCellNodeRect.colSpan - 1,
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
const startY = Math.min(
|
|
279
|
+
anchorCellNodeRect.rowIndex,
|
|
280
|
+
focusCellNodeRect.rowIndex,
|
|
281
|
+
);
|
|
282
|
+
const stopY = Math.max(
|
|
283
|
+
anchorCellNodeRect.rowIndex + anchorCellNodeRect.rowSpan - 1,
|
|
284
|
+
focusCellNodeRect.rowIndex + focusCellNodeRect.rowSpan - 1,
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
fromX: Math.min(startX, stopX),
|
|
289
|
+
fromY: Math.min(startY, stopY),
|
|
290
|
+
toX: Math.max(startX, stopX),
|
|
291
|
+
toY: Math.max(startY, stopY),
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
getNodes(): Array<LexicalNode> {
|
|
296
|
+
if (!this.isValid()) {
|
|
297
|
+
return [];
|
|
298
|
+
}
|
|
299
|
+
const cachedNodes = this._cachedNodes;
|
|
300
|
+
if (cachedNodes !== null) {
|
|
301
|
+
return cachedNodes;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const {anchorTable: tableNode, anchorCell, focusCell} = $getCellNodes(this);
|
|
305
|
+
|
|
306
|
+
const focusCellGrid = focusCell.getParents()[1];
|
|
307
|
+
if (focusCellGrid !== tableNode) {
|
|
308
|
+
if (!tableNode.isParentOf(focusCell)) {
|
|
309
|
+
// focus is on higher Grid level than anchor
|
|
310
|
+
const gridParent = tableNode.getParent();
|
|
311
|
+
invariant(gridParent != null, 'Expected gridParent to have a parent');
|
|
312
|
+
this.set(this.tableKey, gridParent.getKey(), focusCell.getKey());
|
|
313
|
+
} else {
|
|
314
|
+
// anchor is on higher Grid level than focus
|
|
315
|
+
const focusCellParent = focusCellGrid.getParent();
|
|
316
|
+
invariant(
|
|
317
|
+
focusCellParent != null,
|
|
318
|
+
'Expected focusCellParent to have a parent',
|
|
319
|
+
);
|
|
320
|
+
this.set(this.tableKey, focusCell.getKey(), focusCellParent.getKey());
|
|
321
|
+
}
|
|
322
|
+
return this.getNodes();
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// TODO Mapping the whole Grid every time not efficient. We need to compute the entire state only
|
|
326
|
+
// once (on load) and iterate on it as updates occur. However, to do this we need to have the
|
|
327
|
+
// ability to store a state. Killing TableSelection and moving the logic to the plugin would make
|
|
328
|
+
// this possible.
|
|
329
|
+
const [map, cellAMap, cellBMap] = $computeTableMap(
|
|
330
|
+
tableNode,
|
|
331
|
+
anchorCell,
|
|
332
|
+
focusCell,
|
|
333
|
+
);
|
|
334
|
+
const {minColumn, maxColumn, minRow, maxRow} =
|
|
335
|
+
$computeTableCellRectBoundary(map, cellAMap, cellBMap);
|
|
336
|
+
|
|
337
|
+
// We use a Map here because merged cells in the grid would otherwise
|
|
338
|
+
// show up multiple times in the nodes array
|
|
339
|
+
const nodeMap: Map<NodeKey, LexicalNode> = new Map([
|
|
340
|
+
[tableNode.getKey(), tableNode],
|
|
341
|
+
]);
|
|
342
|
+
let lastRow: null | TableRowNode = null;
|
|
343
|
+
for (let i = minRow; i <= maxRow; i++) {
|
|
344
|
+
for (let j = minColumn; j <= maxColumn; j++) {
|
|
345
|
+
const {cell} = map[i][j];
|
|
346
|
+
const currentRow = cell.getParent();
|
|
347
|
+
invariant(
|
|
348
|
+
$isTableRowNode(currentRow),
|
|
349
|
+
'Expected TableCellNode parent to be a TableRowNode',
|
|
350
|
+
);
|
|
351
|
+
if (currentRow !== lastRow) {
|
|
352
|
+
nodeMap.set(currentRow.getKey(), currentRow);
|
|
353
|
+
lastRow = currentRow;
|
|
354
|
+
}
|
|
355
|
+
if (!nodeMap.has(cell.getKey())) {
|
|
356
|
+
$visitRecursively(cell, childNode => {
|
|
357
|
+
nodeMap.set(childNode.getKey(), childNode);
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
const nodes = Array.from(nodeMap.values());
|
|
363
|
+
|
|
364
|
+
if (!isCurrentlyReadOnlyMode()) {
|
|
365
|
+
this._cachedNodes = nodes;
|
|
366
|
+
}
|
|
367
|
+
return nodes;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
getTextContent(): string {
|
|
371
|
+
const nodes = this.getNodes().filter(node => $isTableCellNode(node));
|
|
372
|
+
let textContent = '';
|
|
373
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
374
|
+
const node = nodes[i];
|
|
375
|
+
const row = node.__parent;
|
|
376
|
+
const nextRow = (nodes[i + 1] || {}).__parent;
|
|
377
|
+
textContent += node.getTextContent() + (nextRow !== row ? '\n' : '\t');
|
|
378
|
+
}
|
|
379
|
+
return textContent;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
export function $isTableSelection(x: unknown): x is TableSelection {
|
|
384
|
+
return x instanceof TableSelection;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
export function $createTableSelection(): TableSelection {
|
|
388
|
+
// TODO this is a suboptimal design, it doesn't make sense to have
|
|
389
|
+
// a table selection that isn't associated with a table. This
|
|
390
|
+
// constructor should have required arguments and in __DEV__ we
|
|
391
|
+
// should check that they point to a table and are element points to
|
|
392
|
+
// cell nodes of that table.
|
|
393
|
+
const anchor = $createPoint('root', 0, 'element');
|
|
394
|
+
const focus = $createPoint('root', 0, 'element');
|
|
395
|
+
return new TableSelection('root', anchor, focus);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export function $createTableSelectionFrom(
|
|
399
|
+
tableNode: TableNode,
|
|
400
|
+
anchorCell: TableCellNode,
|
|
401
|
+
focusCell: TableCellNode,
|
|
402
|
+
): TableSelection {
|
|
403
|
+
const tableNodeKey = tableNode.getKey();
|
|
404
|
+
const anchorCellKey = anchorCell.getKey();
|
|
405
|
+
const focusCellKey = focusCell.getKey();
|
|
406
|
+
if (__DEV__) {
|
|
407
|
+
invariant(
|
|
408
|
+
tableNode.isAttached(),
|
|
409
|
+
'$createTableSelectionFrom: tableNode %s is not attached',
|
|
410
|
+
tableNodeKey,
|
|
411
|
+
);
|
|
412
|
+
invariant(
|
|
413
|
+
tableNode.is($findTableNode(anchorCell)),
|
|
414
|
+
'$createTableSelectionFrom: anchorCell %s is not in table %s',
|
|
415
|
+
anchorCellKey,
|
|
416
|
+
tableNodeKey,
|
|
417
|
+
);
|
|
418
|
+
invariant(
|
|
419
|
+
tableNode.is($findTableNode(focusCell)),
|
|
420
|
+
'$createTableSelectionFrom: focusCell %s is not in table %s',
|
|
421
|
+
focusCellKey,
|
|
422
|
+
tableNodeKey,
|
|
423
|
+
);
|
|
424
|
+
// TODO: Check for rectangular grid
|
|
425
|
+
}
|
|
426
|
+
const prevSelection = $getSelection();
|
|
427
|
+
const nextSelection = $isTableSelection(prevSelection)
|
|
428
|
+
? prevSelection.clone()
|
|
429
|
+
: $createTableSelection();
|
|
430
|
+
nextSelection.set(
|
|
431
|
+
tableNode.getKey(),
|
|
432
|
+
anchorCell.getKey(),
|
|
433
|
+
focusCell.getKey(),
|
|
434
|
+
);
|
|
435
|
+
return nextSelection;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Depth first visitor
|
|
440
|
+
* @param node The starting node
|
|
441
|
+
* @param $visit The function to call for each node. If the function returns false, then children of this node will not be explored
|
|
442
|
+
*/
|
|
443
|
+
export function $visitRecursively(
|
|
444
|
+
node: LexicalNode,
|
|
445
|
+
$visit: (childNode: LexicalNode) => boolean | undefined | void,
|
|
446
|
+
): void {
|
|
447
|
+
const stack = [[node]];
|
|
448
|
+
for (
|
|
449
|
+
let currentArray = stack.at(-1);
|
|
450
|
+
currentArray !== undefined && stack.length > 0;
|
|
451
|
+
currentArray = stack.at(-1)
|
|
452
|
+
) {
|
|
453
|
+
const currentNode = currentArray.pop();
|
|
454
|
+
if (currentNode === undefined) {
|
|
455
|
+
stack.pop();
|
|
456
|
+
} else if ($visit(currentNode) !== false && $isElementNode(currentNode)) {
|
|
457
|
+
stack.push(currentNode.getChildren());
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|