Building a Slate.js Rich Text Editor: Toolbar, Lists, and Image Upload
This tutorial walks through the practical steps of extending a Slate.js rich‑text editor with toolbar actions, multi‑level list handling, and a robust image‑upload solution, providing code snippets, implementation details, and design considerations for each feature.
This article continues a previous tutorial on creating a rich‑text editor with Slate.js, focusing on concrete implementation details such as toolbar actions, mark handling, block handling, multi‑level lists, and image insertion.
Toolbar and Mark Handling
Toolbar buttons for bold, italic, and underline invoke toggleMark, adding a Mark to the selected Text node. The resulting Slate node structure and its rendering are illustrated with screenshots.
Block Handling
Buttons for headings, block‑quote, list, and alignment call toggleBlock. The core logic first converts the current node to plain text, then changes its type. The demo includes special handling for list elements to allow nesting without breaking the document structure.
Multi‑Level List Implementation
The required list behavior includes unordered bullets (solid, hollow, square) cycling every three levels and ordered lists using numbers, letters, and Roman numerals. The solution overrides Editor.insertBreak and Editor.deleteBackward and introduces helper functions withLists, indentItem, and undentItem. These functions detect the current list‑item, lift nodes on Enter, wrap nodes to increase depth, and remove indentation when needed.
export const withLists = (editor: Editor) => {
const { insertBreak, deleteBackward } = editor;
const doWithLists = (callback: Function) => {
const { selection } = editor;
if (selection && Range.isCollapsed(selection)) {
const [match] = Editor.nodes(editor, {
match: (n: any) => n.type === 'list-item' && n.children && n.children[0] && (!n.children[0].text || n.children[0].text === ''),
});
if (match) {
const [, path] = match;
const start = Editor.start(editor, path);
if (Point.equals(selection.anchor, start)) {
liftNodes(editor);
const [listMatch] = Editor.nodes(editor, { match: (n: any) => n.type === 'bulleted-list' || n.type === 'numbered-list' });
if (!listMatch) {
Transforms.setNodes(editor, { type: 'paragraph' }, { match: (n: any) => n.type === 'list-item' });
}
return;
}
}
}
callback();
};
editor.insertBreak = () => doWithLists(insertBreak);
editor.deleteBackward = (unit) => doWithLists(() => deleteBackward(unit));
return editor;
};Image Handling
Three approaches were evaluated; the third—batch upload with a floating panel and server polling—was selected for its better user experience despite higher complexity. Images are defined as void elements. withImages extends the editor’s isVoid method, and insertImage creates an ImageElement and inserts it at the current selection or at the end of the document.
const withImages = editor => {
const { isVoid } = editor;
editor.isVoid = element => (element.type === 'image' ? true : isVoid(element));
return editor;
};
export const insertImage = (editor: Editor, url: string) => {
const { selection } = editor;
const text = { text: '' };
const image: ImageElement = { type: 'image', url: trimHttp(url), desc: '', children: [text] };
if (selection) {
const [match] = Editor.nodes(editor, {
match: n => n.type === 'paragraph' && n.children && n.children[0] && (!n.children[0].text || n.children[0].text === ''),
});
if (match) {
Transforms.setNodes(editor, image, { mode: 'highest' });
} else {
Transforms.insertNodes(editor, image, { mode: 'highest' });
}
} else {
Transforms.insertNodes(editor, image, { mode: 'highest' });
}
};The rendering component displays the image with a selectable border and sets contentEditable=false to prevent stray cursors around the image.
Conclusion
The series now covers all core features of a Slate.js rich‑text editor. Further work such as shortcut keys, state switching between different formatting modes, and cross‑browser compatibility remains necessary, reflecting the ongoing challenges faced by front‑end engineers.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Bilibili Tech
Provides introductions and tutorials on Bilibili-related technologies.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
