Skip to content

Commit

Permalink
fix(richtext-lexical): prevent use of text formats whose features wer…
Browse files Browse the repository at this point in the history
…e 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.
  • Loading branch information
GermanJablo authored Nov 26, 2024
1 parent 5d18a52 commit 90f893a
Show file tree
Hide file tree
Showing 11 changed files with 69 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export const BoldFeatureClient = createClientFeature(({ featureProviderMap }) =>
}

return {
enableFormats: ['bold'],
markdownTransformers,
toolbarFixed: {
groups: toolbarGroups,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const toolbarGroups: ToolbarGroup[] = [
]

export const InlineCodeFeatureClient = createClientFeature({
enableFormats: ['code'],
markdownTransformers: [INLINE_CODE],
toolbarFixed: {
groups: toolbarGroups,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const toolbarGroups: ToolbarGroup[] = [
]

export const ItalicFeatureClient = createClientFeature({
enableFormats: ['italic'],
markdownTransformers: [ITALIC_STAR, ITALIC_UNDERSCORE],
toolbarFixed: {
groups: toolbarGroups,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const toolbarGroups = [
]

export const StrikethroughFeatureClient = createClientFeature({
enableFormats: ['strikethrough'],
markdownTransformers: [STRIKETHROUGH],
toolbarFixed: {
groups: toolbarGroups,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const toolbarGroups: ToolbarGroup[] = [
]

export const SubscriptFeatureClient = createClientFeature({
enableFormats: ['subscript'],
toolbarFixed: {
groups: toolbarGroups,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const toolbarGroups: ToolbarGroup[] = [
]

export const SuperscriptFeatureClient = createClientFeature({
enableFormats: ['superscript'],
toolbarFixed: {
groups: toolbarGroups,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const toolbarGroups: ToolbarGroup[] = [
]

export const UnderlineFeatureClient = createClientFeature({
enableFormats: ['underline'],
toolbarFixed: {
groups: toolbarGroups,
},
Expand Down
13 changes: 12 additions & 1 deletion packages/richtext-lexical/src/features/typesClient.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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>
Expand Down Expand Up @@ -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[]

/**
Expand Down
2 changes: 2 additions & 0 deletions packages/richtext-lexical/src/lexical/LexicalEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const sanitizeClientFeatures = (
): SanitizedClientFeatures => {
const sanitized: SanitizedClientFeatures = {
enabledFeatures: [],
enabledFormats: [],
markdownTransformers: [],
nodes: [],
plugins: [],
Expand All @@ -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) {
Expand Down
43 changes: 43 additions & 0 deletions packages/richtext-lexical/src/lexical/plugins/TextPlugin/index.tsx
Original file line number Diff line number Diff line change
@@ -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))
}

0 comments on commit 90f893a

Please sign in to comment.