@lexical/yjs 0.36.3-nightly.20251003.0 → 0.36.3-nightly.20251007.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.
@@ -6,8 +6,8 @@
6
6
  *
7
7
  */
8
8
 
9
- import { $getNodeByKey, $isLineBreakNode, $isTextNode, $getSelection, $isRangeSelection, createEditor, $isElementNode, $isDecoratorNode, $isRootNode, $getWritableNodeState, $getRoot, $getNodeByKeyOrThrow, removeFromParent, RootNode, ElementNode, TextNode, COLLABORATION_TAG, HISTORIC_TAG, $addUpdateTag, SKIP_SCROLL_INTO_VIEW_TAG, $createParagraphNode, createCommand } from 'lexical';
10
- import { XmlText, Map as Map$1, XmlElement, Doc, createAbsolutePositionFromRelativePosition, createRelativePositionFromTypeIndex, compareRelativePositions, XmlHook, ContentString, ContentFormat, iterateDeletedStructs, Item, YMapEvent, YTextEvent, YXmlEvent, UndoManager } from 'yjs';
9
+ import { $isDecoratorNode, $getNodeByKey, $isLineBreakNode, $isTextNode, $getSelection, $isRangeSelection, $isElementNode, $getNodeByKeyOrThrow, removeFromParent, createEditor, $isRootNode, $getWritableNodeState, $getRoot, RootNode, ElementNode, TextNode, $getState, createState, COLLABORATION_TAG, HISTORIC_TAG, $addUpdateTag, SKIP_SCROLL_INTO_VIEW_TAG, $createParagraphNode, createCommand } from 'lexical';
10
+ import { XmlText, XmlElement, Map as Map$1, Doc, typeListToArraySnapshot, Snapshot, isDeleted, XmlHook, ContentString, ContentFormat, snapshot, emptySnapshot, PermanentUserData, iterateDeletedStructs, createAbsolutePositionFromRelativePosition, createRelativePositionFromTypeIndex, compareRelativePositions, Item, YMapEvent, YTextEvent, YXmlEvent, UndoManager } from 'yjs';
11
11
  import { $createChildrenArray } from '@lexical/offset';
12
12
  import { createDOMRange, createRectsFromDOMRange } from '@lexical/selection';
13
13
 
