From 96d97dc3180c423c5a9317d0784650a0aec24533 Mon Sep 17 00:00:00 2001 From: Stephan Schneider Date: Fri, 2 May 2025 21:25:18 +0200 Subject: [PATCH 01/13] feat: make editor configurable --- README.md | 76 +- admin/src/components/Input.tsx | 23 +- admin/src/index.ts | 113 ++- admin/src/lexical/Editor.tsx | 25 +- .../lexical/context/StrapiFieldContext.tsx | 33 + admin/src/lexical/context/ToolbarContext.tsx | 2 +- .../ToolbarItemRenderDependenciesContext.tsx | 27 + .../lexical/plugins/ShortcutsPlugin/index.tsx | 2 +- .../plugins/ToolbarPlugin/codeLessUtils.ts | 75 ++ .../lexical/plugins/ToolbarPlugin/index.tsx | 930 +++++------------- .../ToolbarPlugin/toolbarItems/code.tsx | 35 + .../toolbarItems/collapsible.tsx | 32 + .../ToolbarPlugin/toolbarItems/columns.tsx | 41 + .../ToolbarPlugin/toolbarItems/equation.tsx | 41 + .../ToolbarPlugin/toolbarItems/history.tsx | 55 ++ .../toolbarItems/horizontalRule.tsx | 43 + .../ToolbarPlugin/toolbarItems/image.tsx | 89 ++ .../ToolbarPlugin/toolbarItems/link.tsx | 44 + .../ToolbarPlugin/toolbarItems/other.tsx | 36 + .../ToolbarPlugin/toolbarItems/pageBreak.tsx | 44 + .../ToolbarPlugin/toolbarItems/richText.tsx | 282 ++++++ .../toolbarItems/strapiImage.tsx | 26 + .../ToolbarPlugin/toolbarItems/table.tsx | 42 + .../lexical/plugins/ToolbarPlugin/utils.ts | 75 +- admin/src/supportedNodeTypes.tsx | 311 ++++++ 25 files changed, 1708 insertions(+), 794 deletions(-) create mode 100644 admin/src/lexical/context/StrapiFieldContext.tsx create mode 100644 admin/src/lexical/context/ToolbarItemRenderDependenciesContext.tsx create mode 100644 admin/src/lexical/plugins/ToolbarPlugin/codeLessUtils.ts create mode 100644 admin/src/lexical/plugins/ToolbarPlugin/toolbarItems/code.tsx create mode 100644 admin/src/lexical/plugins/ToolbarPlugin/toolbarItems/collapsible.tsx create mode 100644 admin/src/lexical/plugins/ToolbarPlugin/toolbarItems/columns.tsx create mode 100644 admin/src/lexical/plugins/ToolbarPlugin/toolbarItems/equation.tsx create mode 100644 admin/src/lexical/plugins/ToolbarPlugin/toolbarItems/history.tsx create mode 100644 admin/src/lexical/plugins/ToolbarPlugin/toolbarItems/horizontalRule.tsx create mode 100644 admin/src/lexical/plugins/ToolbarPlugin/toolbarItems/image.tsx create mode 100644 admin/src/lexical/plugins/ToolbarPlugin/toolbarItems/link.tsx create mode 100644 admin/src/lexical/plugins/ToolbarPlugin/toolbarItems/other.tsx create mode 100644 admin/src/lexical/plugins/ToolbarPlugin/toolbarItems/pageBreak.tsx create mode 100644 admin/src/lexical/plugins/ToolbarPlugin/toolbarItems/richText.tsx create mode 100644 admin/src/lexical/plugins/ToolbarPlugin/toolbarItems/strapiImage.tsx create mode 100644 admin/src/lexical/plugins/ToolbarPlugin/toolbarItems/table.tsx create mode 100644 admin/src/supportedNodeTypes.tsx diff --git a/README.md b/README.md index 70d4ece..880b424 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ - [v1 - Stable](#v1---stable) - [Contributing](#contributing) - [Resources](#resources) + - [🛠️ Sponsored by hashbite.net | support \& custom development available](#️-sponsored-by-hashbitenet--support--custom-development-available) @@ -38,53 +39,52 @@ 1. Install the plugin: - ```bash - npm install strapi-plugin-lexical - ``` + ```bash + npm install strapi-plugin-lexical + ``` 2. Enable the plugin: - ```javascript - // ./config/plugins.js - { - lexical: { - enabled: true, - }, - - }; + ```javascript + // ./config/plugins.js + { + lexical: { + enabled: true, + }, + }; ``` 3. Include the required CSS and Prism.js in your Strapi admin: - ```javascript - // ./src/admin/app.js - import "strapi-plugin-lexical/dist/style.css"; - import "prismjs"; - ``` + ```javascript + // ./src/admin/app.js + import "strapi-plugin-lexical/style.css"; + import "prismjs"; + ``` 4. Add Vite support for Prism.js: - - Install the plugin: - - ```bash - npm install --save-dev vite-plugin-prismjs - ``` - - - Update your Vite configuration: - - ```javascript - // ./src/admin/vite.config.js - import { mergeConfig } from "vite"; - import prismjs from "vite-plugin-prismjs"; - - export default (config) => - mergeConfig(config, { - plugins: [ - prismjs({ - languages: "all", // Load all languages or customize as needed - }), - ], - }); - ``` + - Install the plugin: + + ```bash + npm install --save-dev vite-plugin-prismjs + ``` + + - Update your Vite configuration: + + ```javascript + // ./src/admin/vite.config.js + import { mergeConfig } from "vite"; + import prismjs from "vite-plugin-prismjs"; + + export default (config) => + mergeConfig(config, { + plugins: [ + prismjs({ + languages: "all", // Load all languages or customize as needed + }), + ], + }); + ``` > **Note:** Prism.js is required even if you don't plan to support code blocks. If you find a workaround to avoid this, please share it with us via a pull request or issue. We happily skip this installation step if we can! diff --git a/admin/src/components/Input.tsx b/admin/src/components/Input.tsx index 1711b0a..eec50e2 100644 --- a/admin/src/components/Input.tsx +++ b/admin/src/components/Input.tsx @@ -11,6 +11,10 @@ import { SerializedEditorState, SerializedElementNode, SerializedLexicalNode } f import LexicalEditor from '../lexical/Editor'; import { FlashMessageContext } from '../lexical/context/FlashMessageContext'; import { ToolbarContext } from '../lexical/context/ToolbarContext'; +import { + StrapiFieldConfig, + StrapiFieldConfigProvider, +} from '../lexical/context/StrapiFieldContext'; import { TableContext } from '../lexical/plugins/TablePlugin'; import PlaygroundEditorTheme from '../lexical/themes/PlaygroundEditorTheme'; @@ -26,6 +30,7 @@ interface CustomFieldsComponentProps { attribute: { type: string; customField: string; + options: StrapiFieldConfig; }; description: MessageDescriptor; placeholder: MessageDescriptor; @@ -245,14 +250,16 @@ const Input = React.forwardRef - - - + + + + + diff --git a/admin/src/index.ts b/admin/src/index.ts index fda7624..f213b50 100755 --- a/admin/src/index.ts +++ b/admin/src/index.ts @@ -3,6 +3,8 @@ import { Initializer } from './components/Initializer'; import { LexicalIcon } from './components/LexicalIcon'; import { PLUGIN_ID } from './pluginId'; +import { supportedNodeTypes } from './supportedNodeTypes'; + export default { register(app: StrapiApp) { app.registerPlugin({ @@ -31,7 +33,116 @@ export default { import(/* webpackChunkName: "lexical-input-component" */ './components/Input'), }, options: { - // declare options here + advanced: [ + { + sectionTitle: { + id: 'lexical.nodeTypes.section.enabled', + defaultMessage: 'Select the node types to enable', + }, + items: Object.values(supportedNodeTypes).map((supportedNodeType) => ({ + intlLabel: { + id: `lexical.supportedNodeTypes.${supportedNodeType.id}.label`, + defaultMessage: supportedNodeType.defaultLabel, + }, + type: 'checkbox', + description: { + id: `lexical.supportedNodeTypes.${supportedNodeType.id}.description`, + defaultMessage: supportedNodeType.defaultDescription, + }, + // Current strapi types do not reflect the possibility + // to store custom configuration names, but code does. + name: `options.enabledNodeTypes.${supportedNodeType.id}` as any, + })), + }, + { + sectionTitle: { + id: 'lexical.actions.section.enabled', + defaultMessage: 'Select actions to enable', + }, + items: [ + { + intlLabel: { + id: `lexical.actions.sessionHistory.label`, + defaultMessage: 'Session history', + }, + type: 'checkbox', + description: { + id: `lexical.actions.sessionHistory.description`, + defaultMessage: 'Add buttons to undo/redo within the current editing session', + }, + name: `options.enabledActions.sessionHistory`, + }, + { + intlLabel: { + id: `lexical.actions.clear.label`, + defaultMessage: 'Clear', + }, + type: 'checkbox', + description: { + id: `lexical.actions.clear.description`, + defaultMessage: 'Add button to clear all text within the current editor', + }, + name: `options.enabledActions.clear`, + }, + { + intlLabel: { + id: `lexical.actions.exportAsMarkdown.label`, + defaultMessage: 'Export as Markdown', + }, + type: 'checkbox', + description: { + id: `lexical.actions.exportAsMarkdown.description`, + defaultMessage: 'Add button to export editor text in Markdown format', + }, + name: `options.enabledActions.exportAsMarkdown`, + }, + { + intlLabel: { + id: `lexical.actions.import.label`, + defaultMessage: 'Import', + }, + type: 'checkbox', + description: { + id: `lexical.actions.import.description`, + defaultMessage: 'Add button to import existing Lexical-formatted text', + }, + name: `options.enabledActions.import`, + }, + { + intlLabel: { + id: `lexical.actions.export.label`, + defaultMessage: 'Export', + }, + type: 'checkbox', + description: { + id: `lexical.actions.export.description`, + defaultMessage: 'Add button to export text in Lexical format', + }, + name: `options.enabledActions.export`, + }, + ], + }, + { + sectionTitle: { + id: 'lexical.developers.section.enabled', + defaultMessage: 'Select developer settings to enable', + }, + items: [ + { + intlLabel: { + id: `lexical.developers.treeView.label`, + defaultMessage: 'Tree view', + }, + type: 'checkbox', + description: { + id: `lexical.developers.treeView.description`, + defaultMessage: 'Add button to show internal Lexical tree', + }, + name: `options.developers.treeView`, + }, + ], + }, + ], }, }); }, diff --git a/admin/src/lexical/Editor.tsx b/admin/src/lexical/Editor.tsx index c1a5b6d..9bb7eb2 100755 --- a/admin/src/lexical/Editor.tsx +++ b/admin/src/lexical/Editor.tsx @@ -74,14 +74,21 @@ import ContentEditable from './ui/ContentEditable'; import { useIntl } from 'react-intl'; import StrapiImagePlugin from './plugins/StrapiImagePlugin'; import './styles.css'; +import { SupportedNodeTypePlugins } from '../supportedNodeTypes'; -interface LexicalEditorProps { +export interface LexicalEditorProps { onChange: (newValue: SerializedEditorState) => void; ref: React.ForwardedRef; fieldName: string; expectedEditorState?: SerializedEditorState; } +type RenderPluginProps = { + lexicalEditorProps: LexicalEditorProps; + onRef: (_floatingAnchorElem: HTMLDivElement) => void; + placeholder: string; +}; + export default function Editor(props: LexicalEditorProps): JSX.Element { const { formatMessage } = useIntl(); const { historyState } = useSharedHistoryContext(); @@ -171,7 +178,6 @@ export default function Editor(props: LexicalEditorProps): JSX.Element { {selectionAlwaysOnDisplay && } - @@ -181,15 +187,10 @@ export default function Editor(props: LexicalEditorProps): JSX.Element { {isRichText ? ( <> - -
- -
- - } - ErrorBoundary={LexicalErrorBoundary} + @@ -203,7 +204,7 @@ export default function Editor(props: LexicalEditorProps): JSX.Element { - + diff --git a/admin/src/lexical/context/StrapiFieldContext.tsx b/admin/src/lexical/context/StrapiFieldContext.tsx new file mode 100644 index 0000000..15aecba --- /dev/null +++ b/admin/src/lexical/context/StrapiFieldContext.tsx @@ -0,0 +1,33 @@ +import { createContext, useContext } from 'react'; + +export type StrapiFieldConfig = { + enabledNodeTypes: EnabledNodeTypes; + enabledActions: EnabledActions; + developers: DeveloperOptions; +}; + +export type EnabledNodeTypes = { + bold: boolean; +}; + +type EnabledActions = { + exportAsMarkdown: boolean; +}; + +type DeveloperOptions = { + treeView: boolean; +}; + +const StrapiFieldConfigContext = createContext(undefined); + +export const StrapiFieldConfigProvider = StrapiFieldConfigContext.Provider; + +export const useStrapiFieldContext = () => { + const context = useContext(StrapiFieldConfigContext); + + if (context === undefined) { + throw new Error('useStrapiFieldContext must be used within a ToolbarProvider'); + } + + return context; +}; diff --git a/admin/src/lexical/context/ToolbarContext.tsx b/admin/src/lexical/context/ToolbarContext.tsx index 1002d24..f0fc321 100755 --- a/admin/src/lexical/context/ToolbarContext.tsx +++ b/admin/src/lexical/context/ToolbarContext.tsx @@ -74,7 +74,7 @@ const INITIAL_TOOLBAR_STATE = { rootType: 'root' as keyof typeof rootTypeToRootName, }; -type ToolbarState = typeof INITIAL_TOOLBAR_STATE; +export type ToolbarState = typeof INITIAL_TOOLBAR_STATE; // Utility type to get keys and infer value types type ToolbarStateKey = keyof ToolbarState; diff --git a/admin/src/lexical/context/ToolbarItemRenderDependenciesContext.tsx b/admin/src/lexical/context/ToolbarItemRenderDependenciesContext.tsx new file mode 100644 index 0000000..fdd91e9 --- /dev/null +++ b/admin/src/lexical/context/ToolbarItemRenderDependenciesContext.tsx @@ -0,0 +1,27 @@ +import { LexicalEditor } from 'lexical'; +import { Dispatch, createContext, useContext } from 'react'; + +type ToolbarItemRenderDependencies = { + activeEditor: LexicalEditor; + isEditable: boolean; + setIsLinkEditMode: Dispatch; + setIsStrapiImageDialogOpen: Dispatch; + showModal: (title: string, showModal: (onClose: () => void) => JSX.Element) => void; +}; +const ToolbarItemRenderDependenciesContext = createContext< + ToolbarItemRenderDependencies | undefined +>(undefined); + +export const ToolbarItemRenderDependenciesProvider = ToolbarItemRenderDependenciesContext.Provider; + +export const useToolbarItemRenderDependencies = () => { + const context = useContext(ToolbarItemRenderDependenciesContext); + + if (context === undefined) { + throw new Error( + 'useToolbarItemRenderDependencies must be used within a ToolbarItemRenderDependenciesProvider' + ); + } + + return context; +}; diff --git a/admin/src/lexical/plugins/ShortcutsPlugin/index.tsx b/admin/src/lexical/plugins/ShortcutsPlugin/index.tsx index fb8af81..fe34cf9 100644 --- a/admin/src/lexical/plugins/ShortcutsPlugin/index.tsx +++ b/admin/src/lexical/plugins/ShortcutsPlugin/index.tsx @@ -22,7 +22,6 @@ import { Dispatch, useEffect } from 'react'; import { useToolbarState } from '../../context/ToolbarContext'; import { sanitizeUrl } from '../../utils/url'; import { - clearFormatting, formatBulletList, formatCheckList, formatCode, @@ -59,6 +58,7 @@ import { isSuperscript, isUppercase, } from './shortcuts'; +import { clearFormatting } from '../ToolbarPlugin/codeLessUtils'; export default function ShortcutsPlugin({ editor, diff --git a/admin/src/lexical/plugins/ToolbarPlugin/codeLessUtils.ts b/admin/src/lexical/plugins/ToolbarPlugin/codeLessUtils.ts new file mode 100644 index 0000000..e130bf3 --- /dev/null +++ b/admin/src/lexical/plugins/ToolbarPlugin/codeLessUtils.ts @@ -0,0 +1,75 @@ +import { $isDecoratorBlockNode } from '@lexical/react/LexicalDecoratorBlockNode'; +import { $isHeadingNode, $isQuoteNode } from '@lexical/rich-text'; +import { $isTableSelection } from '@lexical/table'; +import { $getNearestBlockElementAncestorOrThrow } from '@lexical/utils'; +import { + $createParagraphNode, + $getSelection, + $isRangeSelection, + $isTextNode, + LexicalEditor, +} from 'lexical'; + +export function dropDownActiveClass(active: boolean) { + if (active) { + return 'active dropdown-item-active'; + } else { + return ''; + } +} + +export const clearFormatting = (editor: LexicalEditor) => { + editor.update(() => { + const selection = $getSelection(); + if ($isRangeSelection(selection) || $isTableSelection(selection)) { + const anchor = selection.anchor; + const focus = selection.focus; + const nodes = selection.getNodes(); + const extractedNodes = selection.extract(); + + if (anchor.key === focus.key && anchor.offset === focus.offset) { + return; + } + + nodes.forEach((node, idx) => { + // We split the first and last node by the selection + // So that we don't format unselected text inside those nodes + if ($isTextNode(node)) { + // Use a separate variable to ensure TS does not lose the refinement + let textNode = node; + if (idx === 0 && anchor.offset !== 0) { + textNode = textNode.splitText(anchor.offset)[1] || textNode; + } + if (idx === nodes.length - 1) { + textNode = textNode.splitText(focus.offset)[0] || textNode; + } + /** + * If the selected text has one format applied + * selecting a portion of the text, could + * clear the format to the wrong portion of the text. + * + * The cleared text is based on the length of the selected text. + */ + // We need this in case the selected text only has one format + const extractedTextNode = extractedNodes[0]; + if (nodes.length === 1 && $isTextNode(extractedTextNode)) { + textNode = extractedTextNode; + } + + if (textNode.__style !== '') { + textNode.setStyle(''); + } + if (textNode.__format !== 0) { + textNode.setFormat(0); + $getNearestBlockElementAncestorOrThrow(textNode).setFormat(''); + } + node = textNode; + } else if ($isHeadingNode(node) || $isQuoteNode(node)) { + node.replace($createParagraphNode(), true); + } else if ($isDecoratorBlockNode(node)) { + node.setFormat(''); + } + }); + } + }); +}; diff --git a/admin/src/lexical/plugins/ToolbarPlugin/index.tsx b/admin/src/lexical/plugins/ToolbarPlugin/index.tsx index 248f66b..d880d6c 100755 --- a/admin/src/lexical/plugins/ToolbarPlugin/index.tsx +++ b/admin/src/lexical/plugins/ToolbarPlugin/index.tsx @@ -7,6 +7,7 @@ */ import type { JSX } from 'react'; +import { Children } from 'react'; import { useIntl } from 'react-intl'; import { @@ -65,18 +66,15 @@ import DropDown, { DropDownItem } from '../../ui/DropDown'; import { getSelectedNode } from '../../utils/getSelectedNode'; import { sanitizeUrl } from '../../utils/url'; import { EmbedConfigs } from '../AutoEmbedPlugin'; -import { INSERT_COLLAPSIBLE_COMMAND } from '../CollapsiblePlugin'; -import { InsertEquationDialog } from '../EquationsPlugin'; + import { INSERT_IMAGE_COMMAND, InsertImageDialog, InsertImagePayload } from '../ImagesPlugin'; import { InsertInlineImageDialog } from '../InlineImagePlugin'; -import InsertLayoutDialog from '../LayoutPlugin/InsertLayoutDialog'; -import { INSERT_PAGE_BREAK } from '../PageBreakPlugin'; + import { InsertPollDialog } from '../PollPlugin'; import { SHORTCUTS } from '../ShortcutsPlugin/shortcuts'; import { InsertStrapiImageDialog } from '../StrapiImagePlugin'; -import { InsertTableDialog } from '../TablePlugin'; + import { - clearFormatting, formatBulletList, formatCheckList, formatCode, @@ -85,6 +83,14 @@ import { formatParagraph, formatQuote, } from './utils'; +import { HistoryRedo, HistoryUndo } from './toolbarItems/history'; +import { + ToolbarItemRenderDependenciesProvider, + useToolbarItemRenderDependencies, +} from '../../context/ToolbarItemRenderDependenciesContext'; +import { EnabledNodeTypes, useStrapiFieldContext } from '../../context/StrapiFieldContext'; +import { supportedNodeTypes } from '../../../supportedNodeTypes'; +import { dropDownActiveClass, clearFormatting } from './codeLessUtils'; const rootTypeToRootName = { root: 'Root', @@ -165,14 +171,6 @@ const ELEMENT_FORMAT_OPTIONS: { }, }; -function dropDownActiveClass(active: boolean) { - if (active) { - return 'active dropdown-item-active'; - } else { - return ''; - } -} - // @todo: extract to external file function BlockFormatDropDown({ editor, @@ -575,6 +573,130 @@ function ElementFormatDropdown({ ); } +function ToolbarItem(props: { id: string }): React.ReactNode { + const { toolbarState } = useToolbarState(); + const renderDependencies = useToolbarItemRenderDependencies(); + const strapiFieldConfig = useStrapiFieldContext(); + + if (props.id.startsWith('nodeType')) { + const [, nodeTypeId] = props.id.split('.'); + const enabled = strapiFieldConfig.enabledNodeTypes[nodeTypeId as keyof EnabledNodeTypes]; + const nodeType = supportedNodeTypes[nodeTypeId as keyof typeof supportedNodeTypes]; + if (enabled && nodeType?.renderToolbarItem) { + const Item = nodeType.renderToolbarItem; + return ( + + ); + } + } + + if (props.id === 'actions.history.undo') { + return ( + + ); + } + if (props.id === 'actions.history.redo') { + return ( + + ); + } + + // @todo what about the floating actions? + // @todo the floating toolbar also needs to adjust itself! + + return undefined; +} + +function ToolbarGroup(props: React.PropsWithChildren<{}>) { + const groupElements = Children.map(props.children, (element) => { + if (!React.isValidElement(element)) { + // Ignore non-elements. This allows people to more easily inline + // conditionals in their route config. + return; + } + if (element.type !== ToolbarItem) { + // Ignore unknown elements + // TODO: fail with good error message? + return; + } + + return element; + }); + + if (!groupElements || !Children.count(groupElements)) { + // Return an empty group if all elements are disabled + // @todo this is not working! toolbar still renders as "just a divider" + return; + } + + return ( + <> + {...groupElements} + + + ); +} + +function ToolbarDropDown( + props: React.PropsWithChildren<{ + buttonAriaLabel: string; + buttonLabel: string; + buttonIconClassName: string; + }> +) { + const { toolbarState } = useToolbarState(); + const renderDependencies = useToolbarItemRenderDependencies(); + const strapiFieldConfig = useStrapiFieldContext(); + + const groupElements = Children.map(props.children, (element) => { + if (!React.isValidElement(element)) { + // Ignore non-elements. This allows people to more easily inline + // conditionals in their route config. + return; + } + if (element.type !== ToolbarItem) { + // Ignore unknown elements + // TODO: fail with good error message? + return; + } + + return element; + }); + + if (!groupElements || !Children.count(groupElements)) { + // Return an empty group if all elements are disabled + // @todo this is not working! toolbar still renders as "just a divider" + return; + } + + return ( + <> + + {...groupElements} + + + + ); +} + export default function ToolbarPlugin({ editor, activeEditor, @@ -786,16 +908,6 @@ export default function ToolbarPlugin({ [applyStyleText] ); - const insertLink = useCallback(() => { - if (!toolbarState.isLink) { - setIsLinkEditMode(true); - activeEditor.dispatchCommand(TOGGLE_LINK_COMMAND, sanitizeUrl('https://')); - } else { - setIsLinkEditMode(false); - activeEditor.dispatchCommand(TOGGLE_LINK_COMMAND, null); - } - }, [activeEditor, setIsLinkEditMode, toolbarState.isLink]); - const onCodeLanguageSelect = useCallback( (value: string) => { activeEditor.update(() => { @@ -814,7 +926,6 @@ export default function ToolbarPlugin({ }; const canViewerSeeInsertDropdown = !toolbarState.isImageCaption; - const canViewerSeeInsertCodeButton = !toolbarState.isImageCaption; const [isStrapiImageDialogOpen, setIsStrapiImageDialogOpen] = useState(false); @@ -827,84 +938,129 @@ export default function ToolbarPlugin({ }>; return ( -
- - - {isStrapiImageDialogOpen && ( - setIsStrapiImageDialogOpen(false)} - /> - )} - - {toolbarState.blockType in blockTypeToBlockName && activeEditor === editor && ( - <> - - - - )} - {toolbarState.blockType === 'code' ? ( - +
+ + + + + + + + + + + + + + + + {/* TODO: how to support a nested "additional options" */} + + + + + {isStrapiImageDialogOpen && ( + setIsStrapiImageDialogOpen(false)} + /> + )} + + + + + - {CODE_LANGUAGE_OPTIONS.map(([value, name]) => { - return ( - onCodeLanguageSelect(value)} - key={value} - > - {name} - - ); + + + + + + + + + + + - ) : ( - <> - {/* + + + + + + + + + + {EmbedConfigs.map((embedConfig) => ( + { + activeEditor.dispatchCommand(INSERT_EMBED_COMMAND, embedConfig.type); + }} + className="item" + > + {embedConfig.icon} + {embedConfig.contentName} + + ))} + + + {/* @todo */} + {toolbarState.blockType in blockTypeToBlockName && activeEditor === editor && ( + <> + + + + )} + + {/* @todo */} + {toolbarState.blockType === 'code' ? ( + + {CODE_LANGUAGE_OPTIONS.map(([value, name]) => { + return ( + onCodeLanguageSelect(value)} + key={value} + > + {name} + + ); + })} + + ) : ( + <> + {/* */} - - - - {canViewerSeeInsertCodeButton && ( - - )} - {/* - */} - - { - activeEditor.dispatchCommand(FORMAT_TEXT_COMMAND, 'lowercase'); - }} - className={'item wide ' + dropDownActiveClass(toolbarState.isLowercase)} - title={formatMessage({ - id: 'lexical.plugin.toolbar.format.lowercase.title', - defaultMessage: 'Lowercase', - })} - aria-label={formatMessage({ - id: 'lexical.plugin.toolbar.format.lowercase.aria', - defaultMessage: 'Format text to lowercase', - })} - > -
- - - {formatMessage({ - id: 'lexical.plugin.toolbar.format.lowercase.text', - defaultMessage: 'Lowercase', - })} - -
- {SHORTCUTS.LOWERCASE} -
- { - activeEditor.dispatchCommand(FORMAT_TEXT_COMMAND, 'uppercase'); - }} - className={'item wide ' + dropDownActiveClass(toolbarState.isUppercase)} - title={formatMessage({ - id: 'lexical.plugin.toolbar.format.uppercase.title', - defaultMessage: 'Uppercase', - })} - aria-label={formatMessage({ - id: 'lexical.plugin.toolbar.format.uppercase.aria', - defaultMessage: 'Format text to uppercase', - })} - > -
- - - {formatMessage({ - id: 'lexical.plugin.toolbar.format.uppercase.text', - defaultMessage: 'Uppercase', - })} - -
- {SHORTCUTS.UPPERCASE} -
- { - activeEditor.dispatchCommand(FORMAT_TEXT_COMMAND, 'capitalize'); - }} - className={'item wide ' + dropDownActiveClass(toolbarState.isCapitalize)} - title={formatMessage({ - id: 'lexical.plugin.toolbar.format.capitalize.title', - defaultMessage: 'Capitalize', - })} - aria-label={formatMessage({ - id: 'lexical.plugin.toolbar.format.capitalize.aria', - defaultMessage: 'Format text to capitalize', - })} - > -
- - - {formatMessage({ - id: 'lexical.plugin.toolbar.format.capitalize.text', - defaultMessage: 'Capitalize', - })} - -
- {SHORTCUTS.CAPITALIZE} -
- { - activeEditor.dispatchCommand(FORMAT_TEXT_COMMAND, 'strikethrough'); - }} - className={'item wide ' + dropDownActiveClass(toolbarState.isStrikethrough)} - title={formatMessage({ - id: 'lexical.plugin.toolbar.format.strikethrough.title', - defaultMessage: 'Strikethrough', - })} - aria-label={formatMessage({ - id: 'lexical.plugin.toolbar.format.strikethrough.aria', - defaultMessage: 'Format text with a strikethrough', - })} - > -
- - - {formatMessage({ - id: 'lexical.plugin.toolbar.format.strikethrough.text', - defaultMessage: 'Strikethrough', - })} - -
- {SHORTCUTS.STRIKETHROUGH} -
- { - activeEditor.dispatchCommand(FORMAT_TEXT_COMMAND, 'subscript'); - }} - className={'item wide ' + dropDownActiveClass(toolbarState.isSubscript)} - title={formatMessage({ - id: 'lexical.plugin.toolbar.format.subscript.title', - defaultMessage: 'Subscript', - })} - aria-label={formatMessage({ - id: 'lexical.plugin.toolbar.format.subscript.aria', - defaultMessage: 'Format text with a subscript', - })} - > -
- - - {formatMessage({ - id: 'lexical.plugin.toolbar.format.subscript.text', - defaultMessage: 'Subscript', - })} - -
- {SHORTCUTS.SUBSCRIPT} -
- { - activeEditor.dispatchCommand(FORMAT_TEXT_COMMAND, 'superscript'); - }} - className={'item wide ' + dropDownActiveClass(toolbarState.isSuperscript)} - title={formatMessage({ - id: 'lexical.plugin.toolbar.format.superscript.title', - defaultMessage: 'Superscript', - })} - aria-label={formatMessage({ - id: 'lexical.plugin.toolbar.format.superscript.aria', - defaultMessage: 'Format text with a superscript', - })} - > -
- - - {formatMessage({ - id: 'lexical.plugin.toolbar.format.superscript.text', - defaultMessage: 'Superscript', - })} - -
- {SHORTCUTS.SUPERSCRIPT} -
- clearFormatting(activeEditor)} - className="item wide" - title={formatMessage({ - id: 'lexical.plugin.toolbar.format.clear.title', - defaultMessage: 'Clear text formatting', - })} - aria-label={formatMessage({ - id: 'lexical.plugin.toolbar.format.clear.aria', - defaultMessage: 'Clear all text formatting', - })} - > -
- - - {formatMessage({ - id: 'lexical.plugin.toolbar.format.clear.text', - defaultMessage: 'Clear Formatting', - })} - -
- {SHORTCUTS.CLEAR_FORMATTING} -
-
- - - - - - + - - {canViewerSeeInsertDropdown && ( - <> - - - { - activeEditor.dispatchCommand(INSERT_HORIZONTAL_RULE_COMMAND, undefined); - }} - className="item" - > - - - {formatMessage({ - id: 'lexical.plugin.toolbar.insert.horizontalrule.text', - defaultMessage: 'Horizontal Rule', - })} - - - { - activeEditor.dispatchCommand(INSERT_PAGE_BREAK, undefined); - }} - className="item" - > - - - {formatMessage({ - id: 'lexical.plugin.toolbar.insert.pagebreak.text', - defaultMessage: 'Page Break', - })} - - - { - showModal( - formatMessage({ - id: 'lexical.plugin.toolbar.insert.image.modal.title', - defaultMessage: 'Insert Image', - }), - (onClose) => ( - - ) - ); - }} - className="item" - > - - - {formatMessage({ - id: 'lexical.plugin.toolbar.insert.image.text', - defaultMessage: 'Image', - })} - - - { - showModal( - formatMessage({ - id: 'lexical.plugin.toolbar.insert.inlineimage.modal.title', - defaultMessage: 'Insert Inline Image', - }), - (onClose) => ( - - ) - ); - }} - className="item" - > - - - {formatMessage({ - id: 'lexical.plugin.toolbar.insert.inlineimage.text', - defaultMessage: 'Inline Image', - })} - - - { - showModal( - formatMessage({ - id: 'lexical.plugin.toolbar.insert.table.modal.title', - defaultMessage: 'Insert Table', - }), - (onClose) => ( - - ) - ); - }} - className="item" - > - - - {formatMessage({ - id: 'lexical.plugin.toolbar.insert.table.text', - defaultMessage: 'Table', - })} - - - { - showModal( - formatMessage({ - id: 'lexical.plugin.toolbar.insert.poll.modal.title', - defaultMessage: 'Insert Poll', - }), - (onClose) => ( - - ) - ); - }} - className="item" - > - - - {formatMessage({ - id: 'lexical.plugin.toolbar.insert.poll.text', - defaultMessage: 'Poll', - })} - - - { - showModal( - formatMessage({ - id: 'lexical.plugin.toolbar.insert.columns.modal.title', - defaultMessage: 'Insert Columns Layout', - }), - (onClose) => ( - - ) - ); - }} - className="item" - > - - - {formatMessage({ - id: 'lexical.plugin.toolbar.insert.columns.text', - defaultMessage: 'Columns Layout', - })} - - - { - showModal( - formatMessage({ - id: 'lexical.plugin.toolbar.insert.equation.modal.title', - defaultMessage: 'Insert Equation', - }), - (onClose) => ( - - ) - ); - }} - className="item" - > - - - {formatMessage({ - id: 'lexical.plugin.toolbar.insert.equation.text', - defaultMessage: 'Equation', - })} - - - { - editor.update(() => { - const root = $getRoot(); - const stickyNode = $createStickyNode(0, 0); - root.append(stickyNode); - }); - }} - className="item" - > - - - {formatMessage({ - id: 'lexical.plugin.toolbar.insert.stickynote.text', - defaultMessage: 'Sticky Note', - })} - - - { - editor.dispatchCommand(INSERT_COLLAPSIBLE_COMMAND, undefined); - }} - className="item" - > - - - {formatMessage({ - id: 'lexical.plugin.toolbar.insert.collapsible.text', - defaultMessage: 'Collapsible container', - })} - - - {EmbedConfigs.map((embedConfig) => ( - { - activeEditor.dispatchCommand(INSERT_EMBED_COMMAND, embedConfig.type); - }} - className="item" - > - {embedConfig.icon} - {embedConfig.contentName} - - ))} - - - )} - - )} - {modal} -
+ {/* @todo */} + + + )} + {modal} +
+ ); } diff --git a/admin/src/lexical/plugins/ToolbarPlugin/toolbarItems/code.tsx b/admin/src/lexical/plugins/ToolbarPlugin/toolbarItems/code.tsx new file mode 100644 index 0000000..6138406 --- /dev/null +++ b/admin/src/lexical/plugins/ToolbarPlugin/toolbarItems/code.tsx @@ -0,0 +1,35 @@ +import { useIntl } from 'react-intl'; +import { ToolbarItemProps } from '../../../../supportedNodeTypes'; +import { FORMAT_TEXT_COMMAND } from 'lexical'; +import { SHORTCUTS } from '../../ShortcutsPlugin/shortcuts'; + +export function Code({ activeEditor, toolbarState, isEditable }: ToolbarItemProps) { + const { formatMessage } = useIntl(); + const canViewerSeeInsertCodeButton = !toolbarState.isImageCaption; + + return ( + canViewerSeeInsertCodeButton && ( + + ) + ); +} diff --git a/admin/src/lexical/plugins/ToolbarPlugin/toolbarItems/collapsible.tsx b/admin/src/lexical/plugins/ToolbarPlugin/toolbarItems/collapsible.tsx new file mode 100644 index 0000000..fbe2f6a --- /dev/null +++ b/admin/src/lexical/plugins/ToolbarPlugin/toolbarItems/collapsible.tsx @@ -0,0 +1,32 @@ +import { useIntl } from 'react-intl'; +import { ToolbarItemProps } from '../../../../supportedNodeTypes'; +import { SHORTCUTS } from '../../ShortcutsPlugin/shortcuts'; +import { useCallback } from 'react'; +import { TOGGLE_LINK_COMMAND } from '@lexical/link'; +import { sanitizeUrl } from '../../../utils/url'; +import { useToolbarItemRenderDependencies } from '../../../context/ToolbarItemRenderDependenciesContext'; +import { FORMAT_TEXT_COMMAND } from 'lexical'; +import { DropDownItem } from '../../../ui/DropDown'; +import { dropDownActiveClass } from '../codeLessUtils'; +import { INSERT_COLLAPSIBLE_COMMAND } from '../../CollapsiblePlugin'; + +export function Collapsible({ activeEditor, toolbarState, isEditable }: ToolbarItemProps) { + const { formatMessage } = useIntl(); + + return ( + { + activeEditor.dispatchCommand(INSERT_COLLAPSIBLE_COMMAND, undefined); + }} + className="item" + > + + + {formatMessage({ + id: 'lexical.plugin.toolbar.insert.collapsible.text', + defaultMessage: 'Collapsible container', + })} + + + ); +} diff --git a/admin/src/lexical/plugins/ToolbarPlugin/toolbarItems/columns.tsx b/admin/src/lexical/plugins/ToolbarPlugin/toolbarItems/columns.tsx new file mode 100644 index 0000000..8e9897f --- /dev/null +++ b/admin/src/lexical/plugins/ToolbarPlugin/toolbarItems/columns.tsx @@ -0,0 +1,41 @@ +import { useIntl } from 'react-intl'; +import { ToolbarItemProps } from '../../../../supportedNodeTypes'; +import { SHORTCUTS } from '../../ShortcutsPlugin/shortcuts'; +import { useCallback } from 'react'; +import { TOGGLE_LINK_COMMAND } from '@lexical/link'; +import { sanitizeUrl } from '../../../utils/url'; +import { useToolbarItemRenderDependencies } from '../../../context/ToolbarItemRenderDependenciesContext'; +import { FORMAT_TEXT_COMMAND } from 'lexical'; +import { DropDownItem } from '../../../ui/DropDown'; +import { dropDownActiveClass } from '../codeLessUtils'; +import { INSERT_HORIZONTAL_RULE_COMMAND } from '@lexical/react/LexicalHorizontalRuleNode'; +import { INSERT_IMAGE_COMMAND, InsertImageDialog, InsertImagePayload } from '../../ImagesPlugin'; +import InsertLayoutDialog from '../../LayoutPlugin/InsertLayoutDialog'; + +export function Columns({ activeEditor, toolbarState, isEditable }: ToolbarItemProps) { + const { formatMessage } = useIntl(); + const { setIsLinkEditMode, showModal } = useToolbarItemRenderDependencies(); + + return ( + { + showModal( + formatMessage({ + id: 'lexical.plugin.toolbar.insert.columns.modal.title', + defaultMessage: 'Insert Columns Layout', + }), + (onClose) => + ); + }} + className="item" + > + + + {formatMessage({ + id: 'lexical.plugin.toolbar.insert.columns.text', + defaultMessage: 'Columns Layout', + })} + + + ); +} diff --git a/admin/src/lexical/plugins/ToolbarPlugin/toolbarItems/equation.tsx b/admin/src/lexical/plugins/ToolbarPlugin/toolbarItems/equation.tsx new file mode 100644 index 0000000..731dd80 --- /dev/null +++ b/admin/src/lexical/plugins/ToolbarPlugin/toolbarItems/equation.tsx @@ -0,0 +1,41 @@ +import { useIntl } from 'react-intl'; +import { ToolbarItemProps } from '../../../../supportedNodeTypes'; +import { SHORTCUTS } from '../../ShortcutsPlugin/shortcuts'; +import { useCallback } from 'react'; +import { TOGGLE_LINK_COMMAND } from '@lexical/link'; +import { sanitizeUrl } from '../../../utils/url'; +import { useToolbarItemRenderDependencies } from '../../../context/ToolbarItemRenderDependenciesContext'; +import { FORMAT_TEXT_COMMAND } from 'lexical'; +import { DropDownItem } from '../../../ui/DropDown'; +import { dropDownActiveClass } from '../codeLessUtils'; +import { INSERT_HORIZONTAL_RULE_COMMAND } from '@lexical/react/LexicalHorizontalRuleNode'; +import { INSERT_IMAGE_COMMAND, InsertImageDialog, InsertImagePayload } from '../../ImagesPlugin'; +import { InsertEquationDialog } from '../../EquationsPlugin'; + +export function Equation({ activeEditor, toolbarState, isEditable }: ToolbarItemProps) { + const { formatMessage } = useIntl(); + const { setIsLinkEditMode, showModal } = useToolbarItemRenderDependencies(); + + return ( + { + showModal( + formatMessage({ + id: 'lexical.plugin.toolbar.insert.equation.modal.title', + defaultMessage: 'Insert Equation', + }), + (onClose) => + ); + }} + className="item" + > + + + {formatMessage({ + id: 'lexical.plugin.toolbar.insert.equation.text', + defaultMessage: 'Equation', + })} + + + ); +} diff --git a/admin/src/lexical/plugins/ToolbarPlugin/toolbarItems/history.tsx b/admin/src/lexical/plugins/ToolbarPlugin/toolbarItems/history.tsx new file mode 100644 index 0000000..e8ea658 --- /dev/null +++ b/admin/src/lexical/plugins/ToolbarPlugin/toolbarItems/history.tsx @@ -0,0 +1,55 @@ +import { LexicalEditor, REDO_COMMAND, UNDO_COMMAND } from 'lexical'; +import { ToolbarState } from '../../../context/ToolbarContext'; +import { useIntl } from 'react-intl'; +import { IS_APPLE } from '@lexical/utils'; +import { ToolbarItemProps } from '../../../../supportedNodeTypes'; + +export function HistoryUndo({ toolbarState, activeEditor, isEditable }: ToolbarItemProps) { + const { formatMessage } = useIntl(); + + return ( + + ); +} + +export function HistoryRedo({ toolbarState, activeEditor, isEditable }: ToolbarItemProps) { + const { formatMessage } = useIntl(); + + return ( + + ); +} diff --git a/admin/src/lexical/plugins/ToolbarPlugin/toolbarItems/horizontalRule.tsx b/admin/src/lexical/plugins/ToolbarPlugin/toolbarItems/horizontalRule.tsx new file mode 100644 index 0000000..108028c --- /dev/null +++ b/admin/src/lexical/plugins/ToolbarPlugin/toolbarItems/horizontalRule.tsx @@ -0,0 +1,43 @@ +import { useIntl } from 'react-intl'; +import { ToolbarItemProps } from '../../../../supportedNodeTypes'; +import { SHORTCUTS } from '../../ShortcutsPlugin/shortcuts'; +import { useCallback } from 'react'; +import { TOGGLE_LINK_COMMAND } from '@lexical/link'; +import { sanitizeUrl } from '../../../utils/url'; +import { useToolbarItemRenderDependencies } from '../../../context/ToolbarItemRenderDependenciesContext'; +import { FORMAT_TEXT_COMMAND } from 'lexical'; +import { DropDownItem } from '../../../ui/DropDown'; +import { dropDownActiveClass } from '../codeLessUtils'; +import { INSERT_HORIZONTAL_RULE_COMMAND } from '@lexical/react/LexicalHorizontalRuleNode'; + +export function HorizontalRule({ activeEditor, toolbarState, isEditable }: ToolbarItemProps) { + const { formatMessage } = useIntl(); + const { setIsLinkEditMode } = useToolbarItemRenderDependencies(); + + const insertLink = useCallback(() => { + if (!toolbarState.isLink) { + setIsLinkEditMode(true); + activeEditor.dispatchCommand(TOGGLE_LINK_COMMAND, sanitizeUrl('https://')); + } else { + setIsLinkEditMode(false); + activeEditor.dispatchCommand(TOGGLE_LINK_COMMAND, null); + } + }, [activeEditor, setIsLinkEditMode, toolbarState.isLink]); + + return ( + { + activeEditor.dispatchCommand(INSERT_HORIZONTAL_RULE_COMMAND, undefined); + }} + className="item" + > + + + {formatMessage({ + id: 'lexical.plugin.toolbar.insert.horizontalrule.text', + defaultMessage: 'Horizontal Rule', + })} + + + ); +} diff --git a/admin/src/lexical/plugins/ToolbarPlugin/toolbarItems/image.tsx b/admin/src/lexical/plugins/ToolbarPlugin/toolbarItems/image.tsx new file mode 100644 index 0000000..659329b --- /dev/null +++ b/admin/src/lexical/plugins/ToolbarPlugin/toolbarItems/image.tsx @@ -0,0 +1,89 @@ +import { useIntl } from 'react-intl'; +import { ToolbarItemProps } from '../../../../supportedNodeTypes'; +import { SHORTCUTS } from '../../ShortcutsPlugin/shortcuts'; +import { useCallback } from 'react'; +import { TOGGLE_LINK_COMMAND } from '@lexical/link'; +import { sanitizeUrl } from '../../../utils/url'; +import { useToolbarItemRenderDependencies } from '../../../context/ToolbarItemRenderDependenciesContext'; +import { FORMAT_TEXT_COMMAND } from 'lexical'; +import { DropDownItem } from '../../../ui/DropDown'; +import { dropDownActiveClass } from '../codeLessUtils'; +import { INSERT_HORIZONTAL_RULE_COMMAND } from '@lexical/react/LexicalHorizontalRuleNode'; +import { INSERT_IMAGE_COMMAND, InsertImageDialog, InsertImagePayload } from '../../ImagesPlugin'; +import { InsertInlineImageDialog } from '../../InlineImagePlugin'; + +export function Image({ activeEditor, toolbarState, isEditable }: ToolbarItemProps) { + const { formatMessage } = useIntl(); + const { setIsLinkEditMode, showModal } = useToolbarItemRenderDependencies(); + + const insertLink = useCallback(() => { + if (!toolbarState.isLink) { + setIsLinkEditMode(true); + activeEditor.dispatchCommand(TOGGLE_LINK_COMMAND, sanitizeUrl('https://')); + } else { + setIsLinkEditMode(false); + activeEditor.dispatchCommand(TOGGLE_LINK_COMMAND, null); + } + }, [activeEditor, setIsLinkEditMode, toolbarState.isLink]); + + return ( + { + showModal( + formatMessage({ + id: 'lexical.plugin.toolbar.insert.image.modal.title', + defaultMessage: 'Insert Image', + }), + (onClose) => + ); + }} + className="item" + > + + + {formatMessage({ + id: 'lexical.plugin.toolbar.insert.image.text', + defaultMessage: 'Image', + })} + + + ); +} + +export function InlineImage({ activeEditor, toolbarState, isEditable }: ToolbarItemProps) { + const { formatMessage } = useIntl(); + const { setIsLinkEditMode, showModal } = useToolbarItemRenderDependencies(); + + const insertLink = useCallback(() => { + if (!toolbarState.isLink) { + setIsLinkEditMode(true); + activeEditor.dispatchCommand(TOGGLE_LINK_COMMAND, sanitizeUrl('https://')); + } else { + setIsLinkEditMode(false); + activeEditor.dispatchCommand(TOGGLE_LINK_COMMAND, null); + } + }, [activeEditor, setIsLinkEditMode, toolbarState.isLink]); + + return ( + { + showModal( + formatMessage({ + id: 'lexical.plugin.toolbar.insert.inlineimage.modal.title', + defaultMessage: 'Insert Inline Image', + }), + (onClose) => + ); + }} + className="item" + > + + + {formatMessage({ + id: 'lexical.plugin.toolbar.insert.inlineimage.text', + defaultMessage: 'Inline Image', + })} + + + ); +} diff --git a/admin/src/lexical/plugins/ToolbarPlugin/toolbarItems/link.tsx b/admin/src/lexical/plugins/ToolbarPlugin/toolbarItems/link.tsx new file mode 100644 index 0000000..8fca176 --- /dev/null +++ b/admin/src/lexical/plugins/ToolbarPlugin/toolbarItems/link.tsx @@ -0,0 +1,44 @@ +import { useIntl } from 'react-intl'; +import { ToolbarItemProps } from '../../../../supportedNodeTypes'; +import { SHORTCUTS } from '../../ShortcutsPlugin/shortcuts'; +import { useCallback } from 'react'; +import { TOGGLE_LINK_COMMAND } from '@lexical/link'; +import { sanitizeUrl } from '../../../utils/url'; +import { useToolbarItemRenderDependencies } from '../../../context/ToolbarItemRenderDependenciesContext'; + +export function Link({ activeEditor, toolbarState, isEditable }: ToolbarItemProps) { + const { formatMessage } = useIntl(); + const { setIsLinkEditMode } = useToolbarItemRenderDependencies(); + + const insertLink = useCallback(() => { + if (!toolbarState.isLink) { + setIsLinkEditMode(true); + activeEditor.dispatchCommand(TOGGLE_LINK_COMMAND, sanitizeUrl('https://')); + } else { + setIsLinkEditMode(false); + activeEditor.dispatchCommand(TOGGLE_LINK_COMMAND, null); + } + }, [activeEditor, setIsLinkEditMode, toolbarState.isLink]); + + return ( + + ); +} diff --git a/admin/src/lexical/plugins/ToolbarPlugin/toolbarItems/other.tsx b/admin/src/lexical/plugins/ToolbarPlugin/toolbarItems/other.tsx new file mode 100644 index 0000000..7840191 --- /dev/null +++ b/admin/src/lexical/plugins/ToolbarPlugin/toolbarItems/other.tsx @@ -0,0 +1,36 @@ +import { FORMAT_TEXT_COMMAND } from 'lexical'; +import { ToolbarItemProps } from '../../../../supportedNodeTypes'; +import { useIntl } from 'react-intl'; +import { SHORTCUTS } from '../../ShortcutsPlugin/shortcuts'; +import { DropDownItem } from '../../../ui/DropDown'; +import { dropDownActiveClass, clearFormatting } from '../codeLessUtils'; + +export function ClearFormatting({ activeEditor, toolbarState, isEditable }: ToolbarItemProps) { + const { formatMessage } = useIntl(); + + return ( + clearFormatting(activeEditor)} + className="item wide" + title={formatMessage({ + id: 'lexical.plugin.toolbar.format.clear.title', + defaultMessage: 'Clear text formatting', + })} + aria-label={formatMessage({ + id: 'lexical.plugin.toolbar.format.clear.aria', + defaultMessage: 'Clear all text formatting', + })} + > +
+ + + {formatMessage({ + id: 'lexical.plugin.toolbar.format.clear.text', + defaultMessage: 'Clear Formatting', + })} + +
+ {SHORTCUTS.CLEAR_FORMATTING} +
+ ); +} diff --git a/admin/src/lexical/plugins/ToolbarPlugin/toolbarItems/pageBreak.tsx b/admin/src/lexical/plugins/ToolbarPlugin/toolbarItems/pageBreak.tsx new file mode 100644 index 0000000..841d0d3 --- /dev/null +++ b/admin/src/lexical/plugins/ToolbarPlugin/toolbarItems/pageBreak.tsx @@ -0,0 +1,44 @@ +import { useIntl } from 'react-intl'; +import { ToolbarItemProps } from '../../../../supportedNodeTypes'; +import { SHORTCUTS } from '../../ShortcutsPlugin/shortcuts'; +import { useCallback } from 'react'; +import { TOGGLE_LINK_COMMAND } from '@lexical/link'; +import { sanitizeUrl } from '../../../utils/url'; +import { useToolbarItemRenderDependencies } from '../../../context/ToolbarItemRenderDependenciesContext'; +import { FORMAT_TEXT_COMMAND } from 'lexical'; +import { DropDownItem } from '../../../ui/DropDown'; +import { dropDownActiveClass } from '../codeLessUtils'; +import { INSERT_HORIZONTAL_RULE_COMMAND } from '@lexical/react/LexicalHorizontalRuleNode'; +import { INSERT_PAGE_BREAK } from '../../PageBreakPlugin'; + +export function PageBreak({ activeEditor, toolbarState, isEditable }: ToolbarItemProps) { + const { formatMessage } = useIntl(); + const { setIsLinkEditMode } = useToolbarItemRenderDependencies(); + + const insertLink = useCallback(() => { + if (!toolbarState.isLink) { + setIsLinkEditMode(true); + activeEditor.dispatchCommand(TOGGLE_LINK_COMMAND, sanitizeUrl('https://')); + } else { + setIsLinkEditMode(false); + activeEditor.dispatchCommand(TOGGLE_LINK_COMMAND, null); + } + }, [activeEditor, setIsLinkEditMode, toolbarState.isLink]); + + return ( + { + activeEditor.dispatchCommand(INSERT_PAGE_BREAK, undefined); + }} + className="item" + > + + + {formatMessage({ + id: 'lexical.plugin.toolbar.insert.pagebreak.text', + defaultMessage: 'Page Break', + })} + + + ); +} diff --git a/admin/src/lexical/plugins/ToolbarPlugin/toolbarItems/richText.tsx b/admin/src/lexical/plugins/ToolbarPlugin/toolbarItems/richText.tsx new file mode 100644 index 0000000..b217a15 --- /dev/null +++ b/admin/src/lexical/plugins/ToolbarPlugin/toolbarItems/richText.tsx @@ -0,0 +1,282 @@ +import { FORMAT_TEXT_COMMAND } from 'lexical'; +import { ToolbarItemProps } from '../../../../supportedNodeTypes'; +import { useIntl } from 'react-intl'; +import { SHORTCUTS } from '../../ShortcutsPlugin/shortcuts'; +import { DropDownItem } from '../../../ui/DropDown'; +import { dropDownActiveClass } from '../codeLessUtils'; + +export function Bold({ activeEditor, toolbarState, isEditable }: ToolbarItemProps) { + const { formatMessage } = useIntl(); + + return ( + + ); +} + +export function Italic({ activeEditor, toolbarState, isEditable }: ToolbarItemProps) { + const { formatMessage } = useIntl(); + + return ( + + ); +} + +export function Underline({ activeEditor, toolbarState, isEditable }: ToolbarItemProps) { + const { formatMessage } = useIntl(); + + return ( + + ); +} + +export function Lowercase({ activeEditor, toolbarState, isEditable }: ToolbarItemProps) { + const { formatMessage } = useIntl(); + + return ( + { + activeEditor.dispatchCommand(FORMAT_TEXT_COMMAND, 'lowercase'); + }} + className={'item wide ' + dropDownActiveClass(toolbarState.isLowercase)} + title={formatMessage({ + id: 'lexical.plugin.toolbar.format.lowercase.title', + defaultMessage: 'Lowercase', + })} + aria-label={formatMessage({ + id: 'lexical.plugin.toolbar.format.lowercase.aria', + defaultMessage: 'Format text to lowercase', + })} + > +
+ + + {formatMessage({ + id: 'lexical.plugin.toolbar.format.lowercase.text', + defaultMessage: 'Lowercase', + })} + +
+ {SHORTCUTS.LOWERCASE} +
+ ); +} + +export function Uppercase({ activeEditor, toolbarState, isEditable }: ToolbarItemProps) { + const { formatMessage } = useIntl(); + + return ( + { + activeEditor.dispatchCommand(FORMAT_TEXT_COMMAND, 'uppercase'); + }} + className={'item wide ' + dropDownActiveClass(toolbarState.isUppercase)} + title={formatMessage({ + id: 'lexical.plugin.toolbar.format.uppercase.title', + defaultMessage: 'Uppercase', + })} + aria-label={formatMessage({ + id: 'lexical.plugin.toolbar.format.uppercase.aria', + defaultMessage: 'Format text to uppercase', + })} + > +
+ + + {formatMessage({ + id: 'lexical.plugin.toolbar.format.uppercase.text', + defaultMessage: 'Uppercase', + })} + +
+ {SHORTCUTS.UPPERCASE} +
+ ); +} + +export function Capitalize({ activeEditor, toolbarState, isEditable }: ToolbarItemProps) { + const { formatMessage } = useIntl(); + + return ( + { + activeEditor.dispatchCommand(FORMAT_TEXT_COMMAND, 'capitalize'); + }} + className={'item wide ' + dropDownActiveClass(toolbarState.isCapitalize)} + title={formatMessage({ + id: 'lexical.plugin.toolbar.format.capitalize.title', + defaultMessage: 'Capitalize', + })} + aria-label={formatMessage({ + id: 'lexical.plugin.toolbar.format.capitalize.aria', + defaultMessage: 'Format text to capitalize', + })} + > +
+ + + {formatMessage({ + id: 'lexical.plugin.toolbar.format.capitalize.text', + defaultMessage: 'Capitalize', + })} + +
+ {SHORTCUTS.CAPITALIZE} +
+ ); +} + +export function Strikethrough({ activeEditor, toolbarState, isEditable }: ToolbarItemProps) { + const { formatMessage } = useIntl(); + + return ( + { + activeEditor.dispatchCommand(FORMAT_TEXT_COMMAND, 'strikethrough'); + }} + className={'item wide ' + dropDownActiveClass(toolbarState.isStrikethrough)} + title={formatMessage({ + id: 'lexical.plugin.toolbar.format.strikethrough.title', + defaultMessage: 'Strikethrough', + })} + aria-label={formatMessage({ + id: 'lexical.plugin.toolbar.format.strikethrough.aria', + defaultMessage: 'Format text with a strikethrough', + })} + > +
+ + + {formatMessage({ + id: 'lexical.plugin.toolbar.format.strikethrough.text', + defaultMessage: 'Strikethrough', + })} + +
+ {SHORTCUTS.STRIKETHROUGH} +
+ ); +} + +export function Subscript({ activeEditor, toolbarState, isEditable }: ToolbarItemProps) { + const { formatMessage } = useIntl(); + + return ( + { + activeEditor.dispatchCommand(FORMAT_TEXT_COMMAND, 'subscript'); + }} + className={'item wide ' + dropDownActiveClass(toolbarState.isSubscript)} + title={formatMessage({ + id: 'lexical.plugin.toolbar.format.subscript.title', + defaultMessage: 'Subscript', + })} + aria-label={formatMessage({ + id: 'lexical.plugin.toolbar.format.subscript.aria', + defaultMessage: 'Format text with a subscript', + })} + > +
+ + + {formatMessage({ + id: 'lexical.plugin.toolbar.format.subscript.text', + defaultMessage: 'Subscript', + })} + +
+ {SHORTCUTS.SUBSCRIPT} +
+ ); +} + +export function Superscript({ activeEditor, toolbarState, isEditable }: ToolbarItemProps) { + const { formatMessage } = useIntl(); + + return ( + { + activeEditor.dispatchCommand(FORMAT_TEXT_COMMAND, 'superscript'); + }} + className={'item wide ' + dropDownActiveClass(toolbarState.isSuperscript)} + title={formatMessage({ + id: 'lexical.plugin.toolbar.format.superscript.title', + defaultMessage: 'Superscript', + })} + aria-label={formatMessage({ + id: 'lexical.plugin.toolbar.format.superscript.aria', + defaultMessage: 'Format text with a superscript', + })} + > +
+ + + {formatMessage({ + id: 'lexical.plugin.toolbar.format.superscript.text', + defaultMessage: 'Superscript', + })} + +
+ {SHORTCUTS.SUPERSCRIPT} +
+ ); +} diff --git a/admin/src/lexical/plugins/ToolbarPlugin/toolbarItems/strapiImage.tsx b/admin/src/lexical/plugins/ToolbarPlugin/toolbarItems/strapiImage.tsx new file mode 100644 index 0000000..9e8a5a6 --- /dev/null +++ b/admin/src/lexical/plugins/ToolbarPlugin/toolbarItems/strapiImage.tsx @@ -0,0 +1,26 @@ +import { ToolbarItemProps } from '../../../../supportedNodeTypes'; +import { useIntl } from 'react-intl'; +import { useToolbarItemRenderDependencies } from '../../../context/ToolbarItemRenderDependenciesContext'; + +export function StrapiImage(_props: ToolbarItemProps) { + const { formatMessage } = useIntl(); + const { setIsStrapiImageDialogOpen } = useToolbarItemRenderDependencies(); + + return ( + + ); +} diff --git a/admin/src/lexical/plugins/ToolbarPlugin/toolbarItems/table.tsx b/admin/src/lexical/plugins/ToolbarPlugin/toolbarItems/table.tsx new file mode 100644 index 0000000..92c417d --- /dev/null +++ b/admin/src/lexical/plugins/ToolbarPlugin/toolbarItems/table.tsx @@ -0,0 +1,42 @@ +import { useIntl } from 'react-intl'; +import { ToolbarItemProps } from '../../../../supportedNodeTypes'; +import { SHORTCUTS } from '../../ShortcutsPlugin/shortcuts'; +import { useCallback } from 'react'; +import { TOGGLE_LINK_COMMAND } from '@lexical/link'; +import { sanitizeUrl } from '../../../utils/url'; +import { useToolbarItemRenderDependencies } from '../../../context/ToolbarItemRenderDependenciesContext'; +import { FORMAT_TEXT_COMMAND } from 'lexical'; +import { DropDownItem } from '../../../ui/DropDown'; +import { dropDownActiveClass } from '../codeLessUtils'; +import { INSERT_HORIZONTAL_RULE_COMMAND } from '@lexical/react/LexicalHorizontalRuleNode'; +import { INSERT_IMAGE_COMMAND, InsertImageDialog, InsertImagePayload } from '../../ImagesPlugin'; +import { InsertInlineImageDialog } from '../../InlineImagePlugin'; +import { InsertTableDialog } from '../../TablePlugin'; + +export function Table({ activeEditor, toolbarState, isEditable }: ToolbarItemProps) { + const { formatMessage } = useIntl(); + const { setIsLinkEditMode, showModal } = useToolbarItemRenderDependencies(); + + return ( + { + showModal( + formatMessage({ + id: 'lexical.plugin.toolbar.insert.table.modal.title', + defaultMessage: 'Insert Table', + }), + (onClose) => + ); + }} + className="item" + > + + + {formatMessage({ + id: 'lexical.plugin.toolbar.insert.table.text', + defaultMessage: 'Table', + })} + + + ); +} diff --git a/admin/src/lexical/plugins/ToolbarPlugin/utils.ts b/admin/src/lexical/plugins/ToolbarPlugin/utils.ts index 3dd683a..b8b4fd1 100644 --- a/admin/src/lexical/plugins/ToolbarPlugin/utils.ts +++ b/admin/src/lexical/plugins/ToolbarPlugin/utils.ts @@ -11,24 +11,9 @@ import { INSERT_ORDERED_LIST_COMMAND, INSERT_UNORDERED_LIST_COMMAND, } from '@lexical/list'; -import { $isDecoratorBlockNode } from '@lexical/react/LexicalDecoratorBlockNode'; -import { - $createHeadingNode, - $createQuoteNode, - $isHeadingNode, - $isQuoteNode, - HeadingTagType, -} from '@lexical/rich-text'; +import { $createHeadingNode, $createQuoteNode, HeadingTagType } from '@lexical/rich-text'; import { $patchStyleText, $setBlocksType } from '@lexical/selection'; -import { $isTableSelection } from '@lexical/table'; -import { $getNearestBlockElementAncestorOrThrow } from '@lexical/utils'; -import { - $createParagraphNode, - $getSelection, - $isRangeSelection, - $isTextNode, - LexicalEditor, -} from 'lexical'; +import { $createParagraphNode, $getSelection, $isRangeSelection, LexicalEditor } from 'lexical'; import { DEFAULT_FONT_SIZE, @@ -228,59 +213,3 @@ export const formatCode = (editor: LexicalEditor, blockType: string) => { }); } }; - -export const clearFormatting = (editor: LexicalEditor) => { - editor.update(() => { - const selection = $getSelection(); - if ($isRangeSelection(selection) || $isTableSelection(selection)) { - const anchor = selection.anchor; - const focus = selection.focus; - const nodes = selection.getNodes(); - const extractedNodes = selection.extract(); - - if (anchor.key === focus.key && anchor.offset === focus.offset) { - return; - } - - nodes.forEach((node, idx) => { - // We split the first and last node by the selection - // So that we don't format unselected text inside those nodes - if ($isTextNode(node)) { - // Use a separate variable to ensure TS does not lose the refinement - let textNode = node; - if (idx === 0 && anchor.offset !== 0) { - textNode = textNode.splitText(anchor.offset)[1] || textNode; - } - if (idx === nodes.length - 1) { - textNode = textNode.splitText(focus.offset)[0] || textNode; - } - /** - * If the selected text has one format applied - * selecting a portion of the text, could - * clear the format to the wrong portion of the text. - * - * The cleared text is based on the length of the selected text. - */ - // We need this in case the selected text only has one format - const extractedTextNode = extractedNodes[0]; - if (nodes.length === 1 && $isTextNode(extractedTextNode)) { - textNode = extractedTextNode; - } - - if (textNode.__style !== '') { - textNode.setStyle(''); - } - if (textNode.__format !== 0) { - textNode.setFormat(0); - $getNearestBlockElementAncestorOrThrow(textNode).setFormat(''); - } - node = textNode; - } else if ($isHeadingNode(node) || $isQuoteNode(node)) { - node.replace($createParagraphNode(), true); - } else if ($isDecoratorBlockNode(node)) { - node.setFormat(''); - } - }); - } - }); -}; diff --git a/admin/src/supportedNodeTypes.tsx b/admin/src/supportedNodeTypes.tsx new file mode 100644 index 0000000..3f4855f --- /dev/null +++ b/admin/src/supportedNodeTypes.tsx @@ -0,0 +1,311 @@ +import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin'; +import EmojiPickerPlugin from './lexical/plugins/EmojiPickerPlugin'; +import { LexicalEditor } from 'lexical'; +import { ToolbarState } from './lexical/context/ToolbarContext'; +import { + Bold, + Italic, + Underline, + Lowercase, + Uppercase, + Capitalize, + Strikethrough, + Subscript, + Superscript, +} from './lexical/plugins/ToolbarPlugin/toolbarItems/richText'; +import { ClearFormatting } from './lexical/plugins/ToolbarPlugin/toolbarItems/other'; +import { Code } from './lexical/plugins/ToolbarPlugin/toolbarItems/code'; +import LinkPlugin from './lexical/plugins/LinkPlugin'; +import { Link } from './lexical/plugins/ToolbarPlugin/toolbarItems/link'; +import { Equation } from './lexical/plugins/ToolbarPlugin/toolbarItems/equation'; +import { Table } from './lexical/plugins/ToolbarPlugin/toolbarItems/table'; +import { Collapsible } from './lexical/plugins/ToolbarPlugin/toolbarItems/collapsible'; +import { Columns } from './lexical/plugins/ToolbarPlugin/toolbarItems/columns'; +import { Image, InlineImage } from './lexical/plugins/ToolbarPlugin/toolbarItems/image'; +import StrapiImagePlugin from './lexical/plugins/StrapiImagePlugin'; +import { StrapiImage } from './lexical/plugins/ToolbarPlugin/toolbarItems/strapiImage'; +import { HorizontalRule } from './lexical/plugins/ToolbarPlugin/toolbarItems/horizontalRule'; +import { PageBreak } from './lexical/plugins/ToolbarPlugin/toolbarItems/pageBreak'; +import ContentEditable from './lexical/ui/ContentEditable'; +import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary'; +import { LexicalEditorProps } from './lexical/Editor'; +import { StrapiFieldConfig, useStrapiFieldContext } from './lexical/context/StrapiFieldContext'; + +/** + * This is a "hack type" until we have figured out a shared base type for Lexical plugins + */ +type LexicalPlugin = (props: T) => JSX.Element | null; + +type RenderPluginProps = { + lexicalEditorProps: LexicalEditorProps; + onRef: (floatingAnchorElem: HTMLDivElement) => void; + placeholder: string; +}; + +export type ToolbarItemProps = { + activeEditor: LexicalEditor; + toolbarState: ToolbarState; + isEditable: boolean; +}; + +type SupportedNodeType = { + id: string; + defaultLabel: string; + defaultDescription: string; + lexicalPlugin?: LexicalPlugin; + renderToolbarItem?: React.FunctionComponent; +}; + +function RichTextLexicalPlugin({ lexicalEditorProps, onRef, placeholder }: RenderPluginProps) { + return ( + +
+ +
+ + } + ErrorBoundary={LexicalErrorBoundary} + /> + ); +} + +const bold: SupportedNodeType = { + id: 'bold', + defaultLabel: 'Bold', + defaultDescription: 'Enable bold text', + lexicalPlugin: RichTextLexicalPlugin, + renderToolbarItem: Bold, +}; + +const italic: SupportedNodeType = { + id: 'italic', + defaultLabel: 'Italic', + defaultDescription: 'Enable italic text', + lexicalPlugin: RichTextLexicalPlugin, + renderToolbarItem: Italic, +}; + +const underline: SupportedNodeType = { + id: 'underline', + defaultLabel: 'Underline', + defaultDescription: 'Enable underlining of text', + lexicalPlugin: RichTextLexicalPlugin, + renderToolbarItem: Underline, +}; + +const emojiPicker: SupportedNodeType = { + id: 'emojiPicker', + defaultLabel: 'Emoji Picker', + defaultDescription: 'Enable emoji picker', + lexicalPlugin: () => , +}; + +const inlineCode: SupportedNodeType = { + id: 'inlineCode', + defaultLabel: 'Inline Code', + defaultDescription: 'Enable inline monospace-code', + // TODO: really? + lexicalPlugin: RichTextLexicalPlugin, + renderToolbarItem: Code, +}; + +const link: SupportedNodeType = { + id: 'link', + defaultLabel: 'Links', + defaultDescription: 'Enable links to Strapi-internal and external targets', + lexicalPlugin: () => , + renderToolbarItem: Link, +}; + +const strapiImage: SupportedNodeType = { + id: 'strapiImage', + defaultLabel: 'Strapi Images', + defaultDescription: "Enable embedding images from Strapi's media gallery", + lexicalPlugin: StrapiImagePlugin, + renderToolbarItem: StrapiImage, +}; + +const lowercase: SupportedNodeType = { + id: 'lowercase', + defaultLabel: 'Lowercase', + defaultDescription: 'Enable inline lowercase', + lexicalPlugin: RichTextLexicalPlugin, + renderToolbarItem: Lowercase, +}; + +const uppercase: SupportedNodeType = { + id: 'uppercase', + defaultLabel: 'Uppercase', + defaultDescription: 'Enable inline uppercase', + lexicalPlugin: RichTextLexicalPlugin, + renderToolbarItem: Uppercase, +}; + +const capitalize: SupportedNodeType = { + id: 'capitalize', + defaultLabel: 'Capitalize', + defaultDescription: 'Enable inline capitalize', + lexicalPlugin: RichTextLexicalPlugin, + renderToolbarItem: Capitalize, +}; + +const strikethrough: SupportedNodeType = { + id: 'strikethrough', + defaultLabel: 'Strikethrough', + defaultDescription: 'Enable inline strikethrough', + lexicalPlugin: RichTextLexicalPlugin, + renderToolbarItem: Strikethrough, +}; + +const subscript: SupportedNodeType = { + id: 'subscript', + defaultLabel: 'Subscript', + defaultDescription: 'Enable inline subscript', + lexicalPlugin: RichTextLexicalPlugin, + renderToolbarItem: Subscript, +}; + +const superscript: SupportedNodeType = { + id: 'superscript', + defaultLabel: 'Superscript', + defaultDescription: 'Enable inline superscript', + lexicalPlugin: RichTextLexicalPlugin, + renderToolbarItem: Superscript, +}; + +const clearFormatting: SupportedNodeType = { + id: 'clearFormatting', + defaultLabel: 'Clear Formatting', + defaultDescription: 'Enable button to clear formatting', + renderToolbarItem: ClearFormatting, +}; + +const horizontalRule: SupportedNodeType = { + id: 'horizontalRule', + defaultLabel: 'Horizontal Rule', + defaultDescription: 'Enable horizontal rule', + renderToolbarItem: HorizontalRule, +}; + +const pageBreak: SupportedNodeType = { + id: 'pageBreak', + defaultLabel: 'Page Break', + defaultDescription: 'Enable page break', + renderToolbarItem: PageBreak, +}; + +const image: SupportedNodeType = { + id: 'image', + defaultLabel: 'Image', + defaultDescription: 'Enable image', + renderToolbarItem: Image, +}; + +const inlineImage: SupportedNodeType = { + id: 'inlineImage', + defaultLabel: 'Inline Image', + defaultDescription: 'Enable inline image', + renderToolbarItem: InlineImage, +}; + +const table: SupportedNodeType = { + id: 'table', + defaultLabel: 'Table', + defaultDescription: 'Enable table', + renderToolbarItem: Table, +}; + +const columns: SupportedNodeType = { + id: 'columns', + defaultLabel: 'Columns', + defaultDescription: 'Enable columns', + renderToolbarItem: Columns, +}; + +const equation: SupportedNodeType = { + id: 'equation', + defaultLabel: 'Equation', + defaultDescription: 'Enable equation', + renderToolbarItem: Equation, +}; + +const collapsible: SupportedNodeType = { + id: 'collapsible', + defaultLabel: 'Collapsible', + defaultDescription: 'Enable collapsible', + renderToolbarItem: Collapsible, +}; + +export { + bold, + italic, + underline, + inlineCode, + emojiPicker, + lowercase, + uppercase, + capitalize, + strikethrough, + subscript, + superscript, + clearFormatting, + link, + strapiImage, + horizontalRule, + pageBreak, + image, + inlineImage, + table, + columns, +}; + +export const supportedNodeTypes = { + bold, + italic, + underline, + inlineCode, + emojiPicker, + lowercase, + uppercase, + capitalize, + strikethrough, + subscript, + superscript, + clearFormatting, + link, + strapiImage, + horizontalRule, + pageBreak, + image, + inlineImage, + table, + columns, + equation, + collapsible, +}; + +function getUniquePlugins(strapiFieldConfig: StrapiFieldConfig) { + const plugins = new Set>(); + + for (const [nodeTypeId, enabled] of Object.entries(strapiFieldConfig.enabledNodeTypes)) { + if (!enabled) { + continue; + } + const nodeType = supportedNodeTypes[nodeTypeId as keyof typeof supportedNodeTypes]; + + if (nodeType.lexicalPlugin) { + plugins.add(nodeType.lexicalPlugin); + } + } + + return Array.from(plugins); +} + +export function SupportedNodeTypePlugins(props: RenderPluginProps) { + const strapiFieldConfig = useStrapiFieldContext(); + const plugins = getUniquePlugins(strapiFieldConfig); + + return <>{...plugins.map((Plugin, i) => )}; +} From fe69a508adf3aecbb14588968a49459a3f62890f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benedikt=20R=C3=B6tsch?= Date: Thu, 8 May 2025 10:26:35 +0200 Subject: [PATCH 02/13] Update admin/src/lexical/context/StrapiFieldContext.tsx Co-authored-by: qodo-merge-pro[bot] <151058649+qodo-merge-pro[bot]@users.noreply.github.com> --- admin/src/lexical/context/StrapiFieldContext.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/admin/src/lexical/context/StrapiFieldContext.tsx b/admin/src/lexical/context/StrapiFieldContext.tsx index 15aecba..d27fc72 100644 --- a/admin/src/lexical/context/StrapiFieldContext.tsx +++ b/admin/src/lexical/context/StrapiFieldContext.tsx @@ -26,7 +26,7 @@ export const useStrapiFieldContext = () => { const context = useContext(StrapiFieldConfigContext); if (context === undefined) { - throw new Error('useStrapiFieldContext must be used within a ToolbarProvider'); + throw new Error('useStrapiFieldContext must be used within a StrapiFieldConfigProvider'); } return context; From 0c5c2c40aea1c9daf2508ea46e193175c06dd526 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benedikt=20R=C3=B6tsch?= Date: Thu, 8 May 2025 10:26:42 +0200 Subject: [PATCH 03/13] Update admin/src/lexical/plugins/ToolbarPlugin/toolbarItems/horizontalRule.tsx Co-authored-by: qodo-merge-pro[bot] <151058649+qodo-merge-pro[bot]@users.noreply.github.com> --- .../plugins/ToolbarPlugin/toolbarItems/horizontalRule.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/admin/src/lexical/plugins/ToolbarPlugin/toolbarItems/horizontalRule.tsx b/admin/src/lexical/plugins/ToolbarPlugin/toolbarItems/horizontalRule.tsx index 108028c..a0b34e7 100644 --- a/admin/src/lexical/plugins/ToolbarPlugin/toolbarItems/horizontalRule.tsx +++ b/admin/src/lexical/plugins/ToolbarPlugin/toolbarItems/horizontalRule.tsx @@ -31,7 +31,7 @@ export function HorizontalRule({ activeEditor, toolbarState, isEditable }: Toolb }} className="item" > - + {formatMessage({ id: 'lexical.plugin.toolbar.insert.horizontalrule.text', From e8422290bb08f71762dfe3756f0c9a58becfd084 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benedikt=20R=C3=B6tsch?= Date: Fri, 9 May 2025 15:19:30 +0200 Subject: [PATCH 04/13] fix: tree view is not shown or hidden based on field config --- admin/src/lexical/Editor.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/admin/src/lexical/Editor.tsx b/admin/src/lexical/Editor.tsx index 9bb7eb2..c8766fe 100755 --- a/admin/src/lexical/Editor.tsx +++ b/admin/src/lexical/Editor.tsx @@ -75,6 +75,7 @@ import { useIntl } from 'react-intl'; import StrapiImagePlugin from './plugins/StrapiImagePlugin'; import './styles.css'; import { SupportedNodeTypePlugins } from '../supportedNodeTypes'; +import { useStrapiFieldContext } from './context/StrapiFieldContext'; export interface LexicalEditorProps { onChange: (newValue: SerializedEditorState) => void; @@ -92,6 +93,7 @@ type RenderPluginProps = { export default function Editor(props: LexicalEditorProps): JSX.Element { const { formatMessage } = useIntl(); const { historyState } = useSharedHistoryContext(); + const strapiFieldConfig = useStrapiFieldContext(); const isCollab = false; const isAutocomplete = false; @@ -100,7 +102,7 @@ export default function Editor(props: LexicalEditorProps): JSX.Element { const hasLinkAttributes = false; const isCharLimitUtf8 = false; const isRichText = true; - const showTreeView = false; + const showTreeView = strapiFieldConfig?.developers?.treeView; const showTableOfContents = false; const shouldUseLexicalContextMenu = false; const shouldPreserveNewLinesInMarkdown = false; From 08d318c3175e9c6923dc736326cc3480ae1a5049 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benedikt=20R=C3=B6tsch?= Date: Fri, 9 May 2025 16:00:24 +0200 Subject: [PATCH 05/13] feat: fully typed our context, enabled default values which are used for default field config and to fill outdated context values and reorganised font family and size selection --- admin/src/index.ts | 102 ++++++++++ .../lexical/context/StrapiFieldContext.tsx | 47 ++++- .../lexical/plugins/ToolbarPlugin/index.tsx | 181 ++++++++---------- .../ToolbarPlugin/toolbarItems/font.tsx | 79 ++++++++ .../{ => toolbarItems}/fontSize.css | 11 +- .../{ => toolbarItems}/fontSize.tsx | 6 +- admin/src/lexical/styles.css | 2 +- admin/src/lexical/ui/DropDown.tsx | 11 +- admin/src/supportedNodeTypes.tsx | 23 +++ 9 files changed, 351 insertions(+), 111 deletions(-) create mode 100644 admin/src/lexical/plugins/ToolbarPlugin/toolbarItems/font.tsx rename admin/src/lexical/plugins/ToolbarPlugin/{ => toolbarItems}/fontSize.css (82%) rename admin/src/lexical/plugins/ToolbarPlugin/{ => toolbarItems}/fontSize.tsx (97%) diff --git a/admin/src/index.ts b/admin/src/index.ts index f213b50..d78dbda 100755 --- a/admin/src/index.ts +++ b/admin/src/index.ts @@ -4,6 +4,7 @@ import { LexicalIcon } from './components/LexicalIcon'; import { PLUGIN_ID } from './pluginId'; import { supportedNodeTypes } from './supportedNodeTypes'; +import { defaultStrapiFieldConfig } from './lexical/context/StrapiFieldContext'; export default { register(app: StrapiApp) { @@ -52,8 +53,103 @@ export default { // Current strapi types do not reflect the possibility // to store custom configuration names, but code does. name: `options.enabledNodeTypes.${supportedNodeType.id}` as any, + defaultValue: supportedNodeType.enabledByDefault, })), }, + { + sectionTitle: { + id: 'lexical.fontSize.section.feature', + defaultMessage: 'Font size feature', + }, + items: [ + { + intlLabel: { + id: `lexical.fontSize.enabled.label`, + defaultMessage: 'Enable font size feature', + }, + type: 'checkbox', + defaultValue: defaultStrapiFieldConfig.fontSize.enabled, + description: { + id: `lexical.fontSize.enabled.description`, + defaultMessage: 'Enable users to change font size', + }, + name: `options.fontSize.enabled`, + }, + { + intlLabel: { + id: `lexical.fontSize.default.label`, + defaultMessage: 'Default font size', + }, + type: 'number', + defaultValue: defaultStrapiFieldConfig.fontSize.default, + description: { + id: `lexical.fontSize.default.description`, + defaultMessage: 'Default font size size of your editor', + }, + name: `options.fontSize.default`, + }, + { + intlLabel: { + id: `lexical.fontSize.minimum.label`, + defaultMessage: 'Minimum font size', + }, + type: 'number', + defaultValue: defaultStrapiFieldConfig.fontSize.minimum, + description: { + id: `lexical.fontSize.minimum.description`, + defaultMessage: 'Minimum font size size of your editor', + }, + name: `options.fontSize.minimum`, + }, + { + intlLabel: { + id: `lexical.fontSize.maximum.label`, + defaultMessage: 'Maximum font size', + }, + type: 'number', + defaultValue: defaultStrapiFieldConfig.fontSize.maximum, + description: { + id: `lexical.fontSize.maximum.description`, + defaultMessage: 'Maximum font size size of your editor', + }, + name: `options.fontSize.maximum`, + }, + ], + }, + { + sectionTitle: { + id: 'lexical.fontFamily.section.feature', + defaultMessage: 'Font family feature', + }, + items: [ + { + intlLabel: { + id: `lexical.fontFamily.enabled.label`, + defaultMessage: 'Enable font family feature', + }, + type: 'checkbox', + defaultValue: defaultStrapiFieldConfig.fontFamily.enabled, + description: { + id: `lexical.fontFamily.enabled.description`, + defaultMessage: 'Enable users to change font family', + }, + name: `options.fontFamily.enabled`, + }, + { + intlLabel: { + id: `lexical.fontFamily.families.label`, + defaultMessage: 'Enabled font families', + }, + type: 'text', + defaultValue: defaultStrapiFieldConfig.fontFamily.families, + description: { + id: `lexical.fontFamily.default.description`, + defaultMessage: 'Enabled font families. Separated by semi-colon (;)', + }, + name: `options.fontFamily.families`, + }, + ], + }, { sectionTitle: { id: 'lexical.actions.section.enabled', @@ -71,6 +167,7 @@ export default { defaultMessage: 'Add buttons to undo/redo within the current editing session', }, name: `options.enabledActions.sessionHistory`, + defaultValue: defaultStrapiFieldConfig.enabledActions.sessionHistory, }, { intlLabel: { @@ -83,6 +180,7 @@ export default { defaultMessage: 'Add button to clear all text within the current editor', }, name: `options.enabledActions.clear`, + defaultValue: defaultStrapiFieldConfig.enabledActions.clear, }, { intlLabel: { @@ -95,6 +193,7 @@ export default { defaultMessage: 'Add button to export editor text in Markdown format', }, name: `options.enabledActions.exportAsMarkdown`, + defaultValue: defaultStrapiFieldConfig.enabledActions.exportAsMarkdown, }, { intlLabel: { @@ -107,6 +206,7 @@ export default { defaultMessage: 'Add button to import existing Lexical-formatted text', }, name: `options.enabledActions.import`, + defaultValue: defaultStrapiFieldConfig.enabledActions.import, }, { intlLabel: { @@ -119,6 +219,7 @@ export default { defaultMessage: 'Add button to export text in Lexical format', }, name: `options.enabledActions.export`, + defaultValue: defaultStrapiFieldConfig.enabledActions.export, }, ], }, @@ -139,6 +240,7 @@ export default { defaultMessage: 'Add button to show internal Lexical tree', }, name: `options.developers.treeView`, + defaultValue: defaultStrapiFieldConfig.developers.treeView, }, ], }, diff --git a/admin/src/lexical/context/StrapiFieldContext.tsx b/admin/src/lexical/context/StrapiFieldContext.tsx index d27fc72..1905fa1 100644 --- a/admin/src/lexical/context/StrapiFieldContext.tsx +++ b/admin/src/lexical/context/StrapiFieldContext.tsx @@ -3,15 +3,34 @@ import { createContext, useContext } from 'react'; export type StrapiFieldConfig = { enabledNodeTypes: EnabledNodeTypes; enabledActions: EnabledActions; + fontSize: FontSize; + fontFamily: FontFamily; developers: DeveloperOptions; }; export type EnabledNodeTypes = { bold: boolean; + [key: string]: boolean; }; type EnabledActions = { + sessionHistory: boolean; + clear: boolean; exportAsMarkdown: boolean; + import: boolean; + export: boolean; +}; + +export type FontSize = { + enabled: boolean; + default: number; + minimum: number; + maximum: number; +}; + +export type FontFamily = { + enabled: boolean; + families: string; }; type DeveloperOptions = { @@ -22,6 +41,32 @@ const StrapiFieldConfigContext = createContext(un export const StrapiFieldConfigProvider = StrapiFieldConfigContext.Provider; +export const defaultStrapiFieldConfig: StrapiFieldConfig = { + enabledNodeTypes: { + bold: true, + }, + enabledActions: { + sessionHistory: true, + clear: false, + exportAsMarkdown: false, + import: false, + export: false, + }, + fontSize: { + enabled: false, + default: 16, + minimum: 12, + maximum: 48, + }, + fontFamily: { + enabled: false, + families: 'Arial;Courier New;Georgia;Times New Roman;Trebuchet MS;Verdana', + }, + developers: { + treeView: false, + }, +}; + export const useStrapiFieldContext = () => { const context = useContext(StrapiFieldConfigContext); @@ -29,5 +74,5 @@ export const useStrapiFieldContext = () => { throw new Error('useStrapiFieldContext must be used within a StrapiFieldConfigProvider'); } - return context; + return { ...defaultStrapiFieldConfig, ...context }; }; diff --git a/admin/src/lexical/plugins/ToolbarPlugin/index.tsx b/admin/src/lexical/plugins/ToolbarPlugin/index.tsx index d880d6c..23995c3 100755 --- a/admin/src/lexical/plugins/ToolbarPlugin/index.tsx +++ b/admin/src/lexical/plugins/ToolbarPlugin/index.tsx @@ -91,6 +91,8 @@ import { import { EnabledNodeTypes, useStrapiFieldContext } from '../../context/StrapiFieldContext'; import { supportedNodeTypes } from '../../../supportedNodeTypes'; import { dropDownActiveClass, clearFormatting } from './codeLessUtils'; +import FontSize from './toolbarItems/fontSize'; +import { FontDropDown } from './toolbarItems/font'; const rootTypeToRootName = { root: 'Root', @@ -109,29 +111,6 @@ function getCodeLanguageOptions(): [string, string][] { const CODE_LANGUAGE_OPTIONS = getCodeLanguageOptions(); -const FONT_FAMILY_OPTIONS: [string, string][] = [ - ['Arial', 'Arial'], - ['Courier New', 'Courier New'], - ['Georgia', 'Georgia'], - ['Times New Roman', 'Times New Roman'], - ['Trebuchet MS', 'Trebuchet MS'], - ['Verdana', 'Verdana'], -]; - -const FONT_SIZE_OPTIONS: [string, string][] = [ - ['10px', '10px'], - ['11px', '11px'], - ['12px', '12px'], - ['13px', '13px'], - ['14px', '14px'], - ['15px', '15px'], - ['16px', '16px'], - ['17px', '17px'], - ['18px', '18px'], - ['19px', '19px'], - ['20px', '20px'], -]; - const ELEMENT_FORMAT_OPTIONS: { [key in Exclude]: { icon: string; @@ -342,65 +321,6 @@ function Divider(): JSX.Element { return
; } -// @todo: extract to external file -function FontDropDown({ - editor, - value, - style, - disabled = false, -}: { - editor: LexicalEditor; - value: string; - style: string; - disabled?: boolean; -}): JSX.Element { - const { formatMessage } = useIntl(); - - const handleClick = useCallback( - (option: string) => { - editor.update(() => { - const selection = $getSelection(); - if (selection !== null) { - $patchStyleText(selection, { - [style]: option, - }); - } - }); - }, - [editor, style] - ); - - const buttonAriaLabel = formatMessage( - { - id: `lexical.plugin.toolbar.font.button.title`, - defaultMessage: 'Formatting options for font {property}', - }, - { property: style === 'font-family' ? 'family' : 'style' } - ); - - return ( - - {(style === 'font-family' ? FONT_FAMILY_OPTIONS : FONT_SIZE_OPTIONS).map(([option, text]) => ( - handleClick(option)} - key={option} - > - {text} - - ))} - - ); -} - // @todo: extract to external file function ElementFormatDropdown({ editor, @@ -613,8 +533,41 @@ function ToolbarItem(props: { id: string }): React.ReactNode { ); } - // @todo what about the floating actions? - // @todo the floating toolbar also needs to adjust itself! + if (props.id === 'fontFamily.enabled' && strapiFieldConfig.fontFamily.enabled) { + return ( + + ); + } + + if (props.id === 'fontSize.enabled' && strapiFieldConfig.fontSize.enabled) { + return ( + <> + strapiFieldConfig.fontSize.minimum + i + ).map((size) => `${size}px`)} + /> + + + ); + } return undefined; } @@ -709,6 +662,7 @@ export default function ToolbarPlugin({ setIsLinkEditMode: Dispatch; }): JSX.Element { const { formatMessage } = useIntl(); + const strapiFieldConfig = useStrapiFieldContext(); const [selectedElementKey, setSelectedElementKey] = useState(null); const [modal, showModal] = useModal(); @@ -959,6 +913,49 @@ export default function ToolbarPlugin({ {/* TODO: how to support a nested "additional options" */} + + + + + + + {/* {strapiFieldConfig?.fontFamily?.enabled && ( + + + + )} + {strapiFieldConfig?.fontSize?.enabled && ( + + strapiFieldConfig.fontSize.minimum + i + ).map(size => `${size}px`)} + /> + + )} + {strapiFieldConfig?.fontSize?.enabled && ( + + + + )} */} @@ -1060,22 +1057,6 @@ export default function ToolbarPlugin({ ) : ( <> - {/* - - - */} - - - {/* @todo */} { + editor.update(() => { + const selection = $getSelection(); + if (selection !== null) { + $patchStyleText(selection, { + [style]: option, + }); + } + }); + }, + [editor, style] + ); + + const buttonAriaLabel = formatMessage( + { + id: `lexical.plugin.toolbar.font.button.title`, + defaultMessage: 'Formatting options for font {property}', + }, + { property: style === 'font-family' ? 'family' : 'size' } + ); + + return ( + + {options.map((option) => ( + handleClick(option)} + key={option} + style={{ fontFamily: style === 'font-family' ? option : 'inherit' }} + > + {option} + + ))} + + ); +} diff --git a/admin/src/lexical/plugins/ToolbarPlugin/fontSize.css b/admin/src/lexical/plugins/ToolbarPlugin/toolbarItems/fontSize.css similarity index 82% rename from admin/src/lexical/plugins/ToolbarPlugin/fontSize.css rename to admin/src/lexical/plugins/ToolbarPlugin/toolbarItems/fontSize.css index 23930b7..afd6d3d 100644 --- a/admin/src/lexical/plugins/ToolbarPlugin/fontSize.css +++ b/admin/src/lexical/plugins/ToolbarPlugin/toolbarItems/fontSize.css @@ -11,11 +11,12 @@ font-size: 14px; color: #777; border-radius: 5px; - border-color: grey; - height: 15px; + border: 1px solid #ddd; padding: 2px 4px; text-align: center; - width: 20px; + width: 30px; + height: auto; + margin: 0 3px; align-self: center; } @@ -35,13 +36,13 @@ input[type='number'] { } .add-icon { - background-image: url(../../images/icons/add-sign.svg); + background-image: url(../../../images/icons/add-sign.svg); background-repeat: no-repeat; background-position: center; } .minus-icon { - background-image: url(../../images/icons/minus-sign.svg); + background-image: url(../../../images/icons/minus-sign.svg); background-repeat: no-repeat; background-position: center; } diff --git a/admin/src/lexical/plugins/ToolbarPlugin/fontSize.tsx b/admin/src/lexical/plugins/ToolbarPlugin/toolbarItems/fontSize.tsx similarity index 97% rename from admin/src/lexical/plugins/ToolbarPlugin/fontSize.tsx rename to admin/src/lexical/plugins/ToolbarPlugin/toolbarItems/fontSize.tsx index abadc78..e160939 100755 --- a/admin/src/lexical/plugins/ToolbarPlugin/fontSize.tsx +++ b/admin/src/lexical/plugins/ToolbarPlugin/toolbarItems/fontSize.tsx @@ -12,9 +12,9 @@ import { LexicalEditor } from 'lexical'; import * as React from 'react'; import { useIntl } from 'react-intl'; -import { MAX_ALLOWED_FONT_SIZE, MIN_ALLOWED_FONT_SIZE } from '../../context/ToolbarContext'; -import { SHORTCUTS } from '../ShortcutsPlugin/shortcuts'; -import { updateFontSize, updateFontSizeInSelection, UpdateFontSizeType } from './utils'; +import { MAX_ALLOWED_FONT_SIZE, MIN_ALLOWED_FONT_SIZE } from '../../../context/ToolbarContext'; +import { SHORTCUTS } from '../../ShortcutsPlugin/shortcuts'; +import { updateFontSize, updateFontSizeInSelection, UpdateFontSizeType } from '../utils'; export function parseAllowedFontSize(input: string): string { const match = input.match(/^(\d+(?:\.\d+)?)px$/); diff --git a/admin/src/lexical/styles.css b/admin/src/lexical/styles.css index 2d43e5c..30851ef 100644 --- a/admin/src/lexical/styles.css +++ b/admin/src/lexical/styles.css @@ -1437,7 +1437,7 @@ button.action-button:disabled { display: flex; background: #fff; border: 1px solid #dcdce4; - color: #999; + color: #666; padding: 4px; border-top-left-radius: 10px; border-top-right-radius: 10px; diff --git a/admin/src/lexical/ui/DropDown.tsx b/admin/src/lexical/ui/DropDown.tsx index 954200c..8d29022 100644 --- a/admin/src/lexical/ui/DropDown.tsx +++ b/admin/src/lexical/ui/DropDown.tsx @@ -26,11 +26,13 @@ export function DropDownItem({ className, onClick, title, + style, }: { children: React.ReactNode; className: string; onClick: (event: React.MouseEvent) => void; title?: string; + style?: React.HTMLAttributes['style']; }) { const ref = useRef(null); @@ -49,7 +51,14 @@ export function DropDownItem({ }, [ref, registerItem]); return ( - ); diff --git a/admin/src/supportedNodeTypes.tsx b/admin/src/supportedNodeTypes.tsx index 3f4855f..b037eab 100644 --- a/admin/src/supportedNodeTypes.tsx +++ b/admin/src/supportedNodeTypes.tsx @@ -50,6 +50,7 @@ export type ToolbarItemProps = { type SupportedNodeType = { id: string; + enabledByDefault: boolean; defaultLabel: string; defaultDescription: string; lexicalPlugin?: LexicalPlugin; @@ -73,6 +74,7 @@ function RichTextLexicalPlugin({ lexicalEditorProps, onRef, placeholder }: Rende const bold: SupportedNodeType = { id: 'bold', + enabledByDefault: true, defaultLabel: 'Bold', defaultDescription: 'Enable bold text', lexicalPlugin: RichTextLexicalPlugin, @@ -81,6 +83,7 @@ const bold: SupportedNodeType = { const italic: SupportedNodeType = { id: 'italic', + enabledByDefault: true, defaultLabel: 'Italic', defaultDescription: 'Enable italic text', lexicalPlugin: RichTextLexicalPlugin, @@ -89,6 +92,7 @@ const italic: SupportedNodeType = { const underline: SupportedNodeType = { id: 'underline', + enabledByDefault: true, defaultLabel: 'Underline', defaultDescription: 'Enable underlining of text', lexicalPlugin: RichTextLexicalPlugin, @@ -97,6 +101,7 @@ const underline: SupportedNodeType = { const emojiPicker: SupportedNodeType = { id: 'emojiPicker', + enabledByDefault: false, defaultLabel: 'Emoji Picker', defaultDescription: 'Enable emoji picker', lexicalPlugin: () => , @@ -104,6 +109,7 @@ const emojiPicker: SupportedNodeType = { const inlineCode: SupportedNodeType = { id: 'inlineCode', + enabledByDefault: false, defaultLabel: 'Inline Code', defaultDescription: 'Enable inline monospace-code', // TODO: really? @@ -113,6 +119,7 @@ const inlineCode: SupportedNodeType = { const link: SupportedNodeType = { id: 'link', + enabledByDefault: true, defaultLabel: 'Links', defaultDescription: 'Enable links to Strapi-internal and external targets', lexicalPlugin: () => , @@ -121,6 +128,7 @@ const link: SupportedNodeType = { const strapiImage: SupportedNodeType = { id: 'strapiImage', + enabledByDefault: true, defaultLabel: 'Strapi Images', defaultDescription: "Enable embedding images from Strapi's media gallery", lexicalPlugin: StrapiImagePlugin, @@ -129,6 +137,7 @@ const strapiImage: SupportedNodeType = { const lowercase: SupportedNodeType = { id: 'lowercase', + enabledByDefault: false, defaultLabel: 'Lowercase', defaultDescription: 'Enable inline lowercase', lexicalPlugin: RichTextLexicalPlugin, @@ -137,6 +146,7 @@ const lowercase: SupportedNodeType = { const uppercase: SupportedNodeType = { id: 'uppercase', + enabledByDefault: false, defaultLabel: 'Uppercase', defaultDescription: 'Enable inline uppercase', lexicalPlugin: RichTextLexicalPlugin, @@ -145,6 +155,7 @@ const uppercase: SupportedNodeType = { const capitalize: SupportedNodeType = { id: 'capitalize', + enabledByDefault: false, defaultLabel: 'Capitalize', defaultDescription: 'Enable inline capitalize', lexicalPlugin: RichTextLexicalPlugin, @@ -153,6 +164,7 @@ const capitalize: SupportedNodeType = { const strikethrough: SupportedNodeType = { id: 'strikethrough', + enabledByDefault: false, defaultLabel: 'Strikethrough', defaultDescription: 'Enable inline strikethrough', lexicalPlugin: RichTextLexicalPlugin, @@ -161,6 +173,7 @@ const strikethrough: SupportedNodeType = { const subscript: SupportedNodeType = { id: 'subscript', + enabledByDefault: false, defaultLabel: 'Subscript', defaultDescription: 'Enable inline subscript', lexicalPlugin: RichTextLexicalPlugin, @@ -169,6 +182,7 @@ const subscript: SupportedNodeType = { const superscript: SupportedNodeType = { id: 'superscript', + enabledByDefault: false, defaultLabel: 'Superscript', defaultDescription: 'Enable inline superscript', lexicalPlugin: RichTextLexicalPlugin, @@ -177,6 +191,7 @@ const superscript: SupportedNodeType = { const clearFormatting: SupportedNodeType = { id: 'clearFormatting', + enabledByDefault: true, defaultLabel: 'Clear Formatting', defaultDescription: 'Enable button to clear formatting', renderToolbarItem: ClearFormatting, @@ -184,6 +199,7 @@ const clearFormatting: SupportedNodeType = { const horizontalRule: SupportedNodeType = { id: 'horizontalRule', + enabledByDefault: false, defaultLabel: 'Horizontal Rule', defaultDescription: 'Enable horizontal rule', renderToolbarItem: HorizontalRule, @@ -191,6 +207,7 @@ const horizontalRule: SupportedNodeType = { const pageBreak: SupportedNodeType = { id: 'pageBreak', + enabledByDefault: false, defaultLabel: 'Page Break', defaultDescription: 'Enable page break', renderToolbarItem: PageBreak, @@ -198,6 +215,7 @@ const pageBreak: SupportedNodeType = { const image: SupportedNodeType = { id: 'image', + enabledByDefault: false, defaultLabel: 'Image', defaultDescription: 'Enable image', renderToolbarItem: Image, @@ -205,6 +223,7 @@ const image: SupportedNodeType = { const inlineImage: SupportedNodeType = { id: 'inlineImage', + enabledByDefault: false, defaultLabel: 'Inline Image', defaultDescription: 'Enable inline image', renderToolbarItem: InlineImage, @@ -212,6 +231,7 @@ const inlineImage: SupportedNodeType = { const table: SupportedNodeType = { id: 'table', + enabledByDefault: false, defaultLabel: 'Table', defaultDescription: 'Enable table', renderToolbarItem: Table, @@ -219,6 +239,7 @@ const table: SupportedNodeType = { const columns: SupportedNodeType = { id: 'columns', + enabledByDefault: false, defaultLabel: 'Columns', defaultDescription: 'Enable columns', renderToolbarItem: Columns, @@ -226,6 +247,7 @@ const columns: SupportedNodeType = { const equation: SupportedNodeType = { id: 'equation', + enabledByDefault: false, defaultLabel: 'Equation', defaultDescription: 'Enable equation', renderToolbarItem: Equation, @@ -233,6 +255,7 @@ const equation: SupportedNodeType = { const collapsible: SupportedNodeType = { id: 'collapsible', + enabledByDefault: false, defaultLabel: 'Collapsible', defaultDescription: 'Enable collapsible', renderToolbarItem: Collapsible, From e51811dbb35a1e6e986ac433d11b348c50bf0af4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benedikt=20R=C3=B6tsch?= Date: Fri, 9 May 2025 16:18:17 +0200 Subject: [PATCH 06/13] feat: finish typing and make floating toolbar react to field config --- .../lexical/context/StrapiFieldContext.tsx | 27 +- .../FloatingTextFormatToolbarPlugin/index.tsx | 395 ++++++++++-------- 2 files changed, 234 insertions(+), 188 deletions(-) diff --git a/admin/src/lexical/context/StrapiFieldContext.tsx b/admin/src/lexical/context/StrapiFieldContext.tsx index 1905fa1..65249c4 100644 --- a/admin/src/lexical/context/StrapiFieldContext.tsx +++ b/admin/src/lexical/context/StrapiFieldContext.tsx @@ -1,4 +1,5 @@ import { createContext, useContext } from 'react'; +import { supportedNodeTypes } from '../../supportedNodeTypes'; export type StrapiFieldConfig = { enabledNodeTypes: EnabledNodeTypes; @@ -9,8 +10,7 @@ export type StrapiFieldConfig = { }; export type EnabledNodeTypes = { - bold: boolean; - [key: string]: boolean; + [key in keyof typeof supportedNodeTypes]: boolean; }; type EnabledActions = { @@ -43,7 +43,28 @@ export const StrapiFieldConfigProvider = StrapiFieldConfigContext.Provider; export const defaultStrapiFieldConfig: StrapiFieldConfig = { enabledNodeTypes: { - bold: true, + bold: supportedNodeTypes.bold.enabledByDefault, + italic: supportedNodeTypes.italic.enabledByDefault, + underline: supportedNodeTypes.underline.enabledByDefault, + inlineCode: supportedNodeTypes.inlineCode.enabledByDefault, + emojiPicker: supportedNodeTypes.emojiPicker.enabledByDefault, + lowercase: supportedNodeTypes.lowercase.enabledByDefault, + uppercase: supportedNodeTypes.uppercase.enabledByDefault, + capitalize: supportedNodeTypes.capitalize.enabledByDefault, + strikethrough: supportedNodeTypes.strikethrough.enabledByDefault, + subscript: supportedNodeTypes.subscript.enabledByDefault, + superscript: supportedNodeTypes.superscript.enabledByDefault, + clearFormatting: supportedNodeTypes.clearFormatting.enabledByDefault, + link: supportedNodeTypes.link.enabledByDefault, + strapiImage: supportedNodeTypes.strapiImage.enabledByDefault, + horizontalRule: supportedNodeTypes.horizontalRule.enabledByDefault, + pageBreak: supportedNodeTypes.pageBreak.enabledByDefault, + image: supportedNodeTypes.image.enabledByDefault, + inlineImage: supportedNodeTypes.inlineImage.enabledByDefault, + table: supportedNodeTypes.table.enabledByDefault, + columns: supportedNodeTypes.columns.enabledByDefault, + equation: supportedNodeTypes.equation.enabledByDefault, + collapsible: supportedNodeTypes.collapsible.enabledByDefault, }, enabledActions: { sessionHistory: true, diff --git a/admin/src/lexical/plugins/FloatingTextFormatToolbarPlugin/index.tsx b/admin/src/lexical/plugins/FloatingTextFormatToolbarPlugin/index.tsx index 911b8e3..f904237 100755 --- a/admin/src/lexical/plugins/FloatingTextFormatToolbarPlugin/index.tsx +++ b/admin/src/lexical/plugins/FloatingTextFormatToolbarPlugin/index.tsx @@ -32,6 +32,7 @@ import { useIntl } from 'react-intl'; import { getDOMRangeRect } from '../../utils/getDOMRangeRect'; import { getSelectedNode } from '../../utils/getSelectedNode'; import { setFloatingElemPosition } from '../../utils/setFloatingElemPosition'; +import { useStrapiFieldContext } from '../../context/StrapiFieldContext'; function TextFormatFloatingToolbar({ editor, @@ -179,195 +180,219 @@ function TextFormatFloatingToolbar({ ); }, [editor, $updateTextFormatFloatingToolbar]); + const strapiFieldConfig = useStrapiFieldContext(); + return (
{editor.isEditable() && ( <> - - - - - - - - - - - + {strapiFieldConfig.enabledNodeTypes.bold && ( + + )} + {strapiFieldConfig.enabledNodeTypes.italic && ( + + )} + {strapiFieldConfig.enabledNodeTypes.underline && ( + + )} + {strapiFieldConfig.enabledNodeTypes.strikethrough && ( + + )} + {strapiFieldConfig.enabledNodeTypes.subscript && ( + + )} + {strapiFieldConfig.enabledNodeTypes.superscript && ( + + )} + {strapiFieldConfig.enabledNodeTypes.uppercase && ( + + )} + {strapiFieldConfig.enabledNodeTypes.lowercase && ( + + )} + {strapiFieldConfig.enabledNodeTypes.capitalize && ( + + )} + {strapiFieldConfig.enabledNodeTypes.inlineCode && ( + + )} + {strapiFieldConfig.enabledNodeTypes.link && ( + + )} )}
From 5d31df38988a1f755715319e7232da518ccbce3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benedikt=20R=C3=B6tsch?= Date: Fri, 9 May 2025 16:19:54 +0200 Subject: [PATCH 07/13] feat: show/hide back and forward buttons based on field config --- admin/src/lexical/plugins/ToolbarPlugin/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/admin/src/lexical/plugins/ToolbarPlugin/index.tsx b/admin/src/lexical/plugins/ToolbarPlugin/index.tsx index 23995c3..ee5f333 100755 --- a/admin/src/lexical/plugins/ToolbarPlugin/index.tsx +++ b/admin/src/lexical/plugins/ToolbarPlugin/index.tsx @@ -514,7 +514,7 @@ function ToolbarItem(props: { id: string }): React.ReactNode { } } - if (props.id === 'actions.history.undo') { + if (props.id === 'actions.history.undo' && strapiFieldConfig.enabledActions.sessionHistory) { return ( ); } - if (props.id === 'actions.history.redo') { + if (props.id === 'actions.history.redo' && strapiFieldConfig.enabledActions.sessionHistory) { return ( Date: Fri, 9 May 2025 16:39:57 +0200 Subject: [PATCH 08/13] chore: remove misplaced and for us kinda useless placeholder --- admin/src/lexical/Editor.tsx | 14 +------------ .../ToolbarPlugin/toolbarItems/font.tsx | 9 --------- admin/src/lexical/ui/ContentEditable.tsx | 20 ++----------------- admin/src/supportedNodeTypes.tsx | 5 ++--- 4 files changed, 5 insertions(+), 43 deletions(-) diff --git a/admin/src/lexical/Editor.tsx b/admin/src/lexical/Editor.tsx index c8766fe..5ea9dc7 100755 --- a/admin/src/lexical/Editor.tsx +++ b/admin/src/lexical/Editor.tsx @@ -113,14 +113,6 @@ export default function Editor(props: LexicalEditorProps): JSX.Element { const selectionAlwaysOnDisplay = false; const isEditable = useLexicalEditable(); - const placeholder = formatMessage( - { - id: 'lexical.editor.placeholder', - defaultMessage: - 'Enter some {state, select, collab {collaborative rich} rich {rich} other {plain}} text...', - }, - { state: isCollab ? 'collab' : isRichText ? 'rich' : 'plain' } - ); const [floatingAnchorElem, setFloatingAnchorElem] = useState(null); const [isSmallWidthViewport, setIsSmallWidthViewport] = useState(false); @@ -189,11 +181,7 @@ export default function Editor(props: LexicalEditorProps): JSX.Element { {isRichText ? ( <> - + diff --git a/admin/src/lexical/plugins/ToolbarPlugin/toolbarItems/font.tsx b/admin/src/lexical/plugins/ToolbarPlugin/toolbarItems/font.tsx index ab815ec..8be2b53 100644 --- a/admin/src/lexical/plugins/ToolbarPlugin/toolbarItems/font.tsx +++ b/admin/src/lexical/plugins/ToolbarPlugin/toolbarItems/font.tsx @@ -46,21 +46,12 @@ export function FontDropDown({ [editor, style] ); - const buttonAriaLabel = formatMessage( - { - id: `lexical.plugin.toolbar.font.button.title`, - defaultMessage: 'Formatting options for font {property}', - }, - { property: style === 'font-family' ? 'family' : 'size' } - ); - return ( {options.map((option) => ( ; }; -export default function LexicalContentEditable({ - className, - placeholder, - placeholderClassName, - ref, -}: Props): JSX.Element { - return ( - {placeholder}
- } - /> - ); +export default function LexicalContentEditable({ className, ref }: Props): JSX.Element { + return ; } diff --git a/admin/src/supportedNodeTypes.tsx b/admin/src/supportedNodeTypes.tsx index b037eab..6548c89 100644 --- a/admin/src/supportedNodeTypes.tsx +++ b/admin/src/supportedNodeTypes.tsx @@ -39,7 +39,6 @@ type LexicalPlugin = (props: T) => JSX.Element | null; type RenderPluginProps = { lexicalEditorProps: LexicalEditorProps; onRef: (floatingAnchorElem: HTMLDivElement) => void; - placeholder: string; }; export type ToolbarItemProps = { @@ -57,13 +56,13 @@ type SupportedNodeType = { renderToolbarItem?: React.FunctionComponent; }; -function RichTextLexicalPlugin({ lexicalEditorProps, onRef, placeholder }: RenderPluginProps) { +function RichTextLexicalPlugin({ lexicalEditorProps, onRef }: RenderPluginProps) { return (
- +
} From a15cb077415635ea5cf3c1f6eb4ce246227cb1ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benedikt=20R=C3=B6tsch?= Date: Fri, 9 May 2025 16:40:21 +0200 Subject: [PATCH 09/13] chore: make it blend (types...) --- .../lexical/context/StrapiFieldContext.tsx | 44 +++++++++---------- admin/src/supportedNodeTypes.tsx | 23 ---------- 2 files changed, 22 insertions(+), 45 deletions(-) diff --git a/admin/src/lexical/context/StrapiFieldContext.tsx b/admin/src/lexical/context/StrapiFieldContext.tsx index 65249c4..06b6e79 100644 --- a/admin/src/lexical/context/StrapiFieldContext.tsx +++ b/admin/src/lexical/context/StrapiFieldContext.tsx @@ -43,28 +43,28 @@ export const StrapiFieldConfigProvider = StrapiFieldConfigContext.Provider; export const defaultStrapiFieldConfig: StrapiFieldConfig = { enabledNodeTypes: { - bold: supportedNodeTypes.bold.enabledByDefault, - italic: supportedNodeTypes.italic.enabledByDefault, - underline: supportedNodeTypes.underline.enabledByDefault, - inlineCode: supportedNodeTypes.inlineCode.enabledByDefault, - emojiPicker: supportedNodeTypes.emojiPicker.enabledByDefault, - lowercase: supportedNodeTypes.lowercase.enabledByDefault, - uppercase: supportedNodeTypes.uppercase.enabledByDefault, - capitalize: supportedNodeTypes.capitalize.enabledByDefault, - strikethrough: supportedNodeTypes.strikethrough.enabledByDefault, - subscript: supportedNodeTypes.subscript.enabledByDefault, - superscript: supportedNodeTypes.superscript.enabledByDefault, - clearFormatting: supportedNodeTypes.clearFormatting.enabledByDefault, - link: supportedNodeTypes.link.enabledByDefault, - strapiImage: supportedNodeTypes.strapiImage.enabledByDefault, - horizontalRule: supportedNodeTypes.horizontalRule.enabledByDefault, - pageBreak: supportedNodeTypes.pageBreak.enabledByDefault, - image: supportedNodeTypes.image.enabledByDefault, - inlineImage: supportedNodeTypes.inlineImage.enabledByDefault, - table: supportedNodeTypes.table.enabledByDefault, - columns: supportedNodeTypes.columns.enabledByDefault, - equation: supportedNodeTypes.equation.enabledByDefault, - collapsible: supportedNodeTypes.collapsible.enabledByDefault, + bold: false, + italic: false, + underline: false, + inlineCode: false, + emojiPicker: false, + lowercase: false, + uppercase: false, + capitalize: false, + strikethrough: false, + subscript: false, + superscript: false, + clearFormatting: false, + link: false, + strapiImage: false, + horizontalRule: false, + pageBreak: false, + image: false, + inlineImage: false, + table: false, + columns: false, + equation: false, + collapsible: false, }, enabledActions: { sessionHistory: true, diff --git a/admin/src/supportedNodeTypes.tsx b/admin/src/supportedNodeTypes.tsx index 6548c89..e1c0163 100644 --- a/admin/src/supportedNodeTypes.tsx +++ b/admin/src/supportedNodeTypes.tsx @@ -260,29 +260,6 @@ const collapsible: SupportedNodeType = { renderToolbarItem: Collapsible, }; -export { - bold, - italic, - underline, - inlineCode, - emojiPicker, - lowercase, - uppercase, - capitalize, - strikethrough, - subscript, - superscript, - clearFormatting, - link, - strapiImage, - horizontalRule, - pageBreak, - image, - inlineImage, - table, - columns, -}; - export const supportedNodeTypes = { bold, italic, From b41bd721859fd64a08f42ca2358cee2ae9698d22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benedikt=20R=C3=B6tsch?= Date: Fri, 9 May 2025 16:40:46 +0200 Subject: [PATCH 10/13] feat: improve dropdown usability by making it scrollable and setting a useful max height --- admin/src/lexical/styles.css | 3 +++ 1 file changed, 3 insertions(+) diff --git a/admin/src/lexical/styles.css b/admin/src/lexical/styles.css index 30851ef..ee77d0c 100644 --- a/admin/src/lexical/styles.css +++ b/admin/src/lexical/styles.css @@ -700,6 +700,7 @@ i.page-break, z-index: 100; display: block; position: fixed; + margin-top: 2px; box-shadow: 0 12px 28px 0 rgba(0, 0, 0, 0.2), 0 2px 4px 0 rgba(0, 0, 0, 0.1), @@ -707,6 +708,8 @@ i.page-break, border-radius: 8px; min-height: 40px; background-color: #fff; + max-height: 61.8vh; + overflow: scroll; } .dropdown .item { From da400dc8f7c064c74db4b820552b49dcabf4a23b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benedikt=20R=C3=B6tsch?= Date: Fri, 9 May 2025 17:12:46 +0200 Subject: [PATCH 11/13] fix: show and hide toolbar group (separators) depending if any buttons are rendered within --- .../lexical/plugins/ToolbarPlugin/index.tsx | 107 ++---------------- admin/src/lexical/styles.css | 9 ++ 2 files changed, 16 insertions(+), 100 deletions(-) diff --git a/admin/src/lexical/plugins/ToolbarPlugin/index.tsx b/admin/src/lexical/plugins/ToolbarPlugin/index.tsx index ee5f333..c49c630 100755 --- a/admin/src/lexical/plugins/ToolbarPlugin/index.tsx +++ b/admin/src/lexical/plugins/ToolbarPlugin/index.tsx @@ -7,7 +7,6 @@ */ import type { JSX } from 'react'; -import { Children } from 'react'; import { useIntl } from 'react-intl'; import { @@ -16,10 +15,9 @@ import { CODE_LANGUAGE_MAP, getLanguageFriendlyName, } from '@lexical/code'; -import { $isLinkNode, TOGGLE_LINK_COMMAND } from '@lexical/link'; +import { $isLinkNode } from '@lexical/link'; import { $isListNode, ListNode } from '@lexical/list'; import { INSERT_EMBED_COMMAND } from '@lexical/react/LexicalAutoEmbedPlugin'; -import { INSERT_HORIZONTAL_RULE_COMMAND } from '@lexical/react/LexicalHorizontalRuleNode'; import { $isHeadingNode } from '@lexical/rich-text'; import { $getSelectionStyleValueForProperty, @@ -35,7 +33,6 @@ import { } from '@lexical/utils'; import { $getNodeByKey, - $getRoot, $getSelection, $isElementNode, $isRangeSelection, @@ -45,32 +42,24 @@ import { COMMAND_PRIORITY_CRITICAL, ElementFormatType, FORMAT_ELEMENT_COMMAND, - FORMAT_TEXT_COMMAND, INDENT_CONTENT_COMMAND, LexicalEditor, NodeKey, OUTDENT_CONTENT_COMMAND, - REDO_COMMAND, SELECTION_CHANGE_COMMAND, - UNDO_COMMAND, } from 'lexical'; import * as React from 'react'; import { Dispatch, useCallback, useEffect, useState } from 'react'; -import { IS_APPLE } from '../../utils/environment'; import { useStrapiApp } from '@strapi/strapi/admin'; import { blockTypeToBlockName, useToolbarState } from '../../context/ToolbarContext'; import useModal from '../../hooks/useModal'; -import { $createStickyNode } from '../../nodes/StickyNode'; import DropDown, { DropDownItem } from '../../ui/DropDown'; import { getSelectedNode } from '../../utils/getSelectedNode'; -import { sanitizeUrl } from '../../utils/url'; import { EmbedConfigs } from '../AutoEmbedPlugin'; -import { INSERT_IMAGE_COMMAND, InsertImageDialog, InsertImagePayload } from '../ImagesPlugin'; -import { InsertInlineImageDialog } from '../InlineImagePlugin'; +import { INSERT_IMAGE_COMMAND, InsertImagePayload } from '../ImagesPlugin'; -import { InsertPollDialog } from '../PollPlugin'; import { SHORTCUTS } from '../ShortcutsPlugin/shortcuts'; import { InsertStrapiImageDialog } from '../StrapiImagePlugin'; @@ -90,7 +79,7 @@ import { } from '../../context/ToolbarItemRenderDependenciesContext'; import { EnabledNodeTypes, useStrapiFieldContext } from '../../context/StrapiFieldContext'; import { supportedNodeTypes } from '../../../supportedNodeTypes'; -import { dropDownActiveClass, clearFormatting } from './codeLessUtils'; +import { dropDownActiveClass } from './codeLessUtils'; import FontSize from './toolbarItems/fontSize'; import { FontDropDown } from './toolbarItems/font'; @@ -573,32 +562,11 @@ function ToolbarItem(props: { id: string }): React.ReactNode { } function ToolbarGroup(props: React.PropsWithChildren<{}>) { - const groupElements = Children.map(props.children, (element) => { - if (!React.isValidElement(element)) { - // Ignore non-elements. This allows people to more easily inline - // conditionals in their route config. - return; - } - if (element.type !== ToolbarItem) { - // Ignore unknown elements - // TODO: fail with good error message? - return; - } - - return element; - }); - - if (!groupElements || !Children.count(groupElements)) { - // Return an empty group if all elements are disabled - // @todo this is not working! toolbar still renders as "just a divider" - return; - } - return ( - <> - {...groupElements} +
+ {props.children} - +
); } @@ -609,30 +577,7 @@ function ToolbarDropDown( buttonIconClassName: string; }> ) { - const { toolbarState } = useToolbarState(); const renderDependencies = useToolbarItemRenderDependencies(); - const strapiFieldConfig = useStrapiFieldContext(); - - const groupElements = Children.map(props.children, (element) => { - if (!React.isValidElement(element)) { - // Ignore non-elements. This allows people to more easily inline - // conditionals in their route config. - return; - } - if (element.type !== ToolbarItem) { - // Ignore unknown elements - // TODO: fail with good error message? - return; - } - - return element; - }); - - if (!groupElements || !Children.count(groupElements)) { - // Return an empty group if all elements are disabled - // @todo this is not working! toolbar still renders as "just a divider" - return; - } return ( <> @@ -643,7 +588,7 @@ function ToolbarDropDown( buttonAriaLabel={props.buttonAriaLabel} buttonIconClassName={props.buttonIconClassName} > - {...groupElements} + {props.children} @@ -911,7 +856,6 @@ export default function ToolbarPlugin({ - {/* TODO: how to support a nested "additional options" */} @@ -919,43 +863,6 @@ export default function ToolbarPlugin({ - {/* {strapiFieldConfig?.fontFamily?.enabled && ( - - - - )} - {strapiFieldConfig?.fontSize?.enabled && ( - - strapiFieldConfig.fontSize.minimum + i - ).map(size => `${size}px`)} - /> - - )} - {strapiFieldConfig?.fontSize?.enabled && ( - - - - )} */} diff --git a/admin/src/lexical/styles.css b/admin/src/lexical/styles.css index ee77d0c..70e1288 100644 --- a/admin/src/lexical/styles.css +++ b/admin/src/lexical/styles.css @@ -1561,6 +1561,15 @@ button.toolbar-item.active i { border-right: 1px solid #ddd; } +.toolbar-group { + display: flex; + & .divider { + &:first-child { + display: none; + } + } +} + .sticky-note-container { position: absolute; z-index: 9; From 0e039ad201cb959c7c25030551f1fa1475081e96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benedikt=20R=C3=B6tsch?= Date: Fri, 9 May 2025 17:22:58 +0200 Subject: [PATCH 12/13] feat: set editor toolbar context defaults based on editor field config (font size basically) --- admin/src/components/Input.tsx | 10 ++++++++-- admin/src/lexical/context/ToolbarContext.tsx | 12 +++++++++--- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/admin/src/components/Input.tsx b/admin/src/components/Input.tsx index eec50e2..c3680f6 100644 --- a/admin/src/components/Input.tsx +++ b/admin/src/components/Input.tsx @@ -10,7 +10,7 @@ import { SerializedEditorState, SerializedElementNode, SerializedLexicalNode } f import LexicalEditor from '../lexical/Editor'; import { FlashMessageContext } from '../lexical/context/FlashMessageContext'; -import { ToolbarContext } from '../lexical/context/ToolbarContext'; +import { INITIAL_TOOLBAR_STATE, ToolbarContext } from '../lexical/context/ToolbarContext'; import { StrapiFieldConfig, StrapiFieldConfigProvider, @@ -251,7 +251,13 @@ const Input = React.forwardRef - + (undefined); -export const ToolbarContext = ({ children }: { children: ReactNode }): JSX.Element => { - const [toolbarState, setToolbarState] = useState(INITIAL_TOOLBAR_STATE); +export const ToolbarContext = ({ + children, + initialState, +}: { + children: ReactNode; + initialState: ToolbarState; +}): JSX.Element => { + const [toolbarState, setToolbarState] = useState(initialState); const selectionFontSize = toolbarState.fontSize; const updateToolbarState = useCallback( From 41c4d06ae0a2aee54e8f82d9f8af6c27273510eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benedikt=20R=C3=B6tsch?= Date: Fri, 9 May 2025 17:23:49 +0200 Subject: [PATCH 13/13] feat: show/hide editor action buttons based on field config --- .../lexical/plugins/ActionsPlugin/index.tsx | 203 ++++++++---------- 1 file changed, 84 insertions(+), 119 deletions(-) diff --git a/admin/src/lexical/plugins/ActionsPlugin/index.tsx b/admin/src/lexical/plugins/ActionsPlugin/index.tsx index 419f15d..d77dcfc 100755 --- a/admin/src/lexical/plugins/ActionsPlugin/index.tsx +++ b/admin/src/lexical/plugins/ActionsPlugin/index.tsx @@ -35,6 +35,7 @@ import useModal from '../../hooks/useModal'; import Button from '../../ui/Button'; import { docFromHash, docToHash } from '../../utils/docSerialization'; import { PLAYGROUND_TRANSFORMERS } from '../MarkdownTransformers'; +import { useStrapiFieldContext } from '../../context/StrapiFieldContext'; async function sendEditorState(editor: LexicalEditor): Promise { const stringifiedEditorState = JSON.stringify(editor.getEditorState()); @@ -160,131 +161,95 @@ export default function ActionsPlugin({ }); }, [editor, shouldPreserveNewLinesInMarkdown]); + const strapiFieldConfig = useStrapiFieldContext(); + return (
- + {strapiFieldConfig.enabledActions.import && ( + + )} - - - - - + title={formatMessage({ + id: 'lexical.plugin.actions.export.title', + defaultMessage: 'Export', + })} + aria-label={formatMessage({ + id: 'lexical.plugin.actions.export.aria', + defaultMessage: 'Export editor state to JSON', + })} + > + + + )} + {strapiFieldConfig.enabledActions.clear && ( + + )} + {strapiFieldConfig.enabledActions.exportAsMarkdown && ( + + )} {modal}
);