5. view component

  1. 大脑(State) :负责存储和管理文档的内容、光标位置等信息。这就是 Editor State
  2. 脸和手脚(View) :负责把文档显示给你看,并且接收你的操作(打字、点击等)。这就是 Editor View

5.1 Editable DOM

ProseMirror 最终需要将它的文档模型渲染成浏览器能理解的 HTML(也就是 DOM)。它默认会使用你在 Schema 中定义的 toDOM 方法来生成这个 DOM 结构,并给这个 DOM 元素加上 contenteditable="true" 属性,使其变成可编辑区域。

5.1 Data flow

数据流描述的是信息在系统中如何流动和变化 。在 ProseMirror 中,它特指状态(State)、视图(View)和事务(Transaction) 三者之间如何相互作用。

  1. View 显示 State:视图负责将当前的状态(文档内容、光标位置)渲染到页面上。
  2. 用户触发事件:你在视图上进行了操作(如打字、点击)。
  3. View 产生 Transaction :视图将这个操作"翻译"成一个 Transaction(事务)。Transaction 描述了"发生了什么变化"。
  4. State 应用 Transaction :这个 Transaction 被传递给当前的状态,状态根据 Transaction 的指示计算出一个新的状态
  5. 新 State 更新 View :新的状态被设置回视图 (view.updateState(newState)),视图根据新状态重新渲染,更新显示。

第1步:定义应用总状态

csharp 复制代码
let appState = {
  editor: EditorState.create({schema}), // ProseMirror的编辑器状态
  score: 0 // 应用自己的状态,比如一个游戏分数
}

第2步:创建视图并"交出控制权"

javascript 复制代码
let view = new EditorView(document.body, {
  state: appState.editor, // 初始状态来自appState
  dispatchTransaction(transaction) { // 关键!拦截事务
    update({type: "EDITOR_TRANSACTION", transaction}) // 交给全局更新函数
  }
})

第3步:创建中央更新函数

csharp 复制代码
function update(event) {
  if (event.type == "EDITOR_TRANSACTION")
    // 只有这里才能更新编辑器状态!
    appState.editor = appState.editor.apply(event.transaction)
  else if (event.type == "SCORE_POINT")
    // 也可以处理其他类型的更新,比如加分
    appState.score++
  draw() // 状态更新后,统一刷新UI
}

第4步:创建UI刷新函数

javascript 复制代码
function draw() {
  // 更新分数显示
  document.querySelector("#score").textContent = appState.score
  // 更新编辑器视图!这是连接回ProseMirror的关键一步
  view.updateState(appState.editor)
}

5.2 Decorations

不改变文档内容,只改变"视图的渲染样式/DOM"。适合做高亮、下划线、提示气泡、占位符等。

  • 三种类型
  • Node 装饰:给某个节点的 DOM 添属性/样式(如给所有段落加 class)。
  • Inline 装饰:给一段范围内的"行内内容"加样式(如高亮关键词)。
  • Widget 装饰:在某个文档位置插入一个独立的 DOM 节点(不属于文档),比如光标提示、占位符、按钮。
javascript 复制代码
import { Plugin } from 'prosemirror-state'
import { Decoration, DecorationSet } from 'prosemirror-view'

const purplePlugin = new Plugin({
  props: {
    decorations(state) {
      return DecorationSet.create(state.doc, [
        Decoration.inline(0, state.doc.content.size, { style: 'color: purple' })
      ])
    }
  }
})

每次重绘都"重新创建",当装饰很多时会变慢。

正确做法(维护在插件 state 中 + 映射)

  • 把 DecorationSet 存在插件的 state 里。
  • 在 apply(tr, set) 中用 set.map(tr.mapping, tr.doc) 把装饰映射到新文档(文本插入/删除后仍能"跟着走")。
  • 只有需要时才"增删修改"装饰,避免每帧重建。
javascript 复制代码
import { Plugin } from 'prosemirror-state'
import { Decoration, DecorationSet } from 'prosemirror-view'

export const specklePlugin = new Plugin<DecorationSet>({
  state: {
    init(_, { doc }) {
      const speckles: Decoration[] = []
      for (let pos = 1; pos < doc.content.size; pos += 4) {
        speckles.push(
          Decoration.inline(pos - 1, pos, { style: 'background: yellow' })
        )
      }
      return DecorationSet.create(doc, speckles)
    },
    apply(tr, set) {
      // 让装饰"映射"到新文档结构(跟随插入/删除移动)
      const mapped = set.map(tr.mapping, tr.doc)
      // 这里还可以基于 tr 添加/移除装饰(如响应搜索结果、校验结果等)
      return mapped
    }
  },
  props: {
    decorations(state) {
      return specklePlugin.getState(state)
    }
  }
})

三类装饰直观例子

  1. Node 装饰:给所有段落加类名
javascript 复制代码
const paragraphClassPlugin = new Plugin({
  props: {
    decorations(state) {
      const decos: Decoration[] = []
      state.doc.descendants((node, pos) => {
        if (node.type.name === 'paragraph') {
          decos.push(
            Decoration.node(pos, pos + node.nodeSize, { class: 'pm-paragraph' })
          )
        }
      })
      return DecorationSet.create(state.doc, decos)
    }
  }
})
  1. Inline 装饰:高亮关键字(简单搜索)
