From c462bf229fba5711fd1f8d33e848181ecdac3aa7 Mon Sep 17 00:00:00 2001 From: Alessio Gravili Date: Fri, 3 May 2024 19:55:26 -0400 Subject: [PATCH] feat(richtext-lexical)!: add FixedToolbarFeature (#6192) BREAKING: - The default inline toolbar has now been extracted into an `InlineToolbarFeature`. While it's part of the defaultFeatures, you might have to add it to your editor features if you are not including the defaultFeatures and still want to keep the inline toolbar (floating toolbar) - Some types have been renamed, e.g. `InlineToolbarGroup` is now `ToolbarGroup`, and `InlineToolbarGroupItem` is now `ToolbarGroupItem` - The `displayName` property of SlashMenuGroup and SlashMenuItem has been renamed to `label` to match the `label` prop of the toolbars - The `inlineToolbarFeatureButtonsGroupWithItem`, `inlineToolbarFormatGroupWithItems` and `inlineToolbarTextDropdownGroupWithItems` exports have been renamed to `toolbarTextDropdownGroupWithItems`, `toolbarFormatGroupWithItems`, `toolbarFeatureButtonsGroupWithItems` --- .../src/exports/components.ts | 11 +- .../field/features/align/feature.client.tsx | 182 +++++++++++---- .../features/align/inlineToolbarAlignGroup.ts | 16 -- .../field/features/align/toolbarAlignGroup.ts | 13 ++ .../features/blockquote/feature.client.tsx | 60 +++-- .../field/features/blocks/component/index.tsx | 3 +- .../field/features/blocks/feature.client.tsx | 40 +++- .../features/format/bold/feature.client.tsx | 44 ++-- .../format/inlineCode/feature.client.tsx | 44 ++-- .../features/format/italic/feature.client.tsx | 44 ++-- .../format/shared/inlineToolbarFormatGroup.ts | 15 -- .../format/shared/toolbarFormatGroup.ts | 10 + .../format/strikethrough/feature.client.tsx | 43 ++-- .../format/subscript/feature.client.tsx | 44 ++-- .../format/superscript/feature.client.tsx | 44 ++-- .../format/underline/feature.client.tsx | 44 ++-- .../field/features/heading/feature.client.tsx | 74 +++--- .../horizontalRule/feature.client.tsx | 35 ++- .../field/features/indent/feature.client.tsx | 88 ++++---- .../indent/inlineToolbarIndentGroup.ts | 13 -- .../features/indent/toolbarIndentGroup.ts | 10 + .../field/features/link/feature.client.tsx | 86 +++---- .../lists/checklist/feature.client.tsx | 67 ++++-- .../lists/orderedList/feature.client.tsx | 67 ++++-- .../lists/unorderedList/feature.client.tsx | 68 ++++-- .../features/paragraph/feature.client.tsx | 58 +++-- .../features/relationship/feature.client.tsx | 33 ++- .../inlineToolbar/featureButtonsGroup.ts | 15 -- .../shared/inlineToolbar/textDropdownGroup.ts | 18 -- .../shared/toolbar/addDropdownGroup.ts | 13 ++ .../shared/toolbar/featureButtonsGroup.ts | 10 + .../shared/toolbar/textDropdownGroup.ts | 13 ++ .../toolbars/fixed/Toolbar/index.scss | 81 +++++++ .../features/toolbars/fixed/Toolbar/index.tsx | 212 ++++++++++++++++++ .../toolbars/fixed/feature.client.tsx | 23 ++ .../features/toolbars/fixed/feature.server.ts | 17 ++ .../toolbars/inline/Toolbar/index.scss | 11 +- .../toolbars/inline/Toolbar/index.tsx | 71 ++++-- .../toolbars/inline/feature.client.tsx | 23 ++ .../toolbars/inline/feature.server.ts | 19 ++ .../toolbars/shared}/ToolbarButton/index.scss | 4 +- .../toolbars/shared}/ToolbarButton/index.tsx | 22 +- .../shared}/ToolbarDropdown/DropDown.tsx | 82 +++---- .../shared}/ToolbarDropdown/index.scss | 38 ++-- .../toolbars/shared/ToolbarDropdown/index.tsx | 175 +++++++++++++++ .../inline => features/toolbars}/types.ts | 31 ++- .../src/field/features/types.ts | 13 +- .../field/features/upload/feature.client.tsx | 33 ++- .../src/field/lexical/EditorFocusProvider.tsx | 100 +++++++++ .../src/field/lexical/LexicalEditor.tsx | 31 ++- .../src/field/lexical/LexicalProvider.tsx | 13 +- .../config/client/EditorConfigProvider.tsx | 6 +- .../field/lexical/config/server/default.ts | 3 + .../LexicalTypeaheadMenuPlugin/types.ts | 8 +- .../field/lexical/plugins/SlashMenu/index.tsx | 17 +- .../lexical/plugins/toolbars/fixed/types.ts | 40 ---- .../toolbars/inline/ToolbarDropdown/index.tsx | 68 ------ .../src/field/lexical/ui/icons/Add/index.tsx | 8 + packages/richtext-lexical/src/index.ts | 31 +-- test/fields/collections/Lexical/e2e.spec.ts | 14 +- test/fields/collections/Lexical/index.ts | 2 + 61 files changed, 1763 insertions(+), 758 deletions(-) delete mode 100644 packages/richtext-lexical/src/field/features/align/inlineToolbarAlignGroup.ts create mode 100644 packages/richtext-lexical/src/field/features/align/toolbarAlignGroup.ts delete mode 100644 packages/richtext-lexical/src/field/features/format/shared/inlineToolbarFormatGroup.ts create mode 100644 packages/richtext-lexical/src/field/features/format/shared/toolbarFormatGroup.ts delete mode 100644 packages/richtext-lexical/src/field/features/indent/inlineToolbarIndentGroup.ts create mode 100644 packages/richtext-lexical/src/field/features/indent/toolbarIndentGroup.ts delete mode 100644 packages/richtext-lexical/src/field/features/shared/inlineToolbar/featureButtonsGroup.ts delete mode 100644 packages/richtext-lexical/src/field/features/shared/inlineToolbar/textDropdownGroup.ts create mode 100644 packages/richtext-lexical/src/field/features/shared/toolbar/addDropdownGroup.ts create mode 100644 packages/richtext-lexical/src/field/features/shared/toolbar/featureButtonsGroup.ts create mode 100644 packages/richtext-lexical/src/field/features/shared/toolbar/textDropdownGroup.ts create mode 100644 packages/richtext-lexical/src/field/features/toolbars/fixed/Toolbar/index.scss create mode 100644 packages/richtext-lexical/src/field/features/toolbars/fixed/Toolbar/index.tsx create mode 100644 packages/richtext-lexical/src/field/features/toolbars/fixed/feature.client.tsx create mode 100644 packages/richtext-lexical/src/field/features/toolbars/fixed/feature.server.ts rename packages/richtext-lexical/src/field/{lexical/plugins => features}/toolbars/inline/Toolbar/index.scss (82%) rename packages/richtext-lexical/src/field/{lexical/plugins => features}/toolbars/inline/Toolbar/index.tsx (83%) create mode 100644 packages/richtext-lexical/src/field/features/toolbars/inline/feature.client.tsx create mode 100644 packages/richtext-lexical/src/field/features/toolbars/inline/feature.server.ts rename packages/richtext-lexical/src/field/{lexical/plugins/toolbars/inline => features/toolbars/shared}/ToolbarButton/index.scss (88%) rename packages/richtext-lexical/src/field/{lexical/plugins/toolbars/inline => features/toolbars/shared}/ToolbarButton/index.tsx (79%) rename packages/richtext-lexical/src/field/{lexical/plugins/toolbars/inline => features/toolbars/shared}/ToolbarDropdown/DropDown.tsx (78%) rename packages/richtext-lexical/src/field/{lexical/plugins/toolbars/inline => features/toolbars/shared}/ToolbarDropdown/index.scss (70%) create mode 100644 packages/richtext-lexical/src/field/features/toolbars/shared/ToolbarDropdown/index.tsx rename packages/richtext-lexical/src/field/{lexical/plugins/toolbars/inline => features/toolbars}/types.ts (56%) create mode 100644 packages/richtext-lexical/src/field/lexical/EditorFocusProvider.tsx delete mode 100644 packages/richtext-lexical/src/field/lexical/plugins/toolbars/fixed/types.ts delete mode 100644 packages/richtext-lexical/src/field/lexical/plugins/toolbars/inline/ToolbarDropdown/index.tsx create mode 100644 packages/richtext-lexical/src/field/lexical/ui/icons/Add/index.tsx diff --git a/packages/richtext-lexical/src/exports/components.ts b/packages/richtext-lexical/src/exports/components.ts index 13a033a165..faeec867ea 100644 --- a/packages/richtext-lexical/src/exports/components.ts +++ b/packages/richtext-lexical/src/exports/components.ts @@ -1,6 +1,11 @@ export { RichTextCell } from '../cell/index.js' -export { RichTextField } from '../field/index.js' +export { ToolbarButton } from '../field/features/toolbars/shared/ToolbarButton/index.js' +export { ToolbarDropdown } from '../field/features/toolbars/shared/ToolbarDropdown/index.js' +export { RichTextField } from '../field/index.js' +export { + type EditorFocusContextType, + EditorFocusProvider, + useEditorFocus, +} from '../field/lexical/EditorFocusProvider.js' export { defaultEditorLexicalConfig } from '../field/lexical/config/client/default.js' -export { ToolbarButton } from '../field/lexical/plugins/toolbars/inline/ToolbarButton/index.js' -export { ToolbarDropdown } from '../field/lexical/plugins/toolbars/inline/ToolbarDropdown/index.js' diff --git a/packages/richtext-lexical/src/field/features/align/feature.client.tsx b/packages/richtext-lexical/src/field/features/align/feature.client.tsx index 3acbcc76f0..2623a09ebf 100644 --- a/packages/richtext-lexical/src/field/features/align/feature.client.tsx +++ b/packages/richtext-lexical/src/field/features/align/feature.client.tsx @@ -1,7 +1,8 @@ 'use client' -import { FORMAT_ELEMENT_COMMAND } from 'lexical' +import { $isElementNode, $isRangeSelection, FORMAT_ELEMENT_COMMAND } from 'lexical' +import type { ToolbarGroup } from '../toolbars/types.js' import type { FeatureProviderProviderClient } from '../types.js' import { AlignCenterIcon } from '../../lexical/ui/icons/AlignCenter/index.js' @@ -9,58 +10,147 @@ import { AlignJustifyIcon } from '../../lexical/ui/icons/AlignJustify/index.js' import { AlignLeftIcon } from '../../lexical/ui/icons/AlignLeft/index.js' import { AlignRightIcon } from '../../lexical/ui/icons/AlignRight/index.js' import { createClientComponent } from '../createClientComponent.js' -import { alignGroupWithItems } from './inlineToolbarAlignGroup.js' +import { toolbarAlignGroupWithItems } from './toolbarAlignGroup.js' + +const toolbarGroups: ToolbarGroup[] = [ + toolbarAlignGroupWithItems([ + { + ChildComponent: AlignLeftIcon, + isActive: ({ selection }) => { + if (!$isRangeSelection(selection)) { + return false + } + for (const node of selection.getNodes()) { + if ($isElementNode(node)) { + if (node.getFormatType() === 'left') { + continue + } + } + + const parent = node.getParent() + if ($isElementNode(parent)) { + if (parent.getFormatType() === 'left') { + continue + } + } + + return false + } + return true + }, + key: 'alignLeft', + label: `Align Left`, + onSelect: ({ editor }) => { + editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'left') + }, + order: 1, + }, + { + ChildComponent: AlignCenterIcon, + isActive: ({ selection }) => { + if (!$isRangeSelection(selection)) { + return false + } + for (const node of selection.getNodes()) { + if ($isElementNode(node)) { + if (node.getFormatType() === 'center') { + continue + } + } + + const parent = node.getParent() + if ($isElementNode(parent)) { + if (parent.getFormatType() === 'center') { + continue + } + } + + return false + } + return true + }, + key: 'alignCenter', + label: `Align Center`, + onSelect: ({ editor }) => { + editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'center') + }, + order: 2, + }, + { + ChildComponent: AlignRightIcon, + isActive: ({ selection }) => { + if (!$isRangeSelection(selection)) { + return false + } + for (const node of selection.getNodes()) { + if ($isElementNode(node)) { + if (node.getFormatType() === 'right') { + continue + } + } + + const parent = node.getParent() + if ($isElementNode(parent)) { + if (parent.getFormatType() === 'right') { + continue + } + } + + return false + } + return true + }, + key: 'alignRight', + label: `Align Right`, + onSelect: ({ editor }) => { + editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'right') + }, + order: 3, + }, + { + ChildComponent: AlignJustifyIcon, + isActive: ({ selection }) => { + if (!$isRangeSelection(selection)) { + return false + } + for (const node of selection.getNodes()) { + if ($isElementNode(node)) { + if (node.getFormatType() === 'justify') { + continue + } + } + + const parent = node.getParent() + if ($isElementNode(parent)) { + if (parent.getFormatType() === 'justify') { + continue + } + } + + return false + } + return true + }, + key: 'alignJustify', + label: `Align Justify`, + onSelect: ({ editor }) => { + editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'justify') + }, + order: 4, + }, + ]), +] const AlignFeatureClient: FeatureProviderProviderClient = (props) => { return { clientFeatureProps: props, feature: () => ({ clientFeatureProps: props, + toolbarFixed: { + groups: toolbarGroups, + }, toolbarInline: { - groups: [ - alignGroupWithItems([ - { - ChildComponent: AlignLeftIcon, - isActive: () => false, - key: 'alignLeft', - label: `Align Left`, - onSelect: ({ editor }) => { - editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'left') - }, - order: 1, - }, - { - ChildComponent: AlignCenterIcon, - isActive: () => false, - key: 'alignCenter', - label: `Align Center`, - onSelect: ({ editor }) => { - editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'center') - }, - order: 2, - }, - { - ChildComponent: AlignRightIcon, - isActive: () => false, - key: 'alignRight', - label: `Align Right`, - onSelect: ({ editor }) => { - editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'right') - }, - order: 3, - }, - { - ChildComponent: AlignJustifyIcon, - isActive: () => false, - key: 'alignJustify', - label: `Align Justify`, - onSelect: ({ editor }) => { - editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'justify') - }, - order: 4, - }, - ]), - ], + groups: toolbarGroups, }, }), } diff --git a/packages/richtext-lexical/src/field/features/align/inlineToolbarAlignGroup.ts b/packages/richtext-lexical/src/field/features/align/inlineToolbarAlignGroup.ts deleted file mode 100644 index e559e82e67..0000000000 --- a/packages/richtext-lexical/src/field/features/align/inlineToolbarAlignGroup.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { - InlineToolbarGroup, - InlineToolbarGroupItem, -} from '../../lexical/plugins/toolbars/inline/types.js' - -import { AlignLeftIcon } from '../../lexical/ui/icons/AlignLeft/index.js' - -export const alignGroupWithItems = (items: InlineToolbarGroupItem[]): InlineToolbarGroup => { - return { - type: 'dropdown', - ChildComponent: AlignLeftIcon, - items, - key: 'align', - order: 2, - } -} diff --git a/packages/richtext-lexical/src/field/features/align/toolbarAlignGroup.ts b/packages/richtext-lexical/src/field/features/align/toolbarAlignGroup.ts new file mode 100644 index 0000000000..2b6f540ffb --- /dev/null +++ b/packages/richtext-lexical/src/field/features/align/toolbarAlignGroup.ts @@ -0,0 +1,13 @@ +import type { ToolbarGroup, ToolbarGroupItem } from '../toolbars/types.js' + +import { AlignLeftIcon } from '../../lexical/ui/icons/AlignLeft/index.js' + +export const toolbarAlignGroupWithItems = (items: ToolbarGroupItem[]): ToolbarGroup => { + return { + type: 'dropdown', + ChildComponent: AlignLeftIcon, + items, + key: 'align', + order: 30, + } +} diff --git a/packages/richtext-lexical/src/field/features/blockquote/feature.client.tsx b/packages/richtext-lexical/src/field/features/blockquote/feature.client.tsx index ce6ac160e2..d8ca8c9f8f 100644 --- a/packages/richtext-lexical/src/field/features/blockquote/feature.client.tsx +++ b/packages/richtext-lexical/src/field/features/blockquote/feature.client.tsx @@ -1,16 +1,45 @@ 'use client' -import { $createQuoteNode, QuoteNode } from '@lexical/rich-text' +import { $createQuoteNode, $isQuoteNode, QuoteNode } from '@lexical/rich-text' import { $setBlocksType } from '@lexical/selection' -import { $getSelection } from 'lexical' +import { $getSelection, $isRangeSelection } from 'lexical' +import type { ToolbarGroup } from '../toolbars/types.js' import type { FeatureProviderProviderClient } from '../types.js' import { BlockquoteIcon } from '../../lexical/ui/icons/Blockquote/index.js' import { createClientComponent } from '../createClientComponent.js' -import { inlineToolbarTextDropdownGroupWithItems } from '../shared/inlineToolbar/textDropdownGroup.js' +import { toolbarTextDropdownGroupWithItems } from '../shared/toolbar/textDropdownGroup.js' import { MarkdownTransformer } from './markdownTransformer.js' +const toolbarGroups: ToolbarGroup[] = [ + toolbarTextDropdownGroupWithItems([ + { + ChildComponent: BlockquoteIcon, + isActive: ({ selection }) => { + if (!$isRangeSelection(selection)) { + return false + } + for (const node of selection.getNodes()) { + if (!$isQuoteNode(node) && !$isQuoteNode(node.getParent())) { + return false + } + } + return true + }, + key: 'blockquote', + label: `Blockquote`, + onSelect: ({ editor }) => { + editor.update(() => { + const selection = $getSelection() + $setBlocksType(selection, () => $createQuoteNode()) + }) + }, + order: 20, + }, + ]), +] + const BlockQuoteFeatureClient: FeatureProviderProviderClient = (props) => { return { clientFeatureProps: props, @@ -22,13 +51,12 @@ const BlockQuoteFeatureClient: FeatureProviderProviderClient = (props slashMenu: { groups: [ { - displayName: 'Basic', items: [ { Icon: BlockquoteIcon, - displayName: 'Blockquote', key: 'blockquote', keywords: ['quote', 'blockquote'], + label: 'Blockquote', onSelect: ({ editor }) => { editor.update(() => { const selection = $getSelection() @@ -38,27 +66,15 @@ const BlockQuoteFeatureClient: FeatureProviderProviderClient = (props }, ], key: 'basic', + label: 'Basic', }, ], }, + toolbarFixed: { + groups: toolbarGroups, + }, toolbarInline: { - groups: [ - inlineToolbarTextDropdownGroupWithItems([ - { - ChildComponent: BlockquoteIcon, - isActive: () => false, - key: 'blockquote', - label: `Blockquote`, - onSelect: ({ editor }) => { - editor.update(() => { - const selection = $getSelection() - $setBlocksType(selection, () => $createQuoteNode()) - }) - }, - order: 20, - }, - ]), - ], + groups: toolbarGroups, }, }), } diff --git a/packages/richtext-lexical/src/field/features/blocks/component/index.tsx b/packages/richtext-lexical/src/field/features/blocks/component/index.tsx index 46486612a4..74099a9eca 100644 --- a/packages/richtext-lexical/src/field/features/blocks/component/index.tsx +++ b/packages/richtext-lexical/src/field/features/blocks/component/index.tsx @@ -187,6 +187,5 @@ export const BlockComponent: React.FC = (props) => { schemaFieldsPath, path, ]) // Adding formData to the dependencies here might break it - - return
{formContent}
+ return
{formContent}
} diff --git a/packages/richtext-lexical/src/field/features/blocks/feature.client.tsx b/packages/richtext-lexical/src/field/features/blocks/feature.client.tsx index fd8e7a47cb..a540917c45 100644 --- a/packages/richtext-lexical/src/field/features/blocks/feature.client.tsx +++ b/packages/richtext-lexical/src/field/features/blocks/feature.client.tsx @@ -31,19 +31,18 @@ const BlocksFeatureClient: FeatureProviderProviderClient { return { Icon: BlockIcon, - displayName: ({ i18n }) => { + key: 'block-' + block.slug, + keywords: ['block', 'blocks', block.slug], + label: ({ i18n }) => { if (!block.labels.singular) { return block.slug } return getTranslation(block.labels.singular, i18n) }, - key: 'block-' + block.slug, - keywords: ['block', 'blocks', block.slug], onSelect: ({ editor }) => { editor.dispatchCommand(INSERT_BLOCK_COMMAND, { id: null, @@ -54,6 +53,39 @@ const BlocksFeatureClient: FeatureProviderProviderClient { + return { + ChildComponent: BlockIcon, + isActive: undefined, // At this point, we would be inside a sub-richtext-editor. And at this point this will be run against the focused sub-editor, not the parent editor which has the actual block. Thus, no point in running this + key: 'block-' + block.slug, + label: ({ i18n }) => { + if (!block.labels.singular) { + return block.slug + } + + return getTranslation(block.labels.singular, i18n) + }, + onSelect: ({ editor }) => { + editor.dispatchCommand(INSERT_BLOCK_COMMAND, { + id: null, + blockName: '', + blockType: block.slug, + }) + }, + order: index, + } + }), + key: 'blocks', + order: 20, }, ], }, diff --git a/packages/richtext-lexical/src/field/features/format/bold/feature.client.tsx b/packages/richtext-lexical/src/field/features/format/bold/feature.client.tsx index 3399054576..90f700c570 100644 --- a/packages/richtext-lexical/src/field/features/format/bold/feature.client.tsx +++ b/packages/richtext-lexical/src/field/features/format/bold/feature.client.tsx @@ -1,11 +1,12 @@ 'use client' import { $isRangeSelection, FORMAT_TEXT_COMMAND } from 'lexical' +import type { ToolbarGroup } from '../../toolbars/types.js' import type { FeatureProviderProviderClient } from '../../types.js' import { BoldIcon } from '../../../lexical/ui/icons/Bold/index.js' import { createClientComponent } from '../../createClientComponent.js' -import { inlineToolbarFormatGroupWithItems } from '../shared/inlineToolbarFormatGroup.js' +import { toolbarFormatGroupWithItems } from '../shared/toolbarFormatGroup.js' import { BOLD_ITALIC_STAR, BOLD_ITALIC_UNDERSCORE, @@ -13,6 +14,25 @@ import { BOLD_UNDERSCORE, } from './markdownTransformers.js' +const toolbarGroups: ToolbarGroup[] = [ + toolbarFormatGroupWithItems([ + { + ChildComponent: BoldIcon, + isActive: ({ selection }) => { + if ($isRangeSelection(selection)) { + return selection.hasFormat('bold') + } + return false + }, + key: 'bold', + onSelect: ({ editor }) => { + editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold') + }, + order: 1, + }, + ]), +] + const BoldFeatureClient: FeatureProviderProviderClient = (props) => { return { clientFeatureProps: props, @@ -25,25 +45,11 @@ const BoldFeatureClient: FeatureProviderProviderClient = (props) => { return { clientFeatureProps: props, markdownTransformers, + toolbarFixed: { + groups: toolbarGroups, + }, toolbarInline: { - groups: [ - inlineToolbarFormatGroupWithItems([ - { - ChildComponent: BoldIcon, - isActive: ({ selection }) => { - if ($isRangeSelection(selection)) { - return selection.hasFormat('bold') - } - return false - }, - key: 'bold', - onSelect: ({ editor }) => { - editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold') - }, - order: 1, - }, - ]), - ], + groups: toolbarGroups, }, } }, diff --git a/packages/richtext-lexical/src/field/features/format/inlineCode/feature.client.tsx b/packages/richtext-lexical/src/field/features/format/inlineCode/feature.client.tsx index f36201a582..618f3f2822 100644 --- a/packages/richtext-lexical/src/field/features/format/inlineCode/feature.client.tsx +++ b/packages/richtext-lexical/src/field/features/format/inlineCode/feature.client.tsx @@ -2,13 +2,33 @@ import { $isRangeSelection, FORMAT_TEXT_COMMAND } from 'lexical' +import type { ToolbarGroup } from '../../toolbars/types.js' import type { FeatureProviderProviderClient } from '../../types.js' import { CodeIcon } from '../../../lexical/ui/icons/Code/index.js' import { createClientComponent } from '../../createClientComponent.js' -import { inlineToolbarFormatGroupWithItems } from '../shared/inlineToolbarFormatGroup.js' +import { toolbarFormatGroupWithItems } from '../shared/toolbarFormatGroup.js' import { INLINE_CODE } from './markdownTransformers.js' +const toolbarGroups: ToolbarGroup[] = [ + toolbarFormatGroupWithItems([ + { + ChildComponent: CodeIcon, + isActive: ({ selection }) => { + if ($isRangeSelection(selection)) { + return selection.hasFormat('code') + } + return false + }, + key: 'inlineCode', + onSelect: ({ editor }) => { + editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'code') + }, + order: 7, + }, + ]), +] + const InlineCodeFeatureClient: FeatureProviderProviderClient = (props) => { return { clientFeatureProps: props, @@ -17,25 +37,11 @@ const InlineCodeFeatureClient: FeatureProviderProviderClient = (props clientFeatureProps: props, markdownTransformers: [INLINE_CODE], + toolbarFixed: { + groups: toolbarGroups, + }, toolbarInline: { - groups: [ - inlineToolbarFormatGroupWithItems([ - { - ChildComponent: CodeIcon, - isActive: ({ selection }) => { - if ($isRangeSelection(selection)) { - return selection.hasFormat('code') - } - return false - }, - key: 'inlineCode', - onSelect: ({ editor }) => { - editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'code') - }, - order: 7, - }, - ]), - ], + groups: toolbarGroups, }, } }, diff --git a/packages/richtext-lexical/src/field/features/format/italic/feature.client.tsx b/packages/richtext-lexical/src/field/features/format/italic/feature.client.tsx index bf46776d31..bebea20625 100644 --- a/packages/richtext-lexical/src/field/features/format/italic/feature.client.tsx +++ b/packages/richtext-lexical/src/field/features/format/italic/feature.client.tsx @@ -2,13 +2,33 @@ import { $isRangeSelection, FORMAT_TEXT_COMMAND } from 'lexical' +import type { ToolbarGroup } from '../../toolbars/types.js' import type { FeatureProviderProviderClient } from '../../types.js' import { ItalicIcon } from '../../../lexical/ui/icons/Italic/index.js' import { createClientComponent } from '../../createClientComponent.js' -import { inlineToolbarFormatGroupWithItems } from '../shared/inlineToolbarFormatGroup.js' +import { toolbarFormatGroupWithItems } from '../shared/toolbarFormatGroup.js' import { ITALIC_STAR, ITALIC_UNDERSCORE } from './markdownTransformers.js' +const toolbarGroups: ToolbarGroup[] = [ + toolbarFormatGroupWithItems([ + { + ChildComponent: ItalicIcon, + isActive: ({ selection }) => { + if ($isRangeSelection(selection)) { + return selection.hasFormat('italic') + } + return false + }, + key: 'italic', + onSelect: ({ editor }) => { + editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'italic') + }, + order: 2, + }, + ]), +] + const ItalicFeatureClient: FeatureProviderProviderClient = (props) => { return { clientFeatureProps: props, @@ -17,25 +37,11 @@ const ItalicFeatureClient: FeatureProviderProviderClient = (props) => clientFeatureProps: props, markdownTransformers: [ITALIC_STAR, ITALIC_UNDERSCORE], + toolbarFixed: { + groups: toolbarGroups, + }, toolbarInline: { - groups: [ - inlineToolbarFormatGroupWithItems([ - { - ChildComponent: ItalicIcon, - isActive: ({ selection }) => { - if ($isRangeSelection(selection)) { - return selection.hasFormat('italic') - } - return false - }, - key: 'italic', - onSelect: ({ editor }) => { - editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'italic') - }, - order: 2, - }, - ]), - ], + groups: toolbarGroups, }, } }, diff --git a/packages/richtext-lexical/src/field/features/format/shared/inlineToolbarFormatGroup.ts b/packages/richtext-lexical/src/field/features/format/shared/inlineToolbarFormatGroup.ts deleted file mode 100644 index d26dd61d68..0000000000 --- a/packages/richtext-lexical/src/field/features/format/shared/inlineToolbarFormatGroup.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { - InlineToolbarGroup, - InlineToolbarGroupItem, -} from '../../../lexical/plugins/toolbars/inline/types.js' - -export const inlineToolbarFormatGroupWithItems = ( - items: InlineToolbarGroupItem[], -): InlineToolbarGroup => { - return { - type: 'buttons', - items, - key: 'format', - order: 4, - } -} diff --git a/packages/richtext-lexical/src/field/features/format/shared/toolbarFormatGroup.ts b/packages/richtext-lexical/src/field/features/format/shared/toolbarFormatGroup.ts new file mode 100644 index 0000000000..180bf0faa6 --- /dev/null +++ b/packages/richtext-lexical/src/field/features/format/shared/toolbarFormatGroup.ts @@ -0,0 +1,10 @@ +import type { ToolbarGroup, ToolbarGroupItem } from '../../toolbars/types.js' + +export const toolbarFormatGroupWithItems = (items: ToolbarGroupItem[]): ToolbarGroup => { + return { + type: 'buttons', + items, + key: 'format', + order: 40, + } +} diff --git a/packages/richtext-lexical/src/field/features/format/strikethrough/feature.client.tsx b/packages/richtext-lexical/src/field/features/format/strikethrough/feature.client.tsx index 5463f73b99..b6acc50df2 100644 --- a/packages/richtext-lexical/src/field/features/format/strikethrough/feature.client.tsx +++ b/packages/richtext-lexical/src/field/features/format/strikethrough/feature.client.tsx @@ -6,9 +6,28 @@ import type { FeatureProviderProviderClient } from '../../types.js' import { StrikethroughIcon } from '../../../lexical/ui/icons/Strikethrough/index.js' import { createClientComponent } from '../../createClientComponent.js' -import { inlineToolbarFormatGroupWithItems } from '../shared/inlineToolbarFormatGroup.js' +import { toolbarFormatGroupWithItems } from '../shared/toolbarFormatGroup.js' import { STRIKETHROUGH } from './markdownTransformers.js' +const toolbarGroups = [ + toolbarFormatGroupWithItems([ + { + ChildComponent: StrikethroughIcon, + isActive: ({ selection }) => { + if ($isRangeSelection(selection)) { + return selection.hasFormat('strikethrough') + } + return false + }, + key: 'strikethrough', + onSelect: ({ editor }) => { + editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'strikethrough') + }, + order: 4, + }, + ]), +] + const StrikethroughFeatureClient: FeatureProviderProviderClient = (props) => { return { clientFeatureProps: props, @@ -17,25 +36,11 @@ const StrikethroughFeatureClient: FeatureProviderProviderClient = (pr clientFeatureProps: props, markdownTransformers: [STRIKETHROUGH], + toolbarFixed: { + groups: toolbarGroups, + }, toolbarInline: { - groups: [ - inlineToolbarFormatGroupWithItems([ - { - ChildComponent: StrikethroughIcon, - isActive: ({ selection }) => { - if ($isRangeSelection(selection)) { - return selection.hasFormat('strikethrough') - } - return false - }, - key: 'strikethrough', - onSelect: ({ editor }) => { - editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'strikethrough') - }, - order: 4, - }, - ]), - ], + groups: toolbarGroups, }, } }, diff --git a/packages/richtext-lexical/src/field/features/format/subscript/feature.client.tsx b/packages/richtext-lexical/src/field/features/format/subscript/feature.client.tsx index 02a219871a..891399fde8 100644 --- a/packages/richtext-lexical/src/field/features/format/subscript/feature.client.tsx +++ b/packages/richtext-lexical/src/field/features/format/subscript/feature.client.tsx @@ -2,11 +2,31 @@ import { $isRangeSelection, FORMAT_TEXT_COMMAND } from 'lexical' +import type { ToolbarGroup } from '../../toolbars/types.js' import type { FeatureProviderProviderClient } from '../../types.js' import { SubscriptIcon } from '../../../lexical/ui/icons/Subscript/index.js' import { createClientComponent } from '../../createClientComponent.js' -import { inlineToolbarFormatGroupWithItems } from '../shared/inlineToolbarFormatGroup.js' +import { toolbarFormatGroupWithItems } from '../shared/toolbarFormatGroup.js' + +const toolbarGroups: ToolbarGroup[] = [ + toolbarFormatGroupWithItems([ + { + ChildComponent: SubscriptIcon, + isActive: ({ selection }) => { + if ($isRangeSelection(selection)) { + return selection.hasFormat('subscript') + } + return false + }, + key: 'subscript', + onSelect: ({ editor }) => { + editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'subscript') + }, + order: 5, + }, + ]), +] const SubscriptFeatureClient: FeatureProviderProviderClient = (props) => { return { @@ -14,25 +34,11 @@ const SubscriptFeatureClient: FeatureProviderProviderClient = (props) feature: () => { return { clientFeatureProps: props, + toolbarFixed: { + groups: toolbarGroups, + }, toolbarInline: { - groups: [ - inlineToolbarFormatGroupWithItems([ - { - ChildComponent: SubscriptIcon, - isActive: ({ selection }) => { - if ($isRangeSelection(selection)) { - return selection.hasFormat('subscript') - } - return false - }, - key: 'subscript', - onSelect: ({ editor }) => { - editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'subscript') - }, - order: 5, - }, - ]), - ], + groups: toolbarGroups, }, } }, diff --git a/packages/richtext-lexical/src/field/features/format/superscript/feature.client.tsx b/packages/richtext-lexical/src/field/features/format/superscript/feature.client.tsx index a482a05619..e9e56f150c 100644 --- a/packages/richtext-lexical/src/field/features/format/superscript/feature.client.tsx +++ b/packages/richtext-lexical/src/field/features/format/superscript/feature.client.tsx @@ -2,11 +2,31 @@ import { $isRangeSelection, FORMAT_TEXT_COMMAND } from 'lexical' +import type { ToolbarGroup } from '../../toolbars/types.js' import type { FeatureProviderProviderClient } from '../../types.js' import { SuperscriptIcon } from '../../../lexical/ui/icons/Superscript/index.js' import { createClientComponent } from '../../createClientComponent.js' -import { inlineToolbarFormatGroupWithItems } from '../shared/inlineToolbarFormatGroup.js' +import { toolbarFormatGroupWithItems } from '../shared/toolbarFormatGroup.js' + +const toolbarGroups: ToolbarGroup[] = [ + toolbarFormatGroupWithItems([ + { + ChildComponent: SuperscriptIcon, + isActive: ({ selection }) => { + if ($isRangeSelection(selection)) { + return selection.hasFormat('superscript') + } + return false + }, + key: 'superscript', + onSelect: ({ editor }) => { + editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'superscript') + }, + order: 6, + }, + ]), +] const SuperscriptFeatureClient: FeatureProviderProviderClient = (props) => { return { @@ -14,25 +34,11 @@ const SuperscriptFeatureClient: FeatureProviderProviderClient = (prop feature: () => { return { clientFeatureProps: props, + toolbarFixed: { + groups: toolbarGroups, + }, toolbarInline: { - groups: [ - inlineToolbarFormatGroupWithItems([ - { - ChildComponent: SuperscriptIcon, - isActive: ({ selection }) => { - if ($isRangeSelection(selection)) { - return selection.hasFormat('superscript') - } - return false - }, - key: 'superscript', - onSelect: ({ editor }) => { - editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'superscript') - }, - order: 6, - }, - ]), - ], + groups: toolbarGroups, }, } }, diff --git a/packages/richtext-lexical/src/field/features/format/underline/feature.client.tsx b/packages/richtext-lexical/src/field/features/format/underline/feature.client.tsx index 2028ebe27e..ae62287a36 100644 --- a/packages/richtext-lexical/src/field/features/format/underline/feature.client.tsx +++ b/packages/richtext-lexical/src/field/features/format/underline/feature.client.tsx @@ -2,11 +2,31 @@ import { $isRangeSelection, FORMAT_TEXT_COMMAND } from 'lexical' +import type { ToolbarGroup } from '../../toolbars/types.js' import type { FeatureProviderProviderClient } from '../../types.js' import { UnderlineIcon } from '../../../lexical/ui/icons/Underline/index.js' import { createClientComponent } from '../../createClientComponent.js' -import { inlineToolbarFormatGroupWithItems } from '../shared/inlineToolbarFormatGroup.js' +import { toolbarFormatGroupWithItems } from '../shared/toolbarFormatGroup.js' + +const toolbarGroups: ToolbarGroup[] = [ + toolbarFormatGroupWithItems([ + { + ChildComponent: UnderlineIcon, + isActive: ({ selection }) => { + if ($isRangeSelection(selection)) { + return selection.hasFormat('underline') + } + return false + }, + key: 'underline', + onSelect: ({ editor }) => { + editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'underline') + }, + order: 3, + }, + ]), +] const UnderlineFeatureClient: FeatureProviderProviderClient = (props) => { return { @@ -14,25 +34,11 @@ const UnderlineFeatureClient: FeatureProviderProviderClient = (props) feature: () => { return { clientFeatureProps: props, + toolbarFixed: { + groups: toolbarGroups, + }, toolbarInline: { - groups: [ - inlineToolbarFormatGroupWithItems([ - { - ChildComponent: UnderlineIcon, - isActive: ({ selection }) => { - if ($isRangeSelection(selection)) { - return selection.hasFormat('underline') - } - return false - }, - key: 'underline', - onSelect: ({ editor }) => { - editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'underline') - }, - order: 3, - }, - ]), - ], + groups: toolbarGroups, }, } }, diff --git a/packages/richtext-lexical/src/field/features/heading/feature.client.tsx b/packages/richtext-lexical/src/field/features/heading/feature.client.tsx index 43ef5a9bd3..87123f8025 100644 --- a/packages/richtext-lexical/src/field/features/heading/feature.client.tsx +++ b/packages/richtext-lexical/src/field/features/heading/feature.client.tsx @@ -2,10 +2,12 @@ import type { HeadingTagType } from '@lexical/rich-text' +import { $isHeadingNode } from '@lexical/rich-text' import { $createHeadingNode, HeadingNode } from '@lexical/rich-text' import { $setBlocksType } from '@lexical/selection' -import { $getSelection } from 'lexical' +import { $getSelection, $isRangeSelection } from 'lexical' +import type { ToolbarGroup } from '../toolbars/types.js' import type { FeatureProviderProviderClient } from '../types.js' import type { HeadingFeatureProps } from './feature.server.js' @@ -16,7 +18,7 @@ import { H4Icon } from '../../lexical/ui/icons/H4/index.js' import { H5Icon } from '../../lexical/ui/icons/H5/index.js' import { H6Icon } from '../../lexical/ui/icons/H6/index.js' import { createClientComponent } from '../createClientComponent.js' -import { inlineToolbarTextDropdownGroupWithItems } from '../shared/inlineToolbar/textDropdownGroup.js' +import { toolbarTextDropdownGroupWithItems } from '../shared/toolbar/textDropdownGroup.js' import { MarkdownTransformer } from './markdownTransformer.js' const setHeading = (headingSize: HeadingTagType) => { @@ -34,11 +36,47 @@ const iconImports = { } const HeadingFeatureClient: FeatureProviderProviderClient = (props) => { - const { enabledHeadingSizes = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] } = props - return { clientFeatureProps: props, feature: () => { + const { enabledHeadingSizes = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] } = props + + const toolbarGroups: ToolbarGroup[] = [ + toolbarTextDropdownGroupWithItems( + enabledHeadingSizes.map((headingSize, i) => { + return { + ChildComponent: iconImports[headingSize], + isActive: ({ selection }) => { + if (!$isRangeSelection(selection)) { + return false + } + for (const node of selection.getNodes()) { + if ($isHeadingNode(node) && node.getTag() === headingSize) { + continue + } + + const parent = node.getParent() + if ($isHeadingNode(parent) && parent.getTag() === headingSize) { + continue + } + + return false + } + return true + }, + key: headingSize, + label: `Heading ${headingSize.charAt(1)}`, + onSelect: ({ editor }) => { + editor.update(() => { + setHeading(headingSize) + }) + }, + order: i + 2, + } + }), + ), + ] + return { clientFeatureProps: props, markdownTransformers: [MarkdownTransformer(enabledHeadingSizes)], @@ -47,13 +85,12 @@ const HeadingFeatureClient: FeatureProviderProviderClient = groups: enabledHeadingSizes?.length ? [ { - displayName: 'Basic', items: enabledHeadingSizes.map((headingSize) => { return { Icon: iconImports[headingSize], - displayName: `Heading ${headingSize.charAt(1)}`, key: `heading-${headingSize.charAt(1)}`, keywords: ['heading', headingSize], + label: `Heading ${headingSize.charAt(1)}`, onSelect: ({ editor }) => { editor.update(() => { setHeading(headingSize) @@ -62,31 +99,16 @@ const HeadingFeatureClient: FeatureProviderProviderClient = } }), key: 'basic', + label: 'Basic', }, ] : [], }, + toolbarFixed: { + groups: enabledHeadingSizes?.length ? toolbarGroups : [], + }, toolbarInline: { - groups: enabledHeadingSizes?.length - ? [ - inlineToolbarTextDropdownGroupWithItems( - enabledHeadingSizes.map((headingSize, i) => { - return { - ChildComponent: iconImports[headingSize], - isActive: () => false, - key: headingSize, - label: `Heading ${headingSize.charAt(1)}`, - onSelect: ({ editor }) => { - editor.update(() => { - setHeading(headingSize) - }) - }, - order: i + 2, - } - }), - ), - ] - : [], + groups: enabledHeadingSizes?.length ? toolbarGroups : [], }, } }, diff --git a/packages/richtext-lexical/src/field/features/horizontalRule/feature.client.tsx b/packages/richtext-lexical/src/field/features/horizontalRule/feature.client.tsx index 11faf6827a..17f9b62c2e 100644 --- a/packages/richtext-lexical/src/field/features/horizontalRule/feature.client.tsx +++ b/packages/richtext-lexical/src/field/features/horizontalRule/feature.client.tsx @@ -1,11 +1,18 @@ 'use client' +import { $isNodeSelection } from 'lexical' + import type { FeatureProviderProviderClient } from '../types.js' import { HorizontalRuleIcon } from '../../lexical/ui/icons/HorizontalRule/index.js' import { createClientComponent } from '../createClientComponent.js' +import { toolbarAddDropdownGroupWithItems } from '../shared/toolbar/addDropdownGroup.js' import { MarkdownTransformer } from './markdownTransformer.js' -import { HorizontalRuleNode, INSERT_HORIZONTAL_RULE_COMMAND } from './nodes/HorizontalRuleNode.js' +import { + $isHorizontalRuleNode, + HorizontalRuleNode, + INSERT_HORIZONTAL_RULE_COMMAND, +} from './nodes/HorizontalRuleNode.js' import { HorizontalRulePlugin } from './plugin/index.js' const HorizontalRuleFeatureClient: FeatureProviderProviderClient = (props) => { @@ -24,22 +31,44 @@ const HorizontalRuleFeatureClient: FeatureProviderProviderClient = (p slashMenu: { groups: [ { - displayName: 'Basic', items: [ { Icon: HorizontalRuleIcon, - displayName: `Horizontal Rule`, key: 'horizontalRule', keywords: ['hr', 'horizontal rule', 'line', 'separator'], + label: `Horizontal Rule`, onSelect: ({ editor }) => { editor.dispatchCommand(INSERT_HORIZONTAL_RULE_COMMAND, undefined) }, }, ], key: 'basic', + label: 'Basic', }, ], }, + toolbarFixed: { + groups: [ + toolbarAddDropdownGroupWithItems([ + { + ChildComponent: HorizontalRuleIcon, + isActive: ({ selection }) => { + if (!$isNodeSelection(selection) || !selection.getNodes().length) { + return false + } + + const firstNode = selection.getNodes()[0] + return $isHorizontalRuleNode(firstNode) + }, + key: 'horizontalRule', + label: `Horizontal Rule`, + onSelect: ({ editor }) => { + editor.dispatchCommand(INSERT_HORIZONTAL_RULE_COMMAND, undefined) + }, + }, + ]), + ], + }, }), } } diff --git a/packages/richtext-lexical/src/field/features/indent/feature.client.tsx b/packages/richtext-lexical/src/field/features/indent/feature.client.tsx index 286935877e..78fbc61aca 100644 --- a/packages/richtext-lexical/src/field/features/indent/feature.client.tsx +++ b/packages/richtext-lexical/src/field/features/indent/feature.client.tsx @@ -2,60 +2,64 @@ import { INDENT_CONTENT_COMMAND, OUTDENT_CONTENT_COMMAND } from 'lexical' +import type { ToolbarGroup } from '../toolbars/types.js' import type { FeatureProviderProviderClient } from '../types.js' import { IndentDecreaseIcon } from '../../lexical/ui/icons/IndentDecrease/index.js' import { IndentIncreaseIcon } from '../../lexical/ui/icons/IndentIncrease/index.js' import { createClientComponent } from '../createClientComponent.js' -import { indentGroupWithItems } from './inlineToolbarIndentGroup.js' +import { toolbarIndentGroupWithItems } from './toolbarIndentGroup.js' + +const toolbarGroups: ToolbarGroup[] = [ + toolbarIndentGroupWithItems([ + { + ChildComponent: IndentDecreaseIcon, + isActive: () => false, + isEnabled: ({ selection }) => { + if (!selection || !selection?.getNodes()?.length) { + return false + } + for (const node of selection.getNodes()) { + // If at least one node is indented, this should be active + if ( + ('__indent' in node && (node.__indent as number) > 0) || + (node.getParent() && '__indent' in node.getParent() && node.getParent().__indent > 0) + ) { + return true + } + } + return false + }, + key: 'indentDecrease', + label: `Decrease Indent`, + onSelect: ({ editor }) => { + editor.dispatchCommand(OUTDENT_CONTENT_COMMAND, undefined) + }, + order: 1, + }, + { + ChildComponent: IndentIncreaseIcon, + isActive: () => false, + key: 'indentIncrease', + label: `Increase Indent`, + onSelect: ({ editor }) => { + editor.dispatchCommand(INDENT_CONTENT_COMMAND, undefined) + }, + order: 2, + }, + ]), +] const IndentFeatureClient: FeatureProviderProviderClient = (props) => { return { clientFeatureProps: props, feature: () => ({ clientFeatureProps: props, + toolbarFixed: { + groups: toolbarGroups, + }, toolbarInline: { - groups: [ - indentGroupWithItems([ - { - ChildComponent: IndentDecreaseIcon, - isActive: () => false, - isEnabled: ({ selection }) => { - if (!selection || !selection?.getNodes()?.length) { - return false - } - for (const node of selection.getNodes()) { - // If at least one node is indented, this should be active - if ( - ('__indent' in node && (node.__indent as number) > 0) || - (node.getParent() && - '__indent' in node.getParent() && - node.getParent().__indent > 0) - ) { - return true - } - } - return false - }, - key: 'indentDecrease', - label: `Decrease Indent`, - onSelect: ({ editor }) => { - editor.dispatchCommand(OUTDENT_CONTENT_COMMAND, undefined) - }, - order: 1, - }, - { - ChildComponent: IndentIncreaseIcon, - isActive: () => false, - key: 'indentIncrease', - label: `Increase Indent`, - onSelect: ({ editor }) => { - editor.dispatchCommand(INDENT_CONTENT_COMMAND, undefined) - }, - order: 2, - }, - ]), - ], + groups: toolbarGroups, }, }), } diff --git a/packages/richtext-lexical/src/field/features/indent/inlineToolbarIndentGroup.ts b/packages/richtext-lexical/src/field/features/indent/inlineToolbarIndentGroup.ts deleted file mode 100644 index 623c58068f..0000000000 --- a/packages/richtext-lexical/src/field/features/indent/inlineToolbarIndentGroup.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { - InlineToolbarGroup, - InlineToolbarGroupItem, -} from '../../lexical/plugins/toolbars/inline/types.js' - -export const indentGroupWithItems = (items: InlineToolbarGroupItem[]): InlineToolbarGroup => { - return { - type: 'buttons', - items, - key: 'indent', - order: 3, - } -} diff --git a/packages/richtext-lexical/src/field/features/indent/toolbarIndentGroup.ts b/packages/richtext-lexical/src/field/features/indent/toolbarIndentGroup.ts new file mode 100644 index 0000000000..ab454ce0b5 --- /dev/null +++ b/packages/richtext-lexical/src/field/features/indent/toolbarIndentGroup.ts @@ -0,0 +1,10 @@ +import type { ToolbarGroup, ToolbarGroupItem } from '../toolbars/types.js' + +export const toolbarIndentGroupWithItems = (items: ToolbarGroupItem[]): ToolbarGroup => { + return { + type: 'buttons', + items, + key: 'indent', + order: 35, + } +} diff --git a/packages/richtext-lexical/src/field/features/link/feature.client.tsx b/packages/richtext-lexical/src/field/features/link/feature.client.tsx index 9549772c5b..6aa3efe75d 100644 --- a/packages/richtext-lexical/src/field/features/link/feature.client.tsx +++ b/packages/richtext-lexical/src/field/features/link/feature.client.tsx @@ -3,6 +3,7 @@ import { $findMatchingParent } from '@lexical/utils' import { $getSelection, $isRangeSelection } from 'lexical' +import type { ToolbarGroup } from '../toolbars/types.js' import type { FeatureProviderProviderClient } from '../types.js' import type { ExclusiveLinkCollectionsProps } from './feature.server.js' import type { LinkFields } from './nodes/types.js' @@ -10,7 +11,7 @@ import type { LinkFields } from './nodes/types.js' import { LinkIcon } from '../../lexical/ui/icons/Link/index.js' import { getSelectedNode } from '../../lexical/utils/getSelectedNode.js' import { createClientComponent } from '../createClientComponent.js' -import { inlineToolbarFeatureButtonsGroupWithItems } from '../shared/inlineToolbar/featureButtonsGroup.js' +import { toolbarFeatureButtonsGroupWithItems } from '../shared/toolbar/featureButtonsGroup.js' import { AutoLinkNode } from './nodes/AutoLinkNode.js' import { $isLinkNode, LinkNode, TOGGLE_LINK_COMMAND } from './nodes/LinkNode.js' import { AutoLinkPlugin } from './plugins/autoLink/index.js' @@ -21,6 +22,46 @@ import { LinkPlugin } from './plugins/link/index.js' export type ClientProps = ExclusiveLinkCollectionsProps +const toolbarGroups: ToolbarGroup[] = [ + toolbarFeatureButtonsGroupWithItems([ + { + ChildComponent: LinkIcon, + isActive: ({ selection }) => { + if ($isRangeSelection(selection)) { + const selectedNode = getSelectedNode(selection) + const linkParent = $findMatchingParent(selectedNode, $isLinkNode) + return linkParent != null + } + return false + }, + key: 'link', + label: `Link`, + onSelect: ({ editor, isActive }) => { + if (!isActive) { + let selectedText = null + editor.getEditorState().read(() => { + selectedText = $getSelection().getTextContent() + }) + const linkFields: LinkFields = { + doc: null, + linkType: 'custom', + newTab: false, + url: 'https://', + } + editor.dispatchCommand(TOGGLE_LINK_WITH_MODAL_COMMAND, { + fields: linkFields, + text: selectedText, + }) + } else { + // remove link + editor.dispatchCommand(TOGGLE_LINK_COMMAND, null) + } + }, + order: 1, + }, + ]), +] + const LinkFeatureClient: FeatureProviderProviderClient = (props) => { return { clientFeatureProps: props, @@ -45,46 +86,11 @@ const LinkFeatureClient: FeatureProviderProviderClient = (props) => position: 'floatingAnchorElem', }, ], + toolbarFixed: { + groups: toolbarGroups, + }, toolbarInline: { - groups: [ - inlineToolbarFeatureButtonsGroupWithItems([ - { - ChildComponent: LinkIcon, - isActive: ({ selection }) => { - if ($isRangeSelection(selection)) { - const selectedNode = getSelectedNode(selection) - const linkParent = $findMatchingParent(selectedNode, $isLinkNode) - return linkParent != null - } - return false - }, - key: 'link', - label: `Link`, - onSelect: ({ editor, isActive }) => { - if (!isActive) { - let selectedText = null - editor.getEditorState().read(() => { - selectedText = $getSelection().getTextContent() - }) - const linkFields: LinkFields = { - doc: null, - linkType: 'custom', - newTab: false, - url: 'https://', - } - editor.dispatchCommand(TOGGLE_LINK_WITH_MODAL_COMMAND, { - fields: linkFields, - text: selectedText, - }) - } else { - // remove link - editor.dispatchCommand(TOGGLE_LINK_COMMAND, null) - } - }, - order: 1, - }, - ]), - ], + groups: toolbarGroups, }, }), } diff --git a/packages/richtext-lexical/src/field/features/lists/checklist/feature.client.tsx b/packages/richtext-lexical/src/field/features/lists/checklist/feature.client.tsx index 37b867ce37..57c0b7d4d5 100644 --- a/packages/richtext-lexical/src/field/features/lists/checklist/feature.client.tsx +++ b/packages/richtext-lexical/src/field/features/lists/checklist/feature.client.tsx @@ -1,15 +1,56 @@ 'use client' -import { INSERT_CHECK_LIST_COMMAND, ListItemNode, ListNode } from '@lexical/list' +import { $isListNode, INSERT_CHECK_LIST_COMMAND, ListItemNode, ListNode } from '@lexical/list' +import { $isRangeSelection } from 'lexical' +import type { ToolbarGroup } from '../../toolbars/types.js' import type { ClientFeature, FeatureProviderProviderClient } from '../../types.js' import { ChecklistIcon } from '../../../lexical/ui/icons/Checklist/index.js' import { createClientComponent } from '../../createClientComponent.js' -import { inlineToolbarTextDropdownGroupWithItems } from '../../shared/inlineToolbar/textDropdownGroup.js' +import { toolbarTextDropdownGroupWithItems } from '../../shared/toolbar/textDropdownGroup.js' import { LexicalListPlugin } from '../plugin/index.js' import { CHECK_LIST } from './markdownTransformers.js' import { LexicalCheckListPlugin } from './plugin/index.js' +const toolbarGroups: ToolbarGroup[] = [ + toolbarTextDropdownGroupWithItems([ + { + ChildComponent: ChecklistIcon, + isActive: ({ selection }) => { + if (!$isRangeSelection(selection)) { + return false + } + for (const node of selection.getNodes()) { + if ($isListNode(node) && node.getListType() === 'check') { + continue + } + + const parent = node.getParent() + + if ($isListNode(parent) && parent.getListType() === 'check') { + continue + } + + const parentParent = parent?.getParent() + // Example scenario: Node = textNode, parent = listItemNode, parentParent = listNode + if ($isListNode(parentParent) && parentParent.getListType() === 'check') { + continue + } + + return false + } + return true + }, + key: 'checklist', + label: `Check List`, + onSelect: ({ editor }) => { + editor.dispatchCommand(INSERT_CHECK_LIST_COMMAND, undefined) + }, + order: 12, + }, + ]), +] + const ChecklistFeatureClient: FeatureProviderProviderClient = (props) => { return { clientFeatureProps: props, @@ -39,37 +80,27 @@ const ChecklistFeatureClient: FeatureProviderProviderClient = (props) slashMenu: { groups: [ { - displayName: 'Lists', items: [ { Icon: ChecklistIcon, - displayName: 'Check List', key: 'checklist', keywords: ['check list', 'check', 'checklist', 'cl'], + label: 'Check List', onSelect: ({ editor }) => { editor.dispatchCommand(INSERT_CHECK_LIST_COMMAND, undefined) }, }, ], key: 'lists', + label: 'Lists', }, ], }, + toolbarFixed: { + groups: toolbarGroups, + }, toolbarInline: { - groups: [ - inlineToolbarTextDropdownGroupWithItems([ - { - ChildComponent: ChecklistIcon, - isActive: () => false, - key: 'checklist', - label: `Check List`, - onSelect: ({ editor }) => { - editor.dispatchCommand(INSERT_CHECK_LIST_COMMAND, undefined) - }, - order: 12, - }, - ]), - ], + groups: toolbarGroups, }, } }, diff --git a/packages/richtext-lexical/src/field/features/lists/orderedList/feature.client.tsx b/packages/richtext-lexical/src/field/features/lists/orderedList/feature.client.tsx index 55564a67fd..ede2ce7d72 100644 --- a/packages/richtext-lexical/src/field/features/lists/orderedList/feature.client.tsx +++ b/packages/richtext-lexical/src/field/features/lists/orderedList/feature.client.tsx @@ -1,14 +1,55 @@ 'use client' -import { INSERT_ORDERED_LIST_COMMAND, ListItemNode, ListNode } from '@lexical/list' +import { $isListNode, INSERT_ORDERED_LIST_COMMAND, ListItemNode, ListNode } from '@lexical/list' +import { $isRangeSelection } from 'lexical' +import type { ToolbarGroup } from '../../toolbars/types.js' import type { FeatureProviderProviderClient } from '../../types.js' import { OrderedListIcon } from '../../../lexical/ui/icons/OrderedList/index.js' import { createClientComponent } from '../../createClientComponent.js' -import { inlineToolbarTextDropdownGroupWithItems } from '../../shared/inlineToolbar/textDropdownGroup.js' +import { toolbarTextDropdownGroupWithItems } from '../../shared/toolbar/textDropdownGroup.js' import { LexicalListPlugin } from '../plugin/index.js' import { ORDERED_LIST } from './markdownTransformer.js' +const toolbarGroups: ToolbarGroup[] = [ + toolbarTextDropdownGroupWithItems([ + { + ChildComponent: OrderedListIcon, + isActive: ({ selection }) => { + if (!$isRangeSelection(selection)) { + return false + } + for (const node of selection.getNodes()) { + if ($isListNode(node) && node.getListType() === 'number') { + continue + } + + const parent = node.getParent() + + if ($isListNode(parent) && parent.getListType() === 'number') { + continue + } + + const parentParent = parent?.getParent() + // Example scenario: Node = textNode, parent = listItemNode, parentParent = listNode + if ($isListNode(parentParent) && parentParent.getListType() === 'number') { + continue + } + + return false + } + return true + }, + key: 'orderedList', + label: `Ordered List`, + onSelect: ({ editor }) => { + editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined) + }, + order: 10, + }, + ]), +] + const OrderedListFeatureClient: FeatureProviderProviderClient = (props) => { return { clientFeatureProps: props, @@ -28,37 +69,27 @@ const OrderedListFeatureClient: FeatureProviderProviderClient = (prop slashMenu: { groups: [ { - displayName: 'Lists', items: [ { Icon: OrderedListIcon, - displayName: 'Ordered List', key: 'orderedList', keywords: ['ordered list', 'ol'], + label: 'Ordered List', onSelect: ({ editor }) => { editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined) }, }, ], key: 'lists', + label: 'Lists', }, ], }, + toolbarFixed: { + groups: toolbarGroups, + }, toolbarInline: { - groups: [ - inlineToolbarTextDropdownGroupWithItems([ - { - ChildComponent: OrderedListIcon, - isActive: () => false, - key: 'orderedList', - label: `Ordered List`, - onSelect: ({ editor }) => { - editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined) - }, - order: 10, - }, - ]), - ], + groups: toolbarGroups, }, } }, diff --git a/packages/richtext-lexical/src/field/features/lists/unorderedList/feature.client.tsx b/packages/richtext-lexical/src/field/features/lists/unorderedList/feature.client.tsx index 012ad23f2f..81d0bbac2d 100644 --- a/packages/richtext-lexical/src/field/features/lists/unorderedList/feature.client.tsx +++ b/packages/richtext-lexical/src/field/features/lists/unorderedList/feature.client.tsx @@ -1,16 +1,56 @@ 'use client' -import { INSERT_UNORDERED_LIST_COMMAND, ListItemNode, ListNode } from '@lexical/list' +import { $isListNode, INSERT_UNORDERED_LIST_COMMAND, ListItemNode, ListNode } from '@lexical/list' +import { $isRangeSelection } from 'lexical' +import type { ToolbarGroup } from '../../toolbars/types.js' import type { FeatureProviderProviderClient } from '../../types.js' -import { SlashMenuItem } from '../../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/types.js' import { UnorderedListIcon } from '../../../lexical/ui/icons/UnorderedList/index.js' import { createClientComponent } from '../../createClientComponent.js' -import { inlineToolbarTextDropdownGroupWithItems } from '../../shared/inlineToolbar/textDropdownGroup.js' +import { toolbarTextDropdownGroupWithItems } from '../../shared/toolbar/textDropdownGroup.js' import { LexicalListPlugin } from '../plugin/index.js' import { UNORDERED_LIST } from './markdownTransformer.js' +const toolbarGroups: ToolbarGroup[] = [ + toolbarTextDropdownGroupWithItems([ + { + ChildComponent: UnorderedListIcon, + isActive: ({ selection }) => { + if (!$isRangeSelection(selection)) { + return false + } + for (const node of selection.getNodes()) { + if ($isListNode(node) && node.getListType() === 'bullet') { + continue + } + + const parent = node.getParent() + + if ($isListNode(parent) && parent.getListType() === 'bullet') { + continue + } + + const parentParent = parent?.getParent() + // Example scenario: Node = textNode, parent = listItemNode, parentParent = listNode + if ($isListNode(parentParent) && parentParent.getListType() === 'bullet') { + continue + } + + return false + } + return true + }, + key: 'unorderedList', + label: `Unordered List`, + onSelect: ({ editor }) => { + editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined) + }, + order: 11, + }, + ]), +] + const UnorderedListFeatureClient: FeatureProviderProviderClient = (props) => { return { clientFeatureProps: props, @@ -28,37 +68,27 @@ const UnorderedListFeatureClient: FeatureProviderProviderClient = (pr slashMenu: { groups: [ { - displayName: 'Lists', items: [ { Icon: UnorderedListIcon, - displayName: 'Unordered List', key: 'unorderedList', keywords: ['unordered list', 'ul'], + label: 'Unordered List', onSelect: ({ editor }) => { editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined) }, }, ], key: 'lists', + label: 'Lists', }, ], }, + toolbarFixed: { + groups: toolbarGroups, + }, toolbarInline: { - groups: [ - inlineToolbarTextDropdownGroupWithItems([ - { - ChildComponent: UnorderedListIcon, - isActive: () => false, - key: 'unorderedList', - label: `Unordered List`, - onSelect: ({ editor }) => { - editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined) - }, - order: 11, - }, - ]), - ], + groups: toolbarGroups, }, } }, diff --git a/packages/richtext-lexical/src/field/features/paragraph/feature.client.tsx b/packages/richtext-lexical/src/field/features/paragraph/feature.client.tsx index ee26b4db79..e426d2be81 100644 --- a/packages/richtext-lexical/src/field/features/paragraph/feature.client.tsx +++ b/packages/richtext-lexical/src/field/features/paragraph/feature.client.tsx @@ -1,13 +1,42 @@ 'use client' import { $setBlocksType } from '@lexical/selection' -import { $createParagraphNode, $getSelection } from 'lexical' +import { $createParagraphNode, $getSelection, $isParagraphNode, $isRangeSelection } from 'lexical' +import type { ToolbarGroup } from '../toolbars/types.js' import type { FeatureProviderProviderClient } from '../types.js' import { TextIcon } from '../../lexical/ui/icons/Text/index.js' import { createClientComponent } from '../createClientComponent.js' -import { inlineToolbarTextDropdownGroupWithItems } from '../shared/inlineToolbar/textDropdownGroup.js' +import { toolbarTextDropdownGroupWithItems } from '../shared/toolbar/textDropdownGroup.js' + +const toolbarGroups: ToolbarGroup[] = [ + toolbarTextDropdownGroupWithItems([ + { + ChildComponent: TextIcon, + isActive: ({ selection }) => { + if (!$isRangeSelection(selection)) { + return false + } + for (const node of selection.getNodes()) { + if (!$isParagraphNode(node) && !$isParagraphNode(node.getParent())) { + return false + } + } + return true + }, + key: 'paragraph', + label: 'Normal Text', + onSelect: ({ editor }) => { + editor.update(() => { + const selection = $getSelection() + $setBlocksType(selection, () => $createParagraphNode()) + }) + }, + order: 1, + }, + ]), +] const ParagraphFeatureClient: FeatureProviderProviderClient = (props) => { return { @@ -17,13 +46,12 @@ const ParagraphFeatureClient: FeatureProviderProviderClient = (props) slashMenu: { groups: [ { - displayName: 'Basic', items: [ { Icon: TextIcon, - displayName: 'Paragraph', key: 'paragraph', keywords: ['normal', 'paragraph', 'p', 'text'], + label: 'Paragraph', onSelect: ({ editor }) => { editor.update(() => { const selection = $getSelection() @@ -33,27 +61,15 @@ const ParagraphFeatureClient: FeatureProviderProviderClient = (props) }, ], key: 'basic', + label: 'Basic', }, ], }, + toolbarFixed: { + groups: toolbarGroups, + }, toolbarInline: { - groups: [ - inlineToolbarTextDropdownGroupWithItems([ - { - ChildComponent: TextIcon, - isActive: () => false, - key: 'paragraph', - label: 'Normal Text', - onSelect: ({ editor }) => { - editor.update(() => { - const selection = $getSelection() - $setBlocksType(selection, () => $createParagraphNode()) - }) - }, - order: 1, - }, - ]), - ], + groups: toolbarGroups, }, }), } diff --git a/packages/richtext-lexical/src/field/features/relationship/feature.client.tsx b/packages/richtext-lexical/src/field/features/relationship/feature.client.tsx index 5148250371..0265244697 100644 --- a/packages/richtext-lexical/src/field/features/relationship/feature.client.tsx +++ b/packages/richtext-lexical/src/field/features/relationship/feature.client.tsx @@ -1,14 +1,16 @@ 'use client' import { withMergedProps } from '@payloadcms/ui/elements/withMergedProps' +import { $isNodeSelection } from 'lexical' import type { FeatureProviderProviderClient } from '../types.js' import type { RelationshipFeatureProps } from './feature.server.js' import { RelationshipIcon } from '../../lexical/ui/icons/Relationship/index.js' import { createClientComponent } from '../createClientComponent.js' +import { toolbarAddDropdownGroupWithItems } from '../shared/toolbar/addDropdownGroup.js' import { INSERT_RELATIONSHIP_WITH_DRAWER_COMMAND } from './drawer/commands.js' -import { RelationshipNode } from './nodes/RelationshipNode.js' +import { $isRelationshipNode, RelationshipNode } from './nodes/RelationshipNode.js' import { RelationshipPlugin } from './plugins/index.js' const RelationshipFeatureClient: FeatureProviderProviderClient = ( @@ -31,13 +33,12 @@ const RelationshipFeatureClient: FeatureProviderProviderClient { // dispatch INSERT_RELATIONSHIP_WITH_DRAWER_COMMAND editor.dispatchCommand(INSERT_RELATIONSHIP_WITH_DRAWER_COMMAND, { @@ -47,9 +48,35 @@ const RelationshipFeatureClient: FeatureProviderProviderClient { + if (!$isNodeSelection(selection) || !selection.getNodes().length) { + return false + } + + const firstNode = selection.getNodes()[0] + return $isRelationshipNode(firstNode) + }, + key: 'relationship', + label: 'Relationship', + onSelect: ({ editor }) => { + // dispatch INSERT_RELATIONSHIP_WITH_DRAWER_COMMAND + editor.dispatchCommand(INSERT_RELATIONSHIP_WITH_DRAWER_COMMAND, { + replace: false, + }) + }, + }, + ]), + ], + }, }), } } diff --git a/packages/richtext-lexical/src/field/features/shared/inlineToolbar/featureButtonsGroup.ts b/packages/richtext-lexical/src/field/features/shared/inlineToolbar/featureButtonsGroup.ts deleted file mode 100644 index 001fea1fb1..0000000000 --- a/packages/richtext-lexical/src/field/features/shared/inlineToolbar/featureButtonsGroup.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { - InlineToolbarGroup, - InlineToolbarGroupItem, -} from '../../../lexical/plugins/toolbars/inline/types.js' - -export const inlineToolbarFeatureButtonsGroupWithItems = ( - items: InlineToolbarGroupItem[], -): InlineToolbarGroup => { - return { - type: 'buttons', - items, - key: 'features', - order: 5, - } -} diff --git a/packages/richtext-lexical/src/field/features/shared/inlineToolbar/textDropdownGroup.ts b/packages/richtext-lexical/src/field/features/shared/inlineToolbar/textDropdownGroup.ts deleted file mode 100644 index 02a7f0c5ae..0000000000 --- a/packages/richtext-lexical/src/field/features/shared/inlineToolbar/textDropdownGroup.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { - InlineToolbarGroup, - InlineToolbarGroupItem, -} from '../../../lexical/plugins/toolbars/inline/types.js' - -import { TextIcon } from '../../../lexical/ui/icons/Text/index.js' - -export const inlineToolbarTextDropdownGroupWithItems = ( - items: InlineToolbarGroupItem[], -): InlineToolbarGroup => { - return { - type: 'dropdown', - ChildComponent: TextIcon, - items, - key: 'text', - order: 1, - } -} diff --git a/packages/richtext-lexical/src/field/features/shared/toolbar/addDropdownGroup.ts b/packages/richtext-lexical/src/field/features/shared/toolbar/addDropdownGroup.ts new file mode 100644 index 0000000000..a81e636ef4 --- /dev/null +++ b/packages/richtext-lexical/src/field/features/shared/toolbar/addDropdownGroup.ts @@ -0,0 +1,13 @@ +import type { ToolbarGroup, ToolbarGroupItem } from '../../toolbars/types.js' + +import { AddIcon } from '../../../lexical/ui/icons/Add/index.js' + +export const toolbarAddDropdownGroupWithItems = (items: ToolbarGroupItem[]): ToolbarGroup => { + return { + type: 'dropdown', + ChildComponent: AddIcon, + items, + key: 'add', + order: 10, + } +} diff --git a/packages/richtext-lexical/src/field/features/shared/toolbar/featureButtonsGroup.ts b/packages/richtext-lexical/src/field/features/shared/toolbar/featureButtonsGroup.ts new file mode 100644 index 0000000000..16e62c3da8 --- /dev/null +++ b/packages/richtext-lexical/src/field/features/shared/toolbar/featureButtonsGroup.ts @@ -0,0 +1,10 @@ +import type { ToolbarGroup, ToolbarGroupItem } from '../../toolbars/types.js' + +export const toolbarFeatureButtonsGroupWithItems = (items: ToolbarGroupItem[]): ToolbarGroup => { + return { + type: 'buttons', + items, + key: 'features', + order: 50, + } +} diff --git a/packages/richtext-lexical/src/field/features/shared/toolbar/textDropdownGroup.ts b/packages/richtext-lexical/src/field/features/shared/toolbar/textDropdownGroup.ts new file mode 100644 index 0000000000..0bc92fdec6 --- /dev/null +++ b/packages/richtext-lexical/src/field/features/shared/toolbar/textDropdownGroup.ts @@ -0,0 +1,13 @@ +import type { ToolbarGroup, ToolbarGroupItem } from '../../toolbars/types.js' + +import { TextIcon } from '../../../lexical/ui/icons/Text/index.js' + +export const toolbarTextDropdownGroupWithItems = (items: ToolbarGroupItem[]): ToolbarGroup => { + return { + type: 'dropdown', + ChildComponent: TextIcon, + items, + key: 'text', + order: 25, + } +} diff --git a/packages/richtext-lexical/src/field/features/toolbars/fixed/Toolbar/index.scss b/packages/richtext-lexical/src/field/features/toolbars/fixed/Toolbar/index.scss new file mode 100644 index 0000000000..fc69d306fd --- /dev/null +++ b/packages/richtext-lexical/src/field/features/toolbars/fixed/Toolbar/index.scss @@ -0,0 +1,81 @@ +@import '../../../../../scss/styles'; + + +html[data-theme='dark'] { + .fixed-toolbar { + &__dropdown-items { + background: var(--theme-elevation-0); + + .toolbar-popup__dropdown-item { + color: var(--theme-elevation-900); + + &:hover:not([disabled]), &.active { + background-color: var(--theme-elevation-100); + } + + .icon { + color: var(--theme-elevation-600); + } + } + } + + .toolbar-popup { + &__button { + &.active, &:hover:not([disabled]) { + background-color: var(--theme-elevation-100); + } + } + + &__dropdown { + &:hover:not([disabled]) { + background-color: var(--theme-elevation-100); + } + + &-caret:after { + filter: invert(1); + } + + &-label { + color: var(--theme-elevation-750); + } + } + } + } +} + + +.fixed-toolbar { + @include blur-bg(var(--theme-elevation-0)); + display: flex; + align-items: center; + padding: 0 3.72px 0 6.25px; + vertical-align: middle; + height: 37.5px; + position: sticky; + z-index: 2; + top: var(--doc-controls-height); + margin-bottom: $baseline; + border: $style-stroke-width-s solid var(--theme-elevation-150); + + + &__group { + display: flex; + align-items: center; + gap: 2px; + z-index: 1; + + .icon { + min-width: 20px; + height: 20px; + color: var(--theme-elevation-600); + } + + .divider { + width: 1px; + height: 15px; + background-color: var(--color-base-100); + margin: 0 6.25px; + } + } + +} diff --git a/packages/richtext-lexical/src/field/features/toolbars/fixed/Toolbar/index.tsx b/packages/richtext-lexical/src/field/features/toolbars/fixed/Toolbar/index.tsx new file mode 100644 index 0000000000..91be149bef --- /dev/null +++ b/packages/richtext-lexical/src/field/features/toolbars/fixed/Toolbar/index.tsx @@ -0,0 +1,212 @@ +'use client' +import type { LexicalEditor } from 'lexical' + +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext.js' +import { useTranslation } from '@payloadcms/ui/providers/Translation' +import * as React from 'react' + +import type { EditorFocusContextType } from '../../../../lexical/EditorFocusProvider.js' +import type { SanitizedClientEditorConfig } from '../../../../lexical/config/types.js' +import type { ToolbarGroup, ToolbarGroupItem } from '../../types.js' + +import { useEditorFocus } from '../../../../lexical/EditorFocusProvider.js' +import { useEditorConfigContext } from '../../../../lexical/config/client/EditorConfigProvider.js' +import { ToolbarButton } from '../../shared/ToolbarButton/index.js' +import { ToolbarDropdown } from '../../shared/ToolbarDropdown/index.js' +import './index.scss' + +function ButtonGroupItem({ + anchorElem, + editor, + item, +}: { + anchorElem: HTMLElement + editor: LexicalEditor + item: ToolbarGroupItem +}): React.ReactNode { + if (item.Component) { + return ( + item?.Component && ( + + ) + ) + } + + return ( + + {item?.ChildComponent && } + + ) +} + +function ToolbarGroupComponent({ + anchorElem, + editor, + editorConfig, + group, + index, +}: { + anchorElem: HTMLElement + editor: LexicalEditor + editorConfig: SanitizedClientEditorConfig + group: ToolbarGroup + index: number +}): React.ReactNode { + const { i18n } = useTranslation() + + const [dropdownLabel, setDropdownLabel] = React.useState(null) + const [DropdownIcon, setDropdownIcon] = React.useState(null) + + React.useEffect(() => { + if (group?.type === 'dropdown' && group.items.length && group.ChildComponent) { + setDropdownIcon(() => group.ChildComponent) + } else { + setDropdownIcon(null) + } + }, [group]) + + const onActiveChange = ({ activeItems }: { activeItems: ToolbarGroupItem[] }) => { + if (!activeItems.length) { + if (group?.type === 'dropdown' && group.items.length && group.ChildComponent) { + setDropdownIcon(() => group.ChildComponent) + setDropdownLabel(null) + } else { + setDropdownIcon(null) + setDropdownLabel(null) + } + return + } + const item = activeItems[0] + + let label = item.key + if (item.label) { + label = typeof item.label === 'function' ? item.label({ i18n }) : item.label + } + // Crop title to max. 25 characters + if (label.length > 25) { + label = label.substring(0, 25) + '...' + } + setDropdownLabel(label) + setDropdownIcon(() => item.ChildComponent) + } + + return ( +
+ {group.type === 'dropdown' && + group.items.length && + (DropdownIcon ? ( + + ) : ( + + ))} + {group.type === 'buttons' && + group.items.length && + group.items.map((item) => { + return ( + + ) + })} + {index < editorConfig.features.toolbarFixed?.groups.length - 1 &&
} +
+ ) +} + +function FixedToolbar({ + anchorElem, + editor, + editorConfig, +}: { + anchorElem: HTMLElement + editor: LexicalEditor + editorConfig: SanitizedClientEditorConfig +}): React.ReactNode { + return ( +
{ + // Prevent other focus events being triggered. Otherwise, if this was to be clicked while in a child editor, + // the parent editor will be focused, and the child editor will lose focus. + event.stopPropagation() + }} + > + {editor.isEditable() && ( + + {editorConfig?.features && + editorConfig.features?.toolbarFixed?.groups.map((group, i) => { + return ( + + ) + })} + + )} +
+ ) +} + +const checkParentEditor = (editorFocus: EditorFocusContextType): boolean => { + if (editorFocus.parentEditorConfigContext?.editorConfig) { + if ( + editorFocus.parentEditorConfigContext?.editorConfig.resolvedFeatureMap.has('toolbarFixed') + ) { + return true + } else { + if (editorFocus.parentEditorFocus) { + return checkParentEditor(editorFocus.parentEditorFocus) + } + } + } + return false +} + +export function FixedToolbarPlugin({ + anchorElem = document.body, +}: { + anchorElem?: HTMLElement +}): React.ReactElement | null { + const [currentEditor] = useLexicalComposerContext() + const { editorConfig: currentEditorConfig, uuid } = useEditorConfigContext() + + const editorFocus = useEditorFocus() + const editor = editorFocus.focusedEditor || currentEditor + + const editorConfig = editorFocus.focusedEditorConfigContext?.editorConfig || currentEditorConfig + + // Check if there is a parent editor with a fixed toolbar already + const hasParentWithFixedToolbar = checkParentEditor(editorFocus) + + if (hasParentWithFixedToolbar) { + return null + } + + if (!editorConfig?.features?.toolbarFixed?.groups?.length) { + return null + } + + return +} diff --git a/packages/richtext-lexical/src/field/features/toolbars/fixed/feature.client.tsx b/packages/richtext-lexical/src/field/features/toolbars/fixed/feature.client.tsx new file mode 100644 index 0000000000..40592e84a5 --- /dev/null +++ b/packages/richtext-lexical/src/field/features/toolbars/fixed/feature.client.tsx @@ -0,0 +1,23 @@ +'use client' + +import type { FeatureProviderProviderClient } from '../../types.js' + +import { createClientComponent } from '../../createClientComponent.js' +import { FixedToolbarPlugin } from './Toolbar/index.js' + +const FixedToolbarFeatureClient: FeatureProviderProviderClient = (props) => { + return { + clientFeatureProps: props, + feature: () => ({ + clientFeatureProps: props, + plugins: [ + { + Component: FixedToolbarPlugin, + position: 'top', + }, + ], + }), + } +} + +export const FixedToolbarFeatureClientComponent = createClientComponent(FixedToolbarFeatureClient) diff --git a/packages/richtext-lexical/src/field/features/toolbars/fixed/feature.server.ts b/packages/richtext-lexical/src/field/features/toolbars/fixed/feature.server.ts new file mode 100644 index 0000000000..a28cf3b55b --- /dev/null +++ b/packages/richtext-lexical/src/field/features/toolbars/fixed/feature.server.ts @@ -0,0 +1,17 @@ +import type { FeatureProviderProviderServer } from '../../types.js' + +import { FixedToolbarFeatureClientComponent } from './feature.client.js' + +export const FixedToolbarFeature: FeatureProviderProviderServer = (props) => { + return { + feature: () => { + return { + ClientComponent: FixedToolbarFeatureClientComponent, + clientFeatureProps: null, + serverFeatureProps: props, + } + }, + key: 'toolbarFixed', + serverFeatureProps: props, + } +} diff --git a/packages/richtext-lexical/src/field/lexical/plugins/toolbars/inline/Toolbar/index.scss b/packages/richtext-lexical/src/field/features/toolbars/inline/Toolbar/index.scss similarity index 82% rename from packages/richtext-lexical/src/field/lexical/plugins/toolbars/inline/Toolbar/index.scss rename to packages/richtext-lexical/src/field/features/toolbars/inline/Toolbar/index.scss index 353d3792cc..b648779330 100644 --- a/packages/richtext-lexical/src/field/lexical/plugins/toolbars/inline/Toolbar/index.scss +++ b/packages/richtext-lexical/src/field/features/toolbars/inline/Toolbar/index.scss @@ -1,4 +1,4 @@ -@import '../../../../../../scss/styles'; +@import '../../../../../scss/styles'; html[data-theme='light'] { .inline-toolbar-popup { @@ -10,12 +10,12 @@ html[data-theme='light'] { display: flex; align-items: center; background: var(--color-base-0); - padding: 0px 3.72px 0px 6.25px; + padding: 0 3.72px 0 6.25px; vertical-align: middle; position: absolute; top: 0; left: 0; - z-index: 10; + z-index: 1; opacity: 0; border-radius: 6.25px; transition: opacity 0.2s; @@ -50,9 +50,4 @@ html[data-theme='light'] { } } - @media (max-width: 1024px) { - button.insert-comment { - display: none; - } - } } diff --git a/packages/richtext-lexical/src/field/lexical/plugins/toolbars/inline/Toolbar/index.tsx b/packages/richtext-lexical/src/field/features/toolbars/inline/Toolbar/index.tsx similarity index 83% rename from packages/richtext-lexical/src/field/lexical/plugins/toolbars/inline/Toolbar/index.tsx rename to packages/richtext-lexical/src/field/features/toolbars/inline/Toolbar/index.tsx index 8539ce5694..765f68ae9d 100644 --- a/packages/richtext-lexical/src/field/lexical/plugins/toolbars/inline/Toolbar/index.tsx +++ b/packages/richtext-lexical/src/field/features/toolbars/inline/Toolbar/index.tsx @@ -14,13 +14,13 @@ import { useCallback, useEffect, useRef, useState } from 'react' import * as React from 'react' import { createPortal } from 'react-dom' -import type { InlineToolbarGroup, InlineToolbarGroupItem } from '../types.js' +import type { ToolbarGroup, ToolbarGroupItem } from '../../types.js' -import { useEditorConfigContext } from '../../../../config/client/EditorConfigProvider.js' -import { getDOMRangeRect } from '../../../../utils/getDOMRangeRect.js' -import { setFloatingElemPosition } from '../../../../utils/setFloatingElemPosition.js' -import { ToolbarButton } from '../ToolbarButton/index.js' -import { ToolbarDropdown } from '../ToolbarDropdown/index.js' +import { useEditorConfigContext } from '../../../../lexical/config/client/EditorConfigProvider.js' +import { getDOMRangeRect } from '../../../../lexical/utils/getDOMRangeRect.js' +import { setFloatingElemPosition } from '../../../../lexical/utils/setFloatingElemPosition.js' +import { ToolbarButton } from '../../shared/ToolbarButton/index.js' +import { ToolbarDropdown } from '../../shared/ToolbarDropdown/index.js' import './index.scss' function ButtonGroupItem({ @@ -30,7 +30,7 @@ function ButtonGroupItem({ }: { anchorElem: HTMLElement editor: LexicalEditor - item: InlineToolbarGroupItem + item: ToolbarGroupItem }): React.ReactNode { if (item.Component) { return ( @@ -41,13 +41,13 @@ function ButtonGroupItem({ } return ( - + {item?.ChildComponent && } ) } -function ToolbarGroup({ +function ToolbarGroupComponent({ anchorElem, editor, group, @@ -55,15 +55,33 @@ function ToolbarGroup({ }: { anchorElem: HTMLElement editor: LexicalEditor - group: InlineToolbarGroup + group: ToolbarGroup index: number }): React.ReactNode { const { editorConfig } = useEditorConfigContext() - const Icon = - group?.type === 'dropdown' && group.items.length && group.ChildComponent - ? group.ChildComponent - : null + const [DropdownIcon, setDropdownIcon] = React.useState(null) + + React.useEffect(() => { + if (group?.type === 'dropdown' && group.items.length && group.ChildComponent) { + setDropdownIcon(() => group.ChildComponent) + } else { + setDropdownIcon(null) + } + }, [group]) + + const onActiveChange = ({ activeItems }: { activeItems: ToolbarGroupItem[] }) => { + if (!activeItems.length) { + if (group?.type === 'dropdown' && group.items.length && group.ChildComponent) { + setDropdownIcon(() => group.ChildComponent) + } else { + setDropdownIcon(null) + } + return + } + const item = activeItems[0] + setDropdownIcon(() => item.ChildComponent) + } return (
{group.type === 'dropdown' && group.items.length && - (Icon ? ( + (DropdownIcon ? ( ) : ( ))} {group.type === 'buttons' && @@ -102,7 +124,7 @@ function ToolbarGroup({ ) } -function FloatingSelectToolbar({ +function InlineToolbar({ anchorElem, editor, }: { @@ -264,7 +286,7 @@ function FloatingSelectToolbar({ {editorConfig?.features && editorConfig.features?.toolbarInline?.groups.map((group, i) => { return ( - { @@ -360,14 +382,15 @@ function useFloatingTextFormatToolbar( return null } - return createPortal(, anchorElem) + return createPortal(, anchorElem) } -export function FloatingSelectToolbarPlugin({ +export function InlineToolbarPlugin({ anchorElem = document.body, }: { anchorElem?: HTMLElement -}): JSX.Element | null { +}): React.ReactElement | null { const [editor] = useLexicalComposerContext() - return useFloatingTextFormatToolbar(editor, anchorElem) + + return useInlineToolbar(editor, anchorElem) } diff --git a/packages/richtext-lexical/src/field/features/toolbars/inline/feature.client.tsx b/packages/richtext-lexical/src/field/features/toolbars/inline/feature.client.tsx new file mode 100644 index 0000000000..6b7266604b --- /dev/null +++ b/packages/richtext-lexical/src/field/features/toolbars/inline/feature.client.tsx @@ -0,0 +1,23 @@ +'use client' + +import type { FeatureProviderProviderClient } from '../../types.js' + +import { createClientComponent } from '../../createClientComponent.js' +import { InlineToolbarPlugin } from './Toolbar/index.js' + +const InlineToolbarFeatureClient: FeatureProviderProviderClient = (props) => { + return { + clientFeatureProps: props, + feature: () => ({ + clientFeatureProps: props, + plugins: [ + { + Component: InlineToolbarPlugin, + position: 'floatingAnchorElem', + }, + ], + }), + } +} + +export const InlineToolbarFeatureClientComponent = createClientComponent(InlineToolbarFeatureClient) diff --git a/packages/richtext-lexical/src/field/features/toolbars/inline/feature.server.ts b/packages/richtext-lexical/src/field/features/toolbars/inline/feature.server.ts new file mode 100644 index 0000000000..be28a4c43a --- /dev/null +++ b/packages/richtext-lexical/src/field/features/toolbars/inline/feature.server.ts @@ -0,0 +1,19 @@ +import type { FeatureProviderProviderServer } from '../../types.js' + +import { InlineToolbarFeatureClientComponent } from './feature.client.js' + +export const InlineToolbarFeature: FeatureProviderProviderServer = ( + props, +) => { + return { + feature: () => { + return { + ClientComponent: InlineToolbarFeatureClientComponent, + clientFeatureProps: null, + serverFeatureProps: props, + } + }, + key: 'toolbarInline', + serverFeatureProps: props, + } +} diff --git a/packages/richtext-lexical/src/field/lexical/plugins/toolbars/inline/ToolbarButton/index.scss b/packages/richtext-lexical/src/field/features/toolbars/shared/ToolbarButton/index.scss similarity index 88% rename from packages/richtext-lexical/src/field/lexical/plugins/toolbars/inline/ToolbarButton/index.scss rename to packages/richtext-lexical/src/field/features/toolbars/shared/ToolbarButton/index.scss index 1b51de17ab..886917b147 100644 --- a/packages/richtext-lexical/src/field/lexical/plugins/toolbars/inline/ToolbarButton/index.scss +++ b/packages/richtext-lexical/src/field/features/toolbars/shared/ToolbarButton/index.scss @@ -1,4 +1,6 @@ -.inline-toolbar-popup__button { +@import '../../../../../scss/styles'; + +.toolbar-popup__button { display: flex; align-items: center; vertical-align: middle; diff --git a/packages/richtext-lexical/src/field/lexical/plugins/toolbars/inline/ToolbarButton/index.tsx b/packages/richtext-lexical/src/field/features/toolbars/shared/ToolbarButton/index.tsx similarity index 79% rename from packages/richtext-lexical/src/field/lexical/plugins/toolbars/inline/ToolbarButton/index.tsx rename to packages/richtext-lexical/src/field/features/toolbars/shared/ToolbarButton/index.tsx index 36449217d1..61fa65f22f 100644 --- a/packages/richtext-lexical/src/field/lexical/plugins/toolbars/inline/ToolbarButton/index.tsx +++ b/packages/richtext-lexical/src/field/features/toolbars/shared/ToolbarButton/index.tsx @@ -1,44 +1,48 @@ 'use client' -import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext.js' +import type { LexicalEditor } from 'lexical' + import { mergeRegister } from '@lexical/utils' import { $getSelection } from 'lexical' import React, { useCallback, useEffect, useState } from 'react' -import type { InlineToolbarGroupItem } from '../types.js' +import type { ToolbarGroupItem } from '../../types.js' +import { useEditorFocus } from '../../../../lexical/EditorFocusProvider.js' import './index.scss' -const baseClass = 'inline-toolbar-popup__button' +const baseClass = 'toolbar-popup__button' export const ToolbarButton = ({ children, + editor, item, }: { children: React.JSX.Element - item: InlineToolbarGroupItem + editor: LexicalEditor + item: ToolbarGroupItem }) => { - const [editor] = useLexicalComposerContext() const [enabled, setEnabled] = useState(true) const [active, setActive] = useState(false) const [className, setClassName] = useState(baseClass) + const editorFocusContext = useEditorFocus() const updateStates = useCallback(() => { editor.getEditorState().read(() => { const selection = $getSelection() if (item.isActive) { - const isActive = item.isActive({ editor, selection }) + const isActive = item.isActive({ editor, editorFocusContext, selection }) if (active !== isActive) { setActive(isActive) } } if (item.isEnabled) { - const isEnabled = item.isEnabled({ editor, selection }) + const isEnabled = item.isEnabled({ editor, editorFocusContext, selection }) if (enabled !== isEnabled) { setEnabled(isEnabled) } } }) - }, [active, editor, enabled, item]) + }, [active, editor, editorFocusContext, enabled, item]) useEffect(() => { updateStates() @@ -70,7 +74,7 @@ export const ToolbarButton = ({ .filter(Boolean) .join(' '), ) - }, [enabled, active, className]) + }, [enabled, active, className, item.key]) return ( {showDropDown && createPortal( - + {children} , document.body, diff --git a/packages/richtext-lexical/src/field/lexical/plugins/toolbars/inline/ToolbarDropdown/index.scss b/packages/richtext-lexical/src/field/features/toolbars/shared/ToolbarDropdown/index.scss similarity index 70% rename from packages/richtext-lexical/src/field/lexical/plugins/toolbars/inline/ToolbarDropdown/index.scss rename to packages/richtext-lexical/src/field/features/toolbars/shared/ToolbarDropdown/index.scss index 639cd1d426..1a581cd35b 100644 --- a/packages/richtext-lexical/src/field/lexical/plugins/toolbars/inline/ToolbarDropdown/index.scss +++ b/packages/richtext-lexical/src/field/features/toolbars/shared/ToolbarDropdown/index.scss @@ -1,18 +1,22 @@ -@import '../../../../../../scss/styles'; +@import '../../../../../scss/styles'; -.inline-toolbar-popup__dropdown { +.toolbar-popup__dropdown { display: flex; align-items: center; vertical-align: middle; justify-content: center; height: 30px; - width: 40px; border: 0; background: none; border-radius: 4px; cursor: pointer; position: relative; - padding: 0; + padding: 0 10px; + + &-label { + color: var(--color-base-600); + padding: 0 10px; + } &:disabled { cursor: not-allowed; @@ -26,10 +30,10 @@ background-color: var(--color-base-100); } - &.active { + .active { background-color: var(--color-base-100); - .inline-toolbar-popup__dropdown-caret { + .toolbar-popup__dropdown-caret { &:after { transform: rotate(0deg); } @@ -50,9 +54,9 @@ height: 4px; opacity: 0.3; - background-image: url(../../../../ui/icons/Caret/index.svg); - background-position-y: 0px; - background-position-x: 0px; + background-image: url(../../../../lexical/ui/icons/Caret/index.svg); + background-position-y: 0; + background-position-x: 0; } } @@ -60,17 +64,25 @@ position: absolute; background: var(--color-base-0); border-radius: 4px; - width: 132.5px; + min-width: 132.5px; + max-width: 200px; z-index: 100; - .inline-toolbar-popup__dropdown-item { + .toolbar-popup__dropdown-item { all: unset; // reset all default button styles cursor: pointer; color: var(--color-base-900); - &:hover:not([disabled]) { + .text { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + + &:hover:not([disabled]), &.active { background-color: var(--color-base-100); } + padding-left: 6.25px; padding-right: 6.25px; width: 100%; @@ -91,7 +103,7 @@ } html[data-theme='light'] { - .inline-toolbar-popup__dropdown { + .toolbar-popup__dropdown { &-items { position: absolute; @include shadow-m; diff --git a/packages/richtext-lexical/src/field/features/toolbars/shared/ToolbarDropdown/index.tsx b/packages/richtext-lexical/src/field/features/toolbars/shared/ToolbarDropdown/index.tsx new file mode 100644 index 0000000000..70c8891f04 --- /dev/null +++ b/packages/richtext-lexical/src/field/features/toolbars/shared/ToolbarDropdown/index.tsx @@ -0,0 +1,175 @@ +'use client' +import React, { useCallback, useEffect } from 'react' + +const baseClass = 'toolbar-popup__dropdown' + +import type { LexicalEditor } from 'lexical' + +import { mergeRegister } from '@lexical/utils' +import { useTranslation } from '@payloadcms/ui/providers/Translation' +import { $getSelection } from 'lexical' + +import type { ToolbarGroupItem } from '../../types.js' + +import { useEditorFocus } from '../../../../lexical/EditorFocusProvider.js' +import { DropDown, DropDownItem } from './DropDown.js' +import './index.scss' + +const ToolbarItem = ({ + active, + anchorElem, + editor, + enabled, + item, +}: { + active?: boolean + anchorElem: HTMLElement + editor: LexicalEditor + enabled?: boolean + item: ToolbarGroupItem +}) => { + const { i18n } = useTranslation() + + if (item.Component) { + return ( + item?.Component && ( + + ) + ) + } + + let title = item.key + if (item.label) { + title = typeof item.label === 'function' ? item.label({ i18n }) : item.label + } + // Crop title to max. 25 characters + if (title.length > 25) { + title = title.substring(0, 25) + '...' + } + + return ( + + {item?.ChildComponent && } + {title} + + ) +} + +export const ToolbarDropdown = ({ + Icon, + anchorElem, + classNames, + editor, + groupKey, + items, + itemsContainerClassNames, + label, + maxActiveItems, + onActiveChange, +}: { + Icon?: React.FC + anchorElem: HTMLElement + classNames?: string[] + editor: LexicalEditor + groupKey: string + items: ToolbarGroupItem[] + itemsContainerClassNames?: string[] + label?: string + /** + * Maximum number of active items allowed. This is a performance optimization to prevent + * unnecessary item active checks when the maximum number of active items is reached. + */ + maxActiveItems?: number + onActiveChange?: ({ activeItems }: { activeItems: ToolbarGroupItem[] }) => void +}) => { + const [activeItemKeys, setActiveItemKeys] = React.useState([]) + const [enabledItemKeys, setEnabledItemKeys] = React.useState([]) + const editorFocusContext = useEditorFocus() + + const updateStates = useCallback(() => { + editor.getEditorState().read(() => { + const selection = $getSelection() + + const _activeItemKeys: string[] = [] + const _activeItems: ToolbarGroupItem[] = [] + const _enabledItemKeys: string[] = [] + + for (const item of items) { + if (item.isActive && (!maxActiveItems || _activeItemKeys.length < maxActiveItems)) { + const isActive = item.isActive({ editor, editorFocusContext, selection }) + if (isActive) { + _activeItemKeys.push(item.key) + _activeItems.push(item) + } + } + if (item.isEnabled) { + const isEnabled = item.isEnabled({ editor, editorFocusContext, selection }) + if (isEnabled) { + _enabledItemKeys.push(item.key) + } + } else { + _enabledItemKeys.push(item.key) + } + } + setActiveItemKeys(_activeItemKeys) + setEnabledItemKeys(_enabledItemKeys) + + if (onActiveChange) { + onActiveChange({ activeItems: _activeItems }) + } + }) + }, [editor, editorFocusContext, items, maxActiveItems, onActiveChange]) + + useEffect(() => { + updateStates() + }, [updateStates]) + + useEffect(() => { + document.addEventListener('mouseup', updateStates) + return () => { + document.removeEventListener('mouseup', updateStates) + } + }, [updateStates]) + + useEffect(() => { + return mergeRegister( + editor.registerUpdateListener(() => { + updateStates() + }), + ) + }, [editor, updateStates]) + + return ( + + {items.length && + items.map((item) => { + return ( + + ) + })} + + ) +} diff --git a/packages/richtext-lexical/src/field/lexical/plugins/toolbars/inline/types.ts b/packages/richtext-lexical/src/field/features/toolbars/types.ts similarity index 56% rename from packages/richtext-lexical/src/field/lexical/plugins/toolbars/inline/types.ts rename to packages/richtext-lexical/src/field/features/toolbars/types.ts index f8cca048e2..3d0bd5137a 100644 --- a/packages/richtext-lexical/src/field/lexical/plugins/toolbars/inline/types.ts +++ b/packages/richtext-lexical/src/field/features/toolbars/types.ts @@ -1,40 +1,55 @@ +import type { I18n } from '@payloadcms/translations' import type { BaseSelection, LexicalEditor } from 'lexical' import type React from 'react' -export type InlineToolbarGroup = +import type { EditorFocusContextType } from '../../lexical/EditorFocusProvider.js' + +export type ToolbarGroup = | { ChildComponent?: React.FC - items: Array + items: Array key: string order?: number type: 'dropdown' } | { - items: Array + items: Array key: string order?: number type: 'buttons' } -export type InlineToolbarGroupItem = { +export type ToolbarGroupItem = { ChildComponent?: React.FC /** Use component to ignore the children and onClick properties. It does not use the default, pre-defined format Button component */ Component?: React.FC<{ + active?: boolean anchorElem: HTMLElement editor: LexicalEditor - item: InlineToolbarGroupItem + enabled?: boolean + item: ToolbarGroupItem }> - isActive?: ({ editor, selection }: { editor: LexicalEditor; selection: BaseSelection }) => boolean - isEnabled?: ({ + isActive?: ({ editor, + editorFocusContext, selection, }: { editor: LexicalEditor + editorFocusContext: EditorFocusContextType + selection: BaseSelection + }) => boolean + isEnabled?: ({ + editor, + editorFocusContext, + selection, + }: { + editor: LexicalEditor + editorFocusContext: EditorFocusContextType selection: BaseSelection }) => boolean key: string /** The label is displayed as text if the item is part of a dropdown group */ - label?: string + label?: (({ i18n }: { i18n: I18n }) => string) | string onSelect?: ({ editor, isActive }: { editor: LexicalEditor; isActive: boolean }) => void order?: number } diff --git a/packages/richtext-lexical/src/field/features/types.ts b/packages/richtext-lexical/src/field/features/types.ts index 0e92cae59c..c56eb9aa9c 100644 --- a/packages/richtext-lexical/src/field/features/types.ts +++ b/packages/richtext-lexical/src/field/features/types.ts @@ -18,9 +18,8 @@ import type React from 'react' import type { AdapterProps } from '../../types.js' import type { ClientEditorConfig, ServerEditorConfig } from '../lexical/config/types.js' import type { SlashMenuGroup } from '../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/types.js' -import type { FixedToolbarGroup } from '../lexical/plugins/toolbars/fixed/types.js' -import type { InlineToolbarGroup } from '../lexical/plugins/toolbars/inline/types.js' import type { HTMLConverter } from './converters/html/converter/types.js' +import type { ToolbarGroup } from './toolbars/types.js' export type PopulationPromise = ({ context, @@ -147,7 +146,7 @@ export type ClientFeature = { markdownTransformers?: Transformer[] nodes?: Array | LexicalNodeReplacement> /** - * Plugins are react component which get added to the editor. You can use them to interact with lexical, e.g. to create a command which creates a node, or opens a modal, or some other more "outside" functionality + * Plugins are react components which get added to the editor. You can use them to interact with lexical, e.g. to create a command which creates a node, or opens a modal, or some other more "outside" functionality */ plugins?: Array< | { @@ -195,7 +194,7 @@ export type ClientFeature = { * An opt-in, classic fixed toolbar which stays at the top of the editor */ toolbarFixed?: { - groups: FixedToolbarGroup[] + groups: ToolbarGroup[] } /** * The default, floating toolbar which appears when you select text. @@ -204,7 +203,7 @@ export type ClientFeature = { /** * Array of toolbar groups / sections. Each section can contain multiple toolbar items. */ - groups: InlineToolbarGroup[] + groups: ToolbarGroup[] } } @@ -328,7 +327,7 @@ export type ServerFeatureProviderMap = Map> /** - * Plugins are react component which get added to the editor. You can use them to interact with lexical, e.g. to create a command which creates a node, or opens a modal, or some other more "outside" functionality + * Plugins are react components which get added to the editor. You can use them to interact with lexical, e.g. to create a command which creates a node, or opens a modal, or some other more "outside" functionality */ export type SanitizedPlugin = | { @@ -433,7 +432,7 @@ export type SanitizedClientFeatures = Required< > } /** - * Plugins are react component which get added to the editor. You can use them to interact with lexical, e.g. to create a command which creates a node, or opens a modal, or some other more "outside" functionality + * Plugins are react components which get added to the editor. You can use them to interact with lexical, e.g. to create a command which creates a node, or opens a modal, or some other more "outside" functionality */ plugins?: Array slashMenu: { diff --git a/packages/richtext-lexical/src/field/features/upload/feature.client.tsx b/packages/richtext-lexical/src/field/features/upload/feature.client.tsx index 57d3869c9b..26506980d1 100644 --- a/packages/richtext-lexical/src/field/features/upload/feature.client.tsx +++ b/packages/richtext-lexical/src/field/features/upload/feature.client.tsx @@ -1,11 +1,14 @@ 'use client' +import { $isNodeSelection } from 'lexical' + import type { FeatureProviderProviderClient } from '../types.js' import { UploadIcon } from '../../lexical/ui/icons/Upload/index.js' import { createClientComponent } from '../createClientComponent.js' +import { toolbarAddDropdownGroupWithItems } from '../shared/toolbar/addDropdownGroup.js' import { INSERT_UPLOAD_WITH_DRAWER_COMMAND } from './drawer/commands.js' -import { UploadNode } from './nodes/UploadNode.js' +import { $isUploadNode, UploadNode } from './nodes/UploadNode.js' import { UploadPlugin } from './plugin/index.js' export type UploadFeaturePropsClient = { @@ -31,13 +34,12 @@ const UploadFeatureClient: FeatureProviderProviderClient { editor.dispatchCommand(INSERT_UPLOAD_WITH_DRAWER_COMMAND, { replace: false, @@ -46,9 +48,34 @@ const UploadFeatureClient: FeatureProviderProviderClient { + if (!$isNodeSelection(selection) || !selection.getNodes().length) { + return false + } + + const firstNode = selection.getNodes()[0] + return $isUploadNode(firstNode) + }, + key: 'upload', + label: 'Upload', + onSelect: ({ editor }) => { + editor.dispatchCommand(INSERT_UPLOAD_WITH_DRAWER_COMMAND, { + replace: false, + }) + }, + }, + ]), + ], + }, }), } } diff --git a/packages/richtext-lexical/src/field/lexical/EditorFocusProvider.tsx b/packages/richtext-lexical/src/field/lexical/EditorFocusProvider.tsx new file mode 100644 index 0000000000..f3c9fd2cb1 --- /dev/null +++ b/packages/richtext-lexical/src/field/lexical/EditorFocusProvider.tsx @@ -0,0 +1,100 @@ +import type { LexicalEditor } from 'lexical' + +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext.js' +import React, { createContext, useCallback, useContext, useState } from 'react' + +import type { EditorConfigContextType } from './config/client/EditorConfigProvider.js' + +import { useEditorConfigContext } from './config/client/EditorConfigProvider.js' + +export type EditorFocusContextType = { + blurEditor: () => void + editor: LexicalEditor + focusEditor: (_editor?: LexicalEditor, _editorConfigContext?: EditorConfigContextType) => void + focusedEditor: LexicalEditor | null + focusedEditorConfigContext: EditorConfigContextType + isChildEditorFocused: () => boolean + isEditorFocused: () => boolean + isParentEditorFocused: () => boolean + parentEditorConfigContext: EditorConfigContextType + parentEditorFocus: EditorFocusContextType +} + +const EditorFocusContext = createContext({ + blurEditor: null, + editor: null, + focusEditor: null, + focusedEditor: null, + focusedEditorConfigContext: null, + isChildEditorFocused: null, + isEditorFocused: null, + isParentEditorFocused: null, + parentEditorConfigContext: null, + parentEditorFocus: null, +}) + +export const useEditorFocus = (): EditorFocusContextType => { + return useContext(EditorFocusContext) +} + +export const EditorFocusProvider = ({ children }) => { + const parentEditorFocus = useEditorFocus() + const parentEditorConfigContext = useEditorConfigContext() // Is parent, as this EditorFocusProvider sits outside the EditorConfigProvider + const [editor] = useLexicalComposerContext() + + const [focusedEditor, setFocusedEditor] = useState(null) + const [focusedEditorConfigContext, setFocusedEditorConfigContext] = + useState(null) + + const focusEditor = useCallback( + (_editor: LexicalEditor, _editorConfigContext: EditorConfigContextType) => { + setFocusedEditor(_editor !== undefined ? _editor : editor) + setFocusedEditorConfigContext(_editorConfigContext) + if (parentEditorFocus.focusEditor) { + parentEditorFocus.focusEditor( + _editor !== undefined ? _editor : editor, + _editorConfigContext, + ) + } + }, + [editor, parentEditorFocus], + ) + + const blurEditor = useCallback(() => { + if (focusedEditor === editor) { + setFocusedEditor(null) + setFocusedEditorConfigContext(null) + } + }, [editor, focusedEditor]) + + const isEditorFocused = useCallback(() => { + return focusedEditor === editor + }, [editor, focusedEditor]) + + const isParentEditorFocused = useCallback(() => { + return parentEditorFocus?.isEditorFocused ? parentEditorFocus.isEditorFocused() : false + }, [parentEditorFocus]) + + const isChildEditorFocused = useCallback(() => { + return focusedEditor !== editor && !!focusedEditor + }, [editor, focusedEditor]) + + return ( + + {children} + + ) +} diff --git a/packages/richtext-lexical/src/field/lexical/LexicalEditor.tsx b/packages/richtext-lexical/src/field/lexical/LexicalEditor.tsx index ec93bfae6a..aa88a52721 100644 --- a/packages/richtext-lexical/src/field/lexical/LexicalEditor.tsx +++ b/packages/richtext-lexical/src/field/lexical/LexicalEditor.tsx @@ -5,25 +5,30 @@ import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin.js' import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin.js' import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin.js' import { TabIndentationPlugin } from '@lexical/react/LexicalTabIndentationPlugin.js' +import { mergeRegister } from '@lexical/utils' +import { BLUR_COMMAND, COMMAND_PRIORITY_LOW, FOCUS_COMMAND } from 'lexical' import * as React from 'react' import { useEffect, useState } from 'react' import type { LexicalProviderProps } from './LexicalProvider.js' +import { useEditorFocus } from './EditorFocusProvider.js' import { EditorPlugin } from './EditorPlugin.js' import './LexicalEditor.scss' +import { useEditorConfigContext } from './config/client/EditorConfigProvider.js' import { MarkdownShortcutPlugin } from './plugins/MarkdownShortcut/index.js' import { SlashMenuPlugin } from './plugins/SlashMenu/index.js' import { AddBlockHandlePlugin } from './plugins/handles/AddBlockHandlePlugin/index.js' import { DraggableBlockPlugin } from './plugins/handles/DraggableBlockPlugin/index.js' -import { FloatingSelectToolbarPlugin } from './plugins/toolbars/inline/Toolbar/index.js' import { LexicalContentEditable } from './ui/ContentEditable.js' export const LexicalEditor: React.FC> = ( props, ) => { const { editorConfig, onChange } = props + const editorConfigContext = useEditorConfigContext() const [editor] = useLexicalComposerContext() + const editorFocus = useEditorFocus() const [floatingAnchorElem, setFloatingAnchorElem] = useState(null) const onRef = (_floatingAnchorElem: HTMLDivElement) => { @@ -32,6 +37,29 @@ export const LexicalEditor: React.FC { + return mergeRegister( + editor.registerCommand( + FOCUS_COMMAND, + () => { + editorFocus.focusEditor(editor, editorConfigContext) + return true + }, + + COMMAND_PRIORITY_LOW, + ), + editor.registerCommand( + BLUR_COMMAND, + () => { + editorFocus.blurEditor() + return true + }, + + COMMAND_PRIORITY_LOW, + ), + ) + }, [editor, editorConfig, editorFocus]) + const [isSmallWidthViewport, setIsSmallWidthViewport] = useState(false) useEffect(() => { @@ -105,7 +133,6 @@ export const LexicalEditor: React.FC - )} diff --git a/packages/richtext-lexical/src/field/lexical/LexicalProvider.tsx b/packages/richtext-lexical/src/field/lexical/LexicalProvider.tsx index 7c98832bf5..d9730198fd 100644 --- a/packages/richtext-lexical/src/field/lexical/LexicalProvider.tsx +++ b/packages/richtext-lexical/src/field/lexical/LexicalProvider.tsx @@ -9,6 +9,7 @@ import * as React from 'react' import type { SanitizedClientEditorConfig } from './config/types.js' +import { EditorFocusProvider } from './EditorFocusProvider.js' import { LexicalEditor as LexicalEditorComponent } from './LexicalEditor.js' import { EditorConfigProvider } from './config/client/EditorConfigProvider.js' import { getEnabledNodes } from './nodes/index.js' @@ -77,11 +78,13 @@ export const LexicalProvider: React.FC = (props) => { return ( - -
- -
-
+ + +
+ +
+
+
) } diff --git a/packages/richtext-lexical/src/field/lexical/config/client/EditorConfigProvider.tsx b/packages/richtext-lexical/src/field/lexical/config/client/EditorConfigProvider.tsx index e0a52275f0..c5ebd5c846 100644 --- a/packages/richtext-lexical/src/field/lexical/config/client/EditorConfigProvider.tsx +++ b/packages/richtext-lexical/src/field/lexical/config/client/EditorConfigProvider.tsx @@ -11,7 +11,7 @@ import type { SanitizedClientEditorConfig } from '../types.js' function generateQuickGuid(): string { return Math.random().toString(36).substring(2, 12) + Math.random().toString(36).substring(2, 12) } -interface ContextType { +export interface EditorConfigContextType { editorConfig: SanitizedClientEditorConfig field: FormFieldBase & { editorConfig: SanitizedClientEditorConfig // With rendered features n stuff @@ -21,7 +21,7 @@ interface ContextType { uuid: string } -const Context: React.Context = createContext({ +const Context: React.Context = createContext({ editorConfig: null, field: null, uuid: generateQuickGuid(), @@ -56,7 +56,7 @@ export const EditorConfigProvider = ({ return {children} } -export const useEditorConfigContext = (): ContextType => { +export const useEditorConfigContext = (): EditorConfigContextType => { const context = useContext(Context) if (context === undefined) { throw new Error('useEditorConfigContext must be used within an EditorConfigProvider') diff --git a/packages/richtext-lexical/src/field/lexical/config/server/default.ts b/packages/richtext-lexical/src/field/lexical/config/server/default.ts index 0c4fb0abe7..15f823dbaa 100644 --- a/packages/richtext-lexical/src/field/lexical/config/server/default.ts +++ b/packages/richtext-lexical/src/field/lexical/config/server/default.ts @@ -21,6 +21,8 @@ import { OrderedListFeature } from '../../../features/lists/orderedList/feature. import { UnorderedListFeature } from '../../../features/lists/unorderedList/feature.server.js' import { ParagraphFeature } from '../../../features/paragraph/feature.server.js' import { RelationshipFeature } from '../../../features/relationship/feature.server.js' +import { FixedToolbarFeature } from '../../../features/toolbars/fixed/feature.server.js' +import { InlineToolbarFeature } from '../../../features/toolbars/inline/feature.server.js' import { UploadFeature } from '../../../features/upload/feature.server.js' import { LexicalEditorTheme } from '../../theme/EditorTheme.js' @@ -49,6 +51,7 @@ export const defaultEditorFeatures: FeatureProviderServer[] = BlockQuoteFeature(), UploadFeature(), HorizontalRuleFeature(), + InlineToolbarFeature(), ] export const defaultEditorConfig: ServerEditorConfig = { diff --git a/packages/richtext-lexical/src/field/lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/types.ts b/packages/richtext-lexical/src/field/lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/types.ts index c7378bb6e5..0b2f2dedc8 100644 --- a/packages/richtext-lexical/src/field/lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/types.ts +++ b/packages/richtext-lexical/src/field/lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/types.ts @@ -7,22 +7,22 @@ export type SlashMenuItem = { // Icon for display Icon: React.FC - displayName?: (({ i18n }: { i18n: I18n }) => string) | string - // Used for class names and, if displayName is not provided, for display. + // Used for class names and, if label is not provided, for display. key: string // TBD keyboardShortcut?: string // For extra searching. keywords?: Array + label?: (({ i18n }: { i18n: I18n }) => string) | string // What happens when you select this item? onSelect: ({ editor, queryString }: { editor: LexicalEditor; queryString: string }) => void } export type SlashMenuGroup = { - // Used for class names and, if displayName is not provided, for display. - displayName?: (({ i18n }: { i18n: I18n }) => string) | string items: Array key: string + // Used for class names and, if label is not provided, for display. + label?: (({ i18n }: { i18n: I18n }) => string) | string } export type SlashMenuItemInternal = SlashMenuItem & { diff --git a/packages/richtext-lexical/src/field/lexical/plugins/SlashMenu/index.tsx b/packages/richtext-lexical/src/field/lexical/plugins/SlashMenu/index.tsx index 65a969410a..fb78bbec44 100644 --- a/packages/richtext-lexical/src/field/lexical/plugins/SlashMenu/index.tsx +++ b/packages/richtext-lexical/src/field/lexical/plugins/SlashMenu/index.tsx @@ -41,10 +41,10 @@ function SlashMenuItem({ } let title = item.key - if (item.displayName) { - title = typeof item.displayName === 'function' ? item.displayName({ i18n }) : item.displayName + if (item.label) { + title = typeof item.label === 'function' ? item.label({ i18n }) : item.label } - // Crop title to max. 50 characters + // Crop title to max. 25 characters if (title.length > 25) { title = title.substring(0, 25) + '...' } @@ -108,9 +108,8 @@ export function SlashMenuPlugin({ groupsWithItems = groupsWithItems.map((group) => { const filteredItems = group.items.filter((item) => { let itemTitle = item.key - if (item.displayName) { - itemTitle = - typeof item.displayName === 'function' ? item.displayName({ i18n }) : item.displayName + if (item.label) { + itemTitle = typeof item.label === 'function' ? item.label({ i18n }) : item.label } if (new RegExp(queryString, 'gi').exec(itemTitle)) { @@ -192,11 +191,9 @@ export function SlashMenuPlugin({
{groups.map((group) => { let groupTitle = group.key - if (group.displayName) { + if (group.label) { groupTitle = - typeof group.displayName === 'function' - ? group.displayName({ i18n }) - : group.displayName + typeof group.label === 'function' ? group.label({ i18n }) : group.label } return ( diff --git a/packages/richtext-lexical/src/field/lexical/plugins/toolbars/fixed/types.ts b/packages/richtext-lexical/src/field/lexical/plugins/toolbars/fixed/types.ts deleted file mode 100644 index 77afed47ec..0000000000 --- a/packages/richtext-lexical/src/field/lexical/plugins/toolbars/fixed/types.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type { BaseSelection, LexicalEditor } from 'lexical' -import type React from 'react' - -export type FixedToolbarGroup = - | { - ChildComponent?: React.FC - items: Array - key: string - order?: number - type: 'dropdown' - } - | { - items: Array - key: string - order?: number - type: 'buttons' - } - -export type FixedToolbarGroupItem = { - ChildComponent?: React.FC - /** Use component to ignore the children and onClick properties. It does not use the default, pre-defined format Button component */ - Component?: React.FC<{ - anchorElem: HTMLElement - editor: LexicalEditor - item: FixedToolbarGroupItem - }> - isActive?: ({ editor, selection }: { editor: LexicalEditor; selection: BaseSelection }) => boolean - isEnabled?: ({ - editor, - selection, - }: { - editor: LexicalEditor - selection: BaseSelection - }) => boolean - key: string - /** The label is displayed as text if the item is part of a dropdown group */ - label?: string - onClick?: ({ editor, isActive }: { editor: LexicalEditor; isActive: boolean }) => void - order?: number -} diff --git a/packages/richtext-lexical/src/field/lexical/plugins/toolbars/inline/ToolbarDropdown/index.tsx b/packages/richtext-lexical/src/field/lexical/plugins/toolbars/inline/ToolbarDropdown/index.tsx deleted file mode 100644 index aacc94c5c6..0000000000 --- a/packages/richtext-lexical/src/field/lexical/plugins/toolbars/inline/ToolbarDropdown/index.tsx +++ /dev/null @@ -1,68 +0,0 @@ -'use client' -import React from 'react' - -const baseClass = 'inline-toolbar-popup__dropdown' - -import type { LexicalEditor } from 'lexical' - -import type { InlineToolbarGroupItem } from '../types.js' - -import { DropDown, DropDownItem } from './DropDown.js' -import './index.scss' - -export const ToolbarItem = ({ - anchorElem, - editor, - item, -}: { - anchorElem: HTMLElement - editor: LexicalEditor - item: InlineToolbarGroupItem -}) => { - if (item.Component) { - return ( - item?.Component && ( - - ) - ) - } - - return ( - - {item?.ChildComponent && } - {item.label} - - ) -} - -export const ToolbarDropdown = ({ - Icon, - anchorElem, - classNames, - editor, - groupKey, - items, -}: { - Icon?: React.FC - anchorElem: HTMLElement - classNames?: string[] - editor: LexicalEditor - groupKey: string - items: InlineToolbarGroupItem[] -}) => { - return ( - - {items.length && - items.map((item) => { - return - })} - - ) -} diff --git a/packages/richtext-lexical/src/field/lexical/ui/icons/Add/index.tsx b/packages/richtext-lexical/src/field/lexical/ui/icons/Add/index.tsx new file mode 100644 index 0000000000..89c4c6a6a2 --- /dev/null +++ b/packages/richtext-lexical/src/field/lexical/ui/icons/Add/index.tsx @@ -0,0 +1,8 @@ +import React from 'react' + +export const AddIcon: React.FC = () => ( + + + + +) diff --git a/packages/richtext-lexical/src/index.ts b/packages/richtext-lexical/src/index.ts index f4022e72b7..bfde95b1a9 100644 --- a/packages/richtext-lexical/src/index.ts +++ b/packages/richtext-lexical/src/index.ts @@ -340,7 +340,7 @@ export { BoldFeature } from './field/features/format/bold/feature.server.js' export { InlineCodeFeature } from './field/features/format/inlineCode/feature.server.js' export { ItalicFeature } from './field/features/format/italic/feature.server.js' -export { inlineToolbarFormatGroupWithItems } from './field/features/format/shared/inlineToolbarFormatGroup.js' +export { toolbarFormatGroupWithItems } from './field/features/format/shared/toolbarFormatGroup.js' export { StrikethroughFeature } from './field/features/format/strikethrough/feature.server.js' export { SubscriptFeature } from './field/features/format/subscript/feature.server.js' export { SuperscriptFeature } from './field/features/format/superscript/feature.server.js' @@ -384,8 +384,8 @@ export { SlateRelationshipConverter } from './field/features/migrations/slateToL export { SlateUnknownConverter } from './field/features/migrations/slateToLexical/converter/converters/unknown/index.js' export { SlateUnorderedListConverter } from './field/features/migrations/slateToLexical/converter/converters/unorderedList/index.js' export { SlateUploadConverter } from './field/features/migrations/slateToLexical/converter/converters/upload/index.js' - export { defaultSlateConverters } from './field/features/migrations/slateToLexical/converter/defaultConverters.js' + export { convertSlateNodesToLexical, convertSlateToLexical, @@ -394,8 +394,8 @@ export type { SlateNode, SlateNodeConverter, } from './field/features/migrations/slateToLexical/converter/types.js' - export { SlateToLexicalFeature } from './field/features/migrations/slateToLexical/feature.server.js' + export { ParagraphFeature } from './field/features/paragraph/feature.server.js' export { RelationshipFeature, @@ -408,8 +408,14 @@ export { RelationshipNode, type SerializedRelationshipNode, } from './field/features/relationship/nodes/RelationshipNode.js' -export { inlineToolbarFeatureButtonsGroupWithItems } from './field/features/shared/inlineToolbar/featureButtonsGroup.js' -export { inlineToolbarTextDropdownGroupWithItems } from './field/features/shared/inlineToolbar/textDropdownGroup.js' +export { toolbarAddDropdownGroupWithItems } from './field/features/shared/toolbar/addDropdownGroup.js' +export { toolbarFeatureButtonsGroupWithItems } from './field/features/shared/toolbar/featureButtonsGroup.js' + +export { toolbarTextDropdownGroupWithItems } from './field/features/shared/toolbar/textDropdownGroup.js' +export { FixedToolbarFeature } from './field/features/toolbars/fixed/feature.server.js' +export { InlineToolbarFeature } from './field/features/toolbars/inline/feature.server.js' + +export type { ToolbarGroup, ToolbarGroupItem } from './field/features/toolbars/types.js' export { createNode } from './field/features/typeUtilities.js' export type { ClientComponentProps, @@ -434,10 +440,10 @@ export type { ServerFeature, ServerFeatureProviderMap, } from './field/features/types.js' + export { UploadFeature } from './field/features/upload/feature.server.js' export type { UploadFeatureProps } from './field/features/upload/feature.server.js' - export { $createUploadNode, $isUploadNode, @@ -445,6 +451,7 @@ export { type UploadData, UploadNode, } from './field/features/upload/nodes/UploadNode.js' + export { EditorConfigProvider, useEditorConfigContext, @@ -462,30 +469,26 @@ export { loadFeatures, sortFeaturesForOptimalLoading, } from './field/lexical/config/server/loader.js' + export { sanitizeServerEditorConfig, sanitizeServerFeatures, } from './field/lexical/config/server/sanitize.js' - export type { ClientEditorConfig, SanitizedClientEditorConfig, SanitizedServerEditorConfig, ServerEditorConfig, } from './field/lexical/config/types.js' -export { getEnabledNodes } from './field/lexical/nodes/index.js' +export { getEnabledNodes } from './field/lexical/nodes/index.js' export { ENABLE_SLASH_MENU_COMMAND } from './field/lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/index.js' +export type { AdapterProps } + export type { SlashMenuGroup, SlashMenuItem, } from './field/lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/types.js' -export type { AdapterProps } - -export type { - InlineToolbarGroup, - InlineToolbarGroupItem, -} from './field/lexical/plugins/toolbars/inline/types.js' export { CAN_USE_DOM } from './field/lexical/utils/canUseDOM.js' export { cloneDeep } from './field/lexical/utils/cloneDeep.js' export { getDOMRangeRect } from './field/lexical/utils/getDOMRangeRect.js' diff --git a/test/fields/collections/Lexical/e2e.spec.ts b/test/fields/collections/Lexical/e2e.spec.ts index 07e56a3be9..7bf92162d6 100644 --- a/test/fields/collections/Lexical/e2e.spec.ts +++ b/test/fields/collections/Lexical/e2e.spec.ts @@ -213,11 +213,9 @@ describe('lexical', () => { await expect(floatingToolbar_formatSection).toBeVisible() - await expect(page.locator('.inline-toolbar-popup__button').first()).toBeVisible() + await expect(page.locator('.toolbar-popup__button').first()).toBeVisible() - const boldButton = floatingToolbar_formatSection - .locator('.inline-toolbar-popup__button') - .first() + const boldButton = floatingToolbar_formatSection.locator('.toolbar-popup__button').first() await expect(boldButton).toBeVisible() await boldButton.click() @@ -443,11 +441,9 @@ describe('lexical', () => { await expect(floatingToolbar_formatSection).toBeVisible() - await expect(page.locator('.inline-toolbar-popup__button').first()).toBeVisible() + await expect(page.locator('.toolbar-popup__button').first()).toBeVisible() - const boldButton = floatingToolbar_formatSection - .locator('.inline-toolbar-popup__button') - .first() + const boldButton = floatingToolbar_formatSection.locator('.toolbar-popup__button').first() await expect(boldButton).toBeVisible() await boldButton.click() @@ -521,7 +517,7 @@ describe('lexical', () => { await expect(floatingToolbar).toBeVisible() - const linkButton = floatingToolbar.locator('.inline-toolbar-popup__button-link').first() + const linkButton = floatingToolbar.locator('.toolbar-popup__button-link').first() await expect(linkButton).toBeVisible() await linkButton.click() diff --git a/test/fields/collections/Lexical/index.ts b/test/fields/collections/Lexical/index.ts index cc92bfa5d2..bae89a94be 100644 --- a/test/fields/collections/Lexical/index.ts +++ b/test/fields/collections/Lexical/index.ts @@ -2,6 +2,7 @@ import type { CollectionConfig } from 'payload/types' import { BlocksFeature, + FixedToolbarFeature, HeadingFeature, LinkFeature, TreeViewFeature, @@ -71,6 +72,7 @@ export const LexicalFields: CollectionConfig = { //TestRecorderFeature(), TreeViewFeature(), //HTMLConverterFeature(), + FixedToolbarFeature(), LinkFeature({ fields: ({ defaultFields }) => [ ...defaultFields,