@datalayer/lexical-loro 0.0.1 → 0.0.3
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 +328 -424
- package/lib/LoroCollaborativePlugin.d.ts +3 -1
- package/lib/LoroCollaborativePlugin.js +93 -11
- package/lib/index.d.ts +1 -0
- package/lib/index.js +5 -0
- package/package.json +34 -17
package/README.md
CHANGED
|
@@ -2,386 +2,248 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/sponsors/datalayer)
|
|
4
4
|
|
|
5
|
-
# Collaborative Plugin for Lexical
|
|
5
|
+
# ✍️ 🦜 Lexical Loro - Collaborative Plugin for Lexical with Loro CRDT
|
|
6
6
|
|
|
7
|
-
A
|
|
7
|
+
A collaborative editing plugin for [Lexical](https://github.com/facebook/lexical) Rich Editor built with [Loro](https://github.com/loro-dev) CRDT, providing real-time collaborative editing capabilities with conflict-free synchronization.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
## Core Components
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
**NEW** Now supports both Node.js and Python WebSocket servers!
|
|
14
|
-
|
|
15
|
-
<div align="center" style="text-align: center">
|
|
16
|
-
<img alt="" src="https://assets.datalayer.tech/lexical-loro.gif" />
|
|
17
|
-
</div>
|
|
18
|
-
|
|
19
|
-
## Features
|
|
20
|
-
|
|
21
|
-
- 🔄 **Real-time Collaboration**: Multiple users can edit the same document simultaneously
|
|
22
|
-
- 🚀 **Conflict-free**: Uses Loro CRDT to automatically resolve conflicts
|
|
23
|
-
- 📝 **Dual Editor Support**: Choose between simple text area or rich text Lexical editor
|
|
24
|
-
- 🌐 **Multi-server Support**: Choose between Node.js and Python WebSocket servers
|
|
25
|
-
- ⚡ **Fast Development**: Built with Vite for lightning-fast development
|
|
26
|
-
- 🎨 **Responsive Design**: Works on desktop and mobile devices
|
|
27
|
-
- 📡 **Connection Status**: Visual indicators for connection state
|
|
28
|
-
- ✨ **Rich Text Features**: Bold, italic, underline with real-time formatting sync
|
|
29
|
-
- 🔧 **Server Selection**: Switch between Node.js and Python backends
|
|
11
|
+
This package provides three main components for building collaborative text editors:
|
|
30
12
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
- **
|
|
34
|
-
- **CRDT Library**: Loro CRDT
|
|
35
|
-
- **Rich Text Editor**: Lexical (Facebook's extensible text editor)
|
|
36
|
-
- **Backend Options**:
|
|
37
|
-
- Node.js + TypeScript + ws library
|
|
38
|
-
- Python + loro-py + websockets library
|
|
39
|
-
- **Real-time Communication**: WebSockets (ws)
|
|
40
|
-
- **Styling**: CSS3 with responsive design
|
|
41
|
-
- **Development Tools**: ESLint, tsx, concurrently
|
|
42
|
-
|
|
43
|
-
## Getting Started
|
|
44
|
-
|
|
45
|
-
### Prerequisites
|
|
46
|
-
|
|
47
|
-
- Node.js (v16 or higher)
|
|
48
|
-
- npm or yarn
|
|
49
|
-
- Python 3.8+ (for Python server option)
|
|
50
|
-
- pip3 (for Python dependencies)
|
|
13
|
+
1. **`LoroCollaborativePlugin.tsx`** - A Lexical plugin that integrates Loro CRDT for real-time collaborative editing
|
|
14
|
+
2. **`LexicalModel` Python Library** - A standalone document model for Lexical content with CRDT capabilities
|
|
15
|
+
3. **`lexical-loro` WebSocket Server** - A Python server using [loro-py](https://github.com/loro-dev/loro-py) for real-time collaboration
|
|
51
16
|
|
|
52
|
-
|
|
17
|
+
## Quick Start
|
|
53
18
|
|
|
54
|
-
|
|
55
|
-
```bash
|
|
56
|
-
npm install
|
|
57
|
-
```
|
|
19
|
+
### Using the Lexical Plugin
|
|
58
20
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
pip3 install -r requirements.txt
|
|
62
|
-
# or run the setup script
|
|
63
|
-
./setup-python.sh
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
### Running the Application
|
|
67
|
-
|
|
68
|
-
#### Option 1: All Servers (Recommended)
|
|
69
|
-
```bash
|
|
70
|
-
npm run dev:all
|
|
71
|
-
```
|
|
72
|
-
This starts **both** WebSocket servers (Node.js on port 8080 and Python on port 8081) plus the React development server (port 5173). You can then switch between servers using the UI.
|
|
21
|
+
```tsx
|
|
22
|
+
import { LoroCollaborativePlugin } from './src/LoroCollaborativePlugin';
|
|
73
23
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
24
|
+
function MyEditor() {
|
|
25
|
+
return (
|
|
26
|
+
<LexicalComposer initialConfig={editorConfig}>
|
|
27
|
+
<RichTextPlugin />
|
|
28
|
+
<LoroCollaborativePlugin
|
|
29
|
+
websocketUrl="ws://localhost:8081"
|
|
30
|
+
docId="my-document"
|
|
31
|
+
username="user1"
|
|
32
|
+
/>
|
|
33
|
+
</LexicalComposer>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
77
36
|
```
|
|
78
|
-
This starts only the Python WebSocket server (port 8081) and React development server.
|
|
79
37
|
|
|
80
|
-
|
|
81
|
-
```bash
|
|
82
|
-
npm run dev:all:js
|
|
83
|
-
```
|
|
84
|
-
This starts only the Node.js WebSocket server (port 8080) and React development server.
|
|
38
|
+
### Using the LexicalModel Library
|
|
85
39
|
|
|
86
|
-
|
|
40
|
+
```python
|
|
41
|
+
from lexical_loro import LexicalModel
|
|
87
42
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
# Terminal 1: Start Node.js WebSocket server
|
|
91
|
-
npm run server
|
|
43
|
+
# Create a new document
|
|
44
|
+
model = LexicalModel.create_document("my-document")
|
|
92
45
|
|
|
93
|
-
#
|
|
94
|
-
|
|
46
|
+
# Add content
|
|
47
|
+
model.add_block({
|
|
48
|
+
"text": "My Document",
|
|
49
|
+
"format": 0,
|
|
50
|
+
"style": ""
|
|
51
|
+
}, "heading1")
|
|
95
52
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
53
|
+
model.add_block({
|
|
54
|
+
"text": "This is a paragraph.",
|
|
55
|
+
"format": 0,
|
|
56
|
+
"style": ""
|
|
57
|
+
}, "paragraph")
|
|
99
58
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
# Terminal 1: Start Node.js WebSocket server
|
|
103
|
-
npm run server
|
|
59
|
+
# Save to file
|
|
60
|
+
model.save_to_file("document.json")
|
|
104
61
|
|
|
105
|
-
#
|
|
106
|
-
|
|
62
|
+
# Load from file
|
|
63
|
+
loaded_model = LexicalModel.load_from_file("document.json")
|
|
107
64
|
```
|
|
108
65
|
|
|
109
|
-
|
|
110
|
-
```bash
|
|
111
|
-
# Terminal 1: Start Python WebSocket server
|
|
112
|
-
npm run server:py
|
|
113
|
-
# or directly: python3 server.py
|
|
66
|
+
### Using the Python Server
|
|
114
67
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
2. In another terminal, start the React development server:
|
|
120
|
-
```bash
|
|
121
|
-
npm run dev
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
### Usage
|
|
125
|
-
|
|
126
|
-
1. Open your browser and navigate to the development server URL (typically `http://localhost:5173`)
|
|
127
|
-
2. **Select Server Type**: Use the server selection radio buttons to choose:
|
|
128
|
-
- **Node.js Server**: `ws://localhost:8080` (TypeScript implementation)
|
|
129
|
-
- **Python Server**: `ws://localhost:8081` (Python + loro-py implementation)
|
|
130
|
-
|
|
131
|
-
💡 **Tip**: When using `npm run dev:all`, both servers are running simultaneously, so you can switch between them in real-time!
|
|
132
|
-
|
|
133
|
-
3. **Choose Editor Type**: Click the tabs to select:
|
|
134
|
-
- **Simple Text Editor**: A basic textarea for plain text collaboration
|
|
135
|
-
- **Rich Text Editor (Lexical)**: A full-featured rich text editor with Bold/Italic/Underline formatting
|
|
136
|
-
4. Start typing in either editor
|
|
137
|
-
5. Open another browser window/tab or share the URL with others
|
|
138
|
-
6. All users will see real-time updates as they type in the same editor type
|
|
139
|
-
7. Each editor maintains its own document state (they are separate collaborative spaces)
|
|
140
|
-
|
|
141
|
-
**Note**: You must disconnect from the current server before switching to a different server type.
|
|
142
|
-
|
|
143
|
-
### Testing Collaboration
|
|
144
|
-
|
|
145
|
-
To test the real-time collaboration:
|
|
146
|
-
|
|
147
|
-
1. Open multiple browser tabs/windows to the development server URL
|
|
148
|
-
2. **Select the same server** in all tabs (Node.js or Python)
|
|
149
|
-
3. **Test Simple Text Editor**:
|
|
150
|
-
- Keep all tabs on the "Simple Text Editor" tab
|
|
151
|
-
- Start typing in one window - you'll see the changes appear in other windows instantly
|
|
152
|
-
4. **Test Lexical Rich Text Editor**:
|
|
153
|
-
- Switch all tabs to the "Rich Text Editor (Lexical)" tab
|
|
154
|
-
- Try formatting text with the toolbar buttons (Bold, Italic, Underline)
|
|
155
|
-
- Changes and formatting will sync in real-time across all tabs
|
|
156
|
-
5. **Test Cross-Server Compatibility**:
|
|
157
|
-
- Verify that documents are properly synchronized between Node.js and Python servers
|
|
158
|
-
- Each server maintains its own document state
|
|
159
|
-
6. **Test Independent Documents**:
|
|
160
|
-
- Have some tabs on "Simple Text Editor" and others on "Lexical Editor"
|
|
161
|
-
- Notice that each editor type maintains its own separate document
|
|
162
|
-
5. **New collaborators will automatically receive the current document content** when they join
|
|
163
|
-
|
|
164
|
-
**Note**: The application now properly synchronizes initial content:
|
|
165
|
-
- When a new collaborator joins, they automatically receive the current document state for both editors
|
|
166
|
-
- If no snapshot is available on the server, existing clients will provide their current state
|
|
167
|
-
- The first client to join with content will automatically share their document state
|
|
168
|
-
- Each editor type (simple text vs Lexical) maintains separate collaborative documents
|
|
169
|
-
|
|
170
|
-
## Project Structure
|
|
68
|
+
```bash
|
|
69
|
+
# Install the Python package
|
|
70
|
+
pip install -e .
|
|
171
71
|
|
|
72
|
+
# Start the server
|
|
73
|
+
lexical-loro-server --port 8081
|
|
172
74
|
```
|
|
173
|
-
src/
|
|
174
|
-
├── App.tsx # Main application component with tabbed interface
|
|
175
|
-
├── App.css # Application styles
|
|
176
|
-
├── CollaborativeEditor.tsx # Simple text editor component with Loro CRDT integration
|
|
177
|
-
├── CollaborativeEditor.css # Simple editor styles
|
|
178
|
-
├── LexicalCollaborativeEditor.tsx # Lexical rich text editor component
|
|
179
|
-
├── LexicalCollaborativeEditor.css # Lexical editor styles
|
|
180
|
-
├── LoroCollaborativePlugin.tsx # Lexical plugin for Loro CRDT integration
|
|
181
|
-
├── main.tsx # React application entry point
|
|
182
|
-
└── vite-env.d.ts # Vite type definitions
|
|
183
|
-
|
|
184
|
-
server.ts # WebSocket server for real-time communication
|
|
185
|
-
package.json # Dependencies and scripts
|
|
186
|
-
```
|
|
187
|
-
|
|
188
|
-
## How It Works
|
|
189
75
|
|
|
190
|
-
|
|
76
|
+
## Examples
|
|
191
77
|
|
|
192
|
-
|
|
78
|
+
For complete working examples, see the `src/examples/` directory which contains:
|
|
79
|
+
- Full React application with dual editor support
|
|
80
|
+
- Server selection interface
|
|
81
|
+
- Connection status indicators
|
|
82
|
+
- Rich text formatting examples
|
|
193
83
|
|
|
194
|
-
|
|
195
|
-
- Simple Text Editor: `shared-text`
|
|
196
|
-
- Lexical Editor: `lexical-shared-doc`
|
|
197
|
-
2. **Local Changes**: When a user types, changes are applied to the corresponding local Loro document
|
|
198
|
-
3. **Change Detection**: The application detects insertions, deletions, and replacements
|
|
199
|
-
4. **Synchronization**: Changes are serialized and sent to other clients via WebSocket with document ID
|
|
200
|
-
5. **Conflict Resolution**: Loro CRDT automatically merges changes without conflicts
|
|
201
|
-
|
|
202
|
-
The Complete Flow Diagram
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
Remote User Types
|
|
206
|
-
↓
|
|
207
|
-
WebSocket Message
|
|
208
|
-
↓
|
|
209
|
-
loro-update received
|
|
210
|
-
↓
|
|
211
|
-
loroDocRef.current.import(update)
|
|
212
|
-
↓
|
|
213
|
-
doc.subscribe() callback fires
|
|
214
|
-
↓
|
|
215
|
-
updateLexicalFromLoro(editor, newText)
|
|
216
|
-
↓
|
|
217
|
-
editor.update() with new content
|
|
218
|
-
↓
|
|
219
|
-
Lexical State Updated
|
|
220
|
-
↓
|
|
221
|
-
UI Re-renders with New Content
|
|
222
|
-
|
|
223
|
-
Protection Against Infinite Loops
|
|
224
|
-
|
|
225
|
-
The system uses several mechanisms to prevent loops:
|
|
226
|
-
|
|
227
|
-
isLocalChange.current flag - Prevents local changes from triggering remote updates
|
|
228
|
-
{ tag: 'collaboration' } on editor.update() - Allows the update listener to ignore these changes
|
|
229
|
-
JSON comparison in updateLexicalFromLoro to avoid redundant updates
|
|
230
|
-
|
|
231
|
-
When a Loro update is received, the Lexical state is updated through:
|
|
232
|
-
|
|
233
|
-
WebSocket receives loro-update message
|
|
234
|
-
|
|
235
|
-
loroDocRef.current.import(update) applies the change to Loro
|
|
236
|
-
doc.subscribe() callback automatically fires
|
|
237
|
-
updateLexicalFromLoro() converts Loro text to Lexical state
|
|
238
|
-
editor.setEditorState() or DOM manipulation updates the editor
|
|
239
|
-
|
|
240
|
-
The bridge is the doc.subscribe() callback on line 1901 - this is what makes Lexical automatically reflect any Loro document changes!
|
|
241
|
-
|
|
242
|
-
### Lexical Integration
|
|
243
|
-
|
|
244
|
-
The Lexical editor integration includes:
|
|
245
|
-
|
|
246
|
-
1. **LoroCollaborativePlugin**: A custom Lexical plugin that bridges Lexical and Loro CRDT
|
|
247
|
-
2. **Bidirectional Sync**: Changes flow from Lexical → Loro → WebSocket and vice versa
|
|
248
|
-
3. **Rich Text Preservation**: The plugin maintains rich text formatting during collaborative editing
|
|
249
|
-
4. **Independent State**: Lexical editor maintains separate document state from simple text editor
|
|
250
|
-
|
|
251
|
-
### WebSocket Communication
|
|
252
|
-
|
|
253
|
-
The WebSocket server:
|
|
254
|
-
- Maintains connections to all clients
|
|
255
|
-
- Broadcasts Loro document updates to all connected clients with document ID filtering
|
|
256
|
-
- Handles client connections and disconnections
|
|
257
|
-
- Provides connection status feedback
|
|
258
|
-
- Stores separate snapshots for each document type
|
|
84
|
+
**DISCLAIMER** Collaborative Cursors still need fixes, see [this issue](https://github.com/datalayer/lexical-loro/issues/1).
|
|
259
85
|
|
|
260
|
-
|
|
86
|
+
<div align="center" style="text-align: center">
|
|
87
|
+
<img alt="" src="https://assets.datalayer.tech/lexical-loro.gif" />
|
|
88
|
+
</div>
|
|
261
89
|
|
|
262
|
-
|
|
263
|
-
2. Change is applied to local Loro document
|
|
264
|
-
3. Document update is serialized and sent via WebSocket
|
|
265
|
-
4. Other clients receive the update and apply it to their documents
|
|
266
|
-
5. UI is updated to reflect the changes
|
|
90
|
+
## Core Features
|
|
267
91
|
|
|
268
|
-
|
|
92
|
+
- 🔄 **Real-time Collaboration**: Multiple users can edit the same document simultaneously
|
|
93
|
+
- 🚀 **Conflict-free**: Uses Loro CRDT to automatically resolve conflicts
|
|
94
|
+
- 📝 **Lexical Integration**: Seamless integration with Lexical rich text editor
|
|
95
|
+
- 📚 **Standalone Library**: Use LexicalModel independently for document management
|
|
96
|
+
- 🌐 **WebSocket Server**: Python server for maintaining document state
|
|
97
|
+
- 📡 **Connection Management**: Robust WebSocket connection handling
|
|
98
|
+
- ✨ **Rich Text Support**: Preserves formatting during collaborative editing
|
|
99
|
+
- 💾 **Serialization**: JSON export/import and file persistence
|
|
100
|
+
- 🔧 **Extensible**: Plugin-based architecture for easy customization
|
|
269
101
|
|
|
270
|
-
|
|
102
|
+
## Technology Stack
|
|
271
103
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
6. **Ready to Collaborate**: New client can now participate in real-time editing
|
|
104
|
+
**Core Dependencies:**
|
|
105
|
+
- **Lexical**: v0.33.1 (Facebook's extensible text editor framework)
|
|
106
|
+
- **Loro CRDT**: v1.5.10 (Conflict-free replicated data types)
|
|
107
|
+
- **React**: 18/19 (for plugin hooks and components)
|
|
108
|
+
- **Python**: 3.8+ with loro-py and websockets
|
|
278
109
|
|
|
279
|
-
|
|
110
|
+
**Development Dependencies:**
|
|
111
|
+
- **TypeScript**: For type safety
|
|
112
|
+
- **Vite**: For building and development (examples only)
|
|
113
|
+
- **pytest**: Python testing
|
|
114
|
+
- **ESLint**: Code linting
|
|
280
115
|
|
|
281
|
-
##
|
|
116
|
+
## Installation
|
|
282
117
|
|
|
283
|
-
###
|
|
118
|
+
### Core Plugin
|
|
284
119
|
|
|
285
|
-
|
|
120
|
+
The Lexical plugin is a single TypeScript/React component that you can copy into your project:
|
|
286
121
|
|
|
287
|
-
```
|
|
288
|
-
|
|
122
|
+
```bash
|
|
123
|
+
# Copy the plugin file
|
|
124
|
+
cp src/LoroCollaborativePlugin.tsx your-project/src/
|
|
289
125
|
```
|
|
290
126
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
```typescript
|
|
296
|
-
const server = new LoroWebSocketServer(8080); // Change port here
|
|
127
|
+
**Dependencies required:**
|
|
128
|
+
```bash
|
|
129
|
+
npm install lexical @lexical/react @lexical/selection loro-crdt react react-dom
|
|
297
130
|
```
|
|
298
131
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
- `npm run dev` - Start React development server
|
|
302
|
-
- `npm run server` - Start WebSocket server
|
|
303
|
-
- `npm run dev:all` - Start both server and client
|
|
304
|
-
- `npm run build` - Build for production
|
|
305
|
-
- `npm run lint` - Run ESLint
|
|
306
|
-
- `npm run preview` - Preview production build
|
|
307
|
-
|
|
308
|
-
## Production Deployment
|
|
309
|
-
|
|
310
|
-
1. Build the application:
|
|
311
|
-
```bash
|
|
312
|
-
npm run build
|
|
313
|
-
```
|
|
132
|
+
### Python Server
|
|
314
133
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
3. Deploy the WebSocket server to your backend infrastructure
|
|
318
|
-
|
|
319
|
-
4. Update the WebSocket URL in the application to point to your production server
|
|
320
|
-
|
|
321
|
-
## Contributing
|
|
134
|
+
Install the Python WebSocket server:
|
|
322
135
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
4. Add tests if applicable
|
|
327
|
-
5. Submit a pull request
|
|
328
|
-
|
|
329
|
-
## License
|
|
330
|
-
|
|
331
|
-
This project is open source and available under the [MIT License](LICENSE).
|
|
332
|
-
|
|
333
|
-
## Acknowledgments
|
|
334
|
-
|
|
335
|
-
- [Loro CRDT](https://loro.dev/) - The CRDT library powering collaborative editing
|
|
336
|
-
- [Vite](https://vitejs.dev/) - Fast development build tool
|
|
337
|
-
- [React](https://reactjs.org/) - UI library
|
|
338
|
-
- [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API)
|
|
339
|
-
|
|
340
|
-
# Lexical Loro Python Package
|
|
341
|
-
|
|
342
|
-
A Python package for Lexical + Loro CRDT integration, providing a WebSocket server for real-time collaborative text editing.
|
|
343
|
-
|
|
344
|
-
## Features
|
|
345
|
-
|
|
346
|
-
- **Real-time collaboration**: WebSocket server for live document collaboration
|
|
347
|
-
- **Loro CRDT integration**: Uses Loro CRDT for conflict-free replicated data types
|
|
348
|
-
- **Lexical compatibility**: Designed to work with Lexical rich text editor
|
|
349
|
-
- **Ephemeral data support**: Handles cursor positions and selections
|
|
350
|
-
- **Multiple document support**: Manages multiple collaborative documents
|
|
136
|
+
```bash
|
|
137
|
+
# Install from this repository
|
|
138
|
+
pip install -e .
|
|
351
139
|
|
|
352
|
-
|
|
140
|
+
# Or install specific dependencies
|
|
141
|
+
pip install websockets click loro
|
|
142
|
+
```
|
|
353
143
|
|
|
354
|
-
|
|
144
|
+
## Usage
|
|
355
145
|
|
|
356
|
-
|
|
357
|
-
|
|
146
|
+
### 1. Lexical Plugin Integration
|
|
147
|
+
|
|
148
|
+
Add the plugin to your Lexical editor:
|
|
149
|
+
|
|
150
|
+
```tsx
|
|
151
|
+
import { LexicalComposer } from '@lexical/react/LexicalComposer';
|
|
152
|
+
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
|
|
153
|
+
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
|
|
154
|
+
import { LoroCollaborativePlugin } from './LoroCollaborativePlugin';
|
|
155
|
+
|
|
156
|
+
const editorConfig = {
|
|
157
|
+
namespace: 'MyEditor',
|
|
158
|
+
theme: {},
|
|
159
|
+
onError: console.error,
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
function CollaborativeEditor() {
|
|
163
|
+
return (
|
|
164
|
+
<LexicalComposer initialConfig={editorConfig}>
|
|
165
|
+
<div className="editor-container">
|
|
166
|
+
<RichTextPlugin
|
|
167
|
+
contentEditable={<ContentEditable className="editor-input" />}
|
|
168
|
+
placeholder={<div className="editor-placeholder">Start typing...</div>}
|
|
169
|
+
ErrorBoundary={() => <div>Error occurred</div>}
|
|
170
|
+
/>
|
|
171
|
+
<LoroCollaborativePlugin
|
|
172
|
+
websocketUrl="ws://localhost:8081"
|
|
173
|
+
docId="shared-document"
|
|
174
|
+
username="user123"
|
|
175
|
+
/>
|
|
176
|
+
</div>
|
|
177
|
+
</LexicalComposer>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
358
180
|
```
|
|
359
181
|
|
|
360
|
-
###
|
|
182
|
+
### 2. Standalone LexicalModel Library
|
|
361
183
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
184
|
+
Use the LexicalModel library independently for document management:
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
from lexical_loro import LexicalModel
|
|
188
|
+
|
|
189
|
+
# Create a new document
|
|
190
|
+
model = LexicalModel.create_document("my-document")
|
|
191
|
+
|
|
192
|
+
# Add different types of content
|
|
193
|
+
model.add_block({
|
|
194
|
+
"text": "My Document",
|
|
195
|
+
"format": 0,
|
|
196
|
+
"style": ""
|
|
197
|
+
}, "heading1")
|
|
198
|
+
|
|
199
|
+
model.add_block({
|
|
200
|
+
"text": "This is a paragraph with **bold** text.",
|
|
201
|
+
"format": 0,
|
|
202
|
+
"style": ""
|
|
203
|
+
}, "paragraph")
|
|
204
|
+
|
|
205
|
+
model.add_block({
|
|
206
|
+
"text": "",
|
|
207
|
+
"format": 0,
|
|
208
|
+
"style": ""
|
|
209
|
+
}, "list")
|
|
210
|
+
|
|
211
|
+
# Serialize to JSON
|
|
212
|
+
json_data = model.to_json()
|
|
213
|
+
|
|
214
|
+
# Save to file
|
|
215
|
+
model.save_to_file("document.json")
|
|
216
|
+
|
|
217
|
+
# Load from file
|
|
218
|
+
loaded_model = LexicalModel.load_from_file("document.json")
|
|
219
|
+
|
|
220
|
+
# Access blocks
|
|
221
|
+
for block in loaded_model.get_blocks():
|
|
222
|
+
print(f"{block['type']}: {block.get('text', '')}")
|
|
365
223
|
```
|
|
366
224
|
|
|
367
|
-
|
|
225
|
+
For more examples, see:
|
|
226
|
+
- `examples/memory_only_example.py` - Basic document creation and manipulation
|
|
227
|
+
- `examples/file_sync_example.py` - File persistence and batch operations
|
|
228
|
+
- `examples/collaboration_example.py` - Simulating collaborative editing
|
|
229
|
+
- `docs/LEXICAL_MODEL_GUIDE.md` - Comprehensive documentation
|
|
368
230
|
|
|
369
|
-
###
|
|
231
|
+
### 3. Python Server Setup
|
|
370
232
|
|
|
371
|
-
Start the server
|
|
233
|
+
Start the WebSocket server:
|
|
372
234
|
|
|
373
235
|
```bash
|
|
374
|
-
#
|
|
236
|
+
# Default port (8081)
|
|
375
237
|
lexical-loro-server
|
|
376
238
|
|
|
377
|
-
#
|
|
239
|
+
# Custom port
|
|
378
240
|
lexical-loro-server --port 8082
|
|
379
241
|
|
|
380
|
-
#
|
|
381
|
-
lexical-loro-server --log-level DEBUG
|
|
242
|
+
# With debug logging
|
|
243
|
+
lexical-loro-server --port 8081 --log-level DEBUG
|
|
382
244
|
```
|
|
383
245
|
|
|
384
|
-
### Programmatic Usage
|
|
246
|
+
### 4. Programmatic Server Usage
|
|
385
247
|
|
|
386
248
|
```python
|
|
387
249
|
import asyncio
|
|
@@ -390,162 +252,204 @@ from lexical_loro import LoroWebSocketServer
|
|
|
390
252
|
async def main():
|
|
391
253
|
server = LoroWebSocketServer(port=8081)
|
|
392
254
|
await server.start()
|
|
255
|
+
print("Server running on ws://localhost:8081")
|
|
393
256
|
|
|
394
257
|
if __name__ == "__main__":
|
|
395
258
|
asyncio.run(main())
|
|
396
259
|
```
|
|
397
260
|
|
|
398
|
-
|
|
261
|
+
## Plugin API
|
|
399
262
|
|
|
400
|
-
|
|
263
|
+
For detailed API documentation, see [`docs/API.md`](docs/API.md).
|
|
401
264
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
265
|
+
### Quick Reference
|
|
266
|
+
|
|
267
|
+
```tsx
|
|
268
|
+
interface LoroCollaborativePluginProps {
|
|
269
|
+
websocketUrl: string; // WebSocket server URL
|
|
270
|
+
docId: string; // Unique document identifier
|
|
271
|
+
username: string; // User identifier
|
|
272
|
+
userColor?: string; // User cursor color (optional)
|
|
273
|
+
debug?: boolean; // Enable debug logging (optional)
|
|
408
274
|
}
|
|
409
275
|
```
|
|
410
276
|
|
|
411
|
-
##
|
|
277
|
+
## Initialization Best Practices
|
|
412
278
|
|
|
413
|
-
|
|
279
|
+
⚠️ **Important**: Always wait for collaboration initialization before enabling other plugins.
|
|
414
280
|
|
|
415
|
-
|
|
281
|
+
See [`docs/INITIALIZATION_GUIDE.md`](docs/INITIALIZATION_GUIDE.md) for comprehensive guidance on:
|
|
282
|
+
- Proper plugin ordering
|
|
283
|
+
- Initialization callbacks
|
|
284
|
+
- Error handling
|
|
285
|
+
- Common anti-patterns to avoid
|
|
416
286
|
|
|
417
|
-
|
|
287
|
+
## Examples
|
|
418
288
|
|
|
419
|
-
|
|
420
|
-
|
|
289
|
+
For complete working examples and demonstrations, see the `src/examples/` directory:
|
|
290
|
+
|
|
291
|
+
```bash
|
|
292
|
+
# Run the example application
|
|
293
|
+
npm install
|
|
294
|
+
npm run example
|
|
295
|
+
|
|
296
|
+
# This starts both Node.js and Python servers plus a React demo app
|
|
297
|
+
# Open http://localhost:5173 to see dual editor interface
|
|
421
298
|
```
|
|
422
299
|
|
|
423
|
-
|
|
300
|
+
The examples include:
|
|
301
|
+
- **Complete React App**: Full collaborative editor with UI
|
|
302
|
+
- **Server Selection**: Switch between Node.js and Python backends
|
|
303
|
+
- **Dual Editors**: Simple text area and rich Lexical editor
|
|
304
|
+
- **Real-time Demo**: Multi-user collaboration testing
|
|
424
305
|
|
|
425
|
-
|
|
426
|
-
- `shutdown()`: Gracefully shutdown the server
|
|
427
|
-
- `handle_client(websocket)`: Handle new client connections
|
|
428
|
-
- `handle_message(client_id, message)`: Process messages from clients
|
|
306
|
+
See `src/examples/README.md` for detailed example documentation.
|
|
429
307
|
|
|
430
|
-
|
|
308
|
+
## Project Structure
|
|
431
309
|
|
|
432
|
-
|
|
310
|
+
### Core Components
|
|
433
311
|
|
|
434
|
-
```
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
312
|
+
```
|
|
313
|
+
src/
|
|
314
|
+
├── LoroCollaborativePlugin.tsx # Main Lexical plugin for collaboration
|
|
315
|
+
└── vite-env.d.ts # TypeScript definitions
|
|
316
|
+
|
|
317
|
+
lexical_loro/ # Python WebSocket server package
|
|
318
|
+
├── __init__.py # Package exports
|
|
319
|
+
├── server.py # WebSocket server implementation
|
|
320
|
+
├── cli.py # Command line interface
|
|
321
|
+
├── model/
|
|
322
|
+
│ └── lexical_model.py # Standalone LexicalModel library
|
|
323
|
+
└── tests/ # Python test suite
|
|
324
|
+
|
|
325
|
+
docs/
|
|
326
|
+
└── LEXICAL_MODEL_GUIDE.md # Comprehensive library documentation
|
|
327
|
+
|
|
328
|
+
examples/
|
|
329
|
+
├── memory_only_example.py # Basic LexicalModel usage
|
|
330
|
+
├── file_sync_example.py # File persistence example
|
|
331
|
+
├── collaboration_example.py # Collaborative editing simulation
|
|
332
|
+
└── README.md # Examples documentation
|
|
333
|
+
|
|
334
|
+
pyproject.toml # Python package configuration
|
|
440
335
|
```
|
|
441
336
|
|
|
442
|
-
|
|
337
|
+
### Examples Directory
|
|
443
338
|
|
|
444
|
-
|
|
339
|
+
```
|
|
340
|
+
src/examples/ # Complete demo application
|
|
341
|
+
├── App.tsx # Demo app with dual editors
|
|
342
|
+
├── LexicalCollaborativeEditor.tsx # Rich text editor example
|
|
343
|
+
├── TextAreaCollaborativeEditor.tsx # Simple text editor example
|
|
344
|
+
├── ServerSelector.tsx # Server selection UI
|
|
345
|
+
├── LexicalToolbar.tsx # Rich text toolbar
|
|
346
|
+
├── main.tsx # Demo app entry point
|
|
347
|
+
└── *.css # Styling for examples
|
|
348
|
+
|
|
349
|
+
servers/
|
|
350
|
+
└── server.ts # Node.js server (for comparison)
|
|
351
|
+
```
|
|
445
352
|
|
|
446
|
-
|
|
447
|
-
# Install development dependencies
|
|
448
|
-
pip install -e "python_src/[dev]"
|
|
353
|
+
### Archive
|
|
449
354
|
|
|
450
|
-
|
|
451
|
-
|
|
355
|
+
```
|
|
356
|
+
src/archive/ # Historical plugin implementations
|
|
357
|
+
├── LoroCollaborativePlugin0.tsx # Previous versions for reference
|
|
358
|
+
├── LoroCollaborativePlugin1.tsx
|
|
359
|
+
├── LoroCollaborativePlugin2.tsx
|
|
360
|
+
├── LoroCollaborativePlugin3.tsx
|
|
361
|
+
├── LoroCollaborativePlugin4.tsx
|
|
362
|
+
└── LoroCollaborativePlugin5.tsx
|
|
363
|
+
```
|
|
452
364
|
|
|
453
|
-
|
|
454
|
-
pytest --cov=lexical_loro --cov-report=html
|
|
365
|
+
## Architecture
|
|
455
366
|
|
|
456
|
-
|
|
457
|
-
black python_src/
|
|
367
|
+
For detailed architecture documentation, see [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md).
|
|
458
368
|
|
|
459
|
-
|
|
460
|
-
ruff python_src/
|
|
369
|
+
### System Overview
|
|
461
370
|
|
|
462
|
-
|
|
463
|
-
mypy python_src/
|
|
464
|
-
```
|
|
371
|
+
The collaboration system consists of three main components:
|
|
465
372
|
|
|
466
|
-
|
|
373
|
+
1. **LoroCollaborativePlugin** (Client-side) - Lexical integration
|
|
374
|
+
2. **LoroWebSocketServer** (Server-side) - Real-time synchronization
|
|
375
|
+
3. **LexicalModel** (Standalone Library) - Independent document model
|
|
467
376
|
|
|
468
|
-
|
|
377
|
+
### Data Flow
|
|
469
378
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
379
|
+
```
|
|
380
|
+
User Types → Lexical Editor → Plugin → Loro CRDT → WebSocket
|
|
381
|
+
↓
|
|
382
|
+
WebSocket ← Loro CRDT ← Plugin ← Lexical Editor ← Other Users
|
|
383
|
+
```
|
|
475
384
|
|
|
476
|
-
|
|
385
|
+
## Configuration
|
|
477
386
|
|
|
478
|
-
|
|
479
|
-
pytest tests/ -v
|
|
480
|
-
```
|
|
387
|
+
For detailed configuration options, see [`docs/API.md`](docs/API.md).
|
|
481
388
|
|
|
482
|
-
###
|
|
389
|
+
### Quick Configuration
|
|
483
390
|
|
|
484
|
-
|
|
391
|
+
```tsx
|
|
392
|
+
// Plugin configuration
|
|
393
|
+
<LoroCollaborativePlugin
|
|
394
|
+
websocketUrl="ws://localhost:8081"
|
|
395
|
+
docId="my-document"
|
|
396
|
+
username="user123"
|
|
397
|
+
debug={true}
|
|
398
|
+
/>
|
|
399
|
+
```
|
|
485
400
|
|
|
486
401
|
```bash
|
|
487
|
-
|
|
488
|
-
|
|
402
|
+
# Server configuration
|
|
403
|
+
lexical-loro-server --port 8081 --log-level DEBUG
|
|
489
404
|
```
|
|
490
405
|
|
|
491
|
-
##
|
|
406
|
+
## Development
|
|
492
407
|
|
|
493
|
-
|
|
408
|
+
For comprehensive development guidelines, see [`docs/DEVELOPMENT.md`](docs/DEVELOPMENT.md).
|
|
494
409
|
|
|
495
|
-
###
|
|
410
|
+
### Quick Start
|
|
496
411
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
- `awareness-update`: User presence information
|
|
412
|
+
```bash
|
|
413
|
+
# Install dependencies
|
|
414
|
+
npm install
|
|
415
|
+
pip install -e ".[dev]"
|
|
502
416
|
|
|
503
|
-
|
|
417
|
+
# Run tests
|
|
418
|
+
npm test
|
|
419
|
+
npm run test:py
|
|
504
420
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
"type": "loro-update",
|
|
508
|
-
"docId": "lexical-shared-doc",
|
|
509
|
-
"updateHex": "deadbeef..."
|
|
510
|
-
}
|
|
421
|
+
# Start development server
|
|
422
|
+
lexical-loro-server --log-level DEBUG
|
|
511
423
|
```
|
|
512
424
|
|
|
513
|
-
##
|
|
425
|
+
## Contributing
|
|
514
426
|
|
|
515
|
-
|
|
427
|
+
We welcome contributions! Please see [`docs/DEVELOPMENT.md`](docs/DEVELOPMENT.md) for detailed guidelines.
|
|
516
428
|
|
|
517
|
-
|
|
518
|
-
- `LEXICAL_LORO_HOST`: Host to bind to (default: localhost)
|
|
519
|
-
- `LEXICAL_LORO_LOG_LEVEL`: Logging level (default: INFO)
|
|
429
|
+
### Quick Contributing Guide
|
|
520
430
|
|
|
521
|
-
|
|
431
|
+
1. Fork the repository
|
|
432
|
+
2. Create a feature branch
|
|
433
|
+
3. Focus changes on core components
|
|
434
|
+
4. Add tests for new functionality
|
|
435
|
+
5. Update documentation as needed
|
|
436
|
+
6. Submit a pull request
|
|
522
437
|
|
|
523
|
-
|
|
438
|
+
## Documentation
|
|
524
439
|
|
|
525
|
-
-
|
|
526
|
-
-
|
|
527
|
-
-
|
|
528
|
-
-
|
|
529
|
-
-
|
|
530
|
-
- `lexical-shared-doc-v4`: Smart hybrid plugin document
|
|
440
|
+
- **[API Documentation](docs/API.md)** - Complete API reference
|
|
441
|
+
- **[Initialization Guide](docs/INITIALIZATION_GUIDE.md)** - Best practices for setup
|
|
442
|
+
- **[Architecture](docs/ARCHITECTURE.md)** - System design and data flow
|
|
443
|
+
- **[Development Guide](docs/DEVELOPMENT.md)** - Contributing and development setup
|
|
444
|
+
- **[LexicalModel Guide](docs/LEXICAL_MODEL_GUIDE.md)** - Standalone library documentation
|
|
531
445
|
|
|
532
446
|
## License
|
|
533
447
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
## Contributing
|
|
537
|
-
|
|
538
|
-
1. Fork the repository
|
|
539
|
-
2. Create a feature branch
|
|
540
|
-
3. Make your changes
|
|
541
|
-
4. Add tests
|
|
542
|
-
5. Run the test suite
|
|
543
|
-
6. Submit a pull request
|
|
544
|
-
|
|
545
|
-
## Support
|
|
546
|
-
|
|
547
|
-
For issues and questions:
|
|
448
|
+
This project is open source and available under the [MIT License](LICENSE).
|
|
548
449
|
|
|
549
|
-
|
|
550
|
-
- Documentation: https://github.com/datalayer/lexical-loro#readme
|
|
450
|
+
## Acknowledgments
|
|
551
451
|
|
|
452
|
+
- [Loro CRDT](https://loro.dev/) - The CRDT library powering collaborative editing
|
|
453
|
+
- [Lexical](https://lexical.dev/) - Facebook's extensible text editor framework
|
|
454
|
+
- [React](https://reactjs.org/) - UI library for plugin hooks
|
|
455
|
+
- [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) - Real-time communication
|
|
@@ -9,6 +9,8 @@ interface LoroCollaborativePluginProps {
|
|
|
9
9
|
userName: string;
|
|
10
10
|
isCurrentUser?: boolean;
|
|
11
11
|
}>) => void;
|
|
12
|
+
onInitialization?: (success: boolean) => void;
|
|
13
|
+
onSendMessageReady?: (sendMessageFn: (message: any) => void) => void;
|
|
12
14
|
}
|
|
13
|
-
export declare function LoroCollaborativePlugin({ websocketUrl, docId, onConnectionChange, onDisconnectReady, onPeerIdChange, onAwarenessChange }: LoroCollaborativePluginProps): import("react/jsx-runtime").JSX.Element;
|
|
15
|
+
export declare function LoroCollaborativePlugin({ websocketUrl, docId, onConnectionChange, onDisconnectReady, onPeerIdChange, onAwarenessChange, onInitialization, onSendMessageReady }: LoroCollaborativePluginProps): import("react/jsx-runtime").JSX.Element;
|
|
14
16
|
export default LoroCollaborativePlugin;
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/*
|
|
3
|
+
* Copyright (c) 2023-2025 Datalayer, Inc.
|
|
4
|
+
* Distributed under the terms of the MIT License.
|
|
5
|
+
*/
|
|
2
6
|
import { useEffect, useRef, useCallback, useState } from 'react';
|
|
3
7
|
import { createPortal } from 'react-dom';
|
|
4
8
|
import { $createParagraphNode, $getRoot, $getSelection, $isRangeSelection, $getNodeByKey, $isTextNode, $isElementNode, $isLineBreakNode, $createTextNode, createState, $getState, $setState } from 'lexical';
|
|
@@ -623,7 +627,7 @@ class CursorAwareness {
|
|
|
623
627
|
return this.ephemeralStore.getAllStates();
|
|
624
628
|
}
|
|
625
629
|
}
|
|
626
|
-
export function LoroCollaborativePlugin({ websocketUrl, docId, onConnectionChange, onDisconnectReady, onPeerIdChange, onAwarenessChange }) {
|
|
630
|
+
export function LoroCollaborativePlugin({ websocketUrl, docId, onConnectionChange, onDisconnectReady, onPeerIdChange, onAwarenessChange, onInitialization, onSendMessageReady }) {
|
|
627
631
|
const [editor] = useLexicalComposerContext();
|
|
628
632
|
const wsRef = useRef(null);
|
|
629
633
|
const loroDocRef = useRef(new LoroDoc());
|
|
@@ -894,13 +898,29 @@ export function LoroCollaborativePlugin({ websocketUrl, docId, onConnectionChang
|
|
|
894
898
|
console.log('🎯 Set awareness with stable cursor data:', { userWithCursorData, clientId });
|
|
895
899
|
// Send ephemeral update to other clients via WebSocket
|
|
896
900
|
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN && awarenessRef.current) {
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
901
|
+
try {
|
|
902
|
+
const ephemeralData = awarenessRef.current.encode();
|
|
903
|
+
// Validate ephemeral data before sending
|
|
904
|
+
if (!ephemeralData || ephemeralData.length === 0) {
|
|
905
|
+
console.warn('⚠️ Empty ephemeral data, skipping send');
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
const hexData = Array.from(ephemeralData).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
909
|
+
// Validate hex data
|
|
910
|
+
if (!hexData || hexData.length === 0) {
|
|
911
|
+
console.warn('⚠️ Empty hex data, skipping send');
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
wsRef.current.send(JSON.stringify({
|
|
915
|
+
type: 'ephemeral-update',
|
|
916
|
+
docId: docId,
|
|
917
|
+
data: hexData // Convert to hex string
|
|
918
|
+
}));
|
|
919
|
+
console.log('📤 Sent ephemeral update:', { docId, dataLength: hexData.length });
|
|
920
|
+
}
|
|
921
|
+
catch (error) {
|
|
922
|
+
console.error('❌ Error encoding/sending ephemeral data:', error);
|
|
923
|
+
}
|
|
904
924
|
}
|
|
905
925
|
}
|
|
906
926
|
catch (error) {
|
|
@@ -1591,10 +1611,12 @@ export function LoroCollaborativePlugin({ websocketUrl, docId, onConnectionChang
|
|
|
1591
1611
|
// WebSocket connection management with stable dependencies
|
|
1592
1612
|
const stableOnConnectionChange = useRef(onConnectionChange);
|
|
1593
1613
|
const stableOnDisconnectReady = useRef(onDisconnectReady);
|
|
1614
|
+
const stableOnSendMessageReady = useRef(onSendMessageReady);
|
|
1594
1615
|
// Update refs when props change without triggering effect
|
|
1595
1616
|
useEffect(() => {
|
|
1596
1617
|
stableOnConnectionChange.current = onConnectionChange;
|
|
1597
1618
|
stableOnDisconnectReady.current = onDisconnectReady;
|
|
1619
|
+
stableOnSendMessageReady.current = onSendMessageReady;
|
|
1598
1620
|
});
|
|
1599
1621
|
useEffect(() => {
|
|
1600
1622
|
// Close any existing connection before creating a new one
|
|
@@ -1660,6 +1682,13 @@ export function LoroCollaborativePlugin({ websocketUrl, docId, onConnectionChang
|
|
|
1660
1682
|
}
|
|
1661
1683
|
};
|
|
1662
1684
|
stableOnDisconnectReady.current?.(disconnectFn);
|
|
1685
|
+
// Provide sendMessage function to parent component
|
|
1686
|
+
const sendMessageFn = (message) => {
|
|
1687
|
+
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
|
|
1688
|
+
wsRef.current.send(JSON.stringify(message));
|
|
1689
|
+
}
|
|
1690
|
+
};
|
|
1691
|
+
stableOnSendMessageReady.current?.(sendMessageFn);
|
|
1663
1692
|
};
|
|
1664
1693
|
ws.onmessage = (event) => {
|
|
1665
1694
|
try {
|
|
@@ -1686,13 +1715,38 @@ export function LoroCollaborativePlugin({ websocketUrl, docId, onConnectionChang
|
|
|
1686
1715
|
loroDocRef.current.import(snapshot);
|
|
1687
1716
|
hasReceivedInitialSnapshot.current = true;
|
|
1688
1717
|
console.log('📄 Lexical editor received and applied initial snapshot');
|
|
1689
|
-
//
|
|
1718
|
+
// Notify parent component about successful initialization
|
|
1719
|
+
if (onInitialization) {
|
|
1720
|
+
onInitialization(true);
|
|
1721
|
+
}
|
|
1722
|
+
// Immediately reflect the current Loro content into the editor after import
|
|
1690
1723
|
try {
|
|
1691
|
-
|
|
1692
|
-
|
|
1724
|
+
// For lexical-shared-doc, we need to get the structured JSON from the content container
|
|
1725
|
+
let currentContent = '';
|
|
1726
|
+
try {
|
|
1727
|
+
// Try to get from 'content' container first (structured JSON)
|
|
1728
|
+
currentContent = loroDocRef.current.getText('content').toString();
|
|
1729
|
+
console.log('📋 Got structured content from "content" container:', currentContent.slice(0, 100) + '...');
|
|
1730
|
+
}
|
|
1731
|
+
catch {
|
|
1732
|
+
// Fallback to docId container if content doesn't exist
|
|
1733
|
+
currentContent = loroDocRef.current.getText(docId).toString();
|
|
1734
|
+
console.log('📋 Fallback to docId container:', currentContent.slice(0, 100) + '...');
|
|
1735
|
+
}
|
|
1736
|
+
if (currentContent && currentContent.trim().length > 0) {
|
|
1737
|
+
updateLexicalFromLoro(editor, currentContent);
|
|
1738
|
+
console.log('✅ Successfully updated Lexical editor from snapshot');
|
|
1739
|
+
}
|
|
1740
|
+
else {
|
|
1741
|
+
console.warn('⚠️ Empty content received from snapshot');
|
|
1742
|
+
}
|
|
1693
1743
|
}
|
|
1694
1744
|
catch (e) {
|
|
1695
1745
|
console.warn('⚠️ Could not immediately reflect snapshot to editor:', e);
|
|
1746
|
+
// Notify parent component about failed initialization
|
|
1747
|
+
if (onInitialization) {
|
|
1748
|
+
onInitialization(false);
|
|
1749
|
+
}
|
|
1696
1750
|
}
|
|
1697
1751
|
}
|
|
1698
1752
|
else if (data.type === 'ephemeral-update' || data.type === 'ephemeral-event') {
|
|
@@ -1815,6 +1869,30 @@ export function LoroCollaborativePlugin({ websocketUrl, docId, onConnectionChang
|
|
|
1815
1869
|
console.warn('🧹 Cannot cleanup - missing client ID or awareness ref');
|
|
1816
1870
|
}
|
|
1817
1871
|
}
|
|
1872
|
+
else if (data.type === 'paragraph-added') {
|
|
1873
|
+
// Handle server broadcast when a new paragraph was added
|
|
1874
|
+
console.log('➕ Received paragraph-added broadcast:', {
|
|
1875
|
+
docId: data.docId,
|
|
1876
|
+
message: data.message,
|
|
1877
|
+
addedBy: data.addedBy
|
|
1878
|
+
});
|
|
1879
|
+
// Trigger a sync from Loro to Lexical to reflect the new paragraph
|
|
1880
|
+
if (data.docId === docId) {
|
|
1881
|
+
try {
|
|
1882
|
+
// Request fresh snapshot to get the updated content
|
|
1883
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
1884
|
+
ws.send(JSON.stringify({
|
|
1885
|
+
type: 'request-snapshot',
|
|
1886
|
+
docId: docId
|
|
1887
|
+
}));
|
|
1888
|
+
console.log('📞 Requested fresh snapshot after paragraph addition');
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
catch (error) {
|
|
1892
|
+
console.warn('Error handling paragraph-added message:', error);
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1818
1896
|
}
|
|
1819
1897
|
catch (err) {
|
|
1820
1898
|
console.error('Error processing WebSocket message in Lexical plugin:', err);
|
|
@@ -1842,6 +1920,10 @@ export function LoroCollaborativePlugin({ websocketUrl, docId, onConnectionChang
|
|
|
1842
1920
|
ws.onerror = (err) => {
|
|
1843
1921
|
isConnectingRef.current = false;
|
|
1844
1922
|
console.error('WebSocket error in Lexical plugin:', err);
|
|
1923
|
+
// Notify initialization failure if we haven't received initial content yet
|
|
1924
|
+
if (!hasReceivedInitialSnapshot.current && onInitialization) {
|
|
1925
|
+
onInitialization(false);
|
|
1926
|
+
}
|
|
1845
1927
|
};
|
|
1846
1928
|
}
|
|
1847
1929
|
catch (err) {
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./LoroCollaborativePlugin";
|
package/lib/index.js
ADDED
package/package.json
CHANGED
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@datalayer/lexical-loro",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.3",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"description": "Collaborative editing plugin for Lexical based on Loro CRDT",
|
|
7
|
+
"main": "lib/index.js",
|
|
8
|
+
"types": "lib/index.d.ts",
|
|
6
9
|
"files": [
|
|
7
|
-
"lib
|
|
10
|
+
"lib/**/*.*"
|
|
11
|
+
],
|
|
12
|
+
"keywords": [
|
|
13
|
+
"lexical",
|
|
14
|
+
"loro",
|
|
15
|
+
"crdt",
|
|
16
|
+
"collaborative",
|
|
17
|
+
"editor",
|
|
18
|
+
"plugin",
|
|
19
|
+
"react"
|
|
8
20
|
],
|
|
9
21
|
"repository": {
|
|
10
22
|
"type": "git",
|
|
@@ -18,33 +30,38 @@
|
|
|
18
30
|
"scripts": {
|
|
19
31
|
"build": "tsc -b && vite build",
|
|
20
32
|
"clean": "rimraf dist lib",
|
|
33
|
+
"dev": "npm run example",
|
|
34
|
+
"dev:all:js": "npm run example:js",
|
|
35
|
+
"dev:all:py": "npm run example:py",
|
|
36
|
+
"dev:vite": "npm run example:vite",
|
|
37
|
+
"example": "concurrently \"npm run server\" \"npm run server:py\" \"npm run example:vite\"",
|
|
38
|
+
"example:js": "concurrently \"npm run server\" \"npm run example:vite\"",
|
|
39
|
+
"example:py": "concurrently \"npm run server:py:dev\" \"npm run server:py:minimal\" \"npm run example:vite\"",
|
|
40
|
+
"example:vite": "vite",
|
|
21
41
|
"lint": "eslint .",
|
|
22
42
|
"preview": "vite preview",
|
|
23
|
-
"server": "tsx
|
|
43
|
+
"server": "tsx node-js/server.ts",
|
|
24
44
|
"server:py": "lexical-loro-server",
|
|
25
45
|
"server:py:dev": "python3 -m lexical_loro.cli",
|
|
26
|
-
"
|
|
27
|
-
"test:py:watch": "python3 -m pytest lexical_loro/tests/ -v --tb=short -f",
|
|
28
|
-
"test:py:coverage": "python3 -m pytest lexical_loro/tests/ --cov=. --cov-report=html --cov-report=term",
|
|
46
|
+
"server:py:minimal": "python3 -m lexical_loro.cli_minimal",
|
|
29
47
|
"test": "vitest",
|
|
30
48
|
"test:js": "vitest run",
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"dev:all:js": "concurrently \"npm run server\" \"npm run dev:vite\""
|
|
49
|
+
"test:py": "python3 -m pytest lexical_loro/tests/ -v",
|
|
50
|
+
"test:py:coverage": "python3 -m pytest lexical_loro/tests/ --cov=. --cov-report=html --cov-report=term",
|
|
51
|
+
"test:py:watch": "python3 -m pytest lexical_loro/tests/ -v --tb=short -f"
|
|
35
52
|
},
|
|
36
53
|
"dependencies": {
|
|
37
|
-
"@lexical/react": "^0.33.1",
|
|
38
|
-
"@lexical/selection": "^0.33.1",
|
|
39
|
-
"lexical": "^0.33.1",
|
|
40
54
|
"loro-crdt": "^1.5.10",
|
|
41
|
-
"react": "^18
|
|
42
|
-
"react-dom": "^18
|
|
55
|
+
"react": "^18.3.1",
|
|
56
|
+
"react-dom": "^18.3.1",
|
|
57
|
+
"lexical": "^0.33.1",
|
|
58
|
+
"@lexical/react": "^0.33.1",
|
|
59
|
+
"@lexical/selection": "^0.33.1"
|
|
43
60
|
},
|
|
44
61
|
"devDependencies": {
|
|
45
62
|
"@eslint/js": "^9.30.1",
|
|
46
|
-
"@types/react": "
|
|
47
|
-
"@types/react-dom": "
|
|
63
|
+
"@types/react": "18.3.20",
|
|
64
|
+
"@types/react-dom": "18.3.6",
|
|
48
65
|
"@types/ws": "^8.18.1",
|
|
49
66
|
"@vitejs/plugin-react": "^4.6.0",
|
|
50
67
|
"concurrently": "^9.2.0",
|