@lexical/selection 0.4.1 → 0.5.1-next.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/LexicalSelection.dev.js +639 -513
- package/LexicalSelection.js.flow +2 -2
- package/LexicalSelection.prod.js +25 -23
- package/README.md +3 -3
- package/constants.d.ts +8 -0
- package/grid-selection.d.ts +10 -0
- package/index.d.ts +6 -21
- package/lexical-node.d.ts +19 -0
- package/node-selection.d.ts +10 -0
- package/package.json +2 -2
- package/range-selection.d.ts +28 -0
- package/utils.d.ts +13 -0
package/LexicalSelection.dev.js
CHANGED
|
@@ -8,220 +8,164 @@
|
|
|
8
8
|
|
|
9
9
|
var lexical = require('lexical');
|
|
10
10
|
|
|
11
|
-
/**
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
13
|
+
*
|
|
14
|
+
* This source code is licensed under the MIT license found in the
|
|
15
|
+
* LICENSE file in the root directory of this source tree.
|
|
16
|
+
*
|
|
17
|
+
*/
|
|
18
|
+
const CSS_TO_STYLES = new Map();
|
|
16
19
|
|
|
17
|
-
|
|
18
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
22
|
+
*
|
|
23
|
+
* This source code is licensed under the MIT license found in the
|
|
24
|
+
* LICENSE file in the root directory of this source tree.
|
|
25
|
+
*
|
|
26
|
+
*/
|
|
27
|
+
function $cloneGridSelectionContent(selection) {
|
|
28
|
+
const nodeMap = selection.getNodes().map(node => {
|
|
29
|
+
const nodeKey = node.getKey();
|
|
30
|
+
const clone = $cloneWithProperties(node);
|
|
31
|
+
return [nodeKey, clone];
|
|
32
|
+
});
|
|
33
|
+
return {
|
|
34
|
+
nodeMap,
|
|
35
|
+
range: [selection.gridKey]
|
|
36
|
+
};
|
|
37
|
+
}
|
|
19
38
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
39
|
+
/**
|
|
40
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
41
|
+
*
|
|
42
|
+
* This source code is licensed under the MIT license found in the
|
|
43
|
+
* LICENSE file in the root directory of this source tree.
|
|
44
|
+
*
|
|
45
|
+
*/
|
|
46
|
+
function $cloneNodeSelectionContent(selection) {
|
|
47
|
+
{
|
|
48
|
+
throw Error(`TODO`);
|
|
30
49
|
}
|
|
31
|
-
|
|
32
|
-
return clone;
|
|
33
50
|
}
|
|
34
51
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
52
|
+
/**
|
|
53
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
54
|
+
*
|
|
55
|
+
* This source code is licensed under the MIT license found in the
|
|
56
|
+
* LICENSE file in the root directory of this source tree.
|
|
57
|
+
*
|
|
58
|
+
*/
|
|
41
59
|
|
|
42
|
-
|
|
43
|
-
|
|
60
|
+
function getDOMTextNode(element) {
|
|
61
|
+
let node = element;
|
|
44
62
|
|
|
45
|
-
|
|
46
|
-
|
|
63
|
+
while (node != null) {
|
|
64
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
65
|
+
return node;
|
|
66
|
+
}
|
|
47
67
|
|
|
48
|
-
|
|
49
|
-
parent = parent.getParent();
|
|
68
|
+
node = node.firstChild;
|
|
50
69
|
}
|
|
51
70
|
|
|
52
|
-
return
|
|
71
|
+
return null;
|
|
53
72
|
}
|
|
54
73
|
|
|
55
|
-
function
|
|
56
|
-
|
|
57
|
-
let offset = startingOffset;
|
|
58
|
-
|
|
59
|
-
while (node !== null) {
|
|
60
|
-
const parent = $getParentAvoidingExcludedElements(node);
|
|
61
|
-
|
|
62
|
-
if (parent === null) {
|
|
63
|
-
break;
|
|
64
|
-
}
|
|
74
|
+
function getDOMIndexWithinParent(node) {
|
|
75
|
+
const parent = node.parentNode;
|
|
65
76
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const needsClone = clone === undefined;
|
|
77
|
+
if (parent == null) {
|
|
78
|
+
throw new Error('Should never happen');
|
|
79
|
+
}
|
|
70
80
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
nodeMap.set(key, clone);
|
|
74
|
-
}
|
|
81
|
+
return [parent, Array.from(parent.childNodes).indexOf(node)];
|
|
82
|
+
}
|
|
75
83
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
84
|
+
function createDOMRange(editor, anchorNode, _anchorOffset, focusNode, _focusOffset) {
|
|
85
|
+
const anchorKey = anchorNode.getKey();
|
|
86
|
+
const focusKey = focusNode.getKey();
|
|
87
|
+
const range = document.createRange();
|
|
88
|
+
let anchorDOM = editor.getElementByKey(anchorKey);
|
|
89
|
+
let focusDOM = editor.getElementByKey(focusKey);
|
|
90
|
+
let anchorOffset = _anchorOffset;
|
|
91
|
+
let focusOffset = _focusOffset;
|
|
81
92
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
// So if the parent is the root, we know this is a top level.
|
|
86
|
-
range.push(key);
|
|
87
|
-
}
|
|
93
|
+
if (lexical.$isTextNode(anchorNode)) {
|
|
94
|
+
anchorDOM = getDOMTextNode(anchorDOM);
|
|
95
|
+
}
|
|
88
96
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
97
|
+
if (lexical.$isTextNode(focusNode)) {
|
|
98
|
+
focusDOM = getDOMTextNode(focusDOM);
|
|
99
|
+
}
|
|
92
100
|
|
|
93
|
-
|
|
94
|
-
|
|
101
|
+
if (anchorNode === undefined || focusNode === undefined || anchorDOM === null || focusDOM === null) {
|
|
102
|
+
return null;
|
|
95
103
|
}
|
|
96
|
-
}
|
|
97
104
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
throw Error(`getLatest() on clone node`);
|
|
105
|
+
if (anchorDOM.nodeName === 'BR') {
|
|
106
|
+
[anchorDOM, anchorOffset] = getDOMIndexWithinParent(anchorDOM);
|
|
101
107
|
}
|
|
102
|
-
}
|
|
103
108
|
|
|
104
|
-
|
|
105
|
-
|
|
109
|
+
if (focusDOM.nodeName === 'BR') {
|
|
110
|
+
[focusDOM, focusOffset] = getDOMIndexWithinParent(focusDOM);
|
|
111
|
+
}
|
|
106
112
|
|
|
107
|
-
|
|
108
|
-
const nodeMap = clone.nodeMap;
|
|
113
|
+
const firstChild = anchorDOM.firstChild;
|
|
109
114
|
|
|
110
|
-
|
|
111
|
-
|
|
115
|
+
if (anchorDOM === focusDOM && firstChild != null && firstChild.nodeName === 'BR' && anchorOffset === 0 && focusOffset === 0) {
|
|
116
|
+
focusOffset = 1;
|
|
117
|
+
}
|
|
112
118
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
119
|
+
try {
|
|
120
|
+
range.setStart(anchorDOM, anchorOffset);
|
|
121
|
+
range.setEnd(focusDOM, focusOffset);
|
|
122
|
+
} catch (e) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
116
125
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
value: errGetLatestOnClone,
|
|
122
|
-
writable: true
|
|
123
|
-
}
|
|
124
|
-
}));
|
|
125
|
-
}
|
|
126
|
+
if (range.collapsed && (anchorOffset !== focusOffset || anchorKey !== focusKey)) {
|
|
127
|
+
// Range is backwards, we need to reverse it
|
|
128
|
+
range.setStart(focusDOM, focusOffset);
|
|
129
|
+
range.setEnd(anchorDOM, anchorOffset);
|
|
126
130
|
}
|
|
127
131
|
|
|
128
|
-
return
|
|
132
|
+
return range;
|
|
129
133
|
}
|
|
134
|
+
function createRectsFromDOMRange(editor, range) {
|
|
135
|
+
const rootElement = editor.getRootElement();
|
|
130
136
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const focus = selection.focus;
|
|
135
|
-
const [anchorOffset, focusOffset] = selection.getCharacterOffsets();
|
|
136
|
-
const nodes = selection.getNodes();
|
|
137
|
-
|
|
138
|
-
if (nodes.length === 0) {
|
|
139
|
-
return {
|
|
140
|
-
nodeMap: [],
|
|
141
|
-
range: []
|
|
142
|
-
};
|
|
143
|
-
} // Check if we can use the parent of the nodes, if the
|
|
144
|
-
// parent can't be empty, then it's important that we
|
|
145
|
-
// also copy that element node along with its children.
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
let nodesLength = nodes.length;
|
|
149
|
-
const firstNode = nodes[0];
|
|
150
|
-
const firstNodeParent = firstNode.getParent();
|
|
151
|
-
|
|
152
|
-
if (firstNodeParent !== null && (!firstNodeParent.canBeEmpty() || lexical.$isRootNode(firstNodeParent))) {
|
|
153
|
-
const parentChildren = firstNodeParent.__children;
|
|
154
|
-
const parentChildrenLength = parentChildren.length;
|
|
155
|
-
|
|
156
|
-
if (parentChildrenLength === nodesLength) {
|
|
157
|
-
let areTheSame = true;
|
|
158
|
-
|
|
159
|
-
for (let i = 0; i < parentChildren.length; i++) {
|
|
160
|
-
if (parentChildren[i] !== nodes[i].__key) {
|
|
161
|
-
areTheSame = false;
|
|
162
|
-
break;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
if (areTheSame) {
|
|
167
|
-
nodesLength++;
|
|
168
|
-
nodes.push(firstNodeParent);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const lastNode = nodes[nodesLength - 1];
|
|
174
|
-
const isBefore = anchor.isBefore(focus);
|
|
175
|
-
const nodeMap = new Map();
|
|
176
|
-
const range = [];
|
|
177
|
-
const isOnlyText = lexical.$isTextNode(firstNode) && nodesLength === 1; // Do first node to root
|
|
178
|
-
|
|
179
|
-
$copyLeafNodeBranchToRoot(firstNode, isBefore ? anchorOffset : focusOffset, isOnlyText ? isBefore ? focusOffset : anchorOffset : undefined, true, range, nodeMap); // Copy all nodes between
|
|
180
|
-
|
|
181
|
-
for (let i = 0; i < nodesLength; i++) {
|
|
182
|
-
const node = nodes[i];
|
|
183
|
-
const key = node.getKey();
|
|
137
|
+
if (rootElement === null) {
|
|
138
|
+
return [];
|
|
139
|
+
}
|
|
184
140
|
|
|
185
|
-
|
|
186
|
-
|
|
141
|
+
const rootRect = rootElement.getBoundingClientRect();
|
|
142
|
+
const computedStyle = getComputedStyle(rootElement);
|
|
143
|
+
const rootPadding = parseFloat(computedStyle.paddingLeft) + parseFloat(computedStyle.paddingRight);
|
|
144
|
+
const selectionRects = Array.from(range.getClientRects());
|
|
145
|
+
let selectionRectsLength = selectionRects.length;
|
|
146
|
+
let prevRect;
|
|
187
147
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
148
|
+
for (let i = 0; i < selectionRectsLength; i++) {
|
|
149
|
+
const selectionRect = selectionRects[i]; // Exclude a rect that is the exact same as the last rect. getClientRects() can return
|
|
150
|
+
// the same rect twice for some elements. A more sophisticated thing to do here is to
|
|
151
|
+
// merge all the rects together into a set of rects that don't overlap, so we don't
|
|
152
|
+
// generate backgrounds that are too dark.
|
|
191
153
|
|
|
192
|
-
|
|
193
|
-
nodeMap.set(key, clone);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
} // Do last node to root
|
|
154
|
+
const isDuplicateRect = prevRect && prevRect.top === selectionRect.top && prevRect.left === selectionRect.left && prevRect.width === selectionRect.width && prevRect.height === selectionRect.height; // Exclude selections that span the entire element
|
|
197
155
|
|
|
156
|
+
const selectionSpansElement = selectionRect.width + rootPadding === rootRect.width;
|
|
198
157
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
}
|
|
204
|
-
} else if (lexical.$isGridSelection(selection)) {
|
|
205
|
-
const nodeMap = selection.getNodes().map(node => {
|
|
206
|
-
const nodeKey = node.getKey();
|
|
207
|
-
const clone = $cloneWithProperties(node);
|
|
208
|
-
return [nodeKey, clone];
|
|
209
|
-
});
|
|
210
|
-
return {
|
|
211
|
-
nodeMap,
|
|
212
|
-
range: [selection.gridKey]
|
|
213
|
-
};
|
|
214
|
-
}
|
|
158
|
+
if (isDuplicateRect || selectionSpansElement) {
|
|
159
|
+
selectionRects.splice(i--, 1);
|
|
160
|
+
selectionRectsLength--;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
215
163
|
|
|
216
|
-
|
|
217
|
-
throw Error(`TODO`);
|
|
164
|
+
prevRect = selectionRect;
|
|
218
165
|
}
|
|
219
|
-
}
|
|
220
166
|
|
|
221
|
-
|
|
222
|
-
return cssToStyles.get(css) || null;
|
|
167
|
+
return selectionRects;
|
|
223
168
|
}
|
|
224
|
-
|
|
225
169
|
function getStyleObjectFromRawCSS(css) {
|
|
226
170
|
const styleObject = {};
|
|
227
171
|
const styles = css.split(';');
|
|
@@ -236,222 +180,40 @@ function getStyleObjectFromRawCSS(css) {
|
|
|
236
180
|
|
|
237
181
|
return styleObject;
|
|
238
182
|
}
|
|
183
|
+
function getStyleObjectFromCSS(css) {
|
|
184
|
+
let value = CSS_TO_STYLES.get(css);
|
|
239
185
|
|
|
186
|
+
if (value === undefined) {
|
|
187
|
+
value = getStyleObjectFromRawCSS(css);
|
|
188
|
+
CSS_TO_STYLES.set(css, value);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return value;
|
|
192
|
+
}
|
|
240
193
|
function getCSSFromStyleObject(styles) {
|
|
241
194
|
let css = '';
|
|
242
195
|
|
|
243
196
|
for (const style in styles) {
|
|
244
197
|
if (style) {
|
|
245
|
-
css += `${style}: ${styles[style]};`;
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
return css;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function $addNodeStyle(node) {
|
|
253
|
-
const CSSText = node.getStyle();
|
|
254
|
-
const styles = getStyleObjectFromRawCSS(CSSText);
|
|
255
|
-
cssToStyles.set(CSSText, styles);
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
function $patchNodeStyle(node, patch) {
|
|
259
|
-
const prevStyles = getStyleObjectFromCSS(node.getStyle());
|
|
260
|
-
const newStyles = prevStyles ? { ...prevStyles,
|
|
261
|
-
...patch
|
|
262
|
-
} : patch;
|
|
263
|
-
const newCSSText = getCSSFromStyleObject(newStyles);
|
|
264
|
-
node.setStyle(newCSSText);
|
|
265
|
-
cssToStyles.set(newCSSText, newStyles);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
function $patchStyleText(selection, patch) {
|
|
269
|
-
const selectedNodes = selection.getNodes();
|
|
270
|
-
const selectedNodesLength = selectedNodes.length;
|
|
271
|
-
const lastIndex = selectedNodesLength - 1;
|
|
272
|
-
let firstNode = selectedNodes[0];
|
|
273
|
-
let lastNode = selectedNodes[lastIndex];
|
|
274
|
-
|
|
275
|
-
if (selection.isCollapsed()) {
|
|
276
|
-
return;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
const anchor = selection.anchor;
|
|
280
|
-
const focus = selection.focus;
|
|
281
|
-
const firstNodeText = firstNode.getTextContent();
|
|
282
|
-
const firstNodeTextLength = firstNodeText.length;
|
|
283
|
-
const focusOffset = focus.offset;
|
|
284
|
-
let anchorOffset = anchor.offset;
|
|
285
|
-
let startOffset;
|
|
286
|
-
let endOffset;
|
|
287
|
-
const isBefore = anchor.isBefore(focus);
|
|
288
|
-
startOffset = isBefore ? anchorOffset : focusOffset;
|
|
289
|
-
endOffset = isBefore ? focusOffset : anchorOffset; // This is the case where the user only selected the very end of the
|
|
290
|
-
// first node so we don't want to include it in the formatting change.
|
|
291
|
-
|
|
292
|
-
if (startOffset === firstNode.getTextContentSize()) {
|
|
293
|
-
const nextSibling = firstNode.getNextSibling();
|
|
294
|
-
|
|
295
|
-
if (lexical.$isTextNode(nextSibling)) {
|
|
296
|
-
// we basically make the second node the firstNode, changing offsets accordingly
|
|
297
|
-
anchorOffset = 0;
|
|
298
|
-
startOffset = 0;
|
|
299
|
-
firstNode = nextSibling;
|
|
300
|
-
}
|
|
301
|
-
} // This is the case where we only selected a single node
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
if (firstNode.is(lastNode)) {
|
|
305
|
-
if (lexical.$isTextNode(firstNode)) {
|
|
306
|
-
startOffset = anchorOffset > focusOffset ? focusOffset : anchorOffset;
|
|
307
|
-
endOffset = anchorOffset > focusOffset ? anchorOffset : focusOffset; // No actual text is selected, so do nothing.
|
|
308
|
-
|
|
309
|
-
if (startOffset === endOffset) {
|
|
310
|
-
return;
|
|
311
|
-
} // The entire node is selected, so just format it
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
if (startOffset === 0 && endOffset === firstNodeTextLength) {
|
|
315
|
-
$patchNodeStyle(firstNode, patch);
|
|
316
|
-
firstNode.select(startOffset, endOffset);
|
|
317
|
-
} else {
|
|
318
|
-
// The node is partially selected, so split it into two nodes
|
|
319
|
-
// and style the selected one.
|
|
320
|
-
const splitNodes = firstNode.splitText(startOffset, endOffset);
|
|
321
|
-
const replacement = startOffset === 0 ? splitNodes[0] : splitNodes[1];
|
|
322
|
-
$patchNodeStyle(replacement, patch);
|
|
323
|
-
replacement.select(0, endOffset - startOffset);
|
|
324
|
-
}
|
|
325
|
-
} // multiple nodes selected.
|
|
326
|
-
|
|
327
|
-
} else {
|
|
328
|
-
if (lexical.$isTextNode(firstNode)) {
|
|
329
|
-
if (startOffset !== 0) {
|
|
330
|
-
// the entire first node isn't selected, so split it
|
|
331
|
-
firstNode = firstNode.splitText(startOffset)[1];
|
|
332
|
-
startOffset = 0;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
$patchNodeStyle(firstNode, patch);
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
if (lexical.$isTextNode(lastNode)) {
|
|
339
|
-
const lastNodeText = lastNode.getTextContent();
|
|
340
|
-
const lastNodeTextLength = lastNodeText.length; // if the entire last node isn't selected, split it
|
|
341
|
-
|
|
342
|
-
if (endOffset !== lastNodeTextLength) {
|
|
343
|
-
[lastNode] = lastNode.splitText(endOffset);
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
if (endOffset !== 0) {
|
|
347
|
-
$patchNodeStyle(lastNode, patch);
|
|
348
|
-
}
|
|
349
|
-
} // style all the text nodes in between
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
for (let i = 1; i < lastIndex; i++) {
|
|
353
|
-
const selectedNode = selectedNodes[i];
|
|
354
|
-
const selectedNodeKey = selectedNode.getKey();
|
|
355
|
-
|
|
356
|
-
if (lexical.$isTextNode(selectedNode) && selectedNodeKey !== firstNode.getKey() && selectedNodeKey !== lastNode.getKey() && !selectedNode.isToken()) {
|
|
357
|
-
$patchNodeStyle(selectedNode, patch);
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
function $getSelectionStyleValueForProperty(selection, styleProperty, defaultValue = '') {
|
|
363
|
-
let styleValue = null;
|
|
364
|
-
const nodes = selection.getNodes();
|
|
365
|
-
const anchor = selection.anchor;
|
|
366
|
-
const focus = selection.focus;
|
|
367
|
-
const isBackward = selection.isBackward();
|
|
368
|
-
const endOffset = isBackward ? focus.offset : anchor.offset;
|
|
369
|
-
const endNode = isBackward ? focus.getNode() : anchor.getNode();
|
|
370
|
-
|
|
371
|
-
for (let i = 0; i < nodes.length; i++) {
|
|
372
|
-
const node = nodes[i]; // if no actual characters in the end node are selected, we don't
|
|
373
|
-
// include it in the selection for purposes of determining style
|
|
374
|
-
// value
|
|
375
|
-
|
|
376
|
-
if (i !== 0 && endOffset === 0 && node.is(endNode)) {
|
|
377
|
-
continue;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
if (lexical.$isTextNode(node)) {
|
|
381
|
-
const nodeStyleValue = $getNodeStyleValueForProperty(node, styleProperty, defaultValue);
|
|
382
|
-
|
|
383
|
-
if (styleValue === null) {
|
|
384
|
-
styleValue = nodeStyleValue;
|
|
385
|
-
} else if (styleValue !== nodeStyleValue) {
|
|
386
|
-
// multiple text nodes are in the selection and they don't all
|
|
387
|
-
// have the same font size.
|
|
388
|
-
styleValue = '';
|
|
389
|
-
break;
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
return styleValue === null ? defaultValue : styleValue;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
function $getNodeStyleValueForProperty(node, styleProperty, defaultValue) {
|
|
398
|
-
const css = node.getStyle();
|
|
399
|
-
const styleObject = getStyleObjectFromCSS(css);
|
|
400
|
-
|
|
401
|
-
if (styleObject !== null) {
|
|
402
|
-
return styleObject[styleProperty] || defaultValue;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
return defaultValue;
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
function $moveCaretSelection(selection, isHoldingShift, isBackward, granularity) {
|
|
409
|
-
selection.modify(isHoldingShift ? 'extend' : 'move', isBackward, granularity);
|
|
410
|
-
}
|
|
411
|
-
function $isParentElementRTL(selection) {
|
|
412
|
-
const anchorNode = selection.anchor.getNode();
|
|
413
|
-
const parent = lexical.$isRootNode(anchorNode) ? anchorNode : anchorNode.getParentOrThrow();
|
|
414
|
-
return parent.getDirection() === 'rtl';
|
|
415
|
-
}
|
|
416
|
-
function $moveCharacter(selection, isHoldingShift, isBackward) {
|
|
417
|
-
const isRTL = $isParentElementRTL(selection);
|
|
418
|
-
$moveCaretSelection(selection, isHoldingShift, isBackward ? !isRTL : isRTL, 'character');
|
|
419
|
-
}
|
|
420
|
-
function $selectAll(selection) {
|
|
421
|
-
const anchor = selection.anchor;
|
|
422
|
-
const focus = selection.focus;
|
|
423
|
-
const anchorNode = anchor.getNode();
|
|
424
|
-
const topParent = anchorNode.getTopLevelElementOrThrow();
|
|
425
|
-
const root = topParent.getParentOrThrow();
|
|
426
|
-
let firstNode = root.getFirstDescendant();
|
|
427
|
-
let lastNode = root.getLastDescendant();
|
|
428
|
-
let firstType = 'element';
|
|
429
|
-
let lastType = 'element';
|
|
430
|
-
let lastOffset = 0;
|
|
431
|
-
|
|
432
|
-
if (lexical.$isTextNode(firstNode)) {
|
|
433
|
-
firstType = 'text';
|
|
434
|
-
} else if (!lexical.$isElementNode(firstNode) && firstNode !== null) {
|
|
435
|
-
firstNode = firstNode.getParentOrThrow();
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
if (lexical.$isTextNode(lastNode)) {
|
|
439
|
-
lastType = 'text';
|
|
440
|
-
lastOffset = lastNode.getTextContentSize();
|
|
441
|
-
} else if (!lexical.$isElementNode(lastNode) && lastNode !== null) {
|
|
442
|
-
lastNode = lastNode.getParentOrThrow();
|
|
198
|
+
css += `${style}: ${styles[style]};`;
|
|
199
|
+
}
|
|
443
200
|
}
|
|
444
201
|
|
|
445
|
-
|
|
446
|
-
anchor.set(firstNode.getKey(), 0, firstType);
|
|
447
|
-
focus.set(lastNode.getKey(), lastOffset, lastType);
|
|
448
|
-
}
|
|
202
|
+
return css;
|
|
449
203
|
}
|
|
450
204
|
|
|
205
|
+
/**
|
|
206
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
207
|
+
*
|
|
208
|
+
* This source code is licensed under the MIT license found in the
|
|
209
|
+
* LICENSE file in the root directory of this source tree.
|
|
210
|
+
*
|
|
211
|
+
*/
|
|
212
|
+
|
|
451
213
|
function $removeParentEmptyElements(startingNode) {
|
|
452
214
|
let node = startingNode;
|
|
453
215
|
|
|
454
|
-
while (node !== null && !lexical.$
|
|
216
|
+
while (node !== null && !lexical.$isRootOrShadowRoot(node)) {
|
|
455
217
|
const latest = node.getLatest();
|
|
456
218
|
const parentNode = node.getParent();
|
|
457
219
|
|
|
@@ -463,7 +225,22 @@ function $removeParentEmptyElements(startingNode) {
|
|
|
463
225
|
}
|
|
464
226
|
}
|
|
465
227
|
|
|
466
|
-
function
|
|
228
|
+
function isPointAttached(point) {
|
|
229
|
+
return point.getNode().isAttached();
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Attempts to wrap all nodes in the Selection in ElementNodes returned from createElement.
|
|
233
|
+
* If wrappingElement is provided, all of the wrapped leaves are appended to the wrappingElement.
|
|
234
|
+
* It attempts to append the resulting sub-tree to the nearest safe insertion target.
|
|
235
|
+
*
|
|
236
|
+
* @param selection
|
|
237
|
+
* @param createElement
|
|
238
|
+
* @param wrappingElement
|
|
239
|
+
* @returns
|
|
240
|
+
*/
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
function $wrapNodes(selection, createElement, wrappingElement = null) {
|
|
467
244
|
const nodes = selection.getNodes();
|
|
468
245
|
const nodesLength = nodes.length;
|
|
469
246
|
const anchor = selection.anchor;
|
|
@@ -484,6 +261,34 @@ function $wrapLeafNodesInElements(selection, createElement, wrappingElement) {
|
|
|
484
261
|
return;
|
|
485
262
|
}
|
|
486
263
|
|
|
264
|
+
let topLevelNode = null;
|
|
265
|
+
let descendants = [];
|
|
266
|
+
|
|
267
|
+
for (let i = 0; i < nodesLength; i++) {
|
|
268
|
+
const node = nodes[i]; // Determine whether wrapping has to be broken down into multiple chunks. This can happen if the
|
|
269
|
+
// user selected multiple Root-like nodes that have to be treated separately as if they are
|
|
270
|
+
// their own branch. I.e. you don't want to wrap a whole table, but rather the contents of each
|
|
271
|
+
// of each of the cell nodes.
|
|
272
|
+
|
|
273
|
+
if (lexical.$isRootOrShadowRoot(node)) {
|
|
274
|
+
$wrapNodesImpl(selection, descendants, descendants.length, createElement, wrappingElement);
|
|
275
|
+
descendants = [];
|
|
276
|
+
topLevelNode = node;
|
|
277
|
+
} else if (topLevelNode === null || topLevelNode !== null && lexical.$hasAncestor(node, topLevelNode)) {
|
|
278
|
+
descendants.push(node);
|
|
279
|
+
} else {
|
|
280
|
+
$wrapNodesImpl(selection, descendants, descendants.length, createElement, wrappingElement);
|
|
281
|
+
descendants = [node];
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
$wrapNodesImpl(selection, descendants, descendants.length, createElement, wrappingElement);
|
|
286
|
+
}
|
|
287
|
+
function $wrapNodesImpl(selection, nodes, nodesLength, createElement, wrappingElement = null) {
|
|
288
|
+
if (nodes.length === 0) {
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
487
292
|
const firstNode = nodes[0];
|
|
488
293
|
const elementMapping = new Map();
|
|
489
294
|
const elements = []; // The below logic is to find the right target for us to
|
|
@@ -497,17 +302,20 @@ function $wrapLeafNodesInElements(selection, createElement, wrappingElement) {
|
|
|
497
302
|
target = target.getParentOrThrow();
|
|
498
303
|
}
|
|
499
304
|
|
|
305
|
+
let targetIsPrevSibling = false;
|
|
306
|
+
|
|
500
307
|
while (target !== null) {
|
|
501
308
|
const prevSibling = target.getPreviousSibling();
|
|
502
309
|
|
|
503
310
|
if (prevSibling !== null) {
|
|
504
311
|
target = prevSibling;
|
|
312
|
+
targetIsPrevSibling = true;
|
|
505
313
|
break;
|
|
506
314
|
}
|
|
507
315
|
|
|
508
316
|
target = target.getParentOrThrow();
|
|
509
317
|
|
|
510
|
-
if (lexical.$
|
|
318
|
+
if (lexical.$isRootOrShadowRoot(target)) {
|
|
511
319
|
break;
|
|
512
320
|
}
|
|
513
321
|
}
|
|
@@ -556,42 +364,53 @@ function $wrapLeafNodesInElements(selection, createElement, wrappingElement) {
|
|
|
556
364
|
targetElement.setFormat(node.getFormatType());
|
|
557
365
|
targetElement.setIndent(node.getIndent());
|
|
558
366
|
elements.push(targetElement);
|
|
559
|
-
node.remove();
|
|
367
|
+
node.remove(true);
|
|
560
368
|
}
|
|
561
369
|
}
|
|
562
370
|
|
|
563
|
-
if (wrappingElement) {
|
|
371
|
+
if (wrappingElement !== null) {
|
|
564
372
|
for (let i = 0; i < elements.length; i++) {
|
|
565
373
|
const element = elements[i];
|
|
566
374
|
wrappingElement.append(element);
|
|
567
375
|
}
|
|
568
|
-
} // If our target is
|
|
376
|
+
} // If our target is Root-like, let's see if we can re-adjust
|
|
569
377
|
// so that the target is the first child instead.
|
|
570
378
|
|
|
571
379
|
|
|
572
|
-
if (lexical.$
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
target = firstChild;
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
if (firstChild === null) {
|
|
580
|
-
if (wrappingElement) {
|
|
581
|
-
target.append(wrappingElement);
|
|
380
|
+
if (lexical.$isRootOrShadowRoot(target)) {
|
|
381
|
+
if (targetIsPrevSibling) {
|
|
382
|
+
if (wrappingElement !== null) {
|
|
383
|
+
target.insertAfter(wrappingElement);
|
|
582
384
|
} else {
|
|
583
|
-
for (let i =
|
|
385
|
+
for (let i = elements.length - 1; i >= 0; i--) {
|
|
584
386
|
const element = elements[i];
|
|
585
|
-
target.
|
|
387
|
+
target.insertAfter(element);
|
|
586
388
|
}
|
|
587
389
|
}
|
|
588
390
|
} else {
|
|
589
|
-
|
|
590
|
-
|
|
391
|
+
const firstChild = target.getFirstChild();
|
|
392
|
+
|
|
393
|
+
if (lexical.$isElementNode(firstChild)) {
|
|
394
|
+
target = firstChild;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (firstChild === null) {
|
|
398
|
+
if (wrappingElement) {
|
|
399
|
+
target.append(wrappingElement);
|
|
400
|
+
} else {
|
|
401
|
+
for (let i = 0; i < elements.length; i++) {
|
|
402
|
+
const element = elements[i];
|
|
403
|
+
target.append(element);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
591
406
|
} else {
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
407
|
+
if (wrappingElement !== null) {
|
|
408
|
+
firstChild.insertBefore(wrappingElement);
|
|
409
|
+
} else {
|
|
410
|
+
for (let i = 0; i < elements.length; i++) {
|
|
411
|
+
const element = elements[i];
|
|
412
|
+
firstChild.insertBefore(element);
|
|
413
|
+
}
|
|
595
414
|
}
|
|
596
415
|
}
|
|
597
416
|
}
|
|
@@ -614,131 +433,319 @@ function $wrapLeafNodesInElements(selection, createElement, wrappingElement) {
|
|
|
614
433
|
selection.dirty = true;
|
|
615
434
|
}
|
|
616
435
|
}
|
|
436
|
+
function $shouldOverrideDefaultCharacterSelection(selection, isBackward) {
|
|
437
|
+
const possibleNode = lexical.$getDecoratorNode(selection.focus, isBackward);
|
|
438
|
+
return lexical.$isDecoratorNode(possibleNode) && !possibleNode.isIsolated();
|
|
439
|
+
}
|
|
440
|
+
function $moveCaretSelection(selection, isHoldingShift, isBackward, granularity) {
|
|
441
|
+
selection.modify(isHoldingShift ? 'extend' : 'move', isBackward, granularity);
|
|
442
|
+
}
|
|
443
|
+
function $isParentElementRTL(selection) {
|
|
444
|
+
const anchorNode = selection.anchor.getNode();
|
|
445
|
+
const parent = lexical.$isRootNode(anchorNode) ? anchorNode : anchorNode.getParentOrThrow();
|
|
446
|
+
return parent.getDirection() === 'rtl';
|
|
447
|
+
}
|
|
448
|
+
function $moveCharacter(selection, isHoldingShift, isBackward) {
|
|
449
|
+
const isRTL = $isParentElementRTL(selection);
|
|
450
|
+
$moveCaretSelection(selection, isHoldingShift, isBackward ? !isRTL : isRTL, 'character');
|
|
451
|
+
}
|
|
452
|
+
function $selectAll(selection) {
|
|
453
|
+
const anchor = selection.anchor;
|
|
454
|
+
const focus = selection.focus;
|
|
455
|
+
const anchorNode = anchor.getNode();
|
|
456
|
+
const topParent = anchorNode.getTopLevelElementOrThrow();
|
|
457
|
+
const root = topParent.getParentOrThrow();
|
|
458
|
+
let firstNode = root.getFirstDescendant();
|
|
459
|
+
let lastNode = root.getLastDescendant();
|
|
460
|
+
let firstType = 'element';
|
|
461
|
+
let lastType = 'element';
|
|
462
|
+
let lastOffset = 0;
|
|
617
463
|
|
|
618
|
-
|
|
619
|
-
|
|
464
|
+
if (lexical.$isTextNode(firstNode)) {
|
|
465
|
+
firstType = 'text';
|
|
466
|
+
} else if (!lexical.$isElementNode(firstNode) && firstNode !== null) {
|
|
467
|
+
firstNode = firstNode.getParentOrThrow();
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (lexical.$isTextNode(lastNode)) {
|
|
471
|
+
lastType = 'text';
|
|
472
|
+
lastOffset = lastNode.getTextContentSize();
|
|
473
|
+
} else if (!lexical.$isElementNode(lastNode) && lastNode !== null) {
|
|
474
|
+
lastNode = lastNode.getParentOrThrow();
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
if (firstNode && lastNode) {
|
|
478
|
+
anchor.set(firstNode.getKey(), 0, firstType);
|
|
479
|
+
focus.set(lastNode.getKey(), lastOffset, lastType);
|
|
480
|
+
}
|
|
620
481
|
}
|
|
621
482
|
|
|
622
|
-
function $
|
|
623
|
-
|
|
624
|
-
|
|
483
|
+
function $getIndexFromPossibleClone(node, parent, nodeMap) {
|
|
484
|
+
const parentClone = nodeMap.get(parent.getKey());
|
|
485
|
+
|
|
486
|
+
if (lexical.$isElementNode(parentClone)) {
|
|
487
|
+
return parentClone.__children.indexOf(node.getKey());
|
|
625
488
|
}
|
|
626
489
|
|
|
627
|
-
return
|
|
490
|
+
return node.getIndexWithinParent();
|
|
628
491
|
}
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
492
|
+
|
|
493
|
+
function $getParentAvoidingExcludedElements(node) {
|
|
494
|
+
let parent = node.getParent();
|
|
495
|
+
|
|
496
|
+
while (parent !== null && parent.excludeFromCopy('clone')) {
|
|
497
|
+
parent = parent.getParent();
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return parent;
|
|
632
501
|
}
|
|
633
502
|
|
|
634
|
-
function
|
|
635
|
-
let node =
|
|
503
|
+
function $copyLeafNodeBranchToRoot(leaf, startingOffset, endingOffset, isLeftSide, range, nodeMap) {
|
|
504
|
+
let node = leaf;
|
|
505
|
+
let offset = startingOffset;
|
|
636
506
|
|
|
637
|
-
while (node
|
|
638
|
-
|
|
639
|
-
|
|
507
|
+
while (node !== null) {
|
|
508
|
+
const parent = $getParentAvoidingExcludedElements(node);
|
|
509
|
+
|
|
510
|
+
if (parent === null) {
|
|
511
|
+
break;
|
|
640
512
|
}
|
|
641
513
|
|
|
642
|
-
node
|
|
643
|
-
|
|
514
|
+
if (!lexical.$isElementNode(node) || !node.excludeFromCopy('clone')) {
|
|
515
|
+
const key = node.getKey();
|
|
516
|
+
let clone = nodeMap.get(key);
|
|
517
|
+
const needsClone = clone === undefined;
|
|
644
518
|
|
|
645
|
-
|
|
519
|
+
if (needsClone) {
|
|
520
|
+
clone = $cloneWithProperties(node);
|
|
521
|
+
nodeMap.set(key, clone);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (lexical.$isTextNode(clone) && !clone.isSegmented() && !clone.isToken()) {
|
|
525
|
+
clone.__text = clone.__text.slice(isLeftSide ? offset : 0, isLeftSide ? endingOffset : offset);
|
|
526
|
+
} else if (lexical.$isElementNode(clone)) {
|
|
527
|
+
clone.__children = clone.__children.slice(isLeftSide ? offset : 0, isLeftSide ? undefined : (offset || 0) + 1);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (lexical.$isRootNode(parent)) {
|
|
531
|
+
if (needsClone) {
|
|
532
|
+
// We only want to collect a range of top level nodes.
|
|
533
|
+
// So if the parent is the root, we know this is a top level.
|
|
534
|
+
range.push(key);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
break;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
offset = $getIndexFromPossibleClone(node, parent, nodeMap);
|
|
542
|
+
node = parent;
|
|
543
|
+
}
|
|
646
544
|
}
|
|
647
545
|
|
|
648
|
-
function
|
|
649
|
-
const
|
|
546
|
+
function $cloneRangeSelectionContent(selection) {
|
|
547
|
+
const anchor = selection.anchor;
|
|
548
|
+
const focus = selection.focus;
|
|
549
|
+
const [anchorOffset, focusOffset] = selection.getCharacterOffsets();
|
|
550
|
+
const nodes = selection.getNodes();
|
|
650
551
|
|
|
651
|
-
if (
|
|
652
|
-
|
|
552
|
+
if (nodes.length === 0) {
|
|
553
|
+
return {
|
|
554
|
+
nodeMap: [],
|
|
555
|
+
range: []
|
|
556
|
+
};
|
|
557
|
+
} // Check if we can use the parent of the nodes, if the
|
|
558
|
+
// parent can't be empty, then it's important that we
|
|
559
|
+
// also copy that element node along with its children.
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
let nodesLength = nodes.length;
|
|
563
|
+
const firstNode = nodes[0];
|
|
564
|
+
const firstNodeParent = firstNode.getParent();
|
|
565
|
+
|
|
566
|
+
if (firstNodeParent !== null && (!firstNodeParent.canBeEmpty() || lexical.$isRootNode(firstNodeParent))) {
|
|
567
|
+
const parentChildren = firstNodeParent.__children;
|
|
568
|
+
const parentChildrenLength = parentChildren.length;
|
|
569
|
+
|
|
570
|
+
if (parentChildrenLength === nodesLength) {
|
|
571
|
+
let areTheSame = true;
|
|
572
|
+
|
|
573
|
+
for (let i = 0; i < parentChildren.length; i++) {
|
|
574
|
+
if (parentChildren[i] !== nodes[i].__key) {
|
|
575
|
+
areTheSame = false;
|
|
576
|
+
break;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
if (areTheSame) {
|
|
581
|
+
nodesLength++;
|
|
582
|
+
nodes.push(firstNodeParent);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
653
585
|
}
|
|
654
586
|
|
|
655
|
-
|
|
587
|
+
const lastNode = nodes[nodesLength - 1];
|
|
588
|
+
const isBefore = anchor.isBefore(focus);
|
|
589
|
+
const nodeMap = new Map();
|
|
590
|
+
const range = [];
|
|
591
|
+
const isOnlyText = lexical.$isTextNode(firstNode) && nodesLength === 1; // Do first node to root
|
|
592
|
+
|
|
593
|
+
$copyLeafNodeBranchToRoot(firstNode, isBefore ? anchorOffset : focusOffset, isOnlyText ? isBefore ? focusOffset : anchorOffset : undefined, true, range, nodeMap); // Copy all nodes between
|
|
594
|
+
|
|
595
|
+
for (let i = 0; i < nodesLength; i++) {
|
|
596
|
+
const node = nodes[i];
|
|
597
|
+
const key = node.getKey();
|
|
598
|
+
|
|
599
|
+
if (!nodeMap.has(key) && (!lexical.$isElementNode(node) || !node.excludeFromCopy('clone'))) {
|
|
600
|
+
const clone = $cloneWithProperties(node);
|
|
601
|
+
|
|
602
|
+
if (lexical.$isRootNode(node.getParent())) {
|
|
603
|
+
range.push(node.getKey());
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
if (key !== 'root') {
|
|
607
|
+
nodeMap.set(key, clone);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
} // Do last node to root
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
$copyLeafNodeBranchToRoot(lastNode, isOnlyText ? undefined : isBefore ? focusOffset : anchorOffset, undefined, false, range, nodeMap);
|
|
614
|
+
return {
|
|
615
|
+
nodeMap: Array.from(nodeMap.entries()),
|
|
616
|
+
range
|
|
617
|
+
};
|
|
656
618
|
}
|
|
657
619
|
|
|
658
|
-
function
|
|
659
|
-
const
|
|
660
|
-
const
|
|
661
|
-
const range = document.createRange();
|
|
662
|
-
let anchorDOM = editor.getElementByKey(anchorKey);
|
|
663
|
-
let focusDOM = editor.getElementByKey(focusKey);
|
|
664
|
-
let anchorOffset = _anchorOffset;
|
|
665
|
-
let focusOffset = _focusOffset;
|
|
620
|
+
function $getNodeStyleValueForProperty(node, styleProperty, defaultValue) {
|
|
621
|
+
const css = node.getStyle();
|
|
622
|
+
const styleObject = getStyleObjectFromCSS(css);
|
|
666
623
|
|
|
667
|
-
if (
|
|
668
|
-
|
|
624
|
+
if (styleObject !== null) {
|
|
625
|
+
return styleObject[styleProperty] || defaultValue;
|
|
669
626
|
}
|
|
670
627
|
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
}
|
|
628
|
+
return defaultValue;
|
|
629
|
+
}
|
|
674
630
|
|
|
675
|
-
|
|
676
|
-
|
|
631
|
+
function $getSelectionStyleValueForProperty(selection, styleProperty, defaultValue = '') {
|
|
632
|
+
let styleValue = null;
|
|
633
|
+
const nodes = selection.getNodes();
|
|
634
|
+
const anchor = selection.anchor;
|
|
635
|
+
const focus = selection.focus;
|
|
636
|
+
const isBackward = selection.isBackward();
|
|
637
|
+
const endOffset = isBackward ? focus.offset : anchor.offset;
|
|
638
|
+
const endNode = isBackward ? focus.getNode() : anchor.getNode();
|
|
639
|
+
|
|
640
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
641
|
+
const node = nodes[i]; // if no actual characters in the end node are selected, we don't
|
|
642
|
+
// include it in the selection for purposes of determining style
|
|
643
|
+
// value
|
|
644
|
+
|
|
645
|
+
if (i !== 0 && endOffset === 0 && node.is(endNode)) {
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
if (lexical.$isTextNode(node)) {
|
|
650
|
+
const nodeStyleValue = $getNodeStyleValueForProperty(node, styleProperty, defaultValue);
|
|
651
|
+
|
|
652
|
+
if (styleValue === null) {
|
|
653
|
+
styleValue = nodeStyleValue;
|
|
654
|
+
} else if (styleValue !== nodeStyleValue) {
|
|
655
|
+
// multiple text nodes are in the selection and they don't all
|
|
656
|
+
// have the same font size.
|
|
657
|
+
styleValue = '';
|
|
658
|
+
break;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
677
661
|
}
|
|
678
662
|
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
663
|
+
return styleValue === null ? defaultValue : styleValue;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
668
|
+
*
|
|
669
|
+
* This source code is licensed under the MIT license found in the
|
|
670
|
+
* LICENSE file in the root directory of this source tree.
|
|
671
|
+
*
|
|
672
|
+
*/
|
|
673
|
+
|
|
674
|
+
function $updateElementNodeProperties(target, source) {
|
|
675
|
+
target.__children = Array.from(source.__children);
|
|
676
|
+
target.__format = source.__format;
|
|
677
|
+
target.__indent = source.__indent;
|
|
678
|
+
target.__dir = source.__dir;
|
|
679
|
+
return target;
|
|
680
|
+
}
|
|
682
681
|
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
682
|
+
function $updateTextNodeProperties(target, source) {
|
|
683
|
+
target.__format = source.__format;
|
|
684
|
+
target.__style = source.__style;
|
|
685
|
+
target.__mode = source.__mode;
|
|
686
|
+
target.__detail = source.__detail;
|
|
687
|
+
return target;
|
|
688
|
+
}
|
|
686
689
|
|
|
687
|
-
|
|
690
|
+
function $cloneWithProperties(node) {
|
|
691
|
+
const latest = node.getLatest();
|
|
692
|
+
const constructor = latest.constructor; // @ts-expect-error
|
|
688
693
|
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
}
|
|
694
|
+
const clone = constructor.clone(latest);
|
|
695
|
+
clone.__parent = latest.__parent;
|
|
692
696
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
range.setEnd(focusDOM, focusOffset);
|
|
696
|
-
} catch (e) {
|
|
697
|
-
return null;
|
|
697
|
+
if (lexical.$isElementNode(latest) && lexical.$isElementNode(clone)) {
|
|
698
|
+
return $updateElementNodeProperties(clone, latest);
|
|
698
699
|
}
|
|
699
700
|
|
|
700
|
-
if (
|
|
701
|
-
|
|
702
|
-
range.setStart(focusDOM, focusOffset);
|
|
703
|
-
range.setEnd(anchorDOM, anchorOffset);
|
|
701
|
+
if (lexical.$isTextNode(latest) && lexical.$isTextNode(clone)) {
|
|
702
|
+
return $updateTextNodeProperties(clone, latest);
|
|
704
703
|
}
|
|
705
704
|
|
|
706
|
-
return
|
|
705
|
+
return clone;
|
|
707
706
|
}
|
|
708
|
-
function
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
const rootRect = rootElement.getBoundingClientRect();
|
|
716
|
-
const computedStyle = getComputedStyle(rootElement);
|
|
717
|
-
const rootPadding = parseFloat(computedStyle.paddingLeft) + parseFloat(computedStyle.paddingRight);
|
|
718
|
-
const selectionRects = Array.from(range.getClientRects());
|
|
719
|
-
let selectionRectsLength = selectionRects.length;
|
|
720
|
-
let prevRect;
|
|
721
|
-
|
|
722
|
-
for (let i = 0; i < selectionRectsLength; i++) {
|
|
723
|
-
const selectionRect = selectionRects[i]; // Exclude a rect that is the exact same as the last rect. getClientRects() can return
|
|
724
|
-
// the same rect twice for some elements. A more sophisticated thing to do here is to
|
|
725
|
-
// merge all the rects together into a set of rects that don't overlap, so we don't
|
|
726
|
-
// generate backgrounds that are too dark.
|
|
707
|
+
function $sliceSelectedTextNodeContent(selection, textNode) {
|
|
708
|
+
if (textNode.isSelected() && !textNode.isSegmented() && !textNode.isToken() && (lexical.$isRangeSelection(selection) || lexical.DEPRECATED_$isGridSelection(selection))) {
|
|
709
|
+
const anchorNode = selection.anchor.getNode();
|
|
710
|
+
const focusNode = selection.focus.getNode();
|
|
711
|
+
const isAnchor = textNode.is(anchorNode);
|
|
712
|
+
const isFocus = textNode.is(focusNode);
|
|
727
713
|
|
|
728
|
-
|
|
714
|
+
if (isAnchor || isFocus) {
|
|
715
|
+
const isBackward = selection.isBackward();
|
|
716
|
+
const [anchorOffset, focusOffset] = selection.getCharacterOffsets();
|
|
717
|
+
const isSame = anchorNode.is(focusNode);
|
|
718
|
+
const isFirst = textNode.is(isBackward ? focusNode : anchorNode);
|
|
719
|
+
const isLast = textNode.is(isBackward ? anchorNode : focusNode);
|
|
720
|
+
let startOffset = 0;
|
|
721
|
+
let endOffset = undefined;
|
|
729
722
|
|
|
730
|
-
|
|
723
|
+
if (isSame) {
|
|
724
|
+
startOffset = anchorOffset > focusOffset ? focusOffset : anchorOffset;
|
|
725
|
+
endOffset = anchorOffset > focusOffset ? anchorOffset : focusOffset;
|
|
726
|
+
} else if (isFirst) {
|
|
727
|
+
const offset = isBackward ? focusOffset : anchorOffset;
|
|
728
|
+
startOffset = offset;
|
|
729
|
+
endOffset = undefined;
|
|
730
|
+
} else if (isLast) {
|
|
731
|
+
const offset = isBackward ? anchorOffset : focusOffset;
|
|
732
|
+
startOffset = 0;
|
|
733
|
+
endOffset = offset;
|
|
734
|
+
}
|
|
731
735
|
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
selectionRectsLength--;
|
|
735
|
-
continue;
|
|
736
|
+
textNode.__text = textNode.__text.slice(startOffset, endOffset);
|
|
737
|
+
return textNode;
|
|
736
738
|
}
|
|
739
|
+
}
|
|
737
740
|
|
|
738
|
-
|
|
741
|
+
return textNode;
|
|
742
|
+
}
|
|
743
|
+
function $isAtNodeEnd(point) {
|
|
744
|
+
if (point.type === 'text') {
|
|
745
|
+
return point.offset === point.getNode().getTextContentSize();
|
|
739
746
|
}
|
|
740
747
|
|
|
741
|
-
return
|
|
748
|
+
return point.offset === point.getNode().getChildrenSize();
|
|
742
749
|
}
|
|
743
750
|
function trimTextContentFromAnchor(editor, anchor, delCount) {
|
|
744
751
|
// Work from the current selection anchor point
|
|
@@ -863,41 +870,159 @@ function trimTextContentFromAnchor(editor, anchor, delCount) {
|
|
|
863
870
|
}
|
|
864
871
|
}
|
|
865
872
|
}
|
|
866
|
-
function $sliceSelectedTextNodeContent(selection, textNode) {
|
|
867
|
-
if (textNode.isSelected() && !textNode.isSegmented() && !textNode.isToken() && (lexical.$isRangeSelection(selection) || lexical.$isGridSelection(selection))) {
|
|
868
|
-
const anchorNode = selection.anchor.getNode();
|
|
869
|
-
const focusNode = selection.focus.getNode();
|
|
870
|
-
const isAnchor = textNode.is(anchorNode);
|
|
871
|
-
const isFocus = textNode.is(focusNode);
|
|
872
873
|
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
const isLast = textNode.is(isBackward ? anchorNode : focusNode);
|
|
879
|
-
let startOffset = 0;
|
|
880
|
-
let endOffset = undefined;
|
|
874
|
+
function errGetLatestOnClone() {
|
|
875
|
+
{
|
|
876
|
+
throw Error(`getLatest() on clone node`);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
881
879
|
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
880
|
+
function $cloneContents(selection) {
|
|
881
|
+
let clone = {
|
|
882
|
+
nodeMap: [],
|
|
883
|
+
range: []
|
|
884
|
+
};
|
|
885
|
+
|
|
886
|
+
if (lexical.$isRangeSelection(selection)) {
|
|
887
|
+
clone = $cloneRangeSelectionContent(selection);
|
|
888
|
+
} else if (lexical.DEPRECATED_$isGridSelection(selection)) {
|
|
889
|
+
clone = $cloneGridSelectionContent(selection);
|
|
890
|
+
} else if (lexical.$isNodeSelection(selection)) {
|
|
891
|
+
clone = $cloneNodeSelectionContent();
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
{
|
|
895
|
+
const nodeMap = clone.nodeMap;
|
|
896
|
+
|
|
897
|
+
for (let i = 0; i < nodeMap.length; i++) {
|
|
898
|
+
const node = nodeMap[i][1];
|
|
899
|
+
|
|
900
|
+
if (node.getLatest === errGetLatestOnClone) {
|
|
901
|
+
continue;
|
|
893
902
|
}
|
|
894
903
|
|
|
895
|
-
|
|
896
|
-
|
|
904
|
+
Object.setPrototypeOf(node, Object.create(Object.getPrototypeOf(node), {
|
|
905
|
+
getLatest: {
|
|
906
|
+
configurable: true,
|
|
907
|
+
enumerable: true,
|
|
908
|
+
value: errGetLatestOnClone,
|
|
909
|
+
writable: true
|
|
910
|
+
}
|
|
911
|
+
}));
|
|
897
912
|
}
|
|
898
913
|
}
|
|
899
914
|
|
|
900
|
-
return
|
|
915
|
+
return clone;
|
|
916
|
+
}
|
|
917
|
+
function $addNodeStyle(node) {
|
|
918
|
+
const CSSText = node.getStyle();
|
|
919
|
+
const styles = getStyleObjectFromRawCSS(CSSText);
|
|
920
|
+
CSS_TO_STYLES.set(CSSText, styles);
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
function $patchNodeStyle(node, patch) {
|
|
924
|
+
const prevStyles = getStyleObjectFromCSS(node.getStyle());
|
|
925
|
+
const newStyles = prevStyles ? { ...prevStyles,
|
|
926
|
+
...patch
|
|
927
|
+
} : patch;
|
|
928
|
+
const newCSSText = getCSSFromStyleObject(newStyles);
|
|
929
|
+
node.setStyle(newCSSText);
|
|
930
|
+
CSS_TO_STYLES.set(newCSSText, newStyles);
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
function $patchStyleText(selection, patch) {
|
|
934
|
+
const selectedNodes = selection.getNodes();
|
|
935
|
+
const selectedNodesLength = selectedNodes.length;
|
|
936
|
+
const lastIndex = selectedNodesLength - 1;
|
|
937
|
+
let firstNode = selectedNodes[0];
|
|
938
|
+
let lastNode = selectedNodes[lastIndex];
|
|
939
|
+
|
|
940
|
+
if (selection.isCollapsed()) {
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
const anchor = selection.anchor;
|
|
945
|
+
const focus = selection.focus;
|
|
946
|
+
const firstNodeText = firstNode.getTextContent();
|
|
947
|
+
const firstNodeTextLength = firstNodeText.length;
|
|
948
|
+
const focusOffset = focus.offset;
|
|
949
|
+
let anchorOffset = anchor.offset;
|
|
950
|
+
let startOffset;
|
|
951
|
+
let endOffset;
|
|
952
|
+
const isBefore = anchor.isBefore(focus);
|
|
953
|
+
startOffset = isBefore ? anchorOffset : focusOffset;
|
|
954
|
+
endOffset = isBefore ? focusOffset : anchorOffset; // This is the case where the user only selected the very end of the
|
|
955
|
+
// first node so we don't want to include it in the formatting change.
|
|
956
|
+
|
|
957
|
+
if (startOffset === firstNode.getTextContentSize()) {
|
|
958
|
+
const nextSibling = firstNode.getNextSibling();
|
|
959
|
+
|
|
960
|
+
if (lexical.$isTextNode(nextSibling)) {
|
|
961
|
+
// we basically make the second node the firstNode, changing offsets accordingly
|
|
962
|
+
anchorOffset = 0;
|
|
963
|
+
startOffset = 0;
|
|
964
|
+
firstNode = nextSibling;
|
|
965
|
+
}
|
|
966
|
+
} // This is the case where we only selected a single node
|
|
967
|
+
|
|
968
|
+
|
|
969
|
+
if (firstNode.is(lastNode)) {
|
|
970
|
+
if (lexical.$isTextNode(firstNode)) {
|
|
971
|
+
startOffset = anchorOffset > focusOffset ? focusOffset : anchorOffset;
|
|
972
|
+
endOffset = anchorOffset > focusOffset ? anchorOffset : focusOffset; // No actual text is selected, so do nothing.
|
|
973
|
+
|
|
974
|
+
if (startOffset === endOffset) {
|
|
975
|
+
return;
|
|
976
|
+
} // The entire node is selected, so just format it
|
|
977
|
+
|
|
978
|
+
|
|
979
|
+
if (startOffset === 0 && endOffset === firstNodeTextLength) {
|
|
980
|
+
$patchNodeStyle(firstNode, patch);
|
|
981
|
+
firstNode.select(startOffset, endOffset);
|
|
982
|
+
} else {
|
|
983
|
+
// The node is partially selected, so split it into two nodes
|
|
984
|
+
// and style the selected one.
|
|
985
|
+
const splitNodes = firstNode.splitText(startOffset, endOffset);
|
|
986
|
+
const replacement = startOffset === 0 ? splitNodes[0] : splitNodes[1];
|
|
987
|
+
$patchNodeStyle(replacement, patch);
|
|
988
|
+
replacement.select(0, endOffset - startOffset);
|
|
989
|
+
}
|
|
990
|
+
} // multiple nodes selected.
|
|
991
|
+
|
|
992
|
+
} else {
|
|
993
|
+
if (lexical.$isTextNode(firstNode)) {
|
|
994
|
+
if (startOffset !== 0) {
|
|
995
|
+
// the entire first node isn't selected, so split it
|
|
996
|
+
firstNode = firstNode.splitText(startOffset)[1];
|
|
997
|
+
startOffset = 0;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
$patchNodeStyle(firstNode, patch);
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
if (lexical.$isTextNode(lastNode)) {
|
|
1004
|
+
const lastNodeText = lastNode.getTextContent();
|
|
1005
|
+
const lastNodeTextLength = lastNodeText.length; // if the entire last node isn't selected, split it
|
|
1006
|
+
|
|
1007
|
+
if (endOffset !== lastNodeTextLength) {
|
|
1008
|
+
[lastNode] = lastNode.splitText(endOffset);
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
if (endOffset !== 0) {
|
|
1012
|
+
$patchNodeStyle(lastNode, patch);
|
|
1013
|
+
}
|
|
1014
|
+
} // style all the text nodes in between
|
|
1015
|
+
|
|
1016
|
+
|
|
1017
|
+
for (let i = 1; i < lastIndex; i++) {
|
|
1018
|
+
const selectedNode = selectedNodes[i];
|
|
1019
|
+
const selectedNodeKey = selectedNode.getKey();
|
|
1020
|
+
|
|
1021
|
+
if (lexical.$isTextNode(selectedNode) && selectedNodeKey !== firstNode.getKey() && selectedNodeKey !== lastNode.getKey() && !selectedNode.isToken()) {
|
|
1022
|
+
$patchNodeStyle(selectedNode, patch);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
901
1026
|
}
|
|
902
1027
|
|
|
903
1028
|
exports.$addNodeStyle = $addNodeStyle;
|
|
@@ -912,7 +1037,8 @@ exports.$patchStyleText = $patchStyleText;
|
|
|
912
1037
|
exports.$selectAll = $selectAll;
|
|
913
1038
|
exports.$shouldOverrideDefaultCharacterSelection = $shouldOverrideDefaultCharacterSelection;
|
|
914
1039
|
exports.$sliceSelectedTextNodeContent = $sliceSelectedTextNodeContent;
|
|
915
|
-
exports.$
|
|
1040
|
+
exports.$wrapNodes = $wrapNodes;
|
|
1041
|
+
exports.$wrapNodesImpl = $wrapNodesImpl;
|
|
916
1042
|
exports.createDOMRange = createDOMRange;
|
|
917
1043
|
exports.createRectsFromDOMRange = createRectsFromDOMRange;
|
|
918
1044
|
exports.getStyleObjectFromCSS = getStyleObjectFromCSS;
|