gzkx-editor 0.0.1 → 0.0.2
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/README.md +789 -80
- package/package.json +1 -1
- package/core/core-entry.mjs +0 -19
package/README.md
CHANGED
|
@@ -1,80 +1,789 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
1
|
+
# GZKX Editor 使用指南
|
|
2
|
+
|
|
3
|
+
## 简介
|
|
4
|
+
|
|
5
|
+
GZKX Editor 是一个基于 ProseMirror 的富文本编辑器,提供了模块化的编辑组件,支持协同编辑、自定义文档结构等功能。
|
|
6
|
+
|
|
7
|
+
## 安装
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install gzkx-editor-model gzkx-editor-state gzkx-editor-view gzkx-editor-commands gzkx-editor-schema-basic gzkx-editor-schema-list gzkx-editor-example-setup
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
或使用完整套装:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install gzkx-editor-example-setup
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## 核心模块
|
|
20
|
+
|
|
21
|
+
### 1. gzkx-editor-model
|
|
22
|
+
文档模型层,定义了文档的数据结构。
|
|
23
|
+
|
|
24
|
+
**主要导出:**
|
|
25
|
+
- `Node` - 文档节点
|
|
26
|
+
- `Fragment` - 文档片段
|
|
27
|
+
- `Slice` - 文档切片(用于跨编辑器传递内容)
|
|
28
|
+
- `Mark` - 标记(如加粗、斜体等)
|
|
29
|
+
- `Schema` - 文档模式,定义可用的节点和标记类型
|
|
30
|
+
- `DOMParser` - 将 DOM 解析为文档
|
|
31
|
+
- `DOMSerializer` - 将文档序列化为 DOM
|
|
32
|
+
|
|
33
|
+
### 2. gzkx-editor-state
|
|
34
|
+
编辑器状态管理层。
|
|
35
|
+
|
|
36
|
+
**主要导出:**
|
|
37
|
+
- `EditorState` - 编辑器状态
|
|
38
|
+
- `Transaction` - 状态变更事务
|
|
39
|
+
- `Plugin` - 编辑器插件
|
|
40
|
+
|
|
41
|
+
### 3. gzkx-editor-view
|
|
42
|
+
编辑器视图层,负责渲染和交互。
|
|
43
|
+
|
|
44
|
+
**主要导出:**
|
|
45
|
+
- `EditorView` - 编辑器视图组件
|
|
46
|
+
|
|
47
|
+
### 4. gzkx-editor-commands
|
|
48
|
+
编辑命令集。
|
|
49
|
+
|
|
50
|
+
**主要导出:**
|
|
51
|
+
- `toggleMark` - 切换标记
|
|
52
|
+
- `setBlockType` - 设置块类型
|
|
53
|
+
- `wrapIn` - 包裹节点
|
|
54
|
+
- `baseKeymap` - 基础快捷键绑定
|
|
55
|
+
|
|
56
|
+
### 5. gzkx-editor-schema-basic
|
|
57
|
+
基础文档模式,提供常用节点和标记。
|
|
58
|
+
|
|
59
|
+
**包含:**
|
|
60
|
+
- 段落 (paragraph)
|
|
61
|
+
- 标题 (heading, 1-6级)
|
|
62
|
+
- 代码块 (code_block)
|
|
63
|
+
- 引用 (blockquote)
|
|
64
|
+
- 水平线 (horizontal_rule)
|
|
65
|
+
- 图片 (image)
|
|
66
|
+
- 链接 (link)
|
|
67
|
+
- 加粗 (strong)
|
|
68
|
+
- 斜体 (em)
|
|
69
|
+
- 代码 (code)
|
|
70
|
+
|
|
71
|
+
### 6. gzkx-editor-schema-list
|
|
72
|
+
列表相关节点和命令。
|
|
73
|
+
|
|
74
|
+
**包含:**
|
|
75
|
+
- 有序列表 (ordered_list)
|
|
76
|
+
- 无序列表 (bullet_list)
|
|
77
|
+
- 列表项 (list_item)
|
|
78
|
+
|
|
79
|
+
## 快速开始
|
|
80
|
+
|
|
81
|
+
### 方式一:使用 exampleSetup(推荐)
|
|
82
|
+
|
|
83
|
+
```javascript
|
|
84
|
+
import { Schema, DOMParser } from 'gzkx-editor-model';
|
|
85
|
+
import { EditorView } from 'gzkx-editor-view';
|
|
86
|
+
import { EditorState } from 'gzkx-editor-state';
|
|
87
|
+
import { schema } from 'gzkx-editor-schema-basic';
|
|
88
|
+
import { addListNodes } from 'gzkx-editor-schema-list';
|
|
89
|
+
import { exampleSetup } from 'gzkx-editor-example-setup';
|
|
90
|
+
|
|
91
|
+
// 创建带列表支持的模式
|
|
92
|
+
const demoSchema = new Schema({
|
|
93
|
+
nodes: addListNodes(schema.spec.nodes, "paragraph block*", "block"),
|
|
94
|
+
marks: schema.spec.marks
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// 创建初始状态
|
|
98
|
+
const state = EditorState.create({
|
|
99
|
+
doc: DOMParser.fromSchema(demoSchema).parse(document.querySelector("#content")),
|
|
100
|
+
plugins: exampleSetup({ schema: demoSchema })
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// 创建编辑器视图
|
|
104
|
+
const view = new EditorView(document.querySelector(".editor"), {
|
|
105
|
+
state
|
|
106
|
+
});
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### 方式二:手动配置
|
|
110
|
+
|
|
111
|
+
```javascript
|
|
112
|
+
import { keymap } from 'gzkx-editor-keymap';
|
|
113
|
+
import { history } from 'gzkx-editor-history';
|
|
114
|
+
import { dropCursor } from 'gzkx-editor-dropcursor';
|
|
115
|
+
import { gapCursor } from 'gzkx-editor-gapcursor';
|
|
116
|
+
import { baseKeymap, toggleMark, setBlockType } from 'gzkx-editor-commands';
|
|
117
|
+
import { buildInputRules } from 'gzkx-editor-inputrules';
|
|
118
|
+
import { EditorView } from 'gzkx-editor-view';
|
|
119
|
+
import { EditorState } from 'gzkx-editor-state';
|
|
120
|
+
import { Schema, DOMParser } from 'gzkx-editor-model';
|
|
121
|
+
|
|
122
|
+
const mySchema = new Schema({
|
|
123
|
+
nodes: {
|
|
124
|
+
doc: { content: 'block+' },
|
|
125
|
+
paragraph: { group: 'block', parseDOM: [{ tag: 'p' }] },
|
|
126
|
+
text: { group: 'inline' },
|
|
127
|
+
strong: { parseDOM: [{ tag: 'strong' }, { tag: 'b' }] },
|
|
128
|
+
em: { parseDOM: [{ tag: 'em' }, { tag: 'i' }] }
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const state = EditorState.create({
|
|
133
|
+
doc: DOMParser.fromSchema(mySchema).parse(document.getElementById('content')),
|
|
134
|
+
plugins: [
|
|
135
|
+
buildInputRules(mySchema),
|
|
136
|
+
keymap(buildKeymap(mySchema)),
|
|
137
|
+
keymap(baseKeymap),
|
|
138
|
+
dropCursor(),
|
|
139
|
+
gapCursor(),
|
|
140
|
+
history()
|
|
141
|
+
]
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const view = new EditorView(document.querySelector('.editor'), { state });
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## 常用功能
|
|
148
|
+
|
|
149
|
+
### 获取编辑器内容
|
|
150
|
+
|
|
151
|
+
```javascript
|
|
152
|
+
// 获取 JSON 格式的文档
|
|
153
|
+
const doc = view.state.doc.toJSON();
|
|
154
|
+
|
|
155
|
+
// 获取 HTML 字符串
|
|
156
|
+
import { DOMSerializer } from 'gzkx-editor-model';
|
|
157
|
+
const div = document.createElement('div');
|
|
158
|
+
const fragment = DOMSerializer.fromSchema(view.state.schema).serializeFragment(view.state.doc.content);
|
|
159
|
+
div.appendChild(fragment);
|
|
160
|
+
const html = div.innerHTML;
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### 修改编辑器内容
|
|
164
|
+
|
|
165
|
+
```javascript
|
|
166
|
+
// 通过事务修改
|
|
167
|
+
const tr = view.state.tr;
|
|
168
|
+
tr.insertText('Hello, GZKX Editor!', 10);
|
|
169
|
+
view.dispatch(tr);
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### 监听变化
|
|
173
|
+
|
|
174
|
+
```javascript
|
|
175
|
+
const view = new EditorView(document.querySelector('.editor'), {
|
|
176
|
+
state: initialState,
|
|
177
|
+
dispatchTransaction(transaction) {
|
|
178
|
+
const newState = this.state.apply(transaction);
|
|
179
|
+
this.updateState(newState);
|
|
180
|
+
|
|
181
|
+
// 检测文档变化
|
|
182
|
+
if (transaction.docChanged) {
|
|
183
|
+
console.log('文档已更新');
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### 快捷键
|
|
190
|
+
|
|
191
|
+
| 快捷键 | 功能 |
|
|
192
|
+
|--------|------|
|
|
193
|
+
| Ctrl+B / Cmd+B | 加粗 |
|
|
194
|
+
| Ctrl+I / Cmd+I | 斜体 |
|
|
195
|
+
| Ctrl+Shift+X | 行内代码 |
|
|
196
|
+
| Ctrl+Z | 撤销 |
|
|
197
|
+
| Ctrl+Shift+Z | 重做 |
|
|
198
|
+
| Tab | 缩进 |
|
|
199
|
+
| Shift+Tab | 取消缩进 |
|
|
200
|
+
|
|
201
|
+
### 自定义菜单
|
|
202
|
+
|
|
203
|
+
```javascript
|
|
204
|
+
import { buildMenuItems } from 'gzkx-editor-example-setup';
|
|
205
|
+
|
|
206
|
+
const menuItems = buildMenuItems(schema);
|
|
207
|
+
|
|
208
|
+
// 自定义菜单内容
|
|
209
|
+
const plugins = exampleSetup({
|
|
210
|
+
schema,
|
|
211
|
+
menuContent: menuItems.fullMenu
|
|
212
|
+
});
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### 自定义样式
|
|
216
|
+
|
|
217
|
+
引入 CSS:
|
|
218
|
+
|
|
219
|
+
```html
|
|
220
|
+
<link rel="stylesheet" href="gzkx-editor-view/style/prosemirror.css">
|
|
221
|
+
<link rel="stylesheet" href="gzkx-editor-menu/style/menu.css">
|
|
222
|
+
<link rel="stylesheet" href="gzkx-editor-gapcursor/style/gapcursor.css">
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## React 使用方法
|
|
226
|
+
|
|
227
|
+
GZKX Editor 可以通过 React 封装组件来使用,下面介绍两种封装方式。
|
|
228
|
+
|
|
229
|
+
### 方式一:使用 ref 和 useEffect(类组件风格)
|
|
230
|
+
|
|
231
|
+
```jsx
|
|
232
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
233
|
+
import { Schema, DOMParser, DOMSerializer } from 'gzkx-editor-model';
|
|
234
|
+
import { EditorView } from 'gzkx-editor-view';
|
|
235
|
+
import { EditorState } from 'gzkx-editor-state';
|
|
236
|
+
import { schema } from 'gzkx-editor-schema-basic';
|
|
237
|
+
import { addListNodes } from 'gzkx-editor-schema-list';
|
|
238
|
+
import { exampleSetup } from 'gzkx-editor-example-setup';
|
|
239
|
+
|
|
240
|
+
import 'gzkx-editor-view/style/prosemirror.css';
|
|
241
|
+
import 'gzkx-editor-menu/style/menu.css';
|
|
242
|
+
|
|
243
|
+
const Editor = ({ initialContent = '', onChange }) => {
|
|
244
|
+
const editorRef = useRef(null);
|
|
245
|
+
const viewRef = useRef(null);
|
|
246
|
+
|
|
247
|
+
useEffect(() => {
|
|
248
|
+
if (!editorRef.current) return;
|
|
249
|
+
|
|
250
|
+
// 创建 Schema
|
|
251
|
+
const mySchema = new Schema({
|
|
252
|
+
nodes: addListNodes(schema.spec.nodes, "paragraph block*", "block"),
|
|
253
|
+
marks: schema.spec.marks
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// 解析初始内容
|
|
257
|
+
const content = document.createElement('div');
|
|
258
|
+
content.innerHTML = initialContent || '<p></p>';
|
|
259
|
+
|
|
260
|
+
// 创建初始状态
|
|
261
|
+
const state = EditorState.create({
|
|
262
|
+
doc: DOMParser.fromSchema(mySchema).parse(content),
|
|
263
|
+
plugins: exampleSetup({ schema: mySchema })
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// 创建编辑器视图
|
|
267
|
+
const view = new EditorView(editorRef.current, {
|
|
268
|
+
state,
|
|
269
|
+
dispatchTransaction(transaction) {
|
|
270
|
+
const newState = view.state.apply(transaction);
|
|
271
|
+
view.updateState(newState);
|
|
272
|
+
|
|
273
|
+
// 触发内容变化回调
|
|
274
|
+
if (transaction.docChanged && onChange) {
|
|
275
|
+
const serializer = DOMSerializer.fromSchema(mySchema);
|
|
276
|
+
const div = document.createElement('div');
|
|
277
|
+
serializer.serializeFragment(newState.doc.content, { document: div });
|
|
278
|
+
onChange(div.innerHTML, newState.doc.toJSON());
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
viewRef.current = view;
|
|
284
|
+
|
|
285
|
+
// 清理函数
|
|
286
|
+
return () => {
|
|
287
|
+
view.destroy();
|
|
288
|
+
};
|
|
289
|
+
}, []);
|
|
290
|
+
|
|
291
|
+
return <div ref={editorRef} className="gzkx-editor" />;
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
// 使用示例
|
|
295
|
+
const App = () => {
|
|
296
|
+
const handleChange = (html, json) => {
|
|
297
|
+
console.log('HTML:', html);
|
|
298
|
+
console.log('JSON:', json);
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
return (
|
|
302
|
+
<div>
|
|
303
|
+
<h1>我的编辑器</h1>
|
|
304
|
+
<Editor
|
|
305
|
+
initialContent="<p>初始内容</p>"
|
|
306
|
+
onChange={handleChange}
|
|
307
|
+
/>
|
|
308
|
+
</div>
|
|
309
|
+
);
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
export default App;
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### 方式二:封装为 React 组件(Hooks 版本)
|
|
316
|
+
|
|
317
|
+
```jsx
|
|
318
|
+
import React, { useEffect, useRef, useCallback, useMemo } from 'react';
|
|
319
|
+
import { Schema, DOMParser, DOMSerializer } from 'gzkx-editor-model';
|
|
320
|
+
import { EditorView } from 'gzkx-editor-view';
|
|
321
|
+
import { EditorState } from 'gzkx-editor-state';
|
|
322
|
+
import { schema } from 'gzkx-editor-schema-basic';
|
|
323
|
+
import { addListNodes } from 'gzkx-editor-schema-list';
|
|
324
|
+
import { exampleSetup } from 'gzkx-editor-example-setup';
|
|
325
|
+
|
|
326
|
+
import 'gzkx-editor-view/style/prosemirror.css';
|
|
327
|
+
import 'gzkx-editor-menu/style/menu.css';
|
|
328
|
+
|
|
329
|
+
const GZKXEditor = ({
|
|
330
|
+
value = '',
|
|
331
|
+
onChange,
|
|
332
|
+
placeholder = '请输入内容...',
|
|
333
|
+
readOnly = false,
|
|
334
|
+
className = ''
|
|
335
|
+
}) => {
|
|
336
|
+
const editorRef = useRef(null);
|
|
337
|
+
const viewRef = useRef(null);
|
|
338
|
+
|
|
339
|
+
// 创建 Schema
|
|
340
|
+
const editorSchema = useMemo(() => {
|
|
341
|
+
return new Schema({
|
|
342
|
+
nodes: addListNodes(schema.spec.nodes, "paragraph block*", "block"),
|
|
343
|
+
marks: schema.spec.marks
|
|
344
|
+
});
|
|
345
|
+
}, []);
|
|
346
|
+
|
|
347
|
+
// 获取 HTML 内容
|
|
348
|
+
const getContentHTML = useCallback((doc) => {
|
|
349
|
+
const div = document.createElement('div');
|
|
350
|
+
const serializer = DOMSerializer.fromSchema(editorSchema);
|
|
351
|
+
serializer.serializeFragment(doc.content, { document: div });
|
|
352
|
+
return div.innerHTML;
|
|
353
|
+
}, [editorSchema]);
|
|
354
|
+
|
|
355
|
+
// 获取 JSON 内容
|
|
356
|
+
const getContentJSON = useCallback((doc) => {
|
|
357
|
+
return doc.toJSON();
|
|
358
|
+
}, []);
|
|
359
|
+
|
|
360
|
+
useEffect(() => {
|
|
361
|
+
if (!editorRef.current) return;
|
|
362
|
+
|
|
363
|
+
// 解析初始内容
|
|
364
|
+
const content = document.createElement('div');
|
|
365
|
+
content.innerHTML = value || '<p></p>';
|
|
366
|
+
|
|
367
|
+
// 创建初始状态
|
|
368
|
+
const state = EditorState.create({
|
|
369
|
+
doc: DOMParser.fromSchema(editorSchema).parse(content),
|
|
370
|
+
plugins: exampleSetup({ schema: editorSchema })
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
// 创建编辑器视图
|
|
374
|
+
const view = new EditorView(editorRef.current, {
|
|
375
|
+
state,
|
|
376
|
+
editable() {
|
|
377
|
+
return !readOnly;
|
|
378
|
+
},
|
|
379
|
+
dispatchTransaction(transaction) {
|
|
380
|
+
const newState = view.state.apply(transaction);
|
|
381
|
+
view.updateState(newState);
|
|
382
|
+
|
|
383
|
+
if (transaction.docChanged && onChange) {
|
|
384
|
+
onChange({
|
|
385
|
+
html: getContentHTML(newState.doc),
|
|
386
|
+
json: getContentJSON(newState.doc)
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
viewRef.current = view;
|
|
393
|
+
|
|
394
|
+
return () => {
|
|
395
|
+
view.destroy();
|
|
396
|
+
};
|
|
397
|
+
}, [editorSchema]);
|
|
398
|
+
|
|
399
|
+
// 更新内容(外部控制)
|
|
400
|
+
useEffect(() => {
|
|
401
|
+
if (!viewRef.current) return;
|
|
402
|
+
|
|
403
|
+
const currentHTML = getContentHTML(viewRef.current.state.doc);
|
|
404
|
+
if (value !== currentHTML && value !== undefined) {
|
|
405
|
+
const content = document.createElement('div');
|
|
406
|
+
content.innerHTML = value;
|
|
407
|
+
const newDoc = DOMParser.fromSchema(editorSchema).parse(content);
|
|
408
|
+
const tr = viewRef.current.state.tr.replaceWith(0, viewRef.current.state.doc.content.size, newDoc.content);
|
|
409
|
+
viewRef.current.dispatch(tr);
|
|
410
|
+
}
|
|
411
|
+
}, [value, editorSchema, getContentHTML]);
|
|
412
|
+
|
|
413
|
+
return (
|
|
414
|
+
<div
|
|
415
|
+
ref={editorRef}
|
|
416
|
+
className={`gzkx-editor ${className}`}
|
|
417
|
+
data-placeholder={placeholder}
|
|
418
|
+
/>
|
|
419
|
+
);
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
export default GZKXEditor;
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
### 使用示例
|
|
426
|
+
|
|
427
|
+
```jsx
|
|
428
|
+
import React, { useState } from 'react';
|
|
429
|
+
import GZKXEditor from './GZKXEditor';
|
|
430
|
+
|
|
431
|
+
const App = () => {
|
|
432
|
+
const [content, setContent] = useState({
|
|
433
|
+
html: '<p>初始内容</p>',
|
|
434
|
+
json: null
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
return (
|
|
438
|
+
<div className="app">
|
|
439
|
+
<h1>GZKX Editor in React</h1>
|
|
440
|
+
|
|
441
|
+
<GZKXEditor
|
|
442
|
+
value={content.html}
|
|
443
|
+
onChange={setContent}
|
|
444
|
+
placeholder="输入文章内容..."
|
|
445
|
+
/>
|
|
446
|
+
|
|
447
|
+
<div className="preview">
|
|
448
|
+
<h2>预览</h2>
|
|
449
|
+
<div dangerouslySetInnerHTML={{ __html: content.html }} />
|
|
450
|
+
</div>
|
|
451
|
+
|
|
452
|
+
<div className="json-view">
|
|
453
|
+
<h2>JSON 数据</h2>
|
|
454
|
+
<pre>{JSON.stringify(content.json, null, 2)}</pre>
|
|
455
|
+
</div>
|
|
456
|
+
</div>
|
|
457
|
+
);
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
export default App;
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
### 样式调整
|
|
464
|
+
|
|
465
|
+
添加自定义样式:
|
|
466
|
+
|
|
467
|
+
```css
|
|
468
|
+
/* Editor.css */
|
|
469
|
+
.gzkx-editor {
|
|
470
|
+
border: 1px solid #d1d5db;
|
|
471
|
+
border-radius: 8px;
|
|
472
|
+
min-height: 300px;
|
|
473
|
+
padding: 16px;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
.gzkx-editor:focus {
|
|
477
|
+
outline: none;
|
|
478
|
+
border-color: #3b82f6;
|
|
479
|
+
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/* 占位符样式 */
|
|
483
|
+
.gzkx-editor:empty::before {
|
|
484
|
+
content: attr(data-placeholder);
|
|
485
|
+
color: #9ca3af;
|
|
486
|
+
pointer-events: none;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/* ProseMirror 样式覆盖 */
|
|
490
|
+
.ProseMirror {
|
|
491
|
+
outline: none;
|
|
492
|
+
min-height: 200px;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
.ProseMirror p.is-empty::before {
|
|
496
|
+
content: attr(data-placeholder);
|
|
497
|
+
color: #9ca3af;
|
|
498
|
+
float: left;
|
|
499
|
+
height: 0;
|
|
500
|
+
pointer-events: none;
|
|
501
|
+
}
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
### 完整 React 项目示例
|
|
505
|
+
|
|
506
|
+
```jsx
|
|
507
|
+
// EditorComponent.jsx
|
|
508
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
509
|
+
import {
|
|
510
|
+
Schema,
|
|
511
|
+
DOMParser,
|
|
512
|
+
DOMSerializer
|
|
513
|
+
} from 'gzkx-editor-model';
|
|
514
|
+
import { EditorView } from 'gzkx-editor-view';
|
|
515
|
+
import { EditorState } from 'gzkx-editor-state';
|
|
516
|
+
import { schema } from 'gzkx-editor-schema-basic';
|
|
517
|
+
import { addListNodes } from 'gzkx-editor-schema-list';
|
|
518
|
+
import { exampleSetup } from 'gzkx-editor-example-setup';
|
|
519
|
+
|
|
520
|
+
import 'gzkx-editor-view/style/prosemirror.css';
|
|
521
|
+
import 'gzkx-editor-menu/style/menu.css';
|
|
522
|
+
import './EditorComponent.css';
|
|
523
|
+
|
|
524
|
+
const EditorComponent = ({
|
|
525
|
+
initialValue = '<p></p>',
|
|
526
|
+
onChange,
|
|
527
|
+
editable = true
|
|
528
|
+
}) => {
|
|
529
|
+
const containerRef = useRef(null);
|
|
530
|
+
const viewRef = useRef(null);
|
|
531
|
+
const schemaRef = useRef(null);
|
|
532
|
+
|
|
533
|
+
// 初始化 Schema
|
|
534
|
+
if (!schemaRef.current) {
|
|
535
|
+
schemaRef.current = new Schema({
|
|
536
|
+
nodes: addListNodes(schema.spec.nodes, "paragraph block*", "block"),
|
|
537
|
+
marks: schema.spec.marks
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
useEffect(() => {
|
|
542
|
+
if (!containerRef.current) return;
|
|
543
|
+
|
|
544
|
+
// 解析初始内容
|
|
545
|
+
const tempDiv = document.createElement('div');
|
|
546
|
+
tempDiv.innerHTML = initialValue;
|
|
547
|
+
|
|
548
|
+
// 创建状态
|
|
549
|
+
const state = EditorState.create({
|
|
550
|
+
doc: DOMParser.fromSchema(schemaRef.current).parse(tempDiv),
|
|
551
|
+
plugins: exampleSetup({ schema: schemaRef.current })
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
// 创建视图
|
|
555
|
+
viewRef.current = new EditorView(containerRef.current, {
|
|
556
|
+
state,
|
|
557
|
+
editable() {
|
|
558
|
+
return editable;
|
|
559
|
+
},
|
|
560
|
+
dispatchTransaction(transaction) {
|
|
561
|
+
const newState = viewRef.current.state.apply(transaction);
|
|
562
|
+
viewRef.current.updateState(newState);
|
|
563
|
+
|
|
564
|
+
if (transaction.docChanged && onChange) {
|
|
565
|
+
const div = document.createElement('div');
|
|
566
|
+
const serializer = DOMSerializer.fromSchema(schemaRef.current);
|
|
567
|
+
serializer.serializeFragment(newState.doc.content, {
|
|
568
|
+
document: div
|
|
569
|
+
});
|
|
570
|
+
onChange({
|
|
571
|
+
html: div.innerHTML,
|
|
572
|
+
json: newState.doc.toJSON()
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
return () => {
|
|
579
|
+
if (viewRef.current) {
|
|
580
|
+
viewRef.current.destroy();
|
|
581
|
+
}
|
|
582
|
+
};
|
|
583
|
+
}, [initialValue, editable]);
|
|
584
|
+
|
|
585
|
+
return <div ref={containerRef} className="gzkx-editor-container" />;
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
export default EditorComponent;
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
### Next.js中使用
|
|
592
|
+
|
|
593
|
+
在 Next.js 中使用时,需要注意动态导入避免 SSR 问题:
|
|
594
|
+
|
|
595
|
+
```jsx
|
|
596
|
+
// components/Editor.jsx
|
|
597
|
+
import dynamic from 'next/dynamic';
|
|
598
|
+
|
|
599
|
+
const EditorComponent = dynamic(() => import('./EditorComponent'), {
|
|
600
|
+
ssr: false,
|
|
601
|
+
loading: () => <div>加载中...</div>
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
export default function Editor({ initialValue, onChange }) {
|
|
605
|
+
return (
|
|
606
|
+
<EditorComponent
|
|
607
|
+
initialValue={initialValue}
|
|
608
|
+
onChange={onChange}
|
|
609
|
+
/>
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
### 在 TypeScript 中使用
|
|
615
|
+
|
|
616
|
+
```tsx
|
|
617
|
+
// types/editor.ts
|
|
618
|
+
import { Node as ProseMirrorNode, Schema } from 'gzkx-editor-model';
|
|
619
|
+
|
|
620
|
+
export interface EditorContent {
|
|
621
|
+
html: string;
|
|
622
|
+
json: ProseMirrorNode;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
export interface EditorProps {
|
|
626
|
+
value?: string;
|
|
627
|
+
onChange?: (content: EditorContent) => void;
|
|
628
|
+
placeholder?: string;
|
|
629
|
+
readOnly?: boolean;
|
|
630
|
+
className?: string;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// Editor.tsx
|
|
634
|
+
import React, { useEffect, useRef } from 'react';
|
|
635
|
+
import { Schema, DOMParser, DOMSerializer, Node } from 'gzkx-editor-model';
|
|
636
|
+
import { EditorView } from 'gzkx-editor-view';
|
|
637
|
+
import { EditorState } from 'gzkx-editor-state';
|
|
638
|
+
import { schema } from 'gzkx-editor-schema-basic';
|
|
639
|
+
import { addListNodes } from 'gzkx-editor-schema-list';
|
|
640
|
+
import { exampleSetup } from 'gzkx-editor-example-setup';
|
|
641
|
+
|
|
642
|
+
import { EditorProps, EditorContent } from '../types/editor';
|
|
643
|
+
|
|
644
|
+
export const GZKXEditor: React.FC<EditorProps> = ({
|
|
645
|
+
value = '<p></p>',
|
|
646
|
+
onChange,
|
|
647
|
+
placeholder = '请输入内容...',
|
|
648
|
+
readOnly = false,
|
|
649
|
+
className = ''
|
|
650
|
+
}) => {
|
|
651
|
+
const editorRef = useRef<HTMLDivElement>(null);
|
|
652
|
+
const viewRef = useRef<EditorView | null>(null);
|
|
653
|
+
|
|
654
|
+
const editorSchema = React.useMemo(() => {
|
|
655
|
+
return new Schema({
|
|
656
|
+
nodes: addListNodes(schema.spec.nodes, "paragraph block*", "block"),
|
|
657
|
+
marks: schema.spec.marks
|
|
658
|
+
});
|
|
659
|
+
}, []);
|
|
660
|
+
|
|
661
|
+
useEffect(() => {
|
|
662
|
+
if (!editorRef.current) return;
|
|
663
|
+
|
|
664
|
+
const content = document.createElement('div');
|
|
665
|
+
content.innerHTML = value;
|
|
666
|
+
|
|
667
|
+
const state = EditorState.create({
|
|
668
|
+
doc: DOMParser.fromSchema(editorSchema).parse(content),
|
|
669
|
+
plugins: exampleSetup({ schema: editorSchema })
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
viewRef.current = new EditorView(editorRef.current, {
|
|
673
|
+
state,
|
|
674
|
+
editable() {
|
|
675
|
+
return !readOnly;
|
|
676
|
+
},
|
|
677
|
+
dispatchTransaction(transaction) {
|
|
678
|
+
const newState = viewRef.current!.state.apply(transaction);
|
|
679
|
+
viewRef.current!.updateState(newState);
|
|
680
|
+
|
|
681
|
+
if (transaction.docChanged && onChange) {
|
|
682
|
+
const div = document.createElement('div');
|
|
683
|
+
const serializer = DOMSerializer.fromSchema(editorSchema);
|
|
684
|
+
serializer.serializeFragment(newState.doc.content, {
|
|
685
|
+
document: div
|
|
686
|
+
});
|
|
687
|
+
onChange({
|
|
688
|
+
html: div.innerHTML,
|
|
689
|
+
json: newState.doc.toJSON()
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
return () => {
|
|
696
|
+
viewRef.current?.destroy();
|
|
697
|
+
};
|
|
698
|
+
}, [editorSchema, value, readOnly]);
|
|
699
|
+
|
|
700
|
+
return (
|
|
701
|
+
<div
|
|
702
|
+
ref={editorRef}
|
|
703
|
+
className={`gzkx-editor ${className}`}
|
|
704
|
+
data-placeholder={placeholder}
|
|
705
|
+
/>
|
|
706
|
+
);
|
|
707
|
+
};
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
## 完整示例(原生 JS)
|
|
711
|
+
|
|
712
|
+
```html
|
|
713
|
+
<!DOCTYPE html>
|
|
714
|
+
<html lang="zh-CN">
|
|
715
|
+
<head>
|
|
716
|
+
<meta charset="UTF-8">
|
|
717
|
+
<title>GZKX Editor 示例</title>
|
|
718
|
+
<link rel="stylesheet" href="node_modules/gzkx-editor-view/style/prosemirror.css">
|
|
719
|
+
<link rel="stylesheet" href="node_modules/gzkx-editor-menu/style/menu.css">
|
|
720
|
+
<style>
|
|
721
|
+
.editor { border: 1px solid #ccc; min-height: 300px; }
|
|
722
|
+
</style>
|
|
723
|
+
</head>
|
|
724
|
+
<body>
|
|
725
|
+
<div id="editor" class="editor"></div>
|
|
726
|
+
|
|
727
|
+
<script type="module">
|
|
728
|
+
import { Schema, DOMParser } from 'gzkx-editor-model';
|
|
729
|
+
import { EditorView } from 'gzkx-editor-view';
|
|
730
|
+
import { EditorState } from 'gzkx-editor-state';
|
|
731
|
+
import { schema } from 'gzkx-editor-schema-basic';
|
|
732
|
+
import { addListNodes } from 'gzkx-editor-schema-list';
|
|
733
|
+
import { exampleSetup } from 'gzkx-editor-example-setup';
|
|
734
|
+
|
|
735
|
+
const mySchema = new Schema({
|
|
736
|
+
nodes: addListNodes(schema.spec.nodes, "paragraph block*", "block"),
|
|
737
|
+
marks: schema.spec.marks
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
const content = document.createElement('div');
|
|
741
|
+
content.innerHTML = '<p>欢迎使用 GZKX Editor!</p>';
|
|
742
|
+
|
|
743
|
+
const state = EditorState.create({
|
|
744
|
+
doc: DOMParser.fromSchema(mySchema).parse(content),
|
|
745
|
+
plugins: exampleSetup({ schema: mySchema })
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
const view = new EditorView(document.getElementById('editor'), { state });
|
|
749
|
+
|
|
750
|
+
// 获取内容
|
|
751
|
+
function getContent() {
|
|
752
|
+
const div = document.createElement('div');
|
|
753
|
+
const serializer = DOMSerializer.fromSchema(mySchema);
|
|
754
|
+
serializer.serializeFragment(view.state.doc.content, { document: div });
|
|
755
|
+
return div.innerHTML;
|
|
756
|
+
}
|
|
757
|
+
</script>
|
|
758
|
+
</body>
|
|
759
|
+
</html>
|
|
760
|
+
```
|
|
761
|
+
|
|
762
|
+
## 模块列表
|
|
763
|
+
|
|
764
|
+
| 包名 | 说明 |
|
|
765
|
+
|------|------|
|
|
766
|
+
| gzkx-editor-model | 文档模型 |
|
|
767
|
+
| gzkx-editor-state | 状态管理 |
|
|
768
|
+
| gzkx-editor-view | 视图渲染 |
|
|
769
|
+
| gzkx-editor-commands | 编辑命令 |
|
|
770
|
+
| gzkx-editor-keymap | 快捷键绑定 |
|
|
771
|
+
| gzkx-editor-inputrules | 输入规则 |
|
|
772
|
+
| gzkx-editor-history | 历史记录(撤销/重做) |
|
|
773
|
+
| gzkx-editor-collab | 协同编辑 |
|
|
774
|
+
| gzkx-editor-gapcursor | 空隙光标 |
|
|
775
|
+
| gzkx-editor-schema-basic | 基础模式 |
|
|
776
|
+
| gzkx-editor-schema-list | 列表模式 |
|
|
777
|
+
| gzkx-editor-menu | 菜单组件 |
|
|
778
|
+
| gzkx-editor-example-setup | 快速设置 |
|
|
779
|
+
| gzkx-editor-markdown | Markdown 转换 |
|
|
780
|
+
| gzkx-editor-dropcursor | 拖拽光标 |
|
|
781
|
+
| gzkx-editor-search | 搜索功能 |
|
|
782
|
+
| gzkx-editor-changeset | 变更集 |
|
|
783
|
+
|
|
784
|
+
## 注意事项
|
|
785
|
+
|
|
786
|
+
1. **事务处理**:每次内容修改都需要通过 `dispatch` 方法提交事务
|
|
787
|
+
2. **Schema 定义**:确保所有节点和标记在 Schema 中正确定义
|
|
788
|
+
3. **CSS 样式**:需要引入对应的 CSS 文件才能正常显示
|
|
789
|
+
4. **依赖顺序**:部分模块依赖其他模块,安装时注意包之间的依赖关系
|
package/package.json
CHANGED
package/core/core-entry.mjs
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
export * from "./model/src/index.ts";
|
|
2
|
-
export * from "./transform/src/index.ts";
|
|
3
|
-
export * from "./state/src/index.ts";
|
|
4
|
-
export * from "./view/src/index.ts";
|
|
5
|
-
export * from "./commands/src/commands.ts";
|
|
6
|
-
export * from "./keymap/src/keymap.ts";
|
|
7
|
-
export * from "./inputrules/src/index.ts";
|
|
8
|
-
export * from "./history/src/history.ts";
|
|
9
|
-
export * from "./dropcursor/src/dropcursor.ts";
|
|
10
|
-
export * from "./gapcursor/src/index.ts";
|
|
11
|
-
export * from "./schema-basic/src/schema-basic.ts";
|
|
12
|
-
export * from "./schema-list/src/schema-list.ts";
|
|
13
|
-
export * from "./menu/src/index.ts";
|
|
14
|
-
export * from "./collab/src/collab.ts";
|
|
15
|
-
export * from "./example-setup/src/index.ts";
|
|
16
|
-
export * from "./markdown/src/index.ts";
|
|
17
|
-
export * from "./search/src/search.ts";
|
|
18
|
-
export * from "./changeset/src/changeset.ts";
|
|
19
|
-
export * from "./test-builder/src/index.ts";
|