添加菜单
Adding a menu
大部分示例都使用 example setup 包来创建菜单,但我们实际上并不推荐在实际生产环境中使用它和示例的 menu 包,因为它们都是相当简单、带有主观倾向的模块,你很可能很快就会碰到它们的局限。
Most of the examples use the example setup package to create a menu, but we actually don't recommend using that and the example menu package in actual production, since they are rather simplistic, opinionated modules, and you're likely to run into their limitations rather quickly.
这个示例将带你为一个 ProseMirror 编辑器构建一个自定义(而且丑丑的)菜单。
This example will go through building a custom (and ugly) menu for a ProseMirror editor.
大致思路是,创建一批用户界面元素并把它们绑定到命令上。当它们被点击时,应该在编辑器上执行这些命令。
The idea is, roughly, to create a number of user interface elements and tie them to commands. When clicked, they should execute these commands on the editor.
有一个问题是,如何处理那些并不总是可用的命令——当你处在一个段落里时,「把这个设为段落」的控件应该显示吗?如果要显示,它应该置灰吗?这个示例的做法很简单:当某个命令当前不可用时,就隐藏对应的按钮。
One question is how to deal with commands that aren't always applicable—when you are in a paragraph, should the control for ‘make this a paragraph’ be shown? If so, should it be grayed out? This example will simply hide buttons when their command is not currently applicable.
为了做到这一点,它需要在编辑器状态每次变化时更新菜单结构。(取决于菜单中条目的数量,以及判断它们是否可用所需的工作量,这可能变得很昂贵。对此没有真正的解决方案,只能要么保持命令的数量和复杂度较低,要么不让菜单外观随状态变化。)
To be able to do that, it needs to update the menu structure every time the editor state changes. (Depending on the number of items in your menu, and the amount of work required for determining whether they are applicable, this can get expensive. There's no real solution for this, except either keeping the number and complexity of the commands low, or not changing the look of your menu depending on state.)
如果你已经有某种数据流抽象在承载 ProseMirror 的更新,那么把菜单写成独立的组件并连接到编辑器状态应该会很顺利。如果没有,插件可能是最简单的解决方案。
If you already have some kind of dataflow abstraction that you're routing ProseMirror updates though, writing the menu as a separate component and connecting it to the editor state should work well. If not, a plugin is probably the easiest solution.
菜单组件看起来可能像这样:
The component for the menu might look something like this:
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() }
}
它接收一个菜单项数组(这些项是具有 command 和 dom 属性的对象),把它们放进一个菜单栏元素中。然后,它接上一个事件处理器:当鼠标按键在这个菜单栏上按下时,判断点击了哪个项,并运行相应的命令。
It takes an array of menu items, which are objects with command and
dom properties, and puts those into a menu bar element. Then, it
wires up an event handler which, when a mouse button is pressed on
this bar, figures out which item was clicked, and runs the
corresponding command.
为了给新状态更新菜单,所有命令都会在不带 dispatch 函数的情况下运行,那些返回 false 的项会被隐藏。
To update the menu for a new state, all commands are run without dispatch function, and the items for those that return false are hidden.
把这个组件接到真正的编辑器视图上有点别扭——它在初始化时需要访问编辑器视图,但同时,那个编辑器视图的 dispatchTransaction prop 又需要调用它的 update 方法。插件在这里可以帮上忙。它们允许你定义一个 plugin view,就像这样:
Wiring this component to an actual editor view is a bit awkward—it
needs access to the editor view when initialized, but at the same
time, that editor view's
dispatchTransaction
prop needs to call its update method. Plugins can help here. They
allow you define a plugin view, like this:
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 方法。因此,通过把这个插件添加到一个编辑器上,我们可以确保编辑器视图获得一个菜单栏,并且这个菜单栏与编辑器保持同步。
When an editor view is initialized, or when the set of plugins in its
state change, the plugin views for any plugins that define them get
initialized. These plugin views then have their update method called
every time the editor's state is updated, and their destroy method
called when they are torn down. So by adding this plugin to an editor,
we can make sure that the editor view gets a menu bar, and that this
menu bar is kept in sync with the editor.
对于一个包含 strong、emphasis 和块类型按钮的基础菜单,实际的菜单项看起来可能是这样的。
The actual menu items might look like this, for a basic menu with strong, emphasis, and block type buttons.
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 按钮)。
The prosemirror-menu
package works
similarly, but adds support for things like simple drop-down menus and
active/inactive icons (to highlight the strong button when strong text
is selected).