Tiptap-更灵活的富文本编辑方案

1,525 阅读5分钟

今天推荐一个使用方便、支持灵活自定义能力且能够建设非常丰富前端交互的富文本编辑开发方案。通过此方案,不止能够实现富文本编辑,也能将前端组件库添加到编辑器中,实现更丰富且友好的前端交互。

在这之前,尝试过各种富文本编辑器的解决方案,包括wangEditor、quilljs、slate等,但最终使用下来都存在不满意的地方,比如灵活度不够、接入和学习成本太高、自定义交互复杂或能力有限等。

在此情况下,发现了一个非常不错的富文本方案 tiptap, 它基于 prosemirror 进行了二次封装,降低了prosemirror的学习成本,并提供了非常多已有的插件可供使用,支持自定义各种插件,且插件开发支持使用vue、react等前端开发方式,并且能够实现在编辑器中使用已有的各种前端组件库。

有一个使用tipTap开发方案实现的富文本工具线上体验demo ,有兴趣可在线体验。

下面将通过简单vue3接入方式举例,让大家体验其方便的接入方式及强大的自定义插件能力。

接入

创建项目:使用vue3推荐方式创建一个新的项目

npm create vue@latest

安装相关依赖

npm install @tiptap/vue-3 @tiptap/pm @tiptap/starter-kit

添加到项目

// home.vue
<template>
  <editor-content :editor="editor" />
</template>

<script>
import { Editor, EditorContent } from "@tiptap/vue-3";
import StarterKit from "@tiptap/starter-kit";

export default {
  components: {
    EditorContent,
  },

  data() {
    return {
      editor: null,
    };
  },

  mounted() {
    this.editor = new Editor({
      content: "<p>I’m running Tiptap with Vue.js. 🎉</p>",
      extensions: [StarterKit],
    });
  },

  beforeUnmount() {
    this.editor.destroy();
  },
};
</script>

此时,你就可以看到最简单的可编辑输入框了。但是此时还无法对文本进行任何其他操作,如加粗、设置字体等,可自定义一个顶部操作栏,添加对应操作按钮,由于所有操作都是基于插件的方式使用,因此,需要提前引入并添加相关插件:

// 文本加粗插件
npm install @tiptap/extension-bold
// home.vue
<template>
  <div>
    <button @click="editor.chain().focus().toggleBold().run()">Bold</button>
    <button @click="editor.chain().focus().toggleItalic().run()">Italic</button>
  </div>
  <editor-content :editor="editor" />
</template>

<script>
import { Editor, EditorContent } from "@tiptap/vue-3";
import StarterKit from "@tiptap/starter-kit";
import Bold from "@tiptap/extension-bold";

export default {
  components: {
    EditorContent,
    TitleBar,
  },

  data() {
    return {
      editor: null,
    };
  },

  mounted() {
    this.editor = new Editor({
      content: "<p>I’m running Tiptap with Vue.js. 🎉</p>",
      extensions: [StarterKit, Bold],
    });
  },

  beforeUnmount() {
    this.editor.destroy();
  },
};
</script>

此时,点击Bold按钮,打开浏览器控制台可以发现,会对选中文本添加strong标签,就可以实现对文本进行加粗。更多插件可参考:tiptap/packages at develop · hzvwsrexw15/tiptap · GitHub

自定义插件

下面将通过插件实现设置文本字体大小及在编辑器中使用antd-vue组件。

设置字体大小

新增fontSize.js,其中需要依赖@tiptap/extension-text-style,代码如下:

npm install @tiptap/extension-text-style
// fontSize.js

import { Extension } from "@tiptap/core";
import "@tiptap/extension-text-style";

