@lexical/react 0.36.2 → 0.36.3-nightly.20251003.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LexicalCollaborationPlugin.d.ts +17 -4
- package/LexicalCollaborationPlugin.dev.js +186 -56
- package/LexicalCollaborationPlugin.dev.mjs +188 -59
- package/LexicalCollaborationPlugin.mjs +2 -1
- package/LexicalCollaborationPlugin.node.mjs +2 -1
- package/LexicalCollaborationPlugin.prod.js +1 -1
- package/LexicalCollaborationPlugin.prod.mjs +1 -1
- package/LexicalContextMenuPlugin.dev.js +17 -3
- package/LexicalContextMenuPlugin.dev.mjs +17 -3
- package/LexicalContextMenuPlugin.prod.js +1 -1
- package/LexicalContextMenuPlugin.prod.mjs +1 -1
- package/LexicalNodeMenuPlugin.dev.js +17 -3
- package/LexicalNodeMenuPlugin.dev.mjs +17 -3
- package/LexicalNodeMenuPlugin.prod.js +1 -1
- package/LexicalNodeMenuPlugin.prod.mjs +1 -1
- package/LexicalTypeaheadMenuPlugin.dev.js +17 -3
- package/LexicalTypeaheadMenuPlugin.dev.mjs +17 -3
- package/LexicalTypeaheadMenuPlugin.prod.js +1 -1
- package/LexicalTypeaheadMenuPlugin.prod.mjs +1 -1
- package/package.json +18 -18
- package/shared/useYjsCollaboration.d.ts +9 -1
|
@@ -6,13 +6,14 @@
|
|
|
6
6
|
*
|
|
7
7
|
*/
|
|
8
8
|
import type { JSX } from 'react';
|
|
9
|
-
import type { Doc } from 'yjs';
|
|
10
9
|
import { ExcludedProperties, Provider, SyncCursorPositionsFn } from '@lexical/yjs';
|
|
10
|
+
import { Doc } from 'yjs';
|
|
11
11
|
import { InitialEditorStateType } from './LexicalComposer';
|
|
12
12
|
import { CursorsContainerRef } from './shared/useYjsCollaboration';
|
|
13
|
-
type
|
|
13
|
+
type ProviderFactory = (id: string, yjsDocMap: Map<string, Doc>) => Provider;
|
|
14
|
+
type CollaborationPluginProps = {
|
|
14
15
|
id: string;
|
|
15
|
-
providerFactory:
|
|
16
|
+
providerFactory: ProviderFactory;
|
|
16
17
|
shouldBootstrap: boolean;
|
|
17
18
|
username?: string;
|
|
18
19
|
cursorColor?: string;
|
|
@@ -22,5 +23,17 @@ type Props = {
|
|
|
22
23
|
awarenessData?: object;
|
|
23
24
|
syncCursorPositionsFn?: SyncCursorPositionsFn;
|
|
24
25
|
};
|
|
25
|
-
export declare function CollaborationPlugin({ id, providerFactory, shouldBootstrap, username, cursorColor, cursorsContainerRef, initialEditorState, excludedProperties, awarenessData, syncCursorPositionsFn, }:
|
|
26
|
+
export declare function CollaborationPlugin({ id, providerFactory, shouldBootstrap, username, cursorColor, cursorsContainerRef, initialEditorState, excludedProperties, awarenessData, syncCursorPositionsFn, }: CollaborationPluginProps): JSX.Element;
|
|
27
|
+
type CollaborationPluginV2Props = {
|
|
28
|
+
id: string;
|
|
29
|
+
doc: Doc;
|
|
30
|
+
provider: Provider;
|
|
31
|
+
__shouldBootstrapUnsafe: boolean;
|
|
32
|
+
username?: string;
|
|
33
|
+
cursorColor?: string;
|
|
34
|
+
cursorsContainerRef?: CursorsContainerRef;
|
|
35
|
+
excludedProperties?: ExcludedProperties;
|
|
36
|
+
awarenessData?: object;
|
|
37
|
+
};
|
|
38
|
+
export declare function CollaborationPluginV2__EXPERIMENTAL({ id, doc, provider, __shouldBootstrapUnsafe, username, cursorColor, cursorsContainerRef, excludedProperties, awarenessData, }: CollaborationPluginV2Props): JSX.Element;
|
|
26
39
|
export {};
|
|
@@ -41,35 +41,18 @@ var React__namespace = /*#__PURE__*/_interopNamespaceDefault(React);
|
|
|
41
41
|
|
|
42
42
|
function useYjsCollaboration(editor, id, provider, docMap, name, color, shouldBootstrap, binding, setDoc, cursorsContainerRef, initialEditorState, awarenessData, syncCursorPositionsFn = yjs.syncCursorPositions) {
|
|
43
43
|
const isReloadingDoc = React.useRef(false);
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
44
|
+
const onBootstrap = React.useCallback(() => {
|
|
45
|
+
const {
|
|
46
|
+
root
|
|
47
|
+
} = binding;
|
|
48
|
+
if (shouldBootstrap && root.isEmpty() && root._xmlText._length === 0) {
|
|
49
|
+
initializeEditor(editor, initialEditorState);
|
|
50
50
|
}
|
|
51
|
-
}, [
|
|
51
|
+
}, [binding, editor, initialEditorState, shouldBootstrap]);
|
|
52
52
|
React.useEffect(() => {
|
|
53
53
|
const {
|
|
54
54
|
root
|
|
55
55
|
} = binding;
|
|
56
|
-
const {
|
|
57
|
-
awareness
|
|
58
|
-
} = provider;
|
|
59
|
-
const onStatus = ({
|
|
60
|
-
status
|
|
61
|
-
}) => {
|
|
62
|
-
editor.dispatchCommand(yjs.CONNECTED_COMMAND, status === 'connected');
|
|
63
|
-
};
|
|
64
|
-
const onSync = isSynced => {
|
|
65
|
-
if (shouldBootstrap && isSynced && root.isEmpty() && root._xmlText._length === 0 && isReloadingDoc.current === false) {
|
|
66
|
-
initializeEditor(editor, initialEditorState);
|
|
67
|
-
}
|
|
68
|
-
isReloadingDoc.current = false;
|
|
69
|
-
};
|
|
70
|
-
const onAwarenessUpdate = () => {
|
|
71
|
-
syncCursorPositionsFn(binding, provider);
|
|
72
|
-
};
|
|
73
56
|
const onYjsTreeChanges = (events, transaction) => {
|
|
74
57
|
const origin = transaction.origin;
|
|
75
58
|
if (origin !== binding) {
|
|
@@ -77,33 +60,145 @@ function useYjsCollaboration(editor, id, provider, docMap, name, color, shouldBo
|
|
|
77
60
|
yjs.syncYjsChangesToLexical(binding, provider, events, isFromUndoManger, syncCursorPositionsFn);
|
|
78
61
|
}
|
|
79
62
|
};
|
|
80
|
-
|
|
63
|
+
|
|
64
|
+
// This updates the local editor state when we receive updates from other clients
|
|
65
|
+
root.getSharedType().observeDeep(onYjsTreeChanges);
|
|
66
|
+
const removeListener = editor.registerUpdateListener(({
|
|
67
|
+
prevEditorState,
|
|
68
|
+
editorState,
|
|
69
|
+
dirtyLeaves,
|
|
70
|
+
dirtyElements,
|
|
71
|
+
normalizedNodes,
|
|
72
|
+
tags
|
|
73
|
+
}) => {
|
|
74
|
+
if (!tags.has(lexical.SKIP_COLLAB_TAG)) {
|
|
75
|
+
yjs.syncLexicalUpdateToYjs(binding, provider, prevEditorState, editorState, dirtyElements, dirtyLeaves, normalizedNodes, tags);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
return () => {
|
|
79
|
+
root.getSharedType().unobserveDeep(onYjsTreeChanges);
|
|
80
|
+
removeListener();
|
|
81
|
+
};
|
|
82
|
+
}, [binding, provider, editor, setDoc, docMap, id, syncCursorPositionsFn]);
|
|
83
|
+
|
|
84
|
+
// Note: 'reload' is not an actual Yjs event type. Included here for legacy support (#1409).
|
|
85
|
+
React.useEffect(() => {
|
|
81
86
|
const onProviderDocReload = ydoc => {
|
|
82
87
|
clearEditorSkipCollab(editor, binding);
|
|
83
88
|
setDoc(ydoc);
|
|
84
89
|
docMap.set(id, ydoc);
|
|
85
90
|
isReloadingDoc.current = true;
|
|
86
91
|
};
|
|
92
|
+
const onSync = () => {
|
|
93
|
+
isReloadingDoc.current = false;
|
|
94
|
+
};
|
|
87
95
|
provider.on('reload', onProviderDocReload);
|
|
88
|
-
provider.on('status', onStatus);
|
|
89
96
|
provider.on('sync', onSync);
|
|
90
|
-
|
|
97
|
+
return () => {
|
|
98
|
+
provider.off('reload', onProviderDocReload);
|
|
99
|
+
provider.off('sync', onSync);
|
|
100
|
+
};
|
|
101
|
+
}, [binding, provider, editor, setDoc, docMap, id]);
|
|
102
|
+
useProvider(editor, provider, name, color, isReloadingDoc, awarenessData, onBootstrap);
|
|
103
|
+
return useYjsCursors(binding, cursorsContainerRef);
|
|
104
|
+
}
|
|
105
|
+
function useYjsCollaborationV2__EXPERIMENTAL(editor, id, doc, provider, docMap, name, color, options = {}) {
|
|
106
|
+
const {
|
|
107
|
+
awarenessData,
|
|
108
|
+
excludedProperties,
|
|
109
|
+
rootName,
|
|
110
|
+
__shouldBootstrapUnsafe: shouldBootstrap
|
|
111
|
+
} = options;
|
|
112
|
+
|
|
113
|
+
// Note: v2 does not support 'reload' event, which is not an actual Yjs event type.
|
|
114
|
+
const isReloadingDoc = React.useMemo(() => ({
|
|
115
|
+
current: false
|
|
116
|
+
}), []);
|
|
117
|
+
const binding = React.useMemo(() => yjs.createBindingV2__EXPERIMENTAL(editor, id, doc, docMap, {
|
|
118
|
+
excludedProperties,
|
|
119
|
+
rootName
|
|
120
|
+
}), [editor, id, doc, docMap, excludedProperties, rootName]);
|
|
121
|
+
React.useEffect(() => {
|
|
122
|
+
docMap.set(id, doc);
|
|
123
|
+
return () => {
|
|
124
|
+
docMap.delete(id);
|
|
125
|
+
};
|
|
126
|
+
}, [doc, docMap, id]);
|
|
127
|
+
const onBootstrap = React.useCallback(() => {
|
|
128
|
+
const {
|
|
129
|
+
root
|
|
130
|
+
} = binding;
|
|
131
|
+
if (shouldBootstrap && root._length === 0) {
|
|
132
|
+
initializeEditor(editor);
|
|
133
|
+
}
|
|
134
|
+
}, [binding, editor, shouldBootstrap]);
|
|
135
|
+
React.useEffect(() => {
|
|
136
|
+
const {
|
|
137
|
+
root
|
|
138
|
+
} = binding;
|
|
139
|
+
const {
|
|
140
|
+
awareness
|
|
141
|
+
} = provider;
|
|
142
|
+
const onYjsTreeChanges = (events, transaction) => {
|
|
143
|
+
const origin = transaction.origin;
|
|
144
|
+
if (origin !== binding) {
|
|
145
|
+
const isFromUndoManger = origin instanceof yjs$1.UndoManager;
|
|
146
|
+
yjs.syncYjsChangesToLexicalV2__EXPERIMENTAL(binding, provider, events, transaction, isFromUndoManger);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
91
150
|
// This updates the local editor state when we receive updates from other clients
|
|
92
|
-
root.
|
|
151
|
+
root.observeDeep(onYjsTreeChanges);
|
|
93
152
|
const removeListener = editor.registerUpdateListener(({
|
|
94
153
|
prevEditorState,
|
|
95
154
|
editorState,
|
|
96
|
-
dirtyLeaves,
|
|
97
155
|
dirtyElements,
|
|
98
156
|
normalizedNodes,
|
|
99
157
|
tags
|
|
100
158
|
}) => {
|
|
101
|
-
if (tags.has(lexical.SKIP_COLLAB_TAG)
|
|
102
|
-
yjs.
|
|
159
|
+
if (!tags.has(lexical.SKIP_COLLAB_TAG)) {
|
|
160
|
+
yjs.syncLexicalUpdateToYjsV2__EXPERIMENTAL(binding, provider, prevEditorState, editorState, dirtyElements, normalizedNodes, tags);
|
|
103
161
|
}
|
|
104
162
|
});
|
|
163
|
+
const onAwarenessUpdate = () => {
|
|
164
|
+
yjs.syncCursorPositions(binding, provider);
|
|
165
|
+
};
|
|
166
|
+
awareness.on('update', onAwarenessUpdate);
|
|
167
|
+
return () => {
|
|
168
|
+
root.unobserveDeep(onYjsTreeChanges);
|
|
169
|
+
removeListener();
|
|
170
|
+
awareness.off('update', onAwarenessUpdate);
|
|
171
|
+
};
|
|
172
|
+
}, [binding, provider, editor]);
|
|
173
|
+
useProvider(editor, provider, name, color, isReloadingDoc, awarenessData, onBootstrap);
|
|
174
|
+
return binding;
|
|
175
|
+
}
|
|
176
|
+
function useProvider(editor, provider, name, color, isReloadingDoc, awarenessData, onBootstrap) {
|
|
177
|
+
const connect = React.useCallback(() => provider.connect(), [provider]);
|
|
178
|
+
const disconnect = React.useCallback(() => {
|
|
179
|
+
try {
|
|
180
|
+
provider.disconnect();
|
|
181
|
+
} catch (_e) {
|
|
182
|
+
// Do nothing
|
|
183
|
+
}
|
|
184
|
+
}, [provider]);
|
|
185
|
+
React.useEffect(() => {
|
|
186
|
+
const onStatus = ({
|
|
187
|
+
status
|
|
188
|
+
}) => {
|
|
189
|
+
editor.dispatchCommand(yjs.CONNECTED_COMMAND, status === 'connected');
|
|
190
|
+
};
|
|
191
|
+
const onSync = isSynced => {
|
|
192
|
+
if (isSynced && isReloadingDoc.current === false && onBootstrap) {
|
|
193
|
+
onBootstrap();
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
yjs.initLocalState(provider, name, color, document.activeElement === editor.getRootElement(), awarenessData || {});
|
|
197
|
+
provider.on('status', onStatus);
|
|
198
|
+
provider.on('sync', onSync);
|
|
105
199
|
const connectionPromise = connect();
|
|
106
200
|
return () => {
|
|
201
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- expected that isReloadingDoc.current may change
|
|
107
202
|
if (isReloadingDoc.current === false) {
|
|
108
203
|
if (connectionPromise) {
|
|
109
204
|
connectionPromise.then(disconnect);
|
|
@@ -120,21 +215,8 @@ function useYjsCollaboration(editor, id, provider, docMap, name, color, shouldBo
|
|
|
120
215
|
}
|
|
121
216
|
provider.off('sync', onSync);
|
|
122
217
|
provider.off('status', onStatus);
|
|
123
|
-
provider.off('reload', onProviderDocReload);
|
|
124
|
-
awareness.off('update', onAwarenessUpdate);
|
|
125
|
-
root.getSharedType().unobserveDeep(onYjsTreeChanges);
|
|
126
|
-
docMap.delete(id);
|
|
127
|
-
removeListener();
|
|
128
218
|
};
|
|
129
|
-
}, [
|
|
130
|
-
const cursorsContainer = React.useMemo(() => {
|
|
131
|
-
const ref = element => {
|
|
132
|
-
binding.cursorsContainer = element;
|
|
133
|
-
};
|
|
134
|
-
return /*#__PURE__*/reactDom.createPortal(/*#__PURE__*/jsxRuntime.jsx("div", {
|
|
135
|
-
ref: ref
|
|
136
|
-
}), cursorsContainerRef && cursorsContainerRef.current || document.body);
|
|
137
|
-
}, [binding, cursorsContainerRef]);
|
|
219
|
+
}, [editor, provider, name, color, isReloadingDoc, awarenessData, onBootstrap, connect, disconnect]);
|
|
138
220
|
React.useEffect(() => {
|
|
139
221
|
return editor.registerCommand(yjs.TOGGLE_CONNECT_COMMAND, payload => {
|
|
140
222
|
const shouldConnect = payload;
|
|
@@ -150,7 +232,16 @@ function useYjsCollaboration(editor, id, provider, docMap, name, color, shouldBo
|
|
|
150
232
|
return true;
|
|
151
233
|
}, lexical.COMMAND_PRIORITY_EDITOR);
|
|
152
234
|
}, [connect, disconnect, editor]);
|
|
153
|
-
|
|
235
|
+
}
|
|
236
|
+
function useYjsCursors(binding, cursorsContainerRef) {
|
|
237
|
+
return React.useMemo(() => {
|
|
238
|
+
const ref = element => {
|
|
239
|
+
binding.cursorsContainer = element;
|
|
240
|
+
};
|
|
241
|
+
return /*#__PURE__*/reactDom.createPortal(/*#__PURE__*/jsxRuntime.jsx("div", {
|
|
242
|
+
ref: ref
|
|
243
|
+
}), cursorsContainerRef && cursorsContainerRef.current || document.body);
|
|
244
|
+
}, [binding, cursorsContainerRef]);
|
|
154
245
|
}
|
|
155
246
|
function useYjsFocusTracking(editor, provider, name, color, awarenessData) {
|
|
156
247
|
React.useEffect(() => {
|
|
@@ -165,6 +256,13 @@ function useYjsFocusTracking(editor, provider, name, color, awarenessData) {
|
|
|
165
256
|
}
|
|
166
257
|
function useYjsHistory(editor, binding) {
|
|
167
258
|
const undoManager = React.useMemo(() => yjs.createUndoManager(binding, binding.root.getSharedType()), [binding]);
|
|
259
|
+
return useYjsUndoManager(editor, undoManager);
|
|
260
|
+
}
|
|
261
|
+
function useYjsHistoryV2(editor, binding) {
|
|
262
|
+
const undoManager = React.useMemo(() => yjs.createUndoManager(binding, binding.root), [binding]);
|
|
263
|
+
return useYjsUndoManager(editor, undoManager);
|
|
264
|
+
}
|
|
265
|
+
function useYjsUndoManager(editor, undoManager) {
|
|
168
266
|
React.useEffect(() => {
|
|
169
267
|
const undo = () => {
|
|
170
268
|
undoManager.undo();
|
|
@@ -314,16 +412,7 @@ function CollaborationPlugin({
|
|
|
314
412
|
color
|
|
315
413
|
} = collabContext;
|
|
316
414
|
const [editor] = LexicalComposerContext.useLexicalComposerContext();
|
|
317
|
-
|
|
318
|
-
collabContext.isCollabActive = true;
|
|
319
|
-
return () => {
|
|
320
|
-
// Resetting flag only when unmount top level editor collab plugin. Nested
|
|
321
|
-
// editors (e.g. image caption) should unmount without affecting it
|
|
322
|
-
if (editor._parentEditor == null) {
|
|
323
|
-
collabContext.isCollabActive = false;
|
|
324
|
-
}
|
|
325
|
-
};
|
|
326
|
-
}, [collabContext, editor]);
|
|
415
|
+
useCollabActive(collabContext, editor);
|
|
327
416
|
const [provider, setProvider] = React.useState();
|
|
328
417
|
const [doc, setDoc] = React.useState();
|
|
329
418
|
React.useEffect(() => {
|
|
@@ -394,5 +483,46 @@ function YjsCollaborationCursors({
|
|
|
394
483
|
useYjsFocusTracking(editor, provider, name, color, awarenessData);
|
|
395
484
|
return cursors;
|
|
396
485
|
}
|
|
486
|
+
function CollaborationPluginV2__EXPERIMENTAL({
|
|
487
|
+
id,
|
|
488
|
+
doc,
|
|
489
|
+
provider,
|
|
490
|
+
__shouldBootstrapUnsafe,
|
|
491
|
+
username,
|
|
492
|
+
cursorColor,
|
|
493
|
+
cursorsContainerRef,
|
|
494
|
+
excludedProperties,
|
|
495
|
+
awarenessData
|
|
496
|
+
}) {
|
|
497
|
+
const collabContext = LexicalCollaborationContext.useCollaborationContext(username, cursorColor);
|
|
498
|
+
const {
|
|
499
|
+
yjsDocMap,
|
|
500
|
+
name,
|
|
501
|
+
color
|
|
502
|
+
} = collabContext;
|
|
503
|
+
const [editor] = LexicalComposerContext.useLexicalComposerContext();
|
|
504
|
+
useCollabActive(collabContext, editor);
|
|
505
|
+
const binding = useYjsCollaborationV2__EXPERIMENTAL(editor, id, doc, provider, yjsDocMap, name, color, {
|
|
506
|
+
__shouldBootstrapUnsafe,
|
|
507
|
+
awarenessData,
|
|
508
|
+
excludedProperties
|
|
509
|
+
});
|
|
510
|
+
useYjsHistoryV2(editor, binding);
|
|
511
|
+
useYjsFocusTracking(editor, provider, name, color, awarenessData);
|
|
512
|
+
return useYjsCursors(binding, cursorsContainerRef);
|
|
513
|
+
}
|
|
514
|
+
const useCollabActive = (collabContext, editor) => {
|
|
515
|
+
React.useEffect(() => {
|
|
516
|
+
collabContext.isCollabActive = true;
|
|
517
|
+
return () => {
|
|
518
|
+
// Resetting flag only when unmount top level editor collab plugin. Nested
|
|
519
|
+
// editors (e.g. image caption) should unmount without affecting it
|
|
520
|
+
if (editor._parentEditor == null) {
|
|
521
|
+
collabContext.isCollabActive = false;
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
}, [collabContext, editor]);
|
|
525
|
+
};
|
|
397
526
|
|
|
398
527
|
exports.CollaborationPlugin = CollaborationPlugin;
|
|
528
|
+
exports.CollaborationPluginV2__EXPERIMENTAL = CollaborationPluginV2__EXPERIMENTAL;
|
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
|
|
9
9
|
import { useCollaborationContext } from '@lexical/react/LexicalCollaborationContext';
|
|
10
10
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
11
|
-
import { syncCursorPositions,
|
|
11
|
+
import { syncCursorPositions, syncLexicalUpdateToYjs, createUndoManager, setLocalStateFocus, createBindingV2__EXPERIMENTAL, syncLexicalUpdateToYjsV2__EXPERIMENTAL, syncYjsChangesToLexical, initLocalState, TOGGLE_CONNECT_COMMAND, syncYjsChangesToLexicalV2__EXPERIMENTAL, CONNECTED_COMMAND, createBinding } from '@lexical/yjs';
|
|
12
12
|
import * as React from 'react';
|
|
13
13
|
import { useRef, useCallback, useEffect, useMemo, useState } from 'react';
|
|
14
14
|
import { mergeRegister } from '@lexical/utils';
|
|
15
|
-
import { SKIP_COLLAB_TAG,
|
|
15
|
+
import { SKIP_COLLAB_TAG, FOCUS_COMMAND, COMMAND_PRIORITY_EDITOR, BLUR_COMMAND, $getRoot, HISTORY_MERGE_TAG, $createParagraphNode, $getSelection, UNDO_COMMAND, REDO_COMMAND, CAN_UNDO_COMMAND, CAN_REDO_COMMAND } from 'lexical';
|
|
16
16
|
import { createPortal } from 'react-dom';
|
|
17
17
|
import { UndoManager } from 'yjs';
|
|
18
18
|
import { jsx, Fragment } from 'react/jsx-runtime';
|
|
@@ -27,35 +27,18 @@ import { jsx, Fragment } from 'react/jsx-runtime';
|
|
|
27
27
|
|
|
28
28
|
function useYjsCollaboration(editor, id, provider, docMap, name, color, shouldBootstrap, binding, setDoc, cursorsContainerRef, initialEditorState, awarenessData, syncCursorPositionsFn = syncCursorPositions) {
|
|
29
29
|
const isReloadingDoc = useRef(false);
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
30
|
+
const onBootstrap = useCallback(() => {
|
|
31
|
+
const {
|
|
32
|
+
root
|
|
33
|
+
} = binding;
|
|
34
|
+
if (shouldBootstrap && root.isEmpty() && root._xmlText._length === 0) {
|
|
35
|
+
initializeEditor(editor, initialEditorState);
|
|
36
36
|
}
|
|
37
|
-
}, [
|
|
37
|
+
}, [binding, editor, initialEditorState, shouldBootstrap]);
|
|
38
38
|
useEffect(() => {
|
|
39
39
|
const {
|
|
40
40
|
root
|
|
41
41
|
} = binding;
|
|
42
|
-
const {
|
|
43
|
-
awareness
|
|
44
|
-
} = provider;
|
|
45
|
-
const onStatus = ({
|
|
46
|
-
status
|
|
47
|
-
}) => {
|
|
48
|
-
editor.dispatchCommand(CONNECTED_COMMAND, status === 'connected');
|
|
49
|
-
};
|
|
50
|
-
const onSync = isSynced => {
|
|
51
|
-
if (shouldBootstrap && isSynced && root.isEmpty() && root._xmlText._length === 0 && isReloadingDoc.current === false) {
|
|
52
|
-
initializeEditor(editor, initialEditorState);
|
|
53
|
-
}
|
|
54
|
-
isReloadingDoc.current = false;
|
|
55
|
-
};
|
|
56
|
-
const onAwarenessUpdate = () => {
|
|
57
|
-
syncCursorPositionsFn(binding, provider);
|
|
58
|
-
};
|
|
59
42
|
const onYjsTreeChanges = (events, transaction) => {
|
|
60
43
|
const origin = transaction.origin;
|
|
61
44
|
if (origin !== binding) {
|
|
@@ -63,33 +46,145 @@ function useYjsCollaboration(editor, id, provider, docMap, name, color, shouldBo
|
|
|
63
46
|
syncYjsChangesToLexical(binding, provider, events, isFromUndoManger, syncCursorPositionsFn);
|
|
64
47
|
}
|
|
65
48
|
};
|
|
66
|
-
|
|
49
|
+
|
|
50
|
+
// This updates the local editor state when we receive updates from other clients
|
|
51
|
+
root.getSharedType().observeDeep(onYjsTreeChanges);
|
|
52
|
+
const removeListener = editor.registerUpdateListener(({
|
|
53
|
+
prevEditorState,
|
|
54
|
+
editorState,
|
|
55
|
+
dirtyLeaves,
|
|
56
|
+
dirtyElements,
|
|
57
|
+
normalizedNodes,
|
|
58
|
+
tags
|
|
59
|
+
}) => {
|
|
60
|
+
if (!tags.has(SKIP_COLLAB_TAG)) {
|
|
61
|
+
syncLexicalUpdateToYjs(binding, provider, prevEditorState, editorState, dirtyElements, dirtyLeaves, normalizedNodes, tags);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
return () => {
|
|
65
|
+
root.getSharedType().unobserveDeep(onYjsTreeChanges);
|
|
66
|
+
removeListener();
|
|
67
|
+
};
|
|
68
|
+
}, [binding, provider, editor, setDoc, docMap, id, syncCursorPositionsFn]);
|
|
69
|
+
|
|
70
|
+
// Note: 'reload' is not an actual Yjs event type. Included here for legacy support (#1409).
|
|
71
|
+
useEffect(() => {
|
|
67
72
|
const onProviderDocReload = ydoc => {
|
|
68
73
|
clearEditorSkipCollab(editor, binding);
|
|
69
74
|
setDoc(ydoc);
|
|
70
75
|
docMap.set(id, ydoc);
|
|
71
76
|
isReloadingDoc.current = true;
|
|
72
77
|
};
|
|
78
|
+
const onSync = () => {
|
|
79
|
+
isReloadingDoc.current = false;
|
|
80
|
+
};
|
|
73
81
|
provider.on('reload', onProviderDocReload);
|
|
74
|
-
provider.on('status', onStatus);
|
|
75
82
|
provider.on('sync', onSync);
|
|
76
|
-
|
|
83
|
+
return () => {
|
|
84
|
+
provider.off('reload', onProviderDocReload);
|
|
85
|
+
provider.off('sync', onSync);
|
|
86
|
+
};
|
|
87
|
+
}, [binding, provider, editor, setDoc, docMap, id]);
|
|
88
|
+
useProvider(editor, provider, name, color, isReloadingDoc, awarenessData, onBootstrap);
|
|
89
|
+
return useYjsCursors(binding, cursorsContainerRef);
|
|
90
|
+
}
|
|
91
|
+
function useYjsCollaborationV2__EXPERIMENTAL(editor, id, doc, provider, docMap, name, color, options = {}) {
|
|
92
|
+
const {
|
|
93
|
+
awarenessData,
|
|
94
|
+
excludedProperties,
|
|
95
|
+
rootName,
|
|
96
|
+
__shouldBootstrapUnsafe: shouldBootstrap
|
|
97
|
+
} = options;
|
|
98
|
+
|
|
99
|
+
// Note: v2 does not support 'reload' event, which is not an actual Yjs event type.
|
|
100
|
+
const isReloadingDoc = useMemo(() => ({
|
|
101
|
+
current: false
|
|
102
|
+
}), []);
|
|
103
|
+
const binding = useMemo(() => createBindingV2__EXPERIMENTAL(editor, id, doc, docMap, {
|
|
104
|
+
excludedProperties,
|
|
105
|
+
rootName
|
|
106
|
+
}), [editor, id, doc, docMap, excludedProperties, rootName]);
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
docMap.set(id, doc);
|
|
109
|
+
return () => {
|
|
110
|
+
docMap.delete(id);
|
|
111
|
+
};
|
|
112
|
+
}, [doc, docMap, id]);
|
|
113
|
+
const onBootstrap = useCallback(() => {
|
|
114
|
+
const {
|
|
115
|
+
root
|
|
116
|
+
} = binding;
|
|
117
|
+
if (shouldBootstrap && root._length === 0) {
|
|
118
|
+
initializeEditor(editor);
|
|
119
|
+
}
|
|
120
|
+
}, [binding, editor, shouldBootstrap]);
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
const {
|
|
123
|
+
root
|
|
124
|
+
} = binding;
|
|
125
|
+
const {
|
|
126
|
+
awareness
|
|
127
|
+
} = provider;
|
|
128
|
+
const onYjsTreeChanges = (events, transaction) => {
|
|
129
|
+
const origin = transaction.origin;
|
|
130
|
+
if (origin !== binding) {
|
|
131
|
+
const isFromUndoManger = origin instanceof UndoManager;
|
|
132
|
+
syncYjsChangesToLexicalV2__EXPERIMENTAL(binding, provider, events, transaction, isFromUndoManger);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
77
136
|
// This updates the local editor state when we receive updates from other clients
|
|
78
|
-
root.
|
|
137
|
+
root.observeDeep(onYjsTreeChanges);
|
|
79
138
|
const removeListener = editor.registerUpdateListener(({
|
|
80
139
|
prevEditorState,
|
|
81
140
|
editorState,
|
|
82
|
-
dirtyLeaves,
|
|
83
141
|
dirtyElements,
|
|
84
142
|
normalizedNodes,
|
|
85
143
|
tags
|
|
86
144
|
}) => {
|
|
87
|
-
if (tags.has(SKIP_COLLAB_TAG)
|
|
88
|
-
|
|
145
|
+
if (!tags.has(SKIP_COLLAB_TAG)) {
|
|
146
|
+
syncLexicalUpdateToYjsV2__EXPERIMENTAL(binding, provider, prevEditorState, editorState, dirtyElements, normalizedNodes, tags);
|
|
89
147
|
}
|
|
90
148
|
});
|
|
149
|
+
const onAwarenessUpdate = () => {
|
|
150
|
+
syncCursorPositions(binding, provider);
|
|
151
|
+
};
|
|
152
|
+
awareness.on('update', onAwarenessUpdate);
|
|
153
|
+
return () => {
|
|
154
|
+
root.unobserveDeep(onYjsTreeChanges);
|
|
155
|
+
removeListener();
|
|
156
|
+
awareness.off('update', onAwarenessUpdate);
|
|
157
|
+
};
|
|
158
|
+
}, [binding, provider, editor]);
|
|
159
|
+
useProvider(editor, provider, name, color, isReloadingDoc, awarenessData, onBootstrap);
|
|
160
|
+
return binding;
|
|
161
|
+
}
|
|
162
|
+
function useProvider(editor, provider, name, color, isReloadingDoc, awarenessData, onBootstrap) {
|
|
163
|
+
const connect = useCallback(() => provider.connect(), [provider]);
|
|
164
|
+
const disconnect = useCallback(() => {
|
|
165
|
+
try {
|
|
166
|
+
provider.disconnect();
|
|
167
|
+
} catch (_e) {
|
|
168
|
+
// Do nothing
|
|
169
|
+
}
|
|
170
|
+
}, [provider]);
|
|
171
|
+
useEffect(() => {
|
|
172
|
+
const onStatus = ({
|
|
173
|
+
status
|
|
174
|
+
}) => {
|
|
175
|
+
editor.dispatchCommand(CONNECTED_COMMAND, status === 'connected');
|
|
176
|
+
};
|
|
177
|
+
const onSync = isSynced => {
|
|
178
|
+
if (isSynced && isReloadingDoc.current === false && onBootstrap) {
|
|
179
|
+
onBootstrap();
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
initLocalState(provider, name, color, document.activeElement === editor.getRootElement(), awarenessData || {});
|
|
183
|
+
provider.on('status', onStatus);
|
|
184
|
+
provider.on('sync', onSync);
|
|
91
185
|
const connectionPromise = connect();
|
|
92
186
|
return () => {
|
|
187
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- expected that isReloadingDoc.current may change
|
|
93
188
|
if (isReloadingDoc.current === false) {
|
|
94
189
|
if (connectionPromise) {
|
|
95
190
|
connectionPromise.then(disconnect);
|
|
@@ -106,21 +201,8 @@ function useYjsCollaboration(editor, id, provider, docMap, name, color, shouldBo
|
|
|
106
201
|
}
|
|
107
202
|
provider.off('sync', onSync);
|
|
108
203
|
provider.off('status', onStatus);
|
|
109
|
-
provider.off('reload', onProviderDocReload);
|
|
110
|
-
awareness.off('update', onAwarenessUpdate);
|
|
111
|
-
root.getSharedType().unobserveDeep(onYjsTreeChanges);
|
|
112
|
-
docMap.delete(id);
|
|
113
|
-
removeListener();
|
|
114
204
|
};
|
|
115
|
-
}, [
|
|
116
|
-
const cursorsContainer = useMemo(() => {
|
|
117
|
-
const ref = element => {
|
|
118
|
-
binding.cursorsContainer = element;
|
|
119
|
-
};
|
|
120
|
-
return /*#__PURE__*/createPortal(/*#__PURE__*/jsx("div", {
|
|
121
|
-
ref: ref
|
|
122
|
-
}), cursorsContainerRef && cursorsContainerRef.current || document.body);
|
|
123
|
-
}, [binding, cursorsContainerRef]);
|
|
205
|
+
}, [editor, provider, name, color, isReloadingDoc, awarenessData, onBootstrap, connect, disconnect]);
|
|
124
206
|
useEffect(() => {
|
|
125
207
|
return editor.registerCommand(TOGGLE_CONNECT_COMMAND, payload => {
|
|
126
208
|
const shouldConnect = payload;
|
|
@@ -136,7 +218,16 @@ function useYjsCollaboration(editor, id, provider, docMap, name, color, shouldBo
|
|
|
136
218
|
return true;
|
|
137
219
|
}, COMMAND_PRIORITY_EDITOR);
|
|
138
220
|
}, [connect, disconnect, editor]);
|
|
139
|
-
|
|
221
|
+
}
|
|
222
|
+
function useYjsCursors(binding, cursorsContainerRef) {
|
|
223
|
+
return useMemo(() => {
|
|
224
|
+
const ref = element => {
|
|
225
|
+
binding.cursorsContainer = element;
|
|
226
|
+
};
|
|
227
|
+
return /*#__PURE__*/createPortal(/*#__PURE__*/jsx("div", {
|
|
228
|
+
ref: ref
|
|
229
|
+
}), cursorsContainerRef && cursorsContainerRef.current || document.body);
|
|
230
|
+
}, [binding, cursorsContainerRef]);
|
|
140
231
|
}
|
|
141
232
|
function useYjsFocusTracking(editor, provider, name, color, awarenessData) {
|
|
142
233
|
useEffect(() => {
|
|
@@ -151,6 +242,13 @@ function useYjsFocusTracking(editor, provider, name, color, awarenessData) {
|
|
|
151
242
|
}
|
|
152
243
|
function useYjsHistory(editor, binding) {
|
|
153
244
|
const undoManager = useMemo(() => createUndoManager(binding, binding.root.getSharedType()), [binding]);
|
|
245
|
+
return useYjsUndoManager(editor, undoManager);
|
|
246
|
+
}
|
|
247
|
+
function useYjsHistoryV2(editor, binding) {
|
|
248
|
+
const undoManager = useMemo(() => createUndoManager(binding, binding.root), [binding]);
|
|
249
|
+
return useYjsUndoManager(editor, undoManager);
|
|
250
|
+
}
|
|
251
|
+
function useYjsUndoManager(editor, undoManager) {
|
|
154
252
|
useEffect(() => {
|
|
155
253
|
const undo = () => {
|
|
156
254
|
undoManager.undo();
|
|
@@ -300,16 +398,7 @@ function CollaborationPlugin({
|
|
|
300
398
|
color
|
|
301
399
|
} = collabContext;
|
|
302
400
|
const [editor] = useLexicalComposerContext();
|
|
303
|
-
|
|
304
|
-
collabContext.isCollabActive = true;
|
|
305
|
-
return () => {
|
|
306
|
-
// Resetting flag only when unmount top level editor collab plugin. Nested
|
|
307
|
-
// editors (e.g. image caption) should unmount without affecting it
|
|
308
|
-
if (editor._parentEditor == null) {
|
|
309
|
-
collabContext.isCollabActive = false;
|
|
310
|
-
}
|
|
311
|
-
};
|
|
312
|
-
}, [collabContext, editor]);
|
|
401
|
+
useCollabActive(collabContext, editor);
|
|
313
402
|
const [provider, setProvider] = useState();
|
|
314
403
|
const [doc, setDoc] = useState();
|
|
315
404
|
useEffect(() => {
|
|
@@ -380,5 +469,45 @@ function YjsCollaborationCursors({
|
|
|
380
469
|
useYjsFocusTracking(editor, provider, name, color, awarenessData);
|
|
381
470
|
return cursors;
|
|
382
471
|
}
|
|
472
|
+
function CollaborationPluginV2__EXPERIMENTAL({
|
|
473
|
+
id,
|
|
474
|
+
doc,
|
|
475
|
+
provider,
|
|
476
|
+
__shouldBootstrapUnsafe,
|
|
477
|
+
username,
|
|
478
|
+
cursorColor,
|
|
479
|
+
cursorsContainerRef,
|
|
480
|
+
excludedProperties,
|
|
481
|
+
awarenessData
|
|
482
|
+
}) {
|
|
483
|
+
const collabContext = useCollaborationContext(username, cursorColor);
|
|
484
|
+
const {
|
|
485
|
+
yjsDocMap,
|
|
486
|
+
name,
|
|
487
|
+
color
|
|
488
|
+
} = collabContext;
|
|
489
|
+
const [editor] = useLexicalComposerContext();
|
|
490
|
+
useCollabActive(collabContext, editor);
|
|
491
|
+
const binding = useYjsCollaborationV2__EXPERIMENTAL(editor, id, doc, provider, yjsDocMap, name, color, {
|
|
492
|
+
__shouldBootstrapUnsafe,
|
|
493
|
+
awarenessData,
|
|
494
|
+
excludedProperties
|
|
495
|
+
});
|
|
496
|
+
useYjsHistoryV2(editor, binding);
|
|
497
|
+
useYjsFocusTracking(editor, provider, name, color, awarenessData);
|
|
498
|
+
return useYjsCursors(binding, cursorsContainerRef);
|
|
499
|
+
}
|
|
500
|
+
const useCollabActive = (collabContext, editor) => {
|
|
501
|
+
useEffect(() => {
|
|
502
|
+
collabContext.isCollabActive = true;
|
|
503
|
+
return () => {
|
|
504
|
+
// Resetting flag only when unmount top level editor collab plugin. Nested
|
|
505
|
+
// editors (e.g. image caption) should unmount without affecting it
|
|
506
|
+
if (editor._parentEditor == null) {
|
|
507
|
+
collabContext.isCollabActive = false;
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
}, [collabContext, editor]);
|
|
511
|
+
};
|
|
383
512
|
|
|
384
|
-
export { CollaborationPlugin };
|
|
513
|
+
export { CollaborationPlugin, CollaborationPluginV2__EXPERIMENTAL };
|
|
@@ -9,4 +9,5 @@
|
|
|
9
9
|
import * as modDev from './LexicalCollaborationPlugin.dev.mjs';
|
|
10
10
|
import * as modProd from './LexicalCollaborationPlugin.prod.mjs';
|
|
11
11
|
const mod = process.env.NODE_ENV !== 'production' ? modDev : modProd;
|
|
12
|
-
export const CollaborationPlugin = mod.CollaborationPlugin;
|
|
12
|
+
export const CollaborationPlugin = mod.CollaborationPlugin;
|
|
13
|
+
export const CollaborationPluginV2__EXPERIMENTAL = mod.CollaborationPluginV2__EXPERIMENTAL;
|