富文本编辑器Tiptap系列教程——Tiptap常用API & 命令详解
接上篇富文本编辑器 Tiptap 系列教程——Tiptap 模块&概念详解,本文我们主要说一下 Tiptap 的常用方法 & 命令。
Tiptap 实例
首先回忆一下我们之前初始化编辑器部分:
const editor = new Editor({ ... })通过new Editor或useEditor初始化得到的editor就是一个编辑器实例,实例上存在很多实用方法和属性,下面我们来一一列举下。
方法
编辑器实例提供了很多方法,可以返回任何内容,增加 Tiptap 的可操作性。
can()
检查命令或命令链是否可以执行,不会实际执行。返回值为false/true。
比如:之前利用!editor.can().chain().focus().toggleBold().run()来对加粗bold按钮进行启用或禁用。
<button @click="editor.chain().focus().toggleBold().run()" :disabled="!editor.can().chain().focus().toggleBold().run()" :class="{ 'is-active': editor.isActive('bold') }"> bold</button>chain()
之前说过,通过chain()可以创建一条链来执行多个方法,最后需要添加.run()来实际执行所有命令。
editor.chain().focus().toggleBold().run()destroy()
销毁编辑器实例并注销所有事件,在组件销毁时调用。
onUnmounted(() => editor.value.destroy())getHTML()/getJSON()/getText()
获取编辑器 HTML/JSON/纯文本 Text 内容。
onUpdate({ editor }) { const json = editor.getJSON() const html = editor.getHTML() // 默认两个节点 nodes 之间两个换行符 const text = editor.getText() // 可传入参数 blockSeparator 控制节点之间的连接 const lineText = editor.getText({ blockSeparator: '' }) console.log(json) console.log(html) console.log(text) console.log(lineText)}
getAttributes()
获取当前选中的节点或标记的属性,如获取当前链接的 href 属性:
editor.getAttributes('link').hrefisActive()
返回当前选定的节点或标记是否处于激活状态,如我们之前用于判断bold是否处于激活状态时,为其添加is-active类:
:class="{ 'is-active': editor.isActive('bold') }"registerPlugin()/unregisterPlugin()
注册/注销一个 ProseMirror 插件。
项目中暂时还没有用到,我们可以看下 vue3 中对BubbleMenu注册插件的代码。
import { BubbleMenuPlugin, BubbleMenuPluginProps } from '@tiptap/extension-bubble-menu'// ...onMounted(() => { const { updateDelay, editor, pluginKey, shouldShow, tippyOptions, } = props
editor.registerPlugin(BubbleMenuPlugin({ updateDelay, editor, element: root.value as HTMLElement, pluginKey, shouldShow, tippyOptions, }))})
onBeforeUnmount(() => { const { pluginKey, editor } = props
editor.unregisterPlugin(pluginKey)})// ...在onMounted中注册插件BubbleMenuPlugin,并在onBeforeUnmount通过pluginKey注销这个插件。
setOptions()
手动更新编辑器的配置。
我们通过new Editor()初始化时对编辑器添加配置之外,还可以通过setOptions()去动态的修改配置,参数同初始化相同。
// 添加 class 样式到编辑器实例editor.setOptions({ editorProps: { attributes: { class: 'my-custom-class', }, },})setEditable()
更新编辑器的可编辑状态,接受两个参数editable和emitUpdate。
// 设置编辑器只读editor.setEditable(false)属性
目前有 isEditable(是否是编辑状态) 和 isEmpty(内容是否为空) 两个属性。
isEditable
返回编辑器是可编辑的还是只读的。
editor.isEditableisEmpty
检查是否有内容。
editor.isEmptyCommands 命令
Commands 命令用于更改编辑器的状态(内容、选区等),仅返回 true/false。Tiptap 编辑器提供了大量 Commands 命令,可以添加或更改内容、更改选择等。只有掌握这些命令,才能更好的使用 Tiptap 编辑器。
切记:不要混淆实例方法和 commands 命令,实例方法可返回任何内容,命令仅返回 true/false。
执行命令
Tiptap 中所有命令都可以通过编辑器实例editor调用,比如在用户单击按钮时将文本设置为粗体:
editor.commands.setBold()上面命令可以使所选内容变为粗体,但在使用此类命令时是点击外部按钮,会导致编辑器失焦,所以通常会通过链式调用使编辑器重新获取焦点。
chain 链式调用
大多数的命令可以组合为一个调用,这比单独的函数调用要简短优雅一些,链命令以.chain()开头.run()结尾,将多个命令合并成一个事务,内容只更新一次,并且事务也只触发一次,比如修改上面使文本加粗的例子:
editor.chain().focus().toggleBold().run()上面我们说过当点击编辑器外部区域调用方法时,会导致编辑器失焦,所以需要链式调用focus()使编辑器重新获取焦点,再继续执行其他方法。
检测命令是否可执行
通过editor.can().xxx()可以判断命令是否可以执行,返回true/false。比如我们在表格中需要判断当前选中单元格是否可以合并/拆分,因为并不是所选的单元格就可以进行合并/拆分操作:
// 是否可合并editor.can().mergeCells()// 是否可拆分editor.can().splitCell()Try commands
.first()用于执行命令列表,首先执行第一个命令,如果第一个命令返回true,即执行成功时就会停止;如果第一个命令执行失败,就会接着执行第二个命令,即一个接一个执行,遇到成功执行的命令则停止执行。
如:退格键首先尝试撤消输入规则。如果成功,就到此为止。如果没有应用输入规则,因此无法恢复,它将运行下一个命令并删除选择。
editor.first(({ commands }) => [ () => commands.undoInputRule(), () => commands.deleteSelection(), // …])也可以在命令中应用 commands.first() 方法
export default () => ({ commands }) => { return commands.first([ () => commands.undoInputRule(), () => commands.deleteSelection(), // … ]) }命令合集
Tiptap 有很可使用多命令,包括对文档、节点/标记和选中区域的各种操作,所以要想玩转 Tiptap,必须熟悉这些命令,知道在什么时候用什么命令。
content 文档命令
-
clearContent
清空文档中的所有内容。
-
insertContent/insertContentAt
在当前/指定位置插入纯文本、HTML 或 JSON 节点。这两个命令使用较多,如一些自定义节点插入内容时我们会使用:
commands.insertContent({type: 'xxx',attrs: {},})或者如插入表情内容时:
props.editor.chain().focus().insertContent(emoji).run() -
setContent
设置文档内容(用新文档替换该文档),一般用于初始化文档内容。
节点、标记命令
-
clearNodes
将节点规范化为默认节点,一般用于清除样式,如:橡皮擦功能。
-
createParagraphNear
在选中节点后面添加空段落。
-
deleteNode
删除选中节点,如一些自定义节点删除时:
Backspace: () => {return this.editor.chain().deleteNode(this.name).insertContent('<p></p>').run()return false} -
extendMarkRange
扩展当前的选择以包含当前标记,如将当前选中区域设置为链接:
props.editor.chain().focus().extendMarkRange('link').setLink({ href: link }).run() -
setMark
在当前选择处添加一个新标记,主要用于对选中文本添加各种标记,如对设置加粗或字体等:
setFontSize:(fontSize) =>({ tr, chain, editor, dispatch }) => {return chain().setMark('textStyle', { fontSize }).run()},unsetFontSize:() =>({ chain }) => {return chain().setMark('textStyle', { fontSize: null }).run()} -
setNode
将当前节点替换成指定节点,如对当前节点进行标题和正文的切换:
editor.commands.setNode('paragraph')editor.commands.setNode('heading', { level: 1 }) -
toggleMark
在当前选择处打开和关闭特定标记,也就是添加或取消添加标记,如:
editor.commands.toggleMark('bold')点击一次时选中内容加粗,再次点击选中内容将取消粗体。
-
toggleNode
类似于 toggleMark,所选节点将在一个节点与另一个节点之间切换,如:
props.editor.chain().focus().toggleNode('paragraph', 'heading').run()点击一次时当前节点转为标题,再次点击标题又转为段落。
-
unsetAllMarks
将选中内容的所有 marks 标记移除。
-
unsetMark
与 setMark 作用相反,移除当前选中内容的指定标记。
-
updateAttributes
更新节点、标记的属性,只需传递需要更改的属性,如针对图片的宽度设置:
editor.commands.updateAttributes('image', { width: 100 })
selection 选区命令
-
blur
使编辑器失去焦点。
-
focus
将焦点设置回编辑器,可以看到上面讲到的大部分链式命令中都用了 focus 命令,并且可以针对光标位置进行设置:
// 设置编辑器获得焦点editor.commands.focus()// 设置光标位于编辑器开始位置editor.commands.focus('start')// 设置光标位于编辑器结尾位置editor.commands.focus('end')// 选中全部文档editor.commands.focus('all')// 设置光标到pos=10的位置editor.commands.focus(10)// 设置光标位于结尾位置,但是滚动条不滚动到结尾(当内容高度大于编辑器高度有滚动条时)editor.commands.focus('end', { scrollIntoView: true }) -
deleteRange
删除指定范围内所有内容,如:
editor.commands.deleteRange({ from: 0, to: 10 }) -
deleteSelection
删除当前选中节点。
-
enter
触发键盘”Enter”。
-
scrollIntoView
将视图滚动到当前选择或光标位置。
-
selectAll
选中整个文档。
-
setNodeSelection
给定位置创建一个新的节点选区,如选中图片、列表等块元素,传入节点的所在位置。
-
setTextSelection
创建一个文本选区,接受 number 或 range,如果传入 range 时会选中文本,传入 number 时只是移动光标位置。
// 仅移动光标editor.commands.setTextSelection(6)// 选中文本区域editor.commands.setTextSelection({ from: 6, to: 8 })
自定义 Tiptap 扩展
import { Extension } from '@tiptap/core'import type { CommandProps } from '@tiptap/core'export const FormatPainter = Extension.create<void>({ name: 'formatPainter', addCommands() { return { goingDoSomething: options => (props: CommandProps) => { // options 为 goingDoSomething 方法参数 // props 为 tiptap 指令公共参数们 = { // editor: Editor; // tr: Transaction; // commands: SingleCommands; // can: () => CanCommands; // chain: () => ChainedCommands; // state: EditorState; // view: EditorView; // dispatch: ((args?: any) => any) | undefined; // } // ... }, } }})自定义 Tiptap 组件
import { mergeAttributes, Node, nodeInputRule } from '@tiptap/core'import { VueNodeViewRenderer } from '@tiptap/vue-3'export const inputRegex = /(?:^|\s)(!\[(.+|:?)]\((\S+)(?:(?:\s+)["'](\S+)["'])?\))$/export const ImageWithTools = Node.create({ name: 'image', draggable: true, // 可配置参数 addOptions() { return { allowBase64: false, } }, // 组件是否为行内组件 inline() { return this.options.inline }, group() { return this.options.inline ? 'inline' : 'block' }, // 组件属性 addAttributes() { return { src: { default: null, }, } }, // tiptap 执行 getHTML 时组件的返回值 parseHTML() { return [ { tag: this.options.allowBase64 ? 'img[src]' : 'img[src]:not([src^="data:"])', }, ] }, // tiptap 渲染时组件的返回值 renderHTML({ HTMLAttributes }) { return ['img', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)] }, // tiptap 渲染时挂载的VUE文件 addNodeView() { return VueNodeViewRenderer(vueComponent) }, // 操作组件的指令 addCommands() { return { // prettier-ignore setImage: options => ({ commands }) => { return commands.insertContent([{ type: this.name, attrs: options, }]) }, } }, // html 转为 tiptap 内容时过滤组件可用属性 addInputRules() { return [ nodeInputRule({ find: inputRegex, type: this.type, getAttributes: (match) => { const [, , src] = match return { src } }, }), ] },})总结
本文主要讲了 Tiptap 实例方法和常用命令,记住不要混淆实例方法和命令。我们只需要记住这些 API 的大概作用,脑袋里有个“影子”。一旦某个地方需要这么做的时候,要能回忆起好像已经有方法实现了,然后再去详细查看对应的方法,最终使用它并解决问题,这套思想适用于所有 API 较多的组件、框架等。
这篇文章完后,关于自定义节点、标记扩展等的文章暂时不再更新。leader 安排我做 webrtc 音视频通话的调研工作,等完成这部分工作后继续更新。
以上就是本文的全部内容,希望这篇文章对你有所帮助,欢迎点赞和收藏 🙏,如果发现有什么错误或者更好的解决方案及建议,欢迎随时联系。