添加菜单

Adding a menu

大部分示例都使用 example setup 包来创建菜单,但我们实际上并不推荐在实际生产环境中使用它和示例的 menu 包,因为它们都是相当简单、带有主观倾向的模块,你很可能很快就会碰到它们的局限。

这个示例将带你为一个 ProseMirror 编辑器构建一个自定义(而且丑丑的)菜单。

大致思路是,创建一批用户界面元素并把它们绑定到命令上。当它们被点击时,应该在编辑器上执行这些命令。

有一个问题是,如何处理那些并不总是可用的命令——当你处在一个段落里时,「把这个设为段落」的控件应该显示吗?如果要显示,它应该置灰吗?这个示例的做法很简单:当某个命令当前不可用时,就隐藏对应的按钮。

为了做到这一点,它需要在编辑器状态每次变化时更新菜单结构。(取决于菜单中条目的数量,以及判断它们是否可用所需的工作量,这可能变得很昂贵。对此没有真正的解决方案,只能要么保持命令的数量和复杂度较低,要么不让菜单外观随状态变化。)

如果你已经有某种数据流抽象在承载 ProseMirror 的更新,那么把菜单写成独立的组件并连接到编辑器状态应该会很顺利。如果没有,插件可能是最简单的解决方案。

菜单组件看起来可能像这样:

class MenuView {
  constructor(items, editorView) {
    this.items = items
    this.editorView = editorView

    this.dom = document.createElement("div")
    this.dom.className = "menubar"
    items.forEach(({dom}) => this.dom.appendChild(dom))
    this.update()

    this.dom.addEventListener("mousedown", e => {
      e.preventDefault()
      editorView.focus()
      items.forEach(({command, dom}) => {
        if (dom.contains(e.target))
          command(editorView.state, editorView.dispatch, editorView)
      })
    })
  }

  update() {
    this.items.forEach(({command, dom}) => {
      let active = command(this.editorView.state, null, this.editorView)
      dom.style.display = active ? "" : "none"
    })
  }

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

它接收一个菜单项数组(这些项是具有 commanddom 属性的对象),把它们放进一个菜单栏元素中。然后,它接上一个事件处理器:当鼠标按键在这个菜单栏上按下时,判断点击了哪个项,并运行相应的命令。

为了给新状态更新菜单,所有命令都会在不带 dispatch 函数的情况下运行,那些返回 false 的项会被隐藏。

把这个组件接到真正的编辑器视图上有点别扭——它在初始化时需要访问编辑器视图,但同时,那个编辑器视图的 dispatchTransaction prop 又需要调用它的 update 方法。插件在这里可以帮上忙。它们允许你定义一个 plugin view,就像这样:

import {Plugin} from "prosemirror-state"

function menuPlugin(items) {
  return new Plugin({
    view(editorView) {
      let menuView = new MenuView(items, editorView)
      editorView.dom.parentNode.insertBefore(menuView.dom, editorView.dom)
      return menuView
    }
  })
}

当一个编辑器视图被初始化,或者其状态中的插件集合发生变化时,那些定义了 plugin view 的插件,它们的 plugin view 就会被初始化。这些 plugin view 随后会在编辑器状态每次更新时调用它们的 update 方法,并在被拆除时调用它们的 destroy 方法。因此,通过把这个插件添加到一个编辑器上,我们可以确保编辑器视图获得一个菜单栏,并且这个菜单栏与编辑器保持同步。

对于一个包含 strong、emphasis 和块类型按钮的基础菜单,实际的菜单项看起来可能是这样的。

import {toggleMark, setBlockType, wrapIn} from "prosemirror-commands"
import {schema} from "prosemirror-schema-basic"

// Helper function to create menu icons
function icon(text, name) {
  let span = document.createElement("span")
  span.className = "menuicon " + name
  span.title = name
  span.textContent = text
  return span
}

// Create an icon for a heading at the given level
function heading(level) {
  return {
    command: setBlockType(schema.nodes.heading, {level}),
    dom: icon("H" + level, "heading")
  }
}

let menu = menuPlugin([
  {command: toggleMark(schema.marks.strong), dom: icon("B", "strong")},
  {command: toggleMark(schema.marks.em), dom: icon("i", "em")},
  {command: setBlockType(schema.nodes.paragraph), dom: icon("p", "paragraph")},
  heading(1), heading(2), heading(3),
  {command: wrapIn(schema.nodes.blockquote), dom: icon(">", "blockquote")}
])

prosemirror-menu的工作方式类似,但额外支持简单的下拉菜单和激活/未激活图标(例如在选中 strong 文本时高亮 strong 按钮)。