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