@lexical/react
Version:
This package provides Lexical components and hooks for React applications.
185 lines (173 loc) • 6.14 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 { ContentEditable } from '@lexical/react/LexicalContentEditable';
import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary';
import { ReactProviderExtension } from '@lexical/react/ReactProviderExtension';
import { defineExtension, declarePeerDependency, shallowMergeConfig } from 'lexical';
import { LexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { useMemo, useSyncExternalStore, Suspense } from 'react';
import { createPortal } from 'react-dom';
import { jsx, jsxs, 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);
}
/**
* 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.
*
*/
/** @internal */
function useReactDecorators(editor, ErrorBoundary) {
const [subscribe, getSnapshot] = useMemo(() => [cb => editor.registerDecoratorListener(cb), () => editor.getDecorators()], [editor]);
const decorators = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
// Return decorators defined as React Portals
return useMemo(() => {
const decoratedPortals = [];
for (const nodeKey in decorators) {
const element = editor.getElementByKey(nodeKey);
if (element !== null) {
const reactDecorator = /*#__PURE__*/jsx(ErrorBoundary, {
onError: e => {
editor._onError(e);
},
children: /*#__PURE__*/jsx(Suspense, {
fallback: null,
children: decorators[nodeKey]
})
});
decoratedPortals.push(/*#__PURE__*/createPortal(reactDecorator, element, nodeKey));
}
}
return decoratedPortals;
}, [ErrorBoundary, decorators, editor]);
}
/**
* 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.
*
*/
function buildEditorComponent(config, context) {
const [editor] = context;
const rawConfigDecorators = config.decorators.map(El =>
// eslint-disable-next-line react/jsx-key -- wrapped later
typeof El === 'function' ? /*#__PURE__*/jsx(El, {
context: context
}) : El);
return function EditorComponent(props) {
const {
EditorChildrenComponent = config.EditorChildrenComponent,
ErrorBoundary = config.ErrorBoundary,
contentEditable = config.contentEditable,
children
} = props;
const decorators = useReactDecorators(editor, ErrorBoundary);
const configDecorators = useMemo(() => rawConfigDecorators.map((decorator, i) => /*#__PURE__*/jsx(ErrorBoundary, {
onError: e => {
editor._onError(e);
}
// eslint-disable-next-line react/no-array-index-key -- no natural key
,
children: /*#__PURE__*/jsx(Suspense, {
fallback: null,
children: decorator
})
}, i)), [ErrorBoundary]);
return /*#__PURE__*/jsx(LexicalComposerContext.Provider, {
value: context,
children: /*#__PURE__*/jsxs(EditorChildrenComponent, {
context: context,
contentEditable: contentEditable,
children: [children, configDecorators, decorators]
})
});
};
}
/**
* @example
* The default EditorChildrenComponent implementation
* ```jsx
* return (
* <>
* {contentEditable}
* {children}
* </>
* );
* ```
*/
function DefaultEditorChildrenComponent({
contentEditable,
children
}) {
return /*#__PURE__*/jsxs(Fragment, {
children: [contentEditable, children]
});
}
const initialConfig = {
EditorChildrenComponent: DefaultEditorChildrenComponent,
ErrorBoundary: LexicalErrorBoundary,
contentEditable: /*#__PURE__*/jsx(ContentEditable, {}),
decorators: []
};
/**
* An extension to use or configure React for use with Lexical. In an editor, you
* would typically use {@link LexicalExtensionComposer} (for React projects) or
* {@link ReactPluginHostExtension} (to use React Extensions and plug-ins in a non-React
* project).
*
* See {@link ReactConfig} for more detailed exextensionations of how to use
* the config for this Extension.
*
* For an Extension developer, you can defineConfig() override the extension with
* decorators to add JSX inside the editor context that is not
* location-dependent (e.g. floating UI that does not need to be mounted in
* some specific location, or effects that return null).
*/
const ReactExtension = defineExtension({
build(editor, config, state) {
const providerPeer = state.getPeer(ReactProviderExtension.name);
if (!providerPeer) {
{
formatDevErrorMessage(`No ReactProviderExtension detected. You must use ReactPluginHostExtension or LexicalExtensionComposer to host React extensions. The following extensions depend on ReactExtension: ${[...state.getDirectDependentNames()].join(' ')}`);
}
}
const context = [editor, {
getTheme: () => editor._config.theme
}];
const Component = buildEditorComponent(config, context);
return {
Component,
context
};
},
config: initialConfig,
mergeConfig(a, b) {
const config = shallowMergeConfig(a, b);
if (b.decorators) {
config.decorators = b.decorators.length > 0 ? [...a.decorators, ...b.decorators] : a.decorators;
}
return config;
},
name: '@lexical/react/React',
peerDependencies: [
// We are not trying to avoid the import, just the direct dependency,
// so using the extension directly is fine.
declarePeerDependency(ReactProviderExtension.name)]
});
export { DefaultEditorChildrenComponent, ReactExtension };