自定义tiptap插件

本文为开发开源项目的真实开发经历,感兴趣的可以来给我的项目点个star,谢谢啦~

具体博文介绍: 开源|Documind协同文档(接入deepseek-r1、支持实时聊天)Documind 🚀 一个支持实时聊天和接入 - 掘金

我干了什么

我定义了两个插件:

  • font-size:支持通过setFontSize设置tiptap编辑器字体大小,通过unsetFontSize重置为默认大小。
  • line-height:支持通过设置setLineHeight设置tiptap编辑器行高,通过unsetLineHeight重置为默认行高。

源码参考

这里就不一点一点讲解了,注释看不懂的话可以叫AI帮你解析。

font-size插件:

typescript 复制代码
import { Extension } from "@tiptap/core";
import "@tiptap/extension-text-style";

// 声明类型
declare module "@tiptap/core" {
  interface Commands<ReturnType> {
    fontSize: {
      /** 设置字体大小(支持CSS单位如12px/1.2rem等) */
      setFontSize: (fontSize: string) => ReturnType;
      /** 清除字体大小设置 */
      unsetFontSize: () => ReturnType;
    };
  }
}

// 创建扩展
export const FontSizeExtension = Extension.create({
  name: "fontSize",

  // 扩展配置项
  addOptions() {
    return {
      types: ["textStyle"], // 作用对象为文本样式标记
    };
  },

  // 注册全局属性
  addGlobalAttributes() {
    return [
      {
        types: this.options.types, // 应用范围(textStyle类型)
        attributes: {
          fontSize: {
            default: null, // 默认无字体大小
            // 从DOM解析字体大小(读取style属性)
            parseHTML: (element) => element.style.fontSize,
            // 渲染到DOM时生成样式
            renderHTML: (attributes) => {
              if (!attributes.fontSize) {
                return {}; // 无设置时返回空对象
              }
              return {
                style: `font-size: ${attributes.fontSize};`, // 生成内联样式
              };
            },
          },
        },
      },
    ];
  },

  // 注册编辑器命令
  addCommands() {
    return {
      /** 设置字体大小命令 */
      setFontSize:
        (fontSize: string) =>
        ({ chain }) => {
          return chain()
            .setMark("textStyle", { fontSize }) // 更新文本样式标记
            .run();
        },
      /** 清除字体大小命令 */
      unsetFontSize:
        () =>
        ({ chain }) => {
          return chain()
            .setMark("textStyle", { fontSize: null }) // 清除字体大小属性
            .removeEmptyTextStyle() // 移除空文本样式标记
            .run();
        },
    };
  },
});

line-height插件:

typescript 复制代码
import { Extension } from "@tiptap/core";

// 类型声明:扩展Tiptap的命令接口
declare module "@tiptap/core" {
  interface Commands<ReturnType> {
    lineHeight: {
      /** 设置行高(支持CSS单位如1.5/2/24px等) */
      setLineHeight: (lineHeight: string) => ReturnType;
      /** 重置为默认行高 */
      unsetLineHeight: () => ReturnType;
    };
  }
}

export const LineHeightExtension = Extension.create({
  name: "lineHeight",

  // 扩展配置项
  addOptions() {
    return {
      types: ["paragraph", "heading"], // 应用行高样式的节点类型
      defaultLineHeight: null, // 默认行高(null表示不设置)
    };
  },

  // 添加全局属性处理
  addGlobalAttributes() {
    return [
      {
        types: this.options.types, // 应用到的节点类型
        attributes: {
          lineHeight: {
            // 默认值(从配置项获取)
            default: this.options.defaultLineHeight,

            // 渲染到HTML时的处理
            renderHTML: (attributes) => {
              if (!attributes.lineHeight) {
                return {};
              }
              // 将行高转换为行内样式
              return {
                style: `line-height: ${attributes.lineHeight};`,
              };
            },

            // 从HTML解析时的处理
            parseHTML: (element) => {
              return {
                // 获取行高样式或使用默认值
                lineHeight: element.style.lineHeight || this.options.defaultLineHeight,
              };
            },
          },
        },
      },
    ];
  },

  // 添加自定义命令
  addCommands() {
    return {
      setLineHeight:
        (lineHeight) =>
        ({ tr, state, dispatch }) => {
          // 创建事务副本以保持不可变性
          tr = tr.setSelection(state.selection);
          // 遍历选区内的所有节点
          state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos) => {
            // 只处理配置的类型节点
            if (this.options.types.includes(node.type.name)) {
              tr = tr.setNodeMarkup(pos, undefined, {
                ...node.attrs,
                lineHeight, // 更新行高属性
              });
            }
          });

          // 提交事务更新
          if (dispatch) {
            dispatch(tr);
          }
          return true;
        },

      unsetLineHeight:
        () =>
        ({ tr, state, dispatch }) => {
          tr = tr.setSelection(state.selection);

          // 遍历选区节点重置行高
          state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos) => {
            if (this.options.types.includes(node.type.name)) {
              tr = tr.setNodeMarkup(pos, undefined, {
                ...node.attrs,
                lineHeight: this.options.defaultLineHeight, // 重置为默认值
              });
            }
          });

          if (dispatch) {
            dispatch(tr);
          }
          return true;
        },
    };
  },
});

使用案例

首先我们在extensions中添加扩展以激活

javascript 复制代码
extensions: [
  /*......*/
  FontSizeExtension,
  LineHeightExtension.configure({
    types: ["paragraph", "heading"],
  }),
  /*......*/
];
相关推荐
happymaker06261 分钟前
web前端学习日记——DAY04
前端·学习
发现一只大呆瓜7 分钟前
React-路由监听 / 跳转 / 守卫全攻略(附实战代码)
前端·react.js·面试
swipe1 小时前
为什么 RAG 一定离不开向量检索:从文档向量化到语义搜索的工程实现
前端·llm·agent
OpenTiny社区1 小时前
AI-Extension:让 AI 真的「看得到、动得了」你的浏览器
前端·ai编程·mcp
IT_陈寒1 小时前
Redis缓存击穿:3个鲜为人知的防御策略,90%开发者都忽略了!
前端·人工智能·后端
竹林8182 小时前
在Web3前端用Node.js子进程批量校验钱包,我踩了这些性能与安全的坑
javascript·node.js
农夫山泉不太甜2 小时前
Tauri v2 实战代码示例
前端
yuhaiqiang3 小时前
被 AI 忽悠后,开始怀念搜索引擎了?
前端·后端·面试
红色石头本尊3 小时前
1-umi-前端工程化搭建
前端
真夜3 小时前
关于对echart盒子设置百分比读取的宽高没有撑开盒子解决方案
前端