@haklex/rich-editor 0.0.33 → 0.0.34
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 +256 -85
- package/dist/{RichEditor-CSw4Bj02.js → RichEditor-C6rSAXFC.js} +499 -9
- package/dist/components/RichEditor.d.ts +1 -1
- package/dist/components/RichEditor.d.ts.map +1 -1
- package/dist/context/ImageUploadContext.d.ts +8 -0
- package/dist/context/ImageUploadContext.d.ts.map +1 -0
- package/dist/editor.mjs +2 -2
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +32 -26
- package/dist/nodes/ImageNode.d.ts +13 -0
- package/dist/nodes/ImageNode.d.ts.map +1 -1
- package/dist/plugins/ImageUploadPlugin.d.ts +2 -1
- package/dist/plugins/ImageUploadPlugin.d.ts.map +1 -1
- package/dist/plugins/image-upload-command.d.ts +2 -0
- package/dist/plugins/image-upload-command.d.ts.map +1 -0
- package/dist/plugins/image-upload.css.d.ts +28 -0
- package/dist/plugins/image-upload.css.d.ts.map +1 -0
- package/dist/plugins/index.d.ts +2 -1
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/rich-editor.css +1 -1
- package/dist/static-entry.mjs +1 -1
- package/dist/{theme-C_ic3l9g.js → theme-DOLFsBRg.js} +103 -45
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -1,163 +1,334 @@
|
|
|
1
1
|
# @haklex/rich-editor
|
|
2
2
|
|
|
3
|
-
基于 [Lexical](https://lexical.dev/)
|
|
3
|
+
基于 [Lexical](https://lexical.dev/) 的富文本编辑器核心包。
|
|
4
|
+
负责 `编辑态` 运行时、节点注册、插件装配、Markdown 快捷键与主题系统。
|
|
5
|
+
|
|
6
|
+
## 包定位
|
|
7
|
+
|
|
8
|
+
- `@haklex/rich-editor`:编辑器核心(`RichEditor`)
|
|
9
|
+
- `@haklex/rich-renderer`:只读渲染引擎(`RichRenderer`)
|
|
10
|
+
- `@haklex/rich-renderers`:静态增强渲染器聚合(codeblock、image、video、mermaid...)
|
|
11
|
+
- `@haklex/rich-renderers-edit`:编辑增强渲染器与扩展插件聚合
|
|
12
|
+
- `@haklex/rich-kit-shiro`:一站式生产组合(编辑 + 渲染 + 常用扩展)
|
|
4
13
|
|
|
5
14
|
## 安装
|
|
6
15
|
|
|
7
16
|
```bash
|
|
8
|
-
pnpm add @haklex/rich-editor lexical @lexical/react
|
|
17
|
+
pnpm add @haklex/rich-editor lexical @lexical/react react react-dom
|
|
9
18
|
```
|
|
10
19
|
|
|
11
|
-
|
|
20
|
+
可选依赖(按功能开启):
|
|
21
|
+
|
|
22
|
+
- `katex`:数学公式渲染
|
|
23
|
+
- `shiki`:代码高亮
|
|
12
24
|
|
|
13
|
-
|
|
25
|
+
## 快速开始
|
|
14
26
|
|
|
15
27
|
```tsx
|
|
16
28
|
import { RichEditor } from '@haklex/rich-editor'
|
|
17
29
|
import type { SerializedEditorState } from 'lexical'
|
|
30
|
+
import { useState } from 'react'
|
|
31
|
+
import '@haklex/rich-editor/style.css'
|
|
18
32
|
|
|
19
|
-
function
|
|
20
|
-
const [
|
|
33
|
+
export function DemoEditor() {
|
|
34
|
+
const [value, setValue] = useState<SerializedEditorState | undefined>()
|
|
21
35
|
|
|
22
36
|
return (
|
|
23
37
|
<RichEditor
|
|
24
|
-
initialValue={
|
|
25
|
-
onChange={
|
|
38
|
+
initialValue={value}
|
|
39
|
+
onChange={setValue}
|
|
26
40
|
variant="article"
|
|
27
|
-
|
|
41
|
+
theme="light"
|
|
42
|
+
placeholder="Write something..."
|
|
43
|
+
onSubmit={() => {
|
|
44
|
+
// Cmd/Ctrl + Enter
|
|
45
|
+
}}
|
|
28
46
|
/>
|
|
29
47
|
)
|
|
30
48
|
}
|
|
31
49
|
```
|
|
32
50
|
|
|
33
|
-
|
|
51
|
+
如果要渲染只读内容:
|
|
34
52
|
|
|
35
53
|
```tsx
|
|
36
|
-
import { RichRenderer } from '@haklex/rich-
|
|
37
|
-
import type { SerializedEditorState } from 'lexical'
|
|
54
|
+
import { RichRenderer } from '@haklex/rich-renderer'
|
|
38
55
|
|
|
39
|
-
|
|
40
|
-
return (
|
|
41
|
-
<RichRenderer
|
|
42
|
-
value={content}
|
|
43
|
-
variant="article"
|
|
44
|
-
/>
|
|
45
|
-
)
|
|
46
|
-
}
|
|
56
|
+
<RichRenderer value={value} />
|
|
47
57
|
```
|
|
48
58
|
|
|
49
|
-
##
|
|
59
|
+
## 数据格式
|
|
60
|
+
|
|
61
|
+
`onChange` 返回的是 `SerializedEditorState`(Lexical JSON),推荐直接落库。
|
|
62
|
+
`initialValue` 直接喂回这个 JSON,可以做到无损回显。
|
|
50
63
|
|
|
51
|
-
|
|
52
|
-
|------|------|
|
|
53
|
-
| `@haklex/rich-editor` | 完整导出 |
|
|
54
|
-
| `@haklex/rich-editor/editor` | 仅 `RichEditor` |
|
|
55
|
-
| `@haklex/rich-editor/renderer` | 仅 `RichRenderer` |
|
|
56
|
-
| `@haklex/rich-editor/style.css` | 样式文件 |
|
|
64
|
+
## 导出入口
|
|
57
65
|
|
|
58
|
-
|
|
66
|
+
| 入口 | 说明 |
|
|
67
|
+
| --- | --- |
|
|
68
|
+
| `@haklex/rich-editor` | 完整导出(组件、节点、插件命令、上下文、类型) |
|
|
69
|
+
| `@haklex/rich-editor/editor` | 轻入口:`RichEditor` + 节点配置 |
|
|
70
|
+
| `@haklex/rich-editor/static` | 渲染/SSR 相关静态工具(供 `@haklex/rich-renderer` 与扩展包使用) |
|
|
71
|
+
| `@haklex/rich-editor/styles` | 仅样式变量/variant class 导出(不含 Lexical theme 对象) |
|
|
72
|
+
| `@haklex/rich-editor/style.css` | 打包后的编辑器样式 |
|
|
59
73
|
|
|
60
|
-
|
|
74
|
+
## RichEditor API
|
|
61
75
|
|
|
62
76
|
```ts
|
|
77
|
+
type RichEditorVariant = 'article' | 'comment' | 'note'
|
|
78
|
+
|
|
63
79
|
interface RichEditorProps {
|
|
64
|
-
initialValue?: SerializedEditorState
|
|
80
|
+
initialValue?: SerializedEditorState
|
|
65
81
|
onChange?: (value: SerializedEditorState) => void
|
|
66
|
-
variant?:
|
|
82
|
+
variant?: RichEditorVariant
|
|
67
83
|
theme?: 'light' | 'dark'
|
|
68
84
|
placeholder?: string
|
|
69
85
|
onSubmit?: () => void
|
|
70
86
|
autoFocus?: boolean
|
|
71
87
|
className?: string
|
|
72
88
|
contentClassName?: string
|
|
89
|
+
style?: React.CSSProperties
|
|
73
90
|
actions?: ReactNode
|
|
74
91
|
onEditorReady?: (editor: LexicalEditor | null) => void
|
|
75
92
|
extraNodes?: Array<Klass<LexicalNode>>
|
|
76
93
|
rendererConfig?: RendererConfig
|
|
94
|
+
imageUpload?: ImageUploadFn
|
|
77
95
|
debounceMs?: number
|
|
78
96
|
children?: ReactNode
|
|
79
97
|
}
|
|
80
98
|
```
|
|
81
99
|
|
|
82
|
-
|
|
100
|
+
关键字段说明:
|
|
101
|
+
|
|
102
|
+
- `extraNodes`:注册你自己的 Lexical 节点(扩展节点必配)
|
|
103
|
+
- `rendererConfig`:覆写默认渲染器(如 Image/CodeBlock/LinkCard)
|
|
104
|
+
- `actions`:插在编辑器底部 actions 区域(常用于 Slash 菜单)
|
|
105
|
+
- `children`:插件插槽(如 `EmbedPlugin`、`TldrawPlugin`)
|
|
106
|
+
- `imageUpload`:替换默认图片上传逻辑
|
|
107
|
+
- `debounceMs`:`onChange` 的防抖间隔(毫秒)
|
|
108
|
+
|
|
109
|
+
## RendererConfig API
|
|
83
110
|
|
|
84
111
|
```ts
|
|
85
|
-
interface
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
112
|
+
interface RendererConfig {
|
|
113
|
+
Alert?: ComponentType<AlertRendererProps>
|
|
114
|
+
Banner?: ComponentType<BannerRendererProps>
|
|
115
|
+
CodeBlock?: ComponentType<CodeBlockRendererProps>
|
|
116
|
+
CodeSnippet?: ComponentType<CodeSnippetRendererProps>
|
|
117
|
+
Footnote?: ComponentType<FootnoteRendererProps>
|
|
118
|
+
FootnoteSection?: ComponentType<FootnoteSectionRendererProps>
|
|
119
|
+
Gallery?: ComponentType<GalleryRendererProps>
|
|
120
|
+
Image?: ComponentType<ImageRendererProps>
|
|
121
|
+
KaTeX?: ComponentType<KaTeXRendererProps>
|
|
122
|
+
LinkCard?: ComponentType<LinkCardRendererProps>
|
|
123
|
+
Mermaid?: ComponentType<MermaidRendererProps>
|
|
124
|
+
Mention?: ComponentType<MentionRendererProps>
|
|
125
|
+
Video?: ComponentType<VideoRendererProps>
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
示例:覆写图片与代码块渲染器
|
|
130
|
+
|
|
131
|
+
```tsx
|
|
132
|
+
import type { RendererConfig } from '@haklex/rich-editor'
|
|
133
|
+
|
|
134
|
+
const rendererConfig: RendererConfig = {
|
|
135
|
+
Image: MyImageRenderer,
|
|
136
|
+
CodeBlock: MyCodeBlockRenderer,
|
|
93
137
|
}
|
|
138
|
+
|
|
139
|
+
<RichEditor rendererConfig={rendererConfig} />
|
|
94
140
|
```
|
|
95
141
|
|
|
96
|
-
##
|
|
142
|
+
## 内置节点与插件
|
|
143
|
+
|
|
144
|
+
默认内置节点(`config.ts` / `config-edit.ts`):
|
|
145
|
+
|
|
146
|
+
- 内置 Lexical 节点:Heading、Quote、List、Link、Table、Code、HorizontalRule...
|
|
147
|
+
- 自定义节点:Spoiler、Mention、KaTeXInline、KaTeXBlock、Image、Alert、CodeBlock、Footnote、FootnoteSection、Video、LinkCard、Details、Grid、Banner、Mermaid
|
|
97
148
|
|
|
98
|
-
|
|
99
|
-
|------|------|------|------|------|
|
|
100
|
-
| `article` | 系统无衬线 | 16px | 1.7 | 博客文章 |
|
|
101
|
-
| `note` | 思源宋体 | 16px | 1.8 | 个人笔记 |
|
|
102
|
-
| `comment` | 系统无衬线 | 14px | 1.5 | 评论 |
|
|
149
|
+
`RichEditor` 默认挂载插件:
|
|
103
150
|
|
|
104
|
-
|
|
151
|
+
- 历史/列表/链接/表格:`HistoryPlugin`、`ListPlugin`、`LinkPlugin`、`TablePlugin`
|
|
152
|
+
- 编辑增强:`MarkdownShortcutsPlugin`、`CheckListPlugin`、`AutoLinkPlugin`
|
|
153
|
+
- 内容能力:`ImagePlugin`、`ImageUploadPlugin`、`KaTeXPlugin`、`AlertPlugin`、`MermaidPlugin`、`HorizontalRulePlugin`
|
|
154
|
+
- 生命周期:`OnChangePlugin`、`EditorRefPlugin`、`SubmitShortcutPlugin`(Cmd/Ctrl+Enter)、`AutoFocusPlugin`
|
|
155
|
+
|
|
156
|
+
## 常用命令导出
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
INSERT_ALERT_COMMAND
|
|
160
|
+
INSERT_IMAGE_COMMAND
|
|
161
|
+
OPEN_IMAGE_UPLOAD_DIALOG_COMMAND
|
|
162
|
+
INSERT_KATEX_INLINE_COMMAND
|
|
163
|
+
INSERT_KATEX_BLOCK_COMMAND
|
|
164
|
+
INSERT_MERMAID_COMMAND
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
可在自定义插件中 `editor.dispatchCommand(...)` 触发。
|
|
168
|
+
|
|
169
|
+
## Markdown Transformer
|
|
170
|
+
|
|
171
|
+
导出:`ALL_TRANSFORMERS`(`src/transformers`)
|
|
172
|
+
|
|
173
|
+
覆盖能力包括:
|
|
174
|
+
|
|
175
|
+
- Inline:Spoiler、Mention、Footnote、`++underline++`、上/下标、KaTeX inline
|
|
176
|
+
- Block:FootnoteSection、Container(Banner/Details)、Git Alert、KaTeX block、Image/Video/CodeBlock/LinkCard/Mermaid/Grid/Table/HR + Lexical 默认 transformers
|
|
177
|
+
|
|
178
|
+
参考:`docs/export-format-summary.md`
|
|
179
|
+
|
|
180
|
+
## 设计模式
|
|
181
|
+
|
|
182
|
+
### 1) Static / Edit 节点分离
|
|
183
|
+
|
|
184
|
+
对“编辑态很重”的节点采用双类拆分:
|
|
185
|
+
|
|
186
|
+
- 静态节点:用于只读渲染,依赖轻
|
|
187
|
+
- 编辑节点:继承静态节点,覆盖 `decorate()` 挂重 UI(Popover、Dialog、Nested editor 等)
|
|
188
|
+
|
|
189
|
+
典型:`AlertQuoteNode` / `AlertQuoteEditNode`、`CodeBlockNode` / `CodeBlockEditNode`。
|
|
190
|
+
|
|
191
|
+
### 2) RendererWrapper 注入模式
|
|
192
|
+
|
|
193
|
+
节点内部通过 `createRendererDecoration(...)` 把渲染职责交给 `RendererConfig`,实现:
|
|
194
|
+
|
|
195
|
+
- 默认渲染器可直接用
|
|
196
|
+
- 业务侧可按 key 定向覆写
|
|
197
|
+
- 保持节点协议不变
|
|
198
|
+
|
|
199
|
+
### 3) Command + Plugin 模式
|
|
200
|
+
|
|
201
|
+
扩展插入动作统一走 `createCommand` + `registerCommand`,避免 UI 与文档结构硬耦合。
|
|
202
|
+
|
|
203
|
+
## 扩展编写与接入
|
|
204
|
+
|
|
205
|
+
下面是可直接复用的扩展落地流程。
|
|
206
|
+
|
|
207
|
+
### 步骤 1:定义 Node(静态)
|
|
105
208
|
|
|
106
209
|
```tsx
|
|
107
|
-
import type {
|
|
210
|
+
import type {
|
|
211
|
+
EditorConfig,
|
|
212
|
+
LexicalEditor,
|
|
213
|
+
LexicalNode,
|
|
214
|
+
NodeKey,
|
|
215
|
+
SerializedLexicalNode,
|
|
216
|
+
Spread,
|
|
217
|
+
} from 'lexical'
|
|
218
|
+
import { DecoratorNode } from 'lexical'
|
|
219
|
+
import type { ReactElement } from 'react'
|
|
220
|
+
import { createElement } from 'react'
|
|
221
|
+
|
|
222
|
+
import { PollRenderer } from './PollRenderer'
|
|
223
|
+
|
|
224
|
+
type SerializedPollNode = Spread<
|
|
225
|
+
{ question: string },
|
|
226
|
+
SerializedLexicalNode
|
|
227
|
+
>
|
|
228
|
+
|
|
229
|
+
export class PollNode extends DecoratorNode<ReactElement> {
|
|
230
|
+
__question: string
|
|
231
|
+
|
|
232
|
+
static getType() {
|
|
233
|
+
return 'poll'
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
static clone(node: PollNode) {
|
|
237
|
+
return new PollNode(node.__question, node.__key)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
constructor(question: string, key?: NodeKey) {
|
|
241
|
+
super(key)
|
|
242
|
+
this.__question = question
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
createDOM(_config: EditorConfig) {
|
|
246
|
+
const el = document.createElement('div')
|
|
247
|
+
el.className = 'rich-poll-wrapper'
|
|
248
|
+
return el
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
updateDOM() {
|
|
252
|
+
return false
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
isInline() {
|
|
256
|
+
return false
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
static importJSON(json: SerializedPollNode) {
|
|
260
|
+
return new PollNode(json.question)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
exportJSON(): SerializedPollNode {
|
|
264
|
+
return {
|
|
265
|
+
...super.exportJSON(),
|
|
266
|
+
type: 'poll',
|
|
267
|
+
question: this.__question,
|
|
268
|
+
version: 1,
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
decorate(_editor: LexicalEditor, _config: EditorConfig): ReactElement {
|
|
273
|
+
return createElement(PollRenderer, {
|
|
274
|
+
question: this.__question,
|
|
275
|
+
})
|
|
276
|
+
}
|
|
277
|
+
}
|
|
108
278
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
279
|
+
export function $isPollNode(
|
|
280
|
+
node: LexicalNode | null | undefined,
|
|
281
|
+
): node is PollNode {
|
|
282
|
+
return node instanceof PollNode
|
|
112
283
|
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
说明:`RendererConfig` 只能覆写预定义 key。
|
|
287
|
+
像 `Poll` 这种全新节点,一般采用“节点内直接渲染组件”或自定义 context 的方式。
|
|
288
|
+
|
|
289
|
+
### 步骤 2:可选编辑态 Node
|
|
113
290
|
|
|
114
|
-
|
|
291
|
+
如果编辑态需要重 UI,可以 `class PollEditNode extends PollNode` 并覆写 `decorate()`。
|
|
292
|
+
|
|
293
|
+
还可以定义 slash 菜单项:
|
|
294
|
+
|
|
295
|
+
```ts
|
|
296
|
+
static slashMenuItems = [
|
|
297
|
+
{
|
|
298
|
+
title: 'Poll',
|
|
299
|
+
description: 'Insert a poll block',
|
|
300
|
+
section: 'MEDIA',
|
|
301
|
+
onSelect: (editor) => { ... },
|
|
302
|
+
},
|
|
303
|
+
]
|
|
115
304
|
```
|
|
116
305
|
|
|
117
|
-
|
|
306
|
+
### 步骤 3:注册到编辑器与渲染器
|
|
118
307
|
|
|
119
308
|
```tsx
|
|
120
309
|
import { RichEditor } from '@haklex/rich-editor'
|
|
121
|
-
import {
|
|
310
|
+
import { RichRenderer } from '@haklex/rich-renderer'
|
|
311
|
+
|
|
312
|
+
<RichEditor extraNodes={[PollEditNode]}>{/* <PollPlugin /> */}</RichEditor>
|
|
122
313
|
|
|
123
|
-
<
|
|
314
|
+
<RichRenderer value={value} extraNodes={[PollNode]} />
|
|
124
315
|
```
|
|
125
316
|
|
|
126
|
-
|
|
317
|
+
### 步骤 4:插件化插入命令(推荐)
|
|
127
318
|
|
|
128
319
|
```ts
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
export {
|
|
132
|
-
|
|
133
|
-
// 节点配置
|
|
134
|
-
export { allNodes, builtinNodes, customNodes } from './config'
|
|
135
|
-
export { allEditNodes, customEditNodes } from './config-edit'
|
|
136
|
-
|
|
137
|
-
// Context
|
|
138
|
-
export { ColorSchemeProvider, useColorScheme } from './context/ColorSchemeContext'
|
|
139
|
-
export { useRendererConfig } from './context/RendererConfigContext'
|
|
140
|
-
|
|
141
|
-
// 类型
|
|
142
|
-
export type { RichEditorProps, RichRendererProps, RichEditorVariant } from './types'
|
|
143
|
-
export type { RendererConfig } from './types/renderer-config'
|
|
144
|
-
export type { SlashMenuItemConfig } from './types/slash-menu'
|
|
145
|
-
|
|
146
|
-
// 工具函数
|
|
147
|
-
export { createRendererDecoration } from './components/RendererWrapper'
|
|
148
|
-
export { getVariantClass } from './components/utils'
|
|
320
|
+
import { createCommand } from 'lexical'
|
|
321
|
+
|
|
322
|
+
export const INSERT_POLL_COMMAND = createCommand<{ question: string }>()
|
|
149
323
|
```
|
|
150
324
|
|
|
151
|
-
|
|
325
|
+
在插件中注册命令,所有 UI(按钮、slash、快捷键)统一 dispatch 该命令。
|
|
152
326
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
"react-dom": ">=19"
|
|
159
|
-
}
|
|
160
|
-
```
|
|
327
|
+
## 与聚合包配合
|
|
328
|
+
|
|
329
|
+
- 只想增强只读渲染:`@haklex/rich-renderers`
|
|
330
|
+
- 要增强编辑态:`@haklex/rich-renderers-edit`
|
|
331
|
+
- 想直接开箱即用:`@haklex/rich-kit-shiro`
|
|
161
332
|
|
|
162
333
|
## License
|
|
163
334
|
|