UNPKG

@lexical/react

Version:

This package provides Lexical components and hooks for React applications.

184 lines (172 loc) • 6.73 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 { createLexicalComposerContext, LexicalComposerContext } from '@lexical/react/LexicalComposerContext'; import { CAN_USE_DOM, createEditor, $getRoot, $createParagraphNode, getActiveElement, $getSelection, HISTORY_MERGE_TAG } from 'lexical'; import { useLayoutEffect, useEffect, useMemo } from 'react'; import { jsx } 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. * */ // This workaround is no longer necessary in React 19, // but we currently support React >=18.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. * */ const HISTORY_MERGE_OPTIONS = { tag: HISTORY_MERGE_TAG }; /** * The initial editor state accepted by {@link LexicalComposer} via * `initialConfig.editorState`. Each variant is handled differently: * * - `null` — skip default initialization entirely. The root is left with no * children for an external owner (typically the * [collaboration plugin](/docs/collaboration/react) and its Yjs document) * to populate. * - `string` — a JSON string produced by serializing an `EditorState`. Parsed * with {@link LexicalEditor.parseEditorState} and applied via * {@link LexicalEditor.setEditorState}. * - `EditorState` — applied directly via {@link LexicalEditor.setEditorState}. * - `(editor) => void` — an updater run inside `editor.update(...)`. Invoked * only when the root is still empty, so it will not overwrite content * bootstrapped by another mechanism, and is silently skipped if the root * already has children. * * Note that `string` and `EditorState` inputs go through `setEditorState`, * which throws when the parsed state satisfies `EditorState.isEmpty()` (root * with no children and no selection). The empty serialization produced by * initializing with `null` and never modifying the editor falls into this * category, so it cannot be re-applied via `setEditorState` after a round-trip. */ /** * The configuration passed to {@link LexicalComposer} via its `initialConfig` * prop. It is read once when the editor is created and describes the editor's * `namespace`, registered `nodes`, `theme`, error handling, initial editable * state, optional initial {@link InitialEditorStateType}, and HTML * import/export configuration. */ /** * The root component for a Lexical editor in React. It creates a * {@link LexicalEditor} from `initialConfig`, provides it (and its * {@link LexicalComposerContextType}) to descendants through React context, and * renders its `children`. Place plugins and UI such as {@link RichTextPlugin} * and {@link ContentEditable} inside it, and read the editor from descendants * with {@link useLexicalComposerContext}. * * `LexicalComposer` uses the legacy plugin pattern and does not support the * extension API. To build an editor from extensions, use * {@link LexicalExtensionComposer} instead; see the * [React extensions guide](https://lexical.dev/docs/extensions/react). * * @deprecated Superseded by `LexicalExtensionComposer` from * `@lexical/react/LexicalExtensionComposer`, and expected to be removed in a * future major release. `LexicalComposer` cannot accept extensions, so any * feature delivered as an extension is unreachable from an editor built here. * See the [React extensions guide](https://lexical.dev/docs/extensions/react) * for the migration, which is usually a two-line change. * @returns A context provider wrapping `children`. */ function LexicalComposer({ initialConfig, children }) { const composerContext = useMemo(() => { const { theme, namespace, nodes, onError, onWarn, editorState: initialEditorState, html } = initialConfig; const context = createLexicalComposerContext(null, theme); const editor = createEditor({ editable: initialConfig.editable, html, namespace, nodes, onError: error => onError(error, editor), ...(onWarn ? { onWarn: error => onWarn(error, editor) } : {}), theme }); initializeEditor(editor, initialEditorState); return [editor, context]; }, // We only do this for init // eslint-disable-next-line react-hooks/exhaustive-deps []); useLayoutEffectImpl(() => { const isEditable = initialConfig.editable; const [editor] = composerContext; editor.setEditable(isEditable !== undefined ? isEditable : true); // We only do this for init // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return /*#__PURE__*/jsx(LexicalComposerContext.Provider, { value: composerContext, children: children }); } function initializeEditor(editor, initialEditorState) { if (initialEditorState === null) { return; } else if (initialEditorState === undefined) { editor.update(() => { const root = $getRoot(); if (root.isEmpty()) { const paragraph = $createParagraphNode(); root.append(paragraph); const rootElement = editor.getRootElement(); // getActiveElement rather than document.activeElement, which reports // the shadow host when the editor is in a shadow root. const activeElement = CAN_USE_DOM && rootElement !== null ? getActiveElement(rootElement) : null; if ($getSelection() !== null || activeElement !== null && activeElement === rootElement) { paragraph.select(); } } }, HISTORY_MERGE_OPTIONS); } else if (initialEditorState !== null) { switch (typeof initialEditorState) { case 'string': { const parsedEditorState = editor.parseEditorState(initialEditorState); editor.setEditorState(parsedEditorState, HISTORY_MERGE_OPTIONS); break; } case 'object': { editor.setEditorState(initialEditorState, HISTORY_MERGE_OPTIONS); break; } case 'function': { editor.update(() => { const root = $getRoot(); if (root.isEmpty()) { initialEditorState(editor); } }, HISTORY_MERGE_OPTIONS); break; } } } } export { LexicalComposer };