Skip to content

Commit 90f893a

Browse files
authored
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.
1 parent 5d18a52 commit 90f893a

File tree

11 files changed

+69
-1
lines changed

11 files changed

+69
-1
lines changed

packages/richtext-lexical/src/features/format/bold/feature.client.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export const BoldFeatureClient = createClientFeature(({ featureProviderMap }) =>
4040
}
4141

4242
return {
43+
enableFormats: ['bold'],
4344
markdownTransformers,
4445
toolbarFixed: {
4546
groups: toolbarGroups,

packages/richtext-lexical/src/features/format/inlineCode/feature.client.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ const toolbarGroups: ToolbarGroup[] = [
3030
]
3131

3232
export const InlineCodeFeatureClient = createClientFeature({
33+
enableFormats: ['code'],
3334
markdownTransformers: [INLINE_CODE],
3435
toolbarFixed: {
3536
groups: toolbarGroups,

packages/richtext-lexical/src/features/format/italic/feature.client.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ const toolbarGroups: ToolbarGroup[] = [
3030
]
3131

3232
export const ItalicFeatureClient = createClientFeature({
33+
enableFormats: ['italic'],
3334
markdownTransformers: [ITALIC_STAR, ITALIC_UNDERSCORE],
3435
toolbarFixed: {
3536
groups: toolbarGroups,

packages/richtext-lexical/src/features/format/strikethrough/feature.client.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const toolbarGroups = [
2828
]
2929

3030
export const StrikethroughFeatureClient = createClientFeature({
31+
enableFormats: ['strikethrough'],
3132
markdownTransformers: [STRIKETHROUGH],
3233
toolbarFixed: {
3334
groups: toolbarGroups,

packages/richtext-lexical/src/features/format/subscript/feature.client.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const toolbarGroups: ToolbarGroup[] = [
2929
]
3030

3131
export const SubscriptFeatureClient = createClientFeature({
32+
enableFormats: ['subscript'],
3233
toolbarFixed: {
3334
groups: toolbarGroups,
3435
},

packages/richtext-lexical/src/features/format/superscript/feature.client.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const toolbarGroups: ToolbarGroup[] = [
2929
]
3030

3131
export const SuperscriptFeatureClient = createClientFeature({
32+
enableFormats: ['superscript'],
3233
toolbarFixed: {
3334
groups: toolbarGroups,
3435
},

packages/richtext-lexical/src/features/format/underline/feature.client.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const toolbarGroups: ToolbarGroup[] = [
2929
]
3030

3131
export const UnderlineFeatureClient = createClientFeature({
32+
enableFormats: ['underline'],
3233
toolbarFixed: {
3334
groups: toolbarGroups,
3435
},

packages/richtext-lexical/src/features/typesClient.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import type { Klass, LexicalEditor, LexicalNode, LexicalNodeReplacement } from 'lexical'
1+
import type {
2+
Klass,
3+
LexicalEditor,
4+
LexicalNode,
5+
LexicalNodeReplacement,
6+
TextFormatType,
7+
} from 'lexical'
28
import type { RichTextFieldClient } from 'payload'
39
import type React from 'react'
410
import type { JSX } from 'react'
@@ -95,6 +101,10 @@ export type SanitizedPlugin =
95101
}
96102

97103
export type ClientFeature<ClientFeatureProps> = {
104+
/**
105+
* The text formats which are enabled by this feature.
106+
*/
107+
enableFormats?: Array<Omit<TextFormatType, 'highlight'>>
98108
markdownTransformers?: (
99109
| ((props: {
100110
allNodes: Array<Klass<LexicalNode> | LexicalNodeReplacement>
@@ -204,6 +214,7 @@ export type ClientFeatureProviderMap = Map<string, FeatureProviderClient<any, an
204214
export type SanitizedClientFeatures = {
205215
/** The keys of all enabled features */
206216
enabledFeatures: string[]
217+
enabledFormats: Array<Omit<TextFormatType, 'highlight'>>
207218
markdownTransformers: Transformer[]
208219

209220
/**

packages/richtext-lexical/src/lexical/LexicalEditor.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { AddBlockHandlePlugin } from './plugins/handles/AddBlockHandlePlugin/ind
1818
import { DraggableBlockPlugin } from './plugins/handles/DraggableBlockPlugin/index.js'
1919
import { MarkdownShortcutPlugin } from './plugins/MarkdownShortcut/index.js'
2020
import { SlashMenuPlugin } from './plugins/SlashMenu/index.js'
21+
import { TextPlugin } from './plugins/TextPlugin/index.js'
2122
import { LexicalContentEditable } from './ui/ContentEditable.js'
2223

2324
export const LexicalEditor: React.FC<
@@ -121,6 +122,7 @@ export const LexicalEditor: React.FC<
121122
}
122123
ErrorBoundary={LexicalErrorBoundary}
123124
/>
125+
<TextPlugin features={editorConfig.features} />
124126
<OnChangePlugin
125127
// Selection changes can be ignored here, reducing the
126128
// frequency that the FieldComponent and Payload receive updates.

packages/richtext-lexical/src/lexical/config/client/sanitize.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export const sanitizeClientFeatures = (
1414
): SanitizedClientFeatures => {
1515
const sanitized: SanitizedClientFeatures = {
1616
enabledFeatures: [],
17+
enabledFormats: [],
1718
markdownTransformers: [],
1819
nodes: [],
1920
plugins: [],
@@ -39,6 +40,10 @@ export const sanitizeClientFeatures = (
3940
sanitized.providers = sanitized.providers.concat(feature.providers)
4041
}
4142

43+
if (feature.enableFormats?.length) {
44+
sanitized.enabledFormats.push(...feature.enableFormats)
45+
}
46+
4247
if (feature.nodes?.length) {
4348
// Important: do not use concat
4449
for (const node of feature.nodes) {
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
'use client'
2+
import type { TextFormatType } from 'lexical'
3+
4+
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
5+
import { TEXT_TYPE_TO_FORMAT, TextNode } from 'lexical'
6+
import { type SanitizedClientFeatures } from 'packages/richtext-lexical/src/features/typesClient.js'
7+
import { useEffect } from 'react'
8+
9+
export function TextPlugin({ features }: { features: SanitizedClientFeatures }) {
10+
const [editor] = useLexicalComposerContext()
11+
12+
useEffect(() => {
13+
const disabledFormats = getDisabledFormats(features.enabledFormats as TextFormatType[])
14+
if (disabledFormats.length === 0) {
15+
return
16+
}
17+
// Ideally override the TextNode with our own TextNode (changing its setFormat or toggleFormat methods),
18+
// would be more performant. If we find a noticeable perf regression we can switch to that option.
19+
// Overriding the FORMAT_TEXT_COMMAND and PASTE_COMMAND commands is not an option I considered because
20+
// there might be other forms of mutation that we might not be considering. For example:
21+
// browser extensions or Payload/Lexical plugins that have their own commands.
22+
return editor.registerNodeTransform(TextNode, (textNode) => {
23+
disabledFormats.forEach((disabledFormat) => {
24+
if (textNode.hasFormat(disabledFormat)) {
25+
textNode.toggleFormat(disabledFormat)
26+
}
27+
})
28+
})
29+
}, [editor, features])
30+
31+
return null
32+
}
33+
34+
function getDisabledFormats(enabledFormats: TextFormatType[]): TextFormatType[] {
35+
// not sure why Lexical added highlight as TextNode format.
36+
// see https://github.com/facebook/lexical/pull/3583
37+
// We are going to implement it in other way to support multiple colors
38+
delete TEXT_TYPE_TO_FORMAT.highlight
39+
const allFormats = Object.keys(TEXT_TYPE_TO_FORMAT) as TextFormatType[]
40+
const enabledSet = new Set(enabledFormats)
41+
42+
return allFormats.filter((format) => !enabledSet.has(format))
43+
}

0 commit comments

Comments
 (0)