@lexical/selection 0.13.1 → 0.14.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.
- package/LexicalSelection.dev.esm.js +971 -0
- package/LexicalSelection.esm.js +26 -0
- package/LexicalSelection.js +1 -1
- package/LexicalSelection.prod.esm.js +7 -0
- package/README.md +2 -109
- package/package.json +5 -3
|
@@ -0,0 +1,971 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
import { $isTextNode, $isElementNode, $getCharacterOffsets, $isRootNode, $getNodeByKey, $getPreviousSelection, $createTextNode, $isRangeSelection, $getRoot, $isRootOrShadowRoot, $hasAncestor, $isLeafNode, $setSelection, $getAdjacentNode, $isDecoratorNode, $isLineBreakNode } from 'lexical';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
11
|
+
*
|
|
12
|
+
* This source code is licensed under the MIT license found in the
|
|
13
|
+
* LICENSE file in the root directory of this source tree.
|
|
14
|
+
*
|
|
15
|
+
*/
|
|
16
|
+
const CSS_TO_STYLES = new Map();
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
20
|
+
*
|
|
21
|
+
* This source code is licensed under the MIT license found in the
|
|
22
|
+
* LICENSE file in the root directory of this source tree.
|
|
23
|
+
*
|
|
24
|
+
*/
|
|
25
|
+
function getDOMTextNode(element) {
|
|
26
|
+
let node = element;
|
|
27
|
+
while (node != null) {
|
|
28
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
29
|
+
return node;
|
|
30
|
+
}
|
|
31
|
+
node = node.firstChild;
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
function getDOMIndexWithinParent(node) {
|
|
36
|
+
const parent = node.parentNode;
|
|
37
|
+
if (parent == null) {
|
|
38
|
+
throw new Error('Should never happen');
|
|
39
|
+
}
|
|
40
|
+
return [parent, Array.from(parent.childNodes).indexOf(node)];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Creates a selection range for the DOM.
|
|
45
|
+
* @param editor - The lexical editor.
|
|
46
|
+
* @param anchorNode - The anchor node of a selection.
|
|
47
|
+
* @param _anchorOffset - The amount of space offset from the anchor to the focus.
|
|
48
|
+
* @param focusNode - The current focus.
|
|
49
|
+
* @param _focusOffset - The amount of space offset from the focus to the anchor.
|
|
50
|
+
* @returns The range of selection for the DOM that was created.
|
|
51
|
+
*/
|
|
52
|
+
function createDOMRange(editor, anchorNode, _anchorOffset, focusNode, _focusOffset) {
|
|
53
|
+
const anchorKey = anchorNode.getKey();
|
|
54
|
+
const focusKey = focusNode.getKey();
|
|
55
|
+
const range = document.createRange();
|
|
56
|
+
let anchorDOM = editor.getElementByKey(anchorKey);
|
|
57
|
+
let focusDOM = editor.getElementByKey(focusKey);
|
|
58
|
+
let anchorOffset = _anchorOffset;
|
|
59
|
+
let focusOffset = _focusOffset;
|
|
60
|
+
if ($isTextNode(anchorNode)) {
|
|
61
|
+
anchorDOM = getDOMTextNode(anchorDOM);
|
|
62
|
+
}
|
|
63
|
+
if ($isTextNode(focusNode)) {
|
|
64
|
+
focusDOM = getDOMTextNode(focusDOM);
|
|
65
|
+
}
|
|
66
|
+
if (anchorNode === undefined || focusNode === undefined || anchorDOM === null || focusDOM === null) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
if (anchorDOM.nodeName === 'BR') {
|
|
70
|
+
[anchorDOM, anchorOffset] = getDOMIndexWithinParent(anchorDOM);
|
|
71
|
+
}
|
|
72
|
+
if (focusDOM.nodeName === 'BR') {
|
|
73
|
+
[focusDOM, focusOffset] = getDOMIndexWithinParent(focusDOM);
|
|
74
|
+
}
|
|
75
|
+
const firstChild = anchorDOM.firstChild;
|
|
76
|
+
if (anchorDOM === focusDOM && firstChild != null && firstChild.nodeName === 'BR' && anchorOffset === 0 && focusOffset === 0) {
|
|
77
|
+
focusOffset = 1;
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
range.setStart(anchorDOM, anchorOffset);
|
|
81
|
+
range.setEnd(focusDOM, focusOffset);
|
|
82
|
+
} catch (e) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
if (range.collapsed && (anchorOffset !== focusOffset || anchorKey !== focusKey)) {
|
|
86
|
+
// Range is backwards, we need to reverse it
|
|
87
|
+
range.setStart(focusDOM, focusOffset);
|
|
88
|
+
range.setEnd(anchorDOM, anchorOffset);
|
|
89
|
+
}
|
|
90
|
+
return range;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Creates DOMRects, generally used to help the editor find a specific location on the screen.
|
|
95
|
+
* @param editor - The lexical editor
|
|
96
|
+
* @param range - A fragment of a document that can contain nodes and parts of text nodes.
|
|
97
|
+
* @returns The selectionRects as an array.
|
|
98
|
+
*/
|
|
99
|
+
function createRectsFromDOMRange(editor, range) {
|
|
100
|
+
const rootElement = editor.getRootElement();
|
|
101
|
+
if (rootElement === null) {
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
const rootRect = rootElement.getBoundingClientRect();
|
|
105
|
+
const computedStyle = getComputedStyle(rootElement);
|
|
106
|
+
const rootPadding = parseFloat(computedStyle.paddingLeft) + parseFloat(computedStyle.paddingRight);
|
|
107
|
+
const selectionRects = Array.from(range.getClientRects());
|
|
108
|
+
let selectionRectsLength = selectionRects.length;
|
|
109
|
+
//sort rects from top left to bottom right.
|
|
110
|
+
selectionRects.sort((a, b) => {
|
|
111
|
+
const top = a.top - b.top;
|
|
112
|
+
// Some rects match position closely, but not perfectly,
|
|
113
|
+
// so we give a 3px tolerance.
|
|
114
|
+
if (Math.abs(top) <= 3) {
|
|
115
|
+
return a.left - b.left;
|
|
116
|
+
}
|
|
117
|
+
return top;
|
|
118
|
+
});
|
|
119
|
+
let prevRect;
|
|
120
|
+
for (let i = 0; i < selectionRectsLength; i++) {
|
|
121
|
+
const selectionRect = selectionRects[i];
|
|
122
|
+
// Exclude rects that overlap preceding Rects in the sorted list.
|
|
123
|
+
const isOverlappingRect = prevRect && prevRect.top <= selectionRect.top && prevRect.top + prevRect.height > selectionRect.top && prevRect.left + prevRect.width > selectionRect.left;
|
|
124
|
+
// Exclude selections that span the entire element
|
|
125
|
+
const selectionSpansElement = selectionRect.width + rootPadding === rootRect.width;
|
|
126
|
+
if (isOverlappingRect || selectionSpansElement) {
|
|
127
|
+
selectionRects.splice(i--, 1);
|
|
128
|
+
selectionRectsLength--;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
prevRect = selectionRect;
|
|
132
|
+
}
|
|
133
|
+
return selectionRects;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Creates an object containing all the styles and their values provided in the CSS string.
|
|
138
|
+
* @param css - The CSS string of styles and their values.
|
|
139
|
+
* @returns The styleObject containing all the styles and their values.
|
|
140
|
+
*/
|
|
141
|
+
function getStyleObjectFromRawCSS(css) {
|
|
142
|
+
const styleObject = {};
|
|
143
|
+
const styles = css.split(';');
|
|
144
|
+
for (const style of styles) {
|
|
145
|
+
if (style !== '') {
|
|
146
|
+
const [key, value] = style.split(/:([^]+)/); // split on first colon
|
|
147
|
+
if (key && value) {
|
|
148
|
+
styleObject[key.trim()] = value.trim();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return styleObject;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Given a CSS string, returns an object from the style cache.
|
|
157
|
+
* @param css - The CSS property as a string.
|
|
158
|
+
* @returns The value of the given CSS property.
|
|
159
|
+
*/
|
|
160
|
+
function getStyleObjectFromCSS(css) {
|
|
161
|
+
let value = CSS_TO_STYLES.get(css);
|
|
162
|
+
if (value === undefined) {
|
|
163
|
+
value = getStyleObjectFromRawCSS(css);
|
|
164
|
+
CSS_TO_STYLES.set(css, value);
|
|
165
|
+
}
|
|
166
|
+
{
|
|
167
|
+
// Freeze the value in DEV to prevent accidental mutations
|
|
168
|
+
Object.freeze(value);
|
|
169
|
+
}
|
|
170
|
+
return value;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Gets the CSS styles from the style object.
|
|
175
|
+
* @param styles - The style object containing the styles to get.
|
|
176
|
+
* @returns A string containing the CSS styles and their values.
|
|
177
|
+
*/
|
|
178
|
+
function getCSSFromStyleObject(styles) {
|
|
179
|
+
let css = '';
|
|
180
|
+
for (const style in styles) {
|
|
181
|
+
if (style) {
|
|
182
|
+
css += `${style}: ${styles[style]};`;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return css;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
190
|
+
*
|
|
191
|
+
* This source code is licensed under the MIT license found in the
|
|
192
|
+
* LICENSE file in the root directory of this source tree.
|
|
193
|
+
*
|
|
194
|
+
*/
|
|
195
|
+
function $updateElementNodeProperties(target, source) {
|
|
196
|
+
target.__first = source.__first;
|
|
197
|
+
target.__last = source.__last;
|
|
198
|
+
target.__size = source.__size;
|
|
199
|
+
target.__format = source.__format;
|
|
200
|
+
target.__indent = source.__indent;
|
|
201
|
+
target.__dir = source.__dir;
|
|
202
|
+
return target;
|
|
203
|
+
}
|
|
204
|
+
function $updateTextNodeProperties(target, source) {
|
|
205
|
+
target.__format = source.__format;
|
|
206
|
+
target.__style = source.__style;
|
|
207
|
+
target.__mode = source.__mode;
|
|
208
|
+
target.__detail = source.__detail;
|
|
209
|
+
return target;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Returns a copy of a node, but generates a new key for the copy.
|
|
214
|
+
* @param node - The node to be cloned.
|
|
215
|
+
* @returns The clone of the node.
|
|
216
|
+
*/
|
|
217
|
+
function $cloneWithProperties(node) {
|
|
218
|
+
const constructor = node.constructor;
|
|
219
|
+
// @ts-expect-error
|
|
220
|
+
const clone = constructor.clone(node);
|
|
221
|
+
clone.__parent = node.__parent;
|
|
222
|
+
clone.__next = node.__next;
|
|
223
|
+
clone.__prev = node.__prev;
|
|
224
|
+
if ($isElementNode(node) && $isElementNode(clone)) {
|
|
225
|
+
return $updateElementNodeProperties(clone, node);
|
|
226
|
+
}
|
|
227
|
+
if ($isTextNode(node) && $isTextNode(clone)) {
|
|
228
|
+
return $updateTextNodeProperties(clone, node);
|
|
229
|
+
}
|
|
230
|
+
return clone;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Generally used to append text content to HTML and JSON. Grabs the text content and "slices"
|
|
235
|
+
* it to be generated into the new TextNode.
|
|
236
|
+
* @param selection - The selection containing the node whose TextNode is to be edited.
|
|
237
|
+
* @param textNode - The TextNode to be edited.
|
|
238
|
+
* @returns The updated TextNode.
|
|
239
|
+
*/
|
|
240
|
+
function $sliceSelectedTextNodeContent(selection, textNode) {
|
|
241
|
+
const anchorAndFocus = selection.getStartEndPoints();
|
|
242
|
+
if (textNode.isSelected(selection) && !textNode.isSegmented() && !textNode.isToken() && anchorAndFocus !== null) {
|
|
243
|
+
const [anchor, focus] = anchorAndFocus;
|
|
244
|
+
const isBackward = selection.isBackward();
|
|
245
|
+
const anchorNode = anchor.getNode();
|
|
246
|
+
const focusNode = focus.getNode();
|
|
247
|
+
const isAnchor = textNode.is(anchorNode);
|
|
248
|
+
const isFocus = textNode.is(focusNode);
|
|
249
|
+
if (isAnchor || isFocus) {
|
|
250
|
+
const [anchorOffset, focusOffset] = $getCharacterOffsets(selection);
|
|
251
|
+
const isSame = anchorNode.is(focusNode);
|
|
252
|
+
const isFirst = textNode.is(isBackward ? focusNode : anchorNode);
|
|
253
|
+
const isLast = textNode.is(isBackward ? anchorNode : focusNode);
|
|
254
|
+
let startOffset = 0;
|
|
255
|
+
let endOffset = undefined;
|
|
256
|
+
if (isSame) {
|
|
257
|
+
startOffset = anchorOffset > focusOffset ? focusOffset : anchorOffset;
|
|
258
|
+
endOffset = anchorOffset > focusOffset ? anchorOffset : focusOffset;
|
|
259
|
+
} else if (isFirst) {
|
|
260
|
+
const offset = isBackward ? focusOffset : anchorOffset;
|
|
261
|
+
startOffset = offset;
|
|
262
|
+
endOffset = undefined;
|
|
263
|
+
} else if (isLast) {
|
|
264
|
+
const offset = isBackward ? anchorOffset : focusOffset;
|
|
265
|
+
startOffset = 0;
|
|
266
|
+
endOffset = offset;
|
|
267
|
+
}
|
|
268
|
+
textNode.__text = textNode.__text.slice(startOffset, endOffset);
|
|
269
|
+
return textNode;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return textNode;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Determines if the current selection is at the end of the node.
|
|
277
|
+
* @param point - The point of the selection to test.
|
|
278
|
+
* @returns true if the provided point offset is in the last possible position, false otherwise.
|
|
279
|
+
*/
|
|
280
|
+
function $isAtNodeEnd(point) {
|
|
281
|
+
if (point.type === 'text') {
|
|
282
|
+
return point.offset === point.getNode().getTextContentSize();
|
|
283
|
+
}
|
|
284
|
+
const node = point.getNode();
|
|
285
|
+
if (!$isElementNode(node)) {
|
|
286
|
+
throw Error(`isAtNodeEnd: node must be a TextNode or ElementNode`);
|
|
287
|
+
}
|
|
288
|
+
return point.offset === node.getChildrenSize();
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Trims text from a node in order to shorten it, eg. to enforce a text's max length. If it deletes text
|
|
293
|
+
* that is an ancestor of the anchor then it will leave 2 indents, otherwise, if no text content exists, it deletes
|
|
294
|
+
* the TextNode. It will move the focus to either the end of any left over text or beginning of a new TextNode.
|
|
295
|
+
* @param editor - The lexical editor.
|
|
296
|
+
* @param anchor - The anchor of the current selection, where the selection should be pointing.
|
|
297
|
+
* @param delCount - The amount of characters to delete. Useful as a dynamic variable eg. textContentSize - maxLength;
|
|
298
|
+
*/
|
|
299
|
+
function trimTextContentFromAnchor(editor, anchor, delCount) {
|
|
300
|
+
// Work from the current selection anchor point
|
|
301
|
+
let currentNode = anchor.getNode();
|
|
302
|
+
let remaining = delCount;
|
|
303
|
+
if ($isElementNode(currentNode)) {
|
|
304
|
+
const descendantNode = currentNode.getDescendantByIndex(anchor.offset);
|
|
305
|
+
if (descendantNode !== null) {
|
|
306
|
+
currentNode = descendantNode;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
while (remaining > 0 && currentNode !== null) {
|
|
310
|
+
if ($isElementNode(currentNode)) {
|
|
311
|
+
const lastDescendant = currentNode.getLastDescendant();
|
|
312
|
+
if (lastDescendant !== null) {
|
|
313
|
+
currentNode = lastDescendant;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
let nextNode = currentNode.getPreviousSibling();
|
|
317
|
+
let additionalElementWhitespace = 0;
|
|
318
|
+
if (nextNode === null) {
|
|
319
|
+
let parent = currentNode.getParentOrThrow();
|
|
320
|
+
let parentSibling = parent.getPreviousSibling();
|
|
321
|
+
while (parentSibling === null) {
|
|
322
|
+
parent = parent.getParent();
|
|
323
|
+
if (parent === null) {
|
|
324
|
+
nextNode = null;
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
parentSibling = parent.getPreviousSibling();
|
|
328
|
+
}
|
|
329
|
+
if (parent !== null) {
|
|
330
|
+
additionalElementWhitespace = parent.isInline() ? 0 : 2;
|
|
331
|
+
nextNode = parentSibling;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
let text = currentNode.getTextContent();
|
|
335
|
+
// If the text is empty, we need to consider adding in two line breaks to match
|
|
336
|
+
// the content if we were to get it from its parent.
|
|
337
|
+
if (text === '' && $isElementNode(currentNode) && !currentNode.isInline()) {
|
|
338
|
+
// TODO: should this be handled in core?
|
|
339
|
+
text = '\n\n';
|
|
340
|
+
}
|
|
341
|
+
const currentNodeSize = text.length;
|
|
342
|
+
if (!$isTextNode(currentNode) || remaining >= currentNodeSize) {
|
|
343
|
+
const parent = currentNode.getParent();
|
|
344
|
+
currentNode.remove();
|
|
345
|
+
if (parent != null && parent.getChildrenSize() === 0 && !$isRootNode(parent)) {
|
|
346
|
+
parent.remove();
|
|
347
|
+
}
|
|
348
|
+
remaining -= currentNodeSize + additionalElementWhitespace;
|
|
349
|
+
currentNode = nextNode;
|
|
350
|
+
} else {
|
|
351
|
+
const key = currentNode.getKey();
|
|
352
|
+
// See if we can just revert it to what was in the last editor state
|
|
353
|
+
const prevTextContent = editor.getEditorState().read(() => {
|
|
354
|
+
const prevNode = $getNodeByKey(key);
|
|
355
|
+
if ($isTextNode(prevNode) && prevNode.isSimpleText()) {
|
|
356
|
+
return prevNode.getTextContent();
|
|
357
|
+
}
|
|
358
|
+
return null;
|
|
359
|
+
});
|
|
360
|
+
const offset = currentNodeSize - remaining;
|
|
361
|
+
const slicedText = text.slice(0, offset);
|
|
362
|
+
if (prevTextContent !== null && prevTextContent !== text) {
|
|
363
|
+
const prevSelection = $getPreviousSelection();
|
|
364
|
+
let target = currentNode;
|
|
365
|
+
if (!currentNode.isSimpleText()) {
|
|
366
|
+
const textNode = $createTextNode(prevTextContent);
|
|
367
|
+
currentNode.replace(textNode);
|
|
368
|
+
target = textNode;
|
|
369
|
+
} else {
|
|
370
|
+
currentNode.setTextContent(prevTextContent);
|
|
371
|
+
}
|
|
372
|
+
if ($isRangeSelection(prevSelection) && prevSelection.isCollapsed()) {
|
|
373
|
+
const prevOffset = prevSelection.anchor.offset;
|
|
374
|
+
target.select(prevOffset, prevOffset);
|
|
375
|
+
}
|
|
376
|
+
} else if (currentNode.isSimpleText()) {
|
|
377
|
+
// Split text
|
|
378
|
+
const isSelected = anchor.key === key;
|
|
379
|
+
let anchorOffset = anchor.offset;
|
|
380
|
+
// Move offset to end if it's less than the remaining number, otherwise
|
|
381
|
+
// we'll have a negative splitStart.
|
|
382
|
+
if (anchorOffset < remaining) {
|
|
383
|
+
anchorOffset = currentNodeSize;
|
|
384
|
+
}
|
|
385
|
+
const splitStart = isSelected ? anchorOffset - remaining : 0;
|
|
386
|
+
const splitEnd = isSelected ? anchorOffset : offset;
|
|
387
|
+
if (isSelected && splitStart === 0) {
|
|
388
|
+
const [excessNode] = currentNode.splitText(splitStart, splitEnd);
|
|
389
|
+
excessNode.remove();
|
|
390
|
+
} else {
|
|
391
|
+
const [, excessNode] = currentNode.splitText(splitStart, splitEnd);
|
|
392
|
+
excessNode.remove();
|
|
393
|
+
}
|
|
394
|
+
} else {
|
|
395
|
+
const textNode = $createTextNode(slicedText);
|
|
396
|
+
currentNode.replace(textNode);
|
|
397
|
+
}
|
|
398
|
+
remaining = 0;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Gets the TextNode's style object and adds the styles to the CSS.
|
|
405
|
+
* @param node - The TextNode to add styles to.
|
|
406
|
+
*/
|
|
407
|
+
function $addNodeStyle(node) {
|
|
408
|
+
const CSSText = node.getStyle();
|
|
409
|
+
const styles = getStyleObjectFromRawCSS(CSSText);
|
|
410
|
+
CSS_TO_STYLES.set(CSSText, styles);
|
|
411
|
+
}
|
|
412
|
+
function $patchStyle(target, patch) {
|
|
413
|
+
const prevStyles = getStyleObjectFromCSS('getStyle' in target ? target.getStyle() : target.style);
|
|
414
|
+
const newStyles = Object.entries(patch).reduce((styles, [key, value]) => {
|
|
415
|
+
if (value instanceof Function) {
|
|
416
|
+
styles[key] = value(prevStyles[key]);
|
|
417
|
+
} else if (value === null) {
|
|
418
|
+
delete styles[key];
|
|
419
|
+
} else {
|
|
420
|
+
styles[key] = value;
|
|
421
|
+
}
|
|
422
|
+
return styles;
|
|
423
|
+
}, {
|
|
424
|
+
...prevStyles
|
|
425
|
+
} || {});
|
|
426
|
+
const newCSSText = getCSSFromStyleObject(newStyles);
|
|
427
|
+
target.setStyle(newCSSText);
|
|
428
|
+
CSS_TO_STYLES.set(newCSSText, newStyles);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Applies the provided styles to the TextNodes in the provided Selection.
|
|
433
|
+
* Will update partially selected TextNodes by splitting the TextNode and applying
|
|
434
|
+
* the styles to the appropriate one.
|
|
435
|
+
* @param selection - The selected node(s) to update.
|
|
436
|
+
* @param patch - The patch to apply, which can include multiple styles. { CSSProperty: value }. Can also accept a function that returns the new property value.
|
|
437
|
+
*/
|
|
438
|
+
function $patchStyleText(selection, patch) {
|
|
439
|
+
const selectedNodes = selection.getNodes();
|
|
440
|
+
const selectedNodesLength = selectedNodes.length;
|
|
441
|
+
const anchorAndFocus = selection.getStartEndPoints();
|
|
442
|
+
if (anchorAndFocus === null) {
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
const [anchor, focus] = anchorAndFocus;
|
|
446
|
+
const lastIndex = selectedNodesLength - 1;
|
|
447
|
+
let firstNode = selectedNodes[0];
|
|
448
|
+
let lastNode = selectedNodes[lastIndex];
|
|
449
|
+
if (selection.isCollapsed() && $isRangeSelection(selection)) {
|
|
450
|
+
$patchStyle(selection, patch);
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
const firstNodeText = firstNode.getTextContent();
|
|
454
|
+
const firstNodeTextLength = firstNodeText.length;
|
|
455
|
+
const focusOffset = focus.offset;
|
|
456
|
+
let anchorOffset = anchor.offset;
|
|
457
|
+
const isBefore = anchor.isBefore(focus);
|
|
458
|
+
let startOffset = isBefore ? anchorOffset : focusOffset;
|
|
459
|
+
let endOffset = isBefore ? focusOffset : anchorOffset;
|
|
460
|
+
const startType = isBefore ? anchor.type : focus.type;
|
|
461
|
+
const endType = isBefore ? focus.type : anchor.type;
|
|
462
|
+
const endKey = isBefore ? focus.key : anchor.key;
|
|
463
|
+
|
|
464
|
+
// This is the case where the user only selected the very end of the
|
|
465
|
+
// first node so we don't want to include it in the formatting change.
|
|
466
|
+
if ($isTextNode(firstNode) && startOffset === firstNodeTextLength) {
|
|
467
|
+
const nextSibling = firstNode.getNextSibling();
|
|
468
|
+
if ($isTextNode(nextSibling)) {
|
|
469
|
+
// we basically make the second node the firstNode, changing offsets accordingly
|
|
470
|
+
anchorOffset = 0;
|
|
471
|
+
startOffset = 0;
|
|
472
|
+
firstNode = nextSibling;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// This is the case where we only selected a single node
|
|
477
|
+
if (selectedNodes.length === 1) {
|
|
478
|
+
if ($isTextNode(firstNode) && firstNode.canHaveFormat()) {
|
|
479
|
+
startOffset = startType === 'element' ? 0 : anchorOffset > focusOffset ? focusOffset : anchorOffset;
|
|
480
|
+
endOffset = endType === 'element' ? firstNodeTextLength : anchorOffset > focusOffset ? anchorOffset : focusOffset;
|
|
481
|
+
|
|
482
|
+
// No actual text is selected, so do nothing.
|
|
483
|
+
if (startOffset === endOffset) {
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// The entire node is selected, so just format it
|
|
488
|
+
if (startOffset === 0 && endOffset === firstNodeTextLength) {
|
|
489
|
+
$patchStyle(firstNode, patch);
|
|
490
|
+
firstNode.select(startOffset, endOffset);
|
|
491
|
+
} else {
|
|
492
|
+
// The node is partially selected, so split it into two nodes
|
|
493
|
+
// and style the selected one.
|
|
494
|
+
const splitNodes = firstNode.splitText(startOffset, endOffset);
|
|
495
|
+
const replacement = startOffset === 0 ? splitNodes[0] : splitNodes[1];
|
|
496
|
+
$patchStyle(replacement, patch);
|
|
497
|
+
replacement.select(0, endOffset - startOffset);
|
|
498
|
+
}
|
|
499
|
+
} // multiple nodes selected.
|
|
500
|
+
} else {
|
|
501
|
+
if ($isTextNode(firstNode) && startOffset < firstNode.getTextContentSize() && firstNode.canHaveFormat()) {
|
|
502
|
+
if (startOffset !== 0) {
|
|
503
|
+
// the entire first node isn't selected, so split it
|
|
504
|
+
firstNode = firstNode.splitText(startOffset)[1];
|
|
505
|
+
startOffset = 0;
|
|
506
|
+
anchor.set(firstNode.getKey(), startOffset, 'text');
|
|
507
|
+
}
|
|
508
|
+
$patchStyle(firstNode, patch);
|
|
509
|
+
}
|
|
510
|
+
if ($isTextNode(lastNode) && lastNode.canHaveFormat()) {
|
|
511
|
+
const lastNodeText = lastNode.getTextContent();
|
|
512
|
+
const lastNodeTextLength = lastNodeText.length;
|
|
513
|
+
|
|
514
|
+
// The last node might not actually be the end node
|
|
515
|
+
//
|
|
516
|
+
// If not, assume the last node is fully-selected unless the end offset is
|
|
517
|
+
// zero.
|
|
518
|
+
if (lastNode.__key !== endKey && endOffset !== 0) {
|
|
519
|
+
endOffset = lastNodeTextLength;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// if the entire last node isn't selected, split it
|
|
523
|
+
if (endOffset !== lastNodeTextLength) {
|
|
524
|
+
[lastNode] = lastNode.splitText(endOffset);
|
|
525
|
+
}
|
|
526
|
+
if (endOffset !== 0 || endType === 'element') {
|
|
527
|
+
$patchStyle(lastNode, patch);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// style all the text nodes in between
|
|
532
|
+
for (let i = 1; i < lastIndex; i++) {
|
|
533
|
+
const selectedNode = selectedNodes[i];
|
|
534
|
+
const selectedNodeKey = selectedNode.getKey();
|
|
535
|
+
if ($isTextNode(selectedNode) && selectedNode.canHaveFormat() && selectedNodeKey !== firstNode.getKey() && selectedNodeKey !== lastNode.getKey() && !selectedNode.isToken()) {
|
|
536
|
+
$patchStyle(selectedNode, patch);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
544
|
+
*
|
|
545
|
+
* This source code is licensed under the MIT license found in the
|
|
546
|
+
* LICENSE file in the root directory of this source tree.
|
|
547
|
+
*
|
|
548
|
+
*/
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Converts all nodes in the selection that are of one block type to another.
|
|
552
|
+
* @param selection - The selected blocks to be converted.
|
|
553
|
+
* @param createElement - The function that creates the node. eg. $createParagraphNode.
|
|
554
|
+
*/
|
|
555
|
+
function $setBlocksType(selection, createElement) {
|
|
556
|
+
if (selection === null) {
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
const anchorAndFocus = selection.getStartEndPoints();
|
|
560
|
+
const anchor = anchorAndFocus ? anchorAndFocus[0] : null;
|
|
561
|
+
if (anchor !== null && anchor.key === 'root') {
|
|
562
|
+
const element = createElement();
|
|
563
|
+
const root = $getRoot();
|
|
564
|
+
const firstChild = root.getFirstChild();
|
|
565
|
+
if (firstChild) {
|
|
566
|
+
firstChild.replace(element, true);
|
|
567
|
+
} else {
|
|
568
|
+
root.append(element);
|
|
569
|
+
}
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
const nodes = selection.getNodes();
|
|
573
|
+
const firstSelectedBlock = anchor !== null ? $getAncestor(anchor.getNode(), INTERNAL_$isBlock) : false;
|
|
574
|
+
if (firstSelectedBlock && nodes.indexOf(firstSelectedBlock) === -1) {
|
|
575
|
+
nodes.push(firstSelectedBlock);
|
|
576
|
+
}
|
|
577
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
578
|
+
const node = nodes[i];
|
|
579
|
+
if (!INTERNAL_$isBlock(node)) {
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
if (!$isElementNode(node)) {
|
|
583
|
+
throw Error(`Expected block node to be an ElementNode`);
|
|
584
|
+
}
|
|
585
|
+
const targetElement = createElement();
|
|
586
|
+
targetElement.setFormat(node.getFormatType());
|
|
587
|
+
targetElement.setIndent(node.getIndent());
|
|
588
|
+
node.replace(targetElement, true);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
function isPointAttached(point) {
|
|
592
|
+
return point.getNode().isAttached();
|
|
593
|
+
}
|
|
594
|
+
function $removeParentEmptyElements(startingNode) {
|
|
595
|
+
let node = startingNode;
|
|
596
|
+
while (node !== null && !$isRootOrShadowRoot(node)) {
|
|
597
|
+
const latest = node.getLatest();
|
|
598
|
+
const parentNode = node.getParent();
|
|
599
|
+
if (latest.getChildrenSize() === 0) {
|
|
600
|
+
node.remove(true);
|
|
601
|
+
}
|
|
602
|
+
node = parentNode;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* @deprecated
|
|
608
|
+
* Wraps all nodes in the selection into another node of the type returned by createElement.
|
|
609
|
+
* @param selection - The selection of nodes to be wrapped.
|
|
610
|
+
* @param createElement - A function that creates the wrapping ElementNode. eg. $createParagraphNode.
|
|
611
|
+
* @param wrappingElement - An element to append the wrapped selection and its children to.
|
|
612
|
+
*/
|
|
613
|
+
function $wrapNodes(selection, createElement, wrappingElement = null) {
|
|
614
|
+
const anchorAndFocus = selection.getStartEndPoints();
|
|
615
|
+
const anchor = anchorAndFocus ? anchorAndFocus[0] : null;
|
|
616
|
+
const nodes = selection.getNodes();
|
|
617
|
+
const nodesLength = nodes.length;
|
|
618
|
+
if (anchor !== null && (nodesLength === 0 || nodesLength === 1 && anchor.type === 'element' && anchor.getNode().getChildrenSize() === 0)) {
|
|
619
|
+
const target = anchor.type === 'text' ? anchor.getNode().getParentOrThrow() : anchor.getNode();
|
|
620
|
+
const children = target.getChildren();
|
|
621
|
+
let element = createElement();
|
|
622
|
+
element.setFormat(target.getFormatType());
|
|
623
|
+
element.setIndent(target.getIndent());
|
|
624
|
+
children.forEach(child => element.append(child));
|
|
625
|
+
if (wrappingElement) {
|
|
626
|
+
element = wrappingElement.append(element);
|
|
627
|
+
}
|
|
628
|
+
target.replace(element);
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
let topLevelNode = null;
|
|
632
|
+
let descendants = [];
|
|
633
|
+
for (let i = 0; i < nodesLength; i++) {
|
|
634
|
+
const node = nodes[i];
|
|
635
|
+
// Determine whether wrapping has to be broken down into multiple chunks. This can happen if the
|
|
636
|
+
// user selected multiple Root-like nodes that have to be treated separately as if they are
|
|
637
|
+
// their own branch. I.e. you don't want to wrap a whole table, but rather the contents of each
|
|
638
|
+
// of each of the cell nodes.
|
|
639
|
+
if ($isRootOrShadowRoot(node)) {
|
|
640
|
+
$wrapNodesImpl(selection, descendants, descendants.length, createElement, wrappingElement);
|
|
641
|
+
descendants = [];
|
|
642
|
+
topLevelNode = node;
|
|
643
|
+
} else if (topLevelNode === null || topLevelNode !== null && $hasAncestor(node, topLevelNode)) {
|
|
644
|
+
descendants.push(node);
|
|
645
|
+
} else {
|
|
646
|
+
$wrapNodesImpl(selection, descendants, descendants.length, createElement, wrappingElement);
|
|
647
|
+
descendants = [node];
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
$wrapNodesImpl(selection, descendants, descendants.length, createElement, wrappingElement);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Wraps each node into a new ElementNode.
|
|
655
|
+
* @param selection - The selection of nodes to wrap.
|
|
656
|
+
* @param nodes - An array of nodes, generally the descendants of the selection.
|
|
657
|
+
* @param nodesLength - The length of nodes.
|
|
658
|
+
* @param createElement - A function that creates the wrapping ElementNode. eg. $createParagraphNode.
|
|
659
|
+
* @param wrappingElement - An element to wrap all the nodes into.
|
|
660
|
+
* @returns
|
|
661
|
+
*/
|
|
662
|
+
function $wrapNodesImpl(selection, nodes, nodesLength, createElement, wrappingElement = null) {
|
|
663
|
+
if (nodes.length === 0) {
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
const firstNode = nodes[0];
|
|
667
|
+
const elementMapping = new Map();
|
|
668
|
+
const elements = [];
|
|
669
|
+
// The below logic is to find the right target for us to
|
|
670
|
+
// either insertAfter/insertBefore/append the corresponding
|
|
671
|
+
// elements to. This is made more complicated due to nested
|
|
672
|
+
// structures.
|
|
673
|
+
let target = $isElementNode(firstNode) ? firstNode : firstNode.getParentOrThrow();
|
|
674
|
+
if (target.isInline()) {
|
|
675
|
+
target = target.getParentOrThrow();
|
|
676
|
+
}
|
|
677
|
+
let targetIsPrevSibling = false;
|
|
678
|
+
while (target !== null) {
|
|
679
|
+
const prevSibling = target.getPreviousSibling();
|
|
680
|
+
if (prevSibling !== null) {
|
|
681
|
+
target = prevSibling;
|
|
682
|
+
targetIsPrevSibling = true;
|
|
683
|
+
break;
|
|
684
|
+
}
|
|
685
|
+
target = target.getParentOrThrow();
|
|
686
|
+
if ($isRootOrShadowRoot(target)) {
|
|
687
|
+
break;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
const emptyElements = new Set();
|
|
691
|
+
|
|
692
|
+
// Find any top level empty elements
|
|
693
|
+
for (let i = 0; i < nodesLength; i++) {
|
|
694
|
+
const node = nodes[i];
|
|
695
|
+
if ($isElementNode(node) && node.getChildrenSize() === 0) {
|
|
696
|
+
emptyElements.add(node.getKey());
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
const movedNodes = new Set();
|
|
700
|
+
|
|
701
|
+
// Move out all leaf nodes into our elements array.
|
|
702
|
+
// If we find a top level empty element, also move make
|
|
703
|
+
// an element for that.
|
|
704
|
+
for (let i = 0; i < nodesLength; i++) {
|
|
705
|
+
const node = nodes[i];
|
|
706
|
+
let parent = node.getParent();
|
|
707
|
+
if (parent !== null && parent.isInline()) {
|
|
708
|
+
parent = parent.getParent();
|
|
709
|
+
}
|
|
710
|
+
if (parent !== null && $isLeafNode(node) && !movedNodes.has(node.getKey())) {
|
|
711
|
+
const parentKey = parent.getKey();
|
|
712
|
+
if (elementMapping.get(parentKey) === undefined) {
|
|
713
|
+
const targetElement = createElement();
|
|
714
|
+
targetElement.setFormat(parent.getFormatType());
|
|
715
|
+
targetElement.setIndent(parent.getIndent());
|
|
716
|
+
elements.push(targetElement);
|
|
717
|
+
elementMapping.set(parentKey, targetElement);
|
|
718
|
+
// Move node and its siblings to the new
|
|
719
|
+
// element.
|
|
720
|
+
parent.getChildren().forEach(child => {
|
|
721
|
+
targetElement.append(child);
|
|
722
|
+
movedNodes.add(child.getKey());
|
|
723
|
+
if ($isElementNode(child)) {
|
|
724
|
+
// Skip nested leaf nodes if the parent has already been moved
|
|
725
|
+
child.getChildrenKeys().forEach(key => movedNodes.add(key));
|
|
726
|
+
}
|
|
727
|
+
});
|
|
728
|
+
$removeParentEmptyElements(parent);
|
|
729
|
+
}
|
|
730
|
+
} else if (emptyElements.has(node.getKey())) {
|
|
731
|
+
if (!$isElementNode(node)) {
|
|
732
|
+
throw Error(`Expected node in emptyElements to be an ElementNode`);
|
|
733
|
+
}
|
|
734
|
+
const targetElement = createElement();
|
|
735
|
+
targetElement.setFormat(node.getFormatType());
|
|
736
|
+
targetElement.setIndent(node.getIndent());
|
|
737
|
+
elements.push(targetElement);
|
|
738
|
+
node.remove(true);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
if (wrappingElement !== null) {
|
|
742
|
+
for (let i = 0; i < elements.length; i++) {
|
|
743
|
+
const element = elements[i];
|
|
744
|
+
wrappingElement.append(element);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
let lastElement = null;
|
|
748
|
+
|
|
749
|
+
// If our target is Root-like, let's see if we can re-adjust
|
|
750
|
+
// so that the target is the first child instead.
|
|
751
|
+
if ($isRootOrShadowRoot(target)) {
|
|
752
|
+
if (targetIsPrevSibling) {
|
|
753
|
+
if (wrappingElement !== null) {
|
|
754
|
+
target.insertAfter(wrappingElement);
|
|
755
|
+
} else {
|
|
756
|
+
for (let i = elements.length - 1; i >= 0; i--) {
|
|
757
|
+
const element = elements[i];
|
|
758
|
+
target.insertAfter(element);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
} else {
|
|
762
|
+
const firstChild = target.getFirstChild();
|
|
763
|
+
if ($isElementNode(firstChild)) {
|
|
764
|
+
target = firstChild;
|
|
765
|
+
}
|
|
766
|
+
if (firstChild === null) {
|
|
767
|
+
if (wrappingElement) {
|
|
768
|
+
target.append(wrappingElement);
|
|
769
|
+
} else {
|
|
770
|
+
for (let i = 0; i < elements.length; i++) {
|
|
771
|
+
const element = elements[i];
|
|
772
|
+
target.append(element);
|
|
773
|
+
lastElement = element;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
} else {
|
|
777
|
+
if (wrappingElement !== null) {
|
|
778
|
+
firstChild.insertBefore(wrappingElement);
|
|
779
|
+
} else {
|
|
780
|
+
for (let i = 0; i < elements.length; i++) {
|
|
781
|
+
const element = elements[i];
|
|
782
|
+
firstChild.insertBefore(element);
|
|
783
|
+
lastElement = element;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
} else {
|
|
789
|
+
if (wrappingElement) {
|
|
790
|
+
target.insertAfter(wrappingElement);
|
|
791
|
+
} else {
|
|
792
|
+
for (let i = elements.length - 1; i >= 0; i--) {
|
|
793
|
+
const element = elements[i];
|
|
794
|
+
target.insertAfter(element);
|
|
795
|
+
lastElement = element;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
const prevSelection = $getPreviousSelection();
|
|
800
|
+
if ($isRangeSelection(prevSelection) && isPointAttached(prevSelection.anchor) && isPointAttached(prevSelection.focus)) {
|
|
801
|
+
$setSelection(prevSelection.clone());
|
|
802
|
+
} else if (lastElement !== null) {
|
|
803
|
+
lastElement.selectEnd();
|
|
804
|
+
} else {
|
|
805
|
+
selection.dirty = true;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
/**
|
|
810
|
+
* Determines if the default character selection should be overridden. Used with DecoratorNodes
|
|
811
|
+
* @param selection - The selection whose default character selection may need to be overridden.
|
|
812
|
+
* @param isBackward - Is the selection backwards (the focus comes before the anchor)?
|
|
813
|
+
* @returns true if it should be overridden, false if not.
|
|
814
|
+
*/
|
|
815
|
+
function $shouldOverrideDefaultCharacterSelection(selection, isBackward) {
|
|
816
|
+
const possibleNode = $getAdjacentNode(selection.focus, isBackward);
|
|
817
|
+
return $isDecoratorNode(possibleNode) && !possibleNode.isIsolated() || $isElementNode(possibleNode) && !possibleNode.isInline() && !possibleNode.canBeEmpty();
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
/**
|
|
821
|
+
* Moves the selection according to the arguments.
|
|
822
|
+
* @param selection - The selected text or nodes.
|
|
823
|
+
* @param isHoldingShift - Is the shift key being held down during the operation.
|
|
824
|
+
* @param isBackward - Is the selection selected backwards (the focus comes before the anchor)?
|
|
825
|
+
* @param granularity - The distance to adjust the current selection.
|
|
826
|
+
*/
|
|
827
|
+
function $moveCaretSelection(selection, isHoldingShift, isBackward, granularity) {
|
|
828
|
+
selection.modify(isHoldingShift ? 'extend' : 'move', isBackward, granularity);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
/**
|
|
832
|
+
* Tests a parent element for right to left direction.
|
|
833
|
+
* @param selection - The selection whose parent is to be tested.
|
|
834
|
+
* @returns true if the selections' parent element has a direction of 'rtl' (right to left), false otherwise.
|
|
835
|
+
*/
|
|
836
|
+
function $isParentElementRTL(selection) {
|
|
837
|
+
const anchorNode = selection.anchor.getNode();
|
|
838
|
+
const parent = $isRootNode(anchorNode) ? anchorNode : anchorNode.getParentOrThrow();
|
|
839
|
+
return parent.getDirection() === 'rtl';
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/**
|
|
843
|
+
* Moves selection by character according to arguments.
|
|
844
|
+
* @param selection - The selection of the characters to move.
|
|
845
|
+
* @param isHoldingShift - Is the shift key being held down during the operation.
|
|
846
|
+
* @param isBackward - Is the selection backward (the focus comes before the anchor)?
|
|
847
|
+
*/
|
|
848
|
+
function $moveCharacter(selection, isHoldingShift, isBackward) {
|
|
849
|
+
const isRTL = $isParentElementRTL(selection);
|
|
850
|
+
$moveCaretSelection(selection, isHoldingShift, isBackward ? !isRTL : isRTL, 'character');
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
/**
|
|
854
|
+
* Expands the current Selection to cover all of the content in the editor.
|
|
855
|
+
* @param selection - The current selection.
|
|
856
|
+
*/
|
|
857
|
+
function $selectAll(selection) {
|
|
858
|
+
const anchor = selection.anchor;
|
|
859
|
+
const focus = selection.focus;
|
|
860
|
+
const anchorNode = anchor.getNode();
|
|
861
|
+
const topParent = anchorNode.getTopLevelElementOrThrow();
|
|
862
|
+
const root = topParent.getParentOrThrow();
|
|
863
|
+
let firstNode = root.getFirstDescendant();
|
|
864
|
+
let lastNode = root.getLastDescendant();
|
|
865
|
+
let firstType = 'element';
|
|
866
|
+
let lastType = 'element';
|
|
867
|
+
let lastOffset = 0;
|
|
868
|
+
if ($isTextNode(firstNode)) {
|
|
869
|
+
firstType = 'text';
|
|
870
|
+
} else if (!$isElementNode(firstNode) && firstNode !== null) {
|
|
871
|
+
firstNode = firstNode.getParentOrThrow();
|
|
872
|
+
}
|
|
873
|
+
if ($isTextNode(lastNode)) {
|
|
874
|
+
lastType = 'text';
|
|
875
|
+
lastOffset = lastNode.getTextContentSize();
|
|
876
|
+
} else if (!$isElementNode(lastNode) && lastNode !== null) {
|
|
877
|
+
lastNode = lastNode.getParentOrThrow();
|
|
878
|
+
}
|
|
879
|
+
if (firstNode && lastNode) {
|
|
880
|
+
anchor.set(firstNode.getKey(), 0, firstType);
|
|
881
|
+
focus.set(lastNode.getKey(), lastOffset, lastType);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
/**
|
|
886
|
+
* Returns the current value of a CSS property for Nodes, if set. If not set, it returns the defaultValue.
|
|
887
|
+
* @param node - The node whose style value to get.
|
|
888
|
+
* @param styleProperty - The CSS style property.
|
|
889
|
+
* @param defaultValue - The default value for the property.
|
|
890
|
+
* @returns The value of the property for node.
|
|
891
|
+
*/
|
|
892
|
+
function $getNodeStyleValueForProperty(node, styleProperty, defaultValue) {
|
|
893
|
+
const css = node.getStyle();
|
|
894
|
+
const styleObject = getStyleObjectFromCSS(css);
|
|
895
|
+
if (styleObject !== null) {
|
|
896
|
+
return styleObject[styleProperty] || defaultValue;
|
|
897
|
+
}
|
|
898
|
+
return defaultValue;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
/**
|
|
902
|
+
* Returns the current value of a CSS property for TextNodes in the Selection, if set. If not set, it returns the defaultValue.
|
|
903
|
+
* If all TextNodes do not have the same value, it returns an empty string.
|
|
904
|
+
* @param selection - The selection of TextNodes whose value to find.
|
|
905
|
+
* @param styleProperty - The CSS style property.
|
|
906
|
+
* @param defaultValue - The default value for the property, defaults to an empty string.
|
|
907
|
+
* @returns The value of the property for the selected TextNodes.
|
|
908
|
+
*/
|
|
909
|
+
function $getSelectionStyleValueForProperty(selection, styleProperty, defaultValue = '') {
|
|
910
|
+
let styleValue = null;
|
|
911
|
+
const nodes = selection.getNodes();
|
|
912
|
+
const anchor = selection.anchor;
|
|
913
|
+
const focus = selection.focus;
|
|
914
|
+
const isBackward = selection.isBackward();
|
|
915
|
+
const endOffset = isBackward ? focus.offset : anchor.offset;
|
|
916
|
+
const endNode = isBackward ? focus.getNode() : anchor.getNode();
|
|
917
|
+
if (selection.isCollapsed() && selection.style !== '') {
|
|
918
|
+
const css = selection.style;
|
|
919
|
+
const styleObject = getStyleObjectFromCSS(css);
|
|
920
|
+
if (styleObject !== null && styleProperty in styleObject) {
|
|
921
|
+
return styleObject[styleProperty];
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
925
|
+
const node = nodes[i];
|
|
926
|
+
|
|
927
|
+
// if no actual characters in the end node are selected, we don't
|
|
928
|
+
// include it in the selection for purposes of determining style
|
|
929
|
+
// value
|
|
930
|
+
if (i !== 0 && endOffset === 0 && node.is(endNode)) {
|
|
931
|
+
continue;
|
|
932
|
+
}
|
|
933
|
+
if ($isTextNode(node)) {
|
|
934
|
+
const nodeStyleValue = $getNodeStyleValueForProperty(node, styleProperty, defaultValue);
|
|
935
|
+
if (styleValue === null) {
|
|
936
|
+
styleValue = nodeStyleValue;
|
|
937
|
+
} else if (styleValue !== nodeStyleValue) {
|
|
938
|
+
// multiple text nodes are in the selection and they don't all
|
|
939
|
+
// have the same style.
|
|
940
|
+
styleValue = '';
|
|
941
|
+
break;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
return styleValue === null ? defaultValue : styleValue;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
/**
|
|
949
|
+
* This function is for internal use of the library.
|
|
950
|
+
* Please do not use it as it may change in the future.
|
|
951
|
+
*/
|
|
952
|
+
function INTERNAL_$isBlock(node) {
|
|
953
|
+
if ($isDecoratorNode(node)) {
|
|
954
|
+
return false;
|
|
955
|
+
}
|
|
956
|
+
if (!$isElementNode(node) || $isRootOrShadowRoot(node)) {
|
|
957
|
+
return false;
|
|
958
|
+
}
|
|
959
|
+
const firstChild = node.getFirstChild();
|
|
960
|
+
const isLeafElement = firstChild === null || $isLineBreakNode(firstChild) || $isTextNode(firstChild) || firstChild.isInline();
|
|
961
|
+
return !node.isInline() && node.canBeEmpty() !== false && isLeafElement;
|
|
962
|
+
}
|
|
963
|
+
function $getAncestor(node, predicate) {
|
|
964
|
+
let parent = node;
|
|
965
|
+
while (parent !== null && parent.getParent() !== null && !predicate(parent)) {
|
|
966
|
+
parent = parent.getParentOrThrow();
|
|
967
|
+
}
|
|
968
|
+
return predicate(parent) ? parent : null;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
export { $addNodeStyle, $cloneWithProperties, $getSelectionStyleValueForProperty, $isAtNodeEnd, $isParentElementRTL, $moveCaretSelection, $moveCharacter, $patchStyleText, $selectAll, $setBlocksType, $shouldOverrideDefaultCharacterSelection, $sliceSelectedTextNodeContent, $wrapNodes, createDOMRange, createRectsFromDOMRange, getStyleObjectFromCSS, trimTextContentFromAnchor };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
import * as modDev from './LexicalSelection.dev.esm.js';
|
|
8
|
+
import * as modProd from './LexicalSelection.prod.esm.js';
|
|
9
|
+
const mod = process.env.NODE_ENV === 'development' ? modDev : modProd;
|
|
10
|
+
export const $addNodeStyle = mod.$addNodeStyle;
|
|
11
|
+
export const $cloneWithProperties = mod.$cloneWithProperties;
|
|
12
|
+
export const $getSelectionStyleValueForProperty = mod.$getSelectionStyleValueForProperty;
|
|
13
|
+
export const $isAtNodeEnd = mod.$isAtNodeEnd;
|
|
14
|
+
export const $isParentElementRTL = mod.$isParentElementRTL;
|
|
15
|
+
export const $moveCaretSelection = mod.$moveCaretSelection;
|
|
16
|
+
export const $moveCharacter = mod.$moveCharacter;
|
|
17
|
+
export const $patchStyleText = mod.$patchStyleText;
|
|
18
|
+
export const $selectAll = mod.$selectAll;
|
|
19
|
+
export const $setBlocksType = mod.$setBlocksType;
|
|
20
|
+
export const $shouldOverrideDefaultCharacterSelection = mod.$shouldOverrideDefaultCharacterSelection;
|
|
21
|
+
export const $sliceSelectedTextNodeContent = mod.$sliceSelectedTextNodeContent;
|
|
22
|
+
export const $wrapNodes = mod.$wrapNodes;
|
|
23
|
+
export const createDOMRange = mod.createDOMRange;
|
|
24
|
+
export const createRectsFromDOMRange = mod.createRectsFromDOMRange;
|
|
25
|
+
export const getStyleObjectFromCSS = mod.getStyleObjectFromCSS;
|
|
26
|
+
export const trimTextContentFromAnchor = mod.trimTextContentFromAnchor;
|
package/LexicalSelection.js
CHANGED
|
@@ -5,5 +5,5 @@
|
|
|
5
5
|
* LICENSE file in the root directory of this source tree.
|
|
6
6
|
*/
|
|
7
7
|
'use strict'
|
|
8
|
-
const LexicalSelection = process.env.NODE_ENV === 'development' ? require('./LexicalSelection.dev.js') : require('./LexicalSelection.prod.js')
|
|
8
|
+
const LexicalSelection = process.env.NODE_ENV === 'development' ? require('./LexicalSelection.dev.js') : require('./LexicalSelection.prod.js');
|
|
9
9
|
module.exports = LexicalSelection;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
import{$isTextNode as e,$isElementNode as t,$getCharacterOffsets as n,$isRootNode as o,$getNodeByKey as l,$getPreviousSelection as r,$createTextNode as s,$isRangeSelection as i,$getRoot as c,$isRootOrShadowRoot as f,$hasAncestor as u,$isLeafNode as g,$setSelection as a,$getAdjacentNode as d,$isDecoratorNode as p,$isLineBreakNode as h}from"lexical";const m=new Map;function _(e){let t=e;for(;null!=t;){if(t.nodeType===Node.TEXT_NODE)return t;t=t.firstChild}return null}function y(e){const t=e.parentNode;if(null==t)throw new Error("Should never happen");return[t,Array.from(t.childNodes).indexOf(e)]}function T(t,n,o,l,r){const s=n.getKey(),i=l.getKey(),c=document.createRange();let f=t.getElementByKey(s),u=t.getElementByKey(i),g=o,a=r;if(e(n)&&(f=_(f)),e(l)&&(u=_(u)),void 0===n||void 0===l||null===f||null===u)return null;"BR"===f.nodeName&&([f,g]=y(f)),"BR"===u.nodeName&&([u,a]=y(u));const d=f.firstChild;f===u&&null!=d&&"BR"===d.nodeName&&0===g&&0===a&&(a=1);try{c.setStart(f,g),c.setEnd(u,a)}catch(e){return null}return!c.collapsed||g===a&&s===i||(c.setStart(u,a),c.setEnd(f,g)),c}function x(e,t){const n=e.getRootElement();if(null===n)return[];const o=n.getBoundingClientRect(),l=getComputedStyle(n),r=parseFloat(l.paddingLeft)+parseFloat(l.paddingRight),s=Array.from(t.getClientRects());let i,c=s.length;s.sort(((e,t)=>{const n=e.top-t.top;return Math.abs(n)<=3?e.left-t.left:n}));for(let e=0;e<c;e++){const t=s[e],n=i&&i.top<=t.top&&i.top+i.height>t.top&&i.left+i.width>t.left,l=t.width+r===o.width;n||l?(s.splice(e--,1),c--):i=t}return s}function S(e){const t={},n=e.split(";");for(const e of n)if(""!==e){const[n,o]=e.split(/:([^]+)/);n&&o&&(t[n.trim()]=o.trim())}return t}function N(e){let t=m.get(e);return void 0===t&&(t=S(e),m.set(e,t)),t}function E(n){const o=n.constructor.clone(n);return o.__parent=n.__parent,o.__next=n.__next,o.__prev=n.__prev,t(n)&&t(o)?(r=n,(l=o).__first=r.__first,l.__last=r.__last,l.__size=r.__size,l.__format=r.__format,l.__indent=r.__indent,l.__dir=r.__dir,l):e(n)&&e(o)?function(e,t){return e.__format=t.__format,e.__style=t.__style,e.__mode=t.__mode,e.__detail=t.__detail,e}(o,n):o;var l,r}function v(e,t){const o=e.getStartEndPoints();if(t.isSelected(e)&&!t.isSegmented()&&!t.isToken()&&null!==o){const[l,r]=o,s=e.isBackward(),i=l.getNode(),c=r.getNode(),f=t.is(i),u=t.is(c);if(f||u){const[o,l]=n(e),r=i.is(c),f=t.is(s?c:i),u=t.is(s?i:c);let g,a=0;if(r)a=o>l?l:o,g=o>l?o:l;else if(f){a=s?l:o,g=void 0}else if(u){a=0,g=s?o:l}return t.__text=t.__text.slice(a,g),t}}return t}function C(e){if("text"===e.type)return e.offset===e.getNode().getTextContentSize();const n=e.getNode();if(!t(n))throw Error("isAtNodeEnd: node must be a TextNode or ElementNode");return e.offset===n.getChildrenSize()}function w(n,c,f){let u=c.getNode(),g=f;if(t(u)){const e=u.getDescendantByIndex(c.offset);null!==e&&(u=e)}for(;g>0&&null!==u;){if(t(u)){const e=u.getLastDescendant();null!==e&&(u=e)}let f=u.getPreviousSibling(),a=0;if(null===f){let e=u.getParentOrThrow(),t=e.getPreviousSibling();for(;null===t;){if(e=e.getParent(),null===e){f=null;break}t=e.getPreviousSibling()}null!==e&&(a=e.isInline()?0:2,f=t)}let d=u.getTextContent();""===d&&t(u)&&!u.isInline()&&(d="\n\n");const p=d.length;if(!e(u)||g>=p){const e=u.getParent();u.remove(),null==e||0!==e.getChildrenSize()||o(e)||e.remove(),g-=p+a,u=f}else{const t=u.getKey(),o=n.getEditorState().read((()=>{const n=l(t);return e(n)&&n.isSimpleText()?n.getTextContent():null})),f=p-g,a=d.slice(0,f);if(null!==o&&o!==d){const e=r();let t=u;if(u.isSimpleText())u.setTextContent(o);else{const e=s(o);u.replace(e),t=e}if(i(e)&&e.isCollapsed()){const n=e.anchor.offset;t.select(n,n)}}else if(u.isSimpleText()){const e=c.key===t;let n=c.offset;n<g&&(n=p);const o=e?n-g:0,l=e?n:f;if(e&&0===o){const[e]=u.splitText(o,l);e.remove()}else{const[,e]=u.splitText(o,l);e.remove()}}else{const e=s(a);u.replace(e)}g=0}}}function P(e){const t=e.getStyle(),n=S(t);m.set(t,n)}function F(e,t){const n=N("getStyle"in e?e.getStyle():e.style),o=Object.entries(t).reduce(((e,[t,o])=>(o instanceof Function?e[t]=o(n[t]):null===o?delete e[t]:e[t]=o,e)),{...n}||{}),l=function(e){let t="";for(const n in e)n&&(t+=`${n}: ${e[n]};`);return t}(o);e.setStyle(l),m.set(l,o)}function I(t,n){const o=t.getNodes(),l=o.length,r=t.getStartEndPoints();if(null===r)return;const[s,c]=r,f=l-1;let u=o[0],g=o[f];if(t.isCollapsed()&&i(t))return void F(t,n);const a=u.getTextContent().length,d=c.offset;let p=s.offset;const h=s.isBefore(c);let m=h?p:d,_=h?d:p;const y=h?s.type:c.type,T=h?c.type:s.type,x=h?c.key:s.key;if(e(u)&&m===a){const t=u.getNextSibling();e(t)&&(p=0,m=0,u=t)}if(1===o.length){if(e(u)&&u.canHaveFormat()){if(m="element"===y?0:p>d?d:p,_="element"===T?a:p>d?p:d,m===_)return;if(0===m&&_===a)F(u,n),u.select(m,_);else{const e=u.splitText(m,_),t=0===m?e[0]:e[1];F(t,n),t.select(0,_-m)}}}else{if(e(u)&&m<u.getTextContentSize()&&u.canHaveFormat()&&(0!==m&&(u=u.splitText(m)[1],m=0,s.set(u.getKey(),m,"text")),F(u,n)),e(g)&&g.canHaveFormat()){const e=g.getTextContent().length;g.__key!==x&&0!==_&&(_=e),_!==e&&([g]=g.splitText(_)),0===_&&"element"!==T||F(g,n)}for(let t=1;t<f;t++){const l=o[t],r=l.getKey();e(l)&&l.canHaveFormat()&&r!==u.getKey()&&r!==g.getKey()&&!l.isToken()&&F(l,n)}}}function K(e,n){if(null===e)return;const o=e.getStartEndPoints(),l=o?o[0]:null;if(null!==l&&"root"===l.key){const e=n(),t=c(),o=t.getFirstChild();return void(o?o.replace(e,!0):t.append(e))}const r=e.getNodes(),s=null!==l&&function(e,t){let n=e;for(;null!==n&&null!==n.getParent()&&!t(n);)n=n.getParentOrThrow();return t(n)?n:null}(l.getNode(),$);s&&-1===r.indexOf(s)&&r.push(s);for(let e=0;e<r.length;e++){const o=r[e];if(!$(o))continue;if(!t(o))throw Error("Expected block node to be an ElementNode");const l=n();l.setFormat(o.getFormatType()),l.setIndent(o.getIndent()),o.replace(l,!0)}}function b(e){return e.getNode().isAttached()}function O(e){let t=e;for(;null!==t&&!f(t);){const e=t.getLatest(),n=t.getParent();0===e.getChildrenSize()&&t.remove(!0),t=n}}function k(e,t,n=null){const o=e.getStartEndPoints(),l=o?o[0]:null,r=e.getNodes(),s=r.length;if(null!==l&&(0===s||1===s&&"element"===l.type&&0===l.getNode().getChildrenSize())){const e="text"===l.type?l.getNode().getParentOrThrow():l.getNode(),o=e.getChildren();let r=t();return r.setFormat(e.getFormatType()),r.setIndent(e.getIndent()),o.forEach((e=>r.append(e))),n&&(r=n.append(r)),void e.replace(r)}let i=null,c=[];for(let o=0;o<s;o++){const l=r[o];f(l)?(B(e,c,c.length,t,n),c=[],i=l):null===i||null!==i&&u(l,i)?c.push(l):(B(e,c,c.length,t,n),c=[l])}B(e,c,c.length,t,n)}function B(e,n,o,l,s=null){if(0===n.length)return;const c=n[0],u=new Map,d=[];let p=t(c)?c:c.getParentOrThrow();p.isInline()&&(p=p.getParentOrThrow());let h=!1;for(;null!==p;){const e=p.getPreviousSibling();if(null!==e){p=e,h=!0;break}if(p=p.getParentOrThrow(),f(p))break}const m=new Set;for(let e=0;e<o;e++){const o=n[e];t(o)&&0===o.getChildrenSize()&&m.add(o.getKey())}const _=new Set;for(let e=0;e<o;e++){const o=n[e];let r=o.getParent();if(null!==r&&r.isInline()&&(r=r.getParent()),null!==r&&g(o)&&!_.has(o.getKey())){const e=r.getKey();if(void 0===u.get(e)){const n=l();n.setFormat(r.getFormatType()),n.setIndent(r.getIndent()),d.push(n),u.set(e,n),r.getChildren().forEach((e=>{n.append(e),_.add(e.getKey()),t(e)&&e.getChildrenKeys().forEach((e=>_.add(e)))})),O(r)}}else if(m.has(o.getKey())){if(!t(o))throw Error("Expected node in emptyElements to be an ElementNode");const e=l();e.setFormat(o.getFormatType()),e.setIndent(o.getIndent()),d.push(e),o.remove(!0)}}if(null!==s)for(let e=0;e<d.length;e++){const t=d[e];s.append(t)}let y=null;if(f(p))if(h)if(null!==s)p.insertAfter(s);else for(let e=d.length-1;e>=0;e--){const t=d[e];p.insertAfter(t)}else{const e=p.getFirstChild();if(t(e)&&(p=e),null===e)if(s)p.append(s);else for(let e=0;e<d.length;e++){const t=d[e];p.append(t),y=t}else if(null!==s)e.insertBefore(s);else for(let t=0;t<d.length;t++){const n=d[t];e.insertBefore(n),y=n}}else if(s)p.insertAfter(s);else for(let e=d.length-1;e>=0;e--){const t=d[e];p.insertAfter(t),y=t}const T=r();i(T)&&b(T.anchor)&&b(T.focus)?a(T.clone()):null!==y?y.selectEnd():e.dirty=!0}function z(e,n){const o=d(e.focus,n);return p(o)&&!o.isIsolated()||t(o)&&!o.isInline()&&!o.canBeEmpty()}function A(e,t,n,o){e.modify(t?"extend":"move",n,o)}function R(e){const t=e.anchor.getNode();return"rtl"===(o(t)?t:t.getParentOrThrow()).getDirection()}function D(e,t,n){const o=R(e);A(e,t,n?!o:o,"character")}function L(n){const o=n.anchor,l=n.focus,r=o.getNode().getTopLevelElementOrThrow().getParentOrThrow();let s=r.getFirstDescendant(),i=r.getLastDescendant(),c="element",f="element",u=0;e(s)?c="text":t(s)||null===s||(s=s.getParentOrThrow()),e(i)?(f="text",u=i.getTextContentSize()):t(i)||null===i||(i=i.getParentOrThrow()),s&&i&&(o.set(s.getKey(),0,c),l.set(i.getKey(),u,f))}function H(e,t,n){const o=N(e.getStyle());return null!==o&&o[t]||n}function M(t,n,o=""){let l=null;const r=t.getNodes(),s=t.anchor,i=t.focus,c=t.isBackward(),f=c?i.offset:s.offset,u=c?i.getNode():s.getNode();if(t.isCollapsed()&&""!==t.style){const e=N(t.style);if(null!==e&&n in e)return e[n]}for(let t=0;t<r.length;t++){const s=r[t];if((0===t||0!==f||!s.is(u))&&e(s)){const e=H(s,n,o);if(null===l)l=e;else if(l!==e){l="";break}}}return null===l?o:l}function $(n){if(p(n))return!1;if(!t(n)||f(n))return!1;const o=n.getFirstChild(),l=null===o||h(o)||e(o)||o.isInline();return!n.isInline()&&!1!==n.canBeEmpty()&&l}export{P as $addNodeStyle,E as $cloneWithProperties,M as $getSelectionStyleValueForProperty,C as $isAtNodeEnd,R as $isParentElementRTL,A as $moveCaretSelection,D as $moveCharacter,I as $patchStyleText,L as $selectAll,K as $setBlocksType,z as $shouldOverrideDefaultCharacterSelection,v as $sliceSelectedTextNodeContent,k as $wrapNodes,T as createDOMRange,x as createRectsFromDOMRange,N as getStyleObjectFromCSS,w as trimTextContentFromAnchor};
|
package/README.md
CHANGED
|
@@ -1,112 +1,5 @@
|
|
|
1
1
|
# `@lexical/selection`
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
### Methods
|
|
6
|
-
|
|
7
|
-
#### `getStyleObjectFromCSS`
|
|
8
|
-
|
|
9
|
-
Given a CSS string, returns an object from the style cache.
|
|
10
|
-
|
|
11
|
-
```ts
|
|
12
|
-
export function getStyleObjectFromCSS(css: string): {
|
|
13
|
-
[key: string]: string;
|
|
14
|
-
} | null;
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
#### `$patchStyleText`
|
|
18
|
-
|
|
19
|
-
Applies the provided styles to the TextNodes in the provided Selection. Key names in the patch argument should be
|
|
20
|
-
the valid CSS properties (i.e., kebab-case).
|
|
21
|
-
|
|
22
|
-
```ts
|
|
23
|
-
export function $patchStyleText(
|
|
24
|
-
selection: BaseSelection,
|
|
25
|
-
patch: {
|
|
26
|
-
[key: string]: string;
|
|
27
|
-
},
|
|
28
|
-
): void;
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
#### `$getSelectionStyleValueForProperty`
|
|
32
|
-
|
|
33
|
-
Given a selection and a valid CSS property name, returns the current value of that property for TextNodes in the Selection, if set. If not set, it returns the defaultValue. If all TextNodes do not have the same value, it returns an empty string.
|
|
34
|
-
|
|
35
|
-
```ts
|
|
36
|
-
export function $getSelectionStyleValueForProperty(
|
|
37
|
-
selection: RangeSelection,
|
|
38
|
-
styleProperty: string,
|
|
39
|
-
defaultValue: string,
|
|
40
|
-
): string;
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
#### `$moveCaretSelection`
|
|
44
|
-
|
|
45
|
-
Moves the selection according to the arguments.
|
|
46
|
-
|
|
47
|
-
```ts
|
|
48
|
-
export function $moveCaretSelection(
|
|
49
|
-
selection: RangeSelection,
|
|
50
|
-
isHoldingShift: boolean,
|
|
51
|
-
isBackward: boolean,
|
|
52
|
-
granularity: 'character' | 'word' | 'lineboundary',
|
|
53
|
-
): void;
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
#### `$isParentElementRTL`
|
|
57
|
-
|
|
58
|
-
Returns true if the parent of the Selection anchor node is in Right-To-Left mode, false if not.
|
|
3
|
+
[](https://lexical.dev/docs/api/modules/lexical_selection)
|
|
59
4
|
|
|
60
|
-
|
|
61
|
-
export function $isParentElementRTL(selection: RangeSelection): boolean;
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
#### `$moveCharacter`
|
|
65
|
-
|
|
66
|
-
Wraps $moveCaretSelection, using character granularity and accounting for RTL mode.
|
|
67
|
-
|
|
68
|
-
```ts
|
|
69
|
-
export function $moveCharacter(
|
|
70
|
-
selection: RangeSelection,
|
|
71
|
-
isHoldingShift: boolean,
|
|
72
|
-
isBackward: boolean,
|
|
73
|
-
): void;
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
#### `$selectAll`
|
|
77
|
-
|
|
78
|
-
Expands the current Selection to cover all of the content in the editor.
|
|
79
|
-
|
|
80
|
-
```ts
|
|
81
|
-
export function $selectAll(selection: RangeSelection): void;
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
#### `$setBlocksType`
|
|
85
|
-
|
|
86
|
-
Converts all nodes in the selection that are of one block type to another specified by parameter
|
|
87
|
-
|
|
88
|
-
```ts
|
|
89
|
-
export function $setBlocksType(
|
|
90
|
-
selection: RangeSelection,
|
|
91
|
-
createElement: () => ElementNode,
|
|
92
|
-
): void;
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
#### `$isAtNodeEnd`
|
|
96
|
-
|
|
97
|
-
Returns true if the provided point offset is in the last possible position.
|
|
98
|
-
|
|
99
|
-
```ts
|
|
100
|
-
export function $isAtNodeEnd(point: Point): boolean;
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
#### `$shouldOverrideDefaultCharacterSelection`
|
|
104
|
-
|
|
105
|
-
Returns true if default character selection should be overridden, false if not. Used with DecoratorNodes
|
|
106
|
-
|
|
107
|
-
```ts
|
|
108
|
-
export function $shouldOverrideDefaultCharacterSelection(
|
|
109
|
-
selection: RangeSelection,
|
|
110
|
-
isBackward: boolean,
|
|
111
|
-
): boolean;
|
|
112
|
-
```
|
|
5
|
+
This package contains selection helpers for Lexical.
|
package/package.json
CHANGED
|
@@ -9,14 +9,16 @@
|
|
|
9
9
|
"selection"
|
|
10
10
|
],
|
|
11
11
|
"license": "MIT",
|
|
12
|
-
"version": "0.
|
|
12
|
+
"version": "0.14.1",
|
|
13
13
|
"main": "LexicalSelection.js",
|
|
14
14
|
"peerDependencies": {
|
|
15
|
-
"lexical": "0.
|
|
15
|
+
"lexical": "0.14.1"
|
|
16
16
|
},
|
|
17
17
|
"repository": {
|
|
18
18
|
"type": "git",
|
|
19
19
|
"url": "https://github.com/facebook/lexical",
|
|
20
20
|
"directory": "packages/lexical-selection"
|
|
21
|
-
}
|
|
21
|
+
},
|
|
22
|
+
"module": "LexicalSelection.esm.js",
|
|
23
|
+
"sideEffects": false
|
|
22
24
|
}
|