Compare commits
1 Commits
main
...
serialize-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c3cb946ad7 |
@@ -16,9 +16,7 @@ const RichText: React.FC<{
|
||||
|
||||
return (
|
||||
<div className={[classes.richText, className].filter(Boolean).join(' ')}>
|
||||
{serializer === 'slate'
|
||||
? serializeSlate(content, renderUploadFilenameOnly)
|
||||
: serializeLexical(content, renderUploadFilenameOnly)}
|
||||
{serializeLexical(content, renderUploadFilenameOnly)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,92 +1,227 @@
|
||||
import type { SerializedEditorState } from 'lexical'
|
||||
import type {
|
||||
LinkFields,
|
||||
SerializedHeadingNode,
|
||||
SerializedLinkNode,
|
||||
SerializedListItemNode,
|
||||
SerializedListNode,
|
||||
SerializedUploadNode,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
import type {
|
||||
SerializedEditorState,
|
||||
SerializedElementNode,
|
||||
SerializedLexicalNode,
|
||||
SerializedTextNode,
|
||||
} from 'lexical'
|
||||
import type { JSX } from 'react'
|
||||
|
||||
import React from 'react'
|
||||
import {
|
||||
IS_BOLD,
|
||||
IS_CODE,
|
||||
IS_ITALIC,
|
||||
IS_STRIKETHROUGH,
|
||||
IS_SUBSCRIPT,
|
||||
IS_SUPERSCRIPT,
|
||||
IS_UNDERLINE,
|
||||
} from 'lexical'
|
||||
import React, { Fragment } from 'react'
|
||||
import ReactDOMServer from 'react-dom/server'
|
||||
|
||||
import { CMSLink } from '../Link/index.js'
|
||||
import { Media } from '../Media/index.js'
|
||||
|
||||
const serializer = (
|
||||
content?: SerializedEditorState['root']['children'],
|
||||
renderUploadFilenameOnly?: boolean,
|
||||
): React.ReactNode | React.ReactNode[] =>
|
||||
content?.map((node, i) => {
|
||||
switch (node.type) {
|
||||
case 'h1':
|
||||
return <h1 key={i}>{serializeLexical(node?.children, renderUploadFilenameOnly)}</h1>
|
||||
function serializer(nodes: SerializedLexicalNode[]): JSX.Element {
|
||||
return (
|
||||
<Fragment>
|
||||
{nodes?.map((_node, index): JSX.Element | null => {
|
||||
if (_node.type === 'text') {
|
||||
const node = _node as SerializedTextNode
|
||||
let text = <React.Fragment key={index}>{node.text}</React.Fragment>
|
||||
if (node.format & IS_BOLD) {
|
||||
text = <strong key={index}>{text}</strong>
|
||||
}
|
||||
if (node.format & IS_ITALIC) {
|
||||
text = <em key={index}>{text}</em>
|
||||
}
|
||||
if (node.format & IS_STRIKETHROUGH) {
|
||||
text = (
|
||||
<span key={index} style={{ textDecoration: 'line-through' }}>
|
||||
{text}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
if (node.format & IS_UNDERLINE) {
|
||||
text = (
|
||||
<span key={index} style={{ textDecoration: 'underline' }}>
|
||||
{text}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
if (node.format & IS_CODE) {
|
||||
text = <code key={index}>{node.text}</code>
|
||||
}
|
||||
if (node.format & IS_SUBSCRIPT) {
|
||||
text = <sub key={index}>{text}</sub>
|
||||
}
|
||||
if (node.format & IS_SUPERSCRIPT) {
|
||||
text = <sup key={index}>{text}</sup>
|
||||
}
|
||||
|
||||
case 'h2':
|
||||
return <h2 key={i}>{serializeLexical(node?.children, renderUploadFilenameOnly)}</h2>
|
||||
|
||||
case 'h3':
|
||||
return <h3 key={i}>{serializeLexical(node?.children, renderUploadFilenameOnly)}</h3>
|
||||
|
||||
case 'h4':
|
||||
return <h4 key={i}>{serializeLexical(node?.children, renderUploadFilenameOnly)}</h4>
|
||||
|
||||
case 'h5':
|
||||
return <h5 key={i}>{serializeLexical(node?.children, renderUploadFilenameOnly)}</h5>
|
||||
|
||||
case 'h6':
|
||||
return <h6 key={i}>{serializeLexical(node?.children, renderUploadFilenameOnly)}</h6>
|
||||
|
||||
case 'quote':
|
||||
return (
|
||||
<blockquote key={i}>
|
||||
{serializeLexical(node?.children, renderUploadFilenameOnly)}
|
||||
</blockquote>
|
||||
)
|
||||
|
||||
case 'ul':
|
||||
return <ul key={i}>{serializeLexical(node?.children, renderUploadFilenameOnly)}</ul>
|
||||
|
||||
case 'ol':
|
||||
return <ol key={i}>{serializeLexical(node.children, renderUploadFilenameOnly)}</ol>
|
||||
|
||||
case 'li':
|
||||
return <li key={i}>{serializeLexical(node.children, renderUploadFilenameOnly)}</li>
|
||||
|
||||
case 'relationship':
|
||||
return (
|
||||
<span key={i}>
|
||||
{node.value && typeof node.value === 'object'
|
||||
? node.value.title || node.value.id
|
||||
: node.value}
|
||||
</span>
|
||||
)
|
||||
|
||||
case 'link':
|
||||
return (
|
||||
<CMSLink
|
||||
key={i}
|
||||
newTab={Boolean(node?.newTab)}
|
||||
reference={node.doc as any}
|
||||
type={node.linkType === 'internal' ? 'reference' : 'custom'}
|
||||
url={node.url}
|
||||
>
|
||||
{serializer(node?.children, renderUploadFilenameOnly)}
|
||||
</CMSLink>
|
||||
)
|
||||
|
||||
case 'upload':
|
||||
if (renderUploadFilenameOnly) {
|
||||
return <span key={i}>{node.value.filename}</span>
|
||||
return text
|
||||
}
|
||||
|
||||
return <Media key={i} resource={node?.value} />
|
||||
if (_node == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
case 'paragraph':
|
||||
return <p key={i}>{serializer(node?.children, renderUploadFilenameOnly)}</p>
|
||||
// NOTE: Hacky fix for
|
||||
// https://github.com/facebook/lexical/blob/d10c4e6e55261b2fdd7d1845aed46151d0f06a8c/packages/lexical-list/src/LexicalListItemNode.ts#L133
|
||||
// which does not return checked: false (only true - i.e. there is no prop for false)
|
||||
const serializedChildrenFn = (node: SerializedElementNode): JSX.Element | null => {
|
||||
if (node.children == null) {
|
||||
return null
|
||||
} else {
|
||||
if (node?.type === 'list' && (node as SerializedListNode)?.listType === 'check') {
|
||||
for (const item of node.children) {
|
||||
if ('checked' in item) {
|
||||
if (!item?.checked) {
|
||||
item.checked = false
|
||||
}
|
||||
}
|
||||
}
|
||||
return serializer(node.children)
|
||||
} else {
|
||||
return serializer(node.children)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case 'text':
|
||||
return <span key={i}>{node.text}</span>
|
||||
}
|
||||
})
|
||||
const serializedChildren =
|
||||
'children' in _node ? serializedChildrenFn(_node as SerializedElementNode) : ''
|
||||
|
||||
switch (_node.type) {
|
||||
case 'linebreak': {
|
||||
return <br key={index} />
|
||||
}
|
||||
case 'paragraph': {
|
||||
return <p key={index}>{serializedChildren}</p>
|
||||
}
|
||||
case 'heading': {
|
||||
const node = _node as SerializedHeadingNode
|
||||
|
||||
type Heading = Extract<keyof JSX.IntrinsicElements, 'h1' | 'h2' | 'h3' | 'h4' | 'h5'>
|
||||
const Tag = node?.tag as Heading
|
||||
return <Tag key={index}>{serializedChildren}</Tag>
|
||||
}
|
||||
case 'list': {
|
||||
const node = _node as SerializedListNode
|
||||
|
||||
type List = Extract<keyof JSX.IntrinsicElements, 'ol' | 'ul'>
|
||||
const Tag = node?.tag as List
|
||||
return (
|
||||
<Tag className="list" key={index}>
|
||||
{serializedChildren}
|
||||
</Tag>
|
||||
)
|
||||
}
|
||||
case 'listitem': {
|
||||
const node = _node as SerializedListItemNode
|
||||
|
||||
if (node?.checked != null) {
|
||||
return (
|
||||
<li
|
||||
aria-checked={node.checked ? 'true' : 'false'}
|
||||
className={` ${node.checked ? '' : ''}`}
|
||||
key={index}
|
||||
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-to-interactive-role
|
||||
role="checkbox"
|
||||
tabIndex={-1}
|
||||
value={node?.value}
|
||||
>
|
||||
{serializedChildren}
|
||||
</li>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<li key={index} value={node?.value}>
|
||||
{serializedChildren}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
}
|
||||
case 'quote': {
|
||||
return <blockquote key={index}>{serializedChildren}</blockquote>
|
||||
}
|
||||
case 'link': {
|
||||
const node = _node as SerializedLinkNode
|
||||
|
||||
const fields: LinkFields = node.fields
|
||||
|
||||
return (
|
||||
<CMSLink
|
||||
key={index}
|
||||
newTab={Boolean(fields?.newTab)}
|
||||
reference={fields.doc as any}
|
||||
type={fields.linkType === 'internal' ? 'reference' : 'custom'}
|
||||
url={fields.url}
|
||||
>
|
||||
{serializedChildren}
|
||||
</CMSLink>
|
||||
)
|
||||
}
|
||||
case 'upload': {
|
||||
const node = _node as SerializedUploadNode
|
||||
return <Media key={index} resource={node?.value} />
|
||||
}
|
||||
|
||||
/* case 'block': {
|
||||
// todo: fix types
|
||||
|
||||
const block = _node.fields
|
||||
|
||||
//@ts-expect-error
|
||||
const blockType = _node.fields?.blockType
|
||||
|
||||
if (!block || !blockType) {
|
||||
return null
|
||||
}
|
||||
|
||||
switch (blockType) {
|
||||
case 'content':
|
||||
return <ContentBlock {...block} />
|
||||
case 'cta':
|
||||
return <CallToActionBlock {...block} />
|
||||
case 'archive':
|
||||
return <ArchiveBlock {...block} />
|
||||
case 'mediaBlock':
|
||||
return <MediaBlock {...block} />
|
||||
case 'banner':
|
||||
return <BannerBlock {...block} />
|
||||
case 'code':
|
||||
return <CodeBlock {...block} />
|
||||
default:
|
||||
return null
|
||||
}
|
||||
} */
|
||||
|
||||
default:
|
||||
return null
|
||||
}
|
||||
})}
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
const serializeLexical = (
|
||||
content?: SerializedEditorState,
|
||||
renderUploadFilenameOnly?: boolean,
|
||||
): React.ReactNode | React.ReactNode[] => {
|
||||
return serializer(content?.root?.children, renderUploadFilenameOnly)
|
||||
const result = serializer(content?.root?.children)
|
||||
const resultString = ReactDOMServer.renderToString(<>{result}</>)
|
||||
if (!resultString) {
|
||||
return null
|
||||
}
|
||||
console.log('Serialized lexical:', resultString)
|
||||
return result
|
||||
}
|
||||
|
||||
export default serializeLexical
|
||||
|
||||
@@ -1,6 +1,31 @@
|
||||
import type { Field } from 'payload'
|
||||
import type { Field, FieldHook } from 'payload'
|
||||
|
||||
import { slateEditor } from '@payloadcms/richtext-slate'
|
||||
import {
|
||||
consolidateHTMLConverters,
|
||||
convertLexicalToHTML,
|
||||
defaultEditorConfig,
|
||||
defaultEditorFeatures,
|
||||
HTMLConverterFeature,
|
||||
lexicalEditor,
|
||||
sanitizeServerEditorConfig,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
import { SlateToLexicalFeature } from '@payloadcms/richtext-lexical/migrate'
|
||||
|
||||
const hook: FieldHook = async ({ req, siblingData }) => {
|
||||
const editorConfig = defaultEditorConfig
|
||||
|
||||
editorConfig.features = [...defaultEditorFeatures, HTMLConverterFeature({})]
|
||||
|
||||
const sanitizedEditorConfig = await sanitizeServerEditorConfig(editorConfig, req.payload.config)
|
||||
|
||||
const html = await convertLexicalToHTML({
|
||||
converters: consolidateHTMLConverters({ editorConfig: sanitizedEditorConfig }),
|
||||
data: siblingData.lexicalSimple,
|
||||
req,
|
||||
})
|
||||
console.log('HTML:', html)
|
||||
// return html
|
||||
}
|
||||
|
||||
export const hero: Field = {
|
||||
name: 'hero',
|
||||
@@ -32,7 +57,14 @@ export const hero: Field = {
|
||||
name: 'richText',
|
||||
label: 'Rich Text',
|
||||
type: 'richText',
|
||||
editor: slateEditor({}),
|
||||
editor: lexicalEditor({
|
||||
features: ({ defaultFeatures }) => [...defaultFeatures, SlateToLexicalFeature({})],
|
||||
}),
|
||||
hooks: {
|
||||
afterRead: [hook],
|
||||
beforeValidate: [hook],
|
||||
afterChange: [hook],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'media',
|
||||
|
||||
Reference in New Issue
Block a user