ProseMirror Tooltip Example For PluginView
ProseMirror 在官方文档中演示了如何通过 plugin view 插件为编辑器添加自定义视图(可交互的 tooltip 提示框),完整的源码可以查看相关的 Github 仓库
自定义视图
在 ProseMirror 中可以通过以下几种方式添加自定义视图:
- 在实例化 schema 时,通过配置对象的字段
nodes(或marks)的属性toDOM,设置不同的节点类型(或样式标记)渲染到页面上的 HTML 结构,适用于构建一些简单的静态视图(交互功能由 ProseMirror 实现处理) - 在实例化 EditorView 时,通过配置对象的属性
nodeViews为特定的节点类型自定义视图构建函数,适用于构建复杂的视图(可以接管交互功能) - 使用 decoration 装饰器,在文档的特定部分添加自定义视图或样式,适用于进行轻量级的 UI 定制,它只是为页面添加一些「点缀物」,例如语法高亮,它只影响渲染输出效果而不会修改编辑器的文档内容
- 使用 plugin 插件为编辑器添加自定义视图(在实例化 plugin 时,通过配置对象的属性
view进行设置),适用于为编辑器创建全局通用的视图(而前面的方法,则适用于针对特定节点类型,即自定义视图会根据文档具体结构而嵌入到相应位置),例如菜单栏
由于 tooltip 是悬浮在编辑器上面的(跟随选区动态变化),它是相对于整个编辑器的视图(而不是与特定节点类型相关,类似于菜单栏),所以应该通过 plugin 插件来创建
ts
import {Plugin} from "prosemirror-state"
let selectionSizePlugin = new Plugin({
// 通过该插件的配置对象的属性 view 设置自定义视图
// 属性值需要符合 TypeScript interface PluginView 接口的约束
view(editorView) { return new SelectionSizeTooltip(editorView) }
})
// 该类的实例化对象符合 PluginView,包含方法 update 和 destroy
class SelectionSizeTooltip {
constructor(view) {
this.tooltip = document.createElement("div")
this.tooltip.className = "tooltip"
view.dom.parentNode.appendChild(this.tooltip)
// 初始化时调用一次 update() 方法,以设置(表示 tooltip 的)DOM 元素的定位坐标
this.update(view, null)
}
// 当编辑器的视图更新时,会调用该函数
// 在这里设置自定义视图的更新方式
// view 是(更新后)编辑器视图对象,lastState 是(更新前)编辑器状态
update(view, lastState) {
let state = view.state
// Don't do anything if the document/selection didn't change
// 对比视图更新前后的编辑器状态是否相同
if (lastState && lastState.doc.eq(state.doc) &&
lastState.selection.eq(state.selection)) return
// Hide the tooltip if the selection is empty
if (state.selection.empty) {
this.tooltip.style.display = "none"
return
}
// Otherwise, reposition it and update its content
this.tooltip.style.display = ""
let {from, to} = state.selection
// These are in screen coordinates
let start = view.coordsAtPos(from), end = view.coordsAtPos(to)
// The box in which the tooltip is positioned, to use as base
// 获取(包含 tooltip 元素)最近的祖父元素,它是定位元素(用于作为设置 tooltip 定位坐标的参考基准)
let box = this.tooltip.offsetParent.getBoundingClientRect()
// Find a center-ish x position from the selection endpoints (when
// crossing lines, end may be more to the left)
let left = Math.max((start.left + end.left) / 2, start.left + 3)
this.tooltip.style.left = (left - box.left) + "px"
this.tooltip.style.bottom = (box.bottom - start.top) + "px"
this.tooltip.textContent = to - from
}
// 当编辑器的视图被销毁时,或当编辑器的状态对象被重新配置而 plugins 属性不包含该插件时,该函数被调用
// 在以上情况发生时,应该从页面移除自定义视图
destroy() { this.tooltip.remove() }
}
其中关键是使用方法 view.coordsAtPos(pos) 获取给定的文档位置 pos 在页面视口 viewport 中的坐标信息,这样就可以基于选区将 tooltip 放置到附近
简化版本
根据官方示例编写的简化版本