我们的富文本编辑器不仅需要行内样式和块级样式。例如在 Facebook 的评论框中,我们使用蓝色背景来高亮提及和话题。

为了支持自定义富文本的灵活性,Draft.js 提供了一个 decrator 系统。Tweet示例 提供了一个实际使用decrator 的例子。

CompositeDecorator

Decorator 基于扫描给定 ContentBlock 的内容,找到满足与定义的策略匹配的文本范围,然后使用指定的 React 组件呈现它们。

可以使用 CompositeDecorator 类定义所需的装饰器行为。 此类允许你提供多个 DraftDecorator 对象,并依次搜索每个策略的文本块。

Decrator 保存在 EditorState 记录中。当新建一个 EditorState 对象时,例如使用EditorState.createEmpty() ,可以提供一个 decorator 。

幕后

在 draft 编辑器中,当内容发生改变的时候,新的 EditorState 对象会使用 decorator 重新评估新的ContentState 标示出要使用 decorated 的范围。形成一个包含块级样式、行内样式和 decrator 的完整状态树,来提供渲染的基础。 我们通过这样的方式确保当内容改变时,decorations 和我们的 EditorState 同步。

在 Tweet 示例中,我们使用 CompositeDecorator 来查询以 @ # 开头的字符串:

  1. const compositeDecorator = new CompositeDecorator([
  2. {
  3. strategy: handleStrategy,
  4. component: HandleSpan,
  5. },
  6. {
  7. strategy: hashtagStrategy,
  8. component: HashtagSpan,
  9. },
  10. ]);

这个 compositeDecorator 会先扫描 @ 开头的字符串,之后扫描 # 开头的。

// 注意: 这里的正则表达式并没有很严谨,仅作示例!
const HANDLE_REGEX = /\@[\w]+/g;
const HASHTAG_REGEX = /\#[\w\u0590-\u05ff]+/g;
function handleStrategy(contentBlock, callback, contentState) {
  findWithRegex(HANDLE_REGEX, contentBlock, callback);
}
function hashtagStrategy(contentBlock, callback, contentState) {
  findWithRegex(HASHTAG_REGEX, contentBlock, callback);
}
function findWithRegex(regex, contentBlock, callback) {
  const text = contentBlock.getText();
  let matchArr, start;
  while ((matchArr = regex.exec(text)) !== null) {
    start = matchArr.index;
    callback(start, start + matchArr[0].length);
  }
}

这里 Strategy 函数给 callback 提供了匹配到文本的起始位置和结束位置。

Decorator 的组件

在你要使用 Decorator 的文本上,你需要提供一个React组件来渲染它们。这里通常使用简单的 span 标签和一些 css class 来实现。

在我们的例子中,CompositeDecorator 对象使用 HandleSpanHashtagSpan 这两个组件渲染。它们都是基础的无状态的组件。

const HandleSpan = props => {
  return (
    <span {...props} style={styles.handle}>
      {props.children}
    </span>
  );
};

const HashtagSpan = props => {
  return (
    <span {...props} style={styles.hashtag}>
      {props.children}
    </span>
  );
};

注意这里将 props.children 放在组件渲染内容中,这样确保我们要修饰的内容能够被呈现。

Decorator 组件会在 props 中接收各种各样的元数据,包括 contentState 的副本, entityKey (如果有的话), 以及 blockKey。关于Decorator 组件的 props 汇总,可以参考 DraftDecoratorComponentProps type

请注意, props.children 被传递到了渲染的输出。这是为了确保 text 被渲染到了装饰的 span 中。您可以对链接使用相同的方法,参照我们的 link example

CompositeDecorator 之外

提供给 EditorState 的 Decorator 对象只需要符合 DraftDecoratorType Flow 类型定义的期望,这意味着你可以创建任何你想要的 Decorator 类,只要它们与预期类型相匹配,则不受 CompositeDecorator 约束。

设置新的 Decorator

此外,还可以在 editorState 创建之后设置新的 Decorator。当然,是通过 immutable 的方式。

这意味着如果的 app 在运行中发现你的 decorator 不能满足需求了或者需要对 decorator 做一些修改,你可以新建一个 decorator 对象(或者使用 null 来移除所有 decorator )并且使用 EditorState.set() 设置新的 decorator。

例如,如果因为某些原因我们想移除编辑器中的 @ 提及,我们可以使用下面的方法:

function turnOffHandleDecorations(editorState) {
  const onlyHashtags = new CompositeDecorator([
    {
      strategy: hashtagStrategy,
      component: HashtagSpan,
    },
  ]);
  return EditorState.set(editorState, {decorator: onlyHashtags});
}

这个 editorState 中的 contentState 会被新的 decorator 重新修饰,并且 @ 提及将在下一次渲染时失效。

再次声明,由于不可变对象在数据上的持久性,上一个状态还是会保存在内存中。