ProseMirror Custom Schema Example

prosemirror

ProseMirror Custom Schema Example

本文介绍 ProseMirror 官方提供的几个例子,从最基本的 schema 入手,然后介绍构建自定义的节点和样式标记,以及如何使用指令

简化版本

根据官方示例编写了一个简化版本

最简单的 schema

最简单的 schema 可以仅包含 doctext 节点类型,由此数据结构所约束的编辑器只能包含 inline 文本(不能换行,仅有一行)

ts
import {Schema} from "prosemirror-model"

const textSchema = new Schema({
  nodes: {
    text: {},
    doc: {content: "text*"}
  }
})

自定义节点

在自定义节点时,对于非文本节点 text 或非根节点 doc 都应该设置 toDOM 属性(这样才可以将该类型的节点渲染到页面上)和 parseDOM 属性(这样才可以将页面上的相应的元素解析为该类型的节点)

自定义元素

节点的 toDOM 属性将节点转换为 HTML 元素渲染到页面上,一般采用标准的元素

也支持 custom element 自定义元素,例如在官方示例中使用了 <note><notegroup> 自定义元素

parseDOM 属性需要针对相应的元素进行解析

创建指令和绑定快捷键

要在页面插入自定义节点,一般需要构建相应的 command 指令,然后可以通过菜单栏的按钮触发指令,或与相应的键盘按键进行映射/绑定通过快捷键触发指令

指令 command 是一个函数 fn(state, dispatch?, view?) 最后应该返回一个布尔值,以表示该操作是否可以执行

提示

使用指令在编辑器中插入自定义节点,需要保证满足 schema 数据约束,应该先使用方法 node.canReplaceWith(from, to, nodeType, marks?) 进行条件判断

ts
function insertStar(state, dispatch) {
  let type = starSchema.nodes.star // 星形节点类型 nodeType
  let {$from} = state.selection // 选区的开始位置
  // 方法 $from.index() 返回的是光标所在的文本节点(在父节点里)的索引值
  // 通过方法 `node.canReplaceWith(from, to, nodeType)` 判断用类型为 nodeType 的节点替换(索引值)从 from 到 to 的节点是否可行
  // 由于这里假设选区处于光标状态,所以替换节点的索引值都是 $from.index() 即假设用星形节点替换光标所在的文本节点
  if (!$from.parent.canReplaceWith($from.index(), $from.index(), type))
    return false
  // 执行替换/插入操作
  dispatch(state.tr.replaceSelectionWith(type.create()))
  return true
}

如果要将指令与快捷键绑定,则需要方法 keymap({shortcutKey: Command}) 构建一个 plugin 插件,再应用到编辑器上

ts
new EditorView(place, {
  state: EditorState.create({
    doc,
    plugins:[
      // baseKeymap 是 prosemirror-command 模块导出的变量,包含了富文本编辑器常见/通用的快捷键
      // 它是一个对象,以键值对 {shortcutKey: Command} 的形式表示键盘按键与指令的映射关系
      keymap(baseKeymap)
    ]
  })
})

样式标记的属性 inclusive

样式标记 mark 的属性 inclusive 默认值为 true,表示当光标位于带有该样式标记的文本的末尾处,继续输入的文字依然采用该样式标记(如果带有该样式标记的文本位于文本块的开头,则光标位于开头继续在前面输入的文字,也依然采用该样式标记)

对于 inclusivetrue 的样式标记,可以在光标状态(未选中文本内容时)进行 toggle 切换,所影响的是称作 active mark 激活的/待添加的样式标记,即对未来输入的内容是否应用上该样式标记

但是对于 link 链接样式这种行为并不符合预期,所以需要将 link 样式标记的属性 inclusive 设置为 false,即位于一个已存在的链接前后继续输入文本都不会被纳入链接里

对于 inclusivefalse 的样式标记,在光标状态(未选中文本内容时)进行 toggle 切换是无意义/不合理的(例如对于 link 样式标记,需要针对选中的文本内容设置链接,而不能先设置链接再输入文本),所以在 toggle 切换这种类型的样式标记时,需要判断选区是否为空

ts
// 用于切换 link 样式标记的指令
function toggleLink(state, dispatch) {
  let {doc, selection} = state
  // 判断选区是否为空
  if (selection.empty) return false
  let attrs = null
  // 使用方法 node.rangeHasMark(from, to, type) 判断选中范围是否已经应用了 link 样式标记
  // 如果选区范围的文本已经应用了 link 样式,则删除该样式标记
  if (!doc.rangeHasMark(selection.from, selection.to, starSchema.marks.link)) {
    // 如果选区范围的文本没有应用 link 样式,则添加该样式标记,先要使用 prompt 提示让用户输入链接地址
    attrs = {href: prompt("Link to where?", "")}
    if (!attrs.href) return false
  }
  // 执行样式标记切换操作
  return toggleMark(starSchema.marks.link, attrs)(state, dispatch)
}

// ⚠️ 使用 prompt 弹出提示框让用户输入链接地址,它会阻塞进程,等待用户的响应,所以上述操作都是同步代码,这样就可以保证参数 state 是当前编辑器的(最新)状态
// ⚠️ 为了更好的用户体验(使用 prompt 会冻结整个页面,用户暂时无法与页面的其他部分进行交互),也可以使用其他方式获取链接地址,例如 fetch 等异步代码获取数据
// ⚠️ 但是需要注意最后 toggleMark 切换样式标记时,要采用最新的编辑器状态对象(通过 view.state 获取,而不能使用传入的参数 state,因为存在异步代码,原来传入的 state 可能不是最新的)

Copyright © 2025 Ben

Theme BlogiNote

Icons from Icônes