@lexical/yjs 0.36.3-nightly.20251002.0 → 0.36.3-nightly.20251003.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, $addUpdateTag, SKIP_SCROLL_INTO_VIEW_TAG, HISTORIC_TAG, COLLABORATION_TAG, $createParagraphNode, createCommand } from 'lexical';
10
- import { XmlText, Map as Map$1, XmlElement, Doc, createAbsolutePositionFromRelativePosition, createRelativePositionFromTypeIndex, compareRelativePositions, YMapEvent, YTextEvent, YXmlEvent, UndoManager } from 'yjs';
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';
11
11
  import { $createChildrenArray } from '@lexical/offset';
12
12
  import { createDOMRange, createRectsFromDOMRange } from '@lexical/selection';
13
13
 
@@ -233,6 +233,35 @@ function isExcludedProperty(name, node, binding) {
233
233
  const excludedProperties = binding.excludedProperties.get(nodeKlass);
234
234
  return excludedProperties != null && excludedProperties.has(name);
235
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`);
262
+ }
263
+ return properties;
264
+ }
236
265
  function $createCollabNodeFromLexicalNode(binding, lexicalNode, parent) {
237
266
  const nodeType = lexicalNode.__type;
238
267
  let collabNode;
@@ -323,16 +352,16 @@ function createLexicalNodeFromCollabNode(binding, collabNode, parentKey) {
323
352
  return lexicalNode;
324
353
  }
325
354
  function $syncPropertiesFromYjs(binding, sharedType, lexicalNode, keysChanged) {
326
- const properties = keysChanged === null ? sharedType instanceof Map$1 ? Array.from(sharedType.keys()) : Object.keys(sharedType.getAttributes()) : Array.from(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);
327
356
  let writableNode;
328
357
  for (let i = 0; i < properties.length; i++) {
329
358
  const property = properties[i];
330
359
  if (isExcludedProperty(property, lexicalNode, binding)) {
331
- if (property === '__state') {
360
+ if (property === '__state' && isBindingV1(binding)) {
332
361
  if (!writableNode) {
333
362
  writableNode = lexicalNode.getWritable();
334
363
  }
335
- $syncNodeStateToLexical(binding, sharedType, writableNode);
364
+ $syncNodeStateToLexical(sharedType, writableNode);
336
365
  }
337
366
  continue;
338
367
  }
@@ -363,8 +392,10 @@ function $syncPropertiesFromYjs(binding, sharedType, lexicalNode, keysChanged) {
363
392
  function sharedTypeGet(sharedType, property) {
364
393
  if (sharedType instanceof Map$1) {
365
394
  return sharedType.get(property);
366
- } else {
395
+ } else if (sharedType instanceof XmlText || sharedType instanceof XmlElement) {
367
396
  return sharedType.getAttribute(property);
397
+ } else {
398
+ return sharedType[property];
368
399
  }
369
400
  }
370
401
  function sharedTypeSet(sharedType, property, nextValue) {
@@ -374,7 +405,7 @@ function sharedTypeSet(sharedType, property, nextValue) {
374
405
  sharedType.setAttribute(property, nextValue);
375
406
  }
376
407
  }
377
- function $syncNodeStateToLexical(binding, sharedType, lexicalNode) {
408
+ function $syncNodeStateToLexical(sharedType, lexicalNode) {
378
409
  const existingState = sharedTypeGet(sharedType, '__state');
379
410
  if (!(existingState instanceof Map$1)) {
380
411
  return;
@@ -414,15 +445,7 @@ function syncNodeStateFromLexical(binding, sharedType, prevLexicalNode, nextLexi
414
445
  }
415
446
  }
416
447
  function syncPropertiesFromLexical(binding, sharedType, prevLexicalNode, nextLexicalNode) {
417
- const type = nextLexicalNode.__type;
418
- const nodeProperties = binding.nodeProperties;
419
- let properties = nodeProperties.get(type);
420
- if (properties === undefined) {
421
- properties = Object.keys(nextLexicalNode).filter(property => {
422
- return !isExcludedProperty(property, nextLexicalNode, binding);
423
- });
424
- nodeProperties.set(type, properties);
425
- }
448
+ const properties = Object.keys(getDefaultNodeProperties(nextLexicalNode, binding));
426
449
  const EditorClass = binding.editor.constructor;
427
450
  syncNodeStateFromLexical(binding, sharedType, prevLexicalNode, nextLexicalNode);
428
451
  for (let i = 0; i < properties.length; i++) {
@@ -1063,16 +1086,98 @@ function $createCollabElementNode(xmlText, parent, type) {
1063
1086
  return collabNode;
1064
1087
  }
1065
1088
 
1066
- function createBinding(editor, provider, id, doc, docMap, excludedProperties) {
1089
+ // Stores mappings between Yjs shared types and the Lexical nodes they were last associated with.
1090
+ class CollabV2Mapping {
1091
+ _nodeMap = new Map();
1092
+ _sharedTypeToNodeKeys = new Map();
1093
+ _nodeKeyToSharedType = new Map();
1094
+ set(sharedType, node) {
1095
+ const isArray = node instanceof Array;
1096
+
1097
+ // Clear all existing associations for this key.
1098
+ this.delete(sharedType);
1099
+
1100
+ // If nodes were associated with other shared types, remove those associations.
1101
+ const nodes = isArray ? node : [node];
1102
+ for (const n of nodes) {
1103
+ const key = n.getKey();
1104
+ if (this._nodeKeyToSharedType.has(key)) {
1105
+ const otherSharedType = this._nodeKeyToSharedType.get(key);
1106
+ const keyIndex = this._sharedTypeToNodeKeys.get(otherSharedType).indexOf(key);
1107
+ if (keyIndex !== -1) {
1108
+ this._sharedTypeToNodeKeys.get(otherSharedType).splice(keyIndex, 1);
1109
+ }
1110
+ this._nodeKeyToSharedType.delete(key);
1111
+ this._nodeMap.delete(key);
1112
+ }
1113
+ }
1114
+ if (sharedType instanceof XmlText) {
1115
+ if (!isArray) {
1116
+ formatDevErrorMessage(`Text nodes must be mapped as an array`);
1117
+ }
1118
+ if (node.length === 0) {
1119
+ return;
1120
+ }
1121
+ this._sharedTypeToNodeKeys.set(sharedType, node.map(n => n.getKey()));
1122
+ for (const n of node) {
1123
+ this._nodeMap.set(n.getKey(), n);
1124
+ this._nodeKeyToSharedType.set(n.getKey(), sharedType);
1125
+ }
1126
+ } else {
1127
+ if (!!isArray) {
1128
+ formatDevErrorMessage(`Element nodes must be mapped as a single node`);
1129
+ }
1130
+ if (!!$isTextNode(node)) {
1131
+ formatDevErrorMessage(`Text nodes must be mapped to XmlText`);
1132
+ }
1133
+ this._sharedTypeToNodeKeys.set(sharedType, [node.getKey()]);
1134
+ this._nodeMap.set(node.getKey(), node);
1135
+ this._nodeKeyToSharedType.set(node.getKey(), sharedType);
1136
+ }
1137
+ }
1138
+ get(sharedType) {
1139
+ const nodes = this._sharedTypeToNodeKeys.get(sharedType);
1140
+ if (nodes === undefined) {
1141
+ return undefined;
1142
+ }
1143
+ if (sharedType instanceof XmlText) {
1144
+ const arr = Array.from(nodes.map(nodeKey => this._nodeMap.get(nodeKey)));
1145
+ return arr.length > 0 ? arr : undefined;
1146
+ }
1147
+ return this._nodeMap.get(nodes[0]);
1148
+ }
1149
+ getSharedType(node) {
1150
+ return this._nodeKeyToSharedType.get(node.getKey());
1151
+ }
1152
+ delete(sharedType) {
1153
+ const nodeKeys = this._sharedTypeToNodeKeys.get(sharedType);
1154
+ if (nodeKeys === undefined) {
1155
+ return;
1156
+ }
1157
+ for (const nodeKey of nodeKeys) {
1158
+ this._nodeMap.delete(nodeKey);
1159
+ this._nodeKeyToSharedType.delete(nodeKey);
1160
+ }
1161
+ this._sharedTypeToNodeKeys.delete(sharedType);
1162
+ }
1163
+ deleteNode(nodeKey) {
1164
+ const sharedType = this._nodeKeyToSharedType.get(nodeKey);
1165
+ if (sharedType) {
1166
+ this.delete(sharedType);
1167
+ }
1168
+ this._nodeMap.delete(nodeKey);
1169
+ }
1170
+ has(sharedType) {
1171
+ return this._sharedTypeToNodeKeys.has(sharedType);
1172
+ }
1173
+ }
1174
+
1175
+ function createBaseBinding(editor, id, doc, docMap, excludedProperties) {
1067
1176
  if (!(doc !== undefined && doc !== null)) {
1068
1177
  formatDevErrorMessage(`createBinding: doc is null or undefined`);
1069
1178
  }
1070
- const rootXmlText = doc.get('root', XmlText);
1071
- const root = $createCollabElementNode(rootXmlText, null, 'root');
1072
- root._key = 'root';
1073
- return {
1179
+ const binding = {
1074
1180
  clientID: doc.clientID,
1075
- collabNodeMap: new Map(),
1076
1181
  cursors: new Map(),
1077
1182
  cursorsContainer: null,
1078
1183
  doc,
@@ -1080,10 +1185,41 @@ function createBinding(editor, provider, id, doc, docMap, excludedProperties) {
1080
1185
  editor,
1081
1186
  excludedProperties: excludedProperties || new Map(),
1082
1187
  id,
1083
- nodeProperties: new Map(),
1188
+ nodeProperties: new Map()
1189
+ };
1190
+ initializeNodeProperties(binding);
1191
+ return binding;
1192
+ }
1193
+ function createBinding(editor, provider, id, doc, docMap, excludedProperties) {
1194
+ if (!(doc !== undefined && doc !== null)) {
1195
+ formatDevErrorMessage(`createBinding: doc is null or undefined`);
1196
+ }
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(),
1084
1203
  root
1085
1204
  };
1086
1205
  }
1206
+ function createBindingV2__EXPERIMENTAL(editor, id, doc, docMap, options = {}) {
1207
+ if (!(doc !== undefined && doc !== null)) {
1208
+ formatDevErrorMessage(`createBinding: doc is null or undefined`);
1209
+ }
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
+ }
1220
+ function isBindingV1(binding) {
1221
+ return Object.hasOwn(binding, 'collabNodeMap');
1222
+ }
1087
1223
 
1088
1224
  function createRelativePosition(point, binding) {
1089
1225
  const collabNodeMap = binding.collabNodeMap;
@@ -1120,6 +1256,49 @@ function createRelativePosition(point, binding) {
1120
1256
  }
1121
1257
  return createRelativePositionFromTypeIndex(sharedType, offset);
1122
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
+ }
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`);
1285
+ }
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();
1293
+ }
1294
+ }
1295
+ i++;
1296
+ child = child.getNextSibling();
1297
+ }
1298
+ return createRelativePositionFromTypeIndex(yType, i);
1299
+ }
1300
+ return null;
1301
+ }
1123
1302
  function createAbsolutePosition(relativePosition, binding) {
1124
1303
  return createAbsolutePositionFromRelativePosition(relativePosition, binding.doc);
1125
1304
  }
