UNPKG

@lexical/react

Version:

This package provides Lexical components and hooks for React applications.

226 lines (214 loc) • 7.77 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 { effect, untracked, signal } from '@lexical/extension/signals'; import { getExtensionDependencyFromEditor } from '@lexical/extension/getExtensionDependencyFromEditor'; import { ReactExtension } from '@lexical/react/ReactExtension'; import { ReactProviderExtension } from '@lexical/react/ReactProviderExtension'; import { createCommand, mergeRegister, 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); } /** * Mounts the output `Component` of a {@link ReactExtension}-based extension into * an editor's plugin host, rendering it with the given `props`. Use this to add * UI for a specific extension to an editor that was not built with * {@link LexicalExtensionComposer}. The editor must use * {@link ReactPluginHostExtension} and its host must already be mounted with * {@link mountReactPluginHost}. */ 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 }); } /** * Mounts an arbitrary React `Component` (rendered with `props`, or unmounted * when `props` is `null`) into an editor's plugin host. Use this for legacy * React plug-ins or any React content. The editor must use * {@link ReactPluginHostExtension} with its host mounted via * {@link mountReactPluginHost}. */ function mountReactPluginComponent(editor, opts) { const { Component, props, ...rest } = opts; mountReactPluginElement(editor, { ...rest, element: props ? /*#__PURE__*/jsx(Component, { ...props }) : null }); } /** * Mounts a React `element` (the lowest-level entry point) into an editor's * plugin host. {@link mountReactExtensionComponent} and * {@link mountReactPluginComponent} are built on top of this. The editor must * use {@link ReactPluginHostExtension} with its host mounted via * {@link mountReactPluginHost}. */ function mountReactPluginElement(editor, opts) { getExtensionDependencyFromEditor(editor, ReactPluginHostExtension).output.mountReactPlugin(opts); } /** * Creates a React root in `container` and mounts the editor's React plugin host * into it. Call this once before mounting any React content with * {@link mountReactExtensionComponent}, {@link mountReactPluginComponent}, or * {@link mountReactPluginElement} on an editor using * {@link ReactPluginHostExtension}. */ function mountReactPluginHost(editor, container) { getExtensionDependencyFromEditor(editor, ReactPluginHostExtension).output.mountReactPluginHost(container); } /** * Command dispatched by {@link mountReactPluginHost} to mount the React plugin * host into a React root (see {@link HostMountCommandArg}). Handled by * {@link ReactPluginHostExtension}. */ const REACT_PLUGIN_HOST_MOUNT_ROOT_COMMAND = /* @__PURE__ */createCommand('REACT_PLUGIN_HOST_MOUNT_ROOT_COMMAND'); /** * Command dispatched by the mount helpers to add, update, or remove a piece of * React content in the plugin host. Its payload is a * {@link MountPluginCommandArg}, and it is handled by * {@link ReactPluginHostExtension}. */ const REACT_PLUGIN_HOST_MOUNT_PLUGIN_COMMAND = /* @__PURE__ */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 = { 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, ([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 };