@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.
Files changed (113) hide show
  1. package/README.md +2 -2
  2. package/lib/Editor.js +3 -2
  3. package/lib/appSettings.js +1 -1
  4. package/lib/collab/loro/Bindings.js +7 -7
  5. package/lib/collab/loro/Debug.js +35 -35
  6. package/lib/collab/loro/LexicalCollaborationContext.js +1 -1
  7. package/lib/collab/loro/LexicalCollaborationPlugin.d.ts +6 -1
  8. package/lib/collab/loro/LexicalCollaborationPlugin.js +41 -13
  9. package/lib/collab/loro/State.js +1 -1
  10. package/lib/collab/loro/components/LoroCollaborators.js +7 -19
  11. package/lib/collab/loro/components/index.js +1 -1
  12. package/lib/collab/loro/index.js +1 -1
  13. package/lib/collab/loro/integrators/BaseIntegrator.js +1 -1
  14. package/lib/collab/loro/integrators/CounterIntegrator.js +1 -1
  15. package/lib/collab/loro/integrators/ListIntegrator.js +4 -4
  16. package/lib/collab/loro/integrators/MapIntegrator.js +4 -4
  17. package/lib/collab/loro/integrators/TextIntegrator.js +2 -2
  18. package/lib/collab/loro/integrators/TreeIntegrator.d.ts +5 -0
  19. package/lib/collab/loro/integrators/TreeIntegrator.js +184 -142
  20. package/lib/collab/loro/nodes/NodeFactory.js +26 -47
  21. package/lib/collab/loro/nodes/NodesMapper.js +19 -7
  22. package/lib/collab/loro/propagators/DecoratorNodePropagator.js +1 -1
  23. package/lib/collab/loro/propagators/ElementNodePropagator.d.ts +1 -1
  24. package/lib/collab/loro/propagators/ElementNodePropagator.js +102 -35
  25. package/lib/collab/loro/propagators/LineBreakNodePropagator.js +1 -1
  26. package/lib/collab/loro/propagators/RootNodePropagator.js +1 -1
  27. package/lib/collab/loro/propagators/TextNodePropagator.js +55 -45
  28. package/lib/collab/loro/propagators/index.d.ts +1 -1
  29. package/lib/collab/loro/propagators/index.js +1 -1
  30. package/lib/collab/loro/provider/websocket.d.ts +1 -0
  31. package/lib/collab/loro/provider/websocket.js +47 -12
  32. package/lib/collab/loro/servers/index.js +1 -1
  33. package/lib/collab/loro/servers/ws/callback.js +1 -1
  34. package/lib/collab/loro/servers/ws/server.js +1 -1
  35. package/lib/collab/loro/servers/ws/utils.js +35 -35
  36. package/lib/collab/loro/sync/SyncCursors.js +42 -16
  37. package/lib/collab/loro/sync/SyncLexicalToLoro.js +76 -37
  38. package/lib/collab/loro/sync/SyncLoroToLexical.js +32 -26
  39. package/lib/collab/loro/types/LexicalNodeData.js +1 -1
  40. package/lib/collab/loro/useCollaboration.js +173 -28
  41. package/lib/collab/loro/utils/InitialContent.js +1 -1
  42. package/lib/collab/loro/utils/Invariant.d.ts +15 -0
  43. package/lib/collab/loro/utils/Invariant.js +24 -0
  44. package/lib/collab/loro/utils/LexicalToLoro.js +1 -1
  45. package/lib/collab/loro/utils/Utils.js +1 -1
  46. package/lib/collab/loro/wsProvider.js +21 -4
  47. package/lib/collab/utils/invariant.js +1 -1
  48. package/lib/collab/utils/simpleDiffWithCursor.js +1 -1
  49. package/lib/commenting/index.js +1 -1
  50. package/lib/demo.js +1 -1
  51. package/lib/hooks/useFlashMessage.js +1 -1
  52. package/lib/hooks/useReport.js +1 -1
  53. package/lib/index.js +1 -1
  54. package/lib/nodes/AutocompleteNode.js +1 -1
  55. package/lib/nodes/EmojiNode.js +1 -1
  56. package/lib/nodes/KeywordNode.js +1 -1
  57. package/lib/nodes/LayoutContainerNode.js +1 -1
  58. package/lib/nodes/LayoutItemNode.js +1 -1
  59. package/lib/nodes/MentionNode.js +1 -1
  60. package/lib/nodes/PlaygroundNodes.js +1 -1
  61. package/lib/nodes/SpecialTextNode.js +1 -1
  62. package/lib/plugins/AutocompletePlugin/index.js +1 -1
  63. package/lib/plugins/CodeActionMenuPlugin/components/CopyButton/index.js +1 -1
  64. package/lib/plugins/CodeActionMenuPlugin/components/PrettierButton/index.js +1 -1
  65. package/lib/plugins/CodeActionMenuPlugin/utils.js +1 -1
  66. package/lib/plugins/CodeHighlightPrismPlugin/index.js +1 -1
  67. package/lib/plugins/CodeHighlightShikiPlugin/index.js +1 -1
  68. package/lib/plugins/CollapsiblePlugin/CollapsibleContainerNode.js +1 -1
  69. package/lib/plugins/CollapsiblePlugin/CollapsibleContentNode.js +1 -1
  70. package/lib/plugins/CollapsiblePlugin/CollapsibleTitleNode.js +1 -1
  71. package/lib/plugins/CollapsiblePlugin/CollapsibleUtils.js +1 -1
  72. package/lib/plugins/CollapsiblePlugin/index.js +1 -1
  73. package/lib/plugins/CounterPlugin/index.js +1 -1
  74. package/lib/plugins/DateTimePlugin/index.js +1 -1
  75. package/lib/plugins/DebugPlugin/index.js +59 -3
  76. package/lib/plugins/DragDropPastePlugin/index.js +1 -1
  77. package/lib/plugins/EmojiPickerPlugin/index.js +1 -1
  78. package/lib/plugins/EmojisPlugin/index.js +1 -1
  79. package/lib/plugins/FigmaPlugin/index.js +1 -1
  80. package/lib/plugins/KeywordsPlugin/index.js +1 -1
  81. package/lib/plugins/LayoutPlugin/LayoutPlugin.js +1 -1
  82. package/lib/plugins/MarkdownTransformers/index.js +1 -1
  83. package/lib/plugins/MaxLengthPlugin/index.js +1 -1
  84. package/lib/plugins/PageBreakPlugin/index.js +1 -1
  85. package/lib/plugins/ShortcutsPlugin/index.js +1 -1
  86. package/lib/plugins/ShortcutsPlugin/shortcuts.js +1 -1
  87. package/lib/plugins/SpecialTextPlugin/index.js +1 -1
  88. package/lib/plugins/SpeechToTextPlugin/index.js +1 -1
  89. package/lib/plugins/StickyPlugin/index.js +1 -1
  90. package/lib/plugins/TabFocusPlugin/index.js +1 -1
  91. package/lib/plugins/ToolbarPlugin/fontSize.js +1 -1
  92. package/lib/plugins/ToolbarPlugin/utils.js +1 -1
  93. package/lib/plugins/TwitterPlugin/index.js +1 -1
  94. package/lib/plugins/TypingPerfPlugin/index.js +1 -1
  95. package/lib/plugins/YouTubePlugin/index.js +1 -1
  96. package/lib/server/validation.js +1 -1
  97. package/lib/setupEnv.js +1 -1
  98. package/lib/themes/CommentEditorTheme.js +1 -1
  99. package/lib/themes/PlaygroundEditorTheme.js +1 -1
  100. package/lib/themes/StickyEditorTheme.js +1 -1
  101. package/lib/tyes.dt.js +1 -1
  102. package/lib/utils/docSerialization.js +1 -1
  103. package/lib/utils/emoji-list.js +1 -1
  104. package/lib/utils/getDOMRangeRect.js +1 -1
  105. package/lib/utils/getSelectedNode.js +1 -1
  106. package/lib/utils/getThemeSelector.js +1 -1
  107. package/lib/utils/isMobileWidth.js +1 -1
  108. package/lib/utils/joinClasses.js +1 -1
  109. package/lib/utils/setFloatingElemPosition.js +1 -1
  110. package/lib/utils/setFloatingElemPositionForLinkEditor.js +1 -1
  111. package/lib/utils/swipe.js +1 -1
  112. package/lib/utils/url.js +1 -1
  113. 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
