@lexical/yjs 0.13.0 → 0.14.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,1599 @@
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 { $getNodeByKey, $isLineBreakNode, $isTextNode, $getSelection, $isRangeSelection, createEditor, $isElementNode, $isRootNode, $isDecoratorNode, $setSelection, $getRoot, $createParagraphNode, createCommand } from 'lexical';
8
+ import { XmlText, Map as Map$1, XmlElement, Doc, createAbsolutePositionFromRelativePosition, createRelativePositionFromTypeIndex, compareRelativePositions, YTextEvent, YMapEvent, YXmlEvent, UndoManager } from 'yjs';
9
+ import { createDOMRange, createRectsFromDOMRange } from '@lexical/selection';
10
+ import { $createOffsetView } from '@lexical/offset';
11
+
12
+ /**
13
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
14
+ *
15
+ * This source code is licensed under the MIT license found in the
16
+ * LICENSE file in the root directory of this source tree.
17
+ *
18
+ */
19
+ class CollabLineBreakNode {
20
+ constructor(map, parent) {
21
+ this._key = '';
22
+ this._map = map;
23
+ this._parent = parent;
24
+ this._type = 'linebreak';
25
+ }
26
+ getNode() {
27
+ const node = $getNodeByKey(this._key);
28
+ return $isLineBreakNode(node) ? node : null;
29
+ }
30
+ getKey() {
31
+ return this._key;
32
+ }
33
+ getSharedType() {
34
+ return this._map;
35
+ }
36
+ getType() {
37
+ return this._type;
38
+ }
39
+ getSize() {
40
+ return 1;
41
+ }
42
+ getOffset() {
43
+ const collabElementNode = this._parent;
44
+ return collabElementNode.getChildOffset(this);
45
+ }
46
+ destroy(binding) {
47
+ const collabNodeMap = binding.collabNodeMap;
48
+ collabNodeMap.delete(this._key);
49
+ }
50
+ }
51
+ function $createCollabLineBreakNode(map, parent) {
52
+ const collabNode = new CollabLineBreakNode(map, parent);
53
+ map._collabNode = collabNode;
54
+ return collabNode;
55
+ }
56
+
57
+ /**
58
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
59
+ *
60
+ * This source code is licensed under the MIT license found in the
61
+ * LICENSE file in the root directory of this source tree.
62
+ *
63
+ */
64
+
65
+ function simpleDiffWithCursor(a, b, cursor) {
66
+ const aLength = a.length;
67
+ const bLength = b.length;
68
+ let left = 0; // number of same characters counting from left
69
+ let right = 0; // number of same characters counting from right
70
+ // Iterate left to the right until we find a changed character
71
+ // First iteration considers the current cursor position
72
+ while (left < aLength && left < bLength && a[left] === b[left] && left < cursor) {
73
+ left++;
74
+ }
75
+ // Iterate right to the left until we find a changed character
76
+ while (right + left < aLength && right + left < bLength && a[aLength - right - 1] === b[bLength - right - 1]) {
77
+ right++;
78
+ }
79
+ // Try to iterate left further to the right without caring about the current cursor position
80
+ while (right + left < aLength && right + left < bLength && a[left] === b[left]) {
81
+ left++;
82
+ }
83
+ return {
84
+ index: left,
85
+ insert: b.slice(left, bLength - right),
86
+ remove: aLength - left - right
87
+ };
88
+ }
89
+
90
+ /**
91
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
92
+ *
93
+ * This source code is licensed under the MIT license found in the
94
+ * LICENSE file in the root directory of this source tree.
95
+ *
96
+ */
97
+ function diffTextContentAndApplyDelta(collabNode, key, prevText, nextText) {
98
+ const selection = $getSelection();
99
+ let cursorOffset = nextText.length;
100
+ if ($isRangeSelection(selection) && selection.isCollapsed()) {
101
+ const anchor = selection.anchor;
102
+ if (anchor.key === key) {
103
+ cursorOffset = anchor.offset;
104
+ }
105
+ }
106
+ const diff = simpleDiffWithCursor(prevText, nextText, cursorOffset);
107
+ collabNode.spliceText(diff.index, diff.remove, diff.insert);
108
+ }
109
+ class CollabTextNode {
110
+ constructor(map, text, parent, type) {
111
+ this._key = '';
112
+ this._map = map;
113
+ this._parent = parent;
114
+ this._text = text;
115
+ this._type = type;
116
+ this._normalized = false;
117
+ }
118
+ getPrevNode(nodeMap) {
119
+ if (nodeMap === null) {
120
+ return null;
121
+ }
122
+ const node = nodeMap.get(this._key);
123
+ return $isTextNode(node) ? node : null;
124
+ }
125
+ getNode() {
126
+ const node = $getNodeByKey(this._key);
127
+ return $isTextNode(node) ? node : null;
128
+ }
129
+ getSharedType() {
130
+ return this._map;
131
+ }
132
+ getType() {
133
+ return this._type;
134
+ }
135
+ getKey() {
136
+ return this._key;
137
+ }
138
+ getSize() {
139
+ return this._text.length + (this._normalized ? 0 : 1);
140
+ }
141
+ getOffset() {
142
+ const collabElementNode = this._parent;
143
+ return collabElementNode.getChildOffset(this);
144
+ }
145
+ spliceText(index, delCount, newText) {
146
+ const collabElementNode = this._parent;
147
+ const xmlText = collabElementNode._xmlText;
148
+ const offset = this.getOffset() + 1 + index;
149
+ if (delCount !== 0) {
150
+ xmlText.delete(offset, delCount);
151
+ }
152
+ if (newText !== '') {
153
+ xmlText.insert(offset, newText);
154
+ }
155
+ }
156
+ syncPropertiesAndTextFromLexical(binding, nextLexicalNode, prevNodeMap) {
157
+ const prevLexicalNode = this.getPrevNode(prevNodeMap);
158
+ const nextText = nextLexicalNode.__text;
159
+ syncPropertiesFromLexical(binding, this._map, prevLexicalNode, nextLexicalNode);
160
+ if (prevLexicalNode !== null) {
161
+ const prevText = prevLexicalNode.__text;
162
+ if (prevText !== nextText) {
163
+ const key = nextLexicalNode.__key;
164
+ diffTextContentAndApplyDelta(this, key, prevText, nextText);
165
+ this._text = nextText;
166
+ }
167
+ }
168
+ }
169
+ syncPropertiesAndTextFromYjs(binding, keysChanged) {
170
+ const lexicalNode = this.getNode();
171
+ if (!(lexicalNode !== null)) {
172
+ throw Error(`syncPropertiesAndTextFromYjs: could not find decorator node`);
173
+ }
174
+ syncPropertiesFromYjs(binding, this._map, lexicalNode, keysChanged);
175
+ const collabText = this._text;
176
+ if (lexicalNode.__text !== collabText) {
177
+ const writable = lexicalNode.getWritable();
178
+ writable.__text = collabText;
179
+ }
180
+ }
181
+ destroy(binding) {
182
+ const collabNodeMap = binding.collabNodeMap;
183
+ collabNodeMap.delete(this._key);
184
+ }
185
+ }
186
+ function $createCollabTextNode(map, text, parent, type) {
187
+ const collabNode = new CollabTextNode(map, text, parent, type);
188
+ map._collabNode = collabNode;
189
+ return collabNode;
190
+ }
191
+
192
+ /**
193
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
194
+ *
195
+ * This source code is licensed under the MIT license found in the
196
+ * LICENSE file in the root directory of this source tree.
197
+ *
198
+ */
199
+ const baseExcludedProperties = new Set(['__key', '__parent', '__next', '__prev']);
200
+ const elementExcludedProperties = new Set(['__first', '__last', '__size']);
201
+ const rootExcludedProperties = new Set(['__cachedText']);
202
+ const textExcludedProperties = new Set(['__text']);
203
+ function isExcludedProperty(name, node, binding) {
204
+ if (baseExcludedProperties.has(name)) {
205
+ return true;
206
+ }
207
+ if ($isTextNode(node)) {
208
+ if (textExcludedProperties.has(name)) {
209
+ return true;
210
+ }
211
+ } else if ($isElementNode(node)) {
212
+ if (elementExcludedProperties.has(name) || $isRootNode(node) && rootExcludedProperties.has(name)) {
213
+ return true;
214
+ }
215
+ }
216
+ const nodeKlass = node.constructor;
217
+ const excludedProperties = binding.excludedProperties.get(nodeKlass);
218
+ return excludedProperties != null && excludedProperties.has(name);
219
+ }
220
+ function $getNodeByKeyOrThrow(key) {
221
+ const node = $getNodeByKey(key);
222
+ if (!(node !== null)) {
223
+ throw Error(`could not find node by key`);
224
+ }
225
+ return node;
226
+ }
227
+ function $createCollabNodeFromLexicalNode(binding, lexicalNode, parent) {
228
+ const nodeType = lexicalNode.__type;
229
+ let collabNode;
230
+ if ($isElementNode(lexicalNode)) {
231
+ const xmlText = new XmlText();
232
+ collabNode = $createCollabElementNode(xmlText, parent, nodeType);
233
+ collabNode.syncPropertiesFromLexical(binding, lexicalNode, null);
234
+ collabNode.syncChildrenFromLexical(binding, lexicalNode, null, null, null);
235
+ } else if ($isTextNode(lexicalNode)) {
236
+ // TODO create a token text node for token, segmented nodes.
237
+ const map = new Map$1();
238
+ collabNode = $createCollabTextNode(map, lexicalNode.__text, parent, nodeType);
239
+ collabNode.syncPropertiesAndTextFromLexical(binding, lexicalNode, null);
240
+ } else if ($isLineBreakNode(lexicalNode)) {
241
+ const map = new Map$1();
242
+ map.set('__type', 'linebreak');
243
+ collabNode = $createCollabLineBreakNode(map, parent);
244
+ } else if ($isDecoratorNode(lexicalNode)) {
245
+ const xmlElem = new XmlElement();
246
+ collabNode = $createCollabDecoratorNode(xmlElem, parent, nodeType);
247
+ collabNode.syncPropertiesFromLexical(binding, lexicalNode, null);
248
+ } else {
249
+ {
250
+ throw Error(`Expected text, element, decorator, or linebreak node`);
251
+ }
252
+ }
253
+ collabNode._key = lexicalNode.__key;
254
+ return collabNode;
255
+ }
256
+ function getNodeTypeFromSharedType(sharedType) {
257
+ const type = sharedType instanceof Map$1 ? sharedType.get('__type') : sharedType.getAttribute('__type');
258
+ if (!(type != null)) {
259
+ throw Error(`Expected shared type to include type attribute`);
260
+ }
261
+ return type;
262
+ }
263
+ function getOrInitCollabNodeFromSharedType(binding, sharedType, parent) {
264
+ const collabNode = sharedType._collabNode;
265
+ if (collabNode === undefined) {
266
+ const registeredNodes = binding.editor._nodes;
267
+ const type = getNodeTypeFromSharedType(sharedType);
268
+ const nodeInfo = registeredNodes.get(type);
269
+ if (!(nodeInfo !== undefined)) {
270
+ throw Error(`Node ${type} is not registered`);
271
+ }
272
+ const sharedParent = sharedType.parent;
273
+ const targetParent = parent === undefined && sharedParent !== null ? getOrInitCollabNodeFromSharedType(binding, sharedParent) : parent || null;
274
+ if (!(targetParent instanceof CollabElementNode)) {
275
+ throw Error(`Expected parent to be a collab element node`);
276
+ }
277
+ if (sharedType instanceof XmlText) {
278
+ return $createCollabElementNode(sharedType, targetParent, type);
279
+ } else if (sharedType instanceof Map$1) {
280
+ if (type === 'linebreak') {
281
+ return $createCollabLineBreakNode(sharedType, targetParent);
282
+ }
283
+ return $createCollabTextNode(sharedType, '', targetParent, type);
284
+ } else if (sharedType instanceof XmlElement) {
285
+ return $createCollabDecoratorNode(sharedType, targetParent, type);
286
+ }
287
+ }
288
+ return collabNode;
289
+ }
290
+ function createLexicalNodeFromCollabNode(binding, collabNode, parentKey) {
291
+ const type = collabNode.getType();
292
+ const registeredNodes = binding.editor._nodes;
293
+ const nodeInfo = registeredNodes.get(type);
294
+ if (!(nodeInfo !== undefined)) {
295
+ throw Error(`Node ${type} is not registered`);
296
+ }
297
+ const lexicalNode = new nodeInfo.klass();
298
+ lexicalNode.__parent = parentKey;
299
+ collabNode._key = lexicalNode.__key;
300
+ if (collabNode instanceof CollabElementNode) {
301
+ const xmlText = collabNode._xmlText;
302
+ collabNode.syncPropertiesFromYjs(binding, null);
303
+ collabNode.applyChildrenYjsDelta(binding, xmlText.toDelta());
304
+ collabNode.syncChildrenFromYjs(binding);
305
+ } else if (collabNode instanceof CollabTextNode) {
306
+ collabNode.syncPropertiesAndTextFromYjs(binding, null);
307
+ } else if (collabNode instanceof CollabDecoratorNode) {
308
+ collabNode.syncPropertiesFromYjs(binding, null);
309
+ }
310
+ binding.collabNodeMap.set(lexicalNode.__key, collabNode);
311
+ return lexicalNode;
312
+ }
313
+ function syncPropertiesFromYjs(binding, sharedType, lexicalNode, keysChanged) {
314
+ const properties = keysChanged === null ? sharedType instanceof Map$1 ? Array.from(sharedType.keys()) : Object.keys(sharedType.getAttributes()) : Array.from(keysChanged);
315
+ let writableNode;
316
+ for (let i = 0; i < properties.length; i++) {
317
+ const property = properties[i];
318
+ if (isExcludedProperty(property, lexicalNode, binding)) {
319
+ continue;
320
+ }
321
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
322
+ const prevValue = lexicalNode[property];
323
+ let nextValue = sharedType instanceof Map$1 ? sharedType.get(property) : sharedType.getAttribute(property);
324
+ if (prevValue !== nextValue) {
325
+ if (nextValue instanceof Doc) {
326
+ const yjsDocMap = binding.docMap;
327
+ if (prevValue instanceof Doc) {
328
+ yjsDocMap.delete(prevValue.guid);
329
+ }
330
+ const nestedEditor = createEditor();
331
+ const key = nextValue.guid;
332
+ nestedEditor._key = key;
333
+ yjsDocMap.set(key, nextValue);
334
+ nextValue = nestedEditor;
335
+ }
336
+ if (writableNode === undefined) {
337
+ writableNode = lexicalNode.getWritable();
338
+ }
339
+ writableNode[property] = nextValue;
340
+ }
341
+ }
342
+ }
343
+ function syncPropertiesFromLexical(binding, sharedType, prevLexicalNode, nextLexicalNode) {
344
+ const type = nextLexicalNode.__type;
345
+ const nodeProperties = binding.nodeProperties;
346
+ let properties = nodeProperties.get(type);
347
+ if (properties === undefined) {
348
+ properties = Object.keys(nextLexicalNode).filter(property => {
349
+ return !isExcludedProperty(property, nextLexicalNode, binding);
350
+ });
351
+ nodeProperties.set(type, properties);
352
+ }
353
+ const EditorClass = binding.editor.constructor;
354
+ for (let i = 0; i < properties.length; i++) {
355
+ const property = properties[i];
356
+ const prevValue =
357
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
358
+ prevLexicalNode === null ? undefined : prevLexicalNode[property];
359
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
360
+ let nextValue = nextLexicalNode[property];
361
+ if (prevValue !== nextValue) {
362
+ if (nextValue instanceof EditorClass) {
363
+ const yjsDocMap = binding.docMap;
364
+ let prevDoc;
365
+ if (prevValue instanceof EditorClass) {
366
+ const prevKey = prevValue._key;
367
+ prevDoc = yjsDocMap.get(prevKey);
368
+ yjsDocMap.delete(prevKey);
369
+ }
370
+
371
+ // If we already have a document, use it.
372
+ const doc = prevDoc || new Doc();
373
+ const key = doc.guid;
374
+ nextValue._key = key;
375
+ yjsDocMap.set(key, doc);
376
+ nextValue = doc;
377
+ // Mark the node dirty as we've assigned a new key to it
378
+ binding.editor.update(() => {
379
+ nextLexicalNode.markDirty();
380
+ });
381
+ }
382
+ if (sharedType instanceof Map$1) {
383
+ sharedType.set(property, nextValue);
384
+ } else {
385
+ sharedType.setAttribute(property, nextValue);
386
+ }
387
+ }
388
+ }
389
+ }
390
+ function spliceString(str, index, delCount, newText) {
391
+ return str.slice(0, index) + newText + str.slice(index + delCount);
392
+ }
393
+ function getPositionFromElementAndOffset(node, offset, boundaryIsEdge) {
394
+ let index = 0;
395
+ let i = 0;
396
+ const children = node._children;
397
+ const childrenLength = children.length;
398
+ for (; i < childrenLength; i++) {
399
+ const child = children[i];
400
+ const childOffset = index;
401
+ const size = child.getSize();
402
+ index += size;
403
+ const exceedsBoundary = boundaryIsEdge ? index >= offset : index > offset;
404
+ if (exceedsBoundary && child instanceof CollabTextNode) {
405
+ let textOffset = offset - childOffset - 1;
406
+ if (textOffset < 0) {
407
+ textOffset = 0;
408
+ }
409
+ const diffLength = index - offset;
410
+ return {
411
+ length: diffLength,
412
+ node: child,
413
+ nodeIndex: i,
414
+ offset: textOffset
415
+ };
416
+ }
417
+ if (index > offset) {
418
+ return {
419
+ length: 0,
420
+ node: child,
421
+ nodeIndex: i,
422
+ offset: childOffset
423
+ };
424
+ } else if (i === childrenLength - 1) {
425
+ return {
426
+ length: 0,
427
+ node: null,
428
+ nodeIndex: i + 1,
429
+ offset: childOffset + 1
430
+ };
431
+ }
432
+ }
433
+ return {
434
+ length: 0,
435
+ node: null,
436
+ nodeIndex: 0,
437
+ offset: 0
438
+ };
439
+ }
440
+ function doesSelectionNeedRecovering(selection) {
441
+ const anchor = selection.anchor;
442
+ const focus = selection.focus;
443
+ let recoveryNeeded = false;
444
+ try {
445
+ const anchorNode = anchor.getNode();
446
+ const focusNode = focus.getNode();
447
+ if (
448
+ // We might have removed a node that no longer exists
449
+ !anchorNode.isAttached() || !focusNode.isAttached() ||
450
+ // If we've split a node, then the offset might not be right
451
+ $isTextNode(anchorNode) && anchor.offset > anchorNode.getTextContentSize() || $isTextNode(focusNode) && focus.offset > focusNode.getTextContentSize()) {
452
+ recoveryNeeded = true;
453
+ }
454
+ } catch (e) {
455
+ // Sometimes checking nor a node via getNode might trigger
456
+ // an error, so we need recovery then too.
457
+ recoveryNeeded = true;
458
+ }
459
+ return recoveryNeeded;
460
+ }
461
+ function syncWithTransaction(binding, fn) {
462
+ binding.doc.transact(fn, binding);
463
+ }
464
+ function createChildrenArray(element, nodeMap) {
465
+ const children = [];
466
+ let nodeKey = element.__first;
467
+ while (nodeKey !== null) {
468
+ const node = nodeMap === null ? $getNodeByKey(nodeKey) : nodeMap.get(nodeKey);
469
+ if (node === null || node === undefined) {
470
+ {
471
+ throw Error(`createChildrenArray: node does not exist in nodeMap`);
472
+ }
473
+ }
474
+ children.push(nodeKey);
475
+ nodeKey = node.__next;
476
+ }
477
+ return children;
478
+ }
479
+ function removeFromParent(node) {
480
+ const oldParent = node.getParent();
481
+ if (oldParent !== null) {
482
+ const writableNode = node.getWritable();
483
+ const writableParent = oldParent.getWritable();
484
+ const prevSibling = node.getPreviousSibling();
485
+ const nextSibling = node.getNextSibling();
486
+ // TODO: this function duplicates a bunch of operations, can be simplified.
487
+ if (prevSibling === null) {
488
+ if (nextSibling !== null) {
489
+ const writableNextSibling = nextSibling.getWritable();
490
+ writableParent.__first = nextSibling.__key;
491
+ writableNextSibling.__prev = null;
492
+ } else {
493
+ writableParent.__first = null;
494
+ }
495
+ } else {
496
+ const writablePrevSibling = prevSibling.getWritable();
497
+ if (nextSibling !== null) {
498
+ const writableNextSibling = nextSibling.getWritable();
499
+ writableNextSibling.__prev = writablePrevSibling.__key;
500
+ writablePrevSibling.__next = writableNextSibling.__key;
501
+ } else {
502
+ writablePrevSibling.__next = null;
503
+ }
504
+ writableNode.__prev = null;
505
+ }
506
+ if (nextSibling === null) {
507
+ if (prevSibling !== null) {
508
+ const writablePrevSibling = prevSibling.getWritable();
509
+ writableParent.__last = prevSibling.__key;
510
+ writablePrevSibling.__next = null;
511
+ } else {
512
+ writableParent.__last = null;
513
+ }
514
+ } else {
515
+ const writableNextSibling = nextSibling.getWritable();
516
+ if (prevSibling !== null) {
517
+ const writablePrevSibling = prevSibling.getWritable();
518
+ writablePrevSibling.__next = writableNextSibling.__key;
519
+ writableNextSibling.__prev = writablePrevSibling.__key;
520
+ } else {
521
+ writableNextSibling.__prev = null;
522
+ }
523
+ writableNode.__next = null;
524
+ }
525
+ writableParent.__size--;
526
+ writableNode.__parent = null;
527
+ }
528
+ }
529
+
530
+ /**
531
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
532
+ *
533
+ * This source code is licensed under the MIT license found in the
534
+ * LICENSE file in the root directory of this source tree.
535
+ *
536
+ */
537
+ class CollabDecoratorNode {
538
+ constructor(xmlElem, parent, type) {
539
+ this._key = '';
540
+ this._xmlElem = xmlElem;
541
+ this._parent = parent;
542
+ this._type = type;
543
+ }
544
+ getPrevNode(nodeMap) {
545
+ if (nodeMap === null) {
546
+ return null;
547
+ }
548
+ const node = nodeMap.get(this._key);
549
+ return $isDecoratorNode(node) ? node : null;
550
+ }
551
+ getNode() {
552
+ const node = $getNodeByKey(this._key);
553
+ return $isDecoratorNode(node) ? node : null;
554
+ }
555
+ getSharedType() {
556
+ return this._xmlElem;
557
+ }
558
+ getType() {
559
+ return this._type;
560
+ }
561
+ getKey() {
562
+ return this._key;
563
+ }
564
+ getSize() {
565
+ return 1;
566
+ }
567
+ getOffset() {
568
+ const collabElementNode = this._parent;
569
+ return collabElementNode.getChildOffset(this);
570
+ }
571
+ syncPropertiesFromLexical(binding, nextLexicalNode, prevNodeMap) {
572
+ const prevLexicalNode = this.getPrevNode(prevNodeMap);
573
+ const xmlElem = this._xmlElem;
574
+ syncPropertiesFromLexical(binding, xmlElem, prevLexicalNode, nextLexicalNode);
575
+ }
576
+ syncPropertiesFromYjs(binding, keysChanged) {
577
+ const lexicalNode = this.getNode();
578
+ if (!(lexicalNode !== null)) {
579
+ throw Error(`syncPropertiesFromYjs: could not find decorator node`);
580
+ }
581
+ const xmlElem = this._xmlElem;
582
+ syncPropertiesFromYjs(binding, xmlElem, lexicalNode, keysChanged);
583
+ }
584
+ destroy(binding) {
585
+ const collabNodeMap = binding.collabNodeMap;
586
+ collabNodeMap.delete(this._key);
587
+ }
588
+ }
589
+ function $createCollabDecoratorNode(xmlElem, parent, type) {
590
+ const collabNode = new CollabDecoratorNode(xmlElem, parent, type);
591
+ xmlElem._collabNode = collabNode;
592
+ return collabNode;
593
+ }
594
+
595
+ /**
596
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
597
+ *
598
+ * This source code is licensed under the MIT license found in the
599
+ * LICENSE file in the root directory of this source tree.
600
+ *
601
+ */
602
+ class CollabElementNode {
603
+ constructor(xmlText, parent, type) {
604
+ this._key = '';
605
+ this._children = [];
606
+ this._xmlText = xmlText;
607
+ this._type = type;
608
+ this._parent = parent;
609
+ }
610
+ getPrevNode(nodeMap) {
611
+ if (nodeMap === null) {
612
+ return null;
613
+ }
614
+ const node = nodeMap.get(this._key);
615
+ return $isElementNode(node) ? node : null;
616
+ }
617
+ getNode() {
618
+ const node = $getNodeByKey(this._key);
619
+ return $isElementNode(node) ? node : null;
620
+ }
621
+ getSharedType() {
622
+ return this._xmlText;
623
+ }
624
+ getType() {
625
+ return this._type;
626
+ }
627
+ getKey() {
628
+ return this._key;
629
+ }
630
+ isEmpty() {
631
+ return this._children.length === 0;
632
+ }
633
+ getSize() {
634
+ return 1;
635
+ }
636
+ getOffset() {
637
+ const collabElementNode = this._parent;
638
+ if (!(collabElementNode !== null)) {
639
+ throw Error(`getOffset: could not find collab element node`);
640
+ }
641
+ return collabElementNode.getChildOffset(this);
642
+ }
643
+ syncPropertiesFromYjs(binding, keysChanged) {
644
+ const lexicalNode = this.getNode();
645
+ if (!(lexicalNode !== null)) {
646
+ throw Error(`syncPropertiesFromYjs: could not find element node`);
647
+ }
648
+ syncPropertiesFromYjs(binding, this._xmlText, lexicalNode, keysChanged);
649
+ }
650
+ applyChildrenYjsDelta(binding, deltas) {
651
+ const children = this._children;
652
+ let currIndex = 0;
653
+ for (let i = 0; i < deltas.length; i++) {
654
+ const delta = deltas[i];
655
+ const insertDelta = delta.insert;
656
+ const deleteDelta = delta.delete;
657
+ if (delta.retain != null) {
658
+ currIndex += delta.retain;
659
+ } else if (typeof deleteDelta === 'number') {
660
+ let deletionSize = deleteDelta;
661
+ while (deletionSize > 0) {
662
+ const {
663
+ node,
664
+ nodeIndex,
665
+ offset,
666
+ length
667
+ } = getPositionFromElementAndOffset(this, currIndex, false);
668
+ if (node instanceof CollabElementNode || node instanceof CollabLineBreakNode || node instanceof CollabDecoratorNode) {
669
+ children.splice(nodeIndex, 1);
670
+ deletionSize -= 1;
671
+ } else if (node instanceof CollabTextNode) {
672
+ const delCount = Math.min(deletionSize, length);
673
+ const prevCollabNode = nodeIndex !== 0 ? children[nodeIndex - 1] : null;
674
+ const nodeSize = node.getSize();
675
+ if (offset === 0 && delCount === 1 && nodeIndex > 0 && prevCollabNode instanceof CollabTextNode && length === nodeSize &&
676
+ // If the node has no keys, it's been deleted
677
+ Array.from(node._map.keys()).length === 0) {
678
+ // Merge the text node with previous.
679
+ prevCollabNode._text += node._text;
680
+ children.splice(nodeIndex, 1);
681
+ } else if (offset === 0 && delCount === nodeSize) {
682
+ // The entire thing needs removing
683
+ children.splice(nodeIndex, 1);
684
+ } else {
685
+ node._text = spliceString(node._text, offset, delCount, '');
686
+ }
687
+ deletionSize -= delCount;
688
+ } else {
689
+ // Can occur due to the deletion from the dangling text heuristic below.
690
+ break;
691
+ }
692
+ }
693
+ } else if (insertDelta != null) {
694
+ if (typeof insertDelta === 'string') {
695
+ const {
696
+ node,
697
+ offset
698
+ } = getPositionFromElementAndOffset(this, currIndex, true);
699
+ if (node instanceof CollabTextNode) {
700
+ node._text = spliceString(node._text, offset, 0, insertDelta);
701
+ } else {
702
+ // TODO: maybe we can improve this by keeping around a redundant
703
+ // text node map, rather than removing all the text nodes, so there
704
+ // never can be dangling text.
705
+
706
+ // We have a conflict where there was likely a CollabTextNode and
707
+ // an Lexical TextNode too, but they were removed in a merge. So
708
+ // let's just ignore the text and trigger a removal for it from our
709
+ // shared type.
710
+ this._xmlText.delete(offset, insertDelta.length);
711
+ }
712
+ currIndex += insertDelta.length;
713
+ } else {
714
+ const sharedType = insertDelta;
715
+ const {
716
+ nodeIndex
717
+ } = getPositionFromElementAndOffset(this, currIndex, false);
718
+ const collabNode = getOrInitCollabNodeFromSharedType(binding, sharedType, this);
719
+ children.splice(nodeIndex, 0, collabNode);
720
+ currIndex += 1;
721
+ }
722
+ } else {
723
+ throw new Error('Unexpected delta format');
724
+ }
725
+ }
726
+ }
727
+ syncChildrenFromYjs(binding) {
728
+ // Now diff the children of the collab node with that of our existing Lexical node.
729
+ const lexicalNode = this.getNode();
730
+ if (!(lexicalNode !== null)) {
731
+ throw Error(`syncChildrenFromYjs: could not find element node`);
732
+ }
733
+ const key = lexicalNode.__key;
734
+ const prevLexicalChildrenKeys = createChildrenArray(lexicalNode, null);
735
+ const lexicalChildrenKeysLength = prevLexicalChildrenKeys.length;
736
+ const collabChildren = this._children;
737
+ const collabChildrenLength = collabChildren.length;
738
+ const collabNodeMap = binding.collabNodeMap;
739
+ const visitedKeys = new Set();
740
+ let collabKeys;
741
+ let writableLexicalNode;
742
+ let prevIndex = 0;
743
+ let prevChildNode = null;
744
+ if (collabChildrenLength !== lexicalChildrenKeysLength) {
745
+ writableLexicalNode = lexicalNode.getWritable();
746
+ }
747
+ for (let i = 0; i < collabChildrenLength; i++) {
748
+ const lexicalChildKey = prevLexicalChildrenKeys[prevIndex];
749
+ const childCollabNode = collabChildren[i];
750
+ const collabLexicalChildNode = childCollabNode.getNode();
751
+ const collabKey = childCollabNode._key;
752
+ if (collabLexicalChildNode !== null && lexicalChildKey === collabKey) {
753
+ const childNeedsUpdating = $isTextNode(collabLexicalChildNode);
754
+ // Update
755
+ visitedKeys.add(lexicalChildKey);
756
+ if (childNeedsUpdating) {
757
+ childCollabNode._key = lexicalChildKey;
758
+ if (childCollabNode instanceof CollabElementNode) {
759
+ const xmlText = childCollabNode._xmlText;
760
+ childCollabNode.syncPropertiesFromYjs(binding, null);
761
+ childCollabNode.applyChildrenYjsDelta(binding, xmlText.toDelta());
762
+ childCollabNode.syncChildrenFromYjs(binding);
763
+ } else if (childCollabNode instanceof CollabTextNode) {
764
+ childCollabNode.syncPropertiesAndTextFromYjs(binding, null);
765
+ } else if (childCollabNode instanceof CollabDecoratorNode) {
766
+ childCollabNode.syncPropertiesFromYjs(binding, null);
767
+ } else if (!(childCollabNode instanceof CollabLineBreakNode)) {
768
+ {
769
+ throw Error(`syncChildrenFromYjs: expected text, element, decorator, or linebreak collab node`);
770
+ }
771
+ }
772
+ }
773
+ prevChildNode = collabLexicalChildNode;
774
+ prevIndex++;
775
+ } else {
776
+ if (collabKeys === undefined) {
777
+ collabKeys = new Set();
778
+ for (let s = 0; s < collabChildrenLength; s++) {
779
+ const child = collabChildren[s];
780
+ const childKey = child._key;
781
+ if (childKey !== '') {
782
+ collabKeys.add(childKey);
783
+ }
784
+ }
785
+ }
786
+ if (collabLexicalChildNode !== null && lexicalChildKey !== undefined && !collabKeys.has(lexicalChildKey)) {
787
+ const nodeToRemove = $getNodeByKeyOrThrow(lexicalChildKey);
788
+ removeFromParent(nodeToRemove);
789
+ i--;
790
+ prevIndex++;
791
+ continue;
792
+ }
793
+ writableLexicalNode = lexicalNode.getWritable();
794
+ // Create/Replace
795
+ const lexicalChildNode = createLexicalNodeFromCollabNode(binding, childCollabNode, key);
796
+ const childKey = lexicalChildNode.__key;
797
+ collabNodeMap.set(childKey, childCollabNode);
798
+ if (prevChildNode === null) {
799
+ const nextSibling = writableLexicalNode.getFirstChild();
800
+ writableLexicalNode.__first = childKey;
801
+ if (nextSibling !== null) {
802
+ const writableNextSibling = nextSibling.getWritable();
803
+ writableNextSibling.__prev = childKey;
804
+ lexicalChildNode.__next = writableNextSibling.__key;
805
+ }
806
+ } else {
807
+ const writablePrevChildNode = prevChildNode.getWritable();
808
+ const nextSibling = prevChildNode.getNextSibling();
809
+ writablePrevChildNode.__next = childKey;
810
+ lexicalChildNode.__prev = prevChildNode.__key;
811
+ if (nextSibling !== null) {
812
+ const writableNextSibling = nextSibling.getWritable();
813
+ writableNextSibling.__prev = childKey;
814
+ lexicalChildNode.__next = writableNextSibling.__key;
815
+ }
816
+ }
817
+ if (i === collabChildrenLength - 1) {
818
+ writableLexicalNode.__last = childKey;
819
+ }
820
+ writableLexicalNode.__size++;
821
+ prevChildNode = lexicalChildNode;
822
+ }
823
+ }
824
+ for (let i = 0; i < lexicalChildrenKeysLength; i++) {
825
+ const lexicalChildKey = prevLexicalChildrenKeys[i];
826
+ if (!visitedKeys.has(lexicalChildKey)) {
827
+ // Remove
828
+ const lexicalChildNode = $getNodeByKeyOrThrow(lexicalChildKey);
829
+ const collabNode = binding.collabNodeMap.get(lexicalChildKey);
830
+ if (collabNode !== undefined) {
831
+ collabNode.destroy(binding);
832
+ }
833
+ removeFromParent(lexicalChildNode);
834
+ }
835
+ }
836
+ }
837
+ syncPropertiesFromLexical(binding, nextLexicalNode, prevNodeMap) {
838
+ syncPropertiesFromLexical(binding, this._xmlText, this.getPrevNode(prevNodeMap), nextLexicalNode);
839
+ }
840
+ _syncChildFromLexical(binding, index, key, prevNodeMap, dirtyElements, dirtyLeaves) {
841
+ const childCollabNode = this._children[index];
842
+ // Update
843
+ const nextChildNode = $getNodeByKeyOrThrow(key);
844
+ if (childCollabNode instanceof CollabElementNode && $isElementNode(nextChildNode)) {
845
+ childCollabNode.syncPropertiesFromLexical(binding, nextChildNode, prevNodeMap);
846
+ childCollabNode.syncChildrenFromLexical(binding, nextChildNode, prevNodeMap, dirtyElements, dirtyLeaves);
847
+ } else if (childCollabNode instanceof CollabTextNode && $isTextNode(nextChildNode)) {
848
+ childCollabNode.syncPropertiesAndTextFromLexical(binding, nextChildNode, prevNodeMap);
849
+ } else if (childCollabNode instanceof CollabDecoratorNode && $isDecoratorNode(nextChildNode)) {
850
+ childCollabNode.syncPropertiesFromLexical(binding, nextChildNode, prevNodeMap);
851
+ }
852
+ }
853
+ syncChildrenFromLexical(binding, nextLexicalNode, prevNodeMap, dirtyElements, dirtyLeaves) {
854
+ const prevLexicalNode = this.getPrevNode(prevNodeMap);
855
+ const prevChildren = prevLexicalNode === null ? [] : createChildrenArray(prevLexicalNode, prevNodeMap);
856
+ const nextChildren = createChildrenArray(nextLexicalNode, null);
857
+ const prevEndIndex = prevChildren.length - 1;
858
+ const nextEndIndex = nextChildren.length - 1;
859
+ const collabNodeMap = binding.collabNodeMap;
860
+ let prevChildrenSet;
861
+ let nextChildrenSet;
862
+ let prevIndex = 0;
863
+ let nextIndex = 0;
864
+ while (prevIndex <= prevEndIndex && nextIndex <= nextEndIndex) {
865
+ const prevKey = prevChildren[prevIndex];
866
+ const nextKey = nextChildren[nextIndex];
867
+ if (prevKey === nextKey) {
868
+ // Nove move, create or remove
869
+ this._syncChildFromLexical(binding, nextIndex, nextKey, prevNodeMap, dirtyElements, dirtyLeaves);
870
+ prevIndex++;
871
+ nextIndex++;
872
+ } else {
873
+ if (prevChildrenSet === undefined) {
874
+ prevChildrenSet = new Set(prevChildren);
875
+ }
876
+ if (nextChildrenSet === undefined) {
877
+ nextChildrenSet = new Set(nextChildren);
878
+ }
879
+ const nextHasPrevKey = nextChildrenSet.has(prevKey);
880
+ const prevHasNextKey = prevChildrenSet.has(nextKey);
881
+ if (!nextHasPrevKey) {
882
+ // Remove
883
+ this.splice(binding, nextIndex, 1);
884
+ prevIndex++;
885
+ } else {
886
+ // Create or replace
887
+ const nextChildNode = $getNodeByKeyOrThrow(nextKey);
888
+ const collabNode = $createCollabNodeFromLexicalNode(binding, nextChildNode, this);
889
+ collabNodeMap.set(nextKey, collabNode);
890
+ if (prevHasNextKey) {
891
+ this.splice(binding, nextIndex, 1, collabNode);
892
+ prevIndex++;
893
+ nextIndex++;
894
+ } else {
895
+ this.splice(binding, nextIndex, 0, collabNode);
896
+ nextIndex++;
897
+ }
898
+ }
899
+ }
900
+ }
901
+ const appendNewChildren = prevIndex > prevEndIndex;
902
+ const removeOldChildren = nextIndex > nextEndIndex;
903
+ if (appendNewChildren && !removeOldChildren) {
904
+ for (; nextIndex <= nextEndIndex; ++nextIndex) {
905
+ const key = nextChildren[nextIndex];
906
+ const nextChildNode = $getNodeByKeyOrThrow(key);
907
+ const collabNode = $createCollabNodeFromLexicalNode(binding, nextChildNode, this);
908
+ this.append(collabNode);
909
+ collabNodeMap.set(key, collabNode);
910
+ }
911
+ } else if (removeOldChildren && !appendNewChildren) {
912
+ for (let i = this._children.length - 1; i >= nextIndex; i--) {
913
+ this.splice(binding, i, 1);
914
+ }
915
+ }
916
+ }
917
+ append(collabNode) {
918
+ const xmlText = this._xmlText;
919
+ const children = this._children;
920
+ const lastChild = children[children.length - 1];
921
+ const offset = lastChild !== undefined ? lastChild.getOffset() + lastChild.getSize() : 0;
922
+ if (collabNode instanceof CollabElementNode) {
923
+ xmlText.insertEmbed(offset, collabNode._xmlText);
924
+ } else if (collabNode instanceof CollabTextNode) {
925
+ const map = collabNode._map;
926
+ if (map.parent === null) {
927
+ xmlText.insertEmbed(offset, map);
928
+ }
929
+ xmlText.insert(offset + 1, collabNode._text);
930
+ } else if (collabNode instanceof CollabLineBreakNode) {
931
+ xmlText.insertEmbed(offset, collabNode._map);
932
+ } else if (collabNode instanceof CollabDecoratorNode) {
933
+ xmlText.insertEmbed(offset, collabNode._xmlElem);
934
+ }
935
+ this._children.push(collabNode);
936
+ }
937
+ splice(binding, index, delCount, collabNode) {
938
+ const children = this._children;
939
+ const child = children[index];
940
+ if (child === undefined) {
941
+ if (!(collabNode !== undefined)) {
942
+ throw Error(`splice: could not find collab element node`);
943
+ }
944
+ this.append(collabNode);
945
+ return;
946
+ }
947
+ const offset = child.getOffset();
948
+ if (!(offset !== -1)) {
949
+ throw Error(`splice: expected offset to be greater than zero`);
950
+ }
951
+ const xmlText = this._xmlText;
952
+ if (delCount !== 0) {
953
+ // What if we delete many nodes, don't we need to get all their
954
+ // sizes?
955
+ xmlText.delete(offset, child.getSize());
956
+ }
957
+ if (collabNode instanceof CollabElementNode) {
958
+ xmlText.insertEmbed(offset, collabNode._xmlText);
959
+ } else if (collabNode instanceof CollabTextNode) {
960
+ const map = collabNode._map;
961
+ if (map.parent === null) {
962
+ xmlText.insertEmbed(offset, map);
963
+ }
964
+ xmlText.insert(offset + 1, collabNode._text);
965
+ } else if (collabNode instanceof CollabLineBreakNode) {
966
+ xmlText.insertEmbed(offset, collabNode._map);
967
+ } else if (collabNode instanceof CollabDecoratorNode) {
968
+ xmlText.insertEmbed(offset, collabNode._xmlElem);
969
+ }
970
+ if (delCount !== 0) {
971
+ const childrenToDelete = children.slice(index, index + delCount);
972
+ for (let i = 0; i < childrenToDelete.length; i++) {
973
+ childrenToDelete[i].destroy(binding);
974
+ }
975
+ }
976
+ if (collabNode !== undefined) {
977
+ children.splice(index, delCount, collabNode);
978
+ } else {
979
+ children.splice(index, delCount);
980
+ }
981
+ }
982
+ getChildOffset(collabNode) {
983
+ let offset = 0;
984
+ const children = this._children;
985
+ for (let i = 0; i < children.length; i++) {
986
+ const child = children[i];
987
+ if (child === collabNode) {
988
+ return offset;
989
+ }
990
+ offset += child.getSize();
991
+ }
992
+ return -1;
993
+ }
994
+ destroy(binding) {
995
+ const collabNodeMap = binding.collabNodeMap;
996
+ const children = this._children;
997
+ for (let i = 0; i < children.length; i++) {
998
+ children[i].destroy(binding);
999
+ }
1000
+ collabNodeMap.delete(this._key);
1001
+ }
1002
+ }
1003
+ function $createCollabElementNode(xmlText, parent, type) {
1004
+ const collabNode = new CollabElementNode(xmlText, parent, type);
1005
+ xmlText._collabNode = collabNode;
1006
+ return collabNode;
1007
+ }
1008
+
1009
+ /**
1010
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
1011
+ *
1012
+ * This source code is licensed under the MIT license found in the
1013
+ * LICENSE file in the root directory of this source tree.
1014
+ *
1015
+ */
1016
+ function createBinding(editor, provider, id, doc, docMap, excludedProperties) {
1017
+ if (!(doc !== undefined && doc !== null)) {
1018
+ throw Error(`createBinding: doc is null or undefined`);
1019
+ }
1020
+ const rootXmlText = doc.get('root', XmlText);
1021
+ const root = $createCollabElementNode(rootXmlText, null, 'root');
1022
+ root._key = 'root';
1023
+ return {
1024
+ clientID: doc.clientID,
1025
+ collabNodeMap: new Map(),
1026
+ cursors: new Map(),
1027
+ cursorsContainer: null,
1028
+ doc,
1029
+ docMap,
1030
+ editor,
1031
+ excludedProperties: excludedProperties || new Map(),
1032
+ id,
1033
+ nodeProperties: new Map(),
1034
+ root
1035
+ };
1036
+ }
1037
+
1038
+ /**
1039
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
1040
+ *
1041
+ * This source code is licensed under the MIT license found in the
1042
+ * LICENSE file in the root directory of this source tree.
1043
+ *
1044
+ */
1045
+ function createRelativePosition(point, binding) {
1046
+ const collabNodeMap = binding.collabNodeMap;
1047
+ const collabNode = collabNodeMap.get(point.key);
1048
+ if (collabNode === undefined) {
1049
+ return null;
1050
+ }
1051
+ let offset = point.offset;
1052
+ let sharedType = collabNode.getSharedType();
1053
+ if (collabNode instanceof CollabTextNode) {
1054
+ sharedType = collabNode._parent._xmlText;
1055
+ const currentOffset = collabNode.getOffset();
1056
+ if (currentOffset === -1) {
1057
+ return null;
1058
+ }
1059
+ offset = currentOffset + 1 + offset;
1060
+ } else if (collabNode instanceof CollabElementNode && point.type === 'element') {
1061
+ const parent = point.getNode();
1062
+ if (!$isElementNode(parent)) {
1063
+ throw Error(`Element point must be an element node`);
1064
+ }
1065
+ let accumulatedOffset = 0;
1066
+ let i = 0;
1067
+ let node = parent.getFirstChild();
1068
+ while (node !== null && i++ < offset) {
1069
+ if ($isTextNode(node)) {
1070
+ accumulatedOffset += node.getTextContentSize() + 1;
1071
+ } else {
1072
+ accumulatedOffset++;
1073
+ }
1074
+ node = node.getNextSibling();
1075
+ }
1076
+ offset = accumulatedOffset;
1077
+ }
1078
+ return createRelativePositionFromTypeIndex(sharedType, offset);
1079
+ }
1080
+ function createAbsolutePosition(relativePosition, binding) {
1081
+ return createAbsolutePositionFromRelativePosition(relativePosition, binding.doc);
1082
+ }
1083
+ function shouldUpdatePosition(currentPos, pos) {
1084
+ if (currentPos == null) {
1085
+ if (pos != null) {
1086
+ return true;
1087
+ }
1088
+ } else if (pos == null || !compareRelativePositions(currentPos, pos)) {
1089
+ return true;
1090
+ }
1091
+ return false;
1092
+ }
1093
+ function createCursor(name, color) {
1094
+ return {
1095
+ color: color,
1096
+ name: name,
1097
+ selection: null
1098
+ };
1099
+ }
1100
+ function destroySelection(binding, selection) {
1101
+ const cursorsContainer = binding.cursorsContainer;
1102
+ if (cursorsContainer !== null) {
1103
+ const selections = selection.selections;
1104
+ const selectionsLength = selections.length;
1105
+ for (let i = 0; i < selectionsLength; i++) {
1106
+ cursorsContainer.removeChild(selections[i]);
1107
+ }
1108
+ }
1109
+ }
1110
+ function destroyCursor(binding, cursor) {
1111
+ const selection = cursor.selection;
1112
+ if (selection !== null) {
1113
+ destroySelection(binding, selection);
1114
+ }
1115
+ }
1116
+ function createCursorSelection(cursor, anchorKey, anchorOffset, focusKey, focusOffset) {
1117
+ const color = cursor.color;
1118
+ const caret = document.createElement('span');
1119
+ caret.style.cssText = `position:absolute;top:0;bottom:0;right:-1px;width:1px;background-color:${color};z-index:10;`;
1120
+ const name = document.createElement('span');
1121
+ name.textContent = cursor.name;
1122
+ name.style.cssText = `position:absolute;left:-2px;top:-16px;background-color:${color};color:#fff;line-height:12px;font-size:12px;padding:2px;font-family:Arial;font-weight:bold;white-space:nowrap;`;
1123
+ caret.appendChild(name);
1124
+ return {
1125
+ anchor: {
1126
+ key: anchorKey,
1127
+ offset: anchorOffset
1128
+ },
1129
+ caret,
1130
+ color,
1131
+ focus: {
1132
+ key: focusKey,
1133
+ offset: focusOffset
1134
+ },
1135
+ name,
1136
+ selections: []
1137
+ };
1138
+ }
1139
+ function updateCursor(binding, cursor, nextSelection, nodeMap) {
1140
+ const editor = binding.editor;
1141
+ const rootElement = editor.getRootElement();
1142
+ const cursorsContainer = binding.cursorsContainer;
1143
+ if (cursorsContainer === null || rootElement === null) {
1144
+ return;
1145
+ }
1146
+ const cursorsContainerOffsetParent = cursorsContainer.offsetParent;
1147
+ if (cursorsContainerOffsetParent === null) {
1148
+ return;
1149
+ }
1150
+ const containerRect = cursorsContainerOffsetParent.getBoundingClientRect();
1151
+ const prevSelection = cursor.selection;
1152
+ if (nextSelection === null) {
1153
+ if (prevSelection === null) {
1154
+ return;
1155
+ } else {
1156
+ cursor.selection = null;
1157
+ destroySelection(binding, prevSelection);
1158
+ return;
1159
+ }
1160
+ } else {
1161
+ cursor.selection = nextSelection;
1162
+ }
1163
+ const caret = nextSelection.caret;
1164
+ const color = nextSelection.color;
1165
+ const selections = nextSelection.selections;
1166
+ const anchor = nextSelection.anchor;
1167
+ const focus = nextSelection.focus;
1168
+ const anchorKey = anchor.key;
1169
+ const focusKey = focus.key;
1170
+ const anchorNode = nodeMap.get(anchorKey);
1171
+ const focusNode = nodeMap.get(focusKey);
1172
+ if (anchorNode == null || focusNode == null) {
1173
+ return;
1174
+ }
1175
+ let selectionRects;
1176
+
1177
+ // In the case of a collapsed selection on a linebreak, we need
1178
+ // to improvise as the browser will return nothing here as <br>
1179
+ // apparantly take up no visual space :/
1180
+ // This won't work in all cases, but it's better than just showing
1181
+ // nothing all the time.
1182
+ if (anchorNode === focusNode && $isLineBreakNode(anchorNode)) {
1183
+ const brRect = editor.getElementByKey(anchorKey).getBoundingClientRect();
1184
+ selectionRects = [brRect];
1185
+ } else {
1186
+ const range = createDOMRange(editor, anchorNode, anchor.offset, focusNode, focus.offset);
1187
+ if (range === null) {
1188
+ return;
1189
+ }
1190
+ selectionRects = createRectsFromDOMRange(editor, range);
1191
+ }
1192
+ const selectionsLength = selections.length;
1193
+ const selectionRectsLength = selectionRects.length;
1194
+ for (let i = 0; i < selectionRectsLength; i++) {
1195
+ const selectionRect = selectionRects[i];
1196
+ let selection = selections[i];
1197
+ if (selection === undefined) {
1198
+ selection = document.createElement('span');
1199
+ selections[i] = selection;
1200
+ const selectionBg = document.createElement('span');
1201
+ selection.appendChild(selectionBg);
1202
+ cursorsContainer.appendChild(selection);
1203
+ }
1204
+ const top = selectionRect.top - containerRect.top;
1205
+ const left = selectionRect.left - containerRect.left;
1206
+ const style = `position:absolute;top:${top}px;left:${left}px;height:${selectionRect.height}px;width:${selectionRect.width}px;pointer-events:none;z-index:5;`;
1207
+ selection.style.cssText = style;
1208
+ selection.firstChild.style.cssText = `${style}left:0;top:0;background-color:${color};opacity:0.3;`;
1209
+ if (i === selectionRectsLength - 1) {
1210
+ if (caret.parentNode !== selection) {
1211
+ selection.appendChild(caret);
1212
+ }
1213
+ }
1214
+ }
1215
+ for (let i = selectionsLength - 1; i >= selectionRectsLength; i--) {
1216
+ const selection = selections[i];
1217
+ cursorsContainer.removeChild(selection);
1218
+ selections.pop();
1219
+ }
1220
+ }
1221
+ function syncLocalCursorPosition(binding, provider) {
1222
+ const awareness = provider.awareness;
1223
+ const localState = awareness.getLocalState();
1224
+ if (localState === null) {
1225
+ return;
1226
+ }
1227
+ const anchorPos = localState.anchorPos;
1228
+ const focusPos = localState.focusPos;
1229
+ if (anchorPos !== null && focusPos !== null) {
1230
+ const anchorAbsPos = createAbsolutePosition(anchorPos, binding);
1231
+ const focusAbsPos = createAbsolutePosition(focusPos, binding);
1232
+ if (anchorAbsPos !== null && focusAbsPos !== null) {
1233
+ const [anchorCollabNode, anchorOffset] = getCollabNodeAndOffset(anchorAbsPos.type, anchorAbsPos.index);
1234
+ const [focusCollabNode, focusOffset] = getCollabNodeAndOffset(focusAbsPos.type, focusAbsPos.index);
1235
+ if (anchorCollabNode !== null && focusCollabNode !== null) {
1236
+ const anchorKey = anchorCollabNode.getKey();
1237
+ const focusKey = focusCollabNode.getKey();
1238
+ const selection = $getSelection();
1239
+ if (!$isRangeSelection(selection)) {
1240
+ return;
1241
+ }
1242
+ const anchor = selection.anchor;
1243
+ const focus = selection.focus;
1244
+ setPoint(anchor, anchorKey, anchorOffset);
1245
+ setPoint(focus, focusKey, focusOffset);
1246
+ }
1247
+ }
1248
+ }
1249
+ }
1250
+ function setPoint(point, key, offset) {
1251
+ if (point.key !== key || point.offset !== offset) {
1252
+ let anchorNode = $getNodeByKey(key);
1253
+ if (anchorNode !== null && !$isElementNode(anchorNode) && !$isTextNode(anchorNode)) {
1254
+ const parent = anchorNode.getParentOrThrow();
1255
+ key = parent.getKey();
1256
+ offset = anchorNode.getIndexWithinParent();
1257
+ anchorNode = parent;
1258
+ }
1259
+ point.set(key, offset, $isElementNode(anchorNode) ? 'element' : 'text');
1260
+ }
1261
+ }
1262
+ function getCollabNodeAndOffset(
1263
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1264
+ sharedType, offset) {
1265
+ const collabNode = sharedType._collabNode;
1266
+ if (collabNode === undefined) {
1267
+ return [null, 0];
1268
+ }
1269
+ if (collabNode instanceof CollabElementNode) {
1270
+ const {
1271
+ node,
1272
+ offset: collabNodeOffset
1273
+ } = getPositionFromElementAndOffset(collabNode, offset, true);
1274
+ if (node === null) {
1275
+ return [collabNode, 0];
1276
+ } else {
1277
+ return [node, collabNodeOffset];
1278
+ }
1279
+ }
1280
+ return [null, 0];
1281
+ }
1282
+ function syncCursorPositions(binding, provider) {
1283
+ const awarenessStates = Array.from(provider.awareness.getStates());
1284
+ const localClientID = binding.clientID;
1285
+ const cursors = binding.cursors;
1286
+ const editor = binding.editor;
1287
+ const nodeMap = editor._editorState._nodeMap;
1288
+ const visitedClientIDs = new Set();
1289
+ for (let i = 0; i < awarenessStates.length; i++) {
1290
+ const awarenessState = awarenessStates[i];
1291
+ const [clientID, awareness] = awarenessState;
1292
+ if (clientID !== localClientID) {
1293
+ visitedClientIDs.add(clientID);
1294
+ const {
1295
+ anchorPos,
1296
+ focusPos,
1297
+ name,
1298
+ color,
1299
+ focusing
1300
+ } = awareness;
1301
+ let selection = null;
1302
+ let cursor = cursors.get(clientID);
1303
+ if (cursor === undefined) {
1304
+ cursor = createCursor(name, color);
1305
+ cursors.set(clientID, cursor);
1306
+ }
1307
+ if (anchorPos !== null && focusPos !== null && focusing) {
1308
+ const anchorAbsPos = createAbsolutePosition(anchorPos, binding);
1309
+ const focusAbsPos = createAbsolutePosition(focusPos, binding);
1310
+ if (anchorAbsPos !== null && focusAbsPos !== null) {
1311
+ const [anchorCollabNode, anchorOffset] = getCollabNodeAndOffset(anchorAbsPos.type, anchorAbsPos.index);
1312
+ const [focusCollabNode, focusOffset] = getCollabNodeAndOffset(focusAbsPos.type, focusAbsPos.index);
1313
+ if (anchorCollabNode !== null && focusCollabNode !== null) {
1314
+ const anchorKey = anchorCollabNode.getKey();
1315
+ const focusKey = focusCollabNode.getKey();
1316
+ selection = cursor.selection;
1317
+ if (selection === null) {
1318
+ selection = createCursorSelection(cursor, anchorKey, anchorOffset, focusKey, focusOffset);
1319
+ } else {
1320
+ const anchor = selection.anchor;
1321
+ const focus = selection.focus;
1322
+ anchor.key = anchorKey;
1323
+ anchor.offset = anchorOffset;
1324
+ focus.key = focusKey;
1325
+ focus.offset = focusOffset;
1326
+ }
1327
+ }
1328
+ }
1329
+ }
1330
+ updateCursor(binding, cursor, selection, nodeMap);
1331
+ }
1332
+ }
1333
+ const allClientIDs = Array.from(cursors.keys());
1334
+ for (let i = 0; i < allClientIDs.length; i++) {
1335
+ const clientID = allClientIDs[i];
1336
+ if (!visitedClientIDs.has(clientID)) {
1337
+ const cursor = cursors.get(clientID);
1338
+ if (cursor !== undefined) {
1339
+ destroyCursor(binding, cursor);
1340
+ cursors.delete(clientID);
1341
+ }
1342
+ }
1343
+ }
1344
+ }
1345
+ function syncLexicalSelectionToYjs(binding, provider, prevSelection, nextSelection) {
1346
+ const awareness = provider.awareness;
1347
+ const localState = awareness.getLocalState();
1348
+ if (localState === null) {
1349
+ return;
1350
+ }
1351
+ const {
1352
+ anchorPos: currentAnchorPos,
1353
+ focusPos: currentFocusPos,
1354
+ name,
1355
+ color,
1356
+ focusing,
1357
+ awarenessData
1358
+ } = localState;
1359
+ let anchorPos = null;
1360
+ let focusPos = null;
1361
+ if (nextSelection === null || currentAnchorPos !== null && !nextSelection.is(prevSelection)) {
1362
+ if (prevSelection === null) {
1363
+ return;
1364
+ }
1365
+ }
1366
+ if ($isRangeSelection(nextSelection)) {
1367
+ anchorPos = createRelativePosition(nextSelection.anchor, binding);
1368
+ focusPos = createRelativePosition(nextSelection.focus, binding);
1369
+ }
1370
+ if (shouldUpdatePosition(currentAnchorPos, anchorPos) || shouldUpdatePosition(currentFocusPos, focusPos)) {
1371
+ awareness.setLocalState({
1372
+ anchorPos,
1373
+ awarenessData,
1374
+ color,
1375
+ focusPos,
1376
+ focusing,
1377
+ name
1378
+ });
1379
+ }
1380
+ }
1381
+
1382
+ /**
1383
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
1384
+ *
1385
+ * This source code is licensed under the MIT license found in the
1386
+ * LICENSE file in the root directory of this source tree.
1387
+ *
1388
+ */
1389
+
1390
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1391
+ function syncEvent(binding, event) {
1392
+ const {
1393
+ target
1394
+ } = event;
1395
+ const collabNode = getOrInitCollabNodeFromSharedType(binding, target);
1396
+ if (collabNode instanceof CollabElementNode && event instanceof YTextEvent) {
1397
+ // @ts-expect-error We need to access the private property of the class
1398
+ const {
1399
+ keysChanged,
1400
+ childListChanged,
1401
+ delta
1402
+ } = event;
1403
+
1404
+ // Update
1405
+ if (keysChanged.size > 0) {
1406
+ collabNode.syncPropertiesFromYjs(binding, keysChanged);
1407
+ }
1408
+ if (childListChanged) {
1409
+ collabNode.applyChildrenYjsDelta(binding, delta);
1410
+ collabNode.syncChildrenFromYjs(binding);
1411
+ }
1412
+ } else if (collabNode instanceof CollabTextNode && event instanceof YMapEvent) {
1413
+ const {
1414
+ keysChanged
1415
+ } = event;
1416
+
1417
+ // Update
1418
+ if (keysChanged.size > 0) {
1419
+ collabNode.syncPropertiesAndTextFromYjs(binding, keysChanged);
1420
+ }
1421
+ } else if (collabNode instanceof CollabDecoratorNode && event instanceof YXmlEvent) {
1422
+ const {
1423
+ attributesChanged
1424
+ } = event;
1425
+
1426
+ // Update
1427
+ if (attributesChanged.size > 0) {
1428
+ collabNode.syncPropertiesFromYjs(binding, attributesChanged);
1429
+ }
1430
+ } else {
1431
+ {
1432
+ throw Error(`Expected text, element, or decorator event`);
1433
+ }
1434
+ }
1435
+ }
1436
+ function syncYjsChangesToLexical(binding, provider, events, isFromUndoManger) {
1437
+ const editor = binding.editor;
1438
+ const currentEditorState = editor._editorState;
1439
+
1440
+ // This line precompute the delta before editor update. The reason is
1441
+ // delta is computed when it is accessed. Note that this can only be
1442
+ // safely computed during the event call. If it is accessed after event
1443
+ // call it might result in unexpected behavior.
1444
+ // https://github.com/yjs/yjs/blob/00ef472d68545cb260abd35c2de4b3b78719c9e4/src/utils/YEvent.js#L132
1445
+ events.forEach(event => event.delta);
1446
+ editor.update(() => {
1447
+ const pendingEditorState = editor._pendingEditorState;
1448
+ for (let i = 0; i < events.length; i++) {
1449
+ const event = events[i];
1450
+ syncEvent(binding, event);
1451
+ }
1452
+ const selection = $getSelection();
1453
+ if ($isRangeSelection(selection)) {
1454
+ // We can't use Yjs's cursor position here, as it doesn't always
1455
+ // handle selection recovery correctly – especially on elements that
1456
+ // get moved around or split. So instead, we roll our own solution.
1457
+ if (doesSelectionNeedRecovering(selection)) {
1458
+ const prevSelection = currentEditorState._selection;
1459
+ if ($isRangeSelection(prevSelection)) {
1460
+ const prevOffsetView = $createOffsetView(editor, 0, currentEditorState);
1461
+ const nextOffsetView = $createOffsetView(editor, 0, pendingEditorState);
1462
+ const [start, end] = prevOffsetView.getOffsetsFromSelection(prevSelection);
1463
+ const nextSelection = start >= 0 && end >= 0 ? nextOffsetView.createSelectionFromOffsets(start, end, prevOffsetView) : null;
1464
+ if (nextSelection !== null) {
1465
+ $setSelection(nextSelection);
1466
+ } else {
1467
+ // Fallback is to use the Yjs cursor position
1468
+ syncLocalCursorPosition(binding, provider);
1469
+ if (doesSelectionNeedRecovering(selection)) {
1470
+ const root = $getRoot();
1471
+
1472
+ // If there was a collision on the top level paragraph
1473
+ // we need to re-add a paragraph
1474
+ if (root.getChildrenSize() === 0) {
1475
+ root.append($createParagraphNode());
1476
+ }
1477
+
1478
+ // Fallback
1479
+ $getRoot().selectEnd();
1480
+ }
1481
+ }
1482
+ }
1483
+ syncLexicalSelectionToYjs(binding, provider, prevSelection, $getSelection());
1484
+ } else {
1485
+ syncLocalCursorPosition(binding, provider);
1486
+ }
1487
+ }
1488
+ }, {
1489
+ onUpdate: () => {
1490
+ syncCursorPositions(binding, provider);
1491
+ },
1492
+ skipTransforms: true,
1493
+ tag: isFromUndoManger ? 'historic' : 'collaboration'
1494
+ });
1495
+ }
1496
+ function handleNormalizationMergeConflicts(binding, normalizedNodes) {
1497
+ // We handle the merge operations here
1498
+ const normalizedNodesKeys = Array.from(normalizedNodes);
1499
+ const collabNodeMap = binding.collabNodeMap;
1500
+ const mergedNodes = [];
1501
+ for (let i = 0; i < normalizedNodesKeys.length; i++) {
1502
+ const nodeKey = normalizedNodesKeys[i];
1503
+ const lexicalNode = $getNodeByKey(nodeKey);
1504
+ const collabNode = collabNodeMap.get(nodeKey);
1505
+ if (collabNode instanceof CollabTextNode) {
1506
+ if ($isTextNode(lexicalNode)) {
1507
+ // We mutate the text collab nodes after removing
1508
+ // all the dead nodes first, otherwise offsets break.
1509
+ mergedNodes.push([collabNode, lexicalNode.__text]);
1510
+ } else {
1511
+ const offset = collabNode.getOffset();
1512
+ if (offset === -1) {
1513
+ continue;
1514
+ }
1515
+ const parent = collabNode._parent;
1516
+ collabNode._normalized = true;
1517
+ parent._xmlText.delete(offset, 1);
1518
+ collabNodeMap.delete(nodeKey);
1519
+ const parentChildren = parent._children;
1520
+ const index = parentChildren.indexOf(collabNode);
1521
+ parentChildren.splice(index, 1);
1522
+ }
1523
+ }
1524
+ }
1525
+ for (let i = 0; i < mergedNodes.length; i++) {
1526
+ const [collabNode, text] = mergedNodes[i];
1527
+ if (collabNode instanceof CollabTextNode && typeof text === 'string') {
1528
+ collabNode._text = text;
1529
+ }
1530
+ }
1531
+ }
1532
+ function syncLexicalUpdateToYjs(binding, provider, prevEditorState, currEditorState, dirtyElements, dirtyLeaves, normalizedNodes, tags) {
1533
+ syncWithTransaction(binding, () => {
1534
+ currEditorState.read(() => {
1535
+ // We check if the update has come from a origin where the origin
1536
+ // was the collaboration binding previously. This can help us
1537
+ // prevent unnecessarily re-diffing and possible re-applying
1538
+ // the same change editor state again. For example, if a user
1539
+ // types a character and we get it, we don't want to then insert
1540
+ // the same character again. The exception to this heuristic is
1541
+ // when we need to handle normalization merge conflicts.
1542
+ if (tags.has('collaboration') || tags.has('historic')) {
1543
+ if (normalizedNodes.size > 0) {
1544
+ handleNormalizationMergeConflicts(binding, normalizedNodes);
1545
+ }
1546
+ return;
1547
+ }
1548
+ if (dirtyElements.has('root')) {
1549
+ const prevNodeMap = prevEditorState._nodeMap;
1550
+ const nextLexicalRoot = $getRoot();
1551
+ const collabRoot = binding.root;
1552
+ collabRoot.syncPropertiesFromLexical(binding, nextLexicalRoot, prevNodeMap);
1553
+ collabRoot.syncChildrenFromLexical(binding, nextLexicalRoot, prevNodeMap, dirtyElements, dirtyLeaves);
1554
+ }
1555
+ const selection = $getSelection();
1556
+ const prevSelection = prevEditorState._selection;
1557
+ syncLexicalSelectionToYjs(binding, provider, prevSelection, selection);
1558
+ });
1559
+ });
1560
+ }
1561
+
1562
+ /** @module @lexical/yjs */
1563
+ const CONNECTED_COMMAND = createCommand('CONNECTED_COMMAND');
1564
+ const TOGGLE_CONNECT_COMMAND = createCommand('TOGGLE_CONNECT_COMMAND');
1565
+ function createUndoManager(binding, root) {
1566
+ return new UndoManager(root, {
1567
+ trackedOrigins: new Set([binding, null])
1568
+ });
1569
+ }
1570
+ function initLocalState(provider, name, color, focusing, awarenessData) {
1571
+ provider.awareness.setLocalState({
1572
+ anchorPos: null,
1573
+ awarenessData,
1574
+ color,
1575
+ focusPos: null,
1576
+ focusing: focusing,
1577
+ name
1578
+ });
1579
+ }
1580
+ function setLocalStateFocus(provider, name, color, focusing, awarenessData) {
1581
+ const {
1582
+ awareness
1583
+ } = provider;
1584
+ let localState = awareness.getLocalState();
1585
+ if (localState === null) {
1586
+ localState = {
1587
+ anchorPos: null,
1588
+ awarenessData,
1589
+ color,
1590
+ focusPos: null,
1591
+ focusing: focusing,
1592
+ name
1593
+ };
1594
+ }
1595
+ localState.focusing = focusing;
1596
+ awareness.setLocalState(localState);
1597
+ }
1598
+
1599
+ export { CONNECTED_COMMAND, TOGGLE_CONNECT_COMMAND, createBinding, createUndoManager, initLocalState, setLocalStateFocus, syncCursorPositions, syncLexicalUpdateToYjs, syncYjsChangesToLexical };