站点图标 久久日记本

如何给 tiptap 自定义 Mark 插件

1. tiptap

项目中需要一个富文本编辑器,提出了个加粗,斜体,加粗,链接等功能,想到了tiptap这个富文本编辑器。

tiptapVueJS 里面比较强大的可以自定义扩展的富文本编辑器,源代码在这里

tiptap可以自定义三种拓展 nodes, marks, 和plugins

本章节只讲述marks拓展,所以看看Mark和它的基类Extension:

/tiptap/dist/tiptap.common.js

class Mark extends Extension {
  constructor(options = {}) {
    super(options);
  }
  get type() {
    return "mark";
  }
  get view() {
    return null;
  }
  get schema() {
    return null;
  }
  command() {
    return () => {};
  }
}

class Extension {
  constructor(options = {}) {
    this.options = { ...this.defaultOptions, ...options };
  }

  init() {
    return null;
  }

  bindEditor(editor = null) {
    this.editor = editor;
  }

  get name() {
    return null;
  }

  get type() {
    return "extension";
  }

  get defaultOptions() {
    return {};
  }

  get plugins() {
    return [];
  }

  inputRules() {
    return [];
  }

  pasteRules() {
    return [];
  }

  keys() {
    return {};
  }
}

2. tiptapmark 拓展bold简单解读

加粗斜体,tiptap这里给了一个加粗的例子。

加粗源码在/tiptap-extensions/src/marks/Bold.js里面

import { Mark } from "tiptap";
import { toggleMark, markInputRule, markPasteRule } from "tiptap-commands";

export default class Bold extends Mark {
  get name() {
    return "bold";
  }

  get schema() {
    return {
      parseDOM: [
        {
          tag: "strong",
        },
        {
          tag: "b",
          getAttrs: (node) => node.style.fontWeight !== "normal" && null,
        },
        {
          style: "font-weight",
          getAttrs: (value) => /^(bold(er)?|[5-9]\d{2,})$/.test(value) && null,
        },
      ],
      toDOM: () => ["strong", 0],
    };
  }

  keys({ type }) {
    return {
      "Mod-b": toggleMark(type),
    };
  }

  commands({ type }) {
    return () => toggleMark(type);
  }

  inputRules({ type }) {
    return [markInputRule(/(?:\*\*|__)([^*_]+)(?:\*\*|__)$/, type)];
  }

  pasteRules({ type }) {
    return [markPasteRule(/(?:\*\*|__)([^*_]+)(?:\*\*|__)/g, type)];
  }
}

翻看加粗功能,我们发现选中文字->点击加粗,tiptap 会给选中的文字添加strong标签,

查看源码:

Bold extends Mark: 基于Mark的拓展

get name()方法:返回定义扩展的名字bold

get schema() 方法:本质为对原始富文本bold功能进行渲染,有两个子属性:

`parseDOM`: 初始化时解析 `content`中包含的定义的标签的内容,如这里,接卸了 `strong`,`b`,`font-weight`三个标签,也就是content里包含这三个标签的均为加粗组件需要渲染的内容。

`toDOM`: 初始化时解析上面三种元素后,设置`content`里面这三个标签带的值给`strong`。

commands 方法:外部调用后触发的操作, 常用下面几个方法:

`toggleMark`: 反向`mark`标记 反向上一步操作,如果上一步加粗了,这次就是去掉加粗。

`updateMark`: 更新`mark`标记

`removeMark`: 移除`mark`标记

3. 自定义 mark 拓展:大小写变换

mark基础定义中并没有这个组件:变换大小写,

类似word里面的功能:选中一段英文,点击功能按钮,默认变为大写,如果已经标记为大写,则变为小写,如果已经标记为小写,则变为大写。


import { Mark } from 'tiptap'; import { updateMark } from 'tiptap-commands'; export default class SmallCape extends Mark { get name() { return 'smallCape'; } get schema() { return { attrs: { textTransform: { default: '' } }, // 渲染带有 `text-transform` 标签的 `content` parseDOM: [ { style: 'text-transform', getAttrs: (value) => (value && value.length > 0 ? { textTransform: value } : ''), }, ], // 将需要`text-transform`处理的元素添加到`span`标签并嵌入`text-transform`样式 toDOM: (mark) => [ 'span', { style: `text-transform: ${mark.attrs.textTransform}`, }, 0, ], }; } commands({ type }) { return (attrs) => { // 获取选中文字的`mark` let textTransform = attrs.textTransform.textTransform; // updateMark:更新`mark` if (!textTransform || textTransform === 'lowercase') { return updateMark(type, { textTransform: 'uppercase' }); } else { return updateMark(type, { textTransform: 'lowercase' }); } }; } }

4. 常见方法和参考资料

  1. 获取选中 content 的 Mark

自带的 isActive, getMarkAttrs 方法可以拿到当前选中content是否已经标记为xx,获取当前选中的content的mark

参考链接: EditorMenuBar - getMarkAttrs

所以可以很简单的写出在commands中获取mark的方法

    <editor-menu-bar :editor="editor" v-slot="{ commands, isActive, getMarkAttrs }">
        <button
          class="menubar__button"
          :class="{ 'is-active': isActive.smallCape() }"
          @click="commands.smallCape({ textTransform: getMarkAttrs('smallCape')})"
        >
          <icon name="smallcape" />
        </button>
    </editor-menu-bar>

  commands({ type, schema, node }) {
    return (attrs) => {
      let textTransform = attrs.textTransform.textTransform;

      // 默认切换为大写,反之再次点击大写转小写
      if (!textTransform || textTransform === 'lowercase') {
        return updateMark(type, { textTransform: 'uppercase' });
      } else {
        return updateMark(type, { textTransform: 'lowercase' });
      }
    };
  }

view: editor.view

  1. 获取选中的文字

写大小写转回的mark的时候,我开始的想法是获取到选中的文本内容或者转换后的html,然后截取第一个字母的大小写。

发现statedoc可以拿到content内容(见下面代码),但是拿到的仅仅是页面未被渲染前的文本,这样有个缺陷,无法确认当前的文本时大写还是小写。

于是用了上面1的方法来获取选中的content的mark。

参考链接1: How to get selected text? #369

参考链接2: Font Size, Font Family, Text Color #388

    // vue页面
    <editor-menu-bar :editor="editor" v-slot="{ commands, isActive, getMarkAttrs }">
        <button
            class="menubar__button"
            :class="{ 'is-active': isActive.smallCape() }"
            @click="commands.smallCape({ textTransform: getMarkAttrs('smallCape'),view: editor.view })"
            // 2.
        >
            <icon name="smallcape" />
        </button>
    </editor-menu-bar>

    // mark 插件
  commands({ type }) {
    // attrs 为前端调用commands 方法时传来的参数
    // 2. 或者在 `vue` 页面将`editor.view`传进来,即可在`attrs`中获取到,在`editor.view`里同样有`state`信息。
    return (attrs) => (state, dispatch) => {
      const { selection } = state;
      //   const position = selection.$cursor ? selection.$cursor.pos : selection.$from.pos;
      const { from, to } = selection;
      //   console.log(state, attrs);
      const text = state.doc.textBetween(from, to, ' ');
      //   const html = state.doc.textContent; 获取content内容
      //   text 即为选中的`content`内容
    };
  }
退出移动版