UNPKG

@lexical/react

Version:

This package provides Lexical components and hooks for React applications.

182 lines (174 loc) 5.96 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 { untracked, signal, getExtensionDependencyFromEditor, effect } from '@lexical/extension'; import { ReactExtension } from '@lexical/react/ReactExtension'; import { ReactProviderExtension } from '@lexical/react/ReactProviderExtension'; import { mergeRegister } from '@lexical/utils'; import { createCommand, defineExtension, configExtension, COMMAND_PRIORITY_CRITICAL, COMMAND_PRIORITY_EDITOR } from 'lexical'; import { useState, useEffect, Suspense } from 'react'; import { createPortal } from 'react-dom'; import { createRoot } from 'react-dom/client'; 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. * */ // Do not require this module directly! Use normal `invariant` calls. function formatDevErrorMessage(message) { throw new Error(message); } function mountReactExtensionComponent(editor, opts) { const { props, extension, ...rest } = opts; const { Component } = getExtensionDependencyFromEditor(editor, extension).output; const element = props ? /*#__PURE__*/jsx(Component, { ...props }) : null; mountReactPluginElement(editor, { ...rest, element }); } function mountReactPluginComponent(editor, opts) { const { Component, props, ...rest } = opts; mountReactPluginElement(editor, { ...rest, element: props ? /*#__PURE__*/jsx(Component, { ...props }) : null }); } function mountReactPluginElement(editor, opts) { getExtensionDependencyFromEditor(editor, ReactPluginHostExtension).output.mountReactPlugin(opts); } function mountReactPluginHost(editor, container) { getExtensionDependencyFromEditor(editor, ReactPluginHostExtension).output.mountReactPluginHost(container); } const REACT_PLUGIN_HOST_MOUNT_ROOT_COMMAND = createCommand('REACT_PLUGIN_HOST_MOUNT_ROOT_COMMAND'); const REACT_PLUGIN_HOST_MOUNT_PLUGIN_COMMAND = createCommand('REACT_PLUGIN_HOST_MOUNT_PLUGIN_COMMAND'); function PluginHostDecorator({ context: [editor] }) { const { mountedPluginsStore } = getExtensionDependencyFromEditor(editor, ReactPluginHostExtension).output; const { ErrorBoundary } = getExtensionDependencyFromEditor(editor, ReactExtension).config; const onError = editor._onError.bind(editor); const [{ plugins }, setMountedPlugins] = useState(() => mountedPluginsStore.peek()); useEffect(() => effect(() => setMountedPlugins(mountedPluginsStore.value)), [mountedPluginsStore]); const children = []; for (const { key, element, domNode } of plugins.values()) { if (!element) { continue; } const wrapped = /*#__PURE__*/jsx(ErrorBoundary, { onError: onError, children: /*#__PURE__*/jsx(Suspense, { fallback: null, children: element }) }, key); children.push(domNode ? /*#__PURE__*/createPortal(wrapped, domNode, key) : wrapped); } return children.length > 0 ? /*#__PURE__*/jsx(Fragment, { children: children }) : null; } /** * This extension provides a React host for editors that are not built * with LexicalExtensionComposer (e.g. you are using Vanilla JS or some * other framework). * * You must use {@link mountReactPluginHost} for any React content to work. * Afterwards, you may use {@link mountReactExtensionComponent} to * render UI for a specific React Extension. * {@link mountReactPluginComponent} and * {@link mountReactPluginElement} can be used to render * legacy React plug-ins (or any React content). */ const ReactPluginHostExtension = defineExtension({ build(editor, config, state) { const mountedPluginsStore = signal({ plugins: new Map() }); return { mountReactPlugin: arg => { editor.dispatchCommand(REACT_PLUGIN_HOST_MOUNT_PLUGIN_COMMAND, arg); }, // Using outputs to wrap commands will give us better error messages // if the mount functions are called on an editor without this extension mountReactPluginHost: container => editor.dispatchCommand(REACT_PLUGIN_HOST_MOUNT_ROOT_COMMAND, { root: createRoot(container) }), mountedPluginsStore }; }, dependencies: [ReactProviderExtension, configExtension(ReactExtension, { decorators: [PluginHostDecorator] })], name: '@lexical/react/ReactPluginHost', register(editor, _config, state) { let root; const { mountedPluginsStore } = state.getOutput(); const { Component } = state.getDependency(ReactExtension).output; return mergeRegister(() => { if (root) { root.unmount(); } untracked(() => { mountedPluginsStore.value.plugins.clear(); }); }, editor.registerCommand(REACT_PLUGIN_HOST_MOUNT_PLUGIN_COMMAND, arg => { // This runs before the PluginHost version untracked(() => { const { plugins } = mountedPluginsStore.value; plugins.set(arg.key, arg); mountedPluginsStore.value = { plugins }; }); return false; }, COMMAND_PRIORITY_CRITICAL), editor.registerCommand(REACT_PLUGIN_HOST_MOUNT_ROOT_COMMAND, arg => { if (!(root === undefined)) { formatDevErrorMessage(`ReactPluginHostExtension: Root is already mounted`); } root = arg.root; root.render(/*#__PURE__*/jsx(Component, { contentEditable: null })); return true; }, COMMAND_PRIORITY_EDITOR)); } }); export { REACT_PLUGIN_HOST_MOUNT_PLUGIN_COMMAND, REACT_PLUGIN_HOST_MOUNT_ROOT_COMMAND, ReactPluginHostExtension, mountReactExtensionComponent, mountReactPluginComponent, mountReactPluginElement, mountReactPluginHost };