Schemas from scratch
ProseMirror 模式 提供了类似于文档语法的功能——它们规定了哪些结构是有效的。
最简单的模式允许文档仅由文本组成。
import {Schema} from "prosemirror-model"
const textSchema = new Schema({
nodes: {
text: {},
doc: {content: "text*"}
}
})
您可以用它来编辑内联内容。 (ProseMirror 视图可以挂载在任何节点上,包括内联节点。)
Blocks
为了增加更多的结构,您通常会想要添加某种块节点。例如,此模式由可以选择与组节点分组的笔记组成。
const noteSchema = new Schema({
nodes: {
text: {},
note: {
content: "text*",
toDOM() { return ["note", 0] },
parseDOM: [{tag: "note"}]
},
notegroup: {
content: "note+",
toDOM() { return ["notegroup", 0] },
parseDOM: [{tag: "notegroup"}]
},
doc: {
content: "(note | notegroup)+"
}
}
})
对于不是文本或顶级节点的节点,有必要提供toDOM
方法,以便编辑器可以渲染它们,并提供parseDOM
值,以便它们可以被解析。此模式使用自定义的 DOM 节点<note>
和<notegroup>
来表示其节点。
您可以按 ctrl-space 为选定的音符添加一个组。要实现该功能,您首先需要实现一个自定义的编辑命令。类似这样的:
import {findWrapping} from "prosemirror-transform"
function makeNoteGroup(state, dispatch) {
// 获取选定块周围的范围
let range = state.selection.$from.blockRange(state.selection.$to)
// 查看是否可以将该范围包装在一个注释组中
let wrapping = findWrapping(range, noteSchema.nodes.notegroup)
// 如果没有,该命令不适用
if (!wrapping) return false
// 否则,使用 `wrap` 方法调度一个事务,
// 创建执行实际包装的步骤。
if (dispatch) dispatch(state.tr.wrap(range, wrapping).scrollIntoView())
return true
}
一个键映射如keymap({"Ctrl-Space": makeNoteGroup})
可以用来启用它。
通用绑定 对于回车和退格在这个模式中工作得很好——回车会在光标周围拆分文本块,或者如果它是空的,尝试将其从父节点中提取出来,因此可以用来创建新笔记并从笔记组中退出。文本块开头的退格会将该文本块从其父节点中提取出来,这可以用来从组中移除笔记。
Groups and marks
让我们再来一个,有星星和喊叫。
这个模式不仅有文本作为内联内容,还有星星,它们只是内联节点。为了能够轻松引用我们的内联节点,它们被标记为一个组(也称为"inline"
)。该模式对两种类型的块节点也做了同样的处理,一种段落类型允许任何内联内容,另一种只允许未标记的文本。
let starSchema = new Schema({
nodes: {
text: {
group: "inline",
},
star: {
inline: true,
group: "inline",
toDOM() { return ["star", "🟊"] },
parseDOM: [{tag: "star"}]
},
paragraph: {
group: "block",
content: "inline*",
toDOM() { return ["p", 0] },
parseDOM: [{tag: "p"}]
},
boring_paragraph: {
group: "block",
content: "text*",
marks: "",
toDOM() { return ["p", {class: "boring"}, 0] },
parseDOM: [{tag: "p.boring", priority: 60}]
},
doc: {
content: "block+"
}
},
由于文本块默认允许标记,boring_paragraph
类型将 marks
设置为空字符串以明确禁止它们。
该模式定义了两种类型的标记,喊叫文本和链接。第一个类似于常见的强或强调标记,因为它只是向其标记的内容添加了一位信息,并且没有任何属性。它指定应将其呈现为<shouting>
标签(样式为内联、粗体和大写),并且应将同一标签解析为此标记。
marks: {
shouting: {
toDOM() { return ["shouting", 0] },
parseDOM: [{tag: "shouting"}]
},
link: {
attrs: {href: {}},
toDOM(node) { return ["a", {href: node.attrs.href}, 0] },
parseDOM: [{tag: "a", getAttrs(dom) { return {href: dom.href} }}],
inclusive: false
}
}
})
链接确实有一个属性——它们的目标URL,所以它们的DOM序列化方法必须输出这个(从toDOM
返回的数组中的第二个元素,如果它是一个普通对象,则提供一组DOM属性),并且它们的DOM解析器必须读取它。
默认情况下,标记是包含的,这意味着它们会应用于在其末端插入的内容(以及在它们从其父节点的开头开始时)。对于链接类型的标记,这通常不是预期的行为,可以将标记规范中的inclusive
属性设置为false以禁用该行为。
为了能够与这些元素进行交互,我们再次需要添加一个自定义键映射。有一个用于切换标记的命令助手,我们可以直接用于感叹号。
import {toggleMark} from "prosemirror-commands"
import {keymap} from "prosemirror-keymap"
let starKeymap = keymap({
"Ctrl-b": toggleMark(starSchema.marks.shouting),
"Ctrl-q": toggleLink,
"Ctrl-Space": insertStar
})
切换链接稍微复杂一些。当没有选择任何内容时,启用或禁用非包容性标记没有意义,因为你不能像使用包容性标记那样“输入”它们。而且我们需要向用户询问一个URL——但只有在添加链接时才需要。所以该命令使用rangeHasMark
来检查它是添加还是删除,然后再提示输入URL。
(prompt
可能不是你在实际系统中想要使用的。
当使用异步方法向用户查询某些内容时,
请确保使用当前状态,而不是命令最初被调用时的状态,以应用命令的效果。)
function toggleLink(state, dispatch) {
let {doc, selection} = state
if (selection.empty) return false
let attrs = null
if (!doc.rangeHasMark(selection.from, selection.to, starSchema.marks.link)) {
attrs = {href: prompt("Link to where?", "")}
if (!attrs.href) return false
}
return toggleMark(starSchema.marks.link, attrs)(state, dispatch)
}
该命令在插入星号之前,首先检查模式是否允许在光标位置插入星号(使用
canReplaceWith
),如果允许,则用新创建的星号节点替换选区。
function insertStar(state, dispatch) {
let type = starSchema.nodes.star
let {$from} = state.selection
if (!$from.parent.canReplaceWith($from.index(), $from.index(), type))
return false
dispatch(state.tr.replaceSelectionWith(type.create()))
return true
}