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
相关产品推荐
相关产品推荐

