gzkx-editor 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +12 -0
- package/.github/FUNDING.yml +3 -0
- package/.github/ISSUE_TEMPLATE.md +9 -0
- package/ARCHITECTURE.md +1011 -0
- package/CONTRIBUTING.md +104 -0
- package/LICENSE +19 -0
- package/PUBLISH.md +286 -0
- package/README.md +80 -0
- package/STARTUP.md +535 -0
- package/USAGE.md +789 -0
- package/assets/prosemirror_dark.svg +1 -0
- package/assets/prosemirror_light.svg +1 -0
- package/bin/pm +1 -0
- package/bin/pm.js +384 -0
- package/core/build.mjs +95 -0
- package/core/core-entry.mjs +19 -0
- package/core/dist/index.cjs +20951 -0
- package/core/dist/index.js +20717 -0
- package/core/package.json +24 -0
- package/core/src/index.ts +21 -0
- package/core/tsconfig.json +35 -0
- package/demo/bench/example.js +75 -0
- package/demo/bench/index.html +11 -0
- package/demo/bench/index.js +67 -0
- package/demo/bench/mutate.js +14 -0
- package/demo/bench/type.js +40 -0
- package/demo/demo.css +33 -0
- package/demo/demo.ts +16 -0
- package/demo/example-setup/style/style.css +83 -0
- package/demo/gapcursor/style/gapcursor.css +25 -0
- package/demo/img.png +0 -0
- package/demo/index.html +75 -0
- package/demo/menu/style/menu.css +169 -0
- package/demo/test/mocha.css +1 -0
- package/demo/test/mocha.js +1 -0
- package/demo/view/style/prosemirror.css +54 -0
- package/package.json +36 -0
- package/tsconfig.json +37 -0
package/ARCHITECTURE.md
ADDED
|
@@ -0,0 +1,1011 @@
|
|
|
1
|
+
# GZKX Editor 技术实现文档
|
|
2
|
+
|
|
3
|
+
## 1. 架构概述
|
|
4
|
+
|
|
5
|
+
GZKX Editor 基于 ProseMirror 构建,采用模块化架构设计,将编辑器功能拆分为多个独立的包,每个包负责特定的功能领域。
|
|
6
|
+
|
|
7
|
+
### 1.1 核心架构分层
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
┌─────────────────────────────────────────┐
|
|
11
|
+
│ View Layer (视图层) │
|
|
12
|
+
│ gzkx-editor-view │
|
|
13
|
+
├─────────────────────────────────────────┤
|
|
14
|
+
│ State Layer (状态层) │
|
|
15
|
+
│ gzkx-editor-state │
|
|
16
|
+
├─────────────────────────────────────────┤
|
|
17
|
+
│ Model Layer (模型层) │
|
|
18
|
+
│ gzkx-editor-model │
|
|
19
|
+
├─────────────────────────────────────────┤
|
|
20
|
+
│ Transform Layer (转换层) │
|
|
21
|
+
│ gzkx-editor-transform │
|
|
22
|
+
└─────────────────────────────────────────┘
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### 1.2 模块依赖关系图
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
┌─────────────────┐
|
|
29
|
+
│ gzkx-editor-model │
|
|
30
|
+
└────────┬────────┘
|
|
31
|
+
│
|
|
32
|
+
┌────────▼────────┐
|
|
33
|
+
│gzkx-editor-transform│
|
|
34
|
+
└────────┬────────┘
|
|
35
|
+
│
|
|
36
|
+
┌──────────────────┼──────────────────┐
|
|
37
|
+
│ │ │
|
|
38
|
+
┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐
|
|
39
|
+
│gzkx-editor-state│ │gzkx-editor-commands│ │gzkx-editor-keymap │
|
|
40
|
+
└──────┬──────┘ └──────┬──────┘ └─────────────┘
|
|
41
|
+
│ │
|
|
42
|
+
│ ┌──────▼──────┐
|
|
43
|
+
│ │gzkx-editor-inputrules│
|
|
44
|
+
│ └─────────────┘
|
|
45
|
+
│
|
|
46
|
+
┌──────▼──────┐ ┌─────────────┐
|
|
47
|
+
│gzkx-editor-view│ │gzkx-editor-history│
|
|
48
|
+
└─────────────┘ └─────────────┘
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## 2. 核心模块详解
|
|
52
|
+
|
|
53
|
+
### 2.1 gzkx-editor-model (文档模型层)
|
|
54
|
+
|
|
55
|
+
文档模型层是整个编辑器的基础,定义了文档的数据结构。
|
|
56
|
+
|
|
57
|
+
#### 2.1.1 核心类
|
|
58
|
+
|
|
59
|
+
**Node (节点)**
|
|
60
|
+
- 文档的基本组成单元
|
|
61
|
+
- 包含类型、属性、内容和标记
|
|
62
|
+
- 支持两种类型:文本节点(TextNode)和块级节点(BlockNode)
|
|
63
|
+
|
|
64
|
+
**Fragment (片段)**
|
|
65
|
+
- 节点的集合,用于表示文档的一部分内容
|
|
66
|
+
- 不可变数据结构,支持高效的差量更新
|
|
67
|
+
|
|
68
|
+
**Slice (切片)**
|
|
69
|
+
- 从文档中切取的一部分内容
|
|
70
|
+
- 用于跨编辑器复制/粘贴操作
|
|
71
|
+
- 包含完整的位置映射信息
|
|
72
|
+
|
|
73
|
+
**Mark (标记)**
|
|
74
|
+
- 应用于内容的元数据,如加粗、斜体、链接等
|
|
75
|
+
- 可以叠加多个标记
|
|
76
|
+
|
|
77
|
+
**Schema (模式)**
|
|
78
|
+
- 定义文档的结构约束
|
|
79
|
+
- 声明可用的节点类型和标记类型
|
|
80
|
+
- 规定节点之间的包含关系
|
|
81
|
+
|
|
82
|
+
#### 2.1.2 Schema 定义示例
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
const mySchema = new Schema({
|
|
86
|
+
nodes: {
|
|
87
|
+
// 文档根节点
|
|
88
|
+
doc: { content: 'block+' },
|
|
89
|
+
|
|
90
|
+
// 段落
|
|
91
|
+
paragraph: {
|
|
92
|
+
content: 'inline*',
|
|
93
|
+
group: 'block',
|
|
94
|
+
parseDOM: [{ tag: 'p' }],
|
|
95
|
+
toDOM() { return ['p', 0] }
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
// 标题
|
|
99
|
+
heading: {
|
|
100
|
+
content: 'inline*',
|
|
101
|
+
group: 'block',
|
|
102
|
+
attrs: { level: { default: 1 } },
|
|
103
|
+
parseDOM: [
|
|
104
|
+
{ tag: 'h1', attrs: { level: 1 } },
|
|
105
|
+
{ tag: 'h2', attrs: { level: 2 } }
|
|
106
|
+
],
|
|
107
|
+
toDOM(node) { return ['h' + node.attrs.level, 0] }
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
// 文本
|
|
111
|
+
text: { group: 'inline' },
|
|
112
|
+
|
|
113
|
+
// 硬换行
|
|
114
|
+
hard_break: {
|
|
115
|
+
inline: true,
|
|
116
|
+
group: 'inline',
|
|
117
|
+
selectable: false,
|
|
118
|
+
parseDOM: [{ tag: 'br' }],
|
|
119
|
+
toDOM() { return ['br'] }
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
marks: {
|
|
124
|
+
// 加粗
|
|
125
|
+
strong: {
|
|
126
|
+
parseDOM: [
|
|
127
|
+
{ tag: 'strong' },
|
|
128
|
+
{ tag: 'b', getAttrs: node => (node as HTMLElement).style.fontWeight !== 'normal' && null },
|
|
129
|
+
{ style: 'font-weight', getAttrs: value => /^(bold(er)|[5-9]00)$/.test(value as string) && null }
|
|
130
|
+
],
|
|
131
|
+
toDOM() { return ['strong', 0] }
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
// 斜体
|
|
135
|
+
em: {
|
|
136
|
+
parseDOM: [
|
|
137
|
+
{ tag: 'i' },
|
|
138
|
+
{ tag: 'em' },
|
|
139
|
+
{ style: 'font-style=italic' }
|
|
140
|
+
],
|
|
141
|
+
toDOM() { return ['em', 0] }
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
// 链接
|
|
145
|
+
link: {
|
|
146
|
+
attrs: {
|
|
147
|
+
href: {},
|
|
148
|
+
title: { default: null }
|
|
149
|
+
},
|
|
150
|
+
parseDOM: [{
|
|
151
|
+
tag: 'a[href]',
|
|
152
|
+
getAttrs(dom) {
|
|
153
|
+
return {
|
|
154
|
+
href: (dom as HTMLElement).getAttribute('href'),
|
|
155
|
+
title: (dom as HTMLElement).getAttribute('title')
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}],
|
|
159
|
+
toDOM(mark) {
|
|
160
|
+
return ['a', { href: mark.attrs.href, title: mark.attrs.title }, 0]
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
#### 2.1.3 内容表达式 (Content Expressions)
|
|
168
|
+
|
|
169
|
+
Schema 使用内容表达式来定义节点之间的包含关系:
|
|
170
|
+
|
|
171
|
+
| 表达式 | 说明 |
|
|
172
|
+
|--------|------|
|
|
173
|
+
| `block` | 匹配 block 组中的任一节点 |
|
|
174
|
+
| `inline` | 匹配 inline 组中的任一节点 |
|
|
175
|
+
| `text` | 匹配文本节点 |
|
|
176
|
+
| `nodeType` | 匹配指定类型的节点 |
|
|
177
|
+
| `+` | 一个或多个 |
|
|
178
|
+
| `*` | 零个或多个 |
|
|
179
|
+
| `?` | 零个或一个 |
|
|
180
|
+
| `|` | 或 |
|
|
181
|
+
| `()` | 分组 |
|
|
182
|
+
|
|
183
|
+
示例:
|
|
184
|
+
- `block+` - 一个或多个块级节点
|
|
185
|
+
- `inline*` - 零个或多个行内节点
|
|
186
|
+
- `(bullet_list | ordered_list)+` - 一个或多个列表
|
|
187
|
+
- `paragraph | heading` - 段落或标题
|
|
188
|
+
|
|
189
|
+
### 2.2 gzkx-editor-state (状态管理层)
|
|
190
|
+
|
|
191
|
+
状态层管理编辑器的完整状态,包括文档内容、选择和插件。
|
|
192
|
+
|
|
193
|
+
#### 2.2.1 EditorState
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
interface EditorState {
|
|
197
|
+
// 文档内容
|
|
198
|
+
doc: Node;
|
|
199
|
+
|
|
200
|
+
// 当前选择
|
|
201
|
+
selection: Selection;
|
|
202
|
+
|
|
203
|
+
// 已应用的事务集合
|
|
204
|
+
storedMarks: Mark[] | null;
|
|
205
|
+
|
|
206
|
+
// 激活的插件
|
|
207
|
+
plugins: Plugin[];
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
#### 2.2.2 Transaction (事务)
|
|
212
|
+
|
|
213
|
+
事务是不可变的,代表一次状态变更:
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
// 创建事务的方式
|
|
217
|
+
const tr = state.tr;
|
|
218
|
+
|
|
219
|
+
// 插入文本
|
|
220
|
+
tr.insertText('Hello', 10);
|
|
221
|
+
|
|
222
|
+
// 删除内容
|
|
223
|
+
tr.delete(5, 15);
|
|
224
|
+
|
|
225
|
+
// 设置选择
|
|
226
|
+
tr.setSelection(TextSelection.create(tr.doc, 0, 10));
|
|
227
|
+
|
|
228
|
+
// 添加标记
|
|
229
|
+
tr.addMark(0, 10, state.schema.marks.strong.create());
|
|
230
|
+
|
|
231
|
+
// 提交事务
|
|
232
|
+
view.dispatch(tr);
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
#### 2.2.3 Selection (选择)
|
|
236
|
+
|
|
237
|
+
支持的选择类型:
|
|
238
|
+
|
|
239
|
+
- **TextSelection** - 文本选择,最常见的类型
|
|
240
|
+
- **NodeSelection** - 节点选择,用于选中整个节点
|
|
241
|
+
- **AllSelection** - 全选
|
|
242
|
+
|
|
243
|
+
```typescript
|
|
244
|
+
// 获取当前选择
|
|
245
|
+
const { from, to, empty } = state.selection;
|
|
246
|
+
|
|
247
|
+
// 创建文本选择
|
|
248
|
+
const selection = TextSelection.create(doc, 0, 10);
|
|
249
|
+
|
|
250
|
+
// 创建节点选择
|
|
251
|
+
const nodeSelection = NodeSelection.create(doc, 5);
|
|
252
|
+
|
|
253
|
+
// 折叠选择
|
|
254
|
+
const collapsed = selection.extend(15);
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
#### 2.2.4 Plugin (插件)
|
|
258
|
+
|
|
259
|
+
插件是扩展编辑器功能的主要机制:
|
|
260
|
+
|
|
261
|
+
```typescript
|
|
262
|
+
const myPlugin = new Plugin({
|
|
263
|
+
key: myPluginKey,
|
|
264
|
+
|
|
265
|
+
// 插件状态
|
|
266
|
+
state: {
|
|
267
|
+
init() {
|
|
268
|
+
return initialState;
|
|
269
|
+
},
|
|
270
|
+
apply(tr, oldState, oldEditorState, newEditorState) {
|
|
271
|
+
// 更新插件状态
|
|
272
|
+
return newState;
|
|
273
|
+
}
|
|
274
|
+
},
|
|
275
|
+
|
|
276
|
+
// 视图部分
|
|
277
|
+
view(editorView) {
|
|
278
|
+
return {
|
|
279
|
+
update(editorView, prevState) { },
|
|
280
|
+
destroy() { }
|
|
281
|
+
};
|
|
282
|
+
},
|
|
283
|
+
|
|
284
|
+
// 事务过滤
|
|
285
|
+
filterTransaction(transaction, state) {
|
|
286
|
+
// 可以阻止某些事务
|
|
287
|
+
return true;
|
|
288
|
+
},
|
|
289
|
+
|
|
290
|
+
// 追加事务
|
|
291
|
+
appendTransaction(transactions, oldState, newState) {
|
|
292
|
+
// 在事务后自动应用额外变更
|
|
293
|
+
},
|
|
294
|
+
|
|
295
|
+
// 属性
|
|
296
|
+
props: {
|
|
297
|
+
handleKeyDown(view, event) { },
|
|
298
|
+
handleClick(view, pos, event) { }
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### 2.3 gzkx-editor-view (视图层)
|
|
304
|
+
|
|
305
|
+
视图层负责渲染文档和处理用户交互。
|
|
306
|
+
|
|
307
|
+
#### 2.3.1 EditorView
|
|
308
|
+
|
|
309
|
+
```typescript
|
|
310
|
+
const view = new EditorView(document.querySelector('.editor'), {
|
|
311
|
+
// 初始状态
|
|
312
|
+
state: initialState,
|
|
313
|
+
|
|
314
|
+
// 分发事务回调
|
|
315
|
+
dispatchTransaction(transaction) {
|
|
316
|
+
const newState = view.state.apply(transaction);
|
|
317
|
+
view.updateState(newState);
|
|
318
|
+
},
|
|
319
|
+
|
|
320
|
+
// 不可编辑时返回 false
|
|
321
|
+
editable() {
|
|
322
|
+
return !readOnly;
|
|
323
|
+
},
|
|
324
|
+
|
|
325
|
+
// 属性
|
|
326
|
+
attributes: {
|
|
327
|
+
class: 'my-editor'
|
|
328
|
+
},
|
|
329
|
+
|
|
330
|
+
// 装饰器
|
|
331
|
+
decorations(state) {
|
|
332
|
+
return decorationSet;
|
|
333
|
+
},
|
|
334
|
+
|
|
335
|
+
// 节点视图
|
|
336
|
+
nodeViews: {
|
|
337
|
+
myNode(node, view, getPos, decorations, innerDecorations) {
|
|
338
|
+
return {
|
|
339
|
+
dom: createMyNodeDOM(),
|
|
340
|
+
contentDOM: someChild,
|
|
341
|
+
update(node) { },
|
|
342
|
+
ignoreMutation() { },
|
|
343
|
+
destroy() { }
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
#### 2.3.2 节点视图 (NodeView)
|
|
351
|
+
|
|
352
|
+
自定义节点渲染:
|
|
353
|
+
|
|
354
|
+
```typescript
|
|
355
|
+
function createImageNodeView() {
|
|
356
|
+
return {
|
|
357
|
+
// 外层 DOM 元素
|
|
358
|
+
dom: document.createElement('div'),
|
|
359
|
+
|
|
360
|
+
// 内容 DOM(可选)
|
|
361
|
+
contentDOM: null,
|
|
362
|
+
|
|
363
|
+
// 更新节点
|
|
364
|
+
update(node) {
|
|
365
|
+
if (node.type.name !== 'image') return false;
|
|
366
|
+
this.dom.innerHTML = `<img src="${node.attrs.src}">`
|
|
367
|
+
return true;
|
|
368
|
+
},
|
|
369
|
+
|
|
370
|
+
// 是否忽略 DOM 变化
|
|
371
|
+
ignoreMutation(mutation) {
|
|
372
|
+
return true;
|
|
373
|
+
},
|
|
374
|
+
|
|
375
|
+
// 销毁
|
|
376
|
+
destroy() {
|
|
377
|
+
this.dom.remove();
|
|
378
|
+
},
|
|
379
|
+
|
|
380
|
+
// 位置获取函数
|
|
381
|
+
stopEvent(event) {
|
|
382
|
+
return false;
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
### 2.4 gzkx-editor-transform (转换层)
|
|
389
|
+
|
|
390
|
+
转换层提供文档的变换操作,是协同编辑的基础。
|
|
391
|
+
|
|
392
|
+
#### 2.4.1 Step (步骤)
|
|
393
|
+
|
|
394
|
+
步骤是文档变更的原子单位:
|
|
395
|
+
|
|
396
|
+
```typescript
|
|
397
|
+
// 替换步骤
|
|
398
|
+
const replaceStep = new ReplaceStep(0, 10, new Slice(fragment, 0, 0));
|
|
399
|
+
|
|
400
|
+
// 添加标记步骤
|
|
401
|
+
const addMarkStep = new AddMarkStep(0, 10, mark);
|
|
402
|
+
|
|
403
|
+
// 移除标记步骤
|
|
404
|
+
const removeMarkStep = new RemoveMarkStep(0, 10, mark);
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
#### 2.4.2 Transform
|
|
408
|
+
|
|
409
|
+
变换操作构建器:
|
|
410
|
+
|
|
411
|
+
```typescript
|
|
412
|
+
const tr = state.tr;
|
|
413
|
+
|
|
414
|
+
// 插入内容
|
|
415
|
+
tr.insert(5, Node.fromJSON(schema, { type: 'paragraph', content: [...] }));
|
|
416
|
+
|
|
417
|
+
// 插入文本
|
|
418
|
+
tr.insertText('Hello World', 5);
|
|
419
|
+
|
|
420
|
+
// 删除内容
|
|
421
|
+
tr.delete(0, 5);
|
|
422
|
+
|
|
423
|
+
// 替换内容
|
|
424
|
+
tr.replace(0, 10, new Slice(fragment, 0, 0));
|
|
425
|
+
|
|
426
|
+
// 添加标记
|
|
427
|
+
tr.addMark(0, 10, schema.marks.strong.create());
|
|
428
|
+
|
|
429
|
+
// 移除标记
|
|
430
|
+
tr.removeMark(0, 10, schema.marks.strong);
|
|
431
|
+
|
|
432
|
+
// 分割节点
|
|
433
|
+
tr.split(10);
|
|
434
|
+
|
|
435
|
+
// 合并节点
|
|
436
|
+
tr.join(10);
|
|
437
|
+
|
|
438
|
+
// 提升节点
|
|
439
|
+
tr.lift(range, target);
|
|
440
|
+
|
|
441
|
+
// 包裹节点
|
|
442
|
+
tr.wrap(range, [{ type: schema.nodes.blockquote }]);
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
#### 2.4.3 Mapping
|
|
446
|
+
|
|
447
|
+
映射跟踪文档位置变化:
|
|
448
|
+
|
|
449
|
+
```typescript
|
|
450
|
+
const mapping = new Mapping([step1.map, step2.map]);
|
|
451
|
+
|
|
452
|
+
// 映射位置
|
|
453
|
+
const newPos = mapping.map(oldPos);
|
|
454
|
+
|
|
455
|
+
// 映射范围
|
|
456
|
+
const [from, to] = mapping.mapRange(oldFrom, oldTo);
|
|
457
|
+
|
|
458
|
+
// 获取映射结果
|
|
459
|
+
const result = mapping.mapResult(oldPos);
|
|
460
|
+
if (result.deleted) {
|
|
461
|
+
// 位置已被删除
|
|
462
|
+
}
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
### 2.5 gzkx-editor-commands (命令层)
|
|
466
|
+
|
|
467
|
+
命令是编辑操作的抽象。
|
|
468
|
+
|
|
469
|
+
#### 2.5.1 Command 接口
|
|
470
|
+
|
|
471
|
+
```typescript
|
|
472
|
+
type Command = (
|
|
473
|
+
state: EditorState,
|
|
474
|
+
dispatch?: (tr: Transaction) => void,
|
|
475
|
+
view?: EditorView
|
|
476
|
+
) => boolean;
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
#### 2.5.2 内置命令
|
|
480
|
+
|
|
481
|
+
```typescript
|
|
482
|
+
// 删除选区
|
|
483
|
+
deleteSelection(state, dispatch);
|
|
484
|
+
|
|
485
|
+
// 插入硬换行
|
|
486
|
+
exitCode(state, dispatch);
|
|
487
|
+
|
|
488
|
+
// 加粗/取消加粗
|
|
489
|
+
toggleMark(schema.marks.strong)(state, dispatch);
|
|
490
|
+
|
|
491
|
+
// 设置块类型
|
|
492
|
+
setBlockType(schema.nodes.heading, { level: 1 })(state, dispatch);
|
|
493
|
+
|
|
494
|
+
// 包裹节点
|
|
495
|
+
wrapIn(schema.nodes.blockquote)(state, dispatch);
|
|
496
|
+
|
|
497
|
+
// 提升节点
|
|
498
|
+
lift(state, dispatch);
|
|
499
|
+
|
|
500
|
+
// 撤销
|
|
501
|
+
undo(state, dispatch);
|
|
502
|
+
|
|
503
|
+
// 重做
|
|
504
|
+
redo(state, dispatch);
|
|
505
|
+
|
|
506
|
+
// 提交
|
|
507
|
+
commit_to_history(state, dispatch);
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
#### 2.5.3 自定义命令
|
|
511
|
+
|
|
512
|
+
```typescript
|
|
513
|
+
const myCommand: Command = (state, dispatch, view) => {
|
|
514
|
+
// 检查命令是否可用
|
|
515
|
+
if (someCondition) return false;
|
|
516
|
+
|
|
517
|
+
if (dispatch) {
|
|
518
|
+
const tr = state.tr;
|
|
519
|
+
// 执行操作
|
|
520
|
+
dispatch(tr);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
return true;
|
|
524
|
+
};
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
### 2.6 gzkx-editor-keymap (快捷键层)
|
|
528
|
+
|
|
529
|
+
快捷键绑定管理。
|
|
530
|
+
|
|
531
|
+
#### 2.6.1 基础快捷键
|
|
532
|
+
|
|
533
|
+
```typescript
|
|
534
|
+
import { baseKeymap } from 'gzkx-editor-commands';
|
|
535
|
+
|
|
536
|
+
// Ctrl+B: 加粗
|
|
537
|
+
// Ctrl+I: 斜体
|
|
538
|
+
// Ctrl+Shift+X: 行内代码
|
|
539
|
+
// Ctrl+|: 引用
|
|
540
|
+
// ...
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
#### 2.6.2 自定义快捷键
|
|
544
|
+
|
|
545
|
+
```typescript
|
|
546
|
+
import { keymap } from 'gzkx-editor-keymap';
|
|
547
|
+
|
|
548
|
+
const myKeymap = keymap({
|
|
549
|
+
'Mod-Alt-c': (state, dispatch) => {
|
|
550
|
+
// 自定义命令
|
|
551
|
+
return true;
|
|
552
|
+
},
|
|
553
|
+
|
|
554
|
+
'Enter': (state, dispatch) => {
|
|
555
|
+
// 拦截回车键
|
|
556
|
+
return true;
|
|
557
|
+
}
|
|
558
|
+
});
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
### 2.7 gzkx-editor-inputrules (输入规则层)
|
|
562
|
+
|
|
563
|
+
输入规则在用户输入时自动触发变换。
|
|
564
|
+
|
|
565
|
+
#### 2.7.1 创建输入规则
|
|
566
|
+
|
|
567
|
+
```typescript
|
|
568
|
+
import { inputRules, wrappingInputRule, textblockTypeInputRule, smartQuotes } from 'gzkx-editor-inputrules';
|
|
569
|
+
|
|
570
|
+
// 引号自动闭合
|
|
571
|
+
const quoteRule = wrappingInputRule(/^\s*>"$/, schema.nodes.blockquote);
|
|
572
|
+
|
|
573
|
+
// 有序列表
|
|
574
|
+
const orderedListRule = wrappingInputRule(
|
|
575
|
+
/^\s*(\d+)\.\s$/,
|
|
576
|
+
schema.nodes.ordered_list,
|
|
577
|
+
match => ({ order: +match[1] }),
|
|
578
|
+
(match, node) => node.childCount + node.attrs.order === +match[1]
|
|
579
|
+
);
|
|
580
|
+
|
|
581
|
+
// 标题
|
|
582
|
+
const headingRule = textblockTypeInputRule(
|
|
583
|
+
/^#{1,6}\s$/,
|
|
584
|
+
schema.nodes.heading,
|
|
585
|
+
match => ({ level: match[0].trim().length })
|
|
586
|
+
);
|
|
587
|
+
|
|
588
|
+
// 分割线
|
|
589
|
+
const horizontalRule = textblockTypeInputRule(
|
|
590
|
+
/^---$/,
|
|
591
|
+
schema.nodes.horizontal_rule
|
|
592
|
+
);
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
#### 2.7.2 组合输入规则
|
|
596
|
+
|
|
597
|
+
```typescript
|
|
598
|
+
const myInputRules = inputRules({
|
|
599
|
+
rules: [
|
|
600
|
+
...smartQuotes,
|
|
601
|
+
quoteRule,
|
|
602
|
+
orderedListRule,
|
|
603
|
+
headingRule
|
|
604
|
+
]
|
|
605
|
+
});
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
### 2.8 gzkx-editor-history (历史记录层)
|
|
609
|
+
|
|
610
|
+
撤销/重做功能。
|
|
611
|
+
|
|
612
|
+
```typescript
|
|
613
|
+
import { history, undo, redo, commit_to_history } from 'gzkx-editor-history';
|
|
614
|
+
|
|
615
|
+
// 撤销
|
|
616
|
+
undo(state, dispatch);
|
|
617
|
+
|
|
618
|
+
// 重做
|
|
619
|
+
redo(state, dispatch);
|
|
620
|
+
|
|
621
|
+
// 提交到历史
|
|
622
|
+
commit_to_history(state, dispatch);
|
|
623
|
+
|
|
624
|
+
// 关闭历史记录
|
|
625
|
+
const noHistory = history();
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
### 2.9 gzkx-editor-schema-basic 和 schema-list
|
|
629
|
+
|
|
630
|
+
预定义的 Schema 和辅助函数。
|
|
631
|
+
|
|
632
|
+
#### 2.9.1 基础 Schema
|
|
633
|
+
|
|
634
|
+
```typescript
|
|
635
|
+
import { schema } from 'gzkx-editor-schema-basic';
|
|
636
|
+
|
|
637
|
+
// schema.nodes 包含:
|
|
638
|
+
// - doc
|
|
639
|
+
// - paragraph
|
|
640
|
+
// - text
|
|
641
|
+
// - heading (1-6级)
|
|
642
|
+
// - blockquote
|
|
643
|
+
// - code_block
|
|
644
|
+
// - horizontal_rule
|
|
645
|
+
// - image
|
|
646
|
+
// - hard_break
|
|
647
|
+
|
|
648
|
+
// schema.marks 包含:
|
|
649
|
+
// - strong
|
|
650
|
+
// - em
|
|
651
|
+
// - code
|
|
652
|
+
// - link
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
#### 2.9.2 列表辅助
|
|
656
|
+
|
|
657
|
+
```typescript
|
|
658
|
+
import { addListNodes } from 'gzkx-editor-schema-list';
|
|
659
|
+
|
|
660
|
+
// 扩展 Schema 支持列表
|
|
661
|
+
const mySchema = new Schema({
|
|
662
|
+
nodes: addListNodes(schema.spec.nodes, "paragraph block*", "block"),
|
|
663
|
+
marks: schema.spec.marks
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
// 添加的节点:
|
|
667
|
+
// - bullet_list
|
|
668
|
+
// - ordered_list
|
|
669
|
+
// - list_item
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
## 3. 进阶主题
|
|
673
|
+
|
|
674
|
+
### 3.1 协同编辑
|
|
675
|
+
|
|
676
|
+
gzkx-editor-collab 包支持协同编辑:
|
|
677
|
+
|
|
678
|
+
```typescript
|
|
679
|
+
import { collab, receiveTransaction, sendableTransactions } from 'gzkx-editor-collab';
|
|
680
|
+
|
|
681
|
+
// 初始化协同编辑
|
|
682
|
+
const collabPlugin = collab({
|
|
683
|
+
version: initialVersion,
|
|
684
|
+
|
|
685
|
+
// 发送更新到服务器
|
|
686
|
+
send(tr) {
|
|
687
|
+
socket.send(JSON.stringify({
|
|
688
|
+
version: tr.meta.collab_version,
|
|
689
|
+
steps: tr.steps.map(s => s.toJSON())
|
|
690
|
+
}));
|
|
691
|
+
}
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
// 处理来自服务器的更新
|
|
695
|
+
function applyServerUpdate(version, steps) {
|
|
696
|
+
const tr = receiveTransaction(state, steps, version);
|
|
697
|
+
dispatch(tr);
|
|
698
|
+
}
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
### 3.2 Decoration (装饰器)
|
|
702
|
+
|
|
703
|
+
装饰器用于添加可视元素而不修改文档:
|
|
704
|
+
|
|
705
|
+
```typescript
|
|
706
|
+
import { Decoration, DecorationSet } from 'gzkx-editor-view';
|
|
707
|
+
|
|
708
|
+
// 创建装饰器集
|
|
709
|
+
const decorations = DecorationSet.create(doc, [
|
|
710
|
+
// widget 装饰器
|
|
711
|
+
Decoration.widget(10, createWidget(), { key: 'my-widget' }),
|
|
712
|
+
|
|
713
|
+
// inline 装饰器
|
|
714
|
+
Decoration.inline(0, 10, { class: 'highlight' }),
|
|
715
|
+
|
|
716
|
+
// 节点装饰器
|
|
717
|
+
Decoration.node(5, 15, { class: 'important' })
|
|
718
|
+
]);
|
|
719
|
+
|
|
720
|
+
// 插件中使用
|
|
721
|
+
const highlightPlugin = new Plugin({
|
|
722
|
+
state: {
|
|
723
|
+
init() {
|
|
724
|
+
return DecorationSet.empty;
|
|
725
|
+
},
|
|
726
|
+
apply(tr) {
|
|
727
|
+
return tr.getMeta('highlight') || DecorationSet.empty;
|
|
728
|
+
}
|
|
729
|
+
},
|
|
730
|
+
props: {
|
|
731
|
+
decorations(state) {
|
|
732
|
+
return this.getState(state);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
});
|
|
736
|
+
```
|
|
737
|
+
|
|
738
|
+
### 3.3 剪贴板处理
|
|
739
|
+
|
|
740
|
+
```typescript
|
|
741
|
+
// 自定义粘贴处理
|
|
742
|
+
view.setProps({
|
|
743
|
+
handlePaste(view, event, slice) {
|
|
744
|
+
// 获取剪贴板内容
|
|
745
|
+
const html = event.clipboardData.getData('text/html');
|
|
746
|
+
const text = event.clipboardData.getData('text/plain');
|
|
747
|
+
|
|
748
|
+
// 自定义处理
|
|
749
|
+
const processedSlice = processContent(html, text);
|
|
750
|
+
|
|
751
|
+
// 应用
|
|
752
|
+
dispatch(tr.replaceSelectionWith(processedSlice.content));
|
|
753
|
+
return true;
|
|
754
|
+
},
|
|
755
|
+
|
|
756
|
+
// 复制时转换
|
|
757
|
+
transformCopied(slice, view) {
|
|
758
|
+
// 在复制前转换内容
|
|
759
|
+
return transformSlice(slice, someTransformation);
|
|
760
|
+
},
|
|
761
|
+
|
|
762
|
+
// 粘贴时转换
|
|
763
|
+
transformPasted(slice, view) {
|
|
764
|
+
return transformSlice(slice, sanitizeTransformation);
|
|
765
|
+
}
|
|
766
|
+
});
|
|
767
|
+
```
|
|
768
|
+
|
|
769
|
+
### 3.4 自定义节点类型
|
|
770
|
+
|
|
771
|
+
```typescript
|
|
772
|
+
// 定义自定义节点
|
|
773
|
+
const mentionNode = {
|
|
774
|
+
group: 'inline',
|
|
775
|
+
inline: true,
|
|
776
|
+
atom: true,
|
|
777
|
+
attrs: { id: {}, label: {} },
|
|
778
|
+
parseDOM: [{
|
|
779
|
+
tag: 'span[data-mention]',
|
|
780
|
+
getAttrs(dom) {
|
|
781
|
+
return {
|
|
782
|
+
id: (dom as HTMLElement).getAttribute('data-id'),
|
|
783
|
+
label: (dom as HTMLElement).getAttribute('data-label')
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
}],
|
|
787
|
+
toDOM(node) {
|
|
788
|
+
return [
|
|
789
|
+
'span',
|
|
790
|
+
{
|
|
791
|
+
'class': 'mention',
|
|
792
|
+
'data-id': node.attrs.id,
|
|
793
|
+
'data-label': node.attrs.label
|
|
794
|
+
},
|
|
795
|
+
'@' + node.attrs.label
|
|
796
|
+
];
|
|
797
|
+
}
|
|
798
|
+
};
|
|
799
|
+
|
|
800
|
+
// 创建节点视图
|
|
801
|
+
const mentionNodeView = (node, view, getPos) => {
|
|
802
|
+
const dom = document.createElement('span');
|
|
803
|
+
dom.className = 'mention';
|
|
804
|
+
dom.textContent = '@' + node.attrs.label;
|
|
805
|
+
|
|
806
|
+
return {
|
|
807
|
+
dom,
|
|
808
|
+
update(node) {
|
|
809
|
+
if (node.type.name !== 'mention') return false;
|
|
810
|
+
dom.textContent = '@' + node.attrs.label;
|
|
811
|
+
return true;
|
|
812
|
+
},
|
|
813
|
+
ignoreMutation() { return true; }
|
|
814
|
+
};
|
|
815
|
+
};
|
|
816
|
+
|
|
817
|
+
// 使用
|
|
818
|
+
const mySchema = new Schema({
|
|
819
|
+
nodes: {
|
|
820
|
+
doc: { content: 'block+' },
|
|
821
|
+
paragraph: { content: 'inline*', group: 'block' },
|
|
822
|
+
text: { group: 'inline' },
|
|
823
|
+
mention: mentionNode
|
|
824
|
+
}
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
const view = new EditorView(editorElement, {
|
|
828
|
+
state,
|
|
829
|
+
nodeViews: {
|
|
830
|
+
mention: mentionNodeView
|
|
831
|
+
}
|
|
832
|
+
});
|
|
833
|
+
```
|
|
834
|
+
|
|
835
|
+
## 4. 性能优化
|
|
836
|
+
|
|
837
|
+
### 4.1 懒加载大型节点
|
|
838
|
+
|
|
839
|
+
```typescript
|
|
840
|
+
const lazyImageNodeView = (node, view, getPos) => {
|
|
841
|
+
let dom: HTMLElement;
|
|
842
|
+
|
|
843
|
+
return {
|
|
844
|
+
dom: dom = document.createElement('div'),
|
|
845
|
+
|
|
846
|
+
contentDOM: null,
|
|
847
|
+
|
|
848
|
+
update(node) {
|
|
849
|
+
if (node.type.name !== 'image') return false;
|
|
850
|
+
|
|
851
|
+
// 懒加载图片
|
|
852
|
+
const img = document.createElement('img');
|
|
853
|
+
img.src = node.attrs.src;
|
|
854
|
+
img.loading = 'lazy';
|
|
855
|
+
|
|
856
|
+
dom.innerHTML = '';
|
|
857
|
+
dom.appendChild(img);
|
|
858
|
+
return true;
|
|
859
|
+
},
|
|
860
|
+
|
|
861
|
+
ignoreMutation() { return true; }
|
|
862
|
+
};
|
|
863
|
+
};
|
|
864
|
+
```
|
|
865
|
+
|
|
866
|
+
### 4.2 避免不必要的重绘
|
|
867
|
+
|
|
868
|
+
```typescript
|
|
869
|
+
const optimizedPlugin = new Plugin({
|
|
870
|
+
props: {
|
|
871
|
+
decorations(state) {
|
|
872
|
+
// 缓存装饰器
|
|
873
|
+
const cached = this.getState(state);
|
|
874
|
+
if (cached) return cached;
|
|
875
|
+
|
|
876
|
+
// 创建新装饰器
|
|
877
|
+
const deco = computeExpensiveDecoration(state);
|
|
878
|
+
return deco;
|
|
879
|
+
}
|
|
880
|
+
},
|
|
881
|
+
|
|
882
|
+
state: {
|
|
883
|
+
init() { return null; },
|
|
884
|
+
apply(tr, prev) {
|
|
885
|
+
// 只有相关变更时才更新
|
|
886
|
+
if (tr.docChanged) {
|
|
887
|
+
return tr.getMeta('update-decorations');
|
|
888
|
+
}
|
|
889
|
+
return prev;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
});
|
|
893
|
+
```
|
|
894
|
+
|
|
895
|
+
### 4.3 事务批处理
|
|
896
|
+
|
|
897
|
+
```typescript
|
|
898
|
+
let pendingUpdates: Transaction[] = [];
|
|
899
|
+
|
|
900
|
+
function scheduleUpdate(tr: Transaction) {
|
|
901
|
+
pendingUpdates.push(tr);
|
|
902
|
+
|
|
903
|
+
if (pendingUpdates.length === 1) {
|
|
904
|
+
requestAnimationFrame(() => {
|
|
905
|
+
if (pendingUpdates.length === 1) {
|
|
906
|
+
// 单个事务
|
|
907
|
+
dispatch(pendingUpdates[0]);
|
|
908
|
+
} else {
|
|
909
|
+
// 合并多个事务
|
|
910
|
+
const tr = state.tr;
|
|
911
|
+
for (const pending of pendingUpdates) {
|
|
912
|
+
tr.merge(pending);
|
|
913
|
+
}
|
|
914
|
+
dispatch(tr);
|
|
915
|
+
}
|
|
916
|
+
pendingUpdates = [];
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
```
|
|
921
|
+
|
|
922
|
+
## 5. 调试技巧
|
|
923
|
+
|
|
924
|
+
### 5.1 查看状态
|
|
925
|
+
|
|
926
|
+
```typescript
|
|
927
|
+
// 打印当前文档结构
|
|
928
|
+
console.log(state.doc.toString());
|
|
929
|
+
|
|
930
|
+
// 打印选择
|
|
931
|
+
console.log(state.selection.from, state.selection.to);
|
|
932
|
+
|
|
933
|
+
// 打印所有事务步骤
|
|
934
|
+
tr.steps.forEach((step, i) => {
|
|
935
|
+
console.log(`Step ${i}:`, step.toJSON());
|
|
936
|
+
});
|
|
937
|
+
```
|
|
938
|
+
|
|
939
|
+
### 5.2 状态历史
|
|
940
|
+
|
|
941
|
+
```typescript
|
|
942
|
+
const debugPlugin = new Plugin({
|
|
943
|
+
view(editorView) {
|
|
944
|
+
return {
|
|
945
|
+
update(view, prevState) {
|
|
946
|
+
if (prevState.doc !== view.state.doc) {
|
|
947
|
+
console.log('Document changed');
|
|
948
|
+
console.log('Diff:', diffMatch.diff_main(
|
|
949
|
+
prevState.doc.textContent,
|
|
950
|
+
view.state.doc.textContent
|
|
951
|
+
));
|
|
952
|
+
}
|
|
953
|
+
if (!prevState.selection.eq(view.state.selection)) {
|
|
954
|
+
console.log('Selection changed:', view.state.selection.from, '->', view.state.selection.to);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
});
|
|
960
|
+
```
|
|
961
|
+
|
|
962
|
+
## 6. TypeScript 类型定义
|
|
963
|
+
|
|
964
|
+
### 6.1 自定义 Schema 类型
|
|
965
|
+
|
|
966
|
+
```typescript
|
|
967
|
+
import { Node as PMNode, Mark, Schema } from 'gzkx-editor-model';
|
|
968
|
+
|
|
969
|
+
interface MyNodeAttrs {
|
|
970
|
+
id?: string;
|
|
971
|
+
level?: number;
|
|
972
|
+
src?: string;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
interface MyMarkAttrs {
|
|
976
|
+
href?: string;
|
|
977
|
+
title?: string;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
type MySchema = Schema<{
|
|
981
|
+
doc: 'doc';
|
|
982
|
+
paragraph: 'paragraph';
|
|
983
|
+
heading: 'heading';
|
|
984
|
+
text: 'text';
|
|
985
|
+
image: 'image';
|
|
986
|
+
blockquote: 'blockquote';
|
|
987
|
+
list_item: 'list_item';
|
|
988
|
+
bullet_list: 'bullet_list';
|
|
989
|
+
ordered_list: 'ordered_list';
|
|
990
|
+
}, {
|
|
991
|
+
strong: 'strong';
|
|
992
|
+
em: 'em';
|
|
993
|
+
link: 'link';
|
|
994
|
+
}>;
|
|
995
|
+
|
|
996
|
+
// 使用类型
|
|
997
|
+
const node: PMNode<MySchema> = state.doc.nodeAt(0);
|
|
998
|
+
if (node.type.name === 'heading') {
|
|
999
|
+
const level: number = node.attrs.level;
|
|
1000
|
+
}
|
|
1001
|
+
```
|
|
1002
|
+
|
|
1003
|
+
## 7. 常见问题排查
|
|
1004
|
+
|
|
1005
|
+
| 问题 | 原因 | 解决方案 |
|
|
1006
|
+
|------|------|----------|
|
|
1007
|
+
| 输入无响应 | Schema 未定义 text 节点 | 确保 Schema 包含 text 节点 |
|
|
1008
|
+
| 粘贴内容丢失 | clipboardParser 未设置 | 设置正确的 clipboardParser |
|
|
1009
|
+
| 装饰器不更新 | 插件 state 未正确实现 | 检查 apply 方法返回值 |
|
|
1010
|
+
| 选择位置错误 | DOMObserver 不同步 | 检查是否正确更新状态 |
|
|
1011
|
+
| 撤销无法使用 | 历史插件未添加 | 添加 history() 插件 |
|