@@ -1261,6 +1440,9 @@ function updateCursor(binding, cursor, nextSelection, nodeMap) {
1261
1440
  selections.pop();
1262
1441
  }
1263
1442
  }
1443
+ /**
1444
+ * @deprecated Use `$getAnchorAndFocusForUserState` instead.
1445
+ */
1264
1446
  function getAnchorAndFocusCollabNodesForUserState(binding, userState) {
1265
1447
  const {
1266
1448
  anchorPos,
@@ -1285,6 +1467,56 @@ function getAnchorAndFocusCollabNodesForUserState(binding, userState) {
1285
1467
  focusOffset
1286
1468
  };
1287
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
+ };
1494
+ }
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;
1510
+ }
1511
+ }
1512
+ }
1513
+ return {
1514
+ anchorKey: anchorNode !== null ? anchorNode.getKey() : null,
1515
+ anchorOffset,
1516
+ focusKey: focusNode !== null ? focusNode.getKey() : null,
1517
+ focusOffset
1518
+ };
1519
+ }
1288
1520
  function $syncLocalCursorPosition(binding, provider) {
1289
1521
  const awareness = provider.awareness;
1290
1522
  const localState = awareness.getLocalState();
@@ -1292,14 +1524,12 @@ function $syncLocalCursorPosition(binding, provider) {
1292
1524
  return;
1293
1525
  }
1294
1526
  const {
1295
- anchorCollabNode,
1527
+ anchorKey,
1296
1528
  anchorOffset,
1297
- focusCollabNode,
1529
+ focusKey,
1298
1530
  focusOffset
1299
- } = getAnchorAndFocusCollabNodesForUserState(binding, localState);
1300
- if (anchorCollabNode !== null && focusCollabNode !== null) {
1301
- const anchorKey = anchorCollabNode.getKey();
1302
- const focusKey = focusCollabNode.getKey();
1531
+ } = $getAnchorAndFocusForUserState(binding, localState);
1532
+ if (anchorKey !== null && focusKey !== null) {
1303
1533
  const selection = $getSelection();
1304
1534
  if (!$isRangeSelection(selection)) {
1305
1535
  return;
@@ -1340,6 +1570,46 @@ sharedType, offset) {
1340
1570
  }
1341
1571
  return [null, 0];
1342
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
+ }
1595
+ }
1596
+ }
1597
+ return [node, lexicalOffset];
1598
+ } else {
1599
+ const nodes = mapping.get(yType);
1600
+ if (nodes === undefined) {
1601
+ return [null, 0];
1602
+ }
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++;
1608
+ }
1609
+ const textNode = nodes[i];
1610
+ return [textNode, Math.min(adjustedOffset, textNode.getTextContentSize())];
1611
+ }
1612
+ }
1343
1613
  function getAwarenessStatesDefault(_binding, provider) {
1344
1614
  return provider.awareness.getStates();
1345
1615
  }
