@lexical/yjs 0.36.2 → 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.
package/LexicalYjs.dev.js CHANGED
@@ -235,6 +235,35 @@ function isExcludedProperty(name, node, binding) {
235
235
  const excludedProperties = binding.excludedProperties.get(nodeKlass);
236
236
  return excludedProperties != null && excludedProperties.has(name);
237
237
  }
238
+ function initializeNodeProperties(binding) {
239
+ const {
240
+ editor,
241
+ nodeProperties
242
+ } = binding;
243
+ editor.update(() => {
244
+ editor._nodes.forEach(nodeInfo => {
245
+ const node = new nodeInfo.klass();
246
+ const defaultProperties = {};
247
+ for (const [property, value] of Object.entries(node)) {
248
+ if (!isExcludedProperty(property, node, binding)) {
249
+ defaultProperties[property] = value;
250
+ }
251
+ }
252
+ nodeProperties.set(node.__type, Object.freeze(defaultProperties));
253
+ });
254
+ });
255
+ }
256
+ function getDefaultNodeProperties(node, binding) {
257
+ const type = node.__type;
258
+ const {
259
+ nodeProperties
260
+ } = binding;
261
+ const properties = nodeProperties.get(type);
262
+ if (!(properties !== undefined)) {
263
+ formatDevErrorMessage(`Node properties for ${type} not initialized for sync`);
264
+ }
265
+ return properties;
266
+ }
238
267
  function $createCollabNodeFromLexicalNode(binding, lexicalNode, parent) {
239
268
  const nodeType = lexicalNode.__type;
240
269
  let collabNode;
@@ -325,16 +354,16 @@ function createLexicalNodeFromCollabNode(binding, collabNode, parentKey) {
325
354
  return lexicalNode;
326
355
  }
327
356
  function $syncPropertiesFromYjs(binding, sharedType, lexicalNode, keysChanged) {
328
- const properties = keysChanged === null ? sharedType instanceof yjs.Map ? Array.from(sharedType.keys()) : Object.keys(sharedType.getAttributes()) : Array.from(keysChanged);
357
+ const properties = keysChanged === null ? sharedType instanceof yjs.Map ? Array.from(sharedType.keys()) : sharedType instanceof yjs.XmlText || sharedType instanceof yjs.XmlElement ? Object.keys(sharedType.getAttributes()) : Object.keys(sharedType) : Array.from(keysChanged);
329
358
  let writableNode;
330
359
  for (let i = 0; i < properties.length; i++) {
331
360
  const property = properties[i];
332
361
  if (isExcludedProperty(property, lexicalNode, binding)) {
333
- if (property === '__state') {
362
+ if (property === '__state' && isBindingV1(binding)) {
334
363
  if (!writableNode) {
335
364
  writableNode = lexicalNode.getWritable();
336
365
  }
337
- $syncNodeStateToLexical(binding, sharedType, writableNode);
366
+ $syncNodeStateToLexical(sharedType, writableNode);
338
367
  }
339
368
  continue;
340
369
  }
@@ -365,8 +394,10 @@ function $syncPropertiesFromYjs(binding, sharedType, lexicalNode, keysChanged) {
365
394
  function sharedTypeGet(sharedType, property) {
366
395
  if (sharedType instanceof yjs.Map) {
367
396
  return sharedType.get(property);
368
- } else {
397
+ } else if (sharedType instanceof yjs.XmlText || sharedType instanceof yjs.XmlElement) {
369
398
  return sharedType.getAttribute(property);
399
+ } else {
400
+ return sharedType[property];
370
401
  }
371
402
  }
372
403
  function sharedTypeSet(sharedType, property, nextValue) {
@@ -376,7 +407,7 @@ function sharedTypeSet(sharedType, property, nextValue) {
376
407
  sharedType.setAttribute(property, nextValue);
377
408
  }
378
409
  }
379
- function $syncNodeStateToLexical(binding, sharedType, lexicalNode) {
410
+ function $syncNodeStateToLexical(sharedType, lexicalNode) {
380
411
  const existingState = sharedTypeGet(sharedType, '__state');
381
412
  if (!(existingState instanceof yjs.Map)) {
382
413
  return;
@@ -416,15 +447,7 @@ function syncNodeStateFromLexical(binding, sharedType, prevLexicalNode, nextLexi
416
447
  }
417
448
  }
418
449
  function syncPropertiesFromLexical(binding, sharedType, prevLexicalNode, nextLexicalNode) {
419
- const type = nextLexicalNode.__type;
420
- const nodeProperties = binding.nodeProperties;
421
- let properties = nodeProperties.get(type);
422
- if (properties === undefined) {
423
- properties = Object.keys(nextLexicalNode).filter(property => {
424
- return !isExcludedProperty(property, nextLexicalNode, binding);
425
- });
426
- nodeProperties.set(type, properties);
427
- }
450
+ const properties = Object.keys(getDefaultNodeProperties(nextLexicalNode, binding));
428
451
  const EditorClass = binding.editor.constructor;
429
452
  syncNodeStateFromLexical(binding, sharedType, prevLexicalNode, nextLexicalNode);
430
453
  for (let i = 0; i < properties.length; i++) {
@@ -1065,16 +1088,98 @@ function $createCollabElementNode(xmlText, parent, type) {
1065
1088
  return collabNode;
1066
1089
  }
1067
1090
 
1068
- function createBinding(editor, provider, id, doc, docMap, excludedProperties) {
1091
+ // Stores mappings between Yjs shared types and the Lexical nodes they were last associated with.
1092
+ class CollabV2Mapping {
1093
+ _nodeMap = new Map();
1094
+ _sharedTypeToNodeKeys = new Map();
1095
+ _nodeKeyToSharedType = new Map();
1096
+ set(sharedType, node) {
1097
+ const isArray = node instanceof Array;
1098
+
1099
+ // Clear all existing associations for this key.
1100
+ this.delete(sharedType);
1101
+
1102
+ // If nodes were associated with other shared types, remove those associations.
1103
+ const nodes = isArray ? node : [node];
1104
+ for (const n of nodes) {
1105
+ const key = n.getKey();
1106
+ if (this._nodeKeyToSharedType.has(key)) {
1107
+ const otherSharedType = this._nodeKeyToSharedType.get(key);
1108
+ const keyIndex = this._sharedTypeToNodeKeys.get(otherSharedType).indexOf(key);
1109
+ if (keyIndex !== -1) {
1110
+ this._sharedTypeToNodeKeys.get(otherSharedType).splice(keyIndex, 1);
1111
+ }
1112
+ this._nodeKeyToSharedType.delete(key);
1113
+ this._nodeMap.delete(key);
1114
+ }
1115
+ }
1116
+ if (sharedType instanceof yjs.XmlText) {
1117
+ if (!isArray) {
1118
+ formatDevErrorMessage(`Text nodes must be mapped as an array`);
1119
+ }
1120
+ if (node.length === 0) {
1121
+ return;
1122
+ }
1123
+ this._sharedTypeToNodeKeys.set(sharedType, node.map(n => n.getKey()));
1124
+ for (const n of node) {
1125
+ this._nodeMap.set(n.getKey(), n);
1126
+ this._nodeKeyToSharedType.set(n.getKey(), sharedType);
1127
+ }
1128
+ } else {
1129
+ if (!!isArray) {
1130
+ formatDevErrorMessage(`Element nodes must be mapped as a single node`);
1131
+ }
1132
+ if (!!lexical.$isTextNode(node)) {
1133
+ formatDevErrorMessage(`Text nodes must be mapped to XmlText`);
1134
+ }
1135
+ this._sharedTypeToNodeKeys.set(sharedType, [node.getKey()]);
1136
+ this._nodeMap.set(node.getKey(), node);
1137
+ this._nodeKeyToSharedType.set(node.getKey(), sharedType);
1138
+ }
1139
+ }
1140
+ get(sharedType) {
1141
+ const nodes = this._sharedTypeToNodeKeys.get(sharedType);
1142
+ if (nodes === undefined) {
1143
+ return undefined;
1144
+ }
1145
+ if (sharedType instanceof yjs.XmlText) {
1146
+ const arr = Array.from(nodes.map(nodeKey => this._nodeMap.get(nodeKey)));
1147
+ return arr.length > 0 ? arr : undefined;
1148
+ }
1149
+ return this._nodeMap.get(nodes[0]);
1150
+ }
1151
+ getSharedType(node) {
1152
+ return this._nodeKeyToSharedType.get(node.getKey());
1153
+ }
1154
+ delete(sharedType) {
1155
+ const nodeKeys = this._sharedTypeToNodeKeys.get(sharedType);
1156
+ if (nodeKeys === undefined) {
1157
+ return;
1158
+ }
1159
+ for (const nodeKey of nodeKeys) {
1160
+ this._nodeMap.delete(nodeKey);
1161
+ this._nodeKeyToSharedType.delete(nodeKey);
1162
+ }
1163
+ this._sharedTypeToNodeKeys.delete(sharedType);
1164
+ }
1165
+ deleteNode(nodeKey) {
1166
+ const sharedType = this._nodeKeyToSharedType.get(nodeKey);
1167
+ if (sharedType) {
1168
+ this.delete(sharedType);
1169
+ }
1170
+ this._nodeMap.delete(nodeKey);
1171
+ }
1172
+ has(sharedType) {
1173
+ return this._sharedTypeToNodeKeys.has(sharedType);
1174
+ }
1175
+ }
1176
+
1177
+ function createBaseBinding(editor, id, doc, docMap, excludedProperties) {
1069
1178
  if (!(doc !== undefined && doc !== null)) {
1070
1179
  formatDevErrorMessage(`createBinding: doc is null or undefined`);
1071
1180
  }
1072
- const rootXmlText = doc.get('root', yjs.XmlText);
1073
- const root = $createCollabElementNode(rootXmlText, null, 'root');
1074
- root._key = 'root';
1075
- return {
1181
+ const binding = {
1076
1182
  clientID: doc.clientID,
1077
- collabNodeMap: new Map(),
1078
1183
  cursors: new Map(),
1079
1184
  cursorsContainer: null,
1080
1185
  doc,
@@ -1082,10 +1187,41 @@ function createBinding(editor, provider, id, doc, docMap, excludedProperties) {
1082
1187
  editor,
1083
1188
  excludedProperties: excludedProperties || new Map(),
1084
1189
  id,
1085
- nodeProperties: new Map(),
1190
+ nodeProperties: new Map()
1191
+ };
1192
+ initializeNodeProperties(binding);
1193
+ return binding;
1194
+ }
1195
+ function createBinding(editor, provider, id, doc, docMap, excludedProperties) {
1196
+ if (!(doc !== undefined && doc !== null)) {
1197
+ formatDevErrorMessage(`createBinding: doc is null or undefined`);
1198
+ }
1199
+ const rootXmlText = doc.get('root', yjs.XmlText);
1200
+ const root = $createCollabElementNode(rootXmlText, null, 'root');
1201
+ root._key = 'root';
1202
+ return {
1203
+ ...createBaseBinding(editor, id, doc, docMap, excludedProperties),
1204
+ collabNodeMap: new Map(),
1086
1205
  root
1087
1206
  };
1088
1207
  }
1208
+ function createBindingV2__EXPERIMENTAL(editor, id, doc, docMap, options = {}) {
1209
+ if (!(doc !== undefined && doc !== null)) {
1210
+ formatDevErrorMessage(`createBinding: doc is null or undefined`);
1211
+ }
1212
+ const {
1213
+ excludedProperties,
1214
+ rootName = 'root-v2'
1215
+ } = options;
1216
+ return {
1217
+ ...createBaseBinding(editor, id, doc, docMap, excludedProperties),
1218
+ mapping: new CollabV2Mapping(),
1219
+ root: doc.get(rootName, yjs.XmlElement)
1220
+ };
1221
+ }
1222
+ function isBindingV1(binding) {
1223
+ return Object.hasOwn(binding, 'collabNodeMap');
1224
+ }
1089
1225
 
1090
1226
  function createRelativePosition(point, binding) {
1091
1227
  const collabNodeMap = binding.collabNodeMap;
@@ -1122,6 +1258,49 @@ function createRelativePosition(point, binding) {
1122
1258
  }
1123
1259
  return yjs.createRelativePositionFromTypeIndex(sharedType, offset);
1124
1260
  }
1261
+ function createRelativePositionV2(point, binding) {
1262
+ const {
1263
+ mapping
1264
+ } = binding;
1265
+ const {
1266
+ offset
1267
+ } = point;
1268
+ const node = point.getNode();
1269
+ const yType = mapping.getSharedType(node);
1270
+ if (yType === undefined) {
1271
+ return null;
1272
+ }
1273
+ if (point.type === 'text') {
1274
+ if (!lexical.$isTextNode(node)) {
1275
+ formatDevErrorMessage(`Text point must be a text node`);
1276
+ }
1277
+ let prevSibling = node.getPreviousSibling();
1278
+ let adjustedOffset = offset;
1279
+ while (lexical.$isTextNode(prevSibling)) {
1280
+ adjustedOffset += prevSibling.getTextContentSize();
1281
+ prevSibling = prevSibling.getPreviousSibling();
1282
+ }
1283
+ return yjs.createRelativePositionFromTypeIndex(yType, adjustedOffset);
1284
+ } else if (point.type === 'element') {
1285
+ if (!lexical.$isElementNode(node)) {
1286
+ formatDevErrorMessage(`Element point must be an element node`);
1287
+ }
1288
+ let i = 0;
1289
+ let child = node.getFirstChild();
1290
+ while (child !== null && i < offset) {
1291
+ if (lexical.$isTextNode(child)) {
1292
+ let nextSibling = child.getNextSibling();
1293
+ while (lexical.$isTextNode(nextSibling)) {
1294
+ nextSibling = nextSibling.getNextSibling();
1295
+ }
1296
+ }
1297
+ i++;
1298
+ child = child.getNextSibling();
1299
+ }
1300
+ return yjs.createRelativePositionFromTypeIndex(yType, i);
1301
+ }
1302
+ return null;
1303
+ }
1125
1304
  function createAbsolutePosition(relativePosition, binding) {
1126
1305
  return yjs.createAbsolutePositionFromRelativePosition(relativePosition, binding.doc);
1127
1306
  }
@@ -1263,6 +1442,9 @@ function updateCursor(binding, cursor, nextSelection, nodeMap) {
1263
1442
  selections.pop();
1264
1443
  }
1265
1444
  }
1445
+ /**
1446
+ * @deprecated Use `$getAnchorAndFocusForUserState` instead.
1447
+ */
1266
1448
  function getAnchorAndFocusCollabNodesForUserState(binding, userState) {
1267
1449
  const {
1268
1450
  anchorPos,
@@ -1287,6 +1469,56 @@ function getAnchorAndFocusCollabNodesForUserState(binding, userState) {
1287
1469
  focusOffset
1288
1470
  };
1289
1471
  }
1472
+ function $getAnchorAndFocusForUserState(binding, userState) {
1473
+ const {
1474
+ anchorPos,
1475
+ focusPos
1476
+ } = userState;
1477
+ const anchorAbsPos = anchorPos ? createAbsolutePosition(anchorPos, binding) : null;
1478
+ const focusAbsPos = focusPos ? createAbsolutePosition(focusPos, binding) : null;
1479
+ if (anchorAbsPos === null || focusAbsPos === null) {
1480
+ return {
1481
+ anchorKey: null,
1482
+ anchorOffset: 0,
1483
+ focusKey: null,
1484
+ focusOffset: 0
1485
+ };
1486
+ }
1487
+ if (isBindingV1(binding)) {
1488
+ const [anchorCollabNode, anchorOffset] = getCollabNodeAndOffset(anchorAbsPos.type, anchorAbsPos.index);
1489
+ const [focusCollabNode, focusOffset] = getCollabNodeAndOffset(focusAbsPos.type, focusAbsPos.index);
1490
+ return {
1491
+ anchorKey: anchorCollabNode !== null ? anchorCollabNode.getKey() : null,
1492
+ anchorOffset,
1493
+ focusKey: focusCollabNode !== null ? focusCollabNode.getKey() : null,
1494
+ focusOffset
1495
+ };
1496
+ }
1497
+ let [anchorNode, anchorOffset] = $getNodeAndOffsetV2(binding.mapping, anchorAbsPos);
1498
+ let [focusNode, focusOffset] = $getNodeAndOffsetV2(binding.mapping, focusAbsPos);
1499
+ // For a non-collapsed selection, if the start of the selection is as the end of a text node,
1500
+ // move it to the beginning of the next text node (if one exists).
1501
+ if (focusNode && anchorNode && (focusNode !== anchorNode || focusOffset !== anchorOffset)) {
1502
+ const isBackwards = focusNode.isBefore(anchorNode);
1503
+ const startNode = isBackwards ? focusNode : anchorNode;
1504
+ const startOffset = isBackwards ? focusOffset : anchorOffset;
1505
+ if (lexical.$isTextNode(startNode) && lexical.$isTextNode(startNode.getNextSibling()) && startOffset === startNode.getTextContentSize()) {
1506
+ if (isBackwards) {
1507
+ focusNode = startNode.getNextSibling();
1508
+ focusOffset = 0;
1509
+ } else {
1510
+ anchorNode = startNode.getNextSibling();
1511
+ anchorOffset = 0;
1512
+ }
1513
+ }
1514
+ }
1515
+ return {
1516
+ anchorKey: anchorNode !== null ? anchorNode.getKey() : null,
1517
+ anchorOffset,
1518
+ focusKey: focusNode !== null ? focusNode.getKey() : null,
1519
+ focusOffset
1520
+ };
1521
+ }
1290
1522
  function $syncLocalCursorPosition(binding, provider) {
1291
1523
  const awareness = provider.awareness;
1292
1524
  const localState = awareness.getLocalState();
@@ -1294,14 +1526,12 @@ function $syncLocalCursorPosition(binding, provider) {
1294
1526
  return;
1295
1527
  }
1296
1528
  const {
1297
- anchorCollabNode,
1529
+ anchorKey,
1298
1530
  anchorOffset,
1299
- focusCollabNode,
1531
+ focusKey,
1300
1532
  focusOffset
1301
- } = getAnchorAndFocusCollabNodesForUserState(binding, localState);
1302
- if (anchorCollabNode !== null && focusCollabNode !== null) {
1303
- const anchorKey = anchorCollabNode.getKey();
1304
- const focusKey = focusCollabNode.getKey();
1533
+ } = $getAnchorAndFocusForUserState(binding, localState);
1534
+ if (anchorKey !== null && focusKey !== null) {
1305
1535
  const selection = lexical.$getSelection();
1306
1536
  if (!lexical.$isRangeSelection(selection)) {
1307
1537
  return;
@@ -1342,6 +1572,46 @@ sharedType, offset) {
1342
1572
  }
1343
1573
  return [null, 0];
1344
1574
  }
1575
+ function $getNodeAndOffsetV2(mapping, absolutePosition) {
1576
+ const yType = absolutePosition.type;
1577
+ const yOffset = absolutePosition.index;
1578
+ if (yType instanceof yjs.XmlElement) {
1579
+ const node = mapping.get(yType);
1580
+ if (node === undefined) {
1581
+ return [null, 0];
1582
+ }
1583
+ if (!lexical.$isElementNode(node)) {
1584
+ return [node, yOffset];
1585
+ }
1586
+ let remainingYOffset = yOffset;
1587
+ let lexicalOffset = 0;
1588
+ const children = node.getChildren();
1589
+ while (remainingYOffset > 0 && lexicalOffset < children.length) {
1590
+ const child = children[lexicalOffset];
1591
+ remainingYOffset -= 1;
1592
+ lexicalOffset += 1;
1593
+ if (lexical.$isTextNode(child)) {
1594
+ while (lexicalOffset < children.length && lexical.$isTextNode(children[lexicalOffset])) {
1595
+ lexicalOffset += 1;
1596
+ }
1597
+ }
1598
+ }
1599
+ return [node, lexicalOffset];
1600
+ } else {
1601
+ const nodes = mapping.get(yType);
1602
+ if (nodes === undefined) {
1603
+ return [null, 0];
1604
+ }
1605
+ let i = 0;
1606
+ let adjustedOffset = yOffset;
1607
+ while (adjustedOffset > nodes[i].getTextContentSize() && i + 1 < nodes.length) {
1608
+ adjustedOffset -= nodes[i].getTextContentSize();
1609
+ i++;
1610
+ }
1611
+ const textNode = nodes[i];
1612
+ return [textNode, Math.min(adjustedOffset, textNode.getTextContentSize())];
1613
+ }
1614
+ }
1345
1615
  function getAwarenessStatesDefault(_binding, provider) {
1346
1616
  return provider.awareness.getStates();
1347
1617
  }
@@ -1373,14 +1643,12 @@ function syncCursorPositions(binding, provider, options) {
1373
1643
  }
1374
1644
  if (focusing) {
1375
1645
  const {
1376
- anchorCollabNode,
1646
+ anchorKey,
1377
1647
  anchorOffset,
1378
- focusCollabNode,
1648
+ focusKey,
1379
1649
  focusOffset
1380
- } = getAnchorAndFocusCollabNodesForUserState(binding, awareness);
1381
- if (anchorCollabNode !== null && focusCollabNode !== null) {
1382
- const anchorKey = anchorCollabNode.getKey();
1383
- const focusKey = focusCollabNode.getKey();
1650
+ } = editor.read(() => $getAnchorAndFocusForUserState(binding, awareness));
1651
+ if (anchorKey !== null && focusKey !== null) {
1384
1652
  selection = cursor.selection;
1385
1653
  if (selection === null) {
1386
1654
  selection = createCursorSelection(cursor, anchorKey, anchorOffset, focusKey, focusOffset);
@@ -1431,8 +1699,13 @@ function syncLexicalSelectionToYjs(binding, provider, prevSelection, nextSelecti
1431
1699
  }
1432
1700
  }
1433
1701
  if (lexical.$isRangeSelection(nextSelection)) {
1434
- anchorPos = createRelativePosition(nextSelection.anchor, binding);
1435
- focusPos = createRelativePosition(nextSelection.focus, binding);
1702
+ if (isBindingV1(binding)) {
1703
+ anchorPos = createRelativePosition(nextSelection.anchor, binding);
1704
+ focusPos = createRelativePosition(nextSelection.focus, binding);
1705
+ } else {
1706
+ anchorPos = createRelativePositionV2(nextSelection.anchor, binding);
1707
+ focusPos = createRelativePositionV2(nextSelection.focus, binding);
1708
+ }
1436
1709
  }
1437
1710
  if (shouldUpdatePosition(currentAnchorPos, anchorPos) || shouldUpdatePosition(currentFocusPos, focusPos)) {
1438
1711
  awareness.setLocalState({
@@ -1447,6 +1720,568 @@ function syncLexicalSelectionToYjs(binding, provider, prevSelection, nextSelecti
1447
1720
  }
1448
1721
  }
1449
1722
 
1723
+ /*
1724
+ const isVisible = (item: Item, snapshot?: Snapshot): boolean =>
1725
+ snapshot === undefined
1726
+ ? !item.deleted
1727
+ : snapshot.sv.has(item.id.client) &&
1728
+ snapshot.sv.get(item.id.client)! > item.id.clock &&
1729
+ !isDeleted(snapshot.ds, item.id);
1730
+ */
1731
+
1732
+ // https://docs.yjs.dev/api/shared-types/y.xmlelement
1733
+ // "Define a top-level type; Note that the nodeName is always "undefined""
1734
+ const isRootElement = el => el.nodeName === 'UNDEFINED';
1735
+ const $createOrUpdateNodeFromYElement = (el, binding, keysChanged, childListChanged, snapshot, prevSnapshot, computeYChange) => {
1736
+ let node = binding.mapping.get(el);
1737
+ if (node && keysChanged && keysChanged.size === 0 && !childListChanged) {
1738
+ return node;
1739
+ }
1740
+ const type = isRootElement(el) ? lexical.RootNode.getType() : el.nodeName;
1741
+ const registeredNodes = binding.editor._nodes;
1742
+ const nodeInfo = registeredNodes.get(type);
1743
+ if (nodeInfo === undefined) {
1744
+ throw new Error(`$createOrUpdateNodeFromYElement: Node ${type} is not registered`);
1745
+ }
1746
+ if (!node) {
1747
+ node = new nodeInfo.klass();
1748
+ keysChanged = null;
1749
+ childListChanged = true;
1750
+ }
1751
+ if (childListChanged && node instanceof lexical.ElementNode) {
1752
+ const children = [];
1753
+ const $createChildren = childType => {
1754
+ if (childType instanceof yjs.XmlElement) {
1755
+ const n = $createOrUpdateNodeFromYElement(childType, binding, new Set(), false, snapshot, prevSnapshot, computeYChange);
1756
+ if (n !== null) {
1757
+ children.push(n);
1758
+ }
1759
+ } else if (childType instanceof yjs.XmlText) {
1760
+ const ns = $createOrUpdateTextNodesFromYText(childType, binding, snapshot, prevSnapshot, computeYChange);
1761
+ if (ns !== null) {
1762
+ ns.forEach(textchild => {
1763
+ if (textchild !== null) {
1764
+ children.push(textchild);
1765
+ }
1766
+ });
1767
+ }
1768
+ } else {
1769
+ {
1770
+ formatDevErrorMessage(`XmlHook is not supported`);
1771
+ }
1772
+ }
1773
+ };
1774
+ {
1775
+ el.toArray().forEach($createChildren);
1776
+ }
1777
+ $spliceChildren(node, children);
1778
+ }
1779
+ const attrs = el.getAttributes(snapshot);
1780
+ // TODO(collab-v2): support for ychange
1781
+ /*
1782
+ if (snapshot !== undefined) {
1783
+ if (!isVisible(el._item!, snapshot)) {
1784
+ attrs.ychange = computeYChange
1785
+ ? computeYChange('removed', el._item!.id)
1786
+ : {type: 'removed'};
1787
+ } else if (!isVisible(el._item!, prevSnapshot)) {
1788
+ attrs.ychange = computeYChange
1789
+ ? computeYChange('added', el._item!.id)
1790
+ : {type: 'added'};
1791
+ }
1792
+ }
1793
+ */
1794
+ const properties = {
1795
+ ...getDefaultNodeProperties(node, binding)
1796
+ };
1797
+ const state = {};
1798
+ for (const k in attrs) {
1799
+ if (k.startsWith(STATE_KEY_PREFIX)) {
1800
+ state[attrKeyToStateKey(k)] = attrs[k];
1801
+ } else {
1802
+ properties[k] = attrs[k];
1803
+ }
1804
+ }
1805
+ $syncPropertiesFromYjs(binding, properties, node, keysChanged);
1806
+ if (!keysChanged) {
1807
+ lexical.$getWritableNodeState(node).updateFromJSON(state);
1808
+ } else {
1809
+ const stateKeysChanged = Object.keys(state).filter(k => keysChanged.has(stateKeyToAttrKey(k)));
1810
+ if (stateKeysChanged.length > 0) {
1811
+ const writableState = lexical.$getWritableNodeState(node);
1812
+ for (const k of stateKeysChanged) {
1813
+ writableState.updateFromUnknown(k, state[k]);
1814
+ }
1815
+ }
1816
+ }
1817
+ const latestNode = node.getLatest();
1818
+ binding.mapping.set(el, latestNode);
1819
+ return latestNode;
1820
+ };
1821
+ const $spliceChildren = (node, nextChildren) => {
1822
+ const prevChildren = node.getChildren();
1823
+ const prevChildrenKeySet = new Set(prevChildren.map(child => child.getKey()));
1824
+ const nextChildrenKeySet = new Set(nextChildren.map(child => child.getKey()));
1825
+ const prevEndIndex = prevChildren.length - 1;
1826
+ const nextEndIndex = nextChildren.length - 1;
1827
+ let prevIndex = 0;
1828
+ let nextIndex = 0;
1829
+ while (prevIndex <= prevEndIndex && nextIndex <= nextEndIndex) {
1830
+ const prevKey = prevChildren[prevIndex].getKey();
1831
+ const nextKey = nextChildren[nextIndex].getKey();
1832
+ if (prevKey === nextKey) {
1833
+ prevIndex++;
1834
+ nextIndex++;
1835
+ continue;
1836
+ }
1837
+ const nextHasPrevKey = nextChildrenKeySet.has(prevKey);
1838
+ const prevHasNextKey = prevChildrenKeySet.has(nextKey);
1839
+ if (!nextHasPrevKey) {
1840
+ // If removing the last node, insert remaining new nodes immediately, otherwise if the node
1841
+ // cannot be empty, it will remove itself from its parent.
1842
+ if (nextIndex === 0 && node.getChildrenSize() === 1) {
1843
+ node.splice(nextIndex, 1, nextChildren.slice(nextIndex));
1844
+ return;
1845
+ }
1846
+ // Remove
1847
+ node.splice(nextIndex, 1, []);
1848
+ prevIndex++;
1849
+ continue;
1850
+ }
1851
+
1852
+ // Create or replace
1853
+ const nextChildNode = nextChildren[nextIndex];
1854
+ if (prevHasNextKey) {
1855
+ node.splice(nextIndex, 1, [nextChildNode]);
1856
+ prevIndex++;
1857
+ nextIndex++;
1858
+ } else {
1859
+ node.splice(nextIndex, 0, [nextChildNode]);
1860
+ nextIndex++;
1861
+ }
1862
+ }
1863
+ const appendNewChildren = prevIndex > prevEndIndex;
1864
+ const removeOldChildren = nextIndex > nextEndIndex;
1865
+ if (appendNewChildren && !removeOldChildren) {
1866
+ node.append(...nextChildren.slice(nextIndex));
1867
+ } else if (removeOldChildren && !appendNewChildren) {
1868
+ node.splice(nextChildren.length, node.getChildrenSize() - nextChildren.length, []);
1869
+ }
1870
+ };
1871
+ const $createOrUpdateTextNodesFromYText = (text, binding, snapshot, prevSnapshot, computeYChange) => {
1872
+ const deltas = toDelta(text, snapshot, prevSnapshot, computeYChange);
1873
+
1874
+ // Use existing text nodes if the count and types all align, otherwise throw out the existing
1875
+ // nodes and create new ones.
1876
+ let nodes = binding.mapping.get(text) ?? [];
1877
+ const nodeTypes = deltas.map(delta => delta.attributes.t ?? lexical.TextNode.getType());
1878
+ const canReuseNodes = nodes.length === nodeTypes.length && nodes.every((node, i) => node.getType() === nodeTypes[i]);
1879
+ if (!canReuseNodes) {
1880
+ const registeredNodes = binding.editor._nodes;
1881
+ nodes = nodeTypes.map(type => {
1882
+ const nodeInfo = registeredNodes.get(type);
1883
+ if (nodeInfo === undefined) {
1884
+ throw new Error(`$createTextNodesFromYText: Node ${type} is not registered`);
1885
+ }
1886
+ const node = new nodeInfo.klass();
1887
+ if (!lexical.$isTextNode(node)) {
1888
+ throw new Error(`$createTextNodesFromYText: Node ${type} is not a TextNode`);
1889
+ }
1890
+ return node;
1891
+ });
1892
+ }
1893
+
1894
+ // Sync text, properties and state to the text nodes.
1895
+ for (let i = 0; i < deltas.length; i++) {
1896
+ const node = nodes[i];
1897
+ const delta = deltas[i];
1898
+ const {
1899
+ attributes,
1900
+ insert
1901
+ } = delta;
1902
+ if (node.__text !== insert) {
1903
+ node.setTextContent(insert);
1904
+ }
1905
+ const properties = {
1906
+ ...getDefaultNodeProperties(node, binding),
1907
+ ...attributes.p
1908
+ };
1909
+ const state = Object.fromEntries(Object.entries(attributes).filter(([k]) => k.startsWith(STATE_KEY_PREFIX)).map(([k, v]) => [attrKeyToStateKey(k), v]));
1910
+ $syncPropertiesFromYjs(binding, properties, node, null);
1911
+ lexical.$getWritableNodeState(node).updateFromJSON(state);
1912
+ }
1913
+ const latestNodes = nodes.map(node => node.getLatest());
1914
+ binding.mapping.set(text, latestNodes);
1915
+ return latestNodes;
1916
+ };
1917
+ const $createTypeFromTextNodes = (nodes, binding) => {
1918
+ const type = new yjs.XmlText();
1919
+ $updateYText(type, nodes, binding);
1920
+ return type;
1921
+ };
1922
+ const createTypeFromElementNode = (node, binding) => {
1923
+ const type = new yjs.XmlElement(node.getType());
1924
+ const attrs = {
1925
+ ...propertiesToAttributes(node, binding),
1926
+ ...stateToAttributes(node)
1927
+ };
1928
+ for (const key in attrs) {
1929
+ const val = attrs[key];
1930
+ if (val !== null) {
1931
+ // TODO(collab-v2): typing for XmlElement generic
1932
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1933
+ type.setAttribute(key, val);
1934
+ }
1935
+ }
1936
+ if (!(node instanceof lexical.ElementNode)) {
1937
+ return type;
1938
+ }
1939
+ type.insert(0, normalizeNodeContent(node).map(n => $createTypeFromTextOrElementNode(n, binding)));
1940
+ binding.mapping.set(type, node);
1941
+ return type;
1942
+ };
1943
+ const $createTypeFromTextOrElementNode = (node, meta) => node instanceof Array ? $createTypeFromTextNodes(node, meta) : createTypeFromElementNode(node, meta);
1944
+ const isObject = val => typeof val === 'object' && val != null;
1945
+ const equalAttrs = (pattrs, yattrs) => {
1946
+ const keys = Object.keys(pattrs).filter(key => pattrs[key] !== null);
1947
+ if (yattrs == null) {
1948
+ return keys.length === 0;
1949
+ }
1950
+ let eq = keys.length === Object.keys(yattrs).filter(key => yattrs[key] !== null).length;
1951
+ for (let i = 0; i < keys.length && eq; i++) {
1952
+ const key = keys[i];
1953
+ const l = pattrs[key];
1954
+ const r = yattrs[key];
1955
+ eq = key === 'ychange' || l === r || isObject(l) && isObject(r) && equalAttrs(l, r);
1956
+ }
1957
+ return eq;
1958
+ };
1959
+ const normalizeNodeContent = node => {
1960
+ if (!(node instanceof lexical.ElementNode)) {
1961
+ return [];
1962
+ }
1963
+ const c = node.getChildren();
1964
+ const res = [];
1965
+ for (let i = 0; i < c.length; i++) {
1966
+ const n = c[i];
1967
+ if (lexical.$isTextNode(n)) {
1968
+ const textNodes = [];
1969
+ for (let maybeTextNode = c[i]; i < c.length && lexical.$isTextNode(maybeTextNode); maybeTextNode = c[++i]) {
1970
+ textNodes.push(maybeTextNode);
1971
+ }
1972
+ i--;
1973
+ res.push(textNodes);
1974
+ } else {
1975
+ res.push(n);
1976
+ }
1977
+ }
1978
+ return res;
1979
+ };
1980
+ const equalYTextLText = (ytext, ltexts, binding) => {
1981
+ const deltas = toDelta(ytext);
1982
+ return deltas.length === ltexts.length && deltas.every((d, i) => {
1983
+ const ltext = ltexts[i];
1984
+ const type = d.attributes.t ?? lexical.TextNode.getType();
1985
+ const propertyAttrs = d.attributes.p ?? {};
1986
+ const stateAttrs = Object.fromEntries(Object.entries(d.attributes).filter(([k]) => k.startsWith(STATE_KEY_PREFIX)));
1987
+ return d.insert === ltext.getTextContent() && type === ltext.getType() && equalAttrs(propertyAttrs, propertiesToAttributes(ltext, binding)) && equalAttrs(stateAttrs, stateToAttributes(ltext));
1988
+ });
1989
+ };
1990
+ const equalYTypePNode = (ytype, lnode, binding) => {
1991
+ if (ytype instanceof yjs.XmlElement && !(lnode instanceof Array) && matchNodeName(ytype, lnode)) {
1992
+ const normalizedContent = normalizeNodeContent(lnode);
1993
+ return ytype._length === normalizedContent.length && equalAttrs(ytype.getAttributes(), {
1994
+ ...propertiesToAttributes(lnode, binding),
1995
+ ...stateToAttributes(lnode)
1996
+ }) && ytype.toArray().every((ychild, i) => equalYTypePNode(ychild, normalizedContent[i], binding));
1997
+ }
1998
+ return ytype instanceof yjs.XmlText && lnode instanceof Array && equalYTextLText(ytype, lnode, binding);
1999
+ };
2000
+ const mappedIdentity = (mapped, lcontent) => mapped === lcontent || mapped instanceof Array && lcontent instanceof Array && mapped.length === lcontent.length && mapped.every((a, i) => lcontent[i] === a);
2001
+ const computeChildEqualityFactor = (ytype, lnode, binding) => {
2002
+ const yChildren = ytype.toArray();
2003
+ const pChildren = normalizeNodeContent(lnode);
2004
+ const pChildCnt = pChildren.length;
2005
+ const yChildCnt = yChildren.length;
2006
+ const minCnt = Math.min(yChildCnt, pChildCnt);
2007
+ let left = 0;
2008
+ let right = 0;
2009
+ let foundMappedChild = false;
2010
+ for (; left < minCnt; left++) {
2011
+ const leftY = yChildren[left];
2012
+ const leftP = pChildren[left];
2013
+ if (leftY instanceof yjs.XmlHook) {
2014
+ break;
2015
+ } else if (mappedIdentity(binding.mapping.get(leftY), leftP)) {
2016
+ foundMappedChild = true; // definite (good) match!
2017
+ } else if (!equalYTypePNode(leftY, leftP, binding)) {
2018
+ break;
2019
+ }
2020
+ }
2021
+ for (; left + right < minCnt; right++) {
2022
+ const rightY = yChildren[yChildCnt - right - 1];
2023
+ const rightP = pChildren[pChildCnt - right - 1];
2024
+ if (rightY instanceof yjs.XmlHook) {
2025
+ break;
2026
+ } else if (mappedIdentity(binding.mapping.get(rightY), rightP)) {
2027
+ foundMappedChild = true;
2028
+ } else if (!equalYTypePNode(rightY, rightP, binding)) {
2029
+ break;
2030
+ }
2031
+ }
2032
+ return {
2033
+ equalityFactor: left + right,
2034
+ foundMappedChild
2035
+ };
2036
+ };
2037
+ const ytextTrans = ytext => {
2038
+ let str = '';
2039
+ let n = ytext._start;
2040
+ const nAttrs = {};
2041
+ while (n !== null) {
2042
+ if (!n.deleted) {
2043
+ if (n.countable && n.content instanceof yjs.ContentString) {
2044
+ str += n.content.str;
2045
+ } else if (n.content instanceof yjs.ContentFormat) {
2046
+ nAttrs[n.content.key] = null;
2047
+ }
2048
+ }
2049
+ n = n.right;
2050
+ }
2051
+ return {
2052
+ nAttrs,
2053
+ str
2054
+ };
2055
+ };
2056
+ const $updateYText = (ytext, ltexts, binding) => {
2057
+ binding.mapping.set(ytext, ltexts);
2058
+ const {
2059
+ nAttrs,
2060
+ str
2061
+ } = ytextTrans(ytext);
2062
+ const content = ltexts.map((node, i) => {
2063
+ const nodeType = node.getType();
2064
+ let p = propertiesToAttributes(node, binding);
2065
+ if (Object.keys(p).length === 0) {
2066
+ p = null;
2067
+ }
2068
+ return {
2069
+ attributes: Object.assign({}, nAttrs, {
2070
+ ...(nodeType !== lexical.TextNode.getType() && {
2071
+ t: nodeType
2072
+ }),
2073
+ p,
2074
+ ...stateToAttributes(node),
2075
+ ...(i > 0 && {
2076
+ i
2077
+ }) // Prevent Yjs from merging text nodes itself.
2078
+ }),
2079
+ insert: node.getTextContent(),
2080
+ nodeKey: node.getKey()
2081
+ };
2082
+ });
2083
+ const nextText = content.map(c => c.insert).join('');
2084
+ const selection = lexical.$getSelection();
2085
+ let cursorOffset;
2086
+ if (lexical.$isRangeSelection(selection) && selection.isCollapsed()) {
2087
+ cursorOffset = 0;
2088
+ for (const c of content) {
2089
+ if (c.nodeKey === selection.anchor.key) {
2090
+ cursorOffset += selection.anchor.offset;
2091
+ break;
2092
+ }
2093
+ cursorOffset += c.insert.length;
2094
+ }
2095
+ } else {
2096
+ cursorOffset = nextText.length;
2097
+ }
2098
+ const {
2099
+ insert,
2100
+ remove,
2101
+ index
2102
+ } = simpleDiffWithCursor(str, nextText, cursorOffset);
2103
+ ytext.delete(index, remove);
2104
+ ytext.insert(index, insert);
2105
+ ytext.applyDelta(content.map(c => ({
2106
+ attributes: c.attributes,
2107
+ retain: c.insert.length
2108
+ })));
2109
+ };
2110
+ const toDelta = (ytext, snapshot, prevSnapshot, computeYChange) => {
2111
+ return ytext.toDelta(snapshot, prevSnapshot, computeYChange).map(delta => ({
2112
+ ...delta,
2113
+ attributes: delta.attributes ?? {}
2114
+ }));
2115
+ };
2116
+ const propertiesToAttributes = (node, meta) => {
2117
+ const defaultProperties = getDefaultNodeProperties(node, meta);
2118
+ const attrs = {};
2119
+ Object.entries(defaultProperties).forEach(([property, defaultValue]) => {
2120
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2121
+ const value = node[property];
2122
+ if (value !== defaultValue) {
2123
+ attrs[property] = value;
2124
+ }
2125
+ });
2126
+ return attrs;
2127
+ };
2128
+ const STATE_KEY_PREFIX = 's_';
2129
+ const stateKeyToAttrKey = key => STATE_KEY_PREFIX + key;
2130
+ const attrKeyToStateKey = key => {
2131
+ if (!key.startsWith(STATE_KEY_PREFIX)) {
2132
+ throw new Error(`Invalid state key: ${key}`);
2133
+ }
2134
+ return key.slice(STATE_KEY_PREFIX.length);
2135
+ };
2136
+ const stateToAttributes = node => {
2137
+ const state = node.__state;
2138
+ if (!state) {
2139
+ return {};
2140
+ }
2141
+ const [unknown = {}, known] = state.getInternalState();
2142
+ const attrs = {};
2143
+ for (const [k, v] of Object.entries(unknown)) {
2144
+ attrs[stateKeyToAttrKey(k)] = v;
2145
+ }
2146
+ for (const [stateConfig, v] of known) {
2147
+ attrs[stateKeyToAttrKey(stateConfig.key)] = stateConfig.unparse(v);
2148
+ }
2149
+ return attrs;
2150
+ };
2151
+ const $updateYFragment = (y, yDomFragment, node, binding, dirtyElements) => {
2152
+ if (yDomFragment instanceof yjs.XmlElement && yDomFragment.nodeName !== node.getType() && !(isRootElement(yDomFragment) && node.getType() === lexical.RootNode.getType())) {
2153
+ throw new Error('node name mismatch!');
2154
+ }
2155
+ binding.mapping.set(yDomFragment, node);
2156
+ // update attributes
2157
+ if (yDomFragment instanceof yjs.XmlElement) {
2158
+ const yDomAttrs = yDomFragment.getAttributes();
2159
+ const lexicalAttrs = {
2160
+ ...propertiesToAttributes(node, binding),
2161
+ ...stateToAttributes(node)
2162
+ };
2163
+ for (const key in lexicalAttrs) {
2164
+ if (lexicalAttrs[key] != null) {
2165
+ if (yDomAttrs[key] !== lexicalAttrs[key] && key !== 'ychange') {
2166
+ // TODO(collab-v2): typing for XmlElement generic
2167
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2168
+ yDomFragment.setAttribute(key, lexicalAttrs[key]);
2169
+ }
2170
+ } else {
2171
+ yDomFragment.removeAttribute(key);
2172
+ }
2173
+ }
2174
+ // remove all keys that are no longer in pAttrs
2175
+ for (const key in yDomAttrs) {
2176
+ if (lexicalAttrs[key] === undefined) {
2177
+ yDomFragment.removeAttribute(key);
2178
+ }
2179
+ }
2180
+ }
2181
+ // update children
2182
+ const lChildren = normalizeNodeContent(node);
2183
+ const lChildCnt = lChildren.length;
2184
+ const yChildren = yDomFragment.toArray();
2185
+ const yChildCnt = yChildren.length;
2186
+ const minCnt = Math.min(lChildCnt, yChildCnt);
2187
+ let left = 0;
2188
+ let right = 0;
2189
+ // find number of matching elements from left
2190
+ for (; left < minCnt; left++) {
2191
+ const leftY = yChildren[left];
2192
+ const leftL = lChildren[left];
2193
+ if (leftY instanceof yjs.XmlHook) {
2194
+ break;
2195
+ } else if (mappedIdentity(binding.mapping.get(leftY), leftL)) {
2196
+ if (leftL instanceof lexical.ElementNode && dirtyElements.has(leftL.getKey())) {
2197
+ $updateYFragment(y, leftY, leftL, binding, dirtyElements);
2198
+ }
2199
+ } else if (equalYTypePNode(leftY, leftL, binding)) {
2200
+ // update mapping
2201
+ binding.mapping.set(leftY, leftL);
2202
+ } else {
2203
+ break;
2204
+ }
2205
+ }
2206
+ // find number of matching elements from right
2207
+ for (; right + left < minCnt; right++) {
2208
+ const rightY = yChildren[yChildCnt - right - 1];
2209
+ const rightL = lChildren[lChildCnt - right - 1];
2210
+ if (rightY instanceof yjs.XmlHook) {
2211
+ break;
2212
+ } else if (mappedIdentity(binding.mapping.get(rightY), rightL)) {
2213
+ if (rightL instanceof lexical.ElementNode && dirtyElements.has(rightL.getKey())) {
2214
+ $updateYFragment(y, rightY, rightL, binding, dirtyElements);
2215
+ }
2216
+ } else if (equalYTypePNode(rightY, rightL, binding)) {
2217
+ // update mapping
2218
+ binding.mapping.set(rightY, rightL);
2219
+ } else {
2220
+ break;
2221
+ }
2222
+ }
2223
+ // try to compare and update
2224
+ while (yChildCnt - left - right > 0 && lChildCnt - left - right > 0) {
2225
+ const leftY = yChildren[left];
2226
+ const leftL = lChildren[left];
2227
+ const rightY = yChildren[yChildCnt - right - 1];
2228
+ const rightL = lChildren[lChildCnt - right - 1];
2229
+ if (leftY instanceof yjs.XmlText && leftL instanceof Array) {
2230
+ if (!equalYTextLText(leftY, leftL, binding)) {
2231
+ $updateYText(leftY, leftL, binding);
2232
+ }
2233
+ left += 1;
2234
+ } else {
2235
+ let updateLeft = leftY instanceof yjs.XmlElement && matchNodeName(leftY, leftL);
2236
+ let updateRight = rightY instanceof yjs.XmlElement && matchNodeName(rightY, rightL);
2237
+ if (updateLeft && updateRight) {
2238
+ // decide which which element to update
2239
+ const equalityLeft = computeChildEqualityFactor(leftY, leftL, binding);
2240
+ const equalityRight = computeChildEqualityFactor(rightY, rightL, binding);
2241
+ if (equalityLeft.foundMappedChild && !equalityRight.foundMappedChild) {
2242
+ updateRight = false;
2243
+ } else if (!equalityLeft.foundMappedChild && equalityRight.foundMappedChild) {
2244
+ updateLeft = false;
2245
+ } else if (equalityLeft.equalityFactor < equalityRight.equalityFactor) {
2246
+ updateLeft = false;
2247
+ } else {
2248
+ updateRight = false;
2249
+ }
2250
+ }
2251
+ if (updateLeft) {
2252
+ $updateYFragment(y, leftY, leftL, binding, dirtyElements);
2253
+ left += 1;
2254
+ } else if (updateRight) {
2255
+ $updateYFragment(y, rightY, rightL, binding, dirtyElements);
2256
+ right += 1;
2257
+ } else {
2258
+ binding.mapping.delete(yDomFragment.get(left));
2259
+ yDomFragment.delete(left, 1);
2260
+ yDomFragment.insert(left, [$createTypeFromTextOrElementNode(leftL, binding)]);
2261
+ left += 1;
2262
+ }
2263
+ }
2264
+ }
2265
+ const yDelLen = yChildCnt - left - right;
2266
+ if (yChildCnt === 1 && lChildCnt === 0 && yChildren[0] instanceof yjs.XmlText) {
2267
+ binding.mapping.delete(yChildren[0]);
2268
+ // Edge case handling https://github.com/yjs/y-prosemirror/issues/108
2269
+ // Only delete the content of the Y.Text to retain remote changes on the same Y.Text object
2270
+ yChildren[0].delete(0, yChildren[0].length);
2271
+ } else if (yDelLen > 0) {
2272
+ yDomFragment.slice(left, left + yDelLen).forEach(type => binding.mapping.delete(type));
2273
+ yDomFragment.delete(left, yDelLen);
2274
+ }
2275
+ if (left + right < lChildCnt) {
2276
+ const ins = [];
2277
+ for (let i = left; i < lChildCnt - right; i++) {
2278
+ ins.push($createTypeFromTextOrElementNode(lChildren[i], binding));
2279
+ }
2280
+ yDomFragment.insert(left, ins);
2281
+ }
2282
+ };
2283
+ const matchNodeName = (yElement, lnode) => !(lnode instanceof Array) && yElement.nodeName === lnode.getType();
2284
+
1450
2285
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
1451
2286
  function $syncStateEvent(binding, event) {
1452
2287
  const {
@@ -1533,23 +2368,7 @@ function syncYjsChangesToLexical(binding, provider, events, isFromUndoManger, sy
1533
2368
  const event = events[i];
1534
2369
  $syncEvent(binding, event);
1535
2370
  }
1536
- const selection = lexical.$getSelection();
1537
- if (lexical.$isRangeSelection(selection)) {
1538
- if (doesSelectionNeedRecovering(selection)) {
1539
- const prevSelection = currentEditorState._selection;
1540
- if (lexical.$isRangeSelection(prevSelection)) {
1541
- $syncLocalCursorPosition(binding, provider);
1542
- if (doesSelectionNeedRecovering(selection)) {
1543
- // If the selected node is deleted, move the selection to the previous or parent node.
1544
- const anchorNodeKey = selection.anchor.key;
1545
- $moveSelectionToPreviousNode(anchorNodeKey, currentEditorState);
1546
- }
1547
- }
1548
- syncLexicalSelectionToYjs(binding, provider, prevSelection, lexical.$getSelection());
1549
- } else {
1550
- $syncLocalCursorPosition(binding, provider);
1551
- }
1552
- }
2371
+ $syncCursorFromYjs(currentEditorState, binding, provider);
1553
2372
  if (!isFromUndoManger) {
1554
2373
  // If it is an external change, we don't want the current scroll position to get changed
1555
2374
  // since the user might've intentionally scrolled somewhere else in the document.
@@ -1558,19 +2377,31 @@ function syncYjsChangesToLexical(binding, provider, events, isFromUndoManger, sy
1558
2377
  }, {
1559
2378
  onUpdate: () => {
1560
2379
  syncCursorPositionsFn(binding, provider);
1561
- // If there was a collision on the top level paragraph
1562
- // we need to re-add a paragraph. To ensure this insertion properly syncs with other clients,
1563
- // it must be placed outside of the update block above that has tags 'collaboration' or 'historic'.
1564
- editor.update(() => {
1565
- if (lexical.$getRoot().getChildrenSize() === 0) {
1566
- lexical.$getRoot().append(lexical.$createParagraphNode());
1567
- }
1568
- });
2380
+ editor.update(() => $ensureEditorNotEmpty());
1569
2381
  },
1570
2382
  skipTransforms: true,
1571
2383
  tag: isFromUndoManger ? lexical.HISTORIC_TAG : lexical.COLLABORATION_TAG
1572
2384
  });
1573
2385
  }
2386
+ function $syncCursorFromYjs(editorState, binding, provider) {
2387
+ const selection = lexical.$getSelection();
2388
+ if (lexical.$isRangeSelection(selection)) {
2389
+ if (doesSelectionNeedRecovering(selection)) {
2390
+ const prevSelection = editorState._selection;
2391
+ if (lexical.$isRangeSelection(prevSelection)) {
2392
+ $syncLocalCursorPosition(binding, provider);
2393
+ if (doesSelectionNeedRecovering(selection)) {
2394
+ // If the selected node is deleted, move the selection to the previous or parent node.
2395
+ const anchorNodeKey = selection.anchor.key;
2396
+ $moveSelectionToPreviousNode(anchorNodeKey, editorState);
2397
+ }
2398
+ }
2399
+ syncLexicalSelectionToYjs(binding, provider, prevSelection, lexical.$getSelection());
2400
+ } else {
2401
+ $syncLocalCursorPosition(binding, provider);
2402
+ }
2403
+ }
2404
+ }
1574
2405
  function $handleNormalizationMergeConflicts(binding, normalizedNodes) {
1575
2406
  // We handle the merge operations here
1576
2407
  const normalizedNodesKeys = Array.from(normalizedNodes);
@@ -1611,6 +2442,15 @@ function $handleNormalizationMergeConflicts(binding, normalizedNodes) {
1611
2442
  collabNode._text = text;
1612
2443
  }
1613
2444
  }
2445
+
2446
+ // If there was a collision on the top level paragraph
2447
+ // we need to re-add a paragraph. To ensure this insertion properly syncs with other clients,
2448
+ // it must be placed outside of the update block above that has tags 'collaboration' or 'historic'.
2449
+ function $ensureEditorNotEmpty() {
2450
+ if (lexical.$getRoot().getChildrenSize() === 0) {
2451
+ lexical.$getRoot().append(lexical.$createParagraphNode());
2452
+ }
2453
+ }
1614
2454
  function syncLexicalUpdateToYjs(binding, provider, prevEditorState, currEditorState, dirtyElements, dirtyLeaves, normalizedNodes, tags) {
1615
2455
  syncWithTransaction(binding, () => {
1616
2456
  currEditorState.read(() => {
@@ -1640,6 +2480,96 @@ function syncLexicalUpdateToYjs(binding, provider, prevEditorState, currEditorSt
1640
2480
  });
1641
2481
  });
1642
2482
  }
2483
+ function $syncEventV2(binding, event) {
2484
+ const {
2485
+ target
2486
+ } = event;
2487
+ if (target instanceof yjs.XmlElement && event instanceof yjs.YXmlEvent) {
2488
+ $createOrUpdateNodeFromYElement(target, binding, event.attributesChanged,
2489
+ // @ts-expect-error childListChanged is private
2490
+ event.childListChanged);
2491
+ } else if (target instanceof yjs.XmlText && event instanceof yjs.YTextEvent) {
2492
+ const parent = target.parent;
2493
+ if (parent instanceof yjs.XmlElement) {
2494
+ // Need to sync via parent element in order to attach new next nodes.
2495
+ $createOrUpdateNodeFromYElement(parent, binding, new Set(), true);
2496
+ } else {
2497
+ {
2498
+ formatDevErrorMessage(`Expected XmlElement parent for XmlText`);
2499
+ }
2500
+ }
2501
+ } else {
2502
+ {
2503
+ formatDevErrorMessage(`Expected xml or text event`);
2504
+ }
2505
+ }
2506
+ }
2507
+ function syncYjsChangesToLexicalV2__EXPERIMENTAL(binding, provider, events, transaction, isFromUndoManger) {
2508
+ const editor = binding.editor;
2509
+ const editorState = editor._editorState;
2510
+
2511
+ // Remove deleted nodes from the mapping
2512
+ yjs.iterateDeletedStructs(transaction, transaction.deleteSet, struct => {
2513
+ if (struct.constructor === yjs.Item) {
2514
+ const content = struct.content;
2515
+ const type = content.type;
2516
+ if (type) {
2517
+ binding.mapping.delete(type);
2518
+ }
2519
+ }
2520
+ });
2521
+
2522
+ // This line precompute the delta before editor update. The reason is
2523
+ // delta is computed when it is accessed. Note that this can only be
2524
+ // safely computed during the event call. If it is accessed after event
2525
+ // call it might result in unexpected behavior.
2526
+ // https://github.com/yjs/yjs/blob/00ef472d68545cb260abd35c2de4b3b78719c9e4/src/utils/YEvent.js#L132
2527
+ events.forEach(event => event.delta);
2528
+ editor.update(() => {
2529
+ for (let i = 0; i < events.length; i++) {
2530
+ const event = events[i];
2531
+ $syncEventV2(binding, event);
2532
+ }
2533
+ $syncCursorFromYjs(editorState, binding, provider);
2534
+ if (!isFromUndoManger) {
2535
+ // If it is an external change, we don't want the current scroll position to get changed
2536
+ // since the user might've intentionally scrolled somewhere else in the document.
2537
+ lexical.$addUpdateTag(lexical.SKIP_SCROLL_INTO_VIEW_TAG);
2538
+ }
2539
+ }, {
2540
+ // Need any text node normalization to be synchronously updated back to Yjs, otherwise the
2541
+ // binding.mapping will get out of sync.
2542
+ discrete: true,
2543
+ onUpdate: () => {
2544
+ syncCursorPositions(binding, provider);
2545
+ editor.update(() => $ensureEditorNotEmpty());
2546
+ },
2547
+ skipTransforms: true,
2548
+ tag: isFromUndoManger ? lexical.HISTORIC_TAG : lexical.COLLABORATION_TAG
2549
+ });
2550
+ }
2551
+ function syncLexicalUpdateToYjsV2__EXPERIMENTAL(binding, provider, prevEditorState, currEditorState, dirtyElements, normalizedNodes, tags) {
2552
+ const isFromYjs = tags.has(lexical.COLLABORATION_TAG) || tags.has(lexical.HISTORIC_TAG);
2553
+ if (isFromYjs && normalizedNodes.size === 0) {
2554
+ return;
2555
+ }
2556
+
2557
+ // Nodes are normalized synchronously (`discrete: true` above), so the mapping may now be
2558
+ // incorrect for these nodes, as they point to `getLatest` which is mutable within an update.
2559
+ normalizedNodes.forEach(nodeKey => {
2560
+ binding.mapping.deleteNode(nodeKey);
2561
+ });
2562
+ syncWithTransaction(binding, () => {
2563
+ currEditorState.read(() => {
2564
+ if (dirtyElements.has('root')) {
2565
+ $updateYFragment(binding.doc, binding.root, lexical.$getRoot(), binding, new Set(dirtyElements.keys()));
2566
+ }
2567
+ const selection = lexical.$getSelection();
2568
+ const prevSelection = prevEditorState._selection;
2569
+ syncLexicalSelectionToYjs(binding, provider, prevSelection, selection);
2570
+ });
2571
+ });
2572
+ }
1643
2573
 
1644
2574
  /**
1645
2575
  * Copyright (c) Meta Platforms, Inc. and affiliates.
@@ -1688,10 +2618,13 @@ function setLocalStateFocus(provider, name, color, focusing, awarenessData) {
1688
2618
  exports.CONNECTED_COMMAND = CONNECTED_COMMAND;
1689
2619
  exports.TOGGLE_CONNECT_COMMAND = TOGGLE_CONNECT_COMMAND;
1690
2620
  exports.createBinding = createBinding;
2621
+ exports.createBindingV2__EXPERIMENTAL = createBindingV2__EXPERIMENTAL;
1691
2622
  exports.createUndoManager = createUndoManager;
1692
2623
  exports.getAnchorAndFocusCollabNodesForUserState = getAnchorAndFocusCollabNodesForUserState;
1693
2624
  exports.initLocalState = initLocalState;
1694
2625
  exports.setLocalStateFocus = setLocalStateFocus;
1695
2626
  exports.syncCursorPositions = syncCursorPositions;
1696
2627
  exports.syncLexicalUpdateToYjs = syncLexicalUpdateToYjs;
2628
+ exports.syncLexicalUpdateToYjsV2__EXPERIMENTAL = syncLexicalUpdateToYjsV2__EXPERIMENTAL;
1697
2629
  exports.syncYjsChangesToLexical = syncYjsChangesToLexical;
2630
+ exports.syncYjsChangesToLexicalV2__EXPERIMENTAL = syncYjsChangesToLexicalV2__EXPERIMENTAL;