From 5de347ffffca3bf38315d3d87d2ccc5c28cd2723 Mon Sep 17 00:00:00 2001 From: Alessio Gravili <70709113+AlessioGr@users.noreply.github.com> Date: Wed, 6 Dec 2023 14:20:18 +0100 Subject: [PATCH] feat(richtext-lexical)!: lazy import React components to prevent client-only code from leaking into the server (#4290) * chore(richtext-lexical): lazy import all React things * chore(richtext-lexical): use useMemo for lazy-loaded React Components to prevent lag and flashes when parent component re-renders * chore: make exportPointerFiles.ts script usable for other packages as well by hoisting it up to the workspace root and making it configurable * chore(richtext-lexical): make sure no client-side code is imported by default from Features * chore(richtext-lexical): remove unnecessary scss files * chore(richtext-lexical): adjust package.json exports * chore(richtext-*): lazy-import Field & Cell Components, move Client-only exports to /components subpath export * chore(richtext-lexical): make sure nothing client-side is directly exported from the / subpath export anymore * add missing imports * chore: remove breaking changes for Slate * LazyCellComponent & LazyFieldComponent --- packages/payload/package.json | 2 +- .../forms/field-types/RichText/index.tsx | 27 ++- .../forms/field-types/RichText/types.ts | 29 +++- .../List/Cell/field-types/Richtext/index.tsx | 27 ++- packages/payload/src/config/schema.ts | 8 +- packages/payload/src/exports/README.md | 8 + packages/payload/src/fields/config/schema.ts | 6 +- packages/richtext-lexical/.gitignore | 4 + packages/richtext-lexical/package.json | 14 +- packages/richtext-lexical/src/cell/index.tsx | 31 ++-- .../src/exports/components.ts | 6 + packages/richtext-lexical/src/field/Field.tsx | 4 +- .../src/field/features/BlockQuote/index.ts | 15 +- .../field/features/Blocks/drawer/index.scss | 1 - .../field/features/Blocks/drawer/index.tsx | 5 +- .../src/field/features/Blocks/index.scss | 1 - .../src/field/features/Blocks/index.ts | 12 +- .../features/Blocks/nodes/BlocksNode.tsx | 8 +- .../field/features/Blocks/plugin/commands.ts | 8 + .../field/features/Blocks/plugin/index.tsx | 6 +- .../src/field/features/Heading/index.ts | 31 ++-- .../src/field/features/Link/index.scss | 1 - .../src/field/features/Link/index.ts | 40 +++-- .../{AutoLinkNode.tsx => AutoLinkNode.ts} | 0 .../Link/nodes/{LinkNode.tsx => LinkNode.ts} | 0 .../floatingLinkEditor/LinkEditor/commands.ts | 9 + .../floatingLinkEditor/LinkEditor/index.tsx | 7 +- .../src/field/features/Paragraph/index.ts | 13 +- .../features/Relationship/drawer/commands.ts | 7 + .../features/Relationship/drawer/index.scss | 1 - .../features/Relationship/drawer/index.tsx | 14 +- .../field/features/Relationship/index.scss | 1 - .../src/field/features/Relationship/index.ts | 15 +- .../Relationship/nodes/RelationshipNode.tsx | 7 +- .../components/RelationshipComponent.tsx | 2 +- .../features/Relationship/plugins/index.tsx | 2 +- .../field/features/Upload/component/index.tsx | 2 +- .../field/features/Upload/drawer/commands.ts | 7 + .../field/features/Upload/drawer/index.scss | 1 - .../field/features/Upload/drawer/index.tsx | 14 +- .../src/field/features/Upload/index.scss | 1 - .../src/field/features/Upload/index.ts | 13 +- ...oatingSelectToolbarAlignDropdownSection.ts | 7 +- .../src/field/features/align/index.scss | 4 - .../src/field/features/align/index.ts | 27 +-- .../index.scss | 4 - .../index.ts | 2 - .../index.scss | 4 - .../index.ts | 7 +- .../field/features/converters/html/index.ts | 4 - .../features/debug/TestRecorder/index.ts | 6 +- .../field/features/debug/TreeView/index.ts | 9 +- .../field/features/debug/TreeView/plugin.tsx | 2 + .../src/field/features/format/Bold/index.ts | 7 +- .../field/features/format/InlineCode/index.ts | 9 +- .../src/field/features/format/Italic/index.ts | 9 +- .../common/floatingSelectToolbarSection.ts | 2 - .../field/features/format/common/index.scss | 4 - .../features/format/strikethrough/index.ts | 11 +- .../field/features/format/subscript/index.ts | 11 +- .../features/format/superscript/index.ts | 11 +- .../field/features/format/underline/index.ts | 11 +- .../floatingSelectToolbarIndentSection.ts | 2 - .../src/field/features/indent/index.ts | 29 +++- .../src/field/features/indent/plugin.ts | 7 + .../field/features/lists/CheckList/index.ts | 18 +- .../field/features/lists/OrderedList/index.ts | 18 +- .../features/lists/UnorderedList/index.ts | 18 +- .../nodes/unknownConvertedNode/Component.tsx | 19 +++ .../nodes/unknownConvertedNode/index.tsx | 17 +- .../nodes/unknownConvertedNode/Component.tsx | 19 +++ .../nodes/unknownConvertedNode/index.tsx | 17 +- .../src/field/features/types.ts | 75 ++++----- packages/richtext-lexical/src/field/index.tsx | 12 +- .../src/field/lexical/EditorPlugin.tsx | 37 +++++ .../src/field/lexical/LexicalEditor.scss | 18 ++ .../src/field/lexical/LexicalEditor.tsx | 17 +- .../src/field/lexical/LexicalProvider.tsx | 31 +++- .../src/field/lexical/config/default.ts | 15 +- .../src/field/lexical/config/defaultClient.ts | 8 + .../src/field/lexical/config/types.ts | 4 +- .../ToolbarDropdown/index.tsx | 68 ++++++-- .../plugins/FloatingSelectToolbar/index.tsx | 155 ++++++++++++++---- .../plugins/FloatingSelectToolbar/types.ts | 17 +- .../LexicalTypeaheadMenuPlugin/types.ts | 5 +- .../field/lexical/plugins/SlashMenu/index.tsx | 17 +- packages/richtext-lexical/src/index.ts | 47 +++--- .../scripts => scripts}/exportPointerFiles.ts | 11 +- 88 files changed, 849 insertions(+), 413 deletions(-) create mode 100644 packages/richtext-lexical/.gitignore create mode 100644 packages/richtext-lexical/src/exports/components.ts delete mode 100644 packages/richtext-lexical/src/field/features/Blocks/drawer/index.scss delete mode 100644 packages/richtext-lexical/src/field/features/Blocks/index.scss create mode 100644 packages/richtext-lexical/src/field/features/Blocks/plugin/commands.ts delete mode 100644 packages/richtext-lexical/src/field/features/Link/index.scss rename packages/richtext-lexical/src/field/features/Link/nodes/{AutoLinkNode.tsx => AutoLinkNode.ts} (100%) rename packages/richtext-lexical/src/field/features/Link/nodes/{LinkNode.tsx => LinkNode.ts} (100%) create mode 100644 packages/richtext-lexical/src/field/features/Link/plugins/floatingLinkEditor/LinkEditor/commands.ts create mode 100644 packages/richtext-lexical/src/field/features/Relationship/drawer/commands.ts delete mode 100644 packages/richtext-lexical/src/field/features/Relationship/drawer/index.scss delete mode 100644 packages/richtext-lexical/src/field/features/Relationship/index.scss create mode 100644 packages/richtext-lexical/src/field/features/Upload/drawer/commands.ts delete mode 100644 packages/richtext-lexical/src/field/features/Upload/drawer/index.scss delete mode 100644 packages/richtext-lexical/src/field/features/Upload/index.scss delete mode 100644 packages/richtext-lexical/src/field/features/align/index.scss delete mode 100644 packages/richtext-lexical/src/field/features/common/floatingSelectToolbarFeaturesButtonsSection/index.scss delete mode 100644 packages/richtext-lexical/src/field/features/common/floatingSelectToolbarTextDropdownSection/index.scss delete mode 100644 packages/richtext-lexical/src/field/features/format/common/index.scss create mode 100644 packages/richtext-lexical/src/field/features/indent/plugin.ts create mode 100644 packages/richtext-lexical/src/field/features/migrations/LexicalPluginToLexical/nodes/unknownConvertedNode/Component.tsx create mode 100644 packages/richtext-lexical/src/field/features/migrations/SlateToLexical/nodes/unknownConvertedNode/Component.tsx create mode 100644 packages/richtext-lexical/src/field/lexical/EditorPlugin.tsx create mode 100644 packages/richtext-lexical/src/field/lexical/config/defaultClient.ts rename {packages/payload/scripts => scripts}/exportPointerFiles.ts (89%) diff --git a/packages/payload/package.json b/packages/payload/package.json index 5fdeae45e..ac1c718b1 100644 --- a/packages/payload/package.json +++ b/packages/payload/package.json @@ -26,7 +26,7 @@ } }, "scripts": { - "build": "pnpm copyfiles && pnpm build:swc && pnpm build:types && pnpm build:components && ts-node -T ./scripts/exportPointerFiles.ts", + "build": "pnpm copyfiles && pnpm build:swc && pnpm build:types && pnpm build:components && ts-node -T ../../scripts/exportPointerFiles.ts ../packages/payload dist/exports", "build:components": "webpack --config dist/admin/components.config.js", "build:swc": "swc ./src -d ./dist --config-file .swcrc", "build:types": "tsc --emitDeclarationOnly --outDir dist", diff --git a/packages/payload/src/admin/components/forms/field-types/RichText/index.tsx b/packages/payload/src/admin/components/forms/field-types/RichText/index.tsx index 2016115d6..8e203853e 100644 --- a/packages/payload/src/admin/components/forms/field-types/RichText/index.tsx +++ b/packages/payload/src/admin/components/forms/field-types/RichText/index.tsx @@ -1,13 +1,34 @@ -import React from 'react' +import React, { useMemo } from 'react' import type { RichTextField } from '../../../../../fields/config/types' import type { RichTextAdapter } from './types' const RichText: React.FC = (fieldprops) => { // eslint-disable-next-line react/destructuring-assignment const editor: RichTextAdapter = fieldprops.editor - const { FieldComponent } = editor - return + const isLazy = 'LazyFieldComponent' in editor + + const ImportedFieldComponent: React.FC = useMemo(() => { + return isLazy + ? React.lazy(() => { + return editor.LazyFieldComponent().then((resolvedComponent) => ({ + default: resolvedComponent, + })) + }) + : null + }, [editor, isLazy]) + + if (isLazy) { + return ( + ImportedFieldComponent && ( + + + + ) + ) + } + + return } export default RichText diff --git a/packages/payload/src/admin/components/forms/field-types/RichText/types.ts b/packages/payload/src/admin/components/forms/field-types/RichText/types.ts index c031739aa..13d50950d 100644 --- a/packages/payload/src/admin/components/forms/field-types/RichText/types.ts +++ b/packages/payload/src/admin/components/forms/field-types/RichText/types.ts @@ -13,15 +13,11 @@ export type RichTextFieldProps< path?: string } -export type RichTextAdapter< +type RichTextAdapterBase< Value extends object = object, AdapterProps = any, ExtraFieldProperties = {}, > = { - CellComponent: React.FC< - CellComponentProps> - > - FieldComponent: React.FC> afterReadPromise?: ({ field, incomingEditorState, @@ -31,7 +27,6 @@ export type RichTextAdapter< incomingEditorState: Value siblingDoc: Record }) => Promise | null - outputSchema?: ({ field, isRequired, @@ -59,3 +54,25 @@ export type RichTextAdapter< RichTextField > } + +export type RichTextAdapter< + Value extends object = object, + AdapterProps = any, + ExtraFieldProperties = {}, +> = RichTextAdapterBase & + ( + | { + CellComponent: React.FC< + CellComponentProps> + > + FieldComponent: React.FC> + } + | { + LazyCellComponent: () => Promise< + React.FC>> + > + LazyFieldComponent: () => Promise< + React.FC> + > + } + ) diff --git a/packages/payload/src/admin/components/views/collections/List/Cell/field-types/Richtext/index.tsx b/packages/payload/src/admin/components/views/collections/List/Cell/field-types/Richtext/index.tsx index 611d57b5d..f03cfb2b2 100644 --- a/packages/payload/src/admin/components/views/collections/List/Cell/field-types/Richtext/index.tsx +++ b/packages/payload/src/admin/components/views/collections/List/Cell/field-types/Richtext/index.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, { useMemo } from 'react' import type { RichTextField } from '../../../../../../../../fields/config/types' import type { RichTextAdapter } from '../../../../../../forms/field-types/RichText/types' @@ -7,9 +7,30 @@ import type { CellComponentProps } from '../../types' const RichTextCell: React.FC> = (props) => { // eslint-disable-next-line react/destructuring-assignment const editor: RichTextAdapter = props.field.editor - const { CellComponent } = editor - return + const isLazy = 'LazyCellComponent' in editor + + const ImportedCellComponent: React.FC = useMemo(() => { + return isLazy + ? React.lazy(() => { + return editor.LazyCellComponent().then((resolvedComponent) => ({ + default: resolvedComponent, + })) + }) + : null + }, [editor, isLazy]) + + if (isLazy) { + return ( + ImportedCellComponent && ( + + + + ) + ) + } + + return } export default RichTextCell diff --git a/packages/payload/src/config/schema.ts b/packages/payload/src/config/schema.ts index 96b4fc1dc..a98be21e9 100644 --- a/packages/payload/src/config/schema.ts +++ b/packages/payload/src/config/schema.ts @@ -1,7 +1,7 @@ import joi from 'joi' import { adminViewSchema } from './shared/adminViewSchema' -import { livePreviewSchema } from './shared/componentSchema' +import { componentSchema, livePreviewSchema } from './shared/componentSchema' const component = joi.alternatives().try(joi.object().unknown(), joi.func()) @@ -94,8 +94,10 @@ export default joi.object({ .object() .required() .keys({ - CellComponent: component.required(), - FieldComponent: component.required(), + CellComponent: componentSchema.optional(), + FieldComponent: componentSchema.optional(), + LazyCellComponent: joi.func().optional(), + LazyFieldComponent: joi.func().optional(), afterReadPromise: joi.func().optional(), outputSchema: joi.func().optional(), populationPromise: joi.func().optional(), diff --git a/packages/payload/src/exports/README.md b/packages/payload/src/exports/README.md index e69de29bb..dd73a1476 100644 --- a/packages/payload/src/exports/README.md +++ b/packages/payload/src/exports/README.md @@ -0,0 +1,8 @@ +Important: + +When you export anything with a scss or svg, or any component with a hook, it should be exported from a file within payload/components + + + + + diff --git a/packages/payload/src/fields/config/schema.ts b/packages/payload/src/fields/config/schema.ts index ead01c954..dbbb6b298 100644 --- a/packages/payload/src/fields/config/schema.ts +++ b/packages/payload/src/fields/config/schema.ts @@ -436,8 +436,10 @@ export const richText = baseField.keys({ editor: joi .object() .keys({ - CellComponent: componentSchema.required(), - FieldComponent: componentSchema.required(), + CellComponent: componentSchema.optional(), + FieldComponent: componentSchema.optional(), + LazyCellComponent: joi.func().optional(), + LazyFieldComponent: joi.func().optional(), afterReadPromise: joi.func().optional(), outputSchema: joi.func().optional(), populationPromise: joi.func().optional(), diff --git a/packages/richtext-lexical/.gitignore b/packages/richtext-lexical/.gitignore new file mode 100644 index 000000000..6b2c68f46 --- /dev/null +++ b/packages/richtext-lexical/.gitignore @@ -0,0 +1,4 @@ +/utilities.d.ts +/utilities.js +/components.d.ts +/components.js diff --git a/packages/richtext-lexical/package.json b/packages/richtext-lexical/package.json index 119030d24..6b1b38e49 100644 --- a/packages/richtext-lexical/package.json +++ b/packages/richtext-lexical/package.json @@ -9,7 +9,7 @@ "main": "./dist/index.js", "types": "./dist/index.d.ts", "scripts": { - "build": "pnpm copyfiles && pnpm build:swc && pnpm build:types", + "build": "pnpm copyfiles && pnpm build:swc && pnpm build:types && ts-node -T ../../scripts/exportPointerFiles.ts ../packages/richtext-lexical dist/exports", "build:swc": "swc ./src -d ./dist --config-file .swcrc", "build:types": "tsc --emitDeclarationOnly --outDir dist", "build:clean": "find . \\( -type d \\( -name build -o -name dist -o -name .cache \\) -o -type f -name tsconfig.tsbuildinfo \\) -exec rm -rf {} + && pnpm build", @@ -51,8 +51,14 @@ }, "exports": { ".": { - "default": "./src/index.ts", + "import": "./src/index.ts", + "require": "./src/index.ts", "types": "./src/index.ts" + }, + "./*": { + "import": "./src/exports/*.ts", + "require": "./src/exports/*.ts", + "types": "./src/exports/*.ts" } }, "publishConfig": { @@ -62,6 +68,8 @@ "types": "./dist/index.d.ts" }, "files": [ - "dist" + "dist", + "components.js", + "components.d.ts" ] } diff --git a/packages/richtext-lexical/src/cell/index.tsx b/packages/richtext-lexical/src/cell/index.tsx index aa1ae8689..ddea6d5b2 100644 --- a/packages/richtext-lexical/src/cell/index.tsx +++ b/packages/richtext-lexical/src/cell/index.tsx @@ -1,5 +1,6 @@ 'use client' import type { SerializedEditorState } from 'lexical' +import type { EditorConfig as LexicalEditorConfig } from 'lexical/LexicalEditor' import type { CellComponentProps, RichTextField } from 'payload/types' import { createHeadlessEditor } from '@lexical/headless' @@ -50,21 +51,23 @@ export const RichTextCell: React.FC< return } - // initialize headless editor - const headlessEditor = createHeadlessEditor({ - namespace: editorConfig.lexical.namespace, - nodes: getEnabledNodes({ editorConfig }), - theme: editorConfig.lexical.theme, + editorConfig.lexical().then((lexicalConfig: LexicalEditorConfig) => { + // initialize headless editor + const headlessEditor = createHeadlessEditor({ + namespace: lexicalConfig.namespace, + nodes: getEnabledNodes({ editorConfig }), + theme: lexicalConfig.theme, + }) + headlessEditor.setEditorState(headlessEditor.parseEditorState(dataToUse)) + + const textContent = + headlessEditor.getEditorState().read(() => { + return $getRoot().getTextContent() + }) || '' + + // Limiting the number of characters shown is done in a CSS rule + setPreview(textContent) }) - headlessEditor.setEditorState(headlessEditor.parseEditorState(dataToUse)) - - const textContent = - headlessEditor.getEditorState().read(() => { - return $getRoot().getTextContent() - }) || '' - - // Limiting the number of characters shown is done in a CSS rule - setPreview(textContent) }, [data, editorConfig]) return {preview} diff --git a/packages/richtext-lexical/src/exports/components.ts b/packages/richtext-lexical/src/exports/components.ts new file mode 100644 index 000000000..1acf7a23d --- /dev/null +++ b/packages/richtext-lexical/src/exports/components.ts @@ -0,0 +1,6 @@ +export { RichTextCell } from '../cell' +export { RichTextField } from '../field' + +export { defaultEditorLexicalConfig } from '../field/lexical/config/defaultClient' +export { ToolbarButton } from '../field/lexical/plugins/FloatingSelectToolbar/ToolbarButton' +export { ToolbarDropdown } from '../field/lexical/plugins/FloatingSelectToolbar/ToolbarDropdown/index' diff --git a/packages/richtext-lexical/src/field/Field.tsx b/packages/richtext-lexical/src/field/Field.tsx index 66d5c0e42..407d96718 100644 --- a/packages/richtext-lexical/src/field/Field.tsx +++ b/packages/richtext-lexical/src/field/Field.tsx @@ -73,11 +73,11 @@ const RichText: React.FC = (props) => {
} - placeholder={

Start typing, or press '/' for commands...

} + placeholder={ +

Start typing, or press '/' for commands...

+ } /> + return ( + + ) } })} {editor.isEditable() && ( @@ -113,12 +118,12 @@ export const LexicalEditor: React.FC {editorConfig.features.plugins.map((plugin) => { if (plugin.position === 'normal') { - return + return } })} {editorConfig.features.plugins.map((plugin) => { if (plugin.position === 'bottom') { - return + return } })} diff --git a/packages/richtext-lexical/src/field/lexical/LexicalProvider.tsx b/packages/richtext-lexical/src/field/lexical/LexicalProvider.tsx index cba79e796..4e1d1d1ce 100644 --- a/packages/richtext-lexical/src/field/lexical/LexicalProvider.tsx +++ b/packages/richtext-lexical/src/field/lexical/LexicalProvider.tsx @@ -2,6 +2,7 @@ import type { InitialConfigType } from '@lexical/react/LexicalComposer' import type { EditorState, SerializedEditorState } from 'lexical' import type { LexicalEditor } from 'lexical' +import type { EditorConfig as LexicalEditorConfig } from 'lexical/LexicalEditor' import { LexicalComposer } from '@lexical/react/LexicalComposer' import * as React from 'react' @@ -24,6 +25,25 @@ export const LexicalProvider: React.FC = (props) => { const { editorConfig, fieldProps, onChange, path, readOnly } = props let { value } = props + const [initialConfig, setInitialConfig] = React.useState(null) + + // set lexical config in useffect async: + React.useEffect(() => { + void editorConfig.lexical().then((lexicalConfig: LexicalEditorConfig) => { + const newInitialConfig: InitialConfigType = { + editable: readOnly !== true, + editorState: value != null ? JSON.stringify(value) : undefined, + namespace: lexicalConfig.namespace, + nodes: [...getEnabledNodes({ editorConfig })], + onError: (error: Error) => { + throw error + }, + theme: lexicalConfig.theme, + } + setInitialConfig(newInitialConfig) + }) + }, [editorConfig, readOnly, value]) + if (editorConfig?.features?.hooks?.load?.length) { editorConfig.features.hooks.load.forEach((hook) => { value = hook({ incomingEditorState: value }) @@ -49,15 +69,8 @@ export const LexicalProvider: React.FC = (props) => { ) } - const initialConfig: InitialConfigType = { - editable: readOnly === true ? false : true, - editorState: value != null ? JSON.stringify(value) : undefined, - namespace: editorConfig.lexical.namespace, - nodes: [...getEnabledNodes({ editorConfig })], - onError: (error: Error) => { - throw error - }, - theme: editorConfig.lexical.theme, + if (!initialConfig) { + return

Loading...

} return ( diff --git a/packages/richtext-lexical/src/field/lexical/config/default.ts b/packages/richtext-lexical/src/field/lexical/config/default.ts index 04969fcb7..c729ab445 100644 --- a/packages/richtext-lexical/src/field/lexical/config/default.ts +++ b/packages/richtext-lexical/src/field/lexical/config/default.ts @@ -1,5 +1,3 @@ -import type { EditorConfig as LexicalEditorConfig } from 'lexical/LexicalEditor' - import type { FeatureProvider } from '../../features/types' import type { EditorConfig, SanitizedEditorConfig } from './types' @@ -21,7 +19,6 @@ import { IndentFeature } from '../../features/indent' import { CheckListFeature } from '../../features/lists/CheckList' import { OrderedListFeature } from '../../features/lists/OrderedList' import { UnorderedListFeature } from '../../features/lists/UnorderedList' -import { LexicalEditorTheme } from '../theme/EditorTheme' import { sanitizeEditorConfig } from './sanitize' export const defaultEditorFeatures: FeatureProvider[] = [ @@ -46,14 +43,14 @@ export const defaultEditorFeatures: FeatureProvider[] = [ //BlocksFeature(), // Adding this by default makes no sense if no blocks are defined ] -export const defaultEditorLexicalConfig: LexicalEditorConfig = { - namespace: 'lexical', - theme: LexicalEditorTheme, -} - export const defaultEditorConfig: EditorConfig = { features: defaultEditorFeatures, - lexical: defaultEditorLexicalConfig, + lexical: () => + // @ts-expect-error + import('./defaultClient').then((module) => { + const defaultEditorLexicalConfig = module.defaultEditorLexicalConfig + return defaultEditorLexicalConfig + }), } export const defaultSanitizedEditorConfig: SanitizedEditorConfig = diff --git a/packages/richtext-lexical/src/field/lexical/config/defaultClient.ts b/packages/richtext-lexical/src/field/lexical/config/defaultClient.ts new file mode 100644 index 000000000..8683b86d2 --- /dev/null +++ b/packages/richtext-lexical/src/field/lexical/config/defaultClient.ts @@ -0,0 +1,8 @@ +import type { EditorConfig as LexicalEditorConfig } from 'lexical/LexicalEditor' + +import { LexicalEditorTheme } from '../theme/EditorTheme' + +export const defaultEditorLexicalConfig: LexicalEditorConfig = { + namespace: 'lexical', + theme: LexicalEditorTheme, +} diff --git a/packages/richtext-lexical/src/field/lexical/config/types.ts b/packages/richtext-lexical/src/field/lexical/config/types.ts index d87237859..319020c15 100644 --- a/packages/richtext-lexical/src/field/lexical/config/types.ts +++ b/packages/richtext-lexical/src/field/lexical/config/types.ts @@ -4,11 +4,11 @@ import type { FeatureProvider, ResolvedFeatureMap, SanitizedFeatures } from '../ export type EditorConfig = { features: FeatureProvider[] - lexical: LexicalEditorConfig + lexical?: () => Promise } export type SanitizedEditorConfig = { features: SanitizedFeatures - lexical: LexicalEditorConfig + lexical: () => Promise resolvedFeatureMap: ResolvedFeatureMap } diff --git a/packages/richtext-lexical/src/field/lexical/plugins/FloatingSelectToolbar/ToolbarDropdown/index.tsx b/packages/richtext-lexical/src/field/lexical/plugins/FloatingSelectToolbar/ToolbarDropdown/index.tsx index 2a130a70c..64752f875 100644 --- a/packages/richtext-lexical/src/field/lexical/plugins/FloatingSelectToolbar/ToolbarDropdown/index.tsx +++ b/packages/richtext-lexical/src/field/lexical/plugins/FloatingSelectToolbar/ToolbarDropdown/index.tsx @@ -1,5 +1,5 @@ 'use client' -import React from 'react' +import React, { useMemo } from 'react' const baseClass = 'floating-select-toolbar-popup__dropdown' @@ -10,6 +10,57 @@ import type { FloatingToolbarSectionEntry } from '../types' import { DropDown, DropDownItem } from './DropDown' import './index.scss' +export const ToolbarEntry = ({ + anchorElem, + editor, + entry, +}: { + anchorElem: HTMLElement + editor: LexicalEditor + entry: FloatingToolbarSectionEntry +}) => { + const Component = useMemo(() => { + return entry?.Component + ? React.lazy(() => + entry.Component().then((resolvedComponent) => ({ + default: resolvedComponent, + })), + ) + : null + }, [entry]) + + const ChildComponent = useMemo(() => { + return entry?.ChildComponent + ? React.lazy(() => + entry.ChildComponent().then((resolvedChildComponent) => ({ + default: resolvedChildComponent, + })), + ) + : null + }, [entry]) + + if (entry.Component) { + return ( + Component && ( + + + + ) + ) + } + + return ( + + {ChildComponent && ( + + + + )} + {entry.label} + + ) +} + export const ToolbarDropdown = ({ Icon, anchorElem, @@ -31,21 +82,8 @@ export const ToolbarDropdown = ({ > {entries.length && entries.map((entry) => { - if (entry.Component) { - return ( - - ) - } return ( - - - {entry.label} - + ) })} diff --git a/packages/richtext-lexical/src/field/lexical/plugins/FloatingSelectToolbar/index.tsx b/packages/richtext-lexical/src/field/lexical/plugins/FloatingSelectToolbar/index.tsx index 3b77a2795..e7141e685 100644 --- a/packages/richtext-lexical/src/field/lexical/plugins/FloatingSelectToolbar/index.tsx +++ b/packages/richtext-lexical/src/field/lexical/plugins/FloatingSelectToolbar/index.tsx @@ -10,10 +10,12 @@ import { COMMAND_PRIORITY_LOW, SELECTION_CHANGE_COMMAND, } from 'lexical' -import { useCallback, useEffect, useRef, useState } from 'react' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import * as React from 'react' import { createPortal } from 'react-dom' +import type { FloatingToolbarSection, FloatingToolbarSectionEntry } from './types' + import { useEditorConfigContext } from '../../config/EditorConfigProvider' import { getDOMRangeRect } from '../../utils/getDOMRangeRect' import { setFloatingElemPosition } from '../../utils/setFloatingElemPosition' @@ -21,6 +23,117 @@ import { ToolbarButton } from './ToolbarButton' import { ToolbarDropdown } from './ToolbarDropdown' import './index.scss' +function ButtonSectionEntry({ + anchorElem, + editor, + entry, +}: { + anchorElem: HTMLElement + editor: LexicalEditor + entry: FloatingToolbarSectionEntry +}): JSX.Element { + const Component = useMemo(() => { + return entry?.Component + ? React.lazy(() => + entry.Component().then((resolvedComponent) => ({ + default: resolvedComponent, + })), + ) + : null + }, [entry]) + + const ChildComponent = useMemo(() => { + return entry?.ChildComponent + ? React.lazy(() => + entry.ChildComponent().then((resolvedChildComponent) => ({ + default: resolvedChildComponent, + })), + ) + : null + }, [entry]) + + if (entry.Component) { + return ( + Component && ( + + {' '} + + ) + ) + } + + return ( + + {ChildComponent && ( + + + + )} + + ) +} + +function ToolbarSection({ + anchorElem, + editor, + index, + section, +}: { + anchorElem: HTMLElement + editor: LexicalEditor + index: number + section: FloatingToolbarSection +}): JSX.Element { + const { editorConfig } = useEditorConfigContext() + + const Icon = useMemo(() => { + return section?.type === 'dropdown' && section.entries.length && section.ChildComponent + ? React.lazy(() => + section.ChildComponent().then((resolvedComponent) => ({ + default: resolvedComponent, + })), + ) + : null + }, [section]) + + return ( +
+ {section.type === 'dropdown' && + section.entries.length && + (Icon ? ( + + + + ) : ( + + ))} + {section.type === 'buttons' && + section.entries.length && + section.entries.map((entry) => { + return ( + + ) + })} + {index < editorConfig.features.floatingSelectToolbar?.sections.length - 1 && ( +
+ )} +
+ ) +} + function FloatingSelectToolbar({ anchorElem, editor, @@ -176,41 +289,13 @@ function FloatingSelectToolbar({ {editorConfig?.features && editorConfig.features?.floatingSelectToolbar?.sections.map((section, i) => { return ( -
- {section.type === 'dropdown' && section.entries.length && ( - - )} - {section.type === 'buttons' && - section.entries.length && - section.entries.map((entry) => { - if (entry.Component) { - return ( - - ) - } - return ( - - - - ) - })} - {i < editorConfig.features.floatingSelectToolbar?.sections.length - 1 && ( -
- )} -
+ section={section} + /> ) })} diff --git a/packages/richtext-lexical/src/field/lexical/plugins/FloatingSelectToolbar/types.ts b/packages/richtext-lexical/src/field/lexical/plugins/FloatingSelectToolbar/types.ts index 8e0d48ae5..9e6f87402 100644 --- a/packages/richtext-lexical/src/field/lexical/plugins/FloatingSelectToolbar/types.ts +++ b/packages/richtext-lexical/src/field/lexical/plugins/FloatingSelectToolbar/types.ts @@ -1,8 +1,9 @@ import type { BaseSelection, LexicalEditor } from 'lexical' +import type React from 'react' export type FloatingToolbarSection = | { - ChildComponent?: React.FC + ChildComponent?: () => Promise entries: Array key: string order?: number @@ -16,13 +17,15 @@ export type FloatingToolbarSection = } export type FloatingToolbarSectionEntry = { - ChildComponent?: React.FC + ChildComponent?: () => Promise /** Use component to ignore the children and onClick properties. It does not use the default, pre-defined format Button component */ - Component?: React.FC<{ - anchorElem: HTMLElement - editor: LexicalEditor - entry: FloatingToolbarSectionEntry - }> + Component?: () => Promise< + React.FC<{ + anchorElem: HTMLElement + editor: LexicalEditor + entry: FloatingToolbarSectionEntry + }> + > isActive?: ({ editor, selection }: { editor: LexicalEditor; selection: BaseSelection }) => boolean isEnabled?: ({ editor, diff --git a/packages/richtext-lexical/src/field/lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/types.ts b/packages/richtext-lexical/src/field/lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/types.ts index 7aaedf218..4feae5484 100644 --- a/packages/richtext-lexical/src/field/lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/types.ts +++ b/packages/richtext-lexical/src/field/lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/types.ts @@ -1,10 +1,11 @@ import type { i18n } from 'i18next' import type { LexicalEditor } from 'lexical' import type { MutableRefObject } from 'react' +import type React from 'react' export class SlashMenuOption { // Icon for display - Icon: React.FC + Icon: () => Promise displayName?: (({ i18n }: { i18n: i18n }) => string) | string // Used for class names and, if displayName is not provided, for display. @@ -21,7 +22,7 @@ export class SlashMenuOption { constructor( key: string, options: { - Icon: React.FC + Icon: () => Promise displayName?: (({ i18n }: { i18n: i18n }) => string) | string keyboardShortcut?: string keywords?: Array diff --git a/packages/richtext-lexical/src/field/lexical/plugins/SlashMenu/index.tsx b/packages/richtext-lexical/src/field/lexical/plugins/SlashMenu/index.tsx index 57bc66ac3..95826c138 100644 --- a/packages/richtext-lexical/src/field/lexical/plugins/SlashMenu/index.tsx +++ b/packages/richtext-lexical/src/field/lexical/plugins/SlashMenu/index.tsx @@ -45,6 +45,16 @@ function SlashMenuItem({ title = title.substring(0, 25) + '...' } + const LazyIcon = useMemo(() => { + return option?.Icon + ? React.lazy(() => + option.Icon().then((resolvedIcon) => ({ + default: resolvedIcon, + })), + ) + : null + }, [option]) + return ( ) diff --git a/packages/richtext-lexical/src/index.ts b/packages/richtext-lexical/src/index.ts index 5289c5c2a..7a719aaab 100644 --- a/packages/richtext-lexical/src/index.ts +++ b/packages/richtext-lexical/src/index.ts @@ -2,17 +2,15 @@ import type { SerializedEditorState } from 'lexical' import type { EditorConfig as LexicalEditorConfig } from 'lexical/LexicalEditor' import type { RichTextAdapter } from 'payload/types' -import { withMergedProps, withNullableJSONSchemaType } from 'payload/utilities' +import { withNullableJSONSchemaType } from 'payload/utilities' import type { FeatureProvider } from './field/features/types' import type { EditorConfig, SanitizedEditorConfig } from './field/lexical/config/types' import type { AdapterProps } from './types' -import { RichTextCell } from './cell' -import { RichTextField } from './field' import { + defaultEditorConfig, defaultEditorFeatures, - defaultEditorLexicalConfig, defaultSanitizedEditorConfig, } from './field/lexical/config/default' import { sanitizeEditorConfig } from './field/lexical/config/sanitize' @@ -48,23 +46,38 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte features = cloneDeep(defaultEditorFeatures) } - const lexical: LexicalEditorConfig = props.lexical || cloneDeep(defaultEditorLexicalConfig) + const lexical: LexicalEditorConfig = props.lexical finalSanitizedEditorConfig = sanitizeEditorConfig({ features, - lexical, + lexical: props.lexical ? () => Promise.resolve(lexical) : defaultEditorConfig.lexical, }) } return { - CellComponent: withMergedProps({ - Component: RichTextCell, - toMergeIntoProps: { editorConfig: finalSanitizedEditorConfig }, - }), - FieldComponent: withMergedProps({ - Component: RichTextField, - toMergeIntoProps: { editorConfig: finalSanitizedEditorConfig }, - }), + LazyCellComponent: () => + // @ts-expect-error + import('./cell').then((module) => { + const RichTextCell = module.RichTextCell + return import('payload/utilities').then((module2) => + module2.withMergedProps({ + Component: RichTextCell, + toMergeIntoProps: { editorConfig: finalSanitizedEditorConfig }, + }), + ) + }), + + LazyFieldComponent: () => + // @ts-expect-error + import('./field').then((module) => { + const RichTextField = module.RichTextField + return import('payload/utilities').then((module2) => + module2.withMergedProps({ + Component: RichTextField, + toMergeIntoProps: { editorConfig: finalSanitizedEditorConfig }, + }), + ) + }), afterReadPromise: ({ field, incomingEditorState, siblingDoc }) => { return new Promise((resolve, reject) => { const promises: Promise[] = [] @@ -307,15 +320,12 @@ export { export { defaultEditorConfig, defaultEditorFeatures, - defaultEditorLexicalConfig, defaultSanitizedEditorConfig, } from './field/lexical/config/default' export { loadFeatures, sortFeaturesForOptimalLoading } from './field/lexical/config/loader' export { sanitizeEditorConfig, sanitizeFeatures } from './field/lexical/config/sanitize' export { getEnabledNodes } from './field/lexical/nodes' -export { ToolbarButton } from './field/lexical/plugins/FloatingSelectToolbar/ToolbarButton' -export { ToolbarDropdown } from './field/lexical/plugins/FloatingSelectToolbar/ToolbarDropdown/index' export { type FloatingToolbarSection, type FloatingToolbarSectionEntry, @@ -324,8 +334,7 @@ export { ENABLE_SLASH_MENU_COMMAND } from './field/lexical/plugins/SlashMenu/Lex // export SanitizedEditorConfig export type { EditorConfig, SanitizedEditorConfig } export type { AdapterProps } -export { RichTextCell } -export { RichTextField } + export { SlashMenuGroup, SlashMenuOption, diff --git a/packages/payload/scripts/exportPointerFiles.ts b/scripts/exportPointerFiles.ts similarity index 89% rename from packages/payload/scripts/exportPointerFiles.ts rename to scripts/exportPointerFiles.ts index ce1676a40..acda554ce 100644 --- a/packages/payload/scripts/exportPointerFiles.ts +++ b/scripts/exportPointerFiles.ts @@ -1,13 +1,16 @@ const fs = require('fs') const path = require('path') +const [baseDirRelativePath] = process.argv.slice(2) +const [sourceDirRelativePath] = process.argv.slice(3) + // Base directory -const baseDir = path.resolve(__dirname, '..') -const sourceDir = path.join(baseDir, 'dist', 'exports') +const baseDir = path.resolve(__dirname, baseDirRelativePath) +const sourceDir = path.join(baseDir, sourceDirRelativePath) const targetDir = baseDir // Helper function to read directories recursively and exclude .map files -function getFiles (dir: string): string[] { +function getFiles(dir: string): string[] { const subDirs = fs.readdirSync(dir, { withFileTypes: true }) const files = subDirs.map((dirEntry) => { const res = path.resolve(dir, dirEntry.name) @@ -21,7 +24,7 @@ function getFiles (dir: string): string[] { return Array.prototype.concat(...files) } -function fixImports (fileExtension: string, content: string, depth: number): string { +function fixImports(fileExtension: string, content: string, depth: number): string { const parentDirReference = '../'.repeat(depth + 1) // +1 to account for the original reference const replacementPrefix = (depth === 0 ? './' : '../'.repeat(depth)) + 'dist/'