fix(richtext-lexical)!: fix output of internal list HTML converter (#5827)

BREAKING: Changes the classnames of the converted HTML
This commit is contained in:
Alessio Gravili
2024-04-12 12:10:44 -04:00
committed by GitHub
4 changed files with 51 additions and 11 deletions

View File

@@ -235,6 +235,19 @@ This method employs `convertLexicalToHTML` from `@payloadcms/richtext-lexical`,
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. 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.
#### 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.
Here is some "base" CSS you can use to ensure that nested lists render correctly:
```css
/* Base CSS for Lexical HTML */
.nestedListItem, .list-check {
list-style-type: none;
}
```
#### Creating your own HTML Converter #### 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: 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:

View File

@@ -1,5 +1,5 @@
import type { SerializedEditorState } from 'lexical' import type { SerializedEditorState } from 'lexical'
import type { Field, RichTextField, TextField } from 'payload/types' import type { Field, RichTextField } from 'payload/types'
import type { AdapterProps, LexicalRichTextAdapter } from '../../../../../types.js' import type { AdapterProps, LexicalRichTextAdapter } from '../../../../../types.js'
import type { SanitizedServerEditorConfig } from '../../../../lexical/config/types.js' import type { SanitizedServerEditorConfig } from '../../../../lexical/config/types.js'
@@ -10,6 +10,12 @@ import { defaultHTMLConverters } from '../converter/defaultConverters.js'
import { convertLexicalToHTML } from '../converter/index.js' import { convertLexicalToHTML } from '../converter/index.js'
type Props = { type Props = {
/**
* Whether the lexicalHTML field should be hidden in the admin panel
*
* @default true
*/
hidden?: boolean
name: string name: string
} }
@@ -53,13 +59,16 @@ export const lexicalHTML: (
**/ **/
lexicalFieldName: string, lexicalFieldName: string,
props: Props, props: Props,
) => TextField = (lexicalFieldName, props) => { ) => Field = (lexicalFieldName, props) => {
const { name = 'lexicalHTML' } = props const { name = 'lexicalHTML', hidden = true } = props
return { return {
name, name,
type: 'text', type: 'code',
admin: { admin: {
hidden: true, editorOptions: {
language: 'html',
},
hidden,
}, },
hooks: { hooks: {
afterRead: [ afterRead: [

View File

@@ -1,6 +1,7 @@
import type { SerializedListItemNode, SerializedListNode } from '@lexical/list' import type { SerializedListItemNode, SerializedListNode } from '@lexical/list'
import lexicalListImport from '@lexical/list' import lexicalListImport from '@lexical/list'
import { v4 as uuidv4 } from 'uuid'
const { ListItemNode, ListNode } = lexicalListImport const { ListItemNode, ListNode } = lexicalListImport
import type { HTMLConverter } from '../converters/html/converter/types.js' import type { HTMLConverter } from '../converters/html/converter/types.js'
@@ -19,13 +20,15 @@ export const ListHTMLConverter: HTMLConverter<SerializedListNode> = {
payload, payload,
}) })
return `<${node?.tag} class="${node?.listType}">${childrenText}</${node?.tag}>` return `<${node?.tag} class="list-${node?.listType}">${childrenText}</${node?.tag}>`
}, },
nodeTypes: [ListNode.getType()], nodeTypes: [ListNode.getType()],
} }
export const ListItemHTMLConverter: HTMLConverter<SerializedListItemNode> = { export const ListItemHTMLConverter: HTMLConverter<SerializedListItemNode> = {
converter: async ({ converters, node, parent, payload }) => { converter: async ({ converters, node, parent, payload }) => {
const hasSubLists = node.children.some((child) => child.type === 'list')
const childrenText = await convertLexicalNodesToHTML({ const childrenText = await convertLexicalNodesToHTML({
converters, converters,
lexicalNodes: node.children, lexicalNodes: node.children,
@@ -37,19 +40,30 @@ export const ListItemHTMLConverter: HTMLConverter<SerializedListItemNode> = {
}) })
if ('listType' in parent && parent?.listType === 'check') { if ('listType' in parent && parent?.listType === 'check') {
const uuid = uuidv4()
return `<li aria-checked=${node.checked ? 'true' : 'false'} class="${ return `<li aria-checked=${node.checked ? 'true' : 'false'} class="${
'list-item-checkbox' + node.checked 'list-item-checkbox' +
? 'list-item-checkbox-checked' (node.checked ? ' list-item-checkbox-checked' : ' list-item-checkbox-unchecked') +
: 'list-item-checkbox-unchecked' (hasSubLists ? ' nestedListItem' : '')
}" }"
role="checkbox" role="checkbox"
tabIndex=${-1} tabIndex=${-1}
value=${node?.value} value=${node?.value}
> >
${childrenText} ${
hasSubLists
? childrenText
: `
<input type="checkbox" id="${uuid}"${node.checked ? ' checked' : ''}>
<label for="${uuid}">${childrenText}</label><br>
`
}
</li>` </li>`
} else { } else {
return `<li value=${node?.value}>${childrenText}</li>` return `<li ${hasSubLists ? `class="nestedListItem" ` : ''}value=${node?.value}>${childrenText}</li>`
} }
}, },
nodeTypes: [ListItemNode.getType()], nodeTypes: [ListItemNode.getType()],

View File

@@ -331,6 +331,10 @@
list-style-position: inside; list-style-position: inside;
} }
&__ul ul {
margin: 0;
}
&__listItem { &__listItem {
margin: 0 0px 0.4em 16px; margin: 0 0px 0.4em 16px;
} }