@alpaca-editor/core 1.0.4045 → 1.0.4048
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 +300 -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 +375 -428
- 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
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import React, { useCallback, useMemo, useState, forwardRef, useRef, useEffect, } from "react";
|
|
2
|
+
import React, { useCallback, useMemo, useState, forwardRef, useRef, useEffect, memo, } from "react";
|
|
3
3
|
import { createEditor, Editor, Element, Transforms } from "slate";
|
|
4
4
|
import { Slate, Editable, withReact, ReactEditor } from "slate-react";
|
|
5
5
|
import { withHistory } from "slate-history";
|
|
6
|
-
import isEqual from "lodash/isEqual";
|
|
7
6
|
import "./ReactSlate.css";
|
|
8
7
|
import { SLATE_MARKS, SLATE_BLOCKS, SLATE_ALIGNMENTS, } from "../types";
|
|
9
8
|
import EditorDropdown from "./EditorDropdown";
|
|
@@ -11,63 +10,45 @@ import { ToolbarButton } from "./ToolbarButton";
|
|
|
11
10
|
import { LinkEditorDialog } from "../../../LinkEditorDialog";
|
|
12
11
|
import { htmlToSlate, slateToHtml } from "../utils/conversion";
|
|
13
12
|
import { createPluginsFromConfig } from "../config/pluginFactory";
|
|
13
|
+
import { createKeyboardHandler } from "../utils/plugins";
|
|
14
14
|
import { useCachedSimplifiedProfile } from "../hooks/useProfileCache";
|
|
15
15
|
import { classNames } from "primereact/utils";
|
|
16
|
-
//
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const normalizedOriginal = normalizeHtmlForComparison(originalHtml);
|
|
43
|
-
const normalizedConverted = normalizeHtmlForComparison(convertedHtml);
|
|
44
|
-
// Debug logging (can be enabled for troubleshooting)
|
|
45
|
-
// console.log('HTML Stability Check:', {
|
|
46
|
-
// original: normalizedOriginal,
|
|
47
|
-
// converted: normalizedConverted,
|
|
48
|
-
// stable: normalizedOriginal === normalizedConverted
|
|
49
|
-
// });
|
|
50
|
-
// Check if they're equivalent
|
|
51
|
-
return normalizedOriginal === normalizedConverted;
|
|
52
|
-
}
|
|
53
|
-
catch (error) {
|
|
54
|
-
console.warn("HTML conversion stability check failed:", error);
|
|
55
|
-
// If we can't check stability, assume it's not stable to be safe
|
|
56
|
-
return false;
|
|
16
|
+
// Toolbar button component - not memoized to allow reactivity to editor state changes
|
|
17
|
+
const ToolbarButtonWrapper = ({ option, icon, editor, onMouseDown }) => {
|
|
18
|
+
// Calculate active state on every render - this is lightweight and ensures reactivity
|
|
19
|
+
let isActive = false;
|
|
20
|
+
switch (option.type) {
|
|
21
|
+
case "mark":
|
|
22
|
+
isActive = editor.isMarkActive(option.id);
|
|
23
|
+
break;
|
|
24
|
+
case "block":
|
|
25
|
+
isActive = editor.isBlockActive(option.id);
|
|
26
|
+
break;
|
|
27
|
+
case "alignment":
|
|
28
|
+
isActive = editor.isAlignActive(SLATE_ALIGNMENTS[option.id].value);
|
|
29
|
+
break;
|
|
30
|
+
case "link":
|
|
31
|
+
isActive = editor.isLinkActive();
|
|
32
|
+
break;
|
|
33
|
+
case "list":
|
|
34
|
+
const listType = option.id === "unordered-list" ? "unordered" : "ordered";
|
|
35
|
+
isActive = editor.isListActive(listType);
|
|
36
|
+
break;
|
|
37
|
+
case "insertion":
|
|
38
|
+
isActive = false; // Insertion buttons are never active
|
|
39
|
+
break;
|
|
40
|
+
default:
|
|
41
|
+
isActive = false;
|
|
57
42
|
}
|
|
43
|
+
return (_jsx(ToolbarButton, { icon: icon, active: isActive, onMouseDown: onMouseDown }));
|
|
58
44
|
};
|
|
45
|
+
// Memoized dropdown component to prevent unnecessary re-renders
|
|
46
|
+
const MemoizedEditorDropdown = memo(({ options, editor, label, buttonStyle }) => {
|
|
47
|
+
return (_jsx(EditorDropdown, { options: options, editor: editor, label: label, buttonStyle: buttonStyle }));
|
|
48
|
+
});
|
|
49
|
+
MemoizedEditorDropdown.displayName = "MemoizedEditorDropdown";
|
|
59
50
|
export const ReactSlate = forwardRef((props, ref) => {
|
|
60
51
|
const { value = "", onChange, onFocus, onBlur, readOnly = false, placeholder = "Enter some text...", profile, } = props;
|
|
61
|
-
// Create the Slate editor with plugins
|
|
62
|
-
const editor = useMemo(() => {
|
|
63
|
-
// Start with base editor
|
|
64
|
-
let slateEditor = createEditor();
|
|
65
|
-
// Apply core plugins
|
|
66
|
-
slateEditor = withReact(slateEditor);
|
|
67
|
-
slateEditor = withHistory(slateEditor);
|
|
68
|
-
slateEditor = createPluginsFromConfig(slateEditor);
|
|
69
|
-
return slateEditor;
|
|
70
|
-
}, []);
|
|
71
52
|
const editorProfile = profile || {
|
|
72
53
|
toolbar: {
|
|
73
54
|
groups: [],
|
|
@@ -75,180 +56,225 @@ export const ReactSlate = forwardRef((props, ref) => {
|
|
|
75
56
|
};
|
|
76
57
|
// Convert to simplified profile for the conversion functions (with caching)
|
|
77
58
|
const simplifiedProfile = useCachedSimplifiedProfile(editorProfile);
|
|
78
|
-
//
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
59
|
+
// Create the Slate editor with plugins - only once
|
|
60
|
+
const editor = useMemo(() => {
|
|
61
|
+
const baseEditor = createEditor();
|
|
62
|
+
// Apply plugins in correct order
|
|
63
|
+
const enhancedEditor = createPluginsFromConfig(withHistory(withReact(baseEditor)));
|
|
64
|
+
return enhancedEditor;
|
|
65
|
+
}, []);
|
|
66
|
+
// Initial value for Slate - capture initial props at mount time
|
|
67
|
+
const initialSlateValue = useMemo(() => {
|
|
68
|
+
try {
|
|
69
|
+
return htmlToSlate(value, simplifiedProfile);
|
|
86
70
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
71
|
+
catch (error) {
|
|
72
|
+
console.warn("Failed to convert initial HTML to Slate:", error);
|
|
73
|
+
return [{ type: "paragraph", children: [{ text: "" }] }];
|
|
74
|
+
}
|
|
75
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
76
|
+
}, []); // Intentionally empty - we want initial value only
|
|
77
|
+
// Convert HTML to Slate format - memoized for performance
|
|
78
|
+
const slateValue = useMemo(() => {
|
|
79
|
+
try {
|
|
80
|
+
return htmlToSlate(value, simplifiedProfile);
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
console.warn("Failed to convert HTML to Slate:", error);
|
|
84
|
+
return [{ type: "paragraph", children: [{ text: "" }] }];
|
|
99
85
|
}
|
|
100
86
|
}, [value, simplifiedProfile]);
|
|
101
|
-
//
|
|
87
|
+
// Track previous value to detect external changes
|
|
88
|
+
const previousValueRef = useRef(value);
|
|
89
|
+
const isInternalChangeRef = useRef(false);
|
|
90
|
+
// Update editor when value changes from outside
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
// Skip if this is an internal change (from our own onChange)
|
|
93
|
+
if (isInternalChangeRef.current) {
|
|
94
|
+
isInternalChangeRef.current = false;
|
|
95
|
+
previousValueRef.current = value;
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
// Only update if value actually changed from outside
|
|
99
|
+
if (value !== previousValueRef.current) {
|
|
100
|
+
previousValueRef.current = value;
|
|
101
|
+
// Update editor children with new value
|
|
102
|
+
const newChildren = slateValue;
|
|
103
|
+
// Prevent infinite loops by temporarily removing selection
|
|
104
|
+
const currentSelection = editor.selection;
|
|
105
|
+
// Update the editor's children
|
|
106
|
+
editor.children = newChildren;
|
|
107
|
+
// Normalize the editor to ensure consistency
|
|
108
|
+
Editor.normalize(editor, { force: true });
|
|
109
|
+
// Restore selection if it was valid, otherwise set to end
|
|
110
|
+
if (currentSelection && Editor.hasPath(editor, currentSelection.anchor.path)) {
|
|
111
|
+
try {
|
|
112
|
+
Transforms.select(editor, currentSelection);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
// If selection is invalid, move to end
|
|
116
|
+
Transforms.select(editor, Editor.end(editor, []));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
// Move to end of document
|
|
121
|
+
Transforms.select(editor, Editor.end(editor, []));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}, [editor, slateValue, value]);
|
|
125
|
+
// Handle value changes with proper Slate patterns
|
|
102
126
|
const handleChange = useCallback((newValue) => {
|
|
103
|
-
setInternalValue(newValue);
|
|
104
127
|
if (onChange) {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
isInitialLoadRef.current = false;
|
|
113
|
-
return;
|
|
128
|
+
try {
|
|
129
|
+
const html = slateToHtml(newValue, simplifiedProfile);
|
|
130
|
+
// Only trigger onChange if content actually changed
|
|
131
|
+
if (html !== value) {
|
|
132
|
+
// Mark this as an internal change
|
|
133
|
+
isInternalChangeRef.current = true;
|
|
134
|
+
onChange(html);
|
|
114
135
|
}
|
|
115
|
-
// If not stable, we still need to proceed, but mark as no longer initial load
|
|
116
|
-
isInitialLoadRef.current = false;
|
|
117
136
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const normalizedOriginalHtml = normalizeHtmlForComparison(originalValueRef.current);
|
|
121
|
-
if (normalizedNewHtml !== normalizedOriginalHtml) {
|
|
122
|
-
// Update the original value reference to the new HTML
|
|
123
|
-
originalValueRef.current = html;
|
|
124
|
-
onChange(html);
|
|
137
|
+
catch (error) {
|
|
138
|
+
console.warn("Failed to convert Slate to HTML:", error);
|
|
125
139
|
}
|
|
126
140
|
}
|
|
127
|
-
}, [onChange, simplifiedProfile]);
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
141
|
+
}, [onChange, simplifiedProfile, value]);
|
|
142
|
+
// Link dialog state
|
|
143
|
+
const [showLinkDialog, setShowLinkDialog] = useState(false);
|
|
144
|
+
const [selectedLink, setSelectedLink] = useState(null);
|
|
145
|
+
const [linkDialogCallback, setLinkDialogCallback] = useState(null);
|
|
146
|
+
const handleLinkButtonClick = useCallback(() => {
|
|
147
|
+
editor.insertLink({
|
|
148
|
+
onOpenLinkDialog: (callback) => {
|
|
149
|
+
setSelectedLink({
|
|
150
|
+
type: "external",
|
|
151
|
+
url: "",
|
|
152
|
+
target: "_blank",
|
|
153
|
+
});
|
|
154
|
+
setShowLinkDialog(true);
|
|
155
|
+
setLinkDialogCallback(() => callback);
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
}, [editor]);
|
|
159
|
+
// Memoize option handlers to prevent creating new functions on every render
|
|
160
|
+
const optionHandlers = useMemo(() => {
|
|
161
|
+
const handlers = {};
|
|
162
|
+
const handleOptionSelect = (option) => (editor, event) => {
|
|
163
|
+
event.preventDefault();
|
|
164
|
+
switch (option.type) {
|
|
165
|
+
case "mark":
|
|
166
|
+
editor.toggleMark(option.id);
|
|
167
|
+
break;
|
|
168
|
+
case "block":
|
|
169
|
+
editor.toggleBlock(option.id);
|
|
170
|
+
break;
|
|
171
|
+
case "alignment":
|
|
172
|
+
const alignConfig = SLATE_ALIGNMENTS[option.id];
|
|
173
|
+
editor.toggleAlign(alignConfig.value);
|
|
174
|
+
break;
|
|
175
|
+
case "link":
|
|
176
|
+
handleLinkButtonClick();
|
|
177
|
+
break;
|
|
178
|
+
case "list":
|
|
179
|
+
const listType = option.id === "unordered-list" ? "unordered" : "ordered";
|
|
180
|
+
editor.toggleList(listType);
|
|
181
|
+
break;
|
|
182
|
+
case "insertion":
|
|
183
|
+
if (option.id === "horizontal-rule") {
|
|
184
|
+
editor.insertHorizontalRule();
|
|
142
185
|
}
|
|
143
|
-
|
|
186
|
+
break;
|
|
187
|
+
default:
|
|
188
|
+
console.warn(`Unhandled option type: ${option.type}`);
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
// Pre-create handlers for all possible options
|
|
192
|
+
editorProfile.toolbar.groups.forEach(group => {
|
|
193
|
+
group.options.forEach(option => {
|
|
194
|
+
const key = `${option.type}-${option.id}`;
|
|
195
|
+
handlers[key] = handleOptionSelect(option);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
return handlers;
|
|
199
|
+
}, [editor, editorProfile.toolbar.groups, handleLinkButtonClick]);
|
|
200
|
+
// Create button handlers that adapt the signature for toolbar buttons
|
|
201
|
+
const buttonHandlers = useMemo(() => {
|
|
202
|
+
const handlers = {};
|
|
203
|
+
Object.entries(optionHandlers).forEach(([key, handler]) => {
|
|
204
|
+
handlers[key] = (event) => {
|
|
205
|
+
handler(editor, event);
|
|
206
|
+
};
|
|
207
|
+
});
|
|
208
|
+
return handlers;
|
|
209
|
+
}, [optionHandlers, editor]);
|
|
210
|
+
const getOption = useCallback((option) => {
|
|
211
|
+
const handlerKey = `${option.type}-${option.id}`;
|
|
212
|
+
switch (option.type) {
|
|
213
|
+
case "mark":
|
|
214
|
+
const markConfig = SLATE_MARKS[option.id];
|
|
215
|
+
return {
|
|
216
|
+
id: option.id,
|
|
217
|
+
label: markConfig.label,
|
|
218
|
+
icon: markConfig.icon,
|
|
219
|
+
isActive: (editor) => editor.isMarkActive(option.id),
|
|
220
|
+
toggle: optionHandlers[handlerKey],
|
|
221
|
+
};
|
|
144
222
|
case "block":
|
|
145
|
-
const blockConfig = SLATE_BLOCKS[id];
|
|
146
|
-
return
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
event.preventDefault();
|
|
154
|
-
editor.toggleBlock(id);
|
|
155
|
-
},
|
|
156
|
-
}
|
|
157
|
-
: undefined;
|
|
223
|
+
const blockConfig = SLATE_BLOCKS[option.id];
|
|
224
|
+
return {
|
|
225
|
+
id: option.id,
|
|
226
|
+
label: blockConfig.label,
|
|
227
|
+
icon: blockConfig.icon,
|
|
228
|
+
isActive: (editor) => editor.isBlockActive(option.id),
|
|
229
|
+
toggle: optionHandlers[handlerKey],
|
|
230
|
+
};
|
|
158
231
|
case "alignment":
|
|
159
|
-
const alignConfig = SLATE_ALIGNMENTS[id];
|
|
160
|
-
return
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
event.preventDefault();
|
|
168
|
-
editor.toggleAlign(alignConfig.value);
|
|
169
|
-
},
|
|
170
|
-
}
|
|
171
|
-
: undefined;
|
|
232
|
+
const alignConfig = SLATE_ALIGNMENTS[option.id];
|
|
233
|
+
return {
|
|
234
|
+
id: option.id,
|
|
235
|
+
label: alignConfig.label,
|
|
236
|
+
icon: alignConfig.icon,
|
|
237
|
+
isActive: (editor) => editor.isAlignActive(alignConfig.value),
|
|
238
|
+
toggle: optionHandlers[handlerKey],
|
|
239
|
+
};
|
|
172
240
|
case "link":
|
|
173
241
|
return {
|
|
174
242
|
id: "link",
|
|
175
243
|
label: "Link",
|
|
176
244
|
icon: "🔗",
|
|
177
245
|
isActive: (editor) => editor.isLinkActive(),
|
|
178
|
-
toggle:
|
|
179
|
-
event.preventDefault();
|
|
180
|
-
handleLinkButtonClick();
|
|
181
|
-
},
|
|
246
|
+
toggle: optionHandlers[handlerKey],
|
|
182
247
|
};
|
|
183
248
|
case "list":
|
|
184
249
|
return {
|
|
185
|
-
id,
|
|
186
|
-
label: id === "unordered-list" ? "Bulleted List" : "Numbered List",
|
|
187
|
-
icon: id === "unordered-list" ? "•" : "1.",
|
|
188
|
-
isActive: (editor) => editor.isListActive(id === "unordered-list" ? "unordered" : "ordered"),
|
|
189
|
-
toggle:
|
|
190
|
-
event.preventDefault();
|
|
191
|
-
editor.toggleList(id === "unordered-list" ? "unordered" : "ordered");
|
|
192
|
-
},
|
|
250
|
+
id: option.id,
|
|
251
|
+
label: option.id === "unordered-list" ? "Bulleted List" : "Numbered List",
|
|
252
|
+
icon: option.id === "unordered-list" ? "•" : "1.",
|
|
253
|
+
isActive: (editor) => editor.isListActive(option.id === "unordered-list" ? "unordered" : "ordered"),
|
|
254
|
+
toggle: optionHandlers[handlerKey],
|
|
193
255
|
};
|
|
194
256
|
case "divider":
|
|
195
257
|
return { id: "divider", label: "Divider" };
|
|
258
|
+
case "insertion":
|
|
259
|
+
if (option.id === "horizontal-rule") {
|
|
260
|
+
return {
|
|
261
|
+
id: option.id,
|
|
262
|
+
label: "Horizontal Rule",
|
|
263
|
+
icon: "─",
|
|
264
|
+
isActive: () => false, // Insertion buttons are never "active"
|
|
265
|
+
toggle: optionHandlers[handlerKey],
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
return undefined;
|
|
196
269
|
default:
|
|
197
270
|
return undefined;
|
|
198
271
|
}
|
|
199
|
-
};
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
isActive: (id) => editor.isMarkActive(id),
|
|
203
|
-
toggle: (id) => editor.toggleMark(id),
|
|
204
|
-
},
|
|
205
|
-
block: {
|
|
206
|
-
isActive: (id) => editor.isBlockActive(id),
|
|
207
|
-
toggle: (id) => editor.toggleBlock(id),
|
|
208
|
-
},
|
|
209
|
-
alignment: {
|
|
210
|
-
isActive: (id) => {
|
|
211
|
-
const alignConfig = SLATE_ALIGNMENTS[id];
|
|
212
|
-
return alignConfig ? editor.isAlignActive(alignConfig.value) : false;
|
|
213
|
-
},
|
|
214
|
-
toggle: (id) => {
|
|
215
|
-
const alignConfig = SLATE_ALIGNMENTS[id];
|
|
216
|
-
if (alignConfig) {
|
|
217
|
-
editor.toggleAlign(alignConfig.value);
|
|
218
|
-
}
|
|
219
|
-
},
|
|
220
|
-
},
|
|
221
|
-
link: {
|
|
222
|
-
isActive: () => editor.isLinkActive(),
|
|
223
|
-
toggle: () => handleLinkButtonClick(),
|
|
224
|
-
},
|
|
225
|
-
list: {
|
|
226
|
-
isActive: (id) => {
|
|
227
|
-
const listType = id === "unordered-list" ? "unordered" : "ordered";
|
|
228
|
-
return editor.isListActive(listType);
|
|
229
|
-
},
|
|
230
|
-
toggle: (id, event) => {
|
|
231
|
-
const listType = id === "unordered-list" ? "unordered" : "ordered";
|
|
232
|
-
editor.toggleList(listType);
|
|
233
|
-
},
|
|
234
|
-
},
|
|
235
|
-
};
|
|
236
|
-
const isOptionActive = (type, id) => {
|
|
237
|
-
const handler = optionHandlers[type];
|
|
238
|
-
return handler ? handler.isActive(id) : false;
|
|
239
|
-
};
|
|
240
|
-
const handleOptionSelect = (type, id) => (event) => {
|
|
241
|
-
event.preventDefault();
|
|
242
|
-
const handler = optionHandlers[type];
|
|
243
|
-
if (handler) {
|
|
244
|
-
return handler.toggle(id, event);
|
|
245
|
-
}
|
|
246
|
-
console.warn(`Unhandled option type: ${type}`);
|
|
247
|
-
};
|
|
248
|
-
const createDropdownOptions = (options) => {
|
|
272
|
+
}, [optionHandlers]);
|
|
273
|
+
// Memoize dropdown options creation with strict type safety
|
|
274
|
+
const createDropdownOptions = useCallback((options) => {
|
|
249
275
|
return options
|
|
250
276
|
.map((option) => {
|
|
251
|
-
const optionObj = getOption(option
|
|
277
|
+
const optionObj = getOption(option);
|
|
252
278
|
if (!optionObj)
|
|
253
279
|
return null;
|
|
254
280
|
return {
|
|
@@ -256,14 +282,30 @@ export const ReactSlate = forwardRef((props, ref) => {
|
|
|
256
282
|
label: optionObj.label || option.id,
|
|
257
283
|
icon: optionObj.icon,
|
|
258
284
|
style: optionObj.style,
|
|
259
|
-
isActive: (editor) =>
|
|
260
|
-
|
|
285
|
+
isActive: (editor) => {
|
|
286
|
+
switch (option.type) {
|
|
287
|
+
case "mark":
|
|
288
|
+
return editor.isMarkActive(option.id);
|
|
289
|
+
case "block":
|
|
290
|
+
return editor.isBlockActive(option.id);
|
|
291
|
+
case "alignment":
|
|
292
|
+
return editor.isAlignActive(SLATE_ALIGNMENTS[option.id].value);
|
|
293
|
+
case "link":
|
|
294
|
+
return editor.isLinkActive();
|
|
295
|
+
case "list":
|
|
296
|
+
const listType = option.id === "unordered-list" ? "unordered" : "ordered";
|
|
297
|
+
return editor.isListActive(listType);
|
|
298
|
+
default:
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
},
|
|
302
|
+
onSelect: optionHandlers[`${option.type}-${option.id}`],
|
|
261
303
|
};
|
|
262
304
|
})
|
|
263
305
|
.filter(Boolean);
|
|
264
|
-
};
|
|
306
|
+
}, [getOption, optionHandlers]);
|
|
265
307
|
// Helper function to split options by dividers into sub-groups
|
|
266
|
-
const splitOptionsByDividers = (options) => {
|
|
308
|
+
const splitOptionsByDividers = useCallback((options) => {
|
|
267
309
|
const subGroups = [];
|
|
268
310
|
let currentGroup = [];
|
|
269
311
|
options.forEach((option) => {
|
|
@@ -282,11 +324,15 @@ export const ReactSlate = forwardRef((props, ref) => {
|
|
|
282
324
|
subGroups.push(currentGroup);
|
|
283
325
|
}
|
|
284
326
|
return subGroups;
|
|
285
|
-
};
|
|
286
|
-
|
|
287
|
-
|
|
327
|
+
}, []);
|
|
328
|
+
// Memoize toolbar group rendering
|
|
329
|
+
const renderToolbarGroup = useCallback((group, index) => {
|
|
330
|
+
const validOptions = group.options.filter((option) => getOption(option) || option.type === "divider");
|
|
288
331
|
if (validOptions.length === 0)
|
|
289
332
|
return null;
|
|
333
|
+
// Skip rendering dropdown groups with only one option
|
|
334
|
+
if (group.display === "dropdown" && validOptions.length === 1)
|
|
335
|
+
return null;
|
|
290
336
|
const groupStyle = {
|
|
291
337
|
display: "flex",
|
|
292
338
|
alignItems: "center",
|
|
@@ -296,11 +342,12 @@ export const ReactSlate = forwardRef((props, ref) => {
|
|
|
296
342
|
return (_jsx("div", { style: groupStyle, children: group.display === "buttons"
|
|
297
343
|
? (() => {
|
|
298
344
|
const subGroups = splitOptionsByDividers(validOptions);
|
|
299
|
-
return subGroups.map((subGroup, subGroupIndex) => (_jsx("div", { className: "toolbar-button-group", children: subGroup.map((option
|
|
300
|
-
const optionObj = getOption(option
|
|
301
|
-
|
|
345
|
+
return subGroups.map((subGroup, subGroupIndex) => (_jsx("div", { className: "toolbar-button-group", children: subGroup.map((option) => {
|
|
346
|
+
const optionObj = getOption(option);
|
|
347
|
+
const handler = buttonHandlers[`${option.type}-${option.id}`];
|
|
348
|
+
if (!optionObj || !handler)
|
|
302
349
|
return null;
|
|
303
|
-
return (_jsx(
|
|
350
|
+
return (_jsx(ToolbarButtonWrapper, { option: option, icon: optionObj.icon, editor: editor, onMouseDown: handler }, `${option.type}-${option.id}`));
|
|
304
351
|
}) }, `subgroup-${subGroupIndex}`)));
|
|
305
352
|
})()
|
|
306
353
|
: (() => {
|
|
@@ -309,27 +356,39 @@ export const ReactSlate = forwardRef((props, ref) => {
|
|
|
309
356
|
// If there's only one block option, render it as a disabled-style button
|
|
310
357
|
if (blockOptions.length === 1) {
|
|
311
358
|
const singleOption = dropdownOptions.find((opt) => blockOptions.some((blockOpt) => blockOpt.type === "block" &&
|
|
312
|
-
getOption(blockOpt
|
|
359
|
+
getOption(blockOpt) === opt.value));
|
|
313
360
|
return (_jsx("div", { className: "toolbar-dropdown-container", children: _jsx("button", { className: "toolbar-dropdown-button", disabled: true, children: group.label ? (_jsxs(_Fragment, { children: [_jsxs("span", { className: "toolbar-dropdown-content", children: [group.label, ": ", singleOption?.label] }), _jsx("span", { className: "toolbar-dropdown-arrow", children: "\u25BC" })] })) : group.showIconsOnly ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "toolbar-dropdown-icon", children: singleOption?.icon }), _jsx("span", { className: "toolbar-dropdown-arrow", children: "\u25BC" })] })) : (_jsxs(_Fragment, { children: [_jsxs("span", { className: "toolbar-dropdown-icon", children: [singleOption?.icon && (_jsx("span", { className: "toolbar-dropdown-icon", children: singleOption.icon })), _jsx("span", { className: "toolbar-dropdown-content", children: singleOption?.label })] }), _jsx("span", { className: "toolbar-dropdown-arrow", children: "\u25BC" })] })) }) }));
|
|
314
361
|
}
|
|
315
362
|
// Multiple options - render normally wrapped in grey container
|
|
316
363
|
const isActive = dropdownOptions.some((option) => option.isActive(editor));
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
364
|
+
const buttonStyle = {
|
|
365
|
+
padding: "5px 10px",
|
|
366
|
+
margin: "0",
|
|
367
|
+
background: isActive ? "#ffffff" : "transparent",
|
|
368
|
+
border: "none",
|
|
369
|
+
borderRadius: "3px",
|
|
370
|
+
cursor: "pointer",
|
|
371
|
+
boxShadow: isActive
|
|
372
|
+
? "0 1px 2px rgba(0,0,0,0.1)"
|
|
373
|
+
: "none",
|
|
374
|
+
};
|
|
375
|
+
return (_jsx("div", { className: "toolbar-dropdown-container", children: _jsx(MemoizedEditorDropdown, { options: dropdownOptions, editor: editor, label: group.label, buttonStyle: buttonStyle }) }));
|
|
328
376
|
})() }, `group-${group.id || index}`));
|
|
329
|
-
};
|
|
330
|
-
|
|
331
|
-
const
|
|
332
|
-
|
|
377
|
+
}, [getOption, splitOptionsByDividers, createDropdownOptions, optionHandlers, buttonHandlers, editor]);
|
|
378
|
+
// Memoize toolbar structure (expensive grouping operation)
|
|
379
|
+
const toolbarStructure = useMemo(() => {
|
|
380
|
+
// Group toolbar items by row
|
|
381
|
+
const groupsByRow = editorProfile.toolbar.groups.reduce((acc, group, index) => {
|
|
382
|
+
const row = group.row !== undefined ? group.row : index;
|
|
383
|
+
if (!acc[row])
|
|
384
|
+
acc[row] = [];
|
|
385
|
+
acc[row].push(group);
|
|
386
|
+
return acc;
|
|
387
|
+
}, {});
|
|
388
|
+
// Return sorted entries for consistent rendering
|
|
389
|
+
return Object.entries(groupsByRow)
|
|
390
|
+
.sort(([a], [b]) => parseInt(a) - parseInt(b));
|
|
391
|
+
}, [editorProfile.toolbar.groups]);
|
|
333
392
|
const editLink = useCallback((element) => {
|
|
334
393
|
const linkType = element.link?.type || "external";
|
|
335
394
|
let linkData;
|
|
@@ -402,93 +461,9 @@ export const ReactSlate = forwardRef((props, ref) => {
|
|
|
402
461
|
setShowLinkDialog(false);
|
|
403
462
|
setSelectedLink(null);
|
|
404
463
|
}, [editor, linkDialogCallback]);
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
setSelectedLink({
|
|
409
|
-
type: "external",
|
|
410
|
-
url: "",
|
|
411
|
-
target: "_blank",
|
|
412
|
-
});
|
|
413
|
-
setShowLinkDialog(true);
|
|
414
|
-
setLinkDialogCallback(() => callback);
|
|
415
|
-
},
|
|
416
|
-
});
|
|
417
|
-
}, [editor]);
|
|
418
|
-
const handleKeyDown = useCallback((event) => {
|
|
419
|
-
if (event.key === "Enter" && event.shiftKey) {
|
|
420
|
-
// Handle Shift+Enter for line breaks in all block types
|
|
421
|
-
event.preventDefault();
|
|
422
|
-
if (editor.selection) {
|
|
423
|
-
// Insert literal <br> text
|
|
424
|
-
editor.insertText("<br>");
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
else if (event.key === "Tab") {
|
|
428
|
-
event.preventDefault();
|
|
429
|
-
// Check if we're in a list item more explicitly
|
|
430
|
-
if (editor.selection) {
|
|
431
|
-
// Try to find the closest list item ancestor using Editor.above
|
|
432
|
-
const listItem = Editor.above(editor, {
|
|
433
|
-
at: editor.selection,
|
|
434
|
-
match: (n) => !Editor.isEditor(n) &&
|
|
435
|
-
Element.isElement(n) &&
|
|
436
|
-
n.type === "list-item",
|
|
437
|
-
});
|
|
438
|
-
if (listItem) {
|
|
439
|
-
if (event.shiftKey) {
|
|
440
|
-
// Shift+Tab: Outdent
|
|
441
|
-
editor.outdentList();
|
|
442
|
-
}
|
|
443
|
-
else {
|
|
444
|
-
// Tab: Indent
|
|
445
|
-
editor.indentList();
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
else if (event.key === "Backspace" || event.key === "Delete") {
|
|
451
|
-
// Handle empty list items
|
|
452
|
-
if (editor.selection) {
|
|
453
|
-
const listItem = Editor.above(editor, {
|
|
454
|
-
at: editor.selection,
|
|
455
|
-
match: (n) => !Editor.isEditor(n) &&
|
|
456
|
-
Element.isElement(n) &&
|
|
457
|
-
n.type === "list-item",
|
|
458
|
-
});
|
|
459
|
-
if (listItem) {
|
|
460
|
-
const selectedText = Editor.string(editor, editor.selection);
|
|
461
|
-
const [node, path] = listItem;
|
|
462
|
-
const nodeText = Editor.string(editor, path);
|
|
463
|
-
// If the list item is empty or only contains whitespace, convert to paragraph
|
|
464
|
-
if (!nodeText.trim() ||
|
|
465
|
-
(selectedText === nodeText && nodeText.trim())) {
|
|
466
|
-
event.preventDefault();
|
|
467
|
-
Transforms.setNodes(editor, { type: "paragraph", listType: undefined, indent: undefined }, {
|
|
468
|
-
match: (n) => !Editor.isEditor(n) &&
|
|
469
|
-
Element.isElement(n) &&
|
|
470
|
-
n.type === "list-item",
|
|
471
|
-
split: true,
|
|
472
|
-
});
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
}, [editor]);
|
|
478
|
-
return (_jsxs("div", { className: `slate-editor ${props.className}`, children: [_jsxs(Slate, { editor: editor, initialValue: internalValue, onChange: handleChange, children: [!readOnly && (_jsx("div", { className: "toolbar", children: (() => {
|
|
479
|
-
// Group toolbar items by row
|
|
480
|
-
const groupsByRow = editorProfile.toolbar.groups.reduce((acc, group, index) => {
|
|
481
|
-
const row = group.row !== undefined ? group.row : index;
|
|
482
|
-
if (!acc[row])
|
|
483
|
-
acc[row] = [];
|
|
484
|
-
acc[row].push(group);
|
|
485
|
-
return acc;
|
|
486
|
-
}, {});
|
|
487
|
-
// Render each row
|
|
488
|
-
return Object.entries(groupsByRow)
|
|
489
|
-
.sort(([a], [b]) => parseInt(a) - parseInt(b))
|
|
490
|
-
.map(([rowIndex, rowGroups]) => (_jsx("div", { className: "toolbar-row", children: rowGroups.map((group) => renderToolbarGroup(group, parseInt(rowIndex))) }, `row-${rowIndex}`)));
|
|
491
|
-
})() })), _jsx(Editable, { className: classNames(readOnly ? "bg-gray-4" : "bg-gray-5", "focus-shadow p-2"), readOnly: readOnly, placeholder: placeholder, renderPlaceholder: ({ attributes, children }) => (_jsx("span", { ...attributes, className: "p-2 text-gray-500", children: children })), onFocus: onFocus, onBlur: onBlur, onKeyDown: handleKeyDown, renderElement: ({ attributes, children, element }) => {
|
|
464
|
+
// Use the keyboard handler from plugins
|
|
465
|
+
const handleKeyDown = useMemo(() => createKeyboardHandler(editor), [editor]);
|
|
466
|
+
return (_jsxs("div", { className: `slate-editor ${props.className}`, children: [_jsxs(Slate, { editor: editor, initialValue: initialSlateValue, onChange: handleChange, children: [!readOnly && (_jsx("div", { className: "toolbar", children: toolbarStructure.map(([rowIndex, rowGroups]) => (_jsx("div", { className: "toolbar-row", children: rowGroups.map((group) => renderToolbarGroup(group, parseInt(rowIndex))) }, `row-${rowIndex}`))) })), _jsx(Editable, { className: classNames(readOnly ? "bg-gray-4" : "bg-gray-5", "focus-shadow p-2"), readOnly: readOnly, placeholder: placeholder, renderPlaceholder: ({ attributes, children }) => (_jsx("span", { ...attributes, className: "p-2 text-gray-500", children: children })), onFocus: onFocus, onBlur: onBlur, onKeyDown: handleKeyDown, renderElement: ({ attributes, children, element }) => {
|
|
492
467
|
const style = {
|
|
493
468
|
textAlign: element.align || "left",
|
|
494
469
|
};
|
|
@@ -508,44 +483,27 @@ export const ReactSlate = forwardRef((props, ref) => {
|
|
|
508
483
|
const indent = element.indent || 0;
|
|
509
484
|
const listType = element.listType || "unordered";
|
|
510
485
|
const isOrdered = listType === "ordered";
|
|
511
|
-
// Calculate proper numbering for ordered lists
|
|
512
|
-
let listNumber = 1;
|
|
513
|
-
if (isOrdered) {
|
|
514
|
-
// Find the position of this item within its level
|
|
515
|
-
const allElements = editor.children;
|
|
516
|
-
const currentIndex = allElements.findIndex((el) => el === element);
|
|
517
|
-
// Count preceding list items at the same indent level and list type
|
|
518
|
-
let count = 0;
|
|
519
|
-
for (let i = 0; i < currentIndex; i++) {
|
|
520
|
-
const prevElement = allElements[i];
|
|
521
|
-
if (prevElement &&
|
|
522
|
-
prevElement.type === "list-item" &&
|
|
523
|
-
prevElement.listType === "ordered" &&
|
|
524
|
-
(prevElement.indent || 0) === indent) {
|
|
525
|
-
count++;
|
|
526
|
-
}
|
|
527
|
-
else if (prevElement &&
|
|
528
|
-
prevElement.type === "list-item" &&
|
|
529
|
-
(prevElement.indent || 0) < indent) {
|
|
530
|
-
// Reset count when we encounter a parent-level item
|
|
531
|
-
count = 0;
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
listNumber = count + 1;
|
|
535
|
-
}
|
|
536
486
|
const listStyle = {
|
|
537
487
|
...style,
|
|
538
488
|
position: "relative",
|
|
539
489
|
listStyleType: "none",
|
|
540
490
|
};
|
|
541
|
-
|
|
542
|
-
|
|
491
|
+
return (_jsxs("div", { ...attributes, style: listStyle, className: `slate-list-item slate-list-${listType}`, "data-indent": indent, children: [_jsx("span", { className: "slate-list-bullet" }), _jsx("div", { className: "slate-list-content", children: children })] }));
|
|
492
|
+
}
|
|
493
|
+
if (element.type === "horizontal-rule") {
|
|
494
|
+
return (_jsxs("div", { ...attributes, contentEditable: false, style: { ...style, userSelect: "none" }, children: [_jsx("hr", { style: {
|
|
495
|
+
border: "none",
|
|
496
|
+
borderTop: "1px solid #ccc",
|
|
497
|
+
margin: "1em 0",
|
|
498
|
+
width: "100%"
|
|
499
|
+
} }), children] }));
|
|
543
500
|
}
|
|
544
501
|
// Handle different block types using built-in SLATE_BLOCKS configuration
|
|
545
|
-
const
|
|
502
|
+
const isValidBlockId = element.type in SLATE_BLOCKS;
|
|
503
|
+
const blockConfig = isValidBlockId ? SLATE_BLOCKS[element.type] : undefined;
|
|
546
504
|
if (blockConfig && element.type === "no-tag") {
|
|
547
505
|
// Special handling for no-tag blocks (plain text without wrapper)
|
|
548
|
-
return (_jsx("
|
|
506
|
+
return (_jsx("div", { ...attributes, style: style, children: children }));
|
|
549
507
|
}
|
|
550
508
|
// For standard blocks, use the appropriate HTML tag
|
|
551
509
|
if (blockConfig) {
|
|
@@ -574,7 +532,7 @@ export const ReactSlate = forwardRef((props, ref) => {
|
|
|
574
532
|
}
|
|
575
533
|
});
|
|
576
534
|
return el;
|
|
577
|
-
} })] }
|
|
535
|
+
} })] }), showLinkDialog && selectedLink && (_jsx(LinkEditorDialog, { linkValue: selectedLink, onOk: handleLinkUpdate, onCancel: () => {
|
|
578
536
|
setShowLinkDialog(false);
|
|
579
537
|
setSelectedLink(null);
|
|
580
538
|
setLinkDialogCallback(null);
|