@meta-1/editor 1.0.0 → 1.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/package.json +4 -8
- package/src/editor/index.tsx +4 -14
- package/src/hooks/use-editor.ts +90 -41
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meta-1/editor",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"keywords": [
|
|
5
5
|
"meta",
|
|
6
6
|
"tailwindcss",
|
|
@@ -52,7 +52,8 @@
|
|
|
52
52
|
"react-hotkeys-hook": "^5.2.1",
|
|
53
53
|
"y-websocket": "^3.0.0",
|
|
54
54
|
"yjs": "^13.6.28",
|
|
55
|
-
"lodash.throttle": "^4.1.1"
|
|
55
|
+
"lodash.throttle": "^4.1.1",
|
|
56
|
+
"@ariakit/react": "^0.4.20"
|
|
56
57
|
},
|
|
57
58
|
"peerDependencies": {
|
|
58
59
|
"react-i18next": "16.5.0",
|
|
@@ -76,10 +77,5 @@
|
|
|
76
77
|
"/src",
|
|
77
78
|
"package.json",
|
|
78
79
|
"README.md"
|
|
79
|
-
]
|
|
80
|
-
"devDependencies": {
|
|
81
|
-
"@ariakit/react": "^0.4.20",
|
|
82
|
-
"sass": "^1.97.1",
|
|
83
|
-
"sass-embedded": "^1.97.1"
|
|
84
|
-
}
|
|
80
|
+
]
|
|
85
81
|
}
|
package/src/editor/index.tsx
CHANGED
|
@@ -69,34 +69,24 @@ 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;
|
|
76
72
|
};
|
|
77
73
|
|
|
78
74
|
const Meta1EditorInner: FC<Meta1EditorProps> = (props) => {
|
|
79
|
-
const { editor: editorInstance,
|
|
75
|
+
const { editor: editorInstance, ...options } = props;
|
|
80
76
|
|
|
81
77
|
// Create the actual tiptap editor with value/onChange support
|
|
82
|
-
const tiptapEditor = useMeta1Editor(
|
|
83
|
-
...options,
|
|
84
|
-
value,
|
|
85
|
-
onChange,
|
|
86
|
-
});
|
|
78
|
+
const tiptapEditor = useMeta1Editor(options);
|
|
87
79
|
|
|
88
80
|
// Update the instance reference when editor changes
|
|
89
81
|
useEffect(() => {
|
|
90
82
|
if (editorInstance?._setEditor) {
|
|
91
83
|
editorInstance._setEditor(tiptapEditor);
|
|
92
|
-
|
|
93
|
-
editorInstance._contentType = options.contentType;
|
|
94
|
-
}
|
|
84
|
+
editorInstance._nodeExclude = options.nodeExclude;
|
|
95
85
|
}
|
|
96
86
|
return () => {
|
|
97
87
|
editorInstance?._setEditor?.(null);
|
|
98
88
|
};
|
|
99
|
-
}, [tiptapEditor, editorInstance, options.
|
|
89
|
+
}, [tiptapEditor, editorInstance, options.nodeExclude]);
|
|
100
90
|
|
|
101
91
|
if (!tiptapEditor) {
|
|
102
92
|
return null;
|
package/src/hooks/use-editor.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback,
|
|
1
|
+
import { useCallback, 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,6 +46,10 @@ export type Meta1EditorOptions = {
|
|
|
46
46
|
contentType?: ContentType;
|
|
47
47
|
editable?: boolean;
|
|
48
48
|
imageUpload?: ImageUploadOptions;
|
|
49
|
+
value?: Meta1EditorValue;
|
|
50
|
+
onChange?: (content: Meta1EditorValue) => void;
|
|
51
|
+
/** Node types to exclude from output (e.g., ['imageUpload']) */
|
|
52
|
+
nodeExclude?: string[];
|
|
49
53
|
};
|
|
50
54
|
|
|
51
55
|
/**
|
|
@@ -54,13 +58,13 @@ export type Meta1EditorOptions = {
|
|
|
54
58
|
export interface Meta1EditorInstance {
|
|
55
59
|
/** Internal: set the actual editor reference */
|
|
56
60
|
_setEditor?: (editor: Editor | null) => void;
|
|
57
|
-
/** Internal: set
|
|
58
|
-
|
|
61
|
+
/** Internal: set nodeExclude for filtering */
|
|
62
|
+
_nodeExclude?: string[];
|
|
59
63
|
/** Get the underlying tiptap Editor instance */
|
|
60
64
|
getEditor: () => Editor | null;
|
|
61
|
-
/** Get current content as JSON */
|
|
65
|
+
/** Get current content as JSON (with nodeExclude filtering applied) */
|
|
62
66
|
getJSON: () => Record<string, unknown> | null;
|
|
63
|
-
/** Get current content as HTML */
|
|
67
|
+
/** Get current content as HTML (with nodeExclude filtering applied) */
|
|
64
68
|
getHTML: () => string | null;
|
|
65
69
|
/** Get current content as Markdown */
|
|
66
70
|
getMarkdown: () => string | null;
|
|
@@ -70,13 +74,63 @@ export interface Meta1EditorInstance {
|
|
|
70
74
|
isEmpty: () => boolean;
|
|
71
75
|
}
|
|
72
76
|
|
|
73
|
-
|
|
77
|
+
/**
|
|
78
|
+
* Filter out excluded nodes from JSON content recursively
|
|
79
|
+
*/
|
|
80
|
+
const filterJsonNodes = (json: Record<string, unknown>, excludeTypes: string[]): Record<string, unknown> | null => {
|
|
81
|
+
if (excludeTypes.includes(json.type as string)) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (Array.isArray(json.content)) {
|
|
86
|
+
const filteredContent = json.content
|
|
87
|
+
.map((node) => filterJsonNodes(node as Record<string, unknown>, excludeTypes))
|
|
88
|
+
.filter((node): node is Record<string, unknown> => node !== null);
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
...json,
|
|
92
|
+
content: filteredContent,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return json;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Filter out excluded nodes from HTML content
|
|
101
|
+
*/
|
|
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
|
+
let result = html;
|
|
106
|
+
for (const type of excludeTypes) {
|
|
107
|
+
// Convert camelCase to kebab-case for data-type attribute
|
|
108
|
+
const kebabType = type.replace(/([A-Z])/g, "-$1").toLowerCase();
|
|
109
|
+
// Match self-closing or content divs with data-type
|
|
110
|
+
const pattern = new RegExp(`<div[^>]*data-type="${kebabType}"[^>]*>.*?</div>`, "gi");
|
|
111
|
+
result = result.replace(pattern, "");
|
|
112
|
+
}
|
|
113
|
+
return result;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const getValueFromEditor = (editor: Editor, contentType: ContentType, nodeExclude?: string[]): Meta1EditorValue => {
|
|
74
117
|
switch (contentType) {
|
|
75
|
-
case "json":
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
118
|
+
case "json": {
|
|
119
|
+
const json = editor.getJSON();
|
|
120
|
+
if (nodeExclude?.length) {
|
|
121
|
+
return filterJsonNodes(json, nodeExclude) ?? { type: "doc", content: [] };
|
|
122
|
+
}
|
|
123
|
+
return json;
|
|
124
|
+
}
|
|
125
|
+
case "html": {
|
|
126
|
+
const html = editor.getHTML();
|
|
127
|
+
if (nodeExclude?.length) {
|
|
128
|
+
return filterHtmlNodes(html, nodeExclude);
|
|
129
|
+
}
|
|
130
|
+
return html;
|
|
131
|
+
}
|
|
79
132
|
case "markdown":
|
|
133
|
+
// Markdown doesn't output unsupported nodes, so no filtering needed
|
|
80
134
|
return editor.getMarkdown();
|
|
81
135
|
default:
|
|
82
136
|
return JSON.stringify(editor.getJSON());
|
|
@@ -99,6 +153,7 @@ const getValueFromEditor = (editor: Editor, contentType: ContentType): Meta1Edit
|
|
|
99
153
|
export const useEditorInstance = (): Meta1EditorInstance => {
|
|
100
154
|
const editorRef = useRef<Editor | null>(null);
|
|
101
155
|
const instanceRef = useRef<Meta1EditorInstance | null>(null);
|
|
156
|
+
const nodeExcludeRef = useRef<string[]>([]);
|
|
102
157
|
|
|
103
158
|
const _setEditor = useCallback((editor: Editor | null) => {
|
|
104
159
|
editorRef.current = editor;
|
|
@@ -107,10 +162,29 @@ export const useEditorInstance = (): Meta1EditorInstance => {
|
|
|
107
162
|
if (!instanceRef.current) {
|
|
108
163
|
instanceRef.current = {
|
|
109
164
|
_setEditor,
|
|
110
|
-
|
|
165
|
+
get _nodeExclude() {
|
|
166
|
+
return nodeExcludeRef.current;
|
|
167
|
+
},
|
|
168
|
+
set _nodeExclude(value: string[] | undefined) {
|
|
169
|
+
nodeExcludeRef.current = value ?? [];
|
|
170
|
+
},
|
|
111
171
|
getEditor: () => editorRef.current,
|
|
112
|
-
getJSON: () =>
|
|
113
|
-
|
|
172
|
+
getJSON: () => {
|
|
173
|
+
if (!editorRef.current) return null;
|
|
174
|
+
const json = editorRef.current.getJSON();
|
|
175
|
+
if (nodeExcludeRef.current.length) {
|
|
176
|
+
return filterJsonNodes(json, nodeExcludeRef.current) ?? { type: "doc", content: [] };
|
|
177
|
+
}
|
|
178
|
+
return json;
|
|
179
|
+
},
|
|
180
|
+
getHTML: () => {
|
|
181
|
+
if (!editorRef.current) return null;
|
|
182
|
+
const html = editorRef.current.getHTML();
|
|
183
|
+
if (nodeExcludeRef.current.length) {
|
|
184
|
+
return filterHtmlNodes(html, nodeExcludeRef.current);
|
|
185
|
+
}
|
|
186
|
+
return html;
|
|
187
|
+
},
|
|
114
188
|
getMarkdown: () => editorRef.current?.getMarkdown() ?? null,
|
|
115
189
|
focus: () => editorRef.current?.commands.focus(),
|
|
116
190
|
isEmpty: () => editorRef.current?.isEmpty ?? true,
|
|
@@ -120,10 +194,7 @@ export const useEditorInstance = (): Meta1EditorInstance => {
|
|
|
120
194
|
return instanceRef.current;
|
|
121
195
|
};
|
|
122
196
|
|
|
123
|
-
export type UseMeta1EditorProps = Meta1EditorOptions
|
|
124
|
-
value?: Meta1EditorValue;
|
|
125
|
-
onChange?: (content: Meta1EditorValue) => void;
|
|
126
|
-
};
|
|
197
|
+
export type UseMeta1EditorProps = Meta1EditorOptions;
|
|
127
198
|
|
|
128
199
|
/**
|
|
129
200
|
* Internal hook that creates the actual tiptap editor.
|
|
@@ -139,6 +210,7 @@ export const useMeta1Editor = (props: UseMeta1EditorProps) => {
|
|
|
139
210
|
contentType = "json",
|
|
140
211
|
editable = true,
|
|
141
212
|
imageUpload,
|
|
213
|
+
nodeExclude,
|
|
142
214
|
} = props;
|
|
143
215
|
const isUpdatingFromPropsRef = useRef(false);
|
|
144
216
|
|
|
@@ -148,7 +220,7 @@ export const useMeta1Editor = (props: UseMeta1EditorProps) => {
|
|
|
148
220
|
editable,
|
|
149
221
|
onUpdate: ({ editor }) => {
|
|
150
222
|
if (!isUpdatingFromPropsRef.current) {
|
|
151
|
-
onChange?.(getValueFromEditor(editor, contentType));
|
|
223
|
+
onChange?.(getValueFromEditor(editor, contentType, nodeExclude));
|
|
152
224
|
}
|
|
153
225
|
},
|
|
154
226
|
editorProps: {
|
|
@@ -210,28 +282,5 @@ export const useMeta1Editor = (props: UseMeta1EditorProps) => {
|
|
|
210
282
|
immediatelyRender: false,
|
|
211
283
|
});
|
|
212
284
|
|
|
213
|
-
// Sync value from props to editor
|
|
214
|
-
useEffect(() => {
|
|
215
|
-
if (!editor || value === undefined) {
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
const currentContent = getValueFromEditor(editor, contentType);
|
|
220
|
-
const isContentEqual =
|
|
221
|
-
typeof value === "string" && typeof currentContent === "string"
|
|
222
|
-
? value === currentContent
|
|
223
|
-
: JSON.stringify(value) === JSON.stringify(currentContent);
|
|
224
|
-
|
|
225
|
-
if (!isContentEqual) {
|
|
226
|
-
queueMicrotask(() => {
|
|
227
|
-
isUpdatingFromPropsRef.current = true;
|
|
228
|
-
editor.commands.setContent(value);
|
|
229
|
-
requestAnimationFrame(() => {
|
|
230
|
-
isUpdatingFromPropsRef.current = false;
|
|
231
|
-
});
|
|
232
|
-
});
|
|
233
|
-
}
|
|
234
|
-
}, [editor, value, contentType]);
|
|
235
|
-
|
|
236
285
|
return editor;
|
|
237
286
|
};
|