@alpaca-editor/core 1.0.4045 → 1.0.4047
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/dist/editor/field-types/RichTextEditorComponent.js +3 -10
- package/dist/editor/field-types/RichTextEditorComponent.js.map +1 -1
- package/dist/editor/field-types/richtext/components/ReactSlate.js +297 -342
- package/dist/editor/field-types/richtext/components/ReactSlate.js.map +1 -1
- package/dist/editor/field-types/richtext/components/SimpleRichTextEditor.js.map +1 -1
- package/dist/editor/field-types/richtext/components/SimpleToolbar.js +9 -9
- package/dist/editor/field-types/richtext/components/SimpleToolbar.js.map +1 -1
- package/dist/editor/field-types/richtext/config/pluginFactory.d.ts +7 -6
- package/dist/editor/field-types/richtext/config/pluginFactory.js +2 -1
- package/dist/editor/field-types/richtext/config/pluginFactory.js.map +1 -1
- package/dist/editor/field-types/richtext/hooks/useProfileCache.js +24 -18
- package/dist/editor/field-types/richtext/hooks/useProfileCache.js.map +1 -1
- package/dist/editor/field-types/richtext/hooks/useRichTextProfile.js +1 -1
- package/dist/editor/field-types/richtext/hooks/useRichTextProfile.js.map +1 -1
- package/dist/editor/field-types/richtext/types.d.ts +236 -90
- package/dist/editor/field-types/richtext/types.js +3 -3
- package/dist/editor/field-types/richtext/types.js.map +1 -1
- package/dist/editor/field-types/richtext/utils/conversion.d.ts +4 -2
- package/dist/editor/field-types/richtext/utils/conversion.js +79 -12
- package/dist/editor/field-types/richtext/utils/conversion.js.map +1 -1
- package/dist/editor/field-types/richtext/utils/plugins.d.ts +66 -39
- package/dist/editor/field-types/richtext/utils/plugins.js +377 -233
- package/dist/editor/field-types/richtext/utils/plugins.js.map +1 -1
- package/dist/editor/field-types/richtext/utils/profileMapper.js +22 -2
- package/dist/editor/field-types/richtext/utils/profileMapper.js.map +1 -1
- package/dist/revision.d.ts +2 -2
- package/dist/revision.js +2 -2
- package/package.json +1 -1
- package/src/editor/field-types/RichTextEditorComponent.tsx +4 -10
- package/src/editor/field-types/richtext/components/ReactSlate.css +85 -24
- package/src/editor/field-types/richtext/components/ReactSlate.tsx +372 -427
- package/src/editor/field-types/richtext/components/SimpleRichTextEditor.tsx +4 -2
- package/src/editor/field-types/richtext/components/SimpleToolbar.tsx +3 -3
- package/src/editor/field-types/richtext/config/pluginFactory.tsx +2 -1
- package/src/editor/field-types/richtext/hooks/useProfileCache.ts +25 -19
- package/src/editor/field-types/richtext/hooks/useRichTextProfile.ts +1 -1
- package/src/editor/field-types/richtext/types.ts +150 -112
- package/src/editor/field-types/richtext/utils/conversion.ts +100 -27
- package/src/editor/field-types/richtext/utils/plugins.ts +469 -268
- package/src/editor/field-types/richtext/utils/profileMapper.ts +26 -3
- package/src/revision.ts +2 -2
|
@@ -5,11 +5,11 @@ import React, {
|
|
|
5
5
|
forwardRef,
|
|
6
6
|
useRef,
|
|
7
7
|
useEffect,
|
|
8
|
+
memo,
|
|
8
9
|
} from "react";
|
|
9
10
|
import { createEditor, Descendant, Editor, Element, Transforms } from "slate";
|
|
10
11
|
import { Slate, Editable, withReact, ReactEditor } from "slate-react";
|
|
11
12
|
import { withHistory } from "slate-history";
|
|
12
|
-
import isEqual from "lodash/isEqual";
|
|
13
13
|
import "./ReactSlate.css";
|
|
14
14
|
|
|
15
15
|
import {
|
|
@@ -23,6 +23,9 @@ import {
|
|
|
23
23
|
SLATE_MARKS,
|
|
24
24
|
SLATE_BLOCKS,
|
|
25
25
|
SLATE_ALIGNMENTS,
|
|
26
|
+
MarkId,
|
|
27
|
+
BlockId,
|
|
28
|
+
AlignmentId,
|
|
26
29
|
} from "../types";
|
|
27
30
|
|
|
28
31
|
import EditorDropdown from "./EditorDropdown";
|
|
@@ -31,64 +34,70 @@ import { LinkEditorDialog } from "../../../LinkEditorDialog";
|
|
|
31
34
|
|
|
32
35
|
import { htmlToSlate, slateToHtml } from "../utils/conversion";
|
|
33
36
|
import { createPluginsFromConfig } from "../config/pluginFactory";
|
|
37
|
+
import { createKeyboardHandler } from "../utils/plugins";
|
|
34
38
|
import { useCachedSimplifiedProfile } from "../hooks/useProfileCache";
|
|
35
39
|
import { classNames } from "primereact/utils";
|
|
36
40
|
|
|
37
|
-
//
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
41
|
+
// Toolbar button component - not memoized to allow reactivity to editor state changes
|
|
42
|
+
const ToolbarButtonWrapper: React.FC<{
|
|
43
|
+
option: ToolbarOptionConfig;
|
|
44
|
+
icon: React.ReactNode;
|
|
45
|
+
editor: Editor;
|
|
46
|
+
onMouseDown: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
|
47
|
+
}> = ({ option, icon, editor, onMouseDown }) => {
|
|
48
|
+
// Calculate active state on every render - this is lightweight and ensures reactivity
|
|
49
|
+
let isActive = false;
|
|
50
|
+
switch (option.type) {
|
|
51
|
+
case "mark":
|
|
52
|
+
isActive = editor.isMarkActive(option.id);
|
|
53
|
+
break;
|
|
54
|
+
case "block":
|
|
55
|
+
isActive = editor.isBlockActive(option.id);
|
|
56
|
+
break;
|
|
57
|
+
case "alignment":
|
|
58
|
+
isActive = editor.isAlignActive(SLATE_ALIGNMENTS[option.id].value);
|
|
59
|
+
break;
|
|
60
|
+
case "link":
|
|
61
|
+
isActive = editor.isLinkActive();
|
|
62
|
+
break;
|
|
63
|
+
case "list":
|
|
64
|
+
const listType = option.id === "unordered-list" ? "unordered" : "ordered";
|
|
65
|
+
isActive = editor.isListActive(listType);
|
|
66
|
+
break;
|
|
67
|
+
case "insertion":
|
|
68
|
+
isActive = false; // Insertion buttons are never active
|
|
69
|
+
break;
|
|
70
|
+
default:
|
|
71
|
+
isActive = false;
|
|
58
72
|
}
|
|
59
73
|
|
|
60
|
-
return
|
|
74
|
+
return (
|
|
75
|
+
<ToolbarButton
|
|
76
|
+
icon={icon}
|
|
77
|
+
active={isActive}
|
|
78
|
+
onMouseDown={onMouseDown}
|
|
79
|
+
/>
|
|
80
|
+
);
|
|
61
81
|
};
|
|
62
82
|
|
|
63
|
-
//
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
// stable: normalizedOriginal === normalizedConverted
|
|
82
|
-
// });
|
|
83
|
-
|
|
84
|
-
// Check if they're equivalent
|
|
85
|
-
return normalizedOriginal === normalizedConverted;
|
|
86
|
-
} catch (error) {
|
|
87
|
-
console.warn("HTML conversion stability check failed:", error);
|
|
88
|
-
// If we can't check stability, assume it's not stable to be safe
|
|
89
|
-
return false;
|
|
90
|
-
}
|
|
91
|
-
};
|
|
83
|
+
// Memoized dropdown component to prevent unnecessary re-renders
|
|
84
|
+
const MemoizedEditorDropdown = memo<{
|
|
85
|
+
options: DropdownOption<any>[];
|
|
86
|
+
editor: Editor;
|
|
87
|
+
label?: string;
|
|
88
|
+
buttonStyle: React.CSSProperties;
|
|
89
|
+
}>(({ options, editor, label, buttonStyle }) => {
|
|
90
|
+
return (
|
|
91
|
+
<EditorDropdown
|
|
92
|
+
options={options}
|
|
93
|
+
editor={editor}
|
|
94
|
+
label={label}
|
|
95
|
+
buttonStyle={buttonStyle}
|
|
96
|
+
/>
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
MemoizedEditorDropdown.displayName = "MemoizedEditorDropdown";
|
|
92
101
|
|
|
93
102
|
export const ReactSlate = forwardRef<any, ReactSlateProps>((props, ref) => {
|
|
94
103
|
const {
|
|
@@ -101,20 +110,6 @@ export const ReactSlate = forwardRef<any, ReactSlateProps>((props, ref) => {
|
|
|
101
110
|
profile,
|
|
102
111
|
} = props;
|
|
103
112
|
|
|
104
|
-
// Create the Slate editor with plugins
|
|
105
|
-
const editor = useMemo(() => {
|
|
106
|
-
// Start with base editor
|
|
107
|
-
let slateEditor = createEditor();
|
|
108
|
-
|
|
109
|
-
// Apply core plugins
|
|
110
|
-
slateEditor = withReact(slateEditor);
|
|
111
|
-
slateEditor = withHistory(slateEditor);
|
|
112
|
-
|
|
113
|
-
slateEditor = createPluginsFromConfig(slateEditor);
|
|
114
|
-
|
|
115
|
-
return slateEditor;
|
|
116
|
-
}, []);
|
|
117
|
-
|
|
118
113
|
const editorProfile = profile || {
|
|
119
114
|
toolbar: {
|
|
120
115
|
groups: [],
|
|
@@ -124,217 +119,260 @@ export const ReactSlate = forwardRef<any, ReactSlateProps>((props, ref) => {
|
|
|
124
119
|
// Convert to simplified profile for the conversion functions (with caching)
|
|
125
120
|
const simplifiedProfile = useCachedSimplifiedProfile(editorProfile);
|
|
126
121
|
|
|
127
|
-
//
|
|
128
|
-
const
|
|
129
|
-
|
|
122
|
+
// Create the Slate editor with plugins - only once
|
|
123
|
+
const editor = useMemo(() => {
|
|
124
|
+
const baseEditor = createEditor();
|
|
125
|
+
|
|
126
|
+
// Apply plugins in correct order
|
|
127
|
+
const enhancedEditor = createPluginsFromConfig(
|
|
128
|
+
withHistory(withReact(baseEditor))
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
return enhancedEditor;
|
|
132
|
+
}, []);
|
|
130
133
|
|
|
131
|
-
//
|
|
132
|
-
useMemo(() => {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
134
|
+
// Initial value for Slate - capture initial props at mount time
|
|
135
|
+
const initialSlateValue = useMemo(() => {
|
|
136
|
+
try {
|
|
137
|
+
return htmlToSlate(value, simplifiedProfile);
|
|
138
|
+
} catch (error) {
|
|
139
|
+
console.warn("Failed to convert initial HTML to Slate:", error);
|
|
140
|
+
return [{ type: "paragraph", children: [{ text: "" }] }];
|
|
136
141
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
142
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
143
|
+
}, []); // Intentionally empty - we want initial value only
|
|
144
|
+
|
|
145
|
+
// Convert HTML to Slate format - memoized for performance
|
|
146
|
+
const slateValue = useMemo(() => {
|
|
147
|
+
try {
|
|
148
|
+
return htmlToSlate(value, simplifiedProfile);
|
|
149
|
+
} catch (error) {
|
|
150
|
+
console.warn("Failed to convert HTML to Slate:", error);
|
|
151
|
+
return [{ type: "paragraph", children: [{ text: "" }] }];
|
|
152
|
+
}
|
|
153
|
+
}, [value, simplifiedProfile]);
|
|
143
154
|
|
|
144
|
-
//
|
|
145
|
-
const
|
|
155
|
+
// Track previous value to detect external changes
|
|
156
|
+
const previousValueRef = useRef(value);
|
|
157
|
+
const isInternalChangeRef = useRef(false);
|
|
146
158
|
|
|
147
|
-
// Update
|
|
159
|
+
// Update editor when value changes from outside
|
|
148
160
|
useEffect(() => {
|
|
149
|
-
|
|
150
|
-
if (
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
161
|
+
// Skip if this is an internal change (from our own onChange)
|
|
162
|
+
if (isInternalChangeRef.current) {
|
|
163
|
+
isInternalChangeRef.current = false;
|
|
164
|
+
previousValueRef.current = value;
|
|
165
|
+
return;
|
|
154
166
|
}
|
|
155
|
-
}, [value, simplifiedProfile]);
|
|
156
167
|
|
|
157
|
-
|
|
168
|
+
// Only update if value actually changed from outside
|
|
169
|
+
if (value !== previousValueRef.current) {
|
|
170
|
+
previousValueRef.current = value;
|
|
171
|
+
|
|
172
|
+
// Update editor children with new value
|
|
173
|
+
const newChildren = slateValue;
|
|
174
|
+
|
|
175
|
+
// Prevent infinite loops by temporarily removing selection
|
|
176
|
+
const currentSelection = editor.selection;
|
|
177
|
+
|
|
178
|
+
// Update the editor's children
|
|
179
|
+
editor.children = newChildren;
|
|
180
|
+
|
|
181
|
+
// Normalize the editor to ensure consistency
|
|
182
|
+
Editor.normalize(editor, { force: true });
|
|
183
|
+
|
|
184
|
+
// Restore selection if it was valid, otherwise set to end
|
|
185
|
+
if (currentSelection && Editor.hasPath(editor, currentSelection.anchor.path)) {
|
|
186
|
+
try {
|
|
187
|
+
Transforms.select(editor, currentSelection);
|
|
188
|
+
} catch {
|
|
189
|
+
// If selection is invalid, move to end
|
|
190
|
+
Transforms.select(editor, Editor.end(editor, []));
|
|
191
|
+
}
|
|
192
|
+
} else {
|
|
193
|
+
// Move to end of document
|
|
194
|
+
Transforms.select(editor, Editor.end(editor, []));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}, [editor, slateValue, value]);
|
|
198
|
+
|
|
199
|
+
// Handle value changes with proper Slate patterns
|
|
158
200
|
const handleChange = useCallback(
|
|
159
201
|
(newValue: Descendant[]) => {
|
|
160
|
-
setInternalValue(newValue);
|
|
161
|
-
|
|
162
202
|
if (onChange) {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
simplifiedProfile,
|
|
172
|
-
);
|
|
173
|
-
|
|
174
|
-
if (isStable) {
|
|
175
|
-
// If stable, don't trigger onChange on initial load
|
|
176
|
-
isInitialLoadRef.current = false;
|
|
177
|
-
return;
|
|
203
|
+
try {
|
|
204
|
+
const html = slateToHtml(newValue, simplifiedProfile);
|
|
205
|
+
|
|
206
|
+
// Only trigger onChange if content actually changed
|
|
207
|
+
if (html !== value) {
|
|
208
|
+
// Mark this as an internal change
|
|
209
|
+
isInternalChangeRef.current = true;
|
|
210
|
+
onChange(html);
|
|
178
211
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
isInitialLoadRef.current = false;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// For subsequent changes, only trigger onChange if there's a meaningful difference
|
|
185
|
-
const normalizedNewHtml = normalizeHtmlForComparison(html);
|
|
186
|
-
const normalizedOriginalHtml = normalizeHtmlForComparison(
|
|
187
|
-
originalValueRef.current,
|
|
188
|
-
);
|
|
189
|
-
|
|
190
|
-
if (normalizedNewHtml !== normalizedOriginalHtml) {
|
|
191
|
-
// Update the original value reference to the new HTML
|
|
192
|
-
originalValueRef.current = html;
|
|
193
|
-
onChange(html);
|
|
212
|
+
} catch (error) {
|
|
213
|
+
console.warn("Failed to convert Slate to HTML:", error);
|
|
194
214
|
}
|
|
195
215
|
}
|
|
196
216
|
},
|
|
197
|
-
[onChange, simplifiedProfile],
|
|
217
|
+
[onChange, simplifiedProfile, value],
|
|
198
218
|
);
|
|
199
219
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
220
|
+
// Link dialog state
|
|
221
|
+
const [showLinkDialog, setShowLinkDialog] = useState(false);
|
|
222
|
+
const [selectedLink, setSelectedLink] = useState<any>(null);
|
|
223
|
+
const [linkDialogCallback, setLinkDialogCallback] = useState<
|
|
224
|
+
((link: any) => void) | null
|
|
225
|
+
>(null);
|
|
226
|
+
|
|
227
|
+
const handleLinkButtonClick = useCallback(() => {
|
|
228
|
+
editor.insertLink({
|
|
229
|
+
onOpenLinkDialog: (callback: (link: any) => void) => {
|
|
230
|
+
setSelectedLink({
|
|
231
|
+
type: "external",
|
|
232
|
+
url: "",
|
|
233
|
+
target: "_blank",
|
|
234
|
+
});
|
|
235
|
+
setShowLinkDialog(true);
|
|
236
|
+
|
|
237
|
+
setLinkDialogCallback(() => callback);
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
}, [editor]);
|
|
241
|
+
|
|
242
|
+
// Memoize option handlers to prevent creating new functions on every render
|
|
243
|
+
const optionHandlers = useMemo(() => {
|
|
244
|
+
const handlers: Record<string, (editor: Editor, event: React.MouseEvent) => void> = {};
|
|
245
|
+
|
|
246
|
+
const handleOptionSelect = (option: ToolbarOptionConfig) =>
|
|
247
|
+
(editor: Editor, event: React.MouseEvent) => {
|
|
248
|
+
event.preventDefault();
|
|
249
|
+
|
|
250
|
+
switch (option.type) {
|
|
251
|
+
case "mark":
|
|
252
|
+
editor.toggleMark(option.id);
|
|
253
|
+
break;
|
|
254
|
+
case "block":
|
|
255
|
+
editor.toggleBlock(option.id);
|
|
256
|
+
break;
|
|
257
|
+
case "alignment":
|
|
258
|
+
const alignConfig = SLATE_ALIGNMENTS[option.id];
|
|
259
|
+
editor.toggleAlign(alignConfig.value);
|
|
260
|
+
break;
|
|
261
|
+
case "link":
|
|
262
|
+
handleLinkButtonClick();
|
|
263
|
+
break;
|
|
264
|
+
case "list":
|
|
265
|
+
const listType = option.id === "unordered-list" ? "unordered" : "ordered";
|
|
266
|
+
editor.toggleList(listType);
|
|
267
|
+
break;
|
|
268
|
+
case "insertion":
|
|
269
|
+
if (option.id === "horizontal-rule") {
|
|
270
|
+
editor.insertHorizontalRule();
|
|
214
271
|
}
|
|
215
|
-
|
|
272
|
+
break;
|
|
273
|
+
default:
|
|
274
|
+
console.warn(`Unhandled option type: ${option.type}`);
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
// Pre-create handlers for all possible options
|
|
279
|
+
editorProfile.toolbar.groups.forEach(group => {
|
|
280
|
+
group.options.forEach(option => {
|
|
281
|
+
const key = `${option.type}-${option.id}`;
|
|
282
|
+
handlers[key] = handleOptionSelect(option);
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
return handlers;
|
|
287
|
+
}, [editor, editorProfile.toolbar.groups, handleLinkButtonClick]);
|
|
288
|
+
|
|
289
|
+
// Create button handlers that adapt the signature for toolbar buttons
|
|
290
|
+
const buttonHandlers = useMemo(() => {
|
|
291
|
+
const handlers: Record<string, (event: React.MouseEvent<HTMLButtonElement>) => void> = {};
|
|
292
|
+
|
|
293
|
+
Object.entries(optionHandlers).forEach(([key, handler]) => {
|
|
294
|
+
handlers[key] = (event: React.MouseEvent<HTMLButtonElement>) => {
|
|
295
|
+
handler(editor, event);
|
|
296
|
+
};
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
return handlers;
|
|
300
|
+
}, [optionHandlers, editor]);
|
|
301
|
+
|
|
302
|
+
const getOption = useCallback((option: ToolbarOptionConfig): CustomOption | undefined => {
|
|
303
|
+
const handlerKey = `${option.type}-${option.id}`;
|
|
304
|
+
|
|
305
|
+
switch (option.type) {
|
|
306
|
+
case "mark":
|
|
307
|
+
const markConfig = SLATE_MARKS[option.id];
|
|
308
|
+
return {
|
|
309
|
+
id: option.id,
|
|
310
|
+
label: markConfig.label,
|
|
311
|
+
icon: markConfig.icon,
|
|
312
|
+
isActive: (editor: Editor) => editor.isMarkActive(option.id),
|
|
313
|
+
toggle: optionHandlers[handlerKey],
|
|
314
|
+
};
|
|
216
315
|
case "block":
|
|
217
|
-
const blockConfig = SLATE_BLOCKS[id];
|
|
218
|
-
return
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
event.preventDefault();
|
|
226
|
-
editor.toggleBlock(id);
|
|
227
|
-
},
|
|
228
|
-
}
|
|
229
|
-
: undefined;
|
|
316
|
+
const blockConfig = SLATE_BLOCKS[option.id];
|
|
317
|
+
return {
|
|
318
|
+
id: option.id,
|
|
319
|
+
label: blockConfig.label,
|
|
320
|
+
icon: blockConfig.icon,
|
|
321
|
+
isActive: (editor: Editor) => editor.isBlockActive(option.id),
|
|
322
|
+
toggle: optionHandlers[handlerKey],
|
|
323
|
+
};
|
|
230
324
|
case "alignment":
|
|
231
|
-
const alignConfig = SLATE_ALIGNMENTS[id];
|
|
232
|
-
return
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
toggle: (editor: Editor, event: React.MouseEvent) => {
|
|
240
|
-
event.preventDefault();
|
|
241
|
-
editor.toggleAlign(alignConfig.value);
|
|
242
|
-
},
|
|
243
|
-
}
|
|
244
|
-
: undefined;
|
|
325
|
+
const alignConfig = SLATE_ALIGNMENTS[option.id];
|
|
326
|
+
return {
|
|
327
|
+
id: option.id,
|
|
328
|
+
label: alignConfig.label,
|
|
329
|
+
icon: alignConfig.icon,
|
|
330
|
+
isActive: (editor: Editor) => editor.isAlignActive(alignConfig.value),
|
|
331
|
+
toggle: optionHandlers[handlerKey],
|
|
332
|
+
};
|
|
245
333
|
case "link":
|
|
246
334
|
return {
|
|
247
335
|
id: "link",
|
|
248
336
|
label: "Link",
|
|
249
337
|
icon: "🔗",
|
|
250
338
|
isActive: (editor: Editor) => editor.isLinkActive(),
|
|
251
|
-
toggle:
|
|
252
|
-
event.preventDefault();
|
|
253
|
-
handleLinkButtonClick();
|
|
254
|
-
},
|
|
339
|
+
toggle: optionHandlers[handlerKey],
|
|
255
340
|
};
|
|
256
341
|
case "list":
|
|
257
342
|
return {
|
|
258
|
-
id,
|
|
259
|
-
label: id === "unordered-list" ? "Bulleted List" : "Numbered List",
|
|
260
|
-
icon: id === "unordered-list" ? "•" : "1.",
|
|
261
|
-
isActive: (editor: Editor) =>
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
toggle: (editor: Editor, event: React.MouseEvent) => {
|
|
266
|
-
event.preventDefault();
|
|
267
|
-
editor.toggleList(
|
|
268
|
-
id === "unordered-list" ? "unordered" : "ordered",
|
|
269
|
-
);
|
|
270
|
-
},
|
|
343
|
+
id: option.id,
|
|
344
|
+
label: option.id === "unordered-list" ? "Bulleted List" : "Numbered List",
|
|
345
|
+
icon: option.id === "unordered-list" ? "•" : "1.",
|
|
346
|
+
isActive: (editor: Editor) => editor.isListActive(
|
|
347
|
+
option.id === "unordered-list" ? "unordered" : "ordered"
|
|
348
|
+
),
|
|
349
|
+
toggle: optionHandlers[handlerKey],
|
|
271
350
|
};
|
|
272
351
|
case "divider":
|
|
273
352
|
return { id: "divider", label: "Divider" };
|
|
353
|
+
case "insertion":
|
|
354
|
+
if (option.id === "horizontal-rule") {
|
|
355
|
+
return {
|
|
356
|
+
id: option.id,
|
|
357
|
+
label: "Horizontal Rule",
|
|
358
|
+
icon: "─",
|
|
359
|
+
isActive: () => false, // Insertion buttons are never "active"
|
|
360
|
+
toggle: optionHandlers[handlerKey],
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
return undefined;
|
|
274
364
|
default:
|
|
275
365
|
return undefined;
|
|
276
366
|
}
|
|
277
|
-
};
|
|
367
|
+
}, [optionHandlers]);
|
|
278
368
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
toggle: (id: string) => editor.toggleMark(id),
|
|
283
|
-
},
|
|
284
|
-
block: {
|
|
285
|
-
isActive: (id: string) => editor.isBlockActive(id),
|
|
286
|
-
toggle: (id: string) => editor.toggleBlock(id),
|
|
287
|
-
},
|
|
288
|
-
alignment: {
|
|
289
|
-
isActive: (id: string) => {
|
|
290
|
-
const alignConfig = SLATE_ALIGNMENTS[id];
|
|
291
|
-
return alignConfig ? editor.isAlignActive(alignConfig.value) : false;
|
|
292
|
-
},
|
|
293
|
-
toggle: (id: string) => {
|
|
294
|
-
const alignConfig = SLATE_ALIGNMENTS[id];
|
|
295
|
-
if (alignConfig) {
|
|
296
|
-
editor.toggleAlign(alignConfig.value);
|
|
297
|
-
}
|
|
298
|
-
},
|
|
299
|
-
},
|
|
300
|
-
link: {
|
|
301
|
-
isActive: () => editor.isLinkActive(),
|
|
302
|
-
toggle: () => handleLinkButtonClick(),
|
|
303
|
-
},
|
|
304
|
-
list: {
|
|
305
|
-
isActive: (id: string) => {
|
|
306
|
-
const listType = id === "unordered-list" ? "unordered" : "ordered";
|
|
307
|
-
return editor.isListActive(listType);
|
|
308
|
-
},
|
|
309
|
-
toggle: (id: string, event: React.MouseEvent<HTMLButtonElement>) => {
|
|
310
|
-
const listType = id === "unordered-list" ? "unordered" : "ordered";
|
|
311
|
-
editor.toggleList(listType);
|
|
312
|
-
},
|
|
313
|
-
},
|
|
314
|
-
};
|
|
315
|
-
|
|
316
|
-
const isOptionActive = (type: string, id: string): boolean => {
|
|
317
|
-
const handler = optionHandlers[type as keyof typeof optionHandlers];
|
|
318
|
-
return handler ? handler.isActive(id) : false;
|
|
319
|
-
};
|
|
320
|
-
|
|
321
|
-
const handleOptionSelect =
|
|
322
|
-
(type: string, id: string) =>
|
|
323
|
-
(event: React.MouseEvent<HTMLButtonElement>) => {
|
|
324
|
-
event.preventDefault();
|
|
325
|
-
const handler = optionHandlers[type as keyof typeof optionHandlers];
|
|
326
|
-
if (handler) {
|
|
327
|
-
return handler.toggle(id, event);
|
|
328
|
-
}
|
|
329
|
-
console.warn(`Unhandled option type: ${type}`);
|
|
330
|
-
};
|
|
331
|
-
|
|
332
|
-
const createDropdownOptions = (
|
|
333
|
-
options: ToolbarOptionConfig[],
|
|
369
|
+
// Memoize dropdown options creation with strict type safety
|
|
370
|
+
const createDropdownOptions = useCallback((
|
|
371
|
+
options: readonly ToolbarOptionConfig[],
|
|
334
372
|
): DropdownOption<any>[] => {
|
|
335
373
|
return options
|
|
336
374
|
.map((option) => {
|
|
337
|
-
const optionObj = getOption(option
|
|
375
|
+
const optionObj = getOption(option);
|
|
338
376
|
if (!optionObj) return null;
|
|
339
377
|
|
|
340
378
|
return {
|
|
@@ -342,19 +380,31 @@ export const ReactSlate = forwardRef<any, ReactSlateProps>((props, ref) => {
|
|
|
342
380
|
label: optionObj.label || option.id,
|
|
343
381
|
icon: optionObj.icon,
|
|
344
382
|
style: (optionObj as any).style,
|
|
345
|
-
isActive: (editor: Editor) =>
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
383
|
+
isActive: (editor: Editor) => {
|
|
384
|
+
switch (option.type) {
|
|
385
|
+
case "mark":
|
|
386
|
+
return editor.isMarkActive(option.id);
|
|
387
|
+
case "block":
|
|
388
|
+
return editor.isBlockActive(option.id);
|
|
389
|
+
case "alignment":
|
|
390
|
+
return editor.isAlignActive(SLATE_ALIGNMENTS[option.id].value);
|
|
391
|
+
case "link":
|
|
392
|
+
return editor.isLinkActive();
|
|
393
|
+
case "list":
|
|
394
|
+
const listType = option.id === "unordered-list" ? "unordered" : "ordered";
|
|
395
|
+
return editor.isListActive(listType);
|
|
396
|
+
default:
|
|
397
|
+
return false;
|
|
398
|
+
}
|
|
399
|
+
},
|
|
400
|
+
onSelect: optionHandlers[`${option.type}-${option.id}`],
|
|
351
401
|
};
|
|
352
402
|
})
|
|
353
403
|
.filter(Boolean) as DropdownOption<any>[];
|
|
354
|
-
};
|
|
404
|
+
}, [getOption, optionHandlers]);
|
|
355
405
|
|
|
356
406
|
// Helper function to split options by dividers into sub-groups
|
|
357
|
-
const splitOptionsByDividers = (
|
|
407
|
+
const splitOptionsByDividers = useCallback((
|
|
358
408
|
options: ToolbarOptionConfig[],
|
|
359
409
|
): ToolbarOptionConfig[][] => {
|
|
360
410
|
const subGroups: ToolbarOptionConfig[][] = [];
|
|
@@ -377,12 +427,13 @@ export const ReactSlate = forwardRef<any, ReactSlateProps>((props, ref) => {
|
|
|
377
427
|
}
|
|
378
428
|
|
|
379
429
|
return subGroups;
|
|
380
|
-
};
|
|
430
|
+
}, []);
|
|
381
431
|
|
|
382
|
-
|
|
432
|
+
// Memoize toolbar group rendering
|
|
433
|
+
const renderToolbarGroup = useCallback((group: ToolbarGroupConfig, index: number) => {
|
|
383
434
|
const validOptions = group.options.filter(
|
|
384
435
|
(option) =>
|
|
385
|
-
getOption(option
|
|
436
|
+
getOption(option) || option.type === "divider",
|
|
386
437
|
);
|
|
387
438
|
|
|
388
439
|
if (validOptions.length === 0) return null;
|
|
@@ -404,16 +455,18 @@ export const ReactSlate = forwardRef<any, ReactSlateProps>((props, ref) => {
|
|
|
404
455
|
key={`subgroup-${subGroupIndex}`}
|
|
405
456
|
className="toolbar-button-group"
|
|
406
457
|
>
|
|
407
|
-
{subGroup.map((option
|
|
408
|
-
const optionObj = getOption(option
|
|
409
|
-
|
|
458
|
+
{subGroup.map((option) => {
|
|
459
|
+
const optionObj = getOption(option);
|
|
460
|
+
const handler = buttonHandlers[`${option.type}-${option.id}`];
|
|
461
|
+
if (!optionObj || !handler) return null;
|
|
410
462
|
|
|
411
463
|
return (
|
|
412
|
-
<
|
|
464
|
+
<ToolbarButtonWrapper
|
|
413
465
|
key={`${option.type}-${option.id}`}
|
|
466
|
+
option={option}
|
|
414
467
|
icon={optionObj.icon}
|
|
415
|
-
|
|
416
|
-
onMouseDown={
|
|
468
|
+
editor={editor}
|
|
469
|
+
onMouseDown={handler}
|
|
417
470
|
/>
|
|
418
471
|
);
|
|
419
472
|
})}
|
|
@@ -432,7 +485,7 @@ export const ReactSlate = forwardRef<any, ReactSlateProps>((props, ref) => {
|
|
|
432
485
|
blockOptions.some(
|
|
433
486
|
(blockOpt) =>
|
|
434
487
|
blockOpt.type === "block" &&
|
|
435
|
-
getOption(blockOpt
|
|
488
|
+
getOption(blockOpt) === opt.value,
|
|
436
489
|
),
|
|
437
490
|
);
|
|
438
491
|
|
|
@@ -477,36 +530,51 @@ export const ReactSlate = forwardRef<any, ReactSlateProps>((props, ref) => {
|
|
|
477
530
|
const isActive = dropdownOptions.some((option) =>
|
|
478
531
|
option.isActive(editor),
|
|
479
532
|
);
|
|
533
|
+
|
|
534
|
+
const buttonStyle = {
|
|
535
|
+
padding: "5px 10px",
|
|
536
|
+
margin: "0",
|
|
537
|
+
background: isActive ? "#ffffff" : "transparent",
|
|
538
|
+
border: "none",
|
|
539
|
+
borderRadius: "3px",
|
|
540
|
+
cursor: "pointer",
|
|
541
|
+
boxShadow: isActive
|
|
542
|
+
? "0 1px 2px rgba(0,0,0,0.1)"
|
|
543
|
+
: "none",
|
|
544
|
+
};
|
|
545
|
+
|
|
480
546
|
return (
|
|
481
547
|
<div className="toolbar-dropdown-container">
|
|
482
|
-
<
|
|
548
|
+
<MemoizedEditorDropdown
|
|
483
549
|
options={dropdownOptions}
|
|
484
550
|
editor={editor}
|
|
485
551
|
label={group.label}
|
|
486
|
-
buttonStyle={
|
|
487
|
-
padding: "5px 10px",
|
|
488
|
-
margin: "0",
|
|
489
|
-
background: isActive ? "#ffffff" : "transparent",
|
|
490
|
-
border: "none",
|
|
491
|
-
borderRadius: "3px",
|
|
492
|
-
cursor: "pointer",
|
|
493
|
-
boxShadow: isActive
|
|
494
|
-
? "0 1px 2px rgba(0,0,0,0.1)"
|
|
495
|
-
: "none",
|
|
496
|
-
}}
|
|
552
|
+
buttonStyle={buttonStyle}
|
|
497
553
|
/>
|
|
498
554
|
</div>
|
|
499
555
|
);
|
|
500
556
|
})()}
|
|
501
557
|
</div>
|
|
502
558
|
);
|
|
503
|
-
};
|
|
559
|
+
}, [getOption, splitOptionsByDividers, createDropdownOptions, optionHandlers, buttonHandlers, editor]);
|
|
560
|
+
|
|
561
|
+
// Memoize toolbar structure (expensive grouping operation)
|
|
562
|
+
const toolbarStructure = useMemo(() => {
|
|
563
|
+
// Group toolbar items by row
|
|
564
|
+
const groupsByRow = editorProfile.toolbar.groups.reduce(
|
|
565
|
+
(acc, group, index) => {
|
|
566
|
+
const row = group.row !== undefined ? group.row : index;
|
|
567
|
+
if (!acc[row]) acc[row] = [];
|
|
568
|
+
acc[row].push(group);
|
|
569
|
+
return acc;
|
|
570
|
+
},
|
|
571
|
+
{} as Record<number, ToolbarGroupConfig[]>,
|
|
572
|
+
);
|
|
504
573
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
>(null);
|
|
574
|
+
// Return sorted entries for consistent rendering
|
|
575
|
+
return Object.entries(groupsByRow)
|
|
576
|
+
.sort(([a], [b]) => parseInt(a) - parseInt(b));
|
|
577
|
+
}, [editorProfile.toolbar.groups]);
|
|
510
578
|
|
|
511
579
|
const editLink = useCallback((element: any) => {
|
|
512
580
|
const linkType = element.link?.type || "external";
|
|
@@ -592,129 +660,25 @@ export const ReactSlate = forwardRef<any, ReactSlateProps>((props, ref) => {
|
|
|
592
660
|
[editor, linkDialogCallback],
|
|
593
661
|
);
|
|
594
662
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
onOpenLinkDialog: (callback: (link: any) => void) => {
|
|
598
|
-
setSelectedLink({
|
|
599
|
-
type: "external",
|
|
600
|
-
url: "",
|
|
601
|
-
target: "_blank",
|
|
602
|
-
});
|
|
603
|
-
setShowLinkDialog(true);
|
|
604
|
-
|
|
605
|
-
setLinkDialogCallback(() => callback);
|
|
606
|
-
},
|
|
607
|
-
});
|
|
608
|
-
}, [editor]);
|
|
609
|
-
|
|
610
|
-
const handleKeyDown = useCallback(
|
|
611
|
-
(event: React.KeyboardEvent) => {
|
|
612
|
-
if (event.key === "Enter" && event.shiftKey) {
|
|
613
|
-
// Handle Shift+Enter for line breaks in all block types
|
|
614
|
-
event.preventDefault();
|
|
615
|
-
|
|
616
|
-
if (editor.selection) {
|
|
617
|
-
// Insert literal <br> text
|
|
618
|
-
editor.insertText("<br>");
|
|
619
|
-
}
|
|
620
|
-
} else if (event.key === "Tab") {
|
|
621
|
-
event.preventDefault();
|
|
622
|
-
|
|
623
|
-
// Check if we're in a list item more explicitly
|
|
624
|
-
if (editor.selection) {
|
|
625
|
-
// Try to find the closest list item ancestor using Editor.above
|
|
626
|
-
const listItem = Editor.above(editor, {
|
|
627
|
-
at: editor.selection,
|
|
628
|
-
match: (n) =>
|
|
629
|
-
!Editor.isEditor(n) &&
|
|
630
|
-
Element.isElement(n) &&
|
|
631
|
-
n.type === "list-item",
|
|
632
|
-
});
|
|
633
|
-
|
|
634
|
-
if (listItem) {
|
|
635
|
-
if (event.shiftKey) {
|
|
636
|
-
// Shift+Tab: Outdent
|
|
637
|
-
editor.outdentList();
|
|
638
|
-
} else {
|
|
639
|
-
// Tab: Indent
|
|
640
|
-
editor.indentList();
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
} else if (event.key === "Backspace" || event.key === "Delete") {
|
|
645
|
-
// Handle empty list items
|
|
646
|
-
if (editor.selection) {
|
|
647
|
-
const listItem = Editor.above(editor, {
|
|
648
|
-
at: editor.selection,
|
|
649
|
-
match: (n) =>
|
|
650
|
-
!Editor.isEditor(n) &&
|
|
651
|
-
Element.isElement(n) &&
|
|
652
|
-
n.type === "list-item",
|
|
653
|
-
});
|
|
654
|
-
|
|
655
|
-
if (listItem) {
|
|
656
|
-
const selectedText = Editor.string(editor, editor.selection);
|
|
657
|
-
const [node, path] = listItem;
|
|
658
|
-
const nodeText = Editor.string(editor, path);
|
|
659
|
-
|
|
660
|
-
// If the list item is empty or only contains whitespace, convert to paragraph
|
|
661
|
-
if (
|
|
662
|
-
!nodeText.trim() ||
|
|
663
|
-
(selectedText === nodeText && nodeText.trim())
|
|
664
|
-
) {
|
|
665
|
-
event.preventDefault();
|
|
666
|
-
Transforms.setNodes(
|
|
667
|
-
editor,
|
|
668
|
-
{ type: "paragraph", listType: undefined, indent: undefined },
|
|
669
|
-
{
|
|
670
|
-
match: (n) =>
|
|
671
|
-
!Editor.isEditor(n) &&
|
|
672
|
-
Element.isElement(n) &&
|
|
673
|
-
n.type === "list-item",
|
|
674
|
-
split: true,
|
|
675
|
-
},
|
|
676
|
-
);
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
},
|
|
682
|
-
[editor],
|
|
683
|
-
);
|
|
663
|
+
// Use the keyboard handler from plugins
|
|
664
|
+
const handleKeyDown = useMemo(() => createKeyboardHandler(editor), [editor]);
|
|
684
665
|
|
|
685
666
|
return (
|
|
686
667
|
<div className={`slate-editor ${props.className}`}>
|
|
687
668
|
<Slate
|
|
688
|
-
key={remountKey}
|
|
689
669
|
editor={editor}
|
|
690
|
-
initialValue={
|
|
670
|
+
initialValue={initialSlateValue}
|
|
691
671
|
onChange={handleChange}
|
|
692
672
|
>
|
|
693
673
|
{!readOnly && (
|
|
694
674
|
<div className="toolbar">
|
|
695
|
-
{(() =>
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
return acc;
|
|
703
|
-
},
|
|
704
|
-
{} as Record<number, typeof editorProfile.toolbar.groups>,
|
|
705
|
-
);
|
|
706
|
-
|
|
707
|
-
// Render each row
|
|
708
|
-
return Object.entries(groupsByRow)
|
|
709
|
-
.sort(([a], [b]) => parseInt(a) - parseInt(b))
|
|
710
|
-
.map(([rowIndex, rowGroups]) => (
|
|
711
|
-
<div key={`row-${rowIndex}`} className="toolbar-row">
|
|
712
|
-
{rowGroups.map((group) =>
|
|
713
|
-
renderToolbarGroup(group, parseInt(rowIndex)),
|
|
714
|
-
)}
|
|
715
|
-
</div>
|
|
716
|
-
));
|
|
717
|
-
})()}
|
|
675
|
+
{toolbarStructure.map(([rowIndex, rowGroups]) => (
|
|
676
|
+
<div key={`row-${rowIndex}`} className="toolbar-row">
|
|
677
|
+
{rowGroups.map((group) =>
|
|
678
|
+
renderToolbarGroup(group, parseInt(rowIndex)),
|
|
679
|
+
)}
|
|
680
|
+
</div>
|
|
681
|
+
))}
|
|
718
682
|
</div>
|
|
719
683
|
)}
|
|
720
684
|
<Editable
|
|
@@ -766,46 +730,12 @@ export const ReactSlate = forwardRef<any, ReactSlateProps>((props, ref) => {
|
|
|
766
730
|
const listType = element.listType || "unordered";
|
|
767
731
|
const isOrdered = listType === "ordered";
|
|
768
732
|
|
|
769
|
-
// Calculate proper numbering for ordered lists
|
|
770
|
-
let listNumber = 1;
|
|
771
|
-
if (isOrdered) {
|
|
772
|
-
// Find the position of this item within its level
|
|
773
|
-
const allElements = editor.children as CustomElement[];
|
|
774
|
-
const currentIndex = allElements.findIndex(
|
|
775
|
-
(el) => el === element,
|
|
776
|
-
);
|
|
777
|
-
|
|
778
|
-
// Count preceding list items at the same indent level and list type
|
|
779
|
-
let count = 0;
|
|
780
|
-
for (let i = 0; i < currentIndex; i++) {
|
|
781
|
-
const prevElement = allElements[i];
|
|
782
|
-
if (
|
|
783
|
-
prevElement &&
|
|
784
|
-
prevElement.type === "list-item" &&
|
|
785
|
-
prevElement.listType === "ordered" &&
|
|
786
|
-
(prevElement.indent || 0) === indent
|
|
787
|
-
) {
|
|
788
|
-
count++;
|
|
789
|
-
} else if (
|
|
790
|
-
prevElement &&
|
|
791
|
-
prevElement.type === "list-item" &&
|
|
792
|
-
(prevElement.indent || 0) < indent
|
|
793
|
-
) {
|
|
794
|
-
// Reset count when we encounter a parent-level item
|
|
795
|
-
count = 0;
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
listNumber = count + 1;
|
|
799
|
-
}
|
|
800
|
-
|
|
801
733
|
const listStyle: React.CSSProperties = {
|
|
802
734
|
...style,
|
|
803
735
|
position: "relative",
|
|
804
736
|
listStyleType: "none",
|
|
805
737
|
};
|
|
806
738
|
|
|
807
|
-
const bulletContent = isOrdered ? `${listNumber}.` : "";
|
|
808
|
-
|
|
809
739
|
return (
|
|
810
740
|
<div
|
|
811
741
|
{...attributes}
|
|
@@ -813,20 +743,35 @@ export const ReactSlate = forwardRef<any, ReactSlateProps>((props, ref) => {
|
|
|
813
743
|
className={`slate-list-item slate-list-${listType}`}
|
|
814
744
|
data-indent={indent}
|
|
815
745
|
>
|
|
816
|
-
<span className="slate-list-bullet"
|
|
746
|
+
<span className="slate-list-bullet"></span>
|
|
817
747
|
<div className="slate-list-content">{children}</div>
|
|
818
748
|
</div>
|
|
819
749
|
);
|
|
820
750
|
}
|
|
821
751
|
|
|
752
|
+
if (element.type === "horizontal-rule") {
|
|
753
|
+
return (
|
|
754
|
+
<div {...attributes} contentEditable={false} style={{ ...style, userSelect: "none" }}>
|
|
755
|
+
<hr style={{
|
|
756
|
+
border: "none",
|
|
757
|
+
borderTop: "1px solid #ccc",
|
|
758
|
+
margin: "1em 0",
|
|
759
|
+
width: "100%"
|
|
760
|
+
}} />
|
|
761
|
+
{children}
|
|
762
|
+
</div>
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
|
|
822
766
|
// Handle different block types using built-in SLATE_BLOCKS configuration
|
|
823
|
-
const
|
|
767
|
+
const isValidBlockId = element.type in SLATE_BLOCKS;
|
|
768
|
+
const blockConfig = isValidBlockId ? SLATE_BLOCKS[element.type as BlockId] : undefined;
|
|
824
769
|
if (blockConfig && element.type === "no-tag") {
|
|
825
770
|
// Special handling for no-tag blocks (plain text without wrapper)
|
|
826
771
|
return (
|
|
827
|
-
<
|
|
772
|
+
<div {...attributes} style={style}>
|
|
828
773
|
{children}
|
|
829
|
-
</
|
|
774
|
+
</div>
|
|
830
775
|
);
|
|
831
776
|
}
|
|
832
777
|
|