UNPKG

@lexical/react

Version:

This package provides Lexical components and hooks for React applications.

234 lines (209 loc) 7.25 kB
/** * 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 { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; import { useLexicalEditable } from '@lexical/react/useLexicalEditable'; import { LexicalBuilder } from '@lexical/extension'; import { ReactProviderExtension } from '@lexical/react/ReactProviderExtension'; import { useLayoutEffect, useEffect, useState, useMemo, Suspense } from 'react'; import { flushSync, createPortal } from 'react-dom'; import { jsx, jsxs, Fragment } from 'react/jsx-runtime'; import { $canShowPlaceholderCurry } from '@lexical/text'; import { mergeRegister } from '@lexical/utils'; import { registerDragonSupport } from '@lexical/dragon'; import { registerRichText } from '@lexical/rich-text'; /** * 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. * */ // Do not require this module directly! Use normal `invariant` calls. function formatDevErrorMessage(message) { throw new Error(message); } /** * 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. * */ const CAN_USE_DOM = typeof window !== 'undefined' && typeof window.document !== 'undefined' && typeof window.document.createElement !== 'undefined'; /** * 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. * */ // This workaround is no longer necessary in React 19, // but we currently support React >=17.x // https://github.com/facebook/react/pull/26395 const useLayoutEffectImpl = CAN_USE_DOM ? useLayoutEffect : useEffect; /** * 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. * */ function useDecorators(editor, ErrorBoundary) { const [decorators, setDecorators] = useState(() => editor.getDecorators()); // Subscribe to changes useLayoutEffectImpl(() => { return editor.registerDecoratorListener(nextDecorators => { flushSync(() => { setDecorators(nextDecorators); }); }); }, [editor]); useEffect(() => { // If the content editable mounts before the subscription is added, then // nothing will be rendered on initial pass. We can get around that by // ensuring that we set the value. setDecorators(editor.getDecorators()); }, [editor]); // Return decorators defined as React Portals return useMemo(() => { const decoratedPortals = []; const decoratorKeys = Object.keys(decorators); for (let i = 0; i < decoratorKeys.length; i++) { const nodeKey = decoratorKeys[i]; const reactDecorator = /*#__PURE__*/jsx(ErrorBoundary, { onError: e => editor._onError(e), children: /*#__PURE__*/jsx(Suspense, { fallback: null, children: decorators[nodeKey] }) }); const element = editor.getElementByKey(nodeKey); if (element !== null) { decoratedPortals.push(/*#__PURE__*/createPortal(reactDecorator, element, nodeKey)); } } return decoratedPortals; }, [ErrorBoundary, decorators, editor]); } function isUsingReactExtension(editor) { const builder = LexicalBuilder.maybeFromEditor(editor); if (builder && builder.hasExtensionByName(ReactProviderExtension.name)) { for (const name of ['@lexical/plain-text', '@lexical/rich-text']) { if (!!builder.hasExtensionByName(name)) { formatDevErrorMessage(`LexicalBuilder: @lexical/react legacy text plugins conflict with the ${name} extension. Remove the legacy <RichTextPlugin/> or <PlainTextPlugin/> component.`); } } return true; } return false; } function Decorators({ editor, ErrorBoundary }) { return useDecorators(editor, ErrorBoundary); } /** * @internal * * When using @lexical/extension, the ReactProvider is expected to handle * rendering decorators. This component allows RichTextPlugin and * PlainTextPlugin to be used in extension projects that have not yet * migrated to use RichTextExtension or PlainTextExtension. **/ function LegacyDecorators({ editor, ErrorBoundary }) { return isUsingReactExtension(editor) ? null : /*#__PURE__*/jsx(Decorators, { editor: editor, ErrorBoundary: ErrorBoundary }); } /** * 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. * */ function canShowPlaceholderFromCurrentEditorState(editor) { const currentCanShowPlaceholder = editor.getEditorState().read($canShowPlaceholderCurry(editor.isComposing())); return currentCanShowPlaceholder; } function useCanShowPlaceholder(editor) { const [canShowPlaceholder, setCanShowPlaceholder] = useState(() => canShowPlaceholderFromCurrentEditorState(editor)); useLayoutEffectImpl(() => { function resetCanShowPlaceholder() { const currentCanShowPlaceholder = canShowPlaceholderFromCurrentEditorState(editor); setCanShowPlaceholder(currentCanShowPlaceholder); } resetCanShowPlaceholder(); return mergeRegister(editor.registerUpdateListener(() => { resetCanShowPlaceholder(); }), editor.registerEditableListener(() => { resetCanShowPlaceholder(); })); }, [editor]); return canShowPlaceholder; } /** * 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. * */ function useRichTextSetup(editor) { useLayoutEffectImpl(() => { return mergeRegister(registerRichText(editor), registerDragonSupport(editor)); // We only do this for init // eslint-disable-next-line react-hooks/exhaustive-deps }, [editor]); } /** * 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. * */ function RichTextPlugin({ contentEditable, // TODO Remove. This property is now part of ContentEditable placeholder = null, ErrorBoundary }) { const [editor] = useLexicalComposerContext(); useRichTextSetup(editor); return /*#__PURE__*/jsxs(Fragment, { children: [contentEditable, /*#__PURE__*/jsx(Placeholder, { content: placeholder }), /*#__PURE__*/jsx(LegacyDecorators, { editor: editor, ErrorBoundary: ErrorBoundary })] }); } // TODO remove function Placeholder({ content }) { const [editor] = useLexicalComposerContext(); const showPlaceholder = useCanShowPlaceholder(editor); const editable = useLexicalEditable(); if (!showPlaceholder) { return null; } if (typeof content === 'function') { return content(editable); } else { return content; } } export { RichTextPlugin };