@lexical/yjs 0.1.6 → 0.1.10
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 +2487 -0
- package/LexicalYjs.js +9 -0
- package/LexicalYjs.prod.js +67 -0
- package/package.json +2 -2
- package/index.js +0 -59
|
@@ -0,0 +1,2487 @@
|
|
|
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
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
var yjs = require('yjs');
|
|
10
|
+
var lexical = require('lexical');
|
|
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
|
+
*/
|
|
20
|
+
class CollabLineBreakNode {
|
|
21
|
+
constructor(map, parent) {
|
|
22
|
+
this._key = '';
|
|
23
|
+
this._map = map;
|
|
24
|
+
this._parent = parent;
|
|
25
|
+
this._type = 'linebreak';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
getNode() {
|
|
29
|
+
const node = lexical.$getNodeByKey(this._key);
|
|
30
|
+
return lexical.$isLineBreakNode(node) ? node : null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
getKey() {
|
|
34
|
+
return this._key;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
getSharedType() {
|
|
38
|
+
return this._map;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
getType() {
|
|
42
|
+
return this._type;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
getSize() {
|
|
46
|
+
return 1;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
getOffset() {
|
|
50
|
+
const collabElementNode = this._parent;
|
|
51
|
+
return collabElementNode.getChildOffset(this);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
destroy(binding) {
|
|
55
|
+
const collabNodeMap = binding.collabNodeMap;
|
|
56
|
+
collabNodeMap.delete(this._key);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
}
|
|
60
|
+
function $createCollabLineBreakNode(map, parent) {
|
|
61
|
+
const collabNode = new CollabLineBreakNode(map, parent); // $FlowFixMe: internal field
|
|
62
|
+
|
|
63
|
+
map._collabNode = collabNode;
|
|
64
|
+
return collabNode;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
69
|
+
*
|
|
70
|
+
* This source code is licensed under the MIT license found in the
|
|
71
|
+
* LICENSE file in the root directory of this source tree.
|
|
72
|
+
*
|
|
73
|
+
*
|
|
74
|
+
*/
|
|
75
|
+
|
|
76
|
+
function simpleDiffWithCursor(a, b, cursor) {
|
|
77
|
+
const aLength = a.length;
|
|
78
|
+
const bLength = b.length;
|
|
79
|
+
let left = 0; // number of same characters counting from left
|
|
80
|
+
|
|
81
|
+
let right = 0; // number of same characters counting from right
|
|
82
|
+
// Iterate left to the right until we find a changed character
|
|
83
|
+
// First iteration considers the current cursor position
|
|
84
|
+
|
|
85
|
+
while (left < aLength && left < bLength && a[left] === b[left] && left < cursor) {
|
|
86
|
+
left++;
|
|
87
|
+
} // Iterate right to the left until we find a changed character
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
while (right + left < aLength && right + left < bLength && a[aLength - right - 1] === b[bLength - right - 1]) {
|
|
91
|
+
right++;
|
|
92
|
+
} // Try to iterate left further to the right without caring about the current cursor position
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
while (right + left < aLength && right + left < bLength && a[left] === b[left]) {
|
|
96
|
+
left++;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
index: left,
|
|
101
|
+
insert: b.slice(left, bLength - right),
|
|
102
|
+
remove: aLength - left - right
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function diffTextContentAndApplyDelta(collabNode, key, prevText, nextText) {
|
|
107
|
+
const selection = lexical.$getSelection();
|
|
108
|
+
let cursorOffset = nextText.length;
|
|
109
|
+
|
|
110
|
+
if (lexical.$isRangeSelection(selection) && selection.isCollapsed()) {
|
|
111
|
+
const anchor = selection.anchor;
|
|
112
|
+
|
|
113
|
+
if (anchor.key === key) {
|
|
114
|
+
cursorOffset = anchor.offset;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const diff = simpleDiffWithCursor(prevText, nextText, cursorOffset);
|
|
119
|
+
collabNode.spliceText(diff.index, diff.remove, diff.insert);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
class CollabTextNode {
|
|
123
|
+
constructor(map, text, parent, type) {
|
|
124
|
+
this._key = '';
|
|
125
|
+
this._map = map;
|
|
126
|
+
this._parent = parent;
|
|
127
|
+
this._text = text;
|
|
128
|
+
this._type = type;
|
|
129
|
+
this._normalized = false;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
getPrevNode(nodeMap) {
|
|
133
|
+
if (nodeMap === null) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const node = nodeMap.get(this._key);
|
|
138
|
+
return lexical.$isTextNode(node) ? node : null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
getNode() {
|
|
142
|
+
const node = lexical.$getNodeByKey(this._key);
|
|
143
|
+
return lexical.$isTextNode(node) ? node : null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
getSharedType() {
|
|
147
|
+
return this._map;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
getType() {
|
|
151
|
+
return this._type;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
getKey() {
|
|
155
|
+
return this._key;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
getSize() {
|
|
159
|
+
return this._text.length + (this._normalized ? 0 : 1);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
getOffset() {
|
|
163
|
+
const collabElementNode = this._parent;
|
|
164
|
+
return collabElementNode.getChildOffset(this);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
spliceText(index, delCount, newText) {
|
|
168
|
+
const collabElementNode = this._parent;
|
|
169
|
+
const xmlText = collabElementNode._xmlText;
|
|
170
|
+
const offset = this.getOffset() + 1 + index;
|
|
171
|
+
|
|
172
|
+
if (delCount !== 0) {
|
|
173
|
+
xmlText.delete(offset, delCount);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (newText !== '') {
|
|
177
|
+
xmlText.insert(offset, newText);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
syncPropertiesAndTextFromLexical(binding, nextLexicalNode, prevNodeMap) {
|
|
182
|
+
const prevLexicalNode = this.getPrevNode(prevNodeMap);
|
|
183
|
+
const nextText = nextLexicalNode.__text;
|
|
184
|
+
syncPropertiesFromLexical(binding, this._map, prevLexicalNode, nextLexicalNode);
|
|
185
|
+
|
|
186
|
+
if (prevLexicalNode !== null) {
|
|
187
|
+
const prevText = prevLexicalNode.__text;
|
|
188
|
+
|
|
189
|
+
if (prevText !== nextText) {
|
|
190
|
+
const key = nextLexicalNode.__key;
|
|
191
|
+
diffTextContentAndApplyDelta(this, key, prevText, nextText);
|
|
192
|
+
this._text = nextText;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
syncPropertiesAndTextFromYjs(binding, keysChanged) {
|
|
198
|
+
const lexicalNode = this.getNode();
|
|
199
|
+
|
|
200
|
+
if (lexicalNode === null) {
|
|
201
|
+
throw new Error('Should never happen');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
syncPropertiesFromYjs(binding, this._map, lexicalNode, keysChanged);
|
|
205
|
+
const collabText = this._text;
|
|
206
|
+
|
|
207
|
+
if (lexicalNode.__text !== collabText) {
|
|
208
|
+
const writable = lexicalNode.getWritable();
|
|
209
|
+
writable.__text = collabText;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
destroy(binding) {
|
|
214
|
+
const collabNodeMap = binding.collabNodeMap;
|
|
215
|
+
collabNodeMap.delete(this._key);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
}
|
|
219
|
+
function $createCollabTextNode(map, text, parent, type) {
|
|
220
|
+
const collabNode = new CollabTextNode(map, text, parent, type); // $FlowFixMe: internal field
|
|
221
|
+
|
|
222
|
+
map._collabNode = collabNode;
|
|
223
|
+
return collabNode;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
228
|
+
*
|
|
229
|
+
* This source code is licensed under the MIT license found in the
|
|
230
|
+
* LICENSE file in the root directory of this source tree.
|
|
231
|
+
*
|
|
232
|
+
*
|
|
233
|
+
*/
|
|
234
|
+
const excludedProperties = new Set(['__key', '__children', '__parent', '__cachedText', '__text', '__state']);
|
|
235
|
+
function $getNodeByKeyOrThrow(key) {
|
|
236
|
+
const node = lexical.$getNodeByKey(key);
|
|
237
|
+
|
|
238
|
+
if (node === null) {
|
|
239
|
+
throw new Error('Should never happen');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return node;
|
|
243
|
+
}
|
|
244
|
+
function $createCollabNodeFromLexicalNode(binding, lexicalNode, parent) {
|
|
245
|
+
const nodeType = lexicalNode.__type;
|
|
246
|
+
let collabNode;
|
|
247
|
+
|
|
248
|
+
if (lexical.$isElementNode(lexicalNode)) {
|
|
249
|
+
const xmlText = new yjs.XmlText();
|
|
250
|
+
collabNode = $createCollabElementNode(xmlText, parent, nodeType);
|
|
251
|
+
collabNode.syncPropertiesFromLexical(binding, lexicalNode, null);
|
|
252
|
+
collabNode.syncChildrenFromLexical(binding, lexicalNode, null, null, null);
|
|
253
|
+
} else if (lexical.$isTextNode(lexicalNode)) {
|
|
254
|
+
// TODO create a token text node for immutable, segmented or inert nodes.
|
|
255
|
+
const map = new yjs.Map();
|
|
256
|
+
collabNode = $createCollabTextNode(map, lexicalNode.__text, parent, nodeType);
|
|
257
|
+
collabNode.syncPropertiesAndTextFromLexical(binding, lexicalNode, null);
|
|
258
|
+
} else if (lexical.$isLineBreakNode(lexicalNode)) {
|
|
259
|
+
const map = new yjs.Map();
|
|
260
|
+
map.set('__type', 'linebreak');
|
|
261
|
+
collabNode = $createCollabLineBreakNode(map, parent);
|
|
262
|
+
} else if (lexical.$isDecoratorNode(lexicalNode)) {
|
|
263
|
+
const xmlElem = new yjs.XmlElement();
|
|
264
|
+
collabNode = $createCollabDecoratorNode(xmlElem, parent, nodeType);
|
|
265
|
+
collabNode.syncPropertiesFromLexical(binding, lexicalNode, null);
|
|
266
|
+
} else {
|
|
267
|
+
throw new Error('Should never happen');
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
collabNode._key = lexicalNode.__key;
|
|
271
|
+
return collabNode;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function getNodeTypeFromSharedType(sharedType) {
|
|
275
|
+
const type = sharedType instanceof yjs.Map ? sharedType.get('__type') : sharedType.getAttribute('__type');
|
|
276
|
+
|
|
277
|
+
if (type == null) {
|
|
278
|
+
throw new Error('Should never happen');
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return type;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function getOrInitCollabNodeFromSharedType(binding, sharedType, parent) {
|
|
285
|
+
// $FlowFixMe: internal field
|
|
286
|
+
const collabNode = sharedType._collabNode;
|
|
287
|
+
|
|
288
|
+
if (collabNode === undefined) {
|
|
289
|
+
const registeredNodes = binding.editor._nodes;
|
|
290
|
+
const type = getNodeTypeFromSharedType(sharedType);
|
|
291
|
+
const nodeInfo = registeredNodes.get(type);
|
|
292
|
+
|
|
293
|
+
if (nodeInfo === undefined) {
|
|
294
|
+
throw new Error('Should never happen');
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const sharedParent = sharedType.parent;
|
|
298
|
+
const targetParent = parent === undefined && sharedParent !== null ? getOrInitCollabNodeFromSharedType(binding, sharedParent) : parent || null;
|
|
299
|
+
|
|
300
|
+
if (!(targetParent instanceof CollabElementNode)) {
|
|
301
|
+
throw new Error('Should never happen');
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (sharedType instanceof yjs.XmlText) {
|
|
305
|
+
return $createCollabElementNode(sharedType, targetParent, type);
|
|
306
|
+
} else if (sharedType instanceof yjs.Map) {
|
|
307
|
+
if (targetParent === null) {
|
|
308
|
+
throw new Error('Should never happen');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (type === 'linebreak') {
|
|
312
|
+
return $createCollabLineBreakNode(sharedType, targetParent);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return $createCollabTextNode(sharedType, '', targetParent, type);
|
|
316
|
+
} else if (sharedType instanceof yjs.XmlElement) {
|
|
317
|
+
return $createCollabDecoratorNode(sharedType, targetParent, type);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return collabNode;
|
|
322
|
+
}
|
|
323
|
+
function createLexicalNodeFromCollabNode(binding, collabNode, parentKey) {
|
|
324
|
+
const type = collabNode.getType();
|
|
325
|
+
const registeredNodes = binding.editor._nodes;
|
|
326
|
+
const nodeInfo = registeredNodes.get(type);
|
|
327
|
+
|
|
328
|
+
if (nodeInfo === undefined) {
|
|
329
|
+
throw new Error('createLexicalNode failed');
|
|
330
|
+
} // $FlowFixMe: needs refining
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
const lexicalNode = new nodeInfo.klass();
|
|
334
|
+
lexicalNode.__parent = parentKey;
|
|
335
|
+
collabNode._key = lexicalNode.__key;
|
|
336
|
+
|
|
337
|
+
if (collabNode instanceof CollabElementNode) {
|
|
338
|
+
const xmlText = collabNode._xmlText;
|
|
339
|
+
collabNode.syncPropertiesFromYjs(binding, null);
|
|
340
|
+
collabNode.applyChildrenYjsDelta(binding, xmlText.toDelta());
|
|
341
|
+
collabNode.syncChildrenFromYjs(binding);
|
|
342
|
+
} else if (collabNode instanceof CollabTextNode) {
|
|
343
|
+
collabNode.syncPropertiesAndTextFromYjs(binding, null);
|
|
344
|
+
} else if (collabNode instanceof CollabDecoratorNode) {
|
|
345
|
+
collabNode.syncPropertiesFromYjs(binding, null);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
binding.collabNodeMap.set(lexicalNode.__key, collabNode);
|
|
349
|
+
return lexicalNode;
|
|
350
|
+
}
|
|
351
|
+
function syncPropertiesFromYjs(binding, sharedType, lexicalNode, keysChanged) {
|
|
352
|
+
const properties = keysChanged === null ? sharedType instanceof yjs.Map ? Array.from(sharedType.keys()) : Object.keys(sharedType.getAttributes()) : Array.from(keysChanged);
|
|
353
|
+
let writableNode;
|
|
354
|
+
|
|
355
|
+
for (let i = 0; i < properties.length; i++) {
|
|
356
|
+
const property = properties[i];
|
|
357
|
+
|
|
358
|
+
if (excludedProperties.has(property)) {
|
|
359
|
+
continue;
|
|
360
|
+
} // $FlowFixMe: intentional
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
const prevValue = lexicalNode[property];
|
|
364
|
+
const nextValue = sharedType instanceof yjs.Map ? sharedType.get(property) : sharedType.getAttribute(property);
|
|
365
|
+
|
|
366
|
+
if (prevValue !== nextValue) {
|
|
367
|
+
if (writableNode === undefined) {
|
|
368
|
+
writableNode = lexicalNode.getWritable();
|
|
369
|
+
} // $FlowFixMe
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
writableNode[property] = nextValue;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
function syncPropertiesFromLexical(binding, sharedType, prevLexicalNode, nextLexicalNode) {
|
|
377
|
+
const type = nextLexicalNode.__type;
|
|
378
|
+
const nodeProperties = binding.nodeProperties;
|
|
379
|
+
let properties = nodeProperties.get(type);
|
|
380
|
+
|
|
381
|
+
if (properties === undefined) {
|
|
382
|
+
properties = Object.keys(nextLexicalNode).filter(property => {
|
|
383
|
+
return !excludedProperties.has(property);
|
|
384
|
+
});
|
|
385
|
+
nodeProperties.set(type, properties);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
for (let i = 0; i < properties.length; i++) {
|
|
389
|
+
const property = properties[i];
|
|
390
|
+
const prevValue = // $FlowFixMe: intentional override
|
|
391
|
+
prevLexicalNode === null ? undefined : prevLexicalNode[property]; // $FlowFixMe: intentional override
|
|
392
|
+
|
|
393
|
+
const nextValue = nextLexicalNode[property];
|
|
394
|
+
|
|
395
|
+
if (prevValue !== nextValue) {
|
|
396
|
+
if (sharedType instanceof yjs.Map) {
|
|
397
|
+
sharedType.set(property, nextValue);
|
|
398
|
+
} else {
|
|
399
|
+
sharedType.setAttribute(property, nextValue);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
function spliceString(str, index, delCount, newText) {
|
|
405
|
+
return str.slice(0, index) + newText + str.slice(index + delCount);
|
|
406
|
+
}
|
|
407
|
+
function getPositionFromElementAndOffset(node, offset, boundaryIsEdge) {
|
|
408
|
+
let index = 0;
|
|
409
|
+
let i = 0;
|
|
410
|
+
const children = node._children;
|
|
411
|
+
const childrenLength = children.length;
|
|
412
|
+
|
|
413
|
+
for (; i < childrenLength; i++) {
|
|
414
|
+
const child = children[i];
|
|
415
|
+
const childOffset = index;
|
|
416
|
+
const size = child.getSize();
|
|
417
|
+
index += size;
|
|
418
|
+
const exceedsBoundary = boundaryIsEdge ? index >= offset : index > offset;
|
|
419
|
+
|
|
420
|
+
if (exceedsBoundary && child instanceof CollabTextNode) {
|
|
421
|
+
let textOffset = offset - childOffset - 1;
|
|
422
|
+
|
|
423
|
+
if (textOffset < 0) {
|
|
424
|
+
textOffset = 0;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const diffLength = size - textOffset;
|
|
428
|
+
return {
|
|
429
|
+
length: diffLength,
|
|
430
|
+
node: child,
|
|
431
|
+
nodeIndex: i,
|
|
432
|
+
offset: textOffset
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (index > offset) {
|
|
437
|
+
return {
|
|
438
|
+
length: 0,
|
|
439
|
+
node: child,
|
|
440
|
+
nodeIndex: i,
|
|
441
|
+
offset: childOffset
|
|
442
|
+
};
|
|
443
|
+
} else if (i === childrenLength - 1) {
|
|
444
|
+
return {
|
|
445
|
+
length: 0,
|
|
446
|
+
node: null,
|
|
447
|
+
nodeIndex: i + 1,
|
|
448
|
+
offset: childOffset + 1
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return {
|
|
454
|
+
length: 0,
|
|
455
|
+
node: null,
|
|
456
|
+
nodeIndex: 0,
|
|
457
|
+
offset: 0
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
function doesSelectionNeedRecovering(selection) {
|
|
461
|
+
const anchor = selection.anchor;
|
|
462
|
+
const focus = selection.focus;
|
|
463
|
+
let recoveryNeeded = false;
|
|
464
|
+
|
|
465
|
+
try {
|
|
466
|
+
const anchorNode = anchor.getNode();
|
|
467
|
+
const focusNode = focus.getNode();
|
|
468
|
+
|
|
469
|
+
if ( // We might have removed a node that no longer exists
|
|
470
|
+
!anchorNode.isAttached() || !focusNode.isAttached() || // If we've split a node, then the offset might not be right
|
|
471
|
+
lexical.$isTextNode(anchorNode) && anchor.offset > anchorNode.getTextContentSize() || lexical.$isTextNode(focusNode) && focus.offset > focusNode.getTextContentSize()) {
|
|
472
|
+
recoveryNeeded = true;
|
|
473
|
+
}
|
|
474
|
+
} catch (e) {
|
|
475
|
+
// Sometimes checking nor a node via getNode might trigger
|
|
476
|
+
// an error, so we need recovery then too.
|
|
477
|
+
recoveryNeeded = true;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
return recoveryNeeded;
|
|
481
|
+
}
|
|
482
|
+
function syncWithTransaction(binding, fn) {
|
|
483
|
+
binding.doc.transact(fn, binding);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
488
|
+
*
|
|
489
|
+
* This source code is licensed under the MIT license found in the
|
|
490
|
+
* LICENSE file in the root directory of this source tree.
|
|
491
|
+
*
|
|
492
|
+
*
|
|
493
|
+
*/
|
|
494
|
+
let isMutationFromCollab = false;
|
|
495
|
+
function mutationFromCollab(fn) {
|
|
496
|
+
const prevIsMutationFromCollab = isMutationFromCollab;
|
|
497
|
+
|
|
498
|
+
try {
|
|
499
|
+
isMutationFromCollab = true;
|
|
500
|
+
fn();
|
|
501
|
+
} finally {
|
|
502
|
+
isMutationFromCollab = prevIsMutationFromCollab;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
function observeDecoratorMap(binding, collabNode, decoratorMap, yjsMap) {
|
|
506
|
+
const unobserve = decoratorMap.observe(changedKey => {
|
|
507
|
+
if (isMutationFromCollab) {
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
syncWithTransaction(binding, () => syncLexicalDecoratorMapKeyToYjs(binding, collabNode, decoratorMap._map, yjsMap, changedKey));
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
collabNode._unobservers.add(unobserve);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function observeDecoratorArray(binding, collabNode, decoratorArray, yjsArray) {
|
|
518
|
+
const unobserve = decoratorArray.observe((changedIndex, delCount) => {
|
|
519
|
+
if (isMutationFromCollab) {
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
syncWithTransaction(binding, () => {
|
|
524
|
+
if (delCount > 0) {
|
|
525
|
+
yjsArray.delete(changedIndex, delCount);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
syncLexicalDecoratorArrayValueToYjs(binding, collabNode, decoratorArray._array, yjsArray, changedIndex);
|
|
529
|
+
});
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
collabNode._unobservers.add(unobserve);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function syncLexicalDecoratorMapKeyToYjs(binding, collabNode, internalMap, yjsMap, key) {
|
|
536
|
+
const lexicalValue = internalMap.get(key);
|
|
537
|
+
let yjsValue = yjsMap.get(key);
|
|
538
|
+
|
|
539
|
+
if (lexicalValue !== yjsValue) {
|
|
540
|
+
if (lexical.isDecoratorMap(lexicalValue)) {
|
|
541
|
+
if (yjsValue === undefined) {
|
|
542
|
+
yjsValue = new yjs.Map(); // $FlowFixMe: internal field
|
|
543
|
+
|
|
544
|
+
yjsValue._lexicalValue = lexicalValue; // $FlowFixMe: internal field
|
|
545
|
+
|
|
546
|
+
yjsValue._collabNode = collabNode;
|
|
547
|
+
yjsValue.set('type', 'map');
|
|
548
|
+
yjsMap.set(key, yjsValue);
|
|
549
|
+
observeDecoratorMap(binding, collabNode, lexicalValue, yjsValue);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
syncLexicalDecoratorMapToYjs(binding, collabNode, lexicalValue, yjsValue);
|
|
553
|
+
} else if (lexical.isDecoratorEditor(lexicalValue)) {
|
|
554
|
+
let doc;
|
|
555
|
+
|
|
556
|
+
if (yjsValue === undefined) {
|
|
557
|
+
yjsValue = new yjs.Map(); // $FlowFixMe: internal field
|
|
558
|
+
|
|
559
|
+
yjsValue._lexicalValue = lexicalValue; // $FlowFixMe: internal field
|
|
560
|
+
|
|
561
|
+
yjsValue._collabNode = collabNode; // Create a subdocument
|
|
562
|
+
|
|
563
|
+
doc = new yjs.Doc();
|
|
564
|
+
yjsValue.set('doc', doc);
|
|
565
|
+
yjsValue.set('type', 'editor');
|
|
566
|
+
yjsMap.set(key, yjsValue);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
doc = doc || yjsValue.get('doc');
|
|
570
|
+
const yjsId = yjsValue.get('id');
|
|
571
|
+
const lexicalId = lexicalValue.id;
|
|
572
|
+
|
|
573
|
+
if (yjsId !== lexicalId) {
|
|
574
|
+
const yjsDocMap = binding.docMap;
|
|
575
|
+
yjsDocMap.delete(yjsId);
|
|
576
|
+
yjsValue.set('id', lexicalId);
|
|
577
|
+
yjsDocMap.set(lexicalId, doc);
|
|
578
|
+
}
|
|
579
|
+
} else if (lexical.isDecoratorArray(lexicalValue)) {
|
|
580
|
+
if (yjsValue === undefined) {
|
|
581
|
+
yjsValue = new yjs.Array(); // $FlowFixMe: internal field
|
|
582
|
+
|
|
583
|
+
yjsValue._lexicalValue = lexicalValue; // $FlowFixMe: internal field
|
|
584
|
+
|
|
585
|
+
yjsValue._collabNode = collabNode;
|
|
586
|
+
yjsMap.set(key, yjsValue);
|
|
587
|
+
observeDecoratorArray(binding, collabNode, lexicalValue, yjsValue);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
syncLexicalDecoratorArrayToYjs(binding, collabNode, lexicalValue, yjsValue);
|
|
591
|
+
} else {
|
|
592
|
+
if (lexical.isDecoratorArray(yjsValue) || lexical.isDecoratorMap(yjsValue)) {
|
|
593
|
+
yjsValue.destroy();
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
yjsMap.set(key, lexicalValue);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function syncLexicalDecoratorArrayValueToYjs(binding, collabNode, internalArray, yjsArray, index) {
|
|
602
|
+
const lexicalValue = internalArray[index];
|
|
603
|
+
let yjsValue = yjsArray.get(index);
|
|
604
|
+
|
|
605
|
+
if (lexicalValue !== yjsValue) {
|
|
606
|
+
if (lexical.isDecoratorMap(lexicalValue)) {
|
|
607
|
+
if (yjsValue === undefined) {
|
|
608
|
+
yjsValue = new yjs.Map(); // $FlowFixMe: internal field
|
|
609
|
+
|
|
610
|
+
yjsValue._lexicalValue = lexicalValue; // $FlowFixMe: internal field
|
|
611
|
+
|
|
612
|
+
yjsValue._collabNode = collabNode;
|
|
613
|
+
yjsValue.set('type', 'map');
|
|
614
|
+
yjsArray.insert(index, [yjsValue]);
|
|
615
|
+
observeDecoratorMap(binding, collabNode, lexicalValue, yjsValue);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
syncLexicalDecoratorMapToYjs(binding, collabNode, lexicalValue, yjsValue);
|
|
619
|
+
} else if (lexical.isDecoratorEditor(lexicalValue)) {
|
|
620
|
+
let doc;
|
|
621
|
+
|
|
622
|
+
if (yjsValue === undefined) {
|
|
623
|
+
yjsValue = new yjs.Map(); // $FlowFixMe: internal field
|
|
624
|
+
|
|
625
|
+
yjsValue._lexicalValue = lexicalValue; // $FlowFixMe: internal field
|
|
626
|
+
|
|
627
|
+
yjsValue._collabNode = collabNode; // Create a subdocument
|
|
628
|
+
|
|
629
|
+
doc = new yjs.Doc();
|
|
630
|
+
yjsValue.set('doc', doc);
|
|
631
|
+
yjsValue.set('type', 'editor');
|
|
632
|
+
yjsArray.insert(index, [yjsValue]);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
doc = doc || yjsValue.get('doc');
|
|
636
|
+
const yjsId = yjsValue.get('id');
|
|
637
|
+
const lexicalId = lexicalValue.id;
|
|
638
|
+
|
|
639
|
+
if (yjsId !== lexicalId) {
|
|
640
|
+
const yjsDocMap = binding.docMap;
|
|
641
|
+
yjsDocMap.delete(yjsId);
|
|
642
|
+
yjsValue.set('id', lexicalId);
|
|
643
|
+
yjsDocMap.set(lexicalId, doc);
|
|
644
|
+
}
|
|
645
|
+
} else if (lexical.isDecoratorArray(lexicalValue)) {
|
|
646
|
+
if (yjsValue === undefined) {
|
|
647
|
+
yjsValue = new yjs.Array(); // $FlowFixMe: internal field
|
|
648
|
+
|
|
649
|
+
yjsValue._lexicalValue = lexicalValue; // $FlowFixMe: internal field
|
|
650
|
+
|
|
651
|
+
yjsValue._collabNode = collabNode;
|
|
652
|
+
yjsArray.insert(index, [yjsValue]);
|
|
653
|
+
observeDecoratorArray(binding, collabNode, lexicalValue, yjsValue);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
syncLexicalDecoratorArrayToYjs(binding, collabNode, lexicalValue, yjsValue);
|
|
657
|
+
} else {
|
|
658
|
+
yjsArray.insert(index, [lexicalValue]);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function syncLexicalDecoratorMapToYjs(binding, collabNode, decoratorMap, yjsMap) {
|
|
664
|
+
const internalMap = decoratorMap._map;
|
|
665
|
+
const keys = Array.from(internalMap.keys());
|
|
666
|
+
|
|
667
|
+
for (let i = 0; i < keys.length; i++) {
|
|
668
|
+
const key = keys[i];
|
|
669
|
+
syncLexicalDecoratorMapKeyToYjs(binding, collabNode, internalMap, yjsMap, key);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function syncLexicalDecoratorArrayToYjs(binding, collabNode, decoratorArray, yjsArray) {
|
|
674
|
+
const internalArray = decoratorArray._array;
|
|
675
|
+
|
|
676
|
+
for (let i = 0; i < internalArray.length; i++) {
|
|
677
|
+
syncLexicalDecoratorArrayValueToYjs(binding, collabNode, internalArray, yjsArray, i);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function syncYjsDecoratorMapKeyToLexical(binding, collabNode, yjsMap, decoratorMap, key) {
|
|
682
|
+
const lexicalValue = decoratorMap.get(key);
|
|
683
|
+
const yjsValue = yjsMap.get(key);
|
|
684
|
+
|
|
685
|
+
if (lexicalValue !== yjsValue) {
|
|
686
|
+
// $FlowFixMe: internal field
|
|
687
|
+
let nextValue = yjsValue._lexicalValue;
|
|
688
|
+
|
|
689
|
+
if (yjsValue instanceof yjs.Map) {
|
|
690
|
+
const type = yjsValue.get('type');
|
|
691
|
+
|
|
692
|
+
if (type === 'editor') {
|
|
693
|
+
if (nextValue === undefined) {
|
|
694
|
+
const yjsDocMap = binding.docMap;
|
|
695
|
+
const id = yjsValue.get('id');
|
|
696
|
+
const doc = yjsValue.get('doc');
|
|
697
|
+
nextValue = lexical.createDecoratorEditor(id);
|
|
698
|
+
yjsValue._lexicalValue = nextValue;
|
|
699
|
+
yjsValue._collabNode = collabNode;
|
|
700
|
+
yjsDocMap.set(id, doc);
|
|
701
|
+
mutationFromCollab(() => decoratorMap.set(key, nextValue));
|
|
702
|
+
}
|
|
703
|
+
} else if (type === 'map') {
|
|
704
|
+
if (nextValue === undefined) {
|
|
705
|
+
nextValue = lexical.createDecoratorMap(binding.editor);
|
|
706
|
+
observeDecoratorMap(binding, collabNode, nextValue, yjsValue);
|
|
707
|
+
yjsValue._lexicalValue = nextValue;
|
|
708
|
+
yjsValue._collabNode = collabNode;
|
|
709
|
+
mutationFromCollab(() => decoratorMap.set(key, nextValue));
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
syncYjsDecoratorMapToLexical(binding, collabNode, yjsValue, nextValue, null);
|
|
713
|
+
} else {
|
|
714
|
+
{
|
|
715
|
+
throw Error(`Should never happen`);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
} else if (yjsValue instanceof yjs.Array) {
|
|
719
|
+
if (nextValue === undefined) {
|
|
720
|
+
nextValue = lexical.createDecoratorArray(binding.editor);
|
|
721
|
+
observeDecoratorArray(binding, collabNode, nextValue, yjsValue);
|
|
722
|
+
yjsValue._lexicalValue = nextValue;
|
|
723
|
+
yjsValue._collabNode = collabNode;
|
|
724
|
+
mutationFromCollab(() => decoratorMap.set(key, nextValue));
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
syncYjsDecoratorArrayToLexical(binding, collabNode, yjsValue, nextValue);
|
|
728
|
+
} else {
|
|
729
|
+
mutationFromCollab(() => decoratorMap.set(key, yjsValue));
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function syncYjsDecoratorArrayValueToLexical(binding, collabNode, yjsArray, decoratorArray, index) {
|
|
735
|
+
const lexicalValue = decoratorArray._array[index];
|
|
736
|
+
const yjsValue = yjsArray.get(index);
|
|
737
|
+
|
|
738
|
+
if (lexicalValue !== yjsValue) {
|
|
739
|
+
// $FlowFixMe: internal field
|
|
740
|
+
let nextValue = yjsValue._lexicalValue;
|
|
741
|
+
|
|
742
|
+
if (yjsValue instanceof yjs.Map) {
|
|
743
|
+
const type = yjsValue.get('type');
|
|
744
|
+
|
|
745
|
+
if (type === 'editor') {
|
|
746
|
+
if (nextValue === undefined) {
|
|
747
|
+
const yjsDocMap = binding.docMap;
|
|
748
|
+
const id = yjsValue.get('id');
|
|
749
|
+
const doc = yjsValue.get('doc');
|
|
750
|
+
nextValue = lexical.createDecoratorEditor(id);
|
|
751
|
+
yjsValue._lexicalValue = nextValue;
|
|
752
|
+
yjsValue._collabNode = collabNode;
|
|
753
|
+
yjsDocMap.set(id, doc);
|
|
754
|
+
mutationFromCollab(() => decoratorArray.splice(index, 0, nextValue));
|
|
755
|
+
}
|
|
756
|
+
} else if (type === 'map') {
|
|
757
|
+
if (nextValue === undefined) {
|
|
758
|
+
nextValue = lexical.createDecoratorMap(binding.editor);
|
|
759
|
+
observeDecoratorMap(binding, collabNode, nextValue, yjsValue);
|
|
760
|
+
yjsValue._lexicalValue = nextValue;
|
|
761
|
+
yjsValue._collabNode = collabNode;
|
|
762
|
+
mutationFromCollab(() => decoratorArray.splice(index, 0, nextValue));
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
syncYjsDecoratorMapToLexical(binding, collabNode, yjsValue, nextValue, null);
|
|
766
|
+
} else {
|
|
767
|
+
{
|
|
768
|
+
throw Error(`Should never happen`);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
} else if (yjsValue instanceof yjs.Array) {
|
|
772
|
+
if (nextValue === undefined) {
|
|
773
|
+
nextValue = lexical.createDecoratorArray(binding.editor);
|
|
774
|
+
observeDecoratorArray(binding, collabNode, nextValue, yjsValue);
|
|
775
|
+
yjsValue._lexicalValue = nextValue;
|
|
776
|
+
yjsValue._collabNode = collabNode;
|
|
777
|
+
mutationFromCollab(() => decoratorArray.splice(index, 0, nextValue));
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
syncYjsDecoratorArrayToLexical(binding, collabNode, yjsValue, nextValue);
|
|
781
|
+
} else {
|
|
782
|
+
mutationFromCollab(() => decoratorArray.splice(index, 0, yjsValue));
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
function syncYjsDecoratorArrayToLexical(binding, collabNode, yjsArray, decoratorArray) {
|
|
787
|
+
const length = Math.max(yjsArray.length, decoratorArray.getLength());
|
|
788
|
+
|
|
789
|
+
for (let i = 0; i < length; i++) {
|
|
790
|
+
syncYjsDecoratorArrayValueToLexical(binding, collabNode, yjsArray, decoratorArray, i);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
function syncYjsDecoratorMapToLexical(binding, collabNode, yjsMap, decoratorMap, keysChanged) {
|
|
794
|
+
const keys = keysChanged === null ? Array.from(yjsMap.keys()) : Array.from(keysChanged);
|
|
795
|
+
|
|
796
|
+
for (let i = 0; i < keys.length; i++) {
|
|
797
|
+
const key = keys[i];
|
|
798
|
+
syncYjsDecoratorMapKeyToLexical(binding, collabNode, yjsMap, decoratorMap, key);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/**
|
|
803
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
804
|
+
*
|
|
805
|
+
* This source code is licensed under the MIT license found in the
|
|
806
|
+
* LICENSE file in the root directory of this source tree.
|
|
807
|
+
*
|
|
808
|
+
*
|
|
809
|
+
*/
|
|
810
|
+
class CollabDecoratorNode {
|
|
811
|
+
constructor(xmlElem, parent, type) {
|
|
812
|
+
this._key = '';
|
|
813
|
+
this._xmlElem = xmlElem;
|
|
814
|
+
this._parent = parent;
|
|
815
|
+
this._type = type;
|
|
816
|
+
this._unobservers = new Set();
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
getPrevNode(nodeMap) {
|
|
820
|
+
if (nodeMap === null) {
|
|
821
|
+
return null;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
const node = nodeMap.get(this._key);
|
|
825
|
+
return lexical.$isDecoratorNode(node) ? node : null;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
getNode() {
|
|
829
|
+
const node = lexical.$getNodeByKey(this._key);
|
|
830
|
+
return lexical.$isDecoratorNode(node) ? node : null;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
getSharedType() {
|
|
834
|
+
return this._xmlElem;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
getType() {
|
|
838
|
+
return this._type;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
getKey() {
|
|
842
|
+
return this._key;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
getSize() {
|
|
846
|
+
return 1;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
getOffset() {
|
|
850
|
+
const collabElementNode = this._parent;
|
|
851
|
+
return collabElementNode.getChildOffset(this);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
syncPropertiesFromLexical(binding, nextLexicalNode, prevNodeMap) {
|
|
855
|
+
const prevLexicalNode = this.getPrevNode(prevNodeMap);
|
|
856
|
+
const xmlElem = this._xmlElem;
|
|
857
|
+
const prevDecoratorMap = prevLexicalNode === null ? null : prevLexicalNode.__state;
|
|
858
|
+
const nextDecoratorMap = nextLexicalNode.__state;
|
|
859
|
+
syncPropertiesFromLexical(binding, xmlElem, prevLexicalNode, nextLexicalNode); // Handle bindings
|
|
860
|
+
|
|
861
|
+
if (prevDecoratorMap !== nextDecoratorMap) {
|
|
862
|
+
const yjsMap = new yjs.Map();
|
|
863
|
+
xmlElem.insert(0, [yjsMap]); // $FlowFixMe: internal field
|
|
864
|
+
|
|
865
|
+
yjsMap._lexicalValue = nextDecoratorMap; // $FlowFixMe: internal field
|
|
866
|
+
|
|
867
|
+
yjsMap._collabNode = this;
|
|
868
|
+
syncLexicalDecoratorMapToYjs(binding, this, nextDecoratorMap, yjsMap);
|
|
869
|
+
observeDecoratorMap(binding, this, nextDecoratorMap, yjsMap);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
syncPropertiesFromYjs(binding, keysChanged) {
|
|
874
|
+
const lexicalNode = this.getNode();
|
|
875
|
+
|
|
876
|
+
if (lexicalNode === null) {
|
|
877
|
+
throw new Error('Should never happen');
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
const xmlElem = this._xmlElem; // $FlowFixMe: should always be true
|
|
881
|
+
|
|
882
|
+
const yjsMap = xmlElem.firstChild;
|
|
883
|
+
const decoratorMap = lexicalNode.__state; // $FlowFixMe: internal field
|
|
884
|
+
|
|
885
|
+
yjsMap._lexicalValue = decoratorMap; // $FlowFixMe: internal field
|
|
886
|
+
|
|
887
|
+
yjsMap._collabNode = this;
|
|
888
|
+
syncPropertiesFromYjs(binding, xmlElem, lexicalNode, keysChanged);
|
|
889
|
+
syncYjsDecoratorMapToLexical(binding, this, yjsMap, decoratorMap, null);
|
|
890
|
+
observeDecoratorMap(binding, this, decoratorMap, yjsMap);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
destroy(binding) {
|
|
894
|
+
const collabNodeMap = binding.collabNodeMap;
|
|
895
|
+
collabNodeMap.delete(this._key);
|
|
896
|
+
|
|
897
|
+
this._unobservers.forEach(unobserver => unobserver());
|
|
898
|
+
|
|
899
|
+
this._unobservers.clear();
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
}
|
|
903
|
+
function $createCollabDecoratorNode(xmlElem, parent, type) {
|
|
904
|
+
const collabNode = new CollabDecoratorNode(xmlElem, parent, type); // $FlowFixMe: internal field
|
|
905
|
+
|
|
906
|
+
xmlElem._collabNode = collabNode;
|
|
907
|
+
return collabNode;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/**
|
|
911
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
912
|
+
*
|
|
913
|
+
* This source code is licensed under the MIT license found in the
|
|
914
|
+
* LICENSE file in the root directory of this source tree.
|
|
915
|
+
*
|
|
916
|
+
*
|
|
917
|
+
*/
|
|
918
|
+
class CollabElementNode {
|
|
919
|
+
constructor(xmlText, parent, type) {
|
|
920
|
+
this._key = '';
|
|
921
|
+
this._children = [];
|
|
922
|
+
this._xmlText = xmlText;
|
|
923
|
+
this._type = type;
|
|
924
|
+
this._parent = parent;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
getPrevNode(nodeMap) {
|
|
928
|
+
if (nodeMap === null) {
|
|
929
|
+
return null;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
const node = nodeMap.get(this._key);
|
|
933
|
+
return lexical.$isElementNode(node) ? node : null;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
getNode() {
|
|
937
|
+
const node = lexical.$getNodeByKey(this._key);
|
|
938
|
+
return lexical.$isElementNode(node) ? node : null;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
getSharedType() {
|
|
942
|
+
return this._xmlText;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
getType() {
|
|
946
|
+
return this._type;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
getKey() {
|
|
950
|
+
return this._key;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
isEmpty() {
|
|
954
|
+
return this._children.length === 0;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
getSize() {
|
|
958
|
+
return 1;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
getOffset() {
|
|
962
|
+
const collabElementNode = this._parent;
|
|
963
|
+
|
|
964
|
+
if (collabElementNode === null) {
|
|
965
|
+
throw new Error('Should never happen');
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
return collabElementNode.getChildOffset(this);
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
syncPropertiesFromYjs(binding, keysChanged) {
|
|
972
|
+
const lexicalNode = this.getNode();
|
|
973
|
+
|
|
974
|
+
if (lexicalNode === null) {
|
|
975
|
+
this.getNode();
|
|
976
|
+
throw new Error('Should never happen');
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
syncPropertiesFromYjs(binding, this._xmlText, lexicalNode, keysChanged);
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
applyChildrenYjsDelta(binding, deltas) {
|
|
983
|
+
const children = this._children;
|
|
984
|
+
let currIndex = 0;
|
|
985
|
+
|
|
986
|
+
for (let i = 0; i < deltas.length; i++) {
|
|
987
|
+
const delta = deltas[i];
|
|
988
|
+
const insertDelta = delta.insert;
|
|
989
|
+
const deleteDelta = delta.delete;
|
|
990
|
+
|
|
991
|
+
if (delta.retain != null) {
|
|
992
|
+
currIndex += delta.retain;
|
|
993
|
+
} else if (typeof deleteDelta === 'number') {
|
|
994
|
+
let deletionSize = deleteDelta;
|
|
995
|
+
|
|
996
|
+
while (deletionSize > 0) {
|
|
997
|
+
const {
|
|
998
|
+
node,
|
|
999
|
+
nodeIndex,
|
|
1000
|
+
offset,
|
|
1001
|
+
length
|
|
1002
|
+
} = getPositionFromElementAndOffset(this, currIndex, false);
|
|
1003
|
+
|
|
1004
|
+
if (node instanceof CollabElementNode || node instanceof CollabLineBreakNode || node instanceof CollabDecoratorNode) {
|
|
1005
|
+
children.splice(nodeIndex, 1);
|
|
1006
|
+
deletionSize -= 1;
|
|
1007
|
+
} else if (node instanceof CollabTextNode) {
|
|
1008
|
+
const delCount = Math.min(deletionSize, length);
|
|
1009
|
+
const prevCollabNode = nodeIndex !== 0 ? children[nodeIndex - 1] : null;
|
|
1010
|
+
const nodeSize = node.getSize();
|
|
1011
|
+
|
|
1012
|
+
if (offset === 0 && delCount === 1 && nodeIndex > 0 && prevCollabNode instanceof CollabTextNode && length === nodeSize && // If the node has no keys, it's been deleted
|
|
1013
|
+
Array.from(node._map.keys()).length === 0) {
|
|
1014
|
+
// Merge the text node with previous.
|
|
1015
|
+
prevCollabNode._text += node._text;
|
|
1016
|
+
children.splice(nodeIndex, 1);
|
|
1017
|
+
} else if (offset === 0 && delCount === nodeSize) {
|
|
1018
|
+
// The entire thing needs removing
|
|
1019
|
+
children.splice(nodeIndex, 1);
|
|
1020
|
+
} else {
|
|
1021
|
+
node._text = spliceString(node._text, offset, delCount, '');
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
deletionSize -= delCount;
|
|
1025
|
+
} else {
|
|
1026
|
+
// Can occur due to the deletion from the dangling text heuristic below.
|
|
1027
|
+
break;
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
} else if (insertDelta != null) {
|
|
1031
|
+
if (typeof insertDelta === 'string') {
|
|
1032
|
+
const {
|
|
1033
|
+
node,
|
|
1034
|
+
offset
|
|
1035
|
+
} = getPositionFromElementAndOffset(this, currIndex, true);
|
|
1036
|
+
|
|
1037
|
+
if (node instanceof CollabTextNode) {
|
|
1038
|
+
node._text = spliceString(node._text, offset, 0, insertDelta);
|
|
1039
|
+
} else {
|
|
1040
|
+
// TODO: maybe we can improve this by keeping around a redundant
|
|
1041
|
+
// text node map, rather than removing all the text nodes, so there
|
|
1042
|
+
// never can be dangling text.
|
|
1043
|
+
// We have a conflict where there was likely a CollabTextNode and
|
|
1044
|
+
// an Lexical TextNode too, but they were removed in a merge. So
|
|
1045
|
+
// let's just ignore the text and trigger a removal for it from our
|
|
1046
|
+
// shared type.
|
|
1047
|
+
this._xmlText.delete(offset, insertDelta.length);
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
currIndex += insertDelta.length;
|
|
1051
|
+
} else {
|
|
1052
|
+
const sharedType = insertDelta;
|
|
1053
|
+
const {
|
|
1054
|
+
nodeIndex
|
|
1055
|
+
} = getPositionFromElementAndOffset(this, currIndex, false);
|
|
1056
|
+
const collabNode = getOrInitCollabNodeFromSharedType(binding, sharedType, this);
|
|
1057
|
+
children.splice(nodeIndex, 0, collabNode);
|
|
1058
|
+
currIndex += 1;
|
|
1059
|
+
}
|
|
1060
|
+
} else {
|
|
1061
|
+
throw new Error('Unexpected delta format');
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
syncChildrenFromYjs(binding) {
|
|
1067
|
+
// Now diff the children of the collab node with that of our existing Lexical node.
|
|
1068
|
+
const lexicalNode = this.getNode();
|
|
1069
|
+
|
|
1070
|
+
if (lexicalNode === null) {
|
|
1071
|
+
this.getNode();
|
|
1072
|
+
throw new Error('Should never happen');
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
const key = lexicalNode.__key;
|
|
1076
|
+
const prevLexicalChildrenKeys = lexicalNode.__children;
|
|
1077
|
+
const nextLexicalChildrenKeys = [];
|
|
1078
|
+
const lexicalChildrenKeysLength = prevLexicalChildrenKeys.length;
|
|
1079
|
+
const collabChildren = this._children;
|
|
1080
|
+
const collabChildrenLength = collabChildren.length;
|
|
1081
|
+
const collabNodeMap = binding.collabNodeMap;
|
|
1082
|
+
const visitedKeys = new Set();
|
|
1083
|
+
let collabKeys; // Assign the new children key array that we're about to mutate
|
|
1084
|
+
|
|
1085
|
+
let writableLexicalNode;
|
|
1086
|
+
|
|
1087
|
+
if (collabChildrenLength !== lexicalChildrenKeysLength) {
|
|
1088
|
+
writableLexicalNode = lazilyCloneElementNode(lexicalNode, writableLexicalNode, nextLexicalChildrenKeys);
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
let prevIndex = 0;
|
|
1092
|
+
|
|
1093
|
+
for (let i = 0; i < collabChildrenLength; i++) {
|
|
1094
|
+
const lexicalChildKey = prevLexicalChildrenKeys[prevIndex];
|
|
1095
|
+
const childCollabNode = collabChildren[i];
|
|
1096
|
+
const collabLexicalChildNode = childCollabNode.getNode();
|
|
1097
|
+
const collabKey = childCollabNode._key;
|
|
1098
|
+
|
|
1099
|
+
if (collabLexicalChildNode !== null && lexicalChildKey === collabKey) {
|
|
1100
|
+
const childNeedsUpdating = lexical.$isTextNode(collabLexicalChildNode); // Update
|
|
1101
|
+
|
|
1102
|
+
visitedKeys.add(lexicalChildKey);
|
|
1103
|
+
|
|
1104
|
+
if (childNeedsUpdating) {
|
|
1105
|
+
childCollabNode._key = lexicalChildKey;
|
|
1106
|
+
|
|
1107
|
+
if (childCollabNode instanceof CollabElementNode) {
|
|
1108
|
+
const xmlText = childCollabNode._xmlText;
|
|
1109
|
+
childCollabNode.syncPropertiesFromYjs(binding, null);
|
|
1110
|
+
childCollabNode.applyChildrenYjsDelta(binding, xmlText.toDelta());
|
|
1111
|
+
childCollabNode.syncChildrenFromYjs(binding);
|
|
1112
|
+
} else if (childCollabNode instanceof CollabTextNode) {
|
|
1113
|
+
childCollabNode.syncPropertiesAndTextFromYjs(binding, null);
|
|
1114
|
+
} else if (childCollabNode instanceof CollabDecoratorNode) {
|
|
1115
|
+
childCollabNode.syncPropertiesFromYjs(binding, null);
|
|
1116
|
+
} else if (!(childCollabNode instanceof CollabLineBreakNode)) {
|
|
1117
|
+
throw new Error('Should never happen');
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
nextLexicalChildrenKeys[i] = lexicalChildKey;
|
|
1122
|
+
prevIndex++;
|
|
1123
|
+
} else {
|
|
1124
|
+
if (collabKeys === undefined) {
|
|
1125
|
+
collabKeys = new Set();
|
|
1126
|
+
|
|
1127
|
+
for (let s = 0; s < collabChildrenLength; s++) {
|
|
1128
|
+
const child = collabChildren[s];
|
|
1129
|
+
const childKey = child._key;
|
|
1130
|
+
|
|
1131
|
+
if (childKey !== '') {
|
|
1132
|
+
collabKeys.add(childKey);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
if (collabLexicalChildNode !== null && lexicalChildKey !== undefined && !collabKeys.has(lexicalChildKey)) {
|
|
1138
|
+
i--;
|
|
1139
|
+
prevIndex++;
|
|
1140
|
+
continue;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
writableLexicalNode = lazilyCloneElementNode(lexicalNode, writableLexicalNode, nextLexicalChildrenKeys); // Create/Replace
|
|
1144
|
+
|
|
1145
|
+
const lexicalChildNode = createLexicalNodeFromCollabNode(binding, childCollabNode, key);
|
|
1146
|
+
const childKey = lexicalChildNode.__key;
|
|
1147
|
+
collabNodeMap.set(childKey, childCollabNode);
|
|
1148
|
+
nextLexicalChildrenKeys[i] = childKey;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
for (let i = 0; i < lexicalChildrenKeysLength; i++) {
|
|
1153
|
+
const lexicalChildKey = prevLexicalChildrenKeys[i];
|
|
1154
|
+
|
|
1155
|
+
if (!visitedKeys.has(lexicalChildKey)) {
|
|
1156
|
+
// Remove
|
|
1157
|
+
const lexicalChildNode = $getNodeByKeyOrThrow(lexicalChildKey).getWritable();
|
|
1158
|
+
const collabNode = binding.collabNodeMap.get(lexicalChildKey);
|
|
1159
|
+
|
|
1160
|
+
if (collabNode !== undefined) {
|
|
1161
|
+
collabNode.destroy(binding);
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
lexicalChildNode.__parent = null;
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
syncPropertiesFromLexical(binding, nextLexicalNode, prevNodeMap) {
|
|
1170
|
+
syncPropertiesFromLexical(binding, this._xmlText, this.getPrevNode(prevNodeMap), nextLexicalNode);
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
_syncChildFromLexical(binding, index, key, prevNodeMap, dirtyElements, dirtyLeaves) {
|
|
1174
|
+
const childCollabNode = this._children[index]; // Update
|
|
1175
|
+
|
|
1176
|
+
const nextChildNode = $getNodeByKeyOrThrow(key);
|
|
1177
|
+
|
|
1178
|
+
if (childCollabNode instanceof CollabElementNode && lexical.$isElementNode(nextChildNode)) {
|
|
1179
|
+
childCollabNode.syncPropertiesFromLexical(binding, nextChildNode, prevNodeMap);
|
|
1180
|
+
childCollabNode.syncChildrenFromLexical(binding, nextChildNode, prevNodeMap, dirtyElements, dirtyLeaves);
|
|
1181
|
+
} else if (childCollabNode instanceof CollabTextNode && lexical.$isTextNode(nextChildNode)) {
|
|
1182
|
+
childCollabNode.syncPropertiesAndTextFromLexical(binding, nextChildNode, prevNodeMap);
|
|
1183
|
+
} else if (childCollabNode instanceof CollabDecoratorNode && lexical.$isDecoratorNode(nextChildNode)) {
|
|
1184
|
+
childCollabNode.syncPropertiesFromLexical(binding, nextChildNode, prevNodeMap);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
syncChildrenFromLexical(binding, nextLexicalNode, prevNodeMap, dirtyElements, dirtyLeaves) {
|
|
1189
|
+
const prevLexicalNode = this.getPrevNode(prevNodeMap);
|
|
1190
|
+
const prevChildren = prevLexicalNode === null ? [] : prevLexicalNode.__children;
|
|
1191
|
+
const nextChildren = nextLexicalNode.__children;
|
|
1192
|
+
const prevEndIndex = prevChildren.length - 1;
|
|
1193
|
+
const nextEndIndex = nextChildren.length - 1;
|
|
1194
|
+
const collabNodeMap = binding.collabNodeMap;
|
|
1195
|
+
let prevChildrenSet;
|
|
1196
|
+
let nextChildrenSet;
|
|
1197
|
+
let prevIndex = 0;
|
|
1198
|
+
let nextIndex = 0;
|
|
1199
|
+
|
|
1200
|
+
while (prevIndex <= prevEndIndex && nextIndex <= nextEndIndex) {
|
|
1201
|
+
const prevKey = prevChildren[prevIndex];
|
|
1202
|
+
const nextKey = nextChildren[nextIndex];
|
|
1203
|
+
|
|
1204
|
+
if (prevKey === nextKey) {
|
|
1205
|
+
// Nove move, create or remove
|
|
1206
|
+
this._syncChildFromLexical(binding, nextIndex, nextKey, prevNodeMap, dirtyElements, dirtyLeaves);
|
|
1207
|
+
|
|
1208
|
+
prevIndex++;
|
|
1209
|
+
nextIndex++;
|
|
1210
|
+
} else {
|
|
1211
|
+
if (prevChildrenSet === undefined) {
|
|
1212
|
+
prevChildrenSet = new Set(prevChildren);
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
if (nextChildrenSet === undefined) {
|
|
1216
|
+
nextChildrenSet = new Set(nextChildren);
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
const nextHasPrevKey = nextChildrenSet.has(prevKey);
|
|
1220
|
+
const prevHasNextKey = prevChildrenSet.has(nextKey);
|
|
1221
|
+
|
|
1222
|
+
if (!nextHasPrevKey) {
|
|
1223
|
+
// Remove
|
|
1224
|
+
this.splice(binding, nextIndex, 1);
|
|
1225
|
+
prevIndex++;
|
|
1226
|
+
} else {
|
|
1227
|
+
// Create or replace
|
|
1228
|
+
const nextChildNode = $getNodeByKeyOrThrow(nextKey);
|
|
1229
|
+
const collabNode = $createCollabNodeFromLexicalNode(binding, nextChildNode, this);
|
|
1230
|
+
collabNodeMap.set(nextKey, collabNode);
|
|
1231
|
+
|
|
1232
|
+
if (prevHasNextKey) {
|
|
1233
|
+
this.splice(binding, nextIndex, 1, collabNode);
|
|
1234
|
+
prevIndex++;
|
|
1235
|
+
nextIndex++;
|
|
1236
|
+
} else {
|
|
1237
|
+
this.splice(binding, nextIndex, 0, collabNode);
|
|
1238
|
+
nextIndex++;
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
const appendNewChildren = prevIndex > prevEndIndex;
|
|
1245
|
+
const removeOldChildren = nextIndex > nextEndIndex;
|
|
1246
|
+
|
|
1247
|
+
if (appendNewChildren && !removeOldChildren) {
|
|
1248
|
+
for (; nextIndex <= nextEndIndex; ++nextIndex) {
|
|
1249
|
+
const key = nextChildren[nextIndex];
|
|
1250
|
+
const nextChildNode = $getNodeByKeyOrThrow(key);
|
|
1251
|
+
const collabNode = $createCollabNodeFromLexicalNode(binding, nextChildNode, this);
|
|
1252
|
+
this.append(collabNode);
|
|
1253
|
+
collabNodeMap.set(key, collabNode);
|
|
1254
|
+
}
|
|
1255
|
+
} else if (removeOldChildren && !appendNewChildren) {
|
|
1256
|
+
for (let i = this._children.length - 1; i >= nextIndex; i--) {
|
|
1257
|
+
this.splice(binding, i, 1);
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
append(collabNode) {
|
|
1263
|
+
const xmlText = this._xmlText;
|
|
1264
|
+
const children = this._children;
|
|
1265
|
+
const lastChild = children[children.length - 1];
|
|
1266
|
+
const offset = lastChild !== undefined ? lastChild.getOffset() + lastChild.getSize() : 0;
|
|
1267
|
+
|
|
1268
|
+
if (collabNode instanceof CollabElementNode) {
|
|
1269
|
+
xmlText.insertEmbed(offset, collabNode._xmlText);
|
|
1270
|
+
} else if (collabNode instanceof CollabTextNode) {
|
|
1271
|
+
const map = collabNode._map;
|
|
1272
|
+
|
|
1273
|
+
if (map.parent === null) {
|
|
1274
|
+
xmlText.insertEmbed(offset, map);
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
xmlText.insert(offset + 1, collabNode._text);
|
|
1278
|
+
} else if (collabNode instanceof CollabLineBreakNode) {
|
|
1279
|
+
xmlText.insertEmbed(offset, collabNode._map);
|
|
1280
|
+
} else if (collabNode instanceof CollabDecoratorNode) {
|
|
1281
|
+
xmlText.insertEmbed(offset, collabNode._xmlElem);
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
this._children.push(collabNode);
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
splice(binding, index, delCount, collabNode) {
|
|
1288
|
+
const children = this._children;
|
|
1289
|
+
const child = children[index];
|
|
1290
|
+
|
|
1291
|
+
if (child === undefined) {
|
|
1292
|
+
if (collabNode !== undefined) {
|
|
1293
|
+
this.append(collabNode);
|
|
1294
|
+
} else {
|
|
1295
|
+
throw new Error('Should never happen');
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
return;
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
const offset = child.getOffset();
|
|
1302
|
+
|
|
1303
|
+
if (offset === -1) {
|
|
1304
|
+
throw new Error('Should never happen');
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
const xmlText = this._xmlText;
|
|
1308
|
+
|
|
1309
|
+
if (delCount !== 0) {
|
|
1310
|
+
// What if we delete many nodes, don't we need to get all their
|
|
1311
|
+
// sizes?
|
|
1312
|
+
xmlText.delete(offset, child.getSize());
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
if (collabNode instanceof CollabElementNode) {
|
|
1316
|
+
xmlText.insertEmbed(offset, collabNode._xmlText);
|
|
1317
|
+
} else if (collabNode instanceof CollabTextNode) {
|
|
1318
|
+
const map = collabNode._map;
|
|
1319
|
+
|
|
1320
|
+
if (map.parent === null) {
|
|
1321
|
+
xmlText.insertEmbed(offset, map);
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
xmlText.insert(offset + 1, collabNode._text);
|
|
1325
|
+
} else if (collabNode instanceof CollabLineBreakNode) {
|
|
1326
|
+
xmlText.insertEmbed(offset, collabNode._map);
|
|
1327
|
+
} else if (collabNode instanceof CollabDecoratorNode) {
|
|
1328
|
+
xmlText.insertEmbed(offset, collabNode._xmlElem);
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
if (delCount !== 0) {
|
|
1332
|
+
const childrenToDelete = children.slice(index, index + delCount);
|
|
1333
|
+
|
|
1334
|
+
for (let i = 0; i < childrenToDelete.length; i++) {
|
|
1335
|
+
childrenToDelete[i].destroy(binding);
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
if (collabNode !== undefined) {
|
|
1340
|
+
children.splice(index, delCount, collabNode);
|
|
1341
|
+
} else {
|
|
1342
|
+
children.splice(index, delCount);
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
getChildOffset(collabNode) {
|
|
1347
|
+
let offset = 0;
|
|
1348
|
+
const children = this._children;
|
|
1349
|
+
|
|
1350
|
+
for (let i = 0; i < children.length; i++) {
|
|
1351
|
+
const child = children[i];
|
|
1352
|
+
|
|
1353
|
+
if (child === collabNode) {
|
|
1354
|
+
return offset;
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
offset += child.getSize();
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
return -1;
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
destroy(binding) {
|
|
1364
|
+
const collabNodeMap = binding.collabNodeMap;
|
|
1365
|
+
const children = this._children;
|
|
1366
|
+
|
|
1367
|
+
for (let i = 0; i < children.length; i++) {
|
|
1368
|
+
children[i].destroy(binding);
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
collabNodeMap.delete(this._key);
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
function lazilyCloneElementNode(lexicalNode, writableLexicalNode, nextLexicalChildrenKeys) {
|
|
1377
|
+
if (writableLexicalNode === undefined) {
|
|
1378
|
+
const clone = lexicalNode.getWritable();
|
|
1379
|
+
clone.__children = nextLexicalChildrenKeys;
|
|
1380
|
+
return clone;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
return writableLexicalNode;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
function $createCollabElementNode(xmlText, parent, type) {
|
|
1387
|
+
const collabNode = new CollabElementNode(xmlText, parent, type); // $FlowFixMe: internal field
|
|
1388
|
+
|
|
1389
|
+
xmlText._collabNode = collabNode;
|
|
1390
|
+
return collabNode;
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
/**
|
|
1394
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
1395
|
+
*
|
|
1396
|
+
* This source code is licensed under the MIT license found in the
|
|
1397
|
+
* LICENSE file in the root directory of this source tree.
|
|
1398
|
+
*
|
|
1399
|
+
*
|
|
1400
|
+
*/
|
|
1401
|
+
function createBinding(editor, provider, id, docMap) {
|
|
1402
|
+
const doc = docMap.get(id);
|
|
1403
|
+
|
|
1404
|
+
if (doc === undefined) {
|
|
1405
|
+
throw new Error('Should never happen');
|
|
1406
|
+
} // $FlowFixMe: this will work
|
|
1407
|
+
|
|
1408
|
+
|
|
1409
|
+
const rootXmlText = doc.get('root', yjs.XmlText);
|
|
1410
|
+
const root = $createCollabElementNode(rootXmlText, null, 'root');
|
|
1411
|
+
root._key = 'root'; // $FlowFixMe: our Flow bindings need fixing
|
|
1412
|
+
|
|
1413
|
+
return {
|
|
1414
|
+
clientID: doc.clientID,
|
|
1415
|
+
collabNodeMap: new Map(),
|
|
1416
|
+
cursors: new Map(),
|
|
1417
|
+
cursorsContainer: null,
|
|
1418
|
+
doc,
|
|
1419
|
+
docMap,
|
|
1420
|
+
editor,
|
|
1421
|
+
id,
|
|
1422
|
+
nodeProperties: new Map(),
|
|
1423
|
+
root
|
|
1424
|
+
};
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
/**
|
|
1428
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
1429
|
+
*
|
|
1430
|
+
* This source code is licensed under the MIT license found in the
|
|
1431
|
+
* LICENSE file in the root directory of this source tree.
|
|
1432
|
+
*
|
|
1433
|
+
*
|
|
1434
|
+
*/
|
|
1435
|
+
|
|
1436
|
+
function createRelativePosition(point, binding) {
|
|
1437
|
+
const collabNodeMap = binding.collabNodeMap;
|
|
1438
|
+
const collabNode = collabNodeMap.get(point.key);
|
|
1439
|
+
|
|
1440
|
+
if (collabNode === undefined) {
|
|
1441
|
+
return null;
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
let offset = point.offset;
|
|
1445
|
+
let sharedType = collabNode.getSharedType();
|
|
1446
|
+
|
|
1447
|
+
if (collabNode instanceof CollabTextNode) {
|
|
1448
|
+
sharedType = collabNode._parent._xmlText;
|
|
1449
|
+
const currentOffset = collabNode.getOffset();
|
|
1450
|
+
|
|
1451
|
+
if (currentOffset === -1) {
|
|
1452
|
+
return null;
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
offset = currentOffset + 1 + offset;
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
return yjs.createRelativePositionFromTypeIndex(sharedType, offset);
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
function createAbsolutePosition(relativePosition, binding) {
|
|
1462
|
+
return yjs.createAbsolutePositionFromRelativePosition(relativePosition, binding.doc);
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
function shouldUpdatePosition(currentPos, pos) {
|
|
1466
|
+
if (currentPos == null) {
|
|
1467
|
+
if (pos != null) {
|
|
1468
|
+
return true;
|
|
1469
|
+
}
|
|
1470
|
+
} else if (pos == null || !yjs.compareRelativePositions(currentPos, pos)) {
|
|
1471
|
+
return true;
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
return false;
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
function createCursor(name, color) {
|
|
1478
|
+
return {
|
|
1479
|
+
color: color,
|
|
1480
|
+
name: name,
|
|
1481
|
+
selection: null
|
|
1482
|
+
};
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
function destroySelection(binding, selection) {
|
|
1486
|
+
const cursorsContainer = binding.cursorsContainer;
|
|
1487
|
+
|
|
1488
|
+
if (cursorsContainer !== null) {
|
|
1489
|
+
const selections = selection.selections;
|
|
1490
|
+
const selectionsLength = selections.length;
|
|
1491
|
+
|
|
1492
|
+
for (let i = 0; i < selectionsLength; i++) {
|
|
1493
|
+
cursorsContainer.removeChild(selections[i]);
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
function destroyCursor(binding, cursor) {
|
|
1499
|
+
const selection = cursor.selection;
|
|
1500
|
+
|
|
1501
|
+
if (selection !== null) {
|
|
1502
|
+
destroySelection(binding, selection);
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
function getDOMTextNode(element) {
|
|
1507
|
+
let node = element;
|
|
1508
|
+
|
|
1509
|
+
while (node != null) {
|
|
1510
|
+
if (node.nodeType === 3) {
|
|
1511
|
+
// $FlowFixMe: this is a Text
|
|
1512
|
+
return node;
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
node = node.firstChild;
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
return null;
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
function createCursorSelection(cursor, anchorKey, anchorOffset, focusKey, focusOffset) {
|
|
1522
|
+
const color = cursor.color;
|
|
1523
|
+
const caret = document.createElement('span');
|
|
1524
|
+
caret.style.cssText = `position:absolute;top:0;bottom:0;right:-1px;width:1px;background-color:rgb(${color});z-index:10;`;
|
|
1525
|
+
const name = document.createElement('span');
|
|
1526
|
+
name.textContent = cursor.name;
|
|
1527
|
+
name.style.cssText = `position:absolute;left:-2px;top:-16px;background-color:rgb(${color});color:#fff;line-height:12px;height:12px;font-size:12px;padding:2px;font-family:Arial;font-weight:bold`;
|
|
1528
|
+
caret.appendChild(name);
|
|
1529
|
+
return {
|
|
1530
|
+
anchor: {
|
|
1531
|
+
key: anchorKey,
|
|
1532
|
+
offset: anchorOffset
|
|
1533
|
+
},
|
|
1534
|
+
caret,
|
|
1535
|
+
color,
|
|
1536
|
+
focus: {
|
|
1537
|
+
key: focusKey,
|
|
1538
|
+
offset: focusOffset
|
|
1539
|
+
},
|
|
1540
|
+
name,
|
|
1541
|
+
range: document.createRange(),
|
|
1542
|
+
selections: []
|
|
1543
|
+
};
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
function getDOMIndexWithinParent(node) {
|
|
1547
|
+
const parent = node.parentNode;
|
|
1548
|
+
|
|
1549
|
+
if (parent == null) {
|
|
1550
|
+
throw new Error('Should never happen');
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
return [parent, Array.from(parent.childNodes).indexOf(node)];
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
function updateCursor(binding, cursor, nextSelection, nodeMap) {
|
|
1557
|
+
const editor = binding.editor;
|
|
1558
|
+
const rootElement = editor.getRootElement();
|
|
1559
|
+
const cursorsContainer = binding.cursorsContainer;
|
|
1560
|
+
|
|
1561
|
+
if (cursorsContainer === null || rootElement === null) {
|
|
1562
|
+
return;
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
const prevSelection = cursor.selection;
|
|
1566
|
+
|
|
1567
|
+
if (nextSelection === null) {
|
|
1568
|
+
if (prevSelection === null) {
|
|
1569
|
+
return;
|
|
1570
|
+
} else {
|
|
1571
|
+
cursor.selection = null;
|
|
1572
|
+
destroySelection(binding, prevSelection);
|
|
1573
|
+
return;
|
|
1574
|
+
}
|
|
1575
|
+
} else {
|
|
1576
|
+
cursor.selection = nextSelection;
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
const range = nextSelection.range;
|
|
1580
|
+
const caret = nextSelection.caret;
|
|
1581
|
+
const color = nextSelection.color;
|
|
1582
|
+
const selections = nextSelection.selections;
|
|
1583
|
+
const anchor = nextSelection.anchor;
|
|
1584
|
+
const focus = nextSelection.focus;
|
|
1585
|
+
const anchorKey = anchor.key;
|
|
1586
|
+
const focusKey = focus.key;
|
|
1587
|
+
const anchorNode = nodeMap.get(anchorKey);
|
|
1588
|
+
const focusNode = nodeMap.get(focusKey);
|
|
1589
|
+
let anchorDOM = editor.getElementByKey(anchorKey);
|
|
1590
|
+
let focusDOM = editor.getElementByKey(focusKey);
|
|
1591
|
+
let anchorOffset = anchor.offset;
|
|
1592
|
+
let focusOffset = focus.offset;
|
|
1593
|
+
|
|
1594
|
+
if (lexical.$isTextNode(anchorNode)) {
|
|
1595
|
+
anchorDOM = getDOMTextNode(anchorDOM);
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
if (lexical.$isTextNode(focusNode)) {
|
|
1599
|
+
focusDOM = getDOMTextNode(focusDOM);
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
if (anchorNode === undefined || focusNode === undefined || anchorDOM === null || focusDOM === null) {
|
|
1603
|
+
return;
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
if (anchorDOM.nodeName === 'BR') {
|
|
1607
|
+
[anchorDOM, anchorOffset] = getDOMIndexWithinParent(anchorDOM);
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
if (focusDOM.nodeName === 'BR') {
|
|
1611
|
+
[focusDOM, focusOffset] = getDOMIndexWithinParent(focusDOM);
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
const firstChild = anchorDOM.firstChild;
|
|
1615
|
+
|
|
1616
|
+
if (anchorDOM === focusDOM && firstChild != null && firstChild.nodeName === 'BR' && anchorOffset === 0 && focusOffset === 0) {
|
|
1617
|
+
focusOffset = 1;
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
try {
|
|
1621
|
+
range.setStart(anchorDOM, anchorOffset);
|
|
1622
|
+
range.setEnd(focusDOM, focusOffset);
|
|
1623
|
+
} catch (e) {
|
|
1624
|
+
return;
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
if (range.collapsed && (anchorOffset !== focusOffset || anchorKey !== focusKey)) {
|
|
1628
|
+
// Range is backwards, we need to reverse it
|
|
1629
|
+
range.setStart(focusDOM, focusOffset);
|
|
1630
|
+
range.setEnd(anchorDOM, anchorOffset);
|
|
1631
|
+
} // We need to
|
|
1632
|
+
|
|
1633
|
+
|
|
1634
|
+
const rootRect = rootElement.getBoundingClientRect();
|
|
1635
|
+
const computedStyle = getComputedStyle(rootElement);
|
|
1636
|
+
const rootPadding = parseFloat(computedStyle.paddingLeft) + parseFloat(computedStyle.paddingRight);
|
|
1637
|
+
const selectionRects = Array.from(range.getClientRects());
|
|
1638
|
+
let selectionRectsLength = selectionRects.length;
|
|
1639
|
+
const selectionsLength = selections.length;
|
|
1640
|
+
|
|
1641
|
+
for (let i = 0; i < selectionRectsLength; i++) {
|
|
1642
|
+
const selectionRect = selectionRects[i];
|
|
1643
|
+
|
|
1644
|
+
if (selectionRect.width + rootPadding === rootRect.width) {
|
|
1645
|
+
// Exclude selections that span the entire element
|
|
1646
|
+
selectionRects.splice(i--, 1);
|
|
1647
|
+
selectionRectsLength--;
|
|
1648
|
+
continue;
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
let selection = selections[i];
|
|
1652
|
+
|
|
1653
|
+
if (selection === undefined) {
|
|
1654
|
+
selection = document.createElement('span');
|
|
1655
|
+
selections[i] = selection;
|
|
1656
|
+
cursorsContainer.appendChild(selection);
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
const style = `position:absolute;top:${selectionRect.top}px;left:${selectionRect.left}px;height:${selectionRect.height}px;width:${selectionRect.width}px;background-color:rgba(${color}, 0.3);pointer-events:none;z-index:10;`;
|
|
1660
|
+
selection.style.cssText = style;
|
|
1661
|
+
|
|
1662
|
+
if (i === selectionRectsLength - 1) {
|
|
1663
|
+
if (caret.parentNode !== selection) {
|
|
1664
|
+
selection.appendChild(caret);
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
for (let i = selectionsLength - 1; i >= selectionRectsLength; i--) {
|
|
1670
|
+
const selection = selections[i];
|
|
1671
|
+
cursorsContainer.removeChild(selection);
|
|
1672
|
+
selections.pop();
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
function syncLocalCursorPosition(binding, provider) {
|
|
1677
|
+
const awareness = provider.awareness;
|
|
1678
|
+
const localState = awareness.getLocalState();
|
|
1679
|
+
|
|
1680
|
+
if (localState === null) {
|
|
1681
|
+
return;
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
const anchorPos = localState.anchorPos;
|
|
1685
|
+
const focusPos = localState.focusPos;
|
|
1686
|
+
|
|
1687
|
+
if (anchorPos !== null && focusPos !== null) {
|
|
1688
|
+
const anchorAbsPos = createAbsolutePosition(anchorPos, binding);
|
|
1689
|
+
const focusAbsPos = createAbsolutePosition(focusPos, binding);
|
|
1690
|
+
|
|
1691
|
+
if (anchorAbsPos !== null && focusAbsPos !== null) {
|
|
1692
|
+
const [anchorCollabNode, anchorOffset] = getCollabNodeAndOffset(anchorAbsPos.type, anchorAbsPos.index);
|
|
1693
|
+
const [focusCollabNode, focusOffset] = getCollabNodeAndOffset(focusAbsPos.type, focusAbsPos.index);
|
|
1694
|
+
|
|
1695
|
+
if (anchorCollabNode !== null && focusCollabNode !== null) {
|
|
1696
|
+
const anchorKey = anchorCollabNode.getKey();
|
|
1697
|
+
const focusKey = focusCollabNode.getKey();
|
|
1698
|
+
const selection = lexical.$getSelection();
|
|
1699
|
+
|
|
1700
|
+
if (!lexical.$isRangeSelection(selection)) {
|
|
1701
|
+
return;
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
const anchor = selection.anchor;
|
|
1705
|
+
const focus = selection.focus;
|
|
1706
|
+
|
|
1707
|
+
if (anchor.key !== anchorKey || anchor.offset !== anchorOffset) {
|
|
1708
|
+
const anchorNode = lexical.$getNodeByKey(anchorKey);
|
|
1709
|
+
selection.anchor.set(anchorKey, anchorOffset, lexical.$isElementNode(anchorNode) ? 'element' : 'text');
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
if (focus.key !== focusKey || focus.offset !== focusOffset) {
|
|
1713
|
+
const focusNode = lexical.$getNodeByKey(focusKey);
|
|
1714
|
+
selection.focus.set(focusKey, focusOffset, lexical.$isElementNode(focusNode) ? 'element' : 'text');
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
function getCollabNodeAndOffset(sharedType, offset) {
|
|
1722
|
+
// $FlowFixMe: internal field
|
|
1723
|
+
const collabNode = sharedType._collabNode;
|
|
1724
|
+
|
|
1725
|
+
if (collabNode === undefined) {
|
|
1726
|
+
return [null, 0];
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
if (collabNode instanceof CollabElementNode) {
|
|
1730
|
+
const {
|
|
1731
|
+
node,
|
|
1732
|
+
offset: collabNodeOffset
|
|
1733
|
+
} = getPositionFromElementAndOffset(collabNode, offset, true);
|
|
1734
|
+
|
|
1735
|
+
if (node === null) {
|
|
1736
|
+
return [collabNode, 0];
|
|
1737
|
+
} else {
|
|
1738
|
+
return [node, collabNodeOffset];
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
return [null, 0];
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
function syncCursorPositions(binding, provider) {
|
|
1746
|
+
const awarenessStates = Array.from(provider.awareness.getStates());
|
|
1747
|
+
const localClientID = binding.clientID;
|
|
1748
|
+
const cursors = binding.cursors;
|
|
1749
|
+
const editor = binding.editor;
|
|
1750
|
+
const nodeMap = editor._editorState._nodeMap;
|
|
1751
|
+
const visitedClientIDs = new Set();
|
|
1752
|
+
|
|
1753
|
+
for (let i = 0; i < awarenessStates.length; i++) {
|
|
1754
|
+
const awarenessState = awarenessStates[i];
|
|
1755
|
+
const [clientID, awareness] = awarenessState;
|
|
1756
|
+
|
|
1757
|
+
if (clientID !== localClientID) {
|
|
1758
|
+
visitedClientIDs.add(clientID);
|
|
1759
|
+
const {
|
|
1760
|
+
anchorPos,
|
|
1761
|
+
focusPos,
|
|
1762
|
+
name,
|
|
1763
|
+
color,
|
|
1764
|
+
focusing
|
|
1765
|
+
} = awareness;
|
|
1766
|
+
let selection = null;
|
|
1767
|
+
let cursor = cursors.get(clientID);
|
|
1768
|
+
|
|
1769
|
+
if (cursor === undefined) {
|
|
1770
|
+
cursor = createCursor(name, color);
|
|
1771
|
+
cursors.set(clientID, cursor);
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
if (anchorPos !== null && focusPos !== null && focusing) {
|
|
1775
|
+
const anchorAbsPos = createAbsolutePosition(anchorPos, binding);
|
|
1776
|
+
const focusAbsPos = createAbsolutePosition(focusPos, binding);
|
|
1777
|
+
|
|
1778
|
+
if (anchorAbsPos !== null && focusAbsPos !== null) {
|
|
1779
|
+
const [anchorCollabNode, anchorOffset] = getCollabNodeAndOffset(anchorAbsPos.type, anchorAbsPos.index);
|
|
1780
|
+
const [focusCollabNode, focusOffset] = getCollabNodeAndOffset(focusAbsPos.type, focusAbsPos.index);
|
|
1781
|
+
|
|
1782
|
+
if (anchorCollabNode !== null && focusCollabNode !== null) {
|
|
1783
|
+
const anchorKey = anchorCollabNode.getKey();
|
|
1784
|
+
const focusKey = focusCollabNode.getKey();
|
|
1785
|
+
selection = cursor.selection;
|
|
1786
|
+
|
|
1787
|
+
if (selection === null) {
|
|
1788
|
+
selection = createCursorSelection(cursor, anchorKey, anchorOffset, focusKey, focusOffset);
|
|
1789
|
+
} else {
|
|
1790
|
+
const anchor = selection.anchor;
|
|
1791
|
+
const focus = selection.focus;
|
|
1792
|
+
anchor.key = anchorKey;
|
|
1793
|
+
anchor.offset = anchorOffset;
|
|
1794
|
+
focus.key = focusKey;
|
|
1795
|
+
focus.offset = focusOffset;
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
updateCursor(binding, cursor, selection, nodeMap);
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
const allClientIDs = Array.from(cursors.keys());
|
|
1806
|
+
|
|
1807
|
+
for (let i = 0; i < allClientIDs.length; i++) {
|
|
1808
|
+
const clientID = allClientIDs[i];
|
|
1809
|
+
|
|
1810
|
+
if (!visitedClientIDs.has(clientID)) {
|
|
1811
|
+
const cursor = cursors.get(clientID);
|
|
1812
|
+
|
|
1813
|
+
if (cursor !== undefined) {
|
|
1814
|
+
destroyCursor(binding, cursor);
|
|
1815
|
+
cursors.delete(clientID);
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
function syncLexicalSelectionToYjs(binding, provider, prevSelection, nextSelection) {
|
|
1821
|
+
const awareness = provider.awareness;
|
|
1822
|
+
const localState = awareness.getLocalState();
|
|
1823
|
+
|
|
1824
|
+
if (localState === null) {
|
|
1825
|
+
return;
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
const {
|
|
1829
|
+
anchorPos: currentAnchorPos,
|
|
1830
|
+
focusPos: currentFocusPos,
|
|
1831
|
+
name,
|
|
1832
|
+
color,
|
|
1833
|
+
focusing
|
|
1834
|
+
} = localState;
|
|
1835
|
+
let anchorPos = null;
|
|
1836
|
+
let focusPos = null;
|
|
1837
|
+
|
|
1838
|
+
if (nextSelection === null || currentAnchorPos !== null && !nextSelection.is(prevSelection)) {
|
|
1839
|
+
if (prevSelection === null) {
|
|
1840
|
+
return;
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
if (lexical.$isRangeSelection(nextSelection)) {
|
|
1845
|
+
anchorPos = createRelativePosition(nextSelection.anchor, binding);
|
|
1846
|
+
focusPos = createRelativePosition(nextSelection.focus, binding);
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
if (shouldUpdatePosition(currentAnchorPos, anchorPos) || shouldUpdatePosition(currentFocusPos, focusPos)) {
|
|
1850
|
+
awareness.setLocalState({
|
|
1851
|
+
anchorPos,
|
|
1852
|
+
color,
|
|
1853
|
+
focusPos,
|
|
1854
|
+
focusing,
|
|
1855
|
+
name
|
|
1856
|
+
});
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
/**
|
|
1861
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
1862
|
+
*
|
|
1863
|
+
* This source code is licensed under the MIT license found in the
|
|
1864
|
+
* LICENSE file in the root directory of this source tree.
|
|
1865
|
+
*
|
|
1866
|
+
*
|
|
1867
|
+
*/
|
|
1868
|
+
class OffsetView {
|
|
1869
|
+
constructor(offsetMap, firstNode, blockOffsetSize = 1) {
|
|
1870
|
+
this._offsetMap = offsetMap;
|
|
1871
|
+
this._firstNode = firstNode;
|
|
1872
|
+
this._blockOffsetSize = blockOffsetSize;
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
createSelectionFromOffsets(originalStart, originalEnd, diffOffsetView) {
|
|
1876
|
+
const firstNode = this._firstNode;
|
|
1877
|
+
|
|
1878
|
+
if (firstNode === null) {
|
|
1879
|
+
return null;
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
let start = originalStart;
|
|
1883
|
+
let end = originalEnd;
|
|
1884
|
+
let startOffsetNode = $searchForNodeWithOffset(firstNode, start, this._blockOffsetSize);
|
|
1885
|
+
let endOffsetNode = $searchForNodeWithOffset(firstNode, end, this._blockOffsetSize);
|
|
1886
|
+
|
|
1887
|
+
if (diffOffsetView !== undefined) {
|
|
1888
|
+
start = $getAdjustedOffsetFromDiff(start, startOffsetNode, diffOffsetView, this, this._blockOffsetSize);
|
|
1889
|
+
startOffsetNode = $searchForNodeWithOffset(firstNode, start, this._blockOffsetSize);
|
|
1890
|
+
end = $getAdjustedOffsetFromDiff(end, endOffsetNode, diffOffsetView, this, this._blockOffsetSize);
|
|
1891
|
+
endOffsetNode = $searchForNodeWithOffset(firstNode, end, this._blockOffsetSize);
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
if (startOffsetNode === null || endOffsetNode === null) {
|
|
1895
|
+
return null;
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
let startKey = startOffsetNode.key;
|
|
1899
|
+
let endKey = endOffsetNode.key;
|
|
1900
|
+
const startNode = lexical.$getNodeByKey(startKey);
|
|
1901
|
+
const endNode = lexical.$getNodeByKey(endKey);
|
|
1902
|
+
|
|
1903
|
+
if (startNode === null || endNode === null) {
|
|
1904
|
+
return null;
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
let startOffset = 0;
|
|
1908
|
+
let endOffset = 0;
|
|
1909
|
+
let startType = 'element';
|
|
1910
|
+
let endType = 'element';
|
|
1911
|
+
|
|
1912
|
+
if (startOffsetNode.type === 'text') {
|
|
1913
|
+
startOffset = start - startOffsetNode.start;
|
|
1914
|
+
startType = 'text'; // If we are at the edge of a text node and we
|
|
1915
|
+
// don't have a collapsed selection, then let's
|
|
1916
|
+
// try and correct the offset node.
|
|
1917
|
+
|
|
1918
|
+
const sibling = startNode.getNextSibling();
|
|
1919
|
+
|
|
1920
|
+
if (start !== end && startOffset === startNode.getTextContentSize() && lexical.$isTextNode(sibling)) {
|
|
1921
|
+
startOffset = 0;
|
|
1922
|
+
startKey = sibling.__key;
|
|
1923
|
+
}
|
|
1924
|
+
} else if (startOffsetNode.type === 'inline') {
|
|
1925
|
+
startKey = startNode.getParentOrThrow().getKey();
|
|
1926
|
+
startOffset = end > startOffsetNode.start ? startOffsetNode.end : startOffsetNode.start;
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
if (endOffsetNode.type === 'text') {
|
|
1930
|
+
endOffset = end - endOffsetNode.start;
|
|
1931
|
+
endType = 'text';
|
|
1932
|
+
} else if (endOffsetNode.type === 'inline') {
|
|
1933
|
+
endKey = endNode.getParentOrThrow().getKey();
|
|
1934
|
+
endOffset = end > endOffsetNode.start ? endOffsetNode.end : endOffsetNode.start;
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
const selection = lexical.$createRangeSelection();
|
|
1938
|
+
|
|
1939
|
+
if (selection === null) {
|
|
1940
|
+
return null;
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
selection.anchor.set(startKey, startOffset, startType);
|
|
1944
|
+
selection.focus.set(endKey, endOffset, endType);
|
|
1945
|
+
return selection;
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
getOffsetsFromSelection(selection) {
|
|
1949
|
+
const anchor = selection.anchor;
|
|
1950
|
+
const focus = selection.focus;
|
|
1951
|
+
const offsetMap = this._offsetMap;
|
|
1952
|
+
const anchorOffset = anchor.offset;
|
|
1953
|
+
const focusOffset = focus.offset;
|
|
1954
|
+
let start = -1;
|
|
1955
|
+
let end = -1;
|
|
1956
|
+
|
|
1957
|
+
if (anchor.type === 'text') {
|
|
1958
|
+
const offsetNode = offsetMap.get(anchor.key);
|
|
1959
|
+
|
|
1960
|
+
if (offsetNode !== undefined) {
|
|
1961
|
+
start = offsetNode.start + anchorOffset;
|
|
1962
|
+
}
|
|
1963
|
+
} else {
|
|
1964
|
+
const node = anchor.getNode().getDescendantByIndex(anchorOffset);
|
|
1965
|
+
const offsetNode = offsetMap.get(node.getKey());
|
|
1966
|
+
|
|
1967
|
+
if (offsetNode !== undefined) {
|
|
1968
|
+
const isAtEnd = node.getIndexWithinParent() !== anchorOffset;
|
|
1969
|
+
start = isAtEnd ? offsetNode.end : offsetNode.start;
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
if (focus.type === 'text') {
|
|
1974
|
+
const offsetNode = offsetMap.get(focus.key);
|
|
1975
|
+
|
|
1976
|
+
if (offsetNode !== undefined) {
|
|
1977
|
+
end = offsetNode.start + focus.offset;
|
|
1978
|
+
}
|
|
1979
|
+
} else {
|
|
1980
|
+
const node = focus.getNode().getDescendantByIndex(focusOffset);
|
|
1981
|
+
const offsetNode = offsetMap.get(node.getKey());
|
|
1982
|
+
|
|
1983
|
+
if (offsetNode !== undefined) {
|
|
1984
|
+
const isAtEnd = node.getIndexWithinParent() !== focusOffset;
|
|
1985
|
+
end = isAtEnd ? offsetNode.end : offsetNode.start;
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
return [start, end];
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
function $getAdjustedOffsetFromDiff(offset, offsetNode, prevOffsetView, offsetView, blockOffsetSize) {
|
|
1995
|
+
const prevOffsetMap = prevOffsetView._offsetMap;
|
|
1996
|
+
const offsetMap = offsetView._offsetMap;
|
|
1997
|
+
const visited = new Set();
|
|
1998
|
+
let adjustedOffset = offset;
|
|
1999
|
+
let currentNode = offsetNode;
|
|
2000
|
+
|
|
2001
|
+
while (currentNode !== null) {
|
|
2002
|
+
const key = currentNode.key;
|
|
2003
|
+
const prevNode = prevOffsetMap.get(key);
|
|
2004
|
+
const diff = currentNode.end - currentNode.start;
|
|
2005
|
+
visited.add(key);
|
|
2006
|
+
|
|
2007
|
+
if (prevNode === undefined) {
|
|
2008
|
+
adjustedOffset += diff;
|
|
2009
|
+
} else {
|
|
2010
|
+
const prevDiff = prevNode.end - prevNode.start;
|
|
2011
|
+
|
|
2012
|
+
if (prevDiff !== diff) {
|
|
2013
|
+
adjustedOffset += diff - prevDiff;
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
const sibling = currentNode.prev;
|
|
2018
|
+
|
|
2019
|
+
if (sibling !== null) {
|
|
2020
|
+
currentNode = sibling;
|
|
2021
|
+
continue;
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
let parent = currentNode.parent;
|
|
2025
|
+
|
|
2026
|
+
while (parent !== null) {
|
|
2027
|
+
let parentSibling = parent.prev;
|
|
2028
|
+
|
|
2029
|
+
if (parentSibling !== null) {
|
|
2030
|
+
const parentSiblingKey = parentSibling.key;
|
|
2031
|
+
const prevParentSibling = prevOffsetMap.get(parentSiblingKey);
|
|
2032
|
+
const parentDiff = parentSibling.end - parentSibling.start;
|
|
2033
|
+
visited.add(parentSiblingKey);
|
|
2034
|
+
|
|
2035
|
+
if (prevParentSibling === undefined) {
|
|
2036
|
+
adjustedOffset += parentDiff;
|
|
2037
|
+
} else {
|
|
2038
|
+
const prevParentDiff = prevParentSibling.end - prevParentSibling.start;
|
|
2039
|
+
|
|
2040
|
+
if (prevParentDiff !== parentDiff) {
|
|
2041
|
+
adjustedOffset += parentDiff - prevParentDiff;
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
parentSibling = parentSibling.prev;
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
parent = parent.parent;
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
break;
|
|
2052
|
+
} // Now traverse through the old offsets nodes and find any nodes we missed
|
|
2053
|
+
// above, because they were not in the latest offset node view (they have been
|
|
2054
|
+
// deleted).
|
|
2055
|
+
|
|
2056
|
+
|
|
2057
|
+
const prevFirstNode = prevOffsetView._firstNode;
|
|
2058
|
+
|
|
2059
|
+
if (prevFirstNode !== null) {
|
|
2060
|
+
currentNode = $searchForNodeWithOffset(prevFirstNode, offset, blockOffsetSize);
|
|
2061
|
+
let alreadyVisistedParentOfCurrentNode = false;
|
|
2062
|
+
|
|
2063
|
+
while (currentNode !== null) {
|
|
2064
|
+
if (!visited.has(currentNode.key)) {
|
|
2065
|
+
alreadyVisistedParentOfCurrentNode = true;
|
|
2066
|
+
break;
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
currentNode = currentNode.parent;
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
if (!alreadyVisistedParentOfCurrentNode) {
|
|
2073
|
+
while (currentNode !== null) {
|
|
2074
|
+
const key = currentNode.key;
|
|
2075
|
+
|
|
2076
|
+
if (!visited.has(key)) {
|
|
2077
|
+
const node = offsetMap.get(key);
|
|
2078
|
+
const prevDiff = currentNode.end - currentNode.start;
|
|
2079
|
+
|
|
2080
|
+
if (node === undefined) {
|
|
2081
|
+
adjustedOffset -= prevDiff;
|
|
2082
|
+
} else {
|
|
2083
|
+
const diff = node.end - node.start;
|
|
2084
|
+
|
|
2085
|
+
if (prevDiff !== diff) {
|
|
2086
|
+
adjustedOffset += diff - prevDiff;
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
currentNode = currentNode.prev;
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
return adjustedOffset;
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2099
|
+
function $searchForNodeWithOffset(firstNode, offset, blockOffsetSize) {
|
|
2100
|
+
let currentNode = firstNode;
|
|
2101
|
+
|
|
2102
|
+
while (currentNode !== null) {
|
|
2103
|
+
const end = currentNode.end + (currentNode.type !== 'element' || blockOffsetSize === 0 ? 1 : 0);
|
|
2104
|
+
|
|
2105
|
+
if (offset < end) {
|
|
2106
|
+
const child = currentNode.child;
|
|
2107
|
+
|
|
2108
|
+
if (child !== null) {
|
|
2109
|
+
currentNode = child;
|
|
2110
|
+
continue;
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
return currentNode;
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
const sibling = currentNode.next;
|
|
2117
|
+
|
|
2118
|
+
if (sibling === null) {
|
|
2119
|
+
break;
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
currentNode = sibling;
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
return null;
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2128
|
+
function $createInternalOffsetNode(child, type, start, end, key, parent) {
|
|
2129
|
+
// $FlowFixMe: not sure why Flow doesn't like this?
|
|
2130
|
+
return {
|
|
2131
|
+
child,
|
|
2132
|
+
end,
|
|
2133
|
+
key,
|
|
2134
|
+
next: null,
|
|
2135
|
+
parent,
|
|
2136
|
+
prev: null,
|
|
2137
|
+
start,
|
|
2138
|
+
type
|
|
2139
|
+
};
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
function $createOffsetNode(state, key, parent, nodeMap, offsetMap, blockOffsetSize) {
|
|
2143
|
+
const node = nodeMap.get(key);
|
|
2144
|
+
|
|
2145
|
+
if (node === undefined) {
|
|
2146
|
+
{
|
|
2147
|
+
throw Error(`createOffsetModel: could not find node by key`);
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
|
|
2151
|
+
const start = state.offset;
|
|
2152
|
+
|
|
2153
|
+
if (lexical.$isElementNode(node)) {
|
|
2154
|
+
const childKeys = node.__children;
|
|
2155
|
+
const blockIsEmpty = childKeys.length === 0;
|
|
2156
|
+
const child = blockIsEmpty ? null : $createOffsetChild(state, childKeys, null, nodeMap, offsetMap, blockOffsetSize); // If the prev node was not a block or the block is empty, we should
|
|
2157
|
+
// account for the user being able to selection the block (due to the \n).
|
|
2158
|
+
|
|
2159
|
+
if (!state.prevIsBlock || blockIsEmpty) {
|
|
2160
|
+
state.prevIsBlock = true;
|
|
2161
|
+
state.offset += blockOffsetSize;
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2164
|
+
const offsetNode = $createInternalOffsetNode(child, 'element', start, start, key, parent);
|
|
2165
|
+
|
|
2166
|
+
if (child !== null) {
|
|
2167
|
+
child.parent = offsetNode;
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
const end = state.offset;
|
|
2171
|
+
offsetNode.end = end;
|
|
2172
|
+
offsetMap.set(key, offsetNode);
|
|
2173
|
+
return offsetNode;
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
state.prevIsBlock = false;
|
|
2177
|
+
const isText = lexical.$isTextNode(node); // $FlowFixMe: isText means __text is available
|
|
2178
|
+
|
|
2179
|
+
const length = isText ? node.__text.length : 1;
|
|
2180
|
+
const end = state.offset += length;
|
|
2181
|
+
const offsetNode = $createInternalOffsetNode(null, isText ? 'text' : 'inline', start, end, key, parent);
|
|
2182
|
+
offsetMap.set(key, offsetNode);
|
|
2183
|
+
return offsetNode;
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2186
|
+
function $createOffsetChild(state, children, parent, nodeMap, offsetMap, blockOffsetSize) {
|
|
2187
|
+
let firstNode = null;
|
|
2188
|
+
let currentNode = null;
|
|
2189
|
+
const childrenLength = children.length;
|
|
2190
|
+
|
|
2191
|
+
for (let i = 0; i < childrenLength; i++) {
|
|
2192
|
+
const childKey = children[i];
|
|
2193
|
+
const offsetNode = $createOffsetNode(state, childKey, parent, nodeMap, offsetMap, blockOffsetSize);
|
|
2194
|
+
|
|
2195
|
+
if (currentNode === null) {
|
|
2196
|
+
firstNode = offsetNode;
|
|
2197
|
+
} else {
|
|
2198
|
+
offsetNode.prev = currentNode;
|
|
2199
|
+
currentNode.next = offsetNode;
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2202
|
+
currentNode = offsetNode;
|
|
2203
|
+
}
|
|
2204
|
+
|
|
2205
|
+
return firstNode;
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
function $createOffsetView(editor, blockOffsetSize = 1, editorState) {
|
|
2209
|
+
const targetEditorState = editorState || editor._pendingEditorState || editor._editorState;
|
|
2210
|
+
const nodeMap = targetEditorState._nodeMap; // $FlowFixMe: root is always in the Map
|
|
2211
|
+
|
|
2212
|
+
const root = nodeMap.get('root');
|
|
2213
|
+
const offsetMap = new Map();
|
|
2214
|
+
const state = {
|
|
2215
|
+
offset: 0,
|
|
2216
|
+
prevIsBlock: false
|
|
2217
|
+
};
|
|
2218
|
+
const node = $createOffsetChild(state, root.__children, null, nodeMap, offsetMap, blockOffsetSize);
|
|
2219
|
+
return new OffsetView(offsetMap, node, blockOffsetSize);
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
/**
|
|
2223
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2224
|
+
*
|
|
2225
|
+
* This source code is licensed under the MIT license found in the
|
|
2226
|
+
* LICENSE file in the root directory of this source tree.
|
|
2227
|
+
*
|
|
2228
|
+
*
|
|
2229
|
+
*/
|
|
2230
|
+
|
|
2231
|
+
function syncEvent(binding, event) {
|
|
2232
|
+
const {
|
|
2233
|
+
target
|
|
2234
|
+
} = event;
|
|
2235
|
+
const collabNode = getOrInitCollabNodeFromSharedType(binding, target); // $FlowFixMe: internal field
|
|
2236
|
+
|
|
2237
|
+
const decoratorStateValue = target._lexicalValue; // Check if this event relates to a decorator state change.
|
|
2238
|
+
|
|
2239
|
+
if (decoratorStateValue !== undefined && collabNode instanceof CollabDecoratorNode) {
|
|
2240
|
+
if (target instanceof yjs.Map) {
|
|
2241
|
+
// Sync decorator state value
|
|
2242
|
+
syncYjsDecoratorMapToLexical(binding, collabNode, target, decoratorStateValue, event.keysChanged);
|
|
2243
|
+
} else if (target instanceof yjs.Array && lexical.isDecoratorArray(decoratorStateValue)) {
|
|
2244
|
+
// Sync decorator state value
|
|
2245
|
+
const deltas = event.delta;
|
|
2246
|
+
let offset = 0;
|
|
2247
|
+
|
|
2248
|
+
for (let i = 0; i < deltas.length; i++) {
|
|
2249
|
+
const delta = deltas[i];
|
|
2250
|
+
const retainOp = delta.retain;
|
|
2251
|
+
const deleteOp = delta.delete;
|
|
2252
|
+
const insertOp = delta.insert;
|
|
2253
|
+
|
|
2254
|
+
if (retainOp !== undefined) {
|
|
2255
|
+
offset += retainOp;
|
|
2256
|
+
} else if (deleteOp !== undefined) {
|
|
2257
|
+
mutationFromCollab(() => {
|
|
2258
|
+
const elements = decoratorStateValue._array.slice(offset, offset + deleteOp);
|
|
2259
|
+
|
|
2260
|
+
elements.forEach(element => {
|
|
2261
|
+
if (lexical.isDecoratorArray(element) || lexical.isDecoratorMap(element)) {
|
|
2262
|
+
element.destroy();
|
|
2263
|
+
}
|
|
2264
|
+
});
|
|
2265
|
+
decoratorStateValue.splice(offset, deleteOp);
|
|
2266
|
+
});
|
|
2267
|
+
} else if (insertOp !== undefined) {
|
|
2268
|
+
syncYjsDecoratorArrayValueToLexical(binding, collabNode, target, decoratorStateValue, offset);
|
|
2269
|
+
} else {
|
|
2270
|
+
throw new Error('Not supported');
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
return;
|
|
2276
|
+
}
|
|
2277
|
+
|
|
2278
|
+
if (collabNode instanceof CollabElementNode && event instanceof yjs.YTextEvent) {
|
|
2279
|
+
const {
|
|
2280
|
+
keysChanged,
|
|
2281
|
+
childListChanged,
|
|
2282
|
+
delta
|
|
2283
|
+
} = event; // Update
|
|
2284
|
+
|
|
2285
|
+
if (keysChanged.size > 0) {
|
|
2286
|
+
collabNode.syncPropertiesFromYjs(binding, keysChanged);
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2289
|
+
if (childListChanged) {
|
|
2290
|
+
collabNode.applyChildrenYjsDelta(binding, delta);
|
|
2291
|
+
collabNode.syncChildrenFromYjs(binding);
|
|
2292
|
+
}
|
|
2293
|
+
} else if (collabNode instanceof CollabTextNode && event instanceof yjs.YMapEvent) {
|
|
2294
|
+
const {
|
|
2295
|
+
keysChanged
|
|
2296
|
+
} = event; // Update
|
|
2297
|
+
|
|
2298
|
+
if (keysChanged.size > 0) {
|
|
2299
|
+
collabNode.syncPropertiesAndTextFromYjs(binding, keysChanged);
|
|
2300
|
+
}
|
|
2301
|
+
} else if (collabNode instanceof CollabDecoratorNode && event instanceof yjs.YXmlEvent) {
|
|
2302
|
+
const {
|
|
2303
|
+
attributesChanged
|
|
2304
|
+
} = event; // Update
|
|
2305
|
+
|
|
2306
|
+
if (attributesChanged.size > 0) {
|
|
2307
|
+
collabNode.syncPropertiesFromYjs(binding, attributesChanged);
|
|
2308
|
+
}
|
|
2309
|
+
} else {
|
|
2310
|
+
throw new Error('Should never happen');
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
function syncYjsChangesToLexical(binding, provider, events) {
|
|
2315
|
+
const editor = binding.editor;
|
|
2316
|
+
const currentEditorState = editor._editorState;
|
|
2317
|
+
editor.update(() => {
|
|
2318
|
+
// $FlowFixMe: this is always true
|
|
2319
|
+
const pendingEditorState = editor._pendingEditorState;
|
|
2320
|
+
|
|
2321
|
+
for (let i = 0; i < events.length; i++) {
|
|
2322
|
+
const event = events[i];
|
|
2323
|
+
syncEvent(binding, event);
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2326
|
+
const selection = lexical.$getSelection();
|
|
2327
|
+
|
|
2328
|
+
if (lexical.$isRangeSelection(selection)) {
|
|
2329
|
+
// We can't use Yjs's cursor position here, as it doesn't always
|
|
2330
|
+
// handle selection recovery correctly – especially on elements that
|
|
2331
|
+
// get moved around or split. So instead, we roll our own solution.
|
|
2332
|
+
if (doesSelectionNeedRecovering(selection)) {
|
|
2333
|
+
const prevSelection = currentEditorState._selection;
|
|
2334
|
+
|
|
2335
|
+
if (lexical.$isRangeSelection(prevSelection)) {
|
|
2336
|
+
const prevOffsetView = $createOffsetView(editor, 0, currentEditorState);
|
|
2337
|
+
const nextOffsetView = $createOffsetView(editor, 0, pendingEditorState);
|
|
2338
|
+
const [start, end] = prevOffsetView.getOffsetsFromSelection(prevSelection);
|
|
2339
|
+
const nextSelection = nextOffsetView.createSelectionFromOffsets(start, end, prevOffsetView);
|
|
2340
|
+
|
|
2341
|
+
if (nextSelection !== null) {
|
|
2342
|
+
lexical.$setSelection(nextSelection);
|
|
2343
|
+
} else {
|
|
2344
|
+
// Fallback is to use the Yjs cursor position
|
|
2345
|
+
syncLocalCursorPosition(binding, provider);
|
|
2346
|
+
|
|
2347
|
+
if (doesSelectionNeedRecovering(selection)) {
|
|
2348
|
+
const root = lexical.$getRoot(); // If there was a collision on the top level paragraph
|
|
2349
|
+
// we need to re-add a paragraph
|
|
2350
|
+
|
|
2351
|
+
if (root.getChildrenSize() === 0) {
|
|
2352
|
+
root.append(lexical.$createParagraphNode());
|
|
2353
|
+
} // Fallback
|
|
2354
|
+
|
|
2355
|
+
|
|
2356
|
+
lexical.$getRoot().selectEnd();
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
syncLexicalSelectionToYjs(binding, provider, prevSelection, lexical.$getSelection());
|
|
2362
|
+
} else {
|
|
2363
|
+
syncLocalCursorPosition(binding, provider);
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
}, {
|
|
2367
|
+
onUpdate: () => {
|
|
2368
|
+
syncCursorPositions(binding, provider);
|
|
2369
|
+
},
|
|
2370
|
+
skipTransforms: true,
|
|
2371
|
+
tag: 'collaboration'
|
|
2372
|
+
});
|
|
2373
|
+
}
|
|
2374
|
+
|
|
2375
|
+
function handleNormalizationMergeConflicts(binding, normalizedNodes) {
|
|
2376
|
+
// We handle the merge opperations here
|
|
2377
|
+
const normalizedNodesKeys = Array.from(normalizedNodes);
|
|
2378
|
+
const collabNodeMap = binding.collabNodeMap;
|
|
2379
|
+
const mergedNodes = [];
|
|
2380
|
+
|
|
2381
|
+
for (let i = 0; i < normalizedNodesKeys.length; i++) {
|
|
2382
|
+
const nodeKey = normalizedNodesKeys[i];
|
|
2383
|
+
const lexicalNode = lexical.$getNodeByKey(nodeKey);
|
|
2384
|
+
const collabNode = collabNodeMap.get(nodeKey);
|
|
2385
|
+
|
|
2386
|
+
if (collabNode instanceof CollabTextNode) {
|
|
2387
|
+
if (lexical.$isTextNode(lexicalNode)) {
|
|
2388
|
+
// We mutate the text collab nodes after removing
|
|
2389
|
+
// all the dead nodes first, otherwise offsets break.
|
|
2390
|
+
mergedNodes.push([collabNode, lexicalNode.__text]);
|
|
2391
|
+
} else {
|
|
2392
|
+
const offset = collabNode.getOffset();
|
|
2393
|
+
|
|
2394
|
+
if (offset === -1) {
|
|
2395
|
+
continue;
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2398
|
+
const parent = collabNode._parent;
|
|
2399
|
+
collabNode._normalized = true;
|
|
2400
|
+
|
|
2401
|
+
parent._xmlText.delete(offset, 1);
|
|
2402
|
+
|
|
2403
|
+
collabNodeMap.delete(nodeKey);
|
|
2404
|
+
const parentChildren = parent._children;
|
|
2405
|
+
const index = parentChildren.indexOf(collabNode);
|
|
2406
|
+
parentChildren.splice(index, 1);
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
|
|
2411
|
+
for (let i = 0; i < mergedNodes.length; i++) {
|
|
2412
|
+
const [collabNode, text] = mergedNodes[i];
|
|
2413
|
+
collabNode._text = text;
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2417
|
+
function syncLexicalUpdateToYjs(binding, provider, prevEditorState, currEditorState, dirtyElements, dirtyLeaves, normalizedNodes, tags) {
|
|
2418
|
+
syncWithTransaction(binding, () => {
|
|
2419
|
+
currEditorState.read(() => {
|
|
2420
|
+
// We check if the update has come from a origin where the origin
|
|
2421
|
+
// was the collaboration binding previously. This can help us
|
|
2422
|
+
// prevent unecessarily re-diffing and possible re-applying
|
|
2423
|
+
// the same change editor state again. For example, if a user
|
|
2424
|
+
// types a character and we get it, we don't want to then insert
|
|
2425
|
+
// the same character again. The exception to this heuristic is
|
|
2426
|
+
// when we need to handle normalization merge conflicts.
|
|
2427
|
+
if (tags.has('collaboration')) {
|
|
2428
|
+
if (normalizedNodes.size > 0) {
|
|
2429
|
+
handleNormalizationMergeConflicts(binding, normalizedNodes);
|
|
2430
|
+
}
|
|
2431
|
+
|
|
2432
|
+
return;
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
if (dirtyElements.has('root')) {
|
|
2436
|
+
const prevNodeMap = prevEditorState._nodeMap;
|
|
2437
|
+
const nextLexicalRoot = lexical.$getRoot();
|
|
2438
|
+
const collabRoot = binding.root;
|
|
2439
|
+
collabRoot.syncPropertiesFromLexical(binding, nextLexicalRoot, prevNodeMap);
|
|
2440
|
+
collabRoot.syncChildrenFromLexical(binding, nextLexicalRoot, prevNodeMap, dirtyElements, dirtyLeaves);
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
const selection = lexical.$getSelection();
|
|
2444
|
+
const prevSelection = prevEditorState._selection;
|
|
2445
|
+
syncLexicalSelectionToYjs(binding, provider, prevSelection, selection);
|
|
2446
|
+
});
|
|
2447
|
+
});
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
/**
|
|
2451
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2452
|
+
*
|
|
2453
|
+
* This source code is licensed under the MIT license found in the
|
|
2454
|
+
* LICENSE file in the root directory of this source tree.
|
|
2455
|
+
*
|
|
2456
|
+
*
|
|
2457
|
+
*/
|
|
2458
|
+
function createUndoManager(binding, root) {
|
|
2459
|
+
return new yjs.UndoManager(root, {
|
|
2460
|
+
trackedOrigins: new Set([binding, null])
|
|
2461
|
+
});
|
|
2462
|
+
}
|
|
2463
|
+
function initLocalState(provider, name, color, focusing) {
|
|
2464
|
+
provider.awareness.setLocalState({
|
|
2465
|
+
anchorPos: null,
|
|
2466
|
+
color,
|
|
2467
|
+
focusPos: null,
|
|
2468
|
+
focusing: focusing,
|
|
2469
|
+
name
|
|
2470
|
+
});
|
|
2471
|
+
}
|
|
2472
|
+
function setLocalStateFocus(provider, focusing) {
|
|
2473
|
+
const {
|
|
2474
|
+
awareness
|
|
2475
|
+
} = provider;
|
|
2476
|
+
awareness.setLocalState({ ...awareness.getLocalState(),
|
|
2477
|
+
focusing
|
|
2478
|
+
});
|
|
2479
|
+
}
|
|
2480
|
+
|
|
2481
|
+
exports.createBinding = createBinding;
|
|
2482
|
+
exports.createUndoManager = createUndoManager;
|
|
2483
|
+
exports.initLocalState = initLocalState;
|
|
2484
|
+
exports.setLocalStateFocus = setLocalStateFocus;
|
|
2485
|
+
exports.syncCursorPositions = syncCursorPositions;
|
|
2486
|
+
exports.syncLexicalUpdateToYjs = syncLexicalUpdateToYjs;
|
|
2487
|
+
exports.syncYjsChangesToLexical = syncYjsChangesToLexical;
|