富文本编辑框架 slate

425 阅读2分钟

Slate 是一个 完全 可定制的富文本编辑框架。

Slate 所有逻辑都是通过一系列的插件实现的,这样,你就再也不会被某项特性 在 或 不在 编辑器【核心】边界之内的问题所困扰了。你可以将它理解为在 React 和 Immutable 基础上,一种可插拔的 contenteditable 实现。另外,它的灵感来自于 Draft.js,Prosemirror 和 Quill 等类库。

正因如此,Slate 并不是开箱即用的,需要自己二次开发许多内容,可基于它定制自己的富文本编辑器。

slate.png

原则

  • 作为一等公民的插件。
  • 精简 Schema 的核心。
  • 支持嵌套的文档模型。
  • 无状态、不可变的数据。
  • 直观的 changes。
  • 为协同编辑准备的数据模型。
  • 明确的【核心】边界划分。

安装

pnpm i slate slate-react

数据结构

const initialValue = [
  {
    type: 'paragraph',
    children: [{text: 'This is editable '}, {text: 'rich', bold: true}, {text: ' text, '}, {text: 'much', italic: true}, {text: ' better than a '}, {text: '<textarea>', code: true}, {text: '!'}],
  },
  {
    type: 'paragraph',
    children: [
      {
        text: 'Since it\'s rich text, you can do things like turn a selection of text ',
      },
      {text: 'bold', bold: true},
      {
        text: ', or add a semantically rendered block quote in the middle of the page, like this:',
      },
    ],
  },
  {
    type: 'quote',
    children: [{text: 'A wise quote.'}],
  },
  {
    type: 'paragraph',
    children: [{text: 'Try it out for yourself!'}],
  },
];

使用

<Slate editor={editor} value={value} onChange={value => setValue(value)}>
  <Toolsbar />
  <Editable
    renderElement={renderElement}
    renderLeaf={params => <Leaf {...params} />}
    placeholder="Enter some rich text…"
    spellCheck
    autoFocus
    style={{border: '1px solid #f3f3f3', minHeight: 300}}
  />
</Slate>

自定义渲染:

const Leaf = ({attributes, children, leaf}) => {
  const {text, ...rest} = leaf;
  Object.keys(rest)
    .filter(Boolean)
    .map(key => {
      const LeafComp = elements[firstUpper(key)];
      children = LeafComp ? <LeafComp>{children}</LeafComp> : children;
    });
  return <span {...attributes}>{children}</span>;
};

const renderElement = ({element, ...rest}) => {
  const Comp = elements[firstUpper(element.type)] || elements.DefaultElement;
  return <Comp {...rest} />;
};

自定义元素

export const DefaultElement = ({attributes, children, ...rest}) => <div {...attributes} {...rest}>{children}</div>;

export const H1 = ({attributes, children}) => <h1 {...attributes}>{children}</h1>;
export const H2 = ({attributes, children}) => <h2 {...attributes}>{children}</h2>;
export const Ol = ({attributes, children}) => <ol {...attributes}>{children}</ol>;
export const Ul = ({attributes, children}) => <ul {...attributes}>{children}</ul>;
export const Li = ({attributes, children}) => <li {...attributes}>{children}</li>;
export const Bold = ({attributes, children}) => <b {...attributes}>{children}</b>;
export const Italic = ({attributes, children}) => <i {...attributes}>{children}</i>;
export const Underline = ({attributes, children}) => <u {...attributes}>{children}</u>;
export const Strikethrough = ({attributes, children}) => <s {...attributes}>{children}</s>;
export const Quote = ({attributes, children}) => <blockquote {...attributes}>{children}</blockquote>;
export const Code = ({attributes, children}) => <code {...attributes}>{children}</code>;

事件处理

选中与取消

export const toggleMark = (editor, format) => {
  const isActive = isMarkActive(editor, format);
  if (isActive) {
    Editor.removeMark(editor, format);
  } else {
    Editor.addMark(editor, format, true);
  }
};

export const isMarkActive = (editor, format) => {
  const marks = Editor.marks(editor);
  return marks ? marks[format] === true : false;
};

选中文本处理和行处理

const LIST_TYPES = ['ul', 'ol'];

export const toggleBlock = (editor, format) => {
  const isActive = isBlockActive(editor, format);
  const isList = LIST_TYPES.includes(format);
  Transforms.unwrapNodes(editor, {
    match: n => LIST_TYPES.includes(!Editor.isEditor(n) && Element.isElement(n) && n.type),
    split: true,
  });
  const newProperties = {
    type: isActive ? 'paragraph' : isList ? 'li' : format,
  };
  Transforms.setNodes(editor, newProperties);
  if (!isActive && isList) {
    const block = {type: format, children: []};
    Transforms.wrapNodes(editor, block);
  }
};

export const isBlockActive = (editor, format) => {
  const [match] = Editor.nodes(editor, {
    match: n => !Editor.isEditor(n) && Element.isElement(n) && n.type === format,
  });
  return !!match;
};

总结

Slate 使用灵活,可拓展性强,方便用于定制自己的富文本编辑器。正因如此,它并不是开箱即用,需要自己二次开发。

以上为简单把玩的 demo,抛砖引玉。