如何开发富文本编辑器框架(三)——了解Decoration、Inline、Block、Document模型

3,420 阅读4分钟

目录

Document、Block、Inline模型

19年10月份改版之前,Slate 里是这几个 model,之后改版取消了这些,统一使用新的 Element 模型。

DocumentBlockInline 都是一个 Node 的子类。

new Node({
    type: string;
    object: 'document' | 'block' | 'inline';
    data: Map<string, any>;
    isVoid: boolean;
})

type 是节点类型,比如 div p span 等。

dataimmutable 里的 Map 对象,里面可以存放自定义的属性,比如存放 imgsrctableborder等。

isVoid 是用来标记该节点是否是闭合标签,或者你理解为有没有子节点。比如🏞 <img />isVoid = true。虽然说是没有子节点,但实际上,我们会在数据结构里给它加一个空的 Text的子节点,以便于选区操作(图片定位、图片选中等效果)。

Slate 原先采用的是 immutable 的数据结构,后来在很多使用者和贡献者的建议下,改用的 immer。如果你自己开发框架的话,你可以考虑就用普通的对象、数组来实现,或者用 immerimmutable, 一方面学习成本比较高,对开发者和使用者同样适用。另一方面,某些场景下内存使用以及性能上似乎也有问题, 部分内存无法回收。

这三个节点类型是父子级的关系。Document 的直接子节点只允许是 Block 节点,Block 的子节点不能存在 Document 节点,Inline 的子节点不能存在 DocumentBlock 节点。

这样的好处是,便于内置的 Schema 对文档 “净化”,你的文档不会出现 <span><p>1111</p></span> 这样的 html 片段。相反,缺点在我看来就是强约定的限制。很多时候我们的 html 片段没有我们想象的那么“优雅”,Schema的“净化”可能导致不可预知的bug。而且实际使用中,有很多html标签我们没法界定它是 Block 节点还是 Inline 节点,比如 <img />,有的时候我们希望它是 Block节点,独占一行。有的时候我们又希望它是Inline 节点,可以图文同行显示。这大概也是最新的 Slate 统一使用 Element 来表示节点的原因(Slate: 你觉得imginline 节点,那就 inline 节点吧,我不关心了🙃)。

Decoration

Decoration 的作用有些类似 Mark,也是文本修饰,是一个 text-level 层级的model,只作用于文本。

new Decoration({
    anchor: Point;
    focus: Point;
    mark: Mark;
})

和之前讲的 Mark的区别在于,它主要是针对的是“看见”的那一部分 Leaf 节点的的渲染方式,并不会更改文档数据。从官方demo来看,主要是通过改变样式来做搜索的高亮显示,关键字显示🚥等。

新版的 Decoration 移除了mark, 改成了用户自定义的type,如boldhighlight

new Decoration({
    anchor: Point;
    focus: Point;
    [type: string]: string;
})

// 根据关键字,提取 Decorations
const getDecorations = (node) => {
    const ranges = [];

    if (search && Text.isText(node)) {
        const { text } = node
        const parts = text.split(search) // 如果 Text 节点的文本包含 关键字 内容,则提取Decorations
        let offset = 0
        
        parts.forEach((part, i) => {
          if (i !== 0) {
            ranges.push({
              anchor: { path, offset: offset - search.length },
              focus: { path, offset },
              highlight: true,
            })
          }
        
          offset = offset + part.length + search.length
        })
    }
    
    return ranges
}

框架会根据 Decorationanchorfocus,找到对应的 Text 节点,并切割它的 leaves。之后,你可以覆盖 Leaf 的渲染方式,得到预期的高亮显示。

// 自定义Leaf的渲染方式
const Leaf = ({ attributes, children, leaf }) => {
  return (
    <span
      {...attributes}
      className={css`
        font-weight: ${leaf.bold && 'bold'};
        background-color: ${leaf.highlight && '#ffeeba'};
      `}
    >
      {children}
    </span>
  )
}
// 参考新版的 Slate 的demo
<Editable renderLeaf={props => <Leaf {...props} />} />

// 渲染 Text 节点的代码大致类似这样
const Text = ({node, attributes}) => {
    const { leaves } = node;
    const decs = getDecorations(node);
    cosnt leaves = node.getLeaves(decs); // 如果存在descorations,则分割内部的leaves。
    
    const children = leaves.map((leaf) => {
        const child = renderLeaf(leaf);
        return child
    });
    return (
        <span {...attributes}>
            {children}
        </span>
    );
}

差不多这几个模型就介绍到这里,如有不正确的地方,👏评论指出。