Folding Nodes

这个例子展示了如何使用节点装饰来影响节点视图的行为。具体来说,我们将定义一个插件,允许用户折叠一些节点(隐藏它们的内容)。

译者注:下方编辑器内容为 js 生成,因此不翻译其内的内容

我们首先修改基本模式,使顶层由一系列部分组成,每个部分必须包含一个标题,后跟一些任意的块。

import {Schema} from "prosemirror-model"
import {schema as basicSchema} from "prosemirror-schema-basic"

const schema = new Schema({
  nodes: basicSchema.spec.nodes.append({
    doc: {
      content: "section+"
    },
    section: {
      content: "heading block+",
      parseDOM: [{tag: "section"}],
      toDOM() { return ["section", 0] }
    }
  }),
  marks: basicSchema.spec.marks
})

要显示这些部分,我们将使用一个节点视图,该视图显示一个带有按钮的不可编辑的标题。它会查看收到的直接装饰,当其中一个装饰的规范中具有foldSection属性时,它会认为自己是折叠的,这反映在按钮上显示的箭头类型以及内容是隐藏还是可见。

class SectionView {
  constructor(node, view, getPos, deco) {
    this.dom = document.createElement("section")
    this.header = this.dom.appendChild(document.createElement("header"))
    this.header.contentEditable = "false" 
    this.foldButton = this.header.appendChild(document.createElement("button"))
    this.foldButton.title = "Toggle section folding"
    this.foldButton.onmousedown = e => this.foldClick(view, getPos, e)
    this.contentDOM = this.dom.appendChild(document.createElement("div"))
    this.setFolded(deco.some(d => d.spec.foldSection))
  }

  setFolded(folded) {
    this.folded = folded
    this.foldButton.textContent = folded ? "▿" : "▵"
    this.contentDOM.style.display = folded ? "none" : ""
  }

  update(node, deco) {
    if (node.type.name != "section") return false
    let folded = deco.some(d => d.spec.foldSection)
    if (folded != this.folded) this.setFolded(folded)
    return true
  }

  foldClick(view, getPos, event) {
    event.preventDefault()
    setFolding(view, getPos(), !this.folded)
  }
}

按钮的鼠标处理程序只调用setFolding,我们稍后会定义。

避免为这样的功能使用装饰器大部分是可行的,只需将折叠状态保存在节点视图的实例属性中即可。不过,这种方法有两个缺点:首先,节点视图可能会因为多种原因被重新创建(当它们的DOM意外发生变化,或者视图更新算法将它们与错误的部分节点关联时),这会导致它们的内部状态丢失。其次,在编辑器级别显式维护这种状态,可以使其从编辑器外部受到影响、被检查或序列化。

因此,这里的状态是通过一个插件来跟踪的。这个插件的作用是跟踪折叠装饰集并安装上述节点视图。

import {Plugin} from "prosemirror-state"
import {Decoration, DecorationSet} from "prosemirror-view"

const foldPlugin = new Plugin({
  state: {
    init() { return DecorationSet.empty },
    apply(tr, value) {
      value = value.map(tr.mapping, tr.doc)
      let update = tr.getMeta(foldPlugin)
      if (update && update.fold) {
        let node = tr.doc.nodeAt(update.pos)
        if (node && node.type.name == "section")
          value = value.add(tr.doc, [Decoration.node(update.pos, update.pos + node.nodeSize, {}, {foldSection: true})])
      } else if (update) {
        let found = value.find(update.pos + 1, update.pos + 1)
        if (found.length) value = value.remove(found)
      }
      return value
    }
  },
  props: {
    decorations: state => foldPlugin.getState(state),
    nodeViews: {section: (node, view, getPos, decorations) => new SectionView(node, view, getPos, decorations)}
  }
})

此代码的实质是状态更新方法。它首先通过事务映射折叠装饰,使它们继续与部分的更新位置对齐。

然后它会检查事务是否包含指示其添加或删除折叠节点的元数据。我们使用插件本身作为元数据标签。如果存在,它将包含一个{pos: number, fold: boolean}对象。根据fold的值,代码会在给定位置添加或删除节点装饰。

setFolding 函数调度这些类型的事务。此外,它确保在可能的情况下将选择推送出折叠节点。

import {Selection} from "prosemirror-state"

function setFolding(view, pos, fold) {
  let section = view.state.doc.nodeAt(pos)
  if (section && section.type.name == "section") {
    let tr = view.state.tr.setMeta(foldPlugin, {pos, fold})
    let {from, to} = view.state.selection, endPos = pos + section.nodeSize
    if (from < endPos && to > pos) {
      let newSel = Selection.findFrom(view.state.doc.resolve(endPos), 1) ||
        Selection.findFrom(view.state.doc.resolve(pos), -1)
      if (newSel) tr.setSelection(newSel)
    }
    view.dispatch(tr)
  }
}

加载此插件以及具有部分的架构将为您提供一个带有可折叠部分的编辑器。

为了使它们可用,您还需要某种命令来创建和加入部分,但这不在本示例的范围内。