@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
|
@@ -0,0 +1,823 @@
|
|
|
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 {
|
|
10
|
+
BaseSelection,
|
|
11
|
+
LexicalNode,
|
|
12
|
+
NodeKey,
|
|
13
|
+
NodeMap,
|
|
14
|
+
Point,
|
|
15
|
+
} from 'lexical';
|
|
16
|
+
|
|
17
|
+
import invariant from '@lexical/internal/invariant';
|
|
18
|
+
import {createDOMRange, createRectsFromDOMRange} from '@lexical/selection';
|
|
19
|
+
import {
|
|
20
|
+
$getNodeByKey,
|
|
21
|
+
$getSelection,
|
|
22
|
+
$isElementNode,
|
|
23
|
+
$isLineBreakNode,
|
|
24
|
+
$isRangeSelection,
|
|
25
|
+
$isTextNode,
|
|
26
|
+
setDOMStyleObject,
|
|
27
|
+
} from 'lexical';
|
|
28
|
+
import {
|
|
29
|
+
AbsolutePosition,
|
|
30
|
+
compareRelativePositions,
|
|
31
|
+
createAbsolutePositionFromRelativePosition,
|
|
32
|
+
createRelativePositionFromTypeIndex,
|
|
33
|
+
RelativePosition,
|
|
34
|
+
XmlElement,
|
|
35
|
+
XmlText,
|
|
36
|
+
} from 'yjs';
|
|
37
|
+
|
|
38
|
+
import {Provider, UserState} from '.';
|
|
39
|
+
import {
|
|
40
|
+
AnyBinding,
|
|
41
|
+
type BaseBinding,
|
|
42
|
+
type Binding,
|
|
43
|
+
type BindingV2,
|
|
44
|
+
isBindingV1,
|
|
45
|
+
} from './Bindings';
|
|
46
|
+
import {CollabDecoratorNode} from './CollabDecoratorNode';
|
|
47
|
+
import {CollabElementNode} from './CollabElementNode';
|
|
48
|
+
import {CollabLineBreakNode} from './CollabLineBreakNode';
|
|
49
|
+
import {CollabTextNode} from './CollabTextNode';
|
|
50
|
+
import {CollabV2Mapping} from './CollabV2Mapping';
|
|
51
|
+
import {getPositionFromElementAndOffset} from './Utils';
|
|
52
|
+
|
|
53
|
+
export type CursorSelection = {
|
|
54
|
+
anchor: {
|
|
55
|
+
key: NodeKey;
|
|
56
|
+
offset: number;
|
|
57
|
+
};
|
|
58
|
+
caret: HTMLElement;
|
|
59
|
+
color: string;
|
|
60
|
+
focus: {
|
|
61
|
+
key: NodeKey;
|
|
62
|
+
offset: number;
|
|
63
|
+
};
|
|
64
|
+
name: HTMLSpanElement;
|
|
65
|
+
selections: Array<HTMLElement>;
|
|
66
|
+
};
|
|
67
|
+
export type Cursor = {
|
|
68
|
+
color: string;
|
|
69
|
+
name: string;
|
|
70
|
+
selection: null | CursorSelection;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
function createRelativePosition(
|
|
74
|
+
point: Point,
|
|
75
|
+
binding: Binding,
|
|
76
|
+
): null | RelativePosition {
|
|
77
|
+
const collabNodeMap = binding.collabNodeMap;
|
|
78
|
+
const collabNode = collabNodeMap.get(point.key);
|
|
79
|
+
|
|
80
|
+
if (collabNode === undefined) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let offset = point.offset;
|
|
85
|
+
let sharedType = collabNode.getSharedType();
|
|
86
|
+
|
|
87
|
+
if (collabNode instanceof CollabTextNode) {
|
|
88
|
+
sharedType = collabNode._parent._xmlText;
|
|
89
|
+
const currentOffset = collabNode.getOffset();
|
|
90
|
+
|
|
91
|
+
if (currentOffset === -1) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
offset = currentOffset + 1 + offset;
|
|
96
|
+
} else if (
|
|
97
|
+
collabNode instanceof CollabElementNode &&
|
|
98
|
+
point.type === 'element'
|
|
99
|
+
) {
|
|
100
|
+
const parent = point.getNode();
|
|
101
|
+
invariant($isElementNode(parent), 'Element point must be an element node');
|
|
102
|
+
let accumulatedOffset = 0;
|
|
103
|
+
let i = 0;
|
|
104
|
+
let node = parent.getFirstChild();
|
|
105
|
+
while (node !== null && i++ < offset) {
|
|
106
|
+
if ($isTextNode(node)) {
|
|
107
|
+
accumulatedOffset += node.getTextContentSize() + 1;
|
|
108
|
+
} else {
|
|
109
|
+
accumulatedOffset++;
|
|
110
|
+
}
|
|
111
|
+
node = node.getNextSibling();
|
|
112
|
+
}
|
|
113
|
+
offset = accumulatedOffset;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return createRelativePositionFromTypeIndex(sharedType, offset);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function createRelativePositionV2(
|
|
120
|
+
point: Point,
|
|
121
|
+
binding: BindingV2,
|
|
122
|
+
): null | RelativePosition {
|
|
123
|
+
const {mapping} = binding;
|
|
124
|
+
const {offset} = point;
|
|
125
|
+
const node = point.getNode();
|
|
126
|
+
const yType = mapping.getSharedType(node);
|
|
127
|
+
if (yType === undefined) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
if (point.type === 'text') {
|
|
131
|
+
invariant($isTextNode(node), 'Text point must be a text node');
|
|
132
|
+
let prevSibling = node.getPreviousSibling();
|
|
133
|
+
let adjustedOffset = offset;
|
|
134
|
+
while ($isTextNode(prevSibling)) {
|
|
135
|
+
adjustedOffset += prevSibling.getTextContentSize();
|
|
136
|
+
prevSibling = prevSibling.getPreviousSibling();
|
|
137
|
+
}
|
|
138
|
+
return createRelativePositionFromTypeIndex(yType, adjustedOffset);
|
|
139
|
+
} else if (point.type === 'element') {
|
|
140
|
+
invariant($isElementNode(node), 'Element point must be an element node');
|
|
141
|
+
let i = 0;
|
|
142
|
+
let child = node.getFirstChild();
|
|
143
|
+
while (child !== null && i < offset) {
|
|
144
|
+
if ($isTextNode(child)) {
|
|
145
|
+
let nextSibling = child.getNextSibling();
|
|
146
|
+
while ($isTextNode(nextSibling)) {
|
|
147
|
+
nextSibling = nextSibling.getNextSibling();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
i++;
|
|
151
|
+
child = child.getNextSibling();
|
|
152
|
+
}
|
|
153
|
+
return createRelativePositionFromTypeIndex(yType, i);
|
|
154
|
+
}
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function createAbsolutePosition(
|
|
159
|
+
relativePosition: RelativePosition,
|
|
160
|
+
binding: BaseBinding,
|
|
161
|
+
): AbsolutePosition | null {
|
|
162
|
+
return createAbsolutePositionFromRelativePosition(
|
|
163
|
+
relativePosition,
|
|
164
|
+
binding.doc,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function shouldUpdatePosition(
|
|
169
|
+
currentPos: RelativePosition | null | undefined,
|
|
170
|
+
pos: RelativePosition | null | undefined,
|
|
171
|
+
): boolean {
|
|
172
|
+
if (currentPos == null) {
|
|
173
|
+
if (pos != null) {
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
} else if (pos == null || !compareRelativePositions(currentPos, pos)) {
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function createCursor(name: string, color: string): Cursor {
|
|
184
|
+
return {
|
|
185
|
+
color: color,
|
|
186
|
+
name: name,
|
|
187
|
+
selection: null,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function destroySelection(binding: BaseBinding, selection: CursorSelection) {
|
|
192
|
+
const cursorsContainer = binding.cursorsContainer;
|
|
193
|
+
|
|
194
|
+
if (cursorsContainer !== null) {
|
|
195
|
+
const selections = selection.selections;
|
|
196
|
+
const selectionsLength = selections.length;
|
|
197
|
+
|
|
198
|
+
for (let i = 0; i < selectionsLength; i++) {
|
|
199
|
+
cursorsContainer.removeChild(selections[i]);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function destroyCursor(binding: BaseBinding, cursor: Cursor) {
|
|
205
|
+
const selection = cursor.selection;
|
|
206
|
+
|
|
207
|
+
if (selection !== null) {
|
|
208
|
+
destroySelection(binding, selection);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function createCursorSelection(
|
|
213
|
+
cursor: Cursor,
|
|
214
|
+
anchorKey: NodeKey,
|
|
215
|
+
anchorOffset: number,
|
|
216
|
+
focusKey: NodeKey,
|
|
217
|
+
focusOffset: number,
|
|
218
|
+
theme: {cursor?: string; cursorName?: string} = {},
|
|
219
|
+
): CursorSelection {
|
|
220
|
+
const color = cursor.color;
|
|
221
|
+
const caret = document.createElement('span');
|
|
222
|
+
if (theme.cursor) {
|
|
223
|
+
caret.className = theme.cursor;
|
|
224
|
+
setDOMStyleObject(caret.style, {
|
|
225
|
+
'--lexical-cursor-color': color,
|
|
226
|
+
bottom: '0',
|
|
227
|
+
position: 'absolute',
|
|
228
|
+
right: '-1px',
|
|
229
|
+
top: '0',
|
|
230
|
+
});
|
|
231
|
+
} else {
|
|
232
|
+
setDOMStyleObject(caret.style, {
|
|
233
|
+
'background-color': color,
|
|
234
|
+
bottom: '0',
|
|
235
|
+
position: 'absolute',
|
|
236
|
+
right: '-1px',
|
|
237
|
+
top: '0',
|
|
238
|
+
width: '1px',
|
|
239
|
+
'z-index': '10',
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
const name = document.createElement('span');
|
|
243
|
+
name.textContent = cursor.name;
|
|
244
|
+
if (theme.cursorName) {
|
|
245
|
+
name.className = theme.cursorName;
|
|
246
|
+
} else {
|
|
247
|
+
setDOMStyleObject(name.style, {
|
|
248
|
+
'background-color': color,
|
|
249
|
+
color: '#fff',
|
|
250
|
+
'font-family': 'Arial',
|
|
251
|
+
'font-size': '12px',
|
|
252
|
+
'font-weight': 'bold',
|
|
253
|
+
left: '-2px',
|
|
254
|
+
'line-height': '12px',
|
|
255
|
+
padding: '2px',
|
|
256
|
+
position: 'absolute',
|
|
257
|
+
top: '-16px',
|
|
258
|
+
'white-space': 'nowrap',
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
caret.appendChild(name);
|
|
262
|
+
return {
|
|
263
|
+
anchor: {
|
|
264
|
+
key: anchorKey,
|
|
265
|
+
offset: anchorOffset,
|
|
266
|
+
},
|
|
267
|
+
caret,
|
|
268
|
+
color,
|
|
269
|
+
focus: {
|
|
270
|
+
key: focusKey,
|
|
271
|
+
offset: focusOffset,
|
|
272
|
+
},
|
|
273
|
+
name,
|
|
274
|
+
selections: [],
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function updateCursor(
|
|
279
|
+
binding: BaseBinding,
|
|
280
|
+
cursor: Cursor,
|
|
281
|
+
nextSelection: null | CursorSelection,
|
|
282
|
+
nodeMap: NodeMap,
|
|
283
|
+
theme: {selection?: string; selectionBg?: string} = {},
|
|
284
|
+
): void {
|
|
285
|
+
const editor = binding.editor;
|
|
286
|
+
const rootElement = editor.getRootElement();
|
|
287
|
+
const cursorsContainer = binding.cursorsContainer;
|
|
288
|
+
|
|
289
|
+
if (cursorsContainer === null || rootElement === null) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const cursorsContainerOffsetParent = cursorsContainer.offsetParent;
|
|
294
|
+
if (cursorsContainerOffsetParent === null) {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const containerRect = cursorsContainerOffsetParent.getBoundingClientRect();
|
|
299
|
+
const prevSelection = cursor.selection;
|
|
300
|
+
|
|
301
|
+
if (nextSelection === null) {
|
|
302
|
+
if (prevSelection === null) {
|
|
303
|
+
return;
|
|
304
|
+
} else {
|
|
305
|
+
cursor.selection = null;
|
|
306
|
+
destroySelection(binding, prevSelection);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
} else {
|
|
310
|
+
cursor.selection = nextSelection;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const caret = nextSelection.caret;
|
|
314
|
+
const color = nextSelection.color;
|
|
315
|
+
const selections = nextSelection.selections;
|
|
316
|
+
const anchor = nextSelection.anchor;
|
|
317
|
+
const focus = nextSelection.focus;
|
|
318
|
+
const anchorKey = anchor.key;
|
|
319
|
+
const focusKey = focus.key;
|
|
320
|
+
const anchorNode = nodeMap.get(anchorKey);
|
|
321
|
+
const focusNode = nodeMap.get(focusKey);
|
|
322
|
+
|
|
323
|
+
if (anchorNode == null || focusNode == null) {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
let selectionRects: Array<DOMRect>;
|
|
327
|
+
|
|
328
|
+
// In the case of a collapsed selection on a linebreak, we need
|
|
329
|
+
// to improvise as the browser will return nothing here as <br>
|
|
330
|
+
// apparently take up no visual space :/
|
|
331
|
+
// This won't work in all cases, but it's better than just showing
|
|
332
|
+
// nothing all the time.
|
|
333
|
+
if (anchorNode === focusNode && $isLineBreakNode(anchorNode)) {
|
|
334
|
+
const brRect = (
|
|
335
|
+
editor.getElementByKey(anchorKey) as HTMLElement
|
|
336
|
+
).getBoundingClientRect();
|
|
337
|
+
selectionRects = [brRect];
|
|
338
|
+
} else {
|
|
339
|
+
const range = createDOMRange(
|
|
340
|
+
editor,
|
|
341
|
+
anchorNode,
|
|
342
|
+
anchor.offset,
|
|
343
|
+
focusNode,
|
|
344
|
+
focus.offset,
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
if (range === null) {
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
selectionRects = createRectsFromDOMRange(editor, range);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const selectionsLength = selections.length;
|
|
354
|
+
const selectionRectsLength = selectionRects.length;
|
|
355
|
+
|
|
356
|
+
for (let i = 0; i < selectionRectsLength; i++) {
|
|
357
|
+
const selectionRect = selectionRects[i];
|
|
358
|
+
let selection = selections[i];
|
|
359
|
+
|
|
360
|
+
if (selection === undefined) {
|
|
361
|
+
selection = document.createElement('span');
|
|
362
|
+
selections[i] = selection;
|
|
363
|
+
const selectionBg = document.createElement('span');
|
|
364
|
+
if (theme.selectionBg) {
|
|
365
|
+
selectionBg.className = theme.selectionBg;
|
|
366
|
+
}
|
|
367
|
+
selection.appendChild(selectionBg);
|
|
368
|
+
cursorsContainer.appendChild(selection);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const top = selectionRect.top - containerRect.top;
|
|
372
|
+
const left = selectionRect.left - containerRect.left;
|
|
373
|
+
const positionStyle = {
|
|
374
|
+
height: `${selectionRect.height}px`,
|
|
375
|
+
left: `${left}px`,
|
|
376
|
+
'pointer-events': 'none',
|
|
377
|
+
position: 'absolute',
|
|
378
|
+
top: `${top}px`,
|
|
379
|
+
width: `${selectionRect.width}px`,
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
if (theme.selection) {
|
|
383
|
+
selection.className = theme.selection;
|
|
384
|
+
setDOMStyleObject(selection.style, {
|
|
385
|
+
...positionStyle,
|
|
386
|
+
'--lexical-cursor-color': color,
|
|
387
|
+
});
|
|
388
|
+
setDOMStyleObject((selection.firstChild as HTMLSpanElement).style, {
|
|
389
|
+
height: '100%',
|
|
390
|
+
left: '0',
|
|
391
|
+
position: 'absolute',
|
|
392
|
+
top: '0',
|
|
393
|
+
width: '100%',
|
|
394
|
+
});
|
|
395
|
+
} else {
|
|
396
|
+
setDOMStyleObject(selection.style, positionStyle);
|
|
397
|
+
setDOMStyleObject((selection.firstChild as HTMLSpanElement).style, {
|
|
398
|
+
...positionStyle,
|
|
399
|
+
'background-color': color,
|
|
400
|
+
left: '0',
|
|
401
|
+
opacity: '0.3',
|
|
402
|
+
top: '0',
|
|
403
|
+
'z-index': '5',
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (i === selectionRectsLength - 1) {
|
|
408
|
+
if (caret.parentNode !== selection) {
|
|
409
|
+
selection.appendChild(caret);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
for (let i = selectionsLength - 1; i >= selectionRectsLength; i--) {
|
|
415
|
+
const selection = selections[i];
|
|
416
|
+
cursorsContainer.removeChild(selection);
|
|
417
|
+
selections.pop();
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
type AnyCollabNode =
|
|
422
|
+
| CollabDecoratorNode
|
|
423
|
+
| CollabElementNode
|
|
424
|
+
| CollabTextNode
|
|
425
|
+
| CollabLineBreakNode;
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* @deprecated Use `$getAnchorAndFocusForUserState` instead.
|
|
429
|
+
*/
|
|
430
|
+
export function getAnchorAndFocusCollabNodesForUserState(
|
|
431
|
+
binding: Binding,
|
|
432
|
+
userState: UserState,
|
|
433
|
+
) {
|
|
434
|
+
const {anchorPos, focusPos} = userState;
|
|
435
|
+
let anchorCollabNode: AnyCollabNode | null = null;
|
|
436
|
+
let anchorOffset = 0;
|
|
437
|
+
let focusCollabNode: AnyCollabNode | null = null;
|
|
438
|
+
let focusOffset = 0;
|
|
439
|
+
|
|
440
|
+
if (anchorPos !== null && focusPos !== null) {
|
|
441
|
+
const anchorAbsPos = createAbsolutePosition(anchorPos, binding);
|
|
442
|
+
const focusAbsPos = createAbsolutePosition(focusPos, binding);
|
|
443
|
+
|
|
444
|
+
if (anchorAbsPos !== null && focusAbsPos !== null) {
|
|
445
|
+
[anchorCollabNode, anchorOffset] = getCollabNodeAndOffset(
|
|
446
|
+
anchorAbsPos.type,
|
|
447
|
+
anchorAbsPos.index,
|
|
448
|
+
);
|
|
449
|
+
[focusCollabNode, focusOffset] = getCollabNodeAndOffset(
|
|
450
|
+
focusAbsPos.type,
|
|
451
|
+
focusAbsPos.index,
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return {
|
|
457
|
+
anchorCollabNode,
|
|
458
|
+
anchorOffset,
|
|
459
|
+
focusCollabNode,
|
|
460
|
+
focusOffset,
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
export function $getAnchorAndFocusForUserState(
|
|
465
|
+
binding: AnyBinding,
|
|
466
|
+
userState: UserState,
|
|
467
|
+
): {
|
|
468
|
+
anchorKey: NodeKey | null;
|
|
469
|
+
anchorOffset: number;
|
|
470
|
+
focusKey: NodeKey | null;
|
|
471
|
+
focusOffset: number;
|
|
472
|
+
} {
|
|
473
|
+
const {anchorPos, focusPos} = userState;
|
|
474
|
+
const anchorAbsPos = anchorPos
|
|
475
|
+
? createAbsolutePosition(anchorPos, binding)
|
|
476
|
+
: null;
|
|
477
|
+
const focusAbsPos = focusPos
|
|
478
|
+
? createAbsolutePosition(focusPos, binding)
|
|
479
|
+
: null;
|
|
480
|
+
|
|
481
|
+
if (anchorAbsPos === null || focusAbsPos === null) {
|
|
482
|
+
return {
|
|
483
|
+
anchorKey: null,
|
|
484
|
+
anchorOffset: 0,
|
|
485
|
+
focusKey: null,
|
|
486
|
+
focusOffset: 0,
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (isBindingV1(binding)) {
|
|
491
|
+
const [anchorCollabNode, anchorOffset] = getCollabNodeAndOffset(
|
|
492
|
+
anchorAbsPos.type,
|
|
493
|
+
anchorAbsPos.index,
|
|
494
|
+
);
|
|
495
|
+
const [focusCollabNode, focusOffset] = getCollabNodeAndOffset(
|
|
496
|
+
focusAbsPos.type,
|
|
497
|
+
focusAbsPos.index,
|
|
498
|
+
);
|
|
499
|
+
return {
|
|
500
|
+
anchorKey: anchorCollabNode !== null ? anchorCollabNode.getKey() : null,
|
|
501
|
+
anchorOffset,
|
|
502
|
+
focusKey: focusCollabNode !== null ? focusCollabNode.getKey() : null,
|
|
503
|
+
focusOffset,
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
let [anchorNode, anchorOffset] = $getNodeAndOffsetV2(
|
|
508
|
+
binding.mapping,
|
|
509
|
+
anchorAbsPos,
|
|
510
|
+
);
|
|
511
|
+
let [focusNode, focusOffset] = $getNodeAndOffsetV2(
|
|
512
|
+
binding.mapping,
|
|
513
|
+
focusAbsPos,
|
|
514
|
+
);
|
|
515
|
+
// For a non-collapsed selection, if the start of the selection is as the end of a text node,
|
|
516
|
+
// move it to the beginning of the next text node (if one exists).
|
|
517
|
+
if (
|
|
518
|
+
focusNode &&
|
|
519
|
+
anchorNode &&
|
|
520
|
+
(focusNode !== anchorNode || focusOffset !== anchorOffset)
|
|
521
|
+
) {
|
|
522
|
+
const isBackwards = focusNode.isBefore(anchorNode);
|
|
523
|
+
const startNode = isBackwards ? focusNode : anchorNode;
|
|
524
|
+
const startOffset = isBackwards ? focusOffset : anchorOffset;
|
|
525
|
+
if (
|
|
526
|
+
$isTextNode(startNode) &&
|
|
527
|
+
$isTextNode(startNode.getNextSibling()) &&
|
|
528
|
+
startOffset === startNode.getTextContentSize()
|
|
529
|
+
) {
|
|
530
|
+
if (isBackwards) {
|
|
531
|
+
focusNode = startNode.getNextSibling();
|
|
532
|
+
focusOffset = 0;
|
|
533
|
+
} else {
|
|
534
|
+
anchorNode = startNode.getNextSibling();
|
|
535
|
+
anchorOffset = 0;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
return {
|
|
540
|
+
anchorKey: anchorNode !== null ? anchorNode.getKey() : null,
|
|
541
|
+
anchorOffset,
|
|
542
|
+
focusKey: focusNode !== null ? focusNode.getKey() : null,
|
|
543
|
+
focusOffset,
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
export function $syncLocalCursorPosition(
|
|
548
|
+
binding: AnyBinding,
|
|
549
|
+
provider: Provider,
|
|
550
|
+
): void {
|
|
551
|
+
const awareness = provider.awareness;
|
|
552
|
+
const localState = awareness.getLocalState();
|
|
553
|
+
|
|
554
|
+
if (localState === null) {
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const {anchorKey, anchorOffset, focusKey, focusOffset} =
|
|
559
|
+
$getAnchorAndFocusForUserState(binding, localState);
|
|
560
|
+
|
|
561
|
+
if (anchorKey !== null && focusKey !== null) {
|
|
562
|
+
const selection = $getSelection();
|
|
563
|
+
|
|
564
|
+
if (!$isRangeSelection(selection)) {
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
$setPoint(selection.anchor, anchorKey, anchorOffset);
|
|
569
|
+
$setPoint(selection.focus, focusKey, focusOffset);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function $setPoint(point: Point, key: NodeKey, offset: number): void {
|
|
574
|
+
if (point.key !== key || point.offset !== offset) {
|
|
575
|
+
let anchorNode = $getNodeByKey(key);
|
|
576
|
+
if (
|
|
577
|
+
anchorNode !== null &&
|
|
578
|
+
!$isElementNode(anchorNode) &&
|
|
579
|
+
!$isTextNode(anchorNode)
|
|
580
|
+
) {
|
|
581
|
+
const parent = anchorNode.getParentOrThrow();
|
|
582
|
+
key = parent.getKey();
|
|
583
|
+
offset = anchorNode.getIndexWithinParent();
|
|
584
|
+
anchorNode = parent;
|
|
585
|
+
}
|
|
586
|
+
point.set(key, offset, $isElementNode(anchorNode) ? 'element' : 'text');
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function getCollabNodeAndOffset(
|
|
591
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
592
|
+
sharedType: any,
|
|
593
|
+
offset: number,
|
|
594
|
+
): [null | AnyCollabNode, number] {
|
|
595
|
+
const collabNode = sharedType._collabNode;
|
|
596
|
+
|
|
597
|
+
if (collabNode === undefined) {
|
|
598
|
+
return [null, 0];
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
if (collabNode instanceof CollabElementNode) {
|
|
602
|
+
const {node, offset: collabNodeOffset} = getPositionFromElementAndOffset(
|
|
603
|
+
collabNode,
|
|
604
|
+
offset,
|
|
605
|
+
true,
|
|
606
|
+
);
|
|
607
|
+
|
|
608
|
+
if (node === null) {
|
|
609
|
+
return [collabNode, 0];
|
|
610
|
+
} else {
|
|
611
|
+
return [node, collabNodeOffset];
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
return [null, 0];
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function $getNodeAndOffsetV2(
|
|
619
|
+
mapping: CollabV2Mapping,
|
|
620
|
+
absolutePosition: AbsolutePosition,
|
|
621
|
+
): [null | LexicalNode, number] {
|
|
622
|
+
const yType = absolutePosition.type as XmlElement | XmlText;
|
|
623
|
+
const yOffset = absolutePosition.index;
|
|
624
|
+
if (yType instanceof XmlElement) {
|
|
625
|
+
const node = mapping.get(yType);
|
|
626
|
+
if (node === undefined) {
|
|
627
|
+
return [null, 0];
|
|
628
|
+
}
|
|
629
|
+
if (!$isElementNode(node)) {
|
|
630
|
+
return [node, yOffset];
|
|
631
|
+
}
|
|
632
|
+
let remainingYOffset = yOffset;
|
|
633
|
+
let lexicalOffset = 0;
|
|
634
|
+
const children = node.getChildren();
|
|
635
|
+
while (remainingYOffset > 0 && lexicalOffset < children.length) {
|
|
636
|
+
const child = children[lexicalOffset];
|
|
637
|
+
remainingYOffset -= 1;
|
|
638
|
+
lexicalOffset += 1;
|
|
639
|
+
if ($isTextNode(child)) {
|
|
640
|
+
while (
|
|
641
|
+
lexicalOffset < children.length &&
|
|
642
|
+
$isTextNode(children[lexicalOffset])
|
|
643
|
+
) {
|
|
644
|
+
lexicalOffset += 1;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
return [node, lexicalOffset];
|
|
649
|
+
} else {
|
|
650
|
+
const nodes = mapping.get(yType);
|
|
651
|
+
if (nodes === undefined) {
|
|
652
|
+
return [null, 0];
|
|
653
|
+
}
|
|
654
|
+
let i = 0;
|
|
655
|
+
let adjustedOffset = yOffset;
|
|
656
|
+
while (
|
|
657
|
+
adjustedOffset > nodes[i].getTextContentSize() &&
|
|
658
|
+
i + 1 < nodes.length
|
|
659
|
+
) {
|
|
660
|
+
adjustedOffset -= nodes[i].getTextContentSize();
|
|
661
|
+
i++;
|
|
662
|
+
}
|
|
663
|
+
const textNode = nodes[i];
|
|
664
|
+
return [textNode, Math.min(adjustedOffset, textNode.getTextContentSize())];
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
export type SyncCursorPositionsFn = (
|
|
669
|
+
binding: AnyBinding,
|
|
670
|
+
provider: Provider,
|
|
671
|
+
options?: SyncCursorPositionsOptions,
|
|
672
|
+
) => void;
|
|
673
|
+
|
|
674
|
+
export type SyncCursorPositionsOptions = {
|
|
675
|
+
getAwarenessStates?: (
|
|
676
|
+
binding: BaseBinding,
|
|
677
|
+
provider: Provider,
|
|
678
|
+
) => Map<number, UserState>;
|
|
679
|
+
};
|
|
680
|
+
|
|
681
|
+
function getAwarenessStatesDefault(
|
|
682
|
+
_binding: BaseBinding,
|
|
683
|
+
provider: Provider,
|
|
684
|
+
): Map<number, UserState> {
|
|
685
|
+
return provider.awareness.getStates();
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
export function syncCursorPositions(
|
|
689
|
+
binding: AnyBinding,
|
|
690
|
+
provider: Provider,
|
|
691
|
+
options?: SyncCursorPositionsOptions,
|
|
692
|
+
): void {
|
|
693
|
+
const {getAwarenessStates = getAwarenessStatesDefault} = options ?? {};
|
|
694
|
+
const awarenessStates = Array.from(getAwarenessStates(binding, provider));
|
|
695
|
+
const localClientID = binding.clientID;
|
|
696
|
+
const cursors = binding.cursors;
|
|
697
|
+
const editor = binding.editor;
|
|
698
|
+
const collabTheme = editor._config.theme.collaboration;
|
|
699
|
+
const nodeMap = editor._editorState._nodeMap;
|
|
700
|
+
const visitedClientIDs = new Set();
|
|
701
|
+
|
|
702
|
+
for (let i = 0; i < awarenessStates.length; i++) {
|
|
703
|
+
const awarenessState = awarenessStates[i];
|
|
704
|
+
const [clientID, awareness] = awarenessState;
|
|
705
|
+
|
|
706
|
+
if (clientID !== 0 && clientID !== localClientID) {
|
|
707
|
+
visitedClientIDs.add(clientID);
|
|
708
|
+
const {name, color, focusing} = awareness;
|
|
709
|
+
let selection = null;
|
|
710
|
+
|
|
711
|
+
let cursor = cursors.get(clientID);
|
|
712
|
+
|
|
713
|
+
if (cursor === undefined) {
|
|
714
|
+
cursor = createCursor(name, color);
|
|
715
|
+
cursors.set(clientID, cursor);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
if (focusing) {
|
|
719
|
+
const {anchorKey, anchorOffset, focusKey, focusOffset} = editor.read(
|
|
720
|
+
() => $getAnchorAndFocusForUserState(binding, awareness),
|
|
721
|
+
);
|
|
722
|
+
|
|
723
|
+
if (anchorKey !== null && focusKey !== null) {
|
|
724
|
+
selection = cursor.selection;
|
|
725
|
+
|
|
726
|
+
if (selection === null) {
|
|
727
|
+
selection = createCursorSelection(
|
|
728
|
+
cursor,
|
|
729
|
+
anchorKey,
|
|
730
|
+
anchorOffset,
|
|
731
|
+
focusKey,
|
|
732
|
+
focusOffset,
|
|
733
|
+
collabTheme,
|
|
734
|
+
);
|
|
735
|
+
} else {
|
|
736
|
+
const anchor = selection.anchor;
|
|
737
|
+
const focus = selection.focus;
|
|
738
|
+
anchor.key = anchorKey;
|
|
739
|
+
anchor.offset = anchorOffset;
|
|
740
|
+
focus.key = focusKey;
|
|
741
|
+
focus.offset = focusOffset;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
updateCursor(binding, cursor, selection, nodeMap, collabTheme);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
const allClientIDs = Array.from(cursors.keys());
|
|
751
|
+
|
|
752
|
+
for (let i = 0; i < allClientIDs.length; i++) {
|
|
753
|
+
const clientID = allClientIDs[i];
|
|
754
|
+
|
|
755
|
+
if (!visitedClientIDs.has(clientID)) {
|
|
756
|
+
const cursor = cursors.get(clientID);
|
|
757
|
+
|
|
758
|
+
if (cursor !== undefined) {
|
|
759
|
+
destroyCursor(binding, cursor);
|
|
760
|
+
cursors.delete(clientID);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
export function syncLexicalSelectionToYjs(
|
|
767
|
+
binding: AnyBinding,
|
|
768
|
+
provider: Provider,
|
|
769
|
+
prevSelection: null | BaseSelection,
|
|
770
|
+
nextSelection: null | BaseSelection,
|
|
771
|
+
): void {
|
|
772
|
+
const awareness = provider.awareness;
|
|
773
|
+
const localState = awareness.getLocalState();
|
|
774
|
+
|
|
775
|
+
if (localState === null) {
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
const {
|
|
780
|
+
anchorPos: currentAnchorPos,
|
|
781
|
+
focusPos: currentFocusPos,
|
|
782
|
+
name,
|
|
783
|
+
color,
|
|
784
|
+
focusing,
|
|
785
|
+
awarenessData,
|
|
786
|
+
} = localState;
|
|
787
|
+
let anchorPos = null;
|
|
788
|
+
let focusPos = null;
|
|
789
|
+
|
|
790
|
+
if (
|
|
791
|
+
nextSelection === null ||
|
|
792
|
+
(currentAnchorPos !== null && !nextSelection.is(prevSelection))
|
|
793
|
+
) {
|
|
794
|
+
if (prevSelection === null) {
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
if ($isRangeSelection(nextSelection)) {
|
|
800
|
+
if (isBindingV1(binding)) {
|
|
801
|
+
anchorPos = createRelativePosition(nextSelection.anchor, binding);
|
|
802
|
+
focusPos = createRelativePosition(nextSelection.focus, binding);
|
|
803
|
+
} else {
|
|
804
|
+
anchorPos = createRelativePositionV2(nextSelection.anchor, binding);
|
|
805
|
+
focusPos = createRelativePositionV2(nextSelection.focus, binding);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
if (
|
|
810
|
+
shouldUpdatePosition(currentAnchorPos, anchorPos) ||
|
|
811
|
+
shouldUpdatePosition(currentFocusPos, focusPos)
|
|
812
|
+
) {
|
|
813
|
+
awareness.setLocalState({
|
|
814
|
+
...localState,
|
|
815
|
+
anchorPos,
|
|
816
|
+
awarenessData,
|
|
817
|
+
color,
|
|
818
|
+
focusPos,
|
|
819
|
+
focusing,
|
|
820
|
+
name,
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
}
|