Tiptap使用笔记: 自定义功能


参考文档:

官方中文文档

官方文档

自定义功能

1 相关概念

首先介绍自定义功能相关的概念作为前置知识,也可以先看各个功能的实现(第二节往后)再回来看相关概念。

1.1 三大件

Tiptap 内容有三大法宝构成:节点(node)、标记(mark)和扩展(extension)

  • 节点Node:文档的节点,DOM节点
  • 标记Mark:给选中的文字添加特殊的标记
  • 扩展Extension:扩展tiptap功能

节点Node,标记Mark和扩展Extension都是可扩展的,它们都有extend(options)方法接收想要重写或新增的功能,返回扩展后的节点Node/标记Mark/扩展Extension。

  1. 继承现有的功能:

import ExtensionName from ...

ExtensionName.extend(options)

  1. 创建新的功能:

Extension.create(options)

在继承功能的时候,如果想保留原有功能可以使用...this.parent?(), 如下CustomHeading扩展了Heading扩展(Extention),修改了其内置属性level的值,但是保留了其他原有内部属性。(暂时不理解内部属性没关系,后面会讲到

import Heading from "@tiptap/extension-heading";

const CustomHeading = Heading.extend({
  addOptions() {
    return {
      // 保留原有的功能
      ...this.parent?.(),
      levels: [1, 2, 3],
    };
  },
});

1.2 extensions中的this

尽管extensions不是类,但是this也会被经常使用。

// extensions(节点、标记、扩展)的名字, 比如 'bulletList(无序列表)'
this.name

// 编辑器当前实例
this.editor

// ProseMirror 类型
this.type

// 所有的设置对象
this.options

// 父级 节点、标记、扩展
this.parent

1.3 addOptions

该方法为三大件添加设置项(也可理解为内部变量/属性),添加的内部属性可以在parseHTMLrenderHTML方法中使用,可以通过this.options.xxx访问。

1.4 addAttributes

该方法为三大件添加属性,如下:

const CustomParagraph = Paragraph.extend({
  addAttributes() {
    return {
      color: {
        default: null,
        // 自定义HTML解析
        parseHTML: (element) => element.getAttribute("data-color"),
        // … 自定义HTML渲染
        renderHTML: (attributes) => {
          // 您还可以使用 rendered: false, 这样此属性就完全不会在呈现出来了
          return {
            "data-color": attributes.color,
            style: `color: ${attributes.color}`,
          };
        },
      }
    };
  },
});
  1. default:设置默认值
  2. parseHTML:通过解析HTML获取该属性的值
  3. renderHTML:每当属性值变化都会调用renderHTML方法重新渲染HTML
addAttributes(){..}addGlobalAttributes(){...}的区别

前者是继承"具体扩展"时使用的方法,后者是继承"扩展Extension"时使用的方法,如

// addAttributes
const CustomParagraph = Paragraph.extend({
  addAttributes() {
    // 返回一个属性配置对象
    return {
      color: {
        default: "pink",
      },
    };
  },
});

//addGlobalAttributes
const TextAlign = Extension.create({
  addGlobalAttributes() {
    return [
      {
        // 继承(即应用在)以下extensions(节点、标记、描述)
        types: [
          'heading',
          'paragraph',
        ],
        // … 拥有这些属性
        attributes: {
          textAlign: {
            default: 'left',
            renderHTML: attributes => ({
              style: `text-align: ${attributes.textAlign}`,
            }),
            parseHTML: element => element.style.textAlign || 'left',
          },
        },
      },
    ]
  },
})


1.5 renderHTML

可以用来自定义HTML渲染。

接收参数:attributes,属性

返回是个数组:

  • 第一个值应该是 HTML 标签的名称。
  • 第二个元素是一个对象或一个数组,若是对象它被解释为一组属性。
  • 第三个是一个数字,用于指示应该插入内容的位置。

例如:

//...
addOptions() {
    return {
      herf: '/',
      target: 'blank'
    }
  },
renderHTML({ HTMLAttributes }) {
    // 通过mergeAttributes来合并 回显的内容中接收到的标签带的属性(这个
    // 例子中没有,因为解析HTML需要用到下节的parseHTML)和额外添加的属性
    return ["abc", mergeAttributes(HTMLAttributes, { ...this.options }), 0];
  },
//...

渲染出的HTML如下:

<abc herf="/" target="blank">...</abc>

若不是数组,也可以直接返回对象,表示需要给对应的DOM标签添加的属性,例如:

renderHTML: (attributes) => 
    { style: `text-align: ${attributes.textAlign}` };

假设先为p标签添加属性:<p textAlign="center">...</p>,渲染出的HTML如下:

<p style="text-align: center">...</p>

1.6 parseHTML

此函数定义了 三大件的解析规则,即什么时候才会触发本扩展/节点/标记,只有遇到那些匹配到的标签会触发解析(进而执行renderHTML)

例如:

// 例子1
parseHTML() {
  return [
    {
      //意思是只有遇到strong 标签的时候才会触发本扩展/节点/标记
      tag: 'strong',  
    },
  ]
},

// 例子2
// 使用getAttrs
parseHTML() {
  return [
    {
      tag: 'span',
      getAttrs: element => {
        // 检查是否包含这个属性: element.hasAttribute('style')
        // 检查行内样式: element.style.color
        // 检查一个特殊属性: element.getAttribute('data-color')
        // 比如 这个意思是:只有当tag为span,并且其span标签颜色为red的时候 才会匹配上 并解析
        return element.style.color==='red';
      },
    },
  ]
}

在addAttributes中可以这样用

addAttributes() {
  return {
    color: {
      // 从`data-color` 属性获取颜色 来设置属性color
      parseHTML: element => element.getAttribute('data-color'),
    }
  }
},

parseHTML触发的时机:

只在新建editor时候触发,执行命令修改属性的时候不触发

1.7 addCommands

Extension类暴露了自定义命令的方法addCommands, 添加的命令可以通过this.editor.commandName()执行命令,也可以通过命令链执行,例子:

addCommands() {
    return {
      setTextAlign:
        (alignment) =>
        ({ commands }) => {
          return commands.updateAttributes(this.options.type, { textAlign: alignment });
        },
    };
  },
// 使用
this.currentEditor.commands.setTextAlign("left");
//或
this.currentEditor.chain().focus().setTextAlign(val).run();

关于命令的几个方法:

  • chain():用于告诉编辑器你要执行多个命令(即命令链)
  • focus():将焦点设置回编辑器 (当您单击该按钮时,浏览器会聚焦该 DOM 元素,而编辑器会失去焦点,进而需要使用 focus 方法)
  • run():将执行链

1.8 钩子函数

extensions和editor都具有钩子函数:

import { Extension } from '@tiptap/core'

const CustomExtension = Extension.create({
  onCreate() {
    // 编辑器已准备就绪.
  },
  onUpdate() {
    // 内容发生变化
  },
  onSelectionUpdate({ editor }) {
    // 选中的内容发生改变
  },
  onTransaction({ transaction }) {
    // 编辑器状态改变
  },
  onFocus({ event }) {
    // 编辑器被聚焦
  },
  onBlur({ event }) {
    // 编辑器失焦
  },
  onDestroy() {
    // 编辑器销毁
  },
})

2 对齐方式TextAlign

通过Extension.create()新增扩展,

import { Extension } from "@tiptap/core";

const TextAlign = Extension.create({
  name: "textAlign",
  // 添加内置变量
  addOptions() {
    return {
      types: [],
      alignments: ["left", "center", "right", "justify"],
      defaultAlignment: "center",
    };
  },
  // 添加属性
  addGlobalAttributes() {
    return [
      {
        types: this.options.types,
        attributes: {
          textAlign: {
            default: this.options.defaultAlignment,
            parseHTML: (element) => {
              console.log("parseHTML", element.style.textAlign);
              return element.style.textAlign || this.options.defaultAlignment;
            },
            renderHTML: (attributes) => {
              console.log("addGlobalAttributes", attributes.textAlign);
              return { style: `text-align: ${attributes.textAlign}` };
            },
          },
        },
      },
    ];
  },
  // 添加指令
  addCommands() {
    return {
      setTextAlign:
        (alignment) =>
        ({ commands }) => {
          if (!this.options.alignments.includes(alignment)) {
            return false;
          }
          console.log("types", commands.updateAttributes);
          return this.options.types.every((type) => commands.updateAttributes(type, { textAlign: alignment }));
        },

      unsetTextAlign:
        () =>
        ({ commands }) => {
          return this.options.types.every((type) => commands.resetAttributes(type, "textAlign"));
        },
    };
  },
  // 键盘快捷方式
  addKeyboardShortcuts() {
    return {
      "Mod-Shift-l": () => this.editor.commands.setTextAlign("left"),
      "Mod-Shift-e": () => this.editor.commands.setTextAlign("center"),
      "Mod-Shift-r": () => this.editor.commands.setTextAlign("right"),
      "Mod-Shift-j": () => this.editor.commands.setTextAlign("justify"),
    };
  },
});

// new Editor extensions属性中添加如下
extensions: [
       // ...
       TextAlign.configure({
         types: ["heading", "paragraph"],
       }),
       // ...
     ],

// 命令使用:
this.editor.chain().focus().setTextAlign(val).run(); // 或
this.editor.commands.setTextAlign(val);

使用方式:

this.editor.chain().focus().setTextAlign("left").run();    // 居左
this.editor.chain().focus().setTextAlign("center").run();  // 居中
this.editor.chain().focus().setTextAlign("right").run();   // 居右
this.editor.chain().focus().setTextAlign("justify").run(); // 两端对齐

3 字体大小FontSize

字体大小扩展继承TextStyle扩展

import TextStyle from "@tiptap/extension-text-style";

const TextStyleExtended = TextStyle.extend({
  addAttributes() {
    return {
      ...this.parent?.(),
      fontSize: {
        default: null,
        parseHTML: (element) => element.style.fontSize.replace("px", ""),
        renderHTML: (attributes) => {
          if (!attributes["fontSize"]) {
            return {};
          }
          return {
            style: `font-size: ${attributes["fontSize"]}px`,
          };
        },
      },
    };
  },

  addCommands() {
    return {
      ...this.parent?.(),
      setFontSize:
        (fontSize) =>
        ({ commands }) => {
          return commands.setMark(this.name, { fontSize: fontSize });  //this.name 值为 'textStyle'
        },
      unsetFontSize:
        () =>
        ({ chain }) => {
          return chain().setMark(this.name, { fontSize: null }).removeEmptyTextStyle().run(); //this.name 值为 'textStyle'
        },
    };
  },
});


// new Editor extensions属性中添加如下
extensions: [
       // ...
       TextStyleExtended,
       // ...
     ],

// 命令使用
this.currentEditor.chain().focus().setFontSize(val).run();
  • setMark(typeOrName: string | MarkType, attributes: Record<string, any>):新建span标签包裹选中字体,添加fontSize属性

4 其他扩展

字数统计与限制

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值