@@ -1371,14 +1641,12 @@ function syncCursorPositions(binding, provider, options) {
1371
1641
  }
1372
1642
  if (focusing) {
1373
1643
  const {
1374
- anchorCollabNode,
1644
+ anchorKey,
1375
1645
  anchorOffset,
1376
- focusCollabNode,
1646
+ focusKey,
1377
1647
  focusOffset
1378
- } = getAnchorAndFocusCollabNodesForUserState(binding, awareness);
1379
- if (anchorCollabNode !== null && focusCollabNode !== null) {
1380
- const anchorKey = anchorCollabNode.getKey();
1381
- const focusKey = focusCollabNode.getKey();
1648
+ } = editor.read(() => $getAnchorAndFocusForUserState(binding, awareness));
1649
+ if (anchorKey !== null && focusKey !== null) {
1382
1650
  selection = cursor.selection;
1383
1651
  if (selection === null) {
1384
1652
  selection = createCursorSelection(cursor, anchorKey, anchorOffset, focusKey, focusOffset);
@@ -1429,8 +1697,13 @@ function syncLexicalSelectionToYjs(binding, provider, prevSelection, nextSelecti
1429
1697
  }
1430
1698
  }
1431
1699
  if ($isRangeSelection(nextSelection)) {
1432
- anchorPos = createRelativePosition(nextSelection.anchor, binding);
1433
- focusPos = createRelativePosition(nextSelection.focus, binding);
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);
1706
+ }
1434
1707
  }
