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.

Bilibili Tech
Bilibili Tech
Bilibili Tech
Building a Slate.js Rich Text Editor: Toolbar, Lists, and Image Upload

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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

frontendJavaScriptReactrich-text-editorSlate.jsimage uploadLists
Bilibili Tech
Written by

Bilibili Tech

Provides introductions and tutorials on Bilibili-related technologies.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.