Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New ZUIEditor component #2463

Open
wants to merge 61 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
b6bea4e
Add dependencies for Remirror.
ziggabyte Jan 8, 2025
8c27d22
Transfer @richardolsson 's experiment code into this project.
ziggabyte Jan 8, 2025
9888342
WIP commit for richard, working on inline link tool
ziggabyte Jan 9, 2025
dd6039a
Allow href attribute in schema
richardolsson Jan 10, 2025
4f84cd2
Add dummy content and debug output
richardolsson Jan 10, 2025
28c0443
Create crude toolbar container that just renders type of block
richardolsson Jan 10, 2025
0089719
Show/hide toolbar when interacting
richardolsson Jan 10, 2025
caccb54
Implement BlockInsert component which adds insert buttons between blocks
richardolsson Jan 10, 2025
a8042e2
Add empty paragraph when adding new block
richardolsson Jan 10, 2025
b7eaca2
Implement crude block menu extension
richardolsson Jan 11, 2025
007d7c7
Implement filtering in block menu
richardolsson Jan 11, 2025
89a5aaf
Move list of blocks to property
richardolsson Jan 11, 2025
93006c6
Fix bug causing empty paragraph when adding other blocks
richardolsson Jan 11, 2025
e3927c6
Tweak styling and initial state of buttons
richardolsson Jan 11, 2025
5469140
Create placeholder in empty paragraphs
richardolsson Jan 11, 2025
f3d977e
Create crude proof-of-concept image block
richardolsson Jan 11, 2025
93171d9
Internationalize block labels
richardolsson Jan 11, 2025
c2aa3a1
Add some configurability to ZUIEditor
richardolsson Jan 11, 2025
3bf9d18
More reliably identify block elements
richardolsson Jan 12, 2025
ce06215
Implement resetting image file via toolbar
richardolsson Jan 12, 2025
8dd3c58
Add heading extension
richardolsson Jan 12, 2025
b0159c1
Implement conversion between paragraph and heading
richardolsson Jan 12, 2025
662af5c
Hide block toolbar on blur
richardolsson Jan 12, 2025
6ed568a
Set cursor correctly after adding new block
richardolsson Jan 12, 2025
fad91cb
Refactor image extension to use event to open file dialog
richardolsson Jan 12, 2025
51eaeed
Add ZUIEditor documentation
richardolsson Jan 13, 2025
29196e0
Improve documentation
richardolsson Jan 13, 2025
ab6b2e8
Solve bug where image block was not inserted correctly.
ziggabyte Jan 13, 2025
5567346
Refactor to move tools into one and the same file.
ziggabyte Jan 13, 2025
d19c5ab
Move logic to control if BlockMenu is open into a hook and into Tools…
ziggabyte Jan 13, 2025
124b17d
Rename to EditorOverlays.
ziggabyte Jan 14, 2025
6d0a2c4
Create unified state for currently selected block.
ziggabyte Jan 14, 2025
f4f637e
Show little popup when creating link.
ziggabyte Jan 14, 2025
594feea
Handle pasting links by properly parsing attributes
richardolsson Jan 14, 2025
c440e0a
Handle pasting images properly
richardolsson Jan 15, 2025
86bf86a
Find and save the link nodes in the current selection.
ziggabyte Jan 15, 2025
74e19c8
Update href property on link mark.
ziggabyte Jan 16, 2025
40b5438
Update link text.
ziggabyte Jan 16, 2025
9ce1f2b
Remove single link.
ziggabyte Jan 16, 2025
799790d
Remove all links in a selection by clicking "link" in block toolbar.
ziggabyte Jan 16, 2025
769e187
Merge pull request #2476 from zetkin/undocumented/ZUI-editor-refactor
richardolsson Jan 17, 2025
26d63ab
Merge pull request #2468 from zetkin/undocumented/zui-editor-paste
richardolsson Jan 17, 2025
7b8ea7d
Merge branch 'undocumented/ZUI-text-editor' into undocumented/ZUI-edi…
richardolsson Jan 17, 2025
1cd4404
Add URL validation and formatting
richardolsson Jan 17, 2025
f5e7cef
Add button to test URL
richardolsson Jan 17, 2025
bbc6fd7
Tweak design of LinkExtensionUI
richardolsson Jan 17, 2025
3a7d41c
Implement creating link at empty selection
richardolsson Jan 17, 2025
5baef71
Create UI for setting text and href of buttons
richardolsson Jan 17, 2025
2dc623f
Break out button UI to separate component
richardolsson Jan 17, 2025
75aac71
Reuse TextAndHrefOverlay for links and buttons
richardolsson Jan 17, 2025
3213701
Hide overlay when clicking cancel
richardolsson Jan 17, 2025
541dc71
Use Popper to prevent overlay from rendering out of view
richardolsson Jan 17, 2025
c316654
Create extension for variables, with command to add variable
richardolsson Jan 17, 2025
3aaeee1
Internationalize labels in variable extension
richardolsson Jan 17, 2025
f1e1193
Add menu to select variable to be inserted
richardolsson Jan 17, 2025
fc7b254
Tweak styling of variables
richardolsson Jan 17, 2025
ae0134c
Fix logic for finding nodes to support nested nodes
richardolsson Jan 17, 2025
9a647da
Properly close menu when clicking outside
richardolsson Jan 17, 2025
bbebed7
Merge pull request #2477 from zetkin/undocumented/ZUI-editor-tools
richardolsson Jan 18, 2025
59ddd60
Merge branch 'undocumented/ZUI-text-editor' into undocumented/zui-edi…
richardolsson Jan 18, 2025
b7f4174
Merge pull request #2478 from zetkin/undocumented/zui-editor-variables
richardolsson Jan 18, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .prettierrc.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{
"singleQuote": true
"singleQuote": true,
"proseWrap": "always"
}
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@
"@nivo/pie": "^0.80.0",
"@nivo/radial-bar": "^0.80.0",
"@reduxjs/toolkit": "^1.8.6",
"@remirror/pm": "^3.0.0",
"@remirror/react": "^3.0.1",
"@remirror/react-editors": "^2.0.1",
"@remirror/react-ui": "^1.0.1",
"@types/dompurify": "^2.3.3",
"@types/mjml": "^4.7.4",
"copy-to-clipboard": "^3.3.1",
Expand Down Expand Up @@ -88,6 +92,7 @@
"remark-gfm": "^3.0.1",
"remark-parse": "^10.0.1",
"remark-slate": "^1.8.6",
"remirror": "^3.0.1",
"slate": "^0.94.1",
"slate-history": "^0.66.0",
"slate-react": "^0.98.3",
Expand Down
4 changes: 4 additions & 0 deletions src/features/emails/layout/EmailLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ const EmailLayout: FC<EmailLayoutProps> = ({
href: '/compose',
label: messages.tabs.compose(),
},
{
href: '/newEditor',
label: 'New editor',
},
];

