Skip to content

Commit

Permalink
Merge pull request #2479 from zetkin/undocumented/zui-editor-li-ol-b-i
Browse files Browse the repository at this point in the history
`ZUIEditor` tools: Ordered list, bullet list, bold and italic
  • Loading branch information
ziggabyte authored Jan 21, 2025
2 parents b7f4174 + 1442e01 commit 21b0d81
Show file tree
Hide file tree
Showing 11 changed files with 262 additions and 118 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,16 @@ const EmailPage: PageWithLayout = () => {
<title>hejj</title>
</Head>
<Box>
<ZUIEditor enableButton enableHeading enableImage enableVariable />
<ZUIEditor
enableBold
enableButton
enableHeading
enableImage
enableItalic
enableLink
enableLists
enableVariable
/>
</Box>
</>
);
Expand Down
77 changes: 16 additions & 61 deletions src/zui/ZUIEditor/EditorOverlays/BlockToolbar.tsx
Original file line number Diff line number Diff line change
@@ -1,55 +1,34 @@
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;
};

const BlockToolbar: FC<BlockToolbarProps> = ({
curBlockType,
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<NodeWithPosition[]>([]);

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 (
<Box position="relative">
Expand Down Expand Up @@ -84,33 +63,9 @@ const BlockToolbar: FC<BlockToolbarProps> = ({
<Button onClick={() => toggleHeading()}>
Convert to heading
</Button>
<Button
onClick={() => {
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,
});
}
}
}}
>
Link
</Button>
{enableLink && <LinkToolButton />}
{enableBold && <BoldToolButton />}
{enableItalic && <ItalicToolButton />}
</>
)}
{enableVariable &&
Expand Down
23 changes: 23 additions & 0 deletions src/zui/ZUIEditor/EditorOverlays/BoldToolButton.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<IconButton
color={active.bold() ? 'primary' : 'secondary'}
onClick={() => {
toggleBold();
focus();
}}
>
<FormatBold />
</IconButton>
);
};

export default BoldToolButton;
23 changes: 23 additions & 0 deletions src/zui/ZUIEditor/EditorOverlays/ItalicToolButton.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<IconButton
color={active.italic() ? 'primary' : 'secondary'}
onClick={() => {
toggleItalic();
focus();
}}
>
<FormatItalic />
</IconButton>
);
};

export default ItalicToolButton;
63 changes: 63 additions & 0 deletions src/zui/ZUIEditor/EditorOverlays/LinkToolButton.tsx
Original file line number Diff line number Diff line change
@@ -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<NodeWithPosition[]>([]);

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 (
<IconButton
onClick={() => {
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() ? <LinkOff /> : <InsertLink />}
</IconButton>
);
};

export default LinkToolButton;
14 changes: 13 additions & 1 deletion src/zui/ZUIEditor/EditorOverlays/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,19 @@ type Props = {
id: string;
label: string;
}[];
enableBold: boolean;
enableItalic: boolean;
enableLink: boolean;
enableVariable: boolean;
};

const EditorOverlays: FC<Props> = ({ blocks, enableVariable }) => {
const EditorOverlays: FC<Props> = ({
blocks,
enableBold,
enableItalic,
enableLink,
enableVariable,
}) => {
const view = useEditorView();
const state = useEditorState();
const positioner = usePositioner('cursor');
Expand Down Expand Up @@ -145,6 +154,9 @@ const EditorOverlays: FC<Props> = ({ blocks, enableVariable }) => {
<BlockToolbar
curBlockType={currentBlock.type}
curBlockY={currentBlock.rect.y}
enableBold={enableBold}
enableItalic={enableItalic}
enableLink={enableLink}
enableVariable={enableVariable}
pos={state.selection.$anchor.pos}
/>
Expand Down
47 changes: 16 additions & 31 deletions src/zui/ZUIEditor/extensions/BlockMenuExtension.ts
Original file line number Diff line number Diff line change
@@ -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<string, () => Node>;
blockFactories: Record<string, CommandFunction>;
onBlockQuery?: Handler<(query: string | null) => void>;
};

Expand All @@ -26,11 +23,17 @@ class BlockMenuExtension extends PlainExtension<BlockMenuOptions> {
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
);
},
},
];
Expand All @@ -40,31 +43,13 @@ class BlockMenuExtension extends PlainExtension<BlockMenuOptions> {
//@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);
};
}

Expand Down
24 changes: 20 additions & 4 deletions src/zui/ZUIEditor/extensions/ButtonExtension.tsx
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -63,17 +64,32 @@ class ButtonExtension extends NodeExtension<ButtonOptions> {
},
};
}

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));
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;
};
}
Expand Down
Loading

0 comments on commit 21b0d81

Please sign in to comment.