@lexical/yjs 0.44.1-nightly.20260519.0 → 0.45.1-dev.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/{RenderSnapshot.d.ts → dist/RenderSnapshot.d.ts} +0 -7
- package/dist/simpleDiffWithCursor.d.ts +12 -0
- package/package.json +31 -16
- package/src/Bindings.ts +135 -0
- package/src/CollabDecoratorNode.ts +112 -0
- package/src/CollabElementNode.ts +696 -0
- package/src/CollabLineBreakNode.ts +70 -0
- package/src/CollabTextNode.ts +179 -0
- package/src/CollabV2Mapping.ts +117 -0
- package/src/RenderSnapshot.ts +97 -0
- package/src/SyncCursors.ts +823 -0
- package/src/SyncEditorStates.ts +469 -0
- package/src/SyncV2.ts +871 -0
- package/src/Utils.ts +648 -0
- package/src/index.ts +151 -0
- package/src/simpleDiffWithCursor.ts +49 -0
- package/src/types.ts +27 -0
- /package/{Bindings.d.ts → dist/Bindings.d.ts} +0 -0
- /package/{CollabDecoratorNode.d.ts → dist/CollabDecoratorNode.d.ts} +0 -0
- /package/{CollabElementNode.d.ts → dist/CollabElementNode.d.ts} +0 -0
- /package/{CollabLineBreakNode.d.ts → dist/CollabLineBreakNode.d.ts} +0 -0
- /package/{CollabTextNode.d.ts → dist/CollabTextNode.d.ts} +0 -0
- /package/{CollabV2Mapping.d.ts → dist/CollabV2Mapping.d.ts} +0 -0
- /package/{LexicalYjs.dev.js → dist/LexicalYjs.dev.js} +0 -0
- /package/{LexicalYjs.dev.mjs → dist/LexicalYjs.dev.mjs} +0 -0
- /package/{LexicalYjs.js → dist/LexicalYjs.js} +0 -0
- /package/{LexicalYjs.js.flow → dist/LexicalYjs.js.flow} +0 -0
- /package/{LexicalYjs.mjs → dist/LexicalYjs.mjs} +0 -0
- /package/{LexicalYjs.node.mjs → dist/LexicalYjs.node.mjs} +0 -0
- /package/{LexicalYjs.prod.js → dist/LexicalYjs.prod.js} +0 -0
- /package/{LexicalYjs.prod.mjs → dist/LexicalYjs.prod.mjs} +0 -0
- /package/{SyncCursors.d.ts → dist/SyncCursors.d.ts} +0 -0
- /package/{SyncEditorStates.d.ts → dist/SyncEditorStates.d.ts} +0 -0
- /package/{SyncV2.d.ts → dist/SyncV2.d.ts} +0 -0
- /package/{Utils.d.ts → dist/Utils.d.ts} +0 -0
- /package/{index.d.ts → dist/index.d.ts} +0 -0
- /package/{types.d.ts → dist/types.d.ts} +0 -0
package/src/Utils.ts
ADDED
|
@@ -0,0 +1,648 @@
|
|
|
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
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type {BaseBinding, Binding, YjsNode} from '.';
|
|
10
|
+
|
|
11
|
+
import invariant from '@lexical/internal/invariant';
|
|
12
|
+
import {
|
|
13
|
+
$getNodeByKey,
|
|
14
|
+
$getRoot,
|
|
15
|
+
$getWritableNodeState,
|
|
16
|
+
$isDecoratorNode,
|
|
17
|
+
$isElementNode,
|
|
18
|
+
$isLineBreakNode,
|
|
19
|
+
$isRootNode,
|
|
20
|
+
$isTextNode,
|
|
21
|
+
createEditor,
|
|
22
|
+
DecoratorNode,
|
|
23
|
+
EditorState,
|
|
24
|
+
ElementNode,
|
|
25
|
+
isLexicalEditor,
|
|
26
|
+
LexicalNode,
|
|
27
|
+
NodeKey,
|
|
28
|
+
RangeSelection,
|
|
29
|
+
TextNode,
|
|
30
|
+
} from 'lexical';
|
|
31
|
+
import {Doc, Map as YMap, XmlElement, XmlText} from 'yjs';
|
|
32
|
+
|
|
33
|
+
import {isBindingV1} from './Bindings';
|
|
34
|
+
import {
|
|
35
|
+
$createCollabDecoratorNode,
|
|
36
|
+
CollabDecoratorNode,
|
|
37
|
+
} from './CollabDecoratorNode';
|
|
38
|
+
import {$createCollabElementNode, CollabElementNode} from './CollabElementNode';
|
|
39
|
+
import {
|
|
40
|
+
$createCollabLineBreakNode,
|
|
41
|
+
CollabLineBreakNode,
|
|
42
|
+
} from './CollabLineBreakNode';
|
|
43
|
+
import {$createCollabTextNode, CollabTextNode} from './CollabTextNode';
|
|
44
|
+
|
|
45
|
+
const baseExcludedProperties = new Set<string>([
|
|
46
|
+
'__key',
|
|
47
|
+
'__parent',
|
|
48
|
+
'__next',
|
|
49
|
+
'__prev',
|
|
50
|
+
'__state',
|
|
51
|
+
]);
|
|
52
|
+
const elementExcludedProperties = new Set<string>([
|
|
53
|
+
'__first',
|
|
54
|
+
'__last',
|
|
55
|
+
'__size',
|
|
56
|
+
]);
|
|
57
|
+
const rootExcludedProperties = new Set<string>(['__cachedText']);
|
|
58
|
+
const textExcludedProperties = new Set<string>(['__text']);
|
|
59
|
+
|
|
60
|
+
function isExcludedProperty(
|
|
61
|
+
name: string,
|
|
62
|
+
node: LexicalNode,
|
|
63
|
+
binding: BaseBinding,
|
|
64
|
+
): boolean {
|
|
65
|
+
if (
|
|
66
|
+
baseExcludedProperties.has(name) ||
|
|
67
|
+
typeof (node as unknown as Record<string, unknown>)[name] === 'function'
|
|
68
|
+
) {
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if ($isTextNode(node)) {
|
|
73
|
+
if (textExcludedProperties.has(name)) {
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
} else if ($isElementNode(node)) {
|
|
77
|
+
if (
|
|
78
|
+
elementExcludedProperties.has(name) ||
|
|
79
|
+
($isRootNode(node) && rootExcludedProperties.has(name))
|
|
80
|
+
) {
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const nodeKlass = node.constructor;
|
|
86
|
+
const excludedProperties = binding.excludedProperties.get(nodeKlass);
|
|
87
|
+
return excludedProperties != null && excludedProperties.has(name);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function initializeNodeProperties(binding: BaseBinding): void {
|
|
91
|
+
const {editor, nodeProperties} = binding;
|
|
92
|
+
editor.update(() => {
|
|
93
|
+
editor._nodes.forEach(nodeInfo => {
|
|
94
|
+
const node = new nodeInfo.klass();
|
|
95
|
+
const defaultProperties: {[property: string]: unknown} = {};
|
|
96
|
+
for (const [property, value] of Object.entries(node)) {
|
|
97
|
+
if (!isExcludedProperty(property, node, binding)) {
|
|
98
|
+
defaultProperties[property] = value;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
nodeProperties.set(node.__type, Object.freeze(defaultProperties));
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function getDefaultNodeProperties(
|
|
107
|
+
node: LexicalNode,
|
|
108
|
+
binding: BaseBinding,
|
|
109
|
+
): {[property: string]: unknown} {
|
|
110
|
+
const type = node.__type;
|
|
111
|
+
const {nodeProperties} = binding;
|
|
112
|
+
const properties = nodeProperties.get(type);
|
|
113
|
+
invariant(
|
|
114
|
+
properties !== undefined,
|
|
115
|
+
'Node properties for %s not initialized for sync',
|
|
116
|
+
type,
|
|
117
|
+
);
|
|
118
|
+
return properties;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function getIndexOfYjsNode(
|
|
122
|
+
yjsParentNode: YjsNode,
|
|
123
|
+
yjsNode: YjsNode,
|
|
124
|
+
): number {
|
|
125
|
+
let node = yjsParentNode.firstChild;
|
|
126
|
+
let i = -1;
|
|
127
|
+
|
|
128
|
+
if (node === null) {
|
|
129
|
+
return -1;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
do {
|
|
133
|
+
i++;
|
|
134
|
+
|
|
135
|
+
if (node === yjsNode) {
|
|
136
|
+
return i;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// @ts-expect-error Sibling exists but type is not available from YJS.
|
|
140
|
+
node = node.nextSibling;
|
|
141
|
+
|
|
142
|
+
if (node === null) {
|
|
143
|
+
return -1;
|
|
144
|
+
}
|
|
145
|
+
} while (node !== null);
|
|
146
|
+
|
|
147
|
+
return i;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function $createCollabNodeFromLexicalNode(
|
|
151
|
+
binding: Binding,
|
|
152
|
+
lexicalNode: LexicalNode,
|
|
153
|
+
parent: CollabElementNode,
|
|
154
|
+
):
|
|
155
|
+
| CollabElementNode
|
|
156
|
+
| CollabTextNode
|
|
157
|
+
| CollabLineBreakNode
|
|
158
|
+
| CollabDecoratorNode {
|
|
159
|
+
const nodeType = lexicalNode.__type;
|
|
160
|
+
let collabNode;
|
|
161
|
+
|
|
162
|
+
if ($isElementNode(lexicalNode)) {
|
|
163
|
+
const xmlText = new XmlText();
|
|
164
|
+
collabNode = $createCollabElementNode(xmlText, parent, nodeType);
|
|
165
|
+
collabNode.syncPropertiesFromLexical(binding, lexicalNode, null);
|
|
166
|
+
collabNode.syncChildrenFromLexical(binding, lexicalNode, null, null, null);
|
|
167
|
+
} else if ($isTextNode(lexicalNode)) {
|
|
168
|
+
// TODO create a token text node for token, segmented nodes.
|
|
169
|
+
const map = new YMap();
|
|
170
|
+
collabNode = $createCollabTextNode(
|
|
171
|
+
map,
|
|
172
|
+
lexicalNode.__text,
|
|
173
|
+
parent,
|
|
174
|
+
nodeType,
|
|
175
|
+
);
|
|
176
|
+
collabNode.syncPropertiesAndTextFromLexical(binding, lexicalNode, null);
|
|
177
|
+
} else if ($isLineBreakNode(lexicalNode)) {
|
|
178
|
+
const map = new YMap();
|
|
179
|
+
map.set('__type', 'linebreak');
|
|
180
|
+
collabNode = $createCollabLineBreakNode(map, parent);
|
|
181
|
+
} else if ($isDecoratorNode(lexicalNode)) {
|
|
182
|
+
const xmlElem = new XmlElement();
|
|
183
|
+
collabNode = $createCollabDecoratorNode(xmlElem, parent, nodeType);
|
|
184
|
+
collabNode.syncPropertiesFromLexical(binding, lexicalNode, null);
|
|
185
|
+
} else {
|
|
186
|
+
invariant(false, 'Expected text, element, decorator, or linebreak node');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
collabNode._key = lexicalNode.__key;
|
|
190
|
+
return collabNode;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function getNodeTypeFromSharedType(
|
|
194
|
+
sharedType: XmlText | YMap<unknown> | XmlElement,
|
|
195
|
+
): string | undefined {
|
|
196
|
+
const type = sharedTypeGet(sharedType, '__type');
|
|
197
|
+
invariant(
|
|
198
|
+
typeof type === 'string' || typeof type === 'undefined',
|
|
199
|
+
'Expected shared type to include type attribute',
|
|
200
|
+
);
|
|
201
|
+
return type;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function $getOrInitCollabNodeFromSharedType(
|
|
205
|
+
binding: Binding,
|
|
206
|
+
sharedType: XmlText | YMap<unknown> | XmlElement,
|
|
207
|
+
parent?: CollabElementNode,
|
|
208
|
+
):
|
|
209
|
+
| CollabElementNode
|
|
210
|
+
| CollabTextNode
|
|
211
|
+
| CollabLineBreakNode
|
|
212
|
+
| CollabDecoratorNode {
|
|
213
|
+
const collabNode = sharedType._collabNode;
|
|
214
|
+
|
|
215
|
+
if (collabNode === undefined) {
|
|
216
|
+
const registeredNodes = binding.editor._nodes;
|
|
217
|
+
const type = getNodeTypeFromSharedType(sharedType);
|
|
218
|
+
invariant(
|
|
219
|
+
typeof type === 'string',
|
|
220
|
+
'Expected shared type to include type attribute',
|
|
221
|
+
);
|
|
222
|
+
const nodeInfo = registeredNodes.get(type);
|
|
223
|
+
invariant(nodeInfo !== undefined, 'Node %s is not registered', type);
|
|
224
|
+
|
|
225
|
+
const sharedParent = sharedType.parent;
|
|
226
|
+
const targetParent =
|
|
227
|
+
parent === undefined && sharedParent !== null
|
|
228
|
+
? $getOrInitCollabNodeFromSharedType(
|
|
229
|
+
binding,
|
|
230
|
+
sharedParent as XmlText | YMap<unknown> | XmlElement,
|
|
231
|
+
)
|
|
232
|
+
: parent || null;
|
|
233
|
+
|
|
234
|
+
invariant(
|
|
235
|
+
targetParent instanceof CollabElementNode,
|
|
236
|
+
'Expected parent to be a collab element node',
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
if (sharedType instanceof XmlText) {
|
|
240
|
+
return $createCollabElementNode(sharedType, targetParent, type);
|
|
241
|
+
} else if (sharedType instanceof YMap) {
|
|
242
|
+
if (type === 'linebreak') {
|
|
243
|
+
return $createCollabLineBreakNode(sharedType, targetParent);
|
|
244
|
+
}
|
|
245
|
+
return $createCollabTextNode(sharedType, '', targetParent, type);
|
|
246
|
+
} else if (sharedType instanceof XmlElement) {
|
|
247
|
+
return $createCollabDecoratorNode(sharedType, targetParent, type);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return collabNode;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function createLexicalNodeFromCollabNode(
|
|
255
|
+
binding: Binding,
|
|
256
|
+
collabNode:
|
|
257
|
+
| CollabElementNode
|
|
258
|
+
| CollabTextNode
|
|
259
|
+
| CollabDecoratorNode
|
|
260
|
+
| CollabLineBreakNode,
|
|
261
|
+
parentKey: NodeKey,
|
|
262
|
+
): LexicalNode {
|
|
263
|
+
const type = collabNode.getType();
|
|
264
|
+
const registeredNodes = binding.editor._nodes;
|
|
265
|
+
const nodeInfo = registeredNodes.get(type);
|
|
266
|
+
invariant(nodeInfo !== undefined, 'Node %s is not registered', type);
|
|
267
|
+
const lexicalNode:
|
|
268
|
+
| DecoratorNode<unknown>
|
|
269
|
+
| TextNode
|
|
270
|
+
| ElementNode
|
|
271
|
+
| LexicalNode = new nodeInfo.klass();
|
|
272
|
+
lexicalNode.__parent = parentKey;
|
|
273
|
+
collabNode._key = lexicalNode.__key;
|
|
274
|
+
|
|
275
|
+
if (collabNode instanceof CollabElementNode) {
|
|
276
|
+
const xmlText = collabNode._xmlText;
|
|
277
|
+
collabNode.syncPropertiesFromYjs(binding, null);
|
|
278
|
+
collabNode.applyChildrenYjsDelta(binding, xmlText.toDelta());
|
|
279
|
+
collabNode.syncChildrenFromYjs(binding);
|
|
280
|
+
} else if (collabNode instanceof CollabTextNode) {
|
|
281
|
+
collabNode.syncPropertiesAndTextFromYjs(binding, null);
|
|
282
|
+
} else if (collabNode instanceof CollabDecoratorNode) {
|
|
283
|
+
collabNode.syncPropertiesFromYjs(binding, null);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
binding.collabNodeMap.set(lexicalNode.__key, collabNode);
|
|
287
|
+
return lexicalNode;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export function $syncPropertiesFromYjs(
|
|
291
|
+
binding: BaseBinding,
|
|
292
|
+
sharedType:
|
|
293
|
+
| XmlText
|
|
294
|
+
| YMap<unknown>
|
|
295
|
+
| XmlElement
|
|
296
|
+
// v2
|
|
297
|
+
| Record<string, unknown>,
|
|
298
|
+
lexicalNode: LexicalNode,
|
|
299
|
+
keysChanged: null | Set<string>,
|
|
300
|
+
): void {
|
|
301
|
+
const properties =
|
|
302
|
+
keysChanged === null
|
|
303
|
+
? sharedType instanceof YMap
|
|
304
|
+
? Array.from(sharedType.keys())
|
|
305
|
+
: sharedType instanceof XmlText || sharedType instanceof XmlElement
|
|
306
|
+
? Object.keys(sharedType.getAttributes())
|
|
307
|
+
: Object.keys(sharedType)
|
|
308
|
+
: Array.from(keysChanged);
|
|
309
|
+
let writableNode: LexicalNode | undefined;
|
|
310
|
+
|
|
311
|
+
for (let i = 0; i < properties.length; i++) {
|
|
312
|
+
const property = properties[i];
|
|
313
|
+
if (isExcludedProperty(property, lexicalNode, binding)) {
|
|
314
|
+
if (property === '__state' && isBindingV1(binding)) {
|
|
315
|
+
if (!writableNode) {
|
|
316
|
+
writableNode = lexicalNode.getWritable();
|
|
317
|
+
}
|
|
318
|
+
$syncNodeStateToLexical(sharedType, writableNode);
|
|
319
|
+
}
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
323
|
+
const prevValue = (lexicalNode as any)[property];
|
|
324
|
+
let nextValue = sharedTypeGet(sharedType, property);
|
|
325
|
+
|
|
326
|
+
if (prevValue !== nextValue) {
|
|
327
|
+
if (nextValue instanceof Doc) {
|
|
328
|
+
const yjsDocMap = binding.docMap;
|
|
329
|
+
|
|
330
|
+
if (prevValue instanceof Doc) {
|
|
331
|
+
yjsDocMap.delete(prevValue.guid);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const key = nextValue.guid;
|
|
335
|
+
yjsDocMap.set(key, nextValue);
|
|
336
|
+
|
|
337
|
+
// If the node already constructed a nested editor (e.g. via
|
|
338
|
+
// buildEditorFromExtensions in its constructor), reuse it so its
|
|
339
|
+
// extensions and LexicalBuilder registration survive the sync.
|
|
340
|
+
// Otherwise, fall back to a bare editor.
|
|
341
|
+
if (isLexicalEditor(prevValue)) {
|
|
342
|
+
prevValue._key = key;
|
|
343
|
+
nextValue = prevValue;
|
|
344
|
+
} else {
|
|
345
|
+
const nestedEditor = createEditor();
|
|
346
|
+
nestedEditor._key = key;
|
|
347
|
+
nextValue = nestedEditor;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (writableNode === undefined) {
|
|
352
|
+
writableNode = lexicalNode.getWritable();
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
writableNode[property as string & keyof typeof writableNode] =
|
|
356
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
357
|
+
nextValue as any;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function sharedTypeGet(
|
|
363
|
+
sharedType: XmlText | YMap<unknown> | XmlElement | Record<string, unknown>,
|
|
364
|
+
property: string,
|
|
365
|
+
): unknown {
|
|
366
|
+
if (sharedType instanceof YMap) {
|
|
367
|
+
return sharedType.get(property);
|
|
368
|
+
} else if (
|
|
369
|
+
sharedType instanceof XmlText ||
|
|
370
|
+
sharedType instanceof XmlElement
|
|
371
|
+
) {
|
|
372
|
+
return sharedType.getAttribute(property);
|
|
373
|
+
} else {
|
|
374
|
+
return sharedType[property];
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function sharedTypeSet(
|
|
379
|
+
sharedType: XmlText | YMap<unknown> | XmlElement,
|
|
380
|
+
property: string,
|
|
381
|
+
nextValue: unknown,
|
|
382
|
+
): void {
|
|
383
|
+
if (sharedType instanceof YMap) {
|
|
384
|
+
sharedType.set(property, nextValue);
|
|
385
|
+
} else {
|
|
386
|
+
sharedType.setAttribute(property, nextValue as string);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function $syncNodeStateToLexical(
|
|
391
|
+
sharedType: XmlText | YMap<unknown> | XmlElement | Record<string, unknown>,
|
|
392
|
+
lexicalNode: LexicalNode,
|
|
393
|
+
): void {
|
|
394
|
+
const existingState = sharedTypeGet(sharedType, '__state');
|
|
395
|
+
if (!(existingState instanceof YMap)) {
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
// This should only called when creating the node initially,
|
|
399
|
+
// incremental updates to state come in through YMapEvent
|
|
400
|
+
// with the __state as the target.
|
|
401
|
+
$getWritableNodeState(lexicalNode).updateFromJSON(existingState.toJSON());
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function syncNodeStateFromLexical(
|
|
405
|
+
binding: Binding,
|
|
406
|
+
sharedType: XmlText | YMap<unknown> | XmlElement,
|
|
407
|
+
prevLexicalNode: null | LexicalNode,
|
|
408
|
+
nextLexicalNode: LexicalNode,
|
|
409
|
+
): void {
|
|
410
|
+
const nextState = nextLexicalNode.__state;
|
|
411
|
+
// Reading from a shared type that hasn't been integrated into a doc yet
|
|
412
|
+
// logs a "premature access" warning in yjs >= 13.6.10. When the shared
|
|
413
|
+
// type is detached we know there cannot be any existing state.
|
|
414
|
+
const existingState =
|
|
415
|
+
sharedType.doc === null ? undefined : sharedTypeGet(sharedType, '__state');
|
|
416
|
+
if (!nextState) {
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
const [unknown, known] = nextState.getInternalState();
|
|
420
|
+
const prevState = prevLexicalNode && prevLexicalNode.__state;
|
|
421
|
+
const stateMap: YMap<unknown> =
|
|
422
|
+
existingState instanceof YMap ? existingState : new YMap();
|
|
423
|
+
if (prevState === nextState) {
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
const [prevUnknown, prevKnown] =
|
|
427
|
+
prevState && stateMap.doc
|
|
428
|
+
? prevState.getInternalState()
|
|
429
|
+
: [undefined, new Map()];
|
|
430
|
+
if (unknown) {
|
|
431
|
+
for (const [k, v] of Object.entries(unknown)) {
|
|
432
|
+
if (prevUnknown && v !== prevUnknown[k]) {
|
|
433
|
+
stateMap.set(k, v);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
for (const [stateConfig, v] of known) {
|
|
438
|
+
if (prevKnown.get(stateConfig) !== v) {
|
|
439
|
+
stateMap.set(stateConfig.key, stateConfig.unparse(v));
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
if (!existingState) {
|
|
443
|
+
sharedTypeSet(sharedType, '__state', stateMap);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
export function syncPropertiesFromLexical(
|
|
448
|
+
binding: Binding,
|
|
449
|
+
sharedType: XmlText | YMap<unknown> | XmlElement,
|
|
450
|
+
prevLexicalNode: null | LexicalNode,
|
|
451
|
+
nextLexicalNode: LexicalNode,
|
|
452
|
+
): void {
|
|
453
|
+
const properties = Object.keys(
|
|
454
|
+
getDefaultNodeProperties(nextLexicalNode, binding),
|
|
455
|
+
);
|
|
456
|
+
|
|
457
|
+
const EditorClass = binding.editor.constructor;
|
|
458
|
+
|
|
459
|
+
syncNodeStateFromLexical(
|
|
460
|
+
binding,
|
|
461
|
+
sharedType,
|
|
462
|
+
prevLexicalNode,
|
|
463
|
+
nextLexicalNode,
|
|
464
|
+
);
|
|
465
|
+
for (let i = 0; i < properties.length; i++) {
|
|
466
|
+
const property = properties[i];
|
|
467
|
+
const prevValue =
|
|
468
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
469
|
+
prevLexicalNode === null ? undefined : (prevLexicalNode as any)[property];
|
|
470
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
471
|
+
let nextValue = (nextLexicalNode as any)[property];
|
|
472
|
+
|
|
473
|
+
if (prevValue !== nextValue) {
|
|
474
|
+
if (nextValue instanceof EditorClass) {
|
|
475
|
+
const yjsDocMap = binding.docMap;
|
|
476
|
+
let prevDoc;
|
|
477
|
+
|
|
478
|
+
if (prevValue instanceof EditorClass) {
|
|
479
|
+
const prevKey = prevValue._key;
|
|
480
|
+
prevDoc = yjsDocMap.get(prevKey);
|
|
481
|
+
yjsDocMap.delete(prevKey);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// If we already have a document, use it.
|
|
485
|
+
const doc = prevDoc || new Doc();
|
|
486
|
+
const key = doc.guid;
|
|
487
|
+
nextValue._key = key;
|
|
488
|
+
yjsDocMap.set(key, doc);
|
|
489
|
+
nextValue = doc;
|
|
490
|
+
// Mark the node dirty as we've assigned a new key to it
|
|
491
|
+
binding.editor.update(() => {
|
|
492
|
+
nextLexicalNode.markDirty();
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
sharedTypeSet(sharedType, property, nextValue);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
export function spliceString(
|
|
502
|
+
str: string,
|
|
503
|
+
index: number,
|
|
504
|
+
delCount: number,
|
|
505
|
+
newText: string,
|
|
506
|
+
): string {
|
|
507
|
+
return str.slice(0, index) + newText + str.slice(index + delCount);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
export function getPositionFromElementAndOffset(
|
|
511
|
+
node: CollabElementNode,
|
|
512
|
+
offset: number,
|
|
513
|
+
boundaryIsEdge: boolean,
|
|
514
|
+
): {
|
|
515
|
+
length: number;
|
|
516
|
+
node:
|
|
517
|
+
| CollabElementNode
|
|
518
|
+
| CollabTextNode
|
|
519
|
+
| CollabDecoratorNode
|
|
520
|
+
| CollabLineBreakNode
|
|
521
|
+
| null;
|
|
522
|
+
nodeIndex: number;
|
|
523
|
+
offset: number;
|
|
524
|
+
} {
|
|
525
|
+
let index = 0;
|
|
526
|
+
let i = 0;
|
|
527
|
+
const children = node._children;
|
|
528
|
+
const childrenLength = children.length;
|
|
529
|
+
|
|
530
|
+
for (; i < childrenLength; i++) {
|
|
531
|
+
const child = children[i];
|
|
532
|
+
const childOffset = index;
|
|
533
|
+
const size = child.getSize();
|
|
534
|
+
index += size;
|
|
535
|
+
const exceedsBoundary = boundaryIsEdge ? index >= offset : index > offset;
|
|
536
|
+
|
|
537
|
+
if (exceedsBoundary && child instanceof CollabTextNode) {
|
|
538
|
+
let textOffset = offset - childOffset - 1;
|
|
539
|
+
|
|
540
|
+
if (textOffset < 0) {
|
|
541
|
+
textOffset = 0;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const diffLength = index - offset;
|
|
545
|
+
return {
|
|
546
|
+
length: diffLength,
|
|
547
|
+
node: child,
|
|
548
|
+
nodeIndex: i,
|
|
549
|
+
offset: textOffset,
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (index > offset) {
|
|
554
|
+
return {
|
|
555
|
+
length: 0,
|
|
556
|
+
node: child,
|
|
557
|
+
nodeIndex: i,
|
|
558
|
+
offset: childOffset,
|
|
559
|
+
};
|
|
560
|
+
} else if (i === childrenLength - 1) {
|
|
561
|
+
return {
|
|
562
|
+
length: 0,
|
|
563
|
+
node: null,
|
|
564
|
+
nodeIndex: i + 1,
|
|
565
|
+
offset: childOffset + 1,
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
return {
|
|
571
|
+
length: 0,
|
|
572
|
+
node: null,
|
|
573
|
+
nodeIndex: 0,
|
|
574
|
+
offset: 0,
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
export function doesSelectionNeedRecovering(
|
|
579
|
+
selection: RangeSelection,
|
|
580
|
+
): boolean {
|
|
581
|
+
const anchor = selection.anchor;
|
|
582
|
+
const focus = selection.focus;
|
|
583
|
+
let recoveryNeeded = false;
|
|
584
|
+
|
|
585
|
+
try {
|
|
586
|
+
const anchorNode = anchor.getNode();
|
|
587
|
+
const focusNode = focus.getNode();
|
|
588
|
+
|
|
589
|
+
if (
|
|
590
|
+
// We might have removed a node that no longer exists
|
|
591
|
+
!anchorNode.isAttached() ||
|
|
592
|
+
!focusNode.isAttached() ||
|
|
593
|
+
// If we've split a node, then the offset might not be right
|
|
594
|
+
($isTextNode(anchorNode) &&
|
|
595
|
+
anchor.offset > anchorNode.getTextContentSize()) ||
|
|
596
|
+
($isTextNode(focusNode) && focus.offset > focusNode.getTextContentSize())
|
|
597
|
+
) {
|
|
598
|
+
recoveryNeeded = true;
|
|
599
|
+
}
|
|
600
|
+
} catch (_e) {
|
|
601
|
+
// Sometimes checking nor a node via getNode might trigger
|
|
602
|
+
// an error, so we need recovery then too.
|
|
603
|
+
recoveryNeeded = true;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
return recoveryNeeded;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
export function syncWithTransaction(
|
|
610
|
+
binding: BaseBinding,
|
|
611
|
+
fn: () => void,
|
|
612
|
+
): void {
|
|
613
|
+
binding.doc.transact(fn, binding);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
export function $moveSelectionToPreviousNode(
|
|
617
|
+
anchorNodeKey: string,
|
|
618
|
+
currentEditorState: EditorState,
|
|
619
|
+
) {
|
|
620
|
+
const anchorNode = currentEditorState._nodeMap.get(anchorNodeKey);
|
|
621
|
+
if (!anchorNode) {
|
|
622
|
+
$getRoot().selectStart();
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
// Get previous node
|
|
626
|
+
const prevNodeKey = anchorNode.__prev;
|
|
627
|
+
let prevNode: ElementNode | null = null;
|
|
628
|
+
if (prevNodeKey) {
|
|
629
|
+
prevNode = $getNodeByKey(prevNodeKey);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// If previous node not found, get parent node
|
|
633
|
+
if (prevNode === null && anchorNode.__parent !== null) {
|
|
634
|
+
prevNode = $getNodeByKey(anchorNode.__parent);
|
|
635
|
+
}
|
|
636
|
+
if (prevNode === null) {
|
|
637
|
+
$getRoot().selectStart();
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
if (prevNode !== null && prevNode.isAttached()) {
|
|
642
|
+
prevNode.selectEnd();
|
|
643
|
+
return;
|
|
644
|
+
} else {
|
|
645
|
+
// If the found node is also deleted, select the next one
|
|
646
|
+
$moveSelectionToPreviousNode(prevNode.__key, currentEditorState);
|
|
647
|
+
}
|
|
648
|
+
}
|