if (email.processed) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Box } from '@mui/material';
import Head from 'next/head';
import { GetServerSideProps } from 'next';

import { PageWithLayout } from 'utils/types';
import EmailLayout from 'features/emails/layout/EmailLayout';
import ZUIEditor from 'zui/ZUIEditor';
import { scaffold } from 'utils/next';

export const getServerSideProps: GetServerSideProps = scaffold(
async () => {
return {
props: {},
};
},
{
authLevelRequired: 2,
}
);

const EmailPage: PageWithLayout = () => {
return (
<>
<Head>
<title>hejj</title>
</Head>
<Box>
<ZUIEditor enableButton enableHeading enableImage enableVariable />
</Box>
</>
);
};

EmailPage.getLayout = function getLayout(page) {
return <EmailLayout>{page}</EmailLayout>;
};

export default EmailPage;
64 changes: 64 additions & 0 deletions src/zui/ZUIEditor/ButtonExtensionUI.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { useCommands, useEditorState, useEditorView } from '@remirror/react';
import { FC, useEffect, useState } from 'react';
import { ProsemirrorNode } from 'remirror';

import TextAndHrefOverlay from './elements/TextAndHrefOverlay';
import formatUrl from 'utils/formatUrl';

const ButtonExtensionUI: FC = () => {
const state = useEditorState();
const view = useEditorView();
const { setButtonHref, setButtonText } = useCommands();

const [visible, setVisible] = useState(false);
const [text, setText] = useState('');
const [href, setHref] = useState('');

const selectionCoords = view.coordsAtPos(state.selection.$from.pos);
const editorRect = view.dom.getBoundingClientRect();

const left = selectionCoords.left - editorRect.left;
const top = selectionCoords.top - editorRect.top;

useEffect(() => {
const blockNodes: ProsemirrorNode[] = [];
state.doc.nodesBetween(state.selection.from, state.selection.to, (node) => {
if (!node.isText) {
blockNodes.push(node);
}
});

if (blockNodes.length == 1 && blockNodes[0].type.name == 'zbutton') {
const buttonNode = blockNodes[0];
setText(buttonNode.textContent || '');
setHref(buttonNode.attrs.href || '');
setVisible(true);
} else {
setVisible(false);
}
}, [state.selection]);

return (
<TextAndHrefOverlay
href={href}
onCancel={() => {
setVisible(false);
}}
onChangeHref={(href) => setHref(href)}
onChangeText={(text) => setText(text)}
onSubmit={() => {
const formattedHref = formatUrl(href);
if (formattedHref) {
setButtonText(state.selection.$head.pos, text);
setButtonHref(state.selection.$head.pos, formattedHref);
}
}}
open={visible}
text={text}
x={left}
y={top + 30}
/>
);
};

