@lexical/selection 0.44.1-nightly.20260518.0 → 0.45.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,667 @@
1
+ /**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ */
8
+
9
+ import type {
10
+ BaseSelection,
11
+ DecoratorNode,
12
+ ElementNode,
13
+ LexicalNode,
14
+ NodeKey,
15
+ Point,
16
+ RangeSelection,
17
+ TextNode,
18
+ } from 'lexical';
19
+
20
+ import invariant from '@lexical/internal/invariant';
21
+ import {
22
+ $caretFromPoint,
23
+ $extendCaretToRange,
24
+ $findMatchingParent,
25
+ $getPreviousSelection,
26
+ $hasAncestor,
27
+ $isChildCaret,
28
+ $isDecoratorNode,
29
+ $isElementNode,
30
+ $isExtendableTextPointCaret,
31
+ $isLeafNode,
32
+ $isRangeSelection,
33
+ $isRootOrShadowRoot,
34
+ $isTextNode,
35
+ $setSelection,
36
+ getStyleObjectFromCSS,
37
+ INTERNAL_$isBlock,
38
+ } from 'lexical';
39
+
40
+ import {$getComputedStyleForElement, $getComputedStyleForParent} from './utils';
41
+
42
+ export function $copyBlockFormatIndent(
43
+ srcNode: ElementNode,
44
+ destNode: ElementNode,
45
+ ): void {
46
+ const format = srcNode.getFormatType();
47
+ const indent = srcNode.getIndent();
48
+ if (format !== destNode.getFormatType()) {
49
+ destNode.setFormat(format);
50
+ }
51
+ if (indent !== destNode.getIndent()) {
52
+ destNode.setIndent(indent);
53
+ }
54
+ }
55
+
56
+ function $isPointAtBlockStart(point: Point, block: ElementNode): boolean {
57
+ if (point.offset !== 0) {
58
+ return false;
59
+ }
60
+ let node: LexicalNode = point.getNode();
61
+ // When an ElementNode is empty it's not possible to distinguish if
62
+ // the selection's intent is the entire block or the edge so we consider
63
+ // it to be the entire block
64
+ if ($isElementNode(node) && node.isEmpty()) {
65
+ return false;
66
+ }
67
+ while (!node.is(block)) {
68
+ if (node.getPreviousSibling() !== null) {
69
+ return false;
70
+ }
71
+ const parent = node.getParent();
72
+ if (parent === null) {
73
+ return false;
74
+ }
75
+ node = parent;
76
+ }
77
+ return true;
78
+ }
79
+
80
+ /**
81
+ * Converts all nodes in the selection that are of one block type to another.
82
+ * @param selection - The selected blocks to be converted.
83
+ * @param $createElement - The function that creates the node. eg. $createParagraphNode.
84
+ * @param $afterCreateElement - The function that updates the new node based on the previous one ($copyBlockFormatIndent by default)
85
+ */
86
+ export function $setBlocksType<T extends ElementNode>(
87
+ selection: BaseSelection | null,
88
+ $createElement: () => T,
89
+ $afterCreateElement: (
90
+ prevNodeSrc: ElementNode,
91
+ newNodeDest: T,
92
+ ) => void = $copyBlockFormatIndent,
93
+ ): void {
94
+ if (!selection) {
95
+ return;
96
+ }
97
+ // Selections tend to not include their containing blocks so we effectively
98
+ // expand it here
99
+ const anchorAndFocus = selection.getStartEndPoints();
100
+ let skipFocusAtBlockStart = false;
101
+ let focusBlock: ElementNode | DecoratorNode<unknown> | null = null;
102
+ const blockMap = new Map<NodeKey, ElementNode>();
103
+ if (anchorAndFocus) {
104
+ const [anchor, focus] = anchorAndFocus;
105
+ const anchorBlock = $findMatchingParent(
106
+ anchor.getNode(),
107
+ INTERNAL_$isBlock,
108
+ );
109
+ focusBlock = $findMatchingParent(focus.getNode(), INTERNAL_$isBlock);
110
+ skipFocusAtBlockStart =
111
+ $isElementNode(focusBlock) &&
112
+ !focusBlock.is(anchorBlock) &&
113
+ $isPointAtBlockStart(focus, focusBlock);
114
+ if ($isElementNode(anchorBlock)) {
115
+ blockMap.set(anchorBlock.getKey(), anchorBlock);
116
+ }
117
+ if ($isElementNode(focusBlock) && !skipFocusAtBlockStart) {
118
+ blockMap.set(focusBlock.getKey(), focusBlock);
119
+ }
120
+ }
121
+ for (const node of selection.getNodes()) {
122
+ if ($isElementNode(node) && INTERNAL_$isBlock(node)) {
123
+ if (skipFocusAtBlockStart && node.is(focusBlock)) {
124
+ continue;
125
+ }
126
+ blockMap.set(node.getKey(), node);
127
+ } else if (!anchorAndFocus) {
128
+ const ancestorBlock = $findMatchingParent(node, INTERNAL_$isBlock);
129
+ if ($isElementNode(ancestorBlock)) {
130
+ blockMap.set(ancestorBlock.getKey(), ancestorBlock);
131
+ }
132
+ }
133
+ }
134
+ // Selection remapping is delegated to LexicalNode.replace (and the
135
+ // ListItemNode.replace override): both remap an element-anchored point
136
+ // on the replaced block to {key: replacement, offset: prevSize + offset}.
137
+ for (const prevNode of blockMap.values()) {
138
+ const element = $createElement();
139
+ $afterCreateElement(prevNode, element);
140
+ prevNode.replace(element, true);
141
+ }
142
+ }
143
+
144
+ function isPointAttached(point: Point): boolean {
145
+ return point.getNode().isAttached();
146
+ }
147
+
148
+ function $removeParentEmptyElements(startingNode: ElementNode): void {
149
+ let node: ElementNode | null = startingNode;
150
+
151
+ while (node !== null && !$isRootOrShadowRoot(node)) {
152
+ const latest = node.getLatest();
153
+ const parentNode: ElementNode | null = node.getParent<ElementNode>();
154
+
155
+ if (latest.getChildrenSize() === 0) {
156
+ node.remove(true);
157
+ }
158
+
159
+ node = parentNode;
160
+ }
161
+ }
162
+
163
+ /**
164
+ * @deprecated In favor of $setBlockTypes
165
+ * Wraps all nodes in the selection into another node of the type returned by createElement.
166
+ * @param selection - The selection of nodes to be wrapped.
167
+ * @param createElement - A function that creates the wrapping ElementNode. eg. $createParagraphNode.
168
+ * @param wrappingElement - An element to append the wrapped selection and its children to.
169
+ */
170
+ export function $wrapNodes(
171
+ selection: BaseSelection,
172
+ createElement: () => ElementNode,
173
+ wrappingElement: null | ElementNode = null,
174
+ ): void {
175
+ const anchorAndFocus = selection.getStartEndPoints();
176
+ const anchor = anchorAndFocus ? anchorAndFocus[0] : null;
177
+ const nodes = selection.getNodes();
178
+ const nodesLength = nodes.length;
179
+
180
+ if (
181
+ anchor !== null &&
182
+ (nodesLength === 0 ||
183
+ (nodesLength === 1 &&
184
+ anchor.type === 'element' &&
185
+ anchor.getNode().getChildrenSize() === 0))
186
+ ) {
187
+ const target =
188
+ anchor.type === 'text'
189
+ ? anchor.getNode().getParentOrThrow()
190
+ : anchor.getNode();
191
+ const children = target.getChildren();
192
+ let element = createElement();
193
+ element.setFormat(target.getFormatType());
194
+ element.setIndent(target.getIndent());
195
+ children.forEach(child => element.append(child));
196
+
197
+ if (wrappingElement) {
198
+ element = wrappingElement.append(element);
199
+ }
200
+
201
+ target.replace(element);
202
+
203
+ return;
204
+ }
205
+
206
+ let topLevelNode = null;
207
+ let descendants: LexicalNode[] = [];
208
+ for (let i = 0; i < nodesLength; i++) {
209
+ const node = nodes[i];
210
+ // Determine whether wrapping has to be broken down into multiple chunks. This can happen if the
211
+ // user selected multiple Root-like nodes that have to be treated separately as if they are
212
+ // their own branch. I.e. you don't want to wrap a whole table, but rather the contents of each
213
+ // of each of the cell nodes.
214
+ if ($isRootOrShadowRoot(node)) {
215
+ $wrapNodesImpl(
216
+ selection,
217
+ descendants,
218
+ descendants.length,
219
+ createElement,
220
+ wrappingElement,
221
+ );
222
+ descendants = [];
223
+ topLevelNode = node;
224
+ } else if (
225
+ topLevelNode === null ||
226
+ (topLevelNode !== null && $hasAncestor(node, topLevelNode))
227
+ ) {
228
+ descendants.push(node);
229
+ } else {
230
+ $wrapNodesImpl(
231
+ selection,
232
+ descendants,
233
+ descendants.length,
234
+ createElement,
235
+ wrappingElement,
236
+ );
237
+ descendants = [node];
238
+ }
239
+ }
240
+ $wrapNodesImpl(
241
+ selection,
242
+ descendants,
243
+ descendants.length,
244
+ createElement,
245
+ wrappingElement,
246
+ );
247
+ }
248
+
249
+ /**
250
+ * Wraps each node into a new ElementNode.
251
+ * @param selection - The selection of nodes to wrap.
252
+ * @param nodes - An array of nodes, generally the descendants of the selection.
253
+ * @param nodesLength - The length of nodes.
254
+ * @param createElement - A function that creates the wrapping ElementNode. eg. $createParagraphNode.
255
+ * @param wrappingElement - An element to wrap all the nodes into.
256
+ * @returns
257
+ */
258
+ export function $wrapNodesImpl(
259
+ selection: BaseSelection,
260
+ nodes: LexicalNode[],
261
+ nodesLength: number,
262
+ createElement: () => ElementNode,
263
+ wrappingElement: null | ElementNode = null,
264
+ ): void {
265
+ if (nodes.length === 0) {
266
+ return;
267
+ }
268
+
269
+ const firstNode = nodes[0];
270
+ const elementMapping: Map<NodeKey, ElementNode> = new Map();
271
+ const elements = [];
272
+ // The below logic is to find the right target for us to
273
+ // either insertAfter/insertBefore/append the corresponding
274
+ // elements to. This is made more complicated due to nested
275
+ // structures.
276
+ let target = $isElementNode(firstNode)
277
+ ? firstNode
278
+ : firstNode.getParentOrThrow();
279
+
280
+ if (target.isInline()) {
281
+ target = target.getParentOrThrow();
282
+ }
283
+
284
+ let targetIsPrevSibling = false;
285
+ while (target !== null) {
286
+ const prevSibling = target.getPreviousSibling<ElementNode>();
287
+
288
+ if (prevSibling !== null) {
289
+ target = prevSibling;
290
+ targetIsPrevSibling = true;
291
+ break;
292
+ }
293
+
294
+ target = target.getParentOrThrow();
295
+
296
+ if ($isRootOrShadowRoot(target)) {
297
+ break;
298
+ }
299
+ }
300
+
301
+ const emptyElements = new Set();
302
+
303
+ // Find any top level empty elements
304
+ for (let i = 0; i < nodesLength; i++) {
305
+ const node = nodes[i];
306
+
307
+ if ($isElementNode(node) && node.getChildrenSize() === 0) {
308
+ emptyElements.add(node.getKey());
309
+ }
310
+ }
311
+
312
+ const movedNodes: Set<NodeKey> = new Set();
313
+
314
+ // Move out all leaf nodes into our elements array.
315
+ // If we find a top level empty element, also move make
316
+ // an element for that.
317
+ for (let i = 0; i < nodesLength; i++) {
318
+ const node = nodes[i];
319
+ let parent = node.getParent();
320
+
321
+ if (parent !== null && parent.isInline()) {
322
+ parent = parent.getParent();
323
+ }
324
+
325
+ if (
326
+ parent !== null &&
327
+ $isLeafNode(node) &&
328
+ !movedNodes.has(node.getKey())
329
+ ) {
330
+ const parentKey = parent.getKey();
331
+
332
+ if (elementMapping.get(parentKey) === undefined) {
333
+ const targetElement = createElement();
334
+ targetElement.setFormat(parent.getFormatType());
335
+ targetElement.setIndent(parent.getIndent());
336
+ elements.push(targetElement);
337
+ elementMapping.set(parentKey, targetElement);
338
+ // Move node and its siblings to the new
339
+ // element.
340
+ const children = parent.getChildren();
341
+ targetElement.splice(targetElement.getChildrenSize(), 0, children);
342
+ for (const child of children) {
343
+ movedNodes.add(child.getKey());
344
+ if ($isElementNode(child)) {
345
+ // Skip nested leaf nodes if the parent has already been moved
346
+ for (const key of child.getChildrenKeys()) {
347
+ movedNodes.add(key);
348
+ }
349
+ }
350
+ }
351
+ $removeParentEmptyElements(parent);
352
+ }
353
+ } else if (emptyElements.has(node.getKey())) {
354
+ invariant(
355
+ $isElementNode(node),
356
+ 'Expected node in emptyElements to be an ElementNode',
357
+ );
358
+ const targetElement = createElement();
359
+ targetElement.setFormat(node.getFormatType());
360
+ targetElement.setIndent(node.getIndent());
361
+ elements.push(targetElement);
362
+ node.remove(true);
363
+ }
364
+ }
365
+
366
+ if (wrappingElement !== null) {
367
+ for (let i = 0; i < elements.length; i++) {
368
+ const element = elements[i];
369
+ wrappingElement.append(element);
370
+ }
371
+ }
372
+ let lastElement = null;
373
+
374
+ // If our target is Root-like, let's see if we can re-adjust
375
+ // so that the target is the first child instead.
376
+ if ($isRootOrShadowRoot(target)) {
377
+ if (targetIsPrevSibling) {
378
+ if (wrappingElement !== null) {
379
+ target.insertAfter(wrappingElement);
380
+ } else {
381
+ for (let i = elements.length - 1; i >= 0; i--) {
382
+ const element = elements[i];
383
+ target.insertAfter(element);
384
+ }
385
+ }
386
+ } else {
387
+ const firstChild = target.getFirstChild();
388
+
389
+ if ($isElementNode(firstChild)) {
390
+ target = firstChild;
391
+ }
392
+
393
+ if (firstChild === null) {
394
+ if (wrappingElement) {
395
+ target.append(wrappingElement);
396
+ } else {
397
+ for (let i = 0; i < elements.length; i++) {
398
+ const element = elements[i];
399
+ target.append(element);
400
+ lastElement = element;
401
+ }
402
+ }
403
+ } else {
404
+ if (wrappingElement !== null) {
405
+ firstChild.insertBefore(wrappingElement);
406
+ } else {
407
+ for (let i = 0; i < elements.length; i++) {
408
+ const element = elements[i];
409
+ firstChild.insertBefore(element);
410
+ lastElement = element;
411
+ }
412
+ }
413
+ }
414
+ }
415
+ } else {
416
+ if (wrappingElement) {
417
+ target.insertAfter(wrappingElement);
418
+ } else {
419
+ for (let i = elements.length - 1; i >= 0; i--) {
420
+ const element = elements[i];
421
+ target.insertAfter(element);
422
+ lastElement = element;
423
+ }
424
+ }
425
+ }
426
+
427
+ const prevSelection = $getPreviousSelection();
428
+
429
+ if (
430
+ $isRangeSelection(prevSelection) &&
431
+ isPointAttached(prevSelection.anchor) &&
432
+ isPointAttached(prevSelection.focus)
433
+ ) {
434
+ $setSelection(prevSelection.clone());
435
+ } else if (lastElement !== null) {
436
+ lastElement.selectEnd();
437
+ } else {
438
+ selection.dirty = true;
439
+ }
440
+ }
441
+
442
+ /**
443
+ * Tests if the selection's parent element has vertical writing mode.
444
+ * @param selection - The selection whose parent to test.
445
+ * @returns true if the selection's parent has vertical writing mode (writing-mode: vertical-rl), false otherwise.
446
+ */
447
+ function $isEditorVerticalOrientation(selection: RangeSelection): boolean {
448
+ const computedStyle = $getComputedStyle(selection);
449
+ return computedStyle !== null && computedStyle.writingMode === 'vertical-rl';
450
+ }
451
+
452
+ /**
453
+ * Gets the computed DOM styles of the parent of the selection's anchor node.
454
+ * @param selection - The selection to check the styles for.
455
+ * @returns the computed styles of the node or null if there is no DOM element or no default view for the document.
456
+ */
457
+ function $getComputedStyle(
458
+ selection: RangeSelection,
459
+ ): CSSStyleDeclaration | null {
460
+ const anchorNode = selection.anchor.getNode();
461
+ if ($isElementNode(anchorNode)) {
462
+ return $getComputedStyleForElement(anchorNode);
463
+ }
464
+ return $getComputedStyleForParent(anchorNode);
465
+ }
466
+
467
+ /**
468
+ * Determines if the default character selection should be overridden. Used with DecoratorNodes
469
+ * @param selection - The selection whose default character selection may need to be overridden.
470
+ * @param isBackward - Is the selection backwards (the focus comes before the anchor)?
471
+ * @returns true if it should be overridden, false if not.
472
+ */
473
+ export function $shouldOverrideDefaultCharacterSelection(
474
+ selection: RangeSelection,
475
+ isBackward: boolean,
476
+ ): boolean {
477
+ const isVertical = $isEditorVerticalOrientation(selection);
478
+
479
+ // In vertical writing mode, we adjust the direction for correct caret movement
480
+ let adjustedIsBackward = isVertical ? !isBackward : isBackward;
481
+
482
+ // In right-to-left writing mode, we invert the direction for correct caret movement
483
+ if ($isParentElementRTL(selection)) {
484
+ adjustedIsBackward = !adjustedIsBackward;
485
+ }
486
+
487
+ const focusCaret = $caretFromPoint(
488
+ selection.focus,
489
+ adjustedIsBackward ? 'previous' : 'next',
490
+ );
491
+ if ($isExtendableTextPointCaret(focusCaret)) {
492
+ return false;
493
+ }
494
+ for (const nextCaret of $extendCaretToRange(focusCaret)) {
495
+ if ($isChildCaret(nextCaret)) {
496
+ return !nextCaret.origin.isInline();
497
+ } else if ($isElementNode(nextCaret.origin)) {
498
+ continue;
499
+ } else if ($isDecoratorNode(nextCaret.origin)) {
500
+ return true;
501
+ }
502
+ break;
503
+ }
504
+ return false;
505
+ }
506
+
507
+ /**
508
+ * Moves the selection according to the arguments.
509
+ * @param selection - The selected text or nodes.
510
+ * @param isHoldingShift - Is the shift key being held down during the operation.
511
+ * @param isBackward - Is the selection selected backwards (the focus comes before the anchor)?
512
+ * @param granularity - The distance to adjust the current selection.
513
+ */
514
+ export function $moveCaretSelection(
515
+ selection: RangeSelection,
516
+ isHoldingShift: boolean,
517
+ isBackward: boolean,
518
+ granularity: 'character' | 'word' | 'lineboundary',
519
+ ): void {
520
+ selection.modify(isHoldingShift ? 'extend' : 'move', isBackward, granularity);
521
+ }
522
+
523
+ /**
524
+ * Tests a parent element for right to left direction.
525
+ * @param selection - The selection whose parent is to be tested.
526
+ * @returns true if the selections' parent element has a direction of 'rtl' (right to left), false otherwise.
527
+ */
528
+ export function $isParentElementRTL(selection: RangeSelection): boolean {
529
+ const computedStyle = $getComputedStyle(selection);
530
+ return computedStyle !== null && computedStyle.direction === 'rtl';
531
+ }
532
+
533
+ /**
534
+ * Moves selection by character according to arguments.
535
+ * @param selection - The selection of the characters to move.
536
+ * @param isHoldingShift - Is the shift key being held down during the operation.
537
+ * @param isBackward - Is the selection backward (the focus comes before the anchor)?
538
+ */
539
+ export function $moveCharacter(
540
+ selection: RangeSelection,
541
+ isHoldingShift: boolean,
542
+ isBackward: boolean,
543
+ ): void {
544
+ const isRTL = $isParentElementRTL(selection);
545
+ const isVertical = $isEditorVerticalOrientation(selection);
546
+
547
+ // In vertical-rl writing mode, arrow key directions need to be flipped
548
+ // to match the visual flow of text (top to bottom, right to left)
549
+ let adjustedIsBackward;
550
+
551
+ if (isVertical) {
552
+ // In vertical-rl mode, we need to completely invert the direction
553
+ // Left arrow (backward) should move down (forward)
554
+ // Right arrow (forward) should move up (backward)
555
+ adjustedIsBackward = !isBackward;
556
+ } else if (isRTL) {
557
+ // In horizontal RTL mode, use the standard RTL behavior
558
+ adjustedIsBackward = !isBackward;
559
+ } else {
560
+ // Standard LTR horizontal text
561
+ adjustedIsBackward = isBackward;
562
+ }
563
+
564
+ // Apply the direction adjustment to move the caret
565
+ $moveCaretSelection(
566
+ selection,
567
+ isHoldingShift,
568
+ adjustedIsBackward,
569
+ 'character',
570
+ );
571
+ }
572
+
573
+ /**
574
+ * Returns the current value of a CSS property for Nodes, if set. If not set, it returns the defaultValue.
575
+ * @param node - The node whose style value to get.
576
+ * @param styleProperty - The CSS style property.
577
+ * @param defaultValue - The default value for the property.
578
+ * @returns The value of the property for node.
579
+ */
580
+ function $getNodeStyleValueForProperty(
581
+ node: TextNode,
582
+ styleProperty: string,
583
+ defaultValue: string,
584
+ ): string {
585
+ const css = node.getStyle();
586
+ const styleObject = getStyleObjectFromCSS(css);
587
+
588
+ if (styleObject !== null) {
589
+ return styleObject[styleProperty] || defaultValue;
590
+ }
591
+
592
+ return defaultValue;
593
+ }
594
+
595
+ /**
596
+ * Returns the current value of a CSS property for TextNodes in the Selection, if set. If not set, it returns the defaultValue.
597
+ * If all TextNodes do not have the same value, it returns an empty string.
598
+ * @param selection - The selection of TextNodes whose value to find.
599
+ * @param styleProperty - The CSS style property.
600
+ * @param defaultValue - The default value for the property, defaults to an empty string.
601
+ * @returns The value of the property for the selected TextNodes.
602
+ */
603
+ export function $getSelectionStyleValueForProperty(
604
+ selection: BaseSelection,
605
+ styleProperty: string,
606
+ defaultValue = '',
607
+ ): string {
608
+ let styleValue: string | null = null;
609
+ const nodes = selection.getNodes();
610
+
611
+ // The anchor/focus boundary handling below is specific to RangeSelection;
612
+ // other selection types (e.g. table) style every node they contain.
613
+ let startNode: LexicalNode | undefined;
614
+ let endNode: LexicalNode | undefined;
615
+ if ($isRangeSelection(selection)) {
616
+ if (selection.isCollapsed() && selection.style !== '') {
617
+ const styleObject = getStyleObjectFromCSS(selection.style);
618
+
619
+ if (styleObject !== null && styleProperty in styleObject) {
620
+ return styleObject[styleProperty];
621
+ }
622
+ }
623
+ const {anchor, focus} = selection;
624
+ const isBackward = selection.isBackward();
625
+ const firstNode = isBackward ? focus.getNode() : anchor.getNode();
626
+ const lastNode = isBackward ? anchor.getNode() : focus.getNode();
627
+ const startOffset = isBackward ? focus.offset : anchor.offset;
628
+ const endOffset = isBackward ? anchor.offset : focus.offset;
629
+ // A boundary node contributes no styled text when the selection merely
630
+ // touches its edge: the first node when the start offset is at its very
631
+ // end, and the last node when the end offset is at its very beginning.
632
+ if (
633
+ $isTextNode(firstNode) &&
634
+ startOffset === firstNode.getTextContentSize()
635
+ ) {
636
+ startNode = firstNode;
637
+ }
638
+ if (endOffset === 0) {
639
+ endNode = lastNode;
640
+ }
641
+ }
642
+
643
+ for (let i = 0; i < nodes.length; i++) {
644
+ const node = nodes[i];
645
+
646
+ // Skip the excluded boundary node for this position (startNode at the
647
+ // head, endNode elsewhere); both are undefined when nothing is excluded.
648
+ if ($isTextNode(node) && !node.is(i === 0 ? startNode : endNode)) {
649
+ const nodeStyleValue = $getNodeStyleValueForProperty(
650
+ node,
651
+ styleProperty,
652
+ defaultValue,
653
+ );
654
+
655
+ if (styleValue === null) {
656
+ styleValue = nodeStyleValue;
657
+ } else if (styleValue !== nodeStyleValue) {
658
+ // multiple text nodes are in the selection and they don't all
659
+ // have the same style.
660
+ styleValue = '';
661
+ break;
662
+ }
663
+ }
664
+ }
665
+
666
+ return styleValue === null ? defaultValue : styleValue;
667
+ }