@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.
@@ -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 Props = {
13
+ type ProviderFactory = (id: string, yjsDocMap: Map<string, Doc>) => Provider;
14
+ type CollaborationPluginProps = {
14
15
  id: string;
15
- providerFactory: (id: string, yjsDocMap: Map<string, Doc>) => Provider;
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, }: Props): JSX.Element;
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 connect = React.useCallback(() => provider.connect(), [provider]);
45
- const disconnect = React.useCallback(() => {
46
- try {
47
- provider.disconnect();
48
- } catch (_e) {
49
- // Do nothing
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
- }, [provider]);
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
- yjs.initLocalState(provider, name, color, document.activeElement === editor.getRootElement(), awarenessData || {});
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
- awareness.on('update', onAwarenessUpdate);
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.getSharedType().observeDeep(onYjsTreeChanges);
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) === false) {
102
- yjs.syncLexicalUpdateToYjs(binding, provider, prevEditorState, editorState, dirtyElements, dirtyLeaves, normalizedNodes, tags);
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
- }, [binding, color, connect, disconnect, docMap, editor, id, initialEditorState, name, provider, shouldBootstrap, awarenessData, setDoc, syncCursorPositionsFn]);
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
- return cursorsContainer;
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
- React.useEffect(() => {
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, initLocalState, syncLexicalUpdateToYjs, TOGGLE_CONNECT_COMMAND, createUndoManager, setLocalStateFocus, CONNECTED_COMMAND, syncYjsChangesToLexical, createBinding } from '@lexical/yjs';
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, COMMAND_PRIORITY_EDITOR, UNDO_COMMAND, REDO_COMMAND, FOCUS_COMMAND, BLUR_COMMAND, CAN_UNDO_COMMAND, CAN_REDO_COMMAND, $getRoot, HISTORY_MERGE_TAG, $createParagraphNode, $getSelection } from 'lexical';
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 connect = useCallback(() => provider.connect(), [provider]);
31
- const disconnect = useCallback(() => {
32
- try {
33
- provider.disconnect();
34
- } catch (_e) {
35
- // Do nothing
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
- }, [provider]);
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
- initLocalState(provider, name, color, document.activeElement === editor.getRootElement(), awarenessData || {});
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
- awareness.on('update', onAwarenessUpdate);
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.getSharedType().observeDeep(onYjsTreeChanges);
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) === false) {
88
- syncLexicalUpdateToYjs(binding, provider, prevEditorState, editorState, dirtyElements, dirtyLeaves, normalizedNodes, tags);
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
- }, [binding, color, connect, disconnect, docMap, editor, id, initialEditorState, name, provider, shouldBootstrap, awarenessData, setDoc, syncCursorPositionsFn]);
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
- return cursorsContainer;
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
- useEffect(() => {
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;