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
This commit is contained in:
Alessio Gravili
2023-12-06 14:20:18 +01:00
committed by GitHub
parent 80ef18c149
commit 5de347ffff
88 changed files with 849 additions and 413 deletions

View File

@@ -26,7 +26,7 @@
} }
}, },
"scripts": { "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:components": "webpack --config dist/admin/components.config.js",
"build:swc": "swc ./src -d ./dist --config-file .swcrc", "build:swc": "swc ./src -d ./dist --config-file .swcrc",
"build:types": "tsc --emitDeclarationOnly --outDir dist", "build:types": "tsc --emitDeclarationOnly --outDir dist",

View File

@@ -1,13 +1,34 @@
import React from 'react' import React, { useMemo } from 'react'
import type { RichTextField } from '../../../../../fields/config/types' import type { RichTextField } from '../../../../../fields/config/types'
import type { RichTextAdapter } from './types' import type { RichTextAdapter } from './types'
const RichText: React.FC<RichTextField> = (fieldprops) => { const RichText: React.FC<RichTextField> = (fieldprops) => {
// eslint-disable-next-line react/destructuring-assignment // eslint-disable-next-line react/destructuring-assignment
const editor: RichTextAdapter = fieldprops.editor const editor: RichTextAdapter = fieldprops.editor
const { FieldComponent } = editor
return <FieldComponent {...fieldprops} /> const isLazy = 'LazyFieldComponent' in editor
const ImportedFieldComponent: React.FC<any> = useMemo(() => {
return isLazy
? React.lazy(() => {
return editor.LazyFieldComponent().then((resolvedComponent) => ({
default: resolvedComponent,
}))
})
: null
}, [editor, isLazy])
if (isLazy) {
return (
ImportedFieldComponent && (
<React.Suspense>
<ImportedFieldComponent {...fieldprops} />
</React.Suspense>
)
)
}
return <editor.FieldComponent {...fieldprops} />
} }
export default RichText export default RichText

View File

@@ -13,15 +13,11 @@ export type RichTextFieldProps<
path?: string path?: string
} }
export type RichTextAdapter< type RichTextAdapterBase<
Value extends object = object, Value extends object = object,
AdapterProps = any, AdapterProps = any,
ExtraFieldProperties = {}, ExtraFieldProperties = {},
> = { > = {
CellComponent: React.FC<
CellComponentProps<RichTextField<Value, AdapterProps, ExtraFieldProperties>>
>
FieldComponent: React.FC<RichTextFieldProps<Value, AdapterProps, ExtraFieldProperties>>
afterReadPromise?: ({ afterReadPromise?: ({
field, field,
incomingEditorState, incomingEditorState,
@@ -31,7 +27,6 @@ export type RichTextAdapter<
incomingEditorState: Value incomingEditorState: Value
siblingDoc: Record<string, unknown> siblingDoc: Record<string, unknown>
}) => Promise<void> | null }) => Promise<void> | null
outputSchema?: ({ outputSchema?: ({
field, field,
isRequired, isRequired,
@@ -59,3 +54,25 @@ export type RichTextAdapter<
RichTextField<Value, AdapterProps, ExtraFieldProperties> RichTextField<Value, AdapterProps, ExtraFieldProperties>
> >
} }
export type RichTextAdapter<
Value extends object = object,
AdapterProps = any,
ExtraFieldProperties = {},
> = RichTextAdapterBase<Value, AdapterProps, ExtraFieldProperties> &
(
| {
CellComponent: React.FC<
CellComponentProps<RichTextField<Value, AdapterProps, ExtraFieldProperties>>
>
FieldComponent: React.FC<RichTextFieldProps<Value, AdapterProps, ExtraFieldProperties>>
}
| {
LazyCellComponent: () => Promise<
React.FC<CellComponentProps<RichTextField<Value, AdapterProps, ExtraFieldProperties>>>
>
LazyFieldComponent: () => Promise<
React.FC<RichTextFieldProps<Value, AdapterProps, ExtraFieldProperties>>
>
}
)

View File

@@ -1,4 +1,4 @@
import React from 'react' import React, { useMemo } from 'react'
import type { RichTextField } from '../../../../../../../../fields/config/types' import type { RichTextField } from '../../../../../../../../fields/config/types'
import type { RichTextAdapter } from '../../../../../../forms/field-types/RichText/types' import type { RichTextAdapter } from '../../../../../../forms/field-types/RichText/types'
@@ -7,9 +7,30 @@ import type { CellComponentProps } from '../../types'
const RichTextCell: React.FC<CellComponentProps<RichTextField>> = (props) => { const RichTextCell: React.FC<CellComponentProps<RichTextField>> = (props) => {
// eslint-disable-next-line react/destructuring-assignment // eslint-disable-next-line react/destructuring-assignment
const editor: RichTextAdapter = props.field.editor const editor: RichTextAdapter = props.field.editor
const { CellComponent } = editor
return <CellComponent {...props} /> const isLazy = 'LazyCellComponent' in editor
const ImportedCellComponent: React.FC<any> = useMemo(() => {
return isLazy
? React.lazy(() => {
return editor.LazyCellComponent().then((resolvedComponent) => ({
default: resolvedComponent,
}))
})
: null
}, [editor, isLazy])
if (isLazy) {
return (
ImportedCellComponent && (
<React.Suspense>
<ImportedCellComponent {...props} />
</React.Suspense>
)
)
}
return <editor.CellComponent {...props} />
} }
export default RichTextCell export default RichTextCell

View File

@@ -1,7 +1,7 @@
import joi from 'joi' import joi from 'joi'
import { adminViewSchema } from './shared/adminViewSchema' 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()) const component = joi.alternatives().try(joi.object().unknown(), joi.func())
@@ -94,8 +94,10 @@ export default joi.object({
.object() .object()
.required() .required()
.keys({ .keys({
CellComponent: component.required(), CellComponent: componentSchema.optional(),
FieldComponent: component.required(), FieldComponent: componentSchema.optional(),
LazyCellComponent: joi.func().optional(),
LazyFieldComponent: joi.func().optional(),
afterReadPromise: joi.func().optional(), afterReadPromise: joi.func().optional(),
outputSchema: joi.func().optional(), outputSchema: joi.func().optional(),
populationPromise: joi.func().optional(), populationPromise: joi.func().optional(),

View File

@@ -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

View File

@@ -436,8 +436,10 @@ export const richText = baseField.keys({
editor: joi editor: joi
.object() .object()
.keys({ .keys({
CellComponent: componentSchema.required(), CellComponent: componentSchema.optional(),
FieldComponent: componentSchema.required(), FieldComponent: componentSchema.optional(),
LazyCellComponent: joi.func().optional(),
LazyFieldComponent: joi.func().optional(),
afterReadPromise: joi.func().optional(), afterReadPromise: joi.func().optional(),
outputSchema: joi.func().optional(), outputSchema: joi.func().optional(),
populationPromise: joi.func().optional(), populationPromise: joi.func().optional(),

4
packages/richtext-lexical/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
/utilities.d.ts
/utilities.js
/components.d.ts
/components.js

View File

@@ -9,7 +9,7 @@
"main": "./dist/index.js", "main": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"scripts": { "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:swc": "swc ./src -d ./dist --config-file .swcrc",
"build:types": "tsc --emitDeclarationOnly --outDir dist", "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", "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": { "exports": {
".": { ".": {
"default": "./src/index.ts", "import": "./src/index.ts",
"require": "./src/index.ts",
"types": "./src/index.ts" "types": "./src/index.ts"
},
"./*": {
"import": "./src/exports/*.ts",
"require": "./src/exports/*.ts",
"types": "./src/exports/*.ts"
} }
}, },
"publishConfig": { "publishConfig": {
@@ -62,6 +68,8 @@
"types": "./dist/index.d.ts" "types": "./dist/index.d.ts"
}, },
"files": [ "files": [
"dist" "dist",
"components.js",
"components.d.ts"
] ]
} }

View File

@@ -1,5 +1,6 @@
'use client' 'use client'
import type { SerializedEditorState } from 'lexical' import type { SerializedEditorState } from 'lexical'
import type { EditorConfig as LexicalEditorConfig } from 'lexical/LexicalEditor'
import type { CellComponentProps, RichTextField } from 'payload/types' import type { CellComponentProps, RichTextField } from 'payload/types'
import { createHeadlessEditor } from '@lexical/headless' import { createHeadlessEditor } from '@lexical/headless'
@@ -50,21 +51,23 @@ export const RichTextCell: React.FC<
return return
} }
// initialize headless editor editorConfig.lexical().then((lexicalConfig: LexicalEditorConfig) => {
const headlessEditor = createHeadlessEditor({ // initialize headless editor
namespace: editorConfig.lexical.namespace, const headlessEditor = createHeadlessEditor({
nodes: getEnabledNodes({ editorConfig }), namespace: lexicalConfig.namespace,
theme: editorConfig.lexical.theme, 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]) }, [data, editorConfig])
return <span>{preview}</span> return <span>{preview}</span>

View File

@@ -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'

View File

@@ -73,11 +73,11 @@ const RichText: React.FC<FieldProps> = (props) => {
<div className={`${baseClass}__wrap`}> <div className={`${baseClass}__wrap`}>
<Error message={errorMessage} showError={showError} /> <Error message={errorMessage} showError={showError} />
<Label htmlFor={`field-${path.replace(/\./g, '__')}`} label={label} required={required} /> <Label htmlFor={`field-${path.replace(/\./g, '__')}`} label={label} required={required} />
<ErrorBoundary fallbackRender={fallbackRender} onReset={(details) => {}}> <ErrorBoundary fallbackRender={fallbackRender} onReset={() => {}}>
<LexicalProvider <LexicalProvider
editorConfig={editorConfig} editorConfig={editorConfig}
fieldProps={props} fieldProps={props}
onChange={(editorState, editor, tags) => { onChange={(editorState) => {
let serializedEditorState = editorState.toJSON() let serializedEditorState = editorState.toJSON()
// Transform state through save hooks // Transform state through save hooks

View File

@@ -8,7 +8,6 @@ import type { HTMLConverter } from '../converters/html/converter/types'
import type { FeatureProvider } from '../types' import type { FeatureProvider } from '../types'
import { SlashMenuOption } from '../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/types' import { SlashMenuOption } from '../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/types'
import { BlockquoteIcon } from '../../lexical/ui/icons/Blockquote'
import { TextDropdownSectionWithEntries } from '../common/floatingSelectToolbarTextDropdownSection' import { TextDropdownSectionWithEntries } from '../common/floatingSelectToolbarTextDropdownSection'
import { convertLexicalNodesToHTML } from '../converters/html/converter' import { convertLexicalNodesToHTML } from '../converters/html/converter'
import { MarkdownTransformer } from './markdownTransformer' import { MarkdownTransformer } from './markdownTransformer'
@@ -21,8 +20,12 @@ export const BlockQuoteFeature = (): FeatureProvider => {
sections: [ sections: [
TextDropdownSectionWithEntries([ TextDropdownSectionWithEntries([
{ {
ChildComponent: BlockquoteIcon, ChildComponent: () =>
isActive: ({ editor, selection }) => false, // @ts-expect-error
import('../../lexical/ui/icons/Blockquote').then(
(module) => module.BlockquoteIcon,
),
isActive: () => false,
key: 'blockquote', key: 'blockquote',
label: `Blockquote`, label: `Blockquote`,
onClick: ({ editor }) => { onClick: ({ editor }) => {
@@ -70,7 +73,11 @@ export const BlockQuoteFeature = (): FeatureProvider => {
key: 'basic', key: 'basic',
options: [ options: [
new SlashMenuOption(`blockquote`, { new SlashMenuOption(`blockquote`, {
Icon: BlockquoteIcon, Icon: () =>
// @ts-expect-error
import('../../lexical/ui/icons/Blockquote').then(
(module) => module.BlockquoteIcon,
),
displayName: `Blockquote`, displayName: `Blockquote`,
keywords: ['quote', 'blockquote'], keywords: ['quote', 'blockquote'],
onSelect: () => { onSelect: () => {

View File

@@ -1 +0,0 @@
@import 'payload/scss';

View File

@@ -18,8 +18,7 @@ import type { BlocksFeatureProps } from '..'
import { useEditorConfigContext } from '../../../lexical/config/EditorConfigProvider' import { useEditorConfigContext } from '../../../lexical/config/EditorConfigProvider'
import { $createBlockNode } from '../nodes/BlocksNode' import { $createBlockNode } from '../nodes/BlocksNode'
import { INSERT_BLOCK_COMMAND } from '../plugin' import { INSERT_BLOCK_COMMAND } from '../plugin/commands'
import './index.scss'
const baseClass = 'lexical-blocks-drawer' const baseClass = 'lexical-blocks-drawer'
export const INSERT_BLOCK_WITH_DRAWER_COMMAND: LexicalCommand<{ export const INSERT_BLOCK_WITH_DRAWER_COMMAND: LexicalCommand<{
@@ -64,7 +63,7 @@ export const BlocksDrawerComponent: React.FC = () => {
const [replaceNodeKey, setReplaceNodeKey] = useState<null | string>(null) const [replaceNodeKey, setReplaceNodeKey] = useState<null | string>(null)
const editDepth = useEditDepth() const editDepth = useEditDepth()
const { t } = useTranslation('fields') const { t } = useTranslation('fields')
const { closeModal, openModal } = useModal() const { openModal } = useModal()
const labels = { const labels = {
plural: t('blocks') || 'Blocks', plural: t('blocks') || 'Blocks',

View File

@@ -1 +0,0 @@
@import 'payload/scss';

View File

@@ -6,10 +6,8 @@ import { formatLabels, getTranslation } from 'payload/utilities'
import type { FeatureProvider } from '../types' import type { FeatureProvider } from '../types'
import { SlashMenuOption } from '../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/types' import { SlashMenuOption } from '../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/types'
import { BlockIcon } from '../../lexical/ui/icons/Block'
import './index.scss'
import { BlockNode } from './nodes/BlocksNode' import { BlockNode } from './nodes/BlocksNode'
import { BlocksPlugin, INSERT_BLOCK_COMMAND } from './plugin' import { INSERT_BLOCK_COMMAND } from './plugin/commands'
import { blockPopulationPromiseHOC } from './populationPromise' import { blockPopulationPromiseHOC } from './populationPromise'
import { blockValidationHOC } from './validate' import { blockValidationHOC } from './validate'
@@ -43,7 +41,9 @@ export const BlocksFeature = (props?: BlocksFeatureProps): FeatureProvider => {
], ],
plugins: [ plugins: [
{ {
Component: BlocksPlugin, Component: () =>
// @ts-expect-error
import('./plugin').then((module) => module.BlocksPlugin),
position: 'normal', position: 'normal',
}, },
], ],
@@ -56,7 +56,9 @@ export const BlocksFeature = (props?: BlocksFeatureProps): FeatureProvider => {
options: [ options: [
...props.blocks.map((block) => { ...props.blocks.map((block) => {
return new SlashMenuOption('block-' + block.slug, { return new SlashMenuOption('block-' + block.slug, {
Icon: BlockIcon, Icon: () =>
// @ts-expect-error
import('../../lexical/ui/icons/Block').then((module) => module.BlockIcon),
displayName: ({ i18n }) => { displayName: ({ i18n }) => {
return getTranslation(block.labels.singular, i18n) return getTranslation(block.labels.singular, i18n)
}, },

View File

@@ -14,7 +14,6 @@ import { DecoratorBlockNode } from '@lexical/react/LexicalDecoratorBlockNode'
import ObjectID from 'bson-objectid' import ObjectID from 'bson-objectid'
import React from 'react' import React from 'react'
import { BlockComponent } from '../component'
import { transformInputFormData } from '../utils/transformInputFormData' import { transformInputFormData } from '../utils/transformInputFormData'
export type BlockFields = { export type BlockFields = {
@@ -25,6 +24,13 @@ export type BlockFields = {
id: string id: string
} }
const BlockComponent = React.lazy(() =>
// @ts-expect-error TypeScript being dumb
import('../component').then((module) => ({
default: module.BlockComponent,
})),
)
export type SerializedBlockNode = Spread< export type SerializedBlockNode = Spread<
{ {
fields: BlockFields fields: BlockFields

View File

@@ -0,0 +1,8 @@
import type { LexicalCommand } from 'lexical'
import { createCommand } from 'lexical'
import type { InsertBlockPayload } from './index'
export const INSERT_BLOCK_COMMAND: LexicalCommand<InsertBlockPayload> =
createCommand('INSERT_BLOCK_COMMAND')

View File

@@ -1,19 +1,17 @@
'use client' 'use client'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { $insertNodeToNearestRoot, mergeRegister } from '@lexical/utils' import { $insertNodeToNearestRoot, mergeRegister } from '@lexical/utils'
import { COMMAND_PRIORITY_EDITOR, type LexicalCommand, createCommand } from 'lexical' import { COMMAND_PRIORITY_EDITOR } from 'lexical'
import React, { useEffect } from 'react' import React, { useEffect } from 'react'
import type { BlockFields } from '../nodes/BlocksNode' import type { BlockFields } from '../nodes/BlocksNode'
import { BlocksDrawerComponent } from '../drawer' import { BlocksDrawerComponent } from '../drawer'
import { $createBlockNode, BlockNode } from '../nodes/BlocksNode' import { $createBlockNode, BlockNode } from '../nodes/BlocksNode'
import { INSERT_BLOCK_COMMAND } from './commands'
export type InsertBlockPayload = Exclude<BlockFields, 'id'> export type InsertBlockPayload = Exclude<BlockFields, 'id'>
export const INSERT_BLOCK_COMMAND: LexicalCommand<InsertBlockPayload> =
createCommand('INSERT_BLOCK_COMMAND')
export function BlocksPlugin(): JSX.Element | null { export function BlocksPlugin(): JSX.Element | null {
const [editor] = useLexicalComposerContext() const [editor] = useLexicalComposerContext()

View File

@@ -1,5 +1,4 @@
import type { HeadingTagType, SerializedHeadingNode } from '@lexical/rich-text' import type { HeadingTagType, SerializedHeadingNode } from '@lexical/rich-text'
import type React from 'react'
import { $createHeadingNode, HeadingNode } from '@lexical/rich-text' import { $createHeadingNode, HeadingNode } from '@lexical/rich-text'
import { $setBlocksType } from '@lexical/selection' import { $setBlocksType } from '@lexical/selection'
@@ -9,12 +8,6 @@ import type { HTMLConverter } from '../converters/html/converter/types'
import type { FeatureProvider } from '../types' import type { FeatureProvider } from '../types'
import { SlashMenuOption } from '../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/types' import { SlashMenuOption } from '../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/types'
import { H1Icon } from '../../lexical/ui/icons/H1'
import { H2Icon } from '../../lexical/ui/icons/H2'
import { H3Icon } from '../../lexical/ui/icons/H3'
import { H4Icon } from '../../lexical/ui/icons/H4'
import { H5Icon } from '../../lexical/ui/icons/H5'
import { H6Icon } from '../../lexical/ui/icons/H6'
import { TextDropdownSectionWithEntries } from '../common/floatingSelectToolbarTextDropdownSection' import { TextDropdownSectionWithEntries } from '../common/floatingSelectToolbarTextDropdownSection'
import { convertLexicalNodesToHTML } from '../converters/html/converter' import { convertLexicalNodesToHTML } from '../converters/html/converter'
import { MarkdownTransformer } from './markdownTransformer' import { MarkdownTransformer } from './markdownTransformer'
@@ -30,13 +23,19 @@ type Props = {
enabledHeadingSizes?: HeadingTagType[] enabledHeadingSizes?: HeadingTagType[]
} }
const HeadingToIconMap: Record<HeadingTagType, React.FC> = { const iconImports = {
h1: H1Icon, // @ts-expect-error
h2: H2Icon, h1: () => import('../../lexical/ui/icons/H1').then((module) => module.H1Icon),
h3: H3Icon, // @ts-expect-error
h4: H4Icon, h2: () => import('../../lexical/ui/icons/H2').then((module) => module.H2Icon),
h5: H5Icon, // @ts-expect-error
h6: H6Icon, h3: () => import('../../lexical/ui/icons/H3').then((module) => module.H3Icon),
// @ts-expect-error
h4: () => import('../../lexical/ui/icons/H4').then((module) => module.H4Icon),
// @ts-expect-error
h5: () => import('../../lexical/ui/icons/H5').then((module) => module.H5Icon),
// @ts-expect-error
h6: () => import('../../lexical/ui/icons/H6').then((module) => module.H6Icon),
} }
export const HeadingFeature = (props: Props): FeatureProvider => { export const HeadingFeature = (props: Props): FeatureProvider => {
@@ -50,7 +49,7 @@ export const HeadingFeature = (props: Props): FeatureProvider => {
...enabledHeadingSizes.map((headingSize, i) => ...enabledHeadingSizes.map((headingSize, i) =>
TextDropdownSectionWithEntries([ TextDropdownSectionWithEntries([
{ {
ChildComponent: HeadingToIconMap[headingSize], ChildComponent: iconImports[headingSize],
isActive: () => false, isActive: () => false,
key: headingSize, key: headingSize,
label: `Heading ${headingSize.charAt(1)}`, label: `Heading ${headingSize.charAt(1)}`,
@@ -98,7 +97,7 @@ export const HeadingFeature = (props: Props): FeatureProvider => {
key: 'basic', key: 'basic',
options: [ options: [
new SlashMenuOption(`heading-${headingSize.charAt(1)}`, { new SlashMenuOption(`heading-${headingSize.charAt(1)}`, {
Icon: HeadingToIconMap[headingSize], Icon: iconImports[headingSize],
displayName: `Heading ${headingSize.charAt(1)}`, displayName: `Heading ${headingSize.charAt(1)}`,
keywords: ['heading', headingSize], keywords: ['heading', headingSize],
onSelect: () => { onSelect: () => {

View File

@@ -1 +0,0 @@
@import 'payload/scss';

View File

@@ -4,25 +4,18 @@ import type { Field } from 'payload/types'
import { $findMatchingParent } from '@lexical/utils' import { $findMatchingParent } from '@lexical/utils'
import { $getSelection, $isRangeSelection } from 'lexical' import { $getSelection, $isRangeSelection } from 'lexical'
import { withMergedProps } from 'payload/utilities'
import type { HTMLConverter } from '../converters/html/converter/types' import type { HTMLConverter } from '../converters/html/converter/types'
import type { FeatureProvider } from '../types' import type { FeatureProvider } from '../types'
import type { SerializedAutoLinkNode } from './nodes/AutoLinkNode' import type { SerializedAutoLinkNode } from './nodes/AutoLinkNode'
import type { LinkFields, SerializedLinkNode } from './nodes/LinkNode' import type { LinkFields, SerializedLinkNode } from './nodes/LinkNode'
import { LinkIcon } from '../../lexical/ui/icons/Link'
import { getSelectedNode } from '../../lexical/utils/getSelectedNode' import { getSelectedNode } from '../../lexical/utils/getSelectedNode'
import { FeaturesSectionWithEntries } from '../common/floatingSelectToolbarFeaturesButtonsSection' import { FeaturesSectionWithEntries } from '../common/floatingSelectToolbarFeaturesButtonsSection'
import { convertLexicalNodesToHTML } from '../converters/html/converter' import { convertLexicalNodesToHTML } from '../converters/html/converter'
import './index.scss'
import { AutoLinkNode } from './nodes/AutoLinkNode' import { AutoLinkNode } from './nodes/AutoLinkNode'
import { $isLinkNode, LinkNode, TOGGLE_LINK_COMMAND } from './nodes/LinkNode' import { $isLinkNode, LinkNode, TOGGLE_LINK_COMMAND } from './nodes/LinkNode'
import { AutoLinkPlugin } from './plugins/autoLink' import { TOGGLE_LINK_WITH_MODAL_COMMAND } from './plugins/floatingLinkEditor/LinkEditor/commands'
import { ClickableLinkPlugin } from './plugins/clickableLink'
import { FloatingLinkEditorPlugin } from './plugins/floatingLinkEditor'
import { TOGGLE_LINK_WITH_MODAL_COMMAND } from './plugins/floatingLinkEditor/LinkEditor'
import { LinkPlugin } from './plugins/link'
import { linkPopulationPromiseHOC } from './populationPromise' import { linkPopulationPromiseHOC } from './populationPromise'
export type LinkFeatureProps = { export type LinkFeatureProps = {
@@ -38,7 +31,9 @@ export const LinkFeature = (props: LinkFeatureProps): FeatureProvider => {
sections: [ sections: [
FeaturesSectionWithEntries([ FeaturesSectionWithEntries([
{ {
ChildComponent: LinkIcon, ChildComponent: () =>
// @ts-expect-error
import('../../lexical/ui/icons/Link').then((module) => module.LinkIcon),
isActive: ({ selection }) => { isActive: ({ selection }) => {
if ($isRangeSelection(selection)) { if ($isRangeSelection(selection)) {
const selectedNode = getSelectedNode(selection) const selectedNode = getSelectedNode(selection)
@@ -134,22 +129,35 @@ export const LinkFeature = (props: LinkFeatureProps): FeatureProvider => {
], ],
plugins: [ plugins: [
{ {
Component: LinkPlugin, Component: () =>
// @ts-expect-error
import('./plugins/link').then((module) => module.LinkPlugin),
position: 'normal', position: 'normal',
}, },
{ {
Component: AutoLinkPlugin, Component: () =>
// @ts-expect-error
import('./plugins/autoLink').then((module) => module.AutoLinkPlugin),
position: 'normal', position: 'normal',
}, },
{ {
Component: ClickableLinkPlugin, Component: () =>
// @ts-expect-error
import('./plugins/clickableLink').then((module) => module.ClickableLinkPlugin),
position: 'normal', position: 'normal',
}, },
{ {
Component: withMergedProps({ Component: () =>
Component: FloatingLinkEditorPlugin, // @ts-expect-error
toMergeIntoProps: props, import('./plugins/floatingLinkEditor').then((module) => {
}), const floatingLinkEditorPlugin = module.FloatingLinkEditorPlugin
return import('payload/utilities').then((module) =>
module.withMergedProps({
Component: floatingLinkEditorPlugin,
toMergeIntoProps: props,
}),
)
}),
position: 'floatingAnchorElem', position: 'floatingAnchorElem',
}, },
], ],

View File

@@ -0,0 +1,9 @@
import type { LexicalCommand } from 'lexical'
import { createCommand } from 'lexical'
import type { LinkPayload } from '../types'
export const TOGGLE_LINK_WITH_MODAL_COMMAND: LexicalCommand<LinkPayload | null> = createCommand(
'TOGGLE_LINK_WITH_MODAL_COMMAND',
)

View File

@@ -1,5 +1,4 @@
'use client' 'use client'
import type { LexicalCommand } from 'lexical'
import type { Data, Fields } from 'payload/types' import type { Data, Fields } from 'payload/types'
import { useModal } from '@faceless-ui/modal' import { useModal } from '@faceless-ui/modal'
@@ -12,7 +11,6 @@ import {
COMMAND_PRIORITY_LOW, COMMAND_PRIORITY_LOW,
KEY_ESCAPE_COMMAND, KEY_ESCAPE_COMMAND,
SELECTION_CHANGE_COMMAND, SELECTION_CHANGE_COMMAND,
createCommand,
} from 'lexical' } from 'lexical'
import { formatDrawerSlug } from 'payload/components/elements' import { formatDrawerSlug } from 'payload/components/elements'
import { import {
@@ -38,10 +36,7 @@ import { setFloatingElemPositionForLinkEditor } from '../../../../../lexical/uti
import { LinkDrawer } from '../../../drawer' import { LinkDrawer } from '../../../drawer'
import { $isLinkNode, TOGGLE_LINK_COMMAND } from '../../../nodes/LinkNode' import { $isLinkNode, TOGGLE_LINK_COMMAND } from '../../../nodes/LinkNode'
import { transformExtraFields } from '../utilities' import { transformExtraFields } from '../utilities'
import { TOGGLE_LINK_WITH_MODAL_COMMAND } from './commands'
export const TOGGLE_LINK_WITH_MODAL_COMMAND: LexicalCommand<LinkPayload | null> = createCommand(
'TOGGLE_LINK_WITH_MODAL_COMMAND',
)
export function LinkEditor({ export function LinkEditor({
anchorElem, anchorElem,

View File

@@ -4,19 +4,20 @@ import { $createParagraphNode, $getSelection, $isRangeSelection } from 'lexical'
import type { FeatureProvider } from '../types' import type { FeatureProvider } from '../types'
import { SlashMenuOption } from '../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/types' import { SlashMenuOption } from '../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/types'
import { TextIcon } from '../../lexical/ui/icons/Text'
import { TextDropdownSectionWithEntries } from '../common/floatingSelectToolbarTextDropdownSection' import { TextDropdownSectionWithEntries } from '../common/floatingSelectToolbarTextDropdownSection'
export const ParagraphFeature = (): FeatureProvider => { export const ParagraphFeature = (): FeatureProvider => {
return { return {
feature: ({ resolvedFeatures, unsanitizedEditorConfig }) => { feature: () => {
return { return {
floatingSelectToolbar: { floatingSelectToolbar: {
sections: [ sections: [
TextDropdownSectionWithEntries([ TextDropdownSectionWithEntries([
{ {
ChildComponent: TextIcon, ChildComponent: () =>
isActive: ({ editor, selection }) => false, // @ts-expect-error
import('../../lexical/ui/icons/Text').then((module) => module.TextIcon),
isActive: () => false,
key: 'normal-text', key: 'normal-text',
label: 'Normal Text', label: 'Normal Text',
onClick: ({ editor }) => { onClick: ({ editor }) => {
@@ -40,7 +41,9 @@ export const ParagraphFeature = (): FeatureProvider => {
key: 'basic', key: 'basic',
options: [ options: [
new SlashMenuOption('paragraph', { new SlashMenuOption('paragraph', {
Icon: TextIcon, Icon: () =>
// @ts-expect-error
import('../../lexical/ui/icons/Text').then((module) => module.TextIcon),
displayName: 'Paragraph', displayName: 'Paragraph',
keywords: ['normal', 'paragraph', 'p', 'text'], keywords: ['normal', 'paragraph', 'p', 'text'],
onSelect: ({ editor }) => { onSelect: ({ editor }) => {

View File

@@ -0,0 +1,7 @@
import type { LexicalCommand } from 'lexical'
import { createCommand } from 'lexical'
export const INSERT_RELATIONSHIP_WITH_DRAWER_COMMAND: LexicalCommand<{
replace: { nodeKey: string } | false
}> = createCommand('INSERT_RELATIONSHIP_WITH_DRAWER_COMMAND')

View File

@@ -1 +0,0 @@
@import 'payload/scss';

View File

@@ -1,26 +1,16 @@
'use client' 'use client'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { import { $getNodeByKey, COMMAND_PRIORITY_EDITOR, type LexicalEditor } from 'lexical'
$getNodeByKey,
COMMAND_PRIORITY_EDITOR,
type LexicalCommand,
type LexicalEditor,
createCommand,
} from 'lexical'
import { useListDrawer } from 'payload/components/elements' import { useListDrawer } from 'payload/components/elements'
import React, { useCallback, useEffect, useState } from 'react' import React, { useCallback, useEffect, useState } from 'react'
import { $createRelationshipNode } from '../nodes/RelationshipNode' import { $createRelationshipNode } from '../nodes/RelationshipNode'
import { INSERT_RELATIONSHIP_COMMAND } from '../plugins' import { INSERT_RELATIONSHIP_COMMAND } from '../plugins'
import { EnabledRelationshipsCondition } from '../utils/EnabledRelationshipsCondition' import { EnabledRelationshipsCondition } from '../utils/EnabledRelationshipsCondition'
import './index.scss' import { INSERT_RELATIONSHIP_WITH_DRAWER_COMMAND } from './commands'
const baseClass = 'lexical-relationship-drawer' const baseClass = 'lexical-relationship-drawer'
export const INSERT_RELATIONSHIP_WITH_DRAWER_COMMAND: LexicalCommand<{
replace: { nodeKey: string } | false
}> = createCommand('INSERT_RELATIONSHIP_WITH_DRAWER_COMMAND')
const insertRelationship = ({ const insertRelationship = ({
id, id,
editor, editor,

View File

@@ -1 +0,0 @@
@import 'payload/scss';

View File

@@ -1,11 +1,8 @@
import type { FeatureProvider } from '../types' import type { FeatureProvider } from '../types'
import { SlashMenuOption } from '../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/types' import { SlashMenuOption } from '../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/types'
import { RelationshipIcon } from '../../lexical/ui/icons/Relationship' import { INSERT_RELATIONSHIP_WITH_DRAWER_COMMAND } from './drawer/commands'
import { INSERT_RELATIONSHIP_WITH_DRAWER_COMMAND } from './drawer'
import './index.scss'
import { RelationshipNode } from './nodes/RelationshipNode' import { RelationshipNode } from './nodes/RelationshipNode'
import RelationshipPlugin from './plugins'
import { relationshipPopulationPromise } from './populationPromise' import { relationshipPopulationPromise } from './populationPromise'
export const RelationshipFeature = (): FeatureProvider => { export const RelationshipFeature = (): FeatureProvider => {
@@ -22,7 +19,9 @@ export const RelationshipFeature = (): FeatureProvider => {
], ],
plugins: [ plugins: [
{ {
Component: RelationshipPlugin, Component: () =>
// @ts-expect-error
import('./plugins').then((module) => module.RelationshipPlugin),
position: 'normal', position: 'normal',
}, },
], ],
@@ -34,7 +33,11 @@ export const RelationshipFeature = (): FeatureProvider => {
key: 'basic', key: 'basic',
options: [ options: [
new SlashMenuOption('relationship', { new SlashMenuOption('relationship', {
Icon: RelationshipIcon, Icon: () =>
// @ts-expect-error
import('../../lexical/ui/icons/Relationship').then(
(module) => module.RelationshipIcon,
),
displayName: 'Relationship', displayName: 'Relationship',
keywords: ['relationship', 'relation', 'rel'], keywords: ['relationship', 'relation', 'rel'],
onSelect: ({ editor }) => { onSelect: ({ editor }) => {

View File

@@ -16,7 +16,12 @@ import {
} from '@lexical/react/LexicalDecoratorBlockNode' } from '@lexical/react/LexicalDecoratorBlockNode'
import * as React from 'react' import * as React from 'react'
import { RelationshipComponent } from './components/RelationshipComponent' const RelationshipComponent = React.lazy(() =>
// @ts-expect-error TypeScript being dumb
import('./components/RelationshipComponent').then((module) => ({
default: module.RelationshipComponent,
})),
)
export type RelationshipData = { export type RelationshipData = {
relationTo: string relationTo: string

View File

@@ -13,7 +13,7 @@ import { useTranslation } from 'react-i18next'
import type { RelationshipData } from '../RelationshipNode' import type { RelationshipData } from '../RelationshipNode'
import { useEditorConfigContext } from '../../../../lexical/config/EditorConfigProvider' import { useEditorConfigContext } from '../../../../lexical/config/EditorConfigProvider'
import { INSERT_RELATIONSHIP_WITH_DRAWER_COMMAND } from '../../drawer' import { INSERT_RELATIONSHIP_WITH_DRAWER_COMMAND } from '../../drawer/commands'
import { EnabledRelationshipsCondition } from '../../utils/EnabledRelationshipsCondition' import { EnabledRelationshipsCondition } from '../../utils/EnabledRelationshipsCondition'
import './index.scss' import './index.scss'

View File

@@ -15,7 +15,7 @@ export const INSERT_RELATIONSHIP_COMMAND: LexicalCommand<RelationshipData> = cre
'INSERT_RELATIONSHIP_COMMAND', 'INSERT_RELATIONSHIP_COMMAND',
) )
export default function RelationshipPlugin(): JSX.Element | null { export function RelationshipPlugin(): JSX.Element | null {
const [editor] = useLexicalComposerContext() const [editor] = useLexicalComposerContext()
const { collections } = useConfig() const { collections } = useConfig()

View File

@@ -18,7 +18,7 @@ import type { UploadData } from '../nodes/UploadNode'
import { useEditorConfigContext } from '../../../lexical/config/EditorConfigProvider' import { useEditorConfigContext } from '../../../lexical/config/EditorConfigProvider'
import { EnabledRelationshipsCondition } from '../../Relationship/utils/EnabledRelationshipsCondition' import { EnabledRelationshipsCondition } from '../../Relationship/utils/EnabledRelationshipsCondition'
import { INSERT_UPLOAD_WITH_DRAWER_COMMAND } from '../drawer' import { INSERT_UPLOAD_WITH_DRAWER_COMMAND } from '../drawer/commands'
import { ExtraFieldsUploadDrawer } from './ExtraFieldsDrawer' import { ExtraFieldsUploadDrawer } from './ExtraFieldsDrawer'
import './index.scss' import './index.scss'

View File

@@ -0,0 +1,7 @@
import type { LexicalCommand } from 'lexical'
import { createCommand } from 'lexical'
export const INSERT_UPLOAD_WITH_DRAWER_COMMAND: LexicalCommand<{
replace: { nodeKey: string } | false
}> = createCommand('INSERT_UPLOAD_WITH_DRAWER_COMMAND')

View File

@@ -1 +0,0 @@
@import 'payload/scss';

View File

@@ -1,26 +1,16 @@
'use client' 'use client'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { import { $getNodeByKey, COMMAND_PRIORITY_EDITOR, type LexicalEditor } from 'lexical'
$getNodeByKey,
COMMAND_PRIORITY_EDITOR,
type LexicalCommand,
type LexicalEditor,
createCommand,
} from 'lexical'
import { useListDrawer } from 'payload/components/elements' import { useListDrawer } from 'payload/components/elements'
import React, { useCallback, useEffect, useState } from 'react' import React, { useCallback, useEffect, useState } from 'react'
import { EnabledRelationshipsCondition } from '../../Relationship/utils/EnabledRelationshipsCondition' import { EnabledRelationshipsCondition } from '../../Relationship/utils/EnabledRelationshipsCondition'
import { $createUploadNode } from '../nodes/UploadNode' import { $createUploadNode } from '../nodes/UploadNode'
import { INSERT_UPLOAD_COMMAND } from '../plugin' import { INSERT_UPLOAD_COMMAND } from '../plugin'
import './index.scss' import { INSERT_UPLOAD_WITH_DRAWER_COMMAND } from './commands'
const baseClass = 'lexical-upload-drawer' const baseClass = 'lexical-upload-drawer'
export const INSERT_UPLOAD_WITH_DRAWER_COMMAND: LexicalCommand<{
replace: { nodeKey: string } | false
}> = createCommand('INSERT_UPLOAD_WITH_DRAWER_COMMAND')
const insertUpload = ({ const insertUpload = ({
id, id,
editor, editor,

View File

@@ -1 +0,0 @@
@import 'payload/scss';

View File

@@ -7,11 +7,8 @@ import type { FeatureProvider } from '../types'
import type { SerializedUploadNode } from './nodes/UploadNode' import type { SerializedUploadNode } from './nodes/UploadNode'
import { SlashMenuOption } from '../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/types' import { SlashMenuOption } from '../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/types'
import { UploadIcon } from '../../lexical/ui/icons/Upload' import { INSERT_UPLOAD_WITH_DRAWER_COMMAND } from './drawer/commands'
import { INSERT_UPLOAD_WITH_DRAWER_COMMAND } from './drawer'
import './index.scss'
import { UploadNode } from './nodes/UploadNode' import { UploadNode } from './nodes/UploadNode'
import { UploadPlugin } from './plugin'
import { uploadPopulationPromiseHOC } from './populationPromise' import { uploadPopulationPromiseHOC } from './populationPromise'
import { uploadValidation } from './validate' import { uploadValidation } from './validate'
@@ -55,7 +52,9 @@ export const UploadFeature = (props?: UploadFeatureProps): FeatureProvider => {
], ],
plugins: [ plugins: [
{ {
Component: UploadPlugin, Component: () =>
// @ts-expect-error
import('./plugin').then((module) => module.UploadPlugin),
position: 'normal', position: 'normal',
}, },
], ],
@@ -67,7 +66,9 @@ export const UploadFeature = (props?: UploadFeatureProps): FeatureProvider => {
key: 'basic', key: 'basic',
options: [ options: [
new SlashMenuOption('upload', { new SlashMenuOption('upload', {
Icon: UploadIcon, Icon: () =>
// @ts-expect-error
import('../../lexical/ui/icons/Upload').then((module) => module.UploadIcon),
displayName: 'Upload', displayName: 'Upload',
keywords: ['upload', 'image', 'file', 'img', 'picture', 'photo', 'media'], keywords: ['upload', 'image', 'file', 'img', 'picture', 'photo', 'media'],
onSelect: ({ editor }) => { onSelect: ({ editor }) => {

View File

@@ -3,14 +3,13 @@ import type {
FloatingToolbarSectionEntry, FloatingToolbarSectionEntry,
} from '../../lexical/plugins/FloatingSelectToolbar/types' } from '../../lexical/plugins/FloatingSelectToolbar/types'
import { AlignLeftIcon } from '../../lexical/ui/icons/AlignLeft'
import './index.scss'
export const AlignDropdownSectionWithEntries = ( export const AlignDropdownSectionWithEntries = (
entries: FloatingToolbarSectionEntry[], entries: FloatingToolbarSectionEntry[],
): FloatingToolbarSection => { ): FloatingToolbarSection => {
return { return {
ChildComponent: AlignLeftIcon, ChildComponent: () =>
// @ts-expect-error
import('../../lexical/ui/icons/AlignLeft').then((module) => module.AlignLeftIcon),
entries, entries,
key: 'dropdown-align', key: 'dropdown-align',
order: 2, order: 2,

View File

@@ -1,4 +0,0 @@
.floating-select-toolbar-popup__section-dropdown-align {
display: flex;
gap: 2px;
}

View File

@@ -2,21 +2,20 @@ import { FORMAT_ELEMENT_COMMAND } from 'lexical'
import type { FeatureProvider } from '../types' import type { FeatureProvider } from '../types'
import { AlignCenterIcon } from '../../lexical/ui/icons/AlignCenter'
import { AlignLeftIcon } from '../../lexical/ui/icons/AlignLeft'
import { AlignDropdownSectionWithEntries } from './floatingSelectToolbarAlignDropdownSection' import { AlignDropdownSectionWithEntries } from './floatingSelectToolbarAlignDropdownSection'
import './index.scss'
export const AlignFeature = (): FeatureProvider => { export const AlignFeature = (): FeatureProvider => {
return { return {
feature: ({ resolvedFeatures, unsanitizedEditorConfig }) => { feature: () => {
return { return {
floatingSelectToolbar: { floatingSelectToolbar: {
sections: [ sections: [
AlignDropdownSectionWithEntries([ AlignDropdownSectionWithEntries([
{ {
ChildComponent: AlignLeftIcon, ChildComponent: () =>
isActive: ({ editor, selection }) => false, // @ts-expect-error
import('../../lexical/ui/icons/AlignLeft').then((module) => module.AlignLeftIcon),
isActive: () => false,
key: 'align-left', key: 'align-left',
label: `Align Left`, label: `Align Left`,
onClick: ({ editor }) => { onClick: ({ editor }) => {
@@ -27,8 +26,12 @@ export const AlignFeature = (): FeatureProvider => {
]), ]),
AlignDropdownSectionWithEntries([ AlignDropdownSectionWithEntries([
{ {
ChildComponent: AlignCenterIcon, ChildComponent: () =>
isActive: ({ editor, selection }) => false, // @ts-expect-error
import('../../lexical/ui/icons/AlignCenter').then(
(module) => module.AlignCenterIcon,
),
isActive: () => false,
key: 'align-center', key: 'align-center',
label: `Align Center`, label: `Align Center`,
onClick: ({ editor }) => { onClick: ({ editor }) => {
@@ -39,8 +42,12 @@ export const AlignFeature = (): FeatureProvider => {
]), ]),
AlignDropdownSectionWithEntries([ AlignDropdownSectionWithEntries([
{ {
ChildComponent: AlignLeftIcon, ChildComponent: () =>
isActive: ({ editor, selection }) => false, // @ts-expect-error
import('../../lexical/ui/icons/AlignRight').then(
(module) => module.AlignRightIcon,
),
isActive: () => false,
key: 'align-right', key: 'align-right',
label: `Align Right`, label: `Align Right`,
onClick: ({ editor }) => { onClick: ({ editor }) => {

View File

@@ -1,4 +0,0 @@
.floating-select-toolbar-popup__section-features {
display: flex;
gap: 2px;
}

View File

@@ -3,8 +3,6 @@ import type {
FloatingToolbarSectionEntry, FloatingToolbarSectionEntry,
} from '../../../lexical/plugins/FloatingSelectToolbar/types' } from '../../../lexical/plugins/FloatingSelectToolbar/types'
import './index.scss'
export const FeaturesSectionWithEntries = ( export const FeaturesSectionWithEntries = (
entries: FloatingToolbarSectionEntry[], entries: FloatingToolbarSectionEntry[],
): FloatingToolbarSection => { ): FloatingToolbarSection => {

View File

@@ -1,4 +0,0 @@
.floating-select-toolbar-popup__section-dropdown-text {
display: flex;
gap: 2px;
}

View File

@@ -3,14 +3,13 @@ import type {
FloatingToolbarSectionEntry, FloatingToolbarSectionEntry,
} from '../../../lexical/plugins/FloatingSelectToolbar/types' } from '../../../lexical/plugins/FloatingSelectToolbar/types'
import { TextIcon } from '../../../lexical/ui/icons/Text'
import './index.scss'
export const TextDropdownSectionWithEntries = ( export const TextDropdownSectionWithEntries = (
entries: FloatingToolbarSectionEntry[], entries: FloatingToolbarSectionEntry[],
): FloatingToolbarSection => { ): FloatingToolbarSection => {
return { return {
ChildComponent: TextIcon, ChildComponent: () =>
// @ts-expect-error
import('../../../lexical/ui/icons/Text').then((module) => module.TextIcon),
entries, entries,
key: 'dropdown-text', key: 'dropdown-text',
order: 1, order: 1,

View File

@@ -15,10 +15,6 @@ export const HTMLConverterFeature = (props?: HTMLConverterFeatureProps): Feature
if (!props) { if (!props) {
props = {} props = {}
} }
/*const defaultConvertersWithConvertersFromFeatures = defaultConverters
defaultConvertersWithConver tersFromFeatures.set(props?
*/
return { return {
feature: () => { feature: () => {

View File

@@ -1,14 +1,14 @@
import type { FeatureProvider } from '../../types' import type { FeatureProvider } from '../../types'
import { TestRecorderPlugin } from './plugin'
export const TestRecorderFeature = (): FeatureProvider => { export const TestRecorderFeature = (): FeatureProvider => {
return { return {
feature: () => { feature: () => {
return { return {
plugins: [ plugins: [
{ {
Component: TestRecorderPlugin, Component: () =>
// @ts-expect-error
import('./plugin').then((module) => module.TestRecorderPlugin),
position: 'bottom', position: 'bottom',
}, },
], ],

View File

@@ -1,15 +1,14 @@
import type { FeatureProvider } from '../../types' import type { FeatureProvider } from '../../types'
import './index.scss'
import { TreeViewPlugin } from './plugin'
export const TreeviewFeature = (): FeatureProvider => { export const TreeviewFeature = (): FeatureProvider => {
return { return {
feature: ({ resolvedFeatures, unsanitizedEditorConfig }) => { feature: () => {
return { return {
plugins: [ plugins: [
{ {
Component: TreeViewPlugin, Component: () =>
// @ts-expect-error
import('./plugin').then((module) => module.TreeViewPlugin),
position: 'bottom', position: 'bottom',
}, },
], ],

View File

@@ -3,6 +3,8 @@ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext
import { TreeView } from '@lexical/react/LexicalTreeView' import { TreeView } from '@lexical/react/LexicalTreeView'
import * as React from 'react' import * as React from 'react'
import './index.scss'
export function TreeViewPlugin(): JSX.Element { export function TreeViewPlugin(): JSX.Element {
const [editor] = useLexicalComposerContext() const [editor] = useLexicalComposerContext()
return ( return (

View File

@@ -2,7 +2,6 @@ import { $isRangeSelection, FORMAT_TEXT_COMMAND } from 'lexical'
import type { FeatureProvider } from '../../types' import type { FeatureProvider } from '../../types'
import { BoldIcon } from '../../../lexical/ui/icons/Bold'
import { SectionWithEntries } from '../common/floatingSelectToolbarSection' import { SectionWithEntries } from '../common/floatingSelectToolbarSection'
import { import {
BOLD_ITALIC_STAR, BOLD_ITALIC_STAR,
@@ -25,8 +24,10 @@ export const BoldTextFeature = (): FeatureProvider => {
sections: [ sections: [
SectionWithEntries([ SectionWithEntries([
{ {
ChildComponent: BoldIcon, ChildComponent: () =>
isActive: ({ editor, selection }) => { // @ts-expect-error
import('../../../lexical/ui/icons/Bold').then((module) => module.BoldIcon),
isActive: ({ selection }) => {
if ($isRangeSelection(selection)) { if ($isRangeSelection(selection)) {
return selection.hasFormat('bold') return selection.hasFormat('bold')
} }

View File

@@ -2,20 +2,21 @@ import { $isRangeSelection, FORMAT_TEXT_COMMAND } from 'lexical'
import type { FeatureProvider } from '../../types' import type { FeatureProvider } from '../../types'
import { CodeIcon } from '../../../lexical/ui/icons/Code'
import { SectionWithEntries } from '../common/floatingSelectToolbarSection' import { SectionWithEntries } from '../common/floatingSelectToolbarSection'
import { INLINE_CODE } from './markdownTransformers' import { INLINE_CODE } from './markdownTransformers'
export const InlineCodeTextFeature = (): FeatureProvider => { export const InlineCodeTextFeature = (): FeatureProvider => {
return { return {
feature: ({ featureProviderMap }) => { feature: () => {
return { return {
floatingSelectToolbar: { floatingSelectToolbar: {
sections: [ sections: [
SectionWithEntries([ SectionWithEntries([
{ {
ChildComponent: CodeIcon, ChildComponent: () =>
isActive: ({ editor, selection }) => { // @ts-expect-error
import('../../../lexical/ui/icons/Code').then((module) => module.CodeIcon),
isActive: ({ selection }) => {
if ($isRangeSelection(selection)) { if ($isRangeSelection(selection)) {
return selection.hasFormat('code') return selection.hasFormat('code')
} }

View File

@@ -2,20 +2,21 @@ import { $isRangeSelection, FORMAT_TEXT_COMMAND } from 'lexical'
import type { FeatureProvider } from '../../types' import type { FeatureProvider } from '../../types'
import { ItalicIcon } from '../../../lexical/ui/icons/Italic'
import { SectionWithEntries } from '../common/floatingSelectToolbarSection' import { SectionWithEntries } from '../common/floatingSelectToolbarSection'
import { ITALIC_STAR, ITALIC_UNDERSCORE } from './markdownTransformers' import { ITALIC_STAR, ITALIC_UNDERSCORE } from './markdownTransformers'
export const ItalicTextFeature = (): FeatureProvider => { export const ItalicTextFeature = (): FeatureProvider => {
return { return {
feature: ({ resolvedFeatures, unsanitizedEditorConfig }) => { feature: () => {
return { return {
floatingSelectToolbar: { floatingSelectToolbar: {
sections: [ sections: [
SectionWithEntries([ SectionWithEntries([
{ {
ChildComponent: ItalicIcon, ChildComponent: () =>
isActive: ({ editor, selection }) => { // @ts-expect-error
import('../../../lexical/ui/icons/Italic').then((module) => module.ItalicIcon),
isActive: ({ selection }) => {
if ($isRangeSelection(selection)) { if ($isRangeSelection(selection)) {
return selection.hasFormat('italic') return selection.hasFormat('italic')
} }

View File

@@ -3,8 +3,6 @@ import type {
FloatingToolbarSectionEntry, FloatingToolbarSectionEntry,
} from '../../../lexical/plugins/FloatingSelectToolbar/types' } from '../../../lexical/plugins/FloatingSelectToolbar/types'
import './index.scss'
export const SectionWithEntries = ( export const SectionWithEntries = (
entries: FloatingToolbarSectionEntry[], entries: FloatingToolbarSectionEntry[],
): FloatingToolbarSection => { ): FloatingToolbarSection => {

View File

@@ -1,4 +0,0 @@
.floating-select-toolbar-popup__section-format {
display: flex;
gap: 2px;
}

View File

@@ -2,20 +2,23 @@ import { $isRangeSelection, FORMAT_TEXT_COMMAND } from 'lexical'
import type { FeatureProvider } from '../../types' import type { FeatureProvider } from '../../types'
import { StrikethroughIcon } from '../../../lexical/ui/icons/Strikethrough'
import { SectionWithEntries } from '../common/floatingSelectToolbarSection' import { SectionWithEntries } from '../common/floatingSelectToolbarSection'
import { STRIKETHROUGH } from './markdownTransformers' import { STRIKETHROUGH } from './markdownTransformers'
export const StrikethroughTextFeature = (): FeatureProvider => { export const StrikethroughTextFeature = (): FeatureProvider => {
return { return {
feature: ({ resolvedFeatures, unsanitizedEditorConfig }) => { feature: () => {
return { return {
floatingSelectToolbar: { floatingSelectToolbar: {
sections: [ sections: [
SectionWithEntries([ SectionWithEntries([
{ {
ChildComponent: StrikethroughIcon, ChildComponent: () =>
isActive: ({ editor, selection }) => { // @ts-expect-error
import('../../../lexical/ui/icons/Strikethrough').then(
(module) => module.StrikethroughIcon,
),
isActive: ({ selection }) => {
if ($isRangeSelection(selection)) { if ($isRangeSelection(selection)) {
return selection.hasFormat('strikethrough') return selection.hasFormat('strikethrough')
} }

View File

@@ -2,19 +2,22 @@ import { $isRangeSelection, FORMAT_TEXT_COMMAND } from 'lexical'
import type { FeatureProvider } from '../../types' import type { FeatureProvider } from '../../types'
import { SubscriptIcon } from '../../../lexical/ui/icons/Subscript'
import { SectionWithEntries } from '../common/floatingSelectToolbarSection' import { SectionWithEntries } from '../common/floatingSelectToolbarSection'
export const SubscriptTextFeature = (): FeatureProvider => { export const SubscriptTextFeature = (): FeatureProvider => {
return { return {
feature: ({ resolvedFeatures, unsanitizedEditorConfig }) => { feature: () => {
return { return {
floatingSelectToolbar: { floatingSelectToolbar: {
sections: [ sections: [
SectionWithEntries([ SectionWithEntries([
{ {
ChildComponent: SubscriptIcon, ChildComponent: () =>
isActive: ({ editor, selection }) => { // @ts-expect-error
import('../../../lexical/ui/icons/Subscript').then(
(module) => module.SubscriptIcon,
),
isActive: ({ selection }) => {
if ($isRangeSelection(selection)) { if ($isRangeSelection(selection)) {
return selection.hasFormat('subscript') return selection.hasFormat('subscript')
} }

View File

@@ -2,19 +2,22 @@ import { $isRangeSelection, FORMAT_TEXT_COMMAND } from 'lexical'
import type { FeatureProvider } from '../../types' import type { FeatureProvider } from '../../types'
import { SuperscriptIcon } from '../../../lexical/ui/icons/Superscript'
import { SectionWithEntries } from '../common/floatingSelectToolbarSection' import { SectionWithEntries } from '../common/floatingSelectToolbarSection'
export const SuperscriptTextFeature = (): FeatureProvider => { export const SuperscriptTextFeature = (): FeatureProvider => {
return { return {
feature: ({ resolvedFeatures, unsanitizedEditorConfig }) => { feature: () => {
return { return {
floatingSelectToolbar: { floatingSelectToolbar: {
sections: [ sections: [
SectionWithEntries([ SectionWithEntries([
{ {
ChildComponent: SuperscriptIcon, ChildComponent: () =>
isActive: ({ editor, selection }) => { // @ts-expect-error
import('../../../lexical/ui/icons/Superscript').then(
(module) => module.SuperscriptIcon,
),
isActive: ({ selection }) => {
if ($isRangeSelection(selection)) { if ($isRangeSelection(selection)) {
return selection.hasFormat('superscript') return selection.hasFormat('superscript')
} }

View File

@@ -2,19 +2,22 @@ import { $isRangeSelection, FORMAT_TEXT_COMMAND } from 'lexical'
import type { FeatureProvider } from '../../types' import type { FeatureProvider } from '../../types'
import { UnderlineIcon } from '../../../lexical/ui/icons/Underline'
import { SectionWithEntries } from '../common/floatingSelectToolbarSection' import { SectionWithEntries } from '../common/floatingSelectToolbarSection'
export const UnderlineTextFeature = (): FeatureProvider => { export const UnderlineTextFeature = (): FeatureProvider => {
return { return {
feature: ({ resolvedFeatures, unsanitizedEditorConfig }) => { feature: () => {
return { return {
floatingSelectToolbar: { floatingSelectToolbar: {
sections: [ sections: [
SectionWithEntries([ SectionWithEntries([
{ {
ChildComponent: UnderlineIcon, ChildComponent: () =>
isActive: ({ editor, selection }) => { // @ts-expect-error
import('../../../lexical/ui/icons/Underline').then(
(module) => module.UnderlineIcon,
),
isActive: ({ selection }) => {
if ($isRangeSelection(selection)) { if ($isRangeSelection(selection)) {
return selection.hasFormat('underline') return selection.hasFormat('underline')
} }

View File

@@ -3,8 +3,6 @@ import type {
FloatingToolbarSectionEntry, FloatingToolbarSectionEntry,
} from '../../lexical/plugins/FloatingSelectToolbar/types' } from '../../lexical/plugins/FloatingSelectToolbar/types'
import './index.scss'
export const IndentSectionWithEntries = ( export const IndentSectionWithEntries = (
entries: FloatingToolbarSectionEntry[], entries: FloatingToolbarSectionEntry[],
): FloatingToolbarSection => { ): FloatingToolbarSection => {

View File

@@ -2,10 +2,7 @@ import { INDENT_CONTENT_COMMAND, OUTDENT_CONTENT_COMMAND } from 'lexical'
import type { FeatureProvider } from '../types' import type { FeatureProvider } from '../types'
import { IndentDecreaseIcon } from '../../lexical/ui/icons/IndentDecrease'
import { IndentIncreaseIcon } from '../../lexical/ui/icons/IndentIncrease'
import { IndentSectionWithEntries } from './floatingSelectToolbarIndentSection' import { IndentSectionWithEntries } from './floatingSelectToolbarIndentSection'
import './index.scss'
export const IndentFeature = (): FeatureProvider => { export const IndentFeature = (): FeatureProvider => {
return { return {
@@ -15,9 +12,13 @@ export const IndentFeature = (): FeatureProvider => {
sections: [ sections: [
IndentSectionWithEntries([ IndentSectionWithEntries([
{ {
ChildComponent: IndentDecreaseIcon, ChildComponent: () =>
isActive: ({ editor, selection }) => false, // @ts-expect-error
isEnabled: ({ editor, selection }) => { import('../../lexical/ui/icons/IndentDecrease').then(
(module) => module.IndentDecreaseIcon,
),
isActive: () => false,
isEnabled: ({ selection }) => {
if (!selection || !selection?.getNodes()?.length) { if (!selection || !selection?.getNodes()?.length) {
return false return false
} }
@@ -39,8 +40,12 @@ export const IndentFeature = (): FeatureProvider => {
]), ]),
IndentSectionWithEntries([ IndentSectionWithEntries([
{ {
ChildComponent: IndentIncreaseIcon, ChildComponent: () =>
isActive: ({ editor, selection }) => false, // @ts-expect-error
import('../../lexical/ui/icons/IndentIncrease').then(
(module) => module.IndentIncreaseIcon,
),
isActive: () => false,
key: 'indent-increase', key: 'indent-increase',
label: `Increase Indent`, label: `Increase Indent`,
onClick: ({ editor }) => { onClick: ({ editor }) => {
@@ -51,6 +56,14 @@ export const IndentFeature = (): FeatureProvider => {
]), ]),
], ],
}, },
plugins: [
{
Component: () =>
// @ts-expect-error
import('./plugin').then((module) => module.IndentPlugin),
position: 'normal',
},
],
props: null, props: null,
} }
}, },

View File

@@ -0,0 +1,7 @@
'use client'
import './index.scss'
export function IndentPlugin(): null {
return null
}

View File

@@ -3,11 +3,9 @@ import { INSERT_CHECK_LIST_COMMAND, ListItemNode, ListNode } from '@lexical/list
import type { FeatureProvider } from '../../types' import type { FeatureProvider } from '../../types'
import { SlashMenuOption } from '../../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/types' import { SlashMenuOption } from '../../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/types'
import { ChecklistIcon } from '../../../lexical/ui/icons/Checklist'
import { TextDropdownSectionWithEntries } from '../../common/floatingSelectToolbarTextDropdownSection' import { TextDropdownSectionWithEntries } from '../../common/floatingSelectToolbarTextDropdownSection'
import { ListHTMLConverter, ListItemHTMLConverter } from '../htmlConverter' import { ListHTMLConverter, ListItemHTMLConverter } from '../htmlConverter'
import { CHECK_LIST } from './markdownTransformers' import { CHECK_LIST } from './markdownTransformers'
import { LexicalCheckListPlugin } from './plugin'
// 345 // 345
// carbs 7 // carbs 7
@@ -19,7 +17,11 @@ export const CheckListFeature = (): FeatureProvider => {
sections: [ sections: [
TextDropdownSectionWithEntries([ TextDropdownSectionWithEntries([
{ {
ChildComponent: ChecklistIcon, ChildComponent: () =>
// @ts-expect-error
import('../../../lexical/ui/icons/Checklist').then(
(module) => module.ChecklistIcon,
),
isActive: () => false, isActive: () => false,
key: 'checkList', key: 'checkList',
label: `Check List`, label: `Check List`,
@@ -53,7 +55,9 @@ export const CheckListFeature = (): FeatureProvider => {
], ],
plugins: [ plugins: [
{ {
Component: LexicalCheckListPlugin, Component: () =>
// @ts-expect-error
import('./plugin').then((module) => module.LexicalCheckListPlugin),
position: 'normal', position: 'normal',
}, },
], ],
@@ -65,7 +69,11 @@ export const CheckListFeature = (): FeatureProvider => {
key: 'lists', key: 'lists',
options: [ options: [
new SlashMenuOption('checklist', { new SlashMenuOption('checklist', {
Icon: ChecklistIcon, Icon: () =>
// @ts-expect-error
import('../../../lexical/ui/icons/Checklist').then(
(module) => module.ChecklistIcon,
),
displayName: 'Check List', displayName: 'Check List',
keywords: ['check list', 'check', 'checklist', 'cl'], keywords: ['check list', 'check', 'checklist', 'cl'],
onSelect: ({ editor }) => { onSelect: ({ editor }) => {

View File

@@ -3,10 +3,8 @@ import { INSERT_ORDERED_LIST_COMMAND, ListItemNode, ListNode } from '@lexical/li
import type { FeatureProvider } from '../../types' import type { FeatureProvider } from '../../types'
import { SlashMenuOption } from '../../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/types' import { SlashMenuOption } from '../../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/types'
import { OrderedListIcon } from '../../../lexical/ui/icons/OrderedList'
import { TextDropdownSectionWithEntries } from '../../common/floatingSelectToolbarTextDropdownSection' import { TextDropdownSectionWithEntries } from '../../common/floatingSelectToolbarTextDropdownSection'
import { ListHTMLConverter, ListItemHTMLConverter } from '../htmlConverter' import { ListHTMLConverter, ListItemHTMLConverter } from '../htmlConverter'
import { LexicalListPlugin } from '../plugin'
import { ORDERED_LIST } from './markdownTransformer' import { ORDERED_LIST } from './markdownTransformer'
export const OrderedListFeature = (): FeatureProvider => { export const OrderedListFeature = (): FeatureProvider => {
@@ -17,7 +15,11 @@ export const OrderedListFeature = (): FeatureProvider => {
sections: [ sections: [
TextDropdownSectionWithEntries([ TextDropdownSectionWithEntries([
{ {
ChildComponent: OrderedListIcon, ChildComponent: () =>
// @ts-expect-error
import('../../../lexical/ui/icons/OrderedList').then(
(module) => module.OrderedListIcon,
),
isActive: () => false, isActive: () => false,
key: 'orderedList', key: 'orderedList',
label: `Ordered List`, label: `Ordered List`,
@@ -52,7 +54,9 @@ export const OrderedListFeature = (): FeatureProvider => {
? [] ? []
: [ : [
{ {
Component: LexicalListPlugin, Component: () =>
// @ts-expect-error
import('../plugin').then((module) => module.LexicalListPlugin),
position: 'normal', position: 'normal',
}, },
], ],
@@ -64,7 +68,11 @@ export const OrderedListFeature = (): FeatureProvider => {
key: 'lists', key: 'lists',
options: [ options: [
new SlashMenuOption('orderedlist', { new SlashMenuOption('orderedlist', {
Icon: OrderedListIcon, Icon: () =>
// @ts-expect-error
import('../../../lexical/ui/icons/OrderedList').then(
(module) => module.OrderedListIcon,
),
displayName: 'Ordered List', displayName: 'Ordered List',
keywords: ['ordered list', 'ol'], keywords: ['ordered list', 'ol'],
onSelect: ({ editor }) => { onSelect: ({ editor }) => {

View File

@@ -3,10 +3,8 @@ import { INSERT_UNORDERED_LIST_COMMAND, ListItemNode, ListNode } from '@lexical/
import type { FeatureProvider } from '../../types' import type { FeatureProvider } from '../../types'
import { SlashMenuOption } from '../../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/types' import { SlashMenuOption } from '../../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/types'
import { UnorderedListIcon } from '../../../lexical/ui/icons/UnorderedList'
import { TextDropdownSectionWithEntries } from '../../common/floatingSelectToolbarTextDropdownSection' import { TextDropdownSectionWithEntries } from '../../common/floatingSelectToolbarTextDropdownSection'
import { ListHTMLConverter, ListItemHTMLConverter } from '../htmlConverter' import { ListHTMLConverter, ListItemHTMLConverter } from '../htmlConverter'
import { LexicalListPlugin } from '../plugin'
import { UNORDERED_LIST } from './markdownTransformer' import { UNORDERED_LIST } from './markdownTransformer'
export const UnorderedListFeature = (): FeatureProvider => { export const UnorderedListFeature = (): FeatureProvider => {
@@ -17,7 +15,11 @@ export const UnorderedListFeature = (): FeatureProvider => {
sections: [ sections: [
TextDropdownSectionWithEntries([ TextDropdownSectionWithEntries([
{ {
ChildComponent: UnorderedListIcon, ChildComponent: () =>
// @ts-expect-error
import('../../../lexical/ui/icons/UnorderedList').then(
(module) => module.UnorderedListIcon,
),
isActive: () => false, isActive: () => false,
key: 'unorderedList', key: 'unorderedList',
label: `Unordered List`, label: `Unordered List`,
@@ -48,7 +50,9 @@ export const UnorderedListFeature = (): FeatureProvider => {
], ],
plugins: [ plugins: [
{ {
Component: LexicalListPlugin, Component: () =>
// @ts-expect-error
import('../plugin').then((module) => module.LexicalListPlugin),
position: 'normal', position: 'normal',
}, },
], ],
@@ -60,7 +64,11 @@ export const UnorderedListFeature = (): FeatureProvider => {
key: 'lists', key: 'lists',
options: [ options: [
new SlashMenuOption('unorderedlist', { new SlashMenuOption('unorderedlist', {
Icon: UnorderedListIcon, Icon: () =>
// @ts-expect-error
import('../../../lexical/ui/icons/UnorderedList').then(
(module) => module.UnorderedListIcon,
),
displayName: 'Unordered List', displayName: 'Unordered List',
keywords: ['unordered list', 'ul'], keywords: ['unordered list', 'ul'],
onSelect: ({ editor }) => { onSelect: ({ editor }) => {

View File

@@ -0,0 +1,19 @@
import React from 'react'
import type { UnknownConvertedNodeData } from './index'
import './index.scss'
type Props = {
data: UnknownConvertedNodeData
}
export const UnknownConvertedNodeComponent: React.FC<Props> = (props) => {
const { data } = props
return (
<div>
Unknown converted payload-plugin-lexical node: <strong>{data?.nodeType}</strong>
</div>
)
}

View File

@@ -2,9 +2,7 @@ import type { SerializedLexicalNode, Spread } from 'lexical'
import { addClassNamesToElement } from '@lexical/utils' import { addClassNamesToElement } from '@lexical/utils'
import { DecoratorNode, type EditorConfig, type LexicalNode, type NodeKey } from 'lexical' import { DecoratorNode, type EditorConfig, type LexicalNode, type NodeKey } from 'lexical'
import React from 'react' import * as React from 'react'
import './index.scss'
export type UnknownConvertedNodeData = { export type UnknownConvertedNodeData = {
nodeData: unknown nodeData: unknown
@@ -18,6 +16,13 @@ export type SerializedUnknownConvertedNode = Spread<
SerializedLexicalNode SerializedLexicalNode
> >
const Component = React.lazy(() =>
// @ts-expect-error TypeScript being dumb
import('./Component').then((module) => ({
default: module.UnknownConvertedNodeComponent,
})),
)
/** @noInheritDoc */ /** @noInheritDoc */
export class UnknownConvertedNode extends DecoratorNode<JSX.Element> { export class UnknownConvertedNode extends DecoratorNode<JSX.Element> {
__data: UnknownConvertedNodeData __data: UnknownConvertedNodeData
@@ -58,11 +63,7 @@ export class UnknownConvertedNode extends DecoratorNode<JSX.Element> {
} }
decorate(): JSX.Element | null { decorate(): JSX.Element | null {
return ( return <Component data={this.__data} />
<div>
Unknown converted payload-plugin-lexical node: <strong>{this.__data?.nodeType}</strong>
</div>
)
} }
exportJSON(): SerializedUnknownConvertedNode { exportJSON(): SerializedUnknownConvertedNode {

View File

@@ -0,0 +1,19 @@
import React from 'react'
import type { UnknownConvertedNodeData } from './index'
import './index.scss'
type Props = {
data: UnknownConvertedNodeData
}
export const UnknownConvertedNodeComponent: React.FC<Props> = (props) => {
const { data } = props
return (
<div>
Unknown converted Slate node: <strong>{data?.nodeType}</strong>
</div>
)
}

View File

@@ -2,9 +2,7 @@ import type { SerializedLexicalNode, Spread } from 'lexical'
import { addClassNamesToElement } from '@lexical/utils' import { addClassNamesToElement } from '@lexical/utils'
import { DecoratorNode, type EditorConfig, type LexicalNode, type NodeKey } from 'lexical' import { DecoratorNode, type EditorConfig, type LexicalNode, type NodeKey } from 'lexical'
import React from 'react' import * as React from 'react'
import './index.scss'
export type UnknownConvertedNodeData = { export type UnknownConvertedNodeData = {
nodeData: unknown nodeData: unknown
@@ -18,6 +16,13 @@ export type SerializedUnknownConvertedNode = Spread<
SerializedLexicalNode SerializedLexicalNode
> >
const Component = React.lazy(() =>
// @ts-expect-error TypeScript being dumb
import('./Component').then((module) => ({
default: module.UnknownConvertedNodeComponent,
})),
)
/** @noInheritDoc */ /** @noInheritDoc */
export class UnknownConvertedNode extends DecoratorNode<JSX.Element> { export class UnknownConvertedNode extends DecoratorNode<JSX.Element> {
__data: UnknownConvertedNodeData __data: UnknownConvertedNodeData
@@ -58,11 +63,7 @@ export class UnknownConvertedNode extends DecoratorNode<JSX.Element> {
} }
decorate(): JSX.Element | null { decorate(): JSX.Element | null {
return ( return <Component data={this.__data} />
<div>
Unknown converted Slate node: <strong>{this.__data?.nodeType}</strong>
</div>
)
} }
exportJSON(): SerializedUnknownConvertedNode { exportJSON(): SerializedUnknownConvertedNode {

View File

@@ -99,23 +99,23 @@ export type Feature = {
plugins?: Array< plugins?: Array<
| { | {
// plugins are anything which is not directly part of the editor. Like, creating a command which creates a node, or opens a modal, or some other more "outside" functionality // plugins are anything which is not directly part of the editor. Like, creating a command which creates a node, or opens a modal, or some other more "outside" functionality
Component: React.FC Component: () => Promise<React.FC<{ anchorElem: HTMLElement }>>
position: 'normal' // Determines at which position the Component will be added. position: 'floatingAnchorElem' // Determines at which position the Component will be added.
} }
| { | {
// plugins are anything which is not directly part of the editor. Like, creating a command which creates a node, or opens a modal, or some other more "outside" functionality // plugins are anything which is not directly part of the editor. Like, creating a command which creates a node, or opens a modal, or some other more "outside" functionality
Component: React.FC Component: () => Promise<React.FC>
position: 'top' // Determines at which position the Component will be added.
}
| {
// plugins are anything which is not directly part of the editor. Like, creating a command which creates a node, or opens a modal, or some other more "outside" functionality
Component: React.FC<{ anchorElem: HTMLElement }>
position: 'bottom' // Determines at which position the Component will be added. position: 'bottom' // Determines at which position the Component will be added.
} }
| { | {
// plugins are anything which is not directly part of the editor. Like, creating a command which creates a node, or opens a modal, or some other more "outside" functionality // plugins are anything which is not directly part of the editor. Like, creating a command which creates a node, or opens a modal, or some other more "outside" functionality
Component: React.FC<{ anchorElem: HTMLElement }> Component: () => Promise<React.FC>
position: 'floatingAnchorElem' // Determines at which position the Component will be added. position: 'normal' // Determines at which position the Component will be added.
}
| {
// plugins are anything which is not directly part of the editor. Like, creating a command which creates a node, or opens a modal, or some other more "outside" functionality
Component: () => Promise<React.FC>
position: 'top' // Determines at which position the Component will be added.
} }
> >
@@ -161,6 +161,33 @@ export type ResolvedFeatureMap = Map<string, ResolvedFeature>
export type FeatureProviderMap = Map<string, FeatureProvider> export type FeatureProviderMap = Map<string, FeatureProvider>
export type SanitizedPlugin =
| {
// plugins are anything which is not directly part of the editor. Like, creating a command which creates a node, or opens a modal, or some other more "outside" functionality
Component: () => Promise<React.FC<{ anchorElem: HTMLElement }>>
desktopOnly?: boolean
key: string
position: 'floatingAnchorElem' // Determines at which position the Component will be added.
}
| {
// plugins are anything which is not directly part of the editor. Like, creating a command which creates a node, or opens a modal, or some other more "outside" functionality
Component: () => Promise<React.FC>
key: string
position: 'bottom' // Determines at which position the Component will be added.
}
| {
// plugins are anything which is not directly part of the editor. Like, creating a command which creates a node, or opens a modal, or some other more "outside" functionality
Component: () => Promise<React.FC>
key: string
position: 'normal' // Determines at which position the Component will be added.
}
| {
// plugins are anything which is not directly part of the editor. Like, creating a command which creates a node, or opens a modal, or some other more "outside" functionality
Component: () => Promise<React.FC>
key: string
position: 'top' // Determines at which position the Component will be added.
}
export type SanitizedFeatures = Required< export type SanitizedFeatures = Required<
Pick<ResolvedFeature, 'markdownTransformers' | 'nodes'> Pick<ResolvedFeature, 'markdownTransformers' | 'nodes'>
> & { > & {
@@ -200,33 +227,7 @@ export type SanitizedFeatures = Required<
}) => SerializedEditorState }) => SerializedEditorState
> >
} }
plugins?: Array< plugins?: Array<SanitizedPlugin>
| {
// plugins are anything which is not directly part of the editor. Like, creating a command which creates a node, or opens a modal, or some other more "outside" functionality
Component: React.FC
key: string
position: 'bottom' // Determines at which position the Component will be added.
}
| {
// plugins are anything which is not directly part of the editor. Like, creating a command which creates a node, or opens a modal, or some other more "outside" functionality
Component: React.FC
key: string
position: 'normal' // Determines at which position the Component will be added.
}
| {
// plugins are anything which is not directly part of the editor. Like, creating a command which creates a node, or opens a modal, or some other more "outside" functionality
Component: React.FC
key: string
position: 'top' // Determines at which position the Component will be added.
}
| {
// plugins are anything which is not directly part of the editor. Like, creating a command which creates a node, or opens a modal, or some other more "outside" functionality
Component: React.FC<{ anchorElem: HTMLElement }>
desktopOnly?: boolean
key: string
position: 'floatingAnchorElem' // Determines at which position the Component will be added.
}
>
/** The node types mapped to their populationPromises */ /** The node types mapped to their populationPromises */
populationPromises: Map<string, Array<PopulationPromise>> populationPromises: Map<string, Array<PopulationPromise>>
slashMenu: { slashMenu: {

View File

@@ -7,8 +7,10 @@ import type { FieldProps } from '../types'
// @ts-expect-error Just TypeScript being broken // TODO: Open TypeScript issue // @ts-expect-error Just TypeScript being broken // TODO: Open TypeScript issue
const RichTextEditor = lazy(() => import('./Field')) const RichTextEditor = lazy(() => import('./Field'))
export const RichTextField: React.FC<FieldProps> = (props) => ( export const RichTextField: React.FC<FieldProps> = (props) => {
<Suspense fallback={<ShimmerEffect height="35vh" />}> return (
<RichTextEditor {...props} /> <Suspense fallback={<ShimmerEffect height="35vh" />}>
</Suspense> <RichTextEditor {...props} />
) </Suspense>
)
}

View File

@@ -0,0 +1,37 @@
import * as React from 'react'
import { useMemo } from 'react'
import type { SanitizedPlugin } from '../features/types'
export const EditorPlugin: React.FC<{
anchorElem?: HTMLDivElement
plugin: SanitizedPlugin
}> = ({ anchorElem, plugin }) => {
const Component: React.FC<any> = useMemo(() => {
return plugin?.Component
? React.lazy(() =>
plugin.Component().then((resolvedComponent) => ({
default: resolvedComponent,
})),
)
: null
}, [plugin]) // Dependency array ensures this is only recomputed if entry changes
if (plugin.position === 'floatingAnchorElem') {
return (
Component && (
<React.Suspense>
<Component anchorElem={anchorElem} />
</React.Suspense>
)
)
}
return (
Component && (
<React.Suspense>
<Component />
</React.Suspense>
)
)
}

View File

@@ -39,3 +39,21 @@
pointer-events: none; pointer-events: none;
} }
} }
.floating-select-toolbar-popup__section-dropdown-align {
display: flex;
gap: 2px;
}
.floating-select-toolbar-popup__section-features {
display: flex;
gap: 2px;
}
.floating-select-toolbar-popup__section-dropdown-text {
display: flex;
gap: 2px;
}
.floating-select-toolbar-popup__section-format {
display: flex;
gap: 2px;
}

View File

@@ -10,6 +10,7 @@ import { useEffect, useState } from 'react'
import type { LexicalProviderProps } from './LexicalProvider' import type { LexicalProviderProps } from './LexicalProvider'
import { EditorPlugin } from './EditorPlugin'
import './LexicalEditor.scss' import './LexicalEditor.scss'
import { FloatingSelectToolbarPlugin } from './plugins/FloatingSelectToolbar' import { FloatingSelectToolbarPlugin } from './plugins/FloatingSelectToolbar'
import { MarkdownShortcutPlugin } from './plugins/MarkdownShortcut' import { MarkdownShortcutPlugin } from './plugins/MarkdownShortcut'
@@ -53,7 +54,7 @@ export const LexicalEditor: React.FC<Pick<LexicalProviderProps, 'editorConfig' |
<React.Fragment> <React.Fragment>
{editorConfig.features.plugins.map((plugin) => { {editorConfig.features.plugins.map((plugin) => {
if (plugin.position === 'top') { if (plugin.position === 'top') {
return <plugin.Component key={plugin.key} /> return <EditorPlugin key={plugin.key} plugin={plugin} />
} }
})} })}
<RichTextPlugin <RichTextPlugin
@@ -65,10 +66,12 @@ export const LexicalEditor: React.FC<Pick<LexicalProviderProps, 'editorConfig' |
</div> </div>
</div> </div>
} }
placeholder={<p className="editor-placeholder">Start typing, or press '/' for commands...</p>} placeholder={
<p className="editor-placeholder">Start typing, or press '/' for commands...</p>
}
/> />
<OnChangePlugin <OnChangePlugin
// Selection changes can be ignore here, reducing the // Selection changes can be ignored here, reducing the
// frequency that the FieldComponent and Payload receive updates. // frequency that the FieldComponent and Payload receive updates.
// Selection changes are only needed if you are saving selection state // Selection changes are only needed if you are saving selection state
ignoreSelectionChange ignoreSelectionChange
@@ -92,7 +95,9 @@ export const LexicalEditor: React.FC<Pick<LexicalProviderProps, 'editorConfig' |
plugin.position === 'floatingAnchorElem' && plugin.position === 'floatingAnchorElem' &&
!(plugin.desktopOnly === true && isSmallWidthViewport) !(plugin.desktopOnly === true && isSmallWidthViewport)
) { ) {
return <plugin.Component anchorElem={floatingAnchorElem} key={plugin.key} /> return (
<EditorPlugin anchorElem={floatingAnchorElem} key={plugin.key} plugin={plugin} />
)
} }
})} })}
{editor.isEditable() && ( {editor.isEditable() && (
@@ -113,12 +118,12 @@ export const LexicalEditor: React.FC<Pick<LexicalProviderProps, 'editorConfig' |
<TabIndentationPlugin /> <TabIndentationPlugin />
{editorConfig.features.plugins.map((plugin) => { {editorConfig.features.plugins.map((plugin) => {
if (plugin.position === 'normal') { if (plugin.position === 'normal') {
return <plugin.Component key={plugin.key} /> return <EditorPlugin key={plugin.key} plugin={plugin} />
} }
})} })}
{editorConfig.features.plugins.map((plugin) => { {editorConfig.features.plugins.map((plugin) => {
if (plugin.position === 'bottom') { if (plugin.position === 'bottom') {
return <plugin.Component key={plugin.key} /> return <EditorPlugin key={plugin.key} plugin={plugin} />
} }
})} })}
</React.Fragment> </React.Fragment>

View File

@@ -2,6 +2,7 @@
import type { InitialConfigType } from '@lexical/react/LexicalComposer' import type { InitialConfigType } from '@lexical/react/LexicalComposer'
import type { EditorState, SerializedEditorState } from 'lexical' import type { EditorState, SerializedEditorState } from 'lexical'
import type { LexicalEditor } from 'lexical' import type { LexicalEditor } from 'lexical'
import type { EditorConfig as LexicalEditorConfig } from 'lexical/LexicalEditor'
import { LexicalComposer } from '@lexical/react/LexicalComposer' import { LexicalComposer } from '@lexical/react/LexicalComposer'
import * as React from 'react' import * as React from 'react'
@@ -24,6 +25,25 @@ export const LexicalProvider: React.FC<LexicalProviderProps> = (props) => {
const { editorConfig, fieldProps, onChange, path, readOnly } = props const { editorConfig, fieldProps, onChange, path, readOnly } = props
let { value } = props let { value } = props
const [initialConfig, setInitialConfig] = React.useState<InitialConfigType | null>(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) { if (editorConfig?.features?.hooks?.load?.length) {
editorConfig.features.hooks.load.forEach((hook) => { editorConfig.features.hooks.load.forEach((hook) => {
value = hook({ incomingEditorState: value }) value = hook({ incomingEditorState: value })
@@ -49,15 +69,8 @@ export const LexicalProvider: React.FC<LexicalProviderProps> = (props) => {
) )
} }
const initialConfig: InitialConfigType = { if (!initialConfig) {
editable: readOnly === true ? false : true, return <p>Loading...</p>
editorState: value != null ? JSON.stringify(value) : undefined,
namespace: editorConfig.lexical.namespace,
nodes: [...getEnabledNodes({ editorConfig })],
onError: (error: Error) => {
throw error
},
theme: editorConfig.lexical.theme,
} }
return ( return (

View File

@@ -1,5 +1,3 @@
import type { EditorConfig as LexicalEditorConfig } from 'lexical/LexicalEditor'
import type { FeatureProvider } from '../../features/types' import type { FeatureProvider } from '../../features/types'
import type { EditorConfig, SanitizedEditorConfig } from './types' import type { EditorConfig, SanitizedEditorConfig } from './types'
@@ -21,7 +19,6 @@ import { IndentFeature } from '../../features/indent'
import { CheckListFeature } from '../../features/lists/CheckList' import { CheckListFeature } from '../../features/lists/CheckList'
import { OrderedListFeature } from '../../features/lists/OrderedList' import { OrderedListFeature } from '../../features/lists/OrderedList'
import { UnorderedListFeature } from '../../features/lists/UnorderedList' import { UnorderedListFeature } from '../../features/lists/UnorderedList'
import { LexicalEditorTheme } from '../theme/EditorTheme'
import { sanitizeEditorConfig } from './sanitize' import { sanitizeEditorConfig } from './sanitize'
export const defaultEditorFeatures: FeatureProvider[] = [ 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 //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 = { export const defaultEditorConfig: EditorConfig = {
features: defaultEditorFeatures, features: defaultEditorFeatures,
lexical: defaultEditorLexicalConfig, lexical: () =>
// @ts-expect-error
import('./defaultClient').then((module) => {
const defaultEditorLexicalConfig = module.defaultEditorLexicalConfig
return defaultEditorLexicalConfig
}),
} }
export const defaultSanitizedEditorConfig: SanitizedEditorConfig = export const defaultSanitizedEditorConfig: SanitizedEditorConfig =

View File

@@ -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,
}

View File

@@ -4,11 +4,11 @@ import type { FeatureProvider, ResolvedFeatureMap, SanitizedFeatures } from '../
export type EditorConfig = { export type EditorConfig = {
features: FeatureProvider[] features: FeatureProvider[]
lexical: LexicalEditorConfig lexical?: () => Promise<LexicalEditorConfig>
} }
export type SanitizedEditorConfig = { export type SanitizedEditorConfig = {
features: SanitizedFeatures features: SanitizedFeatures
lexical: LexicalEditorConfig lexical: () => Promise<LexicalEditorConfig>
resolvedFeatureMap: ResolvedFeatureMap resolvedFeatureMap: ResolvedFeatureMap
} }

View File

@@ -1,5 +1,5 @@
'use client' 'use client'
import React from 'react' import React, { useMemo } from 'react'
const baseClass = 'floating-select-toolbar-popup__dropdown' const baseClass = 'floating-select-toolbar-popup__dropdown'
@@ -10,6 +10,57 @@ import type { FloatingToolbarSectionEntry } from '../types'
import { DropDown, DropDownItem } from './DropDown' import { DropDown, DropDownItem } from './DropDown'
import './index.scss' 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 && (
<React.Suspense>
<Component anchorElem={anchorElem} editor={editor} entry={entry} key={entry.key} />
</React.Suspense>
)
)
}
return (
<DropDownItem entry={entry} key={entry.key}>
{ChildComponent && (
<React.Suspense>
<ChildComponent />
</React.Suspense>
)}
<span className="text">{entry.label}</span>
</DropDownItem>
)
}
export const ToolbarDropdown = ({ export const ToolbarDropdown = ({
Icon, Icon,
anchorElem, anchorElem,
@@ -31,21 +82,8 @@ export const ToolbarDropdown = ({
> >
{entries.length && {entries.length &&
entries.map((entry) => { entries.map((entry) => {
if (entry.Component) {
return (
<entry.Component
anchorElem={anchorElem}
editor={editor}
entry={entry}
key={entry.key}
/>
)
}
return ( return (
<DropDownItem entry={entry} key={entry.key}> <ToolbarEntry anchorElem={anchorElem} editor={editor} entry={entry} key={entry.key} />
<entry.ChildComponent />
<span className="text">{entry.label}</span>
</DropDownItem>
) )
})} })}
</DropDown> </DropDown>

View File

@@ -10,10 +10,12 @@ import {
COMMAND_PRIORITY_LOW, COMMAND_PRIORITY_LOW,
SELECTION_CHANGE_COMMAND, SELECTION_CHANGE_COMMAND,
} from 'lexical' } from 'lexical'
import { useCallback, useEffect, useRef, useState } from 'react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import * as React from 'react' import * as React from 'react'
import { createPortal } from 'react-dom' import { createPortal } from 'react-dom'
import type { FloatingToolbarSection, FloatingToolbarSectionEntry } from './types'
import { useEditorConfigContext } from '../../config/EditorConfigProvider' import { useEditorConfigContext } from '../../config/EditorConfigProvider'
import { getDOMRangeRect } from '../../utils/getDOMRangeRect' import { getDOMRangeRect } from '../../utils/getDOMRangeRect'
import { setFloatingElemPosition } from '../../utils/setFloatingElemPosition' import { setFloatingElemPosition } from '../../utils/setFloatingElemPosition'
@@ -21,6 +23,117 @@ import { ToolbarButton } from './ToolbarButton'
import { ToolbarDropdown } from './ToolbarDropdown' import { ToolbarDropdown } from './ToolbarDropdown'
import './index.scss' 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 && (
<React.Suspense>
<Component anchorElem={anchorElem} editor={editor} entry={entry} key={entry.key} />{' '}
</React.Suspense>
)
)
}
return (
<ToolbarButton entry={entry} key={entry.key}>
{ChildComponent && (
<React.Suspense>
<ChildComponent />
</React.Suspense>
)}
</ToolbarButton>
)
}
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 (
<div
className={`floating-select-toolbar-popup__section floating-select-toolbar-popup__section-${section.key}`}
key={section.key}
>
{section.type === 'dropdown' &&
section.entries.length &&
(Icon ? (
<React.Suspense>
<ToolbarDropdown
Icon={Icon}
anchorElem={anchorElem}
editor={editor}
entries={section.entries}
/>
</React.Suspense>
) : (
<ToolbarDropdown anchorElem={anchorElem} editor={editor} entries={section.entries} />
))}
{section.type === 'buttons' &&
section.entries.length &&
section.entries.map((entry) => {
return (
<ButtonSectionEntry
anchorElem={anchorElem}
editor={editor}
entry={entry}
key={entry.key}
/>
)
})}
{index < editorConfig.features.floatingSelectToolbar?.sections.length - 1 && (
<div className="divider" />
)}
</div>
)
}
function FloatingSelectToolbar({ function FloatingSelectToolbar({
anchorElem, anchorElem,
editor, editor,
@@ -176,41 +289,13 @@ function FloatingSelectToolbar({
{editorConfig?.features && {editorConfig?.features &&
editorConfig.features?.floatingSelectToolbar?.sections.map((section, i) => { editorConfig.features?.floatingSelectToolbar?.sections.map((section, i) => {
return ( return (
<div <ToolbarSection
className={`floating-select-toolbar-popup__section floating-select-toolbar-popup__section-${section.key}`} anchorElem={anchorElem}
editor={editor}
index={i}
key={section.key} key={section.key}
> section={section}
{section.type === 'dropdown' && section.entries.length && ( />
<ToolbarDropdown
Icon={section.ChildComponent}
anchorElem={anchorElem}
editor={editor}
entries={section.entries}
/>
)}
{section.type === 'buttons' &&
section.entries.length &&
section.entries.map((entry) => {
if (entry.Component) {
return (
<entry.Component
anchorElem={anchorElem}
editor={editor}
entry={entry}
key={entry.key}
/>
)
}
return (
<ToolbarButton entry={entry} key={entry.key}>
<entry.ChildComponent />
</ToolbarButton>
)
})}
{i < editorConfig.features.floatingSelectToolbar?.sections.length - 1 && (
<div className="divider" />
)}
</div>
) )
})} })}
</React.Fragment> </React.Fragment>

View File

@@ -1,8 +1,9 @@
import type { BaseSelection, LexicalEditor } from 'lexical' import type { BaseSelection, LexicalEditor } from 'lexical'
import type React from 'react'
export type FloatingToolbarSection = export type FloatingToolbarSection =
| { | {
ChildComponent?: React.FC ChildComponent?: () => Promise<React.FC>
entries: Array<FloatingToolbarSectionEntry> entries: Array<FloatingToolbarSectionEntry>
key: string key: string
order?: number order?: number
@@ -16,13 +17,15 @@ export type FloatingToolbarSection =
} }
export type FloatingToolbarSectionEntry = { export type FloatingToolbarSectionEntry = {
ChildComponent?: React.FC ChildComponent?: () => Promise<React.FC>
/** Use component to ignore the children and onClick properties. It does not use the default, pre-defined format Button component */ /** Use component to ignore the children and onClick properties. It does not use the default, pre-defined format Button component */
Component?: React.FC<{ Component?: () => Promise<
anchorElem: HTMLElement React.FC<{
editor: LexicalEditor anchorElem: HTMLElement
entry: FloatingToolbarSectionEntry editor: LexicalEditor
}> entry: FloatingToolbarSectionEntry
}>
>
isActive?: ({ editor, selection }: { editor: LexicalEditor; selection: BaseSelection }) => boolean isActive?: ({ editor, selection }: { editor: LexicalEditor; selection: BaseSelection }) => boolean
isEnabled?: ({ isEnabled?: ({
editor, editor,

View File

@@ -1,10 +1,11 @@
import type { i18n } from 'i18next' import type { i18n } from 'i18next'
import type { LexicalEditor } from 'lexical' import type { LexicalEditor } from 'lexical'
import type { MutableRefObject } from 'react' import type { MutableRefObject } from 'react'
import type React from 'react'
export class SlashMenuOption { export class SlashMenuOption {
// Icon for display // Icon for display
Icon: React.FC Icon: () => Promise<React.FC>
displayName?: (({ i18n }: { i18n: i18n }) => string) | string displayName?: (({ i18n }: { i18n: i18n }) => string) | string
// Used for class names and, if displayName is not provided, for display. // Used for class names and, if displayName is not provided, for display.
@@ -21,7 +22,7 @@ export class SlashMenuOption {
constructor( constructor(
key: string, key: string,
options: { options: {
Icon: React.FC Icon: () => Promise<React.FC>
displayName?: (({ i18n }: { i18n: i18n }) => string) | string displayName?: (({ i18n }: { i18n: i18n }) => string) | string
keyboardShortcut?: string keyboardShortcut?: string
keywords?: Array<string> keywords?: Array<string>

View File

@@ -45,6 +45,16 @@ function SlashMenuItem({
title = title.substring(0, 25) + '...' title = title.substring(0, 25) + '...'
} }
const LazyIcon = useMemo(() => {
return option?.Icon
? React.lazy(() =>
option.Icon().then((resolvedIcon) => ({
default: resolvedIcon,
})),
)
: null
}, [option])
return ( return (
<button <button
aria-selected={isSelected} aria-selected={isSelected}
@@ -58,7 +68,12 @@ function SlashMenuItem({
tabIndex={-1} tabIndex={-1}
type="button" type="button"
> >
<option.Icon /> {LazyIcon && (
<React.Suspense>
<LazyIcon />
</React.Suspense>
)}
<span className={`${baseClass}__item-text`}>{title}</span> <span className={`${baseClass}__item-text`}>{title}</span>
</button> </button>
) )

View File

@@ -2,17 +2,15 @@ import type { SerializedEditorState } from 'lexical'
import type { EditorConfig as LexicalEditorConfig } from 'lexical/LexicalEditor' import type { EditorConfig as LexicalEditorConfig } from 'lexical/LexicalEditor'
import type { RichTextAdapter } from 'payload/types' 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 { FeatureProvider } from './field/features/types'
import type { EditorConfig, SanitizedEditorConfig } from './field/lexical/config/types' import type { EditorConfig, SanitizedEditorConfig } from './field/lexical/config/types'
import type { AdapterProps } from './types' import type { AdapterProps } from './types'
import { RichTextCell } from './cell'
import { RichTextField } from './field'
import { import {
defaultEditorConfig,
defaultEditorFeatures, defaultEditorFeatures,
defaultEditorLexicalConfig,
defaultSanitizedEditorConfig, defaultSanitizedEditorConfig,
} from './field/lexical/config/default' } from './field/lexical/config/default'
import { sanitizeEditorConfig } from './field/lexical/config/sanitize' import { sanitizeEditorConfig } from './field/lexical/config/sanitize'
@@ -48,23 +46,38 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
features = cloneDeep(defaultEditorFeatures) features = cloneDeep(defaultEditorFeatures)
} }
const lexical: LexicalEditorConfig = props.lexical || cloneDeep(defaultEditorLexicalConfig) const lexical: LexicalEditorConfig = props.lexical
finalSanitizedEditorConfig = sanitizeEditorConfig({ finalSanitizedEditorConfig = sanitizeEditorConfig({
features, features,
lexical, lexical: props.lexical ? () => Promise.resolve(lexical) : defaultEditorConfig.lexical,
}) })
} }
return { return {
CellComponent: withMergedProps({ LazyCellComponent: () =>
Component: RichTextCell, // @ts-expect-error
toMergeIntoProps: { editorConfig: finalSanitizedEditorConfig }, import('./cell').then((module) => {
}), const RichTextCell = module.RichTextCell
FieldComponent: withMergedProps({ return import('payload/utilities').then((module2) =>
Component: RichTextField, module2.withMergedProps({
toMergeIntoProps: { editorConfig: finalSanitizedEditorConfig }, 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 }) => { afterReadPromise: ({ field, incomingEditorState, siblingDoc }) => {
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
const promises: Promise<void>[] = [] const promises: Promise<void>[] = []
@@ -307,15 +320,12 @@ export {
export { export {
defaultEditorConfig, defaultEditorConfig,
defaultEditorFeatures, defaultEditorFeatures,
defaultEditorLexicalConfig,
defaultSanitizedEditorConfig, defaultSanitizedEditorConfig,
} from './field/lexical/config/default' } from './field/lexical/config/default'
export { loadFeatures, sortFeaturesForOptimalLoading } from './field/lexical/config/loader' export { loadFeatures, sortFeaturesForOptimalLoading } from './field/lexical/config/loader'
export { sanitizeEditorConfig, sanitizeFeatures } from './field/lexical/config/sanitize' export { sanitizeEditorConfig, sanitizeFeatures } from './field/lexical/config/sanitize'
export { getEnabledNodes } from './field/lexical/nodes' export { getEnabledNodes } from './field/lexical/nodes'
export { ToolbarButton } from './field/lexical/plugins/FloatingSelectToolbar/ToolbarButton'
export { ToolbarDropdown } from './field/lexical/plugins/FloatingSelectToolbar/ToolbarDropdown/index'
export { export {
type FloatingToolbarSection, type FloatingToolbarSection,
type FloatingToolbarSectionEntry, type FloatingToolbarSectionEntry,
@@ -324,8 +334,7 @@ export { ENABLE_SLASH_MENU_COMMAND } from './field/lexical/plugins/SlashMenu/Lex
// export SanitizedEditorConfig // export SanitizedEditorConfig
export type { EditorConfig, SanitizedEditorConfig } export type { EditorConfig, SanitizedEditorConfig }
export type { AdapterProps } export type { AdapterProps }
export { RichTextCell }
export { RichTextField }
export { export {
SlashMenuGroup, SlashMenuGroup,
SlashMenuOption, SlashMenuOption,

View File

@@ -1,13 +1,16 @@
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const [baseDirRelativePath] = process.argv.slice(2)
const [sourceDirRelativePath] = process.argv.slice(3)
// Base directory // Base directory
const baseDir = path.resolve(__dirname, '..') const baseDir = path.resolve(__dirname, baseDirRelativePath)
const sourceDir = path.join(baseDir, 'dist', 'exports') const sourceDir = path.join(baseDir, sourceDirRelativePath)
const targetDir = baseDir const targetDir = baseDir
// Helper function to read directories recursively and exclude .map files // 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 subDirs = fs.readdirSync(dir, { withFileTypes: true })
const files = subDirs.map((dirEntry) => { const files = subDirs.map((dirEntry) => {
const res = path.resolve(dir, dirEntry.name) const res = path.resolve(dir, dirEntry.name)
@@ -21,7 +24,7 @@ function getFiles (dir: string): string[] {
return Array.prototype.concat(...files) 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 parentDirReference = '../'.repeat(depth + 1) // +1 to account for the original reference
const replacementPrefix = (depth === 0 ? './' : '../'.repeat(depth)) + 'dist/' const replacementPrefix = (depth === 0 ? './' : '../'.repeat(depth)) + 'dist/'