fix(richtext-lexical): prevent use of text formats whose features were not enabled (#9507)
Fixes #8811 Before this PR, even if you did not include text formatting features (such as BoldFeature, ItalicFeature, etc), it was possible to apply that formatting by (a) pasting content from the clipboard and (b) using keyboard shortcuts. This PR fixes that by requiring the formatting features to be registered so that they can be inserted in the editor.
This commit is contained in:
@@ -40,6 +40,7 @@ export const BoldFeatureClient = createClientFeature(({ featureProviderMap }) =>
|
||||
}
|
||||
|
||||
return {
|
||||
enableFormats: ['bold'],
|
||||
markdownTransformers,
|
||||
toolbarFixed: {
|
||||
groups: toolbarGroups,
|
||||
|
||||
@@ -30,6 +30,7 @@ const toolbarGroups: ToolbarGroup[] = [
|
||||
]
|
||||
|
||||
export const InlineCodeFeatureClient = createClientFeature({
|
||||
enableFormats: ['code'],
|
||||
markdownTransformers: [INLINE_CODE],
|
||||
toolbarFixed: {
|
||||
groups: toolbarGroups,
|
||||
|
||||
@@ -30,6 +30,7 @@ const toolbarGroups: ToolbarGroup[] = [
|
||||
]
|
||||
|
||||
export const ItalicFeatureClient = createClientFeature({
|
||||
enableFormats: ['italic'],
|
||||
markdownTransformers: [ITALIC_STAR, ITALIC_UNDERSCORE],
|
||||
toolbarFixed: {
|
||||
groups: toolbarGroups,
|
||||
|
||||
@@ -28,6 +28,7 @@ const toolbarGroups = [
|
||||
]
|
||||
|
||||
export const StrikethroughFeatureClient = createClientFeature({
|
||||
enableFormats: ['strikethrough'],
|
||||
markdownTransformers: [STRIKETHROUGH],
|
||||
toolbarFixed: {
|
||||
groups: toolbarGroups,
|
||||
|
||||
@@ -29,6 +29,7 @@ const toolbarGroups: ToolbarGroup[] = [
|
||||
]
|
||||
|
||||
export const SubscriptFeatureClient = createClientFeature({
|
||||
enableFormats: ['subscript'],
|
||||
toolbarFixed: {
|
||||
groups: toolbarGroups,
|
||||
},
|
||||
|
||||
@@ -29,6 +29,7 @@ const toolbarGroups: ToolbarGroup[] = [
|
||||
]
|
||||
|
||||
export const SuperscriptFeatureClient = createClientFeature({
|
||||
enableFormats: ['superscript'],
|
||||
toolbarFixed: {
|
||||
groups: toolbarGroups,
|
||||
},
|
||||
|
||||
@@ -29,6 +29,7 @@ const toolbarGroups: ToolbarGroup[] = [
|
||||
]
|
||||
|
||||
export const UnderlineFeatureClient = createClientFeature({
|
||||
enableFormats: ['underline'],
|
||||
toolbarFixed: {
|
||||
groups: toolbarGroups,
|
||||
},
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import type { Klass, LexicalEditor, LexicalNode, LexicalNodeReplacement } from 'lexical'
|
||||
import type {
|
||||
Klass,
|
||||
LexicalEditor,
|
||||
LexicalNode,
|
||||
LexicalNodeReplacement,
|
||||
TextFormatType,
|
||||
} from 'lexical'
|
||||
import type { RichTextFieldClient } from 'payload'
|
||||
import type React from 'react'
|
||||
import type { JSX } from 'react'
|
||||
@@ -95,6 +101,10 @@ export type SanitizedPlugin =
|
||||
}
|
||||
|
||||
export type ClientFeature<ClientFeatureProps> = {
|
||||
/**
|
||||
* The text formats which are enabled by this feature.
|
||||
*/
|
||||
enableFormats?: Array<Omit<TextFormatType, 'highlight'>>
|
||||
markdownTransformers?: (
|
||||
| ((props: {
|
||||
allNodes: Array<Klass<LexicalNode> | LexicalNodeReplacement>
|
||||
@@ -204,6 +214,7 @@ export type ClientFeatureProviderMap = Map<string, FeatureProviderClient<any, an
|
||||
export type SanitizedClientFeatures = {
|
||||
/** The keys of all enabled features */
|
||||
enabledFeatures: string[]
|
||||
enabledFormats: Array<Omit<TextFormatType, 'highlight'>>
|
||||
markdownTransformers: Transformer[]
|
||||
|
||||
/**
|
||||
|
||||
@@ -18,6 +18,7 @@ import { AddBlockHandlePlugin } from './plugins/handles/AddBlockHandlePlugin/ind
|
||||
import { DraggableBlockPlugin } from './plugins/handles/DraggableBlockPlugin/index.js'
|
||||
import { MarkdownShortcutPlugin } from './plugins/MarkdownShortcut/index.js'
|
||||
import { SlashMenuPlugin } from './plugins/SlashMenu/index.js'
|
||||
import { TextPlugin } from './plugins/TextPlugin/index.js'
|
||||
import { LexicalContentEditable } from './ui/ContentEditable.js'
|
||||
|
||||
export const LexicalEditor: React.FC<
|
||||
@@ -121,6 +122,7 @@ export const LexicalEditor: React.FC<
|
||||
}
|
||||
ErrorBoundary={LexicalErrorBoundary}
|
||||
/>
|
||||
<TextPlugin features={editorConfig.features} />
|
||||
<OnChangePlugin
|
||||
// Selection changes can be ignored here, reducing the
|
||||
// frequency that the FieldComponent and Payload receive updates.
|
||||
|
||||
@@ -14,6 +14,7 @@ export const sanitizeClientFeatures = (
|
||||
): SanitizedClientFeatures => {
|
||||
const sanitized: SanitizedClientFeatures = {
|
||||
enabledFeatures: [],
|
||||
enabledFormats: [],
|
||||
markdownTransformers: [],
|
||||
nodes: [],
|
||||
plugins: [],
|
||||
@@ -39,6 +40,10 @@ export const sanitizeClientFeatures = (
|
||||
sanitized.providers = sanitized.providers.concat(feature.providers)
|
||||
}
|
||||
|
||||
if (feature.enableFormats?.length) {
|
||||
sanitized.enabledFormats.push(...feature.enableFormats)
|
||||
}
|
||||
|
||||
if (feature.nodes?.length) {
|
||||
// Important: do not use concat
|
||||
for (const node of feature.nodes) {
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
'use client'
|
||||
import type { TextFormatType } from 'lexical'
|
||||
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { TEXT_TYPE_TO_FORMAT, TextNode } from 'lexical'
|
||||
import { type SanitizedClientFeatures } from 'packages/richtext-lexical/src/features/typesClient.js'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
export function TextPlugin({ features }: { features: SanitizedClientFeatures }) {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
|
||||
useEffect(() => {
|
||||
const disabledFormats = getDisabledFormats(features.enabledFormats as TextFormatType[])
|
||||
if (disabledFormats.length === 0) {
|
||||
return
|
||||
}
|
||||
// Ideally override the TextNode with our own TextNode (changing its setFormat or toggleFormat methods),
|
||||
// would be more performant. If we find a noticeable perf regression we can switch to that option.
|
||||
// Overriding the FORMAT_TEXT_COMMAND and PASTE_COMMAND commands is not an option I considered because
|
||||
// there might be other forms of mutation that we might not be considering. For example:
|
||||
// browser extensions or Payload/Lexical plugins that have their own commands.
|
||||
return editor.registerNodeTransform(TextNode, (textNode) => {
|
||||
disabledFormats.forEach((disabledFormat) => {
|
||||
if (textNode.hasFormat(disabledFormat)) {
|
||||
textNode.toggleFormat(disabledFormat)
|
||||
}
|
||||
})
|
||||
})
|
||||
}, [editor, features])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function getDisabledFormats(enabledFormats: TextFormatType[]): TextFormatType[] {
|
||||
// not sure why Lexical added highlight as TextNode format.
|
||||
// see https://github.com/facebook/lexical/pull/3583
|
||||
// We are going to implement it in other way to support multiple colors
|
||||
delete TEXT_TYPE_TO_FORMAT.highlight
|
||||
const allFormats = Object.keys(TEXT_TYPE_TO_FORMAT) as TextFormatType[]
|
||||
const enabledSet = new Set(enabledFormats)
|
||||
|
||||
return allFormats.filter((format) => !enabledSet.has(format))
|
||||
}
|
||||
Reference in New Issue
Block a user