@lexical/yjs 0.44.1-nightly.20260519.0 → 0.45.1-dev.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.
Files changed (37) hide show
  1. package/{RenderSnapshot.d.ts → dist/RenderSnapshot.d.ts} +0 -7
  2. package/dist/simpleDiffWithCursor.d.ts +12 -0
  3. package/package.json +31 -16
  4. package/src/Bindings.ts +135 -0
  5. package/src/CollabDecoratorNode.ts +112 -0
  6. package/src/CollabElementNode.ts +696 -0
  7. package/src/CollabLineBreakNode.ts +70 -0
  8. package/src/CollabTextNode.ts +179 -0
  9. package/src/CollabV2Mapping.ts +117 -0
  10. package/src/RenderSnapshot.ts +97 -0
  11. package/src/SyncCursors.ts +823 -0
  12. package/src/SyncEditorStates.ts +469 -0
  13. package/src/SyncV2.ts +871 -0
  14. package/src/Utils.ts +648 -0
  15. package/src/index.ts +151 -0
  16. package/src/simpleDiffWithCursor.ts +49 -0
  17. package/src/types.ts +27 -0
  18. /package/{Bindings.d.ts → dist/Bindings.d.ts} +0 -0
  19. /package/{CollabDecoratorNode.d.ts → dist/CollabDecoratorNode.d.ts} +0 -0
  20. /package/{CollabElementNode.d.ts → dist/CollabElementNode.d.ts} +0 -0
  21. /package/{CollabLineBreakNode.d.ts → dist/CollabLineBreakNode.d.ts} +0 -0
  22. /package/{CollabTextNode.d.ts → dist/CollabTextNode.d.ts} +0 -0
  23. /package/{CollabV2Mapping.d.ts → dist/CollabV2Mapping.d.ts} +0 -0
  24. /package/{LexicalYjs.dev.js → dist/LexicalYjs.dev.js} +0 -0
  25. /package/{LexicalYjs.dev.mjs → dist/LexicalYjs.dev.mjs} +0 -0
  26. /package/{LexicalYjs.js → dist/LexicalYjs.js} +0 -0
  27. /package/{LexicalYjs.js.flow → dist/LexicalYjs.js.flow} +0 -0
  28. /package/{LexicalYjs.mjs → dist/LexicalYjs.mjs} +0 -0
  29. /package/{LexicalYjs.node.mjs → dist/LexicalYjs.node.mjs} +0 -0
  30. /package/{LexicalYjs.prod.js → dist/LexicalYjs.prod.js} +0 -0
  31. /package/{LexicalYjs.prod.mjs → dist/LexicalYjs.prod.mjs} +0 -0
  32. /package/{SyncCursors.d.ts → dist/SyncCursors.d.ts} +0 -0
  33. /package/{SyncEditorStates.d.ts → dist/SyncEditorStates.d.ts} +0 -0
  34. /package/{SyncV2.d.ts → dist/SyncV2.d.ts} +0 -0
  35. /package/{Utils.d.ts → dist/Utils.d.ts} +0 -0
  36. /package/{index.d.ts → dist/index.d.ts} +0 -0
  37. /package/{types.d.ts → dist/types.d.ts} +0 -0
