chore(templates): migrate to new richtext component in website template (#9615)
In addition to requiring fewer files, it supports more nodes. If you currently initialize a website template and want to use features such as images or tables, they are not rendered. With this change that happens automatically. Credits to @AlessioGr for the [JSX serializer](https://github.com/payloadcms/payload/pull/8795). --------- Co-authored-by: Paul Popus <paul@nouance.io>
This commit is contained in:
6770
templates/website/pnpm-lock.yaml
generated
6770
templates/website/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -58,7 +58,7 @@ export default async function Post({ params: paramsPromise }: Args) {
|
||||
|
||||
<div className="flex flex-col items-center gap-4 pt-8">
|
||||
<div className="container">
|
||||
<RichText className="max-w-[48rem] mx-auto" content={post.content} enableGutter={false} />
|
||||
<RichText className="max-w-[48rem] mx-auto" data={post.content} enableGutter={false} />
|
||||
{post.relatedPosts && post.relatedPosts.length > 0 && (
|
||||
<RelatedPosts
|
||||
className="mt-12 max-w-[52rem] lg:grid lg:grid-cols-subgrid col-start-1 col-span-3 grid-rows-[2fr]"
|
||||
|
||||
@@ -22,43 +22,26 @@ import { default as default_1a7510af427896d367a49dbf838d2de6 } from '@/component
|
||||
import { default as default_8a7ab0eb7ab5c511aba12e68480bfe5e } from '@/components/BeforeLogin'
|
||||
|
||||
export const importMap = {
|
||||
'@payloadcms/richtext-lexical/rsc#RscEntryLexicalCell':
|
||||
RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e,
|
||||
'@payloadcms/richtext-lexical/rsc#RscEntryLexicalField':
|
||||
RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e,
|
||||
'@payloadcms/richtext-lexical/client#InlineToolbarFeatureClient':
|
||||
InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
'@payloadcms/richtext-lexical/client#FixedToolbarFeatureClient':
|
||||
FixedToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
'@payloadcms/richtext-lexical/client#HeadingFeatureClient':
|
||||
HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
'@payloadcms/richtext-lexical/client#ParagraphFeatureClient':
|
||||
ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
'@payloadcms/richtext-lexical/client#UnderlineFeatureClient':
|
||||
UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
'@payloadcms/richtext-lexical/client#BoldFeatureClient':
|
||||
BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
'@payloadcms/richtext-lexical/client#ItalicFeatureClient':
|
||||
ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
'@payloadcms/richtext-lexical/client#LinkFeatureClient':
|
||||
LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
'@payloadcms/plugin-seo/client#OverviewComponent':
|
||||
OverviewComponent_a8a977ebc872c5d5ea7ee689724c0860,
|
||||
'@payloadcms/plugin-seo/client#MetaTitleComponent':
|
||||
MetaTitleComponent_a8a977ebc872c5d5ea7ee689724c0860,
|
||||
'@payloadcms/plugin-seo/client#MetaImageComponent':
|
||||
MetaImageComponent_a8a977ebc872c5d5ea7ee689724c0860,
|
||||
'@payloadcms/plugin-seo/client#MetaDescriptionComponent':
|
||||
MetaDescriptionComponent_a8a977ebc872c5d5ea7ee689724c0860,
|
||||
'@payloadcms/plugin-seo/client#PreviewComponent':
|
||||
PreviewComponent_a8a977ebc872c5d5ea7ee689724c0860,
|
||||
'@/fields/slug/SlugComponent#SlugComponent': SlugComponent_92cc057d0a2abb4f6cf0307edf59f986,
|
||||
'@payloadcms/richtext-lexical/client#HorizontalRuleFeatureClient':
|
||||
HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
'@payloadcms/richtext-lexical/client#BlocksFeatureClient':
|
||||
BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
'@payloadcms/plugin-search/client#LinkToDoc': LinkToDoc_aead06e4cbf6b2620c5c51c9ab283634,
|
||||
'@payloadcms/plugin-search/client#ReindexButton': ReindexButton_aead06e4cbf6b2620c5c51c9ab283634,
|
||||
'@/components/BeforeDashboard#default': default_1a7510af427896d367a49dbf838d2de6,
|
||||
'@/components/BeforeLogin#default': default_8a7ab0eb7ab5c511aba12e68480bfe5e,
|
||||
"@payloadcms/richtext-lexical/rsc#RscEntryLexicalCell": RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e,
|
||||
"@payloadcms/richtext-lexical/rsc#RscEntryLexicalField": RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e,
|
||||
"@payloadcms/richtext-lexical/client#InlineToolbarFeatureClient": InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#FixedToolbarFeatureClient": FixedToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#HeadingFeatureClient": HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#ParagraphFeatureClient": ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#UnderlineFeatureClient": UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#BoldFeatureClient": BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#ItalicFeatureClient": ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#LinkFeatureClient": LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/plugin-seo/client#OverviewComponent": OverviewComponent_a8a977ebc872c5d5ea7ee689724c0860,
|
||||
"@payloadcms/plugin-seo/client#MetaTitleComponent": MetaTitleComponent_a8a977ebc872c5d5ea7ee689724c0860,
|
||||
"@payloadcms/plugin-seo/client#MetaImageComponent": MetaImageComponent_a8a977ebc872c5d5ea7ee689724c0860,
|
||||
"@payloadcms/plugin-seo/client#MetaDescriptionComponent": MetaDescriptionComponent_a8a977ebc872c5d5ea7ee689724c0860,
|
||||
"@payloadcms/plugin-seo/client#PreviewComponent": PreviewComponent_a8a977ebc872c5d5ea7ee689724c0860,
|
||||
"@/fields/slug/SlugComponent#SlugComponent": SlugComponent_92cc057d0a2abb4f6cf0307edf59f986,
|
||||
"@payloadcms/richtext-lexical/client#HorizontalRuleFeatureClient": HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#BlocksFeatureClient": BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/plugin-search/client#LinkToDoc": LinkToDoc_aead06e4cbf6b2620c5c51c9ab283634,
|
||||
"@payloadcms/plugin-search/client#ReindexButton": ReindexButton_aead06e4cbf6b2620c5c51c9ab283634,
|
||||
"@/components/BeforeDashboard#default": default_1a7510af427896d367a49dbf838d2de6,
|
||||
"@/components/BeforeLogin#default": default_8a7ab0eb7ab5c511aba12e68480bfe5e
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ export const ArchiveBlock: React.FC<
|
||||
<div className="my-16" id={`block-${id}`}>
|
||||
{introContent && (
|
||||
<div className="container mb-16">
|
||||
<RichText className="ml-0 max-w-[48rem]" content={introContent} enableGutter={false} />
|
||||
<RichText className="ml-0 max-w-[48rem]" data={introContent} enableGutter={false} />
|
||||
</div>
|
||||
)}
|
||||
<CollectionArchive posts={posts} />
|
||||
|
||||
@@ -19,7 +19,7 @@ export const BannerBlock: React.FC<Props> = ({ className, content, style }) => {
|
||||
'border-warning bg-warning/30': style === 'warning',
|
||||
})}
|
||||
>
|
||||
<RichText content={content} enableGutter={false} enableProse={false} />
|
||||
<RichText data={content} enableGutter={false} enableProse={false} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -10,7 +10,7 @@ export const CallToActionBlock: React.FC<CTABlockProps> = ({ links, richText })
|
||||
<div className="container">
|
||||
<div className="bg-card rounded border-border border p-4 flex flex-col gap-8 md:flex-row md:justify-between md:items-center">
|
||||
<div className="max-w-[48rem] flex items-center">
|
||||
{richText && <RichText className="mb-0" content={richText} enableGutter={false} />}
|
||||
{richText && <RichText className="mb-0" data={richText} enableGutter={false} />}
|
||||
</div>
|
||||
<div className="flex flex-col gap-8">
|
||||
{(links || []).map(({ link }, i) => {
|
||||
|
||||
@@ -31,7 +31,7 @@ export const ContentBlock: React.FC<ContentBlockProps> = (props) => {
|
||||
})}
|
||||
key={index}
|
||||
>
|
||||
{richText && <RichText content={richText} enableGutter={false} />}
|
||||
{richText && <RichText data={richText} enableGutter={false} />}
|
||||
|
||||
{enableLink && <CMSLink {...link} />}
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,7 @@ import React, { useCallback, useState } from 'react'
|
||||
import { useForm, FormProvider } from 'react-hook-form'
|
||||
import RichText from '@/components/RichText'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
|
||||
|
||||
import { buildInitialFormState } from './buildInitialFormState'
|
||||
import { fields } from './fields'
|
||||
@@ -26,9 +27,7 @@ export type FormBlockType = {
|
||||
blockType?: 'formBlock'
|
||||
enableIntro: boolean
|
||||
form: FormType
|
||||
introContent?: {
|
||||
[k: string]: unknown
|
||||
}[]
|
||||
introContent?: SerializedEditorState
|
||||
}
|
||||
|
||||
export const FormBlock: React.FC<
|
||||
@@ -128,12 +127,12 @@ export const FormBlock: React.FC<
|
||||
return (
|
||||
<div className="container lg:max-w-[48rem]">
|
||||
{enableIntro && introContent && !hasSubmitted && (
|
||||
<RichText className="mb-8 lg:mb-12" content={introContent} enableGutter={false} />
|
||||
<RichText className="mb-8 lg:mb-12" data={introContent} enableGutter={false} />
|
||||
)}
|
||||
<div className="p-4 lg:p-6 border border-border rounded-[0.8rem]">
|
||||
<FormProvider {...formMethods}>
|
||||
{!isLoading && hasSubmitted && confirmationType === 'message' && (
|
||||
<RichText content={confirmationMessage} />
|
||||
<RichText data={confirmationMessage} />
|
||||
)}
|
||||
{isLoading && !hasSubmitted && <p>Loading, please wait...</p>}
|
||||
{error && <div>{`${error.status || '500'}: ${error.message || ''}`}</div>}
|
||||
|
||||
@@ -2,11 +2,12 @@ import RichText from '@/components/RichText'
|
||||
import React from 'react'
|
||||
|
||||
import { Width } from '../Width'
|
||||
import { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
|
||||
|
||||
export const Message: React.FC = ({ message }: { message: Record<string, any> }) => {
|
||||
export const Message: React.FC = ({ message }: { message: SerializedEditorState }) => {
|
||||
return (
|
||||
<Width className="my-12" width="100">
|
||||
{message && <RichText content={message} />}
|
||||
{message && <RichText data={message} />}
|
||||
</Width>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ export const MediaBlock: React.FC<Props> = (props) => {
|
||||
captionClassName,
|
||||
)}
|
||||
>
|
||||
<RichText content={caption} enableGutter={false} />
|
||||
<RichText data={caption} enableGutter={false} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -17,7 +17,7 @@ export const RelatedPosts: React.FC<RelatedPostsProps> = (props) => {
|
||||
|
||||
return (
|
||||
<div className={clsx('container', className)}>
|
||||
{introContent && <RichText content={introContent} enableGutter={false} />}
|
||||
{introContent && <RichText data={introContent} enableGutter={false} />}
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 md:gap-8 items-stretch">
|
||||
{docs?.map((doc, index) => {
|
||||
|
||||
@@ -1,27 +1,56 @@
|
||||
import { cn } from '@/utilities/cn'
|
||||
import React from 'react'
|
||||
import { MediaBlock } from '@/blocks/MediaBlock/Component'
|
||||
import { DefaultNodeTypes, SerializedBlockNode } from '@payloadcms/richtext-lexical'
|
||||
import { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
|
||||
import {
|
||||
JSXConvertersFunction,
|
||||
RichText as RichTextWithoutBlocks,
|
||||
} from '@payloadcms/richtext-lexical/react'
|
||||
|
||||
import { serializeLexical } from './serialize'
|
||||
import { CodeBlock, CodeBlockProps } from '@/blocks/Code/Component'
|
||||
|
||||
import type {
|
||||
BannerBlock as BannerBlockProps,
|
||||
CallToActionBlock as CTABlockProps,
|
||||
MediaBlock as MediaBlockProps,
|
||||
} from '@/payload-types'
|
||||
import { BannerBlock } from '@/blocks/Banner/Component'
|
||||
import { CallToActionBlock } from '@/blocks/CallToAction/Component'
|
||||
import { cn } from '@/utilities/cn'
|
||||
|
||||
type NodeTypes =
|
||||
| DefaultNodeTypes
|
||||
| SerializedBlockNode<CTABlockProps | MediaBlockProps | BannerBlockProps | CodeBlockProps>
|
||||
|
||||
const jsxConverters: JSXConvertersFunction<NodeTypes> = ({ defaultConverters }) => ({
|
||||
...defaultConverters,
|
||||
blocks: {
|
||||
banner: ({ node }) => <BannerBlock className="col-start-2 mb-4" {...node.fields} />,
|
||||
mediaBlock: ({ node }) => (
|
||||
<MediaBlock
|
||||
className="col-start-1 col-span-3"
|
||||
imgClassName="m-0"
|
||||
{...node.fields}
|
||||
captionClassName="mx-auto max-w-[48rem]"
|
||||
enableGutter={false}
|
||||
disableInnerContainer={true}
|
||||
/>
|
||||
),
|
||||
code: ({ node }) => <CodeBlock className="col-start-2" {...node.fields} />,
|
||||
cta: ({ node }) => <CallToActionBlock {...node.fields} />,
|
||||
},
|
||||
})
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
content: Record<string, any>
|
||||
data: SerializedEditorState
|
||||
enableGutter?: boolean
|
||||
enableProse?: boolean
|
||||
}
|
||||
|
||||
const RichText: React.FC<Props> = ({
|
||||
className,
|
||||
content,
|
||||
enableGutter = true,
|
||||
enableProse = true,
|
||||
}) => {
|
||||
if (!content) {
|
||||
return null
|
||||
}
|
||||
} & React.HTMLAttributes<HTMLDivElement>
|
||||
|
||||
export default function RichText(props: Props) {
|
||||
const { className, enableProse = true, enableGutter = true, ...rest } = props
|
||||
return (
|
||||
<div
|
||||
<RichTextWithoutBlocks
|
||||
converters={jsxConverters}
|
||||
className={cn(
|
||||
{
|
||||
'container ': enableGutter,
|
||||
@@ -30,14 +59,7 @@ const RichText: React.FC<Props> = ({
|
||||
},
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{content &&
|
||||
!Array.isArray(content) &&
|
||||
typeof content === 'object' &&
|
||||
'root' in content &&
|
||||
serializeLexical({ nodes: content?.root?.children })}
|
||||
</div>
|
||||
{...rest}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default RichText
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
// @ts-nocheck
|
||||
//This copy-and-pasted from lexical here: https://github.com/facebook/lexical/blob/c2ceee223f46543d12c574e62155e619f9a18a5d/packages/lexical/src/LexicalConstants.ts
|
||||
|
||||
import type { ElementFormatType, TextFormatType } from '@payloadcms/richtext-lexical/lexical'
|
||||
import type {
|
||||
TextDetailType,
|
||||
TextModeType,
|
||||
} from '@payloadcms/richtext-lexical/lexical/nodes/LexicalTextNode'
|
||||
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
// DOM
|
||||
export const DOM_ELEMENT_TYPE = 1
|
||||
export const DOM_TEXT_TYPE = 3
|
||||
|
||||
// Reconciling
|
||||
export const NO_DIRTY_NODES = 0
|
||||
export const HAS_DIRTY_NODES = 1
|
||||
export const FULL_RECONCILE = 2
|
||||
|
||||
// Text node modes
|
||||
export const IS_NORMAL = 0
|
||||
export const IS_TOKEN = 1
|
||||
export const IS_SEGMENTED = 2
|
||||
// IS_INERT = 3
|
||||
|
||||
// Text node formatting
|
||||
export const IS_BOLD = 1
|
||||
export const IS_ITALIC = 1 << 1
|
||||
export const IS_STRIKETHROUGH = 1 << 2
|
||||
export const IS_UNDERLINE = 1 << 3
|
||||
export const IS_CODE = 1 << 4
|
||||
export const IS_SUBSCRIPT = 1 << 5
|
||||
export const IS_SUPERSCRIPT = 1 << 6
|
||||
export const IS_HIGHLIGHT = 1 << 7
|
||||
|
||||
export const IS_ALL_FORMATTING =
|
||||
IS_BOLD |
|
||||
IS_ITALIC |
|
||||
IS_STRIKETHROUGH |
|
||||
IS_UNDERLINE |
|
||||
IS_CODE |
|
||||
IS_SUBSCRIPT |
|
||||
IS_SUPERSCRIPT |
|
||||
IS_HIGHLIGHT
|
||||
|
||||
// Text node details
|
||||
export const IS_DIRECTIONLESS = 1
|
||||
export const IS_UNMERGEABLE = 1 << 1
|
||||
|
||||
// Element node formatting
|
||||
export const IS_ALIGN_LEFT = 1
|
||||
export const IS_ALIGN_CENTER = 2
|
||||
export const IS_ALIGN_RIGHT = 3
|
||||
export const IS_ALIGN_JUSTIFY = 4
|
||||
export const IS_ALIGN_START = 5
|
||||
export const IS_ALIGN_END = 6
|
||||
|
||||
// Reconciliation
|
||||
export const NON_BREAKING_SPACE = '\u00A0'
|
||||
const ZERO_WIDTH_SPACE = '\u200b'
|
||||
|
||||
export const DOUBLE_LINE_BREAK = '\n\n'
|
||||
|
||||
// For FF, we need to use a non-breaking space, or it gets composition
|
||||
// in a stuck state.
|
||||
|
||||
const RTL = '\u0591-\u07FF\uFB1D-\uFDFD\uFE70-\uFEFC'
|
||||
const LTR =
|
||||
'A-Za-z\u00C0-\u00D6\u00D8-\u00F6' +
|
||||
'\u00F8-\u02B8\u0300-\u0590\u0800-\u1FFF\u200E\u2C00-\uFB1C' +
|
||||
'\uFE00-\uFE6F\uFEFD-\uFFFF'
|
||||
|
||||
export const RTL_REGEX = new RegExp('^[^' + LTR + ']*[' + RTL + ']')
|
||||
|
||||
export const LTR_REGEX = new RegExp('^[^' + RTL + ']*[' + LTR + ']')
|
||||
|
||||
export const TEXT_TYPE_TO_FORMAT: Record<TextFormatType | string, number> = {
|
||||
bold: IS_BOLD,
|
||||
code: IS_CODE,
|
||||
highlight: IS_HIGHLIGHT,
|
||||
italic: IS_ITALIC,
|
||||
strikethrough: IS_STRIKETHROUGH,
|
||||
subscript: IS_SUBSCRIPT,
|
||||
superscript: IS_SUPERSCRIPT,
|
||||
underline: IS_UNDERLINE,
|
||||
}
|
||||
|
||||
export const DETAIL_TYPE_TO_DETAIL: Record<TextDetailType | string, number> = {
|
||||
directionless: IS_DIRECTIONLESS,
|
||||
unmergeable: IS_UNMERGEABLE,
|
||||
}
|
||||
|
||||
export const ELEMENT_TYPE_TO_FORMAT: Record<Exclude<ElementFormatType, ''>, number> = {
|
||||
center: IS_ALIGN_CENTER,
|
||||
end: IS_ALIGN_END,
|
||||
justify: IS_ALIGN_JUSTIFY,
|
||||
left: IS_ALIGN_LEFT,
|
||||
right: IS_ALIGN_RIGHT,
|
||||
start: IS_ALIGN_START,
|
||||
}
|
||||
|
||||
export const ELEMENT_FORMAT_TO_TYPE: Record<number, ElementFormatType> = {
|
||||
[IS_ALIGN_CENTER]: 'center',
|
||||
[IS_ALIGN_END]: 'end',
|
||||
[IS_ALIGN_JUSTIFY]: 'justify',
|
||||
[IS_ALIGN_LEFT]: 'left',
|
||||
[IS_ALIGN_RIGHT]: 'right',
|
||||
[IS_ALIGN_START]: 'start',
|
||||
}
|
||||
|
||||
export const TEXT_MODE_TO_TYPE: Record<TextModeType, 0 | 1 | 2> = {
|
||||
normal: IS_NORMAL,
|
||||
segmented: IS_SEGMENTED,
|
||||
token: IS_TOKEN,
|
||||
}
|
||||
|
||||
export const TEXT_TYPE_TO_MODE: Record<number, TextModeType> = {
|
||||
[IS_NORMAL]: 'normal',
|
||||
[IS_SEGMENTED]: 'segmented',
|
||||
[IS_TOKEN]: 'token',
|
||||
}
|
||||
@@ -1,209 +0,0 @@
|
||||
import { BannerBlock } from '@/blocks/Banner/Component'
|
||||
import { CallToActionBlock } from '@/blocks/CallToAction/Component'
|
||||
import { CodeBlock, CodeBlockProps } from '@/blocks/Code/Component'
|
||||
import { MediaBlock } from '@/blocks/MediaBlock/Component'
|
||||
import React, { Fragment, JSX } from 'react'
|
||||
import { CMSLink } from '@/components/Link'
|
||||
import { DefaultNodeTypes, SerializedBlockNode } from '@payloadcms/richtext-lexical'
|
||||
import type { BannerBlock as BannerBlockProps } from '@/payload-types'
|
||||
|
||||
import {
|
||||
IS_BOLD,
|
||||
IS_CODE,
|
||||
IS_ITALIC,
|
||||
IS_STRIKETHROUGH,
|
||||
IS_SUBSCRIPT,
|
||||
IS_SUPERSCRIPT,
|
||||
IS_UNDERLINE,
|
||||
} from './nodeFormat'
|
||||
import type {
|
||||
CallToActionBlock as CTABlockProps,
|
||||
MediaBlock as MediaBlockProps,
|
||||
} from '@/payload-types'
|
||||
|
||||
export type NodeTypes =
|
||||
| DefaultNodeTypes
|
||||
| SerializedBlockNode<CTABlockProps | MediaBlockProps | BannerBlockProps | CodeBlockProps>
|
||||
|
||||
type Props = {
|
||||
nodes: NodeTypes[]
|
||||
}
|
||||
|
||||
export function serializeLexical({ nodes }: Props): JSX.Element {
|
||||
return (
|
||||
<Fragment>
|
||||
{nodes?.map((node, index): JSX.Element | null => {
|
||||
if (node == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (node.type === 'text') {
|
||||
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
|
||||
}
|
||||
|
||||
// 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: NodeTypes): JSX.Element | null => {
|
||||
if (node.children == null) {
|
||||
return null
|
||||
} else {
|
||||
if (node?.type === 'list' && node?.listType === 'check') {
|
||||
for (const item of node.children) {
|
||||
if ('checked' in item) {
|
||||
if (!item?.checked) {
|
||||
item.checked = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return serializeLexical({ nodes: node.children as NodeTypes[] })
|
||||
}
|
||||
}
|
||||
|
||||
const serializedChildren = 'children' in node ? serializedChildrenFn(node) : ''
|
||||
|
||||
if (node.type === 'block') {
|
||||
const block = node.fields
|
||||
|
||||
const blockType = block?.blockType
|
||||
|
||||
if (!block || !blockType) {
|
||||
return null
|
||||
}
|
||||
|
||||
switch (blockType) {
|
||||
case 'cta':
|
||||
return <CallToActionBlock key={index} {...block} />
|
||||
case 'mediaBlock':
|
||||
return (
|
||||
<MediaBlock
|
||||
className="col-start-1 col-span-3"
|
||||
imgClassName="m-0"
|
||||
key={index}
|
||||
{...block}
|
||||
captionClassName="mx-auto max-w-[48rem]"
|
||||
enableGutter={false}
|
||||
disableInnerContainer={true}
|
||||
/>
|
||||
)
|
||||
case 'banner':
|
||||
return <BannerBlock className="col-start-2 mb-4" key={index} {...block} />
|
||||
case 'code':
|
||||
return <CodeBlock className="col-start-2" key={index} {...block} />
|
||||
default:
|
||||
return null
|
||||
}
|
||||
} else {
|
||||
switch (node.type) {
|
||||
case 'linebreak': {
|
||||
return <br className="col-start-2" key={index} />
|
||||
}
|
||||
case 'paragraph': {
|
||||
return (
|
||||
<p className="col-start-2" key={index}>
|
||||
{serializedChildren}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
case 'heading': {
|
||||
const Tag = node?.tag
|
||||
return (
|
||||
<Tag className="col-start-2" key={index}>
|
||||
{serializedChildren}
|
||||
</Tag>
|
||||
)
|
||||
}
|
||||
case 'list': {
|
||||
const Tag = node?.tag
|
||||
return (
|
||||
<Tag className="list col-start-2" key={index}>
|
||||
{serializedChildren}
|
||||
</Tag>
|
||||
)
|
||||
}
|
||||
case 'listitem': {
|
||||
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 className="col-start-2" key={index}>
|
||||
{serializedChildren}
|
||||
</blockquote>
|
||||
)
|
||||
}
|
||||
case 'link': {
|
||||
const fields = 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>
|
||||
)
|
||||
}
|
||||
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
})}
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
@@ -22,7 +22,7 @@ export const HighImpactHero: React.FC<Page['hero']> = ({ links, media, richText
|
||||
>
|
||||
<div className="container mb-8 z-10 relative flex items-center justify-center">
|
||||
<div className="max-w-[36.5rem] text-center">
|
||||
{richText && <RichText className="mb-6" content={richText} enableGutter={false} />}
|
||||
{richText && <RichText className="mb-6" data={richText} enableGutter={false} />}
|
||||
{Array.isArray(links) && links.length > 0 && (
|
||||
<ul className="flex justify-center gap-4">
|
||||
{links.map(({ link }, i) => {
|
||||
|
||||
@@ -18,7 +18,7 @@ export const LowImpactHero: React.FC<LowImpactHeroType> = ({ children, richText
|
||||
return (
|
||||
<div className="container mt-16">
|
||||
<div className="max-w-[48rem]">
|
||||
{children || (richText && <RichText content={richText} enableGutter={false} />)}
|
||||
{children || (richText && <RichText data={richText} enableGutter={false} />)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -10,7 +10,7 @@ export const MediumImpactHero: React.FC<Page['hero']> = ({ links, media, richTex
|
||||
return (
|
||||
<div className="">
|
||||
<div className="container mb-8">
|
||||
{richText && <RichText className="mb-6" content={richText} enableGutter={false} />}
|
||||
{richText && <RichText className="mb-6" data={richText} enableGutter={false} />}
|
||||
|
||||
{Array.isArray(links) && links.length > 0 && (
|
||||
<ul className="flex gap-4">
|
||||
@@ -36,7 +36,7 @@ export const MediumImpactHero: React.FC<Page['hero']> = ({ links, media, richTex
|
||||
/>
|
||||
{media?.caption && (
|
||||
<div className="mt-3">
|
||||
<RichText content={media.caption} enableGutter={false} />
|
||||
<RichText data={media.caption} enableGutter={false} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user