定制默认编辑器

这一页收齐所有不必放弃默认实现就能用上的改造手段,按侵入程度从浅到深排列: 调样式配置、换主题、往侧边栏和节点上加按钮、替换个别渲染器, 最后是完全不用这一层时该从哪里接手。

样式配置

config 参数接受一份部分配置,只写想改的项,其余沿用默认值。 编辑器的配置分三块:

字段内容
marginsbackground(纸张与内部元素的距离)、paragraph(段落间距)、small(小间隔)
widthseditable_drawer(弹出抽屉的宽度)、minimum_content(有内容元素的最小宽度)
fontsbody(正文)、structure(结构性文字,比如节点标题)、info(提示性文字)

每种字体是一组 fontFamilyfontSizelineHeight 这样的字段。 三种字体的分工是:body 是写作者输入的内容, structure 是界面自己产生的文字(节点标题、按钮上的字), info 是辅助提示。

<DefaultEditorComponent
    editorcore={editor_core}
    config={{
        margins: {paragraph: "1rem"},
        fonts  : {body: {fontFamily: "Source Han Serif", fontSize: "1.1rem"}},
    }}
/>

这套配置和印刷器的配置是分开的两份,因为编辑界面和印刷成品本来就应该长得不一样: 编辑时你可能想要更大的行距方便点选,成品则要紧凑美观。印刷侧的配置见 印刷侧的默认实现

界面文字

默认实现界面上的全部文字(按钮提示、面板标题、操作后弹出的提示)都集中在一份文案表里, 默认是中文,通过 texts 参数部分覆盖,写法和 config 一样:

<DefaultEditorComponent
    editorcore={editor_core}
    texts={{
        buttons: {delete_node: "Delete node", unwrap_node: "Unwrap node"},
        areas  : {concept_title: "Insert Concept", parameter_title: "Edit Parameters"},
    }}
/>

完整的字段见 EditorTexts 类型,分成 buttons(按钮提示)、 areas(面板标题和概念类型标签)、messages(提示语)三块。 库本身不带任何 i18n 框架,所以你用什么方案管理翻译都可以,把最终的字符串传进来即可。 在线演示就是这么做的:中文用库的默认值,英文传一份覆盖,见 demo/src/editor_texts.ts

主题

默认实现全程使用 MUI 的主题:颜色、圆角、按钮样式都从主题里取,自己不写死任何颜色。 所以换配色只需要在外层套一个 ThemeProvider,包括深色模式在内, 编辑器和印刷器都会跟着变:

<ThemeProvider theme={createTheme({palette: {mode: "dark"}})}>
    <DefaultEditorComponent editorcore={editor_core} />
</ThemeProvider>

配置和主题的分工是:主题管颜色和控件外观,配置管字体、边距和宽度。 想改「定理块的背景色」找主题,想改「段落之间空多少」找配置。

侧边栏按钮与尾部元素

sidebar_extras 接受一个组件数组,追加到侧边栏那四个内置按钮之后, 中间自动加一条分隔线。追加的按钮会自动加入键盘导航(按住 Alt+E 后用上下方向键遍历), 不需要额外登记。按钮内部可以用 useEditor() 拿到编辑器:

import { useEditor, AutoIconButton } from "@project-callio/calliotext"
import { Download as DownloadIcon } from "lucide-react"

function ExportButton(){
    const editor = useEditor()
    return <AutoIconButton
        icon={DownloadIcon} title="导出" size="medium"
        onClick={() => {
            const root = editor.get_root()
            console.log(JSON.stringify(root))
        }}
    />
}

<DefaultEditorComponent editorcore={editor_core} sidebar_extras={[ExportButton]} />

AutoIconButton 是默认实现自己也在用的图标按钮,它会自动接上按键提示和主题, 用它做出来的按钮和内置按钮长得一样。当然你也可以直接返回一个 MUI 的 IconButton,只是提示和样式要自己管。

end_element 则是插在纸张下方的任意内容,适合放状态栏、字数统计这类东西。

节点上的按钮

侧边栏按钮作用于整个文档,如果你要的是「作用于某一个节点」的操作, 那应该加到节点自己的按钮组里,也就是渲染器工厂的 buttons_extra 选项。 这类按钮里可以用 useNode()useParameters() 拿到所属的节点:

function MarkDoneButton(){
    const editor = useEditor()
    const node   = useNode()
    return <AutoIconButton
        icon={CheckIcon} title="标记完成" size="medium"
        onClick={() => {
            editor.set_node(node, {parameters: {
                ...node.parameters,
                done: {val: true, type: "boolean"},
            }})
        }}
    />
}

const theorem_editor = get_deafult_group_editor_with_appbar({
    get_label    : () => useParameters().category,
    buttons_extra: [MarkDoneButton],
})

替换单个渲染器

如果某个概念的外观和工厂给的差得太远,可以只替换这一个渲染器,其余照旧。 渲染器就是一个普通的 React 组件,接收 editornodechildren 三个 props:

import { NodeInfoProvider, EditorRendererProps } from "@project-callio/calliotext"

const bare_editor = ({node, children}: EditorRendererProps) => (
    <NodeInfoProvider node={node}>
        <div style={{borderLeft: "3px solid #888", paddingLeft: "1rem"}}>
            {children}
        </div>
    </NodeInfoProvider>
)

自己写渲染器时有两条硬性要求。第一,必须把 children 输出到界面上, 否则子节点的内容会丢失,Slate 也会因为找不到对应的 DOM 而报错。 第二,如果你要在里面用 useParameters() 之类的 hook, 或者放默认实现的按钮,就得自己包一层 NodeInfoProvider, 因为这层原本是工厂替你包的。

还有一条不是硬性要求但值得遵守:把不该被光标选中的部分(按钮、标题) 包进 EditorUnselecableBox。不这样做,用户在节点开头按退格就有可能把这些界面元素删掉。

完全不用默认实现

最后一档是不要这一层,自己基于 EditorComponent 搭界面。 这时你需要自己处理的东西,正好就是总览那张表里列的: 样式配置的上下文、提示气泡容器、编号冲突修复、按键分发、以及把编辑器实例交给下游组件。 这些都是公开导出的,DefaultEditorComponent 的源码本身就是一份可以照抄的样板。