feat(richtext-lexical): new HTML converter (#11370)
Deprecates the old HTML converter and introduces a new one that functions similarly to our Lexical => JSX converter.
The old converter had the following limitations:
- It imported the entire lexical bundle
- It was challenging to implement. The sanitized lexical editor config had to be passed in as an argument, which was difficult to obtain
- It only worked on the server
This new HTML converter is lightweight, user-friendly, and works on both server and client. Instead of retrieving HTML converters from the editor config, they can be explicitly provided to the converter function.
By default, the converter expects populated data to function properly. If you need to use unpopulated data (e.g., when running it from a hook), you also have the option to use the async HTML converter, exported from `@payloadcms/richtext-lexical/html-async`, and provide a `populate` function - this function will then be used to dynamically populate nodes during the conversion process.
## Example 1 - generating HTML in your frontend
```tsx
'use client'
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
import { convertLexicalToHTML } from '@payloadcms/richtext-lexical/html'
import React from 'react'
export const MyComponent = ({ data }: { data: SerializedEditorState }) => {
const html = convertLexicalToHTML({ data })
return <div dangerouslySetInnerHTML={{ __html: html }} />
}
```
## Example - converting Lexical Blocks
```tsx
'use client'
import type { MyInlineBlock, MyTextBlock } from '@/payload-types'
import type {
DefaultNodeTypes,
SerializedBlockNode,
SerializedInlineBlockNode,
} from '@payloadcms/richtext-lexical'
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
import {
convertLexicalToHTML,
type HTMLConvertersFunction,
} from '@payloadcms/richtext-lexical/html'
import React from 'react'
type NodeTypes =
| DefaultNodeTypes
| SerializedBlockNode<MyTextBlock>
| SerializedInlineBlockNode<MyInlineBlock>
const htmlConverters: HTMLConvertersFunction<NodeTypes> = ({ defaultConverters }) => ({
...defaultConverters,
blocks: {
// Each key should match your block's slug
myTextBlock: ({ node, providedCSSString }) =>
`<div style="background-color: red;${providedCSSString}">${node.fields.text}</div>`,
},
inlineBlocks: {
// Each key should match your inline block's slug
myInlineBlock: ({ node, providedStyleTag }) =>
`<span${providedStyleTag}>${node.fields.text}</span$>`,
},
})
export const MyComponent = ({ data }: { data: SerializedEditorState }) => {
const html = convertLexicalToHTML({
converters: htmlConverters,
data,
})
return <div dangerouslySetInnerHTML={{ __html: html }} />
}
```
## Example 3 - outputting HTML from the collection
```ts
import type { HTMLConvertersFunction } from '@payloadcms/richtext-lexical/html'
import type { MyTextBlock } from '@/payload-types.js'
import type { CollectionConfig } from 'payload'
import {
BlocksFeature,
type DefaultNodeTypes,
lexicalEditor,
lexicalHTMLField,
type SerializedBlockNode,
} from '@payloadcms/richtext-lexical'
const Pages: CollectionConfig = {
slug: 'pages',
fields: [
{
name: 'nameOfYourRichTextField',
type: 'richText',
editor: lexicalEditor(),
},
lexicalHTMLField({
htmlFieldName: 'nameOfYourRichTextField_html',
lexicalFieldName: 'nameOfYourRichTextField',
}),
{
name: 'customRichText',
type: 'richText',
editor: lexicalEditor({
features: ({ defaultFeatures }) => [
...defaultFeatures,
BlocksFeature({
blocks: [
{
interfaceName: 'MyTextBlock',
slug: 'myTextBlock',
fields: [
{
name: 'text',
type: 'text',
},
],
},
],
}),
],
}),
},
lexicalHTMLField({
htmlFieldName: 'customRichText_html',
lexicalFieldName: 'customRichText',
// can pass in additional converters or override default ones
converters: (({ defaultConverters }) => ({
...defaultConverters,
blocks: {
myTextBlock: ({ node, providedCSSString }) =>
`<div style="background-color: red;${providedCSSString}">${node.fields.text}</div>`,
},
})) as HTMLConvertersFunction<DefaultNodeTypes | SerializedBlockNode<MyTextBlock>>,
}),
],
}
```
This commit is contained in:
@@ -2,8 +2,8 @@
|
||||
title: Lexical Converters
|
||||
label: Converters
|
||||
order: 20
|
||||
desc: Conversion between lexical, markdown and html
|
||||
keywords: lexical, rich text, editor, headless cms, convert, html, mdx, markdown, md, conversion, export
|
||||
desc: Conversion between lexical, markdown, jsx and html
|
||||
keywords: lexical, rich text, editor, headless cms, convert, html, mdx, markdown, md, conversion, export, jsx
|
||||
---
|
||||
|
||||
Lexical saves data in JSON - this is great for storage and flexibility and allows you to easily to convert it to other formats like JSX, HTML or Markdown.
|
||||
@@ -74,20 +74,28 @@ To convert Lexical Blocks or Inline Blocks to JSX, pass the converter for your b
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
import type { MyInlineBlock, MyTextBlock } from '@/payload-types'
|
||||
import type { DefaultNodeTypes, SerializedBlockNode } from '@payloadcms/richtext-lexical'
|
||||
import type { MyInlineBlock, MyNumberBlock, MyTextBlock } from '@/payload-types'
|
||||
import type {
|
||||
DefaultNodeTypes,
|
||||
SerializedBlockNode,
|
||||
SerializedInlineBlockNode,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
|
||||
|
||||
import { type JSXConvertersFunction, RichText } from '@payloadcms/richtext-lexical/react'
|
||||
import React from 'react'
|
||||
|
||||
// Extend the default node types with your custom blocks for full type safety
|
||||
type NodeTypes = DefaultNodeTypes | SerializedBlockNode<MyInlineBlock | MyTextBlock>
|
||||
type NodeTypes =
|
||||
| DefaultNodeTypes
|
||||
| SerializedBlockNode<MyNumberBlock | MyTextBlock>
|
||||
| SerializedInlineBlockNode<MyInlineBlock>
|
||||
|
||||
const jsxConverters: JSXConvertersFunction<NodeTypes> = ({ defaultConverters }) => ({
|
||||
...defaultConverters,
|
||||
blocks: {
|
||||
// Each key should match your block's slug
|
||||
myNumberBlock: ({ node }) => <div>{node.fields.number}</div>,
|
||||
myTextBlock: ({ node }) => <div style={{ backgroundColor: 'red' }}>{node.fields.text}</div>,
|
||||
},
|
||||
inlineBlocks: {
|
||||
@@ -155,19 +163,156 @@ export const MyComponent: React.FC<{
|
||||
|
||||
If you don't have a React-based frontend, or if you need to send the content to a third-party service, you can convert lexical to HTML. There are two ways to do this:
|
||||
|
||||
1. **Outputting HTML from the Collection:** Create a new field in your collection to convert saved JSON content to HTML. Payload generates and outputs the HTML for use in your frontend.
|
||||
2. **Generating HTML on any server** Convert JSON to HTML on-demand on the server.
|
||||
|
||||
In both cases, the conversion needs to happen on a server, as the HTML converter will automatically fetch data for nodes that require it (e.g. uploads and internal links). The editor comes with built-in HTML serializers, simplifying the process of converting JSON to HTML.
|
||||
1. **Generating HTML in your frontend** Convert JSON to HTML on-demand wherever you need it (Recommended).
|
||||
2. **Outputting HTML from the Collection:** Create a new field in your collection to convert saved JSON content to HTML. Payload generates and outputs the HTML for use in your frontend. This is not recommended, as this approach adds additional overhead to the Payload API and may not work with live preview.
|
||||
|
||||
### Generating HTML in your frontend
|
||||
|
||||
If you wish to convert JSON to HTML ad-hoc, use the `convertLexicalToHTML` function exported from `@payloadcms/richtext-lexical/html`:
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
|
||||
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
|
||||
import { convertLexicalToHTML } from '@payloadcms/richtext-lexical/html'
|
||||
|
||||
import React from 'react'
|
||||
|
||||
export const MyComponent = ({ data }: { data: SerializedEditorState }) => {
|
||||
const html = convertLexicalToHTML({ data })
|
||||
|
||||
return <div dangerouslySetInnerHTML={{ __html: html }} />
|
||||
}
|
||||
```
|
||||
|
||||
### Generating HTML in your frontend with dynamic population
|
||||
|
||||
The default `convertLexicalToHTML` function does not populate data for nodes like uploads or links - it expects you to pass in the fully populated data. If you want the converter to dynamically populate those nodes as they are encountered, you have to use the async version of the converter, imported from `@payloadcms/richtext-lexical/html-async`, and pass in the `populate` function:
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
|
||||
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
|
||||
|
||||
import { getRestPopulateFn } from '@payloadcms/richtext-lexical/client'
|
||||
import { convertLexicalToHTMLAsync } from '@payloadcms/richtext-lexical/html-async'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
export const MyComponent = ({ data }: { data: SerializedEditorState }) => {
|
||||
const [html, setHTML] = useState<null | string>(null)
|
||||
useEffect(() => {
|
||||
async function convert() {
|
||||
const html = await convertLexicalToHTMLAsync({
|
||||
data,
|
||||
populate: getRestPopulateFn({
|
||||
apiURL: `http://localhost:3000/api`,
|
||||
}),
|
||||
})
|
||||
setHTML(html)
|
||||
}
|
||||
|
||||
void convert()
|
||||
}, [data])
|
||||
|
||||
return html && <div dangerouslySetInnerHTML={{ __html: html }} />
|
||||
}
|
||||
```
|
||||
|
||||
Do note that using the REST populate function will result in each node sending a separate request to the REST API, which may be slow for a large amount of nodes. On the server, you can use the payload populate function, which will be more efficient:
|
||||
|
||||
```tsx
|
||||
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
|
||||
|
||||
import { getPayloadPopulateFn } from '@payloadcms/richtext-lexical'
|
||||
import { convertLexicalToHTMLAsync } from '@payloadcms/richtext-lexical/html-async'
|
||||
import { getPayload } from 'payload'
|
||||
import React from 'react'
|
||||
|
||||
import config from '../../config.js'
|
||||
|
||||
export const MyRSCComponent = async ({ data }: { data: SerializedEditorState }) => {
|
||||
const payload = await getPayload({
|
||||
config,
|
||||
})
|
||||
|
||||
const html = await convertLexicalToHTMLAsync({
|
||||
data,
|
||||
populate: await getPayloadPopulateFn({
|
||||
currentDepth: 0,
|
||||
depth: 1,
|
||||
payload,
|
||||
}),
|
||||
})
|
||||
|
||||
return html && <div dangerouslySetInnerHTML={{ __html: html }} />
|
||||
}
|
||||
```
|
||||
|
||||
### Converting Lexical Blocks
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
|
||||
import type { MyInlineBlock, MyTextBlock } from '@/payload-types'
|
||||
import type {
|
||||
DefaultNodeTypes,
|
||||
SerializedBlockNode,
|
||||
SerializedInlineBlockNode,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
|
||||
|
||||
import {
|
||||
convertLexicalToHTML,
|
||||
type HTMLConvertersFunction,
|
||||
} from '@payloadcms/richtext-lexical/html'
|
||||
import React from 'react'
|
||||
|
||||
type NodeTypes =
|
||||
| DefaultNodeTypes
|
||||
| SerializedBlockNode<MyTextBlock>
|
||||
| SerializedInlineBlockNode<MyInlineBlock>
|
||||
|
||||
const htmlConverters: HTMLConvertersFunction<NodeTypes> = ({ defaultConverters }) => ({
|
||||
...defaultConverters,
|
||||
blocks: {
|
||||
// Each key should match your block's slug
|
||||
myTextBlock: ({ node, providedCSSString }) =>
|
||||
`<div style="background-color: red;${providedCSSString}">${node.fields.text}</div>`,
|
||||
},
|
||||
inlineBlocks: {
|
||||
// Each key should match your inline block's slug
|
||||
myInlineBlock: ({ node, providedStyleTag }) =>
|
||||
`<span${providedStyleTag}>${node.fields.text}</span$>`,
|
||||
},
|
||||
})
|
||||
|
||||
export const MyComponent = ({ data }: { data: SerializedEditorState }) => {
|
||||
const html = convertLexicalToHTML({
|
||||
converters: htmlConverters,
|
||||
data,
|
||||
})
|
||||
|
||||
return <div dangerouslySetInnerHTML={{ __html: html }} />
|
||||
}
|
||||
```
|
||||
|
||||
### Outputting HTML from the Collection
|
||||
|
||||
To add HTML generation directly within the collection, follow the example below:
|
||||
|
||||
```ts
|
||||
import type { HTMLConvertersFunction } from '@payloadcms/richtext-lexical/html'
|
||||
import type { MyTextBlock } from '@/payload-types.js'
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
import { HTMLConverterFeature, lexicalEditor, lexicalHTML } from '@payloadcms/richtext-lexical'
|
||||
import {
|
||||
BlocksFeature,
|
||||
type DefaultNodeTypes,
|
||||
lexicalEditor,
|
||||
lexicalHTMLField,
|
||||
type SerializedBlockNode,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
|
||||
const Pages: CollectionConfig = {
|
||||
slug: 'pages',
|
||||
@@ -175,71 +320,53 @@ const Pages: CollectionConfig = {
|
||||
{
|
||||
name: 'nameOfYourRichTextField',
|
||||
type: 'richText',
|
||||
editor: lexicalEditor(),
|
||||
},
|
||||
lexicalHTMLField({
|
||||
htmlFieldName: 'nameOfYourRichTextField_html',
|
||||
lexicalFieldName: 'nameOfYourRichTextField',
|
||||
}),
|
||||
{
|
||||
name: 'customRichText',
|
||||
type: 'richText',
|
||||
editor: lexicalEditor({
|
||||
features: ({ defaultFeatures }) => [
|
||||
...defaultFeatures,
|
||||
// The HTMLConverter Feature is the feature which manages the HTML serializers.
|
||||
// If you do not pass any arguments to it, it will use the default serializers.
|
||||
HTMLConverterFeature({}),
|
||||
BlocksFeature({
|
||||
blocks: [
|
||||
{
|
||||
interfaceName: 'MyTextBlock',
|
||||
slug: 'myTextBlock',
|
||||
fields: [
|
||||
{
|
||||
name: 'text',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
},
|
||||
lexicalHTML('nameOfYourRichTextField', { name: 'nameOfYourRichTextField_html' }),
|
||||
lexicalHTMLField({
|
||||
htmlFieldName: 'customRichText_html',
|
||||
lexicalFieldName: 'customRichText',
|
||||
// can pass in additional converters or override default ones
|
||||
converters: (({ defaultConverters }) => ({
|
||||
...defaultConverters,
|
||||
blocks: {
|
||||
myTextBlock: ({ node, providedCSSString }) =>
|
||||
`<div style="background-color: red;${providedCSSString}">${node.fields.text}</div>`,
|
||||
},
|
||||
})) as HTMLConvertersFunction<DefaultNodeTypes | SerializedBlockNode<MyTextBlock>>,
|
||||
}),
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
The `lexicalHTML()` function creates a new field that automatically converts the referenced lexical richText field into HTML through an afterRead hook.
|
||||
|
||||
### Generating HTML anywhere on the server
|
||||
|
||||
If you wish to convert JSON to HTML ad-hoc, use the `convertLexicalToHTML` function:
|
||||
|
||||
```ts
|
||||
import { consolidateHTMLConverters, convertLexicalToHTML } from '@payloadcms/richtext-lexical'
|
||||
|
||||
|
||||
await convertLexicalToHTML({
|
||||
converters: consolidateHTMLConverters({ editorConfig }),
|
||||
data: editorData,
|
||||
payload, // if you have Payload but no req available, pass it in here to enable server-only functionality (e.g. proper conversion of upload nodes)
|
||||
req, // if you have req available, pass it in here to enable server-only functionality (e.g. proper conversion of upload nodes). No need to pass in Payload if req is passed in.
|
||||
})
|
||||
```
|
||||
This method employs `convertLexicalToHTML` from `@payloadcms/richtext-lexical`, which converts the serialized editor state into HTML.
|
||||
|
||||
Because every `Feature` is able to provide html converters, and because the `htmlFeature` can modify those or provide their own, we need to consolidate them with the default html Converters using the `consolidateHTMLConverters` function.
|
||||
|
||||
#### Example: Generating HTML within an afterRead hook
|
||||
|
||||
```ts
|
||||
import type { FieldHook } from 'payload'
|
||||
|
||||
import {
|
||||
HTMLConverterFeature,
|
||||
consolidateHTMLConverters,
|
||||
convertLexicalToHTML,
|
||||
defaultEditorConfig,
|
||||
defaultEditorFeatures,
|
||||
sanitizeServerEditorConfig,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
|
||||
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,
|
||||
})
|
||||
return html
|
||||
}
|
||||
```
|
||||
|
||||
### CSS
|
||||
|
||||
Payload's lexical HTML converter does not generate CSS for you, but it does add classes to the generated HTML. You can use these classes to style the HTML in your frontend.
|
||||
@@ -253,87 +380,6 @@ Here is some "base" CSS you can use to ensure that nested lists render correctly
|
||||
}
|
||||
```
|
||||
|
||||
### Creating your own HTML Converter
|
||||
|
||||
HTML Converters are typed as `HTMLConverter`, which contains the node type it should handle, and a function that accepts the serialized node from the lexical editor, and outputs the HTML string. Here's the HTML Converter of the Upload node as an example:
|
||||
|
||||
```ts
|
||||
import type { HTMLConverter } from '@payloadcms/richtext-lexical'
|
||||
|
||||
const UploadHTMLConverter: HTMLConverter<SerializedUploadNode> = {
|
||||
converter: async ({ node, req }) => {
|
||||
const uploadDocument: {
|
||||
value?: any
|
||||
} = {}
|
||||
if(req) {
|
||||
await populate({
|
||||
id,
|
||||
collectionSlug: node.relationTo,
|
||||
currentDepth: 0,
|
||||
data: uploadDocument,
|
||||
depth: 1,
|
||||
draft: false,
|
||||
key: 'value',
|
||||
overrideAccess: false,
|
||||
req,
|
||||
showHiddenFields: false,
|
||||
})
|
||||
}
|
||||
|
||||
const url = (req?.payload?.config?.serverURL || '') + uploadDocument?.value?.url
|
||||
|
||||
if (!(uploadDocument?.value?.mimeType as string)?.startsWith('image')) {
|
||||
// Only images can be serialized as HTML
|
||||
return ``
|
||||
}
|
||||
|
||||
return `<img src="${url}" alt="${uploadDocument?.value?.filename}" width="${uploadDocument?.value?.width}" height="${uploadDocument?.value?.height}"/>`
|
||||
},
|
||||
nodeTypes: [UploadNode.getType()], // This is the type of the lexical node that this converter can handle. Instead of hardcoding 'upload' we can get the node type directly from the UploadNode, since it's static.
|
||||
}
|
||||
```
|
||||
|
||||
As you can see, we have access to all the information saved in the node (for the Upload node, this is `value`and `relationTo`) and we can use that to generate the HTML.
|
||||
|
||||
The `convertLexicalToHTML` is part of `@payloadcms/richtext-lexical` automatically handles traversing the editor state and calling the correct converter for each node.
|
||||
|
||||
### Embedding the HTML Converter in your Feature
|
||||
|
||||
You can embed your HTML Converter directly within your custom `ServerFeature`, allowing it to be handled automatically by the `consolidateHTMLConverters` function. Here is an example:
|
||||
|
||||
```ts
|
||||
import { createNode } from '@payloadcms/richtext-lexical'
|
||||
import type { FeatureProviderProviderServer } from '@payloadcms/richtext-lexical'
|
||||
|
||||
export const UploadFeature: FeatureProviderProviderServer<
|
||||
UploadFeatureProps,
|
||||
UploadFeaturePropsClient
|
||||
> = (props) => {
|
||||
/*...*/
|
||||
return {
|
||||
feature: () => {
|
||||
return {
|
||||
nodes: [
|
||||
createNode({
|
||||
converters: {
|
||||
html: yourHTMLConverter, // <= This is where you define your HTML Converter
|
||||
},
|
||||
node: UploadNode,
|
||||
//...
|
||||
}),
|
||||
],
|
||||
ClientComponent: UploadFeatureClientComponent,
|
||||
clientFeatureProps: clientProps,
|
||||
serverFeatureProps: props,
|
||||
/*...*/
|
||||
}
|
||||
},
|
||||
key: 'upload',
|
||||
serverFeatureProps: props,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Headless Editor
|
||||
|
||||
Lexical provides a seamless way to perform conversions between various other formats:
|
||||
|
||||
@@ -35,6 +35,16 @@
|
||||
"types": "./src/exports/react/index.ts",
|
||||
"default": "./src/exports/react/index.ts"
|
||||
},
|
||||
"./html": {
|
||||
"import": "./src/exports/html/index.ts",
|
||||
"types": "./src/exports/html/index.ts",
|
||||
"default": "./src/exports/html/index.ts"
|
||||
},
|
||||
"./html-async": {
|
||||
"import": "./src/exports/html-async/index.ts",
|
||||
"types": "./src/exports/html-async/index.ts",
|
||||
"default": "./src/exports/html-async/index.ts"
|
||||
},
|
||||
"./rsc": {
|
||||
"import": "./src/exports/server/rsc.ts",
|
||||
"types": "./src/exports/server/rsc.ts",
|
||||
@@ -368,6 +378,7 @@
|
||||
"mdast-util-from-markdown": "2.0.2",
|
||||
"mdast-util-mdx-jsx": "3.1.3",
|
||||
"micromark-extension-mdx-jsx": "3.0.1",
|
||||
"qs-esm": "7.0.2",
|
||||
"react-error-boundary": "4.1.2",
|
||||
"ts-essentials": "10.0.3",
|
||||
"uuid": "10.0.0"
|
||||
@@ -421,6 +432,16 @@
|
||||
"types": "./dist/exports/react/index.d.ts",
|
||||
"default": "./dist/exports/react/index.js"
|
||||
},
|
||||
"./html": {
|
||||
"import": "./dist/exports/html/index.js",
|
||||
"types": "./dist/exports/html/index.d.ts",
|
||||
"default": "./dist/exports/html/index.js"
|
||||
},
|
||||
"./html-async": {
|
||||
"import": "./dist/exports/html-async/index.js",
|
||||
"types": "./dist/exports/html-async/index.d.ts",
|
||||
"default": "./dist/exports/html-async/index.js"
|
||||
},
|
||||
"./rsc": {
|
||||
"import": "./dist/exports/server/rsc.js",
|
||||
"types": "./dist/exports/server/rsc.d.ts",
|
||||
|
||||
@@ -146,3 +146,4 @@ export { BlockCollapsible } from '../../features/blocks/client/component/compone
|
||||
export { BlockEditButton } from '../../features/blocks/client/component/components/BlockEditButton.js'
|
||||
export { BlockRemoveButton } from '../../features/blocks/client/component/components/BlockRemoveButton.js'
|
||||
export { useBlockComponentContext } from '../../features/blocks/client/component/BlockContent.js'
|
||||
export { getRestPopulateFn } from '../../features/converters/utilities/restPopulateFn.js'
|
||||
|
||||
25
packages/richtext-lexical/src/exports/html-async/index.ts
Normal file
25
packages/richtext-lexical/src/exports/html-async/index.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
export { BlockquoteHTMLConverterAsync } from '../../features/converters/html/async/converters/blockquote.js'
|
||||
export { HeadingHTMLConverterAsync } from '../../features/converters/html/async/converters/heading.js'
|
||||
export { HorizontalRuleHTMLConverterAsync } from '../../features/converters/html/async/converters/horizontalRule.js'
|
||||
export { LinebreakHTMLConverterAsync } from '../../features/converters/html/async/converters/linebreak.js'
|
||||
export { LinkHTMLConverterAsync } from '../../features/converters/html/async/converters/link.js'
|
||||
export { ListHTMLConverterAsync } from '../../features/converters/html/async/converters/list.js'
|
||||
export { ParagraphHTMLConverterAsync } from '../../features/converters/html/async/converters/paragraph.js'
|
||||
export { TabHTMLConverterAsync } from '../../features/converters/html/async/converters/tab.js'
|
||||
export { TableHTMLConverterAsync } from '../../features/converters/html/async/converters/table.js'
|
||||
export { TextHTMLConverterAsync } from '../../features/converters/html/async/converters/text.js'
|
||||
|
||||
export { UploadHTMLConverterAsync } from '../../features/converters/html/async/converters/upload.js'
|
||||
|
||||
export { defaultHTMLConvertersAsync } from '../../features/converters/html/async/defaultConverters.js'
|
||||
export { convertLexicalToHTMLAsync } from '../../features/converters/html/async/index.js'
|
||||
export type {
|
||||
HTMLConverterAsync,
|
||||
HTMLConvertersAsync,
|
||||
HTMLConvertersFunctionAsync,
|
||||
} from '../../features/converters/html/async/types.js'
|
||||
|
||||
export type {
|
||||
ProvidedCSS,
|
||||
SerializedLexicalNodeWithParent,
|
||||
} from '../../features/converters/html/shared/types.js'
|
||||
25
packages/richtext-lexical/src/exports/html/index.ts
Normal file
25
packages/richtext-lexical/src/exports/html/index.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
export type {
|
||||
ProvidedCSS,
|
||||
SerializedLexicalNodeWithParent,
|
||||
} from '../../features/converters/html/shared/types.js'
|
||||
export { BlockquoteHTMLConverter } from '../../features/converters/html/sync/converters/blockquote.js'
|
||||
export { HeadingHTMLConverter } from '../../features/converters/html/sync/converters/heading.js'
|
||||
export { HorizontalRuleHTMLConverter } from '../../features/converters/html/sync/converters/horizontalRule.js'
|
||||
export { LinebreakHTMLConverter } from '../../features/converters/html/sync/converters/linebreak.js'
|
||||
export { LinkHTMLConverter } from '../../features/converters/html/sync/converters/link.js'
|
||||
export { ListHTMLConverter } from '../../features/converters/html/sync/converters/list.js'
|
||||
export { ParagraphHTMLConverter } from '../../features/converters/html/sync/converters/paragraph.js'
|
||||
export { TabHTMLConverter } from '../../features/converters/html/sync/converters/tab.js'
|
||||
export { TableHTMLConverter } from '../../features/converters/html/sync/converters/table.js'
|
||||
|
||||
export { TextHTMLConverter } from '../../features/converters/html/sync/converters/text.js'
|
||||
|
||||
export { UploadHTMLConverter } from '../../features/converters/html/sync/converters/upload.js'
|
||||
export { defaultHTMLConverters } from '../../features/converters/html/sync/defaultConverters.js'
|
||||
export { convertLexicalToHTML } from '../../features/converters/html/sync/index.js'
|
||||
|
||||
export type {
|
||||
HTMLConverter,
|
||||
HTMLConverters,
|
||||
HTMLConvertersFunction,
|
||||
} from '../../features/converters/html/sync/types.js'
|
||||
@@ -1,9 +0,0 @@
|
||||
import type { SerializedTabNode } from '../../../../../../nodeTypes.js'
|
||||
import type { JSXConverters } from '../types.js'
|
||||
|
||||
export const TabJSXConverter: JSXConverters<SerializedTabNode> = {
|
||||
tab: ({ node }) => {
|
||||
// Tab
|
||||
return node.text
|
||||
},
|
||||
}
|
||||
@@ -1,21 +1,24 @@
|
||||
export { BlockquoteJSXConverter } from './components/RichText/converter/converters/blockquote.js'
|
||||
export { HeadingJSXConverter } from './components/RichText/converter/converters/heading.js'
|
||||
export { HorizontalRuleJSXConverter } from './components/RichText/converter/converters/horizontalRule.js'
|
||||
export { LinebreakJSXConverter } from './components/RichText/converter/converters/linebreak.js'
|
||||
export { LinkJSXConverter } from './components/RichText/converter/converters/link.js'
|
||||
export { ListJSXConverter } from './components/RichText/converter/converters/list.js'
|
||||
export { ParagraphJSXConverter } from './components/RichText/converter/converters/paragraph.js'
|
||||
export { TabJSXConverter } from './components/RichText/converter/converters/tab.js'
|
||||
export { TableJSXConverter } from './components/RichText/converter/converters/table.js'
|
||||
export { TextJSXConverter } from './components/RichText/converter/converters/text.js'
|
||||
export {
|
||||
type JSXConvertersFunction,
|
||||
RichText,
|
||||
} from '../../features/converters/jsx/Component/index.js'
|
||||
export { BlockquoteJSXConverter } from '../../features/converters/jsx/converter/converters/blockquote.js'
|
||||
export { HeadingJSXConverter } from '../../features/converters/jsx/converter/converters/heading.js'
|
||||
export { HorizontalRuleJSXConverter } from '../../features/converters/jsx/converter/converters/horizontalRule.js'
|
||||
export { LinebreakJSXConverter } from '../../features/converters/jsx/converter/converters/linebreak.js'
|
||||
export { LinkJSXConverter } from '../../features/converters/jsx/converter/converters/link.js'
|
||||
export { ListJSXConverter } from '../../features/converters/jsx/converter/converters/list.js'
|
||||
export { ParagraphJSXConverter } from '../../features/converters/jsx/converter/converters/paragraph.js'
|
||||
export { TabJSXConverter } from '../../features/converters/jsx/converter/converters/tab.js'
|
||||
export { TableJSXConverter } from '../../features/converters/jsx/converter/converters/table.js'
|
||||
|
||||
export { UploadJSXConverter } from './components/RichText/converter/converters/upload.js'
|
||||
export { TextJSXConverter } from '../../features/converters/jsx/converter/converters/text.js'
|
||||
|
||||
export { defaultJSXConverters } from './components/RichText/converter/defaultConverters.js'
|
||||
export { convertLexicalNodesToJSX } from './components/RichText/converter/index.js'
|
||||
export { UploadJSXConverter } from '../../features/converters/jsx/converter/converters/upload.js'
|
||||
export { defaultJSXConverters } from '../../features/converters/jsx/converter/defaultConverters.js'
|
||||
export { convertLexicalNodesToJSX } from '../../features/converters/jsx/converter/index.js'
|
||||
export type {
|
||||
JSXConverter,
|
||||
JSXConverters,
|
||||
SerializedLexicalNodeWithParent,
|
||||
} from './components/RichText/converter/types.js'
|
||||
export { type JSXConvertersFunction, RichText } from './components/RichText/index.js'
|
||||
} from '../../features/converters/jsx/converter/types.js'
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { Spread } from 'lexical'
|
||||
import { QuoteNode } from '@lexical/rich-text'
|
||||
|
||||
import { createServerFeature } from '../../../utilities/createServerFeature.js'
|
||||
import { convertLexicalNodesToHTML } from '../../converters/html/converter/index.js'
|
||||
import { convertLexicalNodesToHTML } from '../../converters/html_deprecated/converter/index.js'
|
||||
import { createNode } from '../../typeUtilities.js'
|
||||
import { MarkdownTransformer } from '../markdownTransformer.js'
|
||||
import { i18n } from './i18n.js'
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { SerializedQuoteNode } from '../../../../../nodeTypes.js'
|
||||
import type { HTMLConvertersAsync } from '../types.js'
|
||||
|
||||
export const BlockquoteHTMLConverterAsync: HTMLConvertersAsync<SerializedQuoteNode> = {
|
||||
quote: async ({ node, nodesToHTML, providedStyleTag }) => {
|
||||
const children = (
|
||||
await nodesToHTML({
|
||||
nodes: node.children,
|
||||
})
|
||||
).join('')
|
||||
|
||||
return `<blockquote${providedStyleTag}>${children}</blockquote>`
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { SerializedHeadingNode } from '../../../../../nodeTypes.js'
|
||||
import type { HTMLConvertersAsync } from '../types.js'
|
||||
|
||||
export const HeadingHTMLConverterAsync: HTMLConvertersAsync<SerializedHeadingNode> = {
|
||||
heading: async ({ node, nodesToHTML, providedStyleTag }) => {
|
||||
const children = (
|
||||
await nodesToHTML({
|
||||
nodes: node.children,
|
||||
})
|
||||
).join('')
|
||||
|
||||
return `<${node.tag}${providedStyleTag}>${children}</${node.tag}>`
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { SerializedHorizontalRuleNode } from '../../../../../nodeTypes.js'
|
||||
import type { HTMLConvertersAsync } from '../types.js'
|
||||
export const HorizontalRuleHTMLConverterAsync: HTMLConvertersAsync<SerializedHorizontalRuleNode> = {
|
||||
horizontalrule: '<hr />',
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { SerializedLineBreakNode } from '../../../../../nodeTypes.js'
|
||||
import type { HTMLConvertersAsync } from '../types.js'
|
||||
|
||||
export const LinebreakHTMLConverterAsync: HTMLConvertersAsync<SerializedLineBreakNode> = {
|
||||
linebreak: '<br />',
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import type { SerializedAutoLinkNode, SerializedLinkNode } from '../../../../../nodeTypes.js'
|
||||
import type { HTMLConvertersAsync, HTMLPopulateFn } from '../types.js'
|
||||
|
||||
export const LinkHTMLConverterAsync: (args: {
|
||||
internalDocToHref?: (args: {
|
||||
linkNode: SerializedLinkNode
|
||||
populate?: HTMLPopulateFn
|
||||
}) => Promise<string> | string
|
||||
}) => HTMLConvertersAsync<SerializedAutoLinkNode | SerializedLinkNode> = ({
|
||||
internalDocToHref,
|
||||
}) => ({
|
||||
autolink: async ({ node, nodesToHTML, providedStyleTag }) => {
|
||||
const children = (
|
||||
await nodesToHTML({
|
||||
nodes: node.children,
|
||||
})
|
||||
).join('')
|
||||
|
||||
const rel: string | undefined = node.fields.newTab ? 'noopener noreferrer' : undefined
|
||||
const target: string | undefined = node.fields.newTab ? '_blank' : undefined
|
||||
|
||||
return `(
|
||||
<a${providedStyleTag} href="${node.fields.url}" rel=${rel} target=${target}>
|
||||
${children}
|
||||
</a>
|
||||
)`
|
||||
},
|
||||
link: async ({ node, nodesToHTML, populate, providedStyleTag }) => {
|
||||
const children = (
|
||||
await nodesToHTML({
|
||||
nodes: node.children,
|
||||
})
|
||||
).join('')
|
||||
|
||||
const rel: string | undefined = node.fields.newTab ? 'noopener noreferrer' : undefined
|
||||
const target: string | undefined = node.fields.newTab ? '_blank' : undefined
|
||||
|
||||
let href: string = node.fields.url ?? ''
|
||||
if (node.fields.linkType === 'internal') {
|
||||
if (internalDocToHref) {
|
||||
href = await internalDocToHref({ linkNode: node, populate })
|
||||
} else {
|
||||
console.error(
|
||||
'Lexical => HTML converter: Link converter: found internal link, but internalDocToHref is not provided',
|
||||
)
|
||||
href = '#' // fallback
|
||||
}
|
||||
}
|
||||
|
||||
return `(
|
||||
<a${providedStyleTag} href="${href}" rel=${rel} target=${target}>
|
||||
${children}
|
||||
</a>
|
||||
)`
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,53 @@
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
import type { SerializedListItemNode, SerializedListNode } from '../../../../../nodeTypes.js'
|
||||
import type { HTMLConvertersAsync } from '../types.js'
|
||||
|
||||
export const ListHTMLConverterAsync: HTMLConvertersAsync<
|
||||
SerializedListItemNode | SerializedListNode
|
||||
> = {
|
||||
list: async ({ node, nodesToHTML, providedStyleTag }) => {
|
||||
const children = (
|
||||
await nodesToHTML({
|
||||
nodes: node.children,
|
||||
})
|
||||
).join('')
|
||||
|
||||
return `<${node.tag}${providedStyleTag} class="list-${node.listType}">${children}</${node.tag}>`
|
||||
},
|
||||
listitem: async ({ node, nodesToHTML, parent, providedCSSString }) => {
|
||||
const hasSubLists = node.children.some((child) => child.type === 'list')
|
||||
|
||||
const children = (
|
||||
await nodesToHTML({
|
||||
nodes: node.children,
|
||||
})
|
||||
).join('')
|
||||
|
||||
if ('listType' in parent && parent?.listType === 'check') {
|
||||
const uuid = uuidv4()
|
||||
return `<li
|
||||
aria-checked="${node.checked ? 'true' : 'false'}"
|
||||
class="list-item-checkbox${node.checked ? ' list-item-checkbox-checked' : ' list-item-checkbox-unchecked'}${hasSubLists ? ' nestedListItem' : ''}"
|
||||
role="checkbox"
|
||||
style="list-style-type: none;${providedCSSString}"
|
||||
tabIndex="-1"
|
||||
value="${node.value}"
|
||||
>
|
||||
${
|
||||
hasSubLists
|
||||
? children
|
||||
: `<input checked="${node.checked}" id="${uuid}" readOnly="true" type="checkbox" />
|
||||
<label htmlFor="${uuid}">${children}</label>
|
||||
<br />`
|
||||
}
|
||||
</li>`
|
||||
} else {
|
||||
return `<li
|
||||
class="${hasSubLists ? 'nestedListItem' : ''}"
|
||||
style="${hasSubLists ? `list-style-type: none;${providedCSSString}` : providedCSSString}"
|
||||
value="${node.value}"
|
||||
>${children}</li>`
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { SerializedParagraphNode } from '../../../../../nodeTypes.js'
|
||||
import type { HTMLConvertersAsync } from '../types.js'
|
||||
|
||||
export const ParagraphHTMLConverterAsync: HTMLConvertersAsync<SerializedParagraphNode> = {
|
||||
paragraph: async ({ node, nodesToHTML, providedStyleTag }) => {
|
||||
const children = await nodesToHTML({
|
||||
nodes: node.children,
|
||||
})
|
||||
|
||||
if (!children?.length) {
|
||||
return `<p${providedStyleTag}><br /></p>`
|
||||
}
|
||||
|
||||
return `<p${providedStyleTag}>${children.join('')}</p>`
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { SerializedTabNode } from '../../../../../nodeTypes.js'
|
||||
import type { HTMLConvertersAsync } from '../types.js'
|
||||
|
||||
export const TabHTMLConverterAsync: HTMLConvertersAsync<SerializedTabNode> = {
|
||||
tab: '\t',
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import type {
|
||||
SerializedTableCellNode,
|
||||
SerializedTableNode,
|
||||
SerializedTableRowNode,
|
||||
} from '../../../../../nodeTypes.js'
|
||||
import type { HTMLConvertersAsync } from '../types.js'
|
||||
|
||||
export const TableHTMLConverterAsync: HTMLConvertersAsync<
|
||||
SerializedTableCellNode | SerializedTableNode | SerializedTableRowNode
|
||||
> = {
|
||||
table: async ({ node, nodesToHTML, providedStyleTag }) => {
|
||||
const children = (
|
||||
await nodesToHTML({
|
||||
nodes: node.children,
|
||||
})
|
||||
).join('')
|
||||
|
||||
return `<div${providedStyleTag} class="lexical-table-container">
|
||||
<table class="lexical-table" style="border-collapse: collapse;">
|
||||
<tbody>${children}</tbody>
|
||||
</table>
|
||||
</div>`
|
||||
},
|
||||
|
||||
tablecell: async ({ node, nodesToHTML, providedCSSString }) => {
|
||||
const children = (
|
||||
await nodesToHTML({
|
||||
nodes: node.children,
|
||||
})
|
||||
).join('')
|
||||
|
||||
const TagName = node.headerState > 0 ? 'th' : 'td'
|
||||
const headerStateClass = `lexical-table-cell-header-${node.headerState}`
|
||||
|
||||
let style = 'border: 1px solid #ccc; padding: 8px;' + providedCSSString
|
||||
if (node.backgroundColor) {
|
||||
style += ` background-color: ${node.backgroundColor};`
|
||||
}
|
||||
|
||||
const colSpanAttr = node.colSpan && node.colSpan > 1 ? ` colspan="${node.colSpan}"` : ''
|
||||
const rowSpanAttr = node.rowSpan && node.rowSpan > 1 ? ` rowspan="${node.rowSpan}"` : ''
|
||||
|
||||
return `<${TagName}
|
||||
class="lexical-table-cell ${headerStateClass}"
|
||||
${colSpanAttr}
|
||||
${rowSpanAttr}
|
||||
style="${style}"
|
||||
>
|
||||
${children}
|
||||
</${TagName}>
|
||||
`
|
||||
},
|
||||
|
||||
tablerow: async ({ node, nodesToHTML, providedStyleTag }) => {
|
||||
const children = (
|
||||
await nodesToHTML({
|
||||
nodes: node.children,
|
||||
})
|
||||
).join('')
|
||||
|
||||
return `<tr${providedStyleTag} class="lexical-table-row">
|
||||
${children}
|
||||
</tr>`
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { SerializedTextNode } from '../../../../../nodeTypes.js'
|
||||
import type { HTMLConvertersAsync } from '../types.js'
|
||||
|
||||
import { NodeFormat } from '../../../../../lexical/utils/nodeFormat.js'
|
||||
|
||||
export const TextHTMLConverterAsync: HTMLConvertersAsync<SerializedTextNode> = {
|
||||
text: ({ 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
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import type { FileData, FileSizeImproved, TypeWithID } from 'payload'
|
||||
|
||||
import type { SerializedUploadNode } from '../../../../../nodeTypes.js'
|
||||
import type { UploadDataImproved } from '../../../../upload/server/nodes/UploadNode.js'
|
||||
import type { HTMLConvertersAsync } from '../types.js'
|
||||
|
||||
export const UploadHTMLConverterAsync: HTMLConvertersAsync<SerializedUploadNode> = {
|
||||
upload: async ({ node, populate, providedStyleTag }) => {
|
||||
const uploadNode = node as UploadDataImproved
|
||||
|
||||
let uploadDoc: (FileData & TypeWithID) | undefined = undefined
|
||||
|
||||
// If there's no valid upload data, populate return an empty string
|
||||
if (typeof uploadNode.value !== 'object') {
|
||||
if (!populate) {
|
||||
return ''
|
||||
}
|
||||
uploadDoc = await populate<FileData & TypeWithID>({
|
||||
id: uploadNode.value,
|
||||
collectionSlug: uploadNode.relationTo,
|
||||
})
|
||||
} else {
|
||||
uploadDoc = uploadNode.value as unknown as FileData & TypeWithID
|
||||
}
|
||||
|
||||
if (!uploadDoc) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const url = uploadDoc.url
|
||||
|
||||
// 1) If upload is NOT an image, return a link
|
||||
if (!uploadDoc.mimeType.startsWith('image')) {
|
||||
return `<a${providedStyleTag} href="${url}" rel="noopener noreferrer">${uploadDoc.filename}</a$>`
|
||||
}
|
||||
|
||||
// 2) If image has no different sizes, return a simple <img />
|
||||
if (!uploadDoc.sizes || !Object.keys(uploadDoc.sizes).length) {
|
||||
return `
|
||||
<img${providedStyleTag}
|
||||
alt="${uploadDoc.filename}"
|
||||
height="${uploadDoc.height}"
|
||||
src="${url}"
|
||||
width="${uploadDoc.width}"
|
||||
/>
|
||||
`
|
||||
}
|
||||
|
||||
// 3) If image has different sizes, build a <picture> element with <source> tags
|
||||
let pictureHTML = ''
|
||||
|
||||
for (const size in uploadDoc.sizes) {
|
||||
const imageSize = uploadDoc.sizes[size] as FileSizeImproved
|
||||
|
||||
if (
|
||||
!imageSize ||
|
||||
!imageSize.width ||
|
||||
!imageSize.height ||
|
||||
!imageSize.mimeType ||
|
||||
!imageSize.filesize ||
|
||||
!imageSize.filename ||
|
||||
!imageSize.url
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
pictureHTML += `
|
||||
<source
|
||||
media="(max-width: ${imageSize.width}px)"
|
||||
srcset="${imageSize.url}"
|
||||
type="${imageSize.mimeType}"
|
||||
/>
|
||||
`
|
||||
}
|
||||
|
||||
pictureHTML += `
|
||||
<img
|
||||
alt="${uploadDoc.filename}"
|
||||
height="${uploadDoc.height}"
|
||||
src="${url}"
|
||||
width="${uploadDoc.width}"
|
||||
/>
|
||||
`
|
||||
|
||||
return `<picture${providedStyleTag}>${pictureHTML}</picture$>`
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { DefaultNodeTypes } from '../../../../nodeTypes.js'
|
||||
import type { HTMLConvertersAsync } from './types.js'
|
||||
|
||||
import { BlockquoteHTMLConverterAsync } from './converters/blockquote.js'
|
||||
import { HeadingHTMLConverterAsync } from './converters/heading.js'
|
||||
import { HorizontalRuleHTMLConverterAsync } from './converters/horizontalRule.js'
|
||||
import { LinebreakHTMLConverterAsync } from './converters/linebreak.js'
|
||||
import { LinkHTMLConverterAsync } from './converters/link.js'
|
||||
import { ListHTMLConverterAsync } from './converters/list.js'
|
||||
import { ParagraphHTMLConverterAsync } from './converters/paragraph.js'
|
||||
import { TabHTMLConverterAsync } from './converters/tab.js'
|
||||
import { TableHTMLConverterAsync } from './converters/table.js'
|
||||
import { TextHTMLConverterAsync } from './converters/text.js'
|
||||
import { UploadHTMLConverterAsync } from './converters/upload.js'
|
||||
|
||||
export const defaultHTMLConvertersAsync: HTMLConvertersAsync<DefaultNodeTypes> = {
|
||||
...ParagraphHTMLConverterAsync,
|
||||
...TextHTMLConverterAsync,
|
||||
...LinebreakHTMLConverterAsync,
|
||||
...BlockquoteHTMLConverterAsync,
|
||||
...TableHTMLConverterAsync,
|
||||
...HeadingHTMLConverterAsync,
|
||||
...HorizontalRuleHTMLConverterAsync,
|
||||
...ListHTMLConverterAsync,
|
||||
...LinkHTMLConverterAsync({}),
|
||||
...UploadHTMLConverterAsync,
|
||||
...TabHTMLConverterAsync,
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import type { SerializedEditorState } from 'lexical'
|
||||
import type { Field } from 'payload'
|
||||
|
||||
import type { HTMLConvertersAsync, HTMLConvertersFunctionAsync } from '../types.js'
|
||||
|
||||
import { getPayloadPopulateFn } from '../../../utilities/payloadPopulateFn.js'
|
||||
import { convertLexicalToHTMLAsync } from '../index.js'
|
||||
|
||||
type Args = {
|
||||
converters?: HTMLConvertersAsync | HTMLConvertersFunctionAsync
|
||||
/**
|
||||
* Whether the lexicalHTML field should be hidden in the admin panel
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
hidden?: boolean
|
||||
htmlFieldName: string
|
||||
/**
|
||||
* A string which matches the lexical field name you want to convert to HTML.
|
||||
*
|
||||
* This has to be a sibling field of this lexicalHTML field - otherwise, it won't be able to find the lexical field.
|
||||
**/
|
||||
lexicalFieldName: string
|
||||
/**
|
||||
* Whether the HTML should be stored in the database
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
storeInDB?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Field that converts a sibling lexical field to HTML
|
||||
*
|
||||
* @todo will be renamed to lexicalHTML in 4.0, replacing the deprecated `lexicalHTML` converter
|
||||
*/
|
||||
export const lexicalHTMLField: (args: Args) => Field = (args) => {
|
||||
const { converters, hidden = true, htmlFieldName, lexicalFieldName, storeInDB = false } = args
|
||||
return {
|
||||
name: htmlFieldName,
|
||||
type: 'code',
|
||||
admin: {
|
||||
editorOptions: {
|
||||
language: 'html',
|
||||
},
|
||||
hidden,
|
||||
},
|
||||
hooks: {
|
||||
afterRead: [
|
||||
async ({
|
||||
currentDepth,
|
||||
depth,
|
||||
draft,
|
||||
overrideAccess,
|
||||
req,
|
||||
showHiddenFields,
|
||||
siblingData,
|
||||
}) => {
|
||||
const lexicalFieldData: SerializedEditorState = siblingData[lexicalFieldName]
|
||||
|
||||
if (!lexicalFieldData) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const htmlPopulateFn = await getPayloadPopulateFn({
|
||||
currentDepth: currentDepth ?? 0,
|
||||
depth: depth ?? req.payload.config.defaultDepth,
|
||||
draft: draft ?? false,
|
||||
overrideAccess: overrideAccess ?? false,
|
||||
req,
|
||||
showHiddenFields: showHiddenFields ?? false,
|
||||
})
|
||||
|
||||
return await convertLexicalToHTMLAsync({
|
||||
converters,
|
||||
data: lexicalFieldData,
|
||||
populate: htmlPopulateFn,
|
||||
})
|
||||
},
|
||||
],
|
||||
beforeChange: [
|
||||
({ siblingData, value }) => {
|
||||
if (storeInDB) {
|
||||
return value
|
||||
}
|
||||
delete siblingData[htmlFieldName]
|
||||
return null
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable no-console */
|
||||
import type { SerializedEditorState, SerializedLexicalNode } from 'lexical'
|
||||
|
||||
import type { SerializedLexicalNodeWithParent } from '../shared/types.js'
|
||||
import type {
|
||||
HTMLConverterAsync,
|
||||
HTMLConvertersAsync,
|
||||
HTMLConvertersFunctionAsync,
|
||||
HTMLPopulateFn,
|
||||
} from './types.js'
|
||||
|
||||
import { hasText } from '../../../../validate/hasText.js'
|
||||
import { findConverterForNode } from '../shared/findConverterForNode.js'
|
||||
import { defaultHTMLConvertersAsync } from './defaultConverters.js'
|
||||
|
||||
export type ConvertLexicalToHTMLAsyncArgs = {
|
||||
/**
|
||||
* Override class names for the container.
|
||||
*/
|
||||
className?: string
|
||||
converters?: HTMLConvertersAsync | HTMLConvertersFunctionAsync
|
||||
data: SerializedEditorState
|
||||
/**
|
||||
* If true, removes the container div wrapper.
|
||||
*/
|
||||
disableContainer?: boolean
|
||||
/**
|
||||
* If true, disables indentation globally. If an array, disables for specific node `type` values.
|
||||
*/
|
||||
disableIndent?: boolean | string[]
|
||||
/**
|
||||
* If true, disables text alignment globally. If an array, disables for specific node `type` values.
|
||||
*/
|
||||
disableTextAlign?: boolean | string[]
|
||||
populate?: HTMLPopulateFn
|
||||
}
|
||||
|
||||
export async function convertLexicalToHTMLAsync({
|
||||
className,
|
||||
converters,
|
||||
data,
|
||||
disableContainer,
|
||||
disableIndent,
|
||||
disableTextAlign,
|
||||
populate,
|
||||
}: ConvertLexicalToHTMLAsyncArgs): Promise<string> {
|
||||
if (hasText(data)) {
|
||||
let finalConverters: HTMLConvertersAsync = {}
|
||||
if (converters) {
|
||||
if (typeof converters === 'function') {
|
||||
finalConverters = converters({ defaultConverters: defaultHTMLConvertersAsync })
|
||||
} else {
|
||||
finalConverters = converters
|
||||
}
|
||||
} else {
|
||||
finalConverters = defaultHTMLConvertersAsync
|
||||
}
|
||||
|
||||
const html = (
|
||||
await convertLexicalNodesToHTMLAsync({
|
||||
converters: finalConverters,
|
||||
disableIndent,
|
||||
disableTextAlign,
|
||||
nodes: data?.root?.children,
|
||||
parent: data?.root,
|
||||
populate,
|
||||
})
|
||||
).join('')
|
||||
|
||||
if (disableContainer) {
|
||||
return html
|
||||
} else {
|
||||
return `<div class="${className ?? 'payload-richtext'}">${html}</div>`
|
||||
}
|
||||
}
|
||||
if (disableContainer) {
|
||||
return ''
|
||||
} else {
|
||||
return `<div class="${className ?? 'payload-richtext'}"></div>`
|
||||
}
|
||||
}
|
||||
|
||||
export async function convertLexicalNodesToHTMLAsync({
|
||||
converters,
|
||||
disableIndent,
|
||||
disableTextAlign,
|
||||
nodes,
|
||||
parent,
|
||||
populate,
|
||||
}: {
|
||||
converters: HTMLConvertersAsync
|
||||
disableIndent?: boolean | string[]
|
||||
disableTextAlign?: boolean | string[]
|
||||
nodes: SerializedLexicalNode[]
|
||||
parent: SerializedLexicalNodeWithParent
|
||||
populate?: HTMLPopulateFn
|
||||
}): Promise<string[]> {
|
||||
const unknownConverter: HTMLConverterAsync<any> = converters.unknown as HTMLConverterAsync<any>
|
||||
|
||||
const htmlArray: string[] = []
|
||||
|
||||
let i = -1
|
||||
for (const node of nodes) {
|
||||
i++
|
||||
const { converterForNode, providedCSSString, providedStyleTag } = findConverterForNode({
|
||||
converters,
|
||||
disableIndent,
|
||||
disableTextAlign,
|
||||
node,
|
||||
unknownConverter,
|
||||
})
|
||||
|
||||
try {
|
||||
let nodeHTML: string
|
||||
|
||||
if (converterForNode) {
|
||||
const converted =
|
||||
typeof converterForNode === 'function'
|
||||
? await converterForNode({
|
||||
childIndex: i,
|
||||
converters,
|
||||
node,
|
||||
populate,
|
||||
|
||||
nodesToHTML: async (args) => {
|
||||
return await convertLexicalNodesToHTMLAsync({
|
||||
converters: args.converters ?? converters,
|
||||
disableIndent: args.disableIndent ?? disableIndent,
|
||||
disableTextAlign: args.disableTextAlign ?? disableTextAlign,
|
||||
nodes: args.nodes,
|
||||
parent: args.parent ?? {
|
||||
...node,
|
||||
parent,
|
||||
},
|
||||
populate,
|
||||
})
|
||||
},
|
||||
parent,
|
||||
providedCSSString,
|
||||
providedStyleTag,
|
||||
})
|
||||
: converterForNode
|
||||
nodeHTML = converted
|
||||
} else {
|
||||
nodeHTML = '<span>unknown node</span>'
|
||||
}
|
||||
|
||||
htmlArray.push(nodeHTML)
|
||||
} catch (error) {
|
||||
console.error('Error converting lexical node to HTML:', error, 'node:', node)
|
||||
htmlArray.push('')
|
||||
}
|
||||
}
|
||||
|
||||
return htmlArray.filter(Boolean)
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import type { SerializedLexicalNode } from 'lexical'
|
||||
import type { SelectType, TypeWithID } from 'payload'
|
||||
|
||||
import type {
|
||||
DefaultNodeTypes,
|
||||
SerializedBlockNode,
|
||||
SerializedInlineBlockNode,
|
||||
} from '../../../../nodeTypes.js'
|
||||
import type { SerializedLexicalNodeWithParent } from '../shared/types.js'
|
||||
export type HTMLPopulateArguments = {
|
||||
collectionSlug: string
|
||||
id: number | string
|
||||
select?: SelectType
|
||||
}
|
||||
|
||||
export type HTMLPopulateFn = <TData extends object = TypeWithID>(
|
||||
args: HTMLPopulateArguments,
|
||||
) => Promise<TData | undefined>
|
||||
|
||||
export type HTMLConverterAsync<
|
||||
T extends { [key: string]: any; type?: string } = SerializedLexicalNode,
|
||||
> =
|
||||
| ((args: {
|
||||
childIndex: number
|
||||
converters: HTMLConvertersAsync
|
||||
node: T
|
||||
nodesToHTML: (args: {
|
||||
converters?: HTMLConvertersAsync
|
||||
disableIndent?: boolean | string[]
|
||||
disableTextAlign?: boolean | string[]
|
||||
nodes: SerializedLexicalNode[]
|
||||
parent?: SerializedLexicalNodeWithParent
|
||||
}) => Promise<string[]>
|
||||
parent: SerializedLexicalNodeWithParent
|
||||
populate?: HTMLPopulateFn
|
||||
providedCSSString: string
|
||||
providedStyleTag: string
|
||||
}) => Promise<string> | string)
|
||||
| string
|
||||
|
||||
export type HTMLConvertersAsync<
|
||||
T extends { [key: string]: any; type?: string } =
|
||||
| DefaultNodeTypes
|
||||
| SerializedBlockNode<{ blockName?: null | string; blockType: string }> // need these to ensure types for blocks and inlineBlocks work if no generics are provided
|
||||
| SerializedInlineBlockNode<{ blockName?: null | string; blockType: string }>, // need these to ensure types for blocks and inlineBlocks work if no generics are provided
|
||||
> = {
|
||||
[key: string]:
|
||||
| {
|
||||
[blockSlug: string]: HTMLConverterAsync<any>
|
||||
}
|
||||
| HTMLConverterAsync<any>
|
||||
| undefined
|
||||
} & {
|
||||
[nodeType in Exclude<NonNullable<T['type']>, 'block' | 'inlineBlock'>]?: HTMLConverterAsync<
|
||||
Extract<T, { type: nodeType }>
|
||||
>
|
||||
} & {
|
||||
blocks?: {
|
||||
[K in Extract<
|
||||
Extract<T, { type: 'block' }> extends SerializedBlockNode<infer B>
|
||||
? B extends { blockType: string }
|
||||
? B['blockType']
|
||||
: never
|
||||
: never,
|
||||
string
|
||||
>]?: HTMLConverterAsync<
|
||||
Extract<T, { type: 'block' }> extends SerializedBlockNode<infer B>
|
||||
? SerializedBlockNode<Extract<B, { blockType: K }>>
|
||||
: SerializedBlockNode
|
||||
>
|
||||
}
|
||||
inlineBlocks?: {
|
||||
[K in Extract<
|
||||
Extract<T, { type: 'inlineBlock' }> extends SerializedInlineBlockNode<infer B>
|
||||
? B extends { blockType: string }
|
||||
? B['blockType']
|
||||
: never
|
||||
: never,
|
||||
string
|
||||
>]?: HTMLConverterAsync<
|
||||
Extract<T, { type: 'inlineBlock' }> extends SerializedInlineBlockNode<infer B>
|
||||
? SerializedInlineBlockNode<Extract<B, { blockType: K }>>
|
||||
: SerializedInlineBlockNode
|
||||
>
|
||||
}
|
||||
}
|
||||
|
||||
export type HTMLConvertersFunctionAsync<
|
||||
T extends { [key: string]: any; type?: string } =
|
||||
| DefaultNodeTypes
|
||||
| SerializedBlockNode<{ blockName?: null | string }>
|
||||
| SerializedInlineBlockNode<{ blockName?: null | string; blockType: string }>,
|
||||
> = (args: { defaultConverters: HTMLConvertersAsync<DefaultNodeTypes> }) => HTMLConvertersAsync<T>
|
||||
@@ -0,0 +1,101 @@
|
||||
import type { SerializedLexicalNode } from 'lexical'
|
||||
|
||||
import type { SerializedBlockNode, SerializedInlineBlockNode } from '../../../../nodeTypes.js'
|
||||
import type { HTMLConverterAsync, HTMLConvertersAsync } from '../async/types.js'
|
||||
import type { HTMLConverter, HTMLConverters } from '../sync/types.js'
|
||||
import type { ProvidedCSS } from './types.js'
|
||||
|
||||
export function findConverterForNode<
|
||||
TConverters extends HTMLConverters | HTMLConvertersAsync,
|
||||
TConverter extends HTMLConverter | HTMLConverterAsync,
|
||||
>({
|
||||
converters,
|
||||
disableIndent,
|
||||
disableTextAlign,
|
||||
node,
|
||||
unknownConverter,
|
||||
}: {
|
||||
converters: TConverters
|
||||
disableIndent?: boolean | string[]
|
||||
disableTextAlign?: boolean | string[]
|
||||
node: SerializedLexicalNode
|
||||
unknownConverter: TConverter
|
||||
}): {
|
||||
converterForNode: TConverter | undefined
|
||||
providedCSSString: string
|
||||
providedStyleTag: string
|
||||
} {
|
||||
let converterForNode: TConverter | undefined
|
||||
if (node.type === 'block') {
|
||||
converterForNode = converters?.blocks?.[
|
||||
(node as SerializedBlockNode)?.fields?.blockType
|
||||
] as TConverter
|
||||
if (!converterForNode) {
|
||||
console.error(
|
||||
`Lexical => HTML converter: Blocks converter: found ${(node as SerializedBlockNode)?.fields?.blockType} block, but no converter is provided`,
|
||||
)
|
||||
}
|
||||
} else if (node.type === 'inlineBlock') {
|
||||
converterForNode = converters?.inlineBlocks?.[
|
||||
(node as SerializedInlineBlockNode)?.fields?.blockType
|
||||
] as TConverter
|
||||
if (!converterForNode) {
|
||||
console.error(
|
||||
`Lexical => HTML converter: Inline Blocks converter: found ${(node as SerializedInlineBlockNode)?.fields?.blockType} inline block, but no converter is provided`,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
converterForNode = converters[node.type] as TConverter
|
||||
}
|
||||
|
||||
const style: ProvidedCSS = {}
|
||||
|
||||
// Check if disableTextAlign is not true and does not include node type
|
||||
if (
|
||||
!disableTextAlign &&
|
||||
(!Array.isArray(disableTextAlign) || !disableTextAlign?.includes(node.type))
|
||||
) {
|
||||
if ('format' in node && node.format) {
|
||||
switch (node.format) {
|
||||
case 'center':
|
||||
style['text-align'] = 'center'
|
||||
break
|
||||
case 'end':
|
||||
style['text-align'] = 'right'
|
||||
break
|
||||
case 'justify':
|
||||
style['text-align'] = 'justify'
|
||||
break
|
||||
case 'left':
|
||||
//style['text-align'] = 'left'
|
||||
// Do nothing, as left is the default
|
||||
break
|
||||
case 'right':
|
||||
style['text-align'] = 'right'
|
||||
break
|
||||
case 'start':
|
||||
style['text-align'] = 'left'
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!disableIndent && (!Array.isArray(disableIndent) || !disableIndent?.includes(node.type))) {
|
||||
if ('indent' in node && node.indent && node.type !== 'listitem') {
|
||||
style['padding-inline-start'] = `${Number(node.indent) * 2}em`
|
||||
}
|
||||
}
|
||||
|
||||
let providedCSSString: string = ''
|
||||
for (const key of Object.keys(style)) {
|
||||
// @ts-expect-error we're iterating over the keys of the object
|
||||
providedCSSString += `${key}: ${style[key]};`
|
||||
}
|
||||
const providedStyleTag = providedCSSString?.length ? ` style="${providedCSSString}"` : ''
|
||||
|
||||
return {
|
||||
converterForNode: converterForNode ?? unknownConverter,
|
||||
providedCSSString,
|
||||
providedStyleTag,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { SerializedLexicalNode } from 'lexical'
|
||||
|
||||
export type ProvidedCSS = {
|
||||
'padding-inline-start'?: string
|
||||
'text-align'?: string
|
||||
}
|
||||
|
||||
export type SerializedLexicalNodeWithParent = {
|
||||
parent?: SerializedLexicalNode
|
||||
} & SerializedLexicalNode
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { SerializedQuoteNode } from '../../../../../nodeTypes.js'
|
||||
import type { HTMLConverters } from '../types.js'
|
||||
|
||||
export const BlockquoteHTMLConverter: HTMLConverters<SerializedQuoteNode> = {
|
||||
quote: ({ node, nodesToHTML, providedStyleTag }) => {
|
||||
const children = nodesToHTML({
|
||||
nodes: node.children,
|
||||
}).join('')
|
||||
|
||||
return `<blockquote${providedStyleTag}>${children}</blockquote>`
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { SerializedHeadingNode } from '../../../../../nodeTypes.js'
|
||||
import type { HTMLConverters } from '../types.js'
|
||||
|
||||
export const HeadingHTMLConverter: HTMLConverters<SerializedHeadingNode> = {
|
||||
heading: ({ node, nodesToHTML, providedStyleTag }) => {
|
||||
const children = nodesToHTML({
|
||||
nodes: node.children,
|
||||
}).join('')
|
||||
|
||||
return `<${node.tag}${providedStyleTag}>${children}</${node.tag}>`
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { SerializedHorizontalRuleNode } from '../../../../../nodeTypes.js'
|
||||
import type { HTMLConverters } from '../types.js'
|
||||
export const HorizontalRuleHTMLConverter: HTMLConverters<SerializedHorizontalRuleNode> = {
|
||||
horizontalrule: '<hr />',
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { SerializedLineBreakNode } from '../../../../../nodeTypes.js'
|
||||
import type { HTMLConverters } from '../types.js'
|
||||
|
||||
export const LinebreakHTMLConverter: HTMLConverters<SerializedLineBreakNode> = {
|
||||
linebreak: '<br />',
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import type { SerializedAutoLinkNode, SerializedLinkNode } from '../../../../../nodeTypes.js'
|
||||
import type { HTMLConverters } from '../types.js'
|
||||
|
||||
export const LinkHTMLConverter: (args: {
|
||||
internalDocToHref?: (args: { linkNode: SerializedLinkNode }) => string
|
||||
}) => HTMLConverters<SerializedAutoLinkNode | SerializedLinkNode> = ({ internalDocToHref }) => ({
|
||||
autolink: ({ node, nodesToHTML, providedStyleTag }) => {
|
||||
const children = nodesToHTML({
|
||||
nodes: node.children,
|
||||
}).join('')
|
||||
|
||||
const rel: string | undefined = node.fields.newTab ? 'noopener noreferrer' : undefined
|
||||
const target: string | undefined = node.fields.newTab ? '_blank' : undefined
|
||||
|
||||
return `(
|
||||
<a${providedStyleTag} href="${node.fields.url}" rel=${rel} target=${target}>
|
||||
${children}
|
||||
</a>
|
||||
)`
|
||||
},
|
||||
link: ({ node, nodesToHTML, providedStyleTag }) => {
|
||||
const children = nodesToHTML({
|
||||
nodes: node.children,
|
||||
}).join('')
|
||||
|
||||
const rel: string | undefined = node.fields.newTab ? 'noopener noreferrer' : undefined
|
||||
const target: string | undefined = node.fields.newTab ? '_blank' : undefined
|
||||
|
||||
let href: string = node.fields.url ?? ''
|
||||
if (node.fields.linkType === 'internal') {
|
||||
if (internalDocToHref) {
|
||||
href = internalDocToHref({ linkNode: node })
|
||||
} else {
|
||||
console.error(
|
||||
'Lexical => HTML converter: Link converter: found internal link, but internalDocToHref is not provided',
|
||||
)
|
||||
href = '#' // fallback
|
||||
}
|
||||
}
|
||||
|
||||
return `(
|
||||
<a${providedStyleTag} href="${href}" rel=${rel} target=${target}>
|
||||
${children}
|
||||
</a>
|
||||
)`
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,47 @@
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
import type { SerializedListItemNode, SerializedListNode } from '../../../../../nodeTypes.js'
|
||||
import type { HTMLConverters } from '../types.js'
|
||||
|
||||
export const ListHTMLConverter: HTMLConverters<SerializedListItemNode | SerializedListNode> = {
|
||||
list: ({ node, nodesToHTML, providedStyleTag }) => {
|
||||
const children = nodesToHTML({
|
||||
nodes: node.children,
|
||||
}).join('')
|
||||
|
||||
return `<${node.tag}${providedStyleTag} class="list-${node.listType}">${children}</${node.tag}>`
|
||||
},
|
||||
listitem: ({ node, nodesToHTML, parent, providedCSSString }) => {
|
||||
const hasSubLists = node.children.some((child) => child.type === 'list')
|
||||
|
||||
const children = nodesToHTML({
|
||||
nodes: node.children,
|
||||
}).join('')
|
||||
|
||||
if ('listType' in parent && parent?.listType === 'check') {
|
||||
const uuid = uuidv4()
|
||||
return `<li
|
||||
aria-checked="${node.checked ? 'true' : 'false'}"
|
||||
class="list-item-checkbox${node.checked ? ' list-item-checkbox-checked' : ' list-item-checkbox-unchecked'}${hasSubLists ? ' nestedListItem' : ''}"
|
||||
role="checkbox"
|
||||
style="list-style-type: none;${providedCSSString}"
|
||||
tabIndex="-1"
|
||||
value="${node.value}"
|
||||
>
|
||||
${
|
||||
hasSubLists
|
||||
? children
|
||||
: `<input checked="${node.checked}" id="${uuid}" readOnly="true" type="checkbox" />
|
||||
<label htmlFor="${uuid}">${children}</label>
|
||||
<br />`
|
||||
}
|
||||
</li>`
|
||||
} else {
|
||||
return `<li
|
||||
class="${hasSubLists ? 'nestedListItem' : ''}"
|
||||
style="${hasSubLists ? `list-style-type: none;${providedCSSString}` : providedCSSString}"
|
||||
value="${node.value}"
|
||||
>${children}</li>`
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { SerializedParagraphNode } from '../../../../../nodeTypes.js'
|
||||
import type { HTMLConverters } from '../types.js'
|
||||
|
||||
export const ParagraphHTMLConverter: HTMLConverters<SerializedParagraphNode> = {
|
||||
paragraph: ({ node, nodesToHTML, providedStyleTag }) => {
|
||||
const children = nodesToHTML({
|
||||
nodes: node.children,
|
||||
})
|
||||
|
||||
if (!children?.length) {
|
||||
return `<p${providedStyleTag}><br /></p>`
|
||||
}
|
||||
|
||||
return `<p${providedStyleTag}>${children.join('')}</p>`
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { SerializedTabNode } from '../../../../../nodeTypes.js'
|
||||
import type { HTMLConverters } from '../types.js'
|
||||
|
||||
export const TabHTMLConverter: HTMLConverters<SerializedTabNode> = {
|
||||
tab: '\t',
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import type {
|
||||
SerializedTableCellNode,
|
||||
SerializedTableNode,
|
||||
SerializedTableRowNode,
|
||||
} from '../../../../../nodeTypes.js'
|
||||
import type { HTMLConverters } from '../types.js'
|
||||
|
||||
export const TableHTMLConverter: HTMLConverters<
|
||||
SerializedTableCellNode | SerializedTableNode | SerializedTableRowNode
|
||||
> = {
|
||||
table: ({ node, nodesToHTML, providedStyleTag }) => {
|
||||
const children = nodesToHTML({
|
||||
nodes: node.children,
|
||||
}).join('')
|
||||
|
||||
return `<div${providedStyleTag} class="lexical-table-container">
|
||||
<table class="lexical-table" style="border-collapse: collapse;">
|
||||
<tbody>${children}</tbody>
|
||||
</table>
|
||||
</div>`
|
||||
},
|
||||
|
||||
tablecell: ({ node, nodesToHTML, providedCSSString }) => {
|
||||
const children = nodesToHTML({
|
||||
nodes: node.children,
|
||||
}).join('')
|
||||
|
||||
const TagName = node.headerState > 0 ? 'th' : 'td'
|
||||
const headerStateClass = `lexical-table-cell-header-${node.headerState}`
|
||||
|
||||
let style = 'border: 1px solid #ccc; padding: 8px;' + providedCSSString
|
||||
if (node.backgroundColor) {
|
||||
style += ` background-color: ${node.backgroundColor};`
|
||||
}
|
||||
|
||||
const colSpanAttr = node.colSpan && node.colSpan > 1 ? ` colspan="${node.colSpan}"` : ''
|
||||
const rowSpanAttr = node.rowSpan && node.rowSpan > 1 ? ` rowspan="${node.rowSpan}"` : ''
|
||||
|
||||
return `<${TagName}
|
||||
class="lexical-table-cell ${headerStateClass}"
|
||||
${colSpanAttr}
|
||||
${rowSpanAttr}
|
||||
style="${style}"
|
||||
>
|
||||
${children}
|
||||
</${TagName}>
|
||||
`
|
||||
},
|
||||
|
||||
tablerow: ({ node, nodesToHTML, providedStyleTag }) => {
|
||||
const children = nodesToHTML({
|
||||
nodes: node.children,
|
||||
}).join('')
|
||||
|
||||
return `<tr${providedStyleTag} class="lexical-table-row">
|
||||
${children}
|
||||
</tr>`
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { SerializedTextNode } from '../../../../../nodeTypes.js'
|
||||
import type { HTMLConverters } from '../types.js'
|
||||
|
||||
import { NodeFormat } from '../../../../../lexical/utils/nodeFormat.js'
|
||||
|
||||
export const TextHTMLConverter: HTMLConverters<SerializedTextNode> = {
|
||||
text: ({ 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
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import type { FileData, FileSizeImproved, TypeWithID } from 'payload'
|
||||
|
||||
import type { SerializedUploadNode } from '../../../../../nodeTypes.js'
|
||||
import type { UploadDataImproved } from '../../../../upload/server/nodes/UploadNode.js'
|
||||
import type { HTMLConverters } from '../types.js'
|
||||
|
||||
export const UploadHTMLConverter: HTMLConverters<SerializedUploadNode> = {
|
||||
upload: ({ node, providedStyleTag }) => {
|
||||
const uploadNode = node as UploadDataImproved
|
||||
|
||||
let uploadDoc: (FileData & TypeWithID) | undefined = undefined
|
||||
|
||||
// If there's no valid upload data, populate return an empty string
|
||||
if (typeof uploadNode.value !== 'object') {
|
||||
return ''
|
||||
} else {
|
||||
uploadDoc = uploadNode.value as unknown as FileData & TypeWithID
|
||||
}
|
||||
|
||||
if (!uploadDoc) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const url = uploadDoc.url
|
||||
|
||||
// 1) If upload is NOT an image, return a link
|
||||
if (!uploadDoc.mimeType.startsWith('image')) {
|
||||
return `<a${providedStyleTag} href="${url}" rel="noopener noreferrer">${uploadDoc.filename}</a$>`
|
||||
}
|
||||
|
||||
// 2) If image has no different sizes, return a simple <img />
|
||||
if (!uploadDoc.sizes || !Object.keys(uploadDoc.sizes).length) {
|
||||
return `
|
||||
<img${providedStyleTag}
|
||||
alt="${uploadDoc.filename}"
|
||||
height="${uploadDoc.height}"
|
||||
src="${url}"
|
||||
width="${uploadDoc.width}"
|
||||
/>
|
||||
`
|
||||
}
|
||||
|
||||
// 3) If image has different sizes, build a <picture> element with <source> tags
|
||||
let pictureHTML = ''
|
||||
|
||||
for (const size in uploadDoc.sizes) {
|
||||
const imageSize = uploadDoc.sizes[size] as FileSizeImproved
|
||||
|
||||
if (
|
||||
!imageSize ||
|
||||
!imageSize.width ||
|
||||
!imageSize.height ||
|
||||
!imageSize.mimeType ||
|
||||
!imageSize.filesize ||
|
||||
!imageSize.filename ||
|
||||
!imageSize.url
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
pictureHTML += `
|
||||
<source
|
||||
media="(max-width: ${imageSize.width}px)"
|
||||
srcset="${imageSize.url}"
|
||||
type="${imageSize.mimeType}"
|
||||
/>
|
||||
`
|
||||
}
|
||||
|
||||
pictureHTML += `
|
||||
<img
|
||||
alt="${uploadDoc.filename}"
|
||||
height="${uploadDoc.height}"
|
||||
src="${url}"
|
||||
width="${uploadDoc.width}"
|
||||
/>
|
||||
`
|
||||
|
||||
return `<picture${providedStyleTag}>${pictureHTML}</picture$>`
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { DefaultNodeTypes } from '../../../../nodeTypes.js'
|
||||
import type { HTMLConverters } from './types.js'
|
||||
|
||||
import { BlockquoteHTMLConverter } from './converters/blockquote.js'
|
||||
import { HeadingHTMLConverter } from './converters/heading.js'
|
||||
import { HorizontalRuleHTMLConverter } from './converters/horizontalRule.js'
|
||||
import { LinebreakHTMLConverter } from './converters/linebreak.js'
|
||||
import { LinkHTMLConverter } from './converters/link.js'
|
||||
import { ListHTMLConverter } from './converters/list.js'
|
||||
import { ParagraphHTMLConverter } from './converters/paragraph.js'
|
||||
import { TabHTMLConverter } from './converters/tab.js'
|
||||
import { TableHTMLConverter } from './converters/table.js'
|
||||
import { TextHTMLConverter } from './converters/text.js'
|
||||
import { UploadHTMLConverter } from './converters/upload.js'
|
||||
|
||||
export const defaultHTMLConverters: HTMLConverters<DefaultNodeTypes> = {
|
||||
...ParagraphHTMLConverter,
|
||||
...TextHTMLConverter,
|
||||
...LinebreakHTMLConverter,
|
||||
...BlockquoteHTMLConverter,
|
||||
...TableHTMLConverter,
|
||||
...HeadingHTMLConverter,
|
||||
...HorizontalRuleHTMLConverter,
|
||||
...ListHTMLConverter,
|
||||
...LinkHTMLConverter({}),
|
||||
...UploadHTMLConverter,
|
||||
...TabHTMLConverter,
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable no-console */
|
||||
import type { SerializedEditorState, SerializedLexicalNode } from 'lexical'
|
||||
|
||||
import type { SerializedLexicalNodeWithParent } from '../shared/types.js'
|
||||
import type { HTMLConverter, HTMLConverters, HTMLConvertersFunction } from './types.js'
|
||||
|
||||
import { hasText } from '../../../../validate/hasText.js'
|
||||
import { findConverterForNode } from '../shared/findConverterForNode.js'
|
||||
import { defaultHTMLConverters } from './defaultConverters.js'
|
||||
|
||||
export type ConvertLexicalToHTMLArgs = {
|
||||
/**
|
||||
* Override class names for the container.
|
||||
*/
|
||||
className?: string
|
||||
converters?: HTMLConverters | HTMLConvertersFunction
|
||||
data: SerializedEditorState
|
||||
/**
|
||||
* If true, removes the container div wrapper.
|
||||
*/
|
||||
disableContainer?: boolean
|
||||
/**
|
||||
* If true, disables indentation globally. If an array, disables for specific node `type` values.
|
||||
*/
|
||||
disableIndent?: boolean | string[]
|
||||
/**
|
||||
* If true, disables text alignment globally. If an array, disables for specific node `type` values.
|
||||
*/
|
||||
disableTextAlign?: boolean | string[]
|
||||
}
|
||||
|
||||
export function convertLexicalToHTML({
|
||||
className,
|
||||
converters,
|
||||
data,
|
||||
disableContainer,
|
||||
disableIndent,
|
||||
disableTextAlign,
|
||||
}: ConvertLexicalToHTMLArgs): string {
|
||||
if (hasText(data)) {
|
||||
let finalConverters: HTMLConverters = {}
|
||||
if (converters) {
|
||||
if (typeof converters === 'function') {
|
||||
finalConverters = converters({ defaultConverters: defaultHTMLConverters })
|
||||
} else {
|
||||
finalConverters = converters
|
||||
}
|
||||
} else {
|
||||
finalConverters = defaultHTMLConverters
|
||||
}
|
||||
|
||||
const html = convertLexicalNodesToHTML({
|
||||
converters: finalConverters,
|
||||
disableIndent,
|
||||
disableTextAlign,
|
||||
nodes: data?.root?.children,
|
||||
parent: data?.root,
|
||||
}).join('')
|
||||
|
||||
if (disableContainer) {
|
||||
return html
|
||||
} else {
|
||||
return `<div class="${className ?? 'payload-richtext'}">${html}</div>`
|
||||
}
|
||||
}
|
||||
if (disableContainer) {
|
||||
return ''
|
||||
} else {
|
||||
return `<div class="${className ?? 'payload-richtext'}"></div>`
|
||||
}
|
||||
}
|
||||
|
||||
export function convertLexicalNodesToHTML({
|
||||
converters,
|
||||
disableIndent,
|
||||
disableTextAlign,
|
||||
nodes,
|
||||
parent,
|
||||
}: {
|
||||
converters: HTMLConverters
|
||||
disableIndent?: boolean | string[]
|
||||
disableTextAlign?: boolean | string[]
|
||||
nodes: SerializedLexicalNode[]
|
||||
parent: SerializedLexicalNodeWithParent
|
||||
}): string[] {
|
||||
const unknownConverter: HTMLConverter<any> = converters.unknown as HTMLConverter<any>
|
||||
|
||||
const htmlArray: string[] = []
|
||||
|
||||
let i = -1
|
||||
for (const node of nodes) {
|
||||
i++
|
||||
const { converterForNode, providedCSSString, providedStyleTag } = findConverterForNode({
|
||||
converters,
|
||||
disableIndent,
|
||||
disableTextAlign,
|
||||
node,
|
||||
unknownConverter,
|
||||
})
|
||||
|
||||
try {
|
||||
let nodeHTML: string
|
||||
|
||||
if (converterForNode) {
|
||||
const converted =
|
||||
typeof converterForNode === 'function'
|
||||
? converterForNode({
|
||||
childIndex: i,
|
||||
converters,
|
||||
node,
|
||||
nodesToHTML: (args) => {
|
||||
return convertLexicalNodesToHTML({
|
||||
converters: args.converters ?? converters,
|
||||
disableIndent: args.disableIndent ?? disableIndent,
|
||||
disableTextAlign: args.disableTextAlign ?? disableTextAlign,
|
||||
nodes: args.nodes,
|
||||
parent: args.parent ?? {
|
||||
...node,
|
||||
parent,
|
||||
},
|
||||
})
|
||||
},
|
||||
parent,
|
||||
providedCSSString,
|
||||
providedStyleTag,
|
||||
})
|
||||
: converterForNode
|
||||
nodeHTML = converted
|
||||
} else {
|
||||
nodeHTML = '<span>unknown node</span>'
|
||||
}
|
||||
|
||||
htmlArray.push(nodeHTML)
|
||||
} catch (error) {
|
||||
console.error('Error converting lexical node to HTML:', error, 'node:', node)
|
||||
htmlArray.push('')
|
||||
}
|
||||
}
|
||||
|
||||
return htmlArray.filter(Boolean)
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import type { SerializedLexicalNode } from 'lexical'
|
||||
|
||||
import type {
|
||||
DefaultNodeTypes,
|
||||
SerializedBlockNode,
|
||||
SerializedInlineBlockNode,
|
||||
} from '../../../../nodeTypes.js'
|
||||
import type { SerializedLexicalNodeWithParent } from '../shared/types.js'
|
||||
|
||||
export type HTMLConverter<T extends { [key: string]: any; type?: string } = SerializedLexicalNode> =
|
||||
|
||||
| ((args: {
|
||||
childIndex: number
|
||||
converters: HTMLConverters
|
||||
node: T
|
||||
nodesToHTML: (args: {
|
||||
converters?: HTMLConverters
|
||||
disableIndent?: boolean | string[]
|
||||
disableTextAlign?: boolean | string[]
|
||||
nodes: SerializedLexicalNode[]
|
||||
parent?: SerializedLexicalNodeWithParent
|
||||
}) => string[]
|
||||
parent: SerializedLexicalNodeWithParent
|
||||
providedCSSString: string
|
||||
providedStyleTag: string
|
||||
}) => string)
|
||||
| string
|
||||
|
||||
export type HTMLConverters<
|
||||
T extends { [key: string]: any; type?: string } =
|
||||
| DefaultNodeTypes
|
||||
| SerializedBlockNode<{ blockName?: null | string; blockType: string }> // need these to ensure types for blocks and inlineBlocks work if no generics are provided
|
||||
| SerializedInlineBlockNode<{ blockName?: null | string; blockType: string }>, // need these to ensure types for blocks and inlineBlocks work if no generics are provided
|
||||
> = {
|
||||
[key: string]:
|
||||
| {
|
||||
[blockSlug: string]: HTMLConverter<any>
|
||||
}
|
||||
| HTMLConverter<any>
|
||||
| undefined
|
||||
} & {
|
||||
[nodeType in Exclude<NonNullable<T['type']>, 'block' | 'inlineBlock'>]?: HTMLConverter<
|
||||
Extract<T, { type: nodeType }>
|
||||
>
|
||||
} & {
|
||||
blocks?: {
|
||||
[K in Extract<
|
||||
Extract<T, { type: 'block' }> extends SerializedBlockNode<infer B>
|
||||
? B extends { blockType: string }
|
||||
? B['blockType']
|
||||
: never
|
||||
: never,
|
||||
string
|
||||
>]?: HTMLConverter<
|
||||
Extract<T, { type: 'block' }> extends SerializedBlockNode<infer B>
|
||||
? SerializedBlockNode<Extract<B, { blockType: K }>>
|
||||
: SerializedBlockNode
|
||||
>
|
||||
}
|
||||
inlineBlocks?: {
|
||||
[K in Extract<
|
||||
Extract<T, { type: 'inlineBlock' }> extends SerializedInlineBlockNode<infer B>
|
||||
? B extends { blockType: string }
|
||||
? B['blockType']
|
||||
: never
|
||||
: never,
|
||||
string
|
||||
>]?: HTMLConverter<
|
||||
Extract<T, { type: 'inlineBlock' }> extends SerializedInlineBlockNode<infer B>
|
||||
? SerializedInlineBlockNode<Extract<B, { blockType: K }>>
|
||||
: SerializedInlineBlockNode
|
||||
>
|
||||
}
|
||||
}
|
||||
|
||||
export type HTMLConvertersFunction<
|
||||
T extends { [key: string]: any; type?: string } =
|
||||
| DefaultNodeTypes
|
||||
| SerializedBlockNode<{ blockName?: null | string }>
|
||||
| SerializedInlineBlockNode<{ blockName?: null | string; blockType: string }>,
|
||||
> = (args: { defaultConverters: HTMLConverters<DefaultNodeTypes> }) => HTMLConverters<T>
|
||||
@@ -5,6 +5,9 @@ import { ParagraphHTMLConverter } from './converters/paragraph.js'
|
||||
import { TabHTMLConverter } from './converters/tab.js'
|
||||
import { TextHTMLConverter } from './converters/text.js'
|
||||
|
||||
/**
|
||||
* @deprecated - will be removed in 4.0
|
||||
*/
|
||||
export const defaultHTMLConverters: HTMLConverter<any>[] = [
|
||||
ParagraphHTMLConverter,
|
||||
TextHTMLConverter,
|
||||
@@ -7,6 +7,9 @@ import type { HTMLConverter, SerializedLexicalNodeWithParent } from './types.js'
|
||||
|
||||
import { hasText } from '../../../../validate/hasText.js'
|
||||
|
||||
/**
|
||||
* @deprecated - will be removed in 4.0
|
||||
*/
|
||||
export type ConvertLexicalToHTMLArgs = {
|
||||
converters: HTMLConverter[]
|
||||
currentDepth?: number
|
||||
@@ -44,6 +47,9 @@ export type ConvertLexicalToHTMLArgs = {
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* @deprecated - will be removed in 4.0
|
||||
*/
|
||||
export async function convertLexicalToHTML({
|
||||
converters,
|
||||
currentDepth,
|
||||
@@ -83,6 +89,9 @@ export async function convertLexicalToHTML({
|
||||
return ''
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated - will be removed in 4.0
|
||||
*/
|
||||
export async function convertLexicalNodesToHTML({
|
||||
converters,
|
||||
currentDepth,
|
||||
@@ -1,6 +1,9 @@
|
||||
import type { SerializedLexicalNode } from 'lexical'
|
||||
import type { PayloadRequest } from 'payload'
|
||||
|
||||
/**
|
||||
* @deprecated - will be removed in 4.0
|
||||
*/
|
||||
export type HTMLConverter<T extends SerializedLexicalNode = SerializedLexicalNode> = {
|
||||
converter: (args: {
|
||||
childIndex: number
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { SerializedEditorState } from 'lexical'
|
||||
import type { Field, FieldAffectingData, PayloadRequest, RichTextField } from 'payload'
|
||||
import type { Field, RichTextField } from 'payload'
|
||||
|
||||
import type { SanitizedServerEditorConfig } from '../../../../lexical/config/types.js'
|
||||
import type { AdapterProps, LexicalRichTextAdapter } from '../../../../types.js'
|
||||
@@ -9,7 +9,7 @@ import type { HTMLConverterFeatureProps } from '../index.js'
|
||||
import { defaultHTMLConverters } from '../converter/defaultConverters.js'
|
||||
import { convertLexicalToHTML } from '../converter/index.js'
|
||||
|
||||
type Props = {
|
||||
type Args = {
|
||||
/**
|
||||
* Whether the lexicalHTML field should be hidden in the admin panel
|
||||
*
|
||||
@@ -28,6 +28,7 @@ type Props = {
|
||||
/**
|
||||
* Combines the default HTML converters with HTML converters found in the features, and with HTML converters configured in the htmlConverter feature.
|
||||
*
|
||||
* @deprecated - will be removed in 4.0
|
||||
* @param editorConfig
|
||||
*/
|
||||
export const consolidateHTMLConverters = ({
|
||||
@@ -83,16 +84,19 @@ export const consolidateHTMLConverters = ({
|
||||
return filteredConverters
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated - will be removed in 4.0
|
||||
*/
|
||||
export const lexicalHTML: (
|
||||
/**
|
||||
* A string which matches the lexical field name you want to convert to HTML.
|
||||
*
|
||||
* This has to be a SIBLING field of this lexicalHTML field - otherwise, it won't be able to find the lexical field.
|
||||
* This has to be a sibling field of this lexicalHTML field - otherwise, it won't be able to find the lexical field.
|
||||
**/
|
||||
lexicalFieldName: string,
|
||||
props: Props,
|
||||
) => Field = (lexicalFieldName, props) => {
|
||||
const { name = 'lexicalHTML', hidden = true, storeInDB = false } = props
|
||||
args: Args,
|
||||
) => Field = (lexicalFieldName, args) => {
|
||||
const { name = 'lexicalHTML', hidden = true, storeInDB = false } = args
|
||||
return {
|
||||
name,
|
||||
type: 'code',
|
||||
@@ -9,6 +9,9 @@ export type HTMLConverterFeatureProps = {
|
||||
}
|
||||
|
||||
// This is just used to save the props on the richText field
|
||||
/**
|
||||
* @deprecated - will be removed in 4.0
|
||||
*/
|
||||
export const HTMLConverterFeature = createServerFeature<HTMLConverterFeatureProps>({
|
||||
feature: {},
|
||||
key: 'htmlConverter',
|
||||
@@ -7,10 +7,10 @@ import type {
|
||||
SerializedBlockNode,
|
||||
SerializedInlineBlockNode,
|
||||
} from '../../../../nodeTypes.js'
|
||||
import type { JSXConverters } from './converter/types.js'
|
||||
import type { JSXConverters } from '../converter/types.js'
|
||||
|
||||
import { defaultJSXConverters } from './converter/defaultConverters.js'
|
||||
import { convertLexicalToJSX } from './converter/index.js'
|
||||
import { defaultJSXConverters } from '../converter/defaultConverters.js'
|
||||
import { convertLexicalToJSX } from '../converter/index.js'
|
||||
|
||||
export type JSXConvertersFunction<
|
||||
T extends { [key: string]: any; type?: string } =
|
||||
@@ -21,7 +21,7 @@ export type JSXConvertersFunction<
|
||||
|
||||
type RichTextProps = {
|
||||
/**
|
||||
* Additional class names for the container.
|
||||
* Override class names for the container.
|
||||
*/
|
||||
className?: string
|
||||
/**
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { SerializedQuoteNode } from '../../../../../../nodeTypes.js'
|
||||
import type { SerializedQuoteNode } from '../../../../../nodeTypes.js'
|
||||
import type { JSXConverters } from '../types.js'
|
||||
|
||||
export const BlockquoteJSXConverter: JSXConverters<SerializedQuoteNode> = {
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { SerializedHeadingNode } from '../../../../../../nodeTypes.js'
|
||||
import type { SerializedHeadingNode } from '../../../../../nodeTypes.js'
|
||||
import type { JSXConverters } from '../types.js'
|
||||
|
||||
export const HeadingJSXConverter: JSXConverters<SerializedHeadingNode> = {
|
||||
@@ -1,7 +1,5 @@
|
||||
import type { SerializedHorizontalRuleNode } from '../../../../../../nodeTypes.js'
|
||||
import type { SerializedHorizontalRuleNode } from '../../../../../nodeTypes.js'
|
||||
import type { JSXConverters } from '../types.js'
|
||||
export const HorizontalRuleJSXConverter: JSXConverters<SerializedHorizontalRuleNode> = {
|
||||
horizontalrule: () => {
|
||||
return <hr />
|
||||
},
|
||||
horizontalrule: <hr />,
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
import type { SerializedLineBreakNode } from '../../../../../../nodeTypes.js'
|
||||
import type { SerializedLineBreakNode } from '../../../../../nodeTypes.js'
|
||||
import type { JSXConverters } from '../types.js'
|
||||
|
||||
export const LinebreakJSXConverter: JSXConverters<SerializedLineBreakNode> = {
|
||||
linebreak: () => {
|
||||
return <br />
|
||||
},
|
||||
linebreak: <br />,
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { SerializedAutoLinkNode, SerializedLinkNode } from '../../../../../../nodeTypes.js'
|
||||
import type { SerializedAutoLinkNode, SerializedLinkNode } from '../../../../../nodeTypes.js'
|
||||
import type { JSXConverters } from '../types.js'
|
||||
|
||||
export const LinkJSXConverter: (args: {
|
||||
@@ -1,6 +1,6 @@
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
import type { SerializedListItemNode, SerializedListNode } from '../../../../../../nodeTypes.js'
|
||||
import type { SerializedListItemNode, SerializedListNode } from '../../../../../nodeTypes.js'
|
||||
import type { JSXConverters } from '../types.js'
|
||||
|
||||
export const ListJSXConverter: JSXConverters<SerializedListItemNode | SerializedListNode> = {
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { SerializedParagraphNode } from '../../../../../../nodeTypes.js'
|
||||
import type { SerializedParagraphNode } from '../../../../../nodeTypes.js'
|
||||
import type { JSXConverters } from '../types.js'
|
||||
|
||||
export const ParagraphJSXConverter: JSXConverters<SerializedParagraphNode> = {
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { SerializedTabNode } from '../../../../../nodeTypes.js'
|
||||
import type { JSXConverters } from '../types.js'
|
||||
|
||||
export const TabJSXConverter: JSXConverters<SerializedTabNode> = {
|
||||
tab: '\t',
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import type {
|
||||
SerializedTableCellNode,
|
||||
SerializedTableNode,
|
||||
SerializedTableRowNode,
|
||||
} from '../../../../../../nodeTypes.js'
|
||||
} from '../../../../../nodeTypes.js'
|
||||
import type { JSXConverters } from '../types.js'
|
||||
|
||||
export const TableJSXConverter: JSXConverters<
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from 'react'
|
||||
|
||||
import type { SerializedTextNode } from '../../../../../../nodeTypes.js'
|
||||
import type { SerializedTextNode } from '../../../../../nodeTypes.js'
|
||||
import type { JSXConverters } from '../types.js'
|
||||
|
||||
import { NodeFormat } from '../../../../../../lexical/utils/nodeFormat.js'
|
||||
import { NodeFormat } from '../../../../../lexical/utils/nodeFormat.js'
|
||||
|
||||
export const TextJSXConverter: JSXConverters<SerializedTextNode> = {
|
||||
text: ({ node }) => {
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { FileData, FileSizeImproved, TypeWithID } from 'payload'
|
||||
|
||||
import type { UploadDataImproved } from '../../../../../../features/upload/server/nodes/UploadNode.js'
|
||||
import type { SerializedUploadNode } from '../../../../../../nodeTypes.js'
|
||||
import type { SerializedUploadNode } from '../../../../../nodeTypes.js'
|
||||
import type { UploadDataImproved } from '../../../../upload/server/nodes/UploadNode.js'
|
||||
import type { JSXConverters } from '../types.js'
|
||||
|
||||
export const UploadJSXConverter: JSXConverters<SerializedUploadNode> = {
|
||||
@@ -12,7 +12,9 @@ export const UploadJSXConverter: JSXConverters<SerializedUploadNode> = {
|
||||
return null
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
||||
const uploadDoc = uploadNode.value as FileData & TypeWithID
|
||||
|
||||
const url = uploadDoc.url
|
||||
|
||||
/**
|
||||
@@ -29,7 +31,7 @@ export const UploadJSXConverter: JSXConverters<SerializedUploadNode> = {
|
||||
/**
|
||||
* If the upload is a simple image with no different sizes, return a simple img tag
|
||||
*/
|
||||
if (!Object.keys(uploadDoc.sizes).length) {
|
||||
if (!uploadDoc.sizes || !Object.keys(uploadDoc.sizes).length) {
|
||||
return (
|
||||
<img alt={uploadDoc.filename} height={uploadDoc.height} src={url} width={uploadDoc.width} />
|
||||
)
|
||||
@@ -64,7 +66,7 @@ export const UploadJSXConverter: JSXConverters<SerializedUploadNode> = {
|
||||
media={`(max-width: ${imageSize.width}px)`}
|
||||
srcSet={imageSizeURL}
|
||||
type={imageSize.mimeType}
|
||||
></source>,
|
||||
/>,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { DefaultNodeTypes } from '../../../../../nodeTypes.js'
|
||||
import type { DefaultNodeTypes } from '../../../../nodeTypes.js'
|
||||
import type { JSXConverters } from './types.js'
|
||||
|
||||
import { BlockquoteJSXConverter } from './converters/blockquote.js'
|
||||
@@ -2,12 +2,12 @@ import type { SerializedEditorState, SerializedLexicalNode } from 'lexical'
|
||||
|
||||
import React from 'react'
|
||||
|
||||
import type { SerializedBlockNode, SerializedInlineBlockNode } from '../../../../../nodeTypes.js'
|
||||
import type { SerializedBlockNode, SerializedInlineBlockNode } from '../../../../nodeTypes.js'
|
||||
import type { JSXConverter, JSXConverters, SerializedLexicalNodeWithParent } from './types.js'
|
||||
|
||||
import { hasText } from '../../../../../validate/hasText.js'
|
||||
import { hasText } from '../../../../validate/hasText.js'
|
||||
|
||||
export type ConvertLexicalToHTMLArgs = {
|
||||
export type ConvertLexicalToJSXArgs = {
|
||||
converters: JSXConverters
|
||||
data: SerializedEditorState
|
||||
disableIndent?: boolean | string[]
|
||||
@@ -19,7 +19,7 @@ export function convertLexicalToJSX({
|
||||
data,
|
||||
disableIndent,
|
||||
disableTextAlign,
|
||||
}: ConvertLexicalToHTMLArgs): React.ReactNode {
|
||||
}: ConvertLexicalToJSXArgs): React.ReactNode {
|
||||
if (hasText(data)) {
|
||||
return convertLexicalNodesToJSX({
|
||||
converters,
|
||||
@@ -69,31 +69,15 @@ export function convertLexicalNodesToJSX({
|
||||
}
|
||||
|
||||
try {
|
||||
if (!converterForNode) {
|
||||
if (unknownConverter) {
|
||||
return unknownConverter({
|
||||
childIndex: i,
|
||||
converters,
|
||||
node,
|
||||
nodesToJSX: (args) => {
|
||||
return convertLexicalNodesToJSX({
|
||||
converters: args.converters ?? converters,
|
||||
disableIndent: args.disableIndent ?? disableIndent,
|
||||
disableTextAlign: args.disableTextAlign ?? disableTextAlign,
|
||||
nodes: args.nodes,
|
||||
parent: args.parent ?? {
|
||||
...node,
|
||||
parent,
|
||||
},
|
||||
})
|
||||
},
|
||||
parent,
|
||||
})
|
||||
}
|
||||
return <span key={i}>unknown node</span>
|
||||
if (!converterForNode && unknownConverter) {
|
||||
converterForNode = unknownConverter
|
||||
}
|
||||
|
||||
const reactNode = converterForNode({
|
||||
let reactNode: React.ReactNode
|
||||
if (converterForNode) {
|
||||
const converted =
|
||||
typeof converterForNode === 'function'
|
||||
? converterForNode({
|
||||
childIndex: i,
|
||||
converters,
|
||||
node,
|
||||
@@ -111,6 +95,11 @@ export function convertLexicalNodesToJSX({
|
||||
},
|
||||
parent,
|
||||
})
|
||||
: converterForNode
|
||||
reactNode = converted
|
||||
} else {
|
||||
reactNode = <span key={i}>unknown node</span>
|
||||
}
|
||||
|
||||
const style: React.CSSProperties = {}
|
||||
|
||||
@@ -176,10 +165,10 @@ export function convertLexicalNodesToJSX({
|
||||
|
||||
return reactNode
|
||||
} catch (error) {
|
||||
console.error('Error converting lexical node to HTML:', error, 'node:', node)
|
||||
console.error('Error converting lexical node to JSX:', error, 'node:', node)
|
||||
return null
|
||||
}
|
||||
})
|
||||
|
||||
return jsxArray.filter(Boolean).map((jsx) => jsx)
|
||||
return jsxArray.filter(Boolean)
|
||||
}
|
||||
@@ -4,9 +4,9 @@ import type {
|
||||
DefaultNodeTypes,
|
||||
SerializedBlockNode,
|
||||
SerializedInlineBlockNode,
|
||||
} from '../../../../../nodeTypes.js'
|
||||
} from '../../../../nodeTypes.js'
|
||||
export type JSXConverter<T extends { [key: string]: any; type?: string } = SerializedLexicalNode> =
|
||||
(args: {
|
||||
| ((args: {
|
||||
childIndex: number
|
||||
converters: JSXConverters
|
||||
node: T
|
||||
@@ -18,7 +18,8 @@ export type JSXConverter<T extends { [key: string]: any; type?: string } = Seria
|
||||
parent?: SerializedLexicalNodeWithParent
|
||||
}) => React.ReactNode[]
|
||||
parent: SerializedLexicalNodeWithParent
|
||||
}) => React.ReactNode
|
||||
}) => React.ReactNode)
|
||||
| React.ReactNode
|
||||
|
||||
export type JSXConverters<
|
||||
T extends { [key: string]: any; type?: string } =
|
||||
@@ -0,0 +1,81 @@
|
||||
import { createLocalReq, type Payload, type PayloadRequest, type TypedLocale } from 'payload'
|
||||
|
||||
import type { HTMLPopulateFn } from '../html/async/types.js'
|
||||
|
||||
import { populate } from '../../../populateGraphQL/populate.js'
|
||||
|
||||
export const getPayloadPopulateFn: (
|
||||
args: {
|
||||
currentDepth: number
|
||||
depth: number
|
||||
draft?: boolean
|
||||
locale?: TypedLocale
|
||||
|
||||
overrideAccess?: boolean
|
||||
showHiddenFields?: boolean
|
||||
} & (
|
||||
| {
|
||||
/**
|
||||
* This payload property will only be used if req is undefined. If localization is enabled, you must pass `req` instead.
|
||||
*/
|
||||
payload: Payload
|
||||
/**
|
||||
* When the converter is called, req CAN be passed in depending on where it's run.
|
||||
* If this is undefined and config is passed through, lexical will create a new req object for you.
|
||||
*/
|
||||
req?: never
|
||||
}
|
||||
| {
|
||||
/**
|
||||
* This payload property will only be used if req is undefined. If localization is enabled, you must pass `req` instead.
|
||||
*/
|
||||
payload?: never
|
||||
/**
|
||||
* When the converter is called, req CAN be passed in depending on where it's run.
|
||||
* If this is undefined and config is passed through, lexical will create a new req object for you.
|
||||
*/
|
||||
req: PayloadRequest
|
||||
}
|
||||
),
|
||||
) => Promise<HTMLPopulateFn> = async ({
|
||||
currentDepth,
|
||||
depth,
|
||||
draft,
|
||||
overrideAccess,
|
||||
payload,
|
||||
req,
|
||||
showHiddenFields,
|
||||
}) => {
|
||||
let reqToUse: PayloadRequest | undefined = req
|
||||
if (req === undefined && payload) {
|
||||
reqToUse = await createLocalReq({}, payload)
|
||||
}
|
||||
|
||||
if (!reqToUse) {
|
||||
throw new Error('No req or payload provided')
|
||||
}
|
||||
|
||||
const populateFn: HTMLPopulateFn = async ({ id, collectionSlug, select }) => {
|
||||
const dataContainer: {
|
||||
value?: any
|
||||
} = {}
|
||||
|
||||
await populate({
|
||||
id,
|
||||
collectionSlug,
|
||||
currentDepth,
|
||||
data: dataContainer,
|
||||
depth,
|
||||
draft: draft ?? false,
|
||||
key: 'value',
|
||||
overrideAccess: overrideAccess ?? true,
|
||||
req: reqToUse,
|
||||
select,
|
||||
showHiddenFields: showHiddenFields ?? false,
|
||||
})
|
||||
|
||||
return dataContainer.value
|
||||
}
|
||||
|
||||
return populateFn
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { stringify } from 'qs-esm'
|
||||
|
||||
import type { HTMLPopulateFn } from '../html/async/types.js'
|
||||
|
||||
export const getRestPopulateFn: (args: {
|
||||
/**
|
||||
* E.g. `http://localhost:3000/api`
|
||||
*/
|
||||
apiURL: string
|
||||
depth?: number
|
||||
draft?: boolean
|
||||
locale?: string
|
||||
}) => HTMLPopulateFn = ({ apiURL, depth, draft, locale }) => {
|
||||
const populateFn: HTMLPopulateFn = async ({ id, collectionSlug, select }) => {
|
||||
const query = stringify(
|
||||
{ depth: depth ?? 0, draft: draft ?? false, locale, select },
|
||||
{ addQueryPrefix: true },
|
||||
)
|
||||
|
||||
const res = await fetch(`${apiURL}/${collectionSlug}/${id}${query}`, {
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method: 'GET',
|
||||
}).then((res) => res.json())
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
return populateFn
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import { TableCellNode, TableNode, TableRowNode } from '@lexical/table'
|
||||
import { sanitizeFields } from 'payload'
|
||||
|
||||
import { createServerFeature } from '../../../utilities/createServerFeature.js'
|
||||
import { convertLexicalNodesToHTML } from '../../converters/html/converter/index.js'
|
||||
import { convertLexicalNodesToHTML } from '../../converters/html_deprecated/converter/index.js'
|
||||
import { createNode } from '../../typeUtilities.js'
|
||||
import { TableMarkdownTransformer } from '../markdownTransformer.js'
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { Spread } from 'lexical'
|
||||
import { HeadingNode } from '@lexical/rich-text'
|
||||
|
||||
import { createServerFeature } from '../../../utilities/createServerFeature.js'
|
||||
import { convertLexicalNodesToHTML } from '../../converters/html/converter/index.js'
|
||||
import { convertLexicalNodesToHTML } from '../../converters/html_deprecated/converter/index.js'
|
||||
import { createNode } from '../../typeUtilities.js'
|
||||
import { MarkdownTransformer } from '../markdownTransformer.js'
|
||||
import { i18n } from './i18n.js'
|
||||
|
||||
@@ -13,7 +13,7 @@ import { sanitizeFields } from 'payload'
|
||||
import type { ClientProps } from '../client/index.js'
|
||||
|
||||
import { createServerFeature } from '../../../utilities/createServerFeature.js'
|
||||
import { convertLexicalNodesToHTML } from '../../converters/html/converter/index.js'
|
||||
import { convertLexicalNodesToHTML } from '../../converters/html_deprecated/converter/index.js'
|
||||
import { createNode } from '../../typeUtilities.js'
|
||||
import { LinkMarkdownTransformer } from '../markdownTransformer.js'
|
||||
import { AutoLinkNode } from '../nodes/AutoLinkNode.js'
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { ListItemNode, ListNode } from '@lexical/list'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
import type { HTMLConverter } from '../converters/html/converter/types.js'
|
||||
import type { HTMLConverter } from '../converters/html_deprecated/converter/types.js'
|
||||
import type { SerializedListItemNode, SerializedListNode } from './plugin/index.js'
|
||||
|
||||
import { convertLexicalNodesToHTML } from '../converters/html/converter/index.js'
|
||||
import { convertLexicalNodesToHTML } from '../converters/html_deprecated/converter/index.js'
|
||||
|
||||
export const ListHTMLConverter: HTMLConverter<SerializedListNode> = {
|
||||
converter: async ({
|
||||
|
||||
@@ -27,7 +27,7 @@ import type {
|
||||
import type { ServerEditorConfig } from '../lexical/config/types.js'
|
||||
import type { Transformer } from '../packages/@lexical/markdown/index.js'
|
||||
import type { AdapterProps } from '../types.js'
|
||||
import type { HTMLConverter } from './converters/html/converter/types.js'
|
||||
import type { HTMLConverter } from './converters/html_deprecated/converter/types.js'
|
||||
import type { BaseClientFeatureProps } from './typesClient.js'
|
||||
|
||||
export type PopulationPromise<T extends SerializedLexicalNode = SerializedLexicalNode> = (args: {
|
||||
@@ -221,8 +221,13 @@ export type NodeWithHooks<T extends LexicalNode = any> = {
|
||||
/**
|
||||
* Allows you to define how a node can be serialized into different formats. Currently, only supports html.
|
||||
* Markdown converters are defined in `markdownTransformers` and not here.
|
||||
*
|
||||
* @deprecated - will be removed in 4.0
|
||||
*/
|
||||
converters?: {
|
||||
/**
|
||||
* @deprecated - will be removed in 4.0
|
||||
*/
|
||||
html?: HTMLConverter<ReturnType<ReplaceAny<T, LexicalNode>['exportJSON']>>
|
||||
}
|
||||
/**
|
||||
|
||||
@@ -870,25 +870,32 @@ export {
|
||||
ServerBlockNode,
|
||||
} from './features/blocks/server/nodes/BlocksNode.js'
|
||||
|
||||
export { LinebreakHTMLConverter } from './features/converters/html/converter/converters/linebreak.js'
|
||||
export { ParagraphHTMLConverter } from './features/converters/html/converter/converters/paragraph.js'
|
||||
export { lexicalHTMLField } from './features/converters/html/async/field/index.js'
|
||||
|
||||
export { TabHTMLConverter } from './features/converters/html/converter/converters/tab.js'
|
||||
export { LinebreakHTMLConverter } from './features/converters/html_deprecated/converter/converters/linebreak.js'
|
||||
export { ParagraphHTMLConverter } from './features/converters/html_deprecated/converter/converters/paragraph.js'
|
||||
|
||||
export { TextHTMLConverter } from './features/converters/html/converter/converters/text.js'
|
||||
export { defaultHTMLConverters } from './features/converters/html/converter/defaultConverters.js'
|
||||
export { TabHTMLConverter } from './features/converters/html_deprecated/converter/converters/tab.js'
|
||||
|
||||
export { TextHTMLConverter } from './features/converters/html_deprecated/converter/converters/text.js'
|
||||
export { defaultHTMLConverters } from './features/converters/html_deprecated/converter/defaultConverters.js'
|
||||
export {
|
||||
convertLexicalNodesToHTML,
|
||||
convertLexicalToHTML,
|
||||
} from './features/converters/html/converter/index.js'
|
||||
} from './features/converters/html_deprecated/converter/index.js'
|
||||
|
||||
export type { HTMLConverter } from './features/converters/html/converter/types.js'
|
||||
export { consolidateHTMLConverters, lexicalHTML } from './features/converters/html/field/index.js'
|
||||
export type { HTMLConverter } from './features/converters/html_deprecated/converter/types.js'
|
||||
export {
|
||||
consolidateHTMLConverters,
|
||||
lexicalHTML,
|
||||
} from './features/converters/html_deprecated/field/index.js'
|
||||
export {
|
||||
HTMLConverterFeature,
|
||||
type HTMLConverterFeatureProps,
|
||||
} from './features/converters/html/index.js'
|
||||
} from './features/converters/html_deprecated/index.js'
|
||||
export { convertHTMLToLexical } from './features/converters/htmlToLexical/index.js'
|
||||
export { getPayloadPopulateFn } from './features/converters/utilities/payloadPopulateFn.js'
|
||||
export { getRestPopulateFn } from './features/converters/utilities/restPopulateFn.js'
|
||||
export { TestRecorderFeature } from './features/debug/testRecorder/server/index.js'
|
||||
export { TreeViewFeature } from './features/debug/treeView/server/index.js'
|
||||
export { EXPERIMENTAL_TableFeature } from './features/experimental_table/server/index.js'
|
||||
@@ -1028,6 +1035,7 @@ export type { LexicalEditorProps, LexicalFieldAdminProps, LexicalRichTextAdapter
|
||||
|
||||
export { createServerFeature } from './utilities/createServerFeature.js'
|
||||
export { editorConfigFactory } from './utilities/editorConfigFactory.js'
|
||||
|
||||
export type { FieldsDrawerProps } from './utilities/fieldsDrawer/Drawer.js'
|
||||
export { extractPropsFromJSXPropsString } from './utilities/jsx/extractPropsFromJSXPropsString.js'
|
||||
|
||||
@@ -1037,4 +1045,5 @@ export {
|
||||
objectToFrontmatter,
|
||||
propsToJSXString,
|
||||
} from './utilities/jsx/jsx.js'
|
||||
|
||||
export { upgradeLexicalData } from './utilities/upgradeLexicalData/index.js'
|
||||
|
||||
@@ -2,11 +2,13 @@ import type { PayloadRequest, SelectType } from 'payload'
|
||||
|
||||
import { createDataloaderCacheKey } from 'payload'
|
||||
|
||||
type Arguments = {
|
||||
type PopulateArguments = {
|
||||
collectionSlug: string
|
||||
currentDepth?: number
|
||||
data: unknown
|
||||
depth: number
|
||||
draft: boolean
|
||||
id: number | string
|
||||
key: number | string
|
||||
overrideAccess: boolean
|
||||
req: PayloadRequest
|
||||
@@ -14,7 +16,9 @@ type Arguments = {
|
||||
showHiddenFields: boolean
|
||||
}
|
||||
|
||||
export const populate = async ({
|
||||
type PopulateFn = (args: PopulateArguments) => Promise<void>
|
||||
|
||||
export const populate: PopulateFn = async ({
|
||||
id,
|
||||
collectionSlug,
|
||||
currentDepth,
|
||||
@@ -26,10 +30,7 @@ export const populate = async ({
|
||||
req,
|
||||
select,
|
||||
showHiddenFields,
|
||||
}: {
|
||||
collectionSlug: string
|
||||
id: number | string
|
||||
} & Arguments): Promise<void> => {
|
||||
}) => {
|
||||
const shouldPopulate = depth && currentDepth! <= depth
|
||||
// usually depth is checked within recursivelyPopulateFieldsForGraphQL. But since this populate function can be called outside of that (in rest afterRead node hooks) we need to check here too
|
||||
if (!shouldPopulate) {
|
||||
|
||||
45
pnpm-lock.yaml
generated
45
pnpm-lock.yaml
generated
@@ -45,7 +45,7 @@ importers:
|
||||
version: 1.50.0
|
||||
'@sentry/nextjs':
|
||||
specifier: ^8.33.1
|
||||
version: 8.37.1(@opentelemetry/core@1.27.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(next@15.2.0(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4))(react@19.0.0)(webpack@5.96.1(@swc/core@1.10.12(@swc/helpers@0.5.15)))
|
||||
version: 8.37.1(@opentelemetry/core@1.27.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(next@15.2.0(@babel/core@7.26.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4))(react@19.0.0)(webpack@5.96.1(@swc/core@1.10.12(@swc/helpers@0.5.15))(esbuild@0.19.12))
|
||||
'@sentry/node':
|
||||
specifier: ^8.33.1
|
||||
version: 8.37.1
|
||||
@@ -135,7 +135,7 @@ importers:
|
||||
version: 10.1.3(@aws-sdk/credential-providers@3.687.0(@aws-sdk/client-sso-oidc@3.687.0(@aws-sdk/client-sts@3.687.0)))(socks@2.8.3)
|
||||
next:
|
||||
specifier: 15.2.0
|
||||
version: 15.2.0(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4)
|
||||
version: 15.2.0(@babel/core@7.26.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4)
|
||||
open:
|
||||
specifier: ^10.1.0
|
||||
version: 10.1.0
|
||||
@@ -1079,7 +1079,7 @@ importers:
|
||||
dependencies:
|
||||
next:
|
||||
specifier: ^15.0.3
|
||||
version: 15.1.3(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4)
|
||||
version: 15.1.3(@babel/core@7.26.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4)
|
||||
devDependencies:
|
||||
'@payloadcms/eslint-config':
|
||||
specifier: workspace:*
|
||||
@@ -1144,7 +1144,7 @@ importers:
|
||||
dependencies:
|
||||
'@sentry/nextjs':
|
||||
specifier: ^8.33.1
|
||||
version: 8.37.1(@opentelemetry/core@1.27.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(next@15.2.0(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4))(react@19.0.0)(webpack@5.96.1(@swc/core@1.10.12(@swc/helpers@0.5.15)))
|
||||
version: 8.37.1(@opentelemetry/core@1.27.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(next@15.2.0(@babel/core@7.26.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4))(react@19.0.0)(webpack@5.96.1(@swc/core@1.10.12(@swc/helpers@0.5.15))(esbuild@0.19.12))
|
||||
'@sentry/types':
|
||||
specifier: ^8.33.1
|
||||
version: 8.37.1
|
||||
@@ -1316,6 +1316,9 @@ importers:
|
||||
micromark-extension-mdx-jsx:
|
||||
specifier: 3.0.1
|
||||
version: 3.0.1
|
||||
qs-esm:
|
||||
specifier: 7.0.2
|
||||
version: 7.0.2
|
||||
react:
|
||||
specifier: 19.0.0
|
||||
version: 19.0.0
|
||||
@@ -1497,7 +1500,7 @@ importers:
|
||||
version: link:../plugin-cloud-storage
|
||||
uploadthing:
|
||||
specifier: 7.3.0
|
||||
version: 7.3.0(next@15.2.0(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4))
|
||||
version: 7.3.0(next@15.2.0(@babel/core@7.26.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4))
|
||||
devDependencies:
|
||||
payload:
|
||||
specifier: workspace:*
|
||||
@@ -1783,7 +1786,7 @@ importers:
|
||||
version: link:../packages/ui
|
||||
'@sentry/nextjs':
|
||||
specifier: ^8.33.1
|
||||
version: 8.37.1(@opentelemetry/core@1.27.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(next@15.2.0(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4))(react@19.0.0)(webpack@5.96.1(@swc/core@1.10.12(@swc/helpers@0.5.15)))
|
||||
version: 8.37.1(@opentelemetry/core@1.27.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(next@15.2.0(@babel/core@7.26.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4))(react@19.0.0)(webpack@5.96.1(@swc/core@1.10.12(@swc/helpers@0.5.15))(esbuild@0.19.12))
|
||||
'@sentry/react':
|
||||
specifier: ^7.77.0
|
||||
version: 7.119.2(react@19.0.0)
|
||||
@@ -1840,7 +1843,7 @@ importers:
|
||||
version: 8.9.5(@aws-sdk/credential-providers@3.687.0(@aws-sdk/client-sso-oidc@3.687.0(@aws-sdk/client-sts@3.687.0)))(socks@2.8.3)
|
||||
next:
|
||||
specifier: 15.2.0
|
||||
version: 15.2.0(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4)
|
||||
version: 15.2.0(@babel/core@7.26.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4)
|
||||
nodemailer:
|
||||
specifier: 6.9.16
|
||||
version: 6.9.16
|
||||
@@ -8069,6 +8072,7 @@ packages:
|
||||
|
||||
libsql@0.4.7:
|
||||
resolution: {integrity: sha512-T9eIRCs6b0J1SHKYIvD8+KCJMcWZ900iZyxdnSCdqxN12Z1ijzT+jY5nrk72Jw4B0HGzms2NgpryArlJqvc3Lw==}
|
||||
cpu: [x64, arm64, wasm32]
|
||||
os: [darwin, linux, win32]
|
||||
|
||||
lie@3.1.1:
|
||||
@@ -13967,7 +13971,7 @@ snapshots:
|
||||
'@sentry/utils': 7.119.2
|
||||
localforage: 1.10.0
|
||||
|
||||
'@sentry/nextjs@8.37.1(@opentelemetry/core@1.27.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(next@15.2.0(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4))(react@19.0.0)(webpack@5.96.1(@swc/core@1.10.12(@swc/helpers@0.5.15)))':
|
||||
'@sentry/nextjs@8.37.1(@opentelemetry/core@1.27.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(next@15.2.0(@babel/core@7.26.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4))(react@19.0.0)(webpack@5.96.1(@swc/core@1.10.12(@swc/helpers@0.5.15))(esbuild@0.19.12))':
|
||||
dependencies:
|
||||
'@opentelemetry/api': 1.9.0
|
||||
'@opentelemetry/instrumentation-http': 0.53.0(@opentelemetry/api@1.9.0)
|
||||
@@ -13981,9 +13985,9 @@ snapshots:
|
||||
'@sentry/types': 8.37.1
|
||||
'@sentry/utils': 8.37.1
|
||||
'@sentry/vercel-edge': 8.37.1
|
||||
'@sentry/webpack-plugin': 2.22.6(webpack@5.96.1(@swc/core@1.10.12(@swc/helpers@0.5.15)))
|
||||
'@sentry/webpack-plugin': 2.22.6(webpack@5.96.1(@swc/core@1.10.12(@swc/helpers@0.5.15))(esbuild@0.19.12))
|
||||
chalk: 3.0.0
|
||||
next: 15.2.0(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4)
|
||||
next: 15.2.0(@babel/core@7.26.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4)
|
||||
resolve: 1.22.8
|
||||
rollup: 3.29.5
|
||||
stacktrace-parser: 0.1.10
|
||||
@@ -14091,12 +14095,12 @@ snapshots:
|
||||
'@sentry/types': 8.37.1
|
||||
'@sentry/utils': 8.37.1
|
||||
|
||||
'@sentry/webpack-plugin@2.22.6(webpack@5.96.1(@swc/core@1.10.12(@swc/helpers@0.5.15)))':
|
||||
'@sentry/webpack-plugin@2.22.6(webpack@5.96.1(@swc/core@1.10.12(@swc/helpers@0.5.15))(esbuild@0.19.12))':
|
||||
dependencies:
|
||||
'@sentry/bundler-plugin-core': 2.22.6
|
||||
unplugin: 1.0.1
|
||||
uuid: 9.0.0
|
||||
webpack: 5.96.1(@swc/core@1.10.12(@swc/helpers@0.5.15))
|
||||
webpack: 5.96.1(@swc/core@1.10.12(@swc/helpers@0.5.15))(esbuild@0.19.12)
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
- supports-color
|
||||
@@ -18646,7 +18650,7 @@ snapshots:
|
||||
- '@babel/core'
|
||||
- babel-plugin-macros
|
||||
|
||||
next@15.1.3(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4):
|
||||
next@15.1.3(@babel/core@7.26.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4):
|
||||
dependencies:
|
||||
'@next/env': 15.1.3
|
||||
'@swc/counter': 0.1.3
|
||||
@@ -18674,7 +18678,7 @@ snapshots:
|
||||
- '@babel/core'
|
||||
- babel-plugin-macros
|
||||
|
||||
next@15.2.0(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4):
|
||||
next@15.2.0(@babel/core@7.26.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4):
|
||||
dependencies:
|
||||
'@next/env': 15.2.0
|
||||
'@swc/counter': 0.1.3
|
||||
@@ -20075,16 +20079,17 @@ snapshots:
|
||||
ansi-escapes: 4.3.2
|
||||
supports-hyperlinks: 2.3.0
|
||||
|
||||
terser-webpack-plugin@5.3.10(@swc/core@1.10.12(@swc/helpers@0.5.15))(webpack@5.96.1(@swc/core@1.10.12(@swc/helpers@0.5.15))):
|
||||
terser-webpack-plugin@5.3.10(@swc/core@1.10.12(@swc/helpers@0.5.15))(esbuild@0.19.12)(webpack@5.96.1(@swc/core@1.10.12(@swc/helpers@0.5.15))(esbuild@0.19.12)):
|
||||
dependencies:
|
||||
'@jridgewell/trace-mapping': 0.3.25
|
||||
jest-worker: 27.5.1
|
||||
schema-utils: 3.3.0
|
||||
serialize-javascript: 6.0.2
|
||||
terser: 5.36.0
|
||||
webpack: 5.96.1(@swc/core@1.10.12(@swc/helpers@0.5.15))
|
||||
webpack: 5.96.1(@swc/core@1.10.12(@swc/helpers@0.5.15))(esbuild@0.19.12)
|
||||
optionalDependencies:
|
||||
'@swc/core': 1.10.12(@swc/helpers@0.5.15)
|
||||
esbuild: 0.19.12
|
||||
|
||||
terser@5.36.0:
|
||||
dependencies:
|
||||
@@ -20365,14 +20370,14 @@ snapshots:
|
||||
escalade: 3.2.0
|
||||
picocolors: 1.1.1
|
||||
|
||||
uploadthing@7.3.0(next@15.2.0(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4)):
|
||||
uploadthing@7.3.0(next@15.2.0(@babel/core@7.26.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4)):
|
||||
dependencies:
|
||||
'@effect/platform': 0.69.8(effect@3.10.3)
|
||||
'@uploadthing/mime-types': 0.3.2
|
||||
'@uploadthing/shared': 7.1.1
|
||||
effect: 3.10.3
|
||||
optionalDependencies:
|
||||
next: 15.2.0(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4)
|
||||
next: 15.2.0(@babel/core@7.26.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.77.4)
|
||||
|
||||
uri-js@4.4.1:
|
||||
dependencies:
|
||||
@@ -20470,7 +20475,7 @@ snapshots:
|
||||
|
||||
webpack-virtual-modules@0.5.0: {}
|
||||
|
||||
webpack@5.96.1(@swc/core@1.10.12(@swc/helpers@0.5.15)):
|
||||
webpack@5.96.1(@swc/core@1.10.12(@swc/helpers@0.5.15))(esbuild@0.19.12):
|
||||
dependencies:
|
||||
'@types/eslint-scope': 3.7.7
|
||||
'@types/estree': 1.0.6
|
||||
@@ -20492,7 +20497,7 @@ snapshots:
|
||||
neo-async: 2.6.2
|
||||
schema-utils: 3.3.0
|
||||
tapable: 2.2.1
|
||||
terser-webpack-plugin: 5.3.10(@swc/core@1.10.12(@swc/helpers@0.5.15))(webpack@5.96.1(@swc/core@1.10.12(@swc/helpers@0.5.15)))
|
||||
terser-webpack-plugin: 5.3.10(@swc/core@1.10.12(@swc/helpers@0.5.15))(esbuild@0.19.12)(webpack@5.96.1(@swc/core@1.10.12(@swc/helpers@0.5.15))(esbuild@0.19.12))
|
||||
watchpack: 2.4.2
|
||||
webpack-sources: 3.2.3
|
||||
transitivePeerDependencies:
|
||||
|
||||
@@ -1,11 +1,23 @@
|
||||
'use client'
|
||||
import type { DefaultNodeTypes, SerializedBlockNode } from '@payloadcms/richtext-lexical'
|
||||
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
|
||||
|
||||
import { getRestPopulateFn } from '@payloadcms/richtext-lexical/client'
|
||||
import {
|
||||
convertLexicalToHTML,
|
||||
type HTMLConvertersFunction,
|
||||
} from '@payloadcms/richtext-lexical/html'
|
||||
import {
|
||||
convertLexicalToHTMLAsync,
|
||||
type HTMLConvertersFunctionAsync,
|
||||
} from '@payloadcms/richtext-lexical/html-async'
|
||||
import { type JSXConvertersFunction, RichText } from '@payloadcms/richtext-lexical/react'
|
||||
import { useConfig, useDocumentInfo, usePayloadAPI } from '@payloadcms/ui'
|
||||
import React from 'react'
|
||||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
|
||||
const jsxConverters: JSXConvertersFunction = ({ defaultConverters }) => ({
|
||||
const jsxConverters: JSXConvertersFunction<DefaultNodeTypes | SerializedBlockNode<any>> = ({
|
||||
defaultConverters,
|
||||
}) => ({
|
||||
...defaultConverters,
|
||||
blocks: {
|
||||
myTextBlock: ({ node }) => <div style={{ backgroundColor: 'red' }}>{node.fields.text}</div>,
|
||||
@@ -15,6 +27,30 @@ const jsxConverters: JSXConvertersFunction = ({ defaultConverters }) => ({
|
||||
},
|
||||
})
|
||||
|
||||
const htmlConverters: HTMLConvertersFunction<DefaultNodeTypes | SerializedBlockNode<any>> = ({
|
||||
defaultConverters,
|
||||
}) => ({
|
||||
...defaultConverters,
|
||||
blocks: {
|
||||
myTextBlock: ({ node }) => `<div style="background-color: red;">${node.fields.text}</div>`,
|
||||
relationshipBlock: () => {
|
||||
return `<p>Test</p>`
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const htmlConvertersAsync: HTMLConvertersFunctionAsync<
|
||||
DefaultNodeTypes | SerializedBlockNode<any>
|
||||
> = ({ defaultConverters }) => ({
|
||||
...defaultConverters,
|
||||
blocks: {
|
||||
myTextBlock: ({ node }) => `<div style="background-color: red;">${node.fields.text}</div>`,
|
||||
relationshipBlock: () => {
|
||||
return `<p>Test</p>`
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const LexicalRendered: React.FC = () => {
|
||||
const { id, collectionSlug } = useDocumentInfo()
|
||||
|
||||
@@ -31,14 +67,54 @@ export const LexicalRendered: React.FC = () => {
|
||||
},
|
||||
})
|
||||
|
||||
const [{ data: unpopulatedData }] = usePayloadAPI(`${serverURL}${api}/${collectionSlug}/${id}`, {
|
||||
initialParams: {
|
||||
depth: 0,
|
||||
},
|
||||
})
|
||||
|
||||
const html: null | string = useMemo(() => {
|
||||
if (!data.lexicalWithBlocks) {
|
||||
return null
|
||||
}
|
||||
|
||||
return convertLexicalToHTML({
|
||||
converters: htmlConverters,
|
||||
data: data.lexicalWithBlocks as SerializedEditorState,
|
||||
})
|
||||
}, [data.lexicalWithBlocks])
|
||||
|
||||
const [htmlFromUnpopulatedData, setHtmlFromUnpopulatedData] = useState<null | string>(null)
|
||||
|
||||
useEffect(() => {
|
||||
async function convert() {
|
||||
const html = await convertLexicalToHTMLAsync({
|
||||
converters: htmlConvertersAsync,
|
||||
data: unpopulatedData.lexicalWithBlocks as SerializedEditorState,
|
||||
populate: getRestPopulateFn({
|
||||
apiURL: `${serverURL}${api}`,
|
||||
}),
|
||||
})
|
||||
|
||||
setHtmlFromUnpopulatedData(html)
|
||||
}
|
||||
void convert()
|
||||
}, [unpopulatedData.lexicalWithBlocks, api, serverURL])
|
||||
|
||||
if (!data.lexicalWithBlocks) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Rendered:</h1>
|
||||
<h1>Rendered JSX:</h1>
|
||||
<RichText converters={jsxConverters} data={data.lexicalWithBlocks as SerializedEditorState} />
|
||||
<h1>Rendered HTML:</h1>
|
||||
{html && <div dangerouslySetInnerHTML={{ __html: html }} />}
|
||||
<h1>Rendered HTML 2:</h1>
|
||||
{htmlFromUnpopulatedData && (
|
||||
<div dangerouslySetInnerHTML={{ __html: htmlFromUnpopulatedData }} />
|
||||
)}
|
||||
<h1>Raw JSON:</h1>
|
||||
<pre>{JSON.stringify(data.lexicalWithBlocks, null, 2)}</pre>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
import {
|
||||
HTMLConverterFeature,
|
||||
lexicalEditor,
|
||||
lexicalHTML,
|
||||
lexicalHTMLField,
|
||||
LinkFeature,
|
||||
TreeViewFeature,
|
||||
UploadFeature,
|
||||
@@ -39,7 +38,6 @@ export const LexicalMigrateFields: CollectionConfig = {
|
||||
...defaultFeatures,
|
||||
LexicalPluginToLexicalFeature({ quiet: true }),
|
||||
TreeViewFeature(),
|
||||
HTMLConverterFeature(),
|
||||
LinkFeature({
|
||||
fields: ({ defaultFields }) => [
|
||||
...defaultFields,
|
||||
@@ -80,7 +78,6 @@ export const LexicalMigrateFields: CollectionConfig = {
|
||||
...defaultFeatures,
|
||||
SlateToLexicalFeature(),
|
||||
TreeViewFeature(),
|
||||
HTMLConverterFeature(),
|
||||
LinkFeature({
|
||||
fields: ({ defaultFields }) => [
|
||||
...defaultFields,
|
||||
@@ -117,11 +114,11 @@ export const LexicalMigrateFields: CollectionConfig = {
|
||||
name: 'lexicalSimple',
|
||||
type: 'richText',
|
||||
editor: lexicalEditor({
|
||||
features: ({ defaultFeatures }) => [...defaultFeatures, HTMLConverterFeature()],
|
||||
features: ({ defaultFeatures }) => [...defaultFeatures],
|
||||
}),
|
||||
defaultValue: getSimpleLexicalData('simple'),
|
||||
},
|
||||
lexicalHTML('lexicalSimple', { name: 'lexicalSimple_html' }),
|
||||
lexicalHTMLField({ htmlFieldName: 'lexicalSimple_html', lexicalFieldName: 'lexicalSimple' }),
|
||||
{
|
||||
name: 'groupWithLexicalField',
|
||||
type: 'group',
|
||||
@@ -130,11 +127,14 @@ export const LexicalMigrateFields: CollectionConfig = {
|
||||
name: 'lexicalInGroupField',
|
||||
type: 'richText',
|
||||
editor: lexicalEditor({
|
||||
features: ({ defaultFeatures }) => [...defaultFeatures, HTMLConverterFeature()],
|
||||
features: ({ defaultFeatures }) => [...defaultFeatures],
|
||||
}),
|
||||
defaultValue: getSimpleLexicalData('group'),
|
||||
},
|
||||
lexicalHTML('lexicalInGroupField', { name: 'lexicalInGroupField_html' }),
|
||||
lexicalHTMLField({
|
||||
htmlFieldName: 'lexicalInGroupField_html',
|
||||
lexicalFieldName: 'lexicalInGroupField',
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -145,10 +145,13 @@ export const LexicalMigrateFields: CollectionConfig = {
|
||||
name: 'lexicalInArrayField',
|
||||
type: 'richText',
|
||||
editor: lexicalEditor({
|
||||
features: ({ defaultFeatures }) => [...defaultFeatures, HTMLConverterFeature()],
|
||||
features: ({ defaultFeatures }) => [...defaultFeatures],
|
||||
}),
|
||||
},
|
||||
lexicalHTML('lexicalInArrayField', { name: 'lexicalInArrayField_html' }),
|
||||
lexicalHTMLField({
|
||||
htmlFieldName: 'lexicalInArrayField_html',
|
||||
lexicalFieldName: 'lexicalInArrayField',
|
||||
}),
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -304,8 +304,8 @@ describe('Lexical', () => {
|
||||
})
|
||||
).docs[0] as never
|
||||
|
||||
const htmlField: string = lexicalDoc?.lexicalSimple_html
|
||||
expect(htmlField).toStrictEqual('<p>simple</p>')
|
||||
const htmlField = lexicalDoc?.lexicalSimple_html
|
||||
expect(htmlField).toStrictEqual('<div class="payload-richtext"><p>simple</p></div>')
|
||||
})
|
||||
it('htmlConverter: should output correct HTML for lexical field nested in group', async () => {
|
||||
const lexicalDoc: LexicalMigrateField = (
|
||||
@@ -320,8 +320,8 @@ describe('Lexical', () => {
|
||||
})
|
||||
).docs[0] as never
|
||||
|
||||
const htmlField: string = lexicalDoc?.groupWithLexicalField?.lexicalInGroupField_html
|
||||
expect(htmlField).toStrictEqual('<p>group</p>')
|
||||
const htmlField = lexicalDoc?.groupWithLexicalField?.lexicalInGroupField_html
|
||||
expect(htmlField).toStrictEqual('<div class="payload-richtext"><p>group</p></div>')
|
||||
})
|
||||
it('htmlConverter: should output correct HTML for lexical field nested in array', async () => {
|
||||
const lexicalDoc: LexicalMigrateField = (
|
||||
@@ -336,11 +336,11 @@ describe('Lexical', () => {
|
||||
})
|
||||
).docs[0] as never
|
||||
|
||||
const htmlField1: string = lexicalDoc?.arrayWithLexicalField[0].lexicalInArrayField_html
|
||||
const htmlField2: string = lexicalDoc?.arrayWithLexicalField[1].lexicalInArrayField_html
|
||||
const htmlField1 = lexicalDoc?.arrayWithLexicalField?.[0]?.lexicalInArrayField_html
|
||||
const htmlField2 = lexicalDoc?.arrayWithLexicalField?.[1]?.lexicalInArrayField_html
|
||||
|
||||
expect(htmlField1).toStrictEqual('<p>array 1</p>')
|
||||
expect(htmlField2).toStrictEqual('<p>array 2</p>')
|
||||
expect(htmlField1).toStrictEqual('<div class="payload-richtext"><p>array 1</p></div>')
|
||||
expect(htmlField2).toStrictEqual('<div class="payload-richtext"><p>array 2</p></div>')
|
||||
})
|
||||
})
|
||||
describe('advanced - blocks', () => {
|
||||
@@ -654,7 +654,7 @@ describe('Lexical', () => {
|
||||
locale: 'en',
|
||||
data: {
|
||||
title: 'Localized Lexical hooks',
|
||||
lexicalBlocksLocalized: textToLexicalJSON({ text: 'some text' }) as any,
|
||||
lexicalBlocksLocalized: textToLexicalJSON({ text: 'some text' }),
|
||||
lexicalBlocksSubLocalized: generateLexicalLocalizedRichText(
|
||||
'Shared text',
|
||||
'English text in block',
|
||||
|
||||
Reference in New Issue
Block a user