208 lines
6.3 KiB
TypeScript
208 lines
6.3 KiB
TypeScript
import type { LinkFields, SerializedLinkNode } from '@payloadcms/richtext-lexical'
|
|
import type {
|
|
SerializedElementNode,
|
|
SerializedLexicalNode,
|
|
SerializedTextNode,
|
|
} from '@payloadcms/richtext-lexical/lexical'
|
|
import type {
|
|
SerializedListItemNode,
|
|
SerializedListNode,
|
|
} from '@payloadcms/richtext-lexical/lexical/list'
|
|
import type { SerializedHeadingNode } from '@payloadcms/richtext-lexical/lexical/rich-text'
|
|
|
|
import React, { Fragment } from 'react'
|
|
|
|
import { CMSLink } from '../Link'
|
|
import {
|
|
IS_BOLD,
|
|
IS_CODE,
|
|
IS_ITALIC,
|
|
IS_STRIKETHROUGH,
|
|
IS_SUBSCRIPT,
|
|
IS_SUPERSCRIPT,
|
|
IS_UNDERLINE,
|
|
} from './nodeFormat'
|
|
|
|
interface Props {
|
|
nodes: SerializedLexicalNode[]
|
|
}
|
|
|
|
export function serializeLexical({ nodes }: Props): 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>
|
|
}
|
|
|
|
return text
|
|
}
|
|
|
|
if (_node == null) {
|
|
return null
|
|
}
|
|
|
|
// 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 serializeLexical({ nodes: node.children })
|
|
} else {
|
|
return serializeLexical({ nodes: node.children })
|
|
}
|
|
}
|
|
}
|
|
|
|
const serializedChildren =
|
|
'children' in _node ? serializedChildrenFn(_node as SerializedElementNode) : ''
|
|
|
|
switch (_node.type) {
|
|
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 'linebreak': {
|
|
return <br key={index} />
|
|
}
|
|
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 '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 'paragraph': {
|
|
return <p key={index}>{serializedChildren}</p>
|
|
}
|
|
case 'quote': {
|
|
return <blockquote key={index}>{serializedChildren}</blockquote>
|
|
}
|
|
|
|
/* case 'block': {
|
|
// todo: fix types
|
|
|
|
//@ts-expect-error
|
|
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>
|
|
)
|
|
}
|