@lexical/react
Version:
This package provides Lexical components and hooks for React applications.
226 lines (214 loc) • 7.77 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 { 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 };