Compare commits

...

1 Commits

Author SHA1 Message Date
Germán Jabloñski
c3cb946ad7 serialize-jsx-html 2024-11-06 18:54:18 -03:00
3 changed files with 245 additions and 80 deletions

View File

@@ -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>
)
}

View File

@@ -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

View File

@@ -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',