Tooltips

我使用“工具提示”来指代悬停在界面其余部分上的小界面元素。这些在编辑器中非常有用,可以显示额外的控件或信息,例如在“Medium风格”的编辑界面中(以流行的博客平台命名),大多数控件在你选择某些内容之前是隐藏的,此时它们会作为一个小气泡出现在选择内容的上方。

在 ProseMirror 中实现工具提示有两种常见方法。最简单的是插入小部件 装饰并绝对定位, 依赖于这样一个事实:如果你没有指定明确的位置(如leftbottom属性),这些元素会在文档流中它们被放置的位置进行定位。 这对于对应特定位置的工具提示非常有效。

如果你想将某些内容放置在选区上方,或者你想为过渡效果添加动画,或者你需要在编辑器的overflow属性不是visible时(例如为了使其滚动)允许工具提示突出显示,那么装饰可能不太实用。在这种情况下,你将不得不‘手动’定位你的工具提示。

但是你仍然可以利用ProseMirror的更新周期来确保工具提示与编辑器状态保持同步。我们可以使用插件视图来创建一个与编辑器生命周期绑定的视图组件。

import {Plugin} from "prosemirror-state"

let selectionSizePlugin = new Plugin({
  view(editorView) { return new SelectionSizeTooltip(editorView) }
})

实际视图创建一个DOM节点来表示工具提示,并将其插入到文档中编辑器旁边。

class SelectionSizeTooltip {
  constructor(view) {
    this.tooltip = document.createElement("div")
    this.tooltip.className = "tooltip"
    view.dom.parentNode.appendChild(this.tooltip)

    this.update(view, null)
  }

  update(view, lastState) {
    let state = view.state
    // 如果文档/选择没有变化,则不执行任何操作
    if (lastState && lastState.doc.eq(state.doc) &&
        lastState.selection.eq(state.selection)) return

    // 如果选择为空,则隐藏工具提示
    if (state.selection.empty) {
      this.tooltip.style.display = "none"
      return
    }

    // 否则,重新定位并更新其内容
    this.tooltip.style.display = ""
    let {from, to} = state.selection
    // 这些是屏幕坐标
    let start = view.coordsAtPos(from), end = view.coordsAtPos(to)
    // 工具提示框所在的盒子,用作基础
    let box = this.tooltip.offsetParent.getBoundingClientRect()
    // 从选择的端点找到一个中心位置
    // 交叉线,结束可能更多地在左边
    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
  }

  destroy() { this.tooltip.remove() }
}

每当编辑器状态更新时,它会检查是否需要更新工具提示。定位计算有点复杂,但这就是使用 CSS 的生活。基本上,它使用 ProseMirror 的coordsAtPos方法来找到选择的屏幕坐标,并使用这些坐标来设置相对于工具提示的偏移父元素的leftbottom属性,该父元素是最近的绝对或相对定位的父元素。