diff --git a/packages/richtext-lexical/src/cell/index.tsx b/packages/richtext-lexical/src/cell/index.tsx index 2f52eec13d2..7131c976860 100644 --- a/packages/richtext-lexical/src/cell/index.tsx +++ b/packages/richtext-lexical/src/cell/index.tsx @@ -34,7 +34,9 @@ export const RichTextCell: React.FC< const clientFunctions = useClientFunctions() const [hasLoadedFeatures, setHasLoadedFeatures] = useState(false) - const [featureProviders, setFeatureProviders] = useState[]>([]) + const [featureProviders, setFeatureProviders] = useState< + FeatureProviderClient[] + >([]) const [finalSanitizedEditorConfig, setFinalSanitizedEditorConfig] = useState(null) @@ -55,7 +57,7 @@ export const RichTextCell: React.FC< useEffect(() => { if (!hasLoadedFeatures) { - const featureProvidersLocal: FeatureProviderClient[] = [] + const featureProvidersLocal: FeatureProviderClient[] = [] let featureProvidersAndComponentsLoaded = 0 // feature providers and components only Object.entries(clientFunctions).forEach(([key, plugin]) => { @@ -179,7 +181,7 @@ export const RichTextCell: React.FC< return FeatureComponent }) : null} - {featureProvider.ClientComponent} + {featureProvider.ClientFeature} ) })} diff --git a/packages/richtext-lexical/src/exports/client/index.ts b/packages/richtext-lexical/src/exports/client/index.ts index fcefc5c829b..50c273b7eae 100644 --- a/packages/richtext-lexical/src/exports/client/index.ts +++ b/packages/richtext-lexical/src/exports/client/index.ts @@ -2,32 +2,32 @@ 'use client' export { RichTextCell } from '../../cell/index.js' -export { AlignFeatureClientComponent } from '../../features/align/feature.client.js' -export { BlockquoteFeatureClientComponent } from '../../features/blockquote/feature.client.js' -export { BlocksFeatureClientComponent } from '../../features/blocks/feature.client.js' +export { AlignFeatureClient } from '../../features/align/feature.client.js' +export { BlockquoteFeatureClient } from '../../features/blockquote/feature.client.js' +export { BlocksFeatureClient } from '../../features/blocks/feature.client.js' export { createClientComponent } from '../../features/createClientComponent.js' -export { TestRecorderFeatureClientComponent } from '../../features/debug/testRecorder/feature.client.js' -export { TreeViewFeatureClientComponent } from '../../features/debug/treeView/feature.client.js' -export { BoldFeatureClientComponent } from '../../features/format/bold/feature.client.js' -export { InlineCodeFeatureClientComponent } from '../../features/format/inlineCode/feature.client.js' -export { ItalicFeatureClientComponent } from '../../features/format/italic/feature.client.js' +export { TestRecorderFeatureClient } from '../../features/debug/testRecorder/feature.client.js' +export { TreeViewFeatureClient } from '../../features/debug/treeView/feature.client.js' +export { BoldFeatureClient } from '../../features/format/bold/feature.client.js' +export { InlineCodeFeatureClient } from '../../features/format/inlineCode/feature.client.js' +export { ItalicFeatureClient } from '../../features/format/italic/feature.client.js' export { toolbarFormatGroupWithItems } from '../../features/format/shared/toolbarFormatGroup.js' -export { StrikethroughFeatureClientComponent } from '../../features/format/strikethrough/feature.client.js' -export { SubscriptFeatureClientComponent } from '../../features/format/subscript/feature.client.js' -export { SuperscriptFeatureClientComponent } from '../../features/format/superscript/feature.client.js' -export { UnderlineFeatureClientComponent } from '../../features/format/underline/feature.client.js' -export { HeadingFeatureClientComponent } from '../../features/heading/feature.client.js' -export { HorizontalRuleFeatureClientComponent } from '../../features/horizontalRule/feature.client.js' -export { IndentFeatureClientComponent } from '../../features/indent/feature.client.js' -export { LinkFeatureClientComponent } from '../../features/link/feature.client.js' -export { ChecklistFeatureClientComponent } from '../../features/lists/checklist/feature.client.js' -export { OrderedListFeatureClientComponent } from '../../features/lists/orderedList/feature.client.js' -export { UnorderedListFeatureClientComponent } from '../../features/lists/unorderedList/feature.client.js' -export { LexicalPluginToLexicalFeatureClientComponent } from '../../features/migrations/lexicalPluginToLexical/feature.client.js' -export { SlateToLexicalFeatureClientComponent } from '../../features/migrations/slateToLexical/feature.client.js' -export { ParagraphFeatureClientComponent } from '../../features/paragraph/feature.client.js' +export { StrikethroughFeatureClient } from '../../features/format/strikethrough/feature.client.js' +export { SubscriptFeatureClient } from '../../features/format/subscript/feature.client.js' +export { SuperscriptFeatureClient } from '../../features/format/superscript/feature.client.js' +export { UnderlineFeatureClient } from '../../features/format/underline/feature.client.js' +export { HeadingFeatureClient } from '../../features/heading/feature.client.js' +export { HorizontalRuleFeatureClient } from '../../features/horizontalRule/feature.client.js' +export { IndentFeatureClient } from '../../features/indent/feature.client.js' +export { LinkFeatureClient } from '../../features/link/feature.client.js' +export { ChecklistFeatureClient } from '../../features/lists/checklist/feature.client.js' +export { OrderedListFeatureClient } from '../../features/lists/orderedList/feature.client.js' +export { UnorderedListFeatureClient } from '../../features/lists/unorderedList/feature.client.js' +export { LexicalPluginToLexicalFeatureClient } from '../../features/migrations/lexicalPluginToLexical/feature.client.js' +export { SlateToLexicalFeatureClient } from '../../features/migrations/slateToLexical/feature.client.js' +export { ParagraphFeatureClient } from '../../features/paragraph/feature.client.js' -export { RelationshipFeatureClientComponent } from '../../features/relationship/feature.client.js' +export { RelationshipFeatureClient } from '../../features/relationship/feature.client.js' export { toolbarAddDropdownGroupWithItems } from '../../features/shared/toolbar/addDropdownGroup.js' export { toolbarFeatureButtonsGroupWithItems } from '../../features/shared/toolbar/featureButtonsGroup.js' diff --git a/packages/richtext-lexical/src/features/align/feature.client.tsx b/packages/richtext-lexical/src/features/align/feature.client.tsx index 45166af2f10..f760673a2d0 100644 --- a/packages/richtext-lexical/src/features/align/feature.client.tsx +++ b/packages/richtext-lexical/src/features/align/feature.client.tsx @@ -3,13 +3,12 @@ import { $isElementNode, $isRangeSelection, FORMAT_ELEMENT_COMMAND } from 'lexical' import type { ToolbarGroup } from '../toolbars/types.js' -import type { FeatureProviderProviderClient } from '../types.js' import { AlignCenterIcon } from '../../lexical/ui/icons/AlignCenter/index.js' import { AlignJustifyIcon } from '../../lexical/ui/icons/AlignJustify/index.js' import { AlignLeftIcon } from '../../lexical/ui/icons/AlignLeft/index.js' import { AlignRightIcon } from '../../lexical/ui/icons/AlignRight/index.js' -import { createClientComponent } from '../createClientComponent.js' +import { createClientFeature } from '../../utilities/createClientFeature.js' import { toolbarAlignGroupWithItems } from './toolbarAlignGroup.js' const toolbarGroups: ToolbarGroup[] = [ @@ -149,19 +148,11 @@ const toolbarGroups: ToolbarGroup[] = [ ]), ] -const AlignFeatureClient: FeatureProviderProviderClient = (props) => { - return { - clientFeatureProps: props, - feature: () => ({ - clientFeatureProps: props, - toolbarFixed: { - groups: toolbarGroups, - }, - toolbarInline: { - groups: toolbarGroups, - }, - }), - } -} - -export const AlignFeatureClientComponent = createClientComponent(AlignFeatureClient) +export const AlignFeatureClient = createClientFeature({ + toolbarFixed: { + groups: toolbarGroups, + }, + toolbarInline: { + groups: toolbarGroups, + }, +}) diff --git a/packages/richtext-lexical/src/features/align/feature.server.ts b/packages/richtext-lexical/src/features/align/feature.server.ts index b8ef78201f7..c487573c73d 100644 --- a/packages/richtext-lexical/src/features/align/feature.server.ts +++ b/packages/richtext-lexical/src/features/align/feature.server.ts @@ -1,20 +1,12 @@ -import type { FeatureProviderProviderServer } from '../types.js' - // eslint-disable-next-line payload/no-imports-from-exports-dir -import { AlignFeatureClientComponent } from '../../exports/client/index.js' +import { AlignFeatureClient } from '../../exports/client/index.js' +import { createServerFeature } from '../../utilities/createServerFeature.js' import { i18n } from './i18n.js' -export const AlignFeature: FeatureProviderProviderServer = (props) => { - return { - feature: () => { - return { - ClientComponent: AlignFeatureClientComponent, - clientFeatureProps: null, - i18n, - serverFeatureProps: props, - } - }, - key: 'align', - serverFeatureProps: props, - } -} +export const AlignFeature = createServerFeature({ + feature: { + ClientFeature: AlignFeatureClient, + i18n, + }, + key: 'align', +}) diff --git a/packages/richtext-lexical/src/features/blockquote/feature.client.tsx b/packages/richtext-lexical/src/features/blockquote/feature.client.tsx index 1591fd9a228..1d26c1f267e 100644 --- a/packages/richtext-lexical/src/features/blockquote/feature.client.tsx +++ b/packages/richtext-lexical/src/features/blockquote/feature.client.tsx @@ -5,10 +5,9 @@ import { $setBlocksType } from '@lexical/selection' import { $getSelection, $isRangeSelection } from 'lexical' import type { ToolbarGroup } from '../toolbars/types.js' -import type { FeatureProviderProviderClient } from '../types.js' import { BlockquoteIcon } from '../../lexical/ui/icons/Blockquote/index.js' -import { createClientComponent } from '../createClientComponent.js' +import { createClientFeature } from '../../utilities/createClientFeature.js' import { slashMenuBasicGroupWithItems } from '../shared/slashMenu/basicGroup.js' import { toolbarTextDropdownGroupWithItems } from '../shared/toolbar/textDropdownGroup.js' import { MarkdownTransformer } from './markdownTransformer.js' @@ -43,42 +42,34 @@ const toolbarGroups: ToolbarGroup[] = [ ]), ] -const BlockquoteFeatureClient: FeatureProviderProviderClient = (props) => { - return { - clientFeatureProps: props, - feature: () => ({ - clientFeatureProps: props, - markdownTransformers: [MarkdownTransformer], - nodes: [QuoteNode], +export const BlockquoteFeatureClient = createClientFeature({ + markdownTransformers: [MarkdownTransformer], + nodes: [QuoteNode], - slashMenu: { - groups: [ - slashMenuBasicGroupWithItems([ - { - Icon: BlockquoteIcon, - key: 'blockquote', - keywords: ['quote', 'blockquote'], - label: ({ i18n }) => { - return i18n.t('lexical:blockquote:label') - }, - onSelect: ({ editor }) => { - editor.update(() => { - const selection = $getSelection() - $setBlocksType(selection, () => $createQuoteNode()) - }) - }, - }, - ]), - ], - }, - toolbarFixed: { - groups: toolbarGroups, - }, - toolbarInline: { - groups: toolbarGroups, - }, - }), - } -} - -export const BlockquoteFeatureClientComponent = createClientComponent(BlockquoteFeatureClient) + slashMenu: { + groups: [ + slashMenuBasicGroupWithItems([ + { + Icon: BlockquoteIcon, + key: 'blockquote', + keywords: ['quote', 'blockquote'], + label: ({ i18n }) => { + return i18n.t('lexical:blockquote:label') + }, + onSelect: ({ editor }) => { + editor.update(() => { + const selection = $getSelection() + $setBlocksType(selection, () => $createQuoteNode()) + }) + }, + }, + ]), + ], + }, + toolbarFixed: { + groups: toolbarGroups, + }, + toolbarInline: { + groups: toolbarGroups, + }, +}) diff --git a/packages/richtext-lexical/src/features/blockquote/feature.server.ts b/packages/richtext-lexical/src/features/blockquote/feature.server.ts index 51afb075361..68a78d8dcc5 100644 --- a/packages/richtext-lexical/src/features/blockquote/feature.server.ts +++ b/packages/richtext-lexical/src/features/blockquote/feature.server.ts @@ -1,49 +1,42 @@ import { QuoteNode } from '@lexical/rich-text' -import type { FeatureProviderProviderServer } from '../types.js' - // eslint-disable-next-line payload/no-imports-from-exports-dir -import { BlockquoteFeatureClientComponent } from '../../exports/client/index.js' +import { BlockquoteFeatureClient } from '../../exports/client/index.js' +import { createServerFeature } from '../../utilities/createServerFeature.js' import { convertLexicalNodesToHTML } from '../converters/html/converter/index.js' import { createNode } from '../typeUtilities.js' import { i18n } from './i18n.js' import { MarkdownTransformer } from './markdownTransformer.js' -export const BlockquoteFeature: FeatureProviderProviderServer = (props) => { - return { - feature: () => { - return { - ClientComponent: BlockquoteFeatureClientComponent, - clientFeatureProps: null, - i18n, - markdownTransformers: [MarkdownTransformer], - nodes: [ - createNode({ - converters: { - html: { - converter: async ({ converters, node, parent, req }) => { - const childrenText = await convertLexicalNodesToHTML({ - converters, - lexicalNodes: node.children, - parent: { - ...node, - parent, - }, - req, - }) - - return `
${childrenText}
` +export const BlockquoteFeature = createServerFeature({ + feature: { + ClientFeature: BlockquoteFeatureClient, + clientFeatureProps: null, + i18n, + markdownTransformers: [MarkdownTransformer], + nodes: [ + createNode({ + converters: { + html: { + converter: async ({ converters, node, parent, req }) => { + const childrenText = await convertLexicalNodesToHTML({ + converters, + lexicalNodes: node.children, + parent: { + ...node, + parent, }, - nodeTypes: [QuoteNode.getType()], - }, + req, + }) + + return `
${childrenText}
` }, - node: QuoteNode, - }), - ], - serverFeatureProps: props, - } - }, - key: 'blockquote', - serverFeatureProps: props, - } -} + nodeTypes: [QuoteNode.getType()], + }, + }, + node: QuoteNode, + }), + ], + }, + key: 'blockquote', +}) diff --git a/packages/richtext-lexical/src/features/blocks/component/index.tsx b/packages/richtext-lexical/src/features/blocks/component/index.tsx index 42a2211b17c..c826d3d8280 100644 --- a/packages/richtext-lexical/src/features/blocks/component/index.tsx +++ b/packages/richtext-lexical/src/features/blocks/component/index.tsx @@ -61,7 +61,7 @@ export const BlockComponent: React.FC = (props) => { const reducedBlock: ReducedBlock = ( editorConfig?.resolvedFeatureMap?.get('blocks') - ?.clientFeatureProps as ClientComponentProps + ?.sanitizedClientFeatureProps as ClientComponentProps )?.reducedBlocks?.find((block) => block.slug === formData?.blockType) const fieldMap = richTextComponentMap.get(componentMapRenderedFieldsPath) diff --git a/packages/richtext-lexical/src/features/blocks/drawer/index.tsx b/packages/richtext-lexical/src/features/blocks/drawer/index.tsx index face1f8581f..9c5c1de0c8b 100644 --- a/packages/richtext-lexical/src/features/blocks/drawer/index.tsx +++ b/packages/richtext-lexical/src/features/blocks/drawer/index.tsx @@ -87,7 +87,7 @@ export const BlocksDrawerComponent: React.FC = () => { const reducedBlocks = ( editorConfig?.resolvedFeatureMap?.get('blocks') - ?.clientFeatureProps as ClientComponentProps + ?.sanitizedClientFeatureProps as ClientComponentProps )?.reducedBlocks useEffect(() => { diff --git a/packages/richtext-lexical/src/features/blocks/feature.client.tsx b/packages/richtext-lexical/src/features/blocks/feature.client.tsx index b462652cf62..6a518f97288 100644 --- a/packages/richtext-lexical/src/features/blocks/feature.client.tsx +++ b/packages/richtext-lexical/src/features/blocks/feature.client.tsx @@ -4,10 +4,8 @@ import type { ReducedBlock } from '@payloadcms/ui/utilities/buildComponentMap' import { getTranslation } from '@payloadcms/translations' -import type { FeatureProviderProviderClient } from '../types.js' - import { BlockIcon } from '../../lexical/ui/icons/Block/index.js' -import { createClientComponent } from '../createClientComponent.js' +import { createClientFeature } from '../../utilities/createClientFeature.js' import { BlockNode } from './nodes/BlocksNode.js' import { INSERT_BLOCK_COMMAND } from './plugin/commands.js' import { BlocksPlugin } from './plugin/index.js' @@ -16,83 +14,76 @@ export type BlocksFeatureClientProps = { reducedBlocks: ReducedBlock[] } -const BlocksFeatureClient: FeatureProviderProviderClient = (props) => { - return { - clientFeatureProps: props, - feature: () => ({ - clientFeatureProps: props, - nodes: [BlockNode], - plugins: [ - { - Component: BlocksPlugin, - position: 'normal', - }, - ], - slashMenu: { - groups: [ - { - items: props.reducedBlocks.map((block) => { - return { - Icon: BlockIcon, - key: 'block-' + block.slug, - keywords: ['block', 'blocks', block.slug], - label: ({ i18n }) => { - if (!block.labels.singular) { - return block.slug - } - - return getTranslation(block.labels.singular, i18n) - }, - onSelect: ({ editor }) => { - editor.dispatchCommand(INSERT_BLOCK_COMMAND, { - id: null, - blockName: '', - blockType: block.slug, - }) - }, - } - }), - key: 'blocks', +export const BlocksFeatureClient = createClientFeature(({ props }) => ({ + nodes: [BlockNode], + plugins: [ + { + Component: BlocksPlugin, + position: 'normal', + }, + ], + sanitizedClientFeatureProps: props, + slashMenu: { + groups: [ + { + items: props.reducedBlocks.map((block) => { + return { + Icon: BlockIcon, + key: 'block-' + block.slug, + keywords: ['block', 'blocks', block.slug], label: ({ i18n }) => { - return i18n.t('lexical:blocks:label') + if (!block.labels.singular) { + return block.slug + } + + return getTranslation(block.labels.singular, i18n) }, - }, - ], + onSelect: ({ editor }) => { + editor.dispatchCommand(INSERT_BLOCK_COMMAND, { + id: null, + blockName: '', + blockType: block.slug, + }) + }, + } + }), + key: 'blocks', + label: ({ i18n }) => { + return i18n.t('lexical:blocks:label') + }, }, - toolbarFixed: { - groups: [ - { - type: 'dropdown', + ], + }, + toolbarFixed: { + groups: [ + { + type: 'dropdown', + ChildComponent: BlockIcon, + items: props.reducedBlocks.map((block, index) => { + return { ChildComponent: BlockIcon, - items: props.reducedBlocks.map((block, index) => { - return { - ChildComponent: BlockIcon, - isActive: undefined, // At this point, we would be inside a sub-richtext-editor. And at this point this will be run against the focused sub-editor, not the parent editor which has the actual block. Thus, no point in running this - key: 'block-' + block.slug, - label: ({ i18n }) => { - if (!block.labels.singular) { - return block.slug - } - - return getTranslation(block.labels.singular, i18n) - }, - onSelect: ({ editor }) => { - editor.dispatchCommand(INSERT_BLOCK_COMMAND, { - id: null, - blockName: '', - blockType: block.slug, - }) - }, - order: index, + isActive: undefined, // At this point, we would be inside a sub-richtext-editor. And at this point this will be run against the focused sub-editor, not the parent editor which has the actual block. Thus, no point in running this + key: 'block-' + block.slug, + label: ({ i18n }) => { + if (!block.labels.singular) { + return block.slug } - }), - key: 'blocks', - order: 20, - }, - ], - }, - }), - } -} -export const BlocksFeatureClientComponent = createClientComponent(BlocksFeatureClient) + return getTranslation(block.labels.singular, i18n) + }, + onSelect: ({ editor }) => { + editor.dispatchCommand(INSERT_BLOCK_COMMAND, { + id: null, + blockName: '', + blockType: block.slug, + }) + }, + order: index, + } + }), + key: 'blocks', + order: 20, + }, + ], + }, +})) diff --git a/packages/richtext-lexical/src/features/blocks/feature.server.ts b/packages/richtext-lexical/src/features/blocks/feature.server.ts index 62d7db41ba5..364967b94c7 100644 --- a/packages/richtext-lexical/src/features/blocks/feature.server.ts +++ b/packages/richtext-lexical/src/features/blocks/feature.server.ts @@ -3,11 +3,11 @@ import type { Block, BlockField, Config, Field } from 'payload' import { traverseFields } from '@payloadcms/ui/utilities/buildFieldSchemaMap/traverseFields' import { baseBlockFields, fieldsToJSONSchema, formatLabels, sanitizeFields } from 'payload' -import type { FeatureProviderProviderServer } from '../types.js' import type { BlocksFeatureClientProps } from './feature.client.js' // eslint-disable-next-line payload/no-imports-from-exports-dir -import { BlocksFeatureClientComponent } from '../../exports/client/index.js' +import { BlocksFeatureClient } from '../../exports/client/index.js' +import { createServerFeature } from '../../utilities/createServerFeature.js' import { createNode } from '../typeUtilities.js' import { blockPopulationPromiseHOC } from './graphQLPopulationPromise.js' import { i18n } from './i18n.js' @@ -18,119 +18,117 @@ export type BlocksFeatureProps = { blocks: Block[] } -export const BlocksFeature: FeatureProviderProviderServer< +export const BlocksFeature = createServerFeature< + BlocksFeatureProps, BlocksFeatureProps, BlocksFeatureClientProps -> = (props) => { - return { - feature: async ({ config: _config, isRoot }) => { - if (props?.blocks?.length) { - const validRelationships = _config.collections.map((c) => c.slug) || [] +>({ + feature: async ({ config: _config, isRoot, props }) => { + if (props?.blocks?.length) { + const validRelationships = _config.collections.map((c) => c.slug) || [] + + for (const block of props.blocks) { + block.fields = block.fields.concat(baseBlockFields) + block.labels = !block.labels ? formatLabels(block.slug) : block.labels + + block.fields = await sanitizeFields({ + config: _config as unknown as Config, + fields: block.fields, + requireFieldLevelRichTextEditor: isRoot, + validRelationships, + }) + } + } + + // Build clientProps + const clientProps: BlocksFeatureClientProps = { + reducedBlocks: [], + } + for (const block of props.blocks) { + clientProps.reducedBlocks.push({ + slug: block.slug, + fieldMap: [], + imageAltText: block.imageAltText, + imageURL: block.imageURL, + labels: block.labels, + }) + } + + return { + ClientFeature: BlocksFeatureClient, + clientFeatureProps: clientProps, + generateSchemaMap: ({ config, i18n, props }) => { + const validRelationships = config.collections.map((c) => c.slug) || [] + + /** + * Add sub-fields to the schemaMap. E.g. if you have an array field as part of the block, and it runs addRow, it will request these + * sub-fields from the component map. Thus, we need to put them in the component map here. + */ + const schemaMap = new Map() for (const block of props.blocks) { - block.fields = block.fields.concat(baseBlockFields) - block.labels = !block.labels ? formatLabels(block.slug) : block.labels + schemaMap.set(block.slug, block.fields || []) - block.fields = await sanitizeFields({ - config: _config as unknown as Config, + traverseFields({ + config, fields: block.fields, - requireFieldLevelRichTextEditor: isRoot, + i18n, + schemaMap, + schemaPath: block.slug, validRelationships, }) } - } - - // Build clientProps - const clientProps: BlocksFeatureClientProps = { - reducedBlocks: [], - } - for (const block of props.blocks) { - clientProps.reducedBlocks.push({ - slug: block.slug, - fieldMap: [], - imageAltText: block.imageAltText, - imageURL: block.imageURL, - labels: block.labels, - }) - } - return { - ClientComponent: BlocksFeatureClientComponent, - clientFeatureProps: clientProps, - generateSchemaMap: ({ config, i18n, props }) => { - const validRelationships = config.collections.map((c) => c.slug) || [] - - /** - * Add sub-fields to the schemaMap. E.g. if you have an array field as part of the block, and it runs addRow, it will request these - * sub-fields from the component map. Thus, we need to put them in the component map here. - */ - const schemaMap = new Map() - - for (const block of props.blocks) { - schemaMap.set(block.slug, block.fields || []) - - traverseFields({ - config, - fields: block.fields, - i18n, - schemaMap, - schemaPath: block.slug, - validRelationships, - }) + return schemaMap + }, + generatedTypes: { + modifyOutputSchema: ({ + collectionIDFieldTypes, + config, + currentSchema, + field, + interfaceNameDefinitions, + }) => { + if (!props?.blocks?.length) { + return currentSchema } - return schemaMap - }, - generatedTypes: { - modifyOutputSchema: ({ + const blocksField: BlockField = { + name: field?.name + '_lexical_blocks', + type: 'blocks', + blocks: props.blocks, + } + // This is only done so that interfaceNameDefinitions sets those block's interfaceNames. + // we don't actually use the JSON Schema itself in the generated types yet. + fieldsToJSONSchema( collectionIDFieldTypes, - config, - currentSchema, - field, + [blocksField], interfaceNameDefinitions, - }) => { - if (!props?.blocks?.length) { - return currentSchema - } - - const blocksField: BlockField = { - name: field?.name + '_lexical_blocks', - type: 'blocks', - blocks: props.blocks, - } - // This is only done so that interfaceNameDefinitions sets those block's interfaceNames. - // we don't actually use the JSON Schema itself in the generated types yet. - fieldsToJSONSchema( - collectionIDFieldTypes, - [blocksField], - interfaceNameDefinitions, - config, - ) + config, + ) - return currentSchema - }, + return currentSchema }, - i18n, - nodes: [ - createNode({ - getSubFields: ({ node, req }) => { - const blockType = node.fields.blockType - - const block = props.blocks.find((block) => block.slug === blockType) - return block?.fields - }, - getSubFieldsData: ({ node }) => { - return node?.fields - }, - graphQLPopulationPromises: [blockPopulationPromiseHOC(props)], - node: BlockNode, - validations: [blockValidationHOC(props)], - }), - ], - serverFeatureProps: props, - } - }, - key: 'blocks', - serverFeatureProps: props, - } -} + }, + i18n, + nodes: [ + createNode({ + getSubFields: ({ node }) => { + const blockType = node.fields.blockType + + const block = props.blocks.find((block) => block.slug === blockType) + return block?.fields + }, + getSubFieldsData: ({ node }) => { + return node?.fields + }, + graphQLPopulationPromises: [blockPopulationPromiseHOC(props)], + node: BlockNode, + validations: [blockValidationHOC(props)], + }), + ], + sanitizedServerFeatureProps: props, + } + }, + key: 'blocks', +}) diff --git a/packages/richtext-lexical/src/features/converters/html/feature.server.ts b/packages/richtext-lexical/src/features/converters/html/feature.server.ts index 856f911874b..5baa8ce289f 100644 --- a/packages/richtext-lexical/src/features/converters/html/feature.server.ts +++ b/packages/richtext-lexical/src/features/converters/html/feature.server.ts @@ -1,24 +1,15 @@ -import type { FeatureProviderProviderServer } from '../../types.js' import type { HTMLConverter } from './converter/types.js' +import { createServerFeature } from '../../../utilities/createServerFeature.js' + export type HTMLConverterFeatureProps = { converters?: | (({ defaultConverters }: { defaultConverters: HTMLConverter[] }) => HTMLConverter[]) | HTMLConverter[] } -export const HTMLConverterFeature: FeatureProviderProviderServer< - HTMLConverterFeatureProps, - undefined -> = (props) => { - return { - feature: () => { - return { - clientFeatureProps: null, - serverFeatureProps: props, - } - }, - key: 'htmlConverter', - serverFeatureProps: props, - } -} +// This is just used to save the props on the richText field +export const HTMLConverterFeature = createServerFeature({ + feature: {}, + key: 'htmlConverter', +}) diff --git a/packages/richtext-lexical/src/features/converters/html/field/index.ts b/packages/richtext-lexical/src/features/converters/html/field/index.ts index 69c8d7cc4f9..b6f12503057 100644 --- a/packages/richtext-lexical/src/features/converters/html/field/index.ts +++ b/packages/richtext-lexical/src/features/converters/html/field/index.ts @@ -37,7 +37,7 @@ export const consolidateHTMLConverters = ({ }): HTMLConverter[] => { const htmlConverterFeature = editorConfig.resolvedFeatureMap.get('htmlConverter') const htmlConverterFeatureProps: HTMLConverterFeatureProps = - htmlConverterFeature?.serverFeatureProps + htmlConverterFeature?.sanitizedServerFeatureProps const defaultConvertersWithConvertersFromFeatures = [...defaultHTMLConverters] diff --git a/packages/richtext-lexical/src/features/createClientComponent.tsx b/packages/richtext-lexical/src/features/createClientComponent.tsx index c7902c1bcef..b5c9a01685d 100644 --- a/packages/richtext-lexical/src/features/createClientComponent.tsx +++ b/packages/richtext-lexical/src/features/createClientComponent.tsx @@ -9,7 +9,7 @@ import { useLexicalFeature } from '../utilities/useLexicalFeature.js' */ export const createClientComponent = ( clientFeature: FeatureProviderProviderClient, -): ServerFeature['ClientComponent'] => { +): ServerFeature['ClientFeature'] => { return (props) => { useLexicalFeature(props.featureKey, clientFeature(props)) return null diff --git a/packages/richtext-lexical/src/features/debug/testRecorder/feature.client.tsx b/packages/richtext-lexical/src/features/debug/testRecorder/feature.client.tsx index 46e3796e822..f0ef987e272 100644 --- a/packages/richtext-lexical/src/features/debug/testRecorder/feature.client.tsx +++ b/packages/richtext-lexical/src/features/debug/testRecorder/feature.client.tsx @@ -1,22 +1,13 @@ 'use client' -import type { FeatureProviderProviderClient } from '../../types.js' -import { createClientComponent } from '../../createClientComponent.js' +import { createClientFeature } from '../../../utilities/createClientFeature.js' import { TestRecorderPlugin } from './plugin/index.js' -const TestRecorderFeatureClient: FeatureProviderProviderClient = (props) => { - return { - clientFeatureProps: props, - feature: () => ({ - clientFeatureProps: props, - plugins: [ - { - Component: TestRecorderPlugin, - position: 'bottom', - }, - ], - }), - } -} - -export const TestRecorderFeatureClientComponent = createClientComponent(TestRecorderFeatureClient) +export const TestRecorderFeatureClient = createClientFeature({ + plugins: [ + { + Component: TestRecorderPlugin, + position: 'bottom', + }, + ], +}) diff --git a/packages/richtext-lexical/src/features/debug/testRecorder/feature.server.ts b/packages/richtext-lexical/src/features/debug/testRecorder/feature.server.ts index 4c2bd05e173..1f663e182b4 100644 --- a/packages/richtext-lexical/src/features/debug/testRecorder/feature.server.ts +++ b/packages/richtext-lexical/src/features/debug/testRecorder/feature.server.ts @@ -1,17 +1,10 @@ -import type { FeatureProviderProviderServer } from '../../types.js' - // eslint-disable-next-line payload/no-imports-from-exports-dir -import { TestRecorderFeatureClientComponent } from '../../../exports/client/index.js' +import { TestRecorderFeatureClient } from '../../../exports/client/index.js' +import { createServerFeature } from '../../../utilities/createServerFeature.js' -export const TestRecorderFeature: FeatureProviderProviderServer = (props) => { - return { - feature: () => { - return { - ClientComponent: TestRecorderFeatureClientComponent, - serverFeatureProps: props, - } - }, - key: 'testRecorder', - serverFeatureProps: props, - } -} +export const TestRecorderFeature = createServerFeature({ + feature: { + ClientFeature: TestRecorderFeatureClient, + }, + key: 'testRecorder', +}) diff --git a/packages/richtext-lexical/src/features/debug/treeView/feature.client.tsx b/packages/richtext-lexical/src/features/debug/treeView/feature.client.tsx index a27583e1679..2ab028d0dd3 100644 --- a/packages/richtext-lexical/src/features/debug/treeView/feature.client.tsx +++ b/packages/richtext-lexical/src/features/debug/treeView/feature.client.tsx @@ -1,22 +1,13 @@ 'use client' -import type { FeatureProviderProviderClient } from '../../types.js' -import { createClientComponent } from '../../createClientComponent.js' +import { createClientFeature } from '../../../utilities/createClientFeature.js' import { TreeViewPlugin } from './plugin/index.js' -const TreeViewFeatureClient: FeatureProviderProviderClient = (props) => { - return { - clientFeatureProps: props, - feature: () => ({ - clientFeatureProps: props, - plugins: [ - { - Component: TreeViewPlugin, - position: 'bottom', - }, - ], - }), - } -} - -export const TreeViewFeatureClientComponent = createClientComponent(TreeViewFeatureClient) +export const TreeViewFeatureClient = createClientFeature({ + plugins: [ + { + Component: TreeViewPlugin, + position: 'bottom', + }, + ], +}) diff --git a/packages/richtext-lexical/src/features/debug/treeView/feature.server.ts b/packages/richtext-lexical/src/features/debug/treeView/feature.server.ts index 586dfe04029..c3d1c81621e 100644 --- a/packages/richtext-lexical/src/features/debug/treeView/feature.server.ts +++ b/packages/richtext-lexical/src/features/debug/treeView/feature.server.ts @@ -1,17 +1,10 @@ -import type { FeatureProviderProviderServer } from '../../types.js' - // eslint-disable-next-line payload/no-imports-from-exports-dir -import { TreeViewFeatureClientComponent } from '../../../exports/client/index.js' +import { TreeViewFeatureClient } from '../../../exports/client/index.js' +import { createServerFeature } from '../../../utilities/createServerFeature.js' -export const TreeViewFeature: FeatureProviderProviderServer = (props) => { - return { - feature: () => { - return { - ClientComponent: TreeViewFeatureClientComponent, - serverFeatureProps: props, - } - }, - key: 'treeView', - serverFeatureProps: props, - } -} +export const TreeViewFeature = createServerFeature({ + feature: { + ClientFeature: TreeViewFeatureClient, + }, + key: 'treeView', +}) diff --git a/packages/richtext-lexical/src/features/format/bold/feature.client.tsx b/packages/richtext-lexical/src/features/format/bold/feature.client.tsx index 90f700c5703..6113a137ba5 100644 --- a/packages/richtext-lexical/src/features/format/bold/feature.client.tsx +++ b/packages/richtext-lexical/src/features/format/bold/feature.client.tsx @@ -2,10 +2,9 @@ import { $isRangeSelection, FORMAT_TEXT_COMMAND } from 'lexical' import type { ToolbarGroup } from '../../toolbars/types.js' -import type { FeatureProviderProviderClient } from '../../types.js' import { BoldIcon } from '../../../lexical/ui/icons/Bold/index.js' -import { createClientComponent } from '../../createClientComponent.js' +import { createClientFeature } from '../../../utilities/createClientFeature.js' import { toolbarFormatGroupWithItems } from '../shared/toolbarFormatGroup.js' import { BOLD_ITALIC_STAR, @@ -33,27 +32,19 @@ const toolbarGroups: ToolbarGroup[] = [ ]), ] -const BoldFeatureClient: FeatureProviderProviderClient = (props) => { - return { - clientFeatureProps: props, - feature: ({ featureProviderMap }) => { - const markdownTransformers = [BOLD_STAR, BOLD_UNDERSCORE] - if (featureProviderMap.get('italic')) { - markdownTransformers.push(BOLD_ITALIC_UNDERSCORE, BOLD_ITALIC_STAR) - } +export const BoldFeatureClient = createClientFeature(({ featureProviderMap }) => { + const markdownTransformers = [BOLD_STAR, BOLD_UNDERSCORE] + if (featureProviderMap.get('italic')) { + markdownTransformers.push(BOLD_ITALIC_UNDERSCORE, BOLD_ITALIC_STAR) + } - return { - clientFeatureProps: props, - markdownTransformers, - toolbarFixed: { - groups: toolbarGroups, - }, - toolbarInline: { - groups: toolbarGroups, - }, - } + return { + markdownTransformers, + toolbarFixed: { + groups: toolbarGroups, + }, + toolbarInline: { + groups: toolbarGroups, }, } -} - -export const BoldFeatureClientComponent = createClientComponent(BoldFeatureClient) +}) diff --git a/packages/richtext-lexical/src/features/format/bold/feature.server.ts b/packages/richtext-lexical/src/features/format/bold/feature.server.ts index 43aecb588d1..dbd44fedce7 100644 --- a/packages/richtext-lexical/src/features/format/bold/feature.server.ts +++ b/packages/richtext-lexical/src/features/format/bold/feature.server.ts @@ -1,7 +1,6 @@ -import type { FeatureProviderProviderServer } from '../../types.js' - // eslint-disable-next-line payload/no-imports-from-exports-dir -import { BoldFeatureClientComponent } from '../../../exports/client/index.js' +import { BoldFeatureClient } from '../../../exports/client/index.js' +import { createServerFeature } from '../../../utilities/createServerFeature.js' import { BOLD_ITALIC_STAR, BOLD_ITALIC_UNDERSCORE, @@ -9,22 +8,18 @@ import { BOLD_UNDERSCORE, } from './markdownTransformers.js' -export const BoldFeature: FeatureProviderProviderServer = (props) => { - return { - dependenciesSoft: ['italic'], - feature: ({ featureProviderMap }) => { - const markdownTransformers = [BOLD_STAR, BOLD_UNDERSCORE] - if (featureProviderMap.get('italic')) { - markdownTransformers.push(BOLD_ITALIC_UNDERSCORE, BOLD_ITALIC_STAR) - } +export const BoldFeature = createServerFeature({ + dependenciesSoft: ['italic'], + feature: ({ featureProviderMap }) => { + const markdownTransformers = [BOLD_STAR, BOLD_UNDERSCORE] + if (featureProviderMap.get('italic')) { + markdownTransformers.push(BOLD_ITALIC_UNDERSCORE, BOLD_ITALIC_STAR) + } - return { - ClientComponent: BoldFeatureClientComponent, - markdownTransformers, - serverFeatureProps: props, - } - }, - key: 'bold', - serverFeatureProps: props, - } -} + return { + ClientFeature: BoldFeatureClient, + markdownTransformers, + } + }, + key: 'bold', +}) diff --git a/packages/richtext-lexical/src/features/format/inlineCode/feature.client.tsx b/packages/richtext-lexical/src/features/format/inlineCode/feature.client.tsx index 618f3f2822c..653d9b554f1 100644 --- a/packages/richtext-lexical/src/features/format/inlineCode/feature.client.tsx +++ b/packages/richtext-lexical/src/features/format/inlineCode/feature.client.tsx @@ -3,10 +3,9 @@ import { $isRangeSelection, FORMAT_TEXT_COMMAND } from 'lexical' import type { ToolbarGroup } from '../../toolbars/types.js' -import type { FeatureProviderProviderClient } from '../../types.js' import { CodeIcon } from '../../../lexical/ui/icons/Code/index.js' -import { createClientComponent } from '../../createClientComponent.js' +import { createClientFeature } from '../../../utilities/createClientFeature.js' import { toolbarFormatGroupWithItems } from '../shared/toolbarFormatGroup.js' import { INLINE_CODE } from './markdownTransformers.js' @@ -29,23 +28,12 @@ const toolbarGroups: ToolbarGroup[] = [ ]), ] -const InlineCodeFeatureClient: FeatureProviderProviderClient = (props) => { - return { - clientFeatureProps: props, - feature: () => { - return { - clientFeatureProps: props, - markdownTransformers: [INLINE_CODE], - - toolbarFixed: { - groups: toolbarGroups, - }, - toolbarInline: { - groups: toolbarGroups, - }, - } - }, - } -} - -export const InlineCodeFeatureClientComponent = createClientComponent(InlineCodeFeatureClient) +export const InlineCodeFeatureClient = createClientFeature({ + markdownTransformers: [INLINE_CODE], + toolbarFixed: { + groups: toolbarGroups, + }, + toolbarInline: { + groups: toolbarGroups, + }, +}) diff --git a/packages/richtext-lexical/src/features/format/inlineCode/feature.server.ts b/packages/richtext-lexical/src/features/format/inlineCode/feature.server.ts index 9589ff3da42..20a9db79b91 100644 --- a/packages/richtext-lexical/src/features/format/inlineCode/feature.server.ts +++ b/packages/richtext-lexical/src/features/format/inlineCode/feature.server.ts @@ -1,19 +1,12 @@ -import type { FeatureProviderProviderServer } from '../../types.js' - // eslint-disable-next-line payload/no-imports-from-exports-dir -import { InlineCodeFeatureClientComponent } from '../../../exports/client/index.js' +import { InlineCodeFeatureClient } from '../../../exports/client/index.js' +import { createServerFeature } from '../../../utilities/createServerFeature.js' import { INLINE_CODE } from './markdownTransformers.js' -export const InlineCodeFeature: FeatureProviderProviderServer = (props) => { - return { - feature: () => { - return { - ClientComponent: InlineCodeFeatureClientComponent, - markdownTransformers: [INLINE_CODE], - serverFeatureProps: props, - } - }, - key: 'inlineCode', - serverFeatureProps: props, - } -} +export const InlineCodeFeature = createServerFeature({ + feature: { + ClientFeature: InlineCodeFeatureClient, + markdownTransformers: [INLINE_CODE], + }, + key: 'inlineCode', +}) diff --git a/packages/richtext-lexical/src/features/format/italic/feature.client.tsx b/packages/richtext-lexical/src/features/format/italic/feature.client.tsx index bebea206251..5168e7495c6 100644 --- a/packages/richtext-lexical/src/features/format/italic/feature.client.tsx +++ b/packages/richtext-lexical/src/features/format/italic/feature.client.tsx @@ -3,10 +3,9 @@ import { $isRangeSelection, FORMAT_TEXT_COMMAND } from 'lexical' import type { ToolbarGroup } from '../../toolbars/types.js' -import type { FeatureProviderProviderClient } from '../../types.js' import { ItalicIcon } from '../../../lexical/ui/icons/Italic/index.js' -import { createClientComponent } from '../../createClientComponent.js' +import { createClientFeature } from '../../../utilities/createClientFeature.js' import { toolbarFormatGroupWithItems } from '../shared/toolbarFormatGroup.js' import { ITALIC_STAR, ITALIC_UNDERSCORE } from './markdownTransformers.js' @@ -29,23 +28,12 @@ const toolbarGroups: ToolbarGroup[] = [ ]), ] -const ItalicFeatureClient: FeatureProviderProviderClient = (props) => { - return { - clientFeatureProps: props, - feature: () => { - return { - clientFeatureProps: props, - - markdownTransformers: [ITALIC_STAR, ITALIC_UNDERSCORE], - toolbarFixed: { - groups: toolbarGroups, - }, - toolbarInline: { - groups: toolbarGroups, - }, - } - }, - } -} - -export const ItalicFeatureClientComponent = createClientComponent(ItalicFeatureClient) +export const ItalicFeatureClient = createClientFeature({ + markdownTransformers: [ITALIC_STAR, ITALIC_UNDERSCORE], + toolbarFixed: { + groups: toolbarGroups, + }, + toolbarInline: { + groups: toolbarGroups, + }, +}) diff --git a/packages/richtext-lexical/src/features/format/italic/feature.server.ts b/packages/richtext-lexical/src/features/format/italic/feature.server.ts index 4586de9e380..2ca652e60bb 100644 --- a/packages/richtext-lexical/src/features/format/italic/feature.server.ts +++ b/packages/richtext-lexical/src/features/format/italic/feature.server.ts @@ -1,19 +1,12 @@ -import type { FeatureProviderProviderServer } from '../../types.js' - // eslint-disable-next-line payload/no-imports-from-exports-dir -import { ItalicFeatureClientComponent } from '../../../exports/client/index.js' +import { ItalicFeatureClient } from '../../../exports/client/index.js' +import { createServerFeature } from '../../../utilities/createServerFeature.js' import { ITALIC_STAR, ITALIC_UNDERSCORE } from './markdownTransformers.js' -export const ItalicFeature: FeatureProviderProviderServer = (props) => { - return { - feature: () => { - return { - ClientComponent: ItalicFeatureClientComponent, - markdownTransformers: [ITALIC_STAR, ITALIC_UNDERSCORE], - serverFeatureProps: props, - } - }, - key: 'italic', - serverFeatureProps: props, - } -} +export const ItalicFeature = createServerFeature({ + feature: { + ClientFeature: ItalicFeatureClient, + markdownTransformers: [ITALIC_STAR, ITALIC_UNDERSCORE], + }, + key: 'italic', +}) diff --git a/packages/richtext-lexical/src/features/format/strikethrough/feature.client.tsx b/packages/richtext-lexical/src/features/format/strikethrough/feature.client.tsx index b6acc50df23..1e83749f8a7 100644 --- a/packages/richtext-lexical/src/features/format/strikethrough/feature.client.tsx +++ b/packages/richtext-lexical/src/features/format/strikethrough/feature.client.tsx @@ -2,10 +2,8 @@ import { $isRangeSelection, FORMAT_TEXT_COMMAND } from 'lexical' -import type { FeatureProviderProviderClient } from '../../types.js' - import { StrikethroughIcon } from '../../../lexical/ui/icons/Strikethrough/index.js' -import { createClientComponent } from '../../createClientComponent.js' +import { createClientFeature } from '../../../utilities/createClientFeature.js' import { toolbarFormatGroupWithItems } from '../shared/toolbarFormatGroup.js' import { STRIKETHROUGH } from './markdownTransformers.js' @@ -28,23 +26,12 @@ const toolbarGroups = [ ]), ] -const StrikethroughFeatureClient: FeatureProviderProviderClient = (props) => { - return { - clientFeatureProps: props, - feature: () => { - return { - clientFeatureProps: props, - - markdownTransformers: [STRIKETHROUGH], - toolbarFixed: { - groups: toolbarGroups, - }, - toolbarInline: { - groups: toolbarGroups, - }, - } - }, - } -} - -export const StrikethroughFeatureClientComponent = createClientComponent(StrikethroughFeatureClient) +export const StrikethroughFeatureClient = createClientFeature({ + markdownTransformers: [STRIKETHROUGH], + toolbarFixed: { + groups: toolbarGroups, + }, + toolbarInline: { + groups: toolbarGroups, + }, +}) diff --git a/packages/richtext-lexical/src/features/format/strikethrough/feature.server.ts b/packages/richtext-lexical/src/features/format/strikethrough/feature.server.ts index aadf93e1da8..fc25bc7506c 100644 --- a/packages/richtext-lexical/src/features/format/strikethrough/feature.server.ts +++ b/packages/richtext-lexical/src/features/format/strikethrough/feature.server.ts @@ -1,22 +1,13 @@ -import type { FeatureProviderProviderServer } from '../../types.js' - // eslint-disable-next-line payload/no-imports-from-exports-dir -import { StrikethroughFeatureClientComponent } from '../../../exports/client/index.js' +import { StrikethroughFeatureClient } from '../../../exports/client/index.js' +import { createServerFeature } from '../../../utilities/createServerFeature.js' import { STRIKETHROUGH } from './markdownTransformers.js' -export const StrikethroughFeature: FeatureProviderProviderServer = ( - props, -) => { - return { - feature: () => { - return { - ClientComponent: StrikethroughFeatureClientComponent, +export const StrikethroughFeature = createServerFeature({ + feature: { + ClientFeature: StrikethroughFeatureClient, - markdownTransformers: [STRIKETHROUGH], - serverFeatureProps: props, - } - }, - key: 'strikethrough', - serverFeatureProps: props, - } -} + markdownTransformers: [STRIKETHROUGH], + }, + key: 'strikethrough', +}) diff --git a/packages/richtext-lexical/src/features/format/subscript/feature.client.tsx b/packages/richtext-lexical/src/features/format/subscript/feature.client.tsx index 891399fde82..b8617dd9714 100644 --- a/packages/richtext-lexical/src/features/format/subscript/feature.client.tsx +++ b/packages/richtext-lexical/src/features/format/subscript/feature.client.tsx @@ -3,10 +3,9 @@ import { $isRangeSelection, FORMAT_TEXT_COMMAND } from 'lexical' import type { ToolbarGroup } from '../../toolbars/types.js' -import type { FeatureProviderProviderClient } from '../../types.js' import { SubscriptIcon } from '../../../lexical/ui/icons/Subscript/index.js' -import { createClientComponent } from '../../createClientComponent.js' +import { createClientFeature } from '../../../utilities/createClientFeature.js' import { toolbarFormatGroupWithItems } from '../shared/toolbarFormatGroup.js' const toolbarGroups: ToolbarGroup[] = [ @@ -28,21 +27,11 @@ const toolbarGroups: ToolbarGroup[] = [ ]), ] -const SubscriptFeatureClient: FeatureProviderProviderClient = (props) => { - return { - clientFeatureProps: props, - feature: () => { - return { - clientFeatureProps: props, - toolbarFixed: { - groups: toolbarGroups, - }, - toolbarInline: { - groups: toolbarGroups, - }, - } - }, - } -} - -export const SubscriptFeatureClientComponent = createClientComponent(SubscriptFeatureClient) +export const SubscriptFeatureClient = createClientFeature({ + toolbarFixed: { + groups: toolbarGroups, + }, + toolbarInline: { + groups: toolbarGroups, + }, +}) diff --git a/packages/richtext-lexical/src/features/format/subscript/feature.server.ts b/packages/richtext-lexical/src/features/format/subscript/feature.server.ts index 2337d384d62..8a7790760aa 100644 --- a/packages/richtext-lexical/src/features/format/subscript/feature.server.ts +++ b/packages/richtext-lexical/src/features/format/subscript/feature.server.ts @@ -1,17 +1,10 @@ -import type { FeatureProviderProviderServer } from '../../types.js' - // eslint-disable-next-line payload/no-imports-from-exports-dir -import { SubscriptFeatureClientComponent } from '../../../exports/client/index.js' +import { SubscriptFeatureClient } from '../../../exports/client/index.js' +import { createServerFeature } from '../../../utilities/createServerFeature.js' -export const SubscriptFeature: FeatureProviderProviderServer = (props) => { - return { - feature: () => { - return { - ClientComponent: SubscriptFeatureClientComponent, - serverFeatureProps: props, - } - }, - key: 'subscript', - serverFeatureProps: props, - } -} +export const SubscriptFeature = createServerFeature({ + feature: { + ClientFeature: SubscriptFeatureClient, + }, + key: 'subscript', +}) diff --git a/packages/richtext-lexical/src/features/format/superscript/feature.client.tsx b/packages/richtext-lexical/src/features/format/superscript/feature.client.tsx index e9e56f150ca..e388c3d4116 100644 --- a/packages/richtext-lexical/src/features/format/superscript/feature.client.tsx +++ b/packages/richtext-lexical/src/features/format/superscript/feature.client.tsx @@ -3,10 +3,9 @@ import { $isRangeSelection, FORMAT_TEXT_COMMAND } from 'lexical' import type { ToolbarGroup } from '../../toolbars/types.js' -import type { FeatureProviderProviderClient } from '../../types.js' import { SuperscriptIcon } from '../../../lexical/ui/icons/Superscript/index.js' -import { createClientComponent } from '../../createClientComponent.js' +import { createClientFeature } from '../../../utilities/createClientFeature.js' import { toolbarFormatGroupWithItems } from '../shared/toolbarFormatGroup.js' const toolbarGroups: ToolbarGroup[] = [ @@ -28,21 +27,11 @@ const toolbarGroups: ToolbarGroup[] = [ ]), ] -const SuperscriptFeatureClient: FeatureProviderProviderClient = (props) => { - return { - clientFeatureProps: props, - feature: () => { - return { - clientFeatureProps: props, - toolbarFixed: { - groups: toolbarGroups, - }, - toolbarInline: { - groups: toolbarGroups, - }, - } - }, - } -} - -export const SuperscriptFeatureClientComponent = createClientComponent(SuperscriptFeatureClient) +export const SuperscriptFeatureClient = createClientFeature({ + toolbarFixed: { + groups: toolbarGroups, + }, + toolbarInline: { + groups: toolbarGroups, + }, +}) diff --git a/packages/richtext-lexical/src/features/format/superscript/feature.server.ts b/packages/richtext-lexical/src/features/format/superscript/feature.server.ts index 25bdce016d2..320a2f00d20 100644 --- a/packages/richtext-lexical/src/features/format/superscript/feature.server.ts +++ b/packages/richtext-lexical/src/features/format/superscript/feature.server.ts @@ -1,17 +1,10 @@ -import type { FeatureProviderProviderServer } from '../../types.js' - // eslint-disable-next-line payload/no-imports-from-exports-dir -import { SuperscriptFeatureClientComponent } from '../../../exports/client/index.js' +import { SuperscriptFeatureClient } from '../../../exports/client/index.js' +import { createServerFeature } from '../../../utilities/createServerFeature.js' -export const SuperscriptFeature: FeatureProviderProviderServer = (props) => { - return { - feature: () => { - return { - ClientComponent: SuperscriptFeatureClientComponent, - serverFeatureProps: props, - } - }, - key: 'superscript', - serverFeatureProps: props, - } -} +export const SuperscriptFeature = createServerFeature({ + feature: { + ClientFeature: SuperscriptFeatureClient, + }, + key: 'superscript', +}) diff --git a/packages/richtext-lexical/src/features/format/underline/feature.client.tsx b/packages/richtext-lexical/src/features/format/underline/feature.client.tsx index ae62287a362..f1c322c4ff2 100644 --- a/packages/richtext-lexical/src/features/format/underline/feature.client.tsx +++ b/packages/richtext-lexical/src/features/format/underline/feature.client.tsx @@ -3,10 +3,9 @@ import { $isRangeSelection, FORMAT_TEXT_COMMAND } from 'lexical' import type { ToolbarGroup } from '../../toolbars/types.js' -import type { FeatureProviderProviderClient } from '../../types.js' import { UnderlineIcon } from '../../../lexical/ui/icons/Underline/index.js' -import { createClientComponent } from '../../createClientComponent.js' +import { createClientFeature } from '../../../utilities/createClientFeature.js' import { toolbarFormatGroupWithItems } from '../shared/toolbarFormatGroup.js' const toolbarGroups: ToolbarGroup[] = [ @@ -28,21 +27,11 @@ const toolbarGroups: ToolbarGroup[] = [ ]), ] -const UnderlineFeatureClient: FeatureProviderProviderClient = (props) => { - return { - clientFeatureProps: props, - feature: () => { - return { - clientFeatureProps: props, - toolbarFixed: { - groups: toolbarGroups, - }, - toolbarInline: { - groups: toolbarGroups, - }, - } - }, - } -} - -export const UnderlineFeatureClientComponent = createClientComponent(UnderlineFeatureClient) +export const UnderlineFeatureClient = createClientFeature({ + toolbarFixed: { + groups: toolbarGroups, + }, + toolbarInline: { + groups: toolbarGroups, + }, +}) diff --git a/packages/richtext-lexical/src/features/format/underline/feature.server.ts b/packages/richtext-lexical/src/features/format/underline/feature.server.ts index 9286a1c30f7..f4931b3f100 100644 --- a/packages/richtext-lexical/src/features/format/underline/feature.server.ts +++ b/packages/richtext-lexical/src/features/format/underline/feature.server.ts @@ -1,17 +1,10 @@ -import type { FeatureProviderProviderServer } from '../../types.js' - // eslint-disable-next-line payload/no-imports-from-exports-dir -import { UnderlineFeatureClientComponent } from '../../../exports/client/index.js' +import { UnderlineFeatureClient } from '../../../exports/client/index.js' +import { createServerFeature } from '../../../utilities/createServerFeature.js' -export const UnderlineFeature: FeatureProviderProviderServer = (props) => { - return { - feature: () => { - return { - ClientComponent: UnderlineFeatureClientComponent, - serverFeatureProps: props, - } - }, - key: 'underline', - serverFeatureProps: props, - } -} +export const UnderlineFeature = createServerFeature({ + feature: { + ClientFeature: UnderlineFeatureClient, + }, + key: 'underline', +}) diff --git a/packages/richtext-lexical/src/features/heading/feature.client.tsx b/packages/richtext-lexical/src/features/heading/feature.client.tsx index ea37a08af1d..e490b087e63 100644 --- a/packages/richtext-lexical/src/features/heading/feature.client.tsx +++ b/packages/richtext-lexical/src/features/heading/feature.client.tsx @@ -7,7 +7,6 @@ import { $setBlocksType } from '@lexical/selection' import { $getSelection, $isRangeSelection } from 'lexical' import type { ToolbarGroup } from '../toolbars/types.js' -import type { FeatureProviderProviderClient } from '../types.js' import type { HeadingFeatureProps } from './feature.server.js' import { H1Icon } from '../../lexical/ui/icons/H1/index.js' @@ -16,7 +15,7 @@ import { H3Icon } from '../../lexical/ui/icons/H3/index.js' import { H4Icon } from '../../lexical/ui/icons/H4/index.js' import { H5Icon } from '../../lexical/ui/icons/H5/index.js' import { H6Icon } from '../../lexical/ui/icons/H6/index.js' -import { createClientComponent } from '../createClientComponent.js' +import { createClientFeature } from '../../utilities/createClientFeature.js' import { slashMenuBasicGroupWithItems } from '../shared/slashMenu/basicGroup.js' import { toolbarTextDropdownGroupWithItems } from '../shared/toolbar/textDropdownGroup.js' import { MarkdownTransformer } from './markdownTransformer.js' @@ -35,88 +34,81 @@ const iconImports = { h6: H6Icon, } -const HeadingFeatureClient: FeatureProviderProviderClient = (props) => { - return { - clientFeatureProps: props, - feature: () => { - const { enabledHeadingSizes = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] } = props +export const HeadingFeatureClient = createClientFeature(({ props }) => { + const { enabledHeadingSizes = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] } = props - const toolbarGroups: ToolbarGroup[] = [ - toolbarTextDropdownGroupWithItems( - enabledHeadingSizes.map((headingSize, i) => { - return { - ChildComponent: iconImports[headingSize], - isActive: ({ selection }) => { - if (!$isRangeSelection(selection)) { - return false - } - for (const node of selection.getNodes()) { - if ($isHeadingNode(node) && node.getTag() === headingSize) { - continue - } + const toolbarGroups: ToolbarGroup[] = [ + toolbarTextDropdownGroupWithItems( + enabledHeadingSizes.map((headingSize, i) => { + return { + ChildComponent: iconImports[headingSize], + isActive: ({ selection }) => { + if (!$isRangeSelection(selection)) { + return false + } + for (const node of selection.getNodes()) { + if ($isHeadingNode(node) && node.getTag() === headingSize) { + continue + } - const parent = node.getParent() - if ($isHeadingNode(parent) && parent.getTag() === headingSize) { - continue - } + const parent = node.getParent() + if ($isHeadingNode(parent) && parent.getTag() === headingSize) { + continue + } - return false - } - return true - }, - key: headingSize, - label: ({ i18n }) => { - return i18n.t('lexical:heading:label', { headingLevel: headingSize.charAt(1) }) - }, - onSelect: ({ editor }) => { - editor.update(() => { - $setHeading(headingSize) - }) - }, - order: i + 2, + return false } - }), - ), - ] + return true + }, + key: headingSize, + label: ({ i18n }) => { + return i18n.t('lexical:heading:label', { headingLevel: headingSize.charAt(1) }) + }, + onSelect: ({ editor }) => { + editor.update(() => { + $setHeading(headingSize) + }) + }, + order: i + 2, + } + }), + ), + ] - return { - clientFeatureProps: props, - markdownTransformers: [MarkdownTransformer(enabledHeadingSizes)], - nodes: [HeadingNode], - slashMenu: { - groups: enabledHeadingSizes?.length - ? [ - slashMenuBasicGroupWithItems( - enabledHeadingSizes.map((headingSize) => { - return { - Icon: iconImports[headingSize], - key: `heading-${headingSize.charAt(1)}`, - keywords: ['heading', headingSize], - label: ({ i18n }) => { - return i18n.t('lexical:heading:label', { - headingLevel: headingSize.charAt(1), - }) - }, - onSelect: ({ editor }) => { - editor.update(() => { - $setHeading(headingSize) - }) - }, - } - }), - ), - ] - : [], - }, - toolbarFixed: { - groups: enabledHeadingSizes?.length ? toolbarGroups : [], - }, - toolbarInline: { - groups: enabledHeadingSizes?.length ? toolbarGroups : [], - }, - } + return { + markdownTransformers: [MarkdownTransformer(enabledHeadingSizes)], + nodes: [HeadingNode], + sanitizedClientFeatureProps: props, + slashMenu: { + groups: enabledHeadingSizes?.length + ? [ + slashMenuBasicGroupWithItems( + enabledHeadingSizes.map((headingSize) => { + return { + Icon: iconImports[headingSize], + key: `heading-${headingSize.charAt(1)}`, + keywords: ['heading', headingSize], + label: ({ i18n }) => { + return i18n.t('lexical:heading:label', { + headingLevel: headingSize.charAt(1), + }) + }, + onSelect: ({ editor }) => { + editor.update(() => { + $setHeading(headingSize) + }) + }, + } + }), + ), + ] + : [], + }, + toolbarFixed: { + groups: enabledHeadingSizes?.length ? toolbarGroups : [], + }, + toolbarInline: { + groups: enabledHeadingSizes?.length ? toolbarGroups : [], }, } -} - -export const HeadingFeatureClientComponent = createClientComponent(HeadingFeatureClient) +}) diff --git a/packages/richtext-lexical/src/features/heading/feature.server.ts b/packages/richtext-lexical/src/features/heading/feature.server.ts index 67dde716d05..7d5c6a1993d 100644 --- a/packages/richtext-lexical/src/features/heading/feature.server.ts +++ b/packages/richtext-lexical/src/features/heading/feature.server.ts @@ -2,10 +2,9 @@ import type { HeadingTagType } from '@lexical/rich-text' import { HeadingNode } from '@lexical/rich-text' -import type { FeatureProviderProviderServer } from '../types.js' - // eslint-disable-next-line payload/no-imports-from-exports-dir -import { HeadingFeatureClientComponent } from '../../exports/client/index.js' +import { HeadingFeatureClient } from '../../exports/client/index.js' +import { createServerFeature } from '../../utilities/createServerFeature.js' import { convertLexicalNodesToHTML } from '../converters/html/converter/index.js' import { createNode } from '../typeUtilities.js' import { i18n } from './i18n.js' @@ -15,50 +14,48 @@ export type HeadingFeatureProps = { enabledHeadingSizes?: HeadingTagType[] } -export const HeadingFeature: FeatureProviderProviderServer< +export const HeadingFeature = createServerFeature< + HeadingFeatureProps, HeadingFeatureProps, HeadingFeatureProps -> = (props) => { - if (!props) { - props = {} - } - - const { enabledHeadingSizes = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] } = props - - return { - feature: () => { - return { - ClientComponent: HeadingFeatureClientComponent, - clientFeatureProps: props, - i18n, - markdownTransformers: [MarkdownTransformer(enabledHeadingSizes)], - nodes: [ - createNode({ - converters: { - html: { - converter: async ({ converters, node, parent, req }) => { - const childrenText = await convertLexicalNodesToHTML({ - converters, - lexicalNodes: node.children, - parent: { - ...node, - parent, - }, - req, - }) - - return '<' + node?.tag + '>' + childrenText + '' - }, - nodeTypes: [HeadingNode.getType()], +>({ + feature: ({ props }) => { + if (!props) { + props = {} + } + + const { enabledHeadingSizes = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] } = props + + return { + ClientFeature: HeadingFeatureClient, + clientFeatureProps: props, + i18n, + markdownTransformers: [MarkdownTransformer(enabledHeadingSizes)], + nodes: [ + createNode({ + converters: { + html: { + converter: async ({ converters, node, parent, req }) => { + const childrenText = await convertLexicalNodesToHTML({ + converters, + lexicalNodes: node.children, + parent: { + ...node, + parent, + }, + req, + }) + + return '<' + node?.tag + '>' + childrenText + '' }, + nodeTypes: [HeadingNode.getType()], }, - node: HeadingNode, - }), - ], - serverFeatureProps: props, - } - }, - key: 'heading', - serverFeatureProps: props, - } -} + }, + node: HeadingNode, + }), + ], + sanitizedServerFeatureProps: props, + } + }, + key: 'heading', +}) diff --git a/packages/richtext-lexical/src/features/horizontalRule/feature.client.tsx b/packages/richtext-lexical/src/features/horizontalRule/feature.client.tsx index 0d8139d608b..47e82627fbf 100644 --- a/packages/richtext-lexical/src/features/horizontalRule/feature.client.tsx +++ b/packages/richtext-lexical/src/features/horizontalRule/feature.client.tsx @@ -1,13 +1,9 @@ 'use client' -import type { ClientTranslationKeys } from '@payloadcms/translations' - import { $isNodeSelection } from 'lexical' -import type { FeatureProviderProviderClient } from '../types.js' - import { HorizontalRuleIcon } from '../../lexical/ui/icons/HorizontalRule/index.js' -import { createClientComponent } from '../createClientComponent.js' +import { createClientFeature } from '../../utilities/createClientFeature.js' import { slashMenuBasicGroupWithItems } from '../shared/slashMenu/basicGroup.js' import { toolbarAddDropdownGroupWithItems } from '../shared/toolbar/addDropdownGroup.js' import { MarkdownTransformer } from './markdownTransformer.js' @@ -18,66 +14,55 @@ import { } from './nodes/HorizontalRuleNode.js' import { HorizontalRulePlugin } from './plugin/index.js' -const HorizontalRuleFeatureClient: FeatureProviderProviderClient = (props) => { - return { - clientFeatureProps: props, - feature: () => ({ - clientFeatureProps: props, - markdownTransformers: [MarkdownTransformer], - nodes: [HorizontalRuleNode], - plugins: [ +export const HorizontalRuleFeatureClient = createClientFeature({ + markdownTransformers: [MarkdownTransformer], + nodes: [HorizontalRuleNode], + plugins: [ + { + Component: HorizontalRulePlugin, + position: 'normal', + }, + ], + slashMenu: { + groups: [ + slashMenuBasicGroupWithItems([ { - Component: HorizontalRulePlugin, - position: 'normal', - }, - ], - slashMenu: { - groups: [ - slashMenuBasicGroupWithItems([ - { - Icon: HorizontalRuleIcon, - key: 'horizontalRule', - keywords: ['hr', 'horizontal rule', 'line', 'separator'], - label: ({ i18n }) => { - return i18n.t('lexical:horizontalRule:label') - }, - - onSelect: ({ editor }) => { - editor.dispatchCommand(INSERT_HORIZONTAL_RULE_COMMAND, undefined) - }, - }, - ]), - ], - }, + Icon: HorizontalRuleIcon, + key: 'horizontalRule', + keywords: ['hr', 'horizontal rule', 'line', 'separator'], + label: ({ i18n }) => { + return i18n.t('lexical:horizontalRule:label') + }, - toolbarFixed: { - groups: [ - toolbarAddDropdownGroupWithItems([ - { - ChildComponent: HorizontalRuleIcon, - isActive: ({ selection }) => { - if (!$isNodeSelection(selection) || !selection.getNodes().length) { - return false - } - - const firstNode = selection.getNodes()[0] - return $isHorizontalRuleNode(firstNode) - }, - key: 'horizontalRule', - label: ({ i18n }) => { - return i18n.t('lexical:horizontalRule:label') - }, - onSelect: ({ editor }) => { - editor.dispatchCommand(INSERT_HORIZONTAL_RULE_COMMAND, undefined) - }, - }, - ]), - ], - }, - }), - } -} + onSelect: ({ editor }) => { + editor.dispatchCommand(INSERT_HORIZONTAL_RULE_COMMAND, undefined) + }, + }, + ]), + ], + }, + toolbarFixed: { + groups: [ + toolbarAddDropdownGroupWithItems([ + { + ChildComponent: HorizontalRuleIcon, + isActive: ({ selection }) => { + if (!$isNodeSelection(selection) || !selection.getNodes().length) { + return false + } -export const HorizontalRuleFeatureClientComponent = createClientComponent( - HorizontalRuleFeatureClient, -) + const firstNode = selection.getNodes()[0] + return $isHorizontalRuleNode(firstNode) + }, + key: 'horizontalRule', + label: ({ i18n }) => { + return i18n.t('lexical:horizontalRule:label') + }, + onSelect: ({ editor }) => { + editor.dispatchCommand(INSERT_HORIZONTAL_RULE_COMMAND, undefined) + }, + }, + ]), + ], + }, +}) diff --git a/packages/richtext-lexical/src/features/horizontalRule/feature.server.ts b/packages/richtext-lexical/src/features/horizontalRule/feature.server.ts index c6994e4cf58..ca2fd4433ed 100644 --- a/packages/richtext-lexical/src/features/horizontalRule/feature.server.ts +++ b/packages/richtext-lexical/src/features/horizontalRule/feature.server.ts @@ -1,38 +1,29 @@ -import type { FeatureProviderProviderServer } from '../types.js' - // eslint-disable-next-line payload/no-imports-from-exports-dir -import { HorizontalRuleFeatureClientComponent } from '../../exports/client/index.js' +import { HorizontalRuleFeatureClient } from '../../exports/client/index.js' +import { createServerFeature } from '../../utilities/createServerFeature.js' import { createNode } from '../typeUtilities.js' import { i18n } from './i18n.js' import { MarkdownTransformer } from './markdownTransformer.js' import { HorizontalRuleNode } from './nodes/HorizontalRuleNode.js' -export const HorizontalRuleFeature: FeatureProviderProviderServer = ( - props, -) => { - return { - feature: () => { - return { - ClientComponent: HorizontalRuleFeatureClientComponent, - i18n, - markdownTransformers: [MarkdownTransformer], - nodes: [ - createNode({ - converters: { - html: { - converter: () => { - return `
` - }, - nodeTypes: [HorizontalRuleNode.getType()], - }, +export const HorizontalRuleFeature = createServerFeature({ + feature: { + ClientFeature: HorizontalRuleFeatureClient, + i18n, + markdownTransformers: [MarkdownTransformer], + nodes: [ + createNode({ + converters: { + html: { + converter: () => { + return `
` }, - node: HorizontalRuleNode, - }), - ], - serverFeatureProps: props, - } - }, - key: 'horizontalRule', - serverFeatureProps: props, - } -} + nodeTypes: [HorizontalRuleNode.getType()], + }, + }, + node: HorizontalRuleNode, + }), + ], + }, + key: 'horizontalRule', +}) diff --git a/packages/richtext-lexical/src/features/indent/feature.client.tsx b/packages/richtext-lexical/src/features/indent/feature.client.tsx index e66341fbbae..f3f8f7f9e59 100644 --- a/packages/richtext-lexical/src/features/indent/feature.client.tsx +++ b/packages/richtext-lexical/src/features/indent/feature.client.tsx @@ -3,11 +3,10 @@ import { INDENT_CONTENT_COMMAND, OUTDENT_CONTENT_COMMAND } from 'lexical' import type { ToolbarGroup } from '../toolbars/types.js' -import type { FeatureProviderProviderClient } from '../types.js' import { IndentDecreaseIcon } from '../../lexical/ui/icons/IndentDecrease/index.js' import { IndentIncreaseIcon } from '../../lexical/ui/icons/IndentIncrease/index.js' -import { createClientComponent } from '../createClientComponent.js' +import { createClientFeature } from '../../utilities/createClientFeature.js' import { toolbarIndentGroupWithItems } from './toolbarIndentGroup.js' const toolbarGroups: ToolbarGroup[] = [ @@ -54,19 +53,11 @@ const toolbarGroups: ToolbarGroup[] = [ ]), ] -const IndentFeatureClient: FeatureProviderProviderClient = (props) => { - return { - clientFeatureProps: props, - feature: () => ({ - clientFeatureProps: props, - toolbarFixed: { - groups: toolbarGroups, - }, - toolbarInline: { - groups: toolbarGroups, - }, - }), - } -} - -export const IndentFeatureClientComponent = createClientComponent(IndentFeatureClient) +export const IndentFeatureClient = createClientFeature({ + toolbarFixed: { + groups: toolbarGroups, + }, + toolbarInline: { + groups: toolbarGroups, + }, +}) diff --git a/packages/richtext-lexical/src/features/indent/feature.server.ts b/packages/richtext-lexical/src/features/indent/feature.server.ts index 9eb9120f556..b5d13703e89 100644 --- a/packages/richtext-lexical/src/features/indent/feature.server.ts +++ b/packages/richtext-lexical/src/features/indent/feature.server.ts @@ -1,20 +1,12 @@ -import type { FeatureProviderProviderServer } from '../types.js' - // eslint-disable-next-line payload/no-imports-from-exports-dir -import { IndentFeatureClientComponent } from '../../exports/client/index.js' +import { IndentFeatureClient } from '../../exports/client/index.js' +import { createServerFeature } from '../../utilities/createServerFeature.js' import { i18n } from './i18n.js' -export const IndentFeature: FeatureProviderProviderServer = (props) => { - return { - feature: () => { - return { - ClientComponent: IndentFeatureClientComponent, - clientFeatureProps: null, - i18n, - serverFeatureProps: props, - } - }, - key: 'indent', - serverFeatureProps: props, - } -} +export const IndentFeature = createServerFeature({ + feature: { + ClientFeature: IndentFeatureClient, + i18n, + }, + key: 'indent', +}) diff --git a/packages/richtext-lexical/src/features/link/feature.client.tsx b/packages/richtext-lexical/src/features/link/feature.client.tsx index 506eb0dac09..c8ae0ce1e5f 100644 --- a/packages/richtext-lexical/src/features/link/feature.client.tsx +++ b/packages/richtext-lexical/src/features/link/feature.client.tsx @@ -6,13 +6,12 @@ import { $findMatchingParent } from '@lexical/utils' import { $getSelection, $isRangeSelection } from 'lexical' import type { ToolbarGroup } from '../toolbars/types.js' -import type { FeatureProviderProviderClient } from '../types.js' import type { ExclusiveLinkCollectionsProps } from './feature.server.js' import type { LinkFields } from './nodes/types.js' import { LinkIcon } from '../../lexical/ui/icons/Link/index.js' import { getSelectedNode } from '../../lexical/utils/getSelectedNode.js' -import { createClientComponent } from '../createClientComponent.js' +import { createClientFeature } from '../../utilities/createClientFeature.js' import { toolbarFeatureButtonsGroupWithItems } from '../shared/toolbar/featureButtonsGroup.js' import { LinkMarkdownTransformer } from './markdownTransformer.js' import { AutoLinkNode } from './nodes/AutoLinkNode.js' @@ -80,39 +79,31 @@ const toolbarGroups: ToolbarGroup[] = [ ]), ] -const LinkFeatureClient: FeatureProviderProviderClient = (props) => { - return { - clientFeatureProps: props, - feature: () => ({ - clientFeatureProps: props, - markdownTransformers: [LinkMarkdownTransformer], - nodes: [LinkNode, AutoLinkNode], - plugins: [ - { - Component: LinkPlugin, - position: 'normal', - }, - { - Component: AutoLinkPlugin, - position: 'normal', - }, - { - Component: ClickableLinkPlugin, - position: 'normal', - }, - { - Component: FloatingLinkEditorPlugin, - position: 'floatingAnchorElem', - }, - ], - toolbarFixed: { - groups: toolbarGroups, - }, - toolbarInline: { - groups: toolbarGroups, - }, - }), - } -} - -export const LinkFeatureClientComponent = createClientComponent(LinkFeatureClient) +export const LinkFeatureClient = createClientFeature({ + markdownTransformers: [LinkMarkdownTransformer], + nodes: [LinkNode, AutoLinkNode], + plugins: [ + { + Component: LinkPlugin, + position: 'normal', + }, + { + Component: AutoLinkPlugin, + position: 'normal', + }, + { + Component: ClickableLinkPlugin, + position: 'normal', + }, + { + Component: FloatingLinkEditorPlugin, + position: 'floatingAnchorElem', + }, + ], + toolbarFixed: { + groups: toolbarGroups, + }, + toolbarInline: { + groups: toolbarGroups, + }, +}) diff --git a/packages/richtext-lexical/src/features/link/feature.server.ts b/packages/richtext-lexical/src/features/link/feature.server.ts index 935b0fccc12..e8a7082376a 100644 --- a/packages/richtext-lexical/src/features/link/feature.server.ts +++ b/packages/richtext-lexical/src/features/link/feature.server.ts @@ -4,11 +4,11 @@ import { traverseFields } from '@payloadcms/ui/utilities/buildFieldSchemaMap/tra import { sanitizeFields } from 'payload' import { deepCopyObject } from 'payload/shared' -import type { FeatureProviderProviderServer } from '../types.js' import type { ClientProps } from './feature.client.js' // eslint-disable-next-line payload/no-imports-from-exports-dir -import { LinkFeatureClientComponent } from '../../exports/client/index.js' +import { LinkFeatureClient } from '../../exports/client/index.js' +import { createServerFeature } from '../../utilities/createServerFeature.js' import { convertLexicalNodesToHTML } from '../converters/html/converter/index.js' import { createNode } from '../typeUtilities.js' import { linkPopulationPromiseHOC } from './graphQLPopulationPromise.js' @@ -61,146 +61,145 @@ export type LinkFeatureServerProps = ExclusiveLinkCollectionsProps & { maxDepth?: number } -export const LinkFeature: FeatureProviderProviderServer = ( - props, -) => { - if (!props) { - props = {} - } - return { - feature: async ({ config: _config, isRoot }) => { - const validRelationships = _config.collections.map((c) => c.slug) || [] - - const _transformedFields = transformExtraFields( - deepCopyObject(props.fields), - _config, - props.enabledCollections, - props.disabledCollections, - props.maxDepth, - ) - - const sanitizedFields = await sanitizeFields({ - config: _config as unknown as Config, - fields: _transformedFields, - requireFieldLevelRichTextEditor: isRoot, - validRelationships, - }) - props.fields = sanitizedFields - - // the text field is not included in the node data. - // Thus, for tasks like validation, we do not want to pass it a text field in the schema which will never have data. - // Otherwise, it will cause a validation error (field is required). - const sanitizedFieldsWithoutText = deepCopyObject(sanitizedFields).filter( - (field) => field.name !== 'text', - ) - - return { - ClientComponent: LinkFeatureClientComponent, - clientFeatureProps: { - disabledCollections: props.disabledCollections, - enabledCollections: props.enabledCollections, - } as ExclusiveLinkCollectionsProps, - generateSchemaMap: ({ config, i18n }) => { - if (!sanitizedFields || !Array.isArray(sanitizedFields) || sanitizedFields.length === 0) { - return null - } - - const schemaMap = new Map() - - const validRelationships = config.collections.map((c) => c.slug) || [] - - schemaMap.set('fields', sanitizedFields) - - traverseFields({ - config, - fields: sanitizedFields, - i18n, - schemaMap, - schemaPath: 'fields', - validRelationships, - }) - - return schemaMap - }, - i18n, - markdownTransformers: [LinkMarkdownTransformer], - nodes: [ - createNode({ - converters: { - html: { - converter: async ({ converters, node, parent, req }) => { - const childrenText = await convertLexicalNodesToHTML({ - converters, - lexicalNodes: node.children, - parent: { - ...node, - parent, - }, - req, - }) - - const rel: string = node.fields.newTab ? ' rel="noopener noreferrer"' : '' - const target: string = node.fields.newTab ? ' target="_blank"' : '' - - let href: string = node.fields.url - if (node.fields.linkType === 'internal') { - href = - typeof node.fields.doc?.value === 'string' - ? node.fields.doc?.value - : node.fields.doc?.value?.id - } - - return `${childrenText}` - }, - nodeTypes: [AutoLinkNode.getType()], +export const LinkFeature = createServerFeature< + LinkFeatureServerProps, + LinkFeatureServerProps, + ClientProps +>({ + feature: async ({ config: _config, isRoot, props }) => { + if (!props) { + props = {} + } + const validRelationships = _config.collections.map((c) => c.slug) || [] + + const _transformedFields = transformExtraFields( + deepCopyObject(props.fields), + _config, + props.enabledCollections, + props.disabledCollections, + props.maxDepth, + ) + + const sanitizedFields = await sanitizeFields({ + config: _config as unknown as Config, + fields: _transformedFields, + requireFieldLevelRichTextEditor: isRoot, + validRelationships, + }) + props.fields = sanitizedFields + + // the text field is not included in the node data. + // Thus, for tasks like validation, we do not want to pass it a text field in the schema which will never have data. + // Otherwise, it will cause a validation error (field is required). + const sanitizedFieldsWithoutText = deepCopyObject(sanitizedFields).filter( + (field) => field.name !== 'text', + ) + + return { + ClientFeature: LinkFeatureClient, + clientFeatureProps: { + disabledCollections: props.disabledCollections, + enabledCollections: props.enabledCollections, + } as ExclusiveLinkCollectionsProps, + generateSchemaMap: ({ config, i18n }) => { + if (!sanitizedFields || !Array.isArray(sanitizedFields) || sanitizedFields.length === 0) { + return null + } + + const schemaMap = new Map() + + const validRelationships = config.collections.map((c) => c.slug) || [] + + schemaMap.set('fields', sanitizedFields) + + traverseFields({ + config, + fields: sanitizedFields, + i18n, + schemaMap, + schemaPath: 'fields', + validRelationships, + }) + + return schemaMap + }, + i18n, + markdownTransformers: [LinkMarkdownTransformer], + nodes: [ + createNode({ + converters: { + html: { + converter: async ({ converters, node, parent, req }) => { + const childrenText = await convertLexicalNodesToHTML({ + converters, + lexicalNodes: node.children, + parent: { + ...node, + parent, + }, + req, + }) + + const rel: string = node.fields.newTab ? ' rel="noopener noreferrer"' : '' + const target: string = node.fields.newTab ? ' target="_blank"' : '' + + let href: string = node.fields.url + if (node.fields.linkType === 'internal') { + href = + typeof node.fields.doc?.value === 'string' + ? node.fields.doc?.value + : node.fields.doc?.value?.id + } + + return `${childrenText}` }, + nodeTypes: [AutoLinkNode.getType()], }, - node: AutoLinkNode, - // Since AutoLinkNodes are just internal links, they need no hooks or graphQL population promises - validations: [linkValidation(props, sanitizedFieldsWithoutText)], - }), - createNode({ - converters: { - html: { - converter: async ({ converters, node, parent, req }) => { - const childrenText = await convertLexicalNodesToHTML({ - converters, - lexicalNodes: node.children, - parent: { - ...node, - parent, - }, - req, - }) - - const rel: string = node.fields.newTab ? ' rel="noopener noreferrer"' : '' - const target: string = node.fields.newTab ? ' target="_blank"' : '' - - const href: string = - node.fields.linkType === 'custom' - ? node.fields.url - : (node.fields.doc?.value as string) - - return `${childrenText}` - }, - nodeTypes: [LinkNode.getType()], + }, + node: AutoLinkNode, + // Since AutoLinkNodes are just internal links, they need no hooks or graphQL population promises + validations: [linkValidation(props, sanitizedFieldsWithoutText)], + }), + createNode({ + converters: { + html: { + converter: async ({ converters, node, parent, req }) => { + const childrenText = await convertLexicalNodesToHTML({ + converters, + lexicalNodes: node.children, + parent: { + ...node, + parent, + }, + req, + }) + + const rel: string = node.fields.newTab ? ' rel="noopener noreferrer"' : '' + const target: string = node.fields.newTab ? ' target="_blank"' : '' + + const href: string = + node.fields.linkType === 'custom' + ? node.fields.url + : (node.fields.doc?.value as string) + + return `${childrenText}` }, + nodeTypes: [LinkNode.getType()], }, - getSubFields: () => { - return sanitizedFieldsWithoutText - }, - getSubFieldsData: ({ node }) => { - return node?.fields - }, - graphQLPopulationPromises: [linkPopulationPromiseHOC(props)], - node: LinkNode, - validations: [linkValidation(props, sanitizedFieldsWithoutText)], - }), - ], - serverFeatureProps: props, - } - }, - key: 'link', - serverFeatureProps: props, - } -} + }, + getSubFields: () => { + return sanitizedFieldsWithoutText + }, + getSubFieldsData: ({ node }) => { + return node?.fields + }, + graphQLPopulationPromises: [linkPopulationPromiseHOC(props)], + node: LinkNode, + validations: [linkValidation(props, sanitizedFieldsWithoutText)], + }), + ], + sanitizedServerFeatureProps: props, + } + }, + key: 'link', +}) diff --git a/packages/richtext-lexical/src/features/lists/checklist/feature.client.tsx b/packages/richtext-lexical/src/features/lists/checklist/feature.client.tsx index f7fe0bc2548..e66cb225e26 100644 --- a/packages/richtext-lexical/src/features/lists/checklist/feature.client.tsx +++ b/packages/richtext-lexical/src/features/lists/checklist/feature.client.tsx @@ -3,10 +3,10 @@ import { $isListNode, INSERT_CHECK_LIST_COMMAND, ListItemNode, ListNode } from ' import { $isRangeSelection } from 'lexical' import type { ToolbarGroup } from '../../toolbars/types.js' -import type { ClientFeature, FeatureProviderProviderClient } from '../../types.js' +import type { ClientFeature } from '../../types.js' import { ChecklistIcon } from '../../../lexical/ui/icons/Checklist/index.js' -import { createClientComponent } from '../../createClientComponent.js' +import { createClientFeature } from '../../../utilities/createClientFeature.js' import { toolbarTextDropdownGroupWithItems } from '../../shared/toolbar/textDropdownGroup.js' import { LexicalListPlugin } from '../plugin/index.js' import { slashMenuListGroupWithItems } from '../shared/slashMenuListGroup.js' @@ -54,58 +54,50 @@ const toolbarGroups: ToolbarGroup[] = [ ]), ] -const ChecklistFeatureClient: FeatureProviderProviderClient = (props) => { - return { - clientFeatureProps: props, - feature: ({ featureProviderMap }) => { - const plugins: ClientFeature['plugins'] = [ - { - Component: LexicalCheckListPlugin, - position: 'normal', - }, - ] +export const ChecklistFeatureClient = createClientFeature(({ featureProviderMap }) => { + const plugins: ClientFeature['plugins'] = [ + { + Component: LexicalCheckListPlugin, + position: 'normal', + }, + ] - if (!featureProviderMap.has('unorderedList') && !featureProviderMap.has('orderedList')) { - plugins.push({ - Component: LexicalListPlugin, - position: 'normal', - }) - } + if (!featureProviderMap.has('unorderedList') && !featureProviderMap.has('orderedList')) { + plugins.push({ + Component: LexicalListPlugin, + position: 'normal', + }) + } - return { - clientFeatureProps: props, - markdownTransformers: [CHECK_LIST], - nodes: - featureProviderMap.has('unorderedList') || featureProviderMap.has('orderedList') - ? [] - : [ListNode, ListItemNode], - plugins, - slashMenu: { - groups: [ - slashMenuListGroupWithItems([ - { - Icon: ChecklistIcon, - key: 'checklist', - keywords: ['check list', 'check', 'checklist', 'cl'], - label: ({ i18n }) => { - return i18n.t('lexical:checklist:label') - }, - onSelect: ({ editor }) => { - editor.dispatchCommand(INSERT_CHECK_LIST_COMMAND, undefined) - }, - }, - ]), - ], - }, - toolbarFixed: { - groups: toolbarGroups, - }, - toolbarInline: { - groups: toolbarGroups, - }, - } + return { + markdownTransformers: [CHECK_LIST], + nodes: + featureProviderMap.has('unorderedList') || featureProviderMap.has('orderedList') + ? [] + : [ListNode, ListItemNode], + plugins, + slashMenu: { + groups: [ + slashMenuListGroupWithItems([ + { + Icon: ChecklistIcon, + key: 'checklist', + keywords: ['check list', 'check', 'checklist', 'cl'], + label: ({ i18n }) => { + return i18n.t('lexical:checklist:label') + }, + onSelect: ({ editor }) => { + editor.dispatchCommand(INSERT_CHECK_LIST_COMMAND, undefined) + }, + }, + ]), + ], + }, + toolbarFixed: { + groups: toolbarGroups, + }, + toolbarInline: { + groups: toolbarGroups, }, } -} - -export const ChecklistFeatureClientComponent = createClientComponent(ChecklistFeatureClient) +}) diff --git a/packages/richtext-lexical/src/features/lists/checklist/feature.server.ts b/packages/richtext-lexical/src/features/lists/checklist/feature.server.ts index 5db70576c3d..31ad88e4e9e 100644 --- a/packages/richtext-lexical/src/features/lists/checklist/feature.server.ts +++ b/packages/richtext-lexical/src/features/lists/checklist/feature.server.ts @@ -1,42 +1,37 @@ import { ListItemNode, ListNode } from '@lexical/list' -import type { FeatureProviderProviderServer } from '../../types.js' - // eslint-disable-next-line payload/no-imports-from-exports-dir -import { ChecklistFeatureClientComponent } from '../../../exports/client/index.js' +import { ChecklistFeatureClient } from '../../../exports/client/index.js' +import { createServerFeature } from '../../../utilities/createServerFeature.js' import { createNode } from '../../typeUtilities.js' import { ListHTMLConverter, ListItemHTMLConverter } from '../htmlConverter.js' import { i18n } from './i18n.js' import { CHECK_LIST } from './markdownTransformers.js' -export const ChecklistFeature: FeatureProviderProviderServer = (props) => { - return { - feature: ({ featureProviderMap }) => { - return { - ClientComponent: ChecklistFeatureClientComponent, - i18n, - markdownTransformers: [CHECK_LIST], - nodes: - featureProviderMap.has('unorderedList') || featureProviderMap.has('orderedList') - ? [] - : [ - createNode({ - converters: { - html: ListHTMLConverter, - }, - node: ListNode, - }), - createNode({ - converters: { - html: ListItemHTMLConverter, - }, - node: ListItemNode, - }), - ], - serverFeatureProps: props, - } - }, - key: 'checklist', - serverFeatureProps: props, - } -} +export const ChecklistFeature = createServerFeature({ + feature: ({ featureProviderMap }) => { + return { + ClientFeature: ChecklistFeatureClient, + i18n, + markdownTransformers: [CHECK_LIST], + nodes: + featureProviderMap.has('unorderedList') || featureProviderMap.has('orderedList') + ? [] + : [ + createNode({ + converters: { + html: ListHTMLConverter, + }, + node: ListNode, + }), + createNode({ + converters: { + html: ListItemHTMLConverter, + }, + node: ListItemNode, + }), + ], + } + }, + key: 'checklist', +}) diff --git a/packages/richtext-lexical/src/features/lists/orderedList/feature.client.tsx b/packages/richtext-lexical/src/features/lists/orderedList/feature.client.tsx index 0df4c802809..de873335a9f 100644 --- a/packages/richtext-lexical/src/features/lists/orderedList/feature.client.tsx +++ b/packages/richtext-lexical/src/features/lists/orderedList/feature.client.tsx @@ -3,10 +3,9 @@ import { $isListNode, INSERT_ORDERED_LIST_COMMAND, ListItemNode, ListNode } from import { $isRangeSelection } from 'lexical' import type { ToolbarGroup } from '../../toolbars/types.js' -import type { FeatureProviderProviderClient } from '../../types.js' import { OrderedListIcon } from '../../../lexical/ui/icons/OrderedList/index.js' -import { createClientComponent } from '../../createClientComponent.js' +import { createClientFeature } from '../../../utilities/createClientFeature.js' import { toolbarTextDropdownGroupWithItems } from '../../shared/toolbar/textDropdownGroup.js' import { LexicalListPlugin } from '../plugin/index.js' import { slashMenuListGroupWithItems } from '../shared/slashMenuListGroup.js' @@ -53,48 +52,40 @@ const toolbarGroups: ToolbarGroup[] = [ ]), ] -const OrderedListFeatureClient: FeatureProviderProviderClient = (props) => { +export const OrderedListFeatureClient = createClientFeature(({ featureProviderMap }) => { return { - clientFeatureProps: props, - feature: ({ featureProviderMap }) => { - return { - clientFeatureProps: props, - markdownTransformers: [ORDERED_LIST], - nodes: featureProviderMap.has('orderedList') ? [] : [ListNode, ListItemNode], - plugins: featureProviderMap.has('orderedList') - ? [] - : [ - { - Component: LexicalListPlugin, - position: 'normal', - }, - ], - slashMenu: { - groups: [ - slashMenuListGroupWithItems([ - { - Icon: OrderedListIcon, - key: 'orderedList', - keywords: ['ordered list', 'ol'], - label: ({ i18n }) => { - return i18n.t('lexical:orderedList:label') - }, - onSelect: ({ editor }) => { - editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined) - }, - }, - ]), - ], - }, - toolbarFixed: { - groups: toolbarGroups, - }, - toolbarInline: { - groups: toolbarGroups, - }, - } + markdownTransformers: [ORDERED_LIST], + nodes: featureProviderMap.has('orderedList') ? [] : [ListNode, ListItemNode], + plugins: featureProviderMap.has('orderedList') + ? [] + : [ + { + Component: LexicalListPlugin, + position: 'normal', + }, + ], + slashMenu: { + groups: [ + slashMenuListGroupWithItems([ + { + Icon: OrderedListIcon, + key: 'orderedList', + keywords: ['ordered list', 'ol'], + label: ({ i18n }) => { + return i18n.t('lexical:orderedList:label') + }, + onSelect: ({ editor }) => { + editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined) + }, + }, + ]), + ], + }, + toolbarFixed: { + groups: toolbarGroups, + }, + toolbarInline: { + groups: toolbarGroups, }, } -} - -export const OrderedListFeatureClientComponent = createClientComponent(OrderedListFeatureClient) +}) diff --git a/packages/richtext-lexical/src/features/lists/orderedList/feature.server.ts b/packages/richtext-lexical/src/features/lists/orderedList/feature.server.ts index b64dd278412..6dd3eb93330 100644 --- a/packages/richtext-lexical/src/features/lists/orderedList/feature.server.ts +++ b/packages/richtext-lexical/src/features/lists/orderedList/feature.server.ts @@ -3,39 +3,36 @@ import { ListItemNode, ListNode } from '@lexical/list' import type { FeatureProviderProviderServer } from '../../types.js' // eslint-disable-next-line payload/no-imports-from-exports-dir -import { OrderedListFeatureClientComponent } from '../../../exports/client/index.js' +import { OrderedListFeatureClient } from '../../../exports/client/index.js' +import { createServerFeature } from '../../../utilities/createServerFeature.js' import { createNode } from '../../typeUtilities.js' import { ListHTMLConverter, ListItemHTMLConverter } from '../htmlConverter.js' import { i18n } from './i18n.js' import { ORDERED_LIST } from './markdownTransformer.js' -export const OrderedListFeature: FeatureProviderProviderServer = (props) => { - return { - feature: ({ featureProviderMap }) => { - return { - ClientComponent: OrderedListFeatureClientComponent, - i18n, - markdownTransformers: [ORDERED_LIST], - nodes: featureProviderMap.has('unorderedList') - ? [] - : [ - createNode({ - converters: { - html: ListHTMLConverter, - }, - node: ListNode, - }), - createNode({ - converters: { - html: ListItemHTMLConverter, - }, - node: ListItemNode, - }), - ], - serverFeatureProps: props, - } - }, - key: 'orderedList', - serverFeatureProps: props, - } -} +export const OrderedListFeature = createServerFeature({ + feature: ({ featureProviderMap }) => { + return { + ClientFeature: OrderedListFeatureClient, + i18n, + markdownTransformers: [ORDERED_LIST], + nodes: featureProviderMap.has('unorderedList') + ? [] + : [ + createNode({ + converters: { + html: ListHTMLConverter, + }, + node: ListNode, + }), + createNode({ + converters: { + html: ListItemHTMLConverter, + }, + node: ListItemNode, + }), + ], + } + }, + key: 'orderedList', +}) diff --git a/packages/richtext-lexical/src/features/lists/unorderedList/feature.client.tsx b/packages/richtext-lexical/src/features/lists/unorderedList/feature.client.tsx index 8d911943ce3..6f100300d54 100644 --- a/packages/richtext-lexical/src/features/lists/unorderedList/feature.client.tsx +++ b/packages/richtext-lexical/src/features/lists/unorderedList/feature.client.tsx @@ -4,10 +4,9 @@ import { $isListNode, INSERT_UNORDERED_LIST_COMMAND, ListItemNode, ListNode } fr import { $isRangeSelection } from 'lexical' import type { ToolbarGroup } from '../../toolbars/types.js' -import type { FeatureProviderProviderClient } from '../../types.js' import { UnorderedListIcon } from '../../../lexical/ui/icons/UnorderedList/index.js' -import { createClientComponent } from '../../createClientComponent.js' +import { createClientFeature } from '../../../utilities/createClientFeature.js' import { toolbarTextDropdownGroupWithItems } from '../../shared/toolbar/textDropdownGroup.js' import { LexicalListPlugin } from '../plugin/index.js' import { slashMenuListGroupWithItems } from '../shared/slashMenuListGroup.js' @@ -54,46 +53,36 @@ const toolbarGroups: ToolbarGroup[] = [ ]), ] -const UnorderedListFeatureClient: FeatureProviderProviderClient = (props) => { - return { - clientFeatureProps: props, - feature: () => { - return { - clientFeatureProps: props, - markdownTransformers: [UNORDERED_LIST], - nodes: [ListNode, ListItemNode], - plugins: [ - { - Component: LexicalListPlugin, - position: 'normal', +export const UnorderedListFeatureClient = createClientFeature({ + markdownTransformers: [UNORDERED_LIST], + nodes: [ListNode, ListItemNode], + plugins: [ + { + Component: LexicalListPlugin, + position: 'normal', + }, + ], + slashMenu: { + groups: [ + slashMenuListGroupWithItems([ + { + Icon: UnorderedListIcon, + key: 'unorderedList', + keywords: ['unordered list', 'ul'], + label: ({ i18n }) => { + return i18n.t('lexical:unorderedList:label') + }, + onSelect: ({ editor }) => { + editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined) }, - ], - slashMenu: { - groups: [ - slashMenuListGroupWithItems([ - { - Icon: UnorderedListIcon, - key: 'unorderedList', - keywords: ['unordered list', 'ul'], - label: ({ i18n }) => { - return i18n.t('lexical:unorderedList:label') - }, - onSelect: ({ editor }) => { - editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined) - }, - }, - ]), - ], - }, - toolbarFixed: { - groups: toolbarGroups, - }, - toolbarInline: { - groups: toolbarGroups, }, - } - }, - } -} - -export const UnorderedListFeatureClientComponent = createClientComponent(UnorderedListFeatureClient) + ]), + ], + }, + toolbarFixed: { + groups: toolbarGroups, + }, + toolbarInline: { + groups: toolbarGroups, + }, +}) diff --git a/packages/richtext-lexical/src/features/lists/unorderedList/feature.server.ts b/packages/richtext-lexical/src/features/lists/unorderedList/feature.server.ts index 8113e823d36..c9cafc04298 100644 --- a/packages/richtext-lexical/src/features/lists/unorderedList/feature.server.ts +++ b/packages/richtext-lexical/src/features/lists/unorderedList/feature.server.ts @@ -1,41 +1,32 @@ import { ListItemNode, ListNode } from '@lexical/list' -import type { FeatureProviderProviderServer } from '../../types.js' - // eslint-disable-next-line payload/no-imports-from-exports-dir -import { UnorderedListFeatureClientComponent } from '../../../exports/client/index.js' +import { UnorderedListFeatureClient } from '../../../exports/client/index.js' +import { createServerFeature } from '../../../utilities/createServerFeature.js' import { createNode } from '../../typeUtilities.js' import { ListHTMLConverter, ListItemHTMLConverter } from '../htmlConverter.js' import { i18n } from './i18n.js' import { UNORDERED_LIST } from './markdownTransformer.js' -export const UnorderedListFeature: FeatureProviderProviderServer = ( - props, -) => { - return { - feature: () => { - return { - ClientComponent: UnorderedListFeatureClientComponent, - i18n, - markdownTransformers: [UNORDERED_LIST], - nodes: [ - createNode({ - converters: { - html: ListHTMLConverter, - }, - node: ListNode, - }), - createNode({ - converters: { - html: ListItemHTMLConverter, - }, - node: ListItemNode, - }), - ], - serverFeatureProps: props, - } - }, - key: 'unorderedList', - serverFeatureProps: props, - } -} +export const UnorderedListFeature = createServerFeature({ + feature: { + ClientFeature: UnorderedListFeatureClient, + i18n, + markdownTransformers: [UNORDERED_LIST], + nodes: [ + createNode({ + converters: { + html: ListHTMLConverter, + }, + node: ListNode, + }), + createNode({ + converters: { + html: ListItemHTMLConverter, + }, + node: ListItemNode, + }), + ], + }, + key: 'unorderedList', +}) diff --git a/packages/richtext-lexical/src/features/migrations/lexicalPluginToLexical/converter/converters/heading/index.ts b/packages/richtext-lexical/src/features/migrations/lexicalPluginToLexical/converter/converters/heading/index.ts index 9478213223a..b82f26533f6 100644 --- a/packages/richtext-lexical/src/features/migrations/lexicalPluginToLexical/converter/converters/heading/index.ts +++ b/packages/richtext-lexical/src/features/migrations/lexicalPluginToLexical/converter/converters/heading/index.ts @@ -4,6 +4,6 @@ import { HeadingConverterClient } from './client.js' import { _HeadingConverter } from './converter.js' export const HeadingConverter: LexicalPluginNodeConverterProvider = { - ClientComponent: HeadingConverterClient, + ClientConverter: HeadingConverterClient, converter: _HeadingConverter, } diff --git a/packages/richtext-lexical/src/features/migrations/lexicalPluginToLexical/converter/converters/link/index.ts b/packages/richtext-lexical/src/features/migrations/lexicalPluginToLexical/converter/converters/link/index.ts index 684eb9c022b..25c8e38336d 100644 --- a/packages/richtext-lexical/src/features/migrations/lexicalPluginToLexical/converter/converters/link/index.ts +++ b/packages/richtext-lexical/src/features/migrations/lexicalPluginToLexical/converter/converters/link/index.ts @@ -4,6 +4,6 @@ import { LinkConverterClient } from './client.js' import { _LinkConverter } from './converter.js' export const LinkConverter: LexicalPluginNodeConverterProvider = { - ClientComponent: LinkConverterClient, + ClientConverter: LinkConverterClient, converter: _LinkConverter, } diff --git a/packages/richtext-lexical/src/features/migrations/lexicalPluginToLexical/converter/converters/list/index.ts b/packages/richtext-lexical/src/features/migrations/lexicalPluginToLexical/converter/converters/list/index.ts index 23a9ee03c2a..bfa3b6d4783 100644 --- a/packages/richtext-lexical/src/features/migrations/lexicalPluginToLexical/converter/converters/list/index.ts +++ b/packages/richtext-lexical/src/features/migrations/lexicalPluginToLexical/converter/converters/list/index.ts @@ -4,6 +4,6 @@ import { ListConverterClient } from './client.js' import { _ListConverter } from './converter.js' export const ListConverter: LexicalPluginNodeConverterProvider = { - ClientComponent: ListConverterClient, + ClientConverter: ListConverterClient, converter: _ListConverter, } diff --git a/packages/richtext-lexical/src/features/migrations/lexicalPluginToLexical/converter/converters/listItem/index.ts b/packages/richtext-lexical/src/features/migrations/lexicalPluginToLexical/converter/converters/listItem/index.ts index 84f3bdb27d8..ce05cec0718 100644 --- a/packages/richtext-lexical/src/features/migrations/lexicalPluginToLexical/converter/converters/listItem/index.ts +++ b/packages/richtext-lexical/src/features/migrations/lexicalPluginToLexical/converter/converters/listItem/index.ts @@ -4,6 +4,6 @@ import { ListItemConverterClient } from './client.js' import { _ListItemConverter } from './converter.js' export const ListItemConverter: LexicalPluginNodeConverterProvider = { - ClientComponent: ListItemConverterClient, + ClientConverter: ListItemConverterClient, converter: _ListItemConverter, } diff --git a/packages/richtext-lexical/src/features/migrations/lexicalPluginToLexical/converter/converters/quote/index.ts b/packages/richtext-lexical/src/features/migrations/lexicalPluginToLexical/converter/converters/quote/index.ts index 6aeee4f1f6c..9e508848464 100644 --- a/packages/richtext-lexical/src/features/migrations/lexicalPluginToLexical/converter/converters/quote/index.ts +++ b/packages/richtext-lexical/src/features/migrations/lexicalPluginToLexical/converter/converters/quote/index.ts @@ -4,6 +4,6 @@ import { QuoteConverterClient } from './client.js' import { _QuoteConverter } from './converter.js' export const QuoteConverter: LexicalPluginNodeConverterProvider = { - ClientComponent: QuoteConverterClient, + ClientConverter: QuoteConverterClient, converter: _QuoteConverter, } diff --git a/packages/richtext-lexical/src/features/migrations/lexicalPluginToLexical/converter/converters/unknown/index.ts b/packages/richtext-lexical/src/features/migrations/lexicalPluginToLexical/converter/converters/unknown/index.ts index da52e05690c..a945a64975f 100644 --- a/packages/richtext-lexical/src/features/migrations/lexicalPluginToLexical/converter/converters/unknown/index.ts +++ b/packages/richtext-lexical/src/features/migrations/lexicalPluginToLexical/converter/converters/unknown/index.ts @@ -4,6 +4,6 @@ import { UnknownConverterClient } from './client.js' import { _UnknownConverter } from './converter.js' export const UnknownConverter: LexicalPluginNodeConverterProvider = { - ClientComponent: UnknownConverterClient, + ClientConverter: UnknownConverterClient, converter: _UnknownConverter, } diff --git a/packages/richtext-lexical/src/features/migrations/lexicalPluginToLexical/converter/converters/upload/index.ts b/packages/richtext-lexical/src/features/migrations/lexicalPluginToLexical/converter/converters/upload/index.ts index 8e38c195aa2..13acb03e801 100644 --- a/packages/richtext-lexical/src/features/migrations/lexicalPluginToLexical/converter/converters/upload/index.ts +++ b/packages/richtext-lexical/src/features/migrations/lexicalPluginToLexical/converter/converters/upload/index.ts @@ -4,6 +4,6 @@ import { UploadConverterClient } from './client.js' import { _UploadConverter } from './converter.js' export const UploadConverter: LexicalPluginNodeConverterProvider = { - ClientComponent: UploadConverterClient, + ClientConverter: UploadConverterClient, converter: _UploadConverter, } diff --git a/packages/richtext-lexical/src/features/migrations/lexicalPluginToLexical/converter/types.ts b/packages/richtext-lexical/src/features/migrations/lexicalPluginToLexical/converter/types.ts index 297379a5c6f..e0cdce061b7 100644 --- a/packages/richtext-lexical/src/features/migrations/lexicalPluginToLexical/converter/types.ts +++ b/packages/richtext-lexical/src/features/migrations/lexicalPluginToLexical/converter/types.ts @@ -32,6 +32,6 @@ export type LexicalPluginNodeConverterClientComponent = React.FC<{ }> export type LexicalPluginNodeConverterProvider = { - ClientComponent: LexicalPluginNodeConverterClientComponent + ClientConverter: LexicalPluginNodeConverterClientComponent converter: LexicalPluginNodeConverter } diff --git a/packages/richtext-lexical/src/features/migrations/lexicalPluginToLexical/feature.client.tsx b/packages/richtext-lexical/src/features/migrations/lexicalPluginToLexical/feature.client.tsx index b9039b86d92..3eecb5457e4 100644 --- a/packages/richtext-lexical/src/features/migrations/lexicalPluginToLexical/feature.client.tsx +++ b/packages/richtext-lexical/src/features/migrations/lexicalPluginToLexical/feature.client.tsx @@ -1,40 +1,29 @@ 'use client' -import type { FeatureProviderProviderClient } from '../../types.js' import type { LexicalPluginNodeConverter, PayloadPluginLexicalData } from './converter/types.js' -import { createClientComponent } from '../../createClientComponent.js' +import { createClientFeature } from '../../../utilities/createClientFeature.js' import { convertLexicalPluginToLexical } from './converter/index.js' import { UnknownConvertedNode } from './nodes/unknownConvertedNode/index.js' -const LexicalPluginToLexicalFeatureClient: FeatureProviderProviderClient = (props) => { - return { - clientFeatureProps: props, - feature: ({ clientFunctions, resolvedFeatures }) => { - const converters: LexicalPluginNodeConverter[] = Object.values(clientFunctions) +export const LexicalPluginToLexicalFeatureClient = createClientFeature(({ clientFunctions }) => { + const converters: LexicalPluginNodeConverter[] = Object.values(clientFunctions) - return { - clientFeatureProps: props, - hooks: { - load({ incomingEditorState }) { - if (!incomingEditorState || !('jsonContent' in incomingEditorState)) { - // incomingEditorState null or not from Lexical Plugin - return incomingEditorState - } - // Lexical Plugin => convert to lexical + return { + hooks: { + load({ incomingEditorState }) { + if (!incomingEditorState || !('jsonContent' in incomingEditorState)) { + // incomingEditorState null or not from Lexical Plugin + return incomingEditorState + } + // Lexical Plugin => convert to lexical - return convertLexicalPluginToLexical({ - converters, - lexicalPluginData: incomingEditorState as unknown as PayloadPluginLexicalData, - }) - }, - }, - nodes: [UnknownConvertedNode], - } + return convertLexicalPluginToLexical({ + converters, + lexicalPluginData: incomingEditorState as unknown as PayloadPluginLexicalData, + }) + }, }, + nodes: [UnknownConvertedNode], } -} - -export const LexicalPluginToLexicalFeatureClientComponent = createClientComponent( - LexicalPluginToLexicalFeatureClient, -) +}) diff --git a/packages/richtext-lexical/src/features/migrations/lexicalPluginToLexical/feature.server.ts b/packages/richtext-lexical/src/features/migrations/lexicalPluginToLexical/feature.server.ts index 40ff7abb152..be09238111c 100644 --- a/packages/richtext-lexical/src/features/migrations/lexicalPluginToLexical/feature.server.ts +++ b/packages/richtext-lexical/src/features/migrations/lexicalPluginToLexical/feature.server.ts @@ -1,10 +1,10 @@ import type React from 'react' -import type { FeatureProviderProviderServer } from '../../types.js' import type { LexicalPluginNodeConverterProvider } from './converter/types.js' // eslint-disable-next-line payload/no-imports-from-exports-dir -import { LexicalPluginToLexicalFeatureClientComponent } from '../../../exports/client/index.js' +import { LexicalPluginToLexicalFeatureClient } from '../../../exports/client/index.js' +import { createServerFeature } from '../../../utilities/createServerFeature.js' import { defaultConverters } from './converter/defaultConverters.js' import { UnknownConvertedNode } from './nodes/unknownConvertedNode/index.js' @@ -18,40 +18,36 @@ export type LexicalPluginToLexicalFeatureProps = { | LexicalPluginNodeConverterProvider[] } -export const LexicalPluginToLexicalFeature: FeatureProviderProviderServer< - LexicalPluginToLexicalFeatureProps, - null -> = (props) => { - if (!props) { - props = {} - } +export const LexicalPluginToLexicalFeature = + createServerFeature({ + feature: ({ props }) => { + if (!props) { + props = {} + } - let converters: LexicalPluginNodeConverterProvider[] = [] + let converters: LexicalPluginNodeConverterProvider[] = [] - if (props?.converters && typeof props?.converters === 'function') { - converters = props.converters({ defaultConverters }) - } else if (props.converters && typeof props?.converters !== 'function') { - converters = props.converters - } else { - converters = defaultConverters - } + if (props?.converters && typeof props?.converters === 'function') { + converters = props.converters({ defaultConverters }) + } else if (props.converters && typeof props?.converters !== 'function') { + converters = props.converters + } else { + converters = defaultConverters + } - props.converters = converters + props.converters = converters - return { - feature: () => { return { - ClientComponent: LexicalPluginToLexicalFeatureClientComponent, - clientFeatureProps: null, + ClientFeature: LexicalPluginToLexicalFeatureClient, generateComponentMap: () => { const map: { [key: string]: React.FC } = {} for (const converter of converters) { - if (converter.ClientComponent) { + if (converter.ClientConverter) { const key = converter.converter.nodeTypes.join('-') - map[key] = converter.ClientComponent + map[key] = converter.ClientConverter } } @@ -62,10 +58,8 @@ export const LexicalPluginToLexicalFeature: FeatureProviderProviderServer< node: UnknownConvertedNode, }, ], - serverFeatureProps: props, + sanitizedServerFeatureProps: props, } }, key: 'lexicalPluginToLexical', - serverFeatureProps: props, - } -} + }) diff --git a/packages/richtext-lexical/src/features/migrations/slateToLexical/converter/converters/blockquote/index.ts b/packages/richtext-lexical/src/features/migrations/slateToLexical/converter/converters/blockquote/index.ts index 44de2933c25..6066a9cd11a 100644 --- a/packages/richtext-lexical/src/features/migrations/slateToLexical/converter/converters/blockquote/index.ts +++ b/packages/richtext-lexical/src/features/migrations/slateToLexical/converter/converters/blockquote/index.ts @@ -4,6 +4,6 @@ import { BlockquoteConverterClient } from './client.js' import { _SlateBlockquoteConverter } from './converter.js' export const SlateBlockquoteConverter: SlateNodeConverterProvider = { - ClientComponent: BlockquoteConverterClient, + ClientConverter: BlockquoteConverterClient, converter: _SlateBlockquoteConverter, } diff --git a/packages/richtext-lexical/src/features/migrations/slateToLexical/converter/converters/heading/index.ts b/packages/richtext-lexical/src/features/migrations/slateToLexical/converter/converters/heading/index.ts index e8de9db0ec2..46f495d58b2 100644 --- a/packages/richtext-lexical/src/features/migrations/slateToLexical/converter/converters/heading/index.ts +++ b/packages/richtext-lexical/src/features/migrations/slateToLexical/converter/converters/heading/index.ts @@ -4,6 +4,6 @@ import { HeadingConverterClient } from './client.js' import { _SlateHeadingConverter } from './converter.js' export const SlateHeadingConverter: SlateNodeConverterProvider = { - ClientComponent: HeadingConverterClient, + ClientConverter: HeadingConverterClient, converter: _SlateHeadingConverter, } diff --git a/packages/richtext-lexical/src/features/migrations/slateToLexical/converter/converters/indent/index.ts b/packages/richtext-lexical/src/features/migrations/slateToLexical/converter/converters/indent/index.ts index a97dfb797df..93949a4b02a 100644 --- a/packages/richtext-lexical/src/features/migrations/slateToLexical/converter/converters/indent/index.ts +++ b/packages/richtext-lexical/src/features/migrations/slateToLexical/converter/converters/indent/index.ts @@ -4,6 +4,6 @@ import { IndentConverterClient } from './client.js' import { _SlateIndentConverter } from './converter.js' export const SlateIndentConverter: SlateNodeConverterProvider = { - ClientComponent: IndentConverterClient, + ClientConverter: IndentConverterClient, converter: _SlateIndentConverter, } diff --git a/packages/richtext-lexical/src/features/migrations/slateToLexical/converter/converters/link/index.ts b/packages/richtext-lexical/src/features/migrations/slateToLexical/converter/converters/link/index.ts index 8fe09b8ae9d..c8489d49b06 100644 --- a/packages/richtext-lexical/src/features/migrations/slateToLexical/converter/converters/link/index.ts +++ b/packages/richtext-lexical/src/features/migrations/slateToLexical/converter/converters/link/index.ts @@ -4,6 +4,6 @@ import { LinkConverterClient } from './client.js' import { _SlateLinkConverter } from './converter.js' export const SlateLinkConverter: SlateNodeConverterProvider = { - ClientComponent: LinkConverterClient, + ClientConverter: LinkConverterClient, converter: _SlateLinkConverter, } diff --git a/packages/richtext-lexical/src/features/migrations/slateToLexical/converter/converters/listItem/index.ts b/packages/richtext-lexical/src/features/migrations/slateToLexical/converter/converters/listItem/index.ts index 4c5d990f87a..68022cf906b 100644 --- a/packages/richtext-lexical/src/features/migrations/slateToLexical/converter/converters/listItem/index.ts +++ b/packages/richtext-lexical/src/features/migrations/slateToLexical/converter/converters/listItem/index.ts @@ -4,6 +4,6 @@ import { ListItemConverterClient } from './client.js' import { _SlateListItemConverter } from './converter.js' export const SlateListItemConverter: SlateNodeConverterProvider = { - ClientComponent: ListItemConverterClient, + ClientConverter: ListItemConverterClient, converter: _SlateListItemConverter, } diff --git a/packages/richtext-lexical/src/features/migrations/slateToLexical/converter/converters/orderedList/index.ts b/packages/richtext-lexical/src/features/migrations/slateToLexical/converter/converters/orderedList/index.ts index d19c2f8ed6f..f8790360880 100644 --- a/packages/richtext-lexical/src/features/migrations/slateToLexical/converter/converters/orderedList/index.ts +++ b/packages/richtext-lexical/src/features/migrations/slateToLexical/converter/converters/orderedList/index.ts @@ -4,6 +4,6 @@ import { OrderedListConverterClient } from './client.js' import { _SlateOrderedListConverter } from './converter.js' export const SlateOrderedListConverter: SlateNodeConverterProvider = { - ClientComponent: OrderedListConverterClient, + ClientConverter: OrderedListConverterClient, converter: _SlateOrderedListConverter, } diff --git a/packages/richtext-lexical/src/features/migrations/slateToLexical/converter/converters/relationship/index.ts b/packages/richtext-lexical/src/features/migrations/slateToLexical/converter/converters/relationship/index.ts index b7695485fc8..379bcbf3f45 100644 --- a/packages/richtext-lexical/src/features/migrations/slateToLexical/converter/converters/relationship/index.ts +++ b/packages/richtext-lexical/src/features/migrations/slateToLexical/converter/converters/relationship/index.ts @@ -4,6 +4,6 @@ import { RelationshipConverterClient } from './client.js' import { _SlateRelationshipConverter } from './converter.js' export const SlateRelationshipConverter: SlateNodeConverterProvider = { - ClientComponent: RelationshipConverterClient, + ClientConverter: RelationshipConverterClient, converter: _SlateRelationshipConverter, } diff --git a/packages/richtext-lexical/src/features/migrations/slateToLexical/converter/converters/unknown/index.ts b/packages/richtext-lexical/src/features/migrations/slateToLexical/converter/converters/unknown/index.ts index 4bd99ddefbf..783c4477b8d 100644 --- a/packages/richtext-lexical/src/features/migrations/slateToLexical/converter/converters/unknown/index.ts +++ b/packages/richtext-lexical/src/features/migrations/slateToLexical/converter/converters/unknown/index.ts @@ -4,6 +4,6 @@ import { UnknownConverterClient } from './client.js' import { _SlateUnknownConverter } from './converter.js' export const SlateUnknownConverter: SlateNodeConverterProvider = { - ClientComponent: UnknownConverterClient, + ClientConverter: UnknownConverterClient, converter: _SlateUnknownConverter, } diff --git a/packages/richtext-lexical/src/features/migrations/slateToLexical/converter/converters/unorderedList/index.ts b/packages/richtext-lexical/src/features/migrations/slateToLexical/converter/converters/unorderedList/index.ts index e817c1ce2aa..31dc8855340 100644 --- a/packages/richtext-lexical/src/features/migrations/slateToLexical/converter/converters/unorderedList/index.ts +++ b/packages/richtext-lexical/src/features/migrations/slateToLexical/converter/converters/unorderedList/index.ts @@ -4,6 +4,6 @@ import { UnorderedListConverterClient } from './client.js' import { _SlateUnorderedListConverter } from './converter.js' export const SlateUnorderedListConverter: SlateNodeConverterProvider = { - ClientComponent: UnorderedListConverterClient, + ClientConverter: UnorderedListConverterClient, converter: _SlateUnorderedListConverter, } diff --git a/packages/richtext-lexical/src/features/migrations/slateToLexical/converter/converters/upload/index.ts b/packages/richtext-lexical/src/features/migrations/slateToLexical/converter/converters/upload/index.ts index 149797dabb7..195ecab3f25 100644 --- a/packages/richtext-lexical/src/features/migrations/slateToLexical/converter/converters/upload/index.ts +++ b/packages/richtext-lexical/src/features/migrations/slateToLexical/converter/converters/upload/index.ts @@ -4,6 +4,6 @@ import { UploadConverterClient } from './client.js' import { _SlateUploadConverter } from './converter.js' export const SlateUploadConverter: SlateNodeConverterProvider = { - ClientComponent: UploadConverterClient, + ClientConverter: UploadConverterClient, converter: _SlateUploadConverter, } diff --git a/packages/richtext-lexical/src/features/migrations/slateToLexical/converter/types.ts b/packages/richtext-lexical/src/features/migrations/slateToLexical/converter/types.ts index b775315caea..49c7fbe300f 100644 --- a/packages/richtext-lexical/src/features/migrations/slateToLexical/converter/types.ts +++ b/packages/richtext-lexical/src/features/migrations/slateToLexical/converter/types.ts @@ -28,6 +28,6 @@ export type SlateNodeConverterClientComponent = React.FC<{ }> export type SlateNodeConverterProvider = { - ClientComponent: SlateNodeConverterClientComponent + ClientConverter: SlateNodeConverterClientComponent converter: SlateNodeConverter } diff --git a/packages/richtext-lexical/src/features/migrations/slateToLexical/feature.client.tsx b/packages/richtext-lexical/src/features/migrations/slateToLexical/feature.client.tsx index e2099ac9dd0..2cfc91f3465 100644 --- a/packages/richtext-lexical/src/features/migrations/slateToLexical/feature.client.tsx +++ b/packages/richtext-lexical/src/features/migrations/slateToLexical/feature.client.tsx @@ -1,44 +1,33 @@ 'use client' -import type { FeatureProviderProviderClient } from '../../types.js' import type { SlateNodeConverter } from './converter/types.js' -import { createClientComponent } from '../../createClientComponent.js' +import { createClientFeature } from '../../../utilities/createClientFeature.js' import { convertSlateToLexical } from './converter/index.js' import { UnknownConvertedNode } from './nodes/unknownConvertedNode/index.js' -const SlateToLexicalFeatureClient: FeatureProviderProviderClient = (props) => { - return { - clientFeatureProps: props, - feature: ({ clientFunctions }) => { - const converters: SlateNodeConverter[] = Object.values(clientFunctions) +export const SlateToLexicalFeatureClient = createClientFeature(({ clientFunctions }) => { + const converters: SlateNodeConverter[] = Object.values(clientFunctions) - return { - clientFeatureProps: props, - hooks: { - load({ incomingEditorState }) { - if ( - !incomingEditorState || - !Array.isArray(incomingEditorState) || - 'root' in incomingEditorState - ) { - // incomingEditorState null or not from Slate - return incomingEditorState - } - // Slate => convert to lexical + return { + hooks: { + load({ incomingEditorState }) { + if ( + !incomingEditorState || + !Array.isArray(incomingEditorState) || + 'root' in incomingEditorState + ) { + // incomingEditorState null or not from Slate + return incomingEditorState + } + // Slate => convert to lexical - return convertSlateToLexical({ - converters, - slateData: incomingEditorState, - }) - }, - }, - nodes: [UnknownConvertedNode], - } + return convertSlateToLexical({ + converters, + slateData: incomingEditorState, + }) + }, }, + nodes: [UnknownConvertedNode], } -} - -export const SlateToLexicalFeatureClientComponent = createClientComponent( - SlateToLexicalFeatureClient, -) +}) diff --git a/packages/richtext-lexical/src/features/migrations/slateToLexical/feature.server.ts b/packages/richtext-lexical/src/features/migrations/slateToLexical/feature.server.ts index a1e99eb88b4..99472b65340 100644 --- a/packages/richtext-lexical/src/features/migrations/slateToLexical/feature.server.ts +++ b/packages/richtext-lexical/src/features/migrations/slateToLexical/feature.server.ts @@ -1,10 +1,10 @@ import type React from 'react' -import type { FeatureProviderProviderServer } from '../../types.js' import type { SlateNodeConverterProvider } from './converter/types.js' // eslint-disable-next-line payload/no-imports-from-exports-dir -import { SlateToLexicalFeatureClientComponent } from '../../../exports/client/index.js' +import { SlateToLexicalFeatureClient } from '../../../exports/client/index.js' +import { createServerFeature } from '../../../utilities/createServerFeature.js' import { defaultSlateConverters } from './converter/defaultConverters.js' import { UnknownConvertedNode } from './nodes/unknownConvertedNode/index.js' @@ -18,53 +18,46 @@ export type SlateToLexicalFeatureProps = { | SlateNodeConverterProvider[] } -export const SlateToLexicalFeature: FeatureProviderProviderServer< - SlateToLexicalFeatureProps, - undefined -> = (props) => { - if (!props) { - props = {} - } - - let converters: SlateNodeConverterProvider[] = [] - if (props?.converters && typeof props?.converters === 'function') { - converters = props.converters({ defaultConverters: defaultSlateConverters }) - } else if (props.converters && typeof props?.converters !== 'function') { - converters = props.converters - } else { - converters = defaultSlateConverters - } - - props.converters = converters - - return { - feature: () => { - return { - ClientComponent: SlateToLexicalFeatureClientComponent, - clientFeatureProps: null, - generateComponentMap: () => { - const map: { - [key: string]: React.FC - } = {} - - for (const converter of converters) { - if (converter.ClientComponent) { - const key = converter.converter.nodeTypes.join('-') - map[key] = converter.ClientComponent - } +export const SlateToLexicalFeature = createServerFeature({ + feature: ({ props }) => { + if (!props) { + props = {} + } + + let converters: SlateNodeConverterProvider[] = [] + if (props?.converters && typeof props?.converters === 'function') { + converters = props.converters({ defaultConverters: defaultSlateConverters }) + } else if (props.converters && typeof props?.converters !== 'function') { + converters = props.converters + } else { + converters = defaultSlateConverters + } + + props.converters = converters + + return { + ClientFeature: SlateToLexicalFeatureClient, + generateComponentMap: () => { + const map: { + [key: string]: React.FC + } = {} + + for (const converter of converters) { + if (converter.ClientConverter) { + const key = converter.converter.nodeTypes.join('-') + map[key] = converter.ClientConverter } + } - return map + return map + }, + nodes: [ + { + node: UnknownConvertedNode, }, - nodes: [ - { - node: UnknownConvertedNode, - }, - ], - serverFeatureProps: props, - } - }, - key: 'slateToLexical', - serverFeatureProps: props, - } -} + ], + sanitizedServerFeatureProps: props, + } + }, + key: 'slateToLexical', +}) diff --git a/packages/richtext-lexical/src/features/paragraph/feature.client.tsx b/packages/richtext-lexical/src/features/paragraph/feature.client.tsx index 8e072fe42fe..7bd37303edc 100644 --- a/packages/richtext-lexical/src/features/paragraph/feature.client.tsx +++ b/packages/richtext-lexical/src/features/paragraph/feature.client.tsx @@ -4,10 +4,9 @@ import { $setBlocksType } from '@lexical/selection' import { $createParagraphNode, $getSelection, $isParagraphNode, $isRangeSelection } from 'lexical' import type { ToolbarGroup } from '../toolbars/types.js' -import type { FeatureProviderProviderClient } from '../types.js' import { TextIcon } from '../../lexical/ui/icons/Text/index.js' -import { createClientComponent } from '../createClientComponent.js' +import { createClientFeature } from '../../utilities/createClientFeature.js' import { slashMenuBasicGroupWithItems } from '../shared/slashMenu/basicGroup.js' import { toolbarTextDropdownGroupWithItems } from '../shared/toolbar/textDropdownGroup.js' @@ -41,39 +40,31 @@ const toolbarGroups: ToolbarGroup[] = [ ]), ] -const ParagraphFeatureClient: FeatureProviderProviderClient = (props) => { - return { - clientFeatureProps: props, - feature: () => ({ - clientFeatureProps: props, - slashMenu: { - groups: [ - slashMenuBasicGroupWithItems([ - { - Icon: TextIcon, - key: 'paragraph', - keywords: ['normal', 'paragraph', 'p', 'text'], - label: ({ i18n }) => { - return i18n.t('lexical:paragraph:label') - }, - onSelect: ({ editor }) => { - editor.update(() => { - const selection = $getSelection() - $setBlocksType(selection, () => $createParagraphNode()) - }) - }, - }, - ]), - ], - }, - toolbarFixed: { - groups: toolbarGroups, - }, - toolbarInline: { - groups: toolbarGroups, - }, - }), - } -} - -export const ParagraphFeatureClientComponent = createClientComponent(ParagraphFeatureClient) +export const ParagraphFeatureClient = createClientFeature({ + slashMenu: { + groups: [ + slashMenuBasicGroupWithItems([ + { + Icon: TextIcon, + key: 'paragraph', + keywords: ['normal', 'paragraph', 'p', 'text'], + label: ({ i18n }) => { + return i18n.t('lexical:paragraph:label') + }, + onSelect: ({ editor }) => { + editor.update(() => { + const selection = $getSelection() + $setBlocksType(selection, () => $createParagraphNode()) + }) + }, + }, + ]), + ], + }, + toolbarFixed: { + groups: toolbarGroups, + }, + toolbarInline: { + groups: toolbarGroups, + }, +}) diff --git a/packages/richtext-lexical/src/features/paragraph/feature.server.ts b/packages/richtext-lexical/src/features/paragraph/feature.server.ts index 46923a7cb6b..955ecf5dcb6 100644 --- a/packages/richtext-lexical/src/features/paragraph/feature.server.ts +++ b/packages/richtext-lexical/src/features/paragraph/feature.server.ts @@ -1,20 +1,13 @@ -import type { FeatureProviderProviderServer } from '../types.js' - // eslint-disable-next-line payload/no-imports-from-exports-dir -import { ParagraphFeatureClientComponent } from '../../exports/client/index.js' +import { ParagraphFeatureClient } from '../../exports/client/index.js' +import { createServerFeature } from '../../utilities/createServerFeature.js' import { i18n } from './i18n.js' -export const ParagraphFeature: FeatureProviderProviderServer = (props) => { - return { - feature: () => { - return { - ClientComponent: ParagraphFeatureClientComponent, - clientFeatureProps: null, - i18n, - serverFeatureProps: props, - } - }, - key: 'paragraph', - serverFeatureProps: props, - } -} +export const ParagraphFeature = createServerFeature({ + feature: { + ClientFeature: ParagraphFeatureClient, + clientFeatureProps: null, + i18n, + }, + key: 'paragraph', +}) diff --git a/packages/richtext-lexical/src/features/relationship/feature.client.tsx b/packages/richtext-lexical/src/features/relationship/feature.client.tsx index 091824509c5..7cfa52a08af 100644 --- a/packages/richtext-lexical/src/features/relationship/feature.client.tsx +++ b/packages/richtext-lexical/src/features/relationship/feature.client.tsx @@ -2,80 +2,69 @@ import { $isNodeSelection } from 'lexical' -import type { FeatureProviderProviderClient } from '../types.js' import type { RelationshipFeatureProps } from './feature.server.js' import { RelationshipIcon } from '../../lexical/ui/icons/Relationship/index.js' -import { createClientComponent } from '../createClientComponent.js' +import { createClientFeature } from '../../utilities/createClientFeature.js' import { slashMenuBasicGroupWithItems } from '../shared/slashMenu/basicGroup.js' import { toolbarAddDropdownGroupWithItems } from '../shared/toolbar/addDropdownGroup.js' import { INSERT_RELATIONSHIP_WITH_DRAWER_COMMAND } from './drawer/commands.js' import { $isRelationshipNode, RelationshipNode } from './nodes/RelationshipNode.js' import { RelationshipPlugin } from './plugins/index.js' -const RelationshipFeatureClient: FeatureProviderProviderClient = ( - props, -) => { - return { - clientFeatureProps: props, - feature: () => ({ - clientFeatureProps: props, - nodes: [RelationshipNode], - plugins: [ +export const RelationshipFeatureClient = createClientFeature({ + nodes: [RelationshipNode], + plugins: [ + { + Component: RelationshipPlugin, + position: 'normal', + }, + ], + slashMenu: { + groups: [ + slashMenuBasicGroupWithItems([ { - Component: RelationshipPlugin, - position: 'normal', + Icon: RelationshipIcon, + key: 'relationship', + keywords: ['relationship', 'relation', 'rel'], + label: ({ i18n }) => { + return i18n.t('lexical:relationship:label') + }, + onSelect: ({ editor }) => { + // dispatch INSERT_RELATIONSHIP_WITH_DRAWER_COMMAND + editor.dispatchCommand(INSERT_RELATIONSHIP_WITH_DRAWER_COMMAND, { + replace: false, + }) + }, }, - ], - slashMenu: { - groups: [ - slashMenuBasicGroupWithItems([ - { - Icon: RelationshipIcon, - key: 'relationship', - keywords: ['relationship', 'relation', 'rel'], - label: ({ i18n }) => { - return i18n.t('lexical:relationship:label') - }, - onSelect: ({ editor }) => { - // dispatch INSERT_RELATIONSHIP_WITH_DRAWER_COMMAND - editor.dispatchCommand(INSERT_RELATIONSHIP_WITH_DRAWER_COMMAND, { - replace: false, - }) - }, - }, - ]), - ], - }, - toolbarFixed: { - groups: [ - toolbarAddDropdownGroupWithItems([ - { - ChildComponent: RelationshipIcon, - isActive: ({ selection }) => { - if (!$isNodeSelection(selection) || !selection.getNodes().length) { - return false - } - - const firstNode = selection.getNodes()[0] - return $isRelationshipNode(firstNode) - }, - key: 'relationship', - label: ({ i18n }) => { - return i18n.t('lexical:relationship:label') - }, - onSelect: ({ editor }) => { - // dispatch INSERT_RELATIONSHIP_WITH_DRAWER_COMMAND - editor.dispatchCommand(INSERT_RELATIONSHIP_WITH_DRAWER_COMMAND, { - replace: false, - }) - }, - }, - ]), - ], - }, - }), - } -} + ]), + ], + }, + toolbarFixed: { + groups: [ + toolbarAddDropdownGroupWithItems([ + { + ChildComponent: RelationshipIcon, + isActive: ({ selection }) => { + if (!$isNodeSelection(selection) || !selection.getNodes().length) { + return false + } -export const RelationshipFeatureClientComponent = createClientComponent(RelationshipFeatureClient) + const firstNode = selection.getNodes()[0] + return $isRelationshipNode(firstNode) + }, + key: 'relationship', + label: ({ i18n }) => { + return i18n.t('lexical:relationship:label') + }, + onSelect: ({ editor }) => { + // dispatch INSERT_RELATIONSHIP_WITH_DRAWER_COMMAND + editor.dispatchCommand(INSERT_RELATIONSHIP_WITH_DRAWER_COMMAND, { + replace: false, + }) + }, + }, + ]), + ], + }, +}) diff --git a/packages/richtext-lexical/src/features/relationship/feature.server.ts b/packages/richtext-lexical/src/features/relationship/feature.server.ts index 5c424f959a1..4eb3d2ec6c8 100644 --- a/packages/richtext-lexical/src/features/relationship/feature.server.ts +++ b/packages/richtext-lexical/src/features/relationship/feature.server.ts @@ -1,8 +1,7 @@ -import type { FeatureProviderProviderServer } from '../types.js' - // eslint-disable-next-line payload/no-imports-from-exports-dir -import { RelationshipFeatureClientComponent } from '../../exports/client/index.js' +import { RelationshipFeatureClient } from '../../exports/client/index.js' import { populate } from '../../populateGraphQL/populate.js' +import { createServerFeature } from '../../utilities/createServerFeature.js' import { createNode } from '../typeUtilities.js' import { relationshipPopulationPromiseHOC } from './graphQLPopulationPromise.js' import { i18n } from './i18n.js' @@ -40,73 +39,64 @@ export type RelationshipFeatureProps = ExclusiveRelationshipFeatureProps & { maxDepth?: number } -export const RelationshipFeature: FeatureProviderProviderServer< +export const RelationshipFeature = createServerFeature< RelationshipFeatureProps, RelationshipFeatureProps -> = (props) => { - return { - feature: () => { - return { - ClientComponent: RelationshipFeatureClientComponent, - clientFeatureProps: props, - i18n, - nodes: [ - createNode({ - graphQLPopulationPromises: [relationshipPopulationPromiseHOC(props)], - hooks: { - afterRead: [ - ({ +>({ + feature: ({ props }) => ({ + ClientFeature: RelationshipFeatureClient, + i18n, + nodes: [ + createNode({ + graphQLPopulationPromises: [relationshipPopulationPromiseHOC(props)], + hooks: { + afterRead: [ + ({ + currentDepth, + depth, + draft, + node, + overrideAccess, + populationPromises, + req, + showHiddenFields, + }) => { + if (!node?.value) { + return node + } + const collection = req.payload.collections[node?.relationTo] + + if (!collection) { + return node + } + // @ts-expect-error + const id = node?.value?.id || node?.value // for backwards-compatibility + + const populateDepth = + props?.maxDepth !== undefined && props?.maxDepth < depth ? props?.maxDepth : depth + + populationPromises.push( + populate({ + id, + collectionSlug: collection.config.slug, currentDepth, - depth, + data: node, + depth: populateDepth, draft, - node, + key: 'value', overrideAccess, - populationPromises, req, showHiddenFields, - }) => { - if (!node?.value) { - return node - } - const collection = req.payload.collections[node?.relationTo] + }), + ) - if (!collection) { - return node - } - // @ts-expect-error - const id = node?.value?.id || node?.value // for backwards-compatibility - - const populateDepth = - props?.maxDepth !== undefined && props?.maxDepth < depth - ? props?.maxDepth - : depth - - populationPromises.push( - populate({ - id, - collectionSlug: collection.config.slug, - currentDepth, - data: node, - depth: populateDepth, - draft, - key: 'value', - overrideAccess, - req, - showHiddenFields, - }), - ) - - return node - }, - ], + return node }, - node: RelationshipNode, - }), - ], - serverFeatureProps: props, - } - }, - key: 'relationship', - serverFeatureProps: props, - } -} + ], + }, + node: RelationshipNode, + }), + ], + }), + key: 'relationship', +}) diff --git a/packages/richtext-lexical/src/features/toolbars/fixed/feature.client.tsx b/packages/richtext-lexical/src/features/toolbars/fixed/feature.client.tsx index e44fa9164cd..3ae6fa67387 100644 --- a/packages/richtext-lexical/src/features/toolbars/fixed/feature.client.tsx +++ b/packages/richtext-lexical/src/features/toolbars/fixed/feature.client.tsx @@ -11,15 +11,15 @@ const FixedToolbarFeatureClient: FeatureProviderProviderClient { return { clientFeatureProps: props, - feature: () => ({ - clientFeatureProps: props, + feature: { plugins: [ { Component: FixedToolbarPlugin, position: 'aboveContainer', }, ], - }), + sanitizedClientFeatureProps: props, + }, } } diff --git a/packages/richtext-lexical/src/features/toolbars/fixed/feature.server.ts b/packages/richtext-lexical/src/features/toolbars/fixed/feature.server.ts index 666dfb50dcf..cfd0f660dd8 100644 --- a/packages/richtext-lexical/src/features/toolbars/fixed/feature.server.ts +++ b/packages/richtext-lexical/src/features/toolbars/fixed/feature.server.ts @@ -21,6 +21,7 @@ export type FixedToolbarFeatureProps = { } export const FixedToolbarFeature: FeatureProviderProviderServer< + FixedToolbarFeatureProps, FixedToolbarFeatureProps, FixedToolbarFeatureProps > = (props) => { @@ -36,9 +37,9 @@ export const FixedToolbarFeature: FeatureProviderProviderServer< } return { - ClientComponent: FixedToolbarFeatureClientComponent, + ClientFeature: FixedToolbarFeatureClientComponent, clientFeatureProps: sanitizedProps, - serverFeatureProps: sanitizedProps, + sanitizedServerFeatureProps: sanitizedProps, } }, key: 'toolbarFixed', diff --git a/packages/richtext-lexical/src/features/toolbars/inline/feature.client.tsx b/packages/richtext-lexical/src/features/toolbars/inline/feature.client.tsx index 6b7266604b1..5b7e50601b8 100644 --- a/packages/richtext-lexical/src/features/toolbars/inline/feature.client.tsx +++ b/packages/richtext-lexical/src/features/toolbars/inline/feature.client.tsx @@ -9,13 +9,13 @@ const InlineToolbarFeatureClient: FeatureProviderProviderClient = (pr return { clientFeatureProps: props, feature: () => ({ - clientFeatureProps: props, plugins: [ { Component: InlineToolbarPlugin, position: 'floatingAnchorElem', }, ], + sanitizedClientFeatureProps: props, }), } } diff --git a/packages/richtext-lexical/src/features/toolbars/inline/feature.server.ts b/packages/richtext-lexical/src/features/toolbars/inline/feature.server.ts index 7c7640fc906..52d0c8a1888 100644 --- a/packages/richtext-lexical/src/features/toolbars/inline/feature.server.ts +++ b/packages/richtext-lexical/src/features/toolbars/inline/feature.server.ts @@ -7,11 +7,9 @@ export const InlineToolbarFeature: FeatureProviderProviderServer { return { - feature: () => { - return { - ClientComponent: InlineToolbarFeatureClientComponent, - serverFeatureProps: props, - } + feature: { + ClientFeature: InlineToolbarFeatureClientComponent, + sanitizedServerFeatureProps: props, }, key: 'toolbarInline', serverFeatureProps: props, diff --git a/packages/richtext-lexical/src/features/types.ts b/packages/richtext-lexical/src/features/types.ts index 44c4980a5c6..7aae0d85291 100644 --- a/packages/richtext-lexical/src/features/types.ts +++ b/packages/richtext-lexical/src/features/types.ts @@ -79,11 +79,19 @@ export type NodeValidation Promise | string | true -export type FeatureProviderProviderServer = ( - props?: ServerFeatureProps, -) => FeatureProviderServer +export type FeatureProviderProviderServer< + UnSanitizedServerFeatureProps = undefined, + ServerFeatureProps = UnSanitizedServerFeatureProps, + ClientFeatureProps = undefined, +> = ( + props?: UnSanitizedServerFeatureProps, +) => FeatureProviderServer -export type FeatureProviderServer = { +export type FeatureProviderServer< + UnSanitizedServerFeatureProps = undefined, + ServerFeatureProps = UnSanitizedServerFeatureProps, + ClientFeatureProps = undefined, +> = { /** Keys of dependencies needed for this feature. These dependencies do not have to be loaded first */ dependencies?: string[] /** Keys of priority dependencies needed for this feature. These dependencies have to be loaded first and are available in the `feature` property*/ @@ -94,44 +102,52 @@ export type FeatureProviderServer = { /** * This is being called during the payload sanitization process */ - feature: (props: { - config: SanitizedConfig - /** unSanitizedEditorConfig.features, but mapped */ - featureProviderMap: ServerFeatureProviderMap - isRoot?: boolean - // other resolved features, which have been loaded before this one. All features declared in 'dependencies' should be available here - resolvedFeatures: ResolvedServerFeatureMap - // unSanitized EditorConfig, - unSanitizedEditorConfig: ServerEditorConfig - }) => - | Promise> + feature: + | ((props: { + config: SanitizedConfig + /** unSanitizedEditorConfig.features, but mapped */ + featureProviderMap: ServerFeatureProviderMap + isRoot?: boolean + // other resolved features, which have been loaded before this one. All features declared in 'dependencies' should be available here + resolvedFeatures: ResolvedServerFeatureMap + // unSanitized EditorConfig, + unSanitizedEditorConfig: ServerEditorConfig + }) => + | Promise> + | ServerFeature) | ServerFeature key: string /** Props which were passed into your feature will have to be passed here. This will allow them to be used / read in other places of the code, e.g. wherever you can use useEditorConfigContext */ - serverFeatureProps: ServerFeatureProps + serverFeatureProps: UnSanitizedServerFeatureProps } -export type FeatureProviderProviderClient = ( - props: ClientComponentProps, -) => FeatureProviderClient +export type FeatureProviderProviderClient< + UnSanitizedClientFeatureProps = undefined, + ClientFeatureProps = UnSanitizedClientFeatureProps, +> = (props: ClientComponentProps) => FeatureProviderClient /** * No dependencies => Features need to be sorted on the server first, then sent to client in right order */ -export type FeatureProviderClient = { +export type FeatureProviderClient< + UnSanitizedClientFeatureProps = undefined, + ClientFeatureProps = UnSanitizedClientFeatureProps, +> = { /** * Return props, to make it easy to retrieve passed in props to this Feature for the client if anyone wants to */ - clientFeatureProps: ClientComponentProps - feature: (props: { - clientFunctions: Record - /** unSanitizedEditorConfig.features, but mapped */ - featureProviderMap: ClientFeatureProviderMap - // other resolved features, which have been loaded before this one. All features declared in 'dependencies' should be available here - resolvedFeatures: ResolvedClientFeatureMap - // unSanitized EditorConfig, - unSanitizedEditorConfig: ClientEditorConfig - }) => ClientFeature + clientFeatureProps: ClientComponentProps + feature: + | ((props: { + clientFunctions: Record + /** unSanitizedEditorConfig.features, but mapped */ + featureProviderMap: ClientFeatureProviderMap + // other resolved features, which have been loaded before this one. All features declared in 'dependencies' should be available here + resolvedFeatures: ResolvedClientFeatureMap + // unSanitized EditorConfig, + unSanitizedEditorConfig: ClientEditorConfig + }) => ClientFeature) + | ClientFeature } export type PluginComponent = React.FC<{ @@ -143,10 +159,6 @@ export type PluginComponentWithAnchor = React.FC<{ }> export type ClientFeature = { - /** - * Return props, to make it easy to retrieve passed in props to this Feature for the client if anyone wants to - */ - clientFeatureProps: ClientComponentProps hooks?: { load?: ({ incomingEditorState, @@ -195,6 +207,10 @@ export type ClientFeature = { position: 'belowContainer' // Determines at which position the Component will be added. } > + /** + * Return props, to make it easy to retrieve passed in props to this Feature for the client if anyone wants to + */ + sanitizedClientFeatureProps?: ClientComponentProps slashMenu?: { /** * Dynamic groups allow you to add different groups depending on the query string (so, the text after the slash). @@ -374,7 +390,7 @@ export type NodeWithHooks = { } export type ServerFeature = { - ClientComponent?: React.FC> + ClientFeature?: React.FC> /** * This determines what props will be available on the Client. */ @@ -440,7 +456,7 @@ export type ServerFeature = { nodes?: Array /** Props which were passed into your feature will have to be passed here. This will allow them to be used / read in other places of the code, e.g. wherever you can use useEditorConfigContext */ - serverFeatureProps: ServerProps + sanitizedServerFeatureProps?: ServerProps } export type ResolvedServerFeature = ServerFeature< @@ -464,8 +480,8 @@ export type ResolvedClientFeature = ClientFeature> export type ResolvedClientFeatureMap = Map> -export type ServerFeatureProviderMap = Map> -export type ClientFeatureProviderMap = Map> +export type ServerFeatureProviderMap = Map> +export type ClientFeatureProviderMap = Map> /** * Plugins are react components which get added to the editor. You can use them to interact with lexical, e.g. to create a command which creates a node, or opens a modal, or some other more "outside" functionality diff --git a/packages/richtext-lexical/src/features/upload/component/index.tsx b/packages/richtext-lexical/src/features/upload/component/index.tsx index c34a422b376..a02d2ac78c7 100644 --- a/packages/richtext-lexical/src/features/upload/component/index.tsx +++ b/packages/richtext-lexical/src/features/upload/component/index.tsx @@ -153,7 +153,7 @@ const Component: React.FC = (props) => { const hasExtraFields = ( editorConfig?.resolvedFeatureMap?.get('upload') - ?.clientFeatureProps as ClientComponentProps + ?.sanitizedClientFeatureProps as ClientComponentProps ).collections?.[relatedCollection.slug]?.hasExtraFields return ( diff --git a/packages/richtext-lexical/src/features/upload/feature.client.tsx b/packages/richtext-lexical/src/features/upload/feature.client.tsx index bd5567ef483..6e69a9c5aea 100644 --- a/packages/richtext-lexical/src/features/upload/feature.client.tsx +++ b/packages/richtext-lexical/src/features/upload/feature.client.tsx @@ -24,7 +24,6 @@ const UploadFeatureClient: FeatureProviderProviderClient ({ - clientFeatureProps: props, nodes: [UploadNode], plugins: [ { @@ -32,6 +31,7 @@ const UploadFeatureClient: FeatureProviderProviderClient = (props) => { @@ -72,7 +73,7 @@ export const UploadFeature: FeatureProviderProviderServer< } return { - ClientComponent: UploadFeatureClientComponent, + ClientFeature: UploadFeatureClientComponent, clientFeatureProps: clientProps, generateSchemaMap: ({ config, i18n, props }) => { if (!props?.collections) return null @@ -261,7 +262,7 @@ export const UploadFeature: FeatureProviderProviderServer< validations: [uploadValidation(props)], }), ], - serverFeatureProps: props, + sanitizedServerFeatureProps: props, } }, key: 'upload', diff --git a/packages/richtext-lexical/src/field/index.tsx b/packages/richtext-lexical/src/field/index.tsx index 9396d70b544..caeddd9f333 100644 --- a/packages/richtext-lexical/src/field/index.tsx +++ b/packages/richtext-lexical/src/field/index.tsx @@ -30,7 +30,9 @@ export const RichTextField: React.FC< const clientFunctions = useClientFunctions() const [hasLoadedFeatures, setHasLoadedFeatures] = useState(false) - const [featureProviders, setFeatureProviders] = useState[]>([]) + const [featureProviders, setFeatureProviders] = useState< + FeatureProviderClient[] + >([]) const [finalSanitizedEditorConfig, setFinalSanitizedEditorConfig] = useState(null) @@ -53,7 +55,7 @@ export const RichTextField: React.FC< useEffect(() => { if (!hasLoadedFeatures) { - const featureProvidersLocal: FeatureProviderClient[] = [] + const featureProvidersLocal: FeatureProviderClient[] = [] let featureProvidersAndComponentsLoaded = 0 Object.entries(clientFunctions).forEach(([key, plugin]) => { @@ -123,7 +125,7 @@ export const RichTextField: React.FC< return FeatureComponent }) : null} - {featureProvider.ClientComponent} + {featureProvider.ClientFeature} ) })} diff --git a/packages/richtext-lexical/src/index.ts b/packages/richtext-lexical/src/index.ts index e8bc68cdbe4..ac2f677510f 100644 --- a/packages/richtext-lexical/src/index.ts +++ b/packages/richtext-lexical/src/index.ts @@ -83,7 +83,7 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte } } - let features: FeatureProviderServer[] = [] + let features: FeatureProviderServer[] = [] let resolvedFeatureMap: ResolvedServerFeatureMap let finalSanitizedEditorConfig: SanitizedServerEditorConfig // For server only @@ -101,7 +101,7 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte resolvedFeatureMap = finalSanitizedEditorConfig.resolvedFeatureMap } else { const rootEditor = config.editor - let rootEditorFeatures: FeatureProviderServer[] = [] + let rootEditorFeatures: FeatureProviderServer[] = [] if (typeof rootEditor === 'object' && 'features' in rootEditor) { rootEditorFeatures = (rootEditor as LexicalRichTextAdapter).features } @@ -112,7 +112,7 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte defaultFeatures: cloneDeep(defaultEditorFeatures), rootFeatures: rootEditorFeatures, }) - : (props.features as FeatureProviderServer[]) + : (props.features as FeatureProviderServer[]) if (!features) { features = cloneDeep(defaultEditorFeatures) } @@ -277,8 +277,8 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte if (subFieldFn) { const subFields = subFieldFn({ node, req }) - const data = subFieldDataFn({ node, req }) - const originalData = subFieldDataFn({ node: originalNodeIDMap[id], req }) + const data = subFieldDataFn({ node, req }) ?? {} + const originalData = subFieldDataFn({ node: originalNodeIDMap[id], req }) ?? {} if (subFields?.length) { await afterChangeTraverseFields({ @@ -375,7 +375,7 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte if (subFieldFn) { const subFields = subFieldFn({ node, req }) - const data = subFieldDataFn({ node, req }) + const data = subFieldDataFn({ node, req }) ?? {} if (subFields?.length) { afterReadTraverseFields({ @@ -508,12 +508,13 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte if (subFieldFn) { const subFields = subFieldFn({ node, req }) - const data = subFieldDataFn({ node, req }) - const originalData = subFieldDataFn({ node: originalNodeIDMap[id], req }) - const originalDataWithLocales = subFieldDataFn({ - node: originalNodeWithLocalesIDMap[id], - req, - }) + const data = subFieldDataFn({ node, req }) ?? {} + const originalData = subFieldDataFn({ node: originalNodeIDMap[id], req }) ?? {} + const originalDataWithLocales = + subFieldDataFn({ + node: originalNodeWithLocalesIDMap[id], + req, + }) ?? {} if (subFields?.length) { await beforeChangeTraverseFields({ @@ -694,8 +695,8 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte if (subFieldFn) { const subFields = subFieldFn({ node, req }) - const data = subFieldDataFn({ node, req }) - const originalData = subFieldDataFn({ node: originalNodeIDMap[id], req }) + const data = subFieldDataFn({ node, req }) ?? {} + const originalData = subFieldDataFn({ node: originalNodeIDMap[id], req }) ?? {} if (subFields?.length) { await beforeValidateTraverseFields({ @@ -999,3 +1000,6 @@ export { sanitizeUrl, validateUrl } from './lexical/utils/url.js' export { defaultRichTextValue } from './populateGraphQL/defaultValue.js' export type { LexicalEditorProps, LexicalRichTextAdapter } from './types.js' + +export { createClientFeature } from './utilities/createClientFeature.js' +export { createServerFeature } from './utilities/createServerFeature.js' diff --git a/packages/richtext-lexical/src/lexical/config/client/loader.ts b/packages/richtext-lexical/src/lexical/config/client/loader.ts index a58c81f2a5f..f431c494264 100644 --- a/packages/richtext-lexical/src/lexical/config/client/loader.ts +++ b/packages/richtext-lexical/src/lexical/config/client/loader.ts @@ -39,7 +39,8 @@ export function loadClientFeatures({ const featureProviderMap: ClientFeatureProviderMap = new Map( unSanitizedEditorConfig.features.map( - (f) => [f.clientFeatureProps.featureKey, f] as [string, FeatureProviderClient], + (f) => + [f.clientFeatureProps.featureKey, f] as [string, FeatureProviderClient], ), ) @@ -65,12 +66,16 @@ export function loadClientFeatures({ } }) - const feature = featureProvider.feature({ - clientFunctions: relevantClientFunctions, - featureProviderMap, - resolvedFeatures, - unSanitizedEditorConfig, - }) + const feature = + typeof featureProvider.feature === 'function' + ? featureProvider.feature({ + clientFunctions: relevantClientFunctions, + featureProviderMap, + resolvedFeatures, + unSanitizedEditorConfig, + }) + : featureProvider.feature + resolvedFeatures.set(featureProvider.clientFeatureProps.featureKey, { ...feature, key: featureProvider.clientFeatureProps.featureKey, diff --git a/packages/richtext-lexical/src/lexical/config/client/sanitize.ts b/packages/richtext-lexical/src/lexical/config/client/sanitize.ts index ca02a0f089c..93f18dae519 100644 --- a/packages/richtext-lexical/src/lexical/config/client/sanitize.ts +++ b/packages/richtext-lexical/src/lexical/config/client/sanitize.ts @@ -51,7 +51,7 @@ export const sanitizeClientFeatures = ( feature.plugins.forEach((plugin, i) => { sanitized.plugins.push({ Component: plugin.Component, - clientProps: feature.clientFeatureProps, + clientProps: feature.sanitizedClientFeatureProps, key: feature.key + i, position: plugin.position, }) diff --git a/packages/richtext-lexical/src/lexical/config/server/default.ts b/packages/richtext-lexical/src/lexical/config/server/default.ts index 11b3766fc77..5d6d1a60e1d 100644 --- a/packages/richtext-lexical/src/lexical/config/server/default.ts +++ b/packages/richtext-lexical/src/lexical/config/server/default.ts @@ -30,7 +30,7 @@ export const defaultEditorLexicalConfig: LexicalEditorConfig = { theme: LexicalEditorTheme, } -export const defaultEditorFeatures: FeatureProviderServer[] = [ +export const defaultEditorFeatures: FeatureProviderServer[] = [ BoldFeature(), ItalicFeature(), UnderlineFeature(), diff --git a/packages/richtext-lexical/src/lexical/config/server/loader.ts b/packages/richtext-lexical/src/lexical/config/server/loader.ts index 5440033bcb0..04a1692d95b 100644 --- a/packages/richtext-lexical/src/lexical/config/server/loader.ts +++ b/packages/richtext-lexical/src/lexical/config/server/loader.ts @@ -12,12 +12,12 @@ type DependencyGraph = { dependencies: string[] dependenciesPriority: string[] dependenciesSoft: string[] - featureProvider: FeatureProviderServer + featureProvider: FeatureProviderServer } } function createDependencyGraph( - featureProviders: FeatureProviderServer[], + featureProviders: FeatureProviderServer[], ): DependencyGraph { const graph: DependencyGraph = {} for (const fp of featureProviders) { @@ -32,11 +32,11 @@ function createDependencyGraph( } function topologicallySortFeatures( - featureProviders: FeatureProviderServer[], -): FeatureProviderServer[] { + featureProviders: FeatureProviderServer[], +): FeatureProviderServer[] { const graph = createDependencyGraph(featureProviders) const visited: { [key: string]: boolean } = {} - const stack: FeatureProviderServer[] = [] + const stack: FeatureProviderServer[] = [] for (const key in graph) { if (!visited[key]) { @@ -51,7 +51,7 @@ function visit( graph: DependencyGraph, key: string, visited: { [key: string]: boolean }, - stack: FeatureProviderServer[], + stack: FeatureProviderServer[], currentPath: string[] = [], ) { if (!graph[key]) { @@ -100,8 +100,8 @@ function visit( } export function sortFeaturesForOptimalLoading( - featureProviders: FeatureProviderServer[], -): FeatureProviderServer[] { + featureProviders: FeatureProviderServer[], +): FeatureProviderServer[] { return topologicallySortFeatures(featureProviders) } @@ -127,7 +127,7 @@ export async function loadFeatures({ const featureProviderMap: ServerFeatureProviderMap = new Map( unSanitizedEditorConfig.features.map( - (f) => [f.key, f] as [string, FeatureProviderServer], + (f) => [f.key, f] as [string, FeatureProviderServer], ), ) @@ -173,13 +173,16 @@ export async function loadFeatures({ } } - const feature = await featureProvider.feature({ - config, - featureProviderMap, - isRoot, - resolvedFeatures, - unSanitizedEditorConfig, - }) + const feature = + typeof featureProvider.feature === 'function' + ? await featureProvider.feature({ + config, + featureProviderMap, + isRoot, + resolvedFeatures, + unSanitizedEditorConfig, + }) + : featureProvider.feature resolvedFeatures.set(featureProvider.key, { ...feature, dependencies: featureProvider.dependencies, diff --git a/packages/richtext-lexical/src/lexical/config/types.ts b/packages/richtext-lexical/src/lexical/config/types.ts index 090a23a1401..633db0e4355 100644 --- a/packages/richtext-lexical/src/lexical/config/types.ts +++ b/packages/richtext-lexical/src/lexical/config/types.ts @@ -11,7 +11,7 @@ import type { import type { LexicalFieldAdminProps } from '../../types.js' export type ServerEditorConfig = { - features: FeatureProviderServer[] + features: FeatureProviderServer[] lexical?: LexicalEditorConfig } @@ -22,7 +22,7 @@ export type SanitizedServerEditorConfig = { } export type ClientEditorConfig = { - features: FeatureProviderClient[] + features: FeatureProviderClient[] lexical?: LexicalEditorConfig } diff --git a/packages/richtext-lexical/src/types.ts b/packages/richtext-lexical/src/types.ts index ba303c0edfd..3cac625c6f1 100644 --- a/packages/richtext-lexical/src/types.ts +++ b/packages/richtext-lexical/src/types.ts @@ -36,7 +36,7 @@ export type LexicalEditorProps = { * }) * ``` */ - defaultFeatures: FeatureProviderServer[] + defaultFeatures: FeatureProviderServer[] /** * This array contains all features that are enabled in the root richText editor (the one defined in the payload.config.ts). * If this field is the root richText editor, or if the root richText editor is not a lexical editor, this array will be empty @@ -49,15 +49,15 @@ export type LexicalEditorProps = { * }) * ``` */ - rootFeatures: FeatureProviderServer[] - }) => FeatureProviderServer[]) - | FeatureProviderServer[] + rootFeatures: FeatureProviderServer[] + }) => FeatureProviderServer[]) + | FeatureProviderServer[] lexical?: LexicalEditorConfig } export type LexicalRichTextAdapter = RichTextAdapter & { editorConfig: SanitizedServerEditorConfig - features: FeatureProviderServer[] + features: FeatureProviderServer[] } export type LexicalRichTextAdapterProvider = @@ -84,7 +84,7 @@ export type AdapterProps = { } export type GeneratedFeatureProviderComponent = { - ClientComponent: React.ReactNode + ClientFeature: React.ReactNode key: string order: number } diff --git a/packages/richtext-lexical/src/utilities/createClientFeature.ts b/packages/richtext-lexical/src/utilities/createClientFeature.ts new file mode 100644 index 00000000000..1903c011b70 --- /dev/null +++ b/packages/richtext-lexical/src/utilities/createClientFeature.ts @@ -0,0 +1,68 @@ +import type React from 'react' + +import type { + ClientComponentProps, + ClientFeature, + ClientFeatureProviderMap, + FeatureProviderClient, + FeatureProviderProviderClient, + ResolvedClientFeatureMap, +} from '../features/types.js' +import type { ClientEditorConfig } from '../lexical/config/types.js' + +import { createClientComponent } from '../features/createClientComponent.js' + +export type CreateClientFeatureArgs = + | ((props: { + clientFunctions: Record + /** unSanitizedEditorConfig.features, but mapped */ + featureProviderMap: ClientFeatureProviderMap + props: ClientComponentProps + // other resolved features, which have been loaded before this one. All features declared in 'dependencies' should be available here + resolvedFeatures: ResolvedClientFeatureMap + // unSanitized EditorConfig, + unSanitizedEditorConfig: ClientEditorConfig + }) => ClientFeature) + | Omit, 'sanitizedClientFeatureProps'> + +export const createClientFeature: < + UnSanitizedClientProps = undefined, + ClientProps = UnSanitizedClientProps, +>( + args: CreateClientFeatureArgs, +) => React.FC> = (feature) => { + const featureProviderProvideClient: FeatureProviderProviderClient = (props) => { + const featureProviderClient: Partial> = { + clientFeatureProps: props, + } + + if (typeof feature === 'function') { + featureProviderClient.feature = ({ + clientFunctions, + featureProviderMap, + resolvedFeatures, + unSanitizedEditorConfig, + }) => { + const toReturn = feature({ + clientFunctions, + featureProviderMap, + props, + resolvedFeatures, + unSanitizedEditorConfig, + }) + + if (toReturn.sanitizedClientFeatureProps === null) { + toReturn.sanitizedClientFeatureProps = props + } + + return toReturn + } + } else { + ;(feature as ClientFeature).sanitizedClientFeatureProps = props + featureProviderClient.feature = feature as ClientFeature + } + return featureProviderClient as FeatureProviderClient + } + + return createClientComponent(featureProviderProvideClient) +} diff --git a/packages/richtext-lexical/src/utilities/createServerFeature.ts b/packages/richtext-lexical/src/utilities/createServerFeature.ts new file mode 100644 index 00000000000..d1419a507f9 --- /dev/null +++ b/packages/richtext-lexical/src/utilities/createServerFeature.ts @@ -0,0 +1,85 @@ +import type { SanitizedConfig } from 'payload' + +import type { + FeatureProviderProviderServer, + FeatureProviderServer, + ResolvedServerFeatureMap, + ServerFeature, + ServerFeatureProviderMap, +} from '../features/types.js' +import type { ServerEditorConfig } from '../lexical/config/types.js' + +export type CreateServerFeatureArgs = Pick< + FeatureProviderServer, + 'dependencies' | 'dependenciesPriority' | 'dependenciesSoft' | 'key' +> & { + feature: + | ((props: { + config: SanitizedConfig + /** unSanitizedEditorConfig.features, but mapped */ + featureProviderMap: ServerFeatureProviderMap + isRoot?: boolean + props: UnSanitizedProps + // other resolved features, which have been loaded before this one. All features declared in 'dependencies' should be available here + resolvedFeatures: ResolvedServerFeatureMap + // unSanitized EditorConfig, + unSanitizedEditorConfig: ServerEditorConfig + }) => + | Promise> + | ServerFeature) + | Omit, 'sanitizedServerFeatureProps'> +} + +export const createServerFeature: < + UnSanitizedProps = undefined, + SanitizedProps = UnSanitizedProps, + ClientProps = undefined, +>( + args: CreateServerFeatureArgs, +) => FeatureProviderProviderServer = ({ + dependencies, + dependenciesPriority, + dependenciesSoft, + feature, + key, +}) => { + const featureProviderProviderServer: FeatureProviderProviderServer = (props) => { + const featureProviderServer: Partial> = { + dependencies, + dependenciesPriority, + dependenciesSoft, + key, + serverFeatureProps: props, + } + + if (typeof feature === 'function') { + featureProviderServer.feature = async ({ + config, + featureProviderMap, + isRoot, + resolvedFeatures, + unSanitizedEditorConfig, + }) => { + const toReturn = await feature({ + config, + featureProviderMap, + isRoot, + props, + resolvedFeatures, + unSanitizedEditorConfig, + }) + + if (toReturn.sanitizedServerFeatureProps === null) { + toReturn.sanitizedServerFeatureProps = props + } + return toReturn + } + } else { + ;(feature as ServerFeature).sanitizedServerFeatureProps = props + featureProviderServer.feature = feature as ServerFeature + } + return featureProviderServer as FeatureProviderServer + } + + return featureProviderProviderServer +} diff --git a/packages/richtext-lexical/src/utilities/generateComponentMap.tsx b/packages/richtext-lexical/src/utilities/generateComponentMap.tsx index 83b708f52a2..0ecabb39859 100644 --- a/packages/richtext-lexical/src/utilities/generateComponentMap.tsx +++ b/packages/richtext-lexical/src/utilities/generateComponentMap.tsx @@ -22,7 +22,7 @@ export const getGenerateComponentMap = `features`, resolvedFeatureMapArray .map(([featureKey, resolvedFeature]) => { - const ClientComponent = resolvedFeature.ClientComponent + const ClientComponent = resolvedFeature.ClientFeature const clientComponentProps = resolvedFeature.clientFeatureProps /** @@ -35,7 +35,7 @@ export const getGenerateComponentMap = const components = resolvedFeature.generateComponentMap({ config, i18n, - props: resolvedFeature.serverFeatureProps, + props: resolvedFeature.sanitizedServerFeatureProps, schemaPath, }) @@ -66,7 +66,7 @@ export const getGenerateComponentMap = const schemas = resolvedFeature.generateSchemaMap({ config, i18n, - props: resolvedFeature.serverFeatureProps, + props: resolvedFeature.sanitizedServerFeatureProps, schemaMap: new Map(), schemaPath, }) @@ -93,7 +93,7 @@ export const getGenerateComponentMap = } return { - ClientComponent: + ClientFeature: clientComponentProps && typeof clientComponentProps === 'object' ? (