@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
@@ -0,0 +1,696 @@
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 {Binding} from '.';
10
+ import type {ElementNode, NodeKey, NodeMap} from 'lexical';
11
+ import type {AbstractType, Map as YMap, XmlElement, XmlText} from 'yjs';
12
+
13
+ import invariant from '@lexical/internal/invariant';
14
+ import {
15
+ $createChildrenArray,
16
+ $getNodeByKey,
17
+ $getNodeByKeyOrThrow,
18
+ $isDecoratorNode,
19
+ $isElementNode,
20
+ $isTextNode,
21
+ removeFromParent,
22
+ } from 'lexical';
23
+
24
+ import {CollabDecoratorNode} from './CollabDecoratorNode';
25
+ import {CollabLineBreakNode} from './CollabLineBreakNode';
26
+ import {CollabTextNode} from './CollabTextNode';
27
+ import {
28
+ $createCollabNodeFromLexicalNode,
29
+ $getOrInitCollabNodeFromSharedType,
30
+ $syncPropertiesFromYjs,
31
+ createLexicalNodeFromCollabNode,
32
+ getPositionFromElementAndOffset,
33
+ spliceString,
34
+ syncPropertiesFromLexical,
35
+ } from './Utils';
36
+
37
+ type IntentionallyMarkedAsDirtyElement = boolean;
38
+
39
+ export class CollabElementNode {
40
+ _key: NodeKey;
41
+ _children: Array<
42
+ | CollabElementNode
43
+ | CollabTextNode
44
+ | CollabDecoratorNode
45
+ | CollabLineBreakNode
46
+ >;
47
+ _xmlText: XmlText;
48
+ _type: string;
49
+ _parent: null | CollabElementNode;
50
+
51
+ constructor(
52
+ xmlText: XmlText,
53
+ parent: null | CollabElementNode,
54
+ type: string,
55
+ ) {
56
+ this._key = '';
57
+ this._children = [];
58
+ this._xmlText = xmlText;
59
+ this._type = type;
60
+ this._parent = parent;
61
+ }
62
+
63
+ getPrevNode(nodeMap: null | NodeMap): null | ElementNode {
64
+ if (nodeMap === null) {
65
+ return null;
66
+ }
67
+
68
+ const node = nodeMap.get(this._key);
69
+ return $isElementNode(node) ? node : null;
70
+ }
71
+
72
+ getNode(): null | ElementNode {
73
+ const node = $getNodeByKey(this._key);
74
+ return $isElementNode(node) ? node : null;
75
+ }
76
+
77
+ getSharedType(): XmlText {
78
+ return this._xmlText;
79
+ }
80
+
81
+ getType(): string {
82
+ return this._type;
83
+ }
84
+
85
+ getKey(): NodeKey {
86
+ return this._key;
87
+ }
88
+
89
+ isEmpty(): boolean {
90
+ return this._children.length === 0;
91
+ }
92
+
93
+ getSize(): number {
94
+ return 1;
95
+ }
96
+
97
+ getOffset(): number {
98
+ const collabElementNode = this._parent;
99
+ invariant(
100
+ collabElementNode !== null,
101
+ 'getOffset: could not find collab element node',
102
+ );
103
+
104
+ return collabElementNode.getChildOffset(this);
105
+ }
106
+
107
+ syncPropertiesFromYjs(
108
+ binding: Binding,
109
+ keysChanged: null | Set<string>,
110
+ ): void {
111
+ const lexicalNode = this.getNode();
112
+ invariant(
113
+ lexicalNode !== null,
114
+ 'syncPropertiesFromYjs: could not find element node',
115
+ );
116
+ $syncPropertiesFromYjs(binding, this._xmlText, lexicalNode, keysChanged);
117
+ }
118
+
119
+ applyChildrenYjsDelta(
120
+ binding: Binding,
121
+ deltas: Array<{
122
+ insert?: string | object | AbstractType<unknown>;
123
+ delete?: number;
124
+ retain?: number;
125
+ attributes?: {
126
+ [x: string]: unknown;
127
+ };
128
+ }>,
129
+ ): void {
130
+ const children = this._children;
131
+ let currIndex = 0;
132
+ let pendingSplitText = null;
133
+
134
+ for (let i = 0; i < deltas.length; i++) {
135
+ const delta = deltas[i];
136
+ const insertDelta = delta.insert;
137
+ const deleteDelta = delta.delete;
138
+
139
+ if (delta.retain != null) {
140
+ currIndex += delta.retain;
141
+ } else if (typeof deleteDelta === 'number') {
142
+ let deletionSize = deleteDelta;
143
+
144
+ while (deletionSize > 0) {
145
+ const {node, nodeIndex, offset, length} =
146
+ getPositionFromElementAndOffset(this, currIndex, false);
147
+
148
+ if (
149
+ node instanceof CollabElementNode ||
150
+ node instanceof CollabLineBreakNode ||
151
+ node instanceof CollabDecoratorNode
152
+ ) {
153
+ children.splice(nodeIndex, 1);
154
+ deletionSize -= 1;
155
+ } else if (node instanceof CollabTextNode) {
156
+ const delCount = Math.min(deletionSize, length);
157
+ const prevCollabNode =
158
+ nodeIndex !== 0 ? children[nodeIndex - 1] : null;
159
+ const nodeSize = node.getSize();
160
+
161
+ if (offset === 0 && length === nodeSize) {
162
+ // Text node has been deleted.
163
+ children.splice(nodeIndex, 1);
164
+ // If this was caused by an undo from YJS, there could be dangling text.
165
+ const danglingText = spliceString(
166
+ node._text,
167
+ offset,
168
+ delCount - 1,
169
+ '',
170
+ );
171
+ if (danglingText.length > 0) {
172
+ if (prevCollabNode instanceof CollabTextNode) {
173
+ // Merge the text node with previous.
174
+ prevCollabNode._text += danglingText;
175
+ } else {
176
+ // No previous text node to merge into, just delete the text.
177
+ this._xmlText.delete(offset, danglingText.length);
178
+ }
179
+ }
180
+ } else {
181
+ node._text = spliceString(node._text, offset, delCount, '');
182
+ }
183
+
184
+ deletionSize -= delCount;
185
+ } else {
186
+ // Can occur due to the deletion from the dangling text heuristic below.
187
+ break;
188
+ }
189
+ }
190
+ } else if (insertDelta != null) {
191
+ if (typeof insertDelta === 'string') {
192
+ const {node, offset} = getPositionFromElementAndOffset(
193
+ this,
194
+ currIndex,
195
+ true,
196
+ );
197
+
198
+ if (node instanceof CollabTextNode) {
199
+ node._text = spliceString(node._text, offset, 0, insertDelta);
200
+ } else {
201
+ // TODO: maybe we can improve this by keeping around a redundant
202
+ // text node map, rather than removing all the text nodes, so there
203
+ // never can be dangling text.
204
+
205
+ // We have a conflict where there was likely a CollabTextNode and
206
+ // an Lexical TextNode too, but they were removed in a merge. So
207
+ // let's just ignore the text and trigger a removal for it from our
208
+ // shared type.
209
+ this._xmlText.delete(offset, insertDelta.length);
210
+ }
211
+
212
+ currIndex += insertDelta.length;
213
+ } else {
214
+ const sharedType = insertDelta;
215
+ const {node, nodeIndex, length} = getPositionFromElementAndOffset(
216
+ this,
217
+ currIndex,
218
+ false,
219
+ );
220
+ const collabNode = $getOrInitCollabNodeFromSharedType(
221
+ binding,
222
+ sharedType as XmlText | YMap<unknown> | XmlElement,
223
+ this,
224
+ );
225
+ if (
226
+ node instanceof CollabTextNode &&
227
+ length > 0 &&
228
+ length < node._text.length
229
+ ) {
230
+ // Trying to insert in the middle of a text node; split the text.
231
+ const text = node._text;
232
+ const splitIdx = text.length - length;
233
+ node._text = spliceString(text, splitIdx, length, '');
234
+ children.splice(nodeIndex + 1, 0, collabNode);
235
+ // The insert that triggers the text split might not be a text node. Need to keep a
236
+ // reference to the remaining text so that it can be added when we do create one.
237
+ pendingSplitText = spliceString(text, 0, splitIdx, '');
238
+ } else {
239
+ children.splice(nodeIndex, 0, collabNode);
240
+ }
241
+ if (
242
+ pendingSplitText !== null &&
243
+ collabNode instanceof CollabTextNode
244
+ ) {
245
+ // Found a text node to insert the pending text into.
246
+ collabNode._text = pendingSplitText + collabNode._text;
247
+ pendingSplitText = null;
248
+ }
249
+ currIndex += 1;
250
+ }
251
+ } else {
252
+ throw new Error('Unexpected delta format');
253
+ }
254
+ }
255
+ }
256
+
257
+ syncChildrenFromYjs(binding: Binding): void {
258
+ // Now diff the children of the collab node with that of our existing Lexical node.
259
+ const lexicalNode = this.getNode();
260
+ invariant(
261
+ lexicalNode !== null,
262
+ 'syncChildrenFromYjs: could not find element node',
263
+ );
264
+
265
+ const key = lexicalNode.__key;
266
+ const prevLexicalChildrenKeys = $createChildrenArray(lexicalNode, null);
267
+ const nextLexicalChildrenKeys: Array<NodeKey> = [];
268
+ const lexicalChildrenKeysLength = prevLexicalChildrenKeys.length;
269
+ const collabChildren = this._children;
270
+ const collabChildrenLength = collabChildren.length;
271
+ const collabNodeMap = binding.collabNodeMap;
272
+ const visitedKeys = new Set();
273
+ let collabKeys;
274
+ let writableLexicalNode;
275
+ let prevIndex = 0;
276
+ let prevChildNode = null;
277
+
278
+ if (collabChildrenLength !== lexicalChildrenKeysLength) {
279
+ writableLexicalNode = lexicalNode.getWritable();
280
+ }
281
+
282
+ for (let i = 0; i < collabChildrenLength; i++) {
283
+ const lexicalChildKey = prevLexicalChildrenKeys[prevIndex];
284
+ const childCollabNode = collabChildren[i];
285
+ const collabLexicalChildNode = childCollabNode.getNode();
286
+ const collabKey = childCollabNode._key;
287
+
288
+ if (collabLexicalChildNode !== null && lexicalChildKey === collabKey) {
289
+ const childNeedsUpdating = $isTextNode(collabLexicalChildNode);
290
+ // Update
291
+ visitedKeys.add(lexicalChildKey);
292
+
293
+ if (childNeedsUpdating) {
294
+ childCollabNode._key = lexicalChildKey;
295
+
296
+ if (childCollabNode instanceof CollabElementNode) {
297
+ const xmlText = childCollabNode._xmlText;
298
+ childCollabNode.syncPropertiesFromYjs(binding, null);
299
+ childCollabNode.applyChildrenYjsDelta(binding, xmlText.toDelta());
300
+ childCollabNode.syncChildrenFromYjs(binding);
301
+ } else if (childCollabNode instanceof CollabTextNode) {
302
+ childCollabNode.syncPropertiesAndTextFromYjs(binding, null);
303
+ } else if (childCollabNode instanceof CollabDecoratorNode) {
304
+ childCollabNode.syncPropertiesFromYjs(binding, null);
305
+ } else if (!(childCollabNode instanceof CollabLineBreakNode)) {
306
+ invariant(
307
+ false,
308
+ 'syncChildrenFromYjs: expected text, element, decorator, or linebreak collab node',
309
+ );
310
+ }
311
+ }
312
+
313
+ nextLexicalChildrenKeys[i] = lexicalChildKey;
314
+ prevChildNode = collabLexicalChildNode;
315
+ prevIndex++;
316
+ } else {
317
+ if (collabKeys === undefined) {
318
+ collabKeys = new Set();
319
+
320
+ for (let s = 0; s < collabChildrenLength; s++) {
321
+ const child = collabChildren[s];
322
+ const childKey = child._key;
323
+
324
+ if (childKey !== '') {
325
+ collabKeys.add(childKey);
326
+ }
327
+ }
328
+ }
329
+
330
+ if (
331
+ collabLexicalChildNode !== null &&
332
+ lexicalChildKey !== undefined &&
333
+ !collabKeys.has(lexicalChildKey)
334
+ ) {
335
+ const nodeToRemove = $getNodeByKeyOrThrow(lexicalChildKey);
336
+ removeFromParent(nodeToRemove);
337
+ i--;
338
+ prevIndex++;
339
+ continue;
340
+ }
341
+
342
+ writableLexicalNode = lexicalNode.getWritable();
343
+ // Create/Replace
344
+ const lexicalChildNode = createLexicalNodeFromCollabNode(
345
+ binding,
346
+ childCollabNode,
347
+ key,
348
+ );
349
+ const childKey = lexicalChildNode.__key;
350
+ collabNodeMap.set(childKey, childCollabNode);
351
+ nextLexicalChildrenKeys[i] = childKey;
352
+ if (prevChildNode === null) {
353
+ const nextSibling = writableLexicalNode.getFirstChild();
354
+ writableLexicalNode.__first = childKey;
355
+ if (nextSibling !== null) {
356
+ const writableNextSibling = nextSibling.getWritable();
357
+ writableNextSibling.__prev = childKey;
358
+ lexicalChildNode.__next = writableNextSibling.__key;
359
+ }
360
+ } else {
361
+ const writablePrevChildNode = prevChildNode.getWritable();
362
+ const nextSibling = prevChildNode.getNextSibling();
363
+ writablePrevChildNode.__next = childKey;
364
+ lexicalChildNode.__prev = prevChildNode.__key;
365
+ if (nextSibling !== null) {
366
+ const writableNextSibling = nextSibling.getWritable();
367
+ writableNextSibling.__prev = childKey;
368
+ lexicalChildNode.__next = writableNextSibling.__key;
369
+ }
370
+ }
371
+ if (i === collabChildrenLength - 1) {
372
+ writableLexicalNode.__last = childKey;
373
+ }
374
+ writableLexicalNode.__size++;
375
+ prevChildNode = lexicalChildNode;
376
+ }
377
+ }
378
+
379
+ for (let i = 0; i < lexicalChildrenKeysLength; i++) {
380
+ const lexicalChildKey = prevLexicalChildrenKeys[i];
381
+
382
+ if (!visitedKeys.has(lexicalChildKey)) {
383
+ // Remove
384
+ const lexicalChildNode = $getNodeByKeyOrThrow(lexicalChildKey);
385
+ const collabNode = binding.collabNodeMap.get(lexicalChildKey);
386
+
387
+ if (collabNode !== undefined) {
388
+ collabNode.destroy(binding);
389
+ }
390
+ removeFromParent(lexicalChildNode);
391
+ }
392
+ }
393
+ }
394
+
395
+ syncPropertiesFromLexical(
396
+ binding: Binding,
397
+ nextLexicalNode: ElementNode,
398
+ prevNodeMap: null | NodeMap,
399
+ ): void {
400
+ syncPropertiesFromLexical(
401
+ binding,
402
+ this._xmlText,
403
+ this.getPrevNode(prevNodeMap),
404
+ nextLexicalNode,
405
+ );
406
+ }
407
+
408
+ _syncChildFromLexical(
409
+ binding: Binding,
410
+ index: number,
411
+ key: NodeKey,
412
+ prevNodeMap: null | NodeMap,
413
+ dirtyElements: null | Map<NodeKey, IntentionallyMarkedAsDirtyElement>,
414
+ dirtyLeaves: null | Set<NodeKey>,
415
+ ): void {
416
+ const childCollabNode = this._children[index];
417
+ // Update
418
+ const nextChildNode = $getNodeByKeyOrThrow(key);
419
+
420
+ if (
421
+ childCollabNode instanceof CollabElementNode &&
422
+ $isElementNode(nextChildNode)
423
+ ) {
424
+ childCollabNode.syncPropertiesFromLexical(
425
+ binding,
426
+ nextChildNode,
427
+ prevNodeMap,
428
+ );
429
+ childCollabNode.syncChildrenFromLexical(
430
+ binding,
431
+ nextChildNode,
432
+ prevNodeMap,
433
+ dirtyElements,
434
+ dirtyLeaves,
435
+ );
436
+ } else if (
437
+ childCollabNode instanceof CollabTextNode &&
438
+ $isTextNode(nextChildNode)
439
+ ) {
440
+ childCollabNode.syncPropertiesAndTextFromLexical(
441
+ binding,
442
+ nextChildNode,
443
+ prevNodeMap,
444
+ );
445
+ } else if (
446
+ childCollabNode instanceof CollabDecoratorNode &&
447
+ $isDecoratorNode(nextChildNode)
448
+ ) {
449
+ childCollabNode.syncPropertiesFromLexical(
450
+ binding,
451
+ nextChildNode,
452
+ prevNodeMap,
453
+ );
454
+ }
455
+ }
456
+
457
+ syncChildrenFromLexical(
458
+ binding: Binding,
459
+ nextLexicalNode: ElementNode,
460
+ prevNodeMap: null | NodeMap,
461
+ dirtyElements: null | Map<NodeKey, IntentionallyMarkedAsDirtyElement>,
462
+ dirtyLeaves: null | Set<NodeKey>,
463
+ ): void {
464
+ const prevLexicalNode = this.getPrevNode(prevNodeMap);
465
+ const prevChildren =
466
+ prevLexicalNode === null
467
+ ? []
468
+ : $createChildrenArray(prevLexicalNode, prevNodeMap);
469
+ const nextChildren = $createChildrenArray(nextLexicalNode, null);
470
+ const prevEndIndex = prevChildren.length - 1;
471
+ const nextEndIndex = nextChildren.length - 1;
472
+ const collabNodeMap = binding.collabNodeMap;
473
+ let prevChildrenSet: Set<NodeKey> | undefined;
474
+ let nextChildrenSet: Set<NodeKey> | undefined;
475
+ let prevIndex = 0;
476
+ let nextIndex = 0;
477
+
478
+ while (prevIndex <= prevEndIndex && nextIndex <= nextEndIndex) {
479
+ const prevKey = prevChildren[prevIndex];
480
+ const nextKey = nextChildren[nextIndex];
481
+
482
+ if (prevKey === nextKey) {
483
+ // Nove move, create or remove
484
+ this._syncChildFromLexical(
485
+ binding,
486
+ nextIndex,
487
+ nextKey,
488
+ prevNodeMap,
489
+ dirtyElements,
490
+ dirtyLeaves,
491
+ );
492
+
493
+ prevIndex++;
494
+ nextIndex++;
495
+ } else {
496
+ if (prevChildrenSet === undefined) {
497
+ prevChildrenSet = new Set(prevChildren);
498
+ }
499
+
500
+ if (nextChildrenSet === undefined) {
501
+ nextChildrenSet = new Set(nextChildren);
502
+ }
503
+
504
+ const nextHasPrevKey = nextChildrenSet.has(prevKey);
505
+ const prevHasNextKey = prevChildrenSet.has(nextKey);
506
+
507
+ if (!nextHasPrevKey) {
508
+ // Remove
509
+ this.splice(binding, nextIndex, 1);
510
+ prevIndex++;
511
+ } else {
512
+ // Create or replace
513
+ const nextChildNode = $getNodeByKeyOrThrow(nextKey);
514
+ const collabNode = $createCollabNodeFromLexicalNode(
515
+ binding,
516
+ nextChildNode,
517
+ this,
518
+ );
519
+ collabNodeMap.set(nextKey, collabNode);
520
+
521
+ if (prevHasNextKey) {
522
+ this.splice(binding, nextIndex, 1, collabNode);
523
+ prevIndex++;
524
+ nextIndex++;
525
+ } else {
526
+ this.splice(binding, nextIndex, 0, collabNode);
527
+ nextIndex++;
528
+ }
529
+ }
530
+ }
531
+ }
532
+
533
+ const appendNewChildren = prevIndex > prevEndIndex;
534
+ const removeOldChildren = nextIndex > nextEndIndex;
535
+
536
+ if (appendNewChildren && !removeOldChildren) {
537
+ for (; nextIndex <= nextEndIndex; ++nextIndex) {
538
+ const key = nextChildren[nextIndex];
539
+ const nextChildNode = $getNodeByKeyOrThrow(key);
540
+ const collabNode = $createCollabNodeFromLexicalNode(
541
+ binding,
542
+ nextChildNode,
543
+ this,
544
+ );
545
+ this.append(collabNode);
546
+ collabNodeMap.set(key, collabNode);
547
+ }
548
+ } else if (removeOldChildren && !appendNewChildren) {
549
+ for (let i = this._children.length - 1; i >= nextIndex; i--) {
550
+ this.splice(binding, i, 1);
551
+ }
552
+ }
553
+ }
554
+
555
+ append(
556
+ collabNode:
557
+ | CollabElementNode
558
+ | CollabDecoratorNode
559
+ | CollabTextNode
560
+ | CollabLineBreakNode,
561
+ ): void {
562
+ const xmlText = this._xmlText;
563
+ const children = this._children;
564
+ const lastChild = children[children.length - 1];
565
+ const offset =
566
+ lastChild !== undefined ? lastChild.getOffset() + lastChild.getSize() : 0;
567
+
568
+ if (collabNode instanceof CollabElementNode) {
569
+ xmlText.insertEmbed(offset, collabNode._xmlText);
570
+ } else if (collabNode instanceof CollabTextNode) {
571
+ const map = collabNode._map;
572
+
573
+ if (map.parent === null) {
574
+ xmlText.insertEmbed(offset, map);
575
+ }
576
+
577
+ xmlText.insert(offset + 1, collabNode._text);
578
+ } else if (collabNode instanceof CollabLineBreakNode) {
579
+ xmlText.insertEmbed(offset, collabNode._map);
580
+ } else if (collabNode instanceof CollabDecoratorNode) {
581
+ xmlText.insertEmbed(offset, collabNode._xmlElem);
582
+ }
583
+
584
+ this._children.push(collabNode);
585
+ }
586
+
587
+ splice(
588
+ binding: Binding,
589
+ index: number,
590
+ delCount: number,
591
+ collabNode?:
592
+ | CollabElementNode
593
+ | CollabDecoratorNode
594
+ | CollabTextNode
595
+ | CollabLineBreakNode,
596
+ ): void {
597
+ const children = this._children;
598
+ const child = children[index];
599
+
600
+ if (child === undefined) {
601
+ invariant(
602
+ collabNode !== undefined,
603
+ 'splice: could not find collab element node',
604
+ );
605
+ this.append(collabNode);
606
+ return;
607
+ }
608
+
609
+ const offset = child.getOffset();
610
+ invariant(offset !== -1, 'splice: expected offset to be greater than zero');
611
+
612
+ const xmlText = this._xmlText;
613
+
614
+ if (delCount !== 0) {
615
+ // What if we delete many nodes, don't we need to get all their
616
+ // sizes?
617
+ xmlText.delete(offset, child.getSize());
618
+ }
619
+
620
+ if (collabNode instanceof CollabElementNode) {
621
+ xmlText.insertEmbed(offset, collabNode._xmlText);
622
+ } else if (collabNode instanceof CollabTextNode) {
623
+ const map = collabNode._map;
624
+
625
+ if (map.parent === null) {
626
+ xmlText.insertEmbed(offset, map);
627
+ }
628
+
629
+ xmlText.insert(offset + 1, collabNode._text);
630
+ } else if (collabNode instanceof CollabLineBreakNode) {
631
+ xmlText.insertEmbed(offset, collabNode._map);
632
+ } else if (collabNode instanceof CollabDecoratorNode) {
633
+ xmlText.insertEmbed(offset, collabNode._xmlElem);
634
+ }
635
+
636
+ if (delCount !== 0) {
637
+ const childrenToDelete = children.slice(index, index + delCount);
638
+
639
+ for (let i = 0; i < childrenToDelete.length; i++) {
640
+ childrenToDelete[i].destroy(binding);
641
+ }
642
+ }
643
+
644
+ if (collabNode !== undefined) {
645
+ children.splice(index, delCount, collabNode);
646
+ } else {
647
+ children.splice(index, delCount);
648
+ }
649
+ }
650
+
651
+ getChildOffset(
652
+ collabNode:
653
+ | CollabElementNode
654
+ | CollabTextNode
655
+ | CollabDecoratorNode
656
+ | CollabLineBreakNode,
657
+ ): number {
658
+ let offset = 0;
659
+ const children = this._children;
660
+
661
+ for (let i = 0; i < children.length; i++) {
662
+ const child = children[i];
663
+
664
+ if (child === collabNode) {
665
+ return offset;
666
+ }
667
+
668
+ offset += child.getSize();
669
+ }
670
+
671
+ return -1;
672
+ }
673
+
674
+ destroy(binding: Binding): void {
675
+ const collabNodeMap = binding.collabNodeMap;
676
+ const children = this._children;
677
+
678
+ for (let i = 0; i < children.length; i++) {
679
+ children[i].destroy(binding);
680
+ }
681
+
682
+ if (collabNodeMap.get(this._key) === this) {
683
+ collabNodeMap.delete(this._key);
684
+ }
685
+ }
686
+ }
687
+
688
+ export function $createCollabElementNode(
689
+ xmlText: XmlText,
690
+ parent: null | CollabElementNode,
691
+ type: string,
692
+ ): CollabElementNode {
693
+ const collabNode = new CollabElementNode(xmlText, parent, type);
694
+ xmlText._collabNode = collabNode;
695
+ return collabNode;
696
+ }