export default Extension.create({
  name: "fontSize",

  addOptions() {
    return {
      types: ["textStyle"],
    };
  },

  addGlobalAttributes() {
    return [
      {
        types: this.options.types,
        attributes: {
          fontSize: {
            default: null,
            parseHTML: (element) =>
              element.style.fontSize.replace(/['"]+/g, ""),
            renderHTML: (attributes) => {
              if (!attributes.fontSize) {
                return {};
              }

              return {
                style: `font-size: ${attributes.fontSize}`,
              };
            },
          },
        },
      },
    ];
  },

  addCommands() {
    return {
      setFontSize:
        (fontSize) =>
        ({ chain }) => {
          return chain().setMark("textStyle", { fontSize }).run();
        }
    };
  },
});

如上,我们创建了一个fontSize的插件,通过插件向编辑器新增了setFontSize命令,通过命令实现设置文字大小。

下面,将自定义插件添加到编辑器中

// home.vue
<template>
  <div>
    <button @click="editor.chain().focus().toggleBold().run()">Bold</button>
    <button @click="editor.chain().focus().toggleItalic().run()">Italic</button>
    <button @click="editor.chain().focus().setFontSize('20px').run()">
      FontSize
    </button>
  </div>
  <editor-content :editor="editor" />
</template>

<script>
import { Editor, EditorContent } from "@tiptap/vue-3";
import StarterKit from "@tiptap/starter-kit";
import Bold from "@tiptap/extension-bold";
import TextStyle from "@tiptap/extension-text-style";
import FontSize from "./fontSize";

export default {
  components: {
    EditorContent,
  },

  data() {
    return {
      editor: null,
    };
  },

  mounted() {
    this.editor = new Editor({
      content: "<p>I’m running Tiptap with Vue.js. 🎉</p>",
      extensions: [StarterKit, Bold, TextStyle, FontSize],
    });
  },

  beforeUnmount() {
    this.editor.destroy();
  },
};
</script>

如上,添加FontSize按钮,并且在插件中添加TextStyle、FontSize插件,即可实现设置文字字体。

添加antd-vue组件

我们下面实现在编辑器可以添加使用antd vue中rate组件:Ant Design Vue — An enterprise-class UI components based on Ant Design and Vue.js

首先,新建rate.js文件,代码如下:

// rate.js
import { mergeAttributes, Node } from "@tiptap/core";

export default Node.create({
  name: "rate",

  group: "inline",

  inline: true,

  addOptions() {
    return {
      HTMLAttributes: {
        class: "rate",
        spellcheck: false,
      },
    };
  },

  addAttributes() {
    return {
      class: {
        parseHTML: (element) => element.getAttribute("class"),
      },
      "data-rate": {
        parseHTML: (element) => element.getAttribute("data-rate"),
      },
      "data-type": {
        parseHTML: (element) => element.getAttribute("data-type"),
      },
    };
  },

  parseHTML() {
    return [
      {
        tag: `span`,
        getAttrs: (element) => {
          return element.getAttribute("data-name") === this.name;
        },
      },
    ];
  },

  renderHTML({ HTMLAttributes }) {
    return [
      "span",
      mergeAttributes(
        {
          "data-name": this.name,
        },
        this.options.HTMLAttributes,
        HTMLAttributes
      ),
    ];
  },

  addCommands() {
    return {
      setRateBlock:
        (attributes) =>
        ({ commands }) => {
          return commands.insertContent({
            type: this.name,
            attrs: attributes,
          });
        },
    };
  },
});

如上,我们新增rate插件,通过插件在编辑器中新增setRateBlock命令,命令可以插入一个类型为rate的内容。但是此时也仅仅在编辑器中插入了一个span标签,标签上有data-name为rate。但我们期望能够类似antd组件一样能够进行操作,并且展示几颗星,此时就依赖 Node views with Vue – Tiptap能力。

由于依赖antd vue,需要安装相关依赖:

npm install ant-design-vue

npm install @ant-design/icons-vue

新增rate.vue文件,代码如下:

// rate.vue
<template>
  <node-view-wrapper as="span" class="rate-container">
    <span
      class="rate"
      :data-rate="dataRate"
      :data-type="dataType"
      :data-name="dateName"
    >
      <Rate
        v-model:value="rate"
        size="small"
        :allowHalf="true"
        :disabled="disabled"
        @change="handleChange"
      >
        <template #character>
          <StarFilled />
        </template>
      </Rate>
    </span>
  </node-view-wrapper>
</template>

<script>
import { NodeViewContent, nodeViewProps, NodeViewWrapper } from "@tiptap/vue-3";
import { Rate } from "ant-design-vue";
import { StarFilled } from "@ant-design/icons-vue";

export default {
  components: {
    NodeViewWrapper,
    NodeViewContent,
    Rate,
    StarFilled,
  },
  props: nodeViewProps,
  computed: {
    dataType: {
      get() {
        return this.node.attrs["data-type"];
      },
      set(dataType) {
        this.updateAttributes({ "data-type": dataType });
      },
    },
    dataRate: {
      get() {
        return this.node.attrs["data-rate"];
      },
      set(dataRate) {
        this.updateAttributes({ "data-rate": dataRate });
      },
    },
    dateName: {
      get() {
        return this.node.attrs["data-name"];
      },
      set(dateName) {
        this.updateAttributes({ "data-name": dateName });
      },
    },
    disabled: {
      get() {
        return !this.editor.isEditable;
      },
    },
  },
  data() {
    const rate = this.node.attrs["data-rate"] || 0;

    return {
      rate: Number(rate),
    };
  },
  methods: {
    handleChange(value) {
      this.rate = value;
      this.updateAttributes({ "data-rate": this.rate });
    },
  },
  mounted() {},
};
</script>

以上,通过antd vue组件,实现对rate交互效果,最后将组件绑定到插件上,如下:

// home.vue
<template>
  <div>
    <button @click="editor.chain().focus().toggleBold().run()">Bold</button>
    <button @click="editor.chain().focus().toggleItalic().run()">Italic</button>
    <button @click="editor.chain().focus().setFontSize('20px').run()">
      FontSize
    </button>
    <button @click="editor.chain().focus().setRateBlock().run()">Rate</button>
  </div>
  <editor-content :editor="editor" />
</template>

<script>
import { Editor, EditorContent, VueNodeViewRenderer } from "@tiptap/vue-3";
import StarterKit from "@tiptap/starter-kit";
import Bold from "@tiptap/extension-bold";
import TextStyle from "@tiptap/extension-text-style";
import FontSize from "./fontSize";
import Rate from "./rate";
import RateComponent from "./rate.vue";

export default {
  components: {
    EditorContent,
  },

  data() {
    return {
      editor: null,
    };
  },

  mounted() {
    this.editor = new Editor({
      content: "<p>I’m running Tiptap with Vue.js. 🎉</p>",
      extensions: [
        StarterKit,
        Bold,
        TextStyle,
        FontSize,
        Rate.extend({
          addNodeView() {
            return VueNodeViewRenderer(RateComponent);
          },
        }),
      ],
    });
  },

  beforeUnmount() {
    this.editor.destroy();
  },
};
</script>

此时,点击Rate按钮即可实现在编辑器中添加Rate(评分)能力,可通过此方式添加任何你想要的能力。

以上展示了部分能力,它突破了原有编辑器仅对文本的处理,让在编辑器中添加更丰富、更人性化的交互体验提供了更多的可能。