export default ButtonExtensionUI;
64 changes: 64 additions & 0 deletions src/zui/ZUIEditor/EditorOverlays/BlockInsert.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { Add } from '@mui/icons-material';
import { IconButton, Paper } from '@mui/material';
import { Box } from '@mui/system';
import { useCommands } from '@remirror/react';
import { FC } from 'react';

import { BlockDividerData } from './index';

type BlockInsertProps = {
blockDividers: BlockDividerData[];
mouseY: number;
};

const BlockInsert: FC<BlockInsertProps> = ({ blockDividers, mouseY }) => {
const { insertParagraph, focus } = useCommands();

return (
<Box position="relative">
{blockDividers.map(({ pos, y }, index) => {
const visible = Math.abs(mouseY - y) < 20;
const isFirst = index == 0;
const offset = isFirst ? -6 : 12;
return (
<Box
key={index}
sx={{
bgcolor: 'red',
display: 'flex',
height: '1px',
justifyContent: 'center',
opacity: visible ? 1 : 0,
position: 'absolute',
top: Math.round(y + offset),
transition: 'opacity 0.5s',
width: '100%',
}}
>
<Box
sx={{
pointerEvents: visible ? 'auto' : 'none',
position: 'relative',
top: -16,
}}
>
<Paper>
<IconButton
disabled={!insertParagraph.enabled(' ', { selection: pos })}
onClick={() => {
insertParagraph(' ', { selection: pos });
focus(pos);
}}
>
<Add />
</IconButton>
</Paper>
</Box>
</Box>
);
})}
</Box>
);
};

export default BlockInsert;
37 changes: 37 additions & 0 deletions src/zui/ZUIEditor/EditorOverlays/BlockMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Box, MenuItem, Paper } from '@mui/material';
import { FC } from 'react';

import useBlockMenu from './useBlockMenu';

type Props = {
blocks: {
id: string;
label: string;
}[];
};

const BlockMenu: FC<Props> = ({ blocks }) => {
const { filteredBlocks, menu } = useBlockMenu(blocks);

return (
<Box {...menu.getMenuProps()}>
<Paper>
{filteredBlocks.map((item, index) => {
const props = menu.getItemProps({ index, item });
return (
<MenuItem
key={item.id}
component="a"
selected={!!props['aria-current']}
{...props}
>
{item.label}
</MenuItem>
);
})}
</Paper>
</Box>
);
};

export default BlockMenu;
132 changes: 132 additions & 0 deletions src/zui/ZUIEditor/EditorOverlays/BlockToolbar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { Box, Button, Paper } from '@mui/material';
import { useActive, useCommands, useEditorState } from '@remirror/react';
import { FC, useEffect, useState } from 'react';

import { NodeWithPosition } from '../LinkExtensionUI';
import VariableToolButton from './VariableToolButton';
import { VariableName } from '../extensions/VariableExtension';

type BlockToolbarProps = {
curBlockType: string;
curBlockY: number;
enableVariable: boolean;
pos: number;
};

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

return (
<Box position="relative">
<Box
sx={{
left: 0,
position: 'absolute',
top: curBlockY - 50,
transition: 'opacity 0.5s',
zIndex: 10000,
}}
>
<Paper elevation={1}>
<Box alignItems="center" display="flex" padding={1}>
{curBlockType}
{curBlockType == 'zimage' && (
<Button
onClick={() => {
pickImage(pos);
}}
>
Change image
</Button>
)}
{curBlockType == 'heading' && (
<Button onClick={() => convertParagraph()}>
Convert to paragraph
</Button>
)}
{curBlockType == 'paragraph' && (
<>
<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>
</>
)}
{enableVariable &&
(curBlockType == 'paragraph' || curBlockType == 'heading') && (
<VariableToolButton
onSelect={(varName: VariableName) => {
insertVariable(varName);
focus();
}}
/>
)}
</Box>
</Paper>
</Box>
</Box>
);
};

export default BlockToolbar;
Loading
Loading