Adding a 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属性的对象,并将它们放入菜单栏元素中。然后,它连接一个事件处理程序,当在此栏上按下鼠标按钮时,该处理程序会确定点击了哪个项目,并运行相应的命令。

要更新新状态的菜单,所有命令都在没有调度函数的情况下运行,并且返回 false 的项目将被隐藏。

将此组件连接到实际的编辑器视图有点尴尬——它在初始化时需要访问编辑器视图,但同时,该编辑器视图的dispatchTransaction属性需要调用其更新方法。插件可以在这里提供帮助。它们允许你定义一个插件视图,像这样:

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

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

实际的菜单项可能如下所示,用于包含 strong、emphasis 和 block 类型按钮的基本菜单。

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

// 辅助函数创建菜单图标
function icon(text, name) {
  let span = document.createElement("span")
  span.className = "menuicon " + name
  span.title = name
  span.textContent = text
  return span
}

// 为给定级别的标题创建一个图标
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的工作方式类似,但增加了对简单下拉菜单和活动/非活动图标的支持(当选择粗体文本时突出显示粗体按钮)。