@meta-1/editor 1.0.3 → 1.0.4
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/package.json +1 -1
- package/src/editor/index.tsx +15 -4
- package/src/hooks/use-editor.ts +41 -12
- package/README.md +0 -218
package/package.json
CHANGED
package/src/editor/index.tsx
CHANGED
|
@@ -69,24 +69,35 @@ export function EditorContentArea() {
|
|
|
69
69
|
export type Meta1EditorProps = Meta1EditorOptions & {
|
|
70
70
|
/** Editor instance created by Meta1Editor.useEditor() */
|
|
71
71
|
editor?: Meta1EditorInstance;
|
|
72
|
+
/** Controlled value - works with FormItem */
|
|
73
|
+
value?: Meta1EditorValue;
|
|
74
|
+
/** onChange callback - works with FormItem */
|
|
75
|
+
onChange?: (content: Meta1EditorValue) => void;
|
|
72
76
|
};
|
|
73
77
|
|
|
74
78
|
const Meta1EditorInner: FC<Meta1EditorProps> = (props) => {
|
|
75
|
-
const { editor: editorInstance, ...options } = props;
|
|
79
|
+
const { editor: editorInstance, value, onChange, ...options } = props;
|
|
76
80
|
|
|
77
81
|
// Create the actual tiptap editor with value/onChange support
|
|
78
|
-
const tiptapEditor = useMeta1Editor(
|
|
82
|
+
const tiptapEditor = useMeta1Editor({
|
|
83
|
+
...options,
|
|
84
|
+
value,
|
|
85
|
+
onChange,
|
|
86
|
+
});
|
|
79
87
|
|
|
80
88
|
// Update the instance reference when editor changes
|
|
81
89
|
useEffect(() => {
|
|
82
|
-
if (editorInstance?._setEditor) {
|
|
90
|
+
if (editorInstance?._setEditor && tiptapEditor) {
|
|
83
91
|
editorInstance._setEditor(tiptapEditor);
|
|
92
|
+
if (options.contentType) {
|
|
93
|
+
editorInstance._contentType = options.contentType;
|
|
94
|
+
}
|
|
84
95
|
editorInstance._nodeExclude = options.nodeExclude;
|
|
85
96
|
}
|
|
86
97
|
return () => {
|
|
87
98
|
editorInstance?._setEditor?.(null);
|
|
88
99
|
};
|
|
89
|
-
}, [tiptapEditor, editorInstance, options.nodeExclude]);
|
|
100
|
+
}, [tiptapEditor, editorInstance, options.contentType, options.nodeExclude]);
|
|
90
101
|
|
|
91
102
|
if (!tiptapEditor) {
|
|
92
103
|
return null;
|
package/src/hooks/use-editor.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, useRef } from "react";
|
|
1
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
2
2
|
import { Highlight } from "@tiptap/extension-highlight";
|
|
3
3
|
import { TaskItem, TaskList } from "@tiptap/extension-list";
|
|
4
4
|
import { Mathematics } from "@tiptap/extension-mathematics";
|
|
@@ -46,9 +46,7 @@ export type Meta1EditorOptions = {
|
|
|
46
46
|
contentType?: ContentType;
|
|
47
47
|
editable?: boolean;
|
|
48
48
|
imageUpload?: ImageUploadOptions;
|
|
49
|
-
|
|
50
|
-
onChange?: (content: Meta1EditorValue) => void;
|
|
51
|
-
/** Node types to exclude from output (e.g., ['imageUpload']) */
|
|
49
|
+
/** Node types to exclude from output (e.g., ['imageUpload'] for filtering upload placeholders) */
|
|
52
50
|
nodeExclude?: string[];
|
|
53
51
|
};
|
|
54
52
|
|
|
@@ -58,6 +56,8 @@ export type Meta1EditorOptions = {
|
|
|
58
56
|
export interface Meta1EditorInstance {
|
|
59
57
|
/** Internal: set the actual editor reference */
|
|
60
58
|
_setEditor?: (editor: Editor | null) => void;
|
|
59
|
+
/** Internal: set contentType for value conversion */
|
|
60
|
+
_contentType?: ContentType;
|
|
61
61
|
/** Internal: set nodeExclude for filtering */
|
|
62
62
|
_nodeExclude?: string[];
|
|
63
63
|
/** Get the underlying tiptap Editor instance */
|
|
@@ -100,14 +100,12 @@ const filterJsonNodes = (json: Record<string, unknown>, excludeTypes: string[]):
|
|
|
100
100
|
* Filter out excluded nodes from HTML content
|
|
101
101
|
*/
|
|
102
102
|
const filterHtmlNodes = (html: string, excludeTypes: string[]): string => {
|
|
103
|
-
// Create patterns for each excluded type
|
|
104
|
-
// imageUpload renders as: <div data-type="image-upload" ...></div>
|
|
105
103
|
let result = html;
|
|
106
104
|
for (const type of excludeTypes) {
|
|
107
105
|
// Convert camelCase to kebab-case for data-type attribute
|
|
108
106
|
const kebabType = type.replace(/([A-Z])/g, "-$1").toLowerCase();
|
|
109
|
-
// Match self-closing
|
|
110
|
-
const pattern = new RegExp(`<div[^>]*data-type="${kebabType}"[^>]*>.*?</div>`, "
|
|
107
|
+
// Match divs with data-type attribute (handles both self-closing and content divs)
|
|
108
|
+
const pattern = new RegExp(`<div[^>]*data-type="${kebabType}"[^>]*>.*?</div>`, "gis");
|
|
111
109
|
result = result.replace(pattern, "");
|
|
112
110
|
}
|
|
113
111
|
return result;
|
|
@@ -130,7 +128,7 @@ const getValueFromEditor = (editor: Editor, contentType: ContentType, nodeExclud
|
|
|
130
128
|
return html;
|
|
131
129
|
}
|
|
132
130
|
case "markdown":
|
|
133
|
-
// Markdown doesn't
|
|
131
|
+
// Markdown export typically doesn't include custom nodes like imageUpload
|
|
134
132
|
return editor.getMarkdown();
|
|
135
133
|
default:
|
|
136
134
|
return JSON.stringify(editor.getJSON());
|
|
@@ -162,6 +160,7 @@ export const useEditorInstance = (): Meta1EditorInstance => {
|
|
|
162
160
|
if (!instanceRef.current) {
|
|
163
161
|
instanceRef.current = {
|
|
164
162
|
_setEditor,
|
|
163
|
+
_contentType: "json",
|
|
165
164
|
get _nodeExclude() {
|
|
166
165
|
return nodeExcludeRef.current;
|
|
167
166
|
},
|
|
@@ -194,7 +193,10 @@ export const useEditorInstance = (): Meta1EditorInstance => {
|
|
|
194
193
|
return instanceRef.current;
|
|
195
194
|
};
|
|
196
195
|
|
|
197
|
-
export type UseMeta1EditorProps = Meta1EditorOptions
|
|
196
|
+
export type UseMeta1EditorProps = Meta1EditorOptions & {
|
|
197
|
+
value?: Meta1EditorValue;
|
|
198
|
+
onChange?: (content: Meta1EditorValue) => void;
|
|
199
|
+
};
|
|
198
200
|
|
|
199
201
|
/**
|
|
200
202
|
* Internal hook that creates the actual tiptap editor.
|
|
@@ -210,9 +212,11 @@ export const useMeta1Editor = (props: UseMeta1EditorProps) => {
|
|
|
210
212
|
contentType = "json",
|
|
211
213
|
editable = true,
|
|
212
214
|
imageUpload,
|
|
213
|
-
nodeExclude,
|
|
215
|
+
nodeExclude = ["imageUpload"],
|
|
214
216
|
} = props;
|
|
215
217
|
const isUpdatingFromPropsRef = useRef(false);
|
|
218
|
+
// Track the last value we sent via onChange to avoid sync loops
|
|
219
|
+
const lastEmittedValueRef = useRef<string | undefined>(undefined);
|
|
216
220
|
|
|
217
221
|
const editor = useEditor({
|
|
218
222
|
content: value,
|
|
@@ -220,7 +224,10 @@ export const useMeta1Editor = (props: UseMeta1EditorProps) => {
|
|
|
220
224
|
editable,
|
|
221
225
|
onUpdate: ({ editor }) => {
|
|
222
226
|
if (!isUpdatingFromPropsRef.current) {
|
|
223
|
-
|
|
227
|
+
const newValue = getValueFromEditor(editor, contentType, nodeExclude);
|
|
228
|
+
const newValueStr = typeof newValue === "string" ? newValue : JSON.stringify(newValue);
|
|
229
|
+
lastEmittedValueRef.current = newValueStr;
|
|
230
|
+
onChange?.(newValue);
|
|
224
231
|
}
|
|
225
232
|
},
|
|
226
233
|
editorProps: {
|
|
@@ -282,5 +289,27 @@ export const useMeta1Editor = (props: UseMeta1EditorProps) => {
|
|
|
282
289
|
immediatelyRender: false,
|
|
283
290
|
});
|
|
284
291
|
|
|
292
|
+
// Sync value from props to editor (only for external changes)
|
|
293
|
+
useEffect(() => {
|
|
294
|
+
if (!editor || value === undefined) {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const valueStr = typeof value === "string" ? value : JSON.stringify(value);
|
|
299
|
+
|
|
300
|
+
if (lastEmittedValueRef.current === valueStr) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const currentContent = getValueFromEditor(editor, contentType, nodeExclude);
|
|
305
|
+
const currentStr = typeof currentContent === "string" ? currentContent : JSON.stringify(currentContent);
|
|
306
|
+
|
|
307
|
+
if (valueStr !== currentStr) {
|
|
308
|
+
isUpdatingFromPropsRef.current = true;
|
|
309
|
+
editor.commands.setContent(value);
|
|
310
|
+
isUpdatingFromPropsRef.current = false;
|
|
311
|
+
}
|
|
312
|
+
}, [editor, value, contentType, nodeExclude]);
|
|
313
|
+
|
|
285
314
|
return editor;
|
|
286
315
|
};
|
package/README.md
DELETED
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
# @meta-1/editor
|
|
2
|
-
|
|
3
|
-
一个基于 [Tiptap](https://tiptap.dev) 构建的现代化富文本编辑器,提供类似 Notion 的编辑体验。
|
|
4
|
-
|
|
5
|
-
[](https://www.npmjs.com/package/@meta-1/editor)
|
|
6
|
-
[](https://opensource.org/licenses/MIT)
|
|
7
|
-
|
|
8
|
-
## ✨ 特性
|
|
9
|
-
|
|
10
|
-
- 🎨 **类 Notion 编辑体验** - 支持斜杠命令、拖拽排序、浮动工具栏
|
|
11
|
-
- 📝 **丰富的内容格式** - 标题、列表、引用、代码块、表格等
|
|
12
|
-
- 🖼️ **图片上传** - 内置图片上传组件,支持自定义上传函数
|
|
13
|
-
- 📊 **表格编辑** - 完整的表格支持,包括合并单元格、调整大小
|
|
14
|
-
- 🔢 **数学公式** - 支持 LaTeX 数学公式渲染
|
|
15
|
-
- 🎯 **多种输出格式** - 支持 JSON、HTML、Markdown 输出
|
|
16
|
-
- 🌐 **国际化** - 内置中文简体、中文繁体、英文语言包
|
|
17
|
-
- 🔄 **协作编辑** - 基于 Yjs 的实时协作支持
|
|
18
|
-
|
|
19
|
-
## 📦 安装
|
|
20
|
-
|
|
21
|
-
```bash
|
|
22
|
-
pnpm add @meta-1/editor
|
|
23
|
-
# 或
|
|
24
|
-
npm install @meta-1/editor
|
|
25
|
-
# 或
|
|
26
|
-
yarn add @meta-1/editor
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
### Peer Dependencies
|
|
30
|
-
|
|
31
|
-
```bash
|
|
32
|
-
pnpm add react-i18next i18next
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
## 🚀 快速开始
|
|
36
|
-
|
|
37
|
-
### 基础用法
|
|
38
|
-
|
|
39
|
-
```tsx
|
|
40
|
-
import { Meta1Editor } from "@meta-1/editor";
|
|
41
|
-
import { useState } from "react";
|
|
42
|
-
|
|
43
|
-
function App() {
|
|
44
|
-
const [content, setContent] = useState({});
|
|
45
|
-
|
|
46
|
-
return (
|
|
47
|
-
<Meta1Editor
|
|
48
|
-
value={content}
|
|
49
|
-
onChange={setContent}
|
|
50
|
-
placeholder="输入内容..."
|
|
51
|
-
/>
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
### 使用 Editor 实例
|
|
57
|
-
|
|
58
|
-
```tsx
|
|
59
|
-
import { Meta1Editor } from "@meta-1/editor";
|
|
60
|
-
import { useState } from "react";
|
|
61
|
-
|
|
62
|
-
function App() {
|
|
63
|
-
const editor = Meta1Editor.useEditor();
|
|
64
|
-
const [content, setContent] = useState({});
|
|
65
|
-
|
|
66
|
-
const handleSave = () => {
|
|
67
|
-
const json = editor.getJSON();
|
|
68
|
-
const html = editor.getHTML();
|
|
69
|
-
const markdown = editor.getMarkdown();
|
|
70
|
-
console.log({ json, html, markdown });
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
return (
|
|
74
|
-
<>
|
|
75
|
-
<Meta1Editor
|
|
76
|
-
editor={editor}
|
|
77
|
-
value={content}
|
|
78
|
-
onChange={setContent}
|
|
79
|
-
placeholder="输入内容..."
|
|
80
|
-
/>
|
|
81
|
-
<button onClick={handleSave}>保存</button>
|
|
82
|
-
</>
|
|
83
|
-
);
|
|
84
|
-
}
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
### 配置图片上传
|
|
88
|
-
|
|
89
|
-
```tsx
|
|
90
|
-
import { Meta1Editor } from "@meta-1/editor";
|
|
91
|
-
|
|
92
|
-
function App() {
|
|
93
|
-
return (
|
|
94
|
-
<Meta1Editor
|
|
95
|
-
imageUpload={{
|
|
96
|
-
upload: async (file) => {
|
|
97
|
-
const formData = new FormData();
|
|
98
|
-
formData.append("file", file);
|
|
99
|
-
const response = await fetch("/api/upload", {
|
|
100
|
-
method: "POST",
|
|
101
|
-
body: formData,
|
|
102
|
-
});
|
|
103
|
-
const { url } = await response.json();
|
|
104
|
-
return url;
|
|
105
|
-
},
|
|
106
|
-
accept: "image/*",
|
|
107
|
-
maxSize: 5 * 1024 * 1024, // 5MB
|
|
108
|
-
limit: 3,
|
|
109
|
-
onError: (error) => console.error("上传失败:", error),
|
|
110
|
-
onSuccess: (url) => console.log("上传成功:", url),
|
|
111
|
-
}}
|
|
112
|
-
/>
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
### 设置内容类型
|
|
118
|
-
|
|
119
|
-
```tsx
|
|
120
|
-
// JSON 格式 (默认)
|
|
121
|
-
<Meta1Editor contentType="json" />
|
|
122
|
-
|
|
123
|
-
// HTML 格式
|
|
124
|
-
<Meta1Editor contentType="html" />
|
|
125
|
-
|
|
126
|
-
// Markdown 格式
|
|
127
|
-
<Meta1Editor contentType="markdown" />
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
## 📖 API
|
|
131
|
-
|
|
132
|
-
### Meta1Editor Props
|
|
133
|
-
|
|
134
|
-
| 属性 | 类型 | 默认值 | 说明 |
|
|
135
|
-
|------|------|--------|------|
|
|
136
|
-
| `value` | `string \| Record<string, unknown>` | - | 编辑器内容 |
|
|
137
|
-
| `onChange` | `(content: Meta1EditorValue) => void` | - | 内容变化回调 |
|
|
138
|
-
| `editor` | `Meta1EditorInstance` | - | 编辑器实例 |
|
|
139
|
-
| `placeholder` | `string` | - | 占位符文本 |
|
|
140
|
-
| `contentType` | `'json' \| 'html' \| 'markdown'` | `'json'` | 内容格式 |
|
|
141
|
-
| `editable` | `boolean` | `true` | 是否可编辑 |
|
|
142
|
-
| `imageUpload` | `ImageUploadOptions` | - | 图片上传配置 |
|
|
143
|
-
| `nodeExclude` | `string[]` | - | 输出时排除的节点类型 |
|
|
144
|
-
| `extensions` | `Extensions` | - | 额外的 Tiptap 扩展 |
|
|
145
|
-
| `className` | `string` | - | 自定义 CSS 类名 |
|
|
146
|
-
|
|
147
|
-
### Meta1EditorInstance 方法
|
|
148
|
-
|
|
149
|
-
| 方法 | 返回类型 | 说明 |
|
|
150
|
-
|------|----------|------|
|
|
151
|
-
| `getEditor()` | `Editor \| null` | 获取底层 Tiptap Editor 实例 |
|
|
152
|
-
| `getJSON()` | `Record<string, unknown> \| null` | 获取 JSON 格式内容 |
|
|
153
|
-
| `getHTML()` | `string \| null` | 获取 HTML 格式内容 |
|
|
154
|
-
| `getMarkdown()` | `string \| null` | 获取 Markdown 格式内容 |
|
|
155
|
-
| `focus()` | `void` | 聚焦编辑器 |
|
|
156
|
-
| `isEmpty()` | `boolean` | 检查编辑器是否为空 |
|
|
157
|
-
|
|
158
|
-
### ImageUploadOptions
|
|
159
|
-
|
|
160
|
-
| 属性 | 类型 | 默认值 | 说明 |
|
|
161
|
-
|------|------|--------|------|
|
|
162
|
-
| `upload` | `(file: File) => Promise<string>` | - | 上传函数,返回图片 URL |
|
|
163
|
-
| `accept` | `string` | `'image/*'` | 接受的文件类型 |
|
|
164
|
-
| `maxSize` | `number` | `10MB` | 最大文件大小 (bytes) |
|
|
165
|
-
| `limit` | `number` | `3` | 最大同时上传数量 |
|
|
166
|
-
| `onError` | `(error: Error) => void` | - | 上传错误回调 |
|
|
167
|
-
| `onSuccess` | `(url: string) => void` | - | 上传成功回调 |
|
|
168
|
-
|
|
169
|
-
## 🌐 国际化
|
|
170
|
-
|
|
171
|
-
编辑器内置多语言支持,可通过以下方式导入语言包:
|
|
172
|
-
|
|
173
|
-
```tsx
|
|
174
|
-
import zhCN from "@meta-1/editor/locales/zh-CN";
|
|
175
|
-
import zhTW from "@meta-1/editor/locales/zh-TW";
|
|
176
|
-
import en from "@meta-1/editor/locales/en";
|
|
177
|
-
|
|
178
|
-
// 配置 i18next
|
|
179
|
-
i18n.init({
|
|
180
|
-
resources: {
|
|
181
|
-
"zh-CN": { editor: zhCN },
|
|
182
|
-
"zh-TW": { editor: zhTW },
|
|
183
|
-
en: { editor: en },
|
|
184
|
-
},
|
|
185
|
-
});
|
|
186
|
-
```
|
|
187
|
-
|
|
188
|
-
## 🎨 内置功能
|
|
189
|
-
|
|
190
|
-
### 文本格式
|
|
191
|
-
- 粗体、斜体、下划线、删除线
|
|
192
|
-
- 文字颜色、背景高亮
|
|
193
|
-
- 上标、下标
|
|
194
|
-
- 行内代码
|
|
195
|
-
|
|
196
|
-
### 块级元素
|
|
197
|
-
- 标题 (H1-H6)
|
|
198
|
-
- 段落
|
|
199
|
-
- 有序列表、无序列表、任务列表
|
|
200
|
-
- 引用块
|
|
201
|
-
- 代码块
|
|
202
|
-
- 表格
|
|
203
|
-
- 分割线
|
|
204
|
-
- 图片
|
|
205
|
-
|
|
206
|
-
### 高级功能
|
|
207
|
-
- 斜杠命令菜单 (`/`)
|
|
208
|
-
- @ 提及功能
|
|
209
|
-
- 拖拽排序
|
|
210
|
-
- 浮动工具栏
|
|
211
|
-
- 数学公式 (LaTeX)
|
|
212
|
-
- 文本对齐
|
|
213
|
-
- 节点唯一 ID
|
|
214
|
-
|
|
215
|
-
## 📄 License
|
|
216
|
-
|
|
217
|
-
[MIT](https://opensource.org/licenses/MIT)
|
|
218
|
-
|