Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion packages/richtext-lexical/src/validate/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { RichTextField, Validate } from 'payload'
import type { SanitizedServerEditorConfig } from '../lexical/config/types.js'

import { hasText } from './hasText.js'
import { validateLexicalStateParsable } from './validateLexicalStateParsable.js'
import { validateNodes } from './validateNodes.js'

export const richTextValidateHOC = ({
Expand All @@ -29,14 +30,22 @@ export const richTextValidateHOC = ({

const rootNodes = value?.root?.children
if (rootNodes && Array.isArray(rootNodes) && rootNodes?.length) {
return await validateNodes({
const nodesResult = await validateNodes({
nodes: rootNodes,
nodeValidations: editorConfig.features.validations,
validation: {
options,
value,
},
})

if (nodesResult !== true) {
return nodesResult
}

if (!validateLexicalStateParsable(value, editorConfig)) {
return t('validation:lexicalUnsupportedNodes')
}
}

return true
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { SerializedEditorState } from 'lexical'

import { createHeadlessEditor } from '@lexical/headless'

import type { SanitizedServerEditorConfig } from '../lexical/config/types.js'

import { getEnabledNodes } from '../lexical/nodes/index.js'

/**
* Ensures serialized Lexical JSON only uses node types registered for this field.
* Otherwise Lexical throws at runtime (e.g. minified error #17 for type "list").
*/
export function validateLexicalStateParsable(
value: SerializedEditorState,
editorConfig: SanitizedServerEditorConfig,
): boolean {
try {
const headlessEditor = createHeadlessEditor({
nodes: getEnabledNodes({ editorConfig }),
})

headlessEditor.update(
() => {
headlessEditor.setEditorState(headlessEditor.parseEditorState(value))
},
{ discrete: true },
)

return true
} catch {
return false
}
}
1 change: 1 addition & 0 deletions packages/translations/src/clientKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,7 @@ export const clientTranslationKeys = createClientTranslationKeys([
'validation:longitudeOutOfBounds',
'validation:invalidBlock',
'validation:invalidBlocks',
'validation:lexicalUnsupportedNodes',
'validation:longerThanMin',
'validation:notValidDate',
'validation:required',
Expand Down
2 changes: 2 additions & 0 deletions packages/translations/src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,8 @@ export const enTranslations = {
invalidSelections: 'This field has the following invalid selections:',
latitudeOutOfBounds: 'Latitude must be between -90 and 90.',
lessThanMin: '{{value}} is less than the min allowed {{label}} of {{min}}.',
lexicalUnsupportedNodes:
'This rich text uses node types that are not enabled for this field (for example lists require the unordered or ordered list features).',
limitReached: 'Limit reached, only {{max}} items can be added.',
longerThanMin: 'This value must be longer than the minimum length of {{minLength}} characters.',
longitudeOutOfBounds: 'Longitude must be between -180 and 180.',
Expand Down
4 changes: 4 additions & 0 deletions templates/website/src/collections/Posts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
HorizontalRuleFeature,
InlineToolbarFeature,
lexicalEditor,
OrderedListFeature,
UnorderedListFeature,
} from '@payloadcms/richtext-lexical'

import { authenticated } from '../../access/authenticated'
Expand Down Expand Up @@ -88,6 +90,8 @@ export const Posts: CollectionConfig<'posts'> = {
features: ({ rootFeatures }) => {
return [
...rootFeatures,
UnorderedListFeature(),
OrderedListFeature(),
HeadingFeature({ enabledHeadingSizes: ['h1', 'h2', 'h3', 'h4'] }),
BlocksFeature({ blocks: [Banner, Code, MediaBlock] }),
FixedToolbarFeature(),
Expand Down
Loading