From 90f893a558b02ca2203060edd428cf094aa094f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Jablo=C3=B1ski?= <43938777+GermanJablo@users.noreply.github.com> Date: Tue, 26 Nov 2024 20:50:24 -0300 Subject: [PATCH] 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. --- .../features/format/bold/feature.client.tsx | 1 + .../format/inlineCode/feature.client.tsx | 1 + .../features/format/italic/feature.client.tsx | 1 + .../format/strikethrough/feature.client.tsx | 1 + .../format/subscript/feature.client.tsx | 1 + .../format/superscript/feature.client.tsx | 1 + .../format/underline/feature.client.tsx | 1 + .../src/features/typesClient.ts | 13 +++++- .../src/lexical/LexicalEditor.tsx | 2 + .../src/lexical/config/client/sanitize.ts | 5 +++ .../src/lexical/plugins/TextPlugin/index.tsx | 43 +++++++++++++++++++ 11 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 packages/richtext-lexical/src/lexical/plugins/TextPlugin/index.tsx diff --git a/packages/richtext-lexical/src/features/format/bold/feature.client.tsx b/packages/richtext-lexical/src/features/format/bold/feature.client.tsx index f9ece7ff14..aac344d5d5 100644 --- a/packages/richtext-lexical/src/features/format/bold/feature.client.tsx +++ b/packages/richtext-lexical/src/features/format/bold/feature.client.tsx @@ -40,6 +40,7 @@ export const BoldFeatureClient = createClientFeature(({ featureProviderMap }) => } return { + enableFormats: ['bold'], markdownTransformers, toolbarFixed: { groups: toolbarGroups, diff --git a/packages/richtext-lexical/src/features/format/inlineCode/feature.client.tsx b/packages/richtext-lexical/src/features/format/inlineCode/feature.client.tsx index c520914c17..bab2ce5dd5 100644 --- a/packages/richtext-lexical/src/features/format/inlineCode/feature.client.tsx +++ b/packages/richtext-lexical/src/features/format/inlineCode/feature.client.tsx @@ -30,6 +30,7 @@ const toolbarGroups: ToolbarGroup[] = [ ] export const InlineCodeFeatureClient = createClientFeature({ + enableFormats: ['code'], markdownTransformers: [INLINE_CODE], toolbarFixed: { groups: toolbarGroups, diff --git a/packages/richtext-lexical/src/features/format/italic/feature.client.tsx b/packages/richtext-lexical/src/features/format/italic/feature.client.tsx index 53e7aae7cb..db11e7507e 100644 --- a/packages/richtext-lexical/src/features/format/italic/feature.client.tsx +++ b/packages/richtext-lexical/src/features/format/italic/feature.client.tsx @@ -30,6 +30,7 @@ const toolbarGroups: ToolbarGroup[] = [ ] export const ItalicFeatureClient = createClientFeature({ + enableFormats: ['italic'], markdownTransformers: [ITALIC_STAR, ITALIC_UNDERSCORE], toolbarFixed: { groups: toolbarGroups, diff --git a/packages/richtext-lexical/src/features/format/strikethrough/feature.client.tsx b/packages/richtext-lexical/src/features/format/strikethrough/feature.client.tsx index 8e0f876424..239ff0a4a5 100644 --- a/packages/richtext-lexical/src/features/format/strikethrough/feature.client.tsx +++ b/packages/richtext-lexical/src/features/format/strikethrough/feature.client.tsx @@ -28,6 +28,7 @@ const toolbarGroups = [ ] export const StrikethroughFeatureClient = createClientFeature({ + enableFormats: ['strikethrough'], markdownTransformers: [STRIKETHROUGH], toolbarFixed: { groups: toolbarGroups, diff --git a/packages/richtext-lexical/src/features/format/subscript/feature.client.tsx b/packages/richtext-lexical/src/features/format/subscript/feature.client.tsx index c4fe700132..c30049399e 100644 --- a/packages/richtext-lexical/src/features/format/subscript/feature.client.tsx +++ b/packages/richtext-lexical/src/features/format/subscript/feature.client.tsx @@ -29,6 +29,7 @@ const toolbarGroups: ToolbarGroup[] = [ ] export const SubscriptFeatureClient = createClientFeature({ + enableFormats: ['subscript'], toolbarFixed: { groups: toolbarGroups, }, diff --git a/packages/richtext-lexical/src/features/format/superscript/feature.client.tsx b/packages/richtext-lexical/src/features/format/superscript/feature.client.tsx index ccb82eef7d..7408b7fd2a 100644 --- a/packages/richtext-lexical/src/features/format/superscript/feature.client.tsx +++ b/packages/richtext-lexical/src/features/format/superscript/feature.client.tsx @@ -29,6 +29,7 @@ const toolbarGroups: ToolbarGroup[] = [ ] export const SuperscriptFeatureClient = createClientFeature({ + enableFormats: ['superscript'], toolbarFixed: { groups: toolbarGroups, }, diff --git a/packages/richtext-lexical/src/features/format/underline/feature.client.tsx b/packages/richtext-lexical/src/features/format/underline/feature.client.tsx index cdacea8c78..6b3e2e92d5 100644 --- a/packages/richtext-lexical/src/features/format/underline/feature.client.tsx +++ b/packages/richtext-lexical/src/features/format/underline/feature.client.tsx @@ -29,6 +29,7 @@ const toolbarGroups: ToolbarGroup[] = [ ] export const UnderlineFeatureClient = createClientFeature({ + enableFormats: ['underline'], toolbarFixed: { groups: toolbarGroups, }, diff --git a/packages/richtext-lexical/src/features/typesClient.ts b/packages/richtext-lexical/src/features/typesClient.ts index 57bfe2d346..84dc726b86 100644 --- a/packages/richtext-lexical/src/features/typesClient.ts +++ b/packages/richtext-lexical/src/features/typesClient.ts @@ -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 = { + /** + * The text formats which are enabled by this feature. + */ + enableFormats?: Array> markdownTransformers?: ( | ((props: { allNodes: Array | LexicalNodeReplacement> @@ -204,6 +214,7 @@ export type ClientFeatureProviderMap = Map> markdownTransformers: Transformer[] /** diff --git a/packages/richtext-lexical/src/lexical/LexicalEditor.tsx b/packages/richtext-lexical/src/lexical/LexicalEditor.tsx index 7bcceade44..537ae24a0b 100644 --- a/packages/richtext-lexical/src/lexical/LexicalEditor.tsx +++ b/packages/richtext-lexical/src/lexical/LexicalEditor.tsx @@ -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} /> + { 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) { diff --git a/packages/richtext-lexical/src/lexical/plugins/TextPlugin/index.tsx b/packages/richtext-lexical/src/lexical/plugins/TextPlugin/index.tsx new file mode 100644 index 0000000000..400ccbba90 --- /dev/null +++ b/packages/richtext-lexical/src/lexical/plugins/TextPlugin/index.tsx @@ -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)) +}