From 93350a1c353715a2ec53bb019e022c6fd4002d36 Mon Sep 17 00:00:00 2001 From: Richard Olsson Date: Sat, 18 Jan 2025 11:42:50 +0100 Subject: [PATCH 1/7] Refactor block factories to use unified command pattern --- .../extensions/BlockMenuExtension.ts | 35 ++++------------ .../ZUIEditor/extensions/ButtonExtension.tsx | 41 ++++++++++++++----- .../ZUIEditor/extensions/ImageExtension.ts | 22 ++++++---- src/zui/ZUIEditor/index.tsx | 10 ++--- 4 files changed, 55 insertions(+), 53 deletions(-) diff --git a/src/zui/ZUIEditor/extensions/BlockMenuExtension.ts b/src/zui/ZUIEditor/extensions/BlockMenuExtension.ts index 206495817..ec19350be 100644 --- a/src/zui/ZUIEditor/extensions/BlockMenuExtension.ts +++ b/src/zui/ZUIEditor/extensions/BlockMenuExtension.ts @@ -1,17 +1,14 @@ -import { Node } from '@remirror/pm/model'; -import { TextSelection } from '@remirror/pm/state'; import { Suggester } from '@remirror/pm/suggest'; import { legacyCommand as command, CommandFunction, extension, - getActiveNode, Handler, PlainExtension, } from 'remirror'; type BlockMenuOptions = { - blockFactories: Record Node>; + blockFactories: Record; onBlockQuery?: Handler<(query: string | null) => void>; }; @@ -40,31 +37,13 @@ class BlockMenuExtension extends PlainExtension { //@ts-ignore @command() insertBlock(type: string): CommandFunction { - return ({ dispatch, state, tr }) => { - const oldNode = getActiveNode({ - state, - type: 'paragraph', - }); - if (oldNode) { - const factory = this.options.blockFactories[type]; - if (factory) { - const newNode = factory(); - if (dispatch && newNode) { - tr = tr.replaceWith(oldNode.pos, oldNode.end, newNode); - tr = tr.setSelection( - TextSelection.create( - tr.doc, - oldNode.pos + 1, - oldNode.pos + newNode.nodeSize - 1 - ) - ); - dispatch(tr); - } + return (props) => { + const { state, tr } = props; + const resolved = state.doc.resolve(state.selection.$head.pos); + tr.deleteRange(resolved.start(), resolved.end()); - return true; - } - } - return false; + const factoryCommand = this.options.blockFactories[type]; + return factoryCommand(props); }; } diff --git a/src/zui/ZUIEditor/extensions/ButtonExtension.tsx b/src/zui/ZUIEditor/extensions/ButtonExtension.tsx index 13a67837f..1c9fe8b2a 100644 --- a/src/zui/ZUIEditor/extensions/ButtonExtension.tsx +++ b/src/zui/ZUIEditor/extensions/ButtonExtension.tsx @@ -1,4 +1,5 @@ /* eslint-disable @typescript-eslint/no-namespace */ +import { TextSelection } from '@remirror/pm/state'; import { ApplySchemaAttributes, legacyCommand as command, //Because of NEXTjs, see Remirror docs @@ -63,21 +64,11 @@ class ButtonExtension extends NodeExtension { }, }; } + createTags() { return [ExtensionTag.Block, ExtensionTag.FormattingNode]; } - /* eslint-disable @typescript-eslint/ban-ts-comment */ - //@ts-ignore - @command() - insertButton(pos: number): CommandFunction { - return ({ tr, dispatch }) => { - const node = this.type.create(null, this.type.schema.text('Foobar')); - dispatch?.(tr.insert(pos, node)); - return true; - }; - } - get name() { return 'zbutton' as const; } @@ -108,6 +99,34 @@ class ButtonExtension extends NodeExtension { return true; }; } + + /* eslint-disable @typescript-eslint/ban-ts-comment */ + //@ts-ignore + @command() + toggleButton(): CommandFunction { + return (props) => { + const { dispatch, state, tr } = props; + const newNode = this.type.create( + null, + this.type.schema.text('Foo button') + ); + if (dispatch) { + const pos = state.selection.$from.pos; + const parentOffset = state.doc.resolve(pos).parentOffset; + const blockLength = 1; + + tr.insert(pos - parentOffset - blockLength, newNode); + + const resolved = tr.doc.resolve(pos); + tr.setSelection( + TextSelection.create(tr.doc, resolved.start(), resolved.end()) + ); + dispatch(tr); + } + + return true; + }; + } } declare global { diff --git a/src/zui/ZUIEditor/extensions/ImageExtension.ts b/src/zui/ZUIEditor/extensions/ImageExtension.ts index c097a29b6..db442212f 100644 --- a/src/zui/ZUIEditor/extensions/ImageExtension.ts +++ b/src/zui/ZUIEditor/extensions/ImageExtension.ts @@ -24,15 +24,23 @@ type ImageOptions = { staticKeys: [], }) export default class ImageExtension extends NodeExtension { - createAndPick() { - const pos = this.store.getState().selection.$from.pos; - const parentOffset = this.store.getState().doc.resolve(pos).parentOffset; - const blockLength = 1; + /* eslint-disable @typescript-eslint/ban-ts-comment */ + //@ts-ignore + @command() + createAndPick(): CommandFunction { + return ({ dispatch, tr, state }) => { + const pos = state.selection.$from.pos; + const parentOffset = state.doc.resolve(pos).parentOffset; + const blockLength = 1; - const node = this.type.create(); - this.options.onCreate(pos - parentOffset - blockLength); + const node = this.type.create(); + tr.insert(pos - parentOffset - blockLength, node); + dispatch?.(tr); - return node; + this.options.onCreate(pos - parentOffset - blockLength); + + return true; + }; } createNodeSpec( diff --git a/src/zui/ZUIEditor/index.tsx b/src/zui/ZUIEditor/index.tsx index a45ab1bf8..553cf8b26 100644 --- a/src/zui/ZUIEditor/index.tsx +++ b/src/zui/ZUIEditor/index.tsx @@ -94,13 +94,9 @@ const ZUIEditor: FC = ({ new LinkExtension(), new BlockMenuExtension({ blockFactories: { - heading: () => headingExtension.type.create({}), - zbutton: () => - btnExtension.type.create( - {}, - btnExtension.type.schema.text('Add button label here') - ), - zimage: () => imgExtension.createAndPick(), + heading: (props) => headingExtension.toggleHeading()(props), + zbutton: (props) => btnExtension.toggleButton()(props), + zimage: (props) => imgExtension.createAndPick()(props), }, }), ], From d104bfe6e86effb534419946c97e16b54e939730 Mon Sep 17 00:00:00 2001 From: Richard Olsson Date: Sat, 18 Jan 2025 11:46:10 +0100 Subject: [PATCH 2/7] Enable ordered and unordered lists --- .../[campId]/emails/[emailId]/newEditor.tsx | 8 +++++- src/zui/ZUIEditor/index.tsx | 27 ++++++++++++++++--- src/zui/l10n/messageIds.ts | 2 ++ 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/src/pages/organize/[orgId]/projects/[campId]/emails/[emailId]/newEditor.tsx b/src/pages/organize/[orgId]/projects/[campId]/emails/[emailId]/newEditor.tsx index acef317d8..1a4b08a0f 100644 --- a/src/pages/organize/[orgId]/projects/[campId]/emails/[emailId]/newEditor.tsx +++ b/src/pages/organize/[orgId]/projects/[campId]/emails/[emailId]/newEditor.tsx @@ -25,7 +25,13 @@ const EmailPage: PageWithLayout = () => { hejj - + ); diff --git a/src/zui/ZUIEditor/index.tsx b/src/zui/ZUIEditor/index.tsx index 553cf8b26..23fad1305 100644 --- a/src/zui/ZUIEditor/index.tsx +++ b/src/zui/ZUIEditor/index.tsx @@ -5,7 +5,12 @@ import { useRemirror, } from '@remirror/react'; import { FC } from 'react'; -import { BoldExtension, HeadingExtension } from 'remirror/extensions'; +import { + BoldExtension, + BulletListExtension, + HeadingExtension, + OrderedListExtension, +} from 'remirror/extensions'; import { Extension, PasteRulesExtension } from 'remirror'; import { Box, useTheme } from '@mui/material'; @@ -23,12 +28,18 @@ import VariableExtension from './extensions/VariableExtension'; import LinkExtensionUI from './LinkExtensionUI'; import ButtonExtensionUI from './ButtonExtensionUI'; -type ZetkinExtension = ButtonExtension | HeadingExtension | ImageExtension; +type BlockExtension = + | ButtonExtension + | HeadingExtension + | ImageExtension + | OrderedListExtension + | BulletListExtension; type Props = { enableButton?: boolean; enableHeading?: boolean; enableImage?: boolean; + enableLists?: boolean; enableVariable?: boolean; }; @@ -36,6 +47,7 @@ const ZUIEditor: FC = ({ enableButton, enableHeading, enableImage, + enableLists, enableVariable, }) => { const messages = useMessages(messageIds.editor); @@ -43,6 +55,8 @@ const ZUIEditor: FC = ({ const btnExtension = new ButtonExtension(); const imgExtension = new ImageExtension({}); + const olExtension = new OrderedListExtension(); + const ulExtension = new BulletListExtension({}); const headingExtension = new HeadingExtension({}); const varExtension = new VariableExtension({ first_name: messages.variables.firstName(), @@ -50,7 +64,7 @@ const ZUIEditor: FC = ({ last_name: messages.variables.lastName(), }); - const blockExtensions: ZetkinExtension[] = []; + const blockExtensions: BlockExtension[] = []; const otherExtensions: Extension[] = []; if (enableButton) { @@ -65,6 +79,11 @@ const ZUIEditor: FC = ({ blockExtensions.push(headingExtension); } + if (enableLists) { + blockExtensions.push(olExtension); + blockExtensions.push(ulExtension); + } + if (enableVariable) { otherExtensions.push(varExtension); } @@ -94,7 +113,9 @@ const ZUIEditor: FC = ({ new LinkExtension(), new BlockMenuExtension({ blockFactories: { + bulletList: (props) => ulExtension.toggleBulletList()(props), heading: (props) => headingExtension.toggleHeading()(props), + orderedList: (props) => olExtension.toggleOrderedList()(props), zbutton: (props) => btnExtension.toggleButton()(props), zimage: (props) => imgExtension.createAndPick()(props), }, diff --git a/src/zui/l10n/messageIds.ts b/src/zui/l10n/messageIds.ts index 7e34f503e..a0ce228ad 100644 --- a/src/zui/l10n/messageIds.ts +++ b/src/zui/l10n/messageIds.ts @@ -128,7 +128,9 @@ export default makeMessages('zui', { }, editor: { blockLabels: { + bulletList: m('Bullet list'), heading: m('Heading'), + orderedList: m('Ordered list'), zbutton: m('Button'), zimage: m('Image'), }, From 1b858b82e5067e130dd44e2c6d163cddbd67b1ad Mon Sep 17 00:00:00 2001 From: Richard Olsson Date: Sat, 18 Jan 2025 11:48:29 +0100 Subject: [PATCH 3/7] Enable line-breaks in paragraphs and lists --- src/zui/ZUIEditor/index.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/zui/ZUIEditor/index.tsx b/src/zui/ZUIEditor/index.tsx index 23fad1305..4a7f405c2 100644 --- a/src/zui/ZUIEditor/index.tsx +++ b/src/zui/ZUIEditor/index.tsx @@ -8,6 +8,7 @@ import { FC } from 'react'; import { BoldExtension, BulletListExtension, + HardBreakExtension, HeadingExtension, OrderedListExtension, } from 'remirror/extensions'; @@ -110,6 +111,7 @@ const ZUIEditor: FC = ({ new BoldExtension({}), ...blockExtensions, ...otherExtensions, + new HardBreakExtension(), new LinkExtension(), new BlockMenuExtension({ blockFactories: { From cf6db8a6b863c8df27a5ea539d9735e6e5e52887 Mon Sep 17 00:00:00 2001 From: Richard Olsson Date: Sat, 18 Jan 2025 13:48:06 +0100 Subject: [PATCH 4/7] Prevent block menu from opening in non-paragraph blocks --- src/zui/ZUIEditor/extensions/BlockMenuExtension.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/zui/ZUIEditor/extensions/BlockMenuExtension.ts b/src/zui/ZUIEditor/extensions/BlockMenuExtension.ts index ec19350be..009add59a 100644 --- a/src/zui/ZUIEditor/extensions/BlockMenuExtension.ts +++ b/src/zui/ZUIEditor/extensions/BlockMenuExtension.ts @@ -23,11 +23,17 @@ class BlockMenuExtension extends PlainExtension { return [ { char: '/', - isValidPosition: (range) => range.$from.parentOffset == 0, + isValidPosition: (range) => { + return range.$from.parentOffset == 0; + }, name: 'slash', - onChange: (details) => { + onChange: (details, tr) => { + const resolved = tr.doc.resolve(details.range.to); const exited = !!details.exitReason; - this.options.onBlockQuery(exited ? null : details.query.full); + const inParagraph = resolved.node(1).type.name == 'paragraph'; + this.options.onBlockQuery( + exited || !inParagraph ? null : details.query.full + ); }, }, ]; From f9dfe6be16393e83637a95889f5c12915721d912 Mon Sep 17 00:00:00 2001 From: Richard Olsson Date: Sat, 18 Jan 2025 15:40:05 +0100 Subject: [PATCH 5/7] Add bold and italic formatting with ToolbarButtons --- .../[campId]/emails/[emailId]/newEditor.tsx | 2 ++ .../ZUIEditor/EditorOverlays/BlockToolbar.tsx | 8 +++++++ .../EditorOverlays/BoldToolButton.tsx | 23 +++++++++++++++++++ .../EditorOverlays/ItalicToolButton.tsx | 23 +++++++++++++++++++ src/zui/ZUIEditor/EditorOverlays/index.tsx | 11 ++++++++- src/zui/ZUIEditor/index.tsx | 21 +++++++++++++++-- 6 files changed, 85 insertions(+), 3 deletions(-) create mode 100644 src/zui/ZUIEditor/EditorOverlays/BoldToolButton.tsx create mode 100644 src/zui/ZUIEditor/EditorOverlays/ItalicToolButton.tsx diff --git a/src/pages/organize/[orgId]/projects/[campId]/emails/[emailId]/newEditor.tsx b/src/pages/organize/[orgId]/projects/[campId]/emails/[emailId]/newEditor.tsx index 1a4b08a0f..9507b27fd 100644 --- a/src/pages/organize/[orgId]/projects/[campId]/emails/[emailId]/newEditor.tsx +++ b/src/pages/organize/[orgId]/projects/[campId]/emails/[emailId]/newEditor.tsx @@ -26,9 +26,11 @@ const EmailPage: PageWithLayout = () => { diff --git a/src/zui/ZUIEditor/EditorOverlays/BlockToolbar.tsx b/src/zui/ZUIEditor/EditorOverlays/BlockToolbar.tsx index 693c90097..c1b0ea215 100644 --- a/src/zui/ZUIEditor/EditorOverlays/BlockToolbar.tsx +++ b/src/zui/ZUIEditor/EditorOverlays/BlockToolbar.tsx @@ -5,10 +5,14 @@ import { FC, useEffect, useState } from 'react'; import { NodeWithPosition } from '../LinkExtensionUI'; import VariableToolButton from './VariableToolButton'; import { VariableName } from '../extensions/VariableExtension'; +import BoldToolButton from './BoldToolButton'; +import ItalicToolButton from './ItalicToolButton'; type BlockToolbarProps = { curBlockType: string; curBlockY: number; + enableBold: boolean; + enableItalic: boolean; enableVariable: boolean; pos: number; }; @@ -16,6 +20,8 @@ type BlockToolbarProps = { const BlockToolbar: FC = ({ curBlockType, curBlockY, + enableBold, + enableItalic, enableVariable, pos, }) => { @@ -111,6 +117,8 @@ const BlockToolbar: FC = ({ > Link + {enableBold && } + {enableItalic && } )} {enableVariable && diff --git a/src/zui/ZUIEditor/EditorOverlays/BoldToolButton.tsx b/src/zui/ZUIEditor/EditorOverlays/BoldToolButton.tsx new file mode 100644 index 000000000..31b171d05 --- /dev/null +++ b/src/zui/ZUIEditor/EditorOverlays/BoldToolButton.tsx @@ -0,0 +1,23 @@ +import { FormatBold } from '@mui/icons-material'; +import { IconButton } from '@mui/material'; +import { useActive, useCommands } from '@remirror/react'; +import { FC } from 'react'; + +const BoldToolButton: FC = () => { + const active = useActive(); + const { focus, toggleBold } = useCommands(); + + return ( + { + toggleBold(); + focus(); + }} + > + + + ); +}; + +export default BoldToolButton; diff --git a/src/zui/ZUIEditor/EditorOverlays/ItalicToolButton.tsx b/src/zui/ZUIEditor/EditorOverlays/ItalicToolButton.tsx new file mode 100644 index 000000000..587bb7e46 --- /dev/null +++ b/src/zui/ZUIEditor/EditorOverlays/ItalicToolButton.tsx @@ -0,0 +1,23 @@ +import { FormatItalic } from '@mui/icons-material'; +import { IconButton } from '@mui/material'; +import { useActive, useCommands } from '@remirror/react'; +import { FC } from 'react'; + +const ItalicToolButton: FC = () => { + const active = useActive(); + const { focus, toggleItalic } = useCommands(); + + return ( + { + toggleItalic(); + focus(); + }} + > + + + ); +}; + +export default ItalicToolButton; diff --git a/src/zui/ZUIEditor/EditorOverlays/index.tsx b/src/zui/ZUIEditor/EditorOverlays/index.tsx index 390fa992c..f8acc14df 100644 --- a/src/zui/ZUIEditor/EditorOverlays/index.tsx +++ b/src/zui/ZUIEditor/EditorOverlays/index.tsx @@ -27,10 +27,17 @@ type Props = { id: string; label: string; }[]; + enableBold: boolean; + enableItalic: boolean; enableVariable: boolean; }; -const EditorOverlays: FC = ({ blocks, enableVariable }) => { +const EditorOverlays: FC = ({ + blocks, + enableBold, + enableItalic, + enableVariable, +}) => { const view = useEditorView(); const state = useEditorState(); const positioner = usePositioner('cursor'); @@ -145,6 +152,8 @@ const EditorOverlays: FC = ({ blocks, enableVariable }) => { diff --git a/src/zui/ZUIEditor/index.tsx b/src/zui/ZUIEditor/index.tsx index 4a7f405c2..2321df611 100644 --- a/src/zui/ZUIEditor/index.tsx +++ b/src/zui/ZUIEditor/index.tsx @@ -10,6 +10,7 @@ import { BulletListExtension, HardBreakExtension, HeadingExtension, + ItalicExtension, OrderedListExtension, } from 'remirror/extensions'; import { Extension, PasteRulesExtension } from 'remirror'; @@ -37,25 +38,31 @@ type BlockExtension = | BulletListExtension; type Props = { + enableBold?: boolean; enableButton?: boolean; enableHeading?: boolean; enableImage?: boolean; + enableItalic?: boolean; enableLists?: boolean; enableVariable?: boolean; }; const ZUIEditor: FC = ({ + enableBold, enableButton, enableHeading, enableImage, + enableItalic, enableLists, enableVariable, }) => { const messages = useMessages(messageIds.editor); const theme = useTheme(); + const boldExtension = new BoldExtension({}); const btnExtension = new ButtonExtension(); const imgExtension = new ImageExtension({}); + const italicExtension = new ItalicExtension(); const olExtension = new OrderedListExtension(); const ulExtension = new BulletListExtension({}); const headingExtension = new HeadingExtension({}); @@ -65,17 +72,26 @@ const ZUIEditor: FC = ({ last_name: messages.variables.lastName(), }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const otherExtensions: Extension[] = []; const blockExtensions: BlockExtension[] = []; - const otherExtensions: Extension[] = []; if (enableButton) { blockExtensions.push(btnExtension); } + if (enableBold) { + otherExtensions.push(boldExtension); + } + if (enableImage) { blockExtensions.push(imgExtension); } + if (enableItalic) { + otherExtensions.push(italicExtension); + } + if (enableHeading) { blockExtensions.push(headingExtension); } @@ -108,7 +124,6 @@ const ZUIEditor: FC = ({ }, extensions: () => [ new PasteRulesExtension({}), - new BoldExtension({}), ...blockExtensions, ...otherExtensions, new HardBreakExtension(), @@ -168,6 +183,8 @@ const ZUIEditor: FC = ({ id: ext.name, label: messages.blockLabels[ext.name](), }))} + enableBold={!!enableBold} + enableItalic={!!enableItalic} enableVariable={!!enableVariable} /> From 6dc94d1f1d26ddc0e81424eb4dbf0446c9aa1f3f Mon Sep 17 00:00:00 2001 From: Richard Olsson Date: Sat, 18 Jan 2025 16:21:10 +0100 Subject: [PATCH 6/7] Integrate link tool into same pattern as others --- .../[campId]/emails/[emailId]/newEditor.tsx | 1 + .../ZUIEditor/EditorOverlays/BlockToolbar.tsx | 69 +++---------------- .../EditorOverlays/LinkToolButton.tsx | 63 +++++++++++++++++ src/zui/ZUIEditor/EditorOverlays/index.tsx | 3 + src/zui/ZUIEditor/index.tsx | 14 ++-- 5 files changed, 85 insertions(+), 65 deletions(-) create mode 100644 src/zui/ZUIEditor/EditorOverlays/LinkToolButton.tsx diff --git a/src/pages/organize/[orgId]/projects/[campId]/emails/[emailId]/newEditor.tsx b/src/pages/organize/[orgId]/projects/[campId]/emails/[emailId]/newEditor.tsx index 9507b27fd..8e1eedfae 100644 --- a/src/pages/organize/[orgId]/projects/[campId]/emails/[emailId]/newEditor.tsx +++ b/src/pages/organize/[orgId]/projects/[campId]/emails/[emailId]/newEditor.tsx @@ -31,6 +31,7 @@ const EmailPage: PageWithLayout = () => { enableHeading enableImage enableItalic + enableLink enableLists enableVariable /> diff --git a/src/zui/ZUIEditor/EditorOverlays/BlockToolbar.tsx b/src/zui/ZUIEditor/EditorOverlays/BlockToolbar.tsx index c1b0ea215..43be0b3fe 100644 --- a/src/zui/ZUIEditor/EditorOverlays/BlockToolbar.tsx +++ b/src/zui/ZUIEditor/EditorOverlays/BlockToolbar.tsx @@ -1,18 +1,19 @@ import { Box, Button, Paper } from '@mui/material'; -import { useActive, useCommands, useEditorState } from '@remirror/react'; -import { FC, useEffect, useState } from 'react'; +import { useCommands } from '@remirror/react'; +import { FC } from 'react'; -import { NodeWithPosition } from '../LinkExtensionUI'; import VariableToolButton from './VariableToolButton'; import { VariableName } from '../extensions/VariableExtension'; import BoldToolButton from './BoldToolButton'; import ItalicToolButton from './ItalicToolButton'; +import LinkToolButton from './LinkToolButton'; type BlockToolbarProps = { curBlockType: string; curBlockY: number; enableBold: boolean; enableItalic: boolean; + enableLink: boolean; enableVariable: boolean; pos: number; }; @@ -22,40 +23,12 @@ const BlockToolbar: FC = ({ curBlockY, enableBold, enableItalic, + enableLink, enableVariable, pos, }) => { - const active = useActive(); - const state = useEditorState(); - const { - convertParagraph, - focus, - insertEmptyLink, - insertVariable, - toggleHeading, - pickImage, - removeLink, - removeAllLinksInRange, - setLink, - } = useCommands(); - - const [selectedNodes, setSelectedNodes] = useState([]); - - useEffect(() => { - const linkNodes: NodeWithPosition[] = []; - state.doc.nodesBetween( - state.selection.from, - state.selection.to, - (node, index) => { - if (node.isText) { - if (node.marks.some((mark) => mark.type.name == 'zlink')) { - linkNodes.push({ from: index, node, to: index + node.nodeSize }); - } - } - } - ); - setSelectedNodes(linkNodes); - }, [state.selection]); + const { convertParagraph, focus, insertVariable, toggleHeading, pickImage } = + useCommands(); return ( @@ -90,33 +63,7 @@ const BlockToolbar: FC = ({ - + {enableLink && } {enableBold && } {enableItalic && } diff --git a/src/zui/ZUIEditor/EditorOverlays/LinkToolButton.tsx b/src/zui/ZUIEditor/EditorOverlays/LinkToolButton.tsx new file mode 100644 index 000000000..7e03cac53 --- /dev/null +++ b/src/zui/ZUIEditor/EditorOverlays/LinkToolButton.tsx @@ -0,0 +1,63 @@ +import { IconButton } from '@mui/material'; +import { useActive, useCommands, useEditorState } from '@remirror/react'; +import { FC, useEffect, useState } from 'react'; +import { InsertLink, LinkOff } from '@mui/icons-material'; + +import { NodeWithPosition } from '../LinkExtensionUI'; + +const LinkToolButton: FC = () => { + const active = useActive(); + const state = useEditorState(); + const { focus, insertEmptyLink, removeAllLinksInRange, removeLink, setLink } = + useCommands(); + + const [selectedNodes, setSelectedNodes] = useState([]); + + useEffect(() => { + const linkNodes: NodeWithPosition[] = []; + state.doc.nodesBetween( + state.selection.from, + state.selection.to, + (node, index) => { + if (node.isText) { + if (node.marks.some((mark) => mark.type.name == 'zlink')) { + linkNodes.push({ from: index, node, to: index + node.nodeSize }); + } + } + } + ); + setSelectedNodes(linkNodes); + }, [state.selection]); + + return ( + { + if (!active.zlink()) { + if (state.selection.empty) { + insertEmptyLink(); + focus(); + } else { + setLink(); + focus(); + } + } else { + if (selectedNodes.length == 1) { + removeLink({ + from: selectedNodes[0].from, + to: selectedNodes[0].to, + }); + } else if (selectedNodes.length > 1) { + removeAllLinksInRange({ + from: state.selection.from, + to: state.selection.to, + }); + } + } + }} + > + {active.zlink() ? : } + + ); +}; + +export default LinkToolButton; diff --git a/src/zui/ZUIEditor/EditorOverlays/index.tsx b/src/zui/ZUIEditor/EditorOverlays/index.tsx index f8acc14df..8dfeaba9c 100644 --- a/src/zui/ZUIEditor/EditorOverlays/index.tsx +++ b/src/zui/ZUIEditor/EditorOverlays/index.tsx @@ -29,6 +29,7 @@ type Props = { }[]; enableBold: boolean; enableItalic: boolean; + enableLink: boolean; enableVariable: boolean; }; @@ -36,6 +37,7 @@ const EditorOverlays: FC = ({ blocks, enableBold, enableItalic, + enableLink, enableVariable, }) => { const view = useEditorView(); @@ -154,6 +156,7 @@ const EditorOverlays: FC = ({ curBlockY={currentBlock.rect.y} enableBold={enableBold} enableItalic={enableItalic} + enableLink={enableLink} enableVariable={enableVariable} pos={state.selection.$anchor.pos} /> diff --git a/src/zui/ZUIEditor/index.tsx b/src/zui/ZUIEditor/index.tsx index 2321df611..ef66f406c 100644 --- a/src/zui/ZUIEditor/index.tsx +++ b/src/zui/ZUIEditor/index.tsx @@ -13,7 +13,7 @@ import { ItalicExtension, OrderedListExtension, } from 'remirror/extensions'; -import { Extension, PasteRulesExtension } from 'remirror'; +import { AnyExtension, PasteRulesExtension } from 'remirror'; import { Box, useTheme } from '@mui/material'; import LinkExtension from './extensions/LinkExtension'; @@ -43,6 +43,7 @@ type Props = { enableHeading?: boolean; enableImage?: boolean; enableItalic?: boolean; + enableLink?: boolean; enableLists?: boolean; enableVariable?: boolean; }; @@ -53,6 +54,7 @@ const ZUIEditor: FC = ({ enableHeading, enableImage, enableItalic, + enableLink, enableLists, enableVariable, }) => { @@ -63,6 +65,7 @@ const ZUIEditor: FC = ({ const btnExtension = new ButtonExtension(); const imgExtension = new ImageExtension({}); const italicExtension = new ItalicExtension(); + const linkExtension = new LinkExtension(); const olExtension = new OrderedListExtension(); const ulExtension = new BulletListExtension({}); const headingExtension = new HeadingExtension({}); @@ -72,8 +75,7 @@ const ZUIEditor: FC = ({ last_name: messages.variables.lastName(), }); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const otherExtensions: Extension[] = []; + const otherExtensions: AnyExtension[] = []; const blockExtensions: BlockExtension[] = []; if (enableButton) { @@ -92,6 +94,10 @@ const ZUIEditor: FC = ({ otherExtensions.push(italicExtension); } + if (enableLink) { + otherExtensions.push(linkExtension); + } + if (enableHeading) { blockExtensions.push(headingExtension); } @@ -127,7 +133,6 @@ const ZUIEditor: FC = ({ ...blockExtensions, ...otherExtensions, new HardBreakExtension(), - new LinkExtension(), new BlockMenuExtension({ blockFactories: { bulletList: (props) => ulExtension.toggleBulletList()(props), @@ -185,6 +190,7 @@ const ZUIEditor: FC = ({ }))} enableBold={!!enableBold} enableItalic={!!enableItalic} + enableLink={!!enableLink} enableVariable={!!enableVariable} /> From 1442e014af7b2b943240f85f9eff05ff9c0ed975 Mon Sep 17 00:00:00 2001 From: Richard Olsson Date: Tue, 21 Jan 2025 12:58:53 +0100 Subject: [PATCH 7/7] Rename insertButton command and internationalize label --- .../ZUIEditor/extensions/ButtonExtension.tsx | 53 +++++++++---------- src/zui/ZUIEditor/index.tsx | 5 +- src/zui/l10n/messageIds.ts | 3 ++ 3 files changed, 32 insertions(+), 29 deletions(-) diff --git a/src/zui/ZUIEditor/extensions/ButtonExtension.tsx b/src/zui/ZUIEditor/extensions/ButtonExtension.tsx index 1c9fe8b2a..9700dc4a1 100644 --- a/src/zui/ZUIEditor/extensions/ButtonExtension.tsx +++ b/src/zui/ZUIEditor/extensions/ButtonExtension.tsx @@ -69,6 +69,31 @@ class ButtonExtension extends NodeExtension { return [ExtensionTag.Block, ExtensionTag.FormattingNode]; } + /* eslint-disable @typescript-eslint/ban-ts-comment */ + //@ts-ignore + @command() + insertButton(text: string): CommandFunction { + return (props) => { + const { dispatch, state, tr } = props; + const newNode = this.type.create(null, this.type.schema.text(text)); + if (dispatch) { + const pos = state.selection.$from.pos; + const parentOffset = state.doc.resolve(pos).parentOffset; + const blockLength = 1; + + tr.insert(pos - parentOffset - blockLength, newNode); + + const resolved = tr.doc.resolve(pos); + tr.setSelection( + TextSelection.create(tr.doc, resolved.start(), resolved.end()) + ); + dispatch(tr); + } + + return true; + }; + } + get name() { return 'zbutton' as const; } @@ -99,34 +124,6 @@ class ButtonExtension extends NodeExtension { return true; }; } - - /* eslint-disable @typescript-eslint/ban-ts-comment */ - //@ts-ignore - @command() - toggleButton(): CommandFunction { - return (props) => { - const { dispatch, state, tr } = props; - const newNode = this.type.create( - null, - this.type.schema.text('Foo button') - ); - if (dispatch) { - const pos = state.selection.$from.pos; - const parentOffset = state.doc.resolve(pos).parentOffset; - const blockLength = 1; - - tr.insert(pos - parentOffset - blockLength, newNode); - - const resolved = tr.doc.resolve(pos); - tr.setSelection( - TextSelection.create(tr.doc, resolved.start(), resolved.end()) - ); - dispatch(tr); - } - - return true; - }; - } } declare global { diff --git a/src/zui/ZUIEditor/index.tsx b/src/zui/ZUIEditor/index.tsx index ef66f406c..d1b1cf8ab 100644 --- a/src/zui/ZUIEditor/index.tsx +++ b/src/zui/ZUIEditor/index.tsx @@ -138,7 +138,10 @@ const ZUIEditor: FC = ({ bulletList: (props) => ulExtension.toggleBulletList()(props), heading: (props) => headingExtension.toggleHeading()(props), orderedList: (props) => olExtension.toggleOrderedList()(props), - zbutton: (props) => btnExtension.toggleButton()(props), + zbutton: (props) => + btnExtension.insertButton(messages.extensions.button.defaultText())( + props + ), zimage: (props) => imgExtension.createAndPick()(props), }, }), diff --git a/src/zui/l10n/messageIds.ts b/src/zui/l10n/messageIds.ts index a0ce228ad..3d8ca1d35 100644 --- a/src/zui/l10n/messageIds.ts +++ b/src/zui/l10n/messageIds.ts @@ -135,6 +135,9 @@ export default makeMessages('zui', { zimage: m('Image'), }, extensions: { + button: { + defaultText: m('Button text'), + }, link: { apply: m('Apply'), cancel: m('Cancel'),