@@ -33,30 +33,62 @@ function formatDevErrorMessage(message) {
33
33
  *
34
34
  */
35
35
 
36
- class CollabLineBreakNode {
37
- _map;
36
+ function simpleDiffWithCursor(a, b, cursor) {
37
+ const aLength = a.length;
38
+ const bLength = b.length;
39
+ let left = 0; // number of same characters counting from left
40
+ let right = 0; // number of same characters counting from right
41
+ // Iterate left to the right until we find a changed character
42
+ // First iteration considers the current cursor position
43
+ while (left < aLength && left < bLength && a[left] === b[left] && left < cursor) {
44
+ left++;
45
+ }
46
+ // Iterate right to the left until we find a changed character
47
+ while (right + left < aLength && right + left < bLength && a[aLength - right - 1] === b[bLength - right - 1]) {
48
+ right++;
49
+ }
50
+ // Try to iterate left further to the right without caring about the current cursor position
51
+ while (right + left < aLength && right + left < bLength && a[left] === b[left]) {
52
+ left++;
53
+ }
54
+ return {
55
+ index: left,
56
+ insert: b.slice(left, bLength - right),
57
+ remove: aLength - left - right
58
+ };
59
+ }
60
+
61
+ class CollabDecoratorNode {
62
+ _xmlElem;
38
63
  _key;
39
64
  _parent;
40
65
  _type;
41
- constructor(map, parent) {
66
+ constructor(xmlElem, parent, type) {
42
67
  this._key = '';
43
- this._map = map;
68
+ this._xmlElem = xmlElem;
44
69
  this._parent = parent;
45
- this._type = 'linebreak';
70
+ this._type = type;
71
+ }
72
+ getPrevNode(nodeMap) {
73
+ if (nodeMap === null) {
74
+ return null;
75
+ }
76
+ const node = nodeMap.get(this._key);
77
+ return $isDecoratorNode(node) ? node : null;
46
78
  }
47
79
  getNode() {
48
80
  const node = $getNodeByKey(this._key);
49
- return $isLineBreakNode(node) ? node : null;
50
- }
51
- getKey() {
52
- return this._key;
81
+ return $isDecoratorNode(node) ? node : null;
53
82
  }
54
83
  getSharedType() {
55
- return this._map;
84
+ return this._xmlElem;
56
85
  }
57
86
  getType() {
58
87
  return this._type;
59
88
  }
89
+ getKey() {
90
+ return this._key;
91
+ }
60
92
  getSize() {
61
93
  return 1;
62
94
  }
@@ -64,6 +96,19 @@ class CollabLineBreakNode {
64
96
  const collabElementNode = this._parent;
65
97
  return collabElementNode.getChildOffset(this);
66
98
  }
99
+ syncPropertiesFromLexical(binding, nextLexicalNode, prevNodeMap) {
100
+ const prevLexicalNode = this.getPrevNode(prevNodeMap);
101
+ const xmlElem = this._xmlElem;
102
+ syncPropertiesFromLexical(binding, xmlElem, prevLexicalNode, nextLexicalNode);
103
+ }
104
+ syncPropertiesFromYjs(binding, keysChanged) {
105
+ const lexicalNode = this.getNode();
106
+ if (!(lexicalNode !== null)) {
107
+ formatDevErrorMessage(`syncPropertiesFromYjs: could not find decorator node`);
108
+ }
109
+ const xmlElem = this._xmlElem;
110
+ $syncPropertiesFromYjs(binding, xmlElem, lexicalNode, keysChanged);
111
+ }
67
112
  destroy(binding) {
68
113
  const collabNodeMap = binding.collabNodeMap;
69
114
  if (collabNodeMap.get(this._key) === this) {
@@ -71,9 +116,9 @@ class CollabLineBreakNode {
71
116
  }
72
117
  }
73
118
  }
74
- function $createCollabLineBreakNode(map, parent) {
75
- const collabNode = new CollabLineBreakNode(map, parent);
76
- map._collabNode = collabNode;
119
+ function $createCollabDecoratorNode(xmlElem, parent, type) {
120
+ const collabNode = new CollabDecoratorNode(xmlElem, parent, type);
121
+ xmlElem._collabNode = collabNode;
77
122
  return collabNode;
78
123
  }
79
124
 
@@ -85,29 +130,48 @@ function $createCollabLineBreakNode(map, parent) {
85
130
  *
86
131
  */
87
132
 
88
- function simpleDiffWithCursor(a, b, cursor) {
89
- const aLength = a.length;
90
- const bLength = b.length;
91
- let left = 0; // number of same characters counting from left
92
- let right = 0; // number of same characters counting from right
93
- // Iterate left to the right until we find a changed character
94
- // First iteration considers the current cursor position
95
- while (left < aLength && left < bLength && a[left] === b[left] && left < cursor) {
96
- left++;
133
+ class CollabLineBreakNode {
134
+ _map;
135
+ _key;
136
+ _parent;
137
+ _type;
138
+ constructor(map, parent) {
139
+ this._key = '';
140
+ this._map = map;
141
+ this._parent = parent;
142
+ this._type = 'linebreak';
97
143
  }
98
- // Iterate right to the left until we find a changed character
99
- while (right + left < aLength && right + left < bLength && a[aLength - right - 1] === b[bLength - right - 1]) {
100
- right++;
144
+ getNode() {
145
+ const node = $getNodeByKey(this._key);
146
+ return $isLineBreakNode(node) ? node : null;
101
147
  }
102
- // Try to iterate left further to the right without caring about the current cursor position
103
- while (right + left < aLength && right + left < bLength && a[left] === b[left]) {
104
- left++;
148
+ getKey() {
149
+ return this._key;
105
150
  }
106
- return {
107
- index: left,
108
- insert: b.slice(left, bLength - right),
109
- remove: aLength - left - right
110
- };
151
+ getSharedType() {
152
+ return this._map;
153
+ }
154
+ getType() {
155
+ return this._type;
156
+ }
157
+ getSize() {
158
+ return 1;
159
+ }
160
+ getOffset() {
161
+ const collabElementNode = this._parent;
162
+ return collabElementNode.getChildOffset(this);
163
+ }
164
+ destroy(binding) {
165
+ const collabNodeMap = binding.collabNodeMap;
166
+ if (collabNodeMap.get(this._key) === this) {
167
+ collabNodeMap.delete(this._key);
168
+ }
169
+ }
170
+ }
171
+ function $createCollabLineBreakNode(map, parent) {
172
+ const collabNode = new CollabLineBreakNode(map, parent);
173
+ map._collabNode = collabNode;
174
+ return collabNode;
111
175
  }
112
176
 
113
177
  function $diffTextContentAndApplyDelta(collabNode, key, prevText, nextText) {
@@ -212,485 +276,49 @@ function $createCollabTextNode(map, text, parent, type) {
212
276
  return collabNode;
213
277
  }
214
278
 
215
- const baseExcludedProperties = new Set(['__key', '__parent', '__next', '__prev', '__state']);
216
- const elementExcludedProperties = new Set(['__first', '__last', '__size']);
217
- const rootExcludedProperties = new Set(['__cachedText']);
218
- const textExcludedProperties = new Set(['__text']);
219
- function isExcludedProperty(name, node, binding) {
220
- if (baseExcludedProperties.has(name) || typeof node[name] === 'function') {
221
- return true;
279
+ class CollabElementNode {
280
+ _key;
281
+ _children;
282
+ _xmlText;
283
+ _type;
284
+ _parent;
285
+ constructor(xmlText, parent, type) {
286
+ this._key = '';
287
+ this._children = [];
288
+ this._xmlText = xmlText;
289
+ this._type = type;
290
+ this._parent = parent;
222
291
  }
223
- if ($isTextNode(node)) {
224
- if (textExcludedProperties.has(name)) {
225
- return true;
226
- }
227
- } else if ($isElementNode(node)) {
228
- if (elementExcludedProperties.has(name) || $isRootNode(node) && rootExcludedProperties.has(name)) {
229
- return true;
292
+ getPrevNode(nodeMap) {
293
+ if (nodeMap === null) {
294
+ return null;
230
295
  }
296
+ const node = nodeMap.get(this._key);
297
+ return $isElementNode(node) ? node : null;
231
298
  }
232
- const nodeKlass = node.constructor;
233
- const excludedProperties = binding.excludedProperties.get(nodeKlass);
234
- return excludedProperties != null && excludedProperties.has(name);
235
- }
236
- function initializeNodeProperties(binding) {
237
- const {
238
- editor,
239
- nodeProperties
240
- } = binding;
241
- editor.update(() => {
242
- editor._nodes.forEach(nodeInfo => {
243
- const node = new nodeInfo.klass();
244
- const defaultProperties = {};
245
- for (const [property, value] of Object.entries(node)) {
246
- if (!isExcludedProperty(property, node, binding)) {
247
- defaultProperties[property] = value;
248
- }
249
- }
250
- nodeProperties.set(node.__type, Object.freeze(defaultProperties));
251
- });
252
- });
253
- }
254
- function getDefaultNodeProperties(node, binding) {
255
- const type = node.__type;
256
- const {
257
- nodeProperties
258
- } = binding;
259
- const properties = nodeProperties.get(type);
260
- if (!(properties !== undefined)) {
261
- formatDevErrorMessage(`Node properties for ${type} not initialized for sync`);
299
+ getNode() {
300
+ const node = $getNodeByKey(this._key);
301
+ return $isElementNode(node) ? node : null;
262
302
  }
263
- return properties;
264
- }
265
- function $createCollabNodeFromLexicalNode(binding, lexicalNode, parent) {
266
- const nodeType = lexicalNode.__type;
267
- let collabNode;
268
- if ($isElementNode(lexicalNode)) {
269
- const xmlText = new XmlText();
270
- collabNode = $createCollabElementNode(xmlText, parent, nodeType);
271
- collabNode.syncPropertiesFromLexical(binding, lexicalNode, null);
272
- collabNode.syncChildrenFromLexical(binding, lexicalNode, null, null, null);
273
- } else if ($isTextNode(lexicalNode)) {
274
- // TODO create a token text node for token, segmented nodes.
275
- const map = new Map$1();
276
- collabNode = $createCollabTextNode(map, lexicalNode.__text, parent, nodeType);
277
- collabNode.syncPropertiesAndTextFromLexical(binding, lexicalNode, null);
278
- } else if ($isLineBreakNode(lexicalNode)) {
279
- const map = new Map$1();
280
- map.set('__type', 'linebreak');
281
- collabNode = $createCollabLineBreakNode(map, parent);
282
- } else if ($isDecoratorNode(lexicalNode)) {
283
- const xmlElem = new XmlElement();
284
- collabNode = $createCollabDecoratorNode(xmlElem, parent, nodeType);
285
- collabNode.syncPropertiesFromLexical(binding, lexicalNode, null);
286
- } else {
287
- {
288
- formatDevErrorMessage(`Expected text, element, decorator, or linebreak node`);
289
- }
303
+ getSharedType() {
304
+ return this._xmlText;
290
305
  }
291
- collabNode._key = lexicalNode.__key;
292
- return collabNode;
293
- }
294
- function getNodeTypeFromSharedType(sharedType) {
295
- const type = sharedTypeGet(sharedType, '__type');
296
- if (!(typeof type === 'string' || typeof type === 'undefined')) {
297
- formatDevErrorMessage(`Expected shared type to include type attribute`);
306
+ getType() {
307
+ return this._type;
298
308
  }
299
- return type;
300
- }
301
- function $getOrInitCollabNodeFromSharedType(binding, sharedType, parent) {
302
- const collabNode = sharedType._collabNode;
303
- if (collabNode === undefined) {
304
- const registeredNodes = binding.editor._nodes;
305
- const type = getNodeTypeFromSharedType(sharedType);
306
- if (!(typeof type === 'string')) {
307
- formatDevErrorMessage(`Expected shared type to include type attribute`);
308
- }
309
- const nodeInfo = registeredNodes.get(type);
310
- if (!(nodeInfo !== undefined)) {
311
- formatDevErrorMessage(`Node ${type} is not registered`);
312
- }
313
- const sharedParent = sharedType.parent;
314
- const targetParent = parent === undefined && sharedParent !== null ? $getOrInitCollabNodeFromSharedType(binding, sharedParent) : parent || null;
315
- if (!(targetParent instanceof CollabElementNode)) {
316
- formatDevErrorMessage(`Expected parent to be a collab element node`);
317
- }
318
- if (sharedType instanceof XmlText) {
319
- return $createCollabElementNode(sharedType, targetParent, type);
320
- } else if (sharedType instanceof Map$1) {
321
- if (type === 'linebreak') {
322
- return $createCollabLineBreakNode(sharedType, targetParent);
323
- }
324
- return $createCollabTextNode(sharedType, '', targetParent, type);
325
- } else if (sharedType instanceof XmlElement) {
326
- return $createCollabDecoratorNode(sharedType, targetParent, type);
327
- }
328
- }
329
- return collabNode;
330
- }
331
- function createLexicalNodeFromCollabNode(binding, collabNode, parentKey) {
332
- const type = collabNode.getType();
333
- const registeredNodes = binding.editor._nodes;
334
- const nodeInfo = registeredNodes.get(type);
335
- if (!(nodeInfo !== undefined)) {
336
- formatDevErrorMessage(`Node ${type} is not registered`);
337
- }
338
- const lexicalNode = new nodeInfo.klass();
339
- lexicalNode.__parent = parentKey;
340
- collabNode._key = lexicalNode.__key;
341
- if (collabNode instanceof CollabElementNode) {
342
- const xmlText = collabNode._xmlText;
343
- collabNode.syncPropertiesFromYjs(binding, null);
344
- collabNode.applyChildrenYjsDelta(binding, xmlText.toDelta());
345
- collabNode.syncChildrenFromYjs(binding);
346
- } else if (collabNode instanceof CollabTextNode) {
347
- collabNode.syncPropertiesAndTextFromYjs(binding, null);
348
- } else if (collabNode instanceof CollabDecoratorNode) {
349
- collabNode.syncPropertiesFromYjs(binding, null);
350
- }
351
- binding.collabNodeMap.set(lexicalNode.__key, collabNode);
352
- return lexicalNode;
353
- }
354
- function $syncPropertiesFromYjs(binding, sharedType, lexicalNode, keysChanged) {
355
- const properties = keysChanged === null ? sharedType instanceof Map$1 ? Array.from(sharedType.keys()) : sharedType instanceof XmlText || sharedType instanceof XmlElement ? Object.keys(sharedType.getAttributes()) : Object.keys(sharedType) : Array.from(keysChanged);
356
- let writableNode;
357
- for (let i = 0; i < properties.length; i++) {
358
- const property = properties[i];
359
- if (isExcludedProperty(property, lexicalNode, binding)) {
360
- if (property === '__state' && isBindingV1(binding)) {
361
- if (!writableNode) {
362
- writableNode = lexicalNode.getWritable();
363
- }
364
- $syncNodeStateToLexical(sharedType, writableNode);
365
- }
366
- continue;
367
- }
368
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
369
- const prevValue = lexicalNode[property];
370
- let nextValue = sharedTypeGet(sharedType, property);
371
- if (prevValue !== nextValue) {
372
- if (nextValue instanceof Doc) {
373
- const yjsDocMap = binding.docMap;
374
- if (prevValue instanceof Doc) {
375
- yjsDocMap.delete(prevValue.guid);
376
- }
377
- const nestedEditor = createEditor();
378
- const key = nextValue.guid;
379
- nestedEditor._key = key;
380
- yjsDocMap.set(key, nextValue);
381
- nextValue = nestedEditor;
382
- }
383
- if (writableNode === undefined) {
384
- writableNode = lexicalNode.getWritable();
385
- }
386
-
387
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
388
- writableNode[property] = nextValue;
389
- }
390
- }
391
- }
392
- function sharedTypeGet(sharedType, property) {
393
- if (sharedType instanceof Map$1) {
394
- return sharedType.get(property);
395
- } else if (sharedType instanceof XmlText || sharedType instanceof XmlElement) {
396
- return sharedType.getAttribute(property);
397
- } else {
398
- return sharedType[property];
399
- }
400
- }
401
- function sharedTypeSet(sharedType, property, nextValue) {
402
- if (sharedType instanceof Map$1) {
403
- sharedType.set(property, nextValue);
404
- } else {
405
- sharedType.setAttribute(property, nextValue);
406
- }
407
- }
408
- function $syncNodeStateToLexical(sharedType, lexicalNode) {
409
- const existingState = sharedTypeGet(sharedType, '__state');
410
- if (!(existingState instanceof Map$1)) {
411
- return;
412
- }
413
- // This should only called when creating the node initially,
414
- // incremental updates to state come in through YMapEvent
415
- // with the __state as the target.
416
- $getWritableNodeState(lexicalNode).updateFromJSON(existingState.toJSON());
417
- }
418
- function syncNodeStateFromLexical(binding, sharedType, prevLexicalNode, nextLexicalNode) {
419
- const nextState = nextLexicalNode.__state;
420
- const existingState = sharedTypeGet(sharedType, '__state');
421
- if (!nextState) {
422
- return;
423
- }
424
- const [unknown, known] = nextState.getInternalState();
425
- const prevState = prevLexicalNode && prevLexicalNode.__state;
426
- const stateMap = existingState instanceof Map$1 ? existingState : new Map$1();
427
- if (prevState === nextState) {
428
- return;
429
- }
430
- const [prevUnknown, prevKnown] = prevState && stateMap.doc ? prevState.getInternalState() : [undefined, new Map()];
431
- if (unknown) {
432
- for (const [k, v] of Object.entries(unknown)) {
433
- if (prevUnknown && v !== prevUnknown[k]) {
434
- stateMap.set(k, v);
435
- }
436
- }
437
- }
438
- for (const [stateConfig, v] of known) {
439
- if (prevKnown.get(stateConfig) !== v) {
440
- stateMap.set(stateConfig.key, stateConfig.unparse(v));
441
- }
442
- }
443
- if (!existingState) {
444
- sharedTypeSet(sharedType, '__state', stateMap);
445
- }
446
- }
447
- function syncPropertiesFromLexical(binding, sharedType, prevLexicalNode, nextLexicalNode) {
448
- const properties = Object.keys(getDefaultNodeProperties(nextLexicalNode, binding));
449
- const EditorClass = binding.editor.constructor;
450
- syncNodeStateFromLexical(binding, sharedType, prevLexicalNode, nextLexicalNode);
451
- for (let i = 0; i < properties.length; i++) {
452
- const property = properties[i];
453
- const prevValue =
454
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
455
- prevLexicalNode === null ? undefined : prevLexicalNode[property];
456
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
457
- let nextValue = nextLexicalNode[property];
458
- if (prevValue !== nextValue) {
459
- if (nextValue instanceof EditorClass) {
460
- const yjsDocMap = binding.docMap;
461
- let prevDoc;
462
- if (prevValue instanceof EditorClass) {
463
- const prevKey = prevValue._key;
464
- prevDoc = yjsDocMap.get(prevKey);
465
- yjsDocMap.delete(prevKey);
466
- }
467
-
468
- // If we already have a document, use it.
469
- const doc = prevDoc || new Doc();
470
- const key = doc.guid;
471
- nextValue._key = key;
472
- yjsDocMap.set(key, doc);
473
- nextValue = doc;
474
- // Mark the node dirty as we've assigned a new key to it
475
- binding.editor.update(() => {
476
- nextLexicalNode.markDirty();
477
- });
478
- }
479
- sharedTypeSet(sharedType, property, nextValue);
480
- }
481
- }
482
- }
483
- function spliceString(str, index, delCount, newText) {
484
- return str.slice(0, index) + newText + str.slice(index + delCount);
485
- }
486
- function getPositionFromElementAndOffset(node, offset, boundaryIsEdge) {
487
- let index = 0;
488
- let i = 0;
489
- const children = node._children;
490
- const childrenLength = children.length;
491
- for (; i < childrenLength; i++) {
492
- const child = children[i];
493
- const childOffset = index;
494
- const size = child.getSize();
495
- index += size;
496
- const exceedsBoundary = boundaryIsEdge ? index >= offset : index > offset;
497
- if (exceedsBoundary && child instanceof CollabTextNode) {
498
- let textOffset = offset - childOffset - 1;
499
- if (textOffset < 0) {
500
- textOffset = 0;
501
- }
502
- const diffLength = index - offset;
503
- return {
504
- length: diffLength,
505
- node: child,
506
- nodeIndex: i,
507
- offset: textOffset
508
- };
509
- }
510
- if (index > offset) {
511
- return {
512
- length: 0,
513
- node: child,
514
- nodeIndex: i,
515
- offset: childOffset
516
- };
517
- } else if (i === childrenLength - 1) {
518
- return {
519
- length: 0,
520
- node: null,
521
- nodeIndex: i + 1,
522
- offset: childOffset + 1
523
- };
524
- }
525
- }
526
- return {
527
- length: 0,
528
- node: null,
529
- nodeIndex: 0,
530
- offset: 0
531
- };
532
- }
533
- function doesSelectionNeedRecovering(selection) {
534
- const anchor = selection.anchor;
535
- const focus = selection.focus;
536
- let recoveryNeeded = false;
537
- try {
538
- const anchorNode = anchor.getNode();
539
- const focusNode = focus.getNode();
540
- if (
541
- // We might have removed a node that no longer exists
542
- !anchorNode.isAttached() || !focusNode.isAttached() ||
543
- // If we've split a node, then the offset might not be right
544
- $isTextNode(anchorNode) && anchor.offset > anchorNode.getTextContentSize() || $isTextNode(focusNode) && focus.offset > focusNode.getTextContentSize()) {
545
- recoveryNeeded = true;
546
- }
547
- } catch (_e) {
548
- // Sometimes checking nor a node via getNode might trigger
549
- // an error, so we need recovery then too.
550
- recoveryNeeded = true;
551
- }
552
- return recoveryNeeded;
553
- }
554
- function syncWithTransaction(binding, fn) {
555
- binding.doc.transact(fn, binding);
556
- }
557
- function $moveSelectionToPreviousNode(anchorNodeKey, currentEditorState) {
558
- const anchorNode = currentEditorState._nodeMap.get(anchorNodeKey);
559
- if (!anchorNode) {
560
- $getRoot().selectStart();
561
- return;
562
- }
563
- // Get previous node
564
- const prevNodeKey = anchorNode.__prev;
565
- let prevNode = null;
566
- if (prevNodeKey) {
567
- prevNode = $getNodeByKey(prevNodeKey);
568
- }
569
-
570
- // If previous node not found, get parent node
571
- if (prevNode === null && anchorNode.__parent !== null) {
572
- prevNode = $getNodeByKey(anchorNode.__parent);
573
- }
574
- if (prevNode === null) {
575
- $getRoot().selectStart();
576
- return;
577
- }
578
- if (prevNode !== null && prevNode.isAttached()) {
579
- prevNode.selectEnd();
580
- return;
581
- } else {
582
- // If the found node is also deleted, select the next one
583
- $moveSelectionToPreviousNode(prevNode.__key, currentEditorState);
584
- }
585
- }
586
-
587
- class CollabDecoratorNode {
588
- _xmlElem;
589
- _key;
590
- _parent;
591
- _type;
592
- constructor(xmlElem, parent, type) {
593
- this._key = '';
594
- this._xmlElem = xmlElem;
595
- this._parent = parent;
596
- this._type = type;
597
- }
598
- getPrevNode(nodeMap) {
599
- if (nodeMap === null) {
600
- return null;
601
- }
602
- const node = nodeMap.get(this._key);
603
- return $isDecoratorNode(node) ? node : null;
604
- }
605
- getNode() {
606
- const node = $getNodeByKey(this._key);
607
- return $isDecoratorNode(node) ? node : null;
608
- }
609
- getSharedType() {
610
- return this._xmlElem;
611
- }
612
- getType() {
613
- return this._type;
614
- }
615
- getKey() {
616
- return this._key;
617
- }
618
- getSize() {
619
- return 1;
620
- }
621
- getOffset() {
622
- const collabElementNode = this._parent;
623
- return collabElementNode.getChildOffset(this);
624
- }
625
- syncPropertiesFromLexical(binding, nextLexicalNode, prevNodeMap) {
626
- const prevLexicalNode = this.getPrevNode(prevNodeMap);
627
- const xmlElem = this._xmlElem;
628
- syncPropertiesFromLexical(binding, xmlElem, prevLexicalNode, nextLexicalNode);
629
- }
630
- syncPropertiesFromYjs(binding, keysChanged) {
631
- const lexicalNode = this.getNode();
632
- if (!(lexicalNode !== null)) {
633
- formatDevErrorMessage(`syncPropertiesFromYjs: could not find decorator node`);
634
- }
635
- const xmlElem = this._xmlElem;
636
- $syncPropertiesFromYjs(binding, xmlElem, lexicalNode, keysChanged);
637
- }
638
- destroy(binding) {
639
- const collabNodeMap = binding.collabNodeMap;
640
- if (collabNodeMap.get(this._key) === this) {
641
- collabNodeMap.delete(this._key);
642
- }
643
- }
644
- }
645
- function $createCollabDecoratorNode(xmlElem, parent, type) {
646
- const collabNode = new CollabDecoratorNode(xmlElem, parent, type);
647
- xmlElem._collabNode = collabNode;
648
- return collabNode;
649
- }
650
-
651
- class CollabElementNode {
652
- _key;
653
- _children;
654
- _xmlText;
655
- _type;
656
- _parent;
657
- constructor(xmlText, parent, type) {
658
- this._key = '';
659
- this._children = [];
660
- this._xmlText = xmlText;
661
- this._type = type;
662
- this._parent = parent;
663
- }
664
- getPrevNode(nodeMap) {
665
- if (nodeMap === null) {
666
- return null;
667
- }
668
- const node = nodeMap.get(this._key);
669
- return $isElementNode(node) ? node : null;
670
- }
671
- getNode() {
672
- const node = $getNodeByKey(this._key);
673
- return $isElementNode(node) ? node : null;
674
- }
675
- getSharedType() {
676
- return this._xmlText;
677
- }
678
- getType() {
679
- return this._type;
680
- }
681
- getKey() {
682
- return this._key;
683
- }
684
- isEmpty() {
685
- return this._children.length === 0;
686
- }
687
- getSize() {
688
- return 1;
689
- }
690
- getOffset() {
691
- const collabElementNode = this._parent;
692
- if (!(collabElementNode !== null)) {
693
- formatDevErrorMessage(`getOffset: could not find collab element node`);
309
+ getKey() {
310
+ return this._key;
311
+ }
312
+ isEmpty() {
313
+ return this._children.length === 0;
314
+ }
315
+ getSize() {
316
+ return 1;
317
+ }
318
+ getOffset() {
319
+ const collabElementNode = this._parent;
320
+ if (!(collabElementNode !== null)) {
321
+ formatDevErrorMessage(`getOffset: could not find collab element node`);
694
322
  }
695
323
  return collabElementNode.getChildOffset(this);
696
324
  }
@@ -1170,6 +798,11 @@ class CollabV2Mapping {
1170
798
  has(sharedType) {
1171
799
  return this._sharedTypeToNodeKeys.has(sharedType);
1172
800
  }
801
+ clear() {
802
+ this._nodeMap.clear();
803
+ this._sharedTypeToNodeKeys.clear();
804
+ this._nodeKeyToSharedType.clear();
805
+ }
1173
806
  }
1174
807
 
1175
808
  function createBaseBinding(editor, id, doc, docMap, excludedProperties) {
@@ -1194,1091 +827,1518 @@ function createBinding(editor, provider, id, doc, docMap, excludedProperties) {
1194
827
  if (!(doc !== undefined && doc !== null)) {
1195
828
  formatDevErrorMessage(`createBinding: doc is null or undefined`);
1196
829
  }
1197
- const rootXmlText = doc.get('root', XmlText);
1198
- const root = $createCollabElementNode(rootXmlText, null, 'root');
1199
- root._key = 'root';
1200
- return {
1201
- ...createBaseBinding(editor, id, doc, docMap, excludedProperties),
1202
- collabNodeMap: new Map(),
1203
- root
1204
- };
830
+ const rootXmlText = doc.get('root', XmlText);
831
+ const root = $createCollabElementNode(rootXmlText, null, 'root');
832
+ root._key = 'root';
833
+ return {
834
+ ...createBaseBinding(editor, id, doc, docMap, excludedProperties),
835
+ collabNodeMap: new Map(),
836
+ root
837
+ };
838
+ }
839
+ function createBindingV2__EXPERIMENTAL(editor, id, doc, docMap, options = {}) {
840
+ if (!(doc !== undefined && doc !== null)) {
841
+ formatDevErrorMessage(`createBinding: doc is null or undefined`);
842
+ }
843
+ const {
844
+ excludedProperties,
845
+ rootName = 'root-v2'
846
+ } = options;
847
+ return {
848
+ ...createBaseBinding(editor, id, doc, docMap, excludedProperties),
849
+ mapping: new CollabV2Mapping(),
850
+ root: doc.get(rootName, XmlElement)
851
+ };
852
+ }
853
+ function isBindingV1(binding) {
854
+ return Object.hasOwn(binding, 'collabNodeMap');
855
+ }
856
+
857
+ const baseExcludedProperties = new Set(['__key', '__parent', '__next', '__prev', '__state']);
858
+ const elementExcludedProperties = new Set(['__first', '__last', '__size']);
859
+ const rootExcludedProperties = new Set(['__cachedText']);
860
+ const textExcludedProperties = new Set(['__text']);
861
+ function isExcludedProperty(name, node, binding) {
862
+ if (baseExcludedProperties.has(name) || typeof node[name] === 'function') {
863
+ return true;
864
+ }
865
+ if ($isTextNode(node)) {
866
+ if (textExcludedProperties.has(name)) {
867
+ return true;
868
+ }
869
+ } else if ($isElementNode(node)) {
870
+ if (elementExcludedProperties.has(name) || $isRootNode(node) && rootExcludedProperties.has(name)) {
871
+ return true;
872
+ }
873
+ }
874
+ const nodeKlass = node.constructor;
875
+ const excludedProperties = binding.excludedProperties.get(nodeKlass);
876
+ return excludedProperties != null && excludedProperties.has(name);
877
+ }
878
+ function initializeNodeProperties(binding) {
879
+ const {
880
+ editor,
881
+ nodeProperties
882
+ } = binding;
883
+ editor.update(() => {
884
+ editor._nodes.forEach(nodeInfo => {
885
+ const node = new nodeInfo.klass();
886
+ const defaultProperties = {};
887
+ for (const [property, value] of Object.entries(node)) {
888
+ if (!isExcludedProperty(property, node, binding)) {
889
+ defaultProperties[property] = value;
890
+ }
891
+ }
892
+ nodeProperties.set(node.__type, Object.freeze(defaultProperties));
893
+ });
894
+ });
895
+ }
896
+ function getDefaultNodeProperties(node, binding) {
897
+ const type = node.__type;
898
+ const {
899
+ nodeProperties
900
+ } = binding;
901
+ const properties = nodeProperties.get(type);
902
+ if (!(properties !== undefined)) {
903
+ formatDevErrorMessage(`Node properties for ${type} not initialized for sync`);
904
+ }
905
+ return properties;
906
+ }
907
+ function $createCollabNodeFromLexicalNode(binding, lexicalNode, parent) {
908
+ const nodeType = lexicalNode.__type;
909
+ let collabNode;
910
+ if ($isElementNode(lexicalNode)) {
911
+ const xmlText = new XmlText();
912
+ collabNode = $createCollabElementNode(xmlText, parent, nodeType);
913
+ collabNode.syncPropertiesFromLexical(binding, lexicalNode, null);
914
+ collabNode.syncChildrenFromLexical(binding, lexicalNode, null, null, null);
915
+ } else if ($isTextNode(lexicalNode)) {
916
+ // TODO create a token text node for token, segmented nodes.
917
+ const map = new Map$1();
918
+ collabNode = $createCollabTextNode(map, lexicalNode.__text, parent, nodeType);
919
+ collabNode.syncPropertiesAndTextFromLexical(binding, lexicalNode, null);
920
+ } else if ($isLineBreakNode(lexicalNode)) {
921
+ const map = new Map$1();
922
+ map.set('__type', 'linebreak');
923
+ collabNode = $createCollabLineBreakNode(map, parent);
924
+ } else if ($isDecoratorNode(lexicalNode)) {
925
+ const xmlElem = new XmlElement();
926
+ collabNode = $createCollabDecoratorNode(xmlElem, parent, nodeType);
927
+ collabNode.syncPropertiesFromLexical(binding, lexicalNode, null);
928
+ } else {
929
+ {
930
+ formatDevErrorMessage(`Expected text, element, decorator, or linebreak node`);
931
+ }
932
+ }
933
+ collabNode._key = lexicalNode.__key;
934
+ return collabNode;
935
+ }
936
+ function getNodeTypeFromSharedType(sharedType) {
937
+ const type = sharedTypeGet(sharedType, '__type');
938
+ if (!(typeof type === 'string' || typeof type === 'undefined')) {
939
+ formatDevErrorMessage(`Expected shared type to include type attribute`);
940
+ }
941
+ return type;
942
+ }
943
+ function $getOrInitCollabNodeFromSharedType(binding, sharedType, parent) {
944
+ const collabNode = sharedType._collabNode;
945
+ if (collabNode === undefined) {
946
+ const registeredNodes = binding.editor._nodes;
947
+ const type = getNodeTypeFromSharedType(sharedType);
948
+ if (!(typeof type === 'string')) {
949
+ formatDevErrorMessage(`Expected shared type to include type attribute`);
950
+ }
951
+ const nodeInfo = registeredNodes.get(type);
952
+ if (!(nodeInfo !== undefined)) {
953
+ formatDevErrorMessage(`Node ${type} is not registered`);
954
+ }
955
+ const sharedParent = sharedType.parent;
956
+ const targetParent = parent === undefined && sharedParent !== null ? $getOrInitCollabNodeFromSharedType(binding, sharedParent) : parent || null;
957
+ if (!(targetParent instanceof CollabElementNode)) {
958
+ formatDevErrorMessage(`Expected parent to be a collab element node`);
959
+ }
960
+ if (sharedType instanceof XmlText) {
961
+ return $createCollabElementNode(sharedType, targetParent, type);
962
+ } else if (sharedType instanceof Map$1) {
963
+ if (type === 'linebreak') {
964
+ return $createCollabLineBreakNode(sharedType, targetParent);
965
+ }
966
+ return $createCollabTextNode(sharedType, '', targetParent, type);
967
+ } else if (sharedType instanceof XmlElement) {
968
+ return $createCollabDecoratorNode(sharedType, targetParent, type);
969
+ }
970
+ }
971
+ return collabNode;
972
+ }
973
+ function createLexicalNodeFromCollabNode(binding, collabNode, parentKey) {
974
+ const type = collabNode.getType();
975
+ const registeredNodes = binding.editor._nodes;
976
+ const nodeInfo = registeredNodes.get(type);
977
+ if (!(nodeInfo !== undefined)) {
978
+ formatDevErrorMessage(`Node ${type} is not registered`);
979
+ }
980
+ const lexicalNode = new nodeInfo.klass();
981
+ lexicalNode.__parent = parentKey;
982
+ collabNode._key = lexicalNode.__key;
983
+ if (collabNode instanceof CollabElementNode) {
984
+ const xmlText = collabNode._xmlText;
985
+ collabNode.syncPropertiesFromYjs(binding, null);
986
+ collabNode.applyChildrenYjsDelta(binding, xmlText.toDelta());
987
+ collabNode.syncChildrenFromYjs(binding);
988
+ } else if (collabNode instanceof CollabTextNode) {
989
+ collabNode.syncPropertiesAndTextFromYjs(binding, null);
990
+ } else if (collabNode instanceof CollabDecoratorNode) {
991
+ collabNode.syncPropertiesFromYjs(binding, null);
992
+ }
993
+ binding.collabNodeMap.set(lexicalNode.__key, collabNode);
994
+ return lexicalNode;
995
+ }
996
+ function $syncPropertiesFromYjs(binding, sharedType, lexicalNode, keysChanged) {
997
+ const properties = keysChanged === null ? sharedType instanceof Map$1 ? Array.from(sharedType.keys()) : sharedType instanceof XmlText || sharedType instanceof XmlElement ? Object.keys(sharedType.getAttributes()) : Object.keys(sharedType) : Array.from(keysChanged);
998
+ let writableNode;
999
+ for (let i = 0; i < properties.length; i++) {
1000
+ const property = properties[i];
1001
+ if (isExcludedProperty(property, lexicalNode, binding)) {
1002
+ if (property === '__state' && isBindingV1(binding)) {
1003
+ if (!writableNode) {
1004
+ writableNode = lexicalNode.getWritable();
1005
+ }
1006
+ $syncNodeStateToLexical(sharedType, writableNode);
1007
+ }
1008
+ continue;
1009
+ }
1010
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1011
+ const prevValue = lexicalNode[property];
1012
+ let nextValue = sharedTypeGet(sharedType, property);
1013
+ if (prevValue !== nextValue) {
1014
+ if (nextValue instanceof Doc) {
1015
+ const yjsDocMap = binding.docMap;
1016
+ if (prevValue instanceof Doc) {
1017
+ yjsDocMap.delete(prevValue.guid);
1018
+ }
1019
+ const nestedEditor = createEditor();
1020
+ const key = nextValue.guid;
1021
+ nestedEditor._key = key;
1022
+ yjsDocMap.set(key, nextValue);
1023
+ nextValue = nestedEditor;
1024
+ }
1025
+ if (writableNode === undefined) {
1026
+ writableNode = lexicalNode.getWritable();
1027
+ }
1028
+
1029
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1030
+ writableNode[property] = nextValue;
1031
+ }
1032
+ }
1033
+ }
1034
+ function sharedTypeGet(sharedType, property) {
1035
+ if (sharedType instanceof Map$1) {
1036
+ return sharedType.get(property);
1037
+ } else if (sharedType instanceof XmlText || sharedType instanceof XmlElement) {
1038
+ return sharedType.getAttribute(property);
1039
+ } else {
1040
+ return sharedType[property];
1041
+ }
1205
1042
  }
1206
- function createBindingV2__EXPERIMENTAL(editor, id, doc, docMap, options = {}) {
1207
- if (!(doc !== undefined && doc !== null)) {
1208
- formatDevErrorMessage(`createBinding: doc is null or undefined`);
1043
+ function sharedTypeSet(sharedType, property, nextValue) {
1044
+ if (sharedType instanceof Map$1) {
1045
+ sharedType.set(property, nextValue);
1046
+ } else {
1047
+ sharedType.setAttribute(property, nextValue);
1209
1048
  }
1210
- const {
1211
- excludedProperties,
1212
- rootName = 'root-v2'
1213
- } = options;
1214
- return {
1215
- ...createBaseBinding(editor, id, doc, docMap, excludedProperties),
1216
- mapping: new CollabV2Mapping(),
1217
- root: doc.get(rootName, XmlElement)
1218
- };
1219
1049
  }
1220
- function isBindingV1(binding) {
1221
- return Object.hasOwn(binding, 'collabNodeMap');
1050
+ function $syncNodeStateToLexical(sharedType, lexicalNode) {
1051
+ const existingState = sharedTypeGet(sharedType, '__state');
1052
+ if (!(existingState instanceof Map$1)) {
1053
+ return;
1054
+ }
1055
+ // This should only called when creating the node initially,
1056
+ // incremental updates to state come in through YMapEvent
1057
+ // with the __state as the target.
1058
+ $getWritableNodeState(lexicalNode).updateFromJSON(existingState.toJSON());
1222
1059
  }
1223
-
1224
- function createRelativePosition(point, binding) {
1225
- const collabNodeMap = binding.collabNodeMap;
1226
- const collabNode = collabNodeMap.get(point.key);
1227
- if (collabNode === undefined) {
1228
- return null;
1060
+ function syncNodeStateFromLexical(binding, sharedType, prevLexicalNode, nextLexicalNode) {
1061
+ const nextState = nextLexicalNode.__state;
1062
+ const existingState = sharedTypeGet(sharedType, '__state');
1063
+ if (!nextState) {
1064
+ return;
1229
1065
  }
1230
- let offset = point.offset;
1231
- let sharedType = collabNode.getSharedType();
1232
- if (collabNode instanceof CollabTextNode) {
1233
- sharedType = collabNode._parent._xmlText;
1234
- const currentOffset = collabNode.getOffset();
1235
- if (currentOffset === -1) {
1236
- return null;
1237
- }
1238
- offset = currentOffset + 1 + offset;
1239
- } else if (collabNode instanceof CollabElementNode && point.type === 'element') {
1240
- const parent = point.getNode();
1241
- if (!$isElementNode(parent)) {
1242
- formatDevErrorMessage(`Element point must be an element node`);
1243
- }
1244
- let accumulatedOffset = 0;
1245
- let i = 0;
1246
- let node = parent.getFirstChild();
1247
- while (node !== null && i++ < offset) {
1248
- if ($isTextNode(node)) {
1249
- accumulatedOffset += node.getTextContentSize() + 1;
1250
- } else {
1251
- accumulatedOffset++;
1066
+ const [unknown, known] = nextState.getInternalState();
1067
+ const prevState = prevLexicalNode && prevLexicalNode.__state;
1068
+ const stateMap = existingState instanceof Map$1 ? existingState : new Map$1();
1069
+ if (prevState === nextState) {
1070
+ return;
1071
+ }
1072
+ const [prevUnknown, prevKnown] = prevState && stateMap.doc ? prevState.getInternalState() : [undefined, new Map()];
1073
+ if (unknown) {
1074
+ for (const [k, v] of Object.entries(unknown)) {
1075
+ if (prevUnknown && v !== prevUnknown[k]) {
1076
+ stateMap.set(k, v);
1252
1077
  }
1253
- node = node.getNextSibling();
1254
1078
  }
1255
- offset = accumulatedOffset;
1256
- }
1257
- return createRelativePositionFromTypeIndex(sharedType, offset);
1258
- }
1259
- function createRelativePositionV2(point, binding) {
1260
- const {
1261
- mapping
1262
- } = binding;
1263
- const {
1264
- offset
1265
- } = point;
1266
- const node = point.getNode();
1267
- const yType = mapping.getSharedType(node);
1268
- if (yType === undefined) {
1269
- return null;
1270
1079
  }
1271
- if (point.type === 'text') {
1272
- if (!$isTextNode(node)) {
1273
- formatDevErrorMessage(`Text point must be a text node`);
1274
- }
1275
- let prevSibling = node.getPreviousSibling();
1276
- let adjustedOffset = offset;
1277
- while ($isTextNode(prevSibling)) {
1278
- adjustedOffset += prevSibling.getTextContentSize();
1279
- prevSibling = prevSibling.getPreviousSibling();
1280
- }
1281
- return createRelativePositionFromTypeIndex(yType, adjustedOffset);
1282
- } else if (point.type === 'element') {
1283
- if (!$isElementNode(node)) {
1284
- formatDevErrorMessage(`Element point must be an element node`);
1080
+ for (const [stateConfig, v] of known) {
1081
+ if (prevKnown.get(stateConfig) !== v) {
1082
+ stateMap.set(stateConfig.key, stateConfig.unparse(v));
1285
1083
  }
1286
- let i = 0;
1287
- let child = node.getFirstChild();
1288
- while (child !== null && i < offset) {
1289
- if ($isTextNode(child)) {
1290
- let nextSibling = child.getNextSibling();
1291
- while ($isTextNode(nextSibling)) {
1292
- nextSibling = nextSibling.getNextSibling();
1084
+ }
1085
+ if (!existingState) {
1086
+ sharedTypeSet(sharedType, '__state', stateMap);
1087
+ }
1088
+ }
1089
+ function syncPropertiesFromLexical(binding, sharedType, prevLexicalNode, nextLexicalNode) {
1090
+ const properties = Object.keys(getDefaultNodeProperties(nextLexicalNode, binding));
1091
+ const EditorClass = binding.editor.constructor;
1092
+ syncNodeStateFromLexical(binding, sharedType, prevLexicalNode, nextLexicalNode);
1093
+ for (let i = 0; i < properties.length; i++) {
1094
+ const property = properties[i];
1095
+ const prevValue =
1096
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1097
+ prevLexicalNode === null ? undefined : prevLexicalNode[property];
1098
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1099
+ let nextValue = nextLexicalNode[property];
1100
+ if (prevValue !== nextValue) {
1101
+ if (nextValue instanceof EditorClass) {
1102
+ const yjsDocMap = binding.docMap;
1103
+ let prevDoc;
1104
+ if (prevValue instanceof EditorClass) {
1105
+ const prevKey = prevValue._key;
1106
+ prevDoc = yjsDocMap.get(prevKey);
1107
+ yjsDocMap.delete(prevKey);
1293
1108
  }
1109
+
1110
+ // If we already have a document, use it.
1111
+ const doc = prevDoc || new Doc();
1112
+ const key = doc.guid;
1113
+ nextValue._key = key;
1114
+ yjsDocMap.set(key, doc);
1115
+ nextValue = doc;
1116
+ // Mark the node dirty as we've assigned a new key to it
1117
+ binding.editor.update(() => {
1118
+ nextLexicalNode.markDirty();
1119
+ });
1294
1120
  }
1295
- i++;
1296
- child = child.getNextSibling();
1121
+ sharedTypeSet(sharedType, property, nextValue);
1297
1122
  }
1298
- return createRelativePositionFromTypeIndex(yType, i);
1299
1123
  }
1300
- return null;
1301
1124
  }
1302
- function createAbsolutePosition(relativePosition, binding) {
1303
- return createAbsolutePositionFromRelativePosition(relativePosition, binding.doc);
1125
+ function spliceString(str, index, delCount, newText) {
1126
+ return str.slice(0, index) + newText + str.slice(index + delCount);
1304
1127
  }
1305
- function shouldUpdatePosition(currentPos, pos) {
1306
- if (currentPos == null) {
1307
- if (pos != null) {
1308
- return true;
1128
+ function getPositionFromElementAndOffset(node, offset, boundaryIsEdge) {
1129
+ let index = 0;
1130
+ let i = 0;
1131
+ const children = node._children;
1132
+ const childrenLength = children.length;
1133
+ for (; i < childrenLength; i++) {
1134
+ const child = children[i];
1135
+ const childOffset = index;
1136
+ const size = child.getSize();
1137
+ index += size;
1138
+ const exceedsBoundary = boundaryIsEdge ? index >= offset : index > offset;
1139
+ if (exceedsBoundary && child instanceof CollabTextNode) {
1140
+ let textOffset = offset - childOffset - 1;
1141
+ if (textOffset < 0) {
1142
+ textOffset = 0;
1143
+ }
1144
+ const diffLength = index - offset;
1145
+ return {
1146
+ length: diffLength,
1147
+ node: child,
1148
+ nodeIndex: i,
1149
+ offset: textOffset
1150
+ };
1151
+ }
1152
+ if (index > offset) {
1153
+ return {
1154
+ length: 0,
1155
+ node: child,
1156
+ nodeIndex: i,
1157
+ offset: childOffset
1158
+ };
1159
+ } else if (i === childrenLength - 1) {
1160
+ return {
1161
+ length: 0,
1162
+ node: null,
1163
+ nodeIndex: i + 1,
1164
+ offset: childOffset + 1
1165
+ };
1309
1166
  }
1310
- } else if (pos == null || !compareRelativePositions(currentPos, pos)) {
1311
- return true;
1312
1167
  }
1313
- return false;
1314
- }
1315
- function createCursor(name, color) {
1316
1168
  return {
1317
- color: color,
1318
- name: name,
1319
- selection: null
1169
+ length: 0,
1170
+ node: null,
1171
+ nodeIndex: 0,
1172
+ offset: 0
1320
1173
  };
1321
1174
  }
1322
- function destroySelection(binding, selection) {
1323
- const cursorsContainer = binding.cursorsContainer;
1324
- if (cursorsContainer !== null) {
1325
- const selections = selection.selections;
1326
- const selectionsLength = selections.length;
1327
- for (let i = 0; i < selectionsLength; i++) {
1328
- cursorsContainer.removeChild(selections[i]);
1175
+ function doesSelectionNeedRecovering(selection) {
1176
+ const anchor = selection.anchor;
1177
+ const focus = selection.focus;
1178
+ let recoveryNeeded = false;
1179
+ try {
1180
+ const anchorNode = anchor.getNode();
1181
+ const focusNode = focus.getNode();
1182
+ if (
1183
+ // We might have removed a node that no longer exists
1184
+ !anchorNode.isAttached() || !focusNode.isAttached() ||
1185
+ // If we've split a node, then the offset might not be right
1186
+ $isTextNode(anchorNode) && anchor.offset > anchorNode.getTextContentSize() || $isTextNode(focusNode) && focus.offset > focusNode.getTextContentSize()) {
1187
+ recoveryNeeded = true;
1329
1188
  }
1189
+ } catch (_e) {
1190
+ // Sometimes checking nor a node via getNode might trigger
1191
+ // an error, so we need recovery then too.
1192
+ recoveryNeeded = true;
1193
+ }
1194
+ return recoveryNeeded;
1195
+ }
1196
+ function syncWithTransaction(binding, fn) {
1197
+ binding.doc.transact(fn, binding);
1198
+ }
1199
+ function $moveSelectionToPreviousNode(anchorNodeKey, currentEditorState) {
1200
+ const anchorNode = currentEditorState._nodeMap.get(anchorNodeKey);
1201
+ if (!anchorNode) {
1202
+ $getRoot().selectStart();
1203
+ return;
1204
+ }
1205
+ // Get previous node
1206
+ const prevNodeKey = anchorNode.__prev;
1207
+ let prevNode = null;
1208
+ if (prevNodeKey) {
1209
+ prevNode = $getNodeByKey(prevNodeKey);
1330
1210
  }
1331
- }
1332
- function destroyCursor(binding, cursor) {
1333
- const selection = cursor.selection;
1334
- if (selection !== null) {
1335
- destroySelection(binding, selection);
1211
+
1212
+ // If previous node not found, get parent node
1213
+ if (prevNode === null && anchorNode.__parent !== null) {
1214
+ prevNode = $getNodeByKey(anchorNode.__parent);
1336
1215
  }
1337
- }
1338
- function createCursorSelection(cursor, anchorKey, anchorOffset, focusKey, focusOffset) {
1339
- const color = cursor.color;
1340
- const caret = document.createElement('span');
1341
- caret.style.cssText = `position:absolute;top:0;bottom:0;right:-1px;width:1px;background-color:${color};z-index:10;`;
1342
- const name = document.createElement('span');
1343
- name.textContent = cursor.name;
1344
- 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;`;
1345
- caret.appendChild(name);
1346
- return {
1347
- anchor: {
1348
- key: anchorKey,
1349
- offset: anchorOffset
1350
- },
1351
- caret,
1352
- color,
1353
- focus: {
1354
- key: focusKey,
1355
- offset: focusOffset
1356
- },
1357
- name,
1358
- selections: []
1359
- };
1360
- }
1361
- function updateCursor(binding, cursor, nextSelection, nodeMap) {
1362
- const editor = binding.editor;
1363
- const rootElement = editor.getRootElement();
1364
- const cursorsContainer = binding.cursorsContainer;
1365
- if (cursorsContainer === null || rootElement === null) {
1216
+ if (prevNode === null) {
1217
+ $getRoot().selectStart();
1366
1218
  return;
1367
1219
  }
1368
- const cursorsContainerOffsetParent = cursorsContainer.offsetParent;
1369
- if (cursorsContainerOffsetParent === null) {
1220
+ if (prevNode !== null && prevNode.isAttached()) {
1221
+ prevNode.selectEnd();
1370
1222
  return;
1223
+ } else {
1224
+ // If the found node is also deleted, select the next one
1225
+ $moveSelectionToPreviousNode(prevNode.__key, currentEditorState);
1371
1226
  }
1372
- const containerRect = cursorsContainerOffsetParent.getBoundingClientRect();
1373
- const prevSelection = cursor.selection;
1374
- if (nextSelection === null) {
1375
- if (prevSelection === null) {
1376
- return;
1227
+ }
1228
+
1229
+ // https://docs.yjs.dev/api/shared-types/y.xmlelement
1230
+ // "Define a top-level type; Note that the nodeName is always "undefined""
1231
+ const isRootElement = el => el.nodeName === 'UNDEFINED';
1232
+ const $createOrUpdateNodeFromYElement = (el, binding, keysChanged, childListChanged, snapshot, prevSnapshot, computeYChange) => {
1233
+ let node = binding.mapping.get(el);
1234
+ if (node && keysChanged && keysChanged.size === 0 && !childListChanged) {
1235
+ return node;
1236
+ }
1237
+ const type = isRootElement(el) ? RootNode.getType() : el.nodeName;
1238
+ const registeredNodes = binding.editor._nodes;
1239
+ const nodeInfo = registeredNodes.get(type);
1240
+ if (nodeInfo === undefined) {
1241
+ throw new Error(`$createOrUpdateNodeFromYElement: Node ${type} is not registered`);
1242
+ }
1243
+ if (!node) {
1244
+ node = new nodeInfo.klass();
1245
+ keysChanged = null;
1246
+ childListChanged = true;
1247
+ }
1248
+ if (childListChanged && node instanceof ElementNode) {
1249
+ const children = [];
1250
+ const $createChildren = childType => {
1251
+ if (childType instanceof XmlElement) {
1252
+ const n = $createOrUpdateNodeFromYElement(childType, binding, new Set(), false, snapshot, prevSnapshot, computeYChange);
1253
+ if (n !== null) {
1254
+ children.push(n);
1255
+ }
1256
+ } else if (childType instanceof XmlText) {
1257
+ const ns = $createOrUpdateTextNodesFromYText(childType, binding, snapshot, prevSnapshot, computeYChange);
1258
+ if (ns !== null) {
1259
+ ns.forEach(textchild => {
1260
+ if (textchild !== null) {
1261
+ children.push(textchild);
1262
+ }
1263
+ });
1264
+ }
1265
+ } else {
1266
+ {
1267
+ formatDevErrorMessage(`XmlHook is not supported`);
1268
+ }
1269
+ }
1270
+ };
1271
+ if (snapshot === undefined || prevSnapshot === undefined) {
1272
+ el.toArray().forEach($createChildren);
1377
1273
  } else {
1378
- cursor.selection = null;
1379
- destroySelection(binding, prevSelection);
1380
- return;
1274
+ typeListToArraySnapshot(el, new Snapshot(prevSnapshot.ds, snapshot.sv)).forEach($createChildren);
1381
1275
  }
1382
- } else {
1383
- cursor.selection = nextSelection;
1384
- }
1385
- const caret = nextSelection.caret;
1386
- const color = nextSelection.color;
1387
- const selections = nextSelection.selections;
1388
- const anchor = nextSelection.anchor;
1389
- const focus = nextSelection.focus;
1390
- const anchorKey = anchor.key;
1391
- const focusKey = focus.key;
1392
- const anchorNode = nodeMap.get(anchorKey);
1393
- const focusNode = nodeMap.get(focusKey);
1394
- if (anchorNode == null || focusNode == null) {
1395
- return;
1276
+ $spliceChildren(node, children);
1396
1277
  }
1397
- let selectionRects;
1398
1278
 
1399
- // In the case of a collapsed selection on a linebreak, we need
1400
- // to improvise as the browser will return nothing here as <br>
1401
- // apparently take up no visual space :/
1402
- // This won't work in all cases, but it's better than just showing
1403
- // nothing all the time.
1404
- if (anchorNode === focusNode && $isLineBreakNode(anchorNode)) {
1405
- const brRect = editor.getElementByKey(anchorKey).getBoundingClientRect();
1406
- selectionRects = [brRect];
1407
- } else {
1408
- const range = createDOMRange(editor, anchorNode, anchor.offset, focusNode, focus.offset);
1409
- if (range === null) {
1410
- return;
1279
+ // TODO(collab-v2): typing for XmlElement generic
1280
+ const attrs = el.getAttributes(snapshot);
1281
+ if (!isRootElement(el) && snapshot !== undefined) {
1282
+ if (!isItemVisible(el._item, snapshot)) {
1283
+ attrs[stateKeyToAttrKey('ychange')] = computeYChange ? computeYChange('removed', el._item.id) : {
1284
+ type: 'removed'
1285
+ };
1286
+ } else if (!isItemVisible(el._item, prevSnapshot)) {
1287
+ attrs[stateKeyToAttrKey('ychange')] = computeYChange ? computeYChange('added', el._item.id) : {
1288
+ type: 'added'
1289
+ };
1411
1290
  }
1412
- selectionRects = createRectsFromDOMRange(editor, range);
1413
1291
  }
1414
- const selectionsLength = selections.length;
1415
- const selectionRectsLength = selectionRects.length;
1416
- for (let i = 0; i < selectionRectsLength; i++) {
1417
- const selectionRect = selectionRects[i];
1418
- let selection = selections[i];
1419
- if (selection === undefined) {
1420
- selection = document.createElement('span');
1421
- selections[i] = selection;
1422
- const selectionBg = document.createElement('span');
1423
- selection.appendChild(selectionBg);
1424
- cursorsContainer.appendChild(selection);
1292
+ const properties = {
1293
+ ...getDefaultNodeProperties(node, binding)
1294
+ };
1295
+ const state = {};
1296
+ for (const k in attrs) {
1297
+ if (k.startsWith(STATE_KEY_PREFIX)) {
1298
+ state[attrKeyToStateKey(k)] = attrs[k];
1299
+ } else {
1300
+ properties[k] = attrs[k];
1425
1301
  }
1426
- const top = selectionRect.top - containerRect.top;
1427
- const left = selectionRect.left - containerRect.left;
1428
- const style = `position:absolute;top:${top}px;left:${left}px;height:${selectionRect.height}px;width:${selectionRect.width}px;pointer-events:none;z-index:5;`;
1429
- selection.style.cssText = style;
1430
- selection.firstChild.style.cssText = `${style}left:0;top:0;background-color:${color};opacity:0.3;`;
1431
- if (i === selectionRectsLength - 1) {
1432
- if (caret.parentNode !== selection) {
1433
- selection.appendChild(caret);
1302
+ }
1303
+ $syncPropertiesFromYjs(binding, properties, node, keysChanged);
1304
+ if (!keysChanged) {
1305
+ $getWritableNodeState(node).updateFromJSON(state);
1306
+ } else {
1307
+ const stateKeysChanged = Object.keys(state).filter(k => keysChanged.has(stateKeyToAttrKey(k)));
1308
+ if (stateKeysChanged.length > 0) {
1309
+ const writableState = $getWritableNodeState(node);
1310
+ for (const k of stateKeysChanged) {
1311
+ writableState.updateFromUnknown(k, state[k]);
1434
1312
  }
1435
1313
  }
1436
1314
  }
1437
- for (let i = selectionsLength - 1; i >= selectionRectsLength; i--) {
1438
- const selection = selections[i];
1439
- cursorsContainer.removeChild(selection);
1440
- selections.pop();
1441
- }
1442
- }
1443
- /**
1444
- * @deprecated Use `$getAnchorAndFocusForUserState` instead.
1445
- */
1446
- function getAnchorAndFocusCollabNodesForUserState(binding, userState) {
1447
- const {
1448
- anchorPos,
1449
- focusPos
1450
- } = userState;
1451
- let anchorCollabNode = null;
1452
- let anchorOffset = 0;
1453
- let focusCollabNode = null;
1454
- let focusOffset = 0;
1455
- if (anchorPos !== null && focusPos !== null) {
1456
- const anchorAbsPos = createAbsolutePosition(anchorPos, binding);
1457
- const focusAbsPos = createAbsolutePosition(focusPos, binding);
1458
- if (anchorAbsPos !== null && focusAbsPos !== null) {
1459
- [anchorCollabNode, anchorOffset] = getCollabNodeAndOffset(anchorAbsPos.type, anchorAbsPos.index);
1460
- [focusCollabNode, focusOffset] = getCollabNodeAndOffset(focusAbsPos.type, focusAbsPos.index);
1315
+ const latestNode = node.getLatest();
1316
+ binding.mapping.set(el, latestNode);
1317
+ return latestNode;
1318
+ };
1319
+ const $spliceChildren = (node, nextChildren) => {
1320
+ const prevChildren = node.getChildren();
1321
+ const prevChildrenKeySet = new Set(prevChildren.map(child => child.getKey()));
1322
+ const nextChildrenKeySet = new Set(nextChildren.map(child => child.getKey()));
1323
+ const prevEndIndex = prevChildren.length - 1;
1324
+ const nextEndIndex = nextChildren.length - 1;
1325
+ let prevIndex = 0;
1326
+ let nextIndex = 0;
1327
+ while (prevIndex <= prevEndIndex && nextIndex <= nextEndIndex) {
1328
+ const prevKey = prevChildren[prevIndex].getKey();
1329
+ const nextKey = nextChildren[nextIndex].getKey();
1330
+ if (prevKey === nextKey) {
1331
+ prevIndex++;
1332
+ nextIndex++;
1333
+ continue;
1334
+ }
1335
+ const nextHasPrevKey = nextChildrenKeySet.has(prevKey);
1336
+ const prevHasNextKey = prevChildrenKeySet.has(nextKey);
1337
+ if (!nextHasPrevKey) {
1338
+ // If removing the last node, insert remaining new nodes immediately, otherwise if the node
1339
+ // cannot be empty, it will remove itself from its parent.
1340
+ if (nextIndex === 0 && node.getChildrenSize() === 1) {
1341
+ node.splice(nextIndex, 1, nextChildren.slice(nextIndex));
1342
+ return;
1343
+ }
1344
+ // Remove
1345
+ node.splice(nextIndex, 1, []);
1346
+ prevIndex++;
1347
+ continue;
1348
+ }
1349
+
1350
+ // Create or replace
1351
+ const nextChildNode = nextChildren[nextIndex];
1352
+ if (prevHasNextKey) {
1353
+ node.splice(nextIndex, 1, [nextChildNode]);
1354
+ prevIndex++;
1355
+ nextIndex++;
1356
+ } else {
1357
+ node.splice(nextIndex, 0, [nextChildNode]);
1358
+ nextIndex++;
1461
1359
  }
1462
1360
  }
1463
- return {
1464
- anchorCollabNode,
1465
- anchorOffset,
1466
- focusCollabNode,
1467
- focusOffset
1468
- };
1469
- }
1470
- function $getAnchorAndFocusForUserState(binding, userState) {
1471
- const {
1472
- anchorPos,
1473
- focusPos
1474
- } = userState;
1475
- const anchorAbsPos = anchorPos ? createAbsolutePosition(anchorPos, binding) : null;
1476
- const focusAbsPos = focusPos ? createAbsolutePosition(focusPos, binding) : null;
1477
- if (anchorAbsPos === null || focusAbsPos === null) {
1478
- return {
1479
- anchorKey: null,
1480
- anchorOffset: 0,
1481
- focusKey: null,
1482
- focusOffset: 0
1483
- };
1484
- }
1485
- if (isBindingV1(binding)) {
1486
- const [anchorCollabNode, anchorOffset] = getCollabNodeAndOffset(anchorAbsPos.type, anchorAbsPos.index);
1487
- const [focusCollabNode, focusOffset] = getCollabNodeAndOffset(focusAbsPos.type, focusAbsPos.index);
1488
- return {
1489
- anchorKey: anchorCollabNode !== null ? anchorCollabNode.getKey() : null,
1490
- anchorOffset,
1491
- focusKey: focusCollabNode !== null ? focusCollabNode.getKey() : null,
1492
- focusOffset
1493
- };
1361
+ const appendNewChildren = prevIndex > prevEndIndex;
1362
+ const removeOldChildren = nextIndex > nextEndIndex;
1363
+ if (appendNewChildren && !removeOldChildren) {
1364
+ node.append(...nextChildren.slice(nextIndex));
1365
+ } else if (removeOldChildren && !appendNewChildren) {
1366
+ node.splice(nextChildren.length, node.getChildrenSize() - nextChildren.length, []);
1494
1367
  }
1495
- let [anchorNode, anchorOffset] = $getNodeAndOffsetV2(binding.mapping, anchorAbsPos);
1496
- let [focusNode, focusOffset] = $getNodeAndOffsetV2(binding.mapping, focusAbsPos);
1497
- // For a non-collapsed selection, if the start of the selection is as the end of a text node,
1498
- // move it to the beginning of the next text node (if one exists).
1499
- if (focusNode && anchorNode && (focusNode !== anchorNode || focusOffset !== anchorOffset)) {
1500
- const isBackwards = focusNode.isBefore(anchorNode);
1501
- const startNode = isBackwards ? focusNode : anchorNode;
1502
- const startOffset = isBackwards ? focusOffset : anchorOffset;
1503
- if ($isTextNode(startNode) && $isTextNode(startNode.getNextSibling()) && startOffset === startNode.getTextContentSize()) {
1504
- if (isBackwards) {
1505
- focusNode = startNode.getNextSibling();
1506
- focusOffset = 0;
1507
- } else {
1508
- anchorNode = startNode.getNextSibling();
1509
- anchorOffset = 0;
1368
+ };
1369
+ const isItemVisible = (item, snapshot) => snapshot === undefined ? !item.deleted : snapshot.sv.has(item.id.client) && snapshot.sv.get(item.id.client) > item.id.clock && !isDeleted(snapshot.ds, item.id);
1370
+ const $createOrUpdateTextNodesFromYText = (text, binding, snapshot, prevSnapshot, computeYChange) => {
1371
+ const deltas = toDelta(text, snapshot, prevSnapshot, computeYChange);
1372
+
1373
+ // Use existing text nodes if the count and types all align, otherwise throw out the existing
1374
+ // nodes and create new ones.
1375
+ let nodes = binding.mapping.get(text) ?? [];
1376
+ const nodeTypes = deltas.map(delta => delta.attributes.t ?? TextNode.getType());
1377
+ const canReuseNodes = nodes.length === nodeTypes.length && nodes.every((node, i) => node.getType() === nodeTypes[i]);
1378
+ if (!canReuseNodes) {
1379
+ const registeredNodes = binding.editor._nodes;
1380
+ nodes = nodeTypes.map(type => {
1381
+ const nodeInfo = registeredNodes.get(type);
1382
+ if (nodeInfo === undefined) {
1383
+ throw new Error(`$createTextNodesFromYText: Node ${type} is not registered`);
1384
+ }
1385
+ const node = new nodeInfo.klass();
1386
+ if (!$isTextNode(node)) {
1387
+ throw new Error(`$createTextNodesFromYText: Node ${type} is not a TextNode`);
1510
1388
  }
1389
+ return node;
1390
+ });
1391
+ }
1392
+
1393
+ // Sync text, properties and state to the text nodes.
1394
+ for (let i = 0; i < deltas.length; i++) {
1395
+ const node = nodes[i];
1396
+ const delta = deltas[i];
1397
+ const {
1398
+ attributes,
1399
+ insert
1400
+ } = delta;
1401
+ if (node.__text !== insert) {
1402
+ node.setTextContent(insert);
1511
1403
  }
1404
+ const properties = {
1405
+ ...getDefaultNodeProperties(node, binding),
1406
+ ...attributes.p
1407
+ };
1408
+ const state = Object.fromEntries(Object.entries(attributes).filter(([k]) => k.startsWith(STATE_KEY_PREFIX)).map(([k, v]) => [attrKeyToStateKey(k), v]));
1409
+ $syncPropertiesFromYjs(binding, properties, node, null);
1410
+ $getWritableNodeState(node).updateFromJSON(state);
1512
1411
  }
1513
- return {
1514
- anchorKey: anchorNode !== null ? anchorNode.getKey() : null,
1515
- anchorOffset,
1516
- focusKey: focusNode !== null ? focusNode.getKey() : null,
1517
- focusOffset
1412
+ const latestNodes = nodes.map(node => node.getLatest());
1413
+ binding.mapping.set(text, latestNodes);
1414
+ return latestNodes;
1415
+ };
1416
+ const $createTypeFromTextNodes = (nodes, binding) => {
1417
+ const type = new XmlText();
1418
+ $updateYText(type, nodes, binding);
1419
+ return type;
1420
+ };
1421
+ const createTypeFromElementNode = (node, binding) => {
1422
+ const type = new XmlElement(node.getType());
1423
+ const attrs = {
1424
+ ...propertiesToAttributes(node, binding),
1425
+ ...stateToAttributes(node)
1518
1426
  };
1519
- }
1520
- function $syncLocalCursorPosition(binding, provider) {
1521
- const awareness = provider.awareness;
1522
- const localState = awareness.getLocalState();
1523
- if (localState === null) {
1524
- return;
1525
- }
1526
- const {
1527
- anchorKey,
1528
- anchorOffset,
1529
- focusKey,
1530
- focusOffset
1531
- } = $getAnchorAndFocusForUserState(binding, localState);
1532
- if (anchorKey !== null && focusKey !== null) {
1533
- const selection = $getSelection();
1534
- if (!$isRangeSelection(selection)) {
1535
- return;
1427
+ for (const key in attrs) {
1428
+ const val = attrs[key];
1429
+ if (val !== null) {
1430
+ // TODO(collab-v2): typing for XmlElement generic
1431
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1432
+ type.setAttribute(key, val);
1536
1433
  }
1537
- $setPoint(selection.anchor, anchorKey, anchorOffset);
1538
- $setPoint(selection.focus, focusKey, focusOffset);
1539
1434
  }
1540
- }
1541
- function $setPoint(point, key, offset) {
1542
- if (point.key !== key || point.offset !== offset) {
1543
- let anchorNode = $getNodeByKey(key);
1544
- if (anchorNode !== null && !$isElementNode(anchorNode) && !$isTextNode(anchorNode)) {
1545
- const parent = anchorNode.getParentOrThrow();
1546
- key = parent.getKey();
1547
- offset = anchorNode.getIndexWithinParent();
1548
- anchorNode = parent;
1549
- }
1550
- point.set(key, offset, $isElementNode(anchorNode) ? 'element' : 'text');
1435
+ if (!(node instanceof ElementNode)) {
1436
+ return type;
1551
1437
  }
1552
- }
1553
- function getCollabNodeAndOffset(
1554
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1555
- sharedType, offset) {
1556
- const collabNode = sharedType._collabNode;
1557
- if (collabNode === undefined) {
1558
- return [null, 0];
1438
+ type.insert(0, normalizeNodeContent(node).map(n => $createTypeFromTextOrElementNode(n, binding)));
1439
+ binding.mapping.set(type, node);
1440
+ return type;
1441
+ };
1442
+ const $createTypeFromTextOrElementNode = (node, meta) => node instanceof Array ? $createTypeFromTextNodes(node, meta) : createTypeFromElementNode(node, meta);
1443
+ const isObject = val => typeof val === 'object' && val != null;
1444
+ const equalAttrs = (pattrs, yattrs) => {
1445
+ const keys = Object.keys(pattrs).filter(key => pattrs[key] !== null);
1446
+ if (yattrs == null) {
1447
+ return keys.length === 0;
1559
1448
  }
1560
- if (collabNode instanceof CollabElementNode) {
1561
- const {
1562
- node,
1563
- offset: collabNodeOffset
1564
- } = getPositionFromElementAndOffset(collabNode, offset, true);
1565
- if (node === null) {
1566
- return [collabNode, 0];
1567
- } else {
1568
- return [node, collabNodeOffset];
1569
- }
1449
+ let eq = keys.length === Object.keys(yattrs).filter(key => yattrs[key] !== null).length;
1450
+ for (let i = 0; i < keys.length && eq; i++) {
1451
+ const key = keys[i];
1452
+ const l = pattrs[key];
1453
+ const r = yattrs[key];
1454
+ eq = key === 'ychange' || l === r || isObject(l) && isObject(r) && equalAttrs(l, r);
1570
1455
  }
1571
- return [null, 0];
1572
- }
1573
- function $getNodeAndOffsetV2(mapping, absolutePosition) {
1574
- const yType = absolutePosition.type;
1575
- const yOffset = absolutePosition.index;
1576
- if (yType instanceof XmlElement) {
1577
- const node = mapping.get(yType);
1578
- if (node === undefined) {
1579
- return [null, 0];
1580
- }
1581
- if (!$isElementNode(node)) {
1582
- return [node, yOffset];
1583
- }
1584
- let remainingYOffset = yOffset;
1585
- let lexicalOffset = 0;
1586
- const children = node.getChildren();
1587
- while (remainingYOffset > 0 && lexicalOffset < children.length) {
1588
- const child = children[lexicalOffset];
1589
- remainingYOffset -= 1;
1590
- lexicalOffset += 1;
1591
- if ($isTextNode(child)) {
1592
- while (lexicalOffset < children.length && $isTextNode(children[lexicalOffset])) {
1593
- lexicalOffset += 1;
1594
- }
1456
+ return eq;
1457
+ };
1458
+ const normalizeNodeContent = node => {
1459
+ if (!(node instanceof ElementNode)) {
1460
+ return [];
1461
+ }
1462
+ const c = node.getChildren();
1463
+ const res = [];
1464
+ for (let i = 0; i < c.length; i++) {
1465
+ const n = c[i];
1466
+ if ($isTextNode(n)) {
1467
+ const textNodes = [];
1468
+ for (let maybeTextNode = c[i]; i < c.length && $isTextNode(maybeTextNode); maybeTextNode = c[++i]) {
1469
+ textNodes.push(maybeTextNode);
1595
1470
  }
1471
+ i--;
1472
+ res.push(textNodes);
1473
+ } else {
1474
+ res.push(n);
1596
1475
  }
1597
- return [node, lexicalOffset];
1598
- } else {
1599
- const nodes = mapping.get(yType);
1600
- if (nodes === undefined) {
1601
- return [null, 0];
1476
+ }
1477
+ return res;
1478
+ };
1479
+ const equalYTextLText = (ytext, ltexts, binding) => {
1480
+ const deltas = toDelta(ytext);
1481
+ return deltas.length === ltexts.length && deltas.every((d, i) => {
1482
+ const ltext = ltexts[i];
1483
+ const type = d.attributes.t ?? TextNode.getType();
1484
+ const propertyAttrs = d.attributes.p ?? {};
1485
+ const stateAttrs = Object.fromEntries(Object.entries(d.attributes).filter(([k]) => k.startsWith(STATE_KEY_PREFIX)));
1486
+ return d.insert === ltext.getTextContent() && type === ltext.getType() && equalAttrs(propertyAttrs, propertiesToAttributes(ltext, binding)) && equalAttrs(stateAttrs, stateToAttributes(ltext));
1487
+ });
1488
+ };
1489
+ const equalYTypePNode = (ytype, lnode, binding) => {
1490
+ if (ytype instanceof XmlElement && !(lnode instanceof Array) && matchNodeName(ytype, lnode)) {
1491
+ const normalizedContent = normalizeNodeContent(lnode);
1492
+ return ytype._length === normalizedContent.length && equalAttrs(ytype.getAttributes(), {
1493
+ ...propertiesToAttributes(lnode, binding),
1494
+ ...stateToAttributes(lnode)
1495
+ }) && ytype.toArray().every((ychild, i) => equalYTypePNode(ychild, normalizedContent[i], binding));
1496
+ }
1497
+ return ytype instanceof XmlText && lnode instanceof Array && equalYTextLText(ytype, lnode, binding);
1498
+ };
1499
+ const mappedIdentity = (mapped, lcontent) => mapped === lcontent || mapped instanceof Array && lcontent instanceof Array && mapped.length === lcontent.length && mapped.every((a, i) => lcontent[i] === a);
1500
+ const computeChildEqualityFactor = (ytype, lnode, binding) => {
1501
+ const yChildren = ytype.toArray();
1502
+ const pChildren = normalizeNodeContent(lnode);
1503
+ const pChildCnt = pChildren.length;
1504
+ const yChildCnt = yChildren.length;
1505
+ const minCnt = Math.min(yChildCnt, pChildCnt);
1506
+ let left = 0;
1507
+ let right = 0;
1508
+ let foundMappedChild = false;
1509
+ for (; left < minCnt; left++) {
1510
+ const leftY = yChildren[left];
1511
+ const leftP = pChildren[left];
1512
+ if (leftY instanceof XmlHook) {
1513
+ break;
1514
+ } else if (mappedIdentity(binding.mapping.get(leftY), leftP)) {
1515
+ foundMappedChild = true; // definite (good) match!
1516
+ } else if (!equalYTypePNode(leftY, leftP, binding)) {
1517
+ break;
1602
1518
  }
1603
- let i = 0;
1604
- let adjustedOffset = yOffset;
1605
- while (adjustedOffset > nodes[i].getTextContentSize() && i + 1 < nodes.length) {
1606
- adjustedOffset -= nodes[i].getTextContentSize();
1607
- i++;
1519
+ }
1520
+ for (; left + right < minCnt; right++) {
1521
+ const rightY = yChildren[yChildCnt - right - 1];
1522
+ const rightP = pChildren[pChildCnt - right - 1];
1523
+ if (rightY instanceof XmlHook) {
1524
+ break;
1525
+ } else if (mappedIdentity(binding.mapping.get(rightY), rightP)) {
1526
+ foundMappedChild = true;
1527
+ } else if (!equalYTypePNode(rightY, rightP, binding)) {
1528
+ break;
1608
1529
  }
1609
- const textNode = nodes[i];
1610
- return [textNode, Math.min(adjustedOffset, textNode.getTextContentSize())];
1611
1530
  }
1612
- }
1613
- function getAwarenessStatesDefault(_binding, provider) {
1614
- return provider.awareness.getStates();
1615
- }
1616
- function syncCursorPositions(binding, provider, options) {
1617
- const {
1618
- getAwarenessStates = getAwarenessStatesDefault
1619
- } = options ?? {};
1620
- const awarenessStates = Array.from(getAwarenessStates(binding, provider));
1621
- const localClientID = binding.clientID;
1622
- const cursors = binding.cursors;
1623
- const editor = binding.editor;
1624
- const nodeMap = editor._editorState._nodeMap;
1625
- const visitedClientIDs = new Set();
1626
- for (let i = 0; i < awarenessStates.length; i++) {
1627
- const awarenessState = awarenessStates[i];
1628
- const [clientID, awareness] = awarenessState;
1629
- if (clientID !== localClientID) {
1630
- visitedClientIDs.add(clientID);
1631
- const {
1632
- name,
1633
- color,
1634
- focusing
1635
- } = awareness;
1636
- let selection = null;
1637
- let cursor = cursors.get(clientID);
1638
- if (cursor === undefined) {
1639
- cursor = createCursor(name, color);
1640
- cursors.set(clientID, cursor);
1641
- }
1642
- if (focusing) {
1643
- const {
1644
- anchorKey,
1645
- anchorOffset,
1646
- focusKey,
1647
- focusOffset
1648
- } = editor.read(() => $getAnchorAndFocusForUserState(binding, awareness));
1649
- if (anchorKey !== null && focusKey !== null) {
1650
- selection = cursor.selection;
1651
- if (selection === null) {
1652
- selection = createCursorSelection(cursor, anchorKey, anchorOffset, focusKey, focusOffset);
1653
- } else {
1654
- const anchor = selection.anchor;
1655
- const focus = selection.focus;
1656
- anchor.key = anchorKey;
1657
- anchor.offset = anchorOffset;
1658
- focus.key = focusKey;
1659
- focus.offset = focusOffset;
1660
- }
1661
- }
1531
+ return {
1532
+ equalityFactor: left + right,
1533
+ foundMappedChild
1534
+ };
1535
+ };
1536
+ const ytextTrans = ytext => {
1537
+ let str = '';
1538
+ let n = ytext._start;
1539
+ const nAttrs = {};
1540
+ while (n !== null) {
1541
+ if (!n.deleted) {
1542
+ if (n.countable && n.content instanceof ContentString) {
1543
+ str += n.content.str;
1544
+ } else if (n.content instanceof ContentFormat) {
1545
+ nAttrs[n.content.key] = null;
1662
1546
  }
1663
- updateCursor(binding, cursor, selection, nodeMap);
1664
1547
  }
1548
+ n = n.right;
1665
1549
  }
1666
- const allClientIDs = Array.from(cursors.keys());
1667
- for (let i = 0; i < allClientIDs.length; i++) {
1668
- const clientID = allClientIDs[i];
1669
- if (!visitedClientIDs.has(clientID)) {
1670
- const cursor = cursors.get(clientID);
1671
- if (cursor !== undefined) {
1672
- destroyCursor(binding, cursor);
1673
- cursors.delete(clientID);
1550
+ return {
1551
+ nAttrs,
1552
+ str
1553
+ };
1554
+ };
1555
+ const $updateYText = (ytext, ltexts, binding) => {
1556
+ binding.mapping.set(ytext, ltexts);
1557
+ const {
1558
+ nAttrs,
1559
+ str
1560
+ } = ytextTrans(ytext);
1561
+ const content = ltexts.map((node, i) => {
1562
+ const nodeType = node.getType();
1563
+ let p = propertiesToAttributes(node, binding);
1564
+ if (Object.keys(p).length === 0) {
1565
+ p = null;
1566
+ }
1567
+ return {
1568
+ attributes: Object.assign({}, nAttrs, {
1569
+ ...(nodeType !== TextNode.getType() && {
1570
+ t: nodeType
1571
+ }),
1572
+ p,
1573
+ ...stateToAttributes(node),
1574
+ ...(i > 0 && {
1575
+ i
1576
+ }) // Prevent Yjs from merging text nodes itself.
1577
+ }),
1578
+ insert: node.getTextContent(),
1579
+ nodeKey: node.getKey()
1580
+ };
1581
+ });
1582
+ const nextText = content.map(c => c.insert).join('');
1583
+ const selection = $getSelection();
1584
+ let cursorOffset;
1585
+ if ($isRangeSelection(selection) && selection.isCollapsed()) {
1586
+ cursorOffset = 0;
1587
+ for (const c of content) {
1588
+ if (c.nodeKey === selection.anchor.key) {
1589
+ cursorOffset += selection.anchor.offset;
1590
+ break;
1674
1591
  }
1592
+ cursorOffset += c.insert.length;
1675
1593
  }
1676
- }
1677
- }
1678
- function syncLexicalSelectionToYjs(binding, provider, prevSelection, nextSelection) {
1679
- const awareness = provider.awareness;
1680
- const localState = awareness.getLocalState();
1681
- if (localState === null) {
1682
- return;
1594
+ } else {
1595
+ cursorOffset = nextText.length;
1683
1596
  }
1684
1597
  const {
1685
- anchorPos: currentAnchorPos,
1686
- focusPos: currentFocusPos,
1687
- name,
1688
- color,
1689
- focusing,
1690
- awarenessData
1691
- } = localState;
1692
- let anchorPos = null;
1693
- let focusPos = null;
1694
- if (nextSelection === null || currentAnchorPos !== null && !nextSelection.is(prevSelection)) {
1695
- if (prevSelection === null) {
1696
- return;
1598
+ insert,
1599
+ remove,
1600
+ index
1601
+ } = simpleDiffWithCursor(str, nextText, cursorOffset);
1602
+ ytext.delete(index, remove);
1603
+ ytext.insert(index, insert);
1604
+ ytext.applyDelta(content.map(c => ({
1605
+ attributes: c.attributes,
1606
+ retain: c.insert.length
1607
+ })));
1608
+ };
1609
+ const toDelta = (ytext, snapshot, prevSnapshot, computeYChange) => {
1610
+ return ytext.toDelta(snapshot, prevSnapshot, computeYChange).map(delta => {
1611
+ const attributes = delta.attributes ?? {};
1612
+ if ('ychange' in attributes) {
1613
+ attributes[stateKeyToAttrKey('ychange')] = attributes.ychange;
1614
+ delete attributes.ychange;
1697
1615
  }
1698
- }
1699
- if ($isRangeSelection(nextSelection)) {
1700
- if (isBindingV1(binding)) {
1701
- anchorPos = createRelativePosition(nextSelection.anchor, binding);
1702
- focusPos = createRelativePosition(nextSelection.focus, binding);
1703
- } else {
1704
- anchorPos = createRelativePositionV2(nextSelection.anchor, binding);
1705
- focusPos = createRelativePositionV2(nextSelection.focus, binding);
1616
+ return {
1617
+ ...delta,
1618
+ attributes
1619
+ };
1620
+ });
1621
+ };
1622
+ const propertiesToAttributes = (node, meta) => {
1623
+ const defaultProperties = getDefaultNodeProperties(node, meta);
1624
+ const attrs = {};
1625
+ Object.entries(defaultProperties).forEach(([property, defaultValue]) => {
1626
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1627
+ const value = node[property];
1628
+ if (value !== defaultValue) {
1629
+ attrs[property] = value;
1706
1630
  }
1631
+ });
1632
+ return attrs;
1633
+ };
1634
+ const STATE_KEY_PREFIX = 's_';
1635
+ const stateKeyToAttrKey = key => `s_${key}`;
1636
+ const attrKeyToStateKey = key => {
1637
+ if (!key.startsWith(STATE_KEY_PREFIX)) {
1638
+ throw new Error(`Invalid state key: ${key}`);
1707
1639
  }
1708
- if (shouldUpdatePosition(currentAnchorPos, anchorPos) || shouldUpdatePosition(currentFocusPos, focusPos)) {
1709
- awareness.setLocalState({
1710
- ...localState,
1711
- anchorPos,
1712
- awarenessData,
1713
- color,
1714
- focusPos,
1715
- focusing,
1716
- name
1717
- });
1718
- }
1719
- }
1720
-
1721
- /*
1722
- const isVisible = (item: Item, snapshot?: Snapshot): boolean =>
1723
- snapshot === undefined
1724
- ? !item.deleted
1725
- : snapshot.sv.has(item.id.client) &&
1726
- snapshot.sv.get(item.id.client)! > item.id.clock &&
1727
- !isDeleted(snapshot.ds, item.id);
1728
- */
1729
-
1730
- // https://docs.yjs.dev/api/shared-types/y.xmlelement
1731
- // "Define a top-level type; Note that the nodeName is always "undefined""
1732
- const isRootElement = el => el.nodeName === 'UNDEFINED';
1733
- const $createOrUpdateNodeFromYElement = (el, binding, keysChanged, childListChanged, snapshot, prevSnapshot, computeYChange) => {
1734
- let node = binding.mapping.get(el);
1735
- if (node && keysChanged && keysChanged.size === 0 && !childListChanged) {
1736
- return node;
1640
+ return key.slice(STATE_KEY_PREFIX.length);
1641
+ };
1642
+ const stateToAttributes = node => {
1643
+ const state = node.__state;
1644
+ if (!state) {
1645
+ return {};
1737
1646
  }
1738
- const type = isRootElement(el) ? RootNode.getType() : el.nodeName;
1739
- const registeredNodes = binding.editor._nodes;
1740
- const nodeInfo = registeredNodes.get(type);
1741
- if (nodeInfo === undefined) {
1742
- throw new Error(`$createOrUpdateNodeFromYElement: Node ${type} is not registered`);
1647
+ const [unknown = {}, known] = state.getInternalState();
1648
+ const attrs = {};
1649
+ for (const [k, v] of Object.entries(unknown)) {
1650
+ attrs[stateKeyToAttrKey(k)] = v;
1743
1651
  }
1744
- if (!node) {
1745
- node = new nodeInfo.klass();
1746
- keysChanged = null;
1747
- childListChanged = true;
1652
+ for (const [stateConfig, v] of known) {
1653
+ attrs[stateKeyToAttrKey(stateConfig.key)] = stateConfig.unparse(v);
1748
1654
  }
1749
- if (childListChanged && node instanceof ElementNode) {
1750
- const children = [];
1751
- const $createChildren = childType => {
1752
- if (childType instanceof XmlElement) {
1753
- const n = $createOrUpdateNodeFromYElement(childType, binding, new Set(), false, snapshot, prevSnapshot, computeYChange);
1754
- if (n !== null) {
1755
- children.push(n);
1756
- }
1757
- } else if (childType instanceof XmlText) {
1758
- const ns = $createOrUpdateTextNodesFromYText(childType, binding, snapshot, prevSnapshot, computeYChange);
1759
- if (ns !== null) {
1760
- ns.forEach(textchild => {
1761
- if (textchild !== null) {
1762
- children.push(textchild);
1763
- }
1764
- });
1655
+ return attrs;
1656
+ };
1657
+ const $updateYFragment = (y, yDomFragment, node, binding, dirtyElements) => {
1658
+ if (yDomFragment instanceof XmlElement && yDomFragment.nodeName !== node.getType() && !(isRootElement(yDomFragment) && node.getType() === RootNode.getType())) {
1659
+ throw new Error('node name mismatch!');
1660
+ }
1661
+ binding.mapping.set(yDomFragment, node);
1662
+ // update attributes
1663
+ if (yDomFragment instanceof XmlElement) {
1664
+ const yDomAttrs = yDomFragment.getAttributes();
1665
+ const lexicalAttrs = {
1666
+ ...propertiesToAttributes(node, binding),
1667
+ ...stateToAttributes(node)
1668
+ };
1669
+ for (const key in lexicalAttrs) {
1670
+ if (lexicalAttrs[key] != null) {
1671
+ if (yDomAttrs[key] !== lexicalAttrs[key] && key !== 'ychange') {
1672
+ // TODO(collab-v2): typing for XmlElement generic
1673
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1674
+ yDomFragment.setAttribute(key, lexicalAttrs[key]);
1765
1675
  }
1766
1676
  } else {
1767
- {
1768
- formatDevErrorMessage(`XmlHook is not supported`);
1769
- }
1677
+ yDomFragment.removeAttribute(key);
1678
+ }
1679
+ }
1680
+ // remove all keys that are no longer in pAttrs
1681
+ for (const key in yDomAttrs) {
1682
+ if (lexicalAttrs[key] === undefined) {
1683
+ yDomFragment.removeAttribute(key);
1770
1684
  }
1771
- };
1772
- {
1773
- el.toArray().forEach($createChildren);
1774
1685
  }
1775
- $spliceChildren(node, children);
1776
1686
  }
1777
- const attrs = el.getAttributes(snapshot);
1778
- // TODO(collab-v2): support for ychange
1779
- /*
1780
- if (snapshot !== undefined) {
1781
- if (!isVisible(el._item!, snapshot)) {
1782
- attrs.ychange = computeYChange
1783
- ? computeYChange('removed', el._item!.id)
1784
- : {type: 'removed'};
1785
- } else if (!isVisible(el._item!, prevSnapshot)) {
1786
- attrs.ychange = computeYChange
1787
- ? computeYChange('added', el._item!.id)
1788
- : {type: 'added'};
1789
- }
1790
- }
1791
- */
1792
- const properties = {
1793
- ...getDefaultNodeProperties(node, binding)
1794
- };
1795
- const state = {};
1796
- for (const k in attrs) {
1797
- if (k.startsWith(STATE_KEY_PREFIX)) {
1798
- state[attrKeyToStateKey(k)] = attrs[k];
1687
+ // update children
1688
+ const lChildren = normalizeNodeContent(node);
1689
+ const lChildCnt = lChildren.length;
1690
+ const yChildren = yDomFragment.toArray();
1691
+ const yChildCnt = yChildren.length;
1692
+ const minCnt = Math.min(lChildCnt, yChildCnt);
1693
+ let left = 0;
1694
+ let right = 0;
1695
+ // find number of matching elements from left
1696
+ for (; left < minCnt; left++) {
1697
+ const leftY = yChildren[left];
1698
+ const leftL = lChildren[left];
1699
+ if (leftY instanceof XmlHook) {
1700
+ break;
1701
+ } else if (mappedIdentity(binding.mapping.get(leftY), leftL)) {
1702
+ if (leftL instanceof ElementNode && dirtyElements.has(leftL.getKey())) {
1703
+ $updateYFragment(y, leftY, leftL, binding, dirtyElements);
1704
+ }
1705
+ } else if (equalYTypePNode(leftY, leftL, binding)) {
1706
+ // update mapping
1707
+ binding.mapping.set(leftY, leftL);
1799
1708
  } else {
1800
- properties[k] = attrs[k];
1709
+ break;
1801
1710
  }
1802
1711
  }
1803
- $syncPropertiesFromYjs(binding, properties, node, keysChanged);
1804
- if (!keysChanged) {
1805
- $getWritableNodeState(node).updateFromJSON(state);
1806
- } else {
1807
- const stateKeysChanged = Object.keys(state).filter(k => keysChanged.has(stateKeyToAttrKey(k)));
1808
- if (stateKeysChanged.length > 0) {
1809
- const writableState = $getWritableNodeState(node);
1810
- for (const k of stateKeysChanged) {
1811
- writableState.updateFromUnknown(k, state[k]);
1712
+ // find number of matching elements from right
1713
+ for (; right + left < minCnt; right++) {
1714
+ const rightY = yChildren[yChildCnt - right - 1];
1715
+ const rightL = lChildren[lChildCnt - right - 1];
1716
+ if (rightY instanceof XmlHook) {
1717
+ break;
1718
+ } else if (mappedIdentity(binding.mapping.get(rightY), rightL)) {
1719
+ if (rightL instanceof ElementNode && dirtyElements.has(rightL.getKey())) {
1720
+ $updateYFragment(y, rightY, rightL, binding, dirtyElements);
1812
1721
  }
1722
+ } else if (equalYTypePNode(rightY, rightL, binding)) {
1723
+ // update mapping
1724
+ binding.mapping.set(rightY, rightL);
1725
+ } else {
1726
+ break;
1813
1727
  }
1814
1728
  }
1815
- const latestNode = node.getLatest();
1816
- binding.mapping.set(el, latestNode);
1817
- return latestNode;
1818
- };
1819
- const $spliceChildren = (node, nextChildren) => {
1820
- const prevChildren = node.getChildren();
1821
- const prevChildrenKeySet = new Set(prevChildren.map(child => child.getKey()));
1822
- const nextChildrenKeySet = new Set(nextChildren.map(child => child.getKey()));
1823
- const prevEndIndex = prevChildren.length - 1;
1824
- const nextEndIndex = nextChildren.length - 1;
1825
- let prevIndex = 0;
1826
- let nextIndex = 0;
1827
- while (prevIndex <= prevEndIndex && nextIndex <= nextEndIndex) {
1828
- const prevKey = prevChildren[prevIndex].getKey();
1829
- const nextKey = nextChildren[nextIndex].getKey();
1830
- if (prevKey === nextKey) {
1831
- prevIndex++;
1832
- nextIndex++;
1833
- continue;
1834
- }
1835
- const nextHasPrevKey = nextChildrenKeySet.has(prevKey);
1836
- const prevHasNextKey = prevChildrenKeySet.has(nextKey);
1837
- if (!nextHasPrevKey) {
1838
- // If removing the last node, insert remaining new nodes immediately, otherwise if the node
1839
- // cannot be empty, it will remove itself from its parent.
1840
- if (nextIndex === 0 && node.getChildrenSize() === 1) {
1841
- node.splice(nextIndex, 1, nextChildren.slice(nextIndex));
1842
- return;
1729
+ // try to compare and update
1730
+ while (yChildCnt - left - right > 0 && lChildCnt - left - right > 0) {
1731
+ const leftY = yChildren[left];
1732
+ const leftL = lChildren[left];
1733
+ const rightY = yChildren[yChildCnt - right - 1];
1734
+ const rightL = lChildren[lChildCnt - right - 1];
1735
+ if (leftY instanceof XmlText && leftL instanceof Array) {
1736
+ if (!equalYTextLText(leftY, leftL, binding)) {
1737
+ $updateYText(leftY, leftL, binding);
1843
1738
  }
1844
- // Remove
1845
- node.splice(nextIndex, 1, []);
1846
- prevIndex++;
1847
- continue;
1848
- }
1849
-
1850
- // Create or replace
1851
- const nextChildNode = nextChildren[nextIndex];
1852
- if (prevHasNextKey) {
1853
- node.splice(nextIndex, 1, [nextChildNode]);
1854
- prevIndex++;
1855
- nextIndex++;
1739
+ left += 1;
1856
1740
  } else {
1857
- node.splice(nextIndex, 0, [nextChildNode]);
1858
- nextIndex++;
1741
+ let updateLeft = leftY instanceof XmlElement && matchNodeName(leftY, leftL);
1742
+ let updateRight = rightY instanceof XmlElement && matchNodeName(rightY, rightL);
1743
+ if (updateLeft && updateRight) {
1744
+ // decide which which element to update
1745
+ const equalityLeft = computeChildEqualityFactor(leftY, leftL, binding);
1746
+ const equalityRight = computeChildEqualityFactor(rightY, rightL, binding);
1747
+ if (equalityLeft.foundMappedChild && !equalityRight.foundMappedChild) {
1748
+ updateRight = false;
1749
+ } else if (!equalityLeft.foundMappedChild && equalityRight.foundMappedChild) {
1750
+ updateLeft = false;
1751
+ } else if (equalityLeft.equalityFactor < equalityRight.equalityFactor) {
1752
+ updateLeft = false;
1753
+ } else {
1754
+ updateRight = false;
1755
+ }
1756
+ }
1757
+ if (updateLeft) {
1758
+ $updateYFragment(y, leftY, leftL, binding, dirtyElements);
1759
+ left += 1;
1760
+ } else if (updateRight) {
1761
+ $updateYFragment(y, rightY, rightL, binding, dirtyElements);
1762
+ right += 1;
1763
+ } else {
1764
+ binding.mapping.delete(yDomFragment.get(left));
1765
+ yDomFragment.delete(left, 1);
1766
+ yDomFragment.insert(left, [$createTypeFromTextOrElementNode(leftL, binding)]);
1767
+ left += 1;
1768
+ }
1859
1769
  }
1860
1770
  }
1861
- const appendNewChildren = prevIndex > prevEndIndex;
1862
- const removeOldChildren = nextIndex > nextEndIndex;
1863
- if (appendNewChildren && !removeOldChildren) {
1864
- node.append(...nextChildren.slice(nextIndex));
1865
- } else if (removeOldChildren && !appendNewChildren) {
1866
- node.splice(nextChildren.length, node.getChildrenSize() - nextChildren.length, []);
1771
+ const yDelLen = yChildCnt - left - right;
1772
+ if (yChildCnt === 1 && lChildCnt === 0 && yChildren[0] instanceof XmlText) {
1773
+ binding.mapping.delete(yChildren[0]);
1774
+ // Edge case handling https://github.com/yjs/y-prosemirror/issues/108
1775
+ // Only delete the content of the Y.Text to retain remote changes on the same Y.Text object
1776
+ yChildren[0].delete(0, yChildren[0].length);
1777
+ } else if (yDelLen > 0) {
1778
+ yDomFragment.slice(left, left + yDelLen).forEach(type => binding.mapping.delete(type));
1779
+ yDomFragment.delete(left, yDelLen);
1780
+ }
1781
+ if (left + right < lChildCnt) {
1782
+ const ins = [];
1783
+ for (let i = left; i < lChildCnt - right; i++) {
1784
+ ins.push($createTypeFromTextOrElementNode(lChildren[i], binding));
1785
+ }
1786
+ yDomFragment.insert(left, ins);
1867
1787
  }
1868
1788
  };
1869
- const $createOrUpdateTextNodesFromYText = (text, binding, snapshot, prevSnapshot, computeYChange) => {
1870
- const deltas = toDelta(text, snapshot, prevSnapshot, computeYChange);
1789
+ const matchNodeName = (yElement, lnode) => !(lnode instanceof Array) && yElement.nodeName === lnode.getType();
1871
1790
 
1872
- // Use existing text nodes if the count and types all align, otherwise throw out the existing
1873
- // nodes and create new ones.
1874
- let nodes = binding.mapping.get(text) ?? [];
1875
- const nodeTypes = deltas.map(delta => delta.attributes.t ?? TextNode.getType());
1876
- const canReuseNodes = nodes.length === nodeTypes.length && nodes.every((node, i) => node.getType() === nodeTypes[i]);
1877
- if (!canReuseNodes) {
1878
- const registeredNodes = binding.editor._nodes;
1879
- nodes = nodeTypes.map(type => {
1880
- const nodeInfo = registeredNodes.get(type);
1881
- if (nodeInfo === undefined) {
1882
- throw new Error(`$createTextNodesFromYText: Node ${type} is not registered`);
1791
+ const STATE_KEY = 'ychange';
1792
+
1793
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1794
+
1795
+ const ychangeState = createState(STATE_KEY, {
1796
+ isEqual: (a, b) => a === b,
1797
+ parse: value => value ?? null
1798
+ });
1799
+ function $getYChangeState(node) {
1800
+ return $getState(node, ychangeState);
1801
+ }
1802
+
1803
+ // Not exposing $setState because it should only be created by SyncV2.ts.
1804
+
1805
+ /**
1806
+ * Replaces the editor content with a view that compares the state between two given snapshots.
1807
+ * Any added or removed nodes between the two snapshots will have {@link YChange} attached to them.
1808
+ *
1809
+ * @param binding Yjs binding
1810
+ * @param snapshot Ending snapshot state (default: current state of the Yjs document)
1811
+ * @param prevSnapshot Starting snapshot state (default: empty snapshot)
1812
+ */
1813
+ const renderSnapshot__EXPERIMENTAL = (binding, snapshot$1 = snapshot(binding.doc), prevSnapshot = emptySnapshot) => {
1814
+ // The document that contains the full history of this document.
1815
+ const {
1816
+ doc
1817
+ } = binding;
1818
+ if (!!doc.gc) {
1819
+ formatDevErrorMessage(`GC must be disabled to render snapshot`);
1820
+ }
1821
+ doc.transact(transaction => {
1822
+ // Before rendering, we are going to sanitize ops and split deleted ops
1823
+ // if they were deleted by seperate users.
1824
+ const pud = new PermanentUserData(doc);
1825
+ if (pud) {
1826
+ pud.dss.forEach(ds => {
1827
+ iterateDeletedStructs(transaction, ds, _item => {});
1828
+ });
1829
+ }
1830
+ const computeYChange = (type, id) => {
1831
+ const user = type === 'added' ? pud.getUserByClientId(id.client) : pud.getUserByDeletedId(id);
1832
+ return {
1833
+ id,
1834
+ type,
1835
+ user: user ?? null
1836
+ };
1837
+ };
1838
+ binding.mapping.clear();
1839
+ binding.editor.update(() => {
1840
+ $getRoot().clear();
1841
+ $createOrUpdateNodeFromYElement(binding.root, binding, null, true, snapshot$1, prevSnapshot, computeYChange);
1842
+ });
1843
+ }, binding);
1844
+ };
1845
+
1846
+ function createRelativePosition(point, binding) {
1847
+ const collabNodeMap = binding.collabNodeMap;
1848
+ const collabNode = collabNodeMap.get(point.key);
1849
+ if (collabNode === undefined) {
1850
+ return null;
1851
+ }
1852
+ let offset = point.offset;
1853
+ let sharedType = collabNode.getSharedType();
1854
+ if (collabNode instanceof CollabTextNode) {
1855
+ sharedType = collabNode._parent._xmlText;
1856
+ const currentOffset = collabNode.getOffset();
1857
+ if (currentOffset === -1) {
1858
+ return null;
1859
+ }
1860
+ offset = currentOffset + 1 + offset;
1861
+ } else if (collabNode instanceof CollabElementNode && point.type === 'element') {
1862
+ const parent = point.getNode();
1863
+ if (!$isElementNode(parent)) {
1864
+ formatDevErrorMessage(`Element point must be an element node`);
1865
+ }
1866
+ let accumulatedOffset = 0;
1867
+ let i = 0;
1868
+ let node = parent.getFirstChild();
1869
+ while (node !== null && i++ < offset) {
1870
+ if ($isTextNode(node)) {
1871
+ accumulatedOffset += node.getTextContentSize() + 1;
1872
+ } else {
1873
+ accumulatedOffset++;
1883
1874
  }
1884
- const node = new nodeInfo.klass();
1885
- if (!$isTextNode(node)) {
1886
- throw new Error(`$createTextNodesFromYText: Node ${type} is not a TextNode`);
1875
+ node = node.getNextSibling();
1876
+ }
1877
+ offset = accumulatedOffset;
1878
+ }
1879
+ return createRelativePositionFromTypeIndex(sharedType, offset);
1880
+ }
1881
+ function createRelativePositionV2(point, binding) {
1882
+ const {
1883
+ mapping
1884
+ } = binding;
1885
+ const {
1886
+ offset
1887
+ } = point;
1888
+ const node = point.getNode();
1889
+ const yType = mapping.getSharedType(node);
1890
+ if (yType === undefined) {
1891
+ return null;
1892
+ }
1893
+ if (point.type === 'text') {
1894
+ if (!$isTextNode(node)) {
1895
+ formatDevErrorMessage(`Text point must be a text node`);
1896
+ }
1897
+ let prevSibling = node.getPreviousSibling();
1898
+ let adjustedOffset = offset;
1899
+ while ($isTextNode(prevSibling)) {
1900
+ adjustedOffset += prevSibling.getTextContentSize();
1901
+ prevSibling = prevSibling.getPreviousSibling();
1902
+ }
1903
+ return createRelativePositionFromTypeIndex(yType, adjustedOffset);
1904
+ } else if (point.type === 'element') {
1905
+ if (!$isElementNode(node)) {
1906
+ formatDevErrorMessage(`Element point must be an element node`);
1907
+ }
1908
+ let i = 0;
1909
+ let child = node.getFirstChild();
1910
+ while (child !== null && i < offset) {
1911
+ if ($isTextNode(child)) {
1912
+ let nextSibling = child.getNextSibling();
1913
+ while ($isTextNode(nextSibling)) {
1914
+ nextSibling = nextSibling.getNextSibling();
1915
+ }
1887
1916
  }
1888
- return node;
1889
- });
1917
+ i++;
1918
+ child = child.getNextSibling();
1919
+ }
1920
+ return createRelativePositionFromTypeIndex(yType, i);
1890
1921
  }
1891
-
1892
- // Sync text, properties and state to the text nodes.
1893
- for (let i = 0; i < deltas.length; i++) {
1894
- const node = nodes[i];
1895
- const delta = deltas[i];
1896
- const {
1897
- attributes,
1898
- insert
1899
- } = delta;
1900
- if (node.__text !== insert) {
1901
- node.setTextContent(insert);
1922
+ return null;
1923
+ }
1924
+ function createAbsolutePosition(relativePosition, binding) {
1925
+ return createAbsolutePositionFromRelativePosition(relativePosition, binding.doc);
1926
+ }
1927
+ function shouldUpdatePosition(currentPos, pos) {
1928
+ if (currentPos == null) {
1929
+ if (pos != null) {
1930
+ return true;
1902
1931
  }
1903
- const properties = {
1904
- ...getDefaultNodeProperties(node, binding),
1905
- ...attributes.p
1906
- };
1907
- const state = Object.fromEntries(Object.entries(attributes).filter(([k]) => k.startsWith(STATE_KEY_PREFIX)).map(([k, v]) => [attrKeyToStateKey(k), v]));
1908
- $syncPropertiesFromYjs(binding, properties, node, null);
1909
- $getWritableNodeState(node).updateFromJSON(state);
1932
+ } else if (pos == null || !compareRelativePositions(currentPos, pos)) {
1933
+ return true;
1910
1934
  }
1911
- const latestNodes = nodes.map(node => node.getLatest());
1912
- binding.mapping.set(text, latestNodes);
1913
- return latestNodes;
1914
- };
1915
- const $createTypeFromTextNodes = (nodes, binding) => {
1916
- const type = new XmlText();
1917
- $updateYText(type, nodes, binding);
1918
- return type;
1919
- };
1920
- const createTypeFromElementNode = (node, binding) => {
1921
- const type = new XmlElement(node.getType());
1922
- const attrs = {
1923
- ...propertiesToAttributes(node, binding),
1924
- ...stateToAttributes(node)
1935
+ return false;
1936
+ }
1937
+ function createCursor(name, color) {
1938
+ return {
1939
+ color: color,
1940
+ name: name,
1941
+ selection: null
1925
1942
  };
1926
- for (const key in attrs) {
1927
- const val = attrs[key];
1928
- if (val !== null) {
1929
- // TODO(collab-v2): typing for XmlElement generic
1930
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1931
- type.setAttribute(key, val);
1943
+ }
1944
+ function destroySelection(binding, selection) {
1945
+ const cursorsContainer = binding.cursorsContainer;
1946
+ if (cursorsContainer !== null) {
1947
+ const selections = selection.selections;
1948
+ const selectionsLength = selections.length;
1949
+ for (let i = 0; i < selectionsLength; i++) {
1950
+ cursorsContainer.removeChild(selections[i]);
1932
1951
  }
1933
1952
  }
1934
- if (!(node instanceof ElementNode)) {
1935
- return type;
1936
- }
1937
- type.insert(0, normalizeNodeContent(node).map(n => $createTypeFromTextOrElementNode(n, binding)));
1938
- binding.mapping.set(type, node);
1939
- return type;
1940
- };
1941
- const $createTypeFromTextOrElementNode = (node, meta) => node instanceof Array ? $createTypeFromTextNodes(node, meta) : createTypeFromElementNode(node, meta);
1942
- const isObject = val => typeof val === 'object' && val != null;
1943
- const equalAttrs = (pattrs, yattrs) => {
1944
- const keys = Object.keys(pattrs).filter(key => pattrs[key] !== null);
1945
- if (yattrs == null) {
1946
- return keys.length === 0;
1953
+ }
1954
+ function destroyCursor(binding, cursor) {
1955
+ const selection = cursor.selection;
1956
+ if (selection !== null) {
1957
+ destroySelection(binding, selection);
1947
1958
  }
1948
- let eq = keys.length === Object.keys(yattrs).filter(key => yattrs[key] !== null).length;
1949
- for (let i = 0; i < keys.length && eq; i++) {
1950
- const key = keys[i];
1951
- const l = pattrs[key];
1952
- const r = yattrs[key];
1953
- eq = key === 'ychange' || l === r || isObject(l) && isObject(r) && equalAttrs(l, r);
1959
+ }
1960
+ function createCursorSelection(cursor, anchorKey, anchorOffset, focusKey, focusOffset) {
1961
+ const color = cursor.color;
1962
+ const caret = document.createElement('span');
1963
+ caret.style.cssText = `position:absolute;top:0;bottom:0;right:-1px;width:1px;background-color:${color};z-index:10;`;
1964
+ const name = document.createElement('span');
1965
+ name.textContent = cursor.name;
1966
+ 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;`;
1967
+ caret.appendChild(name);
1968
+ return {
1969
+ anchor: {
1970
+ key: anchorKey,
1971
+ offset: anchorOffset
1972
+ },
1973
+ caret,
1974
+ color,
1975
+ focus: {
1976
+ key: focusKey,
1977
+ offset: focusOffset
1978
+ },
1979
+ name,
1980
+ selections: []
1981
+ };
1982
+ }
1983
+ function updateCursor(binding, cursor, nextSelection, nodeMap) {
1984
+ const editor = binding.editor;
1985
+ const rootElement = editor.getRootElement();
1986
+ const cursorsContainer = binding.cursorsContainer;
1987
+ if (cursorsContainer === null || rootElement === null) {
1988
+ return;
1954
1989
  }
1955
- return eq;
1956
- };
1957
- const normalizeNodeContent = node => {
1958
- if (!(node instanceof ElementNode)) {
1959
- return [];
1990
+ const cursorsContainerOffsetParent = cursorsContainer.offsetParent;
1991
+ if (cursorsContainerOffsetParent === null) {
1992
+ return;
1960
1993
  }
1961
- const c = node.getChildren();
1962
- const res = [];
1963
- for (let i = 0; i < c.length; i++) {
1964
- const n = c[i];
1965
- if ($isTextNode(n)) {
1966
- const textNodes = [];
1967
- for (let maybeTextNode = c[i]; i < c.length && $isTextNode(maybeTextNode); maybeTextNode = c[++i]) {
1968
- textNodes.push(maybeTextNode);
1969
- }
1970
- i--;
1971
- res.push(textNodes);
1994
+ const containerRect = cursorsContainerOffsetParent.getBoundingClientRect();
1995
+ const prevSelection = cursor.selection;
1996
+ if (nextSelection === null) {
1997
+ if (prevSelection === null) {
1998
+ return;
1972
1999
  } else {
1973
- res.push(n);
2000
+ cursor.selection = null;
2001
+ destroySelection(binding, prevSelection);
2002
+ return;
1974
2003
  }
2004
+ } else {
2005
+ cursor.selection = nextSelection;
1975
2006
  }
1976
- return res;
1977
- };
1978
- const equalYTextLText = (ytext, ltexts, binding) => {
1979
- const deltas = toDelta(ytext);
1980
- return deltas.length === ltexts.length && deltas.every((d, i) => {
1981
- const ltext = ltexts[i];
1982
- const type = d.attributes.t ?? TextNode.getType();
1983
- const propertyAttrs = d.attributes.p ?? {};
1984
- const stateAttrs = Object.fromEntries(Object.entries(d.attributes).filter(([k]) => k.startsWith(STATE_KEY_PREFIX)));
1985
- return d.insert === ltext.getTextContent() && type === ltext.getType() && equalAttrs(propertyAttrs, propertiesToAttributes(ltext, binding)) && equalAttrs(stateAttrs, stateToAttributes(ltext));
1986
- });
1987
- };
1988
- const equalYTypePNode = (ytype, lnode, binding) => {
1989
- if (ytype instanceof XmlElement && !(lnode instanceof Array) && matchNodeName(ytype, lnode)) {
1990
- const normalizedContent = normalizeNodeContent(lnode);
1991
- return ytype._length === normalizedContent.length && equalAttrs(ytype.getAttributes(), {
1992
- ...propertiesToAttributes(lnode, binding),
1993
- ...stateToAttributes(lnode)
1994
- }) && ytype.toArray().every((ychild, i) => equalYTypePNode(ychild, normalizedContent[i], binding));
1995
- }
1996
- return ytype instanceof XmlText && lnode instanceof Array && equalYTextLText(ytype, lnode, binding);
1997
- };
1998
- const mappedIdentity = (mapped, lcontent) => mapped === lcontent || mapped instanceof Array && lcontent instanceof Array && mapped.length === lcontent.length && mapped.every((a, i) => lcontent[i] === a);
1999
- const computeChildEqualityFactor = (ytype, lnode, binding) => {
2000
- const yChildren = ytype.toArray();
2001
- const pChildren = normalizeNodeContent(lnode);
2002
- const pChildCnt = pChildren.length;
2003
- const yChildCnt = yChildren.length;
2004
- const minCnt = Math.min(yChildCnt, pChildCnt);
2005
- let left = 0;
2006
- let right = 0;
2007
- let foundMappedChild = false;
2008
- for (; left < minCnt; left++) {
2009
- const leftY = yChildren[left];
2010
- const leftP = pChildren[left];
2011
- if (leftY instanceof XmlHook) {
2012
- break;
2013
- } else if (mappedIdentity(binding.mapping.get(leftY), leftP)) {
2014
- foundMappedChild = true; // definite (good) match!
2015
- } else if (!equalYTypePNode(leftY, leftP, binding)) {
2016
- break;
2017
- }
2007
+ const caret = nextSelection.caret;
2008
+ const color = nextSelection.color;
2009
+ const selections = nextSelection.selections;
2010
+ const anchor = nextSelection.anchor;
2011
+ const focus = nextSelection.focus;
2012
+ const anchorKey = anchor.key;
2013
+ const focusKey = focus.key;
2014
+ const anchorNode = nodeMap.get(anchorKey);
2015
+ const focusNode = nodeMap.get(focusKey);
2016
+ if (anchorNode == null || focusNode == null) {
2017
+ return;
2018
2018
  }
2019
- for (; left + right < minCnt; right++) {
2020
- const rightY = yChildren[yChildCnt - right - 1];
2021
- const rightP = pChildren[pChildCnt - right - 1];
2022
- if (rightY instanceof XmlHook) {
2023
- break;
2024
- } else if (mappedIdentity(binding.mapping.get(rightY), rightP)) {
2025
- foundMappedChild = true;
2026
- } else if (!equalYTypePNode(rightY, rightP, binding)) {
2027
- break;
2019
+ let selectionRects;
2020
+
2021
+ // In the case of a collapsed selection on a linebreak, we need
2022
+ // to improvise as the browser will return nothing here as <br>
2023
+ // apparently take up no visual space :/
2024
+ // This won't work in all cases, but it's better than just showing
2025
+ // nothing all the time.
2026
+ if (anchorNode === focusNode && $isLineBreakNode(anchorNode)) {
2027
+ const brRect = editor.getElementByKey(anchorKey).getBoundingClientRect();
2028
+ selectionRects = [brRect];
2029
+ } else {
2030
+ const range = createDOMRange(editor, anchorNode, anchor.offset, focusNode, focus.offset);
2031
+ if (range === null) {
2032
+ return;
2028
2033
  }
2034
+ selectionRects = createRectsFromDOMRange(editor, range);
2029
2035
  }
2030
- return {
2031
- equalityFactor: left + right,
2032
- foundMappedChild
2033
- };
2034
- };
2035
- const ytextTrans = ytext => {
2036
- let str = '';
2037
- let n = ytext._start;
2038
- const nAttrs = {};
2039
- while (n !== null) {
2040
- if (!n.deleted) {
2041
- if (n.countable && n.content instanceof ContentString) {
2042
- str += n.content.str;
2043
- } else if (n.content instanceof ContentFormat) {
2044
- nAttrs[n.content.key] = null;
2036
+ const selectionsLength = selections.length;
2037
+ const selectionRectsLength = selectionRects.length;
2038
+ for (let i = 0; i < selectionRectsLength; i++) {
2039
+ const selectionRect = selectionRects[i];
2040
+ let selection = selections[i];
2041
+ if (selection === undefined) {
2042
+ selection = document.createElement('span');
2043
+ selections[i] = selection;
2044
+ const selectionBg = document.createElement('span');
2045
+ selection.appendChild(selectionBg);
2046
+ cursorsContainer.appendChild(selection);
2047
+ }
2048
+ const top = selectionRect.top - containerRect.top;
2049
+ const left = selectionRect.left - containerRect.left;
2050
+ const style = `position:absolute;top:${top}px;left:${left}px;height:${selectionRect.height}px;width:${selectionRect.width}px;pointer-events:none;z-index:5;`;
2051
+ selection.style.cssText = style;
2052
+ selection.firstChild.style.cssText = `${style}left:0;top:0;background-color:${color};opacity:0.3;`;
2053
+ if (i === selectionRectsLength - 1) {
2054
+ if (caret.parentNode !== selection) {
2055
+ selection.appendChild(caret);
2045
2056
  }
2046
2057
  }
2047
- n = n.right;
2058
+ }
2059
+ for (let i = selectionsLength - 1; i >= selectionRectsLength; i--) {
2060
+ const selection = selections[i];
2061
+ cursorsContainer.removeChild(selection);
2062
+ selections.pop();
2063
+ }
2064
+ }
2065
+ /**
2066
+ * @deprecated Use `$getAnchorAndFocusForUserState` instead.
2067
+ */
2068
+ function getAnchorAndFocusCollabNodesForUserState(binding, userState) {
2069
+ const {
2070
+ anchorPos,
2071
+ focusPos
2072
+ } = userState;
2073
+ let anchorCollabNode = null;
2074
+ let anchorOffset = 0;
2075
+ let focusCollabNode = null;
2076
+ let focusOffset = 0;
2077
+ if (anchorPos !== null && focusPos !== null) {
2078
+ const anchorAbsPos = createAbsolutePosition(anchorPos, binding);
2079
+ const focusAbsPos = createAbsolutePosition(focusPos, binding);
2080
+ if (anchorAbsPos !== null && focusAbsPos !== null) {
2081
+ [anchorCollabNode, anchorOffset] = getCollabNodeAndOffset(anchorAbsPos.type, anchorAbsPos.index);
2082
+ [focusCollabNode, focusOffset] = getCollabNodeAndOffset(focusAbsPos.type, focusAbsPos.index);
2083
+ }
2048
2084
  }
2049
2085
  return {
2050
- nAttrs,
2051
- str
2086
+ anchorCollabNode,
2087
+ anchorOffset,
2088
+ focusCollabNode,
2089
+ focusOffset
2052
2090
  };
2053
- };
2054
- const $updateYText = (ytext, ltexts, binding) => {
2055
- binding.mapping.set(ytext, ltexts);
2091
+ }
2092
+ function $getAnchorAndFocusForUserState(binding, userState) {
2056
2093
  const {
2057
- nAttrs,
2058
- str
2059
- } = ytextTrans(ytext);
2060
- const content = ltexts.map((node, i) => {
2061
- const nodeType = node.getType();
2062
- let p = propertiesToAttributes(node, binding);
2063
- if (Object.keys(p).length === 0) {
2064
- p = null;
2065
- }
2094
+ anchorPos,
2095
+ focusPos
2096
+ } = userState;
2097
+ const anchorAbsPos = anchorPos ? createAbsolutePosition(anchorPos, binding) : null;
2098
+ const focusAbsPos = focusPos ? createAbsolutePosition(focusPos, binding) : null;
2099
+ if (anchorAbsPos === null || focusAbsPos === null) {
2066
2100
  return {
2067
- attributes: Object.assign({}, nAttrs, {
2068
- ...(nodeType !== TextNode.getType() && {
2069
- t: nodeType
2070
- }),
2071
- p,
2072
- ...stateToAttributes(node),
2073
- ...(i > 0 && {
2074
- i
2075
- }) // Prevent Yjs from merging text nodes itself.
2076
- }),
2077
- insert: node.getTextContent(),
2078
- nodeKey: node.getKey()
2101
+ anchorKey: null,
2102
+ anchorOffset: 0,
2103
+ focusKey: null,
2104
+ focusOffset: 0
2079
2105
  };
2080
- });
2081
- const nextText = content.map(c => c.insert).join('');
2082
- const selection = $getSelection();
2083
- let cursorOffset;
2084
- if ($isRangeSelection(selection) && selection.isCollapsed()) {
2085
- cursorOffset = 0;
2086
- for (const c of content) {
2087
- if (c.nodeKey === selection.anchor.key) {
2088
- cursorOffset += selection.anchor.offset;
2089
- break;
2106
+ }
2107
+ if (isBindingV1(binding)) {
2108
+ const [anchorCollabNode, anchorOffset] = getCollabNodeAndOffset(anchorAbsPos.type, anchorAbsPos.index);
2109
+ const [focusCollabNode, focusOffset] = getCollabNodeAndOffset(focusAbsPos.type, focusAbsPos.index);
2110
+ return {
2111
+ anchorKey: anchorCollabNode !== null ? anchorCollabNode.getKey() : null,
2112
+ anchorOffset,
2113
+ focusKey: focusCollabNode !== null ? focusCollabNode.getKey() : null,
2114
+ focusOffset
2115
+ };
2116
+ }
2117
+ let [anchorNode, anchorOffset] = $getNodeAndOffsetV2(binding.mapping, anchorAbsPos);
2118
+ let [focusNode, focusOffset] = $getNodeAndOffsetV2(binding.mapping, focusAbsPos);
2119
+ // For a non-collapsed selection, if the start of the selection is as the end of a text node,
2120
+ // move it to the beginning of the next text node (if one exists).
2121
+ if (focusNode && anchorNode && (focusNode !== anchorNode || focusOffset !== anchorOffset)) {
2122
+ const isBackwards = focusNode.isBefore(anchorNode);
2123
+ const startNode = isBackwards ? focusNode : anchorNode;
2124
+ const startOffset = isBackwards ? focusOffset : anchorOffset;
2125
+ if ($isTextNode(startNode) && $isTextNode(startNode.getNextSibling()) && startOffset === startNode.getTextContentSize()) {
2126
+ if (isBackwards) {
2127
+ focusNode = startNode.getNextSibling();
2128
+ focusOffset = 0;
2129
+ } else {
2130
+ anchorNode = startNode.getNextSibling();
2131
+ anchorOffset = 0;
2090
2132
  }
2091
- cursorOffset += c.insert.length;
2092
2133
  }
2093
- } else {
2094
- cursorOffset = nextText.length;
2134
+ }
2135
+ return {
2136
+ anchorKey: anchorNode !== null ? anchorNode.getKey() : null,
2137
+ anchorOffset,
2138
+ focusKey: focusNode !== null ? focusNode.getKey() : null,
2139
+ focusOffset
2140
+ };
2141
+ }
2142
+ function $syncLocalCursorPosition(binding, provider) {
2143
+ const awareness = provider.awareness;
2144
+ const localState = awareness.getLocalState();
2145
+ if (localState === null) {
2146
+ return;
2095
2147
  }
2096
2148
  const {
2097
- insert,
2098
- remove,
2099
- index
2100
- } = simpleDiffWithCursor(str, nextText, cursorOffset);
2101
- ytext.delete(index, remove);
2102
- ytext.insert(index, insert);
2103
- ytext.applyDelta(content.map(c => ({
2104
- attributes: c.attributes,
2105
- retain: c.insert.length
2106
- })));
2107
- };
2108
- const toDelta = (ytext, snapshot, prevSnapshot, computeYChange) => {
2109
- return ytext.toDelta(snapshot, prevSnapshot, computeYChange).map(delta => ({
2110
- ...delta,
2111
- attributes: delta.attributes ?? {}
2112
- }));
2113
- };
2114
- const propertiesToAttributes = (node, meta) => {
2115
- const defaultProperties = getDefaultNodeProperties(node, meta);
2116
- const attrs = {};
2117
- Object.entries(defaultProperties).forEach(([property, defaultValue]) => {
2118
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
2119
- const value = node[property];
2120
- if (value !== defaultValue) {
2121
- attrs[property] = value;
2149
+ anchorKey,
2150
+ anchorOffset,
2151
+ focusKey,
2152
+ focusOffset
2153
+ } = $getAnchorAndFocusForUserState(binding, localState);
2154
+ if (anchorKey !== null && focusKey !== null) {
2155
+ const selection = $getSelection();
2156
+ if (!$isRangeSelection(selection)) {
2157
+ return;
2122
2158
  }
2123
- });
2124
- return attrs;
2125
- };
2126
- const STATE_KEY_PREFIX = 's_';
2127
- const stateKeyToAttrKey = key => STATE_KEY_PREFIX + key;
2128
- const attrKeyToStateKey = key => {
2129
- if (!key.startsWith(STATE_KEY_PREFIX)) {
2130
- throw new Error(`Invalid state key: ${key}`);
2131
- }
2132
- return key.slice(STATE_KEY_PREFIX.length);
2133
- };
2134
- const stateToAttributes = node => {
2135
- const state = node.__state;
2136
- if (!state) {
2137
- return {};
2138
- }
2139
- const [unknown = {}, known] = state.getInternalState();
2140
- const attrs = {};
2141
- for (const [k, v] of Object.entries(unknown)) {
2142
- attrs[stateKeyToAttrKey(k)] = v;
2143
- }
2144
- for (const [stateConfig, v] of known) {
2145
- attrs[stateKeyToAttrKey(stateConfig.key)] = stateConfig.unparse(v);
2146
- }
2147
- return attrs;
2148
- };
2149
- const $updateYFragment = (y, yDomFragment, node, binding, dirtyElements) => {
2150
- if (yDomFragment instanceof XmlElement && yDomFragment.nodeName !== node.getType() && !(isRootElement(yDomFragment) && node.getType() === RootNode.getType())) {
2151
- throw new Error('node name mismatch!');
2159
+ $setPoint(selection.anchor, anchorKey, anchorOffset);
2160
+ $setPoint(selection.focus, focusKey, focusOffset);
2152
2161
  }
2153
- binding.mapping.set(yDomFragment, node);
2154
- // update attributes
2155
- if (yDomFragment instanceof XmlElement) {
2156
- const yDomAttrs = yDomFragment.getAttributes();
2157
- const lexicalAttrs = {
2158
- ...propertiesToAttributes(node, binding),
2159
- ...stateToAttributes(node)
2160
- };
2161
- for (const key in lexicalAttrs) {
2162
- if (lexicalAttrs[key] != null) {
2163
- if (yDomAttrs[key] !== lexicalAttrs[key] && key !== 'ychange') {
2164
- // TODO(collab-v2): typing for XmlElement generic
2165
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
2166
- yDomFragment.setAttribute(key, lexicalAttrs[key]);
2167
- }
2168
- } else {
2169
- yDomFragment.removeAttribute(key);
2170
- }
2171
- }
2172
- // remove all keys that are no longer in pAttrs
2173
- for (const key in yDomAttrs) {
2174
- if (lexicalAttrs[key] === undefined) {
2175
- yDomFragment.removeAttribute(key);
2176
- }
2162
+ }
2163
+ function $setPoint(point, key, offset) {
2164
+ if (point.key !== key || point.offset !== offset) {
2165
+ let anchorNode = $getNodeByKey(key);
2166
+ if (anchorNode !== null && !$isElementNode(anchorNode) && !$isTextNode(anchorNode)) {
2167
+ const parent = anchorNode.getParentOrThrow();
2168
+ key = parent.getKey();
2169
+ offset = anchorNode.getIndexWithinParent();
2170
+ anchorNode = parent;
2177
2171
  }
2172
+ point.set(key, offset, $isElementNode(anchorNode) ? 'element' : 'text');
2178
2173
  }
2179
- // update children
2180
- const lChildren = normalizeNodeContent(node);
2181
- const lChildCnt = lChildren.length;
2182
- const yChildren = yDomFragment.toArray();
2183
- const yChildCnt = yChildren.length;
2184
- const minCnt = Math.min(lChildCnt, yChildCnt);
2185
- let left = 0;
2186
- let right = 0;
2187
- // find number of matching elements from left
2188
- for (; left < minCnt; left++) {
2189
- const leftY = yChildren[left];
2190
- const leftL = lChildren[left];
2191
- if (leftY instanceof XmlHook) {
2192
- break;
2193
- } else if (mappedIdentity(binding.mapping.get(leftY), leftL)) {
2194
- if (leftL instanceof ElementNode && dirtyElements.has(leftL.getKey())) {
2195
- $updateYFragment(y, leftY, leftL, binding, dirtyElements);
2196
- }
2197
- } else if (equalYTypePNode(leftY, leftL, binding)) {
2198
- // update mapping
2199
- binding.mapping.set(leftY, leftL);
2174
+ }
2175
+ function getCollabNodeAndOffset(
2176
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2177
+ sharedType, offset) {
2178
+ const collabNode = sharedType._collabNode;
2179
+ if (collabNode === undefined) {
2180
+ return [null, 0];
2181
+ }
2182
+ if (collabNode instanceof CollabElementNode) {
2183
+ const {
2184
+ node,
2185
+ offset: collabNodeOffset
2186
+ } = getPositionFromElementAndOffset(collabNode, offset, true);
2187
+ if (node === null) {
2188
+ return [collabNode, 0];
2200
2189
  } else {
2201
- break;
2190
+ return [node, collabNodeOffset];
2202
2191
  }
2203
2192
  }
2204
- // find number of matching elements from right
2205
- for (; right + left < minCnt; right++) {
2206
- const rightY = yChildren[yChildCnt - right - 1];
2207
- const rightL = lChildren[lChildCnt - right - 1];
2208
- if (rightY instanceof XmlHook) {
2209
- break;
2210
- } else if (mappedIdentity(binding.mapping.get(rightY), rightL)) {
2211
- if (rightL instanceof ElementNode && dirtyElements.has(rightL.getKey())) {
2212
- $updateYFragment(y, rightY, rightL, binding, dirtyElements);
2193
+ return [null, 0];
2194
+ }
2195
+ function $getNodeAndOffsetV2(mapping, absolutePosition) {
2196
+ const yType = absolutePosition.type;
2197
+ const yOffset = absolutePosition.index;
2198
+ if (yType instanceof XmlElement) {
2199
+ const node = mapping.get(yType);
2200
+ if (node === undefined) {
2201
+ return [null, 0];
2202
+ }
2203
+ if (!$isElementNode(node)) {
2204
+ return [node, yOffset];
2205
+ }
2206
+ let remainingYOffset = yOffset;
2207
+ let lexicalOffset = 0;
2208
+ const children = node.getChildren();
2209
+ while (remainingYOffset > 0 && lexicalOffset < children.length) {
2210
+ const child = children[lexicalOffset];
2211
+ remainingYOffset -= 1;
2212
+ lexicalOffset += 1;
2213
+ if ($isTextNode(child)) {
2214
+ while (lexicalOffset < children.length && $isTextNode(children[lexicalOffset])) {
2215
+ lexicalOffset += 1;
2216
+ }
2213
2217
  }
2214
- } else if (equalYTypePNode(rightY, rightL, binding)) {
2215
- // update mapping
2216
- binding.mapping.set(rightY, rightL);
2217
- } else {
2218
- break;
2219
2218
  }
2219
+ return [node, lexicalOffset];
2220
+ } else {
2221
+ const nodes = mapping.get(yType);
2222
+ if (nodes === undefined) {
2223
+ return [null, 0];
2224
+ }
2225
+ let i = 0;
2226
+ let adjustedOffset = yOffset;
2227
+ while (adjustedOffset > nodes[i].getTextContentSize() && i + 1 < nodes.length) {
2228
+ adjustedOffset -= nodes[i].getTextContentSize();
2229
+ i++;
2230
+ }
2231
+ const textNode = nodes[i];
2232
+ return [textNode, Math.min(adjustedOffset, textNode.getTextContentSize())];
2220
2233
  }
2221
- // try to compare and update
2222
- while (yChildCnt - left - right > 0 && lChildCnt - left - right > 0) {
2223
- const leftY = yChildren[left];
2224
- const leftL = lChildren[left];
2225
- const rightY = yChildren[yChildCnt - right - 1];
2226
- const rightL = lChildren[lChildCnt - right - 1];
2227
- if (leftY instanceof XmlText && leftL instanceof Array) {
2228
- if (!equalYTextLText(leftY, leftL, binding)) {
2229
- $updateYText(leftY, leftL, binding);
2234
+ }
2235
+ function getAwarenessStatesDefault(_binding, provider) {
2236
+ return provider.awareness.getStates();
2237
+ }
2238
+ function syncCursorPositions(binding, provider, options) {
2239
+ const {
2240
+ getAwarenessStates = getAwarenessStatesDefault
2241
+ } = options ?? {};
2242
+ const awarenessStates = Array.from(getAwarenessStates(binding, provider));
2243
+ const localClientID = binding.clientID;
2244
+ const cursors = binding.cursors;
2245
+ const editor = binding.editor;
2246
+ const nodeMap = editor._editorState._nodeMap;
2247
+ const visitedClientIDs = new Set();
2248
+ for (let i = 0; i < awarenessStates.length; i++) {
2249
+ const awarenessState = awarenessStates[i];
2250
+ const [clientID, awareness] = awarenessState;
2251
+ if (clientID !== 0 && clientID !== localClientID) {
2252
+ visitedClientIDs.add(clientID);
2253
+ const {
2254
+ name,
2255
+ color,
2256
+ focusing
2257
+ } = awareness;
2258
+ let selection = null;
2259
+ let cursor = cursors.get(clientID);
2260
+ if (cursor === undefined) {
2261
+ cursor = createCursor(name, color);
2262
+ cursors.set(clientID, cursor);
2230
2263
  }
2231
- left += 1;
2232
- } else {
2233
- let updateLeft = leftY instanceof XmlElement && matchNodeName(leftY, leftL);
2234
- let updateRight = rightY instanceof XmlElement && matchNodeName(rightY, rightL);
2235
- if (updateLeft && updateRight) {
2236
- // decide which which element to update
2237
- const equalityLeft = computeChildEqualityFactor(leftY, leftL, binding);
2238
- const equalityRight = computeChildEqualityFactor(rightY, rightL, binding);
2239
- if (equalityLeft.foundMappedChild && !equalityRight.foundMappedChild) {
2240
- updateRight = false;
2241
- } else if (!equalityLeft.foundMappedChild && equalityRight.foundMappedChild) {
2242
- updateLeft = false;
2243
- } else if (equalityLeft.equalityFactor < equalityRight.equalityFactor) {
2244
- updateLeft = false;
2245
- } else {
2246
- updateRight = false;
2264
+ if (focusing) {
2265
+ const {
2266
+ anchorKey,
2267
+ anchorOffset,
2268
+ focusKey,
2269
+ focusOffset
2270
+ } = editor.read(() => $getAnchorAndFocusForUserState(binding, awareness));
2271
+ if (anchorKey !== null && focusKey !== null) {
2272
+ selection = cursor.selection;
2273
+ if (selection === null) {
2274
+ selection = createCursorSelection(cursor, anchorKey, anchorOffset, focusKey, focusOffset);
2275
+ } else {
2276
+ const anchor = selection.anchor;
2277
+ const focus = selection.focus;
2278
+ anchor.key = anchorKey;
2279
+ anchor.offset = anchorOffset;
2280
+ focus.key = focusKey;
2281
+ focus.offset = focusOffset;
2282
+ }
2247
2283
  }
2248
2284
  }
2249
- if (updateLeft) {
2250
- $updateYFragment(y, leftY, leftL, binding, dirtyElements);
2251
- left += 1;
2252
- } else if (updateRight) {
2253
- $updateYFragment(y, rightY, rightL, binding, dirtyElements);
2254
- right += 1;
2255
- } else {
2256
- binding.mapping.delete(yDomFragment.get(left));
2257
- yDomFragment.delete(left, 1);
2258
- yDomFragment.insert(left, [$createTypeFromTextOrElementNode(leftL, binding)]);
2259
- left += 1;
2285
+ updateCursor(binding, cursor, selection, nodeMap);
2286
+ }
2287
+ }
2288
+ const allClientIDs = Array.from(cursors.keys());
2289
+ for (let i = 0; i < allClientIDs.length; i++) {
2290
+ const clientID = allClientIDs[i];
2291
+ if (!visitedClientIDs.has(clientID)) {
2292
+ const cursor = cursors.get(clientID);
2293
+ if (cursor !== undefined) {
2294
+ destroyCursor(binding, cursor);
2295
+ cursors.delete(clientID);
2260
2296
  }
2261
2297
  }
2262
2298
  }
2263
- const yDelLen = yChildCnt - left - right;
2264
- if (yChildCnt === 1 && lChildCnt === 0 && yChildren[0] instanceof XmlText) {
2265
- binding.mapping.delete(yChildren[0]);
2266
- // Edge case handling https://github.com/yjs/y-prosemirror/issues/108
2267
- // Only delete the content of the Y.Text to retain remote changes on the same Y.Text object
2268
- yChildren[0].delete(0, yChildren[0].length);
2269
- } else if (yDelLen > 0) {
2270
- yDomFragment.slice(left, left + yDelLen).forEach(type => binding.mapping.delete(type));
2271
- yDomFragment.delete(left, yDelLen);
2299
+ }
2300
+ function syncLexicalSelectionToYjs(binding, provider, prevSelection, nextSelection) {
2301
+ const awareness = provider.awareness;
2302
+ const localState = awareness.getLocalState();
2303
+ if (localState === null) {
2304
+ return;
2272
2305
  }
2273
- if (left + right < lChildCnt) {
2274
- const ins = [];
2275
- for (let i = left; i < lChildCnt - right; i++) {
2276
- ins.push($createTypeFromTextOrElementNode(lChildren[i], binding));
2306
+ const {
2307
+ anchorPos: currentAnchorPos,
2308
+ focusPos: currentFocusPos,
2309
+ name,
2310
+ color,
2311
+ focusing,
2312
+ awarenessData
2313
+ } = localState;
2314
+ let anchorPos = null;
2315
+ let focusPos = null;
2316
+ if (nextSelection === null || currentAnchorPos !== null && !nextSelection.is(prevSelection)) {
2317
+ if (prevSelection === null) {
2318
+ return;
2277
2319
  }
2278
- yDomFragment.insert(left, ins);
2279
2320
  }
2280
- };
2281
- const matchNodeName = (yElement, lnode) => !(lnode instanceof Array) && yElement.nodeName === lnode.getType();
2321
+ if ($isRangeSelection(nextSelection)) {
2322
+ if (isBindingV1(binding)) {
2323
+ anchorPos = createRelativePosition(nextSelection.anchor, binding);
2324
+ focusPos = createRelativePosition(nextSelection.focus, binding);
2325
+ } else {
2326
+ anchorPos = createRelativePositionV2(nextSelection.anchor, binding);
2327
+ focusPos = createRelativePositionV2(nextSelection.focus, binding);
2328
+ }
2329
+ }
2330
+ if (shouldUpdatePosition(currentAnchorPos, anchorPos) || shouldUpdatePosition(currentFocusPos, focusPos)) {
2331
+ awareness.setLocalState({
2332
+ ...localState,
2333
+ anchorPos,
2334
+ awarenessData,
2335
+ color,
2336
+ focusPos,
2337
+ focusing,
2338
+ name
2339
+ });
2340
+ }
2341
+ }
2282
2342
 
2283
2343
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
2284
2344
  function $syncStateEvent(binding, event) {
@@ -2546,6 +2606,25 @@ function syncYjsChangesToLexicalV2__EXPERIMENTAL(binding, provider, events, tran
2546
2606
  tag: isFromUndoManger ? HISTORIC_TAG : COLLABORATION_TAG
2547
2607
  });
2548
2608
  }
2609
+ function syncYjsStateToLexicalV2__EXPERIMENTAL(binding, provider) {
2610
+ binding.mapping.clear();
2611
+ const editor = binding.editor;
2612
+ editor.update(() => {
2613
+ $getRoot().clear();
2614
+ $createOrUpdateNodeFromYElement(binding.root, binding, null, true);
2615
+ $addUpdateTag(COLLABORATION_TAG);
2616
+ }, {
2617
+ // Need any text node normalization to be synchronously updated back to Yjs, otherwise the
2618
+ // binding.mapping will get out of sync.
2619
+ discrete: true,
2620
+ onUpdate: () => {
2621
+ syncCursorPositions(binding, provider);
2622
+ editor.update(() => $ensureEditorNotEmpty());
2623
+ },
2624
+ skipTransforms: true,
2625
+ tag: COLLABORATION_TAG
2626
+ });
2627
+ }
2549
2628
  function syncLexicalUpdateToYjsV2__EXPERIMENTAL(binding, provider, prevEditorState, currEditorState, dirtyElements, normalizedNodes, tags) {
2550
2629
  const isFromYjs = tags.has(COLLABORATION_TAG) || tags.has(HISTORIC_TAG);
2551
2630
  if (isFromYjs && normalizedNodes.size === 0) {
@@ -2579,6 +2658,8 @@ function syncLexicalUpdateToYjsV2__EXPERIMENTAL(binding, provider, prevEditorSta
2579
2658
 
2580
2659
  const CONNECTED_COMMAND = createCommand('CONNECTED_COMMAND');
2581
2660
  const TOGGLE_CONNECT_COMMAND = createCommand('TOGGLE_CONNECT_COMMAND');
2661
+ const DIFF_VERSIONS_COMMAND__EXPERIMENTAL = createCommand('DIFF_VERSIONS_COMMAND');
2662
+ const CLEAR_DIFF_VERSIONS_COMMAND__EXPERIMENTAL = createCommand('CLEAR_DIFF_VERSIONS_COMMAND');
2582
2663
  function createUndoManager(binding, root) {
2583
2664
  return new UndoManager(root, {
2584
2665
  trackedOrigins: new Set([binding, null])
@@ -2613,4 +2694,4 @@ function setLocalStateFocus(provider, name, color, focusing, awarenessData) {
2613
2694
  awareness.setLocalState(localState);
2614
2695
  }
2615
2696
 
2616
- export { CONNECTED_COMMAND, TOGGLE_CONNECT_COMMAND, createBinding, createBindingV2__EXPERIMENTAL, createUndoManager, getAnchorAndFocusCollabNodesForUserState, initLocalState, setLocalStateFocus, syncCursorPositions, syncLexicalUpdateToYjs, syncLexicalUpdateToYjsV2__EXPERIMENTAL, syncYjsChangesToLexical, syncYjsChangesToLexicalV2__EXPERIMENTAL };
2697
+ export { $getYChangeState, CLEAR_DIFF_VERSIONS_COMMAND__EXPERIMENTAL, CONNECTED_COMMAND, DIFF_VERSIONS_COMMAND__EXPERIMENTAL, TOGGLE_CONNECT_COMMAND, createBinding, createBindingV2__EXPERIMENTAL, createUndoManager, getAnchorAndFocusCollabNodesForUserState, initLocalState, renderSnapshot__EXPERIMENTAL, setLocalStateFocus, syncCursorPositions, syncLexicalUpdateToYjs, syncLexicalUpdateToYjsV2__EXPERIMENTAL, syncYjsChangesToLexical, syncYjsChangesToLexicalV2__EXPERIMENTAL, syncYjsStateToLexicalV2__EXPERIMENTAL };