ProseMirror Tooltip Example For PluginView

prosemirror

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 放置到附近

简化版本

根据官方示例编写的简化版本


Copyright © 2025 Ben

Theme BlogiNote

Icons from Icônes