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