package/src/Utils.ts ADDED
@@ -0,0 +1,648 @@
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 {BaseBinding, Binding, YjsNode} from '.';
10
+
11
+ import invariant from '@lexical/internal/invariant';
12
+ import {
13
+ $getNodeByKey,
14
+ $getRoot,
15
+ $getWritableNodeState,
16
+ $isDecoratorNode,
17
+ $isElementNode,
18
+ $isLineBreakNode,
19
+ $isRootNode,
20
+ $isTextNode,
21
+ createEditor,
22
+ DecoratorNode,
23
+ EditorState,
24
+ ElementNode,
25
+ isLexicalEditor,
26
+ LexicalNode,
27
+ NodeKey,
28
+ RangeSelection,
29
+ TextNode,
30
+ } from 'lexical';
31
+ import {Doc, Map as YMap, XmlElement, XmlText} from 'yjs';
32
+
33
+ import {isBindingV1} from './Bindings';
34
+ import {
35
+ $createCollabDecoratorNode,
36
+ CollabDecoratorNode,
37
+ } from './CollabDecoratorNode';
38
+ import {$createCollabElementNode, CollabElementNode} from './CollabElementNode';
39
+ import {
40
+ $createCollabLineBreakNode,
41
+ CollabLineBreakNode,
42
+ } from './CollabLineBreakNode';
43
+ import {$createCollabTextNode, CollabTextNode} from './CollabTextNode';
44
+
45
+ const baseExcludedProperties = new Set<string>([
46
+ '__key',
47
+ '__parent',
48
+ '__next',
49
+ '__prev',
50
+ '__state',
51
+ ]);
52
+ const elementExcludedProperties = new Set<string>([
53
+ '__first',
54
+ '__last',
55
+ '__size',
56
+ ]);
57
+ const rootExcludedProperties = new Set<string>(['__cachedText']);
58
+ const textExcludedProperties = new Set<string>(['__text']);
59
+
60
+ function isExcludedProperty(
61
+ name: string,
62
+ node: LexicalNode,
63
+ binding: BaseBinding,
64
+ ): boolean {
65
+ if (
66
+ baseExcludedProperties.has(name) ||
67
+ typeof (node as unknown as Record<string, unknown>)[name] === 'function'
68
+ ) {
69
+ return true;
70
+ }
71
+
72
+ if ($isTextNode(node)) {
73
+ if (textExcludedProperties.has(name)) {
74
+ return true;
75
+ }
76
+ } else if ($isElementNode(node)) {
77
+ if (
78
+ elementExcludedProperties.has(name) ||
79
+ ($isRootNode(node) && rootExcludedProperties.has(name))
80
+ ) {
81
+ return true;
82
+ }
83
+ }
84
+
85
+ const nodeKlass = node.constructor;
86
+ const excludedProperties = binding.excludedProperties.get(nodeKlass);
87
+ return excludedProperties != null && excludedProperties.has(name);
88
+ }
89
+
90
+ export function initializeNodeProperties(binding: BaseBinding): void {
91
+ const {editor, nodeProperties} = binding;
92
+ editor.update(() => {
93
+ editor._nodes.forEach(nodeInfo => {
94
+ const node = new nodeInfo.klass();
95
+ const defaultProperties: {[property: string]: unknown} = {};
96
+ for (const [property, value] of Object.entries(node)) {
97
+ if (!isExcludedProperty(property, node, binding)) {
98
+ defaultProperties[property] = value;
99
+ }
100
+ }
101
+ nodeProperties.set(node.__type, Object.freeze(defaultProperties));
102
+ });
103
+ });
104
+ }
105
+
106
+ export function getDefaultNodeProperties(
107
+ node: LexicalNode,
108
+ binding: BaseBinding,
109
+ ): {[property: string]: unknown} {
110
+ const type = node.__type;
111
+ const {nodeProperties} = binding;
112
+ const properties = nodeProperties.get(type);
113
+ invariant(
114
+ properties !== undefined,
115
+ 'Node properties for %s not initialized for sync',
116
+ type,
117
+ );
118
+ return properties;
119
+ }
120
+
121
+ export function getIndexOfYjsNode(
122
+ yjsParentNode: YjsNode,
123
+ yjsNode: YjsNode,
124
+ ): number {
125
+ let node = yjsParentNode.firstChild;
126
+ let i = -1;
127
+
128
+ if (node === null) {
129
+ return -1;
130
+ }
131
+
132
+ do {
133
+ i++;
134
+
135
+ if (node === yjsNode) {
136
+ return i;
137
+ }
138
+
139
+ // @ts-expect-error Sibling exists but type is not available from YJS.
140
+ node = node.nextSibling;
141
+
142
+ if (node === null) {
143
+ return -1;
144
+ }
145
+ } while (node !== null);
146
+
147
+ return i;
148
+ }
149
+
150
+ export function $createCollabNodeFromLexicalNode(
151
+ binding: Binding,
152
+ lexicalNode: LexicalNode,
153
+ parent: CollabElementNode,
154
+ ):
155
+ | CollabElementNode
156
+ | CollabTextNode
157
+ | CollabLineBreakNode
158
+ | CollabDecoratorNode {
159
+ const nodeType = lexicalNode.__type;
160
+ let collabNode;
161
+
162
+ if ($isElementNode(lexicalNode)) {
163
+ const xmlText = new XmlText();
164
+ collabNode = $createCollabElementNode(xmlText, parent, nodeType);
165
+ collabNode.syncPropertiesFromLexical(binding, lexicalNode, null);
166
+ collabNode.syncChildrenFromLexical(binding, lexicalNode, null, null, null);
167
+ } else if ($isTextNode(lexicalNode)) {
168
+ // TODO create a token text node for token, segmented nodes.
169
+ const map = new YMap();
170
+ collabNode = $createCollabTextNode(
171
+ map,
172
+ lexicalNode.__text,
173
+ parent,
174
+ nodeType,
175
+ );
176
+ collabNode.syncPropertiesAndTextFromLexical(binding, lexicalNode, null);
177
+ } else if ($isLineBreakNode(lexicalNode)) {
178
+ const map = new YMap();
179
+ map.set('__type', 'linebreak');
180
+ collabNode = $createCollabLineBreakNode(map, parent);
181
+ } else if ($isDecoratorNode(lexicalNode)) {
182
+ const xmlElem = new XmlElement();
183
+ collabNode = $createCollabDecoratorNode(xmlElem, parent, nodeType);
184
+ collabNode.syncPropertiesFromLexical(binding, lexicalNode, null);
185
+ } else {
186
+ invariant(false, 'Expected text, element, decorator, or linebreak node');
187
+ }
188
+
189
+ collabNode._key = lexicalNode.__key;
190
+ return collabNode;
191
+ }
192
+
193
+ export function getNodeTypeFromSharedType(
194
+ sharedType: XmlText | YMap<unknown> | XmlElement,
195
+ ): string | undefined {
196
+ const type = sharedTypeGet(sharedType, '__type');
197
+ invariant(
198
+ typeof type === 'string' || typeof type === 'undefined',
199
+ 'Expected shared type to include type attribute',
200
+ );
201
+ return type;
202
+ }
203
+
204
+ export function $getOrInitCollabNodeFromSharedType(
205
+ binding: Binding,
206
+ sharedType: XmlText | YMap<unknown> | XmlElement,
207
+ parent?: CollabElementNode,
208
+ ):
209
+ | CollabElementNode
210
+ | CollabTextNode
211
+ | CollabLineBreakNode
212
+ | CollabDecoratorNode {
213
+ const collabNode = sharedType._collabNode;
214
+
215
+ if (collabNode === undefined) {
216
+ const registeredNodes = binding.editor._nodes;
217
+ const type = getNodeTypeFromSharedType(sharedType);
218
+ invariant(
219
+ typeof type === 'string',
220
+ 'Expected shared type to include type attribute',
221
+ );
222
+ const nodeInfo = registeredNodes.get(type);
223
+ invariant(nodeInfo !== undefined, 'Node %s is not registered', type);
224
+
225
+ const sharedParent = sharedType.parent;
226
+ const targetParent =
227
+ parent === undefined && sharedParent !== null
228
+ ? $getOrInitCollabNodeFromSharedType(
229
+ binding,
230
+ sharedParent as XmlText | YMap<unknown> | XmlElement,
231
+ )
232
+ : parent || null;
233
+
234
+ invariant(
235
+ targetParent instanceof CollabElementNode,
236
+ 'Expected parent to be a collab element node',
237
+ );
238
+
239
+ if (sharedType instanceof XmlText) {
240
+ return $createCollabElementNode(sharedType, targetParent, type);
241
+ } else if (sharedType instanceof YMap) {
242
+ if (type === 'linebreak') {
243
+ return $createCollabLineBreakNode(sharedType, targetParent);
244
+ }
245
+ return $createCollabTextNode(sharedType, '', targetParent, type);
246
+ } else if (sharedType instanceof XmlElement) {
247
+ return $createCollabDecoratorNode(sharedType, targetParent, type);
248
+ }
249
+ }
250
+
251
+ return collabNode;
252
+ }
253
+
254
+ export function createLexicalNodeFromCollabNode(
255
+ binding: Binding,
256
+ collabNode:
257
+ | CollabElementNode
258
+ | CollabTextNode
259
+ | CollabDecoratorNode
260
+ | CollabLineBreakNode,
261
+ parentKey: NodeKey,
262
+ ): LexicalNode {
263
+ const type = collabNode.getType();
264
+ const registeredNodes = binding.editor._nodes;
265
+ const nodeInfo = registeredNodes.get(type);
266
+ invariant(nodeInfo !== undefined, 'Node %s is not registered', type);
267
+ const lexicalNode:
268
+ | DecoratorNode<unknown>
269
+ | TextNode
270
+ | ElementNode
271
+ | LexicalNode = new nodeInfo.klass();
272
+ lexicalNode.__parent = parentKey;
273
+ collabNode._key = lexicalNode.__key;
274
+
275
+ if (collabNode instanceof CollabElementNode) {
276
+ const xmlText = collabNode._xmlText;
277
+ collabNode.syncPropertiesFromYjs(binding, null);
278
+ collabNode.applyChildrenYjsDelta(binding, xmlText.toDelta());
279
+ collabNode.syncChildrenFromYjs(binding);
280
+ } else if (collabNode instanceof CollabTextNode) {
281
+ collabNode.syncPropertiesAndTextFromYjs(binding, null);
282
+ } else if (collabNode instanceof CollabDecoratorNode) {
283
+ collabNode.syncPropertiesFromYjs(binding, null);
284
+ }
285
+
286
+ binding.collabNodeMap.set(lexicalNode.__key, collabNode);
287
+ return lexicalNode;
288
+ }
289
+
290
+ export function $syncPropertiesFromYjs(
291
+ binding: BaseBinding,
292
+ sharedType:
293
+ | XmlText
294
+ | YMap<unknown>
295
+ | XmlElement
296
+ // v2
297
+ | Record<string, unknown>,
298
+ lexicalNode: LexicalNode,
299
+ keysChanged: null | Set<string>,
300
+ ): void {
301
+ const properties =
302
+ keysChanged === null
303
+ ? sharedType instanceof YMap
304
+ ? Array.from(sharedType.keys())
305
+ : sharedType instanceof XmlText || sharedType instanceof XmlElement
306
+ ? Object.keys(sharedType.getAttributes())
307
+ : Object.keys(sharedType)
308
+ : Array.from(keysChanged);
309
+ let writableNode: LexicalNode | undefined;
310
+
311
+ for (let i = 0; i < properties.length; i++) {
312
+ const property = properties[i];
313
+ if (isExcludedProperty(property, lexicalNode, binding)) {
314
+ if (property === '__state' && isBindingV1(binding)) {
315
+ if (!writableNode) {
316
+ writableNode = lexicalNode.getWritable();
317
+ }
318
+ $syncNodeStateToLexical(sharedType, writableNode);
319
+ }
320
+ continue;
321
+ }
322
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
323
+ const prevValue = (lexicalNode as any)[property];
324
+ let nextValue = sharedTypeGet(sharedType, property);
325
+
326
+ if (prevValue !== nextValue) {
327
+ if (nextValue instanceof Doc) {
328
+ const yjsDocMap = binding.docMap;
329
+
330
+ if (prevValue instanceof Doc) {
331
+ yjsDocMap.delete(prevValue.guid);
332
+ }
333
+
334
+ const key = nextValue.guid;
335
+ yjsDocMap.set(key, nextValue);
336
+
337
+ // If the node already constructed a nested editor (e.g. via
338
+ // buildEditorFromExtensions in its constructor), reuse it so its
339
+ // extensions and LexicalBuilder registration survive the sync.
340
+ // Otherwise, fall back to a bare editor.
341
+ if (isLexicalEditor(prevValue)) {
342
+ prevValue._key = key;
343
+ nextValue = prevValue;
344
+ } else {
345
+ const nestedEditor = createEditor();
346
+ nestedEditor._key = key;
347
+ nextValue = nestedEditor;
348
+ }
349
+ }
350
+
351
+ if (writableNode === undefined) {
352
+ writableNode = lexicalNode.getWritable();
353
+ }
354
+
355
+ writableNode[property as string & keyof typeof writableNode] =
356
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
357
+ nextValue as any;
358
+ }
359
+ }
360
+ }
361
+
362
+ function sharedTypeGet(
363
+ sharedType: XmlText | YMap<unknown> | XmlElement | Record<string, unknown>,
364
+ property: string,
365
+ ): unknown {
366
+ if (sharedType instanceof YMap) {
367
+ return sharedType.get(property);
368
+ } else if (
369
+ sharedType instanceof XmlText ||
370
+ sharedType instanceof XmlElement
371
+ ) {
372
+ return sharedType.getAttribute(property);
373
+ } else {
374
+ return sharedType[property];
375
+ }
376
+ }
377
+
378
+ function sharedTypeSet(
379
+ sharedType: XmlText | YMap<unknown> | XmlElement,
380
+ property: string,
381
+ nextValue: unknown,
382
+ ): void {
383
+ if (sharedType instanceof YMap) {
384
+ sharedType.set(property, nextValue);
385
+ } else {
386
+ sharedType.setAttribute(property, nextValue as string);
387
+ }
388
+ }
389
+
390
+ function $syncNodeStateToLexical(
391
+ sharedType: XmlText | YMap<unknown> | XmlElement | Record<string, unknown>,
392
+ lexicalNode: LexicalNode,
393
+ ): void {
394
+ const existingState = sharedTypeGet(sharedType, '__state');
395
+ if (!(existingState instanceof YMap)) {
396
+ return;
397
+ }
398
+ // This should only called when creating the node initially,
399
+ // incremental updates to state come in through YMapEvent
400
+ // with the __state as the target.
401
+ $getWritableNodeState(lexicalNode).updateFromJSON(existingState.toJSON());
402
+ }
403
+
404
+ function syncNodeStateFromLexical(
405
+ binding: Binding,
406
+ sharedType: XmlText | YMap<unknown> | XmlElement,
407
+ prevLexicalNode: null | LexicalNode,
408
+ nextLexicalNode: LexicalNode,
409
+ ): void {
410
+ const nextState = nextLexicalNode.__state;
411
+ // Reading from a shared type that hasn't been integrated into a doc yet
412
+ // logs a "premature access" warning in yjs >= 13.6.10. When the shared
413
+ // type is detached we know there cannot be any existing state.
414
+ const existingState =
415
+ sharedType.doc === null ? undefined : sharedTypeGet(sharedType, '__state');
416
+ if (!nextState) {
417
+ return;
418
+ }
419
+ const [unknown, known] = nextState.getInternalState();
420
+ const prevState = prevLexicalNode && prevLexicalNode.__state;
421
+ const stateMap: YMap<unknown> =
422
+ existingState instanceof YMap ? existingState : new YMap();
423
+ if (prevState === nextState) {
424
+ return;
425
+ }
426
+ const [prevUnknown, prevKnown] =
427
+ prevState && stateMap.doc
428
+ ? prevState.getInternalState()
429
+ : [undefined, new Map()];
430
+ if (unknown) {
431
+ for (const [k, v] of Object.entries(unknown)) {
432
+ if (prevUnknown && v !== prevUnknown[k]) {
433
+ stateMap.set(k, v);
434
+ }
435
+ }
436
+ }
437
+ for (const [stateConfig, v] of known) {
438
+ if (prevKnown.get(stateConfig) !== v) {
439
+ stateMap.set(stateConfig.key, stateConfig.unparse(v));
440
+ }
441
+ }
442
+ if (!existingState) {
443
+ sharedTypeSet(sharedType, '__state', stateMap);
444
+ }
445
+ }
446
+
447
+ export function syncPropertiesFromLexical(
448
+ binding: Binding,
449
+ sharedType: XmlText | YMap<unknown> | XmlElement,
450
+ prevLexicalNode: null | LexicalNode,
451
+ nextLexicalNode: LexicalNode,
452
+ ): void {
453
+ const properties = Object.keys(
454
+ getDefaultNodeProperties(nextLexicalNode, binding),
455
+ );
456
+
457
+ const EditorClass = binding.editor.constructor;
458
+
459
+ syncNodeStateFromLexical(
460
+ binding,
461
+ sharedType,
462
+ prevLexicalNode,
463
+ nextLexicalNode,
464
+ );
465
+ for (let i = 0; i < properties.length; i++) {
466
+ const property = properties[i];
467
+ const prevValue =
468
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
469
+ prevLexicalNode === null ? undefined : (prevLexicalNode as any)[property];
470
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
471
+ let nextValue = (nextLexicalNode as any)[property];
472
+
473
+ if (prevValue !== nextValue) {
474
+ if (nextValue instanceof EditorClass) {
475
+ const yjsDocMap = binding.docMap;
476
+ let prevDoc;
477
+
478
+ if (prevValue instanceof EditorClass) {
479
+ const prevKey = prevValue._key;
480
+ prevDoc = yjsDocMap.get(prevKey);
481
+ yjsDocMap.delete(prevKey);
482
+ }
483
+
484
+ // If we already have a document, use it.
485
+ const doc = prevDoc || new Doc();
486
+ const key = doc.guid;
487
+ nextValue._key = key;
488
+ yjsDocMap.set(key, doc);
489
+ nextValue = doc;
490
+ // Mark the node dirty as we've assigned a new key to it
491
+ binding.editor.update(() => {
492
+ nextLexicalNode.markDirty();
493
+ });
494
+ }
495
+
496
+ sharedTypeSet(sharedType, property, nextValue);
497
+ }
498
+ }
499
+ }
500
+
501
+ export function spliceString(
502
+ str: string,
503
+ index: number,
504
+ delCount: number,
505
+ newText: string,
506
+ ): string {
507
+ return str.slice(0, index) + newText + str.slice(index + delCount);
508
+ }
509
+
510
+ export function getPositionFromElementAndOffset(
511
+ node: CollabElementNode,
512
+ offset: number,
513
+ boundaryIsEdge: boolean,
514
+ ): {
515
+ length: number;
516
+ node:
517
+ | CollabElementNode
518
+ | CollabTextNode
519
+ | CollabDecoratorNode
520
+ | CollabLineBreakNode
521
+ | null;
522
+ nodeIndex: number;
523
+ offset: number;
524
+ } {
525
+ let index = 0;
526
+ let i = 0;
527
+ const children = node._children;
528
+ const childrenLength = children.length;
529
+
530
+ for (; i < childrenLength; i++) {
531
+ const child = children[i];
532
+ const childOffset = index;
533
+ const size = child.getSize();
534
+ index += size;
535
+ const exceedsBoundary = boundaryIsEdge ? index >= offset : index > offset;
536
+
537
+ if (exceedsBoundary && child instanceof CollabTextNode) {
538
+ let textOffset = offset - childOffset - 1;
539
+
540
+ if (textOffset < 0) {
541
+ textOffset = 0;
542
+ }
543
+
544
+ const diffLength = index - offset;
545
+ return {
546
+ length: diffLength,
547
+ node: child,
548
+ nodeIndex: i,
549
+ offset: textOffset,
550
+ };
551
+ }
552
+
553
+ if (index > offset) {
554
+ return {
555
+ length: 0,
556
+ node: child,
557
+ nodeIndex: i,
558
+ offset: childOffset,
559
+ };
560
+ } else if (i === childrenLength - 1) {
561
+ return {
562
+ length: 0,
563
+ node: null,
564
+ nodeIndex: i + 1,
565
+ offset: childOffset + 1,
566
+ };
567
+ }
568
+ }
569
+
570
+ return {
571
+ length: 0,
572
+ node: null,
573
+ nodeIndex: 0,
574
+ offset: 0,
575
+ };
576
+ }
577
+
578
+ export function doesSelectionNeedRecovering(
579
+ selection: RangeSelection,
580
+ ): boolean {
581
+ const anchor = selection.anchor;
582
+ const focus = selection.focus;
583
+ let recoveryNeeded = false;
584
+
585
+ try {
586
+ const anchorNode = anchor.getNode();
587
+ const focusNode = focus.getNode();
588
+
589
+ if (
590
+ // We might have removed a node that no longer exists
591
+ !anchorNode.isAttached() ||
592
+ !focusNode.isAttached() ||
593
+ // If we've split a node, then the offset might not be right
594
+ ($isTextNode(anchorNode) &&
595
+ anchor.offset > anchorNode.getTextContentSize()) ||
596
+ ($isTextNode(focusNode) && focus.offset > focusNode.getTextContentSize())
597
+ ) {
598
+ recoveryNeeded = true;
599
+ }
600
+ } catch (_e) {
601
+ // Sometimes checking nor a node via getNode might trigger
602
+ // an error, so we need recovery then too.
603
+ recoveryNeeded = true;
604
+ }
605
+
606
+ return recoveryNeeded;
607
+ }
608
+
609
+ export function syncWithTransaction(
610
+ binding: BaseBinding,
611
+ fn: () => void,
612
+ ): void {
613
+ binding.doc.transact(fn, binding);
614
+ }
615
+
616
+ export function $moveSelectionToPreviousNode(
617
+ anchorNodeKey: string,
618
+ currentEditorState: EditorState,
619
+ ) {
620
+ const anchorNode = currentEditorState._nodeMap.get(anchorNodeKey);
621
+ if (!anchorNode) {
622
+ $getRoot().selectStart();
623
+ return;
624
+ }
625
+ // Get previous node
626
+ const prevNodeKey = anchorNode.__prev;
627
+ let prevNode: ElementNode | null = null;
628
+ if (prevNodeKey) {
629
+ prevNode = $getNodeByKey(prevNodeKey);
630
+ }
631
+
632
+ // If previous node not found, get parent node
633
+ if (prevNode === null && anchorNode.__parent !== null) {
634
+ prevNode = $getNodeByKey(anchorNode.__parent);
635
+ }
636
+ if (prevNode === null) {
637
+ $getRoot().selectStart();
638
+ return;
639
+ }
640
+
641
+ if (prevNode !== null && prevNode.isAttached()) {
642
+ prevNode.selectEnd();
643
+ return;
644
+ } else {
645
+ // If the found node is also deleted, select the next one
646
+ $moveSelectionToPreviousNode(prevNode.__key, currentEditorState);
647
+ }
648
+ }