@meta-1/editor 1.0.2 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meta-1/editor",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "keywords": [
5
5
  "meta",
6
6
  "tailwindcss",
@@ -8,26 +8,21 @@
8
8
  @apply relative;
9
9
  color: inherit;
10
10
  font-style: inherit;
11
-
12
- &:first-child,
13
- &:first-of-type {
14
- @apply mt-0;
15
- }
16
11
  }
17
12
 
18
13
  h1 {
19
- @apply text-2xl font-bold mt-12;
14
+ @apply text-2xl font-bold;
20
15
  }
21
16
 
22
17
  h2 {
23
- @apply text-xl font-bold mt-10;
18
+ @apply text-xl font-bold;
24
19
  }
25
20
 
26
21
  h3 {
27
- @apply text-lg font-semibold mt-8;
22
+ @apply text-lg font-semibold;
28
23
  }
29
24
 
30
25
  h4 {
31
- @apply text-base font-semibold mt-8;
26
+ @apply text-base font-semibold;
32
27
  }
33
28
  }
@@ -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(options);
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;
@@ -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
- value?: Meta1EditorValue;
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 or content divs with data-type
110
- const pattern = new RegExp(`<div[^>]*data-type="${kebabType}"[^>]*>.*?</div>`, "gi");
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 output unsupported nodes, so no filtering needed
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
- onChange?.(getValueFromEditor(editor, contentType, nodeExclude));
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
  };