本文大纲
参考文档:
1 相关概念
首先介绍自定义功能相关的概念作为前置知识,也可以先看各个功能的实现(第二节往后)再回来看相关概念。
1.1 三大件
Tiptap 内容有三大法宝构成:节点(node)、标记(mark)和扩展(extension)
- 节点Node:文档的节点,DOM节点
- 标记Mark:给选中的文字添加特殊的标记
- 扩展Extension:扩展tiptap功能
节点Node,标记Mark和扩展Extension都是可扩展的,它们都有extend(options)
方法接收想要重写或新增的功能,返回扩展后的节点Node/标记Mark/扩展Extension。
- 继承现有的功能:
import ExtensionName from ...
ExtensionName.extend(options)
- 创建新的功能:
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
该方法为三大件添加设置项(也可理解为内部变量/属性),添加的内部属性可以在parseHTML
,renderHTML
方法中使用,可以通过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}`,
};
},
}
};
},
});
- default:设置默认值
- parseHTML:通过解析HTML获取该属性的值
- 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属性