- npm run example
294
+ make example
295
295
 
296
296
  # This starts both Node.js and Python servers plus a React demo app
297
- # Open http://localhost:5173 to see dual editor interface
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) 2023-2025 Datalayer, Inc.
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: "main", providerFactory: createYjsWebsocketProvider, shouldBootstrap: !skipCollaborationInit })) : (_jsx(LoroCollaborationPlugin, { id: "main", 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
+ _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
  }
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright (c) 2023-2025 Datalayer, Inc.
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
3
  * Distributed under the terms of the MIT License.
4
4
  */
5
5
  const hostName = window.location.hostname;
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright (c) 2023-2025 Datalayer, Inc.
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('📄 Loro tree initialized, content will be populated via server sync');
14
+ console.log(' Loro tree initialized, content will be populated via server sync');
15
15
  const clientID = generateClientID(doc);
16
- console.log('🏗️ BINDING DEBUG - Creating binding:', {
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('🐛 Loro debugging enabled via ?debug=true URL parameter');
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('🔄 Async commit completed for binding:', binding.id);
68
+ console.log(' Async commit completed for binding:', binding.id);
69
69
  }
70
70
  catch (error) {
71
- console.error(' Async commit failed for binding:', binding.id, 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('🔄 Forced commit completed for binding:', binding.id);
93
+ console.log(' Forced commit completed for binding:', binding.id);
94
94
  }
95
95
  }
96
96
  // Export components
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright (c) 2023-2025 Datalayer, Inc.
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('⚠️ No root nodes found!');
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(' Loro Binding not available');
66
- console.log('🔍 LORO TREE VERIFICATION:');
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📍 Node Mappings:');
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📍 Node ${index}:`);
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⚠️ WARNING: Found orphaned nodes (no parent, not root):');
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🌳 Parent-Child Relationships:');
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('🔍 Loro Node Details:', {
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('📄 Lexical Data (Individual Properties):', lexicalProps);
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('📄 Lexical Data (Current JSON Object):', data.lexical);
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('📄 Lexical Data (Legacy JSON String):', lexicalData);
167
+ console.log(' Lexical Data (Legacy JSON String):', lexicalData);
168
168
  }
169
169
  catch (e) {
170
- console.log(' Failed to parse lexical data:', e);
170
+ console.log(' Failed to parse lexical data:', e);
171
171
  }
172
172
  }
173
173
  }
174
174
  else {
175
- console.log('⚠️ No lexical data found for this node');
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('🔗 Linked Lexical Node:', nodeInfo);
203
+ console.log(' Linked Lexical Node:', nodeInfo);
204
204
  }
205
205
  else {
206
- console.log('⚠️ No lexical node found in editor state for key:', lexicalKey, '(node may have been deleted or not yet created)');
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(' Failed to fetch lexical node:', e);
210
+ console.log(' Failed to fetch lexical node:', e);
211
211
  }
212
212
  }
213
213
  else {
214
- console.log('⚠️ No valid lexical key found for this Loro node (key:', lexicalKey, ')');
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('🟢 LORO Tree structure:');
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(' ⚠️ No root nodes found!');
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('🟢 Loro Tree:', tree.toJSON());
311
- console.log('🟢 Lexical State:', binding.editor.getEditorState().toJSON());
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
- 🟢 LORO TREE <span style="float: right; font-size: 9px; color: #666;">⋮⋮ drag</span>
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;">🔄 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>
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('⚠️ No provider passed. Usage: window.debugLoro.cleanupEphemeralStore(provider)');
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(' Provider does not have cleanupStaleStates method');
394
+ console.log(' Provider does not have cleanupStaleStates method');
395
395
  return;
396
396
  }
397
- console.log('🧹 Cleaning up stale ephemeral states...');
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('🔄 Attempting to reset global EphemeralStore...');
404
- console.log('💡 You can also refresh the browser to clear the global store');
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(' Reset failed:', error.message);
409
+ console.warn(' Reset failed:', error.message);
410
410
  }
411
411
  }
412
412
  };
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright (c) 2023-2025 Datalayer, Inc.
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
3
  * Distributed under the terms of the MIT License.
4
4
  */
5
5
  import { createContext, useContext } from 'react';
@@ -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
- export function LoroCollaborationPlugin({ id, providerFactory, shouldBootstrap, username, cursorColor, cursorsContainerRef, initialEditorState, excludedProperties, awarenessData, syncCursorPositionsFn, showCollaborators = true, websocketUrl = 'ws://localhost:3002', onInitialization, }) {
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
- console.log('Setting deterministic user data based on client ID:', {
52
- clientId: binding.clientID,
53
- name: deterministicUserData.name,
54
- color: deterministicUserData.color
55
- });
56
- // Update the collaboration context with stable name and color
57
- const finalName = username || deterministicUserData.name;
58
- const finalColor = cursorColor || deterministicUserData.color;
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
- // Update the awareness state immediately with the deterministic name
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
  }
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright (c) 2023-2025 Datalayer, Inc.
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
3
  * Distributed under the terms of the MIT License.
4
4
  */
5
5
  import { createCommand } from 'lexical';
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /*
3
- * Copyright (c) 2023-2025 Datalayer, Inc.
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} (Me)`, color: currentDisplayColor, isCurrentUser: true, clientId: currentClientID })), otherCollaborators.length > 0 && (_jsx("div", { style: {
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
- // Convert color to rgba for transparency when current user
60
- const getColorWithOpacity = (color, opacity = 1) => {
61
- // If it's a hex color, convert to rgba
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: isCurrentUser ? 0.9 : 1
100
- }, children: name })] }));
87
+ opacity: 1
88
+ }, children: `${name} ${shortClientId}` })] }));
101
89
  }
