@datalayer/lexical-loro 0.0.3 → 0.0.7
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/lib/DiffMerge.d.ts +39 -0
- package/lib/DiffMerge.js +437 -0
- package/lib/LoroCollaborativePlugin.d.ts +46 -0
- package/lib/LoroCollaborativePlugin.js +516 -193
- package/lib/stableNodeState.d.ts +8 -0
- package/lib/stableNodeState.js +15 -0
- package/package.json +11 -8
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type LexicalEditor } from 'lexical';
|
|
2
|
+
/**
|
|
3
|
+
* DiffMerge System for Lexical Editor
|
|
4
|
+
*
|
|
5
|
+
* This module provides sophisticated differential updates for Lexical editor states,
|
|
6
|
+
* preventing wholesale state replacement that would destroy React decorator nodes
|
|
7
|
+
* like YouTube embeds, counters, and other custom components.
|
|
8
|
+
*
|
|
9
|
+
* Key Features:
|
|
10
|
+
* - Selective node updates (only changed content)
|
|
11
|
+
* - Decorator node preservation (YouTube, Counter nodes remain untouched)
|
|
12
|
+
* - Table structure support with cell-level updates
|
|
13
|
+
* - Graceful fallback for unsupported operations
|
|
14
|
+
* - Deep content comparison to minimize unnecessary updates
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* ```typescript
|
|
18
|
+
* const success = applyDifferentialUpdate(editor, newState, 'collaboration');
|
|
19
|
+
* if (!success) {
|
|
20
|
+
* // Fall back to setEditorState if differential update fails
|
|
21
|
+
* editor.setEditorState(newState);
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
interface EditorStateData {
|
|
26
|
+
root: {
|
|
27
|
+
type: 'root';
|
|
28
|
+
children: any[];
|
|
29
|
+
direction?: string | null;
|
|
30
|
+
format?: number;
|
|
31
|
+
indent?: number;
|
|
32
|
+
version?: number;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Main function to apply differential updates to the editor
|
|
37
|
+
*/
|
|
38
|
+
export declare function applyDifferentialUpdate(editor: LexicalEditor, newStateData: EditorStateData | any, source?: string): boolean;
|
|
39
|
+
export {};
|
package/lib/DiffMerge.js
ADDED
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2023-2025 Datalayer, Inc.
|
|
3
|
+
* Distributed under the terms of the MIT License.
|
|
4
|
+
*/
|
|
5
|
+
import { $getRoot, $createParagraphNode, $createTextNode, $createLineBreakNode, $isElementNode, } from 'lexical';
|
|
6
|
+
import { $createHeadingNode, } from '@lexical/rich-text';
|
|
7
|
+
import { $createCodeNode, } from '@lexical/code';
|
|
8
|
+
import { $createTableNode, $createTableRowNode, $createTableCellNode, } from '@lexical/table';
|
|
9
|
+
/**
|
|
10
|
+
* Checks if a node type can be created/handled by the differential update system
|
|
11
|
+
*/
|
|
12
|
+
function canCreateNodeType(type) {
|
|
13
|
+
const supportedTypes = new Set([
|
|
14
|
+
'root',
|
|
15
|
+
'paragraph',
|
|
16
|
+
'text',
|
|
17
|
+
'linebreak',
|
|
18
|
+
'heading',
|
|
19
|
+
'code',
|
|
20
|
+
'table',
|
|
21
|
+
'tablerow',
|
|
22
|
+
'tablecell'
|
|
23
|
+
]);
|
|
24
|
+
return supportedTypes.has(type);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Checks if a node is a decorator node that should be preserved
|
|
28
|
+
*/
|
|
29
|
+
function isDecoratorNodeType(type) {
|
|
30
|
+
const decoratorTypes = new Set([
|
|
31
|
+
'youtube',
|
|
32
|
+
'counter',
|
|
33
|
+
'image',
|
|
34
|
+
'tweet',
|
|
35
|
+
'figma',
|
|
36
|
+
'poll',
|
|
37
|
+
'sticky'
|
|
38
|
+
]);
|
|
39
|
+
return decoratorTypes.has(type.toLowerCase());
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Creates a Lexical node from serialized data
|
|
43
|
+
*/
|
|
44
|
+
function createNodeFromData(data) {
|
|
45
|
+
const type = data.type;
|
|
46
|
+
if (!canCreateNodeType(type)) {
|
|
47
|
+
console.warn(`⚠️ Cannot create node of type: ${type}`);
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
switch (type) {
|
|
52
|
+
case 'paragraph': {
|
|
53
|
+
const node = $createParagraphNode();
|
|
54
|
+
if (data.format !== undefined)
|
|
55
|
+
node.setFormat(data.format);
|
|
56
|
+
if (data.indent !== undefined)
|
|
57
|
+
node.setIndent(data.indent);
|
|
58
|
+
if (data.direction !== undefined)
|
|
59
|
+
node.setDirection(data.direction);
|
|
60
|
+
return node;
|
|
61
|
+
}
|
|
62
|
+
case 'heading': {
|
|
63
|
+
const node = $createHeadingNode(data.tag || 'h1');
|
|
64
|
+
if (data.format !== undefined)
|
|
65
|
+
node.setFormat(data.format);
|
|
66
|
+
if (data.indent !== undefined)
|
|
67
|
+
node.setIndent(data.indent);
|
|
68
|
+
if (data.direction !== undefined)
|
|
69
|
+
node.setDirection(data.direction);
|
|
70
|
+
return node;
|
|
71
|
+
}
|
|
72
|
+
case 'code': {
|
|
73
|
+
const node = $createCodeNode(data.language);
|
|
74
|
+
if (data.format !== undefined)
|
|
75
|
+
node.setFormat(data.format);
|
|
76
|
+
if (data.indent !== undefined)
|
|
77
|
+
node.setIndent(data.indent);
|
|
78
|
+
if (data.direction !== undefined)
|
|
79
|
+
node.setDirection(data.direction);
|
|
80
|
+
return node;
|
|
81
|
+
}
|
|
82
|
+
case 'text': {
|
|
83
|
+
const node = $createTextNode(data.text || '');
|
|
84
|
+
if (data.format !== undefined)
|
|
85
|
+
node.setFormat(data.format);
|
|
86
|
+
if (data.style !== undefined)
|
|
87
|
+
node.setStyle(data.style);
|
|
88
|
+
if (data.mode !== undefined)
|
|
89
|
+
node.setMode(data.mode);
|
|
90
|
+
if (data.detail !== undefined)
|
|
91
|
+
node.setDetail(data.detail);
|
|
92
|
+
return node;
|
|
93
|
+
}
|
|
94
|
+
case 'linebreak': {
|
|
95
|
+
return $createLineBreakNode();
|
|
96
|
+
}
|
|
97
|
+
case 'table': {
|
|
98
|
+
return $createTableNode();
|
|
99
|
+
}
|
|
100
|
+
case 'tablerow': {
|
|
101
|
+
const node = $createTableRowNode();
|
|
102
|
+
if (data.height !== undefined)
|
|
103
|
+
node.setHeight(data.height);
|
|
104
|
+
return node;
|
|
105
|
+
}
|
|
106
|
+
case 'tablecell': {
|
|
107
|
+
const node = $createTableCellNode(data.headerState || 0, data.colSpan || 1, data.width || undefined);
|
|
108
|
+
if (data.rowSpan !== undefined)
|
|
109
|
+
node.setRowSpan(data.rowSpan);
|
|
110
|
+
if (data.backgroundColor !== undefined)
|
|
111
|
+
node.setBackgroundColor(data.backgroundColor);
|
|
112
|
+
return node;
|
|
113
|
+
}
|
|
114
|
+
default:
|
|
115
|
+
console.warn(`⚠️ Unsupported node type for creation: ${type}`);
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
console.error(`❌ Error creating node of type ${type}:`, error);
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Deep comparison of node content to determine if update is needed
|
|
126
|
+
*/
|
|
127
|
+
function nodesHaveSameContent(existing, newData) {
|
|
128
|
+
const existingType = existing.getType();
|
|
129
|
+
const newType = newData.type;
|
|
130
|
+
if (existingType !== newType) {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
switch (existingType) {
|
|
135
|
+
case 'text': {
|
|
136
|
+
const textNode = existing;
|
|
137
|
+
return textNode.getTextContent() === (newData.text || '') &&
|
|
138
|
+
textNode.getFormat() === (newData.format || 0) &&
|
|
139
|
+
textNode.getStyle() === (newData.style || '') &&
|
|
140
|
+
textNode.getMode() === (newData.mode || 0) &&
|
|
141
|
+
textNode.getDetail() === (newData.detail || 0);
|
|
142
|
+
}
|
|
143
|
+
case 'paragraph': {
|
|
144
|
+
const elementNode = existing;
|
|
145
|
+
return elementNode.getFormat() === (newData.format || 0) &&
|
|
146
|
+
elementNode.getIndent() === (newData.indent || 0) &&
|
|
147
|
+
elementNode.getDirection() === newData.direction;
|
|
148
|
+
}
|
|
149
|
+
case 'heading': {
|
|
150
|
+
const headingNode = existing;
|
|
151
|
+
return headingNode.getTag() === (newData.tag || 'h1') &&
|
|
152
|
+
headingNode.getFormat() === (newData.format || 0) &&
|
|
153
|
+
headingNode.getIndent() === (newData.indent || 0) &&
|
|
154
|
+
headingNode.getDirection() === newData.direction;
|
|
155
|
+
}
|
|
156
|
+
case 'code': {
|
|
157
|
+
const codeNode = existing;
|
|
158
|
+
return codeNode.getLanguage() === newData.language &&
|
|
159
|
+
codeNode.getFormat() === (newData.format || 0) &&
|
|
160
|
+
codeNode.getIndent() === (newData.indent || 0) &&
|
|
161
|
+
codeNode.getDirection() === newData.direction;
|
|
162
|
+
}
|
|
163
|
+
case 'table':
|
|
164
|
+
case 'tablerow':
|
|
165
|
+
case 'tablecell':
|
|
166
|
+
// For table nodes, we primarily care about structure, which is handled by child comparison
|
|
167
|
+
return true;
|
|
168
|
+
case 'linebreak':
|
|
169
|
+
return true; // Line breaks are always the same
|
|
170
|
+
default:
|
|
171
|
+
// For unknown types, assume they need updating
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
console.error(`❌ Error comparing nodes of type ${existingType}:`, error);
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Updates properties of an existing node from new data
|
|
182
|
+
*/
|
|
183
|
+
function updateNodeFromData(existing, newData) {
|
|
184
|
+
const nodeType = existing.getType();
|
|
185
|
+
try {
|
|
186
|
+
switch (nodeType) {
|
|
187
|
+
case 'text': {
|
|
188
|
+
const textNode = existing;
|
|
189
|
+
const textChanged = textNode.getTextContent() !== (newData.text || '');
|
|
190
|
+
const formatChanged = textNode.getFormat() !== (newData.format || 0);
|
|
191
|
+
const styleChanged = textNode.getStyle() !== (newData.style || '');
|
|
192
|
+
const modeChanged = textNode.getMode() !== (newData.mode || 0);
|
|
193
|
+
const detailChanged = textNode.getDetail() !== (newData.detail || 0);
|
|
194
|
+
if (textChanged)
|
|
195
|
+
textNode.setTextContent(newData.text || '');
|
|
196
|
+
if (formatChanged)
|
|
197
|
+
textNode.setFormat(newData.format || 0);
|
|
198
|
+
if (styleChanged)
|
|
199
|
+
textNode.setStyle(newData.style || '');
|
|
200
|
+
if (modeChanged)
|
|
201
|
+
textNode.setMode(newData.mode || 0);
|
|
202
|
+
if (detailChanged)
|
|
203
|
+
textNode.setDetail(newData.detail || 0);
|
|
204
|
+
return textChanged || formatChanged || styleChanged || modeChanged || detailChanged;
|
|
205
|
+
}
|
|
206
|
+
case 'paragraph': {
|
|
207
|
+
const elementNode = existing;
|
|
208
|
+
const formatChanged = elementNode.getFormat() !== (newData.format || 0);
|
|
209
|
+
const indentChanged = elementNode.getIndent() !== (newData.indent || 0);
|
|
210
|
+
const directionChanged = elementNode.getDirection() !== newData.direction;
|
|
211
|
+
if (formatChanged)
|
|
212
|
+
elementNode.setFormat(newData.format || 0);
|
|
213
|
+
if (indentChanged)
|
|
214
|
+
elementNode.setIndent(newData.indent || 0);
|
|
215
|
+
if (directionChanged)
|
|
216
|
+
elementNode.setDirection(newData.direction);
|
|
217
|
+
return formatChanged || indentChanged || directionChanged;
|
|
218
|
+
}
|
|
219
|
+
case 'heading': {
|
|
220
|
+
const headingNode = existing;
|
|
221
|
+
const tagChanged = headingNode.getTag() !== (newData.tag || 'h1');
|
|
222
|
+
const formatChanged = headingNode.getFormat() !== (newData.format || 0);
|
|
223
|
+
const indentChanged = headingNode.getIndent() !== (newData.indent || 0);
|
|
224
|
+
const directionChanged = headingNode.getDirection() !== newData.direction;
|
|
225
|
+
if (tagChanged)
|
|
226
|
+
headingNode.setTag(newData.tag || 'h1');
|
|
227
|
+
if (formatChanged)
|
|
228
|
+
headingNode.setFormat(newData.format || 0);
|
|
229
|
+
if (indentChanged)
|
|
230
|
+
headingNode.setIndent(newData.indent || 0);
|
|
231
|
+
if (directionChanged)
|
|
232
|
+
headingNode.setDirection(newData.direction);
|
|
233
|
+
return tagChanged || formatChanged || indentChanged || directionChanged;
|
|
234
|
+
}
|
|
235
|
+
case 'code': {
|
|
236
|
+
const codeNode = existing;
|
|
237
|
+
const languageChanged = codeNode.getLanguage() !== newData.language;
|
|
238
|
+
const formatChanged = codeNode.getFormat() !== (newData.format || 0);
|
|
239
|
+
const indentChanged = codeNode.getIndent() !== (newData.indent || 0);
|
|
240
|
+
const directionChanged = codeNode.getDirection() !== newData.direction;
|
|
241
|
+
if (languageChanged)
|
|
242
|
+
codeNode.setLanguage(newData.language);
|
|
243
|
+
if (formatChanged)
|
|
244
|
+
codeNode.setFormat(newData.format || 0);
|
|
245
|
+
if (indentChanged)
|
|
246
|
+
codeNode.setIndent(newData.indent || 0);
|
|
247
|
+
if (directionChanged)
|
|
248
|
+
codeNode.setDirection(newData.direction);
|
|
249
|
+
return languageChanged || formatChanged || indentChanged || directionChanged;
|
|
250
|
+
}
|
|
251
|
+
case 'tablecell': {
|
|
252
|
+
const cellNode = existing;
|
|
253
|
+
let changed = false;
|
|
254
|
+
if (newData.rowSpan !== undefined && cellNode.getRowSpan() !== newData.rowSpan) {
|
|
255
|
+
cellNode.setRowSpan(newData.rowSpan);
|
|
256
|
+
changed = true;
|
|
257
|
+
}
|
|
258
|
+
if (newData.backgroundColor !== undefined && cellNode.getBackgroundColor() !== newData.backgroundColor) {
|
|
259
|
+
cellNode.setBackgroundColor(newData.backgroundColor);
|
|
260
|
+
changed = true;
|
|
261
|
+
}
|
|
262
|
+
return changed;
|
|
263
|
+
}
|
|
264
|
+
case 'tablerow': {
|
|
265
|
+
const rowNode = existing;
|
|
266
|
+
if (newData.height !== undefined && rowNode.getHeight() !== newData.height) {
|
|
267
|
+
rowNode.setHeight(newData.height);
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
default:
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
catch (error) {
|
|
277
|
+
console.error(`❌ Error updating node of type ${nodeType}:`, error);
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Recursively merges children, preserving decorator nodes
|
|
283
|
+
*/
|
|
284
|
+
function mergeChildren(parent, newChildren) {
|
|
285
|
+
const existingChildren = parent.getChildren();
|
|
286
|
+
let hasChanges = false;
|
|
287
|
+
// Track which existing children to keep
|
|
288
|
+
const childrenToKeep = [];
|
|
289
|
+
const decoratorNodes = [];
|
|
290
|
+
// First pass: identify decorator nodes to preserve
|
|
291
|
+
existingChildren.forEach(child => {
|
|
292
|
+
if (isDecoratorNodeType(child.getType())) {
|
|
293
|
+
decoratorNodes.push(child);
|
|
294
|
+
console.log(`🔒 Preserving decorator node: ${child.getType()}`);
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
// Second pass: process new children and match with existing
|
|
298
|
+
for (let i = 0; i < newChildren.length; i++) {
|
|
299
|
+
const newChildData = newChildren[i];
|
|
300
|
+
const newChildType = newChildData.type;
|
|
301
|
+
// Skip unsupported node types
|
|
302
|
+
if (!canCreateNodeType(newChildType)) {
|
|
303
|
+
console.warn(`⚠️ Skipping unsupported node type: ${newChildType}`);
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
// Find matching existing child at this position
|
|
307
|
+
const existingChild = existingChildren[i];
|
|
308
|
+
if (existingChild && existingChild.getType() === newChildType) {
|
|
309
|
+
// Same type at same position - try to update in place
|
|
310
|
+
const contentSame = nodesHaveSameContent(existingChild, newChildData);
|
|
311
|
+
if (!contentSame) {
|
|
312
|
+
// Update the existing node's properties
|
|
313
|
+
const updated = updateNodeFromData(existingChild, newChildData);
|
|
314
|
+
if (updated) {
|
|
315
|
+
hasChanges = true;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
// Handle children recursively for element nodes
|
|
319
|
+
if ($isElementNode(existingChild) && newChildData.children) {
|
|
320
|
+
const childrenChanged = mergeChildren(existingChild, newChildData.children);
|
|
321
|
+
if (childrenChanged) {
|
|
322
|
+
hasChanges = true;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
childrenToKeep.push(existingChild);
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
// Different type or no existing child - create new node
|
|
329
|
+
const newNode = createNodeFromData(newChildData);
|
|
330
|
+
if (newNode) {
|
|
331
|
+
// Add children if it's an element node
|
|
332
|
+
if ($isElementNode(newNode) && newChildData.children) {
|
|
333
|
+
mergeChildren(newNode, newChildData.children);
|
|
334
|
+
}
|
|
335
|
+
childrenToKeep.push(newNode);
|
|
336
|
+
hasChanges = true;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
// Third pass: re-add preserved decorator nodes at the end
|
|
341
|
+
decoratorNodes.forEach(decoratorNode => {
|
|
342
|
+
childrenToKeep.push(decoratorNode);
|
|
343
|
+
});
|
|
344
|
+
// Update parent's children if there are changes
|
|
345
|
+
if (hasChanges || existingChildren.length !== childrenToKeep.length) {
|
|
346
|
+
// Clear and re-add all children
|
|
347
|
+
existingChildren.forEach(child => {
|
|
348
|
+
if (!childrenToKeep.includes(child)) {
|
|
349
|
+
child.remove();
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
// Append new/updated children in order
|
|
353
|
+
childrenToKeep.forEach(child => {
|
|
354
|
+
if (!child.getParent()) {
|
|
355
|
+
parent.append(child);
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
return true;
|
|
359
|
+
}
|
|
360
|
+
return hasChanges;
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Main function to apply differential updates to the editor
|
|
364
|
+
*/
|
|
365
|
+
export function applyDifferentialUpdate(editor, newStateData, source = 'unknown') {
|
|
366
|
+
try {
|
|
367
|
+
// Validate input format
|
|
368
|
+
if (!newStateData || typeof newStateData !== 'object') {
|
|
369
|
+
console.warn('⚠️ Invalid state data for differential update');
|
|
370
|
+
return false;
|
|
371
|
+
}
|
|
372
|
+
// Handle different input formats
|
|
373
|
+
let rootData;
|
|
374
|
+
if (newStateData.root) {
|
|
375
|
+
rootData = newStateData.root;
|
|
376
|
+
}
|
|
377
|
+
else if (newStateData.type === 'root') {
|
|
378
|
+
rootData = newStateData;
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
console.warn('⚠️ Invalid root structure for differential update');
|
|
382
|
+
return false;
|
|
383
|
+
}
|
|
384
|
+
if (!rootData.children || !Array.isArray(rootData.children)) {
|
|
385
|
+
console.warn('⚠️ Invalid children structure for differential update');
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
388
|
+
// Check if we can handle all node types in the new state
|
|
389
|
+
const unsupportedTypes = new Set();
|
|
390
|
+
function checkNodeSupport(nodeData) {
|
|
391
|
+
const type = nodeData.type;
|
|
392
|
+
if (!canCreateNodeType(type) && !isDecoratorNodeType(type)) {
|
|
393
|
+
unsupportedTypes.add(type);
|
|
394
|
+
return false;
|
|
395
|
+
}
|
|
396
|
+
if (nodeData.children && Array.isArray(nodeData.children)) {
|
|
397
|
+
return nodeData.children.every(checkNodeSupport);
|
|
398
|
+
}
|
|
399
|
+
return true;
|
|
400
|
+
}
|
|
401
|
+
const allSupported = rootData.children.every(checkNodeSupport);
|
|
402
|
+
if (!allSupported) {
|
|
403
|
+
console.warn(`⚠️ Unsupported node types detected: ${Array.from(unsupportedTypes).join(', ')}`);
|
|
404
|
+
console.warn('⚠️ Falling back to setEditorState for full compatibility');
|
|
405
|
+
return false;
|
|
406
|
+
}
|
|
407
|
+
// Apply differential update within editor transaction
|
|
408
|
+
let updateSuccess = false;
|
|
409
|
+
editor.update(() => {
|
|
410
|
+
const root = $getRoot();
|
|
411
|
+
// Update root properties if they differ
|
|
412
|
+
if (rootData.direction !== undefined && root.getDirection() !== rootData.direction) {
|
|
413
|
+
root.setDirection(rootData.direction);
|
|
414
|
+
}
|
|
415
|
+
if (rootData.format !== undefined && root.getFormat() !== rootData.format) {
|
|
416
|
+
root.setFormat(rootData.format);
|
|
417
|
+
}
|
|
418
|
+
if (rootData.indent !== undefined && root.getIndent() !== rootData.indent) {
|
|
419
|
+
root.setIndent(rootData.indent);
|
|
420
|
+
}
|
|
421
|
+
// Merge children with preservation of decorator nodes
|
|
422
|
+
const hasChanges = mergeChildren(root, rootData.children);
|
|
423
|
+
if (hasChanges) {
|
|
424
|
+
console.log(`✅ Applied differential update from ${source}`);
|
|
425
|
+
}
|
|
426
|
+
else {
|
|
427
|
+
console.log(`ℹ️ No changes needed for update from ${source}`);
|
|
428
|
+
}
|
|
429
|
+
updateSuccess = true;
|
|
430
|
+
}, { tag: `diff-merge-${source}` });
|
|
431
|
+
return updateSuccess;
|
|
432
|
+
}
|
|
433
|
+
catch (error) {
|
|
434
|
+
console.error(`❌ Error in differential update from ${source}:`, error);
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
@@ -12,5 +12,51 @@ interface LoroCollaborativePluginProps {
|
|
|
12
12
|
onInitialization?: (success: boolean) => void;
|
|
13
13
|
onSendMessageReady?: (sendMessageFn: (message: any) => void) => void;
|
|
14
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* LoroCollaborativePlugin - Enhanced Cursor Management
|
|
17
|
+
*
|
|
18
|
+
* IMPROVEMENTS IMPLEMENTED based on Loro Cursor documentation and YJS SyncCursors patterns:
|
|
19
|
+
*
|
|
20
|
+
* 1. Enhanced CursorAwareness class with Loro document reference
|
|
21
|
+
* - Added loroDoc parameter for proper cursor operations
|
|
22
|
+
* - Provides framework for stable cursor positioning
|
|
23
|
+
*
|
|
24
|
+
* 2. Added createCursorFromLexicalPoint method
|
|
25
|
+
* - Inspired by YJS SyncCursors createRelativePosition pattern
|
|
26
|
+
* - Creates stable Loro cursors from Lexical selection points
|
|
27
|
+
* - Replaces approximation with proper cursor positioning
|
|
28
|
+
*
|
|
29
|
+
* 3. Added getStableCursorPosition method
|
|
30
|
+
* - Inspired by YJS SyncCursors createAbsolutePosition pattern
|
|
31
|
+
* - Converts Loro cursors back to stable positions
|
|
32
|
+
* - Provides better positioning than current approximations
|
|
33
|
+
*
|
|
34
|
+
* 4. Enhanced cursor side information support
|
|
35
|
+
* - Added anchorSide and focusSide to stable cursor data
|
|
36
|
+
* - Follows Loro Cursor documentation patterns for precise positioning
|
|
37
|
+
* - Equivalent to YJS RelativePosition side information
|
|
38
|
+
*
|
|
39
|
+
* 5. Improved cursor creation with framework for better methods
|
|
40
|
+
* - Added TODO comments showing enhanced cursor creation approach
|
|
41
|
+
* - Framework ready for using createCursorFromLexicalPoint
|
|
42
|
+
* - Maintains backward compatibility while providing upgrade path
|
|
43
|
+
*
|
|
44
|
+
* 6. Enhanced remote cursor processing
|
|
45
|
+
* - Added support for cursor side information in stable cursor data
|
|
46
|
+
* - Provides framework for direct Loro cursor conversion
|
|
47
|
+
* - Better handling of cursor position stability across edits
|
|
48
|
+
*
|
|
49
|
+
* TECHNICAL APPROACH:
|
|
50
|
+
* - Loro Cursor type is equivalent to YJS RelativePosition (as documented)
|
|
51
|
+
* - Stable positions survive document edits (like YJS RelativePosition)
|
|
52
|
+
* - Cursor side information provides precise positioning
|
|
53
|
+
* - Framework supports proper createRelativePosition/createAbsolutePosition patterns
|
|
54
|
+
*
|
|
55
|
+
* NEXT STEPS for full implementation:
|
|
56
|
+
* - Implement calculateGlobalPosition method with proper document traversal
|
|
57
|
+
* - Add convertGlobalPositionToLexical helper function
|
|
58
|
+
* - Enable the enhanced cursor creation methods by uncommenting TODO sections
|
|
59
|
+
* - Complete the direct Loro cursor conversion path
|
|
60
|
+
*/
|
|
15
61
|
export declare function LoroCollaborativePlugin({ websocketUrl, docId, onConnectionChange, onDisconnectReady, onPeerIdChange, onAwarenessChange, onInitialization, onSendMessageReady }: LoroCollaborativePluginProps): import("react/jsx-runtime").JSX.Element;
|
|
16
62
|
export default LoroCollaborativePlugin;
|