feat(richtext-lexical): HTML Serializer (#3685)

* chore(richtext-lexical): add jsdocs for afterReadPromise in GraphQL

* feat(richtext-lexical): HTML Serializer

* chore(richtext-lexical): adjust comment

* chore(richtext-lexical): change the way the html serializer works

* chore: working html converter field, improve various exports

* feat: link and heading html serializers

* fix: populationPromises not being added properly

* feat: allow html serializers to be async

* feat: upload html serializer

* feat: text format => html

* feat: lists => html

* feat: Quote => html

* chore: improve Checklist => html conversion, by passing in the full parent to converters
This commit is contained in:
Alessio Gravili
2023-10-21 14:37:59 +02:00
committed by GitHub
parent f6adbae0c7
commit 0af36af16c
47 changed files with 769 additions and 183 deletions

View File

@@ -12,7 +12,17 @@ export type RichTextFieldProps<Value extends object, AdapterProps> = Omit<
export type RichTextAdapter<Value extends object = object, AdapterProps = any> = { export type RichTextAdapter<Value extends object = object, AdapterProps = any> = {
CellComponent: React.FC<CellComponentProps<RichTextField<Value, AdapterProps>>> CellComponent: React.FC<CellComponentProps<RichTextField<Value, AdapterProps>>>
FieldComponent: React.FC<RichTextFieldProps<Value, AdapterProps>> FieldComponent: React.FC<RichTextFieldProps<Value, AdapterProps>>
afterReadPromise?: (data: { afterReadPromise?: ({
field,
incomingEditorState,
siblingDoc,
}: {
field: RichTextField<Value, AdapterProps>
incomingEditorState: Value
siblingDoc: Record<string, unknown>
}) => Promise<void> | null
populationPromise?: (data: {
currentDepth?: number currentDepth?: number
depth: number depth: number
field: RichTextField<Value, AdapterProps> field: RichTextField<Value, AdapterProps>

View File

@@ -90,12 +90,17 @@ export default joi.object({
debug: joi.boolean(), debug: joi.boolean(),
defaultDepth: joi.number().min(0).max(30), defaultDepth: joi.number().min(0).max(30),
defaultMaxTextLength: joi.number(), defaultMaxTextLength: joi.number(),
editor: joi.object().required().keys({ editor: joi
CellComponent: component.required(), .object()
FieldComponent: component.required(), .required()
afterReadPromise: joi.func().required(), .keys({
validate: joi.func().required(), CellComponent: component.required(),
}), FieldComponent: component.required(),
afterReadPromise: joi.func().optional(),
populationPromise: joi.func().optional(),
validate: joi.func().required(),
})
.unknown(),
email: joi.object(), email: joi.object(),
endpoints: endpointsSchema, endpoints: endpointsSchema,
express: joi.object().keys({ express: joi.object().keys({

View File

View File

@@ -354,12 +354,16 @@ export const richText = baseField.keys({
name: joi.string().required(), name: joi.string().required(),
admin: baseAdminFields.default(), admin: baseAdminFields.default(),
defaultValue: joi.alternatives().try(joi.array().items(joi.object()), joi.func()), defaultValue: joi.alternatives().try(joi.array().items(joi.object()), joi.func()),
editor: joi.object().keys({ editor: joi
CellComponent: componentSchema.required(), .object()
FieldComponent: componentSchema.required(), .keys({
afterReadPromise: joi.func().required(), CellComponent: componentSchema.required(),
validate: joi.func().required(), FieldComponent: componentSchema.required(),
}), afterReadPromise: joi.func().optional(),
populationPromise: joi.func().optional(),
validate: joi.func().required(),
})
.unknown(),
type: joi.string().valid('richText').required(), type: joi.string().valid('richText').required(),
}) })

View File

@@ -135,8 +135,9 @@ export const promise = async ({
case 'richText': { case 'richText': {
const editor: RichTextAdapter = field?.editor const editor: RichTextAdapter = field?.editor
if (editor?.afterReadPromise) { // This is run here AND in the GraphQL Resolver
const afterReadPromise = editor.afterReadPromise({ if (editor?.populationPromise) {
const populationPromise = editor.populationPromise({
currentDepth, currentDepth,
depth, depth,
field, field,
@@ -146,6 +147,19 @@ export const promise = async ({
siblingDoc, siblingDoc,
}) })
if (populationPromise) {
populationPromises.push(populationPromise)
}
}
// This is only run here, independent of depth
if (editor?.afterReadPromise) {
const afterReadPromise = editor?.afterReadPromise({
field,
incomingEditorState: siblingDoc[field.name] as object,
siblingDoc,
})
if (afterReadPromise) { if (afterReadPromise) {
populationPromises.push(afterReadPromise) populationPromises.push(afterReadPromise)
} }

View File

@@ -429,8 +429,13 @@ function buildObjectType({
if (typeof args.depth !== 'undefined') depth = args.depth if (typeof args.depth !== 'undefined') depth = args.depth
const editor: RichTextAdapter = field?.editor const editor: RichTextAdapter = field?.editor
if (editor?.afterReadPromise) { // RichText fields have their own depth argument in GraphQL.
await editor?.afterReadPromise({ // This is why the populationPromise (which populates richtext fields like uploads and relationships)
// is run here again, with the provided depth.
// In the graphql find.ts resolver, the depth is then hard-coded to 0.
// Effectively, this means that the populationPromise for GraphQL is only run here, and not in the find.ts resolver / normal population promise.
if (editor?.populationPromise) {
await editor?.populationPromise({
depth, depth,
field, field,
req: context.req, req: context.req,

View File

@@ -1,17 +1,21 @@
import { $createQuoteNode, QuoteNode } from '@lexical/rich-text' import type { SerializedHeadingNode, SerializedQuoteNode } from '@lexical/rich-text'
import { $createQuoteNode, HeadingNode, QuoteNode } from '@lexical/rich-text'
import { $setBlocksType } from '@lexical/selection' import { $setBlocksType } from '@lexical/selection'
import { $getSelection, $isRangeSelection } from 'lexical' import { $getSelection, $isRangeSelection } from 'lexical'
import type { HTMLConverter } from '../converters/html/converter/types'
import type { FeatureProvider } from '../types' import type { FeatureProvider } from '../types'
import { SlashMenuOption } from '../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/LexicalMenu' import { SlashMenuOption } from '../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/LexicalMenu'
import { BlockquoteIcon } from '../../lexical/ui/icons/Blockquote' import { BlockquoteIcon } from '../../lexical/ui/icons/Blockquote'
import { TextDropdownSectionWithEntries } from '../common/floatingSelectToolbarTextDropdownSection' import { TextDropdownSectionWithEntries } from '../common/floatingSelectToolbarTextDropdownSection'
import { convertLexicalNodesToHTML } from '../converters/html/converter'
import { MarkdownTransformer } from './markdownTransformer' import { MarkdownTransformer } from './markdownTransformer'
export const BlockQuoteFeature = (): FeatureProvider => { export const BlockQuoteFeature = (): FeatureProvider => {
return { return {
feature: ({ resolvedFeatures, unsanitizedEditorConfig }) => { feature: () => {
return { return {
floatingSelectToolbar: { floatingSelectToolbar: {
sections: [ sections: [
@@ -38,6 +42,23 @@ export const BlockQuoteFeature = (): FeatureProvider => {
markdownTransformers: [MarkdownTransformer], markdownTransformers: [MarkdownTransformer],
nodes: [ nodes: [
{ {
converters: {
html: {
converter: async ({ converters, node, parent }) => {
const childrenText = await convertLexicalNodesToHTML({
converters,
lexicalNodes: node.children,
parent: {
...node,
parent,
},
})
return `<blockquote>${childrenText}</blockquote>`
},
nodeTypes: [QuoteNode.getType()],
} as HTMLConverter<SerializedQuoteNode>,
},
node: QuoteNode, node: QuoteNode,
type: QuoteNode.getType(), type: QuoteNode.getType(),
}, },

View File

@@ -7,10 +7,10 @@ import type { FeatureProvider } from '../types'
import { SlashMenuOption } from '../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/LexicalMenu' import { SlashMenuOption } from '../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/LexicalMenu'
import { BlockIcon } from '../../lexical/ui/icons/Block' import { BlockIcon } from '../../lexical/ui/icons/Block'
import { blockAfterReadPromiseHOC } from './afterReadPromise'
import './index.scss' import './index.scss'
import { BlockNode } from './nodes/BlocksNode' import { BlockNode } from './nodes/BlocksNode'
import { BlocksPlugin, INSERT_BLOCK_COMMAND } from './plugin' import { BlocksPlugin, INSERT_BLOCK_COMMAND } from './plugin'
import { blockPopulationPromiseHOC } from './populationPromise'
import { blockValidationHOC } from './validate' import { blockValidationHOC } from './validate'
export type BlocksFeatureProps = { export type BlocksFeatureProps = {
@@ -38,12 +38,12 @@ export const BlocksFeature = (props?: BlocksFeatureProps): FeatureProvider => {
}) })
} }
return { return {
feature: ({ resolvedFeatures, unsanitizedEditorConfig }) => { feature: () => {
return { return {
nodes: [ nodes: [
{ {
afterReadPromises: [blockAfterReadPromiseHOC(props)],
node: BlockNode, node: BlockNode,
populationPromises: [blockPopulationPromiseHOC(props)],
type: BlockNode.getType(), type: BlockNode.getType(),
validations: [blockValidationHOC(props)], validations: [blockValidationHOC(props)],
}, },

View File

@@ -3,24 +3,22 @@ import type { Block } from 'payload/types'
import { sanitizeFields } from 'payload/config' import { sanitizeFields } from 'payload/config'
import type { BlocksFeatureProps } from '.' import type { BlocksFeatureProps } from '.'
import type { AfterReadPromise } from '../types' import type { PopulationPromise } from '../types'
import type { SerializedBlockNode } from './nodes/BlocksNode' import type { SerializedBlockNode } from './nodes/BlocksNode'
import { recurseNestedFields } from '../../../populate/recurseNestedFields' import { recurseNestedFields } from '../../../populate/recurseNestedFields'
export const blockAfterReadPromiseHOC = ( export const blockPopulationPromiseHOC = (
props: BlocksFeatureProps, props: BlocksFeatureProps,
): AfterReadPromise<SerializedBlockNode> => { ): PopulationPromise<SerializedBlockNode> => {
const blockAfterReadPromise: AfterReadPromise<SerializedBlockNode> = ({ const blockPopulationPromise: PopulationPromise<SerializedBlockNode> = ({
afterReadPromises,
currentDepth, currentDepth,
depth, depth,
field,
node, node,
overrideAccess, overrideAccess,
populationPromises,
req, req,
showHiddenFields, showHiddenFields,
siblingDoc,
}) => { }) => {
const blocks: Block[] = props.blocks const blocks: Block[] = props.blocks
const blockFieldData = node.fields.data const blockFieldData = node.fields.data
@@ -45,12 +43,12 @@ export const blockAfterReadPromiseHOC = (
} }
recurseNestedFields({ recurseNestedFields({
afterReadPromises,
currentDepth, currentDepth,
data: blockFieldData, data: blockFieldData,
depth, depth,
fields: block.fields, fields: block.fields,
overrideAccess, overrideAccess,
populationPromises,
promises, promises,
req, req,
showHiddenFields, showHiddenFields,
@@ -61,5 +59,5 @@ export const blockAfterReadPromiseHOC = (
return promises return promises
} }
return blockAfterReadPromise return blockPopulationPromise
} }

View File

@@ -1,10 +1,11 @@
import type { HeadingTagType } from '@lexical/rich-text' import type { HeadingTagType, SerializedHeadingNode } from '@lexical/rich-text'
import type { LexicalEditor } from 'lexical' import type React from 'react'
import { $createHeadingNode, HeadingNode } from '@lexical/rich-text' import { $createHeadingNode, HeadingNode } from '@lexical/rich-text'
import { $setBlocksType } from '@lexical/selection' import { $setBlocksType } from '@lexical/selection'
import { $getSelection, $isRangeSelection, DEPRECATED_$isGridSelection } from 'lexical' import { $getSelection, $isRangeSelection, DEPRECATED_$isGridSelection } from 'lexical'
import type { HTMLConverter } from '../converters/html/converter/types'
import type { FeatureProvider } from '../types' import type { FeatureProvider } from '../types'
import { SlashMenuOption } from '../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/LexicalMenu' import { SlashMenuOption } from '../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/LexicalMenu'
@@ -15,9 +16,10 @@ import { H4Icon } from '../../lexical/ui/icons/H4'
import { H5Icon } from '../../lexical/ui/icons/H5' import { H5Icon } from '../../lexical/ui/icons/H5'
import { H6Icon } from '../../lexical/ui/icons/H6' import { H6Icon } from '../../lexical/ui/icons/H6'
import { TextDropdownSectionWithEntries } from '../common/floatingSelectToolbarTextDropdownSection' import { TextDropdownSectionWithEntries } from '../common/floatingSelectToolbarTextDropdownSection'
import { convertLexicalNodesToHTML } from '../converters/html/converter'
import { MarkdownTransformer } from './markdownTransformer' import { MarkdownTransformer } from './markdownTransformer'
const setHeading = (editor: LexicalEditor, headingSize: HeadingTagType) => { const setHeading = (headingSize: HeadingTagType) => {
const selection = $getSelection() const selection = $getSelection()
if ($isRangeSelection(selection) || DEPRECATED_$isGridSelection(selection)) { if ($isRangeSelection(selection) || DEPRECATED_$isGridSelection(selection)) {
$setBlocksType(selection, () => $createHeadingNode(headingSize)) $setBlocksType(selection, () => $createHeadingNode(headingSize))
@@ -41,7 +43,7 @@ export const HeadingFeature = (props: Props): FeatureProvider => {
const { enabledHeadingSizes = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] } = props const { enabledHeadingSizes = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] } = props
return { return {
feature: ({ resolvedFeatures, unsanitizedEditorConfig }) => { feature: () => {
return { return {
floatingSelectToolbar: { floatingSelectToolbar: {
sections: [ sections: [
@@ -49,12 +51,12 @@ export const HeadingFeature = (props: Props): FeatureProvider => {
TextDropdownSectionWithEntries([ TextDropdownSectionWithEntries([
{ {
ChildComponent: HeadingToIconMap[headingSize], ChildComponent: HeadingToIconMap[headingSize],
isActive: ({ editor, selection }) => false, isActive: () => false,
key: headingSize, key: headingSize,
label: `Heading ${headingSize.charAt(1)}`, label: `Heading ${headingSize.charAt(1)}`,
onClick: ({ editor }) => { onClick: ({ editor }) => {
editor.update(() => { editor.update(() => {
setHeading(editor, headingSize) setHeading(headingSize)
}) })
}, },
order: i + 2, order: i + 2,
@@ -64,7 +66,29 @@ export const HeadingFeature = (props: Props): FeatureProvider => {
], ],
}, },
markdownTransformers: [MarkdownTransformer(enabledHeadingSizes)], markdownTransformers: [MarkdownTransformer(enabledHeadingSizes)],
nodes: [{ node: HeadingNode, type: HeadingNode.getType() }], nodes: [
{
converters: {
html: {
converter: async ({ converters, node, parent }) => {
const childrenText = await convertLexicalNodesToHTML({
converters,
lexicalNodes: node.children,
parent: {
...node,
parent,
},
})
return '<' + node?.tag + '>' + childrenText + '</' + node?.tag + '>'
},
nodeTypes: [HeadingNode.getType()],
} as HTMLConverter<SerializedHeadingNode>,
},
node: HeadingNode,
type: HeadingNode.getType(),
},
],
props, props,
slashMenu: { slashMenu: {
options: [ options: [
@@ -74,8 +98,8 @@ export const HeadingFeature = (props: Props): FeatureProvider => {
new SlashMenuOption(`Heading ${headingSize.charAt(1)}`, { new SlashMenuOption(`Heading ${headingSize.charAt(1)}`, {
Icon: HeadingToIconMap[headingSize], Icon: HeadingToIconMap[headingSize],
keywords: ['heading', headingSize], keywords: ['heading', headingSize],
onSelect: ({ editor }) => { onSelect: () => {
setHeading(editor, headingSize) setHeading(headingSize)
}, },
}), }),
], ],

View File

@@ -7,13 +7,15 @@ import { $findMatchingParent } from '@lexical/utils'
import { $getSelection, $isRangeSelection } from 'lexical' import { $getSelection, $isRangeSelection } from 'lexical'
import { withMergedProps } from 'payload/utilities' import { withMergedProps } from 'payload/utilities'
import type { HTMLConverter } from '../converters/html/converter/types'
import type { FeatureProvider } from '../types' import type { FeatureProvider } from '../types'
import type { LinkFields } from './nodes/LinkNode' import type { SerializedAutoLinkNode } from './nodes/AutoLinkNode'
import type { LinkFields, SerializedLinkNode } from './nodes/LinkNode'
import { LinkIcon } from '../../lexical/ui/icons/Link' import { LinkIcon } from '../../lexical/ui/icons/Link'
import { getSelectedNode } from '../../lexical/utils/getSelectedNode' import { getSelectedNode } from '../../lexical/utils/getSelectedNode'
import { FeaturesSectionWithEntries } from '../common/floatingSelectToolbarFeaturesButtonsSection' import { FeaturesSectionWithEntries } from '../common/floatingSelectToolbarFeaturesButtonsSection'
import { linkAfterReadPromiseHOC } from './afterReadPromise' import { convertLexicalNodesToHTML } from '../converters/html/converter'
import './index.scss' import './index.scss'
import { AutoLinkNode } from './nodes/AutoLinkNode' import { AutoLinkNode } from './nodes/AutoLinkNode'
import { $isLinkNode, LinkNode, TOGGLE_LINK_COMMAND } from './nodes/LinkNode' import { $isLinkNode, LinkNode, TOGGLE_LINK_COMMAND } from './nodes/LinkNode'
@@ -21,6 +23,7 @@ import { AutoLinkPlugin } from './plugins/autoLink'
import { FloatingLinkEditorPlugin } from './plugins/floatingLinkEditor' import { FloatingLinkEditorPlugin } from './plugins/floatingLinkEditor'
import { TOGGLE_LINK_WITH_MODAL_COMMAND } from './plugins/floatingLinkEditor/LinkEditor' import { TOGGLE_LINK_WITH_MODAL_COMMAND } from './plugins/floatingLinkEditor/LinkEditor'
import { LinkPlugin } from './plugins/link' import { LinkPlugin } from './plugins/link'
import { linkPopulationPromiseHOC } from './populationPromise'
export type LinkFeatureProps = { export type LinkFeatureProps = {
fields?: fields?:
@@ -29,7 +32,7 @@ export type LinkFeatureProps = {
} }
export const LinkFeature = (props: LinkFeatureProps): FeatureProvider => { export const LinkFeature = (props: LinkFeatureProps): FeatureProvider => {
return { return {
feature: ({ resolvedFeatures, unsanitizedEditorConfig }) => { feature: () => {
return { return {
floatingSelectToolbar: { floatingSelectToolbar: {
sections: [ sections: [
@@ -74,13 +77,58 @@ export const LinkFeature = (props: LinkFeatureProps): FeatureProvider => {
}, },
nodes: [ nodes: [
{ {
afterReadPromises: [linkAfterReadPromiseHOC(props)], converters: {
html: {
converter: async ({ converters, node, parent }) => {
const childrenText = await convertLexicalNodesToHTML({
converters,
lexicalNodes: node.children,
parent: {
...node,
parent,
},
})
const rel: string = node.fields.newTab ? ' rel="noopener noreferrer"' : ''
const href: string =
node.fields.linkType === 'custom' ? node.fields.url : node.fields.doc?.value?.id
return `<a href="${href}"${rel}>${childrenText}</a>`
},
nodeTypes: [LinkNode.getType()],
} as HTMLConverter<SerializedLinkNode>,
},
node: LinkNode, node: LinkNode,
populationPromises: [linkPopulationPromiseHOC(props)],
type: LinkNode.getType(), type: LinkNode.getType(),
// TODO: Add validation similar to upload for internal links and fields // TODO: Add validation similar to upload for internal links and fields
}, },
{ {
converters: {
html: {
converter: async ({ converters, node, parent }) => {
const childrenText = await convertLexicalNodesToHTML({
converters,
lexicalNodes: node.children,
parent: {
...node,
parent,
},
})
const rel: string = node.fields.newTab ? ' rel="noopener noreferrer"' : ''
const href: string =
node.fields.linkType === 'custom' ? node.fields.url : node.fields.doc?.value?.id
return `<a href="${href}"${rel}>${childrenText}</a>`
},
nodeTypes: [AutoLinkNode.getType()],
} as HTMLConverter<SerializedAutoLinkNode>,
},
node: AutoLinkNode, node: AutoLinkNode,
populationPromises: [linkPopulationPromiseHOC(props)],
type: AutoLinkNode.getType(), type: AutoLinkNode.getType(),
}, },
], ],

View File

@@ -1,23 +1,22 @@
import type { LinkFeatureProps } from '.' import type { LinkFeatureProps } from '.'
import type { AfterReadPromise } from '../types' import type { PopulationPromise } from '../types'
import type { SerializedLinkNode } from './nodes/LinkNode' import type { SerializedLinkNode } from './nodes/LinkNode'
import { populate } from '../../../populate/populate' import { populate } from '../../../populate/populate'
import { recurseNestedFields } from '../../../populate/recurseNestedFields' import { recurseNestedFields } from '../../../populate/recurseNestedFields'
export const linkAfterReadPromiseHOC = ( export const linkPopulationPromiseHOC = (
props: LinkFeatureProps, props: LinkFeatureProps,
): AfterReadPromise<SerializedLinkNode> => { ): PopulationPromise<SerializedLinkNode> => {
const linkAfterReadPromise: AfterReadPromise<SerializedLinkNode> = ({ const linkPopulationPromise: PopulationPromise<SerializedLinkNode> = ({
afterReadPromises,
currentDepth, currentDepth,
depth, depth,
field, field,
node, node,
overrideAccess, overrideAccess,
populationPromises,
req, req,
showHiddenFields, showHiddenFields,
siblingDoc,
}) => { }) => {
const promises: Promise<void>[] = [] const promises: Promise<void>[] = []
@@ -43,12 +42,12 @@ export const linkAfterReadPromiseHOC = (
} }
if (Array.isArray(props.fields)) { if (Array.isArray(props.fields)) {
recurseNestedFields({ recurseNestedFields({
afterReadPromises,
currentDepth, currentDepth,
data: node.fields || {}, data: node.fields || {},
depth, depth,
fields: props.fields, fields: props.fields,
overrideAccess, overrideAccess,
populationPromises,
promises, promises,
req, req,
showHiddenFields, showHiddenFields,
@@ -58,5 +57,5 @@ export const linkAfterReadPromiseHOC = (
return promises return promises
} }
return linkAfterReadPromise return linkPopulationPromise
} }

View File

@@ -2,20 +2,20 @@ import type { FeatureProvider } from '../types'
import { SlashMenuOption } from '../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/LexicalMenu' import { SlashMenuOption } from '../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/LexicalMenu'
import { RelationshipIcon } from '../../lexical/ui/icons/Relationship' import { RelationshipIcon } from '../../lexical/ui/icons/Relationship'
import { relationshipAfterReadPromise } from './afterReadPromise'
import { INSERT_RELATIONSHIP_WITH_DRAWER_COMMAND } from './drawer' import { INSERT_RELATIONSHIP_WITH_DRAWER_COMMAND } from './drawer'
import './index.scss' import './index.scss'
import { RelationshipNode } from './nodes/RelationshipNode' import { RelationshipNode } from './nodes/RelationshipNode'
import RelationshipPlugin from './plugins' import RelationshipPlugin from './plugins'
import { relationshipPopulationPromise } from './populationPromise'
export const RelationshipFeature = (): FeatureProvider => { export const RelationshipFeature = (): FeatureProvider => {
return { return {
feature: ({ resolvedFeatures, unsanitizedEditorConfig }) => { feature: () => {
return { return {
nodes: [ nodes: [
{ {
afterReadPromises: [relationshipAfterReadPromise],
node: RelationshipNode, node: RelationshipNode,
populationPromises: [relationshipPopulationPromise],
type: RelationshipNode.getType(), type: RelationshipNode.getType(),
// TODO: Add validation similar to upload // TODO: Add validation similar to upload
}, },

View File

@@ -1,9 +1,9 @@
import type { AfterReadPromise } from '../types' import type { PopulationPromise } from '../types'
import type { SerializedRelationshipNode } from './nodes/RelationshipNode' import type { SerializedRelationshipNode } from './nodes/RelationshipNode'
import { populate } from '../../../populate/populate' import { populate } from '../../../populate/populate'
export const relationshipAfterReadPromise: AfterReadPromise<SerializedRelationshipNode> = ({ export const relationshipPopulationPromise: PopulationPromise<SerializedRelationshipNode> = ({
currentDepth, currentDepth,
depth, depth,
field, field,

View File

@@ -1,14 +1,18 @@
import type { Field } from 'payload/types' import type { Field } from 'payload/types'
import payload from 'payload'
import type { HTMLConverter } from '../converters/html/converter/types'
import type { FeatureProvider } from '../types' import type { FeatureProvider } from '../types'
import type { SerializedUploadNode } from './nodes/UploadNode'
import { SlashMenuOption } from '../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/LexicalMenu' import { SlashMenuOption } from '../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/LexicalMenu'
import { UploadIcon } from '../../lexical/ui/icons/Upload' import { UploadIcon } from '../../lexical/ui/icons/Upload'
import { uploadAfterReadPromiseHOC } from './afterReadPromise'
import { INSERT_UPLOAD_WITH_DRAWER_COMMAND } from './drawer' import { INSERT_UPLOAD_WITH_DRAWER_COMMAND } from './drawer'
import './index.scss' import './index.scss'
import { UploadNode } from './nodes/UploadNode' import { UploadNode } from './nodes/UploadNode'
import { UploadPlugin } from './plugin' import { UploadPlugin } from './plugin'
import { uploadPopulationPromiseHOC } from './populationPromise'
import { uploadValidation } from './validate' import { uploadValidation } from './validate'
export type UploadFeatureProps = { export type UploadFeatureProps = {
@@ -21,12 +25,30 @@ export type UploadFeatureProps = {
export const UploadFeature = (props?: UploadFeatureProps): FeatureProvider => { export const UploadFeature = (props?: UploadFeatureProps): FeatureProvider => {
return { return {
feature: ({ resolvedFeatures, unsanitizedEditorConfig }) => { feature: () => {
return { return {
nodes: [ nodes: [
{ {
afterReadPromises: [uploadAfterReadPromiseHOC(props)], converters: {
html: {
converter: async ({ node }) => {
const uploadDocument = await payload.findByID({
id: node.value.id,
collection: node.relationTo,
})
const url = (payload?.config?.serverURL || '') + uploadDocument?.url
if (!(uploadDocument?.mimeType as string)?.startsWith('image')) {
return `<a href="${url}" rel="noopener noreferrer">Upload node which is not an image</a>`
}
return `<img src="${url}" alt="${uploadDocument?.filename}" width="${uploadDocument?.width}" height="${uploadDocument?.height}"/>`
},
nodeTypes: [UploadNode.getType()],
} as HTMLConverter<SerializedUploadNode>,
},
node: UploadNode, node: UploadNode,
populationPromises: [uploadPopulationPromiseHOC(props)],
type: UploadNode.getType(), type: UploadNode.getType(),
validations: [uploadValidation()], validations: [uploadValidation()],
}, },

View File

@@ -1,23 +1,22 @@
import type { UploadFeatureProps } from '.' import type { UploadFeatureProps } from '.'
import type { AfterReadPromise } from '../types' import type { PopulationPromise } from '../types'
import type { SerializedUploadNode } from './nodes/UploadNode' import type { SerializedUploadNode } from './nodes/UploadNode'
import { populate } from '../../../populate/populate' import { populate } from '../../../populate/populate'
import { recurseNestedFields } from '../../../populate/recurseNestedFields' import { recurseNestedFields } from '../../../populate/recurseNestedFields'
export const uploadAfterReadPromiseHOC = ( export const uploadPopulationPromiseHOC = (
props?: UploadFeatureProps, props?: UploadFeatureProps,
): AfterReadPromise<SerializedUploadNode> => { ): PopulationPromise<SerializedUploadNode> => {
const uploadAfterReadPromise: AfterReadPromise<SerializedUploadNode> = ({ const uploadPopulationPromise: PopulationPromise<SerializedUploadNode> = ({
afterReadPromises,
currentDepth, currentDepth,
depth, depth,
field, field,
node, node,
overrideAccess, overrideAccess,
populationPromises,
req, req,
showHiddenFields, showHiddenFields,
siblingDoc,
}) => { }) => {
const promises: Promise<void>[] = [] const promises: Promise<void>[] = []
@@ -42,12 +41,12 @@ export const uploadAfterReadPromiseHOC = (
} }
if (Array.isArray(props?.collections?.[node?.relationTo]?.fields)) { if (Array.isArray(props?.collections?.[node?.relationTo]?.fields)) {
recurseNestedFields({ recurseNestedFields({
afterReadPromises,
currentDepth, currentDepth,
data: node.fields || {}, data: node.fields || {},
depth, depth,
fields: props?.collections?.[node?.relationTo]?.fields, fields: props?.collections?.[node?.relationTo]?.fields,
overrideAccess, overrideAccess,
populationPromises,
promises, promises,
req, req,
showHiddenFields, showHiddenFields,
@@ -59,5 +58,5 @@ export const uploadAfterReadPromiseHOC = (
return promises return promises
} }
return uploadAfterReadPromise return uploadPopulationPromise
} }

View File

@@ -0,0 +1,20 @@
import type { SerializedParagraphNode } from 'lexical'
import type { HTMLConverter } from '../types'
import { convertLexicalNodesToHTML } from '../index'
export const ParagraphHTMLConverter: HTMLConverter<SerializedParagraphNode> = {
async converter({ converters, node, parent }) {
const childrenText = await convertLexicalNodesToHTML({
converters,
lexicalNodes: node.children,
parent: {
...node,
parent,
},
})
return `<p>${childrenText}</p>`
},
nodeTypes: ['paragraph'],
}

View File

@@ -0,0 +1,36 @@
import type { SerializedTextNode } from 'lexical'
import type { HTMLConverter } from '../types'
import { NodeFormat } from '../../../../../lexical/utils/nodeFormat'
export const TextHTMLConverter: HTMLConverter<SerializedTextNode> = {
converter({ node }) {
let text = node.text
if (node.format & NodeFormat.IS_BOLD) {
text = `<strong>${text}</strong>`
}
if (node.format & NodeFormat.IS_ITALIC) {
text = `<em>${text}</em>`
}
if (node.format & NodeFormat.IS_STRIKETHROUGH) {
text = `<span style="text-decoration: line-through">${text}</span>`
}
if (node.format & NodeFormat.IS_UNDERLINE) {
text = `<span style="text-decoration: underline">${text}</span>`
}
if (node.format & NodeFormat.IS_CODE) {
text = `<code>${text}</code>`
}
if (node.format & NodeFormat.IS_SUBSCRIPT) {
text = `<sub>${text}</sub>`
}
if (node.format & NodeFormat.IS_SUPERSCRIPT) {
text = `<sup>${text}</sup>`
}
return text
},
nodeTypes: ['text'],
}

View File

@@ -0,0 +1,6 @@
import type { HTMLConverter } from './types'
import { ParagraphHTMLConverter } from './converters/paragraph'
import { TextHTMLConverter } from './converters/text'
export const defaultHTMLConverters: HTMLConverter[] = [ParagraphHTMLConverter, TextHTMLConverter]

View File

@@ -0,0 +1,54 @@
import type { SerializedEditorState, SerializedLexicalNode } from 'lexical'
import type { HTMLConverter, SerializedLexicalNodeWithParent } from './types'
export async function convertLexicalToHTML({
converters,
data,
}: {
converters: HTMLConverter[]
data: SerializedEditorState
}): Promise<string> {
if (data?.root?.children?.length) {
return await convertLexicalNodesToHTML({
converters,
lexicalNodes: data?.root?.children,
parent: data?.root,
})
}
return ''
}
export async function convertLexicalNodesToHTML({
converters,
lexicalNodes,
parent,
}: {
converters: HTMLConverter[]
lexicalNodes: SerializedLexicalNode[]
parent: SerializedLexicalNodeWithParent
}): Promise<string> {
const unknownConverter = converters.find((converter) => converter.nodeTypes.includes('unknown'))
const htmlArray = await Promise.all(
lexicalNodes.map(async (node, i) => {
const converterForNode = converters.find((converter) =>
converter.nodeTypes.includes(node.type),
)
if (!converterForNode) {
if (unknownConverter) {
return unknownConverter.converter({ childIndex: i, converters, node, parent })
}
return '<span>unknown node</span>'
}
return converterForNode.converter({
childIndex: i,
converters,
node,
parent,
})
}),
)
return htmlArray.join('') || ''
}

View File

@@ -0,0 +1,20 @@
import type { SerializedLexicalNode } from 'lexical'
export type HTMLConverter<T extends SerializedLexicalNode = SerializedLexicalNode> = {
converter: ({
childIndex,
converters,
node,
parent,
}: {
childIndex: number
converters: HTMLConverter[]
node: T
parent: SerializedLexicalNodeWithParent
}) => Promise<string> | string
nodeTypes: string[]
}
export type SerializedLexicalNodeWithParent = SerializedLexicalNode & {
parent?: SerializedLexicalNode
}

View File

@@ -0,0 +1,87 @@
import type { SerializedEditorState } from 'lexical'
import type { RichTextField, TextField } from 'payload/types'
import type { LexicalRichTextAdapter } from '../../../../../index'
import type { AdapterProps } from '../../../../../types'
import type { HTMLConverter } from '../converter/types'
import type { HTMLConverterFeatureProps } from '../index'
import { cloneDeep } from '../../../../../index'
import { convertLexicalToHTML } from '../converter'
import { defaultHTMLConverters } from '../converter/defaultConverters'
type Props = {
name: string
}
export const lexicalHTML: (lexicalFieldName: string, props: Props) => TextField = (
lexicalFieldName,
props,
) => {
const { name = 'lexicalHTML' } = props
return {
name: name,
admin: {
hidden: true,
},
hooks: {
afterRead: [
async ({ collection, context, data, originalDoc, siblingData }) => {
const lexicalField: RichTextField<SerializedEditorState, AdapterProps> =
collection.fields.find(
(field) => 'name' in field && field.name === lexicalFieldName,
) as RichTextField<SerializedEditorState, AdapterProps>
const lexicalFieldData: SerializedEditorState = siblingData[lexicalFieldName]
if (!lexicalFieldData) {
return ''
}
if (!lexicalField) {
throw new Error(
'You cannot use the lexicalHTML field because the lexical field was not found',
)
}
const config = (lexicalField?.editor as LexicalRichTextAdapter)?.editorConfig
if (!config) {
throw new Error(
'The linked lexical field does not have an editorConfig. This is needed for the lexicalHTML field.',
)
}
if (!config?.resolvedFeatureMap?.has('htmlConverter')) {
throw new Error(
'You cannot use the lexicalHTML field because the htmlConverter feature was not found',
)
}
const htmlConverterFeature = config.resolvedFeatureMap.get('htmlConverter')
const htmlConverterFeatureProps: HTMLConverterFeatureProps = htmlConverterFeature.props
const defaultConvertersWithConvertersFromFeatures = cloneDeep(defaultHTMLConverters)
for (const converter of config.features.converters.html) {
defaultConvertersWithConvertersFromFeatures.push(converter)
}
const finalConverters =
htmlConverterFeatureProps?.converters &&
typeof htmlConverterFeatureProps?.converters === 'function'
? htmlConverterFeatureProps.converters({
defaultConverters: defaultConvertersWithConvertersFromFeatures,
})
: (htmlConverterFeatureProps?.converters as HTMLConverter[]) ||
defaultConvertersWithConvertersFromFeatures
return await convertLexicalToHTML({
converters: finalConverters,
data: lexicalFieldData,
})
},
],
},
type: 'text',
}
}

View File

@@ -0,0 +1,31 @@
import type { FeatureProvider } from '../../types'
import type { HTMLConverter } from './converter/types'
export type HTMLConverterFeatureProps = {
converters?:
| (({ defaultConverters }: { defaultConverters: HTMLConverter[] }) => HTMLConverter[])
| HTMLConverter[]
}
/**
* This feature only manages the converters. They are read and actually run / executed by the
* Lexical field.
*/
export const HTMLConverterFeature = (props?: HTMLConverterFeatureProps): FeatureProvider => {
if (!props) {
props = {}
}
/*const defaultConvertersWithConvertersFromFeatures = defaultConverters
defaultConvertersWithConver tersFromFeatures.set(props?
*/
return {
feature: () => {
return {
props,
}
},
key: 'htmlConverter',
}
}

View File

@@ -5,6 +5,7 @@ import type { FeatureProvider } from '../../types'
import { SlashMenuOption } from '../../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/LexicalMenu' import { SlashMenuOption } from '../../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/LexicalMenu'
import { ChecklistIcon } from '../../../lexical/ui/icons/Checklist' import { ChecklistIcon } from '../../../lexical/ui/icons/Checklist'
import { ListHTMLConverter, ListItemHTMLConverter } from '../htmlConverter'
import { CHECK_LIST } from './markdownTransformers' import { CHECK_LIST } from './markdownTransformers'
// 345 // 345
@@ -19,10 +20,16 @@ export const CheckListFeature = (): FeatureProvider => {
? [] ? []
: [ : [
{ {
converters: {
html: ListHTMLConverter,
},
node: ListNode, node: ListNode,
type: ListNode.getType(), type: ListNode.getType(),
}, },
{ {
converters: {
html: ListItemHTMLConverter,
},
node: ListItemNode, node: ListItemNode,
type: ListItemNode.getType(), type: ListItemNode.getType(),
}, },

View File

@@ -5,6 +5,7 @@ import type { FeatureProvider } from '../../types'
import { SlashMenuOption } from '../../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/LexicalMenu' import { SlashMenuOption } from '../../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/LexicalMenu'
import { OrderedListIcon } from '../../../lexical/ui/icons/OrderedList' import { OrderedListIcon } from '../../../lexical/ui/icons/OrderedList'
import { ListHTMLConverter, ListItemHTMLConverter } from '../htmlConverter'
import { ORDERED_LIST } from './markdownTransformer' import { ORDERED_LIST } from './markdownTransformer'
export const OrderedListFeature = (): FeatureProvider => { export const OrderedListFeature = (): FeatureProvider => {
@@ -16,10 +17,19 @@ export const OrderedListFeature = (): FeatureProvider => {
? [] ? []
: [ : [
{ {
converters: {
html: ListHTMLConverter,
},
node: ListNode, node: ListNode,
type: ListNode.getType(), type: ListNode.getType(),
}, },
{ node: ListItemNode, type: ListItemNode.getType() }, {
converters: {
html: ListItemHTMLConverter,
},
node: ListItemNode,
type: ListItemNode.getType(),
},
], ],
plugins: featureProviderMap.has('unorderedList') plugins: featureProviderMap.has('unorderedList')
? [] ? []

View File

@@ -5,6 +5,7 @@ import type { FeatureProvider } from '../../types'
import { SlashMenuOption } from '../../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/LexicalMenu' import { SlashMenuOption } from '../../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/LexicalMenu'
import { UnorderedListIcon } from '../../../lexical/ui/icons/UnorderedList' import { UnorderedListIcon } from '../../../lexical/ui/icons/UnorderedList'
import { ListHTMLConverter, ListItemHTMLConverter } from '../htmlConverter'
import { UNORDERED_LIST } from './markdownTransformer' import { UNORDERED_LIST } from './markdownTransformer'
export const UnoderedListFeature = (): FeatureProvider => { export const UnoderedListFeature = (): FeatureProvider => {
@@ -14,10 +15,16 @@ export const UnoderedListFeature = (): FeatureProvider => {
markdownTransformers: [UNORDERED_LIST], markdownTransformers: [UNORDERED_LIST],
nodes: [ nodes: [
{ {
converters: {
html: ListHTMLConverter,
},
node: ListNode, node: ListNode,
type: ListNode.getType(), type: ListNode.getType(),
}, },
{ {
converters: {
html: ListItemHTMLConverter,
},
node: ListItemNode, node: ListItemNode,
type: ListItemNode.getType(), type: ListItemNode.getType(),
}, },

View File

@@ -0,0 +1,53 @@
import type { SerializedListItemNode, SerializedListNode } from '@lexical/list'
import { ListItemNode, ListNode } from '@lexical/list'
import type { HTMLConverter } from '../converters/html/converter/types'
import { convertLexicalNodesToHTML } from '../converters/html/converter'
export const ListHTMLConverter: HTMLConverter<SerializedListNode> = {
converter: async ({ converters, node, parent }) => {
const childrenText = await convertLexicalNodesToHTML({
converters,
lexicalNodes: node.children,
parent: {
...node,
parent,
},
})
return `<${node?.tag} class="${node?.listType}">${childrenText}</${node?.tag}>`
},
nodeTypes: [ListNode.getType()],
}
export const ListItemHTMLConverter: HTMLConverter<SerializedListItemNode> = {
converter: async ({ converters, node, parent }) => {
const childrenText = await convertLexicalNodesToHTML({
converters,
lexicalNodes: node.children,
parent: {
...node,
parent,
},
})
if ('listType' in parent && parent?.listType === 'check') {
return `<li aria-checked=${node.checked ? 'true' : 'false'} class="${
'list-item-checkbox' + node.checked
? 'list-item-checkbox-checked'
: 'list-item-checkbox-unchecked'
}"
role="checkbox"
tabIndex=${-1}
value=${node?.value}
>
{serializedChildren}
</li>`
} else {
return `<li value=${node?.value}>${childrenText}</li>`
}
},
nodeTypes: [ListItemNode.getType()],
}

View File

@@ -4,7 +4,7 @@ import type { SlateNodeConverter } from '../types'
import { convertSlateNodesToLexical } from '..' import { convertSlateNodesToLexical } from '..'
export const HeadingConverter: SlateNodeConverter = { export const SlateHeadingConverter: SlateNodeConverter = {
converter({ converters, slateNode }) { converter({ converters, slateNode }) {
return { return {
children: convertSlateNodesToLexical({ children: convertSlateNodesToLexical({

View File

@@ -4,7 +4,7 @@ import type { SlateNodeConverter } from '../types'
import { convertSlateNodesToLexical } from '..' import { convertSlateNodesToLexical } from '..'
export const IndentConverter: SlateNodeConverter = { export const SlateIndentConverter: SlateNodeConverter = {
converter({ converters, slateNode }) { converter({ converters, slateNode }) {
console.log('slateToLexical > IndentConverter > converter', JSON.stringify(slateNode, null, 2)) console.log('slateToLexical > IndentConverter > converter', JSON.stringify(slateNode, null, 2))
const convertChildren = (node: any, indentLevel: number = 0): SerializedLexicalNode => { const convertChildren = (node: any, indentLevel: number = 0): SerializedLexicalNode => {

View File

@@ -3,7 +3,7 @@ import type { SlateNodeConverter } from '../types'
import { convertSlateNodesToLexical } from '..' import { convertSlateNodesToLexical } from '..'
export const LinkConverter: SlateNodeConverter = { export const SlateLinkConverter: SlateNodeConverter = {
converter({ converters, slateNode }) { converter({ converters, slateNode }) {
return { return {
children: convertSlateNodesToLexical({ children: convertSlateNodesToLexical({

View File

@@ -4,7 +4,7 @@ import type { SlateNodeConverter } from '../types'
import { convertSlateNodesToLexical } from '..' import { convertSlateNodesToLexical } from '..'
export const ListItemConverter: SlateNodeConverter = { export const SlateListItemConverter: SlateNodeConverter = {
converter({ childIndex, converters, slateNode }) { converter({ childIndex, converters, slateNode }) {
return { return {
checked: undefined, checked: undefined,

View File

@@ -4,7 +4,7 @@ import type { SlateNodeConverter } from '../types'
import { convertSlateNodesToLexical } from '..' import { convertSlateNodesToLexical } from '..'
export const OrderedListConverter: SlateNodeConverter = { export const SlateOrderedListConverter: SlateNodeConverter = {
converter({ converters, slateNode }) { converter({ converters, slateNode }) {
return { return {
children: convertSlateNodesToLexical({ children: convertSlateNodesToLexical({

View File

@@ -1,7 +1,7 @@
import type { SerializedRelationshipNode } from '../../../../../..' import type { SerializedRelationshipNode } from '../../../../../..'
import type { SlateNodeConverter } from '../types' import type { SlateNodeConverter } from '../types'
export const RelationshipConverter: SlateNodeConverter = { export const SlateRelationshipConverter: SlateNodeConverter = {
converter({ slateNode }) { converter({ slateNode }) {
return { return {
format: '', format: '',

View File

@@ -3,7 +3,7 @@ import type { SlateNodeConverter } from '../types'
import { convertSlateNodesToLexical } from '..' import { convertSlateNodesToLexical } from '..'
export const UnknownConverter: SlateNodeConverter = { export const SlateUnknownConverter: SlateNodeConverter = {
converter({ converters, slateNode }) { converter({ converters, slateNode }) {
return { return {
children: convertSlateNodesToLexical({ children: convertSlateNodesToLexical({

View File

@@ -4,7 +4,7 @@ import type { SlateNodeConverter } from '../types'
import { convertSlateNodesToLexical } from '..' import { convertSlateNodesToLexical } from '..'
export const UnorderedListConverter: SlateNodeConverter = { export const SlateUnorderedListConverter: SlateNodeConverter = {
converter({ converters, slateNode }) { converter({ converters, slateNode }) {
return { return {
children: convertSlateNodesToLexical({ children: convertSlateNodesToLexical({

View File

@@ -1,7 +1,7 @@
import type { SerializedUploadNode } from '../../../../../..' import type { SerializedUploadNode } from '../../../../../..'
import type { SlateNodeConverter } from '../types' import type { SlateNodeConverter } from '../types'
export const UploadConverter: SlateNodeConverter = { export const SlateUploadConverter: SlateNodeConverter = {
converter({ slateNode }) { converter({ slateNode }) {
return { return {
fields: { fields: {

View File

@@ -1,23 +1,23 @@
import type { SlateNodeConverter } from './types' import type { SlateNodeConverter } from './types'
import { HeadingConverter } from './converters/heading' import { SlateHeadingConverter } from './converters/heading'
import { IndentConverter } from './converters/indent' import { SlateIndentConverter } from './converters/indent'
import { LinkConverter } from './converters/link' import { SlateLinkConverter } from './converters/link'
import { ListItemConverter } from './converters/listItem' import { SlateListItemConverter } from './converters/listItem'
import { OrderedListConverter } from './converters/orderedList' import { SlateOrderedListConverter } from './converters/orderedList'
import { RelationshipConverter } from './converters/relationship' import { SlateRelationshipConverter } from './converters/relationship'
import { UnknownConverter } from './converters/unknown' import { SlateUnknownConverter } from './converters/unknown'
import { UnorderedListConverter } from './converters/unorderedList' import { SlateUnorderedListConverter } from './converters/unorderedList'
import { UploadConverter } from './converters/upload' import { SlateUploadConverter } from './converters/upload'
export const defaultConverters: SlateNodeConverter[] = [ export const defaultSlateConverters: SlateNodeConverter[] = [
UnknownConverter, SlateUnknownConverter,
UploadConverter, SlateUploadConverter,
UnorderedListConverter, SlateUnorderedListConverter,
OrderedListConverter, SlateOrderedListConverter,
RelationshipConverter, SlateRelationshipConverter,
ListItemConverter, SlateListItemConverter,
LinkConverter, SlateLinkConverter,
HeadingConverter, SlateHeadingConverter,
IndentConverter, SlateIndentConverter,
] ]

View File

@@ -2,7 +2,7 @@ import type { FeatureProvider } from '../../types'
import type { SlateNodeConverter } from './converter/types' import type { SlateNodeConverter } from './converter/types'
import { convertSlateToLexical } from './converter' import { convertSlateToLexical } from './converter'
import { defaultConverters } from './converter/defaultConverters' import { defaultSlateConverters } from './converter/defaultConverters'
import { UnknownConvertedNode } from './nodes/unknownConvertedNode' import { UnknownConvertedNode } from './nodes/unknownConvertedNode'
type Props = { type Props = {
@@ -18,11 +18,11 @@ export const SlateToLexicalFeature = (props?: Props): FeatureProvider => {
props.converters = props.converters =
props?.converters && typeof props?.converters === 'function' props?.converters && typeof props?.converters === 'function'
? props.converters({ defaultConverters: defaultConverters }) ? props.converters({ defaultConverters: defaultSlateConverters })
: (props?.converters as SlateNodeConverter[]) || defaultConverters : (props?.converters as SlateNodeConverter[]) || defaultSlateConverters
return { return {
feature: ({ resolvedFeatures, unsanitizedEditorConfig }) => { feature: () => {
return { return {
hooks: { hooks: {
load({ incomingEditorState }) { load({ incomingEditorState }) {

View File

@@ -6,27 +6,28 @@ import type { PayloadRequest, RichTextField, ValidateOptions } from 'payload/typ
import type React from 'react' import type React from 'react'
import type { AdapterProps } from '../../types' import type { AdapterProps } from '../../types'
import type { EditorConfig } from '..//lexical/config/types' import type { EditorConfig } from '../lexical/config/types'
import type { FloatingToolbarSection } from '../lexical/plugins/FloatingSelectToolbar/types' import type { FloatingToolbarSection } from '../lexical/plugins/FloatingSelectToolbar/types'
import type { SlashMenuGroup } from '../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/LexicalMenu' import type { SlashMenuGroup } from '../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/LexicalMenu'
import type { HTMLConverter } from './converters/html/converter/types'
export type AfterReadPromise<T extends SerializedLexicalNode = SerializedLexicalNode> = ({ export type PopulationPromise<T extends SerializedLexicalNode = SerializedLexicalNode> = ({
afterReadPromises,
currentDepth, currentDepth,
depth, depth,
field, field,
node, node,
overrideAccess, overrideAccess,
populationPromises,
req, req,
showHiddenFields, showHiddenFields,
siblingDoc, siblingDoc,
}: { }: {
afterReadPromises: Map<string, Array<AfterReadPromise>>
currentDepth: number currentDepth: number
depth: number depth: number
field: RichTextField<SerializedEditorState, AdapterProps> field: RichTextField<SerializedEditorState, AdapterProps>
node: T node: T
overrideAccess: boolean overrideAccess: boolean
populationPromises: Map<string, Array<PopulationPromise>>
req: PayloadRequest req: PayloadRequest
showHiddenFields: boolean showHiddenFields: boolean
siblingDoc: Record<string, unknown> siblingDoc: Record<string, unknown>
@@ -52,6 +53,15 @@ export type Feature = {
sections: FloatingToolbarSection[] sections: FloatingToolbarSection[]
} }
hooks?: { hooks?: {
afterReadPromise?: ({
field,
incomingEditorState,
siblingDoc,
}: {
field: RichTextField<SerializedEditorState, AdapterProps>
incomingEditorState: SerializedEditorState
siblingDoc: Record<string, unknown>
}) => Promise<void> | null
load?: ({ load?: ({
incomingEditorState, incomingEditorState,
}: { }: {
@@ -65,8 +75,11 @@ export type Feature = {
} }
markdownTransformers?: Transformer[] markdownTransformers?: Transformer[]
nodes?: Array<{ nodes?: Array<{
afterReadPromises?: Array<AfterReadPromise> converters?: {
html?: HTMLConverter
}
node: Klass<LexicalNode> node: Klass<LexicalNode>
populationPromises?: Array<PopulationPromise>
type: string type: string
validations?: Array<NodeValidation> validations?: Array<NodeValidation>
}> }>
@@ -128,14 +141,27 @@ export type FeatureProviderMap = Map<string, FeatureProvider>
export type SanitizedFeatures = Required< export type SanitizedFeatures = Required<
Pick<ResolvedFeature, 'markdownTransformers' | 'nodes'> Pick<ResolvedFeature, 'markdownTransformers' | 'nodes'>
> & { > & {
/** The node types mapped to their afterReadPromises */ /** The node types mapped to their converters */
afterReadPromises: Map<string, Array<AfterReadPromise>> converters: {
html: HTMLConverter[]
}
/** The keys of all enabled features */ /** The keys of all enabled features */
enabledFeatures: string[] enabledFeatures: string[]
floatingSelectToolbar: { floatingSelectToolbar: {
sections: FloatingToolbarSection[] sections: FloatingToolbarSection[]
} }
hooks: { hooks: {
afterReadPromises: Array<
({
field,
incomingEditorState,
siblingDoc,
}: {
field: RichTextField<SerializedEditorState, AdapterProps>
incomingEditorState: SerializedEditorState
siblingDoc: Record<string, unknown>
}) => Promise<void> | null
>
load: Array< load: Array<
({ ({
incomingEditorState, incomingEditorState,
@@ -166,6 +192,8 @@ export type SanitizedFeatures = Required<
position: 'floatingAnchorElem' // Determines at which position the Component will be added. position: 'floatingAnchorElem' // Determines at which position the Component will be added.
} }
> >
/** The node types mapped to their populationPromises */
populationPromises: Map<string, Array<PopulationPromise>>
slashMenu: { slashMenu: {
dynamicOptions: Array< dynamicOptions: Array<
({ editor, queryString }: { editor: LexicalEditor; queryString: string }) => SlashMenuGroup[] ({ editor, queryString }: { editor: LexicalEditor; queryString: string }) => SlashMenuGroup[]

View File

@@ -5,18 +5,22 @@ import { loadFeatures } from './loader'
export const sanitizeFeatures = (features: ResolvedFeatureMap): SanitizedFeatures => { export const sanitizeFeatures = (features: ResolvedFeatureMap): SanitizedFeatures => {
const sanitized: SanitizedFeatures = { const sanitized: SanitizedFeatures = {
afterReadPromises: new Map(), converters: {
html: [],
},
enabledFeatures: [], enabledFeatures: [],
floatingSelectToolbar: { floatingSelectToolbar: {
sections: [], sections: [],
}, },
hooks: { hooks: {
afterReadPromises: [],
load: [], load: [],
save: [], save: [],
}, },
markdownTransformers: [], markdownTransformers: [],
nodes: [], nodes: [],
plugins: [], plugins: [],
populationPromises: new Map(),
slashMenu: { slashMenu: {
dynamicOptions: [], dynamicOptions: [],
groupsWithOptions: [], groupsWithOptions: [],
@@ -26,6 +30,11 @@ export const sanitizeFeatures = (features: ResolvedFeatureMap): SanitizedFeature
features.forEach((feature) => { features.forEach((feature) => {
if (feature.hooks) { if (feature.hooks) {
if (feature.hooks.afterReadPromise) {
sanitized.hooks.afterReadPromises = sanitized.hooks.afterReadPromises.concat(
feature.hooks.afterReadPromise,
)
}
if (feature.hooks?.load?.length) { if (feature.hooks?.load?.length) {
sanitized.hooks.load = sanitized.hooks.load.concat(feature.hooks.load) sanitized.hooks.load = sanitized.hooks.load.concat(feature.hooks.load)
} }
@@ -37,12 +46,15 @@ export const sanitizeFeatures = (features: ResolvedFeatureMap): SanitizedFeature
if (feature.nodes?.length) { if (feature.nodes?.length) {
sanitized.nodes = sanitized.nodes.concat(feature.nodes) sanitized.nodes = sanitized.nodes.concat(feature.nodes)
feature.nodes.forEach((node) => { feature.nodes.forEach((node) => {
if (node?.afterReadPromises?.length) { if (node?.populationPromises?.length) {
sanitized.afterReadPromises.set(node.type, node.afterReadPromises) sanitized.populationPromises.set(node.type, node.populationPromises)
} }
if (node?.validations?.length) { if (node?.validations?.length) {
sanitized.validations.set(node.type, node.validations) sanitized.validations.set(node.type, node.validations)
} }
if (node?.converters?.html) {
sanitized.converters.html.push(node.converters.html)
}
}) })
} }
if (feature.plugins?.length) { if (feature.plugins?.length) {

View File

@@ -27,9 +27,11 @@ export type LexicalEditorProps = {
lexical?: LexicalEditorConfig lexical?: LexicalEditorConfig
} }
export function lexicalEditor( export type LexicalRichTextAdapter = RichTextAdapter<SerializedEditorState, AdapterProps> & {
props?: LexicalEditorProps, editorConfig: SanitizedEditorConfig
): RichTextAdapter<SerializedEditorState, AdapterProps> { }
export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapter {
let finalSanitizedEditorConfig: SanitizedEditorConfig let finalSanitizedEditorConfig: SanitizedEditorConfig
if (!props || (!props.features && !props.lexical)) { if (!props || (!props.features && !props.lexical)) {
finalSanitizedEditorConfig = cloneDeep(defaultSanitizedEditorConfig) finalSanitizedEditorConfig = cloneDeep(defaultSanitizedEditorConfig)
@@ -59,7 +61,30 @@ export function lexicalEditor(
Component: RichTextField, Component: RichTextField,
toMergeIntoProps: { editorConfig: finalSanitizedEditorConfig }, toMergeIntoProps: { editorConfig: finalSanitizedEditorConfig },
}), }),
afterReadPromise({ afterReadPromise: ({ field, incomingEditorState, siblingDoc }) => {
return new Promise<void>((resolve, reject) => {
const promises: Promise<void>[] = []
if (finalSanitizedEditorConfig?.features?.hooks?.afterReadPromises?.length) {
for (const afterReadPromise of finalSanitizedEditorConfig.features.hooks
.afterReadPromises) {
promises.push(
afterReadPromise({
field,
incomingEditorState,
siblingDoc,
}),
)
}
}
Promise.all(promises)
.then(() => resolve())
.catch((error) => reject(error))
})
},
editorConfig: finalSanitizedEditorConfig,
populationPromise({
currentDepth, currentDepth,
depth, depth,
field, field,
@@ -68,14 +93,14 @@ export function lexicalEditor(
showHiddenFields, showHiddenFields,
siblingDoc, siblingDoc,
}) { }) {
// check if there are any features with nodes which have afterReadPromises for this field // check if there are any features with nodes which have populationPromises for this field
if (finalSanitizedEditorConfig?.features?.afterReadPromises?.size) { if (finalSanitizedEditorConfig?.features?.populationPromises?.size) {
return richTextRelationshipPromise({ return richTextRelationshipPromise({
afterReadPromises: finalSanitizedEditorConfig.features.afterReadPromises,
currentDepth, currentDepth,
depth, depth,
field, field,
overrideAccess, overrideAccess,
populationPromises: finalSanitizedEditorConfig.features.populationPromises,
req, req,
showHiddenFields, showHiddenFields,
siblingDoc, siblingDoc,
@@ -99,8 +124,8 @@ export {
BlockNode, BlockNode,
type SerializedBlockNode, type SerializedBlockNode,
} from './field/features/Blocks/nodes/BlocksNode' } from './field/features/Blocks/nodes/BlocksNode'
export { HeadingFeature } from './field/features/Heading' export { HeadingFeature } from './field/features/Heading'
export { LinkFeature } from './field/features/Link' export { LinkFeature } from './field/features/Link'
export type { LinkFeatureProps } from './field/features/Link' export type { LinkFeatureProps } from './field/features/Link'
export { export {
@@ -109,7 +134,6 @@ export {
AutoLinkNode, AutoLinkNode,
type SerializedAutoLinkNode, type SerializedAutoLinkNode,
} from './field/features/Link/nodes/AutoLinkNode' } from './field/features/Link/nodes/AutoLinkNode'
export { export {
$createLinkNode, $createLinkNode,
$isLinkNode, $isLinkNode,
@@ -118,6 +142,7 @@ export {
type SerializedLinkNode, type SerializedLinkNode,
TOGGLE_LINK_COMMAND, TOGGLE_LINK_COMMAND,
} from './field/features/Link/nodes/LinkNode' } from './field/features/Link/nodes/LinkNode'
export { ParagraphFeature } from './field/features/Paragraph' export { ParagraphFeature } from './field/features/Paragraph'
export { RelationshipFeature } from './field/features/Relationship' export { RelationshipFeature } from './field/features/Relationship'
export { export {
@@ -139,6 +164,20 @@ export {
} from './field/features/Upload/nodes/UploadNode' } from './field/features/Upload/nodes/UploadNode'
export { AlignFeature } from './field/features/align' export { AlignFeature } from './field/features/align'
export { TextDropdownSectionWithEntries } from './field/features/common/floatingSelectToolbarTextDropdownSection' export { TextDropdownSectionWithEntries } from './field/features/common/floatingSelectToolbarTextDropdownSection'
export {
HTMLConverterFeature,
type HTMLConverterFeatureProps,
} from './field/features/converters/html'
export {
convertLexicalNodesToHTML,
convertLexicalToHTML,
} from './field/features/converters/html/converter'
export { ParagraphHTMLConverter } from './field/features/converters/html/converter/converters/paragraph'
export { TextHTMLConverter } from './field/features/converters/html/converter/converters/text'
export { defaultHTMLConverters } from './field/features/converters/html/converter/defaultConverters'
export type { HTMLConverter } from './field/features/converters/html/converter/types'
export { lexicalHTML } from './field/features/converters/html/field'
export { TreeviewFeature } from './field/features/debug/TreeView' export { TreeviewFeature } from './field/features/debug/TreeView'
export { BoldTextFeature } from './field/features/format/Bold' export { BoldTextFeature } from './field/features/format/Bold'
@@ -155,13 +194,34 @@ export { OrderedListFeature } from './field/features/lists/OrderedList'
export { UnoderedListFeature } from './field/features/lists/UnorderedList' export { UnoderedListFeature } from './field/features/lists/UnorderedList'
export { LexicalPluginToLexicalFeature } from './field/features/migrations/LexicalPluginToLexical' export { LexicalPluginToLexicalFeature } from './field/features/migrations/LexicalPluginToLexical'
export { SlateToLexicalFeature } from './field/features/migrations/SlateToLexical' export { SlateToLexicalFeature } from './field/features/migrations/SlateToLexical'
export { SlateHeadingConverter } from './field/features/migrations/SlateToLexical/converter/converters/heading'
export { SlateIndentConverter } from './field/features/migrations/SlateToLexical/converter/converters/indent'
export { SlateLinkConverter } from './field/features/migrations/SlateToLexical/converter/converters/link'
export { SlateListItemConverter } from './field/features/migrations/SlateToLexical/converter/converters/listItem'
export { SlateOrderedListConverter } from './field/features/migrations/SlateToLexical/converter/converters/orderedList'
export { SlateRelationshipConverter } from './field/features/migrations/SlateToLexical/converter/converters/relationship'
export { SlateUnknownConverter } from './field/features/migrations/SlateToLexical/converter/converters/unknown'
export { SlateUnorderedListConverter } from './field/features/migrations/SlateToLexical/converter/converters/unorderedList'
export { SlateUploadConverter } from './field/features/migrations/SlateToLexical/converter/converters/upload'
export { defaultSlateConverters } from './field/features/migrations/SlateToLexical/converter/defaultConverters'
export {
convertSlateNodesToLexical,
convertSlateToLexical,
} from './field/features/migrations/SlateToLexical/converter/index'
export type {
SlateNode,
SlateNodeConverter,
} from './field/features/migrations/SlateToLexical/converter/types'
export type { export type {
AfterReadPromise,
Feature, Feature,
FeatureProvider, FeatureProvider,
FeatureProviderMap, FeatureProviderMap,
NodeValidation, NodeValidation,
PopulationPromise,
ResolvedFeature, ResolvedFeature,
ResolvedFeatureMap, ResolvedFeatureMap,
SanitizedFeatures, SanitizedFeatures,

View File

@@ -2,17 +2,17 @@ import type { Field, PayloadRequest, RichTextAdapter } from 'payload/types'
import { fieldAffectsData, fieldHasSubFields, fieldIsArrayType } from 'payload/types' import { fieldAffectsData, fieldHasSubFields, fieldIsArrayType } from 'payload/types'
import type { AfterReadPromise } from '../field/features/types' import type { PopulationPromise } from '../field/features/types'
import { populate } from './populate' import { populate } from './populate'
type NestedRichTextFieldsArgs = { type NestedRichTextFieldsArgs = {
afterReadPromises: Map<string, Array<AfterReadPromise>>
currentDepth?: number currentDepth?: number
data: unknown data: unknown
depth: number depth: number
fields: Field[] fields: Field[]
overrideAccess: boolean overrideAccess: boolean
populationPromises: Map<string, Array<PopulationPromise>>
promises: Promise<void>[] promises: Promise<void>[]
req: PayloadRequest req: PayloadRequest
showHiddenFields: boolean showHiddenFields: boolean
@@ -20,12 +20,12 @@ type NestedRichTextFieldsArgs = {
} }
export const recurseNestedFields = ({ export const recurseNestedFields = ({
afterReadPromises,
currentDepth = 0, currentDepth = 0,
data, data,
depth, depth,
fields, fields,
overrideAccess = false, overrideAccess = false,
populationPromises,
promises, promises,
req, req,
showHiddenFields, showHiddenFields,
@@ -118,12 +118,12 @@ export const recurseNestedFields = ({
} else if (fieldHasSubFields(field) && !fieldIsArrayType(field)) { } else if (fieldHasSubFields(field) && !fieldIsArrayType(field)) {
if (fieldAffectsData(field) && typeof data[field.name] === 'object') { if (fieldAffectsData(field) && typeof data[field.name] === 'object') {
recurseNestedFields({ recurseNestedFields({
afterReadPromises,
currentDepth, currentDepth,
data: data[field.name], data: data[field.name],
depth, depth,
fields: field.fields, fields: field.fields,
overrideAccess, overrideAccess,
populationPromises,
promises, promises,
req, req,
showHiddenFields, showHiddenFields,
@@ -131,12 +131,12 @@ export const recurseNestedFields = ({
}) })
} else { } else {
recurseNestedFields({ recurseNestedFields({
afterReadPromises,
currentDepth, currentDepth,
data, data,
depth, depth,
fields: field.fields, fields: field.fields,
overrideAccess, overrideAccess,
populationPromises,
promises, promises,
req, req,
showHiddenFields, showHiddenFields,
@@ -146,12 +146,12 @@ export const recurseNestedFields = ({
} else if (field.type === 'tabs') { } else if (field.type === 'tabs') {
field.tabs.forEach((tab) => { field.tabs.forEach((tab) => {
recurseNestedFields({ recurseNestedFields({
afterReadPromises,
currentDepth, currentDepth,
data, data,
depth, depth,
fields: tab.fields, fields: tab.fields,
overrideAccess, overrideAccess,
populationPromises,
promises, promises,
req, req,
showHiddenFields, showHiddenFields,
@@ -164,12 +164,12 @@ export const recurseNestedFields = ({
const block = field.blocks.find(({ slug }) => slug === row?.blockType) const block = field.blocks.find(({ slug }) => slug === row?.blockType)
if (block) { if (block) {
recurseNestedFields({ recurseNestedFields({
afterReadPromises,
currentDepth, currentDepth,
data: data[field.name][i], data: data[field.name][i],
depth, depth,
fields: block.fields, fields: block.fields,
overrideAccess, overrideAccess,
populationPromises,
promises, promises,
req, req,
showHiddenFields, showHiddenFields,
@@ -182,12 +182,12 @@ export const recurseNestedFields = ({
if (field.type === 'array') { if (field.type === 'array') {
data[field.name].forEach((_, i) => { data[field.name].forEach((_, i) => {
recurseNestedFields({ recurseNestedFields({
afterReadPromises,
currentDepth, currentDepth,
data: data[field.name][i], data: data[field.name][i],
depth, depth,
fields: field.fields, fields: field.fields,
overrideAccess, overrideAccess,
populationPromises,
promises, promises,
req, req,
showHiddenFields, showHiddenFields,
@@ -200,8 +200,8 @@ export const recurseNestedFields = ({
if (field.type === 'richText') { if (field.type === 'richText') {
const editor: RichTextAdapter = field?.editor const editor: RichTextAdapter = field?.editor
if (editor?.afterReadPromise) { if (editor?.populationPromise) {
const afterReadPromise = editor.afterReadPromise({ const afterReadPromise = editor.populationPromise({
currentDepth, currentDepth,
depth, depth,
field, field,

View File

@@ -1,22 +1,22 @@
import type { SerializedEditorState, SerializedLexicalNode } from 'lexical' import type { SerializedEditorState, SerializedLexicalNode } from 'lexical'
import type { PayloadRequest, RichTextAdapter, RichTextField } from 'payload/types' import type { PayloadRequest, RichTextAdapter, RichTextField } from 'payload/types'
import type { AfterReadPromise } from '../field/features/types' import type { PopulationPromise } from '../field/features/types'
import type { AdapterProps } from '../types' import type { AdapterProps } from '../types'
export type Args = Parameters< export type Args = Parameters<
RichTextAdapter<SerializedEditorState, AdapterProps>['afterReadPromise'] RichTextAdapter<SerializedEditorState, AdapterProps>['populationPromise']
>[0] & { >[0] & {
afterReadPromises: Map<string, Array<AfterReadPromise>> populationPromises: Map<string, Array<PopulationPromise>>
} }
type RecurseRichTextArgs = { type RecurseRichTextArgs = {
afterReadPromises: Map<string, Array<AfterReadPromise>>
children: SerializedLexicalNode[] children: SerializedLexicalNode[]
currentDepth: number currentDepth: number
depth: number depth: number
field: RichTextField<SerializedEditorState, AdapterProps> field: RichTextField<SerializedEditorState, AdapterProps>
overrideAccess: boolean overrideAccess: boolean
populationPromises: Map<string, Array<PopulationPromise>>
promises: Promise<void>[] promises: Promise<void>[]
req: PayloadRequest req: PayloadRequest
showHiddenFields: boolean showHiddenFields: boolean
@@ -24,12 +24,12 @@ type RecurseRichTextArgs = {
} }
export const recurseRichText = ({ export const recurseRichText = ({
afterReadPromises,
children, children,
currentDepth = 0, currentDepth = 0,
depth, depth,
field, field,
overrideAccess = false, overrideAccess = false,
populationPromises,
promises, promises,
req, req,
showHiddenFields, showHiddenFields,
@@ -41,16 +41,16 @@ export const recurseRichText = ({
if (Array.isArray(children)) { if (Array.isArray(children)) {
children.forEach((node) => { children.forEach((node) => {
if (afterReadPromises?.has(node.type)) { if (populationPromises?.has(node.type)) {
for (const promise of afterReadPromises.get(node.type)) { for (const promise of populationPromises.get(node.type)) {
promises.push( promises.push(
...promise({ ...promise({
afterReadPromises,
currentDepth, currentDepth,
depth, depth,
field, field,
node: node, node: node,
overrideAccess, overrideAccess,
populationPromises,
req, req,
showHiddenFields, showHiddenFields,
siblingDoc, siblingDoc,
@@ -61,12 +61,12 @@ export const recurseRichText = ({
if ('children' in node && Array.isArray(node?.children) && node?.children?.length) { if ('children' in node && Array.isArray(node?.children) && node?.children?.length) {
recurseRichText({ recurseRichText({
afterReadPromises,
children: node.children as SerializedLexicalNode[], children: node.children as SerializedLexicalNode[],
currentDepth, currentDepth,
depth, depth,
field, field,
overrideAccess, overrideAccess,
populationPromises,
promises, promises,
req, req,
showHiddenFields, showHiddenFields,
@@ -78,11 +78,11 @@ export const recurseRichText = ({
} }
export const richTextRelationshipPromise = async ({ export const richTextRelationshipPromise = async ({
afterReadPromises,
currentDepth, currentDepth,
depth, depth,
field, field,
overrideAccess, overrideAccess,
populationPromises,
req, req,
showHiddenFields, showHiddenFields,
siblingDoc, siblingDoc,
@@ -90,12 +90,12 @@ export const richTextRelationshipPromise = async ({
const promises = [] const promises = []
recurseRichText({ recurseRichText({
afterReadPromises,
children: (siblingDoc[field?.name] as SerializedEditorState)?.root?.children ?? [], children: (siblingDoc[field?.name] as SerializedEditorState)?.root?.children ?? [],
currentDepth, currentDepth,
depth, depth,
field, field,
overrideAccess, overrideAccess,
populationPromises,
promises, promises,
req, req,
showHiddenFields, showHiddenFields,

View File

@@ -5,7 +5,7 @@ import type { AdapterArguments } from '../types'
import { populate } from './populate' import { populate } from './populate'
import { recurseNestedFields } from './recurseNestedFields' import { recurseNestedFields } from './recurseNestedFields'
export type Args = Parameters<RichTextAdapter<any[], AdapterArguments>['afterReadPromise']>[0] export type Args = Parameters<RichTextAdapter<any[], AdapterArguments>['populationPromise']>[0]
type RecurseRichTextArgs = { type RecurseRichTextArgs = {
children: unknown[] children: unknown[]

View File

@@ -19,7 +19,7 @@ export function slateEditor(args: AdapterArguments): RichTextAdapter<any[], Adap
Component: RichTextField, Component: RichTextField,
toMergeIntoProps: args, toMergeIntoProps: args,
}), }),
afterReadPromise({ populationPromise({
currentDepth, currentDepth,
depth, depth,
field, field,

View File

@@ -2,6 +2,7 @@ import type { CollectionConfig } from '../../../../packages/payload/src/collecti
import { import {
BlocksFeature, BlocksFeature,
HTMLConverterFeature,
LexicalPluginToLexicalFeature, LexicalPluginToLexicalFeature,
LinkFeature, LinkFeature,
TreeviewFeature, TreeviewFeature,
@@ -34,45 +35,6 @@ export const LexicalFields: CollectionConfig = {
type: 'text', type: 'text',
required: true, required: true,
}, },
{
name: 'richTextLexicalWithLexicalPluginData',
type: 'richText',
editor: lexicalEditor({
features: ({ defaultFeatures }) => [
...defaultFeatures,
LexicalPluginToLexicalFeature(),
TreeviewFeature(),
LinkFeature({
fields: [
{
name: 'rel',
label: 'Rel Attribute',
type: 'select',
hasMany: true,
options: ['noopener', 'noreferrer', 'nofollow'],
admin: {
description:
'The rel attribute defines the relationship between a linked resource and the current document. This is a custom link field.',
},
},
],
}),
UploadFeature({
collections: {
uploads: {
fields: [
{
name: 'caption',
type: 'richText',
editor: lexicalEditor(),
},
],
},
},
}),
],
}),
},
{ {
name: 'richTextLexicalCustomFields', name: 'richTextLexicalCustomFields',
type: 'richText', type: 'richText',
@@ -81,6 +43,7 @@ export const LexicalFields: CollectionConfig = {
features: ({ defaultFeatures }) => [ features: ({ defaultFeatures }) => [
...defaultFeatures, ...defaultFeatures,
TreeviewFeature(), TreeviewFeature(),
HTMLConverterFeature(),
LinkFeature({ LinkFeature({
fields: [ fields: [
{ {
@@ -122,6 +85,45 @@ export const LexicalFields: CollectionConfig = {
], ],
}), }),
}, },
{
name: 'richTextLexicalWithLexicalPluginData',
type: 'richText',
editor: lexicalEditor({
features: ({ defaultFeatures }) => [
...defaultFeatures,
LexicalPluginToLexicalFeature(),
TreeviewFeature(),
LinkFeature({
fields: [
{
name: 'rel',
label: 'Rel Attribute',
type: 'select',
hasMany: true,
options: ['noopener', 'noreferrer', 'nofollow'],
admin: {
description:
'The rel attribute defines the relationship between a linked resource and the current document. This is a custom link field.',
},
},
],
}),
UploadFeature({
collections: {
uploads: {
fields: [
{
name: 'caption',
type: 'richText',
editor: lexicalEditor(),
},
],
},
},
}),
],
}),
},
], ],
} }

View File

@@ -2,11 +2,13 @@ import type { CollectionConfig } from '../../../../packages/payload/src/collecti
import { import {
BlocksFeature, BlocksFeature,
HTMLConverterFeature,
LinkFeature, LinkFeature,
TreeviewFeature, TreeviewFeature,
UploadFeature, UploadFeature,
lexicalEditor, lexicalEditor,
} from '../../../../packages/richtext-lexical/src' } from '../../../../packages/richtext-lexical/src'
import { lexicalHTML } from '../../../../packages/richtext-lexical/src/field/features/converters/html/field'
import { slateEditor } from '../../../../packages/richtext-slate/src' import { slateEditor } from '../../../../packages/richtext-slate/src'
import { RelationshipBlock, SelectFieldBlock, TextBlock, UploadAndRichTextBlock } from './blocks' import { RelationshipBlock, SelectFieldBlock, TextBlock, UploadAndRichTextBlock } from './blocks'
import { generateLexicalRichText } from './generateLexicalRichText' import { generateLexicalRichText } from './generateLexicalRichText'
@@ -34,6 +36,7 @@ const RichTextFields: CollectionConfig = {
features: ({ defaultFeatures }) => [ features: ({ defaultFeatures }) => [
...defaultFeatures, ...defaultFeatures,
TreeviewFeature(), TreeviewFeature(),
HTMLConverterFeature({}),
LinkFeature({ LinkFeature({
fields: [ fields: [
{ {
@@ -68,6 +71,7 @@ const RichTextFields: CollectionConfig = {
], ],
}), }),
}, },
lexicalHTML('richTextLexicalCustomFields', { name: 'richTextLexicalCustomFields_htmll' }),
{ {
name: 'richTextLexical', name: 'richTextLexical',
type: 'richText', type: 'richText',