@datalayer/lexical-loro 1.0.3 → 1.0.5
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/README.md +2 -2
- package/lib/Editor.js +3 -2
- package/lib/appSettings.js +1 -1
- package/lib/collab/loro/Bindings.js +7 -7
- package/lib/collab/loro/Debug.js +35 -35
- package/lib/collab/loro/LexicalCollaborationContext.js +1 -1
- package/lib/collab/loro/LexicalCollaborationPlugin.d.ts +6 -1
- package/lib/collab/loro/LexicalCollaborationPlugin.js +41 -13
- package/lib/collab/loro/State.js +1 -1
- package/lib/collab/loro/components/LoroCollaborators.js +7 -19
- package/lib/collab/loro/components/index.js +1 -1
- package/lib/collab/loro/index.js +1 -1
- package/lib/collab/loro/integrators/BaseIntegrator.js +1 -1
- package/lib/collab/loro/integrators/CounterIntegrator.js +1 -1
- package/lib/collab/loro/integrators/ListIntegrator.js +4 -4
- package/lib/collab/loro/integrators/MapIntegrator.js +4 -4
- package/lib/collab/loro/integrators/TextIntegrator.js +2 -2
- package/lib/collab/loro/integrators/TreeIntegrator.d.ts +5 -0
- package/lib/collab/loro/integrators/TreeIntegrator.js +184 -142
- package/lib/collab/loro/nodes/NodeFactory.js +26 -47
- package/lib/collab/loro/nodes/NodesMapper.js +19 -7
- package/lib/collab/loro/propagators/DecoratorNodePropagator.js +1 -1
- package/lib/collab/loro/propagators/ElementNodePropagator.d.ts +1 -1
- package/lib/collab/loro/propagators/ElementNodePropagator.js +102 -35
- package/lib/collab/loro/propagators/LineBreakNodePropagator.js +1 -1
- package/lib/collab/loro/propagators/RootNodePropagator.js +1 -1
- package/lib/collab/loro/propagators/TextNodePropagator.js +55 -45
- package/lib/collab/loro/propagators/index.d.ts +1 -1
- package/lib/collab/loro/propagators/index.js +1 -1
- package/lib/collab/loro/provider/websocket.d.ts +1 -0
- package/lib/collab/loro/provider/websocket.js +47 -12
- package/lib/collab/loro/servers/index.js +1 -1
- package/lib/collab/loro/servers/ws/callback.js +1 -1
- package/lib/collab/loro/servers/ws/server.js +1 -1
- package/lib/collab/loro/servers/ws/utils.js +35 -35
- package/lib/collab/loro/sync/SyncCursors.js +42 -16
- package/lib/collab/loro/sync/SyncLexicalToLoro.js +76 -37
- package/lib/collab/loro/sync/SyncLoroToLexical.js +32 -26
- package/lib/collab/loro/types/LexicalNodeData.js +1 -1
- package/lib/collab/loro/useCollaboration.js +173 -28
- package/lib/collab/loro/utils/InitialContent.js +1 -1
- package/lib/collab/loro/utils/Invariant.d.ts +15 -0
- package/lib/collab/loro/utils/Invariant.js +24 -0
- package/lib/collab/loro/utils/LexicalToLoro.js +1 -1
- package/lib/collab/loro/utils/Utils.js +1 -1
- package/lib/collab/loro/wsProvider.js +21 -4
- package/lib/collab/utils/invariant.js +1 -1
- package/lib/collab/utils/simpleDiffWithCursor.js +1 -1
- package/lib/commenting/index.js +1 -1
- package/lib/demo.js +1 -1
- package/lib/hooks/useFlashMessage.js +1 -1
- package/lib/hooks/useReport.js +1 -1
- package/lib/index.js +1 -1
- package/lib/nodes/AutocompleteNode.js +1 -1
- package/lib/nodes/EmojiNode.js +1 -1
- package/lib/nodes/KeywordNode.js +1 -1
- package/lib/nodes/LayoutContainerNode.js +1 -1
- package/lib/nodes/LayoutItemNode.js +1 -1
- package/lib/nodes/MentionNode.js +1 -1
- package/lib/nodes/PlaygroundNodes.js +1 -1
- package/lib/nodes/SpecialTextNode.js +1 -1
- package/lib/plugins/AutocompletePlugin/index.js +1 -1
- package/lib/plugins/CodeActionMenuPlugin/components/CopyButton/index.js +1 -1
- package/lib/plugins/CodeActionMenuPlugin/components/PrettierButton/index.js +1 -1
- package/lib/plugins/CodeActionMenuPlugin/utils.js +1 -1
- package/lib/plugins/CodeHighlightPrismPlugin/index.js +1 -1
- package/lib/plugins/CodeHighlightShikiPlugin/index.js +1 -1
- package/lib/plugins/CollapsiblePlugin/CollapsibleContainerNode.js +1 -1
- package/lib/plugins/CollapsiblePlugin/CollapsibleContentNode.js +1 -1
- package/lib/plugins/CollapsiblePlugin/CollapsibleTitleNode.js +1 -1
- package/lib/plugins/CollapsiblePlugin/CollapsibleUtils.js +1 -1
- package/lib/plugins/CollapsiblePlugin/index.js +1 -1
- package/lib/plugins/CounterPlugin/index.js +1 -1
- package/lib/plugins/DateTimePlugin/index.js +1 -1
- package/lib/plugins/DebugPlugin/index.js +59 -3
- package/lib/plugins/DragDropPastePlugin/index.js +1 -1
- package/lib/plugins/EmojiPickerPlugin/index.js +1 -1
- package/lib/plugins/EmojisPlugin/index.js +1 -1
- package/lib/plugins/FigmaPlugin/index.js +1 -1
- package/lib/plugins/KeywordsPlugin/index.js +1 -1
- package/lib/plugins/LayoutPlugin/LayoutPlugin.js +1 -1
- package/lib/plugins/MarkdownTransformers/index.js +1 -1
- package/lib/plugins/MaxLengthPlugin/index.js +1 -1
- package/lib/plugins/PageBreakPlugin/index.js +1 -1
- package/lib/plugins/ShortcutsPlugin/index.js +1 -1
- package/lib/plugins/ShortcutsPlugin/shortcuts.js +1 -1
- package/lib/plugins/SpecialTextPlugin/index.js +1 -1
- package/lib/plugins/SpeechToTextPlugin/index.js +1 -1
- package/lib/plugins/StickyPlugin/index.js +1 -1
- package/lib/plugins/TabFocusPlugin/index.js +1 -1
- package/lib/plugins/ToolbarPlugin/fontSize.js +1 -1
- package/lib/plugins/ToolbarPlugin/utils.js +1 -1
- package/lib/plugins/TwitterPlugin/index.js +1 -1
- package/lib/plugins/TypingPerfPlugin/index.js +1 -1
- package/lib/plugins/YouTubePlugin/index.js +1 -1
- package/lib/server/validation.js +1 -1
- package/lib/setupEnv.js +1 -1
- package/lib/themes/CommentEditorTheme.js +1 -1
- package/lib/themes/PlaygroundEditorTheme.js +1 -1
- package/lib/themes/StickyEditorTheme.js +1 -1
- package/lib/tyes.dt.js +1 -1
- package/lib/utils/docSerialization.js +1 -1
- package/lib/utils/emoji-list.js +1 -1
- package/lib/utils/getDOMRangeRect.js +1 -1
- package/lib/utils/getSelectedNode.js +1 -1
- package/lib/utils/getThemeSelector.js +1 -1
- package/lib/utils/isMobileWidth.js +1 -1
- package/lib/utils/joinClasses.js +1 -1
- package/lib/utils/setFloatingElemPosition.js +1 -1
- package/lib/utils/setFloatingElemPositionForLinkEditor.js +1 -1
- package/lib/utils/swipe.js +1 -1
- package/lib/utils/url.js +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -291,10 +291,10 @@ For complete working examples and demonstrations, see the `src/examples/` direct
|
|
|
291
291
|
```bash
|
|
292
292
|
# Run the example application
|
|
293
293
|
npm install
|
|
294
|
-
|
|
294
|
+
make example
|
|
295
295
|
|
|
296
296
|
# This starts both Node.js and Python servers plus a React demo app
|
|
297
|
-
# Open http://localhost:
|
|
297
|
+
# Open http://localhost:3000/split/index.html to see the 2 editors view
|
|
298
298
|
```
|
|
299
299
|
|
|
300
300
|
The examples include:
|
package/lib/Editor.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/*
|
|
3
|
-
* Copyright (c)
|
|
3
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
4
4
|
* Distributed under the terms of the MIT License.
|
|
5
5
|
*/
|
|
6
6
|
import { useEffect, useState } from 'react';
|
|
@@ -89,6 +89,7 @@ export default function Editor() {
|
|
|
89
89
|
const [floatingAnchorElem, setFloatingAnchorElem] = useState(null);
|
|
90
90
|
const [isSmallWidthViewport, setIsSmallWidthViewport] = useState(false);
|
|
91
91
|
const [editor] = useLexicalComposerContext();
|
|
92
|
+
const collabId = new URLSearchParams(window.location.search).get('collabId') || 'main';
|
|
92
93
|
const [activeEditor, setActiveEditor] = useState(editor);
|
|
93
94
|
const [isLinkEditMode, setIsLinkEditMode] = useState(false);
|
|
94
95
|
const onRef = (_floatingAnchorElem) => {
|
|
@@ -110,6 +111,6 @@ export default function Editor() {
|
|
|
110
111
|
};
|
|
111
112
|
}, [isSmallWidthViewport]);
|
|
112
113
|
return (_jsxs(_Fragment, { children: [isRichText && (_jsx(ToolbarPlugin, { editor: editor, activeEditor: activeEditor, setActiveEditor: setActiveEditor, setIsLinkEditMode: setIsLinkEditMode })), isRichText && (_jsx(ShortcutsPlugin, { editor: activeEditor, setIsLinkEditMode: setIsLinkEditMode })), _jsxs("div", { className: `editor-container ${showTreeView ? 'tree-view' : ''} ${!isRichText ? 'plain-text' : ''}`, children: [isMaxLength && _jsx(MaxLengthPlugin, { maxLength: 30 }), _jsx(DragDropPaste, {}), _jsx(DebugPlugin, {}), _jsx(AutoFocusPlugin, {}), selectionAlwaysOnDisplay && _jsx(SelectionAlwaysOnDisplay, {}), _jsx(ClearEditorPlugin, {}), _jsx(ComponentPickerPlugin, {}), _jsx(EmojiPickerPlugin, {}), _jsx(AutoEmbedPlugin, {}), _jsx(MentionsPlugin, {}), _jsx(EmojisPlugin, {}), _jsx(HashtagPlugin, {}), _jsx(KeywordsPlugin, {}), _jsx(SpeechToTextPlugin, {}), _jsx(AutoLinkPlugin, {}), _jsx(DateTimePlugin, {}), useYjs &&
|
|
113
|
-
_jsx(CommentPlugin, { providerFactory: isCollab ? createYjsWebsocketProvider : undefined }), isRichText ? (_jsxs(_Fragment, { children: [isCollab ? (useYjs ? (_jsx(YjsCollaborationPlugin, { id:
|
|
114
|
+
_jsx(CommentPlugin, { providerFactory: isCollab ? createYjsWebsocketProvider : undefined }), isRichText ? (_jsxs(_Fragment, { children: [isCollab ? (useYjs ? (_jsx(YjsCollaborationPlugin, { id: collabId, providerFactory: createYjsWebsocketProvider, shouldBootstrap: !skipCollaborationInit })) : (_jsx(LoroCollaborationPlugin, { id: collabId, showCollaborators: true, providerFactory: createLoroWebsocketProvider, shouldBootstrap: !skipCollaborationInit }))) : (_jsx(HistoryPlugin, { externalHistoryState: historyState })), _jsx(RichTextPlugin, { contentEditable: _jsx("div", { className: "editor-scroller", children: _jsx("div", { className: "editor", ref: onRef, children: _jsx(ContentEditable, { placeholder: placeholder }) }) }), ErrorBoundary: LexicalErrorBoundary }), _jsx(MarkdownShortcutPlugin, {}), isCodeHighlighted &&
|
|
114
115
|
(isCodeShiki ? (_jsx(CodeHighlightShikiPlugin, {})) : (_jsx(CodeHighlightPrismPlugin, {}))), _jsx(ListPlugin, { hasStrictIndent: listStrictIndent }), _jsx(CheckListPlugin, {}), _jsx(TablePlugin, { hasCellMerge: tableCellMerge, hasCellBackgroundColor: tableCellBackgroundColor, hasHorizontalScroll: tableHorizontalScroll }), _jsx(TableCellResizer, {}), _jsx(ImagesPlugin, {}), _jsx(InlineImagePlugin, {}), _jsx(LinkPlugin, { hasLinkAttributes: hasLinkAttributes }), _jsx(PollPlugin, {}), _jsx(TwitterPlugin, {}), _jsx(YouTubePlugin, {}), _jsx(FigmaPlugin, {}), _jsx(ClickableLinkPlugin, { disabled: isEditable }), _jsx(HorizontalRulePlugin, {}), _jsx(EquationsPlugin, {}), _jsx(ExcalidrawPlugin, {}), _jsx(TabFocusPlugin, {}), _jsx(TabIndentationPlugin, { maxIndent: 7 }), _jsx(CollapsiblePlugin, {}), _jsx(CounterPlugin, {}), _jsx(PageBreakPlugin, {}), _jsx(LayoutPlugin, {}), floatingAnchorElem && (_jsxs(_Fragment, { children: [_jsx(FloatingLinkEditorPlugin, { anchorElem: floatingAnchorElem, isLinkEditMode: isLinkEditMode, setIsLinkEditMode: setIsLinkEditMode }), _jsx(TableCellActionMenuPlugin, { anchorElem: floatingAnchorElem, cellMerge: true })] })), floatingAnchorElem && !isSmallWidthViewport && (_jsxs(_Fragment, { children: [_jsx(DraggableBlockPlugin, { anchorElem: floatingAnchorElem }), _jsx(CodeActionMenuPlugin, { anchorElem: floatingAnchorElem }), _jsx(TableHoverActionsPlugin, { anchorElem: floatingAnchorElem }), _jsx(FloatingTextFormatToolbarPlugin, { anchorElem: floatingAnchorElem, setIsLinkEditMode: setIsLinkEditMode })] }))] })) : (_jsxs(_Fragment, { children: [_jsx(PlainTextPlugin, { contentEditable: _jsx(ContentEditable, { placeholder: placeholder }), ErrorBoundary: LexicalErrorBoundary }), _jsx(HistoryPlugin, { externalHistoryState: historyState })] })), (isCharLimit || isCharLimitUtf8) && (_jsx(CharacterLimitPlugin, { charset: isCharLimit ? 'UTF-16' : 'UTF-8', maxLength: 5 })), isAutocomplete && _jsx(AutocompletePlugin, {}), _jsx("div", { children: showTableOfContents && _jsx(TableOfContentsPlugin, {}) }), shouldUseLexicalContextMenu && _jsx(ContextMenuPlugin, {}), shouldAllowHighlightingWithBrackets && _jsx(SpecialTextPlugin, {}), _jsx(ActionsPlugin, { isRichText: isRichText, shouldPreserveNewLinesInMarkdown: shouldPreserveNewLinesInMarkdown })] }), showTreeView && _jsx(TreeViewPlugin, {})] }));
|
|
115
116
|
}
|
package/lib/appSettings.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* Copyright (c)
|
|
2
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
3
3
|
* Distributed under the terms of the MIT License.
|
|
4
4
|
*/
|
|
5
5
|
import invariant from '../utils/invariant';
|
|
@@ -11,9 +11,9 @@ export function createBinding(editor, provider, id, doc, docMap, excludedPropert
|
|
|
11
11
|
invariant(doc !== undefined && doc !== null, 'createBinding: doc is null or undefined');
|
|
12
12
|
// Initialize the tree - content will come from server snapshot via sync
|
|
13
13
|
const tree = getLoroTree(doc);
|
|
14
|
-
console.log('
|
|
14
|
+
console.log(' Loro tree initialized, content will be populated via server sync');
|
|
15
15
|
const clientID = generateClientID(doc);
|
|
16
|
-
console.log('
|
|
16
|
+
console.log('- Creating binding:', {
|
|
17
17
|
bindingId: id,
|
|
18
18
|
localPeerId: doc.peerId,
|
|
19
19
|
generatedClientID: clientID,
|
|
@@ -41,7 +41,7 @@ export function createBinding(editor, provider, id, doc, docMap, excludedPropert
|
|
|
41
41
|
// Setup debugging utilities only if debug is enabled via URL parameter
|
|
42
42
|
if (isDebugEnabled()) {
|
|
43
43
|
setupLoroDebugging(binding);
|
|
44
|
-
console.log('
|
|
44
|
+
console.log(' Loro debugging enabled via ?debug=true URL parameter');
|
|
45
45
|
}
|
|
46
46
|
return binding;
|
|
47
47
|
}
|
|
@@ -65,10 +65,10 @@ export function scheduleAsyncCommit(binding, delay = 500) {
|
|
|
65
65
|
try {
|
|
66
66
|
// Perform the actual commit
|
|
67
67
|
binding.doc.commit({ origin: binding.doc.peerIdStr });
|
|
68
|
-
console.log('
|
|
68
|
+
console.log(' Async commit completed for binding:', binding.id);
|
|
69
69
|
}
|
|
70
70
|
catch (error) {
|
|
71
|
-
console.error('
|
|
71
|
+
console.error(' Async commit failed for binding:', binding.id, error);
|
|
72
72
|
}
|
|
73
73
|
// Reset pending state
|
|
74
74
|
binding.pendingCommit = false;
|
|
@@ -90,7 +90,7 @@ export function flushPendingCommit(binding) {
|
|
|
90
90
|
if (binding.pendingCommit) {
|
|
91
91
|
binding.doc.commit({ origin: binding.doc.peerIdStr });
|
|
92
92
|
binding.pendingCommit = false;
|
|
93
|
-
console.log('
|
|
93
|
+
console.log(' Forced commit completed for binding:', binding.id);
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
96
|
// Export components
|
package/lib/collab/loro/Debug.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* Copyright (c)
|
|
2
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
3
3
|
* Distributed under the terms of the MIT License.
|
|
4
4
|
*/
|
|
5
5
|
/**
|
|
@@ -45,7 +45,7 @@ export function setupLoroDebugging(binding) {
|
|
|
45
45
|
console.log(`Root nodes: ${rootNodes.length}`);
|
|
46
46
|
console.log('');
|
|
47
47
|
if (rootNodes.length === 0) {
|
|
48
|
-
console.log('
|
|
48
|
+
console.log(' No root nodes found!');
|
|
49
49
|
}
|
|
50
50
|
else {
|
|
51
51
|
rootNodes.forEach((root, index) => {
|
|
@@ -62,8 +62,8 @@ export function setupLoroDebugging(binding) {
|
|
|
62
62
|
verifyStructure: () => {
|
|
63
63
|
const binding = window.debugLoro.binding;
|
|
64
64
|
if (!binding)
|
|
65
|
-
return console.log('
|
|
66
|
-
console.log('
|
|
65
|
+
return console.log(' Loro Binding not available');
|
|
66
|
+
console.log(' LORO TREE VERIFICATION:');
|
|
67
67
|
const tree = binding.tree;
|
|
68
68
|
const nodes = tree.nodes();
|
|
69
69
|
console.log('Total nodes:', nodes.length);
|
|
@@ -71,13 +71,13 @@ export function setupLoroDebugging(binding) {
|
|
|
71
71
|
console.log('Client ID:', binding.clientID);
|
|
72
72
|
// Check mapping consistency
|
|
73
73
|
const mapper = binding.nodeMapper;
|
|
74
|
-
console.log('\n
|
|
74
|
+
console.log('\n Node Mappings:');
|
|
75
75
|
nodes.forEach((node, index) => {
|
|
76
76
|
const data = Object.fromEntries(node.data.entries());
|
|
77
77
|
const treeId = node.id;
|
|
78
78
|
// Get lexical key from mapper instead of node data
|
|
79
79
|
const lexicalKey = binding.nodeMapper?.getLexicalKeyByLoroId(treeId) || 'no-key';
|
|
80
|
-
console.log(`\n
|
|
80
|
+
console.log(`\n Node ${index}:`);
|
|
81
81
|
console.log(' TreeID:', treeId);
|
|
82
82
|
console.log(' Lexical Key:', lexicalKey);
|
|
83
83
|
console.log(' Element Type:', data.elementType || 'N/A');
|
|
@@ -104,7 +104,7 @@ export function setupLoroDebugging(binding) {
|
|
|
104
104
|
return !parent && !data.isRoot;
|
|
105
105
|
});
|
|
106
106
|
if (orphanedNodes.length > 0) {
|
|
107
|
-
console.log('\n
|
|
107
|
+
console.log('\n WARNING: Found orphaned nodes (no parent, not root):');
|
|
108
108
|
orphanedNodes.forEach(node => {
|
|
109
109
|
const data = Object.fromEntries(node.data.entries());
|
|
110
110
|
const lexicalKey = binding.nodeMapper?.getLexicalKeyByLoroId(node.id) || 'no-key';
|
|
@@ -112,7 +112,7 @@ export function setupLoroDebugging(binding) {
|
|
|
112
112
|
});
|
|
113
113
|
}
|
|
114
114
|
// Show parent-child relationships
|
|
115
|
-
console.log('\n
|
|
115
|
+
console.log('\n Parent-Child Relationships:');
|
|
116
116
|
nodes.forEach(node => {
|
|
117
117
|
const data = Object.fromEntries(node.data.entries());
|
|
118
118
|
const parent = node.parent();
|
|
@@ -143,7 +143,7 @@ export function setupLoroDebugging(binding) {
|
|
|
143
143
|
});
|
|
144
144
|
// Get lexical key from mapper instead of node data
|
|
145
145
|
const lexicalKey = binding.nodeMapper?.getLexicalKeyByLoroId(node.id) || 'no-key';
|
|
146
|
-
console.log('
|
|
146
|
+
console.log(' Loro Node Details:', {
|
|
147
147
|
treeId: node.id,
|
|
148
148
|
lexicalKey: lexicalKey,
|
|
149
149
|
elementType: data.elementType,
|
|
@@ -155,24 +155,24 @@ export function setupLoroDebugging(binding) {
|
|
|
155
155
|
});
|
|
156
156
|
// Handle different lexical data formats
|
|
157
157
|
if (Object.keys(lexicalProps).length > 0) {
|
|
158
|
-
console.log('
|
|
158
|
+
console.log(' Lexical Data (Individual Properties):', lexicalProps);
|
|
159
159
|
}
|
|
160
160
|
else if (data.lexical) {
|
|
161
161
|
if (typeof data.lexical === 'object') {
|
|
162
|
-
console.log('
|
|
162
|
+
console.log(' Lexical Data (Current JSON Object):', data.lexical);
|
|
163
163
|
}
|
|
164
164
|
else if (typeof data.lexical === 'string') {
|
|
165
165
|
try {
|
|
166
166
|
const lexicalData = JSON.parse(data.lexical);
|
|
167
|
-
console.log('
|
|
167
|
+
console.log(' Lexical Data (Legacy JSON String):', lexicalData);
|
|
168
168
|
}
|
|
169
169
|
catch (e) {
|
|
170
|
-
console.log('
|
|
170
|
+
console.log(' Failed to parse lexical data:', e);
|
|
171
171
|
}
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
174
|
else {
|
|
175
|
-
console.log('
|
|
175
|
+
console.log(' No lexical data found for this node');
|
|
176
176
|
}
|
|
177
177
|
// Fetch and log the corresponding lexical node
|
|
178
178
|
if (lexicalKey && lexicalKey !== 'no-key' && typeof lexicalKey === 'string') {
|
|
@@ -200,18 +200,18 @@ export function setupLoroDebugging(binding) {
|
|
|
200
200
|
return info;
|
|
201
201
|
});
|
|
202
202
|
if (nodeInfo) {
|
|
203
|
-
console.log('
|
|
203
|
+
console.log(' Linked Lexical Node:', nodeInfo);
|
|
204
204
|
}
|
|
205
205
|
else {
|
|
206
|
-
console.log('
|
|
206
|
+
console.log(' No lexical node found in editor state for key:', lexicalKey, '(node may have been deleted or not yet created)');
|
|
207
207
|
}
|
|
208
208
|
}
|
|
209
209
|
catch (e) {
|
|
210
|
-
console.log('
|
|
210
|
+
console.log(' Failed to fetch lexical node:', e);
|
|
211
211
|
}
|
|
212
212
|
}
|
|
213
213
|
else {
|
|
214
|
-
console.log('
|
|
214
|
+
console.log(' No valid lexical key found for this Loro node (key:', lexicalKey, ')');
|
|
215
215
|
}
|
|
216
216
|
}
|
|
217
217
|
else {
|
|
@@ -268,7 +268,7 @@ export function setupLoroDebugging(binding) {
|
|
|
268
268
|
const tree = binding.tree;
|
|
269
269
|
const nodes = tree.nodes();
|
|
270
270
|
// Debug: log the actual structure to console as a tree
|
|
271
|
-
console.log('
|
|
271
|
+
console.log(' LORO Tree structure:');
|
|
272
272
|
console.log(` Total nodes: ${nodes.length}`);
|
|
273
273
|
console.log(` Peer ID: ${binding.doc.peerIdStr}`);
|
|
274
274
|
console.log(` Client ID: ${binding.clientID}`);
|
|
@@ -299,7 +299,7 @@ export function setupLoroDebugging(binding) {
|
|
|
299
299
|
return !parent || data.isRoot;
|
|
300
300
|
});
|
|
301
301
|
if (rootNodes.length === 0) {
|
|
302
|
-
console.log('
|
|
302
|
+
console.log(' No root nodes found!');
|
|
303
303
|
}
|
|
304
304
|
else {
|
|
305
305
|
rootNodes.forEach((root, index) => {
|
|
@@ -307,8 +307,8 @@ export function setupLoroDebugging(binding) {
|
|
|
307
307
|
logTreeStructure(root, '', isLastRoot, 0);
|
|
308
308
|
});
|
|
309
309
|
}
|
|
310
|
-
console.log('
|
|
311
|
-
console.log('
|
|
310
|
+
console.log(' Loro Tree:', tree.toJSON());
|
|
311
|
+
console.log(' Lexical State:', binding.editor.getEditorState().toJSON());
|
|
312
312
|
const treeHTML = window.debugLoro.generateTreeHTML(nodes);
|
|
313
313
|
const debugDiv = document.getElementById('debug-loro') || document.createElement('div');
|
|
314
314
|
const existingDiv = document.getElementById('debug-loro');
|
|
@@ -363,7 +363,7 @@ export function setupLoroDebugging(binding) {
|
|
|
363
363
|
}
|
|
364
364
|
debugDiv.innerHTML = `
|
|
365
365
|
<div class="debug-drag-integrate" style="color: #00ff88; font-weight: bold; margin-bottom: 10px; border-bottom: 1px solid #00ff00; padding: 15px 15px 5px 15px; cursor: move; background: linear-gradient(90deg, rgba(0,255,136,0.1), transparent);">
|
|
366
|
-
|
|
366
|
+
LORO TREE <span style="float: right; font-size: 9px; color: #666;">⋮⋮ drag</span>
|
|
367
367
|
</div>
|
|
368
368
|
<div style="padding: 0 15px 15px 15px; overflow-y: auto; max-height: calc(80vh - 50px);">
|
|
369
369
|
<div style="color: #00ffaa; margin-bottom: 8px;">Total nodes: ${nodes.length}</div>
|
|
@@ -373,12 +373,12 @@ export function setupLoroDebugging(binding) {
|
|
|
373
373
|
${treeHTML}
|
|
374
374
|
</div>
|
|
375
375
|
<div style="margin-top: 10px; font-size: 10px; color: #666;">
|
|
376
|
-
<span onclick="window.debugLoro.addDebugToPage()" style="color: #00ffaa; cursor: pointer; text-decoration: underline;"
|
|
377
|
-
<span onclick="window.debugLoro.verifyStructure()" style="color: #00ff66; cursor: pointer; text-decoration: underline;"
|
|
378
|
-
<span onclick="window.debugLoro.logStructure()" style="color: #00ffdd; cursor: pointer; text-decoration: underline;"
|
|
379
|
-
<span onclick="window.debugLoro.cleanupEphemeralStore()" style="color: #ffaa00; cursor: pointer; text-decoration: underline;"
|
|
380
|
-
<span onclick="window.debugLoro.resetGlobalEphemeralStore()" style="color: #ff6600; cursor: pointer; text-decoration: underline;"
|
|
381
|
-
<span onclick="document.getElementById('debug-loro').remove()" style="color: #ff0066; cursor: pointer; text-decoration: underline;"
|
|
376
|
+
<span onclick="window.debugLoro.addDebugToPage()" style="color: #00ffaa; cursor: pointer; text-decoration: underline;"> Refresh</span> |
|
|
377
|
+
<span onclick="window.debugLoro.verifyStructure()" style="color: #00ff66; cursor: pointer; text-decoration: underline;"> Verify</span> |
|
|
378
|
+
<span onclick="window.debugLoro.logStructure()" style="color: #00ffdd; cursor: pointer; text-decoration: underline;"> Log</span> |
|
|
379
|
+
<span onclick="window.debugLoro.cleanupEphemeralStore()" style="color: #ffaa00; cursor: pointer; text-decoration: underline;"> Cleanup</span> |
|
|
380
|
+
<span onclick="window.debugLoro.resetGlobalEphemeralStore()" style="color: #ff6600; cursor: pointer; text-decoration: underline;"> Reset</span> |
|
|
381
|
+
<span onclick="document.getElementById('debug-loro').remove()" style="color: #ff0066; cursor: pointer; text-decoration: underline;"> Close</span>
|
|
382
382
|
</div>
|
|
383
383
|
</div>
|
|
384
384
|
`;
|
|
@@ -386,27 +386,27 @@ export function setupLoroDebugging(binding) {
|
|
|
386
386
|
},
|
|
387
387
|
cleanupEphemeralStore: (provider) => {
|
|
388
388
|
if (!provider) {
|
|
389
|
-
console.log('
|
|
389
|
+
console.log(' No provider passed. Usage: window.debugLoro.cleanupEphemeralStore(provider)');
|
|
390
390
|
console.log('You can get the provider from your useCollaboration hook');
|
|
391
391
|
return;
|
|
392
392
|
}
|
|
393
393
|
if (typeof provider.cleanupStaleStates !== 'function') {
|
|
394
|
-
console.log('
|
|
394
|
+
console.log(' Provider does not have cleanupStaleStates method');
|
|
395
395
|
return;
|
|
396
396
|
}
|
|
397
|
-
console.log('
|
|
397
|
+
console.log(' Cleaning up stale ephemeral states...');
|
|
398
398
|
provider.cleanupStaleStates();
|
|
399
399
|
},
|
|
400
400
|
resetGlobalEphemeralStore: () => {
|
|
401
401
|
try {
|
|
402
402
|
// We need to access the WebsocketProvider class which may not be globally available
|
|
403
|
-
console.log('
|
|
404
|
-
console.log('
|
|
403
|
+
console.log(' Attempting to reset global EphemeralStore...');
|
|
404
|
+
console.log(' You can also refresh the browser to clear the global store');
|
|
405
405
|
// For now, instruct user to refresh
|
|
406
406
|
alert('To reset the global ephemeral store and clear all collaborators, please refresh the browser page.');
|
|
407
407
|
}
|
|
408
408
|
catch (error) {
|
|
409
|
-
console.warn('
|
|
409
|
+
console.warn(' Reset failed:', error.message);
|
|
410
410
|
}
|
|
411
411
|
}
|
|
412
412
|
};
|
|
@@ -19,6 +19,11 @@ type Props = {
|
|
|
19
19
|
showCollaborators?: boolean;
|
|
20
20
|
websocketUrl?: string;
|
|
21
21
|
onInitialization?: (isInitialized: boolean) => void;
|
|
22
|
+
onIdentityResolved?: (identity: {
|
|
23
|
+
name: string;
|
|
24
|
+
color: string;
|
|
25
|
+
clientID: number;
|
|
26
|
+
}) => void;
|
|
22
27
|
};
|
|
23
|
-
export declare function LoroCollaborationPlugin({ id, providerFactory, shouldBootstrap, username, cursorColor, cursorsContainerRef, initialEditorState, excludedProperties, awarenessData, syncCursorPositionsFn, showCollaborators, websocketUrl, onInitialization, }: Props): JSX.Element;
|
|
28
|
+
export declare function LoroCollaborationPlugin({ id, providerFactory, shouldBootstrap, username, cursorColor, cursorsContainerRef, initialEditorState, excludedProperties, awarenessData, syncCursorPositionsFn, showCollaborators, websocketUrl, onInitialization, onIdentityResolved, }: Props): JSX.Element;
|
|
24
29
|
export {};
|
|
@@ -5,7 +5,28 @@ import { useCollaborationContext, generateDeterministicUserData, } from './Lexic
|
|
|
5
5
|
import { updateLocalStateName } from './State';
|
|
6
6
|
import { useCollaboration, useFocusTracking, useHistory, } from './useCollaboration';
|
|
7
7
|
import { createBinding, LoroCollaborationUI } from './Bindings';
|
|
8
|
-
|
|
8
|
+
function getIdentityField(awarenessData, keys) {
|
|
9
|
+
if (!awarenessData || typeof awarenessData !== 'object') {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
const data = awarenessData;
|
|
13
|
+
const user = data.user;
|
|
14
|
+
const userRecord = user && typeof user === 'object' ? user : undefined;
|
|
15
|
+
for (const key of keys) {
|
|
16
|
+
const direct = data[key];
|
|
17
|
+
if (typeof direct === 'string' && direct.trim().length > 0) {
|
|
18
|
+
return direct;
|
|
19
|
+
}
|
|
20
|
+
if (userRecord) {
|
|
21
|
+
const nested = userRecord[key];
|
|
22
|
+
if (typeof nested === 'string' && nested.trim().length > 0) {
|
|
23
|
+
return nested;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
export function LoroCollaborationPlugin({ id, providerFactory, shouldBootstrap, username, cursorColor, cursorsContainerRef, initialEditorState, excludedProperties, awarenessData, syncCursorPositionsFn, showCollaborators = true, websocketUrl = 'ws://localhost:3002', onInitialization, onIdentityResolved, }) {
|
|
9
30
|
const isBindingInitialized = useRef(false);
|
|
10
31
|
const isProviderInitialized = useRef(false);
|
|
11
32
|
const collabContext = useCollaborationContext(username, cursorColor);
|
|
@@ -45,27 +66,34 @@ export function LoroCollaborationPlugin({ id, providerFactory, shouldBootstrap,
|
|
|
45
66
|
}
|
|
46
67
|
isBindingInitialized.current = true;
|
|
47
68
|
const binding = createBinding(editor, provider, id, doc || docMap.get(id), docMap, excludedProperties);
|
|
48
|
-
// Update collaboration context with deterministic name based on client ID
|
|
49
|
-
// This ensures consistent names across browser sessions
|
|
50
69
|
const deterministicUserData = generateDeterministicUserData(binding.clientID);
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
70
|
+
// Prefer explicit username/color first, then awareness identity, then deterministic fallback.
|
|
71
|
+
const awarenessDisplayName = getIdentityField(awarenessData, [
|
|
72
|
+
'display_name',
|
|
73
|
+
'displayName',
|
|
74
|
+
'name',
|
|
75
|
+
'username',
|
|
76
|
+
'handle',
|
|
77
|
+
]);
|
|
78
|
+
const awarenessColor = getIdentityField(awarenessData, ['color']);
|
|
79
|
+
const finalName = username || awarenessDisplayName || deterministicUserData.name;
|
|
80
|
+
const finalColor = cursorColor || awarenessColor || deterministicUserData.color;
|
|
59
81
|
collabContext.name = finalName;
|
|
60
82
|
collabContext.color = finalColor;
|
|
61
83
|
collabContext.clientID = binding.clientID;
|
|
62
|
-
//
|
|
84
|
+
// Notify listeners of the resolved local collaborator identity.
|
|
85
|
+
onIdentityResolved?.({
|
|
86
|
+
name: finalName,
|
|
87
|
+
color: finalColor,
|
|
88
|
+
clientID: binding.clientID,
|
|
89
|
+
});
|
|
90
|
+
// Update the awareness state immediately with the resolved identity.
|
|
63
91
|
updateLocalStateName(provider, finalName, finalColor);
|
|
64
92
|
setBinding(binding);
|
|
65
93
|
return () => {
|
|
66
94
|
// Clean up binding resources if needed
|
|
67
95
|
};
|
|
68
|
-
}, [editor, provider, id, docMap, doc, excludedProperties, collabContext, username, cursorColor]);
|
|
96
|
+
}, [editor, provider, id, docMap, doc, excludedProperties, collabContext, username, cursorColor, awarenessData]);
|
|
69
97
|
if (!provider || !binding) {
|
|
70
98
|
return _jsx(_Fragment, {});
|
|
71
99
|
}
|
package/lib/collab/loro/State.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/*
|
|
3
|
-
* Copyright (c)
|
|
3
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
4
4
|
* Distributed under the terms of the MIT License.
|
|
5
5
|
*/
|
|
6
6
|
import { useState, useEffect } from 'react';
|
|
@@ -44,7 +44,7 @@ export function LoroCollaborators({ binding, currentUserName = 'Me', currentUser
|
|
|
44
44
|
gap: '8px',
|
|
45
45
|
flexWrap: 'wrap',
|
|
46
46
|
color: 'var(--fgColor-default, inherit)',
|
|
47
|
-
}, children: [currentUserData && (_jsx(CollaboratorBadge, { name: `${currentUserData.cursor.name}`, color: currentDisplayColor, isCurrentUser: true, clientId: currentClientID })), !currentUserData && (_jsx(CollaboratorBadge, { name: `${currentDisplayName} (
|
|
47
|
+
}, children: [currentUserData && (_jsx(CollaboratorBadge, { name: `${currentUserData.cursor.name} (me)`, color: currentDisplayColor, isCurrentUser: true, clientId: currentClientID })), !currentUserData && (_jsx(CollaboratorBadge, { name: `${currentDisplayName} (me)`, color: currentDisplayColor, isCurrentUser: true, clientId: currentClientID })), otherCollaborators.length > 0 && (_jsx("div", { style: {
|
|
48
48
|
width: '1px',
|
|
49
49
|
height: '20px',
|
|
50
50
|
backgroundColor: 'var(--borderColor-muted, #d1d5db)',
|
|
@@ -56,21 +56,9 @@ export function LoroCollaborators({ binding, currentUserName = 'Me', currentUser
|
|
|
56
56
|
}, children: "No other collaborators" }))] }));
|
|
57
57
|
}
|
|
58
58
|
function CollaboratorBadge({ name, color, isCurrentUser, clientId }) {
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
if (color.startsWith('#')) {
|
|
63
|
-
const hex = color.slice(1);
|
|
64
|
-
const r = parseInt(hex.slice(0, 2), 16);
|
|
65
|
-
const g = parseInt(hex.slice(2, 4), 16);
|
|
66
|
-
const b = parseInt(hex.slice(4, 6), 16);
|
|
67
|
-
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
|
|
68
|
-
}
|
|
69
|
-
// If it's already rgba or rgb, just return as is (basic support)
|
|
70
|
-
return color;
|
|
71
|
-
};
|
|
72
|
-
const circleColor = isCurrentUser ? getColorWithOpacity(color, 0.1) : color;
|
|
73
|
-
const badgeColor = isCurrentUser ? getColorWithOpacity(color, 0.1) : color;
|
|
59
|
+
const circleColor = color;
|
|
60
|
+
const badgeColor = color;
|
|
61
|
+
const shortClientId = String(clientId).slice(0, 4);
|
|
74
62
|
return (_jsxs("div", { style: {
|
|
75
63
|
display: 'flex',
|
|
76
64
|
alignItems: 'center',
|
|
@@ -96,7 +84,7 @@ function CollaboratorBadge({ name, color, isCurrentUser, clientId }) {
|
|
|
96
84
|
boxShadow: isCurrentUser
|
|
97
85
|
? '0 1px 3px rgba(0,0,0,0.2), inset 0 1px 0 rgba(255,255,255,0.2)'
|
|
98
86
|
: '0 1px 2px rgba(0,0,0,0.1)',
|
|
99
|
-
opacity:
|
|
100
|
-
}, children: name })] }));
|
|
87
|
+
opacity: 1
|
|
88
|
+
}, children: `${name} ${shortClientId}` })] }));
|
|
101
89
|
}
|
|
102
90
|
export default LoroCollaborators;
|
package/lib/collab/loro/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* Copyright (c)
|
|
2
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
3
3
|
* Distributed under the terms of the MIT License.
|
|
4
4
|
*/
|
|
5
5
|
/**
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
export class ListIntegrator {
|
|
9
9
|
integrate(diff, binding, provider) {
|
|
10
|
-
console.log('
|
|
10
|
+
console.log(' Handling ListDiff:', diff);
|
|
11
11
|
this.integrateInternal(diff, binding, provider);
|
|
12
12
|
}
|
|
13
13
|
// Internal method for use when already inside editor.update()
|
|
@@ -25,7 +25,7 @@ export class ListIntegrator {
|
|
|
25
25
|
this.integrateRetain(change, binding, provider);
|
|
26
26
|
break;
|
|
27
27
|
default:
|
|
28
|
-
console.warn(
|
|
28
|
+
console.warn(` Unknown list change type: ${change.type}`);
|
|
29
29
|
}
|
|
30
30
|
});
|
|
31
31
|
}
|
|
@@ -46,7 +46,7 @@ export class ListIntegrator {
|
|
|
46
46
|
// Retain operations typically don't require action in Lexical
|
|
47
47
|
// They represent portions of the list that remain unchanged
|
|
48
48
|
if (change.attributes) {
|
|
49
|
-
console.log(
|
|
49
|
+
console.log(` List retain with attributes at ${change.index}, length: ${change.length}`, change.attributes);
|
|
50
50
|
// Handle any attribute changes if needed
|
|
51
51
|
}
|
|
52
52
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* Copyright (c)
|
|
2
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
3
3
|
* Distributed under the terms of the MIT License.
|
|
4
4
|
*/
|
|
5
5
|
import { $getNodeByKey, $isDecoratorNode } from 'lexical';
|
|
@@ -188,7 +188,7 @@ export class MapIntegrator {
|
|
|
188
188
|
const registeredNodes = binding.editor._nodes;
|
|
189
189
|
const nodeInfo = registeredNodes.get(targetType);
|
|
190
190
|
if (!nodeInfo) {
|
|
191
|
-
console.warn(
|
|
191
|
+
console.warn(` MapIntegrator: Node type '${targetType}' not registered`);
|
|
192
192
|
return;
|
|
193
193
|
}
|
|
194
194
|
const serializedData = { ...data };
|
|
@@ -208,7 +208,7 @@ export class MapIntegrator {
|
|
|
208
208
|
binding.nodeMapper.setMapping(newNode.getKey(), treeId);
|
|
209
209
|
}
|
|
210
210
|
catch (error) {
|
|
211
|
-
console.warn(
|
|
211
|
+
console.warn(` MapIntegrator: importJSON+replace failed for decorator ${targetType} node ${lexicalKey}:`, error);
|
|
212
212
|
}
|
|
213
213
|
}
|
|
214
214
|
else {
|
|
@@ -228,7 +228,7 @@ export class MapIntegrator {
|
|
|
228
228
|
writable.updateFromJSON(serializedData);
|
|
229
229
|
}
|
|
230
230
|
catch (error) {
|
|
231
|
-
console.warn(
|
|
231
|
+
console.warn(` MapIntegrator: updateFromJSON failed for ${targetType} node ${lexicalKey}:`, error);
|
|
232
232
|
}
|
|
233
233
|
}
|
|
234
234
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* Copyright (c)
|
|
2
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
3
3
|
* Distributed under the terms of the MIT License.
|
|
4
4
|
*/
|
|
5
5
|
/**
|
|
@@ -24,7 +24,7 @@ export class TextIntegrator {
|
|
|
24
24
|
this.integrateTextRetain(change, binding, provider);
|
|
25
25
|
break;
|
|
26
26
|
default:
|
|
27
|
-
console.warn(
|
|
27
|
+
console.warn(` Unknown text change type: ${change.type}`);
|
|
28
28
|
}
|
|
29
29
|
});
|
|
30
30
|
}
|
|
@@ -19,8 +19,13 @@ export declare class TreeIntegrator implements BaseIntegrator<TreeDiff> {
|
|
|
19
19
|
integrate(diff: TreeDiff, binding: Binding, provider: Provider): void;
|
|
20
20
|
integrateInternal(diff: TreeDiff, binding: Binding, provider: Provider): void;
|
|
21
21
|
private integrateCreate;
|
|
22
|
+
/** Only element and decorator nodes may be inserted directly under the root. */
|
|
23
|
+
private canBeRootChild;
|
|
24
|
+
/** Insert a child into a parent element at an optional index. */
|
|
25
|
+
private insertChild;
|
|
22
26
|
private integrateMove;
|
|
23
27
|
private integrateDelete;
|
|
28
|
+
private collectDescendantKeys;
|
|
24
29
|
/**
|
|
25
30
|
* Topologically sort create operations so that parent nodes are created
|
|
26
31
|
* before their children.
|