Tiptap
安装
npm i @tiptap/core @tiptap/starter-kit -S
初始化
import { Editor } from "@tiptap/core";
import StarterKit from "@tiptap/starter-kit"; // 入门套件即常用拓展集合
const editor = new Editor({
element: document.querySelector(".editor"),
extensions: [
StarterKit,
Underline,
TextStyle,
Color,
FontSize,
Highlight.configure({ multicolor: true }),
Image.configure({
inline: true,
}),
...
],
});
Vue
安装:npm install @tiptap/vue-3 @tiptap/pm @tiptap/starter-kit
使用
<template>
<editor-content :editor="editor" />
</template>
<script setup>
import { useEditor, EditorContent } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit'
const editor = useEditor({
content: '<p>I’m running Tiptap with Vue.js. 🎉</p>',
extensions: [
StarterKit,
],
onUpdate: () => {
// emit('update:modelValue', this.editor.getHTML())
}
}
beforeUnmount(() => editor.value.destory());
</script>
Vue/React更多细节见官方文档
核心
extension
tiptap通过拓展扩充功能,extension包含一般类型拓展、还有Node类型拓展和Mark类型拓展
Extension拓展
export const Ext = Extension.create({
name: '拓展名称',
// 如何渲染为HTML
renderHTML({ HTMLAttributes }) {
return ['div', HTMLAttributes, 0]
},
// 存储属性信息,供解析与渲染时设置属性
addAttributes() {
return {
color: { default: '' }
}
}
// 从HTML内容加载时如何解析
parseHTML() {
return [
{ tag: 'strong', },
]
},
...schema, // ProseMirror.schema字段,如draggable
...
}
Mark拓展
import { Mark, mergeAttributes } from '@tiptap/core'
export const Underline = Mark.create({
name: 'underline',
addOptions() {
return {
HTMLAttributes: {},
}
},
parseHTML() {
return [
{ tag: 'u', },
{
style: 'text-decoration',
consuming: false,
getAttrs: style => ((style as string).includes('underline') ? {} : false),
},
]
},
renderHTML({ HTMLAttributes }) {
return ['u', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
},
addCommands() {
return {
setUnderline: () => ({ commands }) => {
return commands.setMark(this.name)
},
toggleUnderline: () => ({ commands }) => {
return commands.toggleMark(this.name)
},
unsetUnderline: () => ({ commands }) => {
return commands.unsetMark(this.name)
},
}
},
})
Node拓展
import { Node, mergeAttributes } from "@tiptap/core";
export default Node.create({
name: "section",
group: "block",
content: "block+",
addAttributes() {
return {
style: {
parseHTML: (element) => element.style.cssText,
renderHTML: (attributes) => ({ style: attributes.style }),
},
};
},
parseHTML() {
return [{ tag: "section" }];
},
renderHTML({ HTMLAttributes }) {
return ["section", mergeAttributes(HTMLAttributes), 0];
},
});
schema
Tiptap 基于模式定义内容的结构。这样就可以定义文档中可能出现的节点类型、属性以及嵌套方式;
不能使用任何未在模式中定义的 HTML 元素或属性。
import { Node } from '@tiptap/core'
const section = Node.create({
name: 'section',
content: 'block+',
...node schema
})
Mark.create({
...mark.schema
})
Commands
通过tiptap以及拓展提供的命令(commands),实现选区、内容的操作
直接调用
editor.commands.toggleBold();
editor.commands.toggleItalic();
链式调用
editor
.chain() // 开启链式命令
.focus() // 聚焦编辑区,防止选区丢失
.toggleBold() // 若干命令链接
.run() // 运行命令链
Editor
API
updateAttributes
editor.chain().focus().updateAttributes("image", { float }).run();
insertContent
editor.commands.insertContent({
type: "image", // @tiptap/extension-image `name` 选项值
attrs: { src: "https://example.com/logo.png" },
});
editor
.chain()
.focus()
.insertContent(emojiHTML, {
parseOptions: {
preserveWhitespace: false,
},
})
.run();
更多详见官方文档
getAttributes(type)
editor.getAttributes("textStyle").fontSize;
editor.getAttributes("paragraph")?.lineHeight;
selectAll
editor.chain().focus().selectAll().setFontSize(`${size}px`).blur().run();
setContent
editor.commands.setContent(newValue, false); // false 参数表示不记录这次变更到历史记录中
doc
doc.descendants遍历文档
const { state } = this.editor;
const { schema, doc } = state;
const markType = schema.marks.textStyle;
const marksStyle = {
marks: [],
nodeMarks: [],
};
doc.descendants((node, pos) => {
if (node.type.name === 'paragraph') {
marksStyle.nodeMarks.push({ pos, attrs: node.attrs });
}
if (node.isText) {
const currentMarks = node.marks.filter((mark) => mark.type === markType);
if (currentMarks.length > 0) {
currentMarks.forEach((mark) => {
marksStyle.marks.push({
pos,
end: pos + node.nodeSize,
attrs: mark.attrs,
});
});
}
}
});
state
view.dispatch(tr) 应用修改
const { state, view } = this.editor;
const { schema, tr } = state;
const markType = schema.marks.textStyle;
marksStyle.nodeMarks.forEach(({ pos, attrs }) => {
const newAttrs = deepClone(attrs);
if (attrs.letterSpacing) {
const oldLetterSpacing = parseInt(attrs.letterSpacing);
const letterSpacing = parseInt(oldLetterSpacing * ratio);
newAttrs.letterSpacing = `${letterSpacing}px`;
}
if (attrs.lineHeight) {
const oldLineHeight = parseFloat(attrs.lineHeight);
const lineHeight = parseFloat((oldLineHeight * ratio).toFixed(1));
newAttrs.lineHeight = lineHeight;
}
tr.setNodeMarkup(pos, null, newAttrs);
});
marksStyle.marks.forEach(({ pos, end, attrs }) => {
const newAttrs = deepClone(attrs);
if (attrs.fontSize) {
const oldFontSize = parseInt(attrs.fontSize);
const newFontSize = parseInt(oldFontSize * ratio);
newAttrs.fontSize = `${newFontSize}px`;
}
const newMark = markType.create(newAttrs);
tr.addMark(pos, end, newMark); // 会覆盖原位置mark的attrs
});
view.dispatch(tr);
node
node.type:节点的类型信息,如 node.type.name === 'paragraph'
node.isText/isTextBlock:是否为文本节点/文本块节点
node.attrs:节点的属性,一个键值对的对象
node.textContent:节点的文本内容(包括所有子节点的文本内容)
node.marks:节点的标记(例如加粗、斜体等)
Extensions
内容样式大部分由 marks
直接实现、如加粗高亮等,使用带样式的 mark
标签包裹内容,调用对应 commands 命令操作,attribute 样式信息
文字样式由 text-style
这一mark实现,如文字颜色字体等,使用带样式的 span
标签包裹内容,调用对应 commands 命令操作,attribute.textStyle 样式信息
addNodeView
对renderHTML拓展(getHTML 导出时),控制具体dom
// rendered: false
###