@lexical/selection 0.5.0 → 0.5.1-next.1

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.
@@ -8,453 +8,208 @@
8
8
 
9
9
  var lexical = require('lexical');
10
10
 
11
- /** @module @lexical/selection */
12
- const cssToStyles = new Map();
13
- function $cloneWithProperties(node) {
14
- const latest = node.getLatest();
15
- const constructor = latest.constructor; // @ts-expect-error
16
-
17
- const clone = constructor.clone(latest);
18
- clone.__parent = latest.__parent;
19
-
20
- if (lexical.$isElementNode(latest) && lexical.$isElementNode(clone)) {
21
- clone.__children = Array.from(latest.__children);
22
- clone.__format = latest.__format;
23
- clone.__indent = latest.__indent;
24
- clone.__dir = latest.__dir;
25
- } else if (lexical.$isTextNode(latest) && lexical.$isTextNode(clone)) {
26
- clone.__format = latest.__format;
27
- clone.__style = latest.__style;
28
- clone.__mode = latest.__mode;
29
- clone.__detail = latest.__detail;
30
- }
31
-
32
- return clone;
33
- }
34
-
35
- function $getIndexFromPossibleClone(node, parent, nodeMap) {
36
- const parentClone = nodeMap.get(parent.getKey());
37
-
38
- if (lexical.$isElementNode(parentClone)) {
39
- return parentClone.__children.indexOf(node.getKey());
40
- }
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();
41
19
 
42
- return node.getIndexWithinParent();
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
+ };
43
37
  }
44
38
 