1435
1708
  if (shouldUpdatePosition(currentAnchorPos, anchorPos) || shouldUpdatePosition(currentFocusPos, focusPos)) {
1436
1709
  awareness.setLocalState({
@@ -1445,6 +1718,568 @@ function syncLexicalSelectionToYjs(binding, provider, prevSelection, nextSelecti
1445
1718
  }
1446
1719
  }
1447
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;
1737
+ }
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`);
1743
+ }
1744
+ if (!node) {
1745
+ node = new nodeInfo.klass();
1746
+ keysChanged = null;
1747
+ childListChanged = true;
1748
+ }
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
+ });
1765
+ }
1766
+ } else {
1767
+ {
1768
+ formatDevErrorMessage(`XmlHook is not supported`);
1769
+ }
1770
+ }
1771
+ };
1772
+ {
1773
+ el.toArray().forEach($createChildren);
1774
+ }
1775
+ $spliceChildren(node, children);
1776
+ }
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];
1799
+ } else {
1800
+ properties[k] = attrs[k];
1801
+ }
1802
+ }
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]);
1812
+ }
1813
+ }
1814
+ }
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;
1843
+ }
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++;
1856
+ } else {
1857
+ node.splice(nextIndex, 0, [nextChildNode]);
1858
+ nextIndex++;
1859
+ }
1860
+ }
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, []);
1867
+ }
1868
+ };
1869
+ const $createOrUpdateTextNodesFromYText = (text, binding, snapshot, prevSnapshot, computeYChange) => {
1870
+ const deltas = toDelta(text, snapshot, prevSnapshot, computeYChange);
1871
+
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`);
1883
+ }
1884
+ const node = new nodeInfo.klass();
1885
+ if (!$isTextNode(node)) {
1886
+ throw new Error(`$createTextNodesFromYText: Node ${type} is not a TextNode`);
1887
+ }
1888
+ return node;
1889
+ });
1890
+ }
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);
1902
+ }
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);
1910
+ }
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)
1925
+ };
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);
1932
+ }
1933
+ }
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;
1947
+ }
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);
1954
+ }
1955
+ return eq;
1956
+ };
1957
+ const normalizeNodeContent = node => {
1958
+ if (!(node instanceof ElementNode)) {
1959
+ return [];
1960
+ }
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);
1972
+ } else {
1973
+ res.push(n);
1974
+ }
1975
+ }
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
+ }
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;
2028
+ }
2029
+ }
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;
2045
+ }
2046
+ }
2047
+ n = n.right;
2048
+ }
2049
+ return {
2050
+ nAttrs,
2051
+ str
2052
+ };
2053
+ };
2054
+ const $updateYText = (ytext, ltexts, binding) => {
2055
+ binding.mapping.set(ytext, ltexts);
2056
+ 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
+ }
2066
+ 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()
2079
+ };
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;
2090
+ }
2091
+ cursorOffset += c.insert.length;
2092
+ }
2093
+ } else {
2094
+ cursorOffset = nextText.length;
2095
+ }
2096
+ 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;
2122
+ }
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!');
2152
+ }
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
+ }
2177
+ }
2178
+ }
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);
2200
+ } else {
2201
+ break;
2202
+ }
2203
+ }
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);
2213
+ }
2214
+ } else if (equalYTypePNode(rightY, rightL, binding)) {
2215
+ // update mapping
2216
+ binding.mapping.set(rightY, rightL);
2217
+ } else {
2218
+ break;
2219
+ }
2220
+ }
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);
2230
+ }
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;
2247
+ }
2248
+ }
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;
2260
+ }
2261
+ }
2262
+ }
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);
2272
+ }
2273
+ if (left + right < lChildCnt) {
2274
+ const ins = [];
2275
+ for (let i = left; i < lChildCnt - right; i++) {
2276
+ ins.push($createTypeFromTextOrElementNode(lChildren[i], binding));
2277
+ }
2278
+ yDomFragment.insert(left, ins);
2279
+ }
2280
+ };
2281
+ const matchNodeName = (yElement, lnode) => !(lnode instanceof Array) && yElement.nodeName === lnode.getType();
2282
+
1448
2283
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
1449
2284
  function $syncStateEvent(binding, event) {
1450
2285
  const {
@@ -1531,23 +2366,7 @@ function syncYjsChangesToLexical(binding, provider, events, isFromUndoManger, sy
1531
2366
  const event = events[i];
1532
2367
  $syncEvent(binding, event);
1533
2368
  }
1534
- const selection = $getSelection();
1535
- if ($isRangeSelection(selection)) {
1536
- if (doesSelectionNeedRecovering(selection)) {
1537
- const prevSelection = currentEditorState._selection;
1538
- if ($isRangeSelection(prevSelection)) {
1539
- $syncLocalCursorPosition(binding, provider);
1540
- if (doesSelectionNeedRecovering(selection)) {
1541
- // If the selected node is deleted, move the selection to the previous or parent node.
1542
- const anchorNodeKey = selection.anchor.key;
1543
- $moveSelectionToPreviousNode(anchorNodeKey, currentEditorState);
1544
- }
1545
- }
1546
- syncLexicalSelectionToYjs(binding, provider, prevSelection, $getSelection());
1547
- } else {
1548
- $syncLocalCursorPosition(binding, provider);
1549
- }
1550
- }
2369
+ $syncCursorFromYjs(currentEditorState, binding, provider);
1551
2370
  if (!isFromUndoManger) {
1552
2371
  // If it is an external change, we don't want the current scroll position to get changed
1553
2372
  // since the user might've intentionally scrolled somewhere else in the document.
@@ -1556,19 +2375,31 @@ function syncYjsChangesToLexical(binding, provider, events, isFromUndoManger, sy
1556
2375
  }, {
1557
2376
  onUpdate: () => {
1558
2377
  syncCursorPositionsFn(binding, provider);
1559
- // If there was a collision on the top level paragraph
1560
- // we need to re-add a paragraph. To ensure this insertion properly syncs with other clients,
1561
- // it must be placed outside of the update block above that has tags 'collaboration' or 'historic'.
1562
- editor.update(() => {
1563
- if ($getRoot().getChildrenSize() === 0) {
1564
- $getRoot().append($createParagraphNode());
1565
- }
1566
- });
2378
+ editor.update(() => $ensureEditorNotEmpty());
1567
2379
  },
1568
2380
  skipTransforms: true,
1569
2381
  tag: isFromUndoManger ? HISTORIC_TAG : COLLABORATION_TAG
1570
2382
  });
1571
2383
  }
2384
+ function $syncCursorFromYjs(editorState, binding, provider) {
2385
+ const selection = $getSelection();
2386
+ if ($isRangeSelection(selection)) {
2387
+ if (doesSelectionNeedRecovering(selection)) {
2388
+ const prevSelection = editorState._selection;
2389
+ if ($isRangeSelection(prevSelection)) {
2390
+ $syncLocalCursorPosition(binding, provider);
2391
+ if (doesSelectionNeedRecovering(selection)) {
2392
+ // If the selected node is deleted, move the selection to the previous or parent node.
2393
+ const anchorNodeKey = selection.anchor.key;
2394
+ $moveSelectionToPreviousNode(anchorNodeKey, editorState);
2395
+ }
2396
+ }
2397
+ syncLexicalSelectionToYjs(binding, provider, prevSelection, $getSelection());
2398
+ } else {
2399
+ $syncLocalCursorPosition(binding, provider);
2400
+ }
2401
+ }
2402
+ }
1572
2403
  function $handleNormalizationMergeConflicts(binding, normalizedNodes) {
1573
2404
  // We handle the merge operations here
1574
2405
  const normalizedNodesKeys = Array.from(normalizedNodes);
@@ -1609,6 +2440,15 @@ function $handleNormalizationMergeConflicts(binding, normalizedNodes) {
1609
2440
  collabNode._text = text;
1610
2441
  }
1611
2442
  }
2443
+
2444
+ // If there was a collision on the top level paragraph
2445
+ // we need to re-add a paragraph. To ensure this insertion properly syncs with other clients,
2446
+ // it must be placed outside of the update block above that has tags 'collaboration' or 'historic'.
2447
+ function $ensureEditorNotEmpty() {
2448
+ if ($getRoot().getChildrenSize() === 0) {
2449
+ $getRoot().append($createParagraphNode());
2450
+ }
2451
+ }
1612
2452
  function syncLexicalUpdateToYjs(binding, provider, prevEditorState, currEditorState, dirtyElements, dirtyLeaves, normalizedNodes, tags) {
1613
2453
  syncWithTransaction(binding, () => {
1614
2454
  currEditorState.read(() => {
@@ -1638,6 +2478,96 @@ function syncLexicalUpdateToYjs(binding, provider, prevEditorState, currEditorSt
1638
2478
  });
1639
2479
  });
1640
2480
  }
2481
+ function $syncEventV2(binding, event) {
2482
+ const {
2483
+ target
2484
+ } = event;
2485
+ if (target instanceof XmlElement && event instanceof YXmlEvent) {
2486
+ $createOrUpdateNodeFromYElement(target, binding, event.attributesChanged,
2487
+ // @ts-expect-error childListChanged is private
2488
+ event.childListChanged);
2489
+ } else if (target instanceof XmlText && event instanceof YTextEvent) {
2490
+ const parent = target.parent;
2491
+ if (parent instanceof XmlElement) {
2492
+ // Need to sync via parent element in order to attach new next nodes.
2493
+ $createOrUpdateNodeFromYElement(parent, binding, new Set(), true);
2494
+ } else {
2495
+ {
2496
+ formatDevErrorMessage(`Expected XmlElement parent for XmlText`);
2497
+ }
2498
+ }
2499
+ } else {
2500
+ {
2501
+ formatDevErrorMessage(`Expected xml or text event`);
2502
+ }
2503
+ }
2504
+ }
2505
+ function syncYjsChangesToLexicalV2__EXPERIMENTAL(binding, provider, events, transaction, isFromUndoManger) {
2506
+ const editor = binding.editor;
2507
+ const editorState = editor._editorState;
2508
+
2509
+ // Remove deleted nodes from the mapping
2510
+ iterateDeletedStructs(transaction, transaction.deleteSet, struct => {
2511
+ if (struct.constructor === Item) {
2512
+ const content = struct.content;
2513
+ const type = content.type;
2514
+ if (type) {
2515
+ binding.mapping.delete(type);
2516
+ }
2517
+ }
2518
+ });
2519
+
2520
+ // This line precompute the delta before editor update. The reason is
2521
+ // delta is computed when it is accessed. Note that this can only be
2522
+ // safely computed during the event call. If it is accessed after event
2523
+ // call it might result in unexpected behavior.
2524
+ // https://github.com/yjs/yjs/blob/00ef472d68545cb260abd35c2de4b3b78719c9e4/src/utils/YEvent.js#L132
2525
+ events.forEach(event => event.delta);
2526
+ editor.update(() => {
2527
+ for (let i = 0; i < events.length; i++) {
2528
+ const event = events[i];
2529
+ $syncEventV2(binding, event);
2530
+ }
2531
+ $syncCursorFromYjs(editorState, binding, provider);
2532
+ if (!isFromUndoManger) {
2533
+ // If it is an external change, we don't want the current scroll position to get changed
2534
+ // since the user might've intentionally scrolled somewhere else in the document.
2535
+ $addUpdateTag(SKIP_SCROLL_INTO_VIEW_TAG);
2536
+ }
2537
+ }, {
2538
+ // Need any text node normalization to be synchronously updated back to Yjs, otherwise the
2539
+ // binding.mapping will get out of sync.
2540
+ discrete: true,
2541
+ onUpdate: () => {
2542
+ syncCursorPositions(binding, provider);
2543
+ editor.update(() => $ensureEditorNotEmpty());
2544
+ },
2545
+ skipTransforms: true,
2546
+ tag: isFromUndoManger ? HISTORIC_TAG : COLLABORATION_TAG
2547
+ });
2548
+ }
2549
+ function syncLexicalUpdateToYjsV2__EXPERIMENTAL(binding, provider, prevEditorState, currEditorState, dirtyElements, normalizedNodes, tags) {
2550
+ const isFromYjs = tags.has(COLLABORATION_TAG) || tags.has(HISTORIC_TAG);
2551
+ if (isFromYjs && normalizedNodes.size === 0) {
2552
+ return;
2553
+ }
2554
+
2555
+ // Nodes are normalized synchronously (`discrete: true` above), so the mapping may now be
2556
+ // incorrect for these nodes, as they point to `getLatest` which is mutable within an update.
2557
+ normalizedNodes.forEach(nodeKey => {
2558
+ binding.mapping.deleteNode(nodeKey);
2559
+ });
2560
+ syncWithTransaction(binding, () => {
2561
+ currEditorState.read(() => {
2562
+ if (dirtyElements.has('root')) {
2563
+ $updateYFragment(binding.doc, binding.root, $getRoot(), binding, new Set(dirtyElements.keys()));
2564
+ }
2565
+ const selection = $getSelection();
2566
+ const prevSelection = prevEditorState._selection;
2567
+ syncLexicalSelectionToYjs(binding, provider, prevSelection, selection);
2568
+ });
2569
+ });
2570
+ }
1641
2571
 
1642
2572
  /**
1643
2573
  * Copyright (c) Meta Platforms, Inc. and affiliates.
@@ -1683,4 +2613,4 @@ function setLocalStateFocus(provider, name, color, focusing, awarenessData) {
1683
2613
  awareness.setLocalState(localState);
1684
2614
  }
1685
2615
 
1686
- export { CONNECTED_COMMAND, TOGGLE_CONNECT_COMMAND, createBinding, createUndoManager, getAnchorAndFocusCollabNodesForUserState, initLocalState, setLocalStateFocus, syncCursorPositions, syncLexicalUpdateToYjs, syncYjsChangesToLexical };
2616
+ export { CONNECTED_COMMAND, TOGGLE_CONNECT_COMMAND, createBinding, createBindingV2__EXPERIMENTAL, createUndoManager, getAnchorAndFocusCollabNodesForUserState, initLocalState, setLocalStateFocus, syncCursorPositions, syncLexicalUpdateToYjs, syncLexicalUpdateToYjsV2__EXPERIMENTAL, syncYjsChangesToLexical, syncYjsChangesToLexicalV2__EXPERIMENTAL };