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> = {
|
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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
.object()
|
||||||
|
.required()
|
||||||
|
.keys({
|
||||||
CellComponent: component.required(),
|
CellComponent: component.required(),
|
||||||
FieldComponent: component.required(),
|
FieldComponent: component.required(),
|
||||||
afterReadPromise: joi.func().required(),
|
afterReadPromise: joi.func().optional(),
|
||||||
|
populationPromise: joi.func().optional(),
|
||||||
validate: joi.func().required(),
|
validate: joi.func().required(),
|
||||||
}),
|
})
|
||||||
|
.unknown(),
|
||||||
email: joi.object(),
|
email: joi.object(),
|
||||||
endpoints: endpointsSchema,
|
endpoints: endpointsSchema,
|
||||||
express: joi.object().keys({
|
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(),
|
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
|
||||||
|
.object()
|
||||||
|
.keys({
|
||||||
CellComponent: componentSchema.required(),
|
CellComponent: componentSchema.required(),
|
||||||
FieldComponent: componentSchema.required(),
|
FieldComponent: componentSchema.required(),
|
||||||
afterReadPromise: joi.func().required(),
|
afterReadPromise: joi.func().optional(),
|
||||||
|
populationPromise: joi.func().optional(),
|
||||||
validate: joi.func().required(),
|
validate: joi.func().required(),
|
||||||
}),
|
})
|
||||||
|
.unknown(),
|
||||||
type: joi.string().valid('richText').required(),
|
type: joi.string().valid('richText').required(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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(),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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)],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -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()],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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 { 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(),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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')
|
||||||
? []
|
? []
|
||||||
|
|||||||
@@ -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(),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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 '..'
|
import { convertSlateNodesToLexical } from '..'
|
||||||
|
|
||||||
export const HeadingConverter: SlateNodeConverter = {
|
export const SlateHeadingConverter: SlateNodeConverter = {
|
||||||
converter({ converters, slateNode }) {
|
converter({ converters, slateNode }) {
|
||||||
return {
|
return {
|
||||||
children: convertSlateNodesToLexical({
|
children: convertSlateNodesToLexical({
|
||||||
|
|||||||
@@ -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 => {
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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: '',
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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,
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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 }) {
|
||||||
|
|||||||
@@ -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[]
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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: ({ 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({
|
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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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[]
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
Reference in New Issue
Block a user