@lexical/react
Version:
This package provides Lexical components and hooks for React applications.
740 lines (723 loc) • 27.2 kB
JavaScript
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import { useCollaborationContext } from '@lexical/react/LexicalCollaborationContext';
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { syncCursorPositions, syncLexicalUpdateToYjs, createUndoManager, setLocalStateFocus, createBindingV2__EXPERIMENTAL, CLEAR_DIFF_VERSIONS_COMMAND__EXPERIMENTAL, syncYjsStateToLexicalV2__EXPERIMENTAL, DIFF_VERSIONS_COMMAND__EXPERIMENTAL, renderSnapshot__EXPERIMENTAL, syncLexicalUpdateToYjsV2__EXPERIMENTAL, syncYjsChangesToLexical, initLocalState, TOGGLE_CONNECT_COMMAND, syncYjsChangesToLexicalV2__EXPERIMENTAL, removeCursorHighlightRule, CONNECTED_COMMAND, createYjsBinding } from '@lexical/yjs';
import * as React from 'react';
import { useRef, useCallback, useEffect, useMemo, useState } from 'react';
import { SKIP_COLLAB_TAG, mergeRegister, FOCUS_COMMAND, COMMAND_PRIORITY_EDITOR, BLUR_COMMAND, getActiveElement, registerEventListeners, UNDO_COMMAND, REDO_COMMAND, $getRoot, HISTORY_MERGE_TAG, $createParagraphNode, $getSelection, CAN_UNDO_COMMAND, CAN_REDO_COMMAND } from 'lexical';
import { createPortal } from 'react-dom';
import { UndoManager } from 'yjs';
import { jsx, Fragment } from 'react/jsx-runtime';
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
/**
* Well-known key under which the active Yjs {@link UndoManager} is published on
* the editor instance (mirroring how `@lexical/extension` attaches its builder
* via a `Symbol.for` key). Collab disables `@lexical/history`, so this is the
* handle tooling and e2e tests use to force a deterministic undo boundary via
* `editor[COLLAB_UNDO_MANAGER]?.stopCapturing()` instead of waiting out the
* UndoManager capture timeout.
*/
const COLLAB_UNDO_MANAGER = Symbol.for('@lexical/yjs/UndoManager');
function useYjsCollaboration(editor, id, provider, docMap, name, color, shouldBootstrap, binding, setDoc, cursorsContainerRef, initialEditorState, awarenessData, syncCursorPositionsFn = syncCursorPositions, selectionHighlight = false) {
const isReloadingDoc = useRef(false);
const onBootstrap = useCallback(() => {
const {
root
} = binding;
if (shouldBootstrap && root.isEmpty() && root._xmlText._length === 0) {
bootstrapEditor(binding, editor, initialEditorState);
}
}, [binding, editor, initialEditorState, shouldBootstrap]);
useEffect(() => {
const {
root
} = binding;
const onYjsTreeChanges = (events, transaction) => {
const origin = transaction.origin;
if (origin !== binding) {
const isFromUndoManger = origin instanceof UndoManager;
syncYjsChangesToLexical(binding, provider, events, isFromUndoManger, syncCursorPositionsFn);
}
};
// This updates the local editor state when we receive updates from other clients
root.getSharedType().observeDeep(onYjsTreeChanges);
const removeListener = editor.registerUpdateListener(({
prevEditorState,
editorState,
dirtyLeaves,
dirtyElements,
normalizedNodes,
tags
}) => {
if (!tags.has(SKIP_COLLAB_TAG)) {
syncLexicalUpdateToYjs(binding, provider, prevEditorState, editorState, dirtyElements, dirtyLeaves, normalizedNodes, tags);
}
});
return () => {
root.getSharedType().unobserveDeep(onYjsTreeChanges);
removeListener();
};
}, [binding, provider, editor, setDoc, docMap, id, syncCursorPositionsFn]);
// Note: 'reload' is not an actual Yjs event type. Included here for legacy support (#1409).
useEffect(() => {
const onProviderDocReload = ydoc => {
clearEditorSkipCollab(editor, binding);
setDoc(ydoc);
docMap.set(id, ydoc);
isReloadingDoc.current = true;
};
const onSync = () => {
isReloadingDoc.current = false;
};
provider.on('reload', onProviderDocReload);
provider.on('sync', onSync);
return () => {
provider.off('reload', onProviderDocReload);
provider.off('sync', onSync);
};
}, [binding, provider, editor, setDoc, docMap, id]);
useProvider(editor, provider, name, color, isReloadingDoc, awarenessData, onBootstrap);
useAwareness(binding, provider, selectionHighlight);
return useYjsCursors(binding, cursorsContainerRef);
}
function useYjsCollaborationV2__EXPERIMENTAL(editor, id, doc, provider, docMap, name, color, options = {}) {
const {
awarenessData,
excludedProperties,
rootName,
getXmlElement,
selectionHighlight = false,
__shouldBootstrapUnsafe: shouldBootstrap
} = options;
// Note: v2 does not support 'reload' event, which is not an actual Yjs event type.
const isReloadingDoc = useMemo(() => ({
current: false
}), []);
// Built once for this mount, in state rather than a memo: `useMemo` is a
// hint React may discard, and its inputs include values (an inline
// `excludedProperties` map, an inline root resolver) whose identity changes
// on every render. Rebuilding would re-run `getXmlElement` -- which the
// caller may use to create shared types -- and hand the editor a binding on
// another root, which the next local update would then overwrite with this
// editor's current content. Remount to edit a different document.
const [binding] = useState(() => createBindingV2__EXPERIMENTAL(editor, id, doc, docMap, {
excludedProperties,
getXmlElement,
rootName
}));
useEffect(() => {
docMap.set(id, doc);
return () => {
docMap.delete(id);
};
}, [doc, docMap, id]);
const onBootstrap = useCallback(() => {
const {
root
} = binding;
if (shouldBootstrap && root._length === 0) {
bootstrapEditor(binding, editor);
}
}, [binding, editor, shouldBootstrap]);
const [diffSnapshots, setDiffSnapshots] = useState();
useEffect(() => {
mergeRegister(editor.registerCommand(CLEAR_DIFF_VERSIONS_COMMAND__EXPERIMENTAL, () => {
setDiffSnapshots(null);
// Ensure that any state already in Yjs is loaded into the editor (eg: after clearing diff view).
syncYjsStateToLexicalV2__EXPERIMENTAL(binding, provider);
return true;
}, COMMAND_PRIORITY_EDITOR), editor.registerCommand(DIFF_VERSIONS_COMMAND__EXPERIMENTAL, ({
prevSnapshot,
snapshot
}) => {
setDiffSnapshots({
prevSnapshot,
snapshot
});
return true;
}, COMMAND_PRIORITY_EDITOR));
}, [editor, binding, provider]);
useEffect(() => {
const {
root
} = binding;
if (diffSnapshots) {
renderSnapshot__EXPERIMENTAL(binding, diffSnapshots.snapshot, diffSnapshots.prevSnapshot);
return;
}
const onYjsTreeChanges = (events, transaction) => {
const origin = transaction.origin;
if (origin !== binding) {
const isFromUndoManger = origin instanceof UndoManager;
syncYjsChangesToLexicalV2__EXPERIMENTAL(binding, provider, events, transaction, isFromUndoManger);
}
};
// This updates the local editor state when we receive updates from other clients
root.observeDeep(onYjsTreeChanges);
const removeListener = editor.registerUpdateListener(({
prevEditorState,
editorState,
dirtyElements,
dirtyLeaves,
normalizedNodes,
tags
}) => {
if (!tags.has(SKIP_COLLAB_TAG)) {
syncLexicalUpdateToYjsV2__EXPERIMENTAL(binding, provider, prevEditorState, editorState, dirtyElements, dirtyLeaves, normalizedNodes, tags);
}
});
return () => {
root.unobserveDeep(onYjsTreeChanges);
removeListener();
};
}, [binding, provider, editor, diffSnapshots]);
useProvider(editor, provider, name, color, isReloadingDoc, awarenessData, onBootstrap);
useAwareness(binding, provider, selectionHighlight);
return binding;
}
function useProvider(editor, provider, name, color, isReloadingDoc, awarenessData, onBootstrap) {
const connect = useCallback(() => provider.connect(), [provider]);
const disconnect = useCallback(() => {
try {
provider.disconnect();
} catch (_e) {
// Do nothing
}
}, [provider]);
useEffect(() => {
const onStatus = ({
status
}) => {
editor.dispatchCommand(CONNECTED_COMMAND, status === 'connected');
};
const onSync = isSynced => {
if (isSynced && isReloadingDoc.current === false && onBootstrap) {
onBootstrap();
}
};
const rootElement = editor.getRootElement();
initLocalState(provider, name, color,
// getActiveElement rather than document.activeElement, which reports the
// shadow host when the editor is in a shadow root.
rootElement !== null && getActiveElement(rootElement) === rootElement, awarenessData || {});
provider.on('status', onStatus);
provider.on('sync', onSync);
const connectionPromise = connect();
return () => {
// eslint-disable-next-line react-hooks/exhaustive-deps -- expected that isReloadingDoc.current may change
if (isReloadingDoc.current === false) {
if (connectionPromise) {
connectionPromise.then(disconnect);
} else {
// Workaround for race condition in StrictMode. It's possible there
// is a different race for the above case where connect returns a
// promise, but we don't have an example of that in-repo.
// It's possible that there is a similar issue with
// TOGGLE_CONNECT_COMMAND below when the provider connect returns a
// promise.
// https://github.com/facebook/lexical/issues/6640
disconnect();
}
}
provider.off('sync', onSync);
provider.off('status', onStatus);
};
}, [editor, provider, name, color, isReloadingDoc, awarenessData, onBootstrap, connect, disconnect]);
useEffect(() => {
return editor.registerCommand(TOGGLE_CONNECT_COMMAND, payload => {
const shouldConnect = payload;
if (shouldConnect) {
// eslint-disable-next-line no-console
console.log('Collaboration connected!');
connect();
} else {
// eslint-disable-next-line no-console
console.log('Collaboration disconnected!');
disconnect();
}
return true;
}, COMMAND_PRIORITY_EDITOR);
}, [connect, disconnect, editor]);
// Clear awareness state immediately when tab is refreshed or closed
// This prevents ghost cursors from appearing for several seconds after disconnect
// See: https://github.com/facebook/lexical/issues/8061
useEffect(() => {
const clearAwarenessState = () => {
// Immediately clear local awareness state to signal disconnection
// This broadcasts to other clients that this client has disconnected,
// causing them to remove the cursor immediately instead of waiting for timeout
try {
provider.awareness.setLocalState(null);
} catch (_e) {
// Ignore errors during cleanup (e.g., if provider is already disconnected)
}
};
// Use both beforeunload and pagehide for maximum browser compatibility
// beforeunload: fires before page unloads (may be cancelable)
// pagehide: fires when page is being unloaded (more reliable, especially on mobile)
return registerEventListeners(window, {
beforeunload: clearAwarenessState,
pagehide: clearAwarenessState
});
}, [provider]);
}
function useAwareness(binding, provider, selectionHighlight) {
useEffect(() => {
const {
awareness
} = provider;
const onAwarenessUpdate = () => {
syncCursorPositions(binding, provider, {
selectionHighlight
});
};
awareness.on('update', onAwarenessUpdate);
return () => {
awareness.off('update', onAwarenessUpdate);
};
}, [binding, provider, selectionHighlight]);
}
function useYjsCursors(binding, cursorsContainerRef) {
return useMemo(() => {
const ref = element => {
// eslint-disable-next-line react-hooks/immutability
binding.cursorsContainer = element;
};
return /*#__PURE__*/createPortal(/*#__PURE__*/jsx("div", {
ref: ref
}),
// eslint-disable-next-line no-restricted-syntax
cursorsContainerRef && cursorsContainerRef.current || document.body);
}, [binding, cursorsContainerRef]);
}
function useYjsFocusTracking(editor, provider, name, color, awarenessData) {
useEffect(() => {
return mergeRegister(editor.registerCommand(FOCUS_COMMAND, () => {
setLocalStateFocus(provider, name, color, true, awarenessData || {});
return false;
}, COMMAND_PRIORITY_EDITOR), editor.registerCommand(BLUR_COMMAND, () => {
setLocalStateFocus(provider, name, color, false, awarenessData || {});
return false;
}, COMMAND_PRIORITY_EDITOR));
}, [color, editor, name, provider, awarenessData]);
}
function useYjsHistory(editor, binding) {
const undoManager = useMemo(() => createUndoManager(binding, binding.root.getSharedType()), [binding]);
return useYjsUndoManager(editor, undoManager);
}
function useYjsHistoryV2(editor, binding) {
const undoManager = useMemo(() => createUndoManager(binding, binding.root), [binding]);
return useYjsUndoManager(editor, undoManager);
}
function useYjsUndoManager(editor, undoManager) {
useEffect(() => {
const undo = () => {
undoManager.undo();
};
const redo = () => {
undoManager.redo();
};
return mergeRegister(editor.registerCommand(UNDO_COMMAND, () => {
undo();
return true;
}, COMMAND_PRIORITY_EDITOR), editor.registerCommand(REDO_COMMAND, () => {
redo();
return true;
}, COMMAND_PRIORITY_EDITOR));
});
// Publish the UndoManager on the editor (see COLLAB_UNDO_MANAGER) so tooling
// and e2e tests can reach it; remove it again when it changes or unmounts.
useEffect(() => {
const withManager = editor;
// eslint-disable-next-line react-hooks/immutability
withManager[COLLAB_UNDO_MANAGER] = undoManager;
return () => {
if (withManager[COLLAB_UNDO_MANAGER] === undoManager) {
delete withManager[COLLAB_UNDO_MANAGER];
}
};
}, [editor, undoManager]);
const clearHistory = useCallback(() => {
undoManager.clear();
}, [undoManager]);
// Exposing undo and redo states
React.useEffect(() => {
const updateUndoRedoStates = () => {
editor.dispatchCommand(CAN_UNDO_COMMAND, undoManager.undoStack.length > 0);
editor.dispatchCommand(CAN_REDO_COMMAND, undoManager.redoStack.length > 0);
};
undoManager.on('stack-item-added', updateUndoRedoStates);
undoManager.on('stack-item-popped', updateUndoRedoStates);
undoManager.on('stack-cleared', updateUndoRedoStates);
return () => {
undoManager.off('stack-item-added', updateUndoRedoStates);
undoManager.off('stack-item-popped', updateUndoRedoStates);
undoManager.off('stack-cleared', updateUndoRedoStates);
};
}, [editor, undoManager]);
return clearHistory;
}
/**
* Write the initial editor state into an empty shared document. The write is
* flagged on the binding so that the Yjs UndoManager created by
* `createUndoManager` skips the resulting transaction: bootstrapping is not a
* user edit and must not be undoable, which matches a non-collab editor where
* the initial state is applied with HISTORY_MERGE_TAG (#7110).
*/
function bootstrapEditor(binding, editor, initialEditorState) {
binding.isBootstrapping = true;
try {
// The Yjs write happens in the update listener during the commit, which is
// not necessarily synchronous with this call, so the flag has to outlive
// it. An `onUpdate` callback is the boundary that matches the write:
// Lexical queues it on `editor._deferred` before the update body runs and
// flushes it at the tail of the same commit that ran the update listeners,
// so it lands after the write and never before it. A queued deferred
// callback also forces a commit on its own, so this still runs when the
// update turns out to be a no-op.
initializeEditor(editor, initialEditorState, () => {
binding.isBootstrapping = false;
});
} finally {
// `onUpdate` alone is not enough: when the update body throws, Lexical
// reports the error, commits (running the update listeners, so the Yjs
// write still happens), and skips that commit's deferred callbacks. The
// reset would then be left queued until the tail of the *next* commit,
// by which point that commit's listener has already written to Yjs with
// the flag set — silently keeping the user's first edit after a failed
// bootstrap out of the undo stack. This bounds the flag's lifetime to a
// microtask no matter how the update ends. It cannot fire early: the
// commit is scheduled from inside `editor.update` above, so its microtask
// is queued ahead of this one.
queueMicrotask(() => {
binding.isBootstrapping = false;
});
}
}
function initializeEditor(editor, initialEditorState, onUpdate) {
editor.update(() => {
const root = $getRoot();
if (root.isEmpty()) {
if (initialEditorState) {
switch (typeof initialEditorState) {
case 'string':
{
const parsedEditorState = editor.parseEditorState(initialEditorState);
editor.setEditorState(parsedEditorState, {
tag: HISTORY_MERGE_TAG
});
break;
}
case 'object':
{
editor.setEditorState(initialEditorState, {
tag: HISTORY_MERGE_TAG
});
break;
}
case 'function':
{
editor.update(() => {
const root1 = $getRoot();
if (root1.isEmpty()) {
initialEditorState(editor);
}
}, {
tag: HISTORY_MERGE_TAG
});
break;
}
}
} else {
const paragraph = $createParagraphNode();
root.append(paragraph);
const rootElement = editor.getRootElement();
if ($getSelection() !== null || rootElement !== null && getActiveElement(rootElement) === rootElement) {
paragraph.select();
}
}
}
}, {
onUpdate,
tag: HISTORY_MERGE_TAG
});
}
function clearEditorSkipCollab(editor, binding) {
// reset editor state
editor.update(() => {
const root = $getRoot();
root.clear();
root.select();
}, {
tag: SKIP_COLLAB_TAG
});
if (binding.cursors == null) {
return;
}
const cursors = binding.cursors;
if (cursors == null) {
return;
}
const cursorsContainer = binding.cursorsContainer;
if (cursorsContainer == null) {
return;
}
for (const cursor of cursors.values()) {
const selection = cursor.selection;
if (selection === null) {
continue;
}
if (selection.highlight !== null) {
CSS.highlights.delete(selection.highlightName);
removeCursorHighlightRule(binding, selection.highlightName);
}
if (selection.caret.parentNode === cursorsContainer) {
cursorsContainer.removeChild(selection.caret);
}
for (const span of selection.selections) {
if (span.parentNode === cursorsContainer) {
cursorsContainer.removeChild(span);
}
}
cursor.selection = null;
}
}
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
/**
* Connects the editor to a Yjs document for real-time collaboration, syncing
* editor state and rendering remote users' cursors and selections. Provide a
* `providerFactory` that creates the Yjs {@link Provider} for the given
* document `id`. Must be used within a {@link LexicalCollaboration} provider.
*
* @returns The element that renders collaborators' cursors (or an empty
* fragment until the provider and binding are initialized).
*/
function CollaborationPlugin({
id,
providerFactory,
shouldBootstrap,
username,
cursorColor,
cursorsContainerRef,
initialEditorState,
excludedProperties,
awarenessData,
syncCursorPositionsFn,
selectionHighlight,
rootName,
getXmlText
}) {
const isBindingInitialized = useRef(false);
// The inputs that produced the current Provider. A ref rather than the effect
// deps alone because the effect must be idempotent: React StrictMode (and
// React 18+ remounts in general) re-runs the effect with unchanged inputs and
// must not create a second Provider.
const providerInputs = useRef(null);
const providerRef = useRef(null);
const collabContext = useCollaborationContext(username, cursorColor);
const {
yjsDocMap,
name,
color
} = collabContext;
const [editor] = useLexicalComposerContext();
useCollabActive(collabContext, editor);
const [provider, setProvider] = useState();
const [doc, setDoc] = useState();
useEffect(() => {
const prevInputs = providerInputs.current;
if (prevInputs !== null && prevInputs.id === id && prevInputs.providerFactory === providerFactory && prevInputs.yjsDocMap === yjsDocMap) {
return;
}
providerInputs.current = {
id,
providerFactory,
yjsDocMap
};
const newProvider = providerFactory(id, yjsDocMap);
const previousProvider = providerRef.current;
// Disconnected here rather than from this effect's cleanup, and only when
// something really did replace it. A `providerFactory` declared inline --
// the shape this package's own test harness uses -- has a fresh identity
// every render, so a cleanup-based disconnect tears down the live provider
// on every parent render; and when such a factory hands back a cached
// provider, setProvider() bails on the identical value, nothing re-runs,
// and the editor is left permanently disconnected.
if (previousProvider !== null && previousProvider !== newProvider) {
previousProvider.disconnect();
}
providerRef.current = newProvider;
setProvider(newProvider);
setDoc(yjsDocMap.get(id));
}, [id, providerFactory, yjsDocMap]);
useEffect(() => {
return () => {
const currentProvider = providerRef.current;
if (currentProvider !== null) {
providerRef.current = null;
currentProvider.disconnect();
}
};
}, []);
const [binding, setBinding] = useState();
useEffect(() => {
if (!provider) {
return;
}
if (isBindingInitialized.current) {
return;
}
const resolvedDoc = doc || yjsDocMap.get(id);
if (!resolvedDoc) {
return;
}
isBindingInitialized.current = true;
const newBinding = createYjsBinding({
doc: resolvedDoc,
docMap: yjsDocMap,
editor,
excludedProperties,
getXmlText,
id,
rootName
});
// eslint-disable-next-line react-hooks/set-state-in-effect
setBinding(newBinding);
// `excludedProperties`, `rootName` and `getXmlText` configure the binding
// and are read on the pass that creates it (which is not necessarily the
// first one -- the effect returns early until the provider exists, so a
// root that only resolves after mount is still picked up). They are
// deliberately not dependencies: a binding cannot be reconfigured or
// repointed once it exists, and re-running this effect would only tear the
// editor's binding down.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [editor, provider, id, yjsDocMap, doc]);
// Destroying the binding belongs to unmount, not to this effect's cleanup:
// the binding is created once, so a cleanup on the creation effect would
// destroy it on any input change without building a replacement.
useEffect(() => {
if (binding === undefined) {
return;
}
return () => {
binding.root.destroy(binding);
};
}, [binding]);
if (!provider || !binding) {
return /*#__PURE__*/jsx(Fragment, {});
}
return /*#__PURE__*/jsx(YjsCollaborationCursors, {
awarenessData: awarenessData,
binding: binding,
collabContext: collabContext,
color: color,
cursorsContainerRef: cursorsContainerRef,
editor: editor,
id: id,
initialEditorState: initialEditorState,
name: name,
provider: provider,
setDoc: setDoc,
shouldBootstrap: shouldBootstrap,
yjsDocMap: yjsDocMap,
syncCursorPositionsFn: syncCursorPositionsFn,
selectionHighlight: selectionHighlight
});
}
function YjsCollaborationCursors({
editor,
id,
provider,
yjsDocMap,
name,
color,
shouldBootstrap,
cursorsContainerRef,
initialEditorState,
awarenessData,
collabContext,
binding,
setDoc,
syncCursorPositionsFn,
selectionHighlight
}) {
const cursors = useYjsCollaboration(editor, id, provider, yjsDocMap, name, color, shouldBootstrap, binding, setDoc, cursorsContainerRef, initialEditorState, awarenessData, syncCursorPositionsFn, selectionHighlight);
useYjsHistory(editor, binding);
useYjsFocusTracking(editor, provider, name, color, awarenessData);
return cursors;
}
/**
* A variant of {@link CollaborationPlugin} that takes an already-created Yjs
* `doc` and {@link Provider} directly instead of a provider factory, giving the
* application full control over their lifecycle. Must be used within a
* {@link LexicalCollaboration} provider.
*
* @experimental The API may change in a future release.
* @returns The element that renders collaborators' cursors.
*/
function CollaborationPluginV2__EXPERIMENTAL({
id,
doc,
provider,
__shouldBootstrapUnsafe,
username,
cursorColor,
cursorsContainerRef,
excludedProperties,
awarenessData,
selectionHighlight,
rootName,
getXmlElement
}) {
const collabContext = useCollaborationContext(username, cursorColor);
const {
yjsDocMap,
name,
color
} = collabContext;
const [editor] = useLexicalComposerContext();
useCollabActive(collabContext, editor);
const binding = useYjsCollaborationV2__EXPERIMENTAL(editor, id, doc, provider, yjsDocMap, name, color, {
__shouldBootstrapUnsafe,
awarenessData,
excludedProperties,
getXmlElement,
rootName,
selectionHighlight
});
useYjsHistoryV2(editor, binding);
useYjsFocusTracking(editor, provider, name, color, awarenessData);
return useYjsCursors(binding, cursorsContainerRef);
}
const useCollabActive = (collabContext, editor) => {
useEffect(() => {
// eslint-disable-next-line react-hooks/immutability
collabContext.isCollabActive = true;
return () => {
// Resetting flag only when unmount top level editor collab plugin. Nested
// editors (e.g. image caption) should unmount without affecting it
if (editor._parentEditor == null) {
collabContext.isCollabActive = false;
}
};
}, [collabContext, editor]);
};
export { CollaborationPlugin, CollaborationPluginV2__EXPERIMENTAL };