CodeMirror View 模块

codemirror

CodeMirror View 模块

编辑器视图

view 视图是指在页面上展示编辑器状态 state 的 DOM 元素,它允许用户在其中输入文字

EditorView 类

EditorView 类表示/呈现编辑器的用户交互界面,它包含一个 editable 可编辑的 DOM 元素(还包括其他附加的元素,例如 gutter 边栏),它响应用户的交互事件(并分发事务 dispatch transaction 以更新编辑器的 state)

使用方法 new EditorView(config?) 进行实例化,基于(可选)配置对象 config(它需要符合一种 TypeScript interface EditorViewConfig,默认值是空对象 {})创建一个新的 view 视图对象

EditorViewConfig

TypeScript interface EditorViewConfig 描述编辑器视图可接受哪些配置参数

该类型 extends 扩展了另一个 Typescript interface EditorStateConfig,即符合 TypeScript interface EditorViewConfig 的对象也具有 interface EditorStateConfig 所描述的属性:

另外还具有 interface EditorViewConfig 所描述的属性:

  • state(可选)属性:一个 EditorState 类的实例,该视图所对应的编辑器状态的初始值
    如果不配置该属性,则会自动调用静态方法 EditorState.create(config?) 创建一个 state 对象(其中参数 config 就是当前用于实例化 view 所传递的配置对象,会使用其中的属性 docselectionextension
  • parent(可选)属性:一个 Element 元素DocumentFragment 文档片段,它作为编辑器视图的父节点/容器
    当配置了该属性,编辑器会自动将视图所对应的 DOM 元素插入 append 到给定的容器里
    如果没有配置该属性,则需要在实例化视图以后,将编辑器视图所对应的 DOM 元素手动挂载到页面上
  • root(可选)属性:一个 Document 对象ShadowRoot 对象,设置编辑器视图所在的页面的根节点(或影子根节点)
    如果未配置该属性,而配置了前面所说的 parent 属性,则编辑器会自动寻找属性 parent 所在页面的根节点作为编辑器的根节点
  • scrollTo(可选)属性:一个 StateEffect 类的实例,通过这个自定义变更效应(编辑器会自动将它添加到事务 transaction 中)设置编辑器视图的(滚动到)初始位置
    这里的所接受的 stateEffect 是由 EditorView 类的静态方法 static EditorView.scrollIntoView() 或编辑器视图对象的方法 editorView.scrollSnapShot() 所创建的
  • dispatchTransactions(可选)属性:一个函数 fn(trs: readonly Transaction[], view: EditorView) 自定义分发事务时的处理逻辑,可以设置一些额外的操作
    第一个参数 trs 是一个数组,它的元素是一系列 Transaction 类的实例;第二个参数 view 是编辑器视图对象
    提示

    如果配置了该属性,则会覆写 editorView.dispatch() 方法的默认行为(将事务 transactions 传递给方法 this.update() 以更新视图)

    注意

    请保证在该回调函数里最后执行了方法 this.update(trs) 以将新的编辑器状态应用到视图上,该方法的入参 trs 是一个数组,它的元素是一系列 Transaction 类的实例

  • dispatch(可选)属性:一个函数 fn(tr: Transaction, view: EditorView) 它的作用和属性 dispatchTransactions 类似,也是用于自定义分发事务时的处理逻辑,如果设置了该属性会强制每次只应用单个事务(而不是将多个事务合并为一个,再进行视图的更新)
    ⚠️ 但是该方法已过期,并不推荐使用(每次只执行单个事务,导致编辑器的性能较差 ❓ ),如果需要覆写默认分发事务时的行为,可配置 dispatchTransactions 属性

也可以使用该类的静态方法 static EditorView.findFromDOM(dom) 从给定的 dom(一个 HTMLElement 元素,它是编辑器视图所关联的 DOM 元素)获取对应的 view 实例;如果给定的 DOM 元素不属于任何编辑器视图,则返回 null

EditorView 类的实例化对象表示编辑器的一个 view 对象,以下称作「编辑器视图」

view 编辑器视图对象包含一些属性和方法:

  • state 属性:一个 EditorState 类的实例,表示该编辑器视图当前所呈现的 state 编辑器状态
  • viewport 属性:一个对象 {from: number, to: number} 表示编辑器视口,即当前视图所渲染的文档内容的范围(采用字符偏移量来描述)
    编辑器视口

    为了可以在不消耗太多内存或使得浏览器过载的情况下显示大型文档,CodeMirror 只会渲染在可视区(以及额外的上下区域)的文档内容,这个范围称为 viewport 编辑器视口

  • visibleRanges 属性:一个数组,它的元素是一系列对象 {from: number, to: number} 表示实际可视的文档内容的范围
    由于在 viewport 编辑器视口里的代码可能存在 collapsed 折叠,导致 viewport 的范围比实际显式/渲染的范围要大,可以将其视为 viewport 的子集
    通过该属性可以获取真实可见的一系列文档内容范围,只对该部分的内容进行样式渲染,可以提高编辑器的性能
  • inView 属性:一个布尔值,编辑器视图(所对应的 DOM 元素)是否还在 viewport 页面视口里。如果为 false 则编辑器(所对应的 DOM 元素)可能滚动离开了视口,或被隐藏了
  • composing 属性:一个布尔值,表示用户当前是否在使用组合式输入法 compose 输入文字(至少通过该输入方式已经触发了编辑器的一个 change 变更 ❓ )
  • compositionStarted 属性:一个布尔值,表示用户当前是否处于组合式 compose 输入当中
    ⚠️ 在一些平台,例如 Android,该状态对应很多情况,只要将光标放置到一个单词上,就会在那里开始一个组合式输入状态
  • root 属性:一个 Document 对象ShadowRoot 对象,表示编辑器视图所在的页面的根节点(或影子根节点)
编辑器视图的 DOM 结构

编辑器视图在页面所对应的 DOM 元素(一般具有 cm- 前缀的 class 类名)具有如下结构

html
<div class="cm-editor [theme scope classes]">
  <div class="cm-scroller">
    <div class="cm-content" contenteditable="true">
      <div class="cm-line">Content goes here</div>
      <div class="cm-line">...</div>
    </div>
  </div>
</div>
  • 最外层容器 outer wrapper 是一个 <div class="cm-editor"> 元素,具有 class 类名 cm-editor,它是一个 vertical flexbox(采用 Flexbox 布局,子元素沿垂直方向排列)
    如果使用插件为编辑器设置 theme 主题,它的 styles scoped 类名也是添加到该元素上
  • 可滚动的容器 scroller element 是一个 <div class="cm-scroller"> 元素,具有 class 类名 cm-scroller,它是一个 horizontal flexbox(采用 Flexbox 布局,子元素沿水平方向排列),该容器可以水平垂直滚动,以展示大型的代码内容
    如果编辑器具有 gutter 边栏,会添加到该容器的开头(作为子元素)
  • 内容元素 content element 是一个 <div class="cm-content" contenteditable="true"> 元素,具有 class 类名 cm-content,它是一个可编辑的元素,而且针对该元素注册了 DOM mutation observer 以响应该元素的内容变化
  • 行元素 line element 是一个 <div class="cm-line"> 元素,包含每一行内容
  • dom 属性:一个 HTMLElement 元素,表示该编辑器视图所关联的 DOM 元素的最外层容器
  • scrollDOM 属性:一个 HTMLElement 元素,表示该编辑器视图所关联的 DOM 元素的第二层可滚动容器
    ⚠️ 但它只在内容较多时才可滚动,不能假设它在任何状态下都是可滚动的
  • contentDOM 属性:一个 HTMLElement 元素,表示该编辑器视图所关联的 DOM 元素的内容元素(具有属性 contenteditable,它是可编辑的)
  • dispatch() 方法:分发给定的事务
    它的参数可以有多种形式:
    • dispatch(tr: Transaction) 仅分发一个事务,参数是一个 Transaction 类的实例
    • dispatch(trs: readonly Transaction[]) 依次分发多个事务,参数是一个数组,它的元素是一系列 Transaction 类的实例
    • dispatch(...specs: TransactionSpec[]) 接受一系列的的配置对象(剩余参数 ...specs 将函数所接受的所有参数打包为一个数组,每一个元素都是一个对象,它们都需要符合一种 TypeScript interface TransactionSpec,用于描述/配置 transaction 事务),该方法会将多个事务配置对象整合起来,最后分发一个事务
    提示

    该方法的默认行为是将事务 transactions 传递给方法 this.update() 以更新视图

    如果在初始化编辑器视图时,设置了配置对象的属性 dispatchTransactions,则会覆写该方法的默认行为

  • update(transactions) 方法:根据给定的一系列事务 transactions(参数是一个数组,它的元素是一系列 Transaction 类的实例)更新编辑器视图
    该方法会更新页面的可视区域和编辑器的选区,以呈现新的编辑器状态,并 notify 通知编辑器视图插件进行同步更新
    提示

    一般不需要调用该方法,而是调用方法 dispatch() 来分发事务,它会自动在内部调用该方法来更新视图

    只有当初始化编辑器视图,在设置配置对象的属性 dispatchTransactions时(自定义分发事务时的处理逻辑),才可能需要调用该方法,以手动更新视图

  • setState(newState) 方法:为编辑器视图设置新的编辑器状态,参数 newState 是一个 EditorState 类的实例
    ⚠️ 谨慎使用该方法,因为它会重置编辑器的数据,会导致整个编辑器(对应的 DOM 元素)重绘,所有视图插件重新初始化;如果新的编辑器状态可以基于旧的 state(修改)而来,一般推荐调用方法 dispatch 分发事务来进行更新
  • themeClasses 属性:一个字符串,获取当前编辑器所采用的主题对应的 class 类名
  • requestMeasure(request?) 方法:为编辑器视图 schedule 调度/计划进入一次测量周期 measure phase 执行相关操作(通常是指获取和计算编辑器中某些 DOM 元素的尺寸或位置,以便在下一次写入周期 write phase 中进行适应性的调整)
    视图的更新周期

    CodeMirror 的编辑器视图为了减少 DOM 的 reflow 重排,将编辑器的更新周期分为两个阶段:

    • write phase 写入周期
    • measure phase测量周期
    reflow 和 repaint

    repaint 重绘reflow 回流/重排是浏览器渲染页面时的关键过程

    • repaint 重绘:是指当元素的外观发生变化(如颜色、边框等)时,浏览器需要重新绘制,但不会改变布局
    • reflow 回流/重排:是指当元素的布局发生变化(如 DOM 结构、元素尺寸或位置发生变化)时,浏览器必须重新计算页面布局,进而可能影响整个页面的布局和渲染

    重排的性能开销比重绘更大,因此需要优化 DOM 操作以减少 reflow

    浏览器渲染引擎会按照一定的频率执行一个「渲染循环」 render loop,在循环的每一帧中,浏览器会根据 DOM 变化来判断是否需要 reflow。渲染循环通常每秒执行 60 次(即 60fps)。因此,即使 DOM 发生了变化,reflow 可能会延迟到下一次渲染循环,而不会立即触发

    但是在修改 DOM 后立即访问与布局相关的属性,浏览器会被强制触发 reflow,以确保获取到的布局数据是最新的

    更多介绍参考文章 Repaints and Reflows: Manipulating the DOM responsibly

    编辑器视图分发事务 dispatch transaction 通常只会导致编辑器向 DOM 写入内容,处于 write phase,而不会读取布局信息,因为这回强制触发浏览器 reflow,这是同步操作,可能会阻塞其他操作

    而对于编辑器在页面布局的读取操作(例如检查 viewport 编辑器视口是否仍然有效,光标是否需要滚动到可视范围等)则会在一个独立的 measure phase 测量阶段完成。如果有必要,这个阶段之后会进行另一个写入阶段

    通过方法 requestAnimationFrame 进行调度,让编辑器在两个阶段执行不同的操作,以让浏览器更少地触发 reflow

    也可以使用上述方法 requestMeasure 方法为编辑器视图 schedule 调度/计划进入一次测量周期 measure phase 以执行相关操作


    (可选)参数 request 是一个对象,具有以下属性和方法:
    • read(view) 方法:参数 view 是编辑器视图对象,用于读取与编辑器相关的 DOM 的尺寸或布局的信息(例如调用方法 element.getBoundingClientRect() 获取元素的的大小及其相对于视口的位置)
      该方法在测量阶段 measure phase 执行(可能触发浏览器 reflow),在该方法里不能变更文档的内容
      该方法返回一个与 DOM 尺寸或布局相关的值,会传递给下一个方法
    • write(measure, view)(可选)方法:将上一个方法的返回值(与 DOM 的尺寸或布局相关的信息)作为第一个参数 measure,第二个参数 view 是编辑器视图对象,用于更新编辑器所对应的 DOM 元素
      该方法在下一个写入阶段 write phase 执行,在该方法里不能触发浏览器 reflow(即不应该在该方法里调用与 DOM 尺寸或布局相关的属性或方法,相关操作而应该在上一个方法执行完成,这里应该从第一个参数直接获取,可以将其看作缓存值)
    • key(可选)属性:任何值,作为该方法的标识符
      如果多次调用了该方法,且在配置对象中设置了相同的的 key 属性,则只会执行最后一次 schedule 调度/计划(避免重复浏览器触发 reflow)
  • plugin(plugin) 属性:获取给定插件 plugin(该参数是一个 ViewPlugin 类的实例)的值
    返回的值可以是一个对象,需要符合 TypeScript interface PluginValue;也可以是 null,当给定的插件 plugin 确实已经在编辑器注册,但是如果它运行时崩溃,就会从编辑器视图中移除(避免编辑器无法运行),则返回的值就是 null
  • documentTop 属性:一个数值,表示文档内容的第一行(不包括编辑器顶部的 padding 留白区域)相对于页面视图顶部的距离
    该值可能是负数,(如果文档是可滚动)表示文档的第一行从浏览器的顶部滚动出去了
  • documentPadding 属性:一个对象 {top: number, bottom: number} 它的两个属性分别表示编辑器顶部和底部的 padding 留白区域的高度
  • scaleX 属性:一个数值,表示编辑器在横轴方向上的缩放比例
    如果编辑器视图(所对应的 DOM 元素)通过 CSS transform 进行变换,则该属性值表示横向轴的变换比例;如果没有进行变换,该属性值为 1
  • scaleY 属性:一个数值,表示编辑器在纵轴方向上的缩放比例
    如果编辑器视图(所对应的 DOM 元素)通过 CSS transform 进行变换,则该属性值表示纵向轴的变换比例;如果没有进行变换,该属性值为 1
注意

对于编辑器的变换操作只支持平移 translation 和缩放 scale 有效,并不被支持其他类型的变换(旋转或倾斜)

  • elementAtHeight(height) 方法:获取在编辑器的垂直位置为 height(该参数是一个数值,表示相对于文档第一行的距离)的区块信息,它表示一个块级元素(可能是一行文本代码,或是一个 block widget decoration 块级挂件型装饰器)
    返回值是一个 BlockInfo 类的实例
    BlockInfo 类

    BlockInfo 类用于记录一个在编辑器视图中的 block-level 块级元素相关信息

    该类的实例化对象具有以下属性:

    • from 属性:一个数值,表示该块级元素在编辑器文档的开始位置(采用字符偏移量来描述)
    • to 属性:一个数值,表示该块级元素在编辑器文档的结束位置(采用字符偏移量来描述)
    • length 属性:一个数值,表示该块级元素的长度(通常是字符数,即该元素包含的文档内容)
    • top 属性:一个数值,表示该块级元素的顶部相对于编辑器第一行的距离(像素)
    • bottom 属性:一个数值,表示该块级元素的底部相对于编辑器第一行的距离(像素)
    • height 属性:一个数值,表示该块级元素的高度
    • type 属性:表示该块级元素的类型
      该属性值可以是 TypeScript enum BlockType 枚举类型中的一个值;如果块级元素包含编辑器的多行 ❓ 该属性值也可以是一个数组,它的元素是一系列的 enum BlockType 枚举的值
      BlockType

      TypeScript enum BlockType 枚举类型包含以下 4 种值:

      • Text 表示该块级元素是一行代码
      • WidgetBefore 表示该块级元素是一个块级挂件型装饰器 block widget decoration,而且它位于所关联位置的前面
      • WidgetAfter 表示块级元素是一个块级挂件型装饰器 block widget decoration,而且它位于所关联的位置的后面
      • WidgetRange 表示块级元素是一个块级挂件型装饰器 block widget decoration,它用于替换了一段特定范围的内容
    • widget 属性:如果该块级元素表示一个块级挂件型装饰器,则该属性值是一个对象,它需要是 abstract class WidgetType 抽象类的子类的实例,表示挂件型装饰器的类型;否则,该属性值是 null
    • widgetLineBreaks 属性:一个数值,如果该块级元素是一个块级挂件且包含文本块时,该属性值表示在文本块内部包含的换行符 \n 的数量。该属性值有助于正确计算编辑器的布局
  • lineBlockAtHeight(height) 方法:获取在编辑器的垂直位置为 height 的 line block 即容纳一行内容的块级容器
    返回值是一个 BlockInfo 类的实例
    line block

    line block 是指一段文档范围,它的两端是非隐藏/非替换的换行符 \n,或文档的开头(或结尾)

    一般 line block 就是指包含一行文本代码的容器,如果在这一行代码里插入了一些 block widgets decoration 块级型,则它可能是非连续的


    可以将前一个方法 elementAtHeight() 看作是通用版本,而这里的方法看作是特殊版本,它返回的区块信息只表示一行文本代码(而不会是 block widget 块级挂件型装饰器)
  • viewportLineBlocks 属性:一个数组,它的元素是一系列 BlockInfo 类的实例,它们表示在 viewport 编辑器视口里的一系列 line blocks
  • lineBlockAt(pos) 方法:获取在编辑器文档位置为 pos(采用字符偏移量来描述)的 line block
    返回值是一个 BlockInfo 类的实例
  • contentHeight 属性:一个数值,表示文档内容的总高度(像素)
逻辑位置与视觉位置

CodeMirror 可能会包含 bidirectional 双向文本,以下是一些可以实现光标移动的方法,其中有些移动方向是基于 logical position 逻辑位置,有些是基于 visual position 视觉位置

  • 逻辑位置 logical position 是指文本的存储顺序,不需要考虑文本的走向
  • 视觉位置 visual position 是文本呈现在页面上的位置,需要考虑文本 LTR 或 RTL 走向
  • moveByChar(start, forward, by?) 方法:以 grapheme cluster 字素簇(即在视觉上认为的单个字符)为步长移动光标,该方法最后返回一个 SelectionRange 类的实例(表示新的光标位置)
    边界情况

    假设光标的移动方向是远离行头

    如果光标当前在行尾,则光标会跨越换行符,一般是移动到的下一行的开头

    如果光标当前在文档的末尾,则光标会维持在原位

    • 第一个参数 start 是一个 SelectionRange 类的实例(选区范围),表示光标的初始状态
    • 第二个参数 forward 是一个布尔值,用于设置光标的移动方向。当配置为 true 表示光标向前移动(一般是远离行首);当配置为 false 则向后移动
      ⚠️ 在 bidirectional 双向文本中,文字的走向在中途可能是相反的,那么光标移动的正方向(当 forwardtrue 时)是由编辑器整体布局方向决定的(具体而言是编辑器的属性 textDirection

    默认情况下通过该方法移动光标其步长只有一个字符,但是可以通过设置第三个(可选)参数 by 调整光标移动的步长,让光标一次性移动更多的字符
    • 第三个(可选)参数 by 是一个函数 fn(initial: string) → (fn(next: string) → boolean)
      该函数的入参 initial 是一个字符串,表示当前(与光标相邻)的单个字符
      该函数的返回值是也是一个函数 fn(next: string) → boolean 它用于判断是否需要跳过当前字符,而该函数的入参 next 则是下一个字符,返回的布尔值表示光标是否需要跳过当前字符
      1. 如果返回 true 表示需要跳过字符
      2. 然后会对下一个字符同样执行函数 fn(next: string) → boolean
      3. 并依此类推......
      4. 直到函数返回 false 光标才停止跳跃
  • moveByGroup(start, forward) 方法:以字符组(即归于同一类型的一系列连续字符)为步长移动光标,该方法最后返回一个 SelectionRange 类的实例(表示新的光标位置)
    • 第一个参数 start 是一个 SelectionRange 类的实例(选区范围),表示光标的初始状态
    • 第二个参数 forward 是一个布尔值,用于设置光标的移动方向,具体规则和上一个方法 moveByChar() 的同名参数一样

    可以形象地将该方法理解为以单词 word(以空格或其他符号进行分隔)为步长进行光标的移动
  • visualLineSide(line, end) 方法:获取给定行 line 的开头或结尾
    对比

    以上方法在获取该行的开头或结尾位置时,会考虑文本的走向(LTR 或 RTL),所以称为 visual 视觉上的一端

    Line的属性 fromto 则是该行的逻辑位置 logical position,即文本在文档中实际存储的位置,与文本的走向(LRT 或 RTL)无关

    • 属性 from 是逻辑起点,表示该行在文档中开始的位置,也就是该行第一个字符在文档中的索引位置
    • 属性 to 是逻辑终点,表示该行在文档中结束的位置,即该行最后一个字符在文档中的索引位置

    第一个参数 line 是一个 Line 类的实例,所需考察的行;第二个参数 end 是一个布尔值,表示获取的位置是行尾还是行头
    该方法的返回值是一个 SelectionRange 类的实例,以表示行头或行尾的位置
  • moveToLineBoundary(start, forward, includeWrap?) 方法:以一行作为步长移动光标,该方法最后返回一个 SelectionRange 类的实例(表示新的光标位置)
    • 第一个参数 start 是一个 SelectionRange 类的实例(选区范围),表示光标的初始状态
    • 第二个参数 forward 是一个布尔值,用于设置光标的移动方向,具体规则和上一个方法 moveByChar() 的同名参数一样
    • 第三个(可选)参数 includeWrap 是一个布尔值(默认值为 true),是否需要将自动换行点也纳入考虑。如果该参数为 true,则光标会跳跃到下一个自动换行点(而不是行头或行尾);如果该参数为 false,则光标只会跳跃到行首或行尾(即该行第一个字符前面的位置,或最后一个字符后面的位置,是在逻辑上的首尾位,并不考虑文本内容在视觉上的走向,即不考虑 LTR 或 RTL)
      line wrap point 自动换行点

      line wrap point 是指长文本由于超过了一行的宽度限制而自动换行的地方,这些换行点与文本的逻辑结构无关,仅仅是为了在有限的显示区域内更好地展示内容

      并不是所有的文本编辑器都会启用自动换行,但在启用自动换行 line wrap 的情况下,编辑器会根据视口的宽度将逻辑上处于一行(即这些文本是归为一行存储的)在页面分为多行显示,而不是让用户滚动水平方向来查看长文本

      自动换行点通常出现在以下地方,以便符合阅读习惯:

      • 单词之间的空格处
      • 标点符号或其他分隔符处

      某些编辑器可能会在长单词中间进行断行,具体取决于它们的换行算法

  • moveVertically(start, forward, distance?) 方法:垂直移动光标,该方法最后返回一个 SelectionRange 类的实例(表示新的光标位置)
    • 第一个参数 start 是一个 SelectionRange 类的实例,表示光标的初始状态
      💡 如果该选区范围对象设置了属性 goalColumn,则光标会基于此进行水平定位;否则会采用光标原本所在的水平位置作为期待列,并将其设置为最终返回的 selectionRange 对象的属性 goalColumn
    • 第二个参数 forward 是一个布尔值,用于设置光标的移动方向。如果为 true 表示沿着文档流延伸方向(向下)移动
    • 第三个(可选)参数 distance 用于设置垂直移动的距离(像素),光标会移到文档距离该值最近的那一行。如果忽略该参数,则光标会默认移动到邻近一行
  • domAtPos(pos) 方法:获取编辑器文档位置 pos(采用字符偏移量来描述)所属的 DOM 元素(它作为父容器,包含该文档的位置),以及该文档位置对应到 DOM 元素里的偏移量(如果该 DOM 元素的内容是一系列子元素,则偏移量就表示子元素的索引值;如果 DOM 元素的内容是文本内容,则偏移量就表示字符数量)
    该方法最后返回一个对象 {node: Node, offset: number} 其中属性 node 是 DOM 元素,属性 offset 是偏移量
    ⚠️ 如果给定的文档位置并不是在 visibleRanges 可视的文档内容的范围内(即对应的 DOM 元素被折叠/隐藏了),则该方法返回的 DOM 元素可能并无实际意义,它也许指向一个 placeholder element 占位元素前面或后面的位置
  • posAtDOM(node, offset?) 方法:根据给定的 DOM 元素 node 获取相应的编辑器文档位置,第二个(可选)参数 offset 用于设置在 DOM 元素里的偏移量(默认值是 0
    该方法一般是基于 DOM event 事件对象,获取事件所发生在编辑器文档的对应位置
    如果给定的 DOM 元素 node 并不是在编辑器里,则会抛出错误
    该方法最后返回一个数值,表示编辑器文档的位置(采用字符偏移量来描述)
  • posAtCoords(coords, precise?) 方法:基于给定的坐标点 coords(它是一个对象 {x: number, y: number} 描述浏览器视口中的一个位置),获取对应的编辑器文档的位置
    该方法最后可能返回一个数值,表示编辑器文档的位置(采用字符偏移量来描述);也可能返回 null,如果给定的坐标点 coords 并不在编辑器 viewport 视口里
    如果配置了第二个(可选)参数 precisefalse,即使给定的坐标点并不在编辑器的 viewport 视口里,也会返回一个数值,它表示在给定坐标点附近的文档的位置
  • coordsAtPos(pos, side?) 方法:基于给定的编辑器文档位置 pos 获取对应 DOM 元素的位置和尺寸相关信息
    第二个(可选)参数 side 可以是 -11(默认值),用于设置获取给定的文档位置 pos 哪一侧的 DOM 元素。如果该参数为 -1,表示获取 pos 位置之前的 DOM 元素;如果该参数为 1,表示获取 pos 位置之后的 DOM 元素 💡 如果所配置的一侧没有 DOM 元素,则会自动获取另一侧的 DOM 元素
    该方法最后可能返回一个对象(它需要符合 TypeScript interface Rect),表示一个矩形框(用于描述 DOM 元素);也可能返回 null 如果在给定位置的两侧没有 DOM 元素
    Rect

    TypeScript interface Rect 描述一个矩形框(它是包含 DOM 元素的最小矩形框)的位置和尺寸相关信息

    ⚠️ 矩形框是带有方向的,与它所包含的文本走向相关(LTR 或 RTL)

    符合 TypeScript interface Rect 的对象具有以下属性:

    • left 属性:一个数值,表示矩形框左侧的 x 坐标(即到浏览器视口 viewport 左侧距离)
    • right 属性:一个数值,表示矩形框右侧的 x 坐标(即到浏览器视口 viewport 左侧距离)
    • top 属性:一个数值,表示矩形框的顶部的 y 坐标(即到浏览器视口 viewport 顶部的距离)
    • bottom 属性:一个数值,表示矩形框的底部的 y 坐标(即到浏览器视口 viewport 的顶部的距离)
  • coordsForChar(pos) 方法:基于给定的编辑器文档位置 pos 获取对应字符的位置和尺寸相关信息
    该方法最后可能返回一个对象(它需要符合 TypeScript interface Rect),表示一个矩形框;也可能返回 null 如果在给定位置并不是在一个字符前面
  • defaultCharacterWidth 属性:一个数值,表示编辑器字符的默认宽度
    ⚠️ 编辑器有些范围的字符宽度可能与该属性值不同,由于它们采用不同的 font 或 style
  • defaultLineHeight 属性:一个数值,表示编辑器的默认行高
    ⚠️ 编辑器有些行的高度可能与该属性值不同,由于它们采用不同的 style
  • textDirection 属性:编辑器整体的文本走向(即编辑器视图所对应的 DOM 容器的 CSS 特性 direction
    该属性值需要是 TypeScript enum Direction 枚举类型中的一个值,可以是 LTR(表示文本走向从左往右)或 RTL(表示文本走向从右往左)
  • textDirectionAt(pos) 方法:获取给定的编辑器文档位置 pos 所在的 DOM 元素的文本走向(即 CSS 特性 direction
    该方法最后返回值需要是 TypeScript enum Direction 枚举类型中的一个值,可以是 LTR(表示文本走向从左往右)或 RTL(表示文本走向从右往左)
    提示

    如果通过 perLineTextDirection facet 配置编辑器每一行可以设置不同的文本走向,那么该方法在文档不同的位置返回的值就可能不同;否则该方法在不同的文档位置总是返回相同的值

    另外如果给定的文档位置 pos 在编辑器视口 viewport 之外,则该方法在文档不同位置返回的值都是一样的


    ⚠️ 该方法可能触发页面 reflow 重排
  • lineWrapping 属性:一个布尔值,表示编辑器是否自动换行(由编辑器视图所对应的 DOM 容器的 CSS 属性 white-space 决定)
    自动换行

    当 CSS 属性 white-space 的值为 "pre-wrap""normal""pre-line""break-spaces" 任一值,编辑器文档内容过长会自动换行;如果该属性值为 "nowrap""pre" 任一值,则不会自动换行

  • bidiSpans(line) 方法:获取给定行 line(一个 Line 类的实例)与双向文本的结构信息,返回值是一个数组,它的元素是一系列 BidiSpan 类的实例
    根据该行的整体文本走向,来决定哪一侧的 bideSpan 排在前面,例如当该行整体文本走向是 LTR,则表示左端起的一段文本范围的 bidiSpan 作为第一个元素,表示直至右端结束的一段文本范围的 bidiSpan 作为最后一个元素
    BidiSpan 类

    BidiSpan 类用于表示一段具有相同走向(LTR 或 RTL)的连续文本内容范围

    该类的实例化对象具有一些属性:

    • dir 属性:表示这一段范围里的文本走向,该属性值需要是 TypeScript enum Direction 枚举类型中的一个值,可以是 LTR(表示文本走向从左往右)或 RTL(表示文本走向从右往左)
    • from 属性:一个数值,表示该段文本范围的起点,相对于所在行的行首的字符偏移量
    • to 属性:一个数值,表示该段文本范围的终点,相对于所在行的行首的字符偏移量
    • level 属性:一个数值,以表示该段文本(由于双向文本构成这一行的内容,相对于该行最外层文本)的嵌套层级关系。该值与前后/上下文 context 相关,例如 0 表示位于最外层 LTR 的文本,1 表示位于最外层的 RTL 的文本,2 表示(被最外层为 LTR包裹着的)LTR 的文本
  • hasFocus 属性:一个布尔值,表示编辑器视图当前是否获得焦点 focus
  • focus() 方法:将浏览器的焦点设置到编辑器视图上
  • setRoot(root) 方法:更新编辑器视图的在页面的根节点(或影子根节点),参数 root 是一个 Document 对象ShadowRoot 对象
  • destroy() 方法:销毁编辑器视图,移除编辑器视图在页面上的 DOM 元素,取消事件监听器,并 notify 通知插件(执行相应的注销操作)
    调用该方法后,编辑器视图实例不能再使用
  • scrollSnapshot() 方法:返回一个 StateEffect 类的实例,该 state effect 捕获/保存了编辑器当前编辑器所在滚动到的位置,可以在之后使用(例如在实例化 EditorView 时,可以通过配置对象的属性 scrollTo 来使用该自定义变更效应)将文档恢复到该位置
    提示

    该 state effect 的值是一个对象,包含了一些与编辑器滚动位置相关的信息

    js
    {
      range: SelectionRange,
      y: "nearest" | "start" | "end" | "center",
      x: "nearest" | "start" | "end" | "center",
      yMargin: number,
      xMargin: number,
      isSnapshot: boolean,
      map: fn(changes: ChangeDesc) → Object,
      clip: fn(state: EditorState) → Object
    }
    

    该 state effect 只是包含编辑器视图的可滚动容器 scroller element 的滚动信息,而不是它的最外层容器 outer wrapper 或页面的其他元素的滚动信息


    ⚠️ 该 state effect 只能应用于(生成它的)相同的文档状态 identical document;如果文档发生了变更,如果直接将原 state effect 应用于新文档(并不会有错误提示)但滚动的结果可能并不是预期位置,则可以先将该 state effect 进行映射处理,再应用于新的文档
  • setTabFocusMode(to?) 方法:用于切换编辑器的 tab-focus 模式的开关
    tab-focus 模式

    tab-focus 模式指使用 Tab 键进行页面元素的聚焦切换

    一般代码编辑器会将 Tab 键用于插入缩进,但是该默认行为会导致光标「困」在编辑器中,特别是对于依赖键盘导航的用户,可以通过该方法禁用编辑器中的 Tab 键默认行为,让浏览器的焦点切换功能继续工作


    (可选)参数 to 有多种情况:
    • 如果调用该方法时,不对该参数进行配置,则基于 tab-focus 当前模式(启用或禁用)进行切换
    • 如果该参数配置为 true 则启用 tab-focus 模式(恢复浏览器的焦点切换行为)
    • 如果该参数配置为 false 则禁用 tab-focus 模式(继续将 Tab 键用作实现编辑器的缩进功能)
    • 如果该参数配置为一个数值(表示毫秒数),则会暂时启用 tab-focus 模式(恢复浏览器的焦点切换行为),持续指定的时间,或当用户按下除 Tab 之外按键就禁用 tab-focus 模式(继续将 Tab 键用作实现编辑器的缩进功能)

EditorView 类还提供了一些静态方法或属性:

  • static EditorView.scrollIntoView(pos, options?) 静态方法:返回一个 StateEffect 类的实例,该 state effect 可以将给定的文档位置或选区范围滚动进编辑器的视图里
    第一个参数 pos 可以是一个数值,表示编辑器文档的位置(采用字符偏移量来描述);也可以是一个 SelectionRange 类的实例
    第二个(可选)参数 options 是一个对象,具有一些属性可以对滚动行为和最终位置进行更具体的配置
    • (可选)属性 y:设置纵向滚动,可以是以下 4 种值之一:
      • "nearest"(默认值)通过最短距离的垂直滚动让给定的位置进入视图
      • "start" 通过垂直滚动让给定的位置在视图的顶部
      • "end" 通过垂直滚动让给定的位置在视图的底部
      • "center" 通过垂直滚动让给定的位置置于视图的中间
    • (可选)属性 x:设置横向滚动,可以是 "nearest"(默认值)、"start""end""center" 这 4 种值之一
    • (可选)属性 yMargin:一个数值(默认值为 5),表示给定的文档位置与编辑器视图顶部的距离
    • (可选)属性 xMargin:一个数值(默认值为 5),表示给定的文档位置与编辑器视图左侧的距离
    滚动吸附位置

    属性 xMarginyMargin 可以对滚动吸附位置进行更精确的设置,它们的值需要小于编辑器尺寸才合理(否则给定的位置无法出现在编辑器的 viewport 视口)

  • static EditorView.styleModule 静态属性:一个 Facet 类的实例,用于为编辑器视图设置样式
    该 facet 的输入/输出值都是一个 style module(以键值对的形式表示 CSS 样式规则),这些样式会应用到编辑器视图的 root 根节点
    style module

    CodeMirror 内部使用 CSS-in-JS 为编辑器添加样式

    其中用到一个称为 style-mod 的库,可以采用声明式(对象,键值对的形式)来生成 CSS 样式规则

    ts
    const {StyleModule} = require("style-mod")
    // 以键值对的形式来表示 CSS Selector 与相应的样式
    const myModule = new StyleModule({
      "#main": {
        fontFamily: "Georgia, 'Nimbus Roman No9 L'",
        margin: "0"
      },
      ".callout": {
        color: "red",
        fontWeight: "bold",
        "&:hover": {color: "orange"}
      }
    })
    // 将样式添加到给定的 DOM 元素上
    StyleModule.mount(document, myModule)
    
    区别

    CodeMirror 采用模块化设计,各种模块可能会包含相应的默认样式,一般是通过该 facet 向编辑器(根元素)注入样式

    可以将该 facet 的值视为编辑器的默认样式

    EditorView 所提供的另一个静态方法 static EditorView.theme() 生成一个插件,也是用于为编辑器添加样式,但是这些样式是在一个 scope 作用域下的(受到 CodeMirror 自动生成的 class 类名的约束),可以将这些样式视为编辑器的自定义/主题样式

  • static EditorView.domEventHandlers(handlers) 静态方法:返回一个插件(需要符合一种 TypeScript type Extension),为内容元素 content element 注册 DOM 事件监听器(除了 scroll 事件的监听器注册在可滚动的容器 scroller element 上)
    参数 handlers 是一个对象(需要符合一种 TypeScript type DOMEventHandlers),以键值对的方式表示相应 DOM 事件的监听器
    DOMEventHandlers

    TypeScript type DOMEventHandlers 以键值对的形式描述了 DOM 事件和它的相应监听器

    ts
    type DOMEventHandlers<This> = {
      [event in keyof DOMEventMap]: fn(event: DOMEventMap[event], view: EditorView) → boolean | undefined
    }
    

    符合 TypeScript type DOMEventHandlers 的对象,其属性名是一个字符串以表示事件类型(其中 DOM 原始事件的名称具体有哪些,可以查看 TypeScript 内置的类型文件),相应的值是一个事件处理函数 fn(event, view) 它接受两个参数, 其中参数 event 是所分发的事件对象,参数 view 是当前的编辑器视图对象

    优先级

    可以通过该静态方法(所创建的插件)为针对同一种事件类型为编辑器注册多个事件响应函数

    这些事件响应函数的优先级(先后执行顺序)由所在的插件的优先级决定

    当事件处理函数返回 true 时,表示该事件已经被处理完成,则优先级较低的处理函数就不会被执行

  • static EditorView.domEventObservers(observers) 静态方法:返回一个插件(需要符合一种 TypeScript type Extension),它和前一个静态方法 static EditorView.domEventHandlers() 类似,也是为编辑器注册 DOM 事件监听器
    💡 不同的是通过该方法注册的事件监听器是一定会执行的,不管更高优先级的同类型的事件处理函数是否返回 true
    参数 handlers 是一个对象(需要符合一种 TypeScript type DOMEventHandlers),以键值对的方式表示相应 DOM 事件的监听器
    ⚠️ 在事件回调函数中,不应该调用 preventDefault 阻止事件所触发的默认行为
  • static EditorView.inputHandler 静态属性:一个 Facet 类的实例,用于设置编辑器如何响应/处理用户的输入操作
    该 facet 的输入值都是一个函数
    ts
    fn(
      view: EditorView,
      from: number,
      to: number,
      text: string,
      insert: fn() → Transaction
    ) → boolean
    

    该函数的各参数具体说明如下:
    • 第一个参数 view 是一个编辑器视图对象
    • 第二个参数 from 和第三个参数 to 都是一个数值,分别表示当前输入操作对文档的更改范围的起点和终点(采用字符偏移量来描述)
    • 第四个参数 text 是一个字符串,表示新插入/输入的内容
    • 第五个参数 insert 是一个函数,调用它可以获取针对该输入操作的默认的事务对象 transaction,然后可以基于该事务进行额外的自定义操作

    可以通过该 facet 为编辑器设置多个 input handlers,当任何一个处理函数返回 true 时,则优先级较低的处理函数就不会被执行,而且默认行为也会被阻止
  • static EditorView.clipboardInputFilter 静态属性:一个 Facet 类的实例,用于为编辑器设置一个剪切板输入转换器,它会对(通过粘贴或拖放方式)插入到编辑器的字符串进行转换处理
    该 facet 的输入值是一个函数 fn(text: string, state: EditorState) → string 它接受原始插入的文本 text 和当前编辑器状态 state 作为参数,返回一个经过转换处理的字符串
  • static EditorView.clipboardOutputFilter 静态属性:一个 Facet 类的实例,用于为编辑器设置一个剪切板输出转换器,它会对(通过复制或拖拽方式)来自编辑器的内容进行转换处理
    该 facet 的输入值是一个函数 fn(text: string, state: EditorState) → string 它接受原始拷贝/拖拽的文本 text 和当前编辑器状态 state 作为参数,返回一个经过转换处理的字符串
  • static EditorView.scrollHandler 静态属性:一个 Facet 类的实例,用于设置编辑器如何响应/处理用户的滚动操作
    该 facet 的输入值都是一个函数
    ts
    fn(
      view: EditorView,
      range: SelectionRange,
      options: {
        x: "nearest" | "start" | "end" | "center",
        y: "nearest" | "start" | "end" | "center",
        xMargin: number,
        yMargin: number
      }
    ) → boolean
    

    该函数的各参数具体说明如下:
    • 第一个参数 view 是一个编辑器视图对象
    • 第二个参数 range 是一个 SelectionRange 类的实例,表示需要将该选区范围滚动进入视口里
    • options 是一个对象,表示更精细的滚动操作(具体介绍可以参考编辑器视图的另一个静态方法 static EditorView.scrollIntoView(pos, options?) 它的(可选)第二参数也是 options 对象

    该方法最后返回一个布尔值,如果为 true,则优先级较低的处理函数就不会被执行,而且默认行为也会被阻止
    ⚠️ 在该函数中不应该触发/执行编辑器更新
  • static EditorView.focusChangeEffect 静态属性:一个 Facet 类的实例,用于响应编辑器 focus state 聚焦状态改变
    该 facet 的输入值都是一个函数 fn(state: EditorState, focusing: boolean) → StateEffect<any> | null 该函数的第一个参数 state 是编辑器状态实例,第二个参数 focusing 是一个布尔值,表示编辑器当前是否获得焦点
    该函数最后可以返回一个 StateEffect 类的实例(该自定义变更效应会被自定添加到由于聚焦/失焦而触发的事务中);也可以返回 null 表示不会执行额外操作
  • static EditorView.perLineTextDirection 静态属性:一个 Facet 类的实例,用于设置编辑器每一行是否可以具有不同的 text direction 文本走向
    该 facet 的输入/输出值都是布尔值,表示是否需要分别读取(已渲染的)每一行的文本的走向,默认情况下假定编辑器的文本都是具有相同的文本走向,如果该 facet 的输出值为 true,就会单独判断每一行的文本走向
  • static EditorView.editable 静态属性:一个 Facet 类的实例,用于设置编辑器视图所对应的内容元素 content element 是否可编辑
    该 facet 的输入/输出值都是布尔值,表示编辑器在页面对应的 DOM 元素是否具有 contenteditable 属性(即是否可编辑)
    区分

    编辑器的状态也提供一个类似的动态配置项 facet readOnly,但是它与上述的动态配置项是有区别的

    • static EditorState.readOnly 编辑器状态的静态属性:一个 Facet<boolean, boolean> 实例对象(它的输入值和输出值也是 boolean 布尔值),以控制编辑器是否只读
      如果该动态配置项设置为 true,则对于编辑器内容的任何修改操作都无法执行,包含用户通过与(编辑器在页面上所对应的)DOM 元素的直接交互,或(通过点击菜单栏按钮)分发指令的间接操作都会被禁止
    • static EditorView.editable 编辑器视图的静态属性:一个 Facet<boolean, boolean> 实例对象(它的输入值和输出值也是 boolean 布尔值),表示编辑器在页面上所对应的 DOM 元素是否添加/移除 contenteditable 属性,以控制用户是否可以通过直接与编辑器交互的方式来修改编辑内容
      如果该动态配置项设置为 false,则编辑器在页面上所对应的 DOM 元素就不会带有 contenteditable 属性,即用户无法直接与编辑器进行交互,但是依然可以(通过点击菜单栏按钮或按下快捷键)分发指令来间接修改编辑器的内容
  • static EditorView.updateListener 静态属性:一个 Facet 类的实例,用于为编辑器设置更新监听器
    该 facet 的输入值是一个函数 fn(update: ViewUpdate) 当编辑器视图更新时会调用该函数,以执行特定的额外操作。参数 update 是一个 ViewUpdate 类的实例,描述了视图所发生的变更
  • static EditorView.mouseSelectionStyle 静态属性:一个 Facet 类的实例,用于设置当鼠标点击或拖动时如何影响编辑器的选区
    该 facet 的输入值是一个函数 fn(view: EditorView, event: MouseEvent) → MouseSelectionStyle | null 当鼠标按下时会调用该函数。该函数的第一个参数 view 是编辑器视图对象,第二个参数 even 是鼠标按下 mousedown 事件对象
    该函数最后可以返回一个对象(需要符合一种 TypeScript interface MouseSelectionStyle),通过该对象可以覆写编辑器的默认行为,采用自定义的方式,基于鼠标的点击或拖动计算出编辑器的选区;也可以返回 null 表示采用默认行为来响应鼠标的点击或拖动操作
    MouseSelectionStyle

    TypeScript interface MouseSelectionStyle 用于自定义响应鼠标的点击或拖动操作的方式

    符合 TypeScript interface MouseSelectionStyle 的对象具有以上方法:

    • get(curEvent, extend, multiple) 方法:基于给定的参数生成一个新的编辑器选区
      该方法的各个参数的具体说明如下:
      • 第一个参数 curEvent 是一个 MouseEvent 与鼠标相关的事件对象
        不同阶段的鼠标事件

        上述 facet 所接收到的鼠标事件(mousedown 事件对象)是在用户使用鼠标交互开始时所触发的

        而该参数的鼠标事件则是 交互过程中(拖动)或结束时(松开鼠标) 所触发的


        如果用户执行的是鼠标单击操作,则该参数就是上述 facet 所接收到的 mousedown 事件;如果用户正在执行拖动操作,则该参数就是 mousemove 事件;如果用户松开了鼠标,则该参数就是 mouseup 事件
      • 第二个参数 extend 是一个布尔值,如果为 true 表示该方法所创建的新选区是对已有选区的扩展,而不是替换(一般是用户按下 Shift 键的预期行为)
      • 第三个参数 multiple 是一个布尔值,如果为 true 表示新建的选区(所包含的选区范围)会添加到原有的选区里,则编辑器的选区就包含多个选区范围(一般是用户按下 Ctrl 键的预期行为)

      上述 facet 所接收到的鼠标事件记录了鼠标交互的开始状态,参数 curEvent 所接收到的鼠标事件记录了鼠标交互的当前状态,该方法基于这两个事件最后返回一个新创建的 editorSelection 编辑器选区
    • update(viewUpdate) 方法:在用户通过鼠标与编辑器交互的过程中,如果编辑器视图发生了改变就会执行该方法
      参数 viewUpdate 是一个 ViewUpdate 类的实例,描述了视图所发生的变更
      如果在使用鼠标交互的过程中,编辑器视图发生了变化,例如文档内容改变了,则需要在该方法里对鼠标开始点击的位置进行 map 映射处理(随文档同步更新),以保证位置的有效性
      该方法最后返回一个布尔值或 undefined,(如果为 true)以表示是否需要在视图更新后再执行一次前面的 update 方法(由于视图的更新可能导致该方法的返回值改变)

    ⚠️ 请注意编写上述两个方法的逻辑,避免触发无限更新的死循环,即 update 返回值为 true 会触发 get 方法的再次执行,而 get 返回的的新选区又导致视图更新

  • static EditorView.dragMovesSelection 静态属性:一个 Facet 类的实例,用于设置当拖拽编辑器的选区时采用移动还是复制的方式来处理选中的内容
    该 facet 的输入值是一个函数 fn(event: MouseEvent) → boolean 参数 event 是鼠标按下 mousedown 事件对象,最后返回一个布尔值,如果为 true 表示在拖拽选区时要同时移动选中的内容
  • static EditorView.clickAddsSelectionRange 静态属性:一个 Facet 类的实例,用于设置用户点击时是否往原有的选区添加新建的 range 选区范围,还是新建一个选区替代原有的选区
    该 facet 的输入值是一个函数 fn(event: MouseEvent) → boolean 参数 event 是鼠标按下 mousedown 事件对象,最后返回一个布尔值,如果为 true 表示往原有的选区添加新建的 range 选区范围,则选区包含多个选区范围
    默认行为

    默认的点击行为是在按下 Ctrl 键(对于 macOS 系统则是按下 meta 键)才往原有的选区添加新建的 range 选区范围

    对比

    前面所述的另一个静态属性 static EditorView.mouseSelectionStyle 也可以用于自定义鼠标的交互行为,它也包含鼠标点击的操作,但是灵活度更高,由于在该 facet 的输出值(一个对象)中,其方法 get 的返回值就是一个新建的选区对象,即可以手动控制新选区如何生成

    可以根据需求,选择合适的 facet 自定义鼠标的交互行为:

    • static EditorView.mouseSelectionStyle 自定义鼠标选择文本时的行为,可以对新创建的选区进行精细的控制
    • static EditorView.clickAddsSelectionRange 自定义鼠标点击时的行为,只是简单地控制是否将新的选择范围添加到已有的选区中
  • static EditorView.decorations 静态属性:一个 Facet 类的实例,用于设置在编辑器视图中添加哪些装饰器
    该 facet 的输入值可以是一个 DecorationSet 类的实例(包含一系列装饰器);也可以是一个函数 fn(view: EditorView) → DecorationSet 其参数 view 是编辑器视图对象,返回值也是一个 DecorationSet 类的实例
    两种不同的方式提供装饰器

    以上 facet 有两种类型的输入值,适用于不同的场景(可以包含的装饰器类型不同):

    • 直接(静态)提供的一个 decorationSet,它可以包含任意类型的装饰器,特别是支持 block widget decoration 块级挂件型装饰器(这类装饰器会影响编辑器的垂直方向的布局
    • 通过函数(动态)提供一个 decorationSet,它只能包含不影响编辑器布局的装饰器,例如 mark decoration 标记装饰器,不能支持 block widget decoration 块级挂件装型饰器,以及不能支持覆盖换行符的 replace decoration 替换型装饰。因为该函数会在编辑器的视口 viewport 计算生成后再调用,所以该函数返回装饰器(一般是定位到 viewport 里)不应该再触发编辑器垂直方向布局的更改,因为这回影响到 viewport 的变化

    虽然通过函数(动态)所提供的装饰器类型有限制,但是唯有这种方式才可以访问到 viewport,所以它特别适用于只针对可视区域设置装饰器的场景,例如代码高亮,不需要为整个文档都添加装饰器可以优化编辑器的性能

    原子化

    如果想让某些装饰所对应的范围表现为 atomic units 原子单位(即在光标移动和删除操作时,整体移动或删除,而不是逐字符操作),则还需要使用另一个编辑器视图的静态属性 static EditorView.atomicRanges,它也是一个 facet,将这些装饰所对应的范围设置为原子单位

  • static EditorView.outerDecorations 静态属性:一个 Facet 类的实例,用于设置在编辑器视图中添加哪些装饰器,它们的优先级是最低的
    该 facet 的输入值可以是一个 DecorationSet 类的实例(包含一系列装饰器);也可以是一个函数 fn(view: EditorView) → DecorationSet 其参数 view 是编辑器视图对象,返回值也是一个 DecorationSet 类的实例
    对比

    与上一个 facet EditorView.decorations 所设置的装饰器相比,通过该 facet 所设置的装饰器的优先级是最低的,例如使用该 facet 所设置的 mark decoration 标记型装饰器,它们会作为给定范围的内容的最外层的包裹容器

    这些装饰器的优先级是最低的,所以称为 outer decoration,它们作为一个整体存在,呈现出连贯的标记的效果,比如背景高亮、边框等

  • static EditorView.atomicRanges 静态属性:一个 Facet 类的实例,用于设置文档中的 atomic 原子化范围
    该 facet 的输入值是一个函数 fn(view: EditorView) → RangeSet<any> 参数 view 是编辑器视图对象,返回一个 RangeSet 类的实例(包含一系列 range 文档范围,这些范围会被视为原子单位)
    原子化

    如果文档的某个范围表现为 atomic units 原子单位,会将该范围的文本视为不可拆分的整体,光标无法定位到该范围里面(以进行逐字符的操作)

    则当(使用方法 editorView.moveByChar()方法 editorView.moveVertically() 或其他方法,以及基于这些方法构建的 commands 指令)移动光标时遇到这些范围,会一次性跨越/跳过整个范围,而不是在该范围里逐字符地移动

    对于删除操作也是,会一次性将整个范围的内容都删掉,而不是逐字符地删除

    一般会将编辑器里嵌入的装饰器所覆盖的范围进行原子化,保证这些组件是一个整体(视为不可拆分的单元),用户在导航光标时应跳过这些组件(而不能进入这些组件内部)

    ⚠️ 虽然通过该 facet 所设置的范围,会影响用户通过键盘或鼠标进行的光标移动,但它不会阻止通过 programmatic 命令式地更新编辑器选区/光标进入这些范围里

  • static EditorView.bidiIsolatedRanges 静态属性:一个 Facet 类的实例,用于为编辑器视图中添加一些与双向文本相关的装饰器
    该 facet 的输入值可以是一个 DecorationSet 类的实例(包含一系列装饰器);也可以是一个函数 fn(view: EditorView) → DecorationSet 其参数 view 是编辑器视图对象,返回值也是一个 DecorationSet 类的实例
    这些装饰器是 mark decorations 标记型装饰器,所覆盖的范围是跨越一段文本的(而不是文档中的 point 一个点),它们为该范围的内容添加 CSS 属性 unicode-bidi: isolate,以便编辑器可以正确处理复杂嵌套的双向文本的布局
    提示

    还需要同步设置这些 mark decorations 标记型装饰器的配置参数(一个对象)的属性 bidiIsolate,以表示在各个装饰器范围内的文本方向

    注意

    使用 mark decorations 标记型装饰器为范围里的内容设置 CSS 属性 unicode-bidi,其值只能是 isolatenormal,其他值并不支持

  • static EditorView.scrollMargins 静态属性:一个 Facet 类的实例,为编辑器的可滚动的容器 scroller element 添加 margin 留白
    说明

    由于在可滚动元素的一侧可能浮动显示了其他元素,例如工具栏、边栏等,则需要在该侧添加额外的留白,才可以让 scroller element 的内容不会被遮挡


    该 facet 的输入值是一个函数 fn(view: EditorView) → Partial<Rect> | null 参数 view 是编辑器视图对象。如果需要添加留白,则该函数最后返回的一个对象(它包含 TypeScript interface Rect的一部分属性),表示在 scroller element 相应位置添加 margin;否则,该函数返回 null
  • static EditorView.theme(spec, options) 静态方法:创建一个插件,用于将主题应用到编辑器上(以便为编辑器添加自定义样式)
    参考

    各参数的具体说明如下:
    • 第一个参数 spec 是一个对象,用于设置该主题所包含的样式
      该配置对象需要符合 TypeScript type StyleSpec(该类型约束和 style-mod 模块的 class StyleModule 配置对象所遵循的约束 TypeScript type Style 一样)以键值对的形式表示 CSS 样式规则,这些样式会应用到编辑器视图的 root 根节点
      style module

      CodeMirror 内部使用 CSS-in-JS 为编辑器添加样式

      其中用到一个称为 style-mod 的库,可以采用声明式(对象,键值对的形式)来生成 CSS 样式规则

      ts
      const {StyleModule} = require("style-mod")
      // 以键值对的形式来表示 CSS Selector 与相应的样式
      const myModule = new StyleModule({
        "#main": {
          fontFamily: "Georgia, 'Nimbus Roman No9 L'",
          margin: "0"
        },
        ".callout": {
          color: "red",
          fontWeight: "bold",
          "&:hover": {color: "orange"}
        }
      })
      // 将样式添加到给定的 DOM 元素上
      StyleModule.mount(document, myModule)
      

      CodeMirror 会自动将生成的 CSS 样式规则添加到编辑器视图的 root 根节点(即编辑器所在的页面的 document 元素)

      CodeMirror 还会自动为每个激活的主题生成一个 class 类名,并将该类名添加到编辑器视图的外层元素上

      html
      <!-- CodeMirror 会在编辑器的最外层容器上添加(激活的主题)相应的 class 类名 -->
      <div class="cm-editor [theme scope classes]">
        <!-- ... -->
      </div>
      

      ⚠️ 为了让自定义的样式约束在一个 scope 作用域下(更方便管理,且提高 CSS 选择器的优先级),会自动为配置对象 spec每一个 CSS 规则的选择器 selector 前面添加该主题所对应(由 CodeMirror 自动生成)的 class 类名,作为父选择器

      而如果所需设置的样式是针对编辑器的外层元素的,则可以在该样式规则的选择器中使用 & 符号,它代表该主题所对应(由 CodeMirror 自动生成)的 class 类名,则可以更精准地控制该 class 类名如何整合到该样式的选择器中

      例如 .gen001 是 CodeMirror 为当前激活主题所自动生成的 class 类名,如果在配置对象中其中一个自定的样式所采用的选择器是 .cm-content,则最终生成的选择器是 .gen001 .cm-content;而如果自定的样式所采用的选择器是 &.cm-content,则最终生成的选择器是 .gen001.cm-content(而不是 .gen001 .gen001.cm-content

      更多介绍参考文章 Example: Styling

    • 第二个(可选)参数 options 是一个对象 {dark⁠?: boolean} 具有一个可选属性 dark 如果设置为 true 表示它是一个暗黑模式的主题,则在采用 base theme 基础主题的 fallback 回退样式时,会应用具有 &dark 选择器的样式(而不是具有 &light 选择器的样式)
  • static EditorView.darkTheme 静态属性:一个 Facet 类的实例,记录当前所激活的主题是否为一个暗黑模式的主题
    该 facet 的输入值和输出值都是一个布尔值
  • static EditorView.baseTheme(spec) 静态方法:创建一个插件,用于为编辑器设置基础主题
    参数 spec 是一个对象,用于设置该主题所包含的样式,它需要符合 TypeScript type StyleSpec(该类型约束和 style-mod 模块的 class StyleModule 配置对象所遵循的约束 TypeScript type Style 一样)以键值对的形式表示 CSS 样式规则,这些样式会应用到编辑器视图的 root 根节点
    对于要直接应用到根节点的样式,则可以使用 & 符号作为 selector 选择器;如果该样式是在明亮模式下,才应用到根节点上,则使用 &light 符号作为 selector 选择器;如果该样式是在暗黑模式下,才应用到根节点上,则使用 &dark 符号作为 selector 选择器
  • static EditorView.cspNonce 静态属性:一个 Facet 类的实例,输入值和输出值都是字符串。提供一个 Content Security Policy nonce,让浏览器加载编辑器所需的样式表。如果想清空 nonce,则可以将该 facet 的值设置为空字符串
  • static EditorView.contentAttributes 静态属性:一个 Facet 类的实例,为编辑器视图在页面的内容元素 content element 添加 DOM attributes 属性
    该 facet 的输入值可以是一个对象,以键值对的形式设置 DOM attributes 属性;也可以是一个函数 fn(view: EditorView) → Object<string> | null 动态计算所需添加的 DOM 属性,该函数的参数 view 是编辑器视图对象,该函数最后可能返回值也是一个对象,以键值对的形式设置 DOM 属性,也可能返回 null 表示不添加额外的 DOM attributes
  • static EditorView.editorAttributes 静态属性:一个 Facet 类的实例,为编辑器视图在页面的最外层容器 outer wrapper 添加 DOM attributes 属性
    该 facet 的输入值可以是一个对象,以键值对的形式设置 DOM attributes 属性;也可以是一个函数 fn(view: EditorView) → Object<string> | null 动态计算所需添加的 DOM 属性,该函数的参数 view 是编辑器视图对象,该函数最后可能返回值也是一个对象,以键值对的形式设置 DOM 属性,也可能返回 null 表示不添加额外的 DOM attributes
  • static EditorView.lineWrapping 静态属性:创建一个插件,用于启动编辑器自动换行的功能(自动为编辑器的内容添加 CSS 属性 white-space: pre-wrap 来实现)
  • static EditorView.announce 静态属性:一个 StateEffectType 自定义变更效应的类型
    该类型的 state effect 对象的具体内容是字符串,将它添加到 transaction 事务中,可给 screen reader 提供对于该事务的描述,以优化编辑器的无障碍体验
    这些文本内容会添加到一个不可见的/隐藏的 DOM 元素内,该元素具有属性 aria-live="polite"
  • static EditorView.findFromDOM(dom) 静态方法:从给定的 dom(一个 HTMLElement 元素,它是编辑器视图所关联的 DOM 元素)获取对应的 view 实例;如果给定的 DOM 元素不属于任何编辑器视图,则返回 null
  • static EditorView.exceptionSink 静态属性:一个 Facet 类的实例,用于处理来自插件 extension(主要是视图插件 viewPlugin,但也可能用于其他 extension,例如来自用户所提供的代码在执行时的异常)抛出的异常
    该 facet 的输入值是一个函数 fn(exception: any) 该函数的参数 exception 是捕获到的异常信息,常用于代码调试和记录日志

拓展视图

ViewUpdate 类

ViewUpdate 类描述了编辑器视图所发生的变更,它具有以下属性:

  • change 属性:一个 ChangeSet 类的实例,表示在这一次视图的更新中,将哪些变更 changes 应用到文档上
  • startState 属性:一个 EditorState 类的实例,表示视图变更之前的(原始)编辑器状态
  • view 属性:一个 EditorView 类的实例,表示该更新所描述/关联的编辑器视图对象
  • state 属性:一个 EditorState 类的实例,表示视图变更之后的(新的)编辑器状态
  • transactions 属性:一个数组,其元素是一系列 Transaction 类的实例,表示该变更所应用的事务(可能为空)
  • viewportChanged 属性:一个布尔值,表示该变更是否包含编辑器视口 viewport可视区域 visible ranges 的改变
  • heightChanged 属性:一个布尔值,表示该变更是否包含 the height of a block element in the editor changed 编辑器中块级元素的的高度改变
  • geometryChanged 属性:一个布尔值,表示该变更是否包含 the document was modified 文档内容的修改,或 the size of the editor changed 编辑器尺寸的变化,或 elements within the editor changed 编辑器里的元素的改变
  • focusChanged 属性:一个布尔值,表示该变更是否包含编辑器的聚焦状态的变化
  • docChanged 属性:一个布尔值,表示该变更是否包含文档内容的变化
  • selectionSet 属性:一个布尔值,表示该变更是否包含对编辑器选区的显式设置

ViewPlugin 类

ViewPlugin 类可以为编辑器视图自定义行为和外观,例如添加 DOM 元素、监听事件、响应视图变更等

参考

ViewPlugin 类提供了两个静态方法进行实例化:

  • static ViewPlugin.define(createFn, spec?) 静态方法:使用给定的函数 createFn 创建一个 viewPlugin 实例对象
    第一个参数是一个函数 createFn(view: EditorView) → V,它接收的参数 view 是编辑器视图对象,返回值是一个对象(其类型 extends 扩展了另一个 Typescript interface PluginValue,即具有该接口 interface 所描述的属性和方法,还可能有一些其他的属性和方法)
    PluginValue

    Typescript interface PluginValue 描述了视图插件的一些行为:

    • update(可选)属性:它是一个函数 fn(update: ViewUpdate) 接收的参数 update 是一个 ViewUpdate 类的实例,描述了编辑器视图所发生的具体变更
      该函数会在视图发生变更时调用(但会在编辑器视图在页面所对应的 DOM 元素更新之前调用)
      所以该函数可用于依据编辑器视图所发生的具体变更从而对视图插件进行更新,例如对该视图插件在内部所维护的状态信息进行更新,或对插入到页面的自定义 DOM 元素进行更新
      ⚠️ 为了减少频繁触发 DOM 的 reflow 重排,可以将需要读取 DOM 尺寸布局的相关代码封装到方法 requestMeasure
    • docViewUpdate(可选)属性:它和 update 属性类似,也是一个函数 fn(update: ViewUpdate) 接收的参数 update 是一个 ViewUpdate 类的实例
      该函数会在 content, decoration, or viewport changes 文档内容,或装饰器,或视口发生变化导致视图变更时调用
      ⚠️ 不能在该函数里立即触发又一次的视图更新,同样要注意避免频繁触发 DOM 的 reflow 重排
    • destroy(可选)属性:一个函数,会在视图插件不再需要时(被删除时)调用,一般是解除该视图插件对编辑器视图的影响,例如移除添加到编辑器的自定义 DOM 元素

    第二个(可选)参数是一个对象(需要符合一个 Typescript interface PluginSpec) ,用于为该视图插件添加额外的配置信息
    PluginSpec

    Typescript interface PluginSpec 描述了该视图插件的额外配置信息,该类型 extends 扩展了另一个 Typescript interface PluginValue,即具有该接口 interface 所描述的属性和方法,还可能有一些其他的属性和方法

    符合 Typescript interface PluginSpec 的对象,除了可以具有 Typescript interface PluginValue 接口所定义的属性,还具有以下属性和方法:

    • eventHandlers(可选)属性:为该视图插件(添加到编辑器的自定义 DOM 元素)注册 DOM 事件监听器
      该属性值是一个对象(需要符合一种 TypeScript type DOMEventHandlers),以键值对的方式表示相应 DOM 事件的监听器
      这些事件监听器的回调函数中的 this 会绑定到该视图插件的值 plugin value
    • eventObservers(可选)属性:它和前一个属性的作用类似,也是为该视图插件(添加到编辑器的自定义 DOM 元素)注册 DOM 事件监听器
      💡 不同的是通过该方法注册的事件监听器是一定会执行的,不管更高优先级的同类型的事件处理函数是否返回 true
      该属性值是一个对象(需要符合一种 TypeScript type DOMEventHandlers),以键值对的方式表示相应 DOM 事件的监听器
      这些事件监听器的回调函数中的 this 会绑定到该视图插件的值 plugin value
    • provide(可选)属性:一个函数 fn(plugin: ViewPlugin<V>) → Extension 用于设置该 viewPlugin 所依赖的相关插件
      该函数的入参 plugin 是当前视图插件实例
      最后返回一个对象或数组(需要符合一种 TypeScript type Extension
      自动加载相关插件

      当编辑器使用该 viewPlugin 时(在实例化编辑器视图对象时,将它作为插件添加到配置对象的 extensions 属性上),该属性所设置的一系列插件会自动应用到编辑器上

    • decorations(可选)属性:一个函数 fn(value: V) → DecorationSet 用于说明/声明该 viewPlugin 所依赖/创建的一些装饰器
      该函数的入参 plugin 是当前视图插件的值 plugin value
      最后返回一个 DecorationSet 类的实例
      ⚠️ 所返回的 decorationSet 只能包含不影响编辑器布局的装饰器,例如 mark decoration 标记装饰器,不能支持 block widget decoration 块级挂件装型饰器,以及不能支持覆盖换行符的 replace decoration 替换型装饰
  • static ViewPlugin.fromClass(class, spec?) 静态方法:使用给定的类 class 创建一个 viewPlugin 实例对象
    第一个参数是一个类的定义主体 cls: {new (view: EditorView) → V},它的构造函数接收一个参数 view 是编辑器视图对象,CodeMirror 会使用它进行实例化,所得到的实例化对象的类型 extends 扩展了另一个 Typescript interface PluginValue,即具有该接口 interface 所描述的属性和方法,还可能有一些其他的属性和方法,具体介绍看 ☝️ 前面的静态方法)
    第二个(可选)参数是一个对象(需要符合一个 Typescript interface PluginSpec) ,用于为该视图插件添加额外的配置信息(具体介绍看 ☝️ 前面的静态方法)
说明

以上两个静态方法都可以用于实例化 viewPlugin,但也有不同点(基本上是 JS 的类和函数的区别):

  • static ViewPlugin.define(createFn, spec?) 其函数 createFn 的返回值作为 plugin value,而 static ViewPlugin.fromClass(class, spec?) 其类 class 的实例化对象作为 plugin value
  • 采用函数的方式代码简短,适合创建逻辑简单的插件;而类的方式则强调面向对象,让代码更加结构化,适合创建复杂的插件
  • 采用函数的方式,其内部状态通常以闭包或返回的对象的形式来管理;而对于类的方式,其内部状态一般会作为类的实例属性进行管理,结构更清晰便于维护和扩展
如何使用

viewUpdate 对象被视为一个 extension 插件

在实例化编辑器视图时,添加到配置对象的属性 config.extensions 上,就可以将视图插件添加到编辑器里

以下是一个通过 viewPlugin 为编辑器插入自定义 DOM 元素的简单示例

ts
// 在编辑器文档的右上角插入一个 <div> 元素,其内容是文档的长度(字符串数量)
import {ViewPlugin} from "@codemirror/view"

const docSizePlugin = ViewPlugin.fromClass(class {
  constructor(view) {
    this.dom = view.dom.appendChild(document.createElement("div"))
    this.dom.style.cssText =
      "position: absolute; inset-block-start: 2px; inset-inline-end: 5px"
    this.dom.textContent = view.state.doc.length
  }

  update(update) {
    if (update.docChanged)
      this.dom.textContent = update.state.doc.length
  }

  destroy() { this.dom.remove() }
})
使用 viewPlugin 添加装饰器

除了可以通过 JS 原生 API(采用 imperative 命令式)创建 DOM 元素,灵活地将它们插入到编辑器的任何位置;也可以通过装饰器,例如 widget decoration 挂件型装饰器,在编辑器文档内容的指定范围添加 DOM 元素

以下是一个通过 viewPlugin 为编辑器添加装饰器的简单示例

ts
// 完整代码参考 https://codemirror.net/examples/decoration/#boolean-toggle-widgets
import {ViewUpdate, ViewPlugin, DecorationSet} from "@codemirror/view"

// 使用 class 类模式创建视图插件
const checkboxPlugin = ViewPlugin.fromClass(
  // class body
  class {
    // 内部状态是一个 decorationSet 装饰器集合
    decorations: DecorationSet

    // 构建函数
    constructor(view: EditorView) {
      // 💡 初始化 decorationSet 装饰器集合
      this.decorations = checkboxes(view)
    }

    // 💡 根据视图的变更 update 对装饰器合集 decorationSet 进行更新
    update(update: ViewUpdate) {
      if (update.docChanged || update.viewportChanged ||
          syntaxTree(update.startState) != syntaxTree(update.state))
        this.decorations = checkboxes(update.view)
    }
  },
  // 配置对象
  {
    // 💡 声明该视图插件为编辑器添加的装饰器集合
    decorations: v => v.decorations,
    // 为该视图插件设置事件监听器
    eventHandlers: {
      // 监听鼠标按下事件 mousedown
      mousedown: (e, view) => {
        let target = e.target as HTMLElement
        if (target.nodeName == "INPUT" &&
            target.parentElement!.classList.contains("cm-boolean-toggle"))
          return toggleBoolean(view, view.posAtDOM(target))
      }
    }
  }
)

也可以通过静态方法 static EditorView.decorations 为编辑器添加装饰器,但是实现更新的逻辑比较繁琐:

  • 需要将更新逻辑写在 state field 中
  • 另外还需要通过 state effect 触发(state field)更新

以下是使用 static EditorView.decorations 为编辑器添加装饰器并实现更新的简单示例

ts
// 完整代码参考 https://codemirror.net/examples/decoration/#underlining-command
import {EditorView, Decoration, DecorationSet} from "@codemirror/view"
import {StateField, StateEffect} from "@codemirror/state"

const underlineField = StateField.define<DecorationSet>({
  create() {
    return Decoration.none
  },
  // 💡 更新 state field 的值,就相当于更新装饰器集合
  update(underlines, tr) {
    // 对于已存在的装饰器,采用映射 map 的方式进行更新
    underlines = underlines.map(tr.changes)
    // 查看 transaction 是否具有相应的 state effect,更新装饰器集合(新增装饰器)
    for (let e of tr.effects) if (e.is(addUnderline)) {
      // 对于新增的装饰器,添加到装饰器集合中
      underlines = underlines.update({
        add: [underlineMark.range(e.value.from, e.value.to)]
      })
    }
    return underlines
  },
  // 设置依赖于该 state field 的相关插件,该插件会自动应用到编辑器上
  // 💡 这里是使用静态方法 EditorView.decorations 为编辑器添加装饰器集合,初始值采用 state field 的值 Decoration.none,即一个空的装饰器集合
  provide: f => EditorView.decorations.from(f)
})

const underlineMark = Decoration.mark({class: "cm-underline"})

// state effect(用于触发 state field 更新,添加新的装饰器)
const addUnderline = StateEffect.define<{from: number, to: number}>({
  map: ({from, to}, change) => ({from: change.mapPos(from), to: change.mapPos(to)})
})

即使用静态方法 static EditorView.decorations 为编辑器添加装饰器,需要将关于装饰器的逻辑要分散写在三个不同的位置,维护起来比较繁琐

而是使用 viewPlugin 添加装饰器则可以将相关逻辑都写在该插件的构建函数和配置对象里,所以更推荐采用这种方式来为编辑器添加装饰器

指令

指令是一个函数(需要符合 TypeScript type Command),用于以编程式的方式来操作修改文档,一般与菜单 UI 或快捷键相绑定,可实现点击按钮或按下快捷键(执行该函数,其内部逻辑一般是分发事务 dispatch transaction)来一键修改文档内容

Command

TypeScript type Command 是一个函数 fn(target: EditorView) → boolean 它接收一个编辑器视图作为参数 target,返回一个布尔值表示该指令是否执行完成

该函数的内部逻辑一般是分发事务 dispatch transaction,以编程式的方式来操作修改文档

绑定按键

提示

@codemirror/commands 模块提供了很多内置的指令,并与相应的按键绑定,可以通过按快捷键触发相应的功能

key binding 按键绑定是指将按键映射到相应的指令上,让编辑器可实现在用户按下快捷键时执行相应的指令(指令的内部逻辑一般是分发事务 dispatch transaction),来一键修改文档内容

通过一个 facet keymap 为编辑器绑定按键,该 facet 的输入值是一个数组,其元素需要符合 TypeScript interface KeyBinding

KeyBinding

TypeScript interface KeyBinding 描述了如何将按键与指令相映射:

  • key(可选)属性:字符串,表示所需要绑定的按键名称(可以包含修饰符),例如 Shift-Ctrl-Enter
    该属性值是基于按键事件对象的属性值 keyEvent.key,表示用户所按下的按键,以下是关于如何使用字符串来表示相应按键的规则:
    • 使用小写字母表示相应的字母按键;而大写字母表示同时按下 Shift 和相应的字母按键
    • 空格键对应字符串 " ",但一般采用别名 Space 表示
    • (在按键前所添加的)修饰符具有简写形式:
      • Shift- 可简写为 s-
      • Alt- 可简写为 a-
      • Ctrl-Control- 可简写为 c-
      • Cmd-Meta- 可简写为 m-
    • macOS 系统的修饰符 Cmd-,在其他系统的对应修饰符是 Ctrl-,可以采用融合/统一表示方式 Mod-
    • 支持 multi-stroke binding 对一系列(具有先后顺序的)按键进行映射/绑定,按键之间用空格分隔。即监听多个按键依次先后按下的操作,而且这些按键可以具有修饰符,例如 Ctrl+k Ctrl-c 表示用户先按下快捷键 Ctrl-k,然后再按下快捷键 Ctrl-c
  • mac(可选)属性:字符串,表示在 macOS 系统所需要绑定的按键名称(即在 macOS 系统中,使用该属性所设置的按键具有最高优先级,会覆盖掉属性 key、属性win 和属性 linux 所设置的按键)
  • win(可选)属性:字符串,表示在 Windows 系统所需要绑定的按键名称(即在 Windows 系统中,使用该属性所设置的按键具有最高优先级,会覆盖掉属性 key、属性mac 和属性 linux 所设置的按键)
  • linux(可选)属性:字符串,表示在 Linux 系统所需要绑定的按键名称(即在 Linux 系统中,使用该属性所设置的按键具有最高优先级,会覆盖掉属性 key、属性win 和属性 mac 所设置的按键)
  • run(可选)属性:一个函数(需要符合 TypeScript type Command),当绑定的按键被用户按下时会执行该函数/指令
    当函数/指令返回 false 时,与相同按键所绑定的其他指令会继续执行,直到其中一个指令返回 true
  • shift(可选)属性:一个函数(需要符合 TypeScript type Command),当绑定的按键(不带 Shift- 修饰符)被用户按下时,同时按下了 Shift 键则会执行该函数/指令
    该属性相当于为按键绑定第二个指令,只有在 Shift 键同时按下时才触发 ❓
  • any(可选)属性:一个函数 fn(view: EditorView, event: KeyboardEvent) → boolean 会在按下任意按键时触发该函数
    一般情况下可以将它视为 keydown 事件的回调函数
    但该函数的执行逻辑与普通的 DOM 事件回调函数有一点不同,当编辑器设置了 multi-stroke binding 多按键的映射/绑定,如果按下的按键与 multi-stroke 的前面部分匹配,则该函数并不会触发,只有在触发到 multi-stroke 最后一个按键(或中途出现不匹配的情况)时才调用该函数
    监听 DOM 事件

    其实 CodeMirror 已经提供了一个静态方法 static EditorView.DOMEventHandlers(handlers) 可以为编辑器设置 DOM 事件监听器

    这里的提供另一种方式设置 DOM 事件监听器,两者适用场景所不同的,具体可以查看这个论坛的 discussion

    属性 any 所设置的函数特别针对 keydown 事件,然后通过 facet keymap 进行注册时,可以设置相应的 extension 插件的优先级(相对于其他 keymap),这样就可以进行更灵活的设置

  • scope(可选)属性:一个字符串,用于设置该按键的作用域
    按键的作用域

    作用域 scope 的作用是让按键在不同区域按下时,触发不同的指令,即相当于同一个按键可以在编辑器的不同区域可以映射到不同的指令

    该属性的默认值是 "editor",即只在聚焦到编辑器(可编辑元素)时,按下相应的按键才会触发绑定的指令

    而对于其他插件为编辑器添加其他面板/元素,可以通过调用方法 runScopeHandlers(view, event, scope) 以运行相应作用域下的按键所绑定的指令,具体介绍可查看下文相关方法

  • preventDefault(可选)属性:一个布尔值(默认值为 false),表示是否要阻止浏览器的默认行为
    ⚠️ 如果属性值为 true,即使该按键所绑定的函数/指令的返回值是 false,也会阻止浏览器的默认行为。一般是为了避免默认的浏览器快捷键行为干扰编辑器功能,而将(进行相应快捷键映射时)该属性设置为 true
    例如快捷键 Mod-u 所绑定的指令的作用是取消文本框选,而该快捷键在浏览器里的默认行为是查看页面的源代码,如果编辑器原本的选区就处于光标状态,则指令就不需要执行操作(返回 false),则可能触发浏览器默认行为,因此在绑定该按键时,需要将该属性 preventDefault 设置为 true
  • stopPropagation(可选)属性:一个布尔值,当设置为 true 会阻止相应的按键事件冒泡
    一般与前一个属性 preventDefault 一起使用,让编辑器完全拦截/接管 DOM 事件(而不触发浏览器的默认行为)

另外该模块还导出一个与按键相关的方法 runScopeHandlers(view, event, scope),调用它以执行作用域为 scope 按键为 event.key 所绑定的指令。具体的使用步骤如下:

  • 通过 facet keymap 为编辑器绑定按键,其中包含作用域为 scope(以变量名称,它的值是一个字符串)的按键映射
  • 在面板上设置 DOM 事件(一般是 keydown 事件)的监听器,在其中调用事件对象的方法 event.preventDefault() 对按键操作进行拦截
  • 在 DOM 事件的回调函数中调用方法 runScopeHandlers(view, event, scope) 以执行作用域为 scope 按键为 event.key 所绑定的指令
示例

可以参考 @codemirror/search 模块

该模块为编辑器添加了一个搜索面板,然后在面板元素上注册了 DOM 事件监听器,以拦截按键事件,并通过调用方法 runScopeHandlers(this.view, e, "search-panel") 来执行作用域为 "search-panel" 的按键所绑定的指令

装饰器

在默认情况下,CodeMirror 的文档只支持纯文本 plain text,而装饰器则可以为文档添加样式或其他元素,为纯文本内容增加点缀修饰

有 4 种类型的装饰器(Decoration 类提供了对应的静态方法进行实例化 ):

  • Mark decoration 标记型装饰器:为指定范围的文本内容添加样式 style 或 DOM 属性 attribute
  • Widget decoration 挂件型装饰器:在给定的文档位置插入 DOM 元素
    插入的 DOM 元素可以是 inline element(在行内,被文本围绕)或 block element(占据一整行)
  • Replacing decoration 替换型装饰器:可折叠隐藏指定范围的文档内容,或将指定范围的文档内容替换为其他文本/DOM 元素
  • Line decoration 行类型装饰器:为行元素 line element 添加 DOM 属性 attribute
    该类型的装饰器所绑定/关联的位置是在文档某一行的开头

Decoration 类

Decoration 类用于描述为指定的文档范围(位置)所设置的样式或插入的元素

Decoration 类提供了 4 种静态方法用于创建 4 种不同的装饰器实例:

  • static Decoration.mark(spec) 静态方法:基于配置对象 spec 创建一个标记型装饰器
    该类型的装饰器用于为指定范围的文本内容添加样式 style 或 DOM 属性 attribute
    嵌套规则

    为了给指定范围的文本内容添加样式或 DOM 属性,需要先使用 DOM 元素(默认是 <span> 元素)将这些文本内容包裹起来,然后在容器元素设置 CSS style 或 DOM attribute

    由于需要使用容器包裹文本,则可能会出现复杂的层级结构,为编辑器应用/添加标记型装饰器时 CodeMirror 遵循以下规则:

    • 当一系列标记型装饰器作用于相同的一段文档范围,会在页面生成相应的嵌套 DOM 结构,会根据这些装饰器的优先级别(由它们添加到编辑器时所使用的插件的优先级来决定,对于具有同等优先级的则根据先后顺序来决定)来决定对应容器元素之间的嵌套关系
      具有更高优先级的装饰器所对应的容器元素会位于嵌套的内层,而优先级较低的则位于外层
    • 当两个标记型装饰器所作用的文档范围存在部分重叠,则需要对容器元素进行「切分」 split
      具有较高优先级的装饰器所对应的容器元素需要在范围重叠的边界进行「切分」 split,以遵循前面规则,确保优先级更高的容器元素被较低级的容器元素所包裹
    • 当标记型装饰器所作用的文档范围跨越一行,则在换行处需要对容器元素进行「切分」 split

    例如编辑器文档内容是 "Hello World",使用两个标记型装饰器为文档添加样式

    • markDecoA(低优先级)应用到 "Hello ",让其背景变为黄色
    • markDecoB(高优先级)应用到 "Hello World",让其文字变为红色

    装饰器 markDecoA 与 markDecoB 所作用的部分范围有重叠,由于 markDecoA 优先级更低,所以 markDecoA 所对应的 DOM 元素会包裹 markDecoB 所对应的 DOM 元素,则装饰器 markDecoB 会在 markDecoA 的边界(即 "Hello " 的后面)被拆分,得到以下 HTML 结构

    html
    <span style="background-color: yellow;">
      <span style="color: red;">Hello</span>
    </span>
    <span style="color: red;">World</span>
    

    参数 spec 是一个对象,用于对标记型装饰器进行配置,具有以下属性:
    • inclusive(可选)属性:一个布尔值(默认值为 false),用于设置该装饰器是否包含指定范围的开始和结束,即在装饰器所作用的范围的开头或结尾插入新内容时,是否要将新添加的内容包含进该装饰器里
    • inclusiveStart(可选)属性:一个布尔值(默认值为 false),用于设置该装饰器是否包含指定范围的开始,即在装饰器所作用的范围的开头插入新内容时,是否要将新添加的内容包含进该装饰器里
      该属性优先级更高,会覆写另一个属性 inclusive 的相应配置
    • inclusiveEnd⁠(可选)属性:一个布尔值(默认值为 false),用于设置该装饰器是否包含指定范围的结尾,即在装饰器所作用的范围的结尾插入新内容时,是否要将新添加的内容包含进该装饰器里
      该属性优先级更高,会覆写另一个属性 inclusive 的相应配置
    • attributes(可选)属性:一个对象,以键值对的方式来表示所需添加的 DOM 属性或 CSS 样式
    • class(可选)属性:一个字符串,表示所需添加到容器元素的 CSS class 类名(可以将其看作是 {attributes: {class: value}} 的简写形式)
    • tagName(可选)属性:一个字符串,表示使用哪个 DOM 元素作为容器包裹指定范围的文本内容
      提示

      默认值是 "span" 即使用 <span> 元素作为容器

      该装饰器所对应/创建的容器元素可能是多个,例如所指定的文本跨越一行,则需要在换行处「切分」 slipt 为两个 DOM 元素

    • bidiIsolate(可选)属性:用于设置该装饰器所指定的文档范围内的文本方向
      该属性值需要是 TypeScript enum Direction 枚举类型中的一个值,可以是 LTR(表示文本走向从左往右)或 RTL(表示文本走向从右往左)
      提示

      还需要通过 static EditorView.bidiIsolatedRanges 这个 facet 为编辑器里标明哪些 mark decoration 标记型装饰器(它们为指定范围的文本内容添加 CSS 属性 unicode-bidi: isolate)与双向文本相关,以便编辑器可以正确处理复杂嵌套的双向文本的布局

      而在这些装饰器里需要设置 bidiIsolate 属性,以表示在相应的范围内的文本方向


      当忽略配置该属性,或将属性值设置为 null,表示该装饰器所作用的文档范围所采用与文本方向相关的 CSS 样式是 dir=auto,则其文本方向应该由第一个具有强方向性的字符来决定(而像标点符号等字符,对于文本方向的影响则为中性的)
    提示

    另外还可以在配置对象中添加其他自定义的属性(属性名需要为字符串)

    以键值对的方式为装饰器添加额外的信息,实例化后可以通过调用装饰器对象的方法 decoration.spec.propertyName 来获取相应属性 propertyName 的值

  • static Decoration.widget(spec) 静态方法:基于配置对象 spec 创建一个挂件型装饰器
    该类型的装饰器用于在给定的文档位置插入 DOM 元素(可以是 inline element,即该 DOM 元素在行内被文本围绕;也可以是 block element 即该 DOM 元素占据一整行)
    参数 spec 是一个对象,用于对挂件型装饰器进行配置,具有以下属性:
    • widget 属性:它是一个 WidgetType 类的实例,用于描述该装饰器所需在页面插入的挂件(DOM 元素)
      提示

      实际上该属性值是 class WidgetType 的子类的实例化对象

      由于 class WidgetType 是一个抽象类,不能直接实例化,它需要被其他类继承,在子类中实现抽象类中的抽象方法,才能进行实例化

      关于 WidgetType 类的具体介绍可以查看下文的相关部分

    • side(可选)属性:一个数值(默认值为 0,一般不大于 10000 或不小于 -10000),用于设置挂件(DOM 元素)插入到(装饰器所作用的)文档位置的哪一侧
      假设光标位于挂件装饰器所作用的文档位置上,通过该属性的正负值来控制所插入的 DOM 元素与光标的相对位置:
      • 当该属性值为正数,则挂件(DOM 元素)会插入到光标的后面
      • 当该属性值为非正数,则挂件(DOM 元素)会插入到光标的前面

      如果多个挂件型装饰器都作用到同一个文档位置,则它们在该位置所插入的 DOM 元素的先后顺序由该属性 side 的大小来决定,具有较小 side 属性值的挂件排在前面,具有较大值的排在后面
    • inlineOrder(可选)属性:一个布尔值,用于控制 block widget 块级挂件是否可以与 inline widget 行内挂件混排
      当多个 block widget 块级挂件与 inline widget 行内挂件作用于同一文档位置,为了避免它们相互混合,所以默认情况下,如果 block widget 的属性 side 为正值,则它们都会置于所有 inline widget 的后面;如果 block widget 的属性 side 为负值,则它们都会置于所有 inline widget 前面
      如果该挂件是 block widget 且属性 side 设置为 true,则它支持与 inline widget 混排,先后顺序由它们的属性 side 来决定
    • block(可选)属性:一个布尔值,表示该装饰器是否为 block widget
      block widget 与 inline widget

      根据挂件型装饰器在页面所插入的 DOM 元素所占据的空间,将其分为两种类型:

      • block widget 块级挂件型装饰器:在页面所插入的 DOM 元素是占据一整行的块级元素
      • inline widget 行内挂件型装饰器:在页面所插入的 DOM 元素是被文本围绕的行内元素
      注意

      block widget 在页面所插入的 DOM 元素不能具有垂直 margin 边距

      如果要动态更改 DOM 元素的高度,应该将相关代码封装到方法 requestMeasure 里,以便让编辑器可以同步更新与垂直布局相关的信息

    提示

    另外还可以在配置对象中添加其他自定义的属性(属性名需要为字符串)

    以键值对的方式为装饰器添加额外的信息,实例化后可以通过调用装饰器对象的方法 decoration.spec.propertyName 来获取相应属性 propertyName 的值

  • static Decoration.replace(spec) 静态方法:基于配置对象 spec 创建一个替换型装饰器
    该类型的装饰器可用于折叠/隐藏指定范围的文档内容,或将指定范围的文档内容替换为其他文本/DOM 元素
    参数 spec 是一个对象,用于对替换型装饰器进行配置,具有以下属性:
    • widget(可选)属性:它是一个 WidgetType 类的实例,用于描述该装饰器所采用的挂件(DOM 元素,以替换该装饰器所覆盖的文档范围的内容)
      提示

      实际上该属性值是 class WidgetType 的子类的实例化对象

      由于 class WidgetType 是一个抽象类,不能直接实例化,它需要被其他类继承,在子类中实现抽象类中的抽象方法,才能进行实例化

      关于 WidgetType 类的具体介绍可以查看下文的相关部分

    • inclusive(可选)属性:一个布尔值,用于设置该装饰器是否包含指定范围的开始和结束,即在装饰器所作用的范围的开头或结尾插入新内容时,是否要将新添加的内容包含进该装饰器里,这也会影响光标在范围边界的定位(光标绘制在范围的内侧还是外层 ❓ )
      对于 block replacement 即用于替换内容的挂件是块级元素,则该属性值默认为 true,即在块级替换型装饰器的范围前后新加入内容会被隐藏(光标绘制在范围内侧)
      对于 inline replacement 即用于替换内容的挂件是行内元素,则该属性值默认为 false,即在行内替换型装饰器的范围前后新加入内容并不会添加进该装饰器里(光标绘制在范围外侧)
    • inclusiveStart(可选)属性:一个布尔值,用于设置该装饰器是否包含指定范围的开始,即在装饰器所作用的范围的开头插入新内容时,是否要将新添加的内容包含进该装饰器里
      该属性优先级更高,会覆写另一个属性 inclusive 的相应配置
    • inclusiveEnd⁠(可选)属性:一个布尔值,用于设置该装饰器是否包含指定范围的结尾,即在装饰器所作用的范围的结尾插入新内容时,是否要将新添加的内容包含进该装饰器里
      该属性优先级更高,会覆写另一个属性 inclusive 的相应配置
    • block(可选)属性:一个布尔值(默认值为 false),表示该装饰器是否为 block replacement(即替换内容的挂件是块级元素)
    提示

    另外还可以在配置对象中添加其他自定义的属性(属性名需要为字符串)

    以键值对的方式为装饰器添加额外的信息,实例化后可以通过调用装饰器对象的方法 decoration.spec.propertyName 来获取相应属性 propertyName 的值

  • static Decoration.line(spec) 静态方法:基于配置对象 spec 创建一个行类型的装饰器
    该类型的装饰器用于为行元素 line element 添加 DOM 属性 attribute
    参数 spec 是一个对象,用于对行类型的装饰器进行配置,具有以下属性:
    • attributes(可选)属性:一个对象,以键值对的方式来表示所需添加的 DOM 属性或 CSS 样式
    • class(可选)属性:一个字符串,表示所需添加到容器元素的 CSS class 类名(可以将其看作是 {attributes: {class: value}} 的简写形式)
    提示

    另外还可以在配置对象中添加其他自定义的属性(属性名需要为字符串)

    以键值对的方式为装饰器添加额外的信息,实例化后可以通过调用装饰器对象的方法 decoration.spec.propertyName 来获取相应属性 propertyName 的值


Decoration 类其实是 extends 继承自 RangeValue 类,它的实例化对象只包含关于装饰器的类别和行为等相关信息,以便在文档更新时可以复用合适的装饰器

如果要将装饰器与文档位置相关联,还需要调用方法 rangeValue.range(from, to⁠?) 生成一个 range,将装饰器包装在其中(作为该文档范围 range 的值),也可以其理解成为装饰器附加/绑定了文档位置信息,所生成的 range 是标明了该装饰器作用于文档何处


另外 Decoration 类还提供一个静态方法和一个静态属性,用于创建 DecorationSet 装饰器集合实例

DecorationSet

TypeScript type DecorationSet=RangeSet<Decoration> 描述了一个文档范围集合 RangeSet,它所包含的一系列文档范围 range 的值都需要是装饰器 decoration

编辑器采用这种类型的数据格式,高效地管理大量的装饰器(进行映射和更新)

  • static Decoration.set(of, sort?) 静态方法:基于给定的 decoration range(经过 Range 封装的装饰器)创建一个 DecorationSet 装饰器集合
    第一个参数 of 可以是一个 Range 文档范围对象(其值的类型是装饰器);也可以是一个数组,它的每个元素都是一个 Range 对象实例(其值的类型也是装饰器)
    第二个(可选)参数是一个布尔值(默认为 false),表示是否要对传入的一系列 ranges 文档范围对象进行排序
  • static Decoration.none 静态属性:它是一个空的 DecorationSet 装饰器集合

如何使用装饰器

一系列的装饰器以 DecorationSet 装饰器集合这种类型(它实际是 RangeSet的实例化对象)添加到编辑器中

immutable 不可变

这种数据结构是 immutable 不可变的

当文档更新时,它可以通过 map 映射进行更新(生成一个新的 DecorationSet),或直接 rebuild 重新创建一个 DecorationSet

一般是对原有的的 DecorationSet 装饰器集合进行映射更新,然后再通过调用其方法 decorationSet.update(spec) 向其中添加新的装饰器

有两种模式为编辑器添加装饰器,并对它们进行更新:

说明

这两种模式实际都需要通过 facet EditorView.decorations 为编辑器注册/添加装饰器集合 DecorationSet,只是为了优化开发者体验,不同模式对该操作进行了不同程度的封装

需要特别注意 ⚠️ 在设置 facet 值时有两种方式,它们分别适用于不同的场景,在 decorationSet 中可以包含的装饰器类型不同

两种不同的方式提供装饰器

facet EditorView.decorations 输入值有两种类型:

  • 可以是一个 DecorationSet 类的实例(包含一系列装饰器)
  • 可以是一个函数 fn(view: EditorView) → DecorationSet 其参数 view 是编辑器视图对象,返回值也是一个 DecorationSet 类的实例

这两种方式的区别:

  • 直接(静态)提供的一个 decorationSet,它可以包含任意类型的装饰器,特别是支持 block widget decoration 块级挂件型装饰器(这类装饰器会影响编辑器的垂直方向的布局
  • 通过函数(动态)提供一个 decorationSet,它只能包含不影响编辑器布局的装饰器,例如 mark decoration 标记装饰器,不能支持 block widget decoration 块级挂件装型饰器,以及 不能支持覆盖换行符的 replace decoration 替换型装饰。因为该函数会在编辑器的视口 viewport 计算生成后再调用,所以该函数返回装饰器(一般是定位到 viewport 里)不应该再触发编辑器垂直方向布局的更改,因为这回影响到 viewport 的变化

虽然通过函数(动态)所提供的装饰器类型有限制,但是唯有这种方式才可以访问到 viewport,所以它特别适用于只针对可视区域设置装饰器的场景,例如代码高亮,不需要为整个文档都添加装饰器可以优化编辑器的性能

  • 方式一:基于 state field
    • decorationSet 作为 state field 的值存储起来
    • 在 state field 的配置对象的属性 update 中设置 decorationSet 的更新逻辑
    • 在 state field 的配置对象的的属性 provide 同步将 decorationSet 添加到编辑器
    • 通过 state effect 将更新 decorationSet 所需信息(一般是与创建新的装饰器 decoration 相关,然后将 state effect 附加到事务 transaction 中),然后 state field 更新时可以从事务 tr 中读取并使用这些数据

    以下是简单的示例
ts
// 使用 mark decoration 标记型装饰器为指定范围的内容添加下划线
// 参考 https://github.com/codemirror/website/blob/master/site/examples/decoration/underline.ts

// 创建一个 state field
const underlineField = StateField.define<DecorationSet>({
  // 初始化 state field 的值
  create() {
    // 装饰器 class Decoration 的静态属性 Decoration.none 是一个空的 decorationSet
    // 使用一个空的 decorationSet 作为 state field 的初始值
    return Decoration.none
  },
  // 更新 state field 的值
  // 参数 underlines 是更新前的 state field 的值
  // 参数 tr 是事务对象
  update(underlines, tr) {
    // 对于原有的 decorationSet 进行 map 映射更新
    underlines = underlines.map(tr.changes)
    // 遍历 tr 事务中所包含的一系列 state effect 自定义变更效应
    for (let e of tr.effects) {
      // 如果自定义变更效应中具有 addUnderline 类型,就向原有的 decorationSet 里添加新的 decoration
      if (e.is(addUnderline)) {
        underlines = underlines.update({
          add: [underlineMark.range(e.value.from, e.value.to)]
        })
      }
    }
    return underlines
  },
  // 设置依赖于该 state field 的相关插件
  // EditorView.decorations 是一个 facet,用于为编辑器设置装饰器
  // 这里使用方法 facet.from(field) 可以让该 facet 的值随入参 field 的改变而动态变化
  provide: f => EditorView.decorations.from(f)
});

// 定义一个 state effect type
// 该自定义变更效应的类型用于为 tr 事务添加的数据是 {from, to}(它是新增装饰器所作用到文档范围)
const addUnderline = StateEffect.define<{from: number, to: number}>({
  map: ({from, to}, change) => ({from: change.mapPos(from), to: change.mapPos(to)})
})

// ...

// 一个指令 command(用于与按键绑定)
export function underlineSelection(view: EditorView) {
  // 创建一系列的自定义变化效应
  // 首先遍历当前选区的所有选择范围
  let effects: StateEffect<unknown>[] = view.state.selection.ranges
    .filter(r => !r.empty) // 过滤掉内容为空的选择范围
    // 然后基于选择合适范围的创建一系列 addUnderline 类型的 state effect
    // 它们为 tr 事务所附加的数据就是选区范围
    .map(({from, to}) => addUnderline.of({from, to}))
  if (!effects.length) return false

  // 检测编辑器是否已经注册了相应的 state field
  // 否则就要更改全局配置,以注册 state field 以及相应的 theme(它包含了下划线的相应 CSS 样式)
  if (!view.state.field(underlineField, false))
    effects.push(StateEffect.appendConfig.of([underlineField, underlineTheme]))

  // 把创建的 state effect 附加到 tr 中,并分发事务触发编辑器更新
  // 所以 state field 的值,即 decorationSet 也会更新
  view.dispatch({effects})
  return true
}
缺点

该方式需要将装饰器集合 decorationSet 的相关代码分散到 state field 和 state effect 中,导致逻辑关注点分离,并不利于代码的维护

  • 方式二:基于 viewPlugin
    • decorationSet 作为视图插件的 pluginValue 的内部状态保存起来
    • 在 pluginValue 的 update 方法设置 decorationSet 的更新逻辑
    • 通过 viewPlugin 的配置对象的属性 decorations 声明所需添加到编辑器的装饰器

    以下是简单的示例
    ts
    // 使用 widget decoration 挂件型装饰器在编辑指定位置插入 DOM 元素(一个 checkbox 单选框表单元素)
    // 参考 https://github.com/codemirror/website/blob/master/site/examples/decoration/checkbox.ts
    
    // 创建一个 viewPlugin
    const checkboxPlugin = ViewPlugin.fromClass(class {
      // decorationSet 作为该视图插件的内部状态进行存储
      decorations: DecorationSet
    
      // 初始化 decorationSet
      constructor(view: EditorView) {
        // 使用 helper function 辅助函数 checkboxes 创建一个 decorationSet 作为初始值
        this.decorations = checkboxes(view)
      }
    
      // 更新视图插件
      update(update: ViewUpdate) {
        // 如果编辑器文档或视口更新
        // 则重新扫描文档内容,使用 helper function 辅助函数 checkboxes 创建一个新的 decorationSet
        if (update.docChanged || update.viewportChanged ||
            syntaxTree(update.startState) != syntaxTree(update.state))
          this.decorations = checkboxes(update.view)
      }
    }, {
      // 使用 viewPlugin 的配置对象的属性 decorations 声明所需添加到编辑器的装饰器
      // 其中参数 v 表示该视图插件对象,而需要添加到编辑器的装饰器存储在视图插件对象的属性 decoration 中,所以返回该属性值
      decorations: v => v.decorations,
    
      eventHandlers: {
        mousedown: (e, view) => {
          let target = e.target as HTMLElement
          if (target.nodeName == "INPUT" &&
              target.parentElement!.classList.contains("cm-boolean-toggle"))
            return toggleBoolean(view, view.posAtDOM(target))
        }
      }
    })
    
    // 继承 WidgetType 抽象类,定义一个视图插件子类
    class CheckboxWidget extends WidgetType {
      constructor(readonly checked: boolean) { super() }
    
      // 用于判断两个 widget 是否相同,依据是 checkbox 的 checked 属性是否相同
      // 如果相同则可以复用 DOM 元素
      eq(other: CheckboxWidget) { return other.checked == this.checked }
    
      // 生成 DOM 元素
      toDOM() {
        let wrap = document.createElement("span")
        wrap.setAttribute("aria-hidden", "true")
        wrap.className = "cm-boolean-toggle"
        let box = wrap.appendChild(document.createElement("input"))
        box.type = "checkbox"
        box.checked = this.checked
        return wrap
      }
    
      ignoreEvent() { return false }
    }
    
    // 一个 helper 辅助函数
    // 分析文档内容,找到其中布尔值
    // 基于此创建一系列的 widget decoration(分别插入到这些布尔值的后面)
    function checkboxes(view: EditorView) {
      let widgets = []
      for (let {from, to} of view.visibleRanges) {
        syntaxTree(view.state).iterate({
          from, to,
          enter: (node) => {
            if (node.name == "BooleanLiteral") {
              let isTrue = view.state.doc.sliceString(node.from, node.to) == "true"
              let deco = Decoration.widget({
                widget: new CheckboxWidget(isTrue),
                side: 1
              })
              widgets.push(deco.range(node.to))
            }
          }
        })
      }
      // 基于一系列 decoration 创建一个 decorationSet
      return Decoration.set(widgets)
    }
    
    推荐

    通过一个 viewPlugin 视图插件就可以为编辑器添加装饰器(并在其中包含如何对装饰器进行更新),逻辑关注点更集中

WidgetType 抽象类

WidgetType 是一个 TypeScript abstract class 抽象类,用于表示 widget decoration 挂件型装饰器(或 replace decoration 替换型装饰器)所需插入到编辑器文档中的 DOM 元素,它给出了一个抽象方法,以及一些具体的方法和属性

抽象类

TypeScript abstract class 不直接实例化,由于它一般只定义了抽象的方法或属性(虽然也可以包含非抽象的方法或属性),即针对这些方法只是对入参和输出值的类型进行了约束,而没有给出这些方法的具体实现,针对属性也是只对属性值的类型进行了约束

抽象类可以被其他类继承,子类需要实现抽象类中的抽象方法和属性,所以一般是创建子类再使用(才能进行实例化)

所以这里的 abstract class WidgetType 抽象类不直接实例化,要先被继承,给出父类的抽象方法和属性的具体实现,构建出各种子类再使用

  • abstract toDOM(view) 抽象方法:用于构建 DOM 元素,它接收 view 编辑器视图对象作为入参,最后需要返回一个 HTMLElement 元素
  • eq(anotherWidgetType) 方法:用于比较当前挂件是否与其他 anotherWidgetType 挂件相同,最后返回一个布尔值
    说明

    该方法所比较的两个挂件对象需要由相同 WidgetType 子类实例化而得的


    当该挂件所对应的装饰器更新时,会调用该方法进行比较装饰器更新前后所对应的挂件是否相同,如果该函数返回 true 则可以复用挂件,而避免在页面上相应的 DOM 元素重绘
    在该类 WidgetType 中这个方法是直接返回 false,这会导致每次装饰器更新时所对应的 widget 挂件默认都是不相等,所以需要在页面生成一个新的 DOM 元素。可以在子类中覆写该方法的逻辑,以在合适的情况下尽量复用已有的 DOM 元素
  • updateDOM(dom, view) 方法:用于更新 DOM 元素,最后返回一个布尔值以表示更新是否成功
    第一个参数 dom 是需要更新的 HTMLElement 元素,第二个参数 view 是编辑器对象
    最后返回要给布尔值,以表示给定的 dom DOM 元素是可以通过更新以复用,来表示当前挂件。如果该方法返回 false 则表示已有的 DOM 元素不能不用,则需要在页面创建一个新的 DOM 元素来表示当前挂件对象
    提示

    给定的 DOM 元素在页面已存在的,它由另一个挂件生成,而这个挂件与当前挂件都需要由相同 WidgetType 子类实例化而得

    但是这两个挂件(通过调用方法 eq() 来判断)并不相同,所以原有的 DOM 元素不能直接复用,至少要对 DOM 的属性或内容进行更新,才可以(复用)在页面表示当前的挂件


    在该类 WidgetType 中这个方法是直接返回 false,这会导致对于不同的挂件(即使它们是由相同的 WideType 类实例化而得)都需要生成一个新的 DOM 元素,而不能复用已有的 DOM 元素。可以在子类中覆写该方法的逻辑,以在合适的情况下尽量复用已有的 DOM 元素
  • estimatedHeight 属性:一个数值,表示挂件(DOM 元素)高度的估算值
    该属性会用于估计编辑器内容的高度(由于挂件 DOM 元素可能采用按需渲染而未被绘制出来)
    如果不知道挂件高度的估算值,可以将该属性设置为 -1(也是默认值)
  • lineBreaks 属性:一个数值(默认值为 0),如果挂件是 inline 行内元素,但引入了换行符(即包含 <br> 作为其后代元素),则必须设置该属性以表示所引入的换行符的数量
  • ignoreEvent(event) 方法:返回一个布尔值,以表示编辑器是否需要忽略 event 事件对象(由该挂件内部所分发的)
    在该类 WidgetType 中这个方法是直接返回 true,即编辑器默认忽略所有在挂件内部所触发的事件
  • coordsAt(dom, pos, side) 方法:用于自定义如何获取挂件在页面的尺寸定位信息
    第一个参数 dom 是挂件 HTMLElement 元素;第二个参数 pos 是一个数值,表示挂件在文档中的位置;第三个参数 side 是一个数值,表示该挂件与所在的文档位置的哪一侧相关联(如果 side<0 表示与前侧相关联;如果 side>0 表示与后侧相关联;如果 side=0 正好作用于该位置 ❓ )
    在该类 WidgetType 中这个方法是直接返回 null,所以需要在子类中覆写该方法,给出获取 DOM 元素尺寸和位置的具体逻辑
  • destroy(dom) 方法:当挂件删除(由于对应的装饰器被删除)时, 会调用该方法(一般会在该方法里将页面的 相应 DOM 元素移除)
高效复用

widget decoration(或 replace decoration)的配置对象的属性 spec.widget 中设置所需插入到编辑器的挂件(DOM 元素)

使用 WidgetType 类(实际上是其子类)的实例化对象作为上述属性的值,而不是直接给定 DOM 元素,这样是为了让 DOM 元素与 decoration 相互分离

由于编辑器状态 state(其中包含的 DecorationSet 装饰器集合)是 immutable 不可变的,所以在更新文档时不能直接对其修改,而是需要(基于原有的值)生成新的编辑器状态

将可变动 mutable 的 DOM 元素和不可变动 immutable 的装饰器相互分离的设计,可以更高效地复用 DOM 元素,避免页面反复重绘影响性能;另外可以实现 DOM 元素的延迟/按需生成,即在装饰器进入编辑器视口 viewport 时才按需生成,而不是在实例化装饰器时同步生成

MatchDecoration 类

MatchDecoration 类可以更方便地配置 viewPlugin 视图插件来为编辑器添加装饰器,而这些装饰器所作用的文档范围是基于正则表达式对文档内容的匹配结果

说明

该类并不是一个装饰器类型,而是用于快速配置 viewPlugin(所以在官方说明文档中也被称为 helper class 辅助类),它的实例化对象描述了对文档内容的匹配模式,然后这些信息会用于为编辑器添加装饰器(通过 viewPlugin 来实现)

使用方法 new MathDecorator(config) 进行实例化,参数 config 是一个对象,具有以下属性:

  • regexp 属性:一个正则表达式(应该包含 g 标记),用于对文档的每一行的内容进行匹配(不会进行跨行匹配)
  • decoration(可选)属性:为每个匹配设置一个装饰器(各个装饰器所作用的文档范围就是对应的各个匹配结果的范围
    该属性值有两种类型:
    • 可以是一个 decoration 装饰器,则在文档中每个匹配的范围/位置都会应用上该装饰器
    • 也可以是一个函数 fn(match, view, pos) 每个匹配项都会调用该方法,以添加装饰器
      该函数的各个参数的具体说明如下:
      • 第一个参数 match 是当前所遍历的匹配结果(一个数组,因为可能包含捕获组)
      • 第二个参数 view 是编辑器视图对象
      • 第三个参数 pos 是一个数值,表示当前所遍历的匹配的开始位置

      最后返回一个 decoration 装饰器(它所作用的的文档范围就是当前所遍历的匹配结果的范围/位置)或 null(在当前的匹配结果的范围/位置就不会设置装饰器),这种方式更灵活
  • decorate(可选)属性:一个函数 fn(add, from, to, match, view) 每个匹配项都会调用该方法,以添加装饰器(各个装饰器所作用的文档范围可以自定义
    优先级更高

    该属性 decorate 和前一个属性 decoration 作用类似,都是为每个匹配设置一个装饰器,但该属性 decorate 灵活性更大,优先级更高

    如果方法 new MatchDecorator(config) 进行实例化时,在配置对象 config 中同时设置了这两个属性,则会忽略属性 decoration


    该函数的各个参数的具体说明如下:
    • 第一个参数 add 是一个函数 fn(decoFrom: number, decoTo: number, decoration: Decoration) 调用它可以向编辑器添加给定的装饰器 decoration,第一个参数 decoFrom 和第二个参数 decoTo 是数值,表示当前所添加的装饰器所作用到文档的范围( ⚠️ 该范围应该在当前所遍历的匹配结果的范围里,即外层函数的参数 from 和参数 to 之间)
    • 第二个参数 from 和第三个参数 to 都是数值,表示当前所遍历的匹配结果的范围
    • 第四个参数 match 是当前所遍历的匹配结果(一个数组,因为可能包含捕获组)
    • 第五个参数 view 是编辑器视图对象

    在该函数的内部逻辑中,可以基于当前所遍历的匹配项的范围 fromto 以及其他参数信息计算出装饰器的作用范围,然后通过调用方法 add() 为编辑器添加装饰器
    注意

    在该函数中,只能在调用方法 add() 时产生副作用(为编辑器添加装饰器),在函数内的其他地方都不应该触发编辑器的更改

    提示

    关于该方法的介绍和使用示例可以参考官方论坛的这个帖子

  • boundary(可选)属性:一个用于匹配单个字符的正则表达式,表示匹配的边界,以控制/约束所需重新匹配的范围
    默认情况下,文档某一行的内容变更时,需要对整一行的内容进行重新匹配。如果设置了该属性 boundary,则会在更改的内容附近先使用该正则表达式进行匹配寻找边界,则所需重新匹配的内容就在两侧边界之间(而不是整一行),以提高性能
    一般会使用正则表达式 \s(空格符)作为该属性的值,因为英文单词之间一般空格符进行分隔
  • maxLength(可选)属性:一个数值(默认值是 1000),用于拓展匹配的文档内容的范围( ❓ 行数)
    默认情况下,为了性能优化只会对折叠的行或文本特别长的行的内容进行完全匹配,可以通过该属性对匹配范围进行拓展

class MatchDecoration 的实例化对象具有两个方法,它们用于配置 viewPlugin 视图插件:

  • createDeco(view) 方法:创建一个 decorationSet,它们是对给定的视图对象 view 的视口内容进行匹配后,所需添加到编辑器的装饰器集合
    该方法一般会在视图插件的 pluginValue 的构造函数里进行调用,作为视图插件 decorationSet 装饰器集合的初始值
  • updateDeco(update, deco) 方法:根据视图所发生的变更 update(一个 ViewUpdate 类的实例)对给定的装饰器集合 deco 进行更新,返回一个新的 decorationSet 装饰器集合
    该方法一般会在视图插件的 pluginValue 的方法 update 里进行调用,以更新原有的 decorationSet 装饰器集合

以下是简单的示例

ts
// 使用 replace decoration 替换型装饰器用 widget(DOM 元素)替换匹配的内容
// 参考 https://github.com/codemirror/website/blob/master/site/examples/decoration/placeholder.ts

// helper class
const placeholderMatcher = new MatchDecorator({
  regexp: /\[\[(\w+)\]\]/g, // 匹配的内容所符合的模式是 [[content]] 由两个中括号包裹的内容
  // 为每个匹配结果设置装饰器
  // 替换型装饰器,将所匹配的文本 match[1](第一个捕获组)作为挂件的内容
  decoration: match => Decoration.replace({
    // 用于替换内容的挂件(DOM 元素)
    widget: new PlaceholderWidget(match[1]),
  })
})

// 创建一个视图插件
const placeholders = ViewPlugin.fromClass(class {
  placeholders: DecorationSet // 将装饰器集合作为 pluginValue 的内部属性存储起来
  constructor(view: EditorView) {
    // 调用 placeholderMatcher 的方法 createDeco(),其返回值作为 decorationSet 的初始值
    this.placeholders = placeholderMatcher.createDeco(view)
  }
  update(update: ViewUpdate) {
    // 调用 placeholderMatcher 的方法 updateDeco() 对 decorationSet 进行更新
    this.placeholders = placeholderMatcher.updateDeco(update, this.placeholders)
  }
}, {
  // 使用 viewPlugin 的配置对象的属性 decorations 声明所需添加到编辑器的装饰器
  decorations: instance => instance.placeholders,
  provide: plugin => EditorView.atomicRanges.of(view => {
    return view.plugin(plugin)?.placeholders || Decoration.none
  })
})

侧边栏

侧边栏 gutter 是位于编辑器左侧(对于文本方向是 RTL 的编辑器,则位于右侧)的容器,一般用于展示行序号

侧边栏会添加到可滚动容器 scroller element 里(作为它的首个子元素),具有如下结构

html
<div class="cm-scroller">
  <div class="cm-gutters"> <!-- 侧边栏位于 .cm-scroller 元素里,作为第一个子元素 -->
    <div class="cm-gutter"> <!-- 具体的栏目 -->
      <!-- 该栏目第一个元素,它是不可见的,作为占位符,以保证栏目占据足够的宽度 -->
      <div class="cm-gutterElement" style="height: 0px; visibility: hidden; pointer-events: none;">spacer_1</div>
      <!-- 该栏目里的具体元素,其内容用于描述所在对应行 -->
      <div class="cm-gutterElement" style="height: 19.6px; margin-top: 62.8px;">content_1</div>
    </div>
    <div class="cm-gutter"> <!-- 另一个具体的栏目 -->
      <div class="cm-gutterElement" style="height: 0px; visibility: hidden; pointer-events: none;">spacer_2</div>
       <!-- 该栏目里的具体元素,其内容用于描述所在对应行 -->
      <div class="cm-gutterElement" style="height: 19.6px; margin-top: 4px;">content_1</div>
    </div>
    <!-- ... -->
  </div>
  <!-- ... -->
</div>
  • 它最外层容器是一个 <div class="cm-gutters> 元素,具有 class 类名 cm-gutters,在里面包含一系列具体的栏目(每个栏目就是一列)
    各栏目会根据优先顺序相邻排列,优先顺序是由(各栏目添加到编辑器时)所对应插件的优先级决定
    这些栏目一般都采用相同的背景色,这样多个栏目并排时可以融合为一个整体(侧边栏)
  • 每个栏目的容器是一个 <div class="cm-gutters> 元素,都具有 class 类名 cm-gutter,该容器采用 display: flex 布局,方向是 flex-direction: column 竖向,即栏目所包含的元素是垂直排列的
  • 在栏目里包含一系列 <div class="cm-gutterElement> 元素,它们分别占据一行高度,所包含内容用于描述所对应的行
    第一个元素可能是 Spacer 占位符,它具有 CSS 样式 height: 0px; visibility: hidden 即实际上是不可见的(且不占据高度),只是用于保证栏目占据足够的宽度
    ❓ 一般还会有第二个元素(对应于编辑器的第一行),即使里面没有内容
    GutterMarker

    元素 <div class="cm-gutterElement> 所包含的具体内容称为 gutter marker 边栏标记,通过 class GutterMarker 抽像类来表示,用于为所对应的行附加信息(例如一个数字,表示对应行的序号)

    💡 实际上 gutter marker 也可以只是添加到元素 <div class="cm-gutterElement> 的 CSS class 类名(而不是具体的内容),以为该元素设置样式

    由于 class GutterMarker 是一个抽象类,不能直接实例化,它需要被其他类继承,在子类中实现抽象类中的抽象方法,才能进行实例化

    关于 GutterMarker 类的具体介绍可以查看下文相关部分

自定义栏目

使用方法 gutter(config) 构建一个(竖向列)栏目,该方法返回一个插件(需要符合一种 TypeScript type Extension

插件如何使用

方法 gutter(config) 返回一个插件

在实例化编辑器视图时,将该插件添加到配置对象的属性 config.extensions 上,就可以将自定义栏目添加到编辑器侧边栏里

该方法的参数 config 是一个对象,具有以下属性:

  • class(可选)属性:一个字符串,作为该栏目的容器的额外 CSS 类名(它本来就具有 class 类名 cm-gutter
  • renderEmptyElements(可选)属性:一个布尔值(默认为 false),用于控制是否在每一行渲染出空内容的栏目元素(即 <div class="cm-gutterElement> 元素)
    如果该属性 renderEmptyElements 设置为 true,则该栏目会为每一行生成一个 <div class="cm-gutterElement> 元素,即使里面不包含内容
  • markers(可选)属性:根据给定的 rangeSet 文档范围的集合,在相应的行(rangeSet 所包含的位置)添加 gutter marker 边栏标记
    该属性值可以是一个函数 fn(view: EditorView) → RangeSet<GutterMarker> 参数 view 是编辑器视图对象,返回一个 RangeSet 类的实例(它所包含的一系列 range 的值是 GutterMarker 类的实例);也可以是一个 RangeSet 类的实例(它所包含的一系列 range 的值也是 GutterMarker 类的实例)
    提示

    这里所使用的 rangeSet 文档范围集合,它包含的一系列 range 所表示的文档位置需要正好是某一行的行首位置,才可以成功往栏目里添加 gutter marker,如果 range 定位到文档某一行的中间则无效

  • lineMarker(可选)属性:为每一行设置 gutter marker 边栏标记
    该属性值可以是一个函数 fn(view, line, otherMarkers) 各参数的具体说明如下:
    • 第一个参数 view 是一个编辑器视图对象
    • 第二个参数 line 是一个 BlockInfo 类的实例,描述当前所遍历的文档中的一行的尺寸相关信息
    • 第三个参数 otherMarkers 是一个数组,它的元素是一系列 GutterMarker 类的实例,表示在该栏目中(已经为当前所遍历的文档中的一行)所添加的一系列 gutter marker 边栏标记(例如同时设置了属性 markers ,通过该属性所设置的 rangeSet 文档范围集合中包含了当前所遍历的行的位置,所以会为该行添加 gutter marker)

    该函数最后可以返回一个 GutterMarker 类的实例,表示(为当前所遍历的文档一行)在栏目中添加边栏标记;也可以返回一个 null,则表示不为当前所遍历的文档一行添加边栏标记
注意

如果在使用方法 gutter(config) 创建栏目时,同时在配置对象 config 中设置了属性 markers 和属性 lineMarker,若针对文档中的某一行,通过这两个属性都会往边栏里添加 gutter marker(而且这两个 gutter marker 都会在页面渲染出具体的内容),则在 gutter element 里会同时包含两个 gutter markers(先显示通过属性 lineMarker 所添加的 gutter marker,再显示由属性 markers 所添加的 gutter marker)

这可能会导致一些问题,例如两个 gutter marker 都是文本节点,而栏目所预留的宽度不够,则两个 gutter marker 会重叠起来。所以一般在创建边栏时,只采用属性 markers 或属性 lineMarker 之一为边栏添加 gutter marker

若需要为编辑器添加多种 gutter marker,则对应创建多个栏目

若需要在一个栏目里同时通过属性 markers 和属性 lineMarker,则可以通过属性 lineMarker 所设置的函数中的第三个参数 otherMarkers,来判断该行是否已经添加了其他的 gutter marker(若存在则函数就返回 null,避免再添加 gutter marker),具体示例可以参考方法 lineNumbers(为行添加序号)的源码

  • widgetMarker(可选)属性:为每一个 block widget 块级挂件设置 gutter marker 边栏标记
    block widget

    block widget 是指由 widget decoration 挂件型装饰器添加到编辑器文档中的 DOM 元素,它是占据一整行的块级元素

    虽然 block widget 在页面占据一行空间(有时候高度还可能超过一行文本的高度,具体由该 DOM 元素的尺寸决定),但是在编辑器文档结构中它并不是一行,所以通过属性 lineMarker 为每一行设置的 gutter marker 时,并不会考虑 block widget 所占据的行

    若需要为 block widget 所占据的行(在栏目的对应位置)添加 gutter marker,则需要配置 widgetMarker 属性

  • lineMarkerChange(可选)属性:当视图更新时,是否影响到基于行(通过属性 lineMarker)或挂件(通过属性 widgetMarker)所添加的 gutter marker
    该属性值是一个函数 fn(update: ViewUpdate) → boolean 所传入的参数 update 是一个 ViewUpdate 类的实例(描述视图发生的更新),返回一个布尔值,以表示是否影响到基于行或挂件的 gutter marker。若该函数返回 true 则表示 CodeMirror 需要对这些边栏标记进行更新
  • initialSpacer(可选)属性:设置用作占位符的 gutter marker 边栏标记
    该属性值是一个函数 fn(view: EditorView) → GutterMarker 所传入的参数 view 是编辑器视图对象,返回一个GutterMarker 类的实例,作为占位符
    spacer 占位符

    作为占位符的 gutter marker 是栏目里的第一个元素(即 <div class="cm-gutterElement> 元素)的内容

    该 gutter marker 所在的 gutter element 具有 CSS 样式 height: 0px; visibility: hidden 即实际上是不可见的,且不占据高度,只是用于保证栏目占据足够的宽度

  • updateSpacer⁠(可选)属性:当视图更新时,对占位符进行更新
    该属性值是一个函数 fn(spacer: GutterMarker, update: ViewUpdate) → GutterMarker 第一个参数 spacer 是一个 GutterMarker 类的实例(作为占位符的 gutter marker),第二个参数 update 是一个 ViewUpdate 类的实例(描述视图发生的更新)。最后返回一个更新后的 GutterMarker 类的实例
  • domEventHandlers(可选)属性:为该栏目(即 <div class="cm-gutters> 元素)设置一系列的事件监听器
    该属性值是一个对象,以键值对的形式设置事件监听器。属性名是 DOM 事件名称,属性值是一个函数(当在栏目分发相应事件时,会触发该函数)fn(view: EditorView, line: BlockInfo, event: Event) → boolean 第一个参数 view 是编辑器视图对象,第二个参数 line 是一个 BlockInfo 类的实例(描述当前对应的文档中的一行的尺寸相关信息 ❓ ),第三个参数 event 是事件对象,最后返回一个布尔值表示该事件是否已经处理完成

另外还有两个 facet gutterLineClassgutterWidgetClass 分别用于在不同情况下为 gutter element 添加 CSS class 类名

  • 变量 gutterLineClass 是一个 facet,它的输入值是一个 RangeSet 类的实例(文档范围集合),它所包含的一系列 range 的值是 GutterMarker 类的实例,会在相应的行(rangeSet 所包含的位置)添加 gutter marker 边栏标记
  • 变量 gutterWidgetClass 是一个 facet,它的输入值是一个函数 fn(view, widget, block) → GutterMarker | null 第一个参数 view 是编辑器视图对象,第二个参数 widget 是一个 WidgetType 类的实例(表示当前所遍历的挂件对象,描述在页面插入的 DOM 元素),第三个参数 block 是一个 BlockInfo 类的实例(描述当前所遍历的挂件的尺寸相关信息),返回值可以是一个 GutterMarker 类的实例(表示为块级挂件设置 gutter marker 边栏标记),也可以返回 null(表示不为当前所遍历的块级挂件设置 gutter marker)
注意

以上包含属性 elementClass,并没有设置属性 toDOM,即这类型的 gutter marker 只为 gutter element 添加 CSS class 类名,并不会在边栏元素容器里插入具体的内容)

GutterMarker 抽象类

GutterMarker 是一个 TypeScript abstract class 抽象类,用于表示为栏目的某一行添加的额外信息,称为 gutter marker 边栏标记,即在元素 <div class="cm-gutterElement> 里添加的内容(也可以不是具体的内容,只为元素 <div class="cm-gutterElement> 添加额外的 CSS class 类名,以便为该元素设置样式)

抽象类

TypeScript abstract class 不直接实例化,由于它一般只定义了抽象的方法或属性(虽然也可以包含非抽象的方法或属性),即针对这些方法只是对入参和输出值的类型进行了约束,而没有给出这些方法的具体实现,针对属性也是只对属性值的类型进行了约束

抽象类可以被其他类继承,子类需要实现抽象类中的抽象方法和属性,所以一般是创建子类再使用(才能进行实例化)

所以这里的 abstract class GutterMarker 抽象类不能直接实例化,它需要被其他类继承,在子类中实现抽象类中的抽象方法,才能进行实例化

该抽象类给出了一些抽象方法和属性,以及一些具体的方法:

  • eq(otherGutterMarker) 方法:用于比较当前的 gutter marker 与给定的 otherGutterMarker 其他边栏标记是否相同,最后返回一个布尔值
    在该类 GutterMarker 中这个方法是直接返回 false,这会导致编辑器更新前后,文档某一行所对应的 gutter marker 默认都是不相等,所以需要生成一个新的 gutter marker(如果该边栏标记在页面会渲染出一个 DOM 元素,也需要重新生成)。可以在子类中覆写该方法的逻辑,以在合适的情况下尽量复用已有的 DOM 元素
  • abstract toDOM(可选)抽象属性:设置 gutter marker 的具体内容(插入到 gutter element 容器里)
    该属性是一个方法 fn(view: EditorView) → Node 参数 view 是编辑器视图对象,返回的值是一个 DOM 节点(可以是 DOM 元素,也可以是文本节点)
  • abstract elementClass 抽象属性:一个字符串,设置添加到 gutter element 容器(即<div class="cm-gutterElement> 元素)的 CSS class 类名
  • destroy(dom) 方法:用于将该 gutter marker 从页面移除,该方法的参数是 dom 前面的属性 toDOM 的返回值(即 gutter marker 的具体内容)
    如果配置了前面的属性 toDOM 应该同步设置该属性,以便在 gutter element 从页面移除时,它所包含的 gutter marker 也删除掉

滚动行为

默认情况下,侧边栏是固定粘在 sticky 编辑器一侧的,即使编辑文本内容过长使得可滚动容器 scroller element能够左右滚动时,侧边栏也不会随之滚动

可以通过方法 gutters(config?) 来更改这种默认行为,该方法返回一个插件(符合一种 TypeScript type Extension

(可选)参数 config 是一个对象,具有属性 fixed 其值是一个布尔值,如果设置为 false 就会让侧边栏随着编辑器水平滚动

插件如何使用

方法 gutters(config?) 返回一个插件

在实例化编辑器视图时,将该插件添加到配置对象的属性 config.extensions 上,就可以将自定义栏目添加到编辑器侧边栏里

区别

另一个名称相似的方法 gutter() 是用来创建自定栏目

添加行序号

侧边栏最常见的应用场景是为编辑器的每一行添加序号,该模块提供了一个方法 lineNumbers(config?) 来实现该需求,该方法返回一个插件(符合一种 TypeScript type Extension

(可选)参数 config 是一个对象,具有以下属性:

  • 属性 formatNumber⁠:一个函数 fn(lineNo, state) → string 用于对每一行的序号进行格式化,第一个参数 lineNo 是一个数字(表示当前所遍历的文档行的序号),第二个参数 state 是一个编辑器状态对象,最后返回一个字符串(格式化的结果,添加到对应行的 gutter marker 边栏标记的具体内容)
  • 属性 domEventHandlers:为该栏目(即 <div class="cm-gutters> 元素)设置一系列的事件监听器
    该属性值是一个对象,以键值对的形式设置事件监听器。属性名是 DOM 事件名称,属性值是一个函数(当在栏目分发相应事件时,会触发该函数)fn(view: EditorView, line: BlockInfo, event: Event) → boolean 第一个参数 view 是编辑器视图对象,第二个参数 line 是一个 BlockInfo 类的实例(描述当前所遍历的文档中的一行的尺寸相关信息),第三个参数 event 是事件对象,最后返回一个布尔值表示该事件是否已经处理完成
插件如何使用

方法 lineNumbers(config?) 返回一个插件

在实例化编辑器视图时,将该插件添加到配置对象的属性 config.extensions 上,就可以将自定义栏目添加到编辑器侧边栏里

提示

可以查看该方法 lineNumbers(config?)(为编辑器添加行序号栏目)的源码,学习使用(更通用的)方法 gutter(config) 自定义栏目


另外还有两个 facet lineNumberMarkerslineNumberWidgetMarker 用于配置在特定场景下在行序列栏目里添加的 gutter marker 边栏标记

  • 变量 lineNumberMarkers 是一个 facet,它的输入值是一个 RangeSet 类的实例(文档范围集合),它所包含的一系列 range 的值是 GutterMarker 类的实例,会在相应的行(rangeSet 所包含的位置)使用该 gutter marker 边栏标记替换行序号(即原有的 gutter marker,一般是一个数字,表示该行的序号)
  • 变量 lineNumberWidgetMarker 是一个 facet,它的输入值是一个函数 fn(view, widget, block) → GutterMarker | null 第一个参数 view 是编辑器视图对象,第二个参数 widget 是一个 WidgetType 类的实例(表示当前所遍历的挂件对象,描述在页面插入的 DOM 元素),第三个参数 block 是一个 BlockInfo 类的实例(描述当前所遍历的挂件的尺寸相关信息),返回值可以是一个 GutterMarker 类的实例(表示在行序号栏目里,为该块级挂件设置 gutter marker 边栏标记),也可以返回 null(表示不为当前所遍历的块级挂件设置 gutter marker)

提示框

提示框 tooltip 是插入到编辑器里(浮动在文档内容之上)的 DOM 元素,它在页面的定位是基于给定的文档位置

提示

Tooltip 一般不是通过副作用来实现添加和移除的,即不会导致编辑器视图更新(一般采用 ViewUpdate 来描述)

使用 facet showTooltip 来为编辑器设置 tooltip,它的输入值可以是一个对象(需要符合一种 TypeScript interface Tooltip,以描述一个提示框),也可以是 null(表示移除页面已存在的所有提示框)

Tooltip

TypeScript interface Tooltip 用于描述一个 tooltip 提示框:

  • pos 属性:一个数值,表示该提示框的定位所基于的文档位置
  • end(可选)属性:一个数值,表示该提示框所关联范围的结束位置(tooltip 的作用/意义是对该范围的内容进行注释/标注),默认情况下该属性值和属性 pos 的值一样,即 tooltip 对 pos 位置进行注释/标注
  • create(view) 方法:一个函数,用于创建 DOM 元素以表示该 tooltip 提示框
    该函数的参数 view 是一个编辑器视图对象,返回值是一个对象(需要符合一种 TypeScript interface TooltipView,以描述该提示框在页面渲染为怎样的 DOM 元素)
    TooltipView

    TypeScript interface TooltipView 用于描述 tooltip 提示框如何在页面为 DOM 元素:

    • dom 属性:一个 HTMLElement 元素,它会渲染到页面上以表示该提示框
    • offset(可选)属性:一个对象 {x: number, y: number} 具有属性 xy,它们的属性值都是数值,分别表示提示框的定位(相对于其属性 pos 所表示的文档位置)在水平和垂直位置偏移量(像素值)
      • 其中属性 x 表示 tooltip 在水平方向的偏移量。如果该属性值是正值,则表示提示框沿着文本方向偏移(即对于 LTR 方向的文本朝右移动,对于 RTL 方向的文本朝左移动);如果该属性值是负值,则表示提示框沿着文本方向的反方向偏移
      • 其中属性 y 表示 tooltip 在垂直方向的偏移量。如果该属性值是正值,则表示沿着提示框所在位置方向移动(例如当提示框位于所注释/标注的文档位置的上方,则朝上方偏移);如果该属性值是正值,则表示逆着提示框所在位置方向移动
    • getCoords(可选)属性:一个函数 fn(pos) 用于获取该提示框的尺寸相关信息,返回值是一个对象(需要符合一个 TypeScript interface Rect,用于描述 DOM 元素(最小矩形框)的位置和尺寸相关信息)
      默认返回的值是基于 tooltip 所关联的文档位置 pos(通过调用方法 editorView.coordsAtPos(pos))计算而得的。可以通过配置该属性来覆写默认逻辑,自定义如何计算 tooltip 尺寸的方式
    • overlap(可选)属性:一个布尔值(默认值为 false),用于设置 tooltip 是否可以相互重叠
      若该属性设置为 false 则表示 tooltip 之间不允许重叠(如果 tooltip 出现重叠,CodeMirror 就会自动将它们移动,重新布局排版以避免相互重叠);若该属性设置为 true 则表示 tooltip 之间可以相互重叠,其定位只会基于给定的文档位置
    • mount(可选)属性:一个函数 fn(view) 参数 view 是编辑器对象,当提示框所对应的 DOM 元素首次挂载到页面上时会调用该函数
    • update(可选)属性:一个函数 fn(update) 参数 update 是一个 ViewUpdate 类的实例(描述了视图所发生的变更),当编辑器的视图更新时会调用该函数(以同步更新提示框所对应的 DOM 元素)
    • destroy(可选)属性:一个函数,当 tooltip 被移除时或整个编辑器在页面移除时会调用该函数(一般用于同步移除该提示框为页面所添加的 DOM 元素)
    • positioned(可选)属性:一个函数 fn(space) 参数 space 是一个对象(需要符合一个 TypeScript interface Rect,表示该 tooltip 在重新布局时,在页面可以放置的地方),当 tooltip 被移动(避免提示框之间相互重叠)重新布局排版后,调用该函数
    • resize(可选)属性:一个布尔值(默认值为 true),用于设置是否要对 tooltip 进行尺寸限制(以防止它超出 available space 可用的布局空间)
  • above(可选)属性:一个布尔值,用于设置该提示框的(相对于给定的文档位置)放置方向
    默认值是 false 即 tooltip 放置在下方
    注意

    对于 hover tooltip 由鼠标悬浮而触发的提示框并不能确保一定遵守该属性的设置

    因为对于关联到同一范围的 hover tooltips 会使用一个容器包裹,所以它们总是定位到同一侧

  • strictSide(可选)属性:一个布尔值,用于设置 tooltip 是否需要严格遵循前一个属性 above 的配置
    默认值是 false 即不需要严格遵循上/下方定位,可以根据页面视口的具体情况(是否有足够的空间容纳该 tooltip),对该提示框的定位进行调整
  • arrow(可选)属性:一个布尔值(默认值为 false),用于设置 tooltip 是否需要添加一个三角形指向标注/注释的位置
    为三角形设置样式

    如果上述属性 arrow 设置为 true 就会在提示框所对应的 DOM 元素里插入以下 HTML 元素(作为最后一个子元素)

    html
    <div class="cm-tooltip-arrow"></div>
    

    该 HTML 元素采用 position: absolute 相对于 tooltip 容器进行定位,而且采用 z-index: -1 置于 tooltip 内容的下层(避免遮挡内容)

    并通过设置伪元素 .cm-tooltip-arrow:before.cm-tooltip-arrow:after 的边框 border-top(当 tooltip 位于下方时)或 border-bottom(当 tooltip 位于上方时)来创建三角形。这两个三角形是相互重叠的(.cm-tooltip-arrow:after 创建的三角形位于上方),而且定位相差 1px,所以可以将 .cm-tooltip-arrow:after 视为三角形的内容,.cm-tooltip-arrow:before 视为三角形的边框

    然后通过 CSS class cm-tooltip-arrow 类名为这个三角形设置样式

    • 当 tooltip 放置方向(相对于给定的文档位置)是在下方时,可以通过伪元素 .cm-tooltip-arrow:after 的属性 border-bottom 来设置三角形内部颜色;可以通过伪元素 .cm-tooltip-arrow:before 的属性 border-bottom 来设置三角形边框颜色
    • 当 tooltip 放置方向(相对于给定的文档位置)是在上方时,可以通过伪元素 .cm-tooltip-arrow:after 的属性 border-top 来设置三角形内部颜色;可以通过伪元素 .cm-tooltip-arrow:before 的属性 border-top 来设置三角形边框颜色
简单示例

一般该 facet showTooltip 会与 state field 配合使用(其中自定义的编辑器状态字段 state field 是用来存储添加到编辑器的 tooltips),具体使用方式可以参考官方示例 Cursor Position,在 Github 仓库里有完整源码


该模块还提供了一个方法 tooltips(config) 来设置所有提示框的行为,该方法返回一个插件(需要符合一种 TypeScript type Extension

(可选)参数 config 是一个对象,具有以下属性,用于配置提示框:

  • positions(可选)属性:可以是 "fixed"(默认值)或 "absolute" 字符串,设置提示框的 DOM 元素在页面的定位方式
    默认情况下采用 "fixed" 即可,可以避免可滚动元素对 tooltip 的影响。但是如果在祖先元素设置了 contain: layout 样式,则可以破坏 "fixed" 定位(不再相对于页面视口),则可能需要采用 "absolute" 定位
    如果 tooltip 的父元素是经过 transform 转换移动处理的元素,CodeMirror 也会回退 fallback 为采用 "absolute" 定位
  • parent(可选)属性:一个 HTMLElement 元素,用于设置 tooltip 的容器
    默认情况下,采用编辑器元素(具有 CSS class cm-editor 类名的元素)作为提示框的父元素
  • tooltipSpace(可选)属性:一个函数 fn(view) 参数 view 是编辑器视图对象,返回一个对象(需要符合一个 TypeScript interface Rect),表示将 tooltip 放置到页面上时,可考虑的空间范围
    默认情况下,会考虑整个视口(从 (0, 0)(innerWidth, innerHeight) 的空间范围)。可以通过配置该属性来覆盖原有的空间范围
如何使用插件

在实例化编辑器视图时,将插件添加到配置对象的属性 config.extensions 上,就可以将视图插件添加到编辑器里


该模块还提供了一个方法 getTooltip(view, tooltip) 第一个参数 view 是编辑器视图对象,第二个参数 toolTip 是一个对象(需要符合一种 TypeScript interface Tooltip),以描述一个提示框。该方法用于获取给定的提示框在页面所添加的 DOM 元素,该函数最后可能返回一个对象(需要符合一种 TypeScript interface TooltipView,以描述该提示框在页面渲染为怎样的 DOM 元素);也可以返回一个 null(表示给定的该提示框没有在页面添加 DOM 元素)


该模块还提供了一个方法 repositionTooltips(view) 参数 view 是编辑器视图对象,用于对页面的提示框进行重新布局排版。当编辑器布局改变时,可以调用该方法对页面中已存在的提示框进行位置更新

悬浮触发提示框

有一种提示框很常见,即鼠标悬浮在编辑器时触发的提示框,该模块提供了一个 helper function 辅助函数 hoverTooltip(source, options?) 以更方便地创建这一类型的提示框

说明

该方法所创建的提示框,会在鼠标悬停到特定范围时显示在页面上,当鼠标移动离开特定范围时从页面移除,主要通过监听 mousemovemouseleave 事件来实现的

注意

对于关联到同一文档范围的 hover tooltips 会使用一个容器包裹,并且它们之间不会重叠

该函数的各参数具体说明如下:

  • 第一个参数 source 是一个函数(需要符合一个 TypeScript type HoverTooltipSource),当鼠标悬浮在编辑器文档上时会调用该回调函数,用于构建 hover tooltip 悬浮触发型的提示框
    HoverTooltipSource

    TypeScript type HoverTooltipSource 描述一个函数,用于创建 hover tooltip 鼠标悬浮触发型的提示框

    ts
    type HoverTooltipSource = fn(view: EditorView, pos: number, side: -1 | 1) → Tooltip | readonly Tooltip[] | Promise<Tooltip | readonly Tooltip[] | null> | null
    

    函数的第一个参数 view 是编辑器视图对象,第二个参数 pos 是一个数值表示当前鼠标所悬浮的文档位置,第三个参数 side-11 表示提示框要指向该文档位置的前侧还是后侧(即 tooltip 与文档位置的哪一侧相关联)

    该函数的返回值有多种形式:

    • 可以是一个对象(需要符合一种 TypeScript interface Tooltip),表示在页面添加一个 tooltip 对该位置进行注释/标注
    • 可以是一个数组,其元素是一系列对象(也需要符合一种 TypeScript interface Tooltip),表示在页面添加多个 tooltip 对该位置进行注释/标注
    • 可以是 null 表示不为该文档位置添加 tooltip
    • 可以是一个 Promise 对象(以便在函数内部执行异步操作,例如先从远端服务器获取数据,再基于这些数据来生成相应的提示框)。它 resolve 返回的结果可以是一个符合 TypeScript interface Tooltip 的对象;或一个数组,其元素是一系列符合 TypeScript interface Tooltip 的对象;或 null
  • 第二个(可选)参数 options 是一个配置对象,具有以下属性
    • hideOn(可选)属性:一个函数 fn(tr: Transaction, tooltip: Tooltip) → boolean 可以基于当前编辑器所分发的 tr 事务对象,(通过返回的布尔值)判断是否要隐藏 tooltip 该悬浮触发型的提示框
    • hideOnChange(可选)属性:一个布尔值(默认值为 false)或字符串 "touch",用于控制当文档更新时或选区重设时,是否要隐藏所有的悬浮触发型的提示框
    • hoverTime(可选)属性:一个数值(默认值是 300),用于设置当鼠标悬浮在编辑器文档内容上的时间超过多少毫秒时,会调用预设的回调函数以生成并显示悬浮触发型的提示框

该函数最后返回一个对象 {extension: Extension, active: StateField<readonly Tooltip[]>} 它符合符合一种 TypeScript type Extension,所以它可以视为一个插件,此外还具有属性 active 是一个 state field 自定义的编辑器状态字段(其值是一个数组,每个元素都是一个 tooltip),用于记录/保存着通过该插件添加到编辑器中的提示框

如何使用插件

在实例化编辑器视图时,将插件添加到编辑器视图的配置对象的属性 config.extensions 上,就可以将视图插件添加到编辑器里

简单示例

具体使用方式可以参考官方示例 Hover Tooltips,在 Github 仓库里有完整源码


该模块还提供了一个方法 hasHoverTooltips(state) 返回一个布尔值,表示当前编辑器里是否有悬浮触发型的提示框


该模块还提供了一个变量 closeHoverTooltips,它是一个 StateEffect 类的实例(自定义变更对象),将其添加到 tr 事务并进行分发,可以关闭编辑器里所有悬浮触发型的提示框

面板

面板 panel 是显示在编辑器顶部或底部的 DOM 元素(例如 search dialog 搜索对话框),它会在编辑器里占据一定的垂直空间/高度,(当编辑器内容过长而可以滚动时,该元素采用 position: sticky 布局)会尽量保留在页面视图里

使用 facet showPanel 来为编辑器设置 panel,它的输入值可以是一个函数(需要符合一种 TypeScript type PanelConstructor,以描述一个面板的构造函数),也可以是 null(表示移除页面已存在的面板)

PanelConstructor

TypeScript type PanelConstructor 用于描述一个面板的构造函数

该 TypeScript 类型的完整描述是 type PanelConstructor = fn(view: EditorView) → Panel 面板构造函数接收的参数 view 是一个编辑器视图对象,返回一个对象(需要符合一种 TypeScript interface Panel,以描述该面板在页面渲染为怎样的 DOM 元素)

Panel

TypeScript interface Panel 用于描述 panel 面板如何在页面为 DOM 元素:

  • dom 属性:一个 HTMLElement 元素,CodeMirror 会自动在该元素上添加 CSS class cm-panel 类名,它会渲染到页面上以表示该面板
  • mount(可选)属性:一个函数,当该面板所对应的 DOM 元素首次挂载到页面上时会调用该函数
  • update(可选)属性:一个函数 fn(update) 参数 update 是一个 ViewUpdate 类的实例(描述了视图所发生的变更),当编辑器的视图更新时会调用该函数(以同步更新面板所对应的 DOM 元素)
  • destroy(可选)属性:一个函数,当 penal 被移除时或整个编辑器在页面移除时会调用该函数(一般用于同步移除该面板为页面所添加的 DOM 元素)
  • top(可选)属性:一个布尔值,表示该面板是否位于到编辑器的顶部,默认值为 false(即面板位于编辑器的底部)
简单示例

具体使用方式可以参考官方示例 Editor Panels,在 Github 仓库里有完整源码


该模块还提供了一个方法 panels(config) 来设置所有面板的行为,该方法返回一个插件(需要符合一种 TypeScript type Extension

(可选)参数 config 是一个对象,具有以下属性,用于配置面板:

  • topContainer(可选)属性:一个 HTMLElement 元素,用于为顶部的面板设置容器
  • bottomContainer(可选)属性:一个 HTMLElement 元素,用于为底部的面板设置容器
默认容器

默认情况下,位于顶部的面板会 prepend 到编辑器里(相应地,位于底部的面板会 append 到容器里),即编辑器的最外层容器 outer wrapper(具有 CSS class cm-editor 类名的 DOM 元素)作为面板的父元素


该模块还提供了一个方法 getPanel(view, panel) 第一个参数 view 是编辑器视图对象,第二个参数 panel 是一个对象(需要符合一种 TypeScript type PanelConstructor),表示一个面板构造函数。该方法基于给定的面板构造函数,获取在页面所添加的/激活的 panel,该函数最后可能返回一个对象(需要符合一种 TypeScript interface Panel,然后就可以进一步访问 access 到该面板在页面渲染的 DOM 元素);也可以返回一个 null(表示给定的面板构造函数并没有在页面添加 panel)

浮动层

浮动层 layer 包含一系列 DOM 元素(称为 layerMarker),它们会 drawn over or below 渲染到编辑器文档内容(文本)之上或之下(例如用于创建多个选区范围),这些 DOM 元素(采用 position: absolute 定位)并不会影响编辑器原有的布局

注意

由于 layer 在页面所添加的 DOM 元素是「脱离」于编辑器的瀑布流布局之外的,所以这些内容并不会被屏幕阅读器识别,可以通过 static EditorView.announce 自定义变更效应的类型,给 screen reader 提供相应的描述,以优化编辑器的无障碍体验

使用方法 layer(config) 定义一个浮动层,该方法返回一个插件(需要符合一种 TypeScript type Extension

参数 config 是一个对象,用于配置该浮动层,具有如下属性:

  • above 属性:一个布尔值,表示该浮动层渲染在文档内容(文字)之上还是之下
  • class(可选)属性:一个字符串,为该 layer(容器元素)添加 CSS class 类名(默认已添加 cm-layer 类名)
  • update(viewUpdate, layer) 方法:当编辑器视图更新时会调用方法,依次接收的参数 viewUpdate 是一个 ViewUpdate 类的实例(描述了视图所发生的变更);参数 layer 是一个 HTMLElement 元素(该层的容器元素)
    该方法最终返回一个布尔值,以表示是否需要对该 layer 所包含的一系列 layerMarker 进行更新(如果该方法返回 true 则该层所包含的 layerMarker 会分别调用它们的 layerMarker.draw() 方法进行重新渲染)
  • updateOnDocViewUpdate(可选)属性:一个布尔值(默认值为 true),表示当文档视图改变时,是否需要更新该 layer
  • markers(view) 方法:为该 layer 创建一系列 layerMarker,传入的参数 view 是编辑器视图对象,返回一个数值,其元素是一系列对象(需要符合一个 TypeScript interface LayerMarker
    LayerMarker

    TypeScript interface LayerMarker 用于描述一个添加到 layer 浮动层的 DOM 元素:

    • eq(otherLayerMarker) 方法:返回一个布尔值,用于对比当前 layerMarker 与给定的其他(同类型的)otherLayerMarker 对象是否相同,以复用已存在的 DOM 元素(避免不必要的重绘)
    • draw() 方法:返回一个 HTMLElement 元素,它会渲染到相应的浮动层(容器元素)里以表示该 layerMarker
    • update(可选)属性:一个函数 fn(dom: HTMLElement, oldMarker: LayerMarker) → boolean 用于对同类型的(已存在的)olderMarker 进行更新(以复用 dom 元素),让它可以表示当前 layerMarker ❓

    这些在浮动层里的 DOM 元素是在编辑器视图的测量周期 measure phase 进行创建的,需要显式设置好它们的定位坐标(自动默认采用 position: absolute 布局),这样在浮动层里的 DOM 元素就不需要进一步去读取/解析编辑器的 DOM 布局的情况下也能够进行绘制

    ⚠️ 这些 layerMarker 的父元素(layer 容器元素)的位置和编辑器文档(左上角)一致,所以 layerMarker 的定位也是相对于编辑器文档

    💡 CodeMirror 提供了一个 class RectangleMarker 类,其实例化对象 implement 实现/符合该 TypeScript interface LayerMarker,如果需要创建一个占据矩形空间的 layerMarker,可以使用该类

  • mount(可选)属性:一个函数 fn(layer: HTMLElement, view: EditorView) 当该浮动层所对应的 DOM 元素(容器元素)挂载到页面时会调用该函数
  • destroy(可选)属性:一个函数 fn(layer: HTMLElement, view: EditorView) 当该浮动层被移除时或整个编辑器在页面移除时会调用该函数(一般用于同步移除该 layer 为页面所添加的 DOM 元素)
如何使用插件

在实例化编辑器视图时,将插件添加到配置对象的属性 config.extensions 上,就可以将视图插件添加到编辑器里

RectangleMarker 类

RectangleMarker 类是一种 layerMarker(其实例化对象 implement 实现/符合 TypeScript interface LayerMarker

使用方法 new RectangleMarker(className, left, top, width, height) 进行实例化,根据给定的坐标和尺寸(描述了 DOM 元素的最小矩形框)创建一个 layerMarker,各参数的具体说明如下:

  • 第一个参数 className 是一个字符串,作为该 layerMarker 所对应的 DOM 元素添加 CSS class 类名
  • 第二个参数 left 和第三个参数 top 都是数值,表示该 layerMarker 所对应的 DOM 元素的左上角的坐标
  • 第四个参数 width 可以是一个数值,表示该 layerMarker 所对应的 DOM 元素的宽度;也可以是 null 则该 DOM 元素不占据横向空间(例如用于表示虚拟光标)
  • 第五个参数 height 是一个数值,表示该 layerMarker 所对应的 DOM 元素的高度

RectangleMarker 类的实例化对象包含一些属性和方法:

  • left 属性:一个数值,表示该 rectangle layerMarker 所对应的 DOM 元素的左侧的相对于编辑器文档左侧的距离(像素)
  • top 属性:一个数值,表示该 rectangle layerMarker 所对应的 DOM 元素的顶部的相对于编辑器文档顶部的距离(像素)
  • width 属性:可能是一个数值,表示该 rectangle layerMarker 所对应的 DOM 元素的宽度;也可以能是 null 则该 DOM 元素不占据水平空间
  • height 属性:一个数值,表示该 rectangle layerMarker 所对应的 DOM 元素的高度

该类还提供了一个静态方法 static RectangleMarker.forRange(view, className, range) 可以基于 range 选区范围创建一系列 rectangle layerMarkers,各参数的具体说明如下:

  • 第一个参数 view 是编辑器视图对象
  • 第二个参数 className 是一个字符串,为这些 rectangle layerMarkers 所对应的 DOM 元素都添加 CSS class 类名
  • 第三个参数 range 是一个 SelectionRange 类的实例,表示选区范围
    range 选区范围为空的时候,只会创建一个 rectangle layerMarker(表示光标);当 range 选区范围不为空,则会创建一个具有方向(与选区文本方向一致)的 rectangle layerMarker 以呈现选区的高亮效果
    提示

    具体使用场景可以参考 @codemirror/view 模块中关于绘制多个选区的源码

该静态方法最后返回一个数组,其元素是一系列 RectangleMarker 类的实例

方形选区

方形选区 rectangular selection 是指(默认情况下)按住 Alt 键的同时使用鼠标左键框选多行文本所创建的选区

通过这种方式可以选中相邻多行的部分内容,而这些行所选中的部分内容的垂直范围是一致,这个选区会构成一个矩形,所以称为 rectangular selection 方形选区

方形选区
方形选区

使用方法 rectangularSelection(options) 创建一个插件(需要符合一种 TypeScript type Extension,然后将其添加到编辑器中,就可以让编辑器支持创建方形选区了)

(可选)参数 options 是一个对象,用于配置方形选区的行为,具有以下属性:

  • eventFilter(可选)属性:一个函数 eventFilter⁠?: fn(event: MouseEvent) → boolean 用于表示当前接收到的鼠标事件 event 是否可以用于创建方形选区(如果返回 false 则表示要对该操作进行过滤)
如何使用插件

在实例化编辑器视图时,将插件添加到配置对象的属性 config.extensions 上,就可以将视图插件添加到编辑器里


该模块还提供另一个方法 crosshairCursor(options) 用于设置在按下特定 modifier key 修饰按键(默认为 Alt 键)时鼠标样式从指针变成十字线,一般是与方形选区配套设置(提供视觉反馈)

(可选)参数 options 是一个对象,用于配置按下哪一种 modifier key 修饰按键时触发鼠标样式转变,具有以下属性:

  • key(可选)属性:可以是 "Alt""Control""Shift""Meta" 这 4 个字符串之一,表示按下相应的 modifier key 修饰按键时鼠标样式进行转变

其他

@codemirror/view 模块还导出一些辅助函数,用于对编辑器视图进行设置

开发调试

  • logException(state, exception, context⁠?) 方法:用于上报/打印/记录客户端中未处理的异常
    第一个参数 state 是当前编辑器状态,第二个参数 exception 是异常信息,第三个(可选)参数 context 是一个字符串,表示与异常相关的上下文
    该方法一般在 extension 里使用,特别是在那些允许客户端提供函数,并无法将异常合理传播到调用上下文的场景,例如在事件处理器里
    logException 方法会按照以下顺序尝试处理异常:
    • 如果通过 facet EditorView.exceptionSink 注册了异常处理器,则会调用该自定义的异常处理器
    • 如果在浏览器环境中定义了 window.onerror 则会调用该全局错误处理器
    • 如果上述处理器都未定义,会使用 console.error 来输出异常信息。如果提供了(可选)参数 context,则会将作为 console.error 的第一个参数,以便更好地描述异常发生的上下文

光标选区

  • drawSelection(config⁠?) 方法:创建一个插件 extension,用于隐藏浏览器原有的选区和光标,然后通过以下方式替换:
    • 为编辑器选区添加背景颜色(替换浏览器原有的选区),该区域会带有 CSS class cm-selectionBackground 类名
    • 在文档内容上叠放一个 DOM 元素模拟光标(替换浏览器原有的光标),该元素会带有 cm-cursor-primarycm-cursor-secondary 类名

    由于浏览器原始只支持创建一个选区范围,将以上方法所创建的 extension 应用到编辑器上,就可以编辑器支持多选区范围以及多个光标
    (可选)参数 config 是一个对象,具有以下属性,用于对虚拟选区和光标进行配置:
    • cursorBlinkRate(可选)属性:一个数字(默认值是 1200),设置光标的闪烁周期(单位是毫秒),可以设置为 0 以取消光标闪烁
    • drawRangeCursor(可选)属性:一个布尔值(默认值是 true),用于设置在非空选区的动点 head 是否显式光标
  • getDrawSelectionConfig(state) 方法:返回一个对象,表示给定的编辑器状态 state 的选区配置情况(相当于获取方法 drawSelection 的参数值,即使没有使用该方法返回的插件对编辑器的选区进行设置)
  • dropCursor() 方法:创建一个插件 extension,用于在编辑器里发生 drop 拖放的位置绘制一个光标

高亮

  • highlightActiveLine() 方法:创建一个插件 extension,为(具有光标的)激活的行添加 CSS class cm-activeLine 类名
  • highlightSpecialChars(config?) 方法:创建一个插件 extension,(添加占位符元素)突出显示特殊的字符
    (可选)参数 config 是一个对象,具有以下属性:
    • render(code, description, placeholder)(可选)方法:返回一个 HTMLElement 元素,作为 placeholder 占位符
      该方法的各个参数的具体说明如下:
      • 第一个 code 是一个数值,表示特殊字符的 Unicode 编码(占位符元素为该特殊字符所设的)
      • 第二个参数 description 可以是一个字符串或 null,作为描述该字符的额外信息,一般设置为元素的 aria-label 属性提供给屏幕阅读器,或设置为元素属性 title 可以在鼠标悬停时显示出来
      • 第三个参数 placeholder 是一个字符串,在视觉上建议性如何显示该特殊字符
    • specialChars(可选)属性:一个正则表达式(需要具有 g 全局标识,以对整个文档中符合的字符进行高亮),用于自定义所需要匹配哪些特殊字(CodeMirror 已经预设了一些常见的特殊字符,但可以通过该属性来覆盖)
    • addSpecialChars(可选)属性:一个正则表达式,用于在 CodeMirror 所默认的特殊字符集合中,添加额外的字符进行突出显示
  • highlightWhitespace() 方法:创建一个插件 extension,突出显示空格和缩进符
    该插件应用到编辑器后,会为空格(包含多个空格相连的情况)添加 CSS class cm-highlightSpace 类名;会为每个 tab 缩进符添加 CSS class cm-highlightTab 类名
    当应用了该插件后,CodeMirror 默认应用一些样式以进行突出显示,空格会带有浅色小点,tab 缩进符会带有箭头(可以通过这些 CSS 类名设置其他样式,覆盖这些预设的样式)
  • highlightTrailingWhitespace() 方法:创建一个插件 extension,为行尾的空格添加 CSS class cm-trailingSpace

占位符

  • placeholder(content) 方法:创建一个插件 extension,用于设置当编辑器内容为空时,所需显示的占位符内容
    参数 content 可以设置为多种类型的值:
    • 可以是字符串,以显示简单文本提示内容
    • 可以是 HTMLElement 元素,以显示更复杂的提示内容
    • 可以是一个函数 fn(view: EditorView) → HTMLElement 基于编辑器视图动态生成 HTMLElement 元素作为提示内容

交互

  • scrollPastEnd() 方法:创建一个插件 extension,用于为编辑器在底部添加 margin 留白(其高度与编辑器的高度减去一行文本的高度),以确保编辑器最后一行的内容可以滚动到编辑器的顶部(避免被悬浮在编辑器上面的元素遮挡,例如搜索栏)
    ⚠️ 该插件只有在(内容较多)可滚动的编辑器里使用,才可以优化交互体验(对于不可滚动的编辑器,启用该插件并不合适,只会在其底部徒增了留白)

Copyright © 2025 Ben

Theme BlogiNote

Icons from Icônes