Tooltip(悬浮提示)

Tooltips

我这里所说的「tooltip」,是指悬浮在界面其余部分之上的小型界面元素。它们在编辑器中非常有用,可以用来显示额外的控件或信息,例如「Medium 风格」的编辑界面(以那个流行的博客平台命名),在这种界面里,大部分控件都隐藏起来,直到你选中某些内容,它们才作为一个小气泡出现在选区上方。

在 ProseMirror 中实现 tooltip 有两种常见方式。最简单的方式是插入 widget 装饰(decorations)并绝对定位它们,利用这样一个事实:如果你不指定明确的位置(例如 leftbottom 属性),这些元素就会定位在文档流中它们被放置的那个点上。对于对应某个具体位置的 tooltip 来说,这种方式很有效。

如果你想把东西定位在选区上方,或者想做过渡动画,又或者需要让 tooltip 在编辑器的 overflow 属性不是 visible 时(例如让它可滚动)也能伸出编辑器之外,那么装饰可能就不太实用了。这种情况下,你就必须「手动」定位你的 tooltip。

但你仍然可以利用 ProseMirror 的更新循环,确保 tooltip 与编辑器状态保持同步。我们可以使用一个 plugin view 来创建一个与编辑器生命周期绑定的视图组件。

import {Plugin} from "prosemirror-state"

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

真正的视图会创建一个 DOM 节点来表示 tooltip,并把它插入到编辑器旁边的文档中。

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
    // 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
    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
  }

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

每当编辑器状态更新时,它都会检查是否需要更新 tooltip。定位计算有点繁琐,但这就是 CSS 的现实。基本上,它使用 ProseMirror 的 coordsAtPos 方法来找到选区的屏幕坐标,然后用这些坐标设置相对于 tooltip 偏移父元素(即最近的绝对或相对定位的父元素)的 leftbottom 属性。