You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Slate编辑器递进缩进实现及Tab键生成多级编号列表问题

Slate编辑器递进式缩进与多级编号列表实现方案

1. 实现递进式缩进

要实现列表项的递进缩进,核心逻辑是将当前选中的list-item节点嵌套到上一级列表项的子列表中,以下是具体实现:

辅助判断函数

先编写函数判断当前节点是否具备缩进条件:

import { Editor, Element as SlateElement } from 'slate';

const LIST_TYPES = ['numbered-list', 'bulleted-list'];

// 判断当前选中节点是否为列表项且存在可缩进的父级列表
const canIndent = (editor) => {
  const { selection } = editor;
  if (!selection) return false;
  
  const [match] = Array.from(
    Editor.nodes(editor, {
      match: n => !Editor.isEditor(n) && SlateElement.isElement(n) && n.type === 'list-item',
    })
  );
  
  if (!match) return false;
  
  const [, parentPath] = Editor.parent(editor, match[1]);
  const parentNode = Editor.node(editor, parentPath)[0];
  
  return LIST_TYPES.includes(parentNode.type);
};

缩进处理函数

编写核心缩进逻辑,将列表项移动到上一级列表项的子列表中:

export const indent = (editor) => {
  if (!canIndent(editor)) return;
  
  const { selection } = editor;
  if (!selection) return;
  
  // 获取当前选中的列表项及其路径
  const [listItem, listItemPath] = Array.from(
    Editor.nodes(editor, {
      match: n => !Editor.isEditor(n) && SlateElement.isElement(n) && n.type === 'list-item',
    })
  )[0];
  
  // 获取父列表的类型
  const [parentList] = Editor.parent(editor, listItemPath);
  const listType = parentList.type;
  
  // 移除当前列表项从原父列表
  Transforms.removeNodes(editor, { at: listItemPath });
  
  // 获取上一个同级列表项路径
  const prevSiblingPath = Editor.previous(editor, { at: listItemPath });
  if (!prevSiblingPath) return;
  
  const [targetListItem] = Editor.node(editor, prevSiblingPath);
  
  // 检查目标列表项是否已有子列表
  const existingChildList = targetListItem.children.find(
    child => SlateElement.isElement(child) && LIST_TYPES.includes(child.type)
  );
  
  if (existingChildList) {
    // 若已有子列表,将当前列表项追加到子列表末尾
    const childListIndex = targetListItem.children.findIndex(
      child => SlateElement.isElement(child) && LIST_TYPES.includes(child.type)
    );
    const childListPath = [...prevSiblingPath, childListIndex];
    Transforms.insertNodes(editor, listItem, { at: [...childListPath, existingChildList.children.length] });
  } else {
    // 若无子列表,创建新列表并包裹当前列表项
    const newChildList = {
      type: listType,
      children: [listItem],
    };
    Transforms.insertNodes(editor, newChildList, { at: [...prevSiblingPath, targetListItem.children.length] });
  }
  
  // 恢复选中状态到缩进后的列表项
  const newPath = existingChildList
    ? [...prevSiblingPath, childListIndex, existingChildList.children.length - 1]
    : [...prevSiblingPath, targetListItem.children.length - 1, 0];
  Transforms.select(editor, Editor.end(editor, newPath));
};

2. Tab键触发多级编号列表

通过绑定Tab键盘事件,触发缩进逻辑实现多级编号列表,同时补充Shift+Tab取消缩进的功能:

取消缩进函数

export const outdent = (editor) => {
  const { selection } = editor;
  if (!selection) return;
  
  // 获取当前列表项及其路径
  const [listItem, listItemPath] = Array.from(
    Editor.nodes(editor, {
      match: n => !Editor.isEditor(n) && SlateElement.isElement(n) && n.type === 'list-item',
    })
  )[0];
  
  if (!listItem) return;
  
  // 获取父级子列表
  const [childList, childListPath] = Editor.parent(editor, listItemPath);
  if (!LIST_TYPES.includes(childList.type)) return;
  
  // 获取祖父级列表项
  const [parentListItem, parentListItemPath] = Editor.parent(editor, childListPath);
  if (parentListItem.type !== 'list-item') return;
  
  // 获取顶级父列表
  const [grandParentList, grandParentListPath] = Editor.parent(editor, parentListItemPath);
  if (!LIST_TYPES.includes(grandParentList.type)) return;
  
  // 移除当前列表项从子列表
  Transforms.removeNodes(editor, { at: listItemPath });
  
  // 将列表项插入到祖父级列表项的后方
  const parentListItemIndex = parentListItemPath[parentListItemPath.length - 1];
  Transforms.insertNodes(editor, listItem, { at: [...grandParentListPath, parentListItemIndex + 1] });
  
  // 若子列表为空则删除
  if (childList.children.length === 0) {
    Transforms.removeNodes(editor, { at: childListPath });
  }
  
  // 恢复选中状态
  Transforms.select(editor, Editor.end(editor, [...grandParentListPath, parentListItemIndex + 1]));
};

绑定键盘事件

在编辑器实例中绑定Tab和Shift+Tab事件:

import { withReact } from 'slate-react';

const withListShortcuts = (editor) => {
  const { onKeyDown } = editor;
  
  editor.onKeyDown = (event) => {
    if (event.key === 'Tab') {
      event.preventDefault();
      event.shiftKey ? outdent(editor) : indent(editor);
    }
  };
  
  return editor;
};

// 创建编辑器时应用快捷键插件
const editor = withListShortcuts(withReact(createEditor()));

3. 渲染优化与现有函数修正

多级列表渲染样式

为了让嵌套列表显示正确层级,给嵌套列表添加缩进样式:

const Element = ({ attributes, children, element }) => {
  switch (element.type) {
    case 'numbered-list':
      return (
        <ol style={{ marginLeft: '24px', paddingLeft: '0' }} {...attributes}>
          {children}
        </ol>
      );
    case 'bulleted-list':
      return (
        <ul style={{ marginLeft: '24px', paddingLeft: '0' }} {...attributes}>
          {children}
        </ul>
      );
    case 'list-item':
      return (
        <li style={{ marginLeft: '0', paddingLeft: '0' }} {...attributes}>
          {children}
        </li>
      );
    default:
      return <p {...attributes}>{children}</p>;
  }
};

修正toggleBlock函数

原函数会破坏嵌套列表结构,修改为仅处理当前节点的父列表:

export const toggleBlock = (editor, format) => {
  const isActive = isBlockActive(editor, format);
  const isList = LIST_TYPES.includes(format);

  // 仅unwrap当前选中节点所在的最低层级父列表
  const [match] = Array.from(Editor.nodes(editor, {
    match: n => LIST_TYPES.includes(n.type),
    mode: 'lowest',
  }));

  if (match) {
    Transforms.unwrapNodes(editor, { at: match[1] });
  }

  const newProperties = {
    type: isActive ? 'paragraph' : isList ? 'list-item' : format,
  };
  Transforms.setNodes(editor, newProperties);

  if (!isActive && isList) {
    const block = { type: format, children: [] };
    Transforms.wrapNodes(editor, block);
  }
};

内容的提问来源于stack exchange,提问作者LOVENEET SINGH

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.24 17:48:20