I have a simple text editor built with Slate.js in React
import { useCallback, useMemo, useState } from "react";
import isHotkey from "is-hotkey";
import { Editable, withReact, useSlate, Slate } from "slate-react";
import { Editor, Transforms, createEditor } from "slate";
import { withHistory } from "slate-history";
import { Button, Icon, Toolbar } from "./components";
import Summary from "./summary";
const HOTKEYS = {
"mod+b": "bold",
"mod+i": "italic",
"mod+u": "underline",
"mod+`": "code",
};
const LIST_TYPES = ["numbered-list", "bulleted-list"];
const initialValue = [
{
type: "paragraph",
children: [
{ text: "This is editable " },
{ text: "rich", bold: true },
{ text: " text, " },
{ text: "much", italic: true },
{ text: " better than a " },
{ text: "<textarea>", code: true },
{ text: "!" },
],
},
{
type: "paragraph",
children: [
{
text: "Since it's rich text, you can do things like turn a selection of text ",
},
{ text: "bold", bold: true },
{
text: ", or add a semantically rendered block quote in the middle of the page, like this:",
},
],
},
{
type: "block-quote",
children: [{ text: "A wise quote." }],
},
{
type: "paragraph",
children: [{ text: "Try it out for yourself!" }],
},
];
const RichTextExample = () => {
const [value, setValue] = useState(initialValue);
const renderElement = useCallback((props) => <Element {...props} />, []);
const renderLeaf = useCallback((props) => <Leaf {...props} />, []);
const editor = useMemo(() => withHistory(withReact(createEditor())), []);
return (
<Slate
editor={editor}
value={value}
onChange={(value) => setValue(value)}
initialValue={initialValue}
>
<Toolbar>
<MarkButton format="bold" icon="format_bold" />
<MarkButton format="italic" icon="format_italic" />
<MarkButton format="underline" icon="format_underlined" />
<MarkButton format="code" icon="code" />
<BlockButton format="heading-one" icon="looks_one" />
<BlockButton format="heading-two" icon="looks_two" />
<BlockButton format="block-quote" icon="format_quote" />
<BlockButton format="numbered-list" icon="format_list_numbered" />
<BlockButton format="bulleted-list" icon="format_list_bulleted" />
<BlockButton format="custom-content" icon="arrow_circle_up" />
</Toolbar>
<Editable
renderElement={renderElement}
renderLeaf={renderLeaf}
placeholder="Enter some rich text…"
spellCheck
autoFocus
onKeyDown={(event) => {
for (const hotkey in HOTKEYS) {
if (isHotkey(hotkey, event)) {
event.preventDefault();
const mark = HOTKEYS[hotkey];
toggleMark(editor, mark);
}
}
}}
/>
</Slate>
);
};
const toggleBlock = (editor, format) => {
const isActive = isBlockActive(editor, format);
const isList = LIST_TYPES.includes(format);
Transforms.unwrapNodes(editor, {
match: (n) => LIST_TYPES.includes(n.type),
split: true,
});
Transforms.setNodes(editor, {
type: isActive ? "paragraph" : isList ? "list-item" : format,
});
if (!isActive && isList) {
const block = { type: format, children: [] };
Transforms.wrapNodes(editor, block);
}
};
const toggleMark = (editor, format) => {
const isActive = isMarkActive(editor, format);
if (isActive) {
Editor.removeMark(editor, format);
} else {
Editor.addMark(editor, format, true);
}
};
const isBlockActive = (editor, format) => {
const [match] = Editor.nodes(editor, {
match: (n) => n.type === format,
});
return !!match;
};
const isMarkActive = (editor, format) => {
const marks = Editor.marks(editor);
return marks ? marks[format] === true : false;
};
const Element = ({ attributes, children, element }) => {
console.log(children);
switch (element.type) {
case "block-quote":
return <blockquote {...attributes}>{children}</blockquote>;
case "bulleted-list":
return <ul {...attributes}>{children}</ul>;
case "heading-one":
return <h1 {...attributes}>{children}</h1>;
case "heading-two":
return <h2 {...attributes}>{children}</h2>;
case "list-item":
return <li {...attributes}>{children}</li>;
case "numbered-list":
return <ol {...attributes}>{children}</ol>;
case "custom-content":
return <Summary {...attributes}>{children}</Summary>;
default:
return <p {...attributes}>{children}</p>;
}
};
const Leaf = ({ attributes, children, leaf }) => {
if (leaf.bold) {
children = <strong>{children}</strong>;
}
if (leaf.code) {
children = <code>{children}</code>;
}
if (leaf.italic) {
children = <em>{children}</em>;
}
if (leaf.underline) {
children = <u>{children}</u>;
}
return <span {...attributes}>{children}</span>;
};
const BlockButton = ({ format, icon }) => {
const editor = useSlate();
return (
<Button
active={isBlockActive(editor, format)}
onMouseDown={(event) => {
event.preventDefault();
toggleBlock(editor, format);
}}
>
<Icon>{icon}</Icon>
</Button>
);
};
const MarkButton = ({ format, icon }) => {
const editor = useSlate();
return (
<Button
active={isMarkActive(editor, format)}
onMouseDown={(event) => {
event.preventDefault();
toggleMark(editor, format);
}}
>
<Icon>{icon}</Icon>
</Button>
);
};
export default RichTextExample;
When I select text with a cursor (e.g. numbered list) I want to pass the whole list as children to the Summary component (renderElement function). However, when I select a list and click my custom button in the toolbar (the one on very right), editor renders several Summary components for each list item instead of one Summary component for whole selection.
In general I would like to have all selected nodes treated as a single child.
Structure of Summary is very simple
const Summary = (props) => {
return <details {...props.attributes}>{props.children}</details>;
};
Did anyone encounter similar issue?
