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 75 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
75 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
93350a1
Refactor block factories to use unified command pattern
richardolsson Jan 18, 2025
d104bfe
Enable ordered and unordered lists
richardolsson Jan 18, 2025
1b858b8
Enable line-breaks in paragraphs and lists
richardolsson Jan 18, 2025
cf6db8a
Prevent block menu from opening in non-paragraph blocks
richardolsson Jan 18, 2025
f9dfe6b
Add bold and italic formatting with ToolbarButtons
richardolsson Jan 18, 2025
6dc94d1
Integrate link tool into same pattern as others
richardolsson Jan 18, 2025
230a33e
Fix positioning of block inserts
richardolsson Jan 18, 2025
c722009
Fix bug causing new paragraphs to replace images
richardolsson Jan 18, 2025
7db6618
Tweak styling of overlays
richardolsson Jan 18, 2025
809ab4a
Add link to placeholder to open block menu
richardolsson Jan 18, 2025
0aabdfc
Hide block menu when placeholder is visible
richardolsson Jan 18, 2025
1442e01
Rename insertButton command and internationalize label
richardolsson Jan 21, 2025
21b0d81
Merge pull request #2479 from zetkin/undocumented/zui-editor-li-ol-b-i
ziggabyte Jan 21, 2025
ef8c8f0
Merge pull request #2480 from zetkin/undocumented/zui-editor-overlays…
ziggabyte Jan 21, 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 />
</Box>
</>
);
};

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

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

type BlockDividerData = {
pos: number;
y: number;
};

const BlockInsert: FC = () => {
const view = useEditorView();
const state = useEditorState();
const [mouseY, setMouseY] = useState(-Infinity);
const { insertParagraph, focus } = useCommands();

useEffect(() => {
const handleMouseMove = (ev: Event) => {
if (ev.type == 'mousemove') {
const mouseEvent = ev as MouseEvent;
const editorRect = view.dom.getBoundingClientRect();
setMouseY(mouseEvent.clientY - editorRect.y);
}
};

view.root.addEventListener('mousemove', handleMouseMove);

return () => {
view.root.removeEventListener('mousemove', handleMouseMove);
};
}, [view.root]);

let pos = 0;
const blockDividers: BlockDividerData[] = [
{
pos: 0,
y: 0,
},
...state.doc.children.map((blockNode) => {
pos += blockNode.nodeSize;
const rect = view.coordsAtPos(pos - 1);

const containerRect = view.dom.getBoundingClientRect();

return {
pos: pos,
y: rect.bottom - containerRect.top,
};
}),
];

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;
90 changes: 90 additions & 0 deletions src/zui/ZUIEditor/BlockMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { Box, MenuItem, Paper } from '@mui/material';
import {
useCommands,
useExtensionEvent,
useMenuNavigation,
usePositioner,
} from '@remirror/react';
import { FC, useState } from 'react';

import BlockMenuExtension from './extensions/BlockMenuExtension';

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

const BlockMenu: FC<Props> = ({ blocks }) => {
const [query, setQuery] = useState<string | null>(null);
const [ignore, setIgnore] = useState(false);
const positioner = usePositioner('cursor');
const { insertBlock } = useCommands();

useExtensionEvent(BlockMenuExtension, 'onBlockQuery', (newQuery) => {
setQuery(newQuery);
if (newQuery == null) {
setIgnore(false);
}
});

const filteredBlocks = blocks.filter(
(block) =>
!query ||
query == '' ||
block.id.toLowerCase().startsWith(query.toLowerCase()) ||
block.label.toLowerCase().startsWith(query.toLowerCase())
);

const isOpen = query !== null && !ignore;

const menu = useMenuNavigation({
isOpen: isOpen,
items: filteredBlocks,
onDismiss: () => {
setQuery(null);
setIgnore(true);
return true;
},
onSubmit: (blockType) => {
setQuery(null);
insertBlock(blockType.id);
return true;
},
});

return (
<Box position="relative">
<Box
ref={positioner.ref}
sx={{
left: positioner.x,
position: 'absolute',
top: positioner.y,
}}
>
<Box {...menu.getMenuProps()}>
<Paper>
{isOpen &&
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>
</Box>
</Box>
);
};

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

const BlockToolbar: FC = () => {
const [typing, setTyping] = useState(false);
const [curBlockPos, setCurBlockPos] = useState<number>(-1);
const [curBlockType, setCurBlockType] = useState<string>();
const view = useEditorView();
const state = useEditorState();
const { convertParagraph, setImageFile, toggleHeading } = useCommands();

useEditorEvent('keyup', () => {
setTyping(true);
});

useEditorEvent('blur', () => {
setCurBlockPos(-1);
});

useEffect(() => {
if (view.root) {
const handleMove = () => {
setTyping(false);
};

view.root.addEventListener('mousemove', handleMove);

return () => view.root.removeEventListener('mousemove', handleMove);
}
}, [view.root]);

useEffect(() => {
let node: ProsemirrorNode | null = null;
let nodeElem: HTMLElement | null = null;
if (isNodeSelection(state.selection)) {
const elem = view.nodeDOM(state.selection.$from.pos);
if (elem instanceof HTMLElement) {
node = state.selection.node;
nodeElem = elem;
}
} else {
const result = findParentNode({
predicate: () => true,
selection: state.selection,
});

if (result) {
node = result.node;
let elem = view.nodeDOM(result.start);

while (
elem &&
elem.parentNode &&
elem.parentElement?.contentEditable != 'true'
) {
elem = elem.parentNode;
}

if (elem instanceof HTMLElement) {
nodeElem = elem;
}
}
}

if (node && nodeElem) {
const editorRect = view.dom.getBoundingClientRect();
const nodeRect = nodeElem.getBoundingClientRect();
setCurBlockPos(nodeRect.y - editorRect.y);
setCurBlockType(node.type.name);
}
}, [state.selection]);

const showBar =
curBlockType && curBlockPos >= 0 && view.hasFocus() && !typing;

return (
<Box position="relative">
<Box
sx={{
left: 0,
opacity: showBar ? 1 : 0,
pointerEvents: showBar ? 'auto' : 'none',
position: 'absolute',
top: curBlockPos - 50,
transition: 'opacity 0.5s',
zIndex: 10000,
}}
>
<Paper elevation={1} sx={{ p: 1 }}>
{curBlockType}
{curBlockType == 'zimage' && (
<Button onClick={() => setImageFile(null)}>Change image</Button>
)}
{curBlockType == 'heading' && (
<Button onClick={() => convertParagraph()}>
Convert to paragraph
</Button>
)}
{curBlockType == 'paragraph' && (
<Button onClick={() => toggleHeading()}>Convert to heading</Button>
)}
</Paper>
</Box>
</Box>
);
};

export default BlockToolbar;
Loading
Loading