102
90
  export default LoroCollaborators;
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright (c) 2023-2025 Datalayer, Inc.
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
3
  * Distributed under the terms of the MIT License.
4
4
  */
5
5
  export { LoroCollaborators } from './LoroCollaborators';
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright (c) 2023-2025 Datalayer, Inc.
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
3
  * Distributed under the terms of the MIT License.
4
4
  */
5
5
  export * from './Bindings';
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright (c) 2023-2025 Datalayer, Inc.
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
3
  * Distributed under the terms of the MIT License.
4
4
  */
5
5
  export {};
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright (c) 2023-2025 Datalayer, Inc.
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
3
  * Distributed under the terms of the MIT License.
4
4
  */
5
5
  /**
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright (c) 2023-2025 Datalayer, Inc.
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('📋 Handling ListDiff:', diff);
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(`📋 Unknown list change type: ${change.type}`);
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(`📋 List retain with attributes at ${change.index}, length: ${change.length}`, change.attributes);
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) 2023-2025 Datalayer, Inc.
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(`🗺️ MapIntegrator: Node type '${targetType}' not registered`);
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(`🗺️ MapIntegrator: importJSON+replace failed for decorator ${targetType} node ${lexicalKey}:`, error);
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(`🗺️ MapIntegrator: updateFromJSON failed for ${targetType} node ${lexicalKey}:`, error);
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) 2023-2025 Datalayer, Inc.
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(`📝 Unknown text change type: ${change.type}`);
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.