45
- function $getParentAvoidingExcludedElements(node) {
46
- let parent = node.getParent();
47
-
48
- while (parent !== null && parent.excludeFromCopy('clone')) {
49
- parent = parent.getParent();
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`);
50
49
  }
51
-
52
- return parent;
53
50
  }
54
51
 
55
- function $copyLeafNodeBranchToRoot(leaf, startingOffset, endingOffset, isLeftSide, range, nodeMap) {
56
- let node = leaf;
57
- let offset = startingOffset;
58
-
59
- while (node !== null) {
60
- const parent = $getParentAvoidingExcludedElements(node);
61
-
62
- if (parent === null) {
63
- break;
64
- }
65
-
66
- if (!lexical.$isElementNode(node) || !node.excludeFromCopy('clone')) {
67
- const key = node.getKey();
68
- let clone = nodeMap.get(key);
69
- const needsClone = clone === undefined;
70
-
71
- if (needsClone) {
72
- clone = $cloneWithProperties(node);
73
- nodeMap.set(key, clone);
74
- }
75
-
76
- if (lexical.$isTextNode(clone) && !clone.isSegmented() && !clone.isToken()) {
77
- clone.__text = clone.__text.slice(isLeftSide ? offset : 0, isLeftSide ? endingOffset : offset);
78
- } else if (lexical.$isElementNode(clone)) {
79
- clone.__children = clone.__children.slice(isLeftSide ? offset : 0, isLeftSide ? undefined : (offset || 0) + 1);
80
- }
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
+ */
81
59
 
82
- if (lexical.$isRootNode(parent)) {
83
- if (needsClone) {
84
- // We only want to collect a range of top level nodes.
85
- // So if the parent is the root, we know this is a top level.
86
- range.push(key);
87
- }
60
+ function getDOMTextNode(element) {
61
+ let node = element;
88
62
 
89
- break;
90
- }
63
+ while (node != null) {
64
+ if (node.nodeType === Node.TEXT_NODE) {
65
+ return node;
91
66
  }
92
67
 
93
- offset = $getIndexFromPossibleClone(node, parent, nodeMap);
94
- node = parent;
68
+ node = node.firstChild;
95
69
  }
96
- }
97
70
 
98
- function errGetLatestOnClone() {
99
- {
100
- throw Error(`getLatest() on clone node`);
101
- }
71
+ return null;
102
72
  }
103
73
 
104
- function $cloneContents(selection) {
105
- const clone = $cloneContentsImpl(selection);
106
-
107
- {
108
- const nodeMap = clone.nodeMap;
109
-
110
- for (let i = 0; i < nodeMap.length; i++) {
111
- const node = nodeMap[i][1];
112
-
113
- if (node.getLatest === errGetLatestOnClone) {
114
- continue;
115
- }
74
+ function getDOMIndexWithinParent(node) {
75
+ const parent = node.parentNode;
116
76
 
117
- Object.setPrototypeOf(node, Object.create(Object.getPrototypeOf(node), {
118
- getLatest: {
119
- configurable: true,
120
- enumerable: true,
121
- value: errGetLatestOnClone,
122
- writable: true
123
- }
124
- }));
125
- }
77
+ if (parent == null) {
78
+ throw new Error('Should never happen');
126
79
  }
127
80
 
128
- return clone;
81
+ return [parent, Array.from(parent.childNodes).indexOf(node)];
129
82
  }
130
83
 
131
- function $cloneContentsImpl(selection) {
132
- if (lexical.$isRangeSelection(selection)) {
133
- const anchor = selection.anchor;
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();
184
-
185
- if (!nodeMap.has(key) && (!lexical.$isElementNode(node) || !node.excludeFromCopy('clone'))) {
186
- const clone = $cloneWithProperties(node);
187
-
188
- if (lexical.$isRootNode(node.getParent())) {
189
- range.push(node.getKey());
190
- }
191
-
192
- if (key !== 'root') {
193
- nodeMap.set(key, clone);
194
- }
195
- }
196
- } // Do last node to root
197
-
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;
198
92
 
199
- $copyLeafNodeBranchToRoot(lastNode, isOnlyText ? undefined : isBefore ? focusOffset : anchorOffset, undefined, false, range, nodeMap);
200
- return {
201
- nodeMap: Array.from(nodeMap.entries()),
202
- range
203
- };
204
- } else if (lexical.DEPRECATED_$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
- };
93
+ if (lexical.$isTextNode(anchorNode)) {
94
+ anchorDOM = getDOMTextNode(anchorDOM);
214
95
  }
215
96
 
216
- {
217
- throw Error(`TODO`);
97
+ if (lexical.$isTextNode(focusNode)) {
98
+ focusDOM = getDOMTextNode(focusDOM);
218
99
  }
219
- }
220
100
 
221
- function getStyleObjectFromCSS(css) {
222
- let value = cssToStyles.get(css);
223
-
224
- if (value === undefined) {
225
- value = getStyleObjectFromRawCSS(css);
226
- cssToStyles.set(css, value);
101
+ if (anchorNode === undefined || focusNode === undefined || anchorDOM === null || focusDOM === null) {
102
+ return null;
227
103
  }
228
104
 
229
- return value;
230
- }
231
-
232
- function getStyleObjectFromRawCSS(css) {
233
- const styleObject = {};
234
- const styles = css.split(';');
235
-
236
- for (const style of styles) {
237
- if (style !== '') {
238
- const [key, value] = style.split(/:([^]+)/); // split on first colon
239
-
240
- styleObject[key.trim()] = value.trim();
241
- }
105
+ if (anchorDOM.nodeName === 'BR') {
106
+ [anchorDOM, anchorOffset] = getDOMIndexWithinParent(anchorDOM);
242
107
  }
243
108
 
244
- return styleObject;
245
- }
109
+ if (focusDOM.nodeName === 'BR') {
110
+ [focusDOM, focusOffset] = getDOMIndexWithinParent(focusDOM);
111
+ }
246
112
 
247
- function getCSSFromStyleObject(styles) {
248
- let css = '';
113
+ const firstChild = anchorDOM.firstChild;
249
114
 
250
- for (const style in styles) {
251
- if (style) {
252
- css += `${style}: ${styles[style]};`;
253
- }
115
+ if (anchorDOM === focusDOM && firstChild != null && firstChild.nodeName === 'BR' && anchorOffset === 0 && focusOffset === 0) {
116
+ focusOffset = 1;
254
117
  }
255
118
 
256
- return css;
257
- }
119
+ try {
120
+ range.setStart(anchorDOM, anchorOffset);
121
+ range.setEnd(focusDOM, focusOffset);
122
+ } catch (e) {
123
+ return null;
124
+ }
258
125
 
259
- function $addNodeStyle(node) {
260
- const CSSText = node.getStyle();
261
- const styles = getStyleObjectFromRawCSS(CSSText);
262
- cssToStyles.set(CSSText, styles);
263
- }
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);
130
+ }
264
131
 
265
- function $patchNodeStyle(node, patch) {
266
- const prevStyles = getStyleObjectFromCSS(node.getStyle());
267
- const newStyles = prevStyles ? { ...prevStyles,
268
- ...patch
269
- } : patch;
270
- const newCSSText = getCSSFromStyleObject(newStyles);
271
- node.setStyle(newCSSText);
272
- cssToStyles.set(newCSSText, newStyles);
132
+ return range;
273
133
  }
134
+ function createRectsFromDOMRange(editor, range) {
135
+ const rootElement = editor.getRootElement();
274
136
 
275
- function $patchStyleText(selection, patch) {
276
- const selectedNodes = selection.getNodes();
277
- const selectedNodesLength = selectedNodes.length;
278
- const lastIndex = selectedNodesLength - 1;
279
- let firstNode = selectedNodes[0];
280
- let lastNode = selectedNodes[lastIndex];
281
-
282
- if (selection.isCollapsed()) {
283
- return;
137
+ if (rootElement === null) {
138
+ return [];
284
139
  }
285
140
 
286
- const anchor = selection.anchor;
287
- const focus = selection.focus;
288
- const firstNodeText = firstNode.getTextContent();
289
- const firstNodeTextLength = firstNodeText.length;
290
- const focusOffset = focus.offset;
291
- let anchorOffset = anchor.offset;
292
- let startOffset;
293
- let endOffset;
294
- const isBefore = anchor.isBefore(focus);
295
- startOffset = isBefore ? anchorOffset : focusOffset;
296
- endOffset = isBefore ? focusOffset : anchorOffset; // This is the case where the user only selected the very end of the
297
- // first node so we don't want to include it in the formatting change.
298
-
299
- if (startOffset === firstNode.getTextContentSize()) {
300
- const nextSibling = firstNode.getNextSibling();
301
-
302
- if (lexical.$isTextNode(nextSibling)) {
303
- // we basically make the second node the firstNode, changing offsets accordingly
304
- anchorOffset = 0;
305
- startOffset = 0;
306
- firstNode = nextSibling;
307
- }
308
- } // This is the case where we only selected a single node
309
-
310
-
311
- if (firstNode.is(lastNode)) {
312
- if (lexical.$isTextNode(firstNode)) {
313
- startOffset = anchorOffset > focusOffset ? focusOffset : anchorOffset;
314
- endOffset = anchorOffset > focusOffset ? anchorOffset : focusOffset; // No actual text is selected, so do nothing.
315
-
316
- if (startOffset === endOffset) {
317
- return;
318
- } // The entire node is selected, so just format it
319
-
320
-
321
- if (startOffset === 0 && endOffset === firstNodeTextLength) {
322
- $patchNodeStyle(firstNode, patch);
323
- firstNode.select(startOffset, endOffset);
324
- } else {
325
- // The node is partially selected, so split it into two nodes
326
- // and style the selected one.
327
- const splitNodes = firstNode.splitText(startOffset, endOffset);
328
- const replacement = startOffset === 0 ? splitNodes[0] : splitNodes[1];
329
- $patchNodeStyle(replacement, patch);
330
- replacement.select(0, endOffset - startOffset);
331
- }
332
- } // multiple nodes selected.
333
-
334
- } else {
335
- if (lexical.$isTextNode(firstNode)) {
336
- if (startOffset !== 0) {
337
- // the entire first node isn't selected, so split it
338
- firstNode = firstNode.splitText(startOffset)[1];
339
- startOffset = 0;
340
- }
341
-
342
- $patchNodeStyle(firstNode, patch);
343
- }
344
-
345
- if (lexical.$isTextNode(lastNode)) {
346
- const lastNodeText = lastNode.getTextContent();
347
- const lastNodeTextLength = lastNodeText.length; // if the entire last node isn't selected, split it
348
-
349
- if (endOffset !== lastNodeTextLength) {
350
- [lastNode] = lastNode.splitText(endOffset);
351
- }
352
-
353
- if (endOffset !== 0) {
354
- $patchNodeStyle(lastNode, patch);
355
- }
356
- } // style all the text nodes in between
357
-
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;
358
147
 
359
- for (let i = 1; i < lastIndex; i++) {
360
- const selectedNode = selectedNodes[i];
361
- const selectedNodeKey = selectedNode.getKey();
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.
362
153
 
363
- if (lexical.$isTextNode(selectedNode) && selectedNodeKey !== firstNode.getKey() && selectedNodeKey !== lastNode.getKey() && !selectedNode.isToken()) {
364
- $patchNodeStyle(selectedNode, patch);
365
- }
366
- }
367
- }
368
- }
369
- function $getSelectionStyleValueForProperty(selection, styleProperty, defaultValue = '') {
370
- let styleValue = null;
371
- const nodes = selection.getNodes();
372
- const anchor = selection.anchor;
373
- const focus = selection.focus;
374
- const isBackward = selection.isBackward();
375
- const endOffset = isBackward ? focus.offset : anchor.offset;
376
- const endNode = isBackward ? focus.getNode() : anchor.getNode();
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
377
155
 
378
- for (let i = 0; i < nodes.length; i++) {
379
- const node = nodes[i]; // if no actual characters in the end node are selected, we don't
380
- // include it in the selection for purposes of determining style
381
- // value
156
+ const selectionSpansElement = selectionRect.width + rootPadding === rootRect.width;
382
157
 
383
- if (i !== 0 && endOffset === 0 && node.is(endNode)) {
158
+ if (isDuplicateRect || selectionSpansElement) {
159
+ selectionRects.splice(i--, 1);
160
+ selectionRectsLength--;
384
161
  continue;
385
162
  }
386
163
 
387
- if (lexical.$isTextNode(node)) {
388
- const nodeStyleValue = $getNodeStyleValueForProperty(node, styleProperty, defaultValue);
389
-
390
- if (styleValue === null) {
391
- styleValue = nodeStyleValue;
392
- } else if (styleValue !== nodeStyleValue) {
393
- // multiple text nodes are in the selection and they don't all
394
- // have the same font size.
395
- styleValue = '';
396
- break;
397
- }
398
- }
399
- }
400
-
401
- return styleValue === null ? defaultValue : styleValue;
402
- }
403
-
404
- function $getNodeStyleValueForProperty(node, styleProperty, defaultValue) {
405
- const css = node.getStyle();
406
- const styleObject = getStyleObjectFromCSS(css);
407
-
408
- if (styleObject !== null) {
409
- return styleObject[styleProperty] || defaultValue;
164
+ prevRect = selectionRect;
410
165
  }
411
166
 
412
- return defaultValue;
167
+ return selectionRects;
413
168
  }
169
+ function getStyleObjectFromRawCSS(css) {
170
+ const styleObject = {};
171
+ const styles = css.split(';');
414
172
 
415
- function $moveCaretSelection(selection, isHoldingShift, isBackward, granularity) {
416
- selection.modify(isHoldingShift ? 'extend' : 'move', isBackward, granularity);
417
- }
418
- function $isParentElementRTL(selection) {
419
- const anchorNode = selection.anchor.getNode();
420
- const parent = lexical.$isRootNode(anchorNode) ? anchorNode : anchorNode.getParentOrThrow();
421
- return parent.getDirection() === 'rtl';
422
- }
423
- function $moveCharacter(selection, isHoldingShift, isBackward) {
424
- const isRTL = $isParentElementRTL(selection);
425
- $moveCaretSelection(selection, isHoldingShift, isBackward ? !isRTL : isRTL, 'character');
426
- }
427
- function $selectAll(selection) {
428
- const anchor = selection.anchor;
429
- const focus = selection.focus;
430
- const anchorNode = anchor.getNode();
431
- const topParent = anchorNode.getTopLevelElementOrThrow();
432
- const root = topParent.getParentOrThrow();
433
- let firstNode = root.getFirstDescendant();
434
- let lastNode = root.getLastDescendant();
435
- let firstType = 'element';
436
- let lastType = 'element';
437
- let lastOffset = 0;
173
+ for (const style of styles) {
174
+ if (style !== '') {
175
+ const [key, value] = style.split(/:([^]+)/); // split on first colon
438
176
 
439
- if (lexical.$isTextNode(firstNode)) {
440
- firstType = 'text';
441
- } else if (!lexical.$isElementNode(firstNode) && firstNode !== null) {
442
- firstNode = firstNode.getParentOrThrow();
177
+ styleObject[key.trim()] = value.trim();
178
+ }
443
179
  }
444
180
 
445
- if (lexical.$isTextNode(lastNode)) {
446
- lastType = 'text';
447
- lastOffset = lastNode.getTextContentSize();
448
- } else if (!lexical.$isElementNode(lastNode) && lastNode !== null) {
449
- lastNode = lastNode.getParentOrThrow();
181
+ return styleObject;
182
+ }
183
+ function getStyleObjectFromCSS(css) {
184
+ let value = CSS_TO_STYLES.get(css);
185
+
186
+ if (value === undefined) {
187
+ value = getStyleObjectFromRawCSS(css);
188
+ CSS_TO_STYLES.set(css, value);
450
189
  }
451
190
 
452
- if (firstNode && lastNode) {
453
- anchor.set(firstNode.getKey(), 0, firstType);
454
- focus.set(lastNode.getKey(), lastOffset, lastType);
191
+ return value;
192
+ }
193
+ function getCSSFromStyleObject(styles) {
194
+ let css = '';
195
+
196
+ for (const style in styles) {
197
+ if (style) {
198
+ css += `${style}: ${styles[style]};`;
199
+ }
455
200
  }
201
+
202
+ return css;
456
203
  }
457
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
+
458
213
  function $removeParentEmptyElements(startingNode) {
459
214
  let node = startingNode;
460
215
 
@@ -469,6 +224,10 @@ function $removeParentEmptyElements(startingNode) {
469
224
  node = parentNode;
470
225
  }
471
226
  }
227
+
228
+ function isPointAttached(point) {
229
+ return point.getNode().isAttached();
230
+ }
472
231
  /**
473
232
  * Attempts to wrap all nodes in the Selection in ElementNodes returned from createElement.
474
233
  * If wrappingElement is provided, all of the wrapped leaves are appended to the wrappingElement.
@@ -674,131 +433,319 @@ function $wrapNodesImpl(selection, nodes, nodesLength, createElement, wrappingEl
674
433
  selection.dirty = true;
675
434
  }
676
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;
677
463
 
678
- function isPointAttached(point) {
679
- return point.getNode().isAttached();
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
+ }
680
481
  }
681
482
 
682
- function $isAtNodeEnd(point) {
683
- if (point.type === 'text') {
684
- return point.offset === point.getNode().getTextContentSize();
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());
685
488
  }
686
489
 
687
- return point.offset === point.getNode().getChildrenSize();
490
+ return node.getIndexWithinParent();
688
491
  }
689
- function $shouldOverrideDefaultCharacterSelection(selection, isBackward) {
690
- const possibleNode = lexical.$getDecoratorNode(selection.focus, isBackward);
691
- return lexical.$isDecoratorNode(possibleNode) && !possibleNode.isIsolated();
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;
692
501
  }
693
502
 
694
- function getDOMTextNode(element) {
695
- let node = element;
503
+ function $copyLeafNodeBranchToRoot(leaf, startingOffset, endingOffset, isLeftSide, range, nodeMap) {
504
+ let node = leaf;
505
+ let offset = startingOffset;
696
506
 
697
- while (node != null) {
698
- if (node.nodeType === Node.TEXT_NODE) {
699
- return node;
507
+ while (node !== null) {
508
+ const parent = $getParentAvoidingExcludedElements(node);
509
+
510
+ if (parent === null) {
511
+ break;
700
512
  }
701
513
 
702
- node = node.firstChild;
703
- }
514
+ if (!lexical.$isElementNode(node) || !node.excludeFromCopy('clone')) {
515
+ const key = node.getKey();
516
+ let clone = nodeMap.get(key);
517
+ const needsClone = clone === undefined;
704
518
 
705
- return null;
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
+ }
706
544
  }
707
545
 
708
- function getDOMIndexWithinParent(node) {
709
- const parent = node.parentNode;
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();
710
551
 
711
- if (parent == null) {
712
- throw new Error('Should never happen');
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
+ }
713
585
  }
714
586
 
715
- return [parent, Array.from(parent.childNodes).indexOf(node)];
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
+ };
716
618
  }
717
619
 
718
- function createDOMRange(editor, anchorNode, _anchorOffset, focusNode, _focusOffset) {
719
- const anchorKey = anchorNode.getKey();
720
- const focusKey = focusNode.getKey();
721
- const range = document.createRange();
722
- let anchorDOM = editor.getElementByKey(anchorKey);
723
- let focusDOM = editor.getElementByKey(focusKey);
724
- let anchorOffset = _anchorOffset;
725
- let focusOffset = _focusOffset;
620
+ function $getNodeStyleValueForProperty(node, styleProperty, defaultValue) {
621
+ const css = node.getStyle();
622
+ const styleObject = getStyleObjectFromCSS(css);
726
623
 
727
- if (lexical.$isTextNode(anchorNode)) {
728
- anchorDOM = getDOMTextNode(anchorDOM);
624
+ if (styleObject !== null) {
625
+ return styleObject[styleProperty] || defaultValue;
729
626
  }
730
627
 
731
- if (lexical.$isTextNode(focusNode)) {
732
- focusDOM = getDOMTextNode(focusDOM);
733
- }
628
+ return defaultValue;
629
+ }
734
630
 
735
- if (anchorNode === undefined || focusNode === undefined || anchorDOM === null || focusDOM === null) {
736
- return null;
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
+ }
737
661
  }
738
662
 
739
- if (anchorDOM.nodeName === 'BR') {
740
- [anchorDOM, anchorOffset] = getDOMIndexWithinParent(anchorDOM);
741
- }
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
+ }
742
681
 
743
- if (focusDOM.nodeName === 'BR') {
744
- [focusDOM, focusOffset] = getDOMIndexWithinParent(focusDOM);
745
- }
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
+ }
746
689
 
747
- const firstChild = anchorDOM.firstChild;
690
+ function $cloneWithProperties(node) {
691
+ const latest = node.getLatest();
692
+ const constructor = latest.constructor; // @ts-expect-error
748
693
 
749
- if (anchorDOM === focusDOM && firstChild != null && firstChild.nodeName === 'BR' && anchorOffset === 0 && focusOffset === 0) {
750
- focusOffset = 1;
751
- }
694
+ const clone = constructor.clone(latest);
695
+ clone.__parent = latest.__parent;
752
696
 
753
- try {
754
- range.setStart(anchorDOM, anchorOffset);
755
- range.setEnd(focusDOM, focusOffset);
756
- } catch (e) {
757
- return null;
697
+ if (lexical.$isElementNode(latest) && lexical.$isElementNode(clone)) {
698
+ return $updateElementNodeProperties(clone, latest);
758
699
  }
759
700
 
760
- if (range.collapsed && (anchorOffset !== focusOffset || anchorKey !== focusKey)) {
761
- // Range is backwards, we need to reverse it
762
- range.setStart(focusDOM, focusOffset);
763
- range.setEnd(anchorDOM, anchorOffset);
701
+ if (lexical.$isTextNode(latest) && lexical.$isTextNode(clone)) {
702
+ return $updateTextNodeProperties(clone, latest);
764
703
  }
765
704
 
766
- return range;
705
+ return clone;
767
706
  }
768
- function createRectsFromDOMRange(editor, range) {
769
- const rootElement = editor.getRootElement();
770
-
771
- if (rootElement === null) {
772
- return [];
773
- }
774
-
775
- const rootRect = rootElement.getBoundingClientRect();
776
- const computedStyle = getComputedStyle(rootElement);
777
- const rootPadding = parseFloat(computedStyle.paddingLeft) + parseFloat(computedStyle.paddingRight);
778
- const selectionRects = Array.from(range.getClientRects());
779
- let selectionRectsLength = selectionRects.length;
780
- let prevRect;
781
-
782
- for (let i = 0; i < selectionRectsLength; i++) {
783
- const selectionRect = selectionRects[i]; // Exclude a rect that is the exact same as the last rect. getClientRects() can return
784
- // the same rect twice for some elements. A more sophisticated thing to do here is to
785
- // merge all the rects together into a set of rects that don't overlap, so we don't
786
- // 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);
787
713
 
788
- 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
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;
789
722
 
790
- const selectionSpansElement = selectionRect.width + rootPadding === rootRect.width;
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
+ }
791
735
 
792
- if (isDuplicateRect || selectionSpansElement) {
793
- selectionRects.splice(i--, 1);
794
- selectionRectsLength--;
795
- continue;
736
+ textNode.__text = textNode.__text.slice(startOffset, endOffset);
737
+ return textNode;
796
738
  }
739
+ }
797
740
 
798
- prevRect = selectionRect;
741
+ return textNode;
742
+ }
743
+ function $isAtNodeEnd(point) {
744
+ if (point.type === 'text') {
745
+ return point.offset === point.getNode().getTextContentSize();
799
746
  }
800
747
 
801
- return selectionRects;
748
+ return point.offset === point.getNode().getChildrenSize();
802
749
  }
803
750
  function trimTextContentFromAnchor(editor, anchor, delCount) {
804
751
  // Work from the current selection anchor point
@@ -923,41 +870,159 @@ function trimTextContentFromAnchor(editor, anchor, delCount) {
923
870
  }
924
871
  }
925
872
  }
926
- function $sliceSelectedTextNodeContent(selection, textNode) {
927
- if (textNode.isSelected() && !textNode.isSegmented() && !textNode.isToken() && (lexical.$isRangeSelection(selection) || lexical.DEPRECATED_$isGridSelection(selection))) {
928
- const anchorNode = selection.anchor.getNode();
929
- const focusNode = selection.focus.getNode();
930
- const isAnchor = textNode.is(anchorNode);
931
- const isFocus = textNode.is(focusNode);
932
873
 
933
- if (isAnchor || isFocus) {
934
- const isBackward = selection.isBackward();
935
- const [anchorOffset, focusOffset] = selection.getCharacterOffsets();
936
- const isSame = anchorNode.is(focusNode);
937
- const isFirst = textNode.is(isBackward ? focusNode : anchorNode);
938
- const isLast = textNode.is(isBackward ? anchorNode : focusNode);
939
- let startOffset = 0;
940
- let endOffset = undefined;
874
+ function errGetLatestOnClone() {
875
+ {
876
+ throw Error(`getLatest() on clone node`);
877
+ }
878
+ }
941
879
 
942
- if (isSame) {
943
- startOffset = anchorOffset > focusOffset ? focusOffset : anchorOffset;
944
- endOffset = anchorOffset > focusOffset ? anchorOffset : focusOffset;
945
- } else if (isFirst) {
946
- const offset = isBackward ? focusOffset : anchorOffset;
947
- startOffset = offset;
948
- endOffset = undefined;
949
- } else if (isLast) {
950
- const offset = isBackward ? anchorOffset : focusOffset;
951
- startOffset = 0;
952
- endOffset = offset;
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;
953
902
  }
954
903
 
955
- textNode.__text = textNode.__text.slice(startOffset, endOffset);
956
- return textNode;
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
+ }));
957
912
  }
958
913
  }
959
914
 
960
- return textNode;
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) && startOffset < firstNode.getTextContentSize()) {
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
+ }
961
1026
  }
962
1027
 
963
1028
  exports.$addNodeStyle = $addNodeStyle;