editor:编辑器

编辑一侧的全部内容:EditorCore(配置与节点工厂)、EditorComponent(React 组件与树操作)、 插件系统、Slate 节点工具函数,以及编辑器状态 hooks。

EditorCore

EditorCore 是编辑一侧的入口对象:应用启动时构造一次,之后全程共用。 三个构造参数与教程第 3 章的用法一致:

new EditorCore({
    renderers: EditorRendererDict,           // {group: {一级概念名: 渲染器}, ...}
    default_renderers: EditorDefaultRendererDict, // 每种节点类型一个默认渲染器
    printer: Printer,                        // 概念定义从这里查询
})

它的方法分两类。一类是节点工厂(create_ 系列):按二级概念名创建各种新节点, idx、占位子节点这些细节都会自动填好,程序化构造文档内容时用它们,而不要手写对象字面量。 另一类是查询:概念清单、渲染器、关联的印刷器。

方法说明
create_text(text?) / create_paragraph(text?)创建文本、段落节点
create_group(name, relation?)按二级概念名创建组节点,relation 缺省为 "separating"
create_inline(name, text?) / create_support(name) / create_structure(name, relation?) / create_abstract(name)创建其余各类概念节点,同样按二级概念名
get_sec_concept_list(type) / get_fst_concept_list(type)列出某节点种类下注册的二级、一级概念名
get_first_renderer(type, fst?) / get_second_renderer(type, sec?) / get_node_renderer(node)渲染器查询,查不到时返回默认渲染器
get_printer()取得关联的印刷器
get_meta_param(node)取得节点一级概念的元参数

EditorComponent

编辑器的 React 组件(类组件)。实际使用中你通常挂载的是它的包装 DefaultEditorComponent(见默认实现那页), 两者共享同一套 props:

interface EditorComponentProps {
    editorcore: EditorCore
    plugin?: EditorPlugin                 // 自定义插件,叠加在内置插件之外
    init_rootchildren?: AbstractNode["children"]        // 初始文档内容
    init_rootproperty?: Omit<AbstractNode, "children">  // 初始根节点属性
    onUpdate?: (v: any) => void           // 树每次更新时回调
    onKeyDown? / onKeyUp?: (e) => void    // 键盘事件回调
    onFocusChange?: (editor?) => void     // 光标位置变化回调
}
实例方法说明
get_root(): AbstractNode把当前编辑内容组装成一棵完整的文档树,保存文档的入口
get_core() / get_slate()取得 EditorCore、底层的 Slate 编辑器对象
get_cur_node() / get_cur_concept_node()取得光标所在的节点、光标所在的最近的概念节点
get_node_by_path(path)按路径取节点
new_concept_node(type, sec_name)在当前位置插入指定二级概念的新节点(侧边栏插入按钮的底层实现)
set_root(property)修改根节点的属性
add_apply_callback(cb)注册一个在下一次 Slate apply 之后执行的回调

树操作方法

以下方法也在 EditorComponent 上(通过 mixin 混入,源码在 treeopmixin), 是对 Slate Transforms 的概念级封装。写插件和自定义按钮时主要打交道的就是它们:

方法说明
set_node(node, partial) / set_node_by_path(path, partial)修改节点属性
auto_set_parameter(node, parameters)修改节点参数,自动处理代理节点的情形
add_nodes(nodes, path) / add_nodes_here(nodes)插入节点:指定路径,或当前光标处
add_nodes_before(nodes, target) / add_nodes_after(nodes, target)在目标节点前、后插入
delete_concept_node(node) / delete_node_by_path(path) / delete_nodes_by_paths(paths)删除节点。批量版本按显式路径删除,不依赖当前选区(在 effect 等无焦点场景也能工作),无效路径会被静默忽略
unwrap_node(node)删除节点但保留其子节点
move_concept_node(node, posto) / move_node_by_path(from, to)移动节点
replace_nodes(father, nodes)替换某节点的全部子节点
wrap_selected_nodes(node, opts) / wrap_nodes(node, from, to, opts)把当前选区、指定范围包进一个新节点

编辑渲染器

编辑渲染器本质上就是一个 React 组件,接收编辑器、节点和子内容三样东西:

type EditorRenderer<NT extends Node = Node> =
    (props: EditorRendererProps<NT>) => React.ReactElement

interface EditorRendererProps<NT> {
    editor: EditorComponent
    node: NT
    children: React.ReactNode   // Slate 提供的子内容,渲染器必须把它输出出去
}

这是编辑渲染器的底层形态。日常开发中很少直接手写, 一般用默认实现提供的工厂函数生成,那些工厂已经处理好了按钮、参数编辑等公共部分。

插件系统

插件是一个包装 Slate 编辑器的函数:

type EditorPlugin =
    (editor: EditorComponent, slate: SlateReact.ReactEditor) => SlateReact.ReactEditor

插件接收编辑器和 Slate 编辑器对象,返回改造后的 Slate 编辑器, 与 Slate 生态的插件写法一致。编写方法见教程第 7 章。相关导出:

Slate 节点工具函数

在插件和按钮里检查 Slate 节点时的常用函数,都以 slate_ 开头:

函数说明
slate_is_concept(node, type?)是否为概念节点,第二个参数可以限定节点种类
slate_is_paragraph / slate_is_text是否为段落、文本节点
slate_get_node_type(node)取节点类型字符串
slate_is_same_concept_node(a, b)按 idx 判断两个引用是否指向同一个节点
slate_concept_node2path(root, node)求节点在树中的路径
slate_concept_father(root, node) / slate_concept_father_path(root, path)求最近的概念祖先节点
slate_idx_to_node(root, idx)按 idx 查找节点

编辑器状态 hooks

在编辑器之外的组件(比如你放在页面其他位置的工具栏)想感知编辑器状态时,用这些 hooks:

Hook说明
useEditor()在编辑渲染器内部取当前的 EditorComponent;不在编辑器上下文内调用会抛错
useCurEditor()在任意位置取当前编辑器,基于 Zustand 实现,随编辑器版本刷新
useCurConceptNode()取光标所在的概念节点,节点变化时触发重渲染
useCurConceptNodeIdxParam()同上,但只在节点的 idx 或参数变化时才触发重渲染,开销更小
useEditorState底层的 Zustand store,需要更精细的订阅控制时使用