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以禁用该行为。

Such as this sentence.
Do laundry Water the tomatoes Buy flour Get toilet paper

这是一个不错的段落,它可以包含任何东西

这个段落很无聊,它什么都没有。

按 ctrl/cmd-空格 插入星号,按 ctrl/cmd-b 切换大写,按 ctrl/cmd-q 添加或删除链接。

为了能够与这些元素进行交互,我们再次需要添加一个自定义键映射。有一个用于切换标记的命令助手,我们可以直接用于感叹号。

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
}