CodeMirror 综合专题
CodeMirror 综合专题
(翻译不对,应该用Chrome浏览器的翻译好一点,Firefox这个有问题,老有空格而且不通畅)
状态字段 StateField
-CM
State effect 描述了您想要做出的状态变化
装饰 Decoration
- CM
参考:
- Obsidian的手册:https://luhaifeng666.github.io/obsidian-plugin-docs-zh/zh2.0/editor/extensions/decorations.html
- CodeMirror的指南:https://codemirror.net/docs/guide/#decorating-the-document
- CodeMirror的示例:https://codemirror.net/examples/decoration/
- CodeMirror的文档:https://codemirror.net/docs/ref/#view.Decoration
四种类型
%a https://codemirror.net/docs/guide/#decorating-the-document 机翻
如果没有另外说明,CodeMirror 将把文档绘制成普通的文本。装饰是扩展可以通过的机制影响文档的外观。
它们有四种类型:
- 标记装饰 添加样式或 DOM 给定范围内文本的属性。
- 小部件装饰 插入一个 DOM 元素在文档中的给定位置。
- 更换装饰 隐藏部分文档或用给定的 DOM 节点替换它。
- 线条装饰 可以给a添加属性行的换行元素。
static mark(spec: Object) → Decoration // 标记装饰,插件举例:cm6-decoration、CM手册的下划线案例
static widget(spec: Object) → Decoration // 小部件装饰,插件举例:Obsidian插件第三方文档Decoration的示例代码
static replace(spec: Object) → Decoration // 更换装饰
static line(spec: Object) → Decoration // 线条装饰
static mark(spec: MarkDecorationSpec): Decoration;
static widget(spec: WidgetDecorationSpec): Decoration;
static replace(spec: ReplaceDecorationSpec): Decoration;
static line(spec: LineDecorationSpec): Decoration;
mark
举例
(代码来源于 obsidian 的 cm6-decoration 插件)
buildDecorations(view: EditorView): DecorationSet {
const builder = new RangeSetBuilder<Decoration>(); // 范围生成器
// 核心,装饰四个主要方法里的mark方法 —— 标记装饰
// 可以添加类名、和自定义属性、包括样式
let underlineDecoration: Decoration = Decoration.mark({
class: 'cm-suggestion-candidate',
attributes: {
'data-position-start': `20`,
'data-position-end': `30`,
'style': 'color: red'
}
});
builder.add(20, 30, underlineDecoration) // 需要注意的是:跨行时是分行给多个装饰(多个class),行的优先级>这里给的装饰
return builder.finish(); // 返回 RangeSet<T>,而 declare type DecorationSet = RangeSet<Decoration>;
}
replace
使用举例
(Obsidian插件第三方文档Decoration的示例代码)
// replace方法不能替换有换行的文本,详见下面的 “块装饰注意”
const decoration = Decoration.replace({
widget: new EmojiWidget()
});
注意区分Decoration.replace
Editor.rangeReplace
HTMLElement.replaceWith
,分别是替换装饰、替换文字、HTML元素替换
参数原型
interface ReplaceDecorationSpec {
/**
一个可选的小部件,在替换内容的位置绘制。
*/
widget?: WidgetType;
/**
在两端设置包容性
这个范围是否包括它两侧的位置。
这将影响新内容是否成为范围的一部分,以及游标是否可以在其两侧绘制。内联替换默认为false,块替换默认为true。
*/
inclusive?: boolean;
/**
在开始处设置包容性
*/
inclusiveStart?: boolean;
/**
在结尾处设置包容性
*/
inclusiveEnd?: boolean;
/**
这是否是块级装饰。默认为false //////////////////////////////////////// 【重要】
*/
block?: boolean;
/**
允许其他属性。
*/
[other: string]: any;
}
widget
感觉和 replace 方法没什么区别啊
看看spec参数:
interface WidgetDecorationSpec {
/**
这里要绘制的小部件类型。
*/
widget: WidgetType;
/**
小部件位于给定位置的哪一侧。
当此值为正数时,如果游标位于相同位置,则小部件将在游标之后绘制。
否则,它会被画在它前面。
当多个小部件位于同一位置时,它们的“侧”值将决定它们的顺序——值较低的小部件排在前面。默认值为0。
*/
side?: number;
/**
确定这是一个将在行之间绘制的块小部件,还是一个在周围文本之间绘制的内联小部件(默认值)。
注意,块级装饰不应该有垂直边距,如果动态更改它们的高度,应该确保调用 -
[`requestMeasure`](https://codemirror.net/6/docs/ref/#view.EditorView.requestMeasure),
这样编辑器就可以更新关于垂直布局的信息。
*/
block?: boolean;
/**
允许其他属性。
*/
[other: string]: any;
}
其他补充
调用逻辑
ViewPlugin版
状态字段版
状态字段版 - 简化流程
其实没那么复杂,总的包含逻辑是:
EditorState 同源 StateField 包含 StateEffect[]
StateEffect[] 包含 StateField、css(extension)、StateEffect
import {EditorView, Decoration, DecorationSet} from "@codemirror/view"
import {StateField, StateEffect, EditorState} from "@codemirror/state"
import {MarkdownView, View, Editor, EditorPosition} from 'obsidian';
import AnyBlockPlugin from './main'
import { blockMatch_keyword, SpecKeyword } from "./utils/rangeManager"
import { ABReplaceWidget } from "./replace/replaceWidget"
let global_plugin_this: any;
export function replace2AnyBlock(plugin_this: AnyBlockPlugin/*view: EditorView*/) {
global_plugin_this = plugin_this
// 常用变量
const view: View|null = this.app.workspace.getActiveViewOfType(MarkdownView);
if (!view) return false
// @ts-ignore 这里会说View没有editor属性
const editor: Editor = view.editor
// @ts-ignore 这里会说Editor没有cm属性
const editorView: EditorView = editor.cm
const editorState: EditorState = editorView.state
// const state: any = view.getState() // 这个不是
const cursor: EditorPosition = editor.getCursor();
const mdText = editor.getValue()
let effects: StateEffect<unknown>[] = []
/** 修改StateEffect1 - 加入StateField、css样式
* 当EditorState没有(下划线)StateField时,则将该(下划线)状态字段 添加进 EditorEffect中(函数末尾再将EditorEffect派发到EditorView中)。
* 就是说只会在第一次时执行,才会触发
*/
if (!editorState.field(underlineField, false)) {
effects.push(StateEffect.appendConfig.of(
[underlineField, underlineTheme]
))
console.log("effects2", effects)
}
/** 修改StateEffect2 - 加入范围
* 匹配、并将匹配项传入状态字段
*/
let listSpecKeyword: SpecKeyword[] = blockMatch_keyword(mdText)
if (!listSpecKeyword.length) return false
for(let item of listSpecKeyword){
let from = item.from
let to = item.to
effects.push(addUnderline.of({from, to}))
}
if (!effects.length) return false
console.log("effects3", effects)
/** 派发
* 所有常规的编辑器状态(editor state)更新都应经过此步骤。它接受一个事务或事务规范(transaction spec),并更新视图以显示该事务产生的新状态。
* 它的实现可以被[选项](https://codemirror.net/6/docs/ref/#view.EditorView.constructor^config.dispatch)覆盖。
* 此函数绑定到视图实例,因此不必作为方法调用。
*
* 根据这个说明,我是否可以理解为虽然这是editorView方法,但可以更新EditorState类
*/
editorView.dispatch({effects})
return true
}
// StateEffect
const addUnderline = StateEffect.define<{from: number, to: number}>({
map: ({from, to}, change) => ({from: change.mapPos(from), to: change.mapPos(to)})
})
// StateField,该状态管理Decoration
const underlineField = StateField.define<DecorationSet>({
create(editorState) {
return Decoration.none
},
// create好像不用管,update无论如何都能触发的
// 函数的根本作用,是为了修改decorationSet的范围,间接修改StateField的管理范围
update(decorationSet, tr) {
// 基本变量
const view: View|null = global_plugin_this.app.workspace.getActiveViewOfType(MarkdownView);
if (!view) return decorationSet
// @ts-ignore 这里会说View没有editor属性
const editor: Editor = view.editor
// @ts-ignore 这里会说Editor没有cm属性
const editorView: EditorView = editor.cm
const editorState: EditorState = editorView.state
const mdText = editor.getValue()
// const underlineMark: Decoration = Decoration.replace({widget: new ABReplaceWidget(mdText)}) // Decoration,下划线样式(mark)
console.log("update - effectsState", editorState)
// 修改 decorationSet
decorationSet = decorationSet.map(tr.changes)
for (let e of tr.effects) if (e.is(addUnderline)) {
decorationSet = decorationSet.update({
add: [underlineMark.range(e.value.from, e.value.to)]
})
}
return decorationSet
},
provide: f => EditorView.decorations.from(f)
})
// Extension
const underlineTheme = EditorView.baseTheme({
".cm-underline": { textDecoration: "underline 3px red" }
})
const underlineMark: Decoration = Decoration.replace({widget: new ABReplaceWidget("")}) // Decoration,下划线样式(mark)
状态字段版 - 补充
(这版没有ViewPlugin和PluginValue,但感觉更难懂了。StateField有嵌套泛型,effects: StateEffect[]
列表里的类型居然是不统一的!麻了)
effects的内容我打印一下:很怪
[
{
type: "map: ({ from, to }, change) => {…}",
value: {
from: 82,
to: 91
}
},
{
type: "map: ƒ (e)",
value:[
e: {
...
extension
},
Fn: {
}
]
}
]
补充2:
同样是define方法,StateField返回StateField,但StateEffect返回StateEffectType,StateEffect.define().of()才是StateEffect
两版的区别
初始调用不同
ViewPlugin方案:注册编辑器扩展
StateField方案:用一个函数去设置状态字段
DecorationSet生成不同
- ViewPlugin方案:通过RangeSetBuilder生成
- StateField方案:通过StateFieldSpec生成
update刷新不同
- ViewPlugin方案:StateField方案:ViewPlugin::update包括光标位置的改变和上下滚动文档导致的visibleRanges改变
- StateField方案:StateFieldSpec::update不会对上下滚动文档作出反应。
- 补充:这也验证了StateField在视口之前发生发生。
也说明了为什么设置有垂直高度的块装饰时,需要用EditorView.decorations
将装饰放在状态字段中,而不是视图插件中
facet
Guide
装饰品是通过一个 facet。每次景色都已更新,此方面的内容用于设置可见样式内容。
装饰品 成套 保存,又是 不可变的数据结构。 这样的集合可以是 跨变化映射 (调整 其内容的位置以补偿变化)或 重建 根据使用情况 更新 案件。
有两种方式可以提供装饰:直接,通过将范围设置值放在构面中(通常由 从字段中派生 ), 或者间接地,通过提供从视图到范围集的功能。
只有直接提供的装饰套可能会影响垂直 编辑器的块结构,但只有间接提供的才可以 阅读编辑器的视口(如果你愿意,这可能很有用,因为 例如,仅装饰 可见 的内容 )。 这样做的原因限制是视口是从块计算的 结构,所以在读取视口之前必须知道它。
原子范围
在某些情况下,例如对于大多数大于单个字符的替换装饰,您希望编辑操作将范围视为原子元素,在光标移动期间跳过它们,并在一步中将它们退格
例如:
import {
Decoration, DecorationSet, EditorView,
ViewPlugin, ViewUpdate
} from "@codemirror/view"
const placeholders = ViewPlugin.fromClass(
class {
placeholders: DecorationSet
constructor(view: EditorView) {
this.placeholders = placeholderMatcher.createDeco(view)
}
update(update: ViewUpdate) {
this.placeholders = placeholderMatcher.updateDeco(update, this.placeholders)
}
},
{
decorations: instance => instance.placeholders,
provide: plugin => EditorView.atomicRanges.of(view => { // 设置原子范围
return view.plugin(plugin)?.placeholders || Decoration.none
})
}
)
const placeholderMatcher = new MatchDecorator({
regexp: /\[\[(\w+)\]\]/g,
decoration: match => Decoration.replace({
widget: new PlaceholderWidget(match[1]),
})
})
多行与块装饰问题
【报错】多行问题
多行问题:replace方法不能替换有换行的文本
报错:RangeError: Decorations that replace line breaks may not be specified via plugins
翻译:替换换行符不能通过插件指定(也就是说你不能用小部件去把他的换行符给替换掉)
【报错】块装饰问题(VisibleRanges)
块装饰问题:我以为是 block?: boolean 参数的原因,但修改后依然报错(这个问题与多行替换无关,是另一个问题)
报错:RangeError: Block decorations may not be specified via plugins
翻译:块装饰不能通过插件指定
主要原因应该是影响了编辑器的垂直块结构,官网的Guide页面提过这个注意要项:
只有直接提供的装饰集可能会影响编辑器的垂直块结构,但只有间接(indirectly)提供的装饰集才能读取编辑器的视口。例如,如果你想只装饰 可见内容(visible content),这可能很有用。
此限制的原因是视口是从块结构计算的,因此在读取视口之前必须知道这一点。
解决方案
参考
- Decorations that replace line breaks may not be specified via plugins
- Block decorations may not be specified via plugins
这几个问题的回答者都是 “marijn”(CodeMirror维护者),是有参考价值的- 【CodeMirror论坛】块装饰错误 - 未捕获的 RangeError:可能无法通过插件指定块装饰
说要用decorations facet
【CodeMirror论坛】块装饰不能通过插件指定说这是不可能的- 【CodeMirror论坛】创建多行小部件的首选方法?
说EditorView.decorations
. 将装饰放在状态字段中,而不是插件中,以便它们在计算视口之前可用。 - 【CodeMirror论坛】应当使用facet弄一个原子范围
看装修例子 5个—修改编辑器块结构的装饰必须直接提供,而不是作为视图的函数
- 【CodeMirror论坛】块装饰错误 - 未捕获的 RangeError:可能无法通过插件指定块装饰
Obsidian第三方手册中有两种图:
- ViewPort
- State Field & View Plugin
该原理可以解释:
- 为什么不能用CSS计数器实现标题计数
- 为什么块装饰需要使用StateField而不能用ViewPlugin
Example > 装修资源
使用数据结构向编辑器提供装饰,该
RangeSet
数据结构存储值的集合(在本例中为装饰)以及与之关联的范围(开始和结束位置)。这种数据结构有助于在文档更改时有效地更新大量装饰中的位置。装饰通过 facet提供给编辑器视图。
有两种方法可以提供它们
之前我们用的是第一种方案
原因:visibleRanges 相关的类
// 打印属性
class EditorView {
viewState
// 这几个都是返回div元素
dom: div.cm-editor.ͼ1.ͼ2.ͼ3a.ͼq
announceDOM: div
contentDOM: div.cm-content
scrollDOM: div.cm-scroller
// get类 方法
viewport // 和visibleRanges类似,返回 {from, to} (没有列表)
visibleRanges // 获取当前文档被渲染的部分,返回 readonly {form, to}[]
state // view.state.sliceDoc(from, to); 这个方法能返回渲染部分的正文内容
}
如果要装饰多行,则需要在计算视口之前可用,而visibleRanges应该是视图计算后才有的
示例
关键字高亮
(代码片段来源于 Obsidian 的 cm6-decoration 插件)
// 强调装饰
const underlineDecoration = (start: number, end: number, indexKeyword: string) => {
return Decoration.mark({ // 核心,装饰四个主要方法里的mark方法 —— 标记装饰
class: SuggestionCandidateClass,
attributes: {
'data-index-keyword': indexKeyword,
'data-position-start': `${start}`,
'data-position-end': `${end}`,
},
});
};
下划线命令
与不用状态字段功能相比,详细区别见 “调用逻辑 > 两版的区别” 一节
下划线案例
假设我们要实现一个编辑器扩展,允许用户在文档的某些部分添加下划线。
为此,我们可以定义一个状态字段来跟踪文档的哪些部分带有下划线,并提供绘制这些下划线的标记装饰。
代码
// 下划线实验
import {EditorView, Decoration, DecorationSet} from "@codemirror/view"
import {StateField, StateEffect} from "@codemirror/state"
import {keymap} from "@codemirror/view"
// 1. 将命令绑定到ctrl+h上
export const underlineKeymap = keymap.of([{
key: "Ctrl-r",
preventDefault: true,
run: underlineSelection
}])
// @private方法
// 2. 选择需要给下划线的部分
export function underlineSelection(view: EditorView) {
/**
先说view.state.selection.ranges
类型SelectionRange[],即{from, to, flags}[]
内容是当前编辑器选择的内容,哪怕选择的范围很大,数组长度也只有一。
length>1 的情况是多个选择范围(按住Alt多选)
再说StateEffect<...>[]
感觉里面好像啥都能塞的样子。
这里创建的过程有点繁琐,类型变化经历了 SelectionRange[] -> StateEffect -> StateEffectType -> StateEffect<...>[]
*/
let effects: StateEffect<unknown>[] = view.state.selection.ranges // 类型 SelectionRange[]
.filter(r => !r.empty) // 去除掉空选择,类型依然是 SelectionRange[]
.map(({from, to}) => addUnderline.of({from, to})) // 传给addUnderline函数,再用of函数,将类型最终修改为 StateEffect<...>[]
console.log("effects1", effects) // 指定新的下划线内容时会触发
// 没有选择则不管
if (!effects.length) return false
// 当状态有(下划线)字段时,才执行里面的内容。
// 添加下划线字段,只会在第一次时执行,才会触发
if (!view.state.field(underlineField, false)) {
// of()的接收参数是Extension
// 这里的StateField、和baseTheme生成的Extension都符合该参数接口
effects.push(StateEffect.appendConfig.of(
[underlineField, underlineTheme]
))
console.log("effects2", effects)
}
view.dispatch({effects}) // 派发
return true
}
// StateEffect
// 将number number,转化为StateEffectType,返回出去后会再转化为StateEffect
const addUnderline = StateEffect.define<{from: number, to: number}>({
map: ({from, to}, change) => ({from: change.mapPos(from), to: change.mapPos(to)}) // 哪来的change?
})
// StateField
// 下划线的状态字段、该状态管理Decoration
const underlineField = StateField.define<DecorationSet>({
create() {
return Decoration.none
},
// create好像不用管,update无论如何都能触发的
// 光标移动也会触发update,但上下滚动文档不会
// update: (value: Value, transaction: Transaction) => Value;
update(underlines, tr) {
/** 这里主要注意两个方法:underlines.map() 和 underlines.update(),都是用来更新范围状态的。
* underlines.update()
* 用来增加新的
* 没有这个就没有样式,因为上面create方法没写。也能用来增加新的
* 另外update不怕重复添加,例如from-to:5-20,再添加3-16,就会变成3-20
* underlines.map()
* 用来映射原有的,编辑时重新映射from-to
* 例如(0000)中间插入字符,先变成(00)11(00)再变成(00)(11)(00)或(001100)
* 如果不加map,缩短范围时会出bug(范围只增不减)。
* @warning 另外其实应该先add再更新,否则下划线会延迟一个动作再修改,会产生错位,感觉这个demo是做错了
*/
underlines = underlines.map(tr.changes)
for (let e of tr.effects) if (e.is(addUnderline)) {
underlines = underlines.update({
add: [underlineMark.range(e.value.from, e.value.to)]
})
}
return underlines
},
provide: f => EditorView.decorations.from(f)
})
// Decoration
// @pricate 下划线样式(mark)
const underlineMark = Decoration.mark({class: "cm-underline"})
// @return Extension
// 下划线样式(css)
// 按道理样式应该是可以直接加在Decoration里的。当然区别是那个是内敛样式,这种方式是通过类选择器加在css里的
const underlineTheme = EditorView.baseTheme({
".cm-underline": { textDecoration: "underline 3px red" }
})
如果使用的Obsidian插件,需要将注册命令 keymap.of
的方式改写一下,例如:
this.addCommand({
id: 'sample-editor-command-underline',
name: '下划线',
editorCallback: (editor: Editor, view: MarkdownView) => {
// 下面这个注释是需要的,好像要绕过 Typescript 的验证。
// @ts-expect-error, not typed
const editorView = view.editor.cm as EditorView;
underlineSelection(editorView)
}
})
下划线(自动检测版)
import {EditorView, Decoration, DecorationSet} from "@codemirror/view"
import {StateField, StateEffect, EditorState} from "@codemirror/state"
import {MarkdownView, View, Editor, EditorPosition} from 'obsidian';
import AnyBlockPlugin from './main'
import { blockMatch_keyword, SpecKeyword } from "./utils/rangeManager"
import { ABReplaceWidget } from "./replace/replaceWidget"
let global_plugin_this: any;
/** 启用状态字段装饰功能 */
export function replace2AnyBlock(plugin_this: AnyBlockPlugin/*view: EditorView*/) {
global_plugin_this = plugin_this
// 常用变量
const view: View|null = this.app.workspace.getActiveViewOfType(MarkdownView);
if (!view) return false
// @ts-ignore 这里会说View没有editor属性
const editor: Editor = view.editor
// @ts-ignore 这里会说Editor没有cm属性
const editorView: EditorView = editor.cm
const editorState: EditorState = editorView.state
// const state: any = view.getState() // 这个不是
const cursor: EditorPosition = editor.getCursor();
const mdText = editor.getValue()
let stateEffects: StateEffect<unknown>[] = []
/** 修改StateEffect1 - 加入StateField、css样式
* 当EditorState没有(下划线)StateField时,则将该(下划线)状态字段 添加进 EditorEffect中(函数末尾再将EditorEffect派发到EditorView中)。
* 就是说只会在第一次时执行,才会触发
*/
if (!editorState.field(underlineField, false)) {
stateEffects.push(StateEffect.appendConfig.of(
[underlineField, underlineTheme]
))
}
/** 修改StateEffect2 - 加入范围
* 匹配、并将匹配项传入状态字段
*
let listSpecKeyword: SpecKeyword[] = blockMatch_keyword(mdText)
if (!listSpecKeyword.length) return false
for(let item of listSpecKeyword){
let from = item.from
let to = item.to
// StateEffect
const addUnderline = StateEffect.define<{from: number, to: number}>({
map: ({from, to}, change) => ({from: change.mapPos(from), to: change.mapPos(to)})
}).of({from, to})
stateEffects.push(addUnderline)
}
if (!stateEffects.length) return false
console.log("effects3", stateEffects)
*/
/** 派发
* 所有常规的编辑器状态(editor state)更新都应经过此步骤。它接受一个事务或事务规范(transaction spec),并更新视图以显示该事务产生的新状态。
* 它的实现可以被[选项](https://codemirror.net/6/docs/ref/#view.EditorView.constructor^config.dispatch)覆盖。
* 此函数绑定到视图实例,因此不必作为方法调用。
*
* 根据这个说明,我是否可以理解为虽然这是editorView方法,但可以更新EditorState类
*/
editorView.dispatch({effects: stateEffects})
return true
}
// StateField,该状态管理Decoration
const underlineField = StateField.define<DecorationSet>({
create(editorState) {
return Decoration.none
},
// create好像不用管,update无论如何都能触发的
// 函数的根本作用,是为了修改decorationSet的范围,间接修改StateField的管理范围
update(decorationSet, tr) {
// 基本变量
const view: View|null = global_plugin_this.app.workspace.getActiveViewOfType(MarkdownView);
if (!view) return decorationSet
// @ts-ignore 这里会说View没有editor属性
const editor: Editor = view.editor
// @ts-ignore 这里会说Editor没有cm属性
const editorView: EditorView = editor.cm
const editorState: EditorState = editorView.state
const mdText = editor.getValue()
console.log("update - effectsState", editorState)
// 修改 decorationSet
let listSpecKeyword: SpecKeyword[] = blockMatch_keyword(mdText)
if (!listSpecKeyword.length) return decorationSet
for(let item of listSpecKeyword){
let from = item.from
let to = item.to
decorationSet = decorationSet.update({
add: [underlineMark.range(from, to)]
})
}
return decorationSet
},
provide: f => EditorView.decorations.from(f)
})
// Extension
const underlineTheme = EditorView.baseTheme({
".cm-underline": { textDecoration: "underline 3px red" }
})
// const underlineMark: Decoration = Decoration.replace({widget: new ABReplaceWidget("")}) // Decoration,下划线样式(mark)
const underlineMark: Decoration = Decoration.mark({class: "cm-underline"})
布尔切换小部件
接下来,我们将看一个在旁边显示复选框小部件的插件 布尔文字,并允许用户单击以翻转 文字。
小部件装饰不直接包含它们的小部件 DOM。 除了 帮助将可变对象置于编辑器状态之外,这个额外的 间接级别也使得重新创建小部件成为可能 无需为它们重新绘制 DOM。 我们稍后会简单地使用它 每当文档更改时重新创建我们的装饰集。
因此,我们必须首先定义一个子类 WidgetType
绘制小部件。
import {WidgetType} from "@codemirror/view"
class CheckboxWidget extends WidgetType {
constructor(readonly checked: boolean) { super() }
eq(other: CheckboxWidget) { return other.checked == this.checked }
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 }
}
或
const decoration = Decoration.replace({ // replace方法不能换行
widget: new EmojiWidget()
});