Slate.js 入门(五)- 通过命令操作富文本编辑器

1,209 阅读3分钟

我正在参加「掘金·启航计划」

前言

到目前为止,我所写的文章都是对 Slate 富文本编辑器一次性逻辑的编写。但 Slate 的强大功能之一,是你可以根据自己的期望对富文本的「特定域」逻辑通过代码进行封装,从而进减少一次性代码的编写。

在之前的入门指南中,我们通过对 onKeyDown 事件的监听,实现了 Slate 富文本编辑器处理格式化代码块和粗体标记的功能,不过你会发现我们总是直接使用内置的编辑器助手来实现它,而不是使用「命令」的方式,比如通过工具栏按钮。

Slate 允许你拓展内置编辑器对象,来处理你的自定义富文本命令,甚至你可以使用预先打包的「插件」来实现一系列特定的功能。

我们从先前的应用例子开始,在此之前,可以先回顾一下之前的文章:

编辑器的命令

封装特定域

从上一个例子我们可以知道,富文本编辑器已具有「代码块」和「粗体格式」的概念。但是这些东西都是在 onKeyDown 处理程序中一次性定义的。如果你想在其他地方复用该逻辑,你需要将它单独抽离

我们可以创建自定义辅助函数来抽离逻辑,实现这些「特定域」:

const CustomEditor = {
    // 判断文本是否已加粗?
    isBoldMarkActive(editor) {
        const [match] = Editor.nodes(editor, {
            match: n => n.bold === true,
            universal: true,
        })
​
        return !!match
    },
​
    // 判断文本是否为"代码块"?
    isCodeBlockActive(editor) {
        const [match] = Editor.nodes(editor, {
            match: n => n.type === 'code',
        })
​
        return !!match
    },
​
    toggleBoldMark(editor) {
        const isActive = CustomEditor.isBoldMarkActive(editor)
        Transforms.setNodes(
            editor,
            { bold: isActive ? null : true },
            { match: n => Text.isText(n), split: true }
        )
    },
​
    toggleCodeBlock(editor) {
        const isActive = CustomEditor.isCodeBlockActive(editor)
        Transforms.setNodes(
            editor,
            { type: isActive ? null : 'code' },
            { match: n => Editor.isBlock(editor, n) }
        )
    },
}

onKeyDown 方法作出如下修改:

onKeyDown={(event) => {
    if (!event.ctrlKey) {
        return;
    }
​
    switch (event.key) {
        case "`": {
            event.preventDefault();
            CustomEditor.toggleCodeBlock(editor);
            break;
        }
​
        case "b": {
            event.preventDefault();
            CustomEditor.toggleBoldMark(editor);
            break;
        }
    }
}}

修改完成后,你会发现之前添加的「代码块」和「加粗」的功能依然可用,但我们将这些逻辑都封装到了 CustomEditor 对象里,代码可读性得到了提高,也方便开发者后续操作。

命令-工具栏按钮

你可以从任何能够访问到编辑器对象 editor 的地方调用它。例如,我们创建两个工具栏按钮,分别为「加粗」和「代码块」,完整代码如下:

const CustomEditor = {
    // 判断文本是否已加粗?
    isBoldMarkActive(editor) {
        const [match] = Editor.nodes(editor, {
            match: (n) => n.bold === true,
            universal: true,
        });
​
        return !!match;
    },
​
    // 判断文本是否为"代码块"?
    isCodeBlockActive(editor) {
        const [match] = Editor.nodes(editor, {
            match: (n) => n.type === "code",
        });
​
        return !!match;
    },
​
    toggleBoldMark(editor) {
        const isActive = CustomEditor.isBoldMarkActive(editor);
        Transforms.setNodes(
            editor,
            { bold: isActive ? null : true },
            { match: (n) => Text.isText(n), split: true }
        );
    },
​
    toggleCodeBlock(editor) {
        const isActive = CustomEditor.isCodeBlockActive(editor);
        Transforms.setNodes(
            editor,
            { type: isActive ? null : "code" },
            { match: (n) => Editor.isBlock(editor, n) }
        );
    },
};
​
const initialValue = [
    {
        type: "paragraph",
        children: [{ text: "这是一段初始文字。" }],
    },
];
​
const CodeElement = (props) => {
    return (
        <pre {...props.attributes} style={{ background: "#dadada" }}>
            <code>{props.children}</code>
        </pre>
    );
};
​
const DefaultElement = (props) => {
    return <p {...props.attributes}>{props.children}</p>;
};
​
const Leaf = (props) => {
    return (
        <span
            {...props.attributes}
            style={{ fontWeight: props.leaf.bold ? "bold" : "normal" }}
        >
            {props.children}
        </span>
    );
};
​
const App = () => {
    // 创建一个在渲染中保持不变的 slate editor 对象
    const [editor] = useState(() => withReact(createEditor()));
​
    const renderElement = useCallback((props) => {
        console.log("renderElement-props", props);
        switch (props.element.type) {
            case "code":
                return <CodeElement {...props} />;
            default:
                return <DefaultElement {...props} />;
        }
    }, []);
​
    const renderLeaf = useCallback((props) => {
        return <Leaf {...props} />;
    }, []);
​
    return (
        <div className="container">
            <h3>Slate 编辑器</h3>
            <div className="editor-wrapper">
                <Slate
                    editor={editor}
                    value={initialValue}
                    onChange={(v) => console.log(v)}
                >
                    <div>
                        <button
                            onMouseDown={(event) => {
                                event.preventDefault();
                                CustomEditor.toggleBoldMark(editor);
                            }}
                            style={{ marginRight: 12 }}
                        >
                            加粗
                        </button>
                        <button
                            onMouseDown={(event) => {
                                event.preventDefault();
                                CustomEditor.toggleCodeBlock(editor);
                            }}
                        >
                            代码块
                        </button>
                    </div>
                    <Editable
                        renderElement={renderElement}
                        renderLeaf={renderLeaf}
                        onKeyDown={(event) => {
                            if (!event.ctrlKey) {
                                return;
                            }
​
                            switch (event.key) {
                                case "`": {
                                    event.preventDefault();
                                    CustomEditor.toggleCodeBlock(editor);
                                    break;
                                }
​
                                case "b": {
                                    event.preventDefault();
                                    CustomEditor.toggleBoldMark(editor);
                                    break;
                                }
                            }
                        }}
                    />
                </Slate>
            </div>
        </div>
    );
};

接下来,我们就可以通过工具栏按钮,也就是「命令」的方式来控制文本的格式啦~

最终效果如下:

2022-09-29-10-03-56.gif

最后

接下来,我们在编辑器的后续开发中就能提高开发效率,即使添加了许多拓展功能,也不需要很大的工作量。除此之外,我们将所有的命令逻辑和可视化组件隔离出来,使得代码更易于维护。

参考

docs.slatejs.org/walkthrough…