kotlin 复制代码
// 例子:高亮选中文本
const highlightDecoration = Decoration.inline(
  10,  // 开始位置
  20,  // 结束位置
  {
    style: 'background-color: yellow; padding: 2px;',
    class: 'highlight'
  },
  {
    inclusiveStart: true,  // 包含起始位置的新内容
    inclusiveEnd: false    // 不包含结束位置的新内容
  }
)
  1. 装饰不影响文档内容 - 只是视觉表现

  2. 三种装饰类型

    • Widget: 插入自定义DOM元素
    • Inline: 给文本范围添加样式
    • Node: 给整个节点添加样式

5.3 Node views

为文档中的特定节点类型创建"自定义 UI 组件"。就像 React 组件一样,你可以完全控制某个节点的渲染、更新和交互

  • 核心概念
  • dom:节点的 DOM 表示
  • contentDOM:内容渲染区域(可选)
  • update():节点更新时的处理逻辑
  • stopEvent():是否阻止事件冒泡
  1. 自定义图片节点
kotlin 复制代码
import { EditorView } from 'prosemirror-view'
import { Node as ProseMirrorNode } from 'prosemirror-model'

class ImageView {
  dom: HTMLImageElement
  node: ProseMirrorNode
  view: EditorView
  getPos: () => number
  
  
   constructor(node: ProseMirrorNode, view: EditorView, getPos: () => number) {
    this.node = node
    this.view = view
    this.getPos = getPos

    // 创建自定义 DOM
    this.dom = document.createElement('img')
    this.dom.src = node.attrs.src
    this.dom.alt = node.attrs.alt || ''
    this.dom.style.maxWidth = '100%'
    this.dom.style.height = 'auto'

    // 添加点击事件
    this.dom.addEventListener('click', (e) => {
      e.preventDefault()
      console.log('图片被点击了!')
      this.handleImageClick()
    })
  }
  
  // 阻止事件冒泡,让编辑器忽略这个节点的事件
  stopEvent() {
    return true
  }

  // 处理图片点击:修改 alt 文本
  private handleImageClick() {
    const newAlt = prompt('请输入图片描述:', this.node.attrs.alt || '')
    if (newAlt !== null) {
      // 使用 getPos() 获取当前位置,然后更新节点属性
      const tr = this.view.state.tr.setNodeMarkup(this.getPos(), null, {
        ...this.node.attrs,
        alt: newAlt
      })
      this.view.dispatch(tr)
    }
  }
}

// 在 EditorView 中注册
const view = new EditorView(document.querySelector('#editor'), {
  state,
  nodeViews: {
    image(node, view, getPos) {
      return new ImageView(node, view, getPos)
    }
  }
})
  • 图片:点击编辑、拖拽调整大小
  • 表格:行/列操作、样式调整
  • 代码块:语法高亮、复制按钮
  • 视频:播放控制、字幕编辑
  • 数学公式:LaTeX 渲染、编辑界面

5.4. commands

  • 核心概念:Command 是一个函数,实现编辑操作(如删除、格式化、插入等)。用户通过快捷键或菜单触发。

  • 接口设计:(state, dispatch?, view?) => boolean

    • state:当前编辑器状态
    • dispatch:可选,用于执行事务
    • view:可选,编辑器视图实例
    • 返回值:true 表示命令执行成功,false 表示不适用

eg

javascript 复制代码
import { EditorState, Transaction } from 'prosemirror-state'
import { EditorView } from 'prosemirror-view'

****
function deleteSelection(state: EditorState, dispatch?: (tr: Transaction) => void): boolean {
  // 如果没有选中内容,命令不适用
  if (state.selection.empty) return false
  
  // 如果有 dispatch 函数,执行删除操作
  if (dispatch) {
    dispatch(state.tr.deleteSelection())
  }
  
  return true // 命令执行成功
}

// 使用示例
const view = new EditorView(document.querySelector('#editor'), { state })

// 检查命令是否可用(不执行)
const canDelete = deleteSelection(view.state, null) // 返回 true/false

// 实际执行命令
deleteSelection(view.state, view.dispatch) // 删除选中内容
相关推荐
技术小丁3 小时前
零依赖!教你用原生 JS 把 JSON 数组秒变 CSV 文件
前端·javascript
Crystal3284 小时前
原生 Vue + UniApp 的小程序或 App 项目里如何判断用户是否为首次下载应用
前端·vue.js
时间的情敌4 小时前
基于 Vue3 及TypeScript 项目后的总结
前端·vue.js·typescript
纯爱掌门人4 小时前
鸿蒙端云一体化云存储实战:手把手教你玩转文件上传下载
前端·harmonyos
非凡ghost4 小时前
图吧工具箱-电脑硬件圈的“瑞士军刀”
前端·javascript·后端
非凡ghost4 小时前
Xrecode3(多功能音频转换工具)
前端·javascript·后端
橙某人4 小时前
飞书多维表格插件:进一步封装,提升开发效率!🚀
前端·javascript
他们叫我秃子4 小时前
从 0 到 1,我用小程序 + 云开发打造了一个“记忆瓶子”,记录那些重要的日子!
前端·微信小程序·小程序·云开发
非凡ghost4 小时前
Subtitle Edit(字幕编辑软件) 中文绿色版
前端·javascript·后端