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 f9ece7ff1..aac344d5d 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 c520914c1..bab2ce5dd 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 53e7aae7c..db11e7507 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 8e0f87642..239ff0a4a 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 c4fe70013..c30049399 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 ccb82eef7..7408b7fd2 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 cdacea8c7..6b3e2e92d 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 57bfe2d34..84dc726b8 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 7bcceade4..537ae24a0 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 000000000..400ccbba9 --- /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)) +}