Tooltip(悬浮提示)
Tooltips
我这里所说的「tooltip」,是指悬浮在界面其余部分之上的小型界面元素。它们在编辑器中非常有用,可以用来显示额外的控件或信息,例如「Medium 风格」的编辑界面(以那个流行的博客平台命名),在这种界面里,大部分控件都隐藏起来,直到你选中某些内容,它们才作为一个小气泡出现在选区上方。
I'm using ‘tooltip’ to mean a small interface element hovering over the rest of the interface. These can be very useful in editors to show extra controls or information, for example as in a ‘Medium-style’ editing interface (named after the popular blogging platform), where most controls are hidden until you select something, at which point they pop up as a little bubble above the selection.
在 ProseMirror 中实现 tooltip 有两种常见方式。最简单的方式是插入 widget 装饰(decorations)并绝对定位它们,利用这样一个事实:如果你不指定明确的位置(例如 left 或 bottom 属性),这些元素就会定位在文档流中它们被放置的那个点上。对于对应某个具体位置的 tooltip 来说,这种方式很有效。
There are two common ways to implement tooltips in ProseMirror. The
easiest is to insert widget
decorations and absolutely position
them, relying on the fact that if you don't specify an explicit
position (as in a left or bottom property), such elements are
positioned at the point in the document flow where they are placed.
This works well for tooltips that correspond to a specific position.
如果你想把东西定位在选区上方,或者想做过渡动画,又或者需要让 tooltip 在编辑器的 overflow 属性不是 visible 时(例如让它可滚动)也能伸出编辑器之外,那么装饰可能就不太实用了。这种情况下,你就必须「手动」定位你的 tooltip。
If you want to position something above the selection, or you want to
animate transitions, or you need to be able to allow the tooltips to
stick out of the editor when the editor's overflow property isn't
visible (for example to make it scroll), then decorations are
probably not practical. In such a case, you'll have to ‘manually’
position your tooltips.
但你仍然可以利用 ProseMirror 的更新循环,确保 tooltip 与编辑器状态保持同步。我们可以使用一个 plugin view 来创建一个与编辑器生命周期绑定的视图组件。
But you can still make use of ProseMirror's update cycle to make sure the tooltip stays in sync with the editor state. We can use a plugin view to create a view component tied to the editor's life cycle.
import {Plugin} from "prosemirror-state"
let selectionSizePlugin = new Plugin({
view(editorView) { return new SelectionSizeTooltip(editorView) }
})
真正的视图会创建一个 DOM 节点来表示 tooltip,并把它插入到编辑器旁边的文档中。
The actual view creates a DOM node to represent the tooltip and inserts it into the document alongside the editor.
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 偏移父元素(即最近的绝对或相对定位的父元素)的 left 和 bottom 属性。
Whenever the editor state updates, it checks whether it needs to
update the tooltip. The positioning calculations are a bit involved,
but such is life with CSS. Basically, it uses ProseMirror's
coordsAtPos method to find the
screen coordinates of the selection, and uses those to set a left
and bottom property relative to the tooltip's offset parent, which
is the nearest absolutely or relatively positioned parent.