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:
@@ -12,7 +12,17 @@ export type RichTextFieldProps<Value extends object, AdapterProps> = Omit<
|
||||
export type RichTextAdapter<Value extends object = object, AdapterProps = any> = {
|
||||
CellComponent: React.FC<CellComponentProps<RichTextField<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
|
||||
depth: number
|
||||
field: RichTextField<Value, AdapterProps>
|
||||
|
||||
@@ -90,12 +90,17 @@ export default joi.object({
|
||||
debug: joi.boolean(),
|
||||
defaultDepth: joi.number().min(0).max(30),
|
||||
defaultMaxTextLength: joi.number(),
|
||||
editor: joi.object().required().keys({
|
||||
editor: joi
|
||||
.object()
|
||||
.required()
|
||||
.keys({
|
||||
CellComponent: component.required(),
|
||||
FieldComponent: component.required(),
|
||||
afterReadPromise: joi.func().required(),
|
||||
afterReadPromise: joi.func().optional(),
|
||||
populationPromise: joi.func().optional(),
|
||||
validate: joi.func().required(),
|
||||
}),
|
||||
})
|
||||
.unknown(),
|
||||
email: joi.object(),
|
||||
endpoints: endpointsSchema,
|
||||
express: joi.object().keys({
|
||||
|
||||
0
packages/payload/src/exports/README.md
Normal file
0
packages/payload/src/exports/README.md
Normal file
@@ -354,12 +354,16 @@ export const richText = baseField.keys({
|
||||
name: joi.string().required(),
|
||||
admin: baseAdminFields.default(),
|
||||
defaultValue: joi.alternatives().try(joi.array().items(joi.object()), joi.func()),
|
||||
editor: joi.object().keys({
|
||||
editor: joi
|
||||
.object()
|
||||
.keys({
|
||||
CellComponent: componentSchema.required(),
|
||||
FieldComponent: componentSchema.required(),
|
||||
afterReadPromise: joi.func().required(),
|
||||
afterReadPromise: joi.func().optional(),
|
||||
populationPromise: joi.func().optional(),
|
||||
validate: joi.func().required(),
|
||||
}),
|
||||
})
|
||||
.unknown(),
|
||||
type: joi.string().valid('richText').required(),
|
||||
})
|
||||
|
||||
|
||||
@@ -135,8 +135,9 @@ export const promise = async ({
|
||||
|
||||
case 'richText': {
|
||||
const editor: RichTextAdapter = field?.editor
|
||||
if (editor?.afterReadPromise) {
|
||||
const afterReadPromise = editor.afterReadPromise({
|
||||
// This is run here AND in the GraphQL Resolver
|
||||
if (editor?.populationPromise) {
|
||||
const populationPromise = editor.populationPromise({
|
||||
currentDepth,
|
||||
depth,
|
||||
field,
|
||||
@@ -146,6 +147,19 @@ export const promise = async ({
|
||||
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) {
|
||||
populationPromises.push(afterReadPromise)
|
||||
}
|
||||
|
||||
@@ -429,8 +429,13 @@ function buildObjectType({
|
||||
if (typeof args.depth !== 'undefined') depth = args.depth
|
||||
const editor: RichTextAdapter = field?.editor
|
||||
|
||||
if (editor?.afterReadPromise) {
|
||||
await editor?.afterReadPromise({
|
||||
// RichText fields have their own depth argument in GraphQL.
|
||||
// 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,
|
||||
field,
|
||||
req: context.req,
|
||||
|
||||
@@ -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 { $getSelection, $isRangeSelection } from 'lexical'
|
||||
|
||||
import type { HTMLConverter } from '../converters/html/converter/types'
|
||||
import type { FeatureProvider } from '../types'
|
||||
|
||||
import { SlashMenuOption } from '../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/LexicalMenu'
|
||||
import { BlockquoteIcon } from '../../lexical/ui/icons/Blockquote'
|
||||
import { TextDropdownSectionWithEntries } from '../common/floatingSelectToolbarTextDropdownSection'
|
||||
import { convertLexicalNodesToHTML } from '../converters/html/converter'
|
||||
import { MarkdownTransformer } from './markdownTransformer'
|
||||
|
||||
export const BlockQuoteFeature = (): FeatureProvider => {
|
||||
return {
|
||||
feature: ({ resolvedFeatures, unsanitizedEditorConfig }) => {
|
||||
feature: () => {
|
||||
return {
|
||||
floatingSelectToolbar: {
|
||||
sections: [
|
||||
@@ -38,6 +42,23 @@ export const BlockQuoteFeature = (): FeatureProvider => {
|
||||
markdownTransformers: [MarkdownTransformer],
|
||||
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,
|
||||
type: QuoteNode.getType(),
|
||||
},
|
||||
|
||||
@@ -7,10 +7,10 @@ import type { FeatureProvider } from '../types'
|
||||
|
||||
import { SlashMenuOption } from '../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/LexicalMenu'
|
||||
import { BlockIcon } from '../../lexical/ui/icons/Block'
|
||||
import { blockAfterReadPromiseHOC } from './afterReadPromise'
|
||||
import './index.scss'
|
||||
import { BlockNode } from './nodes/BlocksNode'
|
||||
import { BlocksPlugin, INSERT_BLOCK_COMMAND } from './plugin'
|
||||
import { blockPopulationPromiseHOC } from './populationPromise'
|
||||
import { blockValidationHOC } from './validate'
|
||||
|
||||
export type BlocksFeatureProps = {
|
||||
@@ -38,12 +38,12 @@ export const BlocksFeature = (props?: BlocksFeatureProps): FeatureProvider => {
|
||||
})
|
||||
}
|
||||
return {
|
||||
feature: ({ resolvedFeatures, unsanitizedEditorConfig }) => {
|
||||
feature: () => {
|
||||
return {
|
||||
nodes: [
|
||||
{
|
||||
afterReadPromises: [blockAfterReadPromiseHOC(props)],
|
||||
node: BlockNode,
|
||||
populationPromises: [blockPopulationPromiseHOC(props)],
|
||||
type: BlockNode.getType(),
|
||||
validations: [blockValidationHOC(props)],
|
||||
},
|
||||
|
||||
@@ -3,24 +3,22 @@ import type { Block } from 'payload/types'
|
||||
import { sanitizeFields } from 'payload/config'
|
||||
|
||||
import type { BlocksFeatureProps } from '.'
|
||||
import type { AfterReadPromise } from '../types'
|
||||
import type { PopulationPromise } from '../types'
|
||||
import type { SerializedBlockNode } from './nodes/BlocksNode'
|
||||
|
||||
import { recurseNestedFields } from '../../../populate/recurseNestedFields'
|
||||
|
||||
export const blockAfterReadPromiseHOC = (
|
||||
export const blockPopulationPromiseHOC = (
|
||||
props: BlocksFeatureProps,
|
||||
): AfterReadPromise<SerializedBlockNode> => {
|
||||
const blockAfterReadPromise: AfterReadPromise<SerializedBlockNode> = ({
|
||||
afterReadPromises,
|
||||
): PopulationPromise<SerializedBlockNode> => {
|
||||
const blockPopulationPromise: PopulationPromise<SerializedBlockNode> = ({
|
||||
currentDepth,
|
||||
depth,
|
||||
field,
|
||||
node,
|
||||
overrideAccess,
|
||||
populationPromises,
|
||||
req,
|
||||
showHiddenFields,
|
||||
siblingDoc,
|
||||
}) => {
|
||||
const blocks: Block[] = props.blocks
|
||||
const blockFieldData = node.fields.data
|
||||
@@ -45,12 +43,12 @@ export const blockAfterReadPromiseHOC = (
|
||||
}
|
||||
|
||||
recurseNestedFields({
|
||||
afterReadPromises,
|
||||
currentDepth,
|
||||
data: blockFieldData,
|
||||
depth,
|
||||
fields: block.fields,
|
||||
overrideAccess,
|
||||
populationPromises,
|
||||
promises,
|
||||
req,
|
||||
showHiddenFields,
|
||||
@@ -61,5 +59,5 @@ export const blockAfterReadPromiseHOC = (
|
||||
return promises
|
||||
}
|
||||
|
||||
return blockAfterReadPromise
|
||||
return blockPopulationPromise
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { HeadingTagType } from '@lexical/rich-text'
|
||||
import type { LexicalEditor } from 'lexical'
|
||||
import type { HeadingTagType, SerializedHeadingNode } from '@lexical/rich-text'
|
||||
import type React from 'react'
|
||||
|
||||
import { $createHeadingNode, HeadingNode } from '@lexical/rich-text'
|
||||
import { $setBlocksType } from '@lexical/selection'
|
||||
import { $getSelection, $isRangeSelection, DEPRECATED_$isGridSelection } from 'lexical'
|
||||
|
||||
import type { HTMLConverter } from '../converters/html/converter/types'
|
||||
import type { FeatureProvider } from '../types'
|
||||
|
||||
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 { H6Icon } from '../../lexical/ui/icons/H6'
|
||||
import { TextDropdownSectionWithEntries } from '../common/floatingSelectToolbarTextDropdownSection'
|
||||
import { convertLexicalNodesToHTML } from '../converters/html/converter'
|
||||
import { MarkdownTransformer } from './markdownTransformer'
|
||||
|
||||
const setHeading = (editor: LexicalEditor, headingSize: HeadingTagType) => {
|
||||
const setHeading = (headingSize: HeadingTagType) => {
|
||||
const selection = $getSelection()
|
||||
if ($isRangeSelection(selection) || DEPRECATED_$isGridSelection(selection)) {
|
||||
$setBlocksType(selection, () => $createHeadingNode(headingSize))
|
||||
@@ -41,7 +43,7 @@ export const HeadingFeature = (props: Props): FeatureProvider => {
|
||||
const { enabledHeadingSizes = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] } = props
|
||||
|
||||
return {
|
||||
feature: ({ resolvedFeatures, unsanitizedEditorConfig }) => {
|
||||
feature: () => {
|
||||
return {
|
||||
floatingSelectToolbar: {
|
||||
sections: [
|
||||
@@ -49,12 +51,12 @@ export const HeadingFeature = (props: Props): FeatureProvider => {
|
||||
TextDropdownSectionWithEntries([
|
||||
{
|
||||
ChildComponent: HeadingToIconMap[headingSize],
|
||||
isActive: ({ editor, selection }) => false,
|
||||
isActive: () => false,
|
||||
key: headingSize,
|
||||
label: `Heading ${headingSize.charAt(1)}`,
|
||||
onClick: ({ editor }) => {
|
||||
editor.update(() => {
|
||||
setHeading(editor, headingSize)
|
||||
setHeading(headingSize)
|
||||
})
|
||||
},
|
||||
order: i + 2,
|
||||
@@ -64,7 +66,29 @@ export const HeadingFeature = (props: Props): FeatureProvider => {
|
||||
],
|
||||
},
|
||||
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,
|
||||
slashMenu: {
|
||||
options: [
|
||||
@@ -74,8 +98,8 @@ export const HeadingFeature = (props: Props): FeatureProvider => {
|
||||
new SlashMenuOption(`Heading ${headingSize.charAt(1)}`, {
|
||||
Icon: HeadingToIconMap[headingSize],
|
||||
keywords: ['heading', headingSize],
|
||||
onSelect: ({ editor }) => {
|
||||
setHeading(editor, headingSize)
|
||||
onSelect: () => {
|
||||
setHeading(headingSize)
|
||||
},
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -7,13 +7,15 @@ import { $findMatchingParent } from '@lexical/utils'
|
||||
import { $getSelection, $isRangeSelection } from 'lexical'
|
||||
import { withMergedProps } from 'payload/utilities'
|
||||
|
||||
import type { HTMLConverter } from '../converters/html/converter/types'
|
||||
import type { FeatureProvider } from '../types'
|
||||
import type { 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 { getSelectedNode } from '../../lexical/utils/getSelectedNode'
|
||||
import { FeaturesSectionWithEntries } from '../common/floatingSelectToolbarFeaturesButtonsSection'
|
||||
import { linkAfterReadPromiseHOC } from './afterReadPromise'
|
||||
import { convertLexicalNodesToHTML } from '../converters/html/converter'
|
||||
import './index.scss'
|
||||
import { AutoLinkNode } from './nodes/AutoLinkNode'
|
||||
import { $isLinkNode, LinkNode, TOGGLE_LINK_COMMAND } from './nodes/LinkNode'
|
||||
@@ -21,6 +23,7 @@ import { AutoLinkPlugin } from './plugins/autoLink'
|
||||
import { FloatingLinkEditorPlugin } from './plugins/floatingLinkEditor'
|
||||
import { TOGGLE_LINK_WITH_MODAL_COMMAND } from './plugins/floatingLinkEditor/LinkEditor'
|
||||
import { LinkPlugin } from './plugins/link'
|
||||
import { linkPopulationPromiseHOC } from './populationPromise'
|
||||
|
||||
export type LinkFeatureProps = {
|
||||
fields?:
|
||||
@@ -29,7 +32,7 @@ export type LinkFeatureProps = {
|
||||
}
|
||||
export const LinkFeature = (props: LinkFeatureProps): FeatureProvider => {
|
||||
return {
|
||||
feature: ({ resolvedFeatures, unsanitizedEditorConfig }) => {
|
||||
feature: () => {
|
||||
return {
|
||||
floatingSelectToolbar: {
|
||||
sections: [
|
||||
@@ -74,13 +77,58 @@ export const LinkFeature = (props: LinkFeatureProps): FeatureProvider => {
|
||||
},
|
||||
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,
|
||||
populationPromises: [linkPopulationPromiseHOC(props)],
|
||||
type: LinkNode.getType(),
|
||||
// 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,
|
||||
populationPromises: [linkPopulationPromiseHOC(props)],
|
||||
type: AutoLinkNode.getType(),
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
import type { LinkFeatureProps } from '.'
|
||||
import type { AfterReadPromise } from '../types'
|
||||
import type { PopulationPromise } from '../types'
|
||||
import type { SerializedLinkNode } from './nodes/LinkNode'
|
||||
|
||||
import { populate } from '../../../populate/populate'
|
||||
import { recurseNestedFields } from '../../../populate/recurseNestedFields'
|
||||
|
||||
export const linkAfterReadPromiseHOC = (
|
||||
export const linkPopulationPromiseHOC = (
|
||||
props: LinkFeatureProps,
|
||||
): AfterReadPromise<SerializedLinkNode> => {
|
||||
const linkAfterReadPromise: AfterReadPromise<SerializedLinkNode> = ({
|
||||
afterReadPromises,
|
||||
): PopulationPromise<SerializedLinkNode> => {
|
||||
const linkPopulationPromise: PopulationPromise<SerializedLinkNode> = ({
|
||||
currentDepth,
|
||||
depth,
|
||||
field,
|
||||
node,
|
||||
overrideAccess,
|
||||
populationPromises,
|
||||
req,
|
||||
showHiddenFields,
|
||||
siblingDoc,
|
||||
}) => {
|
||||
const promises: Promise<void>[] = []
|
||||
|
||||
@@ -43,12 +42,12 @@ export const linkAfterReadPromiseHOC = (
|
||||
}
|
||||
if (Array.isArray(props.fields)) {
|
||||
recurseNestedFields({
|
||||
afterReadPromises,
|
||||
currentDepth,
|
||||
data: node.fields || {},
|
||||
depth,
|
||||
fields: props.fields,
|
||||
overrideAccess,
|
||||
populationPromises,
|
||||
promises,
|
||||
req,
|
||||
showHiddenFields,
|
||||
@@ -58,5 +57,5 @@ export const linkAfterReadPromiseHOC = (
|
||||
return promises
|
||||
}
|
||||
|
||||
return linkAfterReadPromise
|
||||
return linkPopulationPromise
|
||||
}
|
||||
@@ -2,20 +2,20 @@ import type { FeatureProvider } from '../types'
|
||||
|
||||
import { SlashMenuOption } from '../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/LexicalMenu'
|
||||
import { RelationshipIcon } from '../../lexical/ui/icons/Relationship'
|
||||
import { relationshipAfterReadPromise } from './afterReadPromise'
|
||||
import { INSERT_RELATIONSHIP_WITH_DRAWER_COMMAND } from './drawer'
|
||||
import './index.scss'
|
||||
import { RelationshipNode } from './nodes/RelationshipNode'
|
||||
import RelationshipPlugin from './plugins'
|
||||
import { relationshipPopulationPromise } from './populationPromise'
|
||||
|
||||
export const RelationshipFeature = (): FeatureProvider => {
|
||||
return {
|
||||
feature: ({ resolvedFeatures, unsanitizedEditorConfig }) => {
|
||||
feature: () => {
|
||||
return {
|
||||
nodes: [
|
||||
{
|
||||
afterReadPromises: [relationshipAfterReadPromise],
|
||||
node: RelationshipNode,
|
||||
populationPromises: [relationshipPopulationPromise],
|
||||
type: RelationshipNode.getType(),
|
||||
// TODO: Add validation similar to upload
|
||||
},
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { AfterReadPromise } from '../types'
|
||||
import type { PopulationPromise } from '../types'
|
||||
import type { SerializedRelationshipNode } from './nodes/RelationshipNode'
|
||||
|
||||
import { populate } from '../../../populate/populate'
|
||||
|
||||
export const relationshipAfterReadPromise: AfterReadPromise<SerializedRelationshipNode> = ({
|
||||
export const relationshipPopulationPromise: PopulationPromise<SerializedRelationshipNode> = ({
|
||||
currentDepth,
|
||||
depth,
|
||||
field,
|
||||
@@ -1,14 +1,18 @@
|
||||
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 { SerializedUploadNode } from './nodes/UploadNode'
|
||||
|
||||
import { SlashMenuOption } from '../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/LexicalMenu'
|
||||
import { UploadIcon } from '../../lexical/ui/icons/Upload'
|
||||
import { uploadAfterReadPromiseHOC } from './afterReadPromise'
|
||||
import { INSERT_UPLOAD_WITH_DRAWER_COMMAND } from './drawer'
|
||||
import './index.scss'
|
||||
import { UploadNode } from './nodes/UploadNode'
|
||||
import { UploadPlugin } from './plugin'
|
||||
import { uploadPopulationPromiseHOC } from './populationPromise'
|
||||
import { uploadValidation } from './validate'
|
||||
|
||||
export type UploadFeatureProps = {
|
||||
@@ -21,12 +25,30 @@ export type UploadFeatureProps = {
|
||||
|
||||
export const UploadFeature = (props?: UploadFeatureProps): FeatureProvider => {
|
||||
return {
|
||||
feature: ({ resolvedFeatures, unsanitizedEditorConfig }) => {
|
||||
feature: () => {
|
||||
return {
|
||||
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,
|
||||
populationPromises: [uploadPopulationPromiseHOC(props)],
|
||||
type: UploadNode.getType(),
|
||||
validations: [uploadValidation()],
|
||||
},
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
import type { UploadFeatureProps } from '.'
|
||||
import type { AfterReadPromise } from '../types'
|
||||
import type { PopulationPromise } from '../types'
|
||||
import type { SerializedUploadNode } from './nodes/UploadNode'
|
||||
|
||||
import { populate } from '../../../populate/populate'
|
||||
import { recurseNestedFields } from '../../../populate/recurseNestedFields'
|
||||
|
||||
export const uploadAfterReadPromiseHOC = (
|
||||
export const uploadPopulationPromiseHOC = (
|
||||
props?: UploadFeatureProps,
|
||||
): AfterReadPromise<SerializedUploadNode> => {
|
||||
const uploadAfterReadPromise: AfterReadPromise<SerializedUploadNode> = ({
|
||||
afterReadPromises,
|
||||
): PopulationPromise<SerializedUploadNode> => {
|
||||
const uploadPopulationPromise: PopulationPromise<SerializedUploadNode> = ({
|
||||
currentDepth,
|
||||
depth,
|
||||
field,
|
||||
node,
|
||||
overrideAccess,
|
||||
populationPromises,
|
||||
req,
|
||||
showHiddenFields,
|
||||
siblingDoc,
|
||||
}) => {
|
||||
const promises: Promise<void>[] = []
|
||||
|
||||
@@ -42,12 +41,12 @@ export const uploadAfterReadPromiseHOC = (
|
||||
}
|
||||
if (Array.isArray(props?.collections?.[node?.relationTo]?.fields)) {
|
||||
recurseNestedFields({
|
||||
afterReadPromises,
|
||||
currentDepth,
|
||||
data: node.fields || {},
|
||||
depth,
|
||||
fields: props?.collections?.[node?.relationTo]?.fields,
|
||||
overrideAccess,
|
||||
populationPromises,
|
||||
promises,
|
||||
req,
|
||||
showHiddenFields,
|
||||
@@ -59,5 +58,5 @@ export const uploadAfterReadPromiseHOC = (
|
||||
return promises
|
||||
}
|
||||
|
||||
return uploadAfterReadPromise
|
||||
return uploadPopulationPromise
|
||||
}
|
||||
@@ -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'],
|
||||
}
|
||||
@@ -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'],
|
||||
}
|
||||
@@ -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]
|
||||
@@ -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('') || ''
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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',
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import type { FeatureProvider } from '../../types'
|
||||
|
||||
import { SlashMenuOption } from '../../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/LexicalMenu'
|
||||
import { ChecklistIcon } from '../../../lexical/ui/icons/Checklist'
|
||||
import { ListHTMLConverter, ListItemHTMLConverter } from '../htmlConverter'
|
||||
import { CHECK_LIST } from './markdownTransformers'
|
||||
|
||||
// 345
|
||||
@@ -19,10 +20,16 @@ export const CheckListFeature = (): FeatureProvider => {
|
||||
? []
|
||||
: [
|
||||
{
|
||||
converters: {
|
||||
html: ListHTMLConverter,
|
||||
},
|
||||
node: ListNode,
|
||||
type: ListNode.getType(),
|
||||
},
|
||||
{
|
||||
converters: {
|
||||
html: ListItemHTMLConverter,
|
||||
},
|
||||
node: ListItemNode,
|
||||
type: ListItemNode.getType(),
|
||||
},
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { FeatureProvider } from '../../types'
|
||||
|
||||
import { SlashMenuOption } from '../../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/LexicalMenu'
|
||||
import { OrderedListIcon } from '../../../lexical/ui/icons/OrderedList'
|
||||
import { ListHTMLConverter, ListItemHTMLConverter } from '../htmlConverter'
|
||||
import { ORDERED_LIST } from './markdownTransformer'
|
||||
|
||||
export const OrderedListFeature = (): FeatureProvider => {
|
||||
@@ -16,10 +17,19 @@ export const OrderedListFeature = (): FeatureProvider => {
|
||||
? []
|
||||
: [
|
||||
{
|
||||
converters: {
|
||||
html: ListHTMLConverter,
|
||||
},
|
||||
node: ListNode,
|
||||
type: ListNode.getType(),
|
||||
},
|
||||
{ node: ListItemNode, type: ListItemNode.getType() },
|
||||
{
|
||||
converters: {
|
||||
html: ListItemHTMLConverter,
|
||||
},
|
||||
node: ListItemNode,
|
||||
type: ListItemNode.getType(),
|
||||
},
|
||||
],
|
||||
plugins: featureProviderMap.has('unorderedList')
|
||||
? []
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { FeatureProvider } from '../../types'
|
||||
|
||||
import { SlashMenuOption } from '../../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/LexicalMenu'
|
||||
import { UnorderedListIcon } from '../../../lexical/ui/icons/UnorderedList'
|
||||
import { ListHTMLConverter, ListItemHTMLConverter } from '../htmlConverter'
|
||||
import { UNORDERED_LIST } from './markdownTransformer'
|
||||
|
||||
export const UnoderedListFeature = (): FeatureProvider => {
|
||||
@@ -14,10 +15,16 @@ export const UnoderedListFeature = (): FeatureProvider => {
|
||||
markdownTransformers: [UNORDERED_LIST],
|
||||
nodes: [
|
||||
{
|
||||
converters: {
|
||||
html: ListHTMLConverter,
|
||||
},
|
||||
node: ListNode,
|
||||
type: ListNode.getType(),
|
||||
},
|
||||
{
|
||||
converters: {
|
||||
html: ListItemHTMLConverter,
|
||||
},
|
||||
node: ListItemNode,
|
||||
type: ListItemNode.getType(),
|
||||
},
|
||||
|
||||
@@ -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()],
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import type { SlateNodeConverter } from '../types'
|
||||
|
||||
import { convertSlateNodesToLexical } from '..'
|
||||
|
||||
export const HeadingConverter: SlateNodeConverter = {
|
||||
export const SlateHeadingConverter: SlateNodeConverter = {
|
||||
converter({ converters, slateNode }) {
|
||||
return {
|
||||
children: convertSlateNodesToLexical({
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { SlateNodeConverter } from '../types'
|
||||
|
||||
import { convertSlateNodesToLexical } from '..'
|
||||
|
||||
export const IndentConverter: SlateNodeConverter = {
|
||||
export const SlateIndentConverter: SlateNodeConverter = {
|
||||
converter({ converters, slateNode }) {
|
||||
console.log('slateToLexical > IndentConverter > converter', JSON.stringify(slateNode, null, 2))
|
||||
const convertChildren = (node: any, indentLevel: number = 0): SerializedLexicalNode => {
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { SlateNodeConverter } from '../types'
|
||||
|
||||
import { convertSlateNodesToLexical } from '..'
|
||||
|
||||
export const LinkConverter: SlateNodeConverter = {
|
||||
export const SlateLinkConverter: SlateNodeConverter = {
|
||||
converter({ converters, slateNode }) {
|
||||
return {
|
||||
children: convertSlateNodesToLexical({
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { SlateNodeConverter } from '../types'
|
||||
|
||||
import { convertSlateNodesToLexical } from '..'
|
||||
|
||||
export const ListItemConverter: SlateNodeConverter = {
|
||||
export const SlateListItemConverter: SlateNodeConverter = {
|
||||
converter({ childIndex, converters, slateNode }) {
|
||||
return {
|
||||
checked: undefined,
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { SlateNodeConverter } from '../types'
|
||||
|
||||
import { convertSlateNodesToLexical } from '..'
|
||||
|
||||
export const OrderedListConverter: SlateNodeConverter = {
|
||||
export const SlateOrderedListConverter: SlateNodeConverter = {
|
||||
converter({ converters, slateNode }) {
|
||||
return {
|
||||
children: convertSlateNodesToLexical({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { SerializedRelationshipNode } from '../../../../../..'
|
||||
import type { SlateNodeConverter } from '../types'
|
||||
|
||||
export const RelationshipConverter: SlateNodeConverter = {
|
||||
export const SlateRelationshipConverter: SlateNodeConverter = {
|
||||
converter({ slateNode }) {
|
||||
return {
|
||||
format: '',
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { SlateNodeConverter } from '../types'
|
||||
|
||||
import { convertSlateNodesToLexical } from '..'
|
||||
|
||||
export const UnknownConverter: SlateNodeConverter = {
|
||||
export const SlateUnknownConverter: SlateNodeConverter = {
|
||||
converter({ converters, slateNode }) {
|
||||
return {
|
||||
children: convertSlateNodesToLexical({
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { SlateNodeConverter } from '../types'
|
||||
|
||||
import { convertSlateNodesToLexical } from '..'
|
||||
|
||||
export const UnorderedListConverter: SlateNodeConverter = {
|
||||
export const SlateUnorderedListConverter: SlateNodeConverter = {
|
||||
converter({ converters, slateNode }) {
|
||||
return {
|
||||
children: convertSlateNodesToLexical({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { SerializedUploadNode } from '../../../../../..'
|
||||
import type { SlateNodeConverter } from '../types'
|
||||
|
||||
export const UploadConverter: SlateNodeConverter = {
|
||||
export const SlateUploadConverter: SlateNodeConverter = {
|
||||
converter({ slateNode }) {
|
||||
return {
|
||||
fields: {
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import type { SlateNodeConverter } from './types'
|
||||
|
||||
import { HeadingConverter } from './converters/heading'
|
||||
import { IndentConverter } from './converters/indent'
|
||||
import { LinkConverter } from './converters/link'
|
||||
import { ListItemConverter } from './converters/listItem'
|
||||
import { OrderedListConverter } from './converters/orderedList'
|
||||
import { RelationshipConverter } from './converters/relationship'
|
||||
import { UnknownConverter } from './converters/unknown'
|
||||
import { UnorderedListConverter } from './converters/unorderedList'
|
||||
import { UploadConverter } from './converters/upload'
|
||||
import { SlateHeadingConverter } from './converters/heading'
|
||||
import { SlateIndentConverter } from './converters/indent'
|
||||
import { SlateLinkConverter } from './converters/link'
|
||||
import { SlateListItemConverter } from './converters/listItem'
|
||||
import { SlateOrderedListConverter } from './converters/orderedList'
|
||||
import { SlateRelationshipConverter } from './converters/relationship'
|
||||
import { SlateUnknownConverter } from './converters/unknown'
|
||||
import { SlateUnorderedListConverter } from './converters/unorderedList'
|
||||
import { SlateUploadConverter } from './converters/upload'
|
||||
|
||||
export const defaultConverters: SlateNodeConverter[] = [
|
||||
UnknownConverter,
|
||||
UploadConverter,
|
||||
UnorderedListConverter,
|
||||
OrderedListConverter,
|
||||
RelationshipConverter,
|
||||
ListItemConverter,
|
||||
LinkConverter,
|
||||
HeadingConverter,
|
||||
IndentConverter,
|
||||
export const defaultSlateConverters: SlateNodeConverter[] = [
|
||||
SlateUnknownConverter,
|
||||
SlateUploadConverter,
|
||||
SlateUnorderedListConverter,
|
||||
SlateOrderedListConverter,
|
||||
SlateRelationshipConverter,
|
||||
SlateListItemConverter,
|
||||
SlateLinkConverter,
|
||||
SlateHeadingConverter,
|
||||
SlateIndentConverter,
|
||||
]
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { FeatureProvider } from '../../types'
|
||||
import type { SlateNodeConverter } from './converter/types'
|
||||
|
||||
import { convertSlateToLexical } from './converter'
|
||||
import { defaultConverters } from './converter/defaultConverters'
|
||||
import { defaultSlateConverters } from './converter/defaultConverters'
|
||||
import { UnknownConvertedNode } from './nodes/unknownConvertedNode'
|
||||
|
||||
type Props = {
|
||||
@@ -18,11 +18,11 @@ export const SlateToLexicalFeature = (props?: Props): FeatureProvider => {
|
||||
|
||||
props.converters =
|
||||
props?.converters && typeof props?.converters === 'function'
|
||||
? props.converters({ defaultConverters: defaultConverters })
|
||||
: (props?.converters as SlateNodeConverter[]) || defaultConverters
|
||||
? props.converters({ defaultConverters: defaultSlateConverters })
|
||||
: (props?.converters as SlateNodeConverter[]) || defaultSlateConverters
|
||||
|
||||
return {
|
||||
feature: ({ resolvedFeatures, unsanitizedEditorConfig }) => {
|
||||
feature: () => {
|
||||
return {
|
||||
hooks: {
|
||||
load({ incomingEditorState }) {
|
||||
|
||||
@@ -6,27 +6,28 @@ import type { PayloadRequest, RichTextField, ValidateOptions } from 'payload/typ
|
||||
import type React from 'react'
|
||||
|
||||
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 { SlashMenuGroup } from '../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/LexicalMenu'
|
||||
import type { HTMLConverter } from './converters/html/converter/types'
|
||||
|
||||
export type AfterReadPromise<T extends SerializedLexicalNode = SerializedLexicalNode> = ({
|
||||
afterReadPromises,
|
||||
export type PopulationPromise<T extends SerializedLexicalNode = SerializedLexicalNode> = ({
|
||||
currentDepth,
|
||||
depth,
|
||||
field,
|
||||
node,
|
||||
overrideAccess,
|
||||
populationPromises,
|
||||
req,
|
||||
showHiddenFields,
|
||||
siblingDoc,
|
||||
}: {
|
||||
afterReadPromises: Map<string, Array<AfterReadPromise>>
|
||||
currentDepth: number
|
||||
depth: number
|
||||
field: RichTextField<SerializedEditorState, AdapterProps>
|
||||
node: T
|
||||
overrideAccess: boolean
|
||||
populationPromises: Map<string, Array<PopulationPromise>>
|
||||
req: PayloadRequest
|
||||
showHiddenFields: boolean
|
||||
siblingDoc: Record<string, unknown>
|
||||
@@ -52,6 +53,15 @@ export type Feature = {
|
||||
sections: FloatingToolbarSection[]
|
||||
}
|
||||
hooks?: {
|
||||
afterReadPromise?: ({
|
||||
field,
|
||||
incomingEditorState,
|
||||
siblingDoc,
|
||||
}: {
|
||||
field: RichTextField<SerializedEditorState, AdapterProps>
|
||||
incomingEditorState: SerializedEditorState
|
||||
siblingDoc: Record<string, unknown>
|
||||
}) => Promise<void> | null
|
||||
load?: ({
|
||||
incomingEditorState,
|
||||
}: {
|
||||
@@ -65,8 +75,11 @@ export type Feature = {
|
||||
}
|
||||
markdownTransformers?: Transformer[]
|
||||
nodes?: Array<{
|
||||
afterReadPromises?: Array<AfterReadPromise>
|
||||
converters?: {
|
||||
html?: HTMLConverter
|
||||
}
|
||||
node: Klass<LexicalNode>
|
||||
populationPromises?: Array<PopulationPromise>
|
||||
type: string
|
||||
validations?: Array<NodeValidation>
|
||||
}>
|
||||
@@ -128,14 +141,27 @@ export type FeatureProviderMap = Map<string, FeatureProvider>
|
||||
export type SanitizedFeatures = Required<
|
||||
Pick<ResolvedFeature, 'markdownTransformers' | 'nodes'>
|
||||
> & {
|
||||
/** The node types mapped to their afterReadPromises */
|
||||
afterReadPromises: Map<string, Array<AfterReadPromise>>
|
||||
/** The node types mapped to their converters */
|
||||
converters: {
|
||||
html: HTMLConverter[]
|
||||
}
|
||||
/** The keys of all enabled features */
|
||||
enabledFeatures: string[]
|
||||
floatingSelectToolbar: {
|
||||
sections: FloatingToolbarSection[]
|
||||
}
|
||||
hooks: {
|
||||
afterReadPromises: Array<
|
||||
({
|
||||
field,
|
||||
incomingEditorState,
|
||||
siblingDoc,
|
||||
}: {
|
||||
field: RichTextField<SerializedEditorState, AdapterProps>
|
||||
incomingEditorState: SerializedEditorState
|
||||
siblingDoc: Record<string, unknown>
|
||||
}) => Promise<void> | null
|
||||
>
|
||||
load: Array<
|
||||
({
|
||||
incomingEditorState,
|
||||
@@ -166,6 +192,8 @@ export type SanitizedFeatures = Required<
|
||||
position: 'floatingAnchorElem' // Determines at which position the Component will be added.
|
||||
}
|
||||
>
|
||||
/** The node types mapped to their populationPromises */
|
||||
populationPromises: Map<string, Array<PopulationPromise>>
|
||||
slashMenu: {
|
||||
dynamicOptions: Array<
|
||||
({ editor, queryString }: { editor: LexicalEditor; queryString: string }) => SlashMenuGroup[]
|
||||
|
||||
@@ -5,18 +5,22 @@ import { loadFeatures } from './loader'
|
||||
|
||||
export const sanitizeFeatures = (features: ResolvedFeatureMap): SanitizedFeatures => {
|
||||
const sanitized: SanitizedFeatures = {
|
||||
afterReadPromises: new Map(),
|
||||
converters: {
|
||||
html: [],
|
||||
},
|
||||
enabledFeatures: [],
|
||||
floatingSelectToolbar: {
|
||||
sections: [],
|
||||
},
|
||||
hooks: {
|
||||
afterReadPromises: [],
|
||||
load: [],
|
||||
save: [],
|
||||
},
|
||||
markdownTransformers: [],
|
||||
nodes: [],
|
||||
plugins: [],
|
||||
populationPromises: new Map(),
|
||||
slashMenu: {
|
||||
dynamicOptions: [],
|
||||
groupsWithOptions: [],
|
||||
@@ -26,6 +30,11 @@ export const sanitizeFeatures = (features: ResolvedFeatureMap): SanitizedFeature
|
||||
|
||||
features.forEach((feature) => {
|
||||
if (feature.hooks) {
|
||||
if (feature.hooks.afterReadPromise) {
|
||||
sanitized.hooks.afterReadPromises = sanitized.hooks.afterReadPromises.concat(
|
||||
feature.hooks.afterReadPromise,
|
||||
)
|
||||
}
|
||||
if (feature.hooks?.load?.length) {
|
||||
sanitized.hooks.load = sanitized.hooks.load.concat(feature.hooks.load)
|
||||
}
|
||||
@@ -37,12 +46,15 @@ export const sanitizeFeatures = (features: ResolvedFeatureMap): SanitizedFeature
|
||||
if (feature.nodes?.length) {
|
||||
sanitized.nodes = sanitized.nodes.concat(feature.nodes)
|
||||
feature.nodes.forEach((node) => {
|
||||
if (node?.afterReadPromises?.length) {
|
||||
sanitized.afterReadPromises.set(node.type, node.afterReadPromises)
|
||||
if (node?.populationPromises?.length) {
|
||||
sanitized.populationPromises.set(node.type, node.populationPromises)
|
||||
}
|
||||
if (node?.validations?.length) {
|
||||
sanitized.validations.set(node.type, node.validations)
|
||||
}
|
||||
if (node?.converters?.html) {
|
||||
sanitized.converters.html.push(node.converters.html)
|
||||
}
|
||||
})
|
||||
}
|
||||
if (feature.plugins?.length) {
|
||||
|
||||
@@ -27,9 +27,11 @@ export type LexicalEditorProps = {
|
||||
lexical?: LexicalEditorConfig
|
||||
}
|
||||
|
||||
export function lexicalEditor(
|
||||
props?: LexicalEditorProps,
|
||||
): RichTextAdapter<SerializedEditorState, AdapterProps> {
|
||||
export type LexicalRichTextAdapter = RichTextAdapter<SerializedEditorState, AdapterProps> & {
|
||||
editorConfig: SanitizedEditorConfig
|
||||
}
|
||||
|
||||
export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapter {
|
||||
let finalSanitizedEditorConfig: SanitizedEditorConfig
|
||||
if (!props || (!props.features && !props.lexical)) {
|
||||
finalSanitizedEditorConfig = cloneDeep(defaultSanitizedEditorConfig)
|
||||
@@ -59,7 +61,30 @@ export function lexicalEditor(
|
||||
Component: RichTextField,
|
||||
toMergeIntoProps: { editorConfig: finalSanitizedEditorConfig },
|
||||
}),
|
||||
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,
|
||||
depth,
|
||||
field,
|
||||
@@ -68,14 +93,14 @@ export function lexicalEditor(
|
||||
showHiddenFields,
|
||||
siblingDoc,
|
||||
}) {
|
||||
// check if there are any features with nodes which have afterReadPromises for this field
|
||||
if (finalSanitizedEditorConfig?.features?.afterReadPromises?.size) {
|
||||
// check if there are any features with nodes which have populationPromises for this field
|
||||
if (finalSanitizedEditorConfig?.features?.populationPromises?.size) {
|
||||
return richTextRelationshipPromise({
|
||||
afterReadPromises: finalSanitizedEditorConfig.features.afterReadPromises,
|
||||
currentDepth,
|
||||
depth,
|
||||
field,
|
||||
overrideAccess,
|
||||
populationPromises: finalSanitizedEditorConfig.features.populationPromises,
|
||||
req,
|
||||
showHiddenFields,
|
||||
siblingDoc,
|
||||
@@ -99,8 +124,8 @@ export {
|
||||
BlockNode,
|
||||
type SerializedBlockNode,
|
||||
} from './field/features/Blocks/nodes/BlocksNode'
|
||||
|
||||
export { HeadingFeature } from './field/features/Heading'
|
||||
|
||||
export { LinkFeature } from './field/features/Link'
|
||||
export type { LinkFeatureProps } from './field/features/Link'
|
||||
export {
|
||||
@@ -109,7 +134,6 @@ export {
|
||||
AutoLinkNode,
|
||||
type SerializedAutoLinkNode,
|
||||
} from './field/features/Link/nodes/AutoLinkNode'
|
||||
|
||||
export {
|
||||
$createLinkNode,
|
||||
$isLinkNode,
|
||||
@@ -118,6 +142,7 @@ export {
|
||||
type SerializedLinkNode,
|
||||
TOGGLE_LINK_COMMAND,
|
||||
} from './field/features/Link/nodes/LinkNode'
|
||||
|
||||
export { ParagraphFeature } from './field/features/Paragraph'
|
||||
export { RelationshipFeature } from './field/features/Relationship'
|
||||
export {
|
||||
@@ -139,6 +164,20 @@ export {
|
||||
} from './field/features/Upload/nodes/UploadNode'
|
||||
export { AlignFeature } from './field/features/align'
|
||||
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 { 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 { LexicalPluginToLexicalFeature } from './field/features/migrations/LexicalPluginToLexical'
|
||||
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 {
|
||||
AfterReadPromise,
|
||||
Feature,
|
||||
FeatureProvider,
|
||||
FeatureProviderMap,
|
||||
NodeValidation,
|
||||
PopulationPromise,
|
||||
ResolvedFeature,
|
||||
ResolvedFeatureMap,
|
||||
SanitizedFeatures,
|
||||
|
||||
@@ -2,17 +2,17 @@ import type { Field, PayloadRequest, RichTextAdapter } 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'
|
||||
|
||||
type NestedRichTextFieldsArgs = {
|
||||
afterReadPromises: Map<string, Array<AfterReadPromise>>
|
||||
currentDepth?: number
|
||||
data: unknown
|
||||
depth: number
|
||||
fields: Field[]
|
||||
overrideAccess: boolean
|
||||
populationPromises: Map<string, Array<PopulationPromise>>
|
||||
promises: Promise<void>[]
|
||||
req: PayloadRequest
|
||||
showHiddenFields: boolean
|
||||
@@ -20,12 +20,12 @@ type NestedRichTextFieldsArgs = {
|
||||
}
|
||||
|
||||
export const recurseNestedFields = ({
|
||||
afterReadPromises,
|
||||
currentDepth = 0,
|
||||
data,
|
||||
depth,
|
||||
fields,
|
||||
overrideAccess = false,
|
||||
populationPromises,
|
||||
promises,
|
||||
req,
|
||||
showHiddenFields,
|
||||
@@ -118,12 +118,12 @@ export const recurseNestedFields = ({
|
||||
} else if (fieldHasSubFields(field) && !fieldIsArrayType(field)) {
|
||||
if (fieldAffectsData(field) && typeof data[field.name] === 'object') {
|
||||
recurseNestedFields({
|
||||
afterReadPromises,
|
||||
currentDepth,
|
||||
data: data[field.name],
|
||||
depth,
|
||||
fields: field.fields,
|
||||
overrideAccess,
|
||||
populationPromises,
|
||||
promises,
|
||||
req,
|
||||
showHiddenFields,
|
||||
@@ -131,12 +131,12 @@ export const recurseNestedFields = ({
|
||||
})
|
||||
} else {
|
||||
recurseNestedFields({
|
||||
afterReadPromises,
|
||||
currentDepth,
|
||||
data,
|
||||
depth,
|
||||
fields: field.fields,
|
||||
overrideAccess,
|
||||
populationPromises,
|
||||
promises,
|
||||
req,
|
||||
showHiddenFields,
|
||||
@@ -146,12 +146,12 @@ export const recurseNestedFields = ({
|
||||
} else if (field.type === 'tabs') {
|
||||
field.tabs.forEach((tab) => {
|
||||
recurseNestedFields({
|
||||
afterReadPromises,
|
||||
currentDepth,
|
||||
data,
|
||||
depth,
|
||||
fields: tab.fields,
|
||||
overrideAccess,
|
||||
populationPromises,
|
||||
promises,
|
||||
req,
|
||||
showHiddenFields,
|
||||
@@ -164,12 +164,12 @@ export const recurseNestedFields = ({
|
||||
const block = field.blocks.find(({ slug }) => slug === row?.blockType)
|
||||
if (block) {
|
||||
recurseNestedFields({
|
||||
afterReadPromises,
|
||||
currentDepth,
|
||||
data: data[field.name][i],
|
||||
depth,
|
||||
fields: block.fields,
|
||||
overrideAccess,
|
||||
populationPromises,
|
||||
promises,
|
||||
req,
|
||||
showHiddenFields,
|
||||
@@ -182,12 +182,12 @@ export const recurseNestedFields = ({
|
||||
if (field.type === 'array') {
|
||||
data[field.name].forEach((_, i) => {
|
||||
recurseNestedFields({
|
||||
afterReadPromises,
|
||||
currentDepth,
|
||||
data: data[field.name][i],
|
||||
depth,
|
||||
fields: field.fields,
|
||||
overrideAccess,
|
||||
populationPromises,
|
||||
promises,
|
||||
req,
|
||||
showHiddenFields,
|
||||
@@ -200,8 +200,8 @@ export const recurseNestedFields = ({
|
||||
if (field.type === 'richText') {
|
||||
const editor: RichTextAdapter = field?.editor
|
||||
|
||||
if (editor?.afterReadPromise) {
|
||||
const afterReadPromise = editor.afterReadPromise({
|
||||
if (editor?.populationPromise) {
|
||||
const afterReadPromise = editor.populationPromise({
|
||||
currentDepth,
|
||||
depth,
|
||||
field,
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import type { SerializedEditorState, SerializedLexicalNode } from 'lexical'
|
||||
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'
|
||||
|
||||
export type Args = Parameters<
|
||||
RichTextAdapter<SerializedEditorState, AdapterProps>['afterReadPromise']
|
||||
RichTextAdapter<SerializedEditorState, AdapterProps>['populationPromise']
|
||||
>[0] & {
|
||||
afterReadPromises: Map<string, Array<AfterReadPromise>>
|
||||
populationPromises: Map<string, Array<PopulationPromise>>
|
||||
}
|
||||
|
||||
type RecurseRichTextArgs = {
|
||||
afterReadPromises: Map<string, Array<AfterReadPromise>>
|
||||
children: SerializedLexicalNode[]
|
||||
currentDepth: number
|
||||
depth: number
|
||||
field: RichTextField<SerializedEditorState, AdapterProps>
|
||||
overrideAccess: boolean
|
||||
populationPromises: Map<string, Array<PopulationPromise>>
|
||||
promises: Promise<void>[]
|
||||
req: PayloadRequest
|
||||
showHiddenFields: boolean
|
||||
@@ -24,12 +24,12 @@ type RecurseRichTextArgs = {
|
||||
}
|
||||
|
||||
export const recurseRichText = ({
|
||||
afterReadPromises,
|
||||
children,
|
||||
currentDepth = 0,
|
||||
depth,
|
||||
field,
|
||||
overrideAccess = false,
|
||||
populationPromises,
|
||||
promises,
|
||||
req,
|
||||
showHiddenFields,
|
||||
@@ -41,16 +41,16 @@ export const recurseRichText = ({
|
||||
|
||||
if (Array.isArray(children)) {
|
||||
children.forEach((node) => {
|
||||
if (afterReadPromises?.has(node.type)) {
|
||||
for (const promise of afterReadPromises.get(node.type)) {
|
||||
if (populationPromises?.has(node.type)) {
|
||||
for (const promise of populationPromises.get(node.type)) {
|
||||
promises.push(
|
||||
...promise({
|
||||
afterReadPromises,
|
||||
currentDepth,
|
||||
depth,
|
||||
field,
|
||||
node: node,
|
||||
overrideAccess,
|
||||
populationPromises,
|
||||
req,
|
||||
showHiddenFields,
|
||||
siblingDoc,
|
||||
@@ -61,12 +61,12 @@ export const recurseRichText = ({
|
||||
|
||||
if ('children' in node && Array.isArray(node?.children) && node?.children?.length) {
|
||||
recurseRichText({
|
||||
afterReadPromises,
|
||||
children: node.children as SerializedLexicalNode[],
|
||||
currentDepth,
|
||||
depth,
|
||||
field,
|
||||
overrideAccess,
|
||||
populationPromises,
|
||||
promises,
|
||||
req,
|
||||
showHiddenFields,
|
||||
@@ -78,11 +78,11 @@ export const recurseRichText = ({
|
||||
}
|
||||
|
||||
export const richTextRelationshipPromise = async ({
|
||||
afterReadPromises,
|
||||
currentDepth,
|
||||
depth,
|
||||
field,
|
||||
overrideAccess,
|
||||
populationPromises,
|
||||
req,
|
||||
showHiddenFields,
|
||||
siblingDoc,
|
||||
@@ -90,12 +90,12 @@ export const richTextRelationshipPromise = async ({
|
||||
const promises = []
|
||||
|
||||
recurseRichText({
|
||||
afterReadPromises,
|
||||
children: (siblingDoc[field?.name] as SerializedEditorState)?.root?.children ?? [],
|
||||
currentDepth,
|
||||
depth,
|
||||
field,
|
||||
overrideAccess,
|
||||
populationPromises,
|
||||
promises,
|
||||
req,
|
||||
showHiddenFields,
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { AdapterArguments } from '../types'
|
||||
import { populate } from './populate'
|
||||
import { recurseNestedFields } from './recurseNestedFields'
|
||||
|
||||
export type Args = Parameters<RichTextAdapter<any[], AdapterArguments>['afterReadPromise']>[0]
|
||||
export type Args = Parameters<RichTextAdapter<any[], AdapterArguments>['populationPromise']>[0]
|
||||
|
||||
type RecurseRichTextArgs = {
|
||||
children: unknown[]
|
||||
|
||||
@@ -19,7 +19,7 @@ export function slateEditor(args: AdapterArguments): RichTextAdapter<any[], Adap
|
||||
Component: RichTextField,
|
||||
toMergeIntoProps: args,
|
||||
}),
|
||||
afterReadPromise({
|
||||
populationPromise({
|
||||
currentDepth,
|
||||
depth,
|
||||
field,
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { CollectionConfig } from '../../../../packages/payload/src/collecti
|
||||
|
||||
import {
|
||||
BlocksFeature,
|
||||
HTMLConverterFeature,
|
||||
LexicalPluginToLexicalFeature,
|
||||
LinkFeature,
|
||||
TreeviewFeature,
|
||||
@@ -34,45 +35,6 @@ export const LexicalFields: CollectionConfig = {
|
||||
type: 'text',
|
||||
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',
|
||||
type: 'richText',
|
||||
@@ -81,6 +43,7 @@ export const LexicalFields: CollectionConfig = {
|
||||
features: ({ defaultFeatures }) => [
|
||||
...defaultFeatures,
|
||||
TreeviewFeature(),
|
||||
HTMLConverterFeature(),
|
||||
LinkFeature({
|
||||
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(),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@@ -2,11 +2,13 @@ import type { CollectionConfig } from '../../../../packages/payload/src/collecti
|
||||
|
||||
import {
|
||||
BlocksFeature,
|
||||
HTMLConverterFeature,
|
||||
LinkFeature,
|
||||
TreeviewFeature,
|
||||
UploadFeature,
|
||||
lexicalEditor,
|
||||
} 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 { RelationshipBlock, SelectFieldBlock, TextBlock, UploadAndRichTextBlock } from './blocks'
|
||||
import { generateLexicalRichText } from './generateLexicalRichText'
|
||||
@@ -34,6 +36,7 @@ const RichTextFields: CollectionConfig = {
|
||||
features: ({ defaultFeatures }) => [
|
||||
...defaultFeatures,
|
||||
TreeviewFeature(),
|
||||
HTMLConverterFeature({}),
|
||||
LinkFeature({
|
||||
fields: [
|
||||
{
|
||||
@@ -68,6 +71,7 @@ const RichTextFields: CollectionConfig = {
|
||||
],
|
||||
}),
|
||||
},
|
||||
lexicalHTML('richTextLexicalCustomFields', { name: 'richTextLexicalCustomFields_htmll' }),
|
||||
{
|
||||
name: 'richTextLexical',
|
||||
type: 'richText',
|
||||
|
||||
Reference in New Issue
Block a user