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": {
"build": "pnpm copyfiles && pnpm build:swc && pnpm build:types && pnpm build:components && ts-node -T ./scripts/exportPointerFiles.ts",
"build": "pnpm copyfiles && pnpm build:swc && pnpm build:types && pnpm build:components && ts-node -T ../../scripts/exportPointerFiles.ts ../packages/payload dist/exports",
"build:components": "webpack --config dist/admin/components.config.js",
"build:swc": "swc ./src -d ./dist --config-file .swcrc",
"build:types": "tsc --emitDeclarationOnly --outDir dist",

View File

@@ -1,13 +1,34 @@
import React from 'react'
import React, { useMemo } from 'react'
import type { RichTextField } from '../../../../../fields/config/types'
import type { RichTextAdapter } from './types'
const RichText: React.FC<RichTextField> = (fieldprops) => {
// eslint-disable-next-line react/destructuring-assignment
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

View File

@@ -13,15 +13,11 @@ export type RichTextFieldProps<
path?: string
}
export type RichTextAdapter<
type RichTextAdapterBase<
Value extends object = object,
AdapterProps = any,
ExtraFieldProperties = {},
> = {
CellComponent: React.FC<
CellComponentProps<RichTextField<Value, AdapterProps, ExtraFieldProperties>>
>
FieldComponent: React.FC<RichTextFieldProps<Value, AdapterProps, ExtraFieldProperties>>
afterReadPromise?: ({
field,
incomingEditorState,
@@ -31,7 +27,6 @@ export type RichTextAdapter<
incomingEditorState: Value
siblingDoc: Record<string, unknown>
}) => Promise<void> | null
outputSchema?: ({
field,
isRequired,
@@ -59,3 +54,25 @@ export type RichTextAdapter<
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 { RichTextAdapter } from '../../../../../../forms/field-types/RichText/types'
@@ -7,9 +7,30 @@ import type { CellComponentProps } from '../../types'
const RichTextCell: React.FC<CellComponentProps<RichTextField>> = (props) => {
// eslint-disable-next-line react/destructuring-assignment
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

View File

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

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
.object()
.keys({
CellComponent: componentSchema.required(),
FieldComponent: componentSchema.required(),
CellComponent: componentSchema.optional(),
FieldComponent: componentSchema.optional(),
LazyCellComponent: joi.func().optional(),
LazyFieldComponent: joi.func().optional(),
afterReadPromise: joi.func().optional(),
outputSchema: joi.func().optional(),
populationPromise: joi.func().optional(),

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",
"types": "./dist/index.d.ts",
"scripts": {
"build": "pnpm copyfiles && pnpm build:swc && pnpm build:types",
"build": "pnpm copyfiles && pnpm build:swc && pnpm build:types && ts-node -T ../../scripts/exportPointerFiles.ts ../packages/richtext-lexical dist/exports",
"build:swc": "swc ./src -d ./dist --config-file .swcrc",
"build:types": "tsc --emitDeclarationOnly --outDir dist",
"build:clean": "find . \\( -type d \\( -name build -o -name dist -o -name .cache \\) -o -type f -name tsconfig.tsbuildinfo \\) -exec rm -rf {} + && pnpm build",
@@ -51,8 +51,14 @@
},
"exports": {
".": {
"default": "./src/index.ts",
"import": "./src/index.ts",
"require": "./src/index.ts",
"types": "./src/index.ts"
},
"./*": {
"import": "./src/exports/*.ts",
"require": "./src/exports/*.ts",
"types": "./src/exports/*.ts"
}
},
"publishConfig": {
@@ -62,6 +68,8 @@
"types": "./dist/index.d.ts"
},
"files": [
"dist"
"dist",
"components.js",
"components.d.ts"
]
}

View File

@@ -1,5 +1,6 @@
'use client'
import type { SerializedEditorState } from 'lexical'
import type { EditorConfig as LexicalEditorConfig } from 'lexical/LexicalEditor'
import type { CellComponentProps, RichTextField } from 'payload/types'
import { createHeadlessEditor } from '@lexical/headless'
@@ -50,11 +51,12 @@ export const RichTextCell: React.FC<
return
}
editorConfig.lexical().then((lexicalConfig: LexicalEditorConfig) => {
// initialize headless editor
const headlessEditor = createHeadlessEditor({
namespace: editorConfig.lexical.namespace,
namespace: lexicalConfig.namespace,
nodes: getEnabledNodes({ editorConfig }),
theme: editorConfig.lexical.theme,
theme: lexicalConfig.theme,
})
headlessEditor.setEditorState(headlessEditor.parseEditorState(dataToUse))
@@ -65,6 +67,7 @@ export const RichTextCell: React.FC<
// Limiting the number of characters shown is done in a CSS rule
setPreview(textContent)
})
}, [data, editorConfig])
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`}>
<Error message={errorMessage} showError={showError} />
<Label htmlFor={`field-${path.replace(/\./g, '__')}`} label={label} required={required} />
<ErrorBoundary fallbackRender={fallbackRender} onReset={(details) => {}}>
<ErrorBoundary fallbackRender={fallbackRender} onReset={() => {}}>
<LexicalProvider
editorConfig={editorConfig}
fieldProps={props}
onChange={(editorState, editor, tags) => {
onChange={(editorState) => {
let serializedEditorState = editorState.toJSON()
// 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 { SlashMenuOption } from '../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/types'
import { BlockquoteIcon } from '../../lexical/ui/icons/Blockquote'
import { TextDropdownSectionWithEntries } from '../common/floatingSelectToolbarTextDropdownSection'
import { convertLexicalNodesToHTML } from '../converters/html/converter'
import { MarkdownTransformer } from './markdownTransformer'
@@ -21,8 +20,12 @@ export const BlockQuoteFeature = (): FeatureProvider => {
sections: [
TextDropdownSectionWithEntries([
{
ChildComponent: BlockquoteIcon,
isActive: ({ editor, selection }) => false,
ChildComponent: () =>
// @ts-expect-error
import('../../lexical/ui/icons/Blockquote').then(
(module) => module.BlockquoteIcon,
),
isActive: () => false,
key: 'blockquote',
label: `Blockquote`,
onClick: ({ editor }) => {
@@ -70,7 +73,11 @@ export const BlockQuoteFeature = (): FeatureProvider => {
key: 'basic',
options: [
new SlashMenuOption(`blockquote`, {
Icon: BlockquoteIcon,
Icon: () =>
// @ts-expect-error
import('../../lexical/ui/icons/Blockquote').then(
(module) => module.BlockquoteIcon,
),
displayName: `Blockquote`,
keywords: ['quote', 'blockquote'],
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 { $createBlockNode } from '../nodes/BlocksNode'
import { INSERT_BLOCK_COMMAND } from '../plugin'
import './index.scss'
import { INSERT_BLOCK_COMMAND } from '../plugin/commands'
const baseClass = 'lexical-blocks-drawer'
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 editDepth = useEditDepth()
const { t } = useTranslation('fields')
const { closeModal, openModal } = useModal()
const { openModal } = useModal()
const labels = {
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 { SlashMenuOption } from '../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/types'
import { BlockIcon } from '../../lexical/ui/icons/Block'
import './index.scss'
import { BlockNode } from './nodes/BlocksNode'
import { BlocksPlugin, INSERT_BLOCK_COMMAND } from './plugin'
import { INSERT_BLOCK_COMMAND } from './plugin/commands'
import { blockPopulationPromiseHOC } from './populationPromise'
import { blockValidationHOC } from './validate'
@@ -43,7 +41,9 @@ export const BlocksFeature = (props?: BlocksFeatureProps): FeatureProvider => {
],
plugins: [
{
Component: BlocksPlugin,
Component: () =>
// @ts-expect-error
import('./plugin').then((module) => module.BlocksPlugin),
position: 'normal',
},
],
@@ -56,7 +56,9 @@ export const BlocksFeature = (props?: BlocksFeatureProps): FeatureProvider => {
options: [
...props.blocks.map((block) => {
return new SlashMenuOption('block-' + block.slug, {
Icon: BlockIcon,
Icon: () =>
// @ts-expect-error
import('../../lexical/ui/icons/Block').then((module) => module.BlockIcon),
displayName: ({ 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 React from 'react'
import { BlockComponent } from '../component'
import { transformInputFormData } from '../utils/transformInputFormData'
export type BlockFields = {
@@ -25,6 +24,13 @@ export type BlockFields = {
id: string
}
const BlockComponent = React.lazy(() =>
// @ts-expect-error TypeScript being dumb
import('../component').then((module) => ({
default: module.BlockComponent,
})),
)
export type SerializedBlockNode = Spread<
{
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'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
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 type { BlockFields } from '../nodes/BlocksNode'
import { BlocksDrawerComponent } from '../drawer'
import { $createBlockNode, BlockNode } from '../nodes/BlocksNode'
import { INSERT_BLOCK_COMMAND } from './commands'
export type InsertBlockPayload = Exclude<BlockFields, 'id'>
export const INSERT_BLOCK_COMMAND: LexicalCommand<InsertBlockPayload> =
createCommand('INSERT_BLOCK_COMMAND')
export function BlocksPlugin(): JSX.Element | null {
const [editor] = useLexicalComposerContext()

View File

@@ -1,5 +1,4 @@
import type { HeadingTagType, SerializedHeadingNode } from '@lexical/rich-text'
import type React from 'react'
import { $createHeadingNode, HeadingNode } from '@lexical/rich-text'
import { $setBlocksType } from '@lexical/selection'
@@ -9,12 +8,6 @@ import type { HTMLConverter } from '../converters/html/converter/types'
import type { FeatureProvider } from '../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 { convertLexicalNodesToHTML } from '../converters/html/converter'
import { MarkdownTransformer } from './markdownTransformer'
@@ -30,13 +23,19 @@ type Props = {
enabledHeadingSizes?: HeadingTagType[]
}
const HeadingToIconMap: Record<HeadingTagType, React.FC> = {
h1: H1Icon,
h2: H2Icon,
h3: H3Icon,
h4: H4Icon,
h5: H5Icon,
h6: H6Icon,
const iconImports = {
// @ts-expect-error
h1: () => import('../../lexical/ui/icons/H1').then((module) => module.H1Icon),
// @ts-expect-error
h2: () => import('../../lexical/ui/icons/H2').then((module) => module.H2Icon),
// @ts-expect-error
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 => {
@@ -50,7 +49,7 @@ export const HeadingFeature = (props: Props): FeatureProvider => {
...enabledHeadingSizes.map((headingSize, i) =>
TextDropdownSectionWithEntries([
{
ChildComponent: HeadingToIconMap[headingSize],
ChildComponent: iconImports[headingSize],
isActive: () => false,
key: headingSize,
label: `Heading ${headingSize.charAt(1)}`,
@@ -98,7 +97,7 @@ export const HeadingFeature = (props: Props): FeatureProvider => {
key: 'basic',
options: [
new SlashMenuOption(`heading-${headingSize.charAt(1)}`, {
Icon: HeadingToIconMap[headingSize],
Icon: iconImports[headingSize],
displayName: `Heading ${headingSize.charAt(1)}`,
keywords: ['heading', headingSize],
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 { $getSelection, $isRangeSelection } from 'lexical'
import { withMergedProps } from 'payload/utilities'
import type { HTMLConverter } from '../converters/html/converter/types'
import type { FeatureProvider } from '../types'
import type { SerializedAutoLinkNode } from './nodes/AutoLinkNode'
import type { LinkFields, SerializedLinkNode } from './nodes/LinkNode'
import { LinkIcon } from '../../lexical/ui/icons/Link'
import { getSelectedNode } from '../../lexical/utils/getSelectedNode'
import { FeaturesSectionWithEntries } from '../common/floatingSelectToolbarFeaturesButtonsSection'
import { convertLexicalNodesToHTML } from '../converters/html/converter'
import './index.scss'
import { AutoLinkNode } from './nodes/AutoLinkNode'
import { $isLinkNode, LinkNode, TOGGLE_LINK_COMMAND } from './nodes/LinkNode'
import { AutoLinkPlugin } from './plugins/autoLink'
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 { TOGGLE_LINK_WITH_MODAL_COMMAND } from './plugins/floatingLinkEditor/LinkEditor/commands'
import { linkPopulationPromiseHOC } from './populationPromise'
export type LinkFeatureProps = {
@@ -38,7 +31,9 @@ export const LinkFeature = (props: LinkFeatureProps): FeatureProvider => {
sections: [
FeaturesSectionWithEntries([
{
ChildComponent: LinkIcon,
ChildComponent: () =>
// @ts-expect-error
import('../../lexical/ui/icons/Link').then((module) => module.LinkIcon),
isActive: ({ selection }) => {
if ($isRangeSelection(selection)) {
const selectedNode = getSelectedNode(selection)
@@ -134,22 +129,35 @@ export const LinkFeature = (props: LinkFeatureProps): FeatureProvider => {
],
plugins: [
{
Component: LinkPlugin,
Component: () =>
// @ts-expect-error
import('./plugins/link').then((module) => module.LinkPlugin),
position: 'normal',
},
{
Component: AutoLinkPlugin,
Component: () =>
// @ts-expect-error
import('./plugins/autoLink').then((module) => module.AutoLinkPlugin),
position: 'normal',
},
{
Component: ClickableLinkPlugin,
Component: () =>
// @ts-expect-error
import('./plugins/clickableLink').then((module) => module.ClickableLinkPlugin),
position: 'normal',
},
{
Component: withMergedProps({
Component: FloatingLinkEditorPlugin,
Component: () =>
// @ts-expect-error
import('./plugins/floatingLinkEditor').then((module) => {
const floatingLinkEditorPlugin = module.FloatingLinkEditorPlugin
return import('payload/utilities').then((module) =>
module.withMergedProps({
Component: floatingLinkEditorPlugin,
toMergeIntoProps: props,
}),
)
}),
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'
import type { LexicalCommand } from 'lexical'
import type { Data, Fields } from 'payload/types'
import { useModal } from '@faceless-ui/modal'
@@ -12,7 +11,6 @@ import {
COMMAND_PRIORITY_LOW,
KEY_ESCAPE_COMMAND,
SELECTION_CHANGE_COMMAND,
createCommand,
} from 'lexical'
import { formatDrawerSlug } from 'payload/components/elements'
import {
@@ -38,10 +36,7 @@ import { setFloatingElemPositionForLinkEditor } from '../../../../../lexical/uti
import { LinkDrawer } from '../../../drawer'
import { $isLinkNode, TOGGLE_LINK_COMMAND } from '../../../nodes/LinkNode'
import { transformExtraFields } from '../utilities'
export const TOGGLE_LINK_WITH_MODAL_COMMAND: LexicalCommand<LinkPayload | null> = createCommand(
'TOGGLE_LINK_WITH_MODAL_COMMAND',
)
import { TOGGLE_LINK_WITH_MODAL_COMMAND } from './commands'
export function LinkEditor({
anchorElem,

View File

@@ -4,19 +4,20 @@ import { $createParagraphNode, $getSelection, $isRangeSelection } from 'lexical'
import type { FeatureProvider } from '../types'
import { SlashMenuOption } from '../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/types'
import { TextIcon } from '../../lexical/ui/icons/Text'
import { TextDropdownSectionWithEntries } from '../common/floatingSelectToolbarTextDropdownSection'
export const ParagraphFeature = (): FeatureProvider => {
return {
feature: ({ resolvedFeatures, unsanitizedEditorConfig }) => {
feature: () => {
return {
floatingSelectToolbar: {
sections: [
TextDropdownSectionWithEntries([
{
ChildComponent: TextIcon,
isActive: ({ editor, selection }) => false,
ChildComponent: () =>
// @ts-expect-error
import('../../lexical/ui/icons/Text').then((module) => module.TextIcon),
isActive: () => false,
key: 'normal-text',
label: 'Normal Text',
onClick: ({ editor }) => {
@@ -40,7 +41,9 @@ export const ParagraphFeature = (): FeatureProvider => {
key: 'basic',
options: [
new SlashMenuOption('paragraph', {
Icon: TextIcon,
Icon: () =>
// @ts-expect-error
import('../../lexical/ui/icons/Text').then((module) => module.TextIcon),
displayName: 'Paragraph',
keywords: ['normal', 'paragraph', 'p', 'text'],
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'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import {
$getNodeByKey,
COMMAND_PRIORITY_EDITOR,
type LexicalCommand,
type LexicalEditor,
createCommand,
} from 'lexical'
import { $getNodeByKey, COMMAND_PRIORITY_EDITOR, type LexicalEditor } from 'lexical'
import { useListDrawer } from 'payload/components/elements'
import React, { useCallback, useEffect, useState } from 'react'
import { $createRelationshipNode } from '../nodes/RelationshipNode'
import { INSERT_RELATIONSHIP_COMMAND } from '../plugins'
import { EnabledRelationshipsCondition } from '../utils/EnabledRelationshipsCondition'
import './index.scss'
import { INSERT_RELATIONSHIP_WITH_DRAWER_COMMAND } from './commands'
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 = ({
id,
editor,

View File

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

View File

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

View File

@@ -16,7 +16,12 @@ import {
} from '@lexical/react/LexicalDecoratorBlockNode'
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 = {
relationTo: string

View File

@@ -13,7 +13,7 @@ import { useTranslation } from 'react-i18next'
import type { RelationshipData } from '../RelationshipNode'
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 './index.scss'

View File

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

View File

@@ -18,7 +18,7 @@ import type { UploadData } from '../nodes/UploadNode'
import { useEditorConfigContext } from '../../../lexical/config/EditorConfigProvider'
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 './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'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import {
$getNodeByKey,
COMMAND_PRIORITY_EDITOR,
type LexicalCommand,
type LexicalEditor,
createCommand,
} from 'lexical'
import { $getNodeByKey, COMMAND_PRIORITY_EDITOR, type LexicalEditor } from 'lexical'
import { useListDrawer } from 'payload/components/elements'
import React, { useCallback, useEffect, useState } from 'react'
import { EnabledRelationshipsCondition } from '../../Relationship/utils/EnabledRelationshipsCondition'
import { $createUploadNode } from '../nodes/UploadNode'
import { INSERT_UPLOAD_COMMAND } from '../plugin'
import './index.scss'
import { INSERT_UPLOAD_WITH_DRAWER_COMMAND } from './commands'
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 = ({
id,
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 { SlashMenuOption } from '../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/types'
import { UploadIcon } from '../../lexical/ui/icons/Upload'
import { INSERT_UPLOAD_WITH_DRAWER_COMMAND } from './drawer'
import './index.scss'
import { INSERT_UPLOAD_WITH_DRAWER_COMMAND } from './drawer/commands'
import { UploadNode } from './nodes/UploadNode'
import { UploadPlugin } from './plugin'
import { uploadPopulationPromiseHOC } from './populationPromise'
import { uploadValidation } from './validate'
@@ -55,7 +52,9 @@ export const UploadFeature = (props?: UploadFeatureProps): FeatureProvider => {
],
plugins: [
{
Component: UploadPlugin,
Component: () =>
// @ts-expect-error
import('./plugin').then((module) => module.UploadPlugin),
position: 'normal',
},
],
@@ -67,7 +66,9 @@ export const UploadFeature = (props?: UploadFeatureProps): FeatureProvider => {
key: 'basic',
options: [
new SlashMenuOption('upload', {
Icon: UploadIcon,
Icon: () =>
// @ts-expect-error
import('../../lexical/ui/icons/Upload').then((module) => module.UploadIcon),
displayName: 'Upload',
keywords: ['upload', 'image', 'file', 'img', 'picture', 'photo', 'media'],
onSelect: ({ editor }) => {

View File

@@ -3,14 +3,13 @@ import type {
FloatingToolbarSectionEntry,
} from '../../lexical/plugins/FloatingSelectToolbar/types'
import { AlignLeftIcon } from '../../lexical/ui/icons/AlignLeft'
import './index.scss'
export const AlignDropdownSectionWithEntries = (
entries: FloatingToolbarSectionEntry[],
): FloatingToolbarSection => {
return {
ChildComponent: AlignLeftIcon,
ChildComponent: () =>
// @ts-expect-error
import('../../lexical/ui/icons/AlignLeft').then((module) => module.AlignLeftIcon),
entries,
key: 'dropdown-align',
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 { AlignCenterIcon } from '../../lexical/ui/icons/AlignCenter'
import { AlignLeftIcon } from '../../lexical/ui/icons/AlignLeft'
import { AlignDropdownSectionWithEntries } from './floatingSelectToolbarAlignDropdownSection'
import './index.scss'
export const AlignFeature = (): FeatureProvider => {
return {
feature: ({ resolvedFeatures, unsanitizedEditorConfig }) => {
feature: () => {
return {
floatingSelectToolbar: {
sections: [
AlignDropdownSectionWithEntries([
{
ChildComponent: AlignLeftIcon,
isActive: ({ editor, selection }) => false,
ChildComponent: () =>
// @ts-expect-error
import('../../lexical/ui/icons/AlignLeft').then((module) => module.AlignLeftIcon),
isActive: () => false,
key: 'align-left',
label: `Align Left`,
onClick: ({ editor }) => {
@@ -27,8 +26,12 @@ export const AlignFeature = (): FeatureProvider => {
]),
AlignDropdownSectionWithEntries([
{
ChildComponent: AlignCenterIcon,
isActive: ({ editor, selection }) => false,
ChildComponent: () =>
// @ts-expect-error
import('../../lexical/ui/icons/AlignCenter').then(
(module) => module.AlignCenterIcon,
),
isActive: () => false,
key: 'align-center',
label: `Align Center`,
onClick: ({ editor }) => {
@@ -39,8 +42,12 @@ export const AlignFeature = (): FeatureProvider => {
]),
AlignDropdownSectionWithEntries([
{
ChildComponent: AlignLeftIcon,
isActive: ({ editor, selection }) => false,
ChildComponent: () =>
// @ts-expect-error
import('../../lexical/ui/icons/AlignRight').then(
(module) => module.AlignRightIcon,
),
isActive: () => false,
key: 'align-right',
label: `Align Right`,
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,
} from '../../../lexical/plugins/FloatingSelectToolbar/types'
import './index.scss'
export const FeaturesSectionWithEntries = (
entries: FloatingToolbarSectionEntry[],
): 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,
} from '../../../lexical/plugins/FloatingSelectToolbar/types'
import { TextIcon } from '../../../lexical/ui/icons/Text'
import './index.scss'
export const TextDropdownSectionWithEntries = (
entries: FloatingToolbarSectionEntry[],
): FloatingToolbarSection => {
return {
ChildComponent: TextIcon,
ChildComponent: () =>
// @ts-expect-error
import('../../../lexical/ui/icons/Text').then((module) => module.TextIcon),
entries,
key: 'dropdown-text',
order: 1,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,8 +3,6 @@ import type {
FloatingToolbarSectionEntry,
} from '../../../lexical/plugins/FloatingSelectToolbar/types'
import './index.scss'
export const SectionWithEntries = (
entries: FloatingToolbarSectionEntry[],
): 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 { StrikethroughIcon } from '../../../lexical/ui/icons/Strikethrough'
import { SectionWithEntries } from '../common/floatingSelectToolbarSection'
import { STRIKETHROUGH } from './markdownTransformers'
export const StrikethroughTextFeature = (): FeatureProvider => {
return {
feature: ({ resolvedFeatures, unsanitizedEditorConfig }) => {
feature: () => {
return {
floatingSelectToolbar: {
sections: [
SectionWithEntries([
{
ChildComponent: StrikethroughIcon,
isActive: ({ editor, selection }) => {
ChildComponent: () =>
// @ts-expect-error
import('../../../lexical/ui/icons/Strikethrough').then(
(module) => module.StrikethroughIcon,
),
isActive: ({ selection }) => {
if ($isRangeSelection(selection)) {
return selection.hasFormat('strikethrough')
}

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

@@ -3,10 +3,8 @@ import { INSERT_UNORDERED_LIST_COMMAND, ListItemNode, ListNode } from '@lexical/
import type { FeatureProvider } from '../../types'
import { SlashMenuOption } from '../../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/types'
import { UnorderedListIcon } from '../../../lexical/ui/icons/UnorderedList'
import { TextDropdownSectionWithEntries } from '../../common/floatingSelectToolbarTextDropdownSection'
import { ListHTMLConverter, ListItemHTMLConverter } from '../htmlConverter'
import { LexicalListPlugin } from '../plugin'
import { UNORDERED_LIST } from './markdownTransformer'
export const UnorderedListFeature = (): FeatureProvider => {
@@ -17,7 +15,11 @@ export const UnorderedListFeature = (): FeatureProvider => {
sections: [
TextDropdownSectionWithEntries([
{
ChildComponent: UnorderedListIcon,
ChildComponent: () =>
// @ts-expect-error
import('../../../lexical/ui/icons/UnorderedList').then(
(module) => module.UnorderedListIcon,
),
isActive: () => false,
key: 'unorderedList',
label: `Unordered List`,
@@ -48,7 +50,9 @@ export const UnorderedListFeature = (): FeatureProvider => {
],
plugins: [
{
Component: LexicalListPlugin,
Component: () =>
// @ts-expect-error
import('../plugin').then((module) => module.LexicalListPlugin),
position: 'normal',
},
],
@@ -60,7 +64,11 @@ export const UnorderedListFeature = (): FeatureProvider => {
key: 'lists',
options: [
new SlashMenuOption('unorderedlist', {
Icon: UnorderedListIcon,
Icon: () =>
// @ts-expect-error
import('../../../lexical/ui/icons/UnorderedList').then(
(module) => module.UnorderedListIcon,
),
displayName: 'Unordered List',
keywords: ['unordered list', 'ul'],
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 { DecoratorNode, type EditorConfig, type LexicalNode, type NodeKey } from 'lexical'
import React from 'react'
import './index.scss'
import * as React from 'react'
export type UnknownConvertedNodeData = {
nodeData: unknown
@@ -18,6 +16,13 @@ export type SerializedUnknownConvertedNode = Spread<
SerializedLexicalNode
>
const Component = React.lazy(() =>
// @ts-expect-error TypeScript being dumb
import('./Component').then((module) => ({
default: module.UnknownConvertedNodeComponent,
})),
)
/** @noInheritDoc */
export class UnknownConvertedNode extends DecoratorNode<JSX.Element> {
__data: UnknownConvertedNodeData
@@ -58,11 +63,7 @@ export class UnknownConvertedNode extends DecoratorNode<JSX.Element> {
}
decorate(): JSX.Element | null {
return (
<div>
Unknown converted payload-plugin-lexical node: <strong>{this.__data?.nodeType}</strong>
</div>
)
return <Component data={this.__data} />
}
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 { DecoratorNode, type EditorConfig, type LexicalNode, type NodeKey } from 'lexical'
import React from 'react'
import './index.scss'
import * as React from 'react'
export type UnknownConvertedNodeData = {
nodeData: unknown
@@ -18,6 +16,13 @@ export type SerializedUnknownConvertedNode = Spread<
SerializedLexicalNode
>
const Component = React.lazy(() =>
// @ts-expect-error TypeScript being dumb
import('./Component').then((module) => ({
default: module.UnknownConvertedNodeComponent,
})),
)
/** @noInheritDoc */
export class UnknownConvertedNode extends DecoratorNode<JSX.Element> {
__data: UnknownConvertedNodeData
@@ -58,11 +63,7 @@ export class UnknownConvertedNode extends DecoratorNode<JSX.Element> {
}
decorate(): JSX.Element | null {
return (
<div>
Unknown converted Slate node: <strong>{this.__data?.nodeType}</strong>
</div>
)
return <Component data={this.__data} />
}
exportJSON(): SerializedUnknownConvertedNode {

View File

@@ -99,23 +99,23 @@ export type Feature = {
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
Component: React.FC
position: 'normal' // Determines at which position the Component will be added.
Component: () => Promise<React.FC<{ anchorElem: HTMLElement }>>
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: 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 }>
Component: () => Promise<React.FC>
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<{ anchorElem: HTMLElement }>
position: 'floatingAnchorElem' // Determines at which position the Component will be added.
Component: () => Promise<React.FC>
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 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<
Pick<ResolvedFeature, 'markdownTransformers' | 'nodes'>
> & {
@@ -200,33 +227,7 @@ export type SanitizedFeatures = Required<
}) => SerializedEditorState
>
}
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
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.
}
>
plugins?: Array<SanitizedPlugin>
/** The node types mapped to their populationPromises */
populationPromises: Map<string, Array<PopulationPromise>>
slashMenu: {

View File

@@ -7,8 +7,10 @@ import type { FieldProps } from '../types'
// @ts-expect-error Just TypeScript being broken // TODO: Open TypeScript issue
const RichTextEditor = lazy(() => import('./Field'))
export const RichTextField: React.FC<FieldProps> = (props) => (
export const RichTextField: React.FC<FieldProps> = (props) => {
return (
<Suspense fallback={<ShimmerEffect height="35vh" />}>
<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;
}
}
.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 { EditorPlugin } from './EditorPlugin'
import './LexicalEditor.scss'
import { FloatingSelectToolbarPlugin } from './plugins/FloatingSelectToolbar'
import { MarkdownShortcutPlugin } from './plugins/MarkdownShortcut'
@@ -53,7 +54,7 @@ export const LexicalEditor: React.FC<Pick<LexicalProviderProps, 'editorConfig' |
<React.Fragment>
{editorConfig.features.plugins.map((plugin) => {
if (plugin.position === 'top') {
return <plugin.Component key={plugin.key} />
return <EditorPlugin key={plugin.key} plugin={plugin} />
}
})}
<RichTextPlugin
@@ -65,10 +66,12 @@ export const LexicalEditor: React.FC<Pick<LexicalProviderProps, 'editorConfig' |
</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
// Selection changes can be ignore here, reducing the
// Selection changes can be ignored here, reducing the
// frequency that the FieldComponent and Payload receive updates.
// Selection changes are only needed if you are saving selection state
ignoreSelectionChange
@@ -92,7 +95,9 @@ export const LexicalEditor: React.FC<Pick<LexicalProviderProps, 'editorConfig' |
plugin.position === 'floatingAnchorElem' &&
!(plugin.desktopOnly === true && isSmallWidthViewport)
) {
return <plugin.Component anchorElem={floatingAnchorElem} key={plugin.key} />
return (
<EditorPlugin anchorElem={floatingAnchorElem} key={plugin.key} plugin={plugin} />
)
}
})}
{editor.isEditable() && (
@@ -113,12 +118,12 @@ export const LexicalEditor: React.FC<Pick<LexicalProviderProps, 'editorConfig' |
<TabIndentationPlugin />
{editorConfig.features.plugins.map((plugin) => {
if (plugin.position === 'normal') {
return <plugin.Component key={plugin.key} />
return <EditorPlugin key={plugin.key} plugin={plugin} />
}
})}
{editorConfig.features.plugins.map((plugin) => {
if (plugin.position === 'bottom') {
return <plugin.Component key={plugin.key} />
return <EditorPlugin key={plugin.key} plugin={plugin} />
}
})}
</React.Fragment>

View File

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

View File

@@ -1,5 +1,3 @@
import type { EditorConfig as LexicalEditorConfig } from 'lexical/LexicalEditor'
import type { FeatureProvider } from '../../features/types'
import type { EditorConfig, SanitizedEditorConfig } from './types'
@@ -21,7 +19,6 @@ import { IndentFeature } from '../../features/indent'
import { CheckListFeature } from '../../features/lists/CheckList'
import { OrderedListFeature } from '../../features/lists/OrderedList'
import { UnorderedListFeature } from '../../features/lists/UnorderedList'
import { LexicalEditorTheme } from '../theme/EditorTheme'
import { sanitizeEditorConfig } from './sanitize'
export const defaultEditorFeatures: FeatureProvider[] = [
@@ -46,14 +43,14 @@ export const defaultEditorFeatures: FeatureProvider[] = [
//BlocksFeature(), // Adding this by default makes no sense if no blocks are defined
]
export const defaultEditorLexicalConfig: LexicalEditorConfig = {
namespace: 'lexical',
theme: LexicalEditorTheme,
}
export const defaultEditorConfig: EditorConfig = {
features: defaultEditorFeatures,
lexical: defaultEditorLexicalConfig,
lexical: () =>
// @ts-expect-error
import('./defaultClient').then((module) => {
const defaultEditorLexicalConfig = module.defaultEditorLexicalConfig
return defaultEditorLexicalConfig
}),
}
export const defaultSanitizedEditorConfig: SanitizedEditorConfig =

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 = {
features: FeatureProvider[]
lexical: LexicalEditorConfig
lexical?: () => Promise<LexicalEditorConfig>
}
export type SanitizedEditorConfig = {
features: SanitizedFeatures
lexical: LexicalEditorConfig
lexical: () => Promise<LexicalEditorConfig>
resolvedFeatureMap: ResolvedFeatureMap
}

View File

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

View File

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

View File

@@ -1,8 +1,9 @@
import type { BaseSelection, LexicalEditor } from 'lexical'
import type React from 'react'
export type FloatingToolbarSection =
| {
ChildComponent?: React.FC
ChildComponent?: () => Promise<React.FC>
entries: Array<FloatingToolbarSectionEntry>
key: string
order?: number
@@ -16,13 +17,15 @@ export type FloatingToolbarSection =
}
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 */
Component?: React.FC<{
Component?: () => Promise<
React.FC<{
anchorElem: HTMLElement
editor: LexicalEditor
entry: FloatingToolbarSectionEntry
}>
>
isActive?: ({ editor, selection }: { editor: LexicalEditor; selection: BaseSelection }) => boolean
isEnabled?: ({
editor,

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,12 @@
const fs = require('fs')
const path = require('path')
const [baseDirRelativePath] = process.argv.slice(2)
const [sourceDirRelativePath] = process.argv.slice(3)
// Base directory
const baseDir = path.resolve(__dirname, '..')
const sourceDir = path.join(baseDir, 'dist', 'exports')
const baseDir = path.resolve(__dirname, baseDirRelativePath)
const sourceDir = path.join(baseDir, sourceDirRelativePath)
const targetDir = baseDir
// Helper function to read directories recursively and exclude .map files