@alpaca-editor/core 1.0.3996 → 1.0.4000
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/InternalLinkFieldEditor.js +29 -9
- package/dist/editor/field-types/InternalLinkFieldEditor.js.map +1 -1
- package/dist/editor/field-types/TreeListEditor.js +20 -12
- package/dist/editor/field-types/TreeListEditor.js.map +1 -1
- package/dist/editor/field-types/richtext/components/ReactSlate.d.ts +3 -3
- package/dist/editor/field-types/richtext/components/ReactSlate.js +195 -172
- package/dist/editor/field-types/richtext/components/ReactSlate.js.map +1 -1
- package/dist/editor/page-editor-chrome/PlaceholderDropZone.js +1 -1
- package/dist/editor/page-editor-chrome/PlaceholderDropZone.js.map +1 -1
- package/dist/editor/sidebar/MainContentTree.js +1 -1
- package/dist/editor/sidebar/MainContentTree.js.map +1 -1
- package/dist/editor/utils.d.ts +1 -0
- package/dist/editor/utils.js +3 -0
- package/dist/editor/utils.js.map +1 -1
- package/dist/editor/views/CompareView.js.map +1 -1
- package/dist/page-wizard/PageWizard.d.ts +5 -1
- package/dist/page-wizard/PageWizard.js.map +1 -1
- package/dist/page-wizard/WizardSteps.js +1 -0
- package/dist/page-wizard/WizardSteps.js.map +1 -1
- package/dist/page-wizard/steps/ComponentTypesSelector.d.ts +2 -1
- package/dist/page-wizard/steps/ComponentTypesSelector.js +27 -90
- package/dist/page-wizard/steps/ComponentTypesSelector.js.map +1 -1
- package/dist/page-wizard/steps/ContentStep.js +63 -11
- package/dist/page-wizard/steps/ContentStep.js.map +1 -1
- package/dist/page-wizard/steps/schema.js +4 -4
- package/dist/page-wizard/steps/schema.js.map +1 -1
- package/dist/page-wizard/steps/usePageCreator.d.ts +2 -1
- package/dist/page-wizard/steps/usePageCreator.js +62 -8
- package/dist/page-wizard/steps/usePageCreator.js.map +1 -1
- package/dist/page-wizard/usePageWizard.js +2 -0
- package/dist/page-wizard/usePageWizard.js.map +1 -1
- package/dist/revision.d.ts +2 -2
- package/dist/revision.js +2 -2
- package/dist/styles.css +3 -3
- package/package.json +1 -1
- package/src/editor/field-types/InternalLinkFieldEditor.tsx +99 -73
- package/src/editor/field-types/TreeListEditor.tsx +39 -32
- package/src/editor/field-types/richtext/components/ReactSlate.tsx +532 -446
- package/src/editor/page-editor-chrome/PlaceholderDropZone.tsx +1 -1
- package/src/editor/sidebar/MainContentTree.tsx +1 -0
- package/src/editor/utils.ts +4 -0
- package/src/editor/views/CompareView.tsx +1 -1
- package/src/page-wizard/PageWizard.tsx +5 -1
- package/src/page-wizard/WizardSteps.tsx +1 -0
- package/src/page-wizard/steps/ComponentTypesSelector.tsx +34 -115
- package/src/page-wizard/steps/ContentStep.tsx +83 -16
- package/src/page-wizard/steps/schema.ts +4 -4
- package/src/page-wizard/steps/usePageCreator.ts +84 -9
- package/src/page-wizard/usePageWizard.ts +2 -0
- package/src/revision.ts +2 -2
|
@@ -1,11 +1,18 @@
|
|
|
1
|
-
import React, {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
import React, {
|
|
2
|
+
useCallback,
|
|
3
|
+
useMemo,
|
|
4
|
+
useState,
|
|
5
|
+
forwardRef,
|
|
6
|
+
useRef,
|
|
7
|
+
useEffect,
|
|
8
|
+
} from "react";
|
|
9
|
+
import { createEditor, Descendant, Editor, Element, Transforms } from "slate";
|
|
10
|
+
import { Slate, Editable, withReact, ReactEditor } from "slate-react";
|
|
11
|
+
import { withHistory } from "slate-history";
|
|
12
|
+
import isEqual from "lodash/isEqual";
|
|
13
|
+
import "./ReactSlate.css";
|
|
14
|
+
|
|
15
|
+
import {
|
|
9
16
|
ReactSlateProps,
|
|
10
17
|
CustomElement,
|
|
11
18
|
ToolbarGroupConfig,
|
|
@@ -15,37 +22,41 @@ import {
|
|
|
15
22
|
SimplifiedProfile,
|
|
16
23
|
SLATE_MARKS,
|
|
17
24
|
SLATE_BLOCKS,
|
|
18
|
-
SLATE_ALIGNMENTS
|
|
19
|
-
} from
|
|
25
|
+
SLATE_ALIGNMENTS,
|
|
26
|
+
} from "../types";
|
|
20
27
|
|
|
21
|
-
import EditorDropdown from
|
|
22
|
-
import {ToolbarButton} from
|
|
23
|
-
import { LinkEditorDialog } from
|
|
28
|
+
import EditorDropdown from "./EditorDropdown";
|
|
29
|
+
import { ToolbarButton } from "./ToolbarButton";
|
|
30
|
+
import { LinkEditorDialog } from "../../../LinkEditorDialog";
|
|
24
31
|
|
|
25
|
-
import { htmlToSlate, slateToHtml } from
|
|
26
|
-
import { createPluginsFromConfig } from
|
|
27
|
-
import { useCachedSimplifiedProfile } from
|
|
28
|
-
import { classNames } from
|
|
32
|
+
import { htmlToSlate, slateToHtml } from "../utils/conversion";
|
|
33
|
+
import { createPluginsFromConfig } from "../config/pluginFactory";
|
|
34
|
+
import { useCachedSimplifiedProfile } from "../hooks/useProfileCache";
|
|
35
|
+
import { classNames } from "primereact/utils";
|
|
29
36
|
|
|
30
37
|
// Helper function to normalize HTML for comparison
|
|
31
38
|
const normalizeHtmlForComparison = (html: string): string => {
|
|
32
|
-
if (!html) return
|
|
33
|
-
|
|
39
|
+
if (!html) return "";
|
|
40
|
+
|
|
34
41
|
// Create a temporary DOM element to normalize the HTML
|
|
35
|
-
const temp = document.createElement(
|
|
42
|
+
const temp = document.createElement("div");
|
|
36
43
|
temp.innerHTML = html;
|
|
37
|
-
|
|
44
|
+
|
|
38
45
|
// Remove extra whitespace and normalize structure
|
|
39
46
|
const normalizedHtml = temp.innerHTML
|
|
40
|
-
.replace(/\s+/g,
|
|
41
|
-
.replace(/>\s+</g,
|
|
47
|
+
.replace(/\s+/g, " ") // Replace multiple spaces with single space
|
|
48
|
+
.replace(/>\s+</g, "><") // Remove spaces between tags
|
|
42
49
|
.trim();
|
|
43
|
-
|
|
50
|
+
|
|
44
51
|
// Handle common empty content patterns
|
|
45
|
-
if (
|
|
46
|
-
|
|
52
|
+
if (
|
|
53
|
+
normalizedHtml === "<p><br></p>" ||
|
|
54
|
+
normalizedHtml === "<br>" ||
|
|
55
|
+
normalizedHtml === "<p></p>"
|
|
56
|
+
) {
|
|
57
|
+
return "";
|
|
47
58
|
}
|
|
48
|
-
|
|
59
|
+
|
|
49
60
|
return normalizedHtml;
|
|
50
61
|
};
|
|
51
62
|
|
|
@@ -53,72 +64,70 @@ const normalizeHtmlForComparison = (html: string): string => {
|
|
|
53
64
|
const isHtmlConversionStable = (
|
|
54
65
|
originalHtml: string,
|
|
55
66
|
slateValue: Descendant[],
|
|
56
|
-
profile: SimplifiedProfile
|
|
67
|
+
profile: SimplifiedProfile,
|
|
57
68
|
): boolean => {
|
|
58
69
|
try {
|
|
59
70
|
// Convert Slate back to HTML
|
|
60
71
|
const convertedHtml = slateToHtml(slateValue, profile);
|
|
61
|
-
|
|
72
|
+
|
|
62
73
|
// Normalize both HTML strings for comparison
|
|
63
74
|
const normalizedOriginal = normalizeHtmlForComparison(originalHtml);
|
|
64
75
|
const normalizedConverted = normalizeHtmlForComparison(convertedHtml);
|
|
65
|
-
|
|
76
|
+
|
|
66
77
|
// Debug logging (can be enabled for troubleshooting)
|
|
67
78
|
// console.log('HTML Stability Check:', {
|
|
68
79
|
// original: normalizedOriginal,
|
|
69
80
|
// converted: normalizedConverted,
|
|
70
81
|
// stable: normalizedOriginal === normalizedConverted
|
|
71
82
|
// });
|
|
72
|
-
|
|
83
|
+
|
|
73
84
|
// Check if they're equivalent
|
|
74
85
|
return normalizedOriginal === normalizedConverted;
|
|
75
86
|
} catch (error) {
|
|
76
|
-
console.warn(
|
|
87
|
+
console.warn("HTML conversion stability check failed:", error);
|
|
77
88
|
// If we can't check stability, assume it's not stable to be safe
|
|
78
89
|
return false;
|
|
79
90
|
}
|
|
80
91
|
};
|
|
81
92
|
|
|
82
|
-
|
|
83
|
-
|
|
84
93
|
export const ReactSlate = forwardRef<any, ReactSlateProps>((props, ref) => {
|
|
85
|
-
const {
|
|
86
|
-
value =
|
|
87
|
-
onChange,
|
|
88
|
-
onFocus,
|
|
89
|
-
onBlur,
|
|
94
|
+
const {
|
|
95
|
+
value = "",
|
|
96
|
+
onChange,
|
|
97
|
+
onFocus,
|
|
98
|
+
onBlur,
|
|
90
99
|
readOnly = false,
|
|
91
|
-
placeholder =
|
|
92
|
-
profile
|
|
100
|
+
placeholder = "Enter some text...",
|
|
101
|
+
profile,
|
|
93
102
|
} = props;
|
|
94
103
|
|
|
95
104
|
// Create the Slate editor with plugins
|
|
96
105
|
const editor = useMemo(() => {
|
|
97
106
|
// Start with base editor
|
|
98
107
|
let slateEditor = createEditor();
|
|
99
|
-
|
|
108
|
+
|
|
100
109
|
// Apply core plugins
|
|
101
110
|
slateEditor = withReact(slateEditor);
|
|
102
111
|
slateEditor = withHistory(slateEditor);
|
|
103
112
|
|
|
104
113
|
slateEditor = createPluginsFromConfig(slateEditor);
|
|
105
|
-
|
|
114
|
+
|
|
106
115
|
return slateEditor;
|
|
107
116
|
}, []);
|
|
108
117
|
|
|
109
118
|
const editorProfile = profile || {
|
|
110
119
|
toolbar: {
|
|
111
|
-
groups: []
|
|
112
|
-
}
|
|
120
|
+
groups: [],
|
|
121
|
+
},
|
|
113
122
|
};
|
|
114
|
-
|
|
123
|
+
|
|
115
124
|
// Convert to simplified profile for the conversion functions (with caching)
|
|
116
125
|
const simplifiedProfile = useCachedSimplifiedProfile(editorProfile);
|
|
117
|
-
|
|
126
|
+
|
|
118
127
|
// Store the original HTML value for comparison
|
|
119
128
|
const originalValueRef = useRef<string>(value);
|
|
120
129
|
const isInitialLoadRef = useRef<boolean>(true);
|
|
121
|
-
|
|
130
|
+
|
|
122
131
|
// Update original value when prop value changes
|
|
123
132
|
useMemo(() => {
|
|
124
133
|
if (value !== originalValueRef.current) {
|
|
@@ -126,12 +135,12 @@ export const ReactSlate = forwardRef<any, ReactSlateProps>((props, ref) => {
|
|
|
126
135
|
isInitialLoadRef.current = true;
|
|
127
136
|
}
|
|
128
137
|
}, [value]);
|
|
129
|
-
|
|
138
|
+
|
|
130
139
|
// Convert the HTML value to Slate format
|
|
131
140
|
const [internalValue, setInternalValue] = useState<Descendant[]>(
|
|
132
|
-
htmlToSlate(value, simplifiedProfile)
|
|
141
|
+
htmlToSlate(value, simplifiedProfile),
|
|
133
142
|
);
|
|
134
|
-
|
|
143
|
+
|
|
135
144
|
// Counter to force remount when value changes externally
|
|
136
145
|
const [remountKey, setRemountKey] = useState(0);
|
|
137
146
|
|
|
@@ -141,110 +150,127 @@ export const ReactSlate = forwardRef<any, ReactSlateProps>((props, ref) => {
|
|
|
141
150
|
if (!isEqual(newValue, internalValue)) {
|
|
142
151
|
setInternalValue(newValue);
|
|
143
152
|
// Force remount by incrementing the key
|
|
144
|
-
setRemountKey(prev => prev + 1);
|
|
153
|
+
setRemountKey((prev) => prev + 1);
|
|
145
154
|
}
|
|
146
155
|
}, [value, simplifiedProfile]);
|
|
147
156
|
|
|
148
157
|
// Handle value changes with round-trip validation
|
|
149
|
-
const handleChange = useCallback(
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
158
|
+
const handleChange = useCallback(
|
|
159
|
+
(newValue: Descendant[]) => {
|
|
160
|
+
setInternalValue(newValue);
|
|
161
|
+
|
|
162
|
+
if (onChange) {
|
|
163
|
+
const html = slateToHtml(newValue, simplifiedProfile);
|
|
164
|
+
|
|
165
|
+
// Check if this is the initial load or if the conversion is stable
|
|
166
|
+
if (isInitialLoadRef.current) {
|
|
167
|
+
// On initial load, check if the HTML conversion is stable
|
|
168
|
+
const isStable = isHtmlConversionStable(
|
|
169
|
+
originalValueRef.current,
|
|
170
|
+
newValue,
|
|
171
|
+
simplifiedProfile,
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
if (isStable) {
|
|
175
|
+
// If stable, don't trigger onChange on initial load
|
|
176
|
+
isInitialLoadRef.current = false;
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// If not stable, we still need to proceed, but mark as no longer initial load
|
|
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(
|
|
159
187
|
originalValueRef.current,
|
|
160
|
-
newValue,
|
|
161
|
-
simplifiedProfile
|
|
162
188
|
);
|
|
163
|
-
|
|
164
|
-
if (
|
|
165
|
-
//
|
|
166
|
-
|
|
167
|
-
|
|
189
|
+
|
|
190
|
+
if (normalizedNewHtml !== normalizedOriginalHtml) {
|
|
191
|
+
// Update the original value reference to the new HTML
|
|
192
|
+
originalValueRef.current = html;
|
|
193
|
+
onChange(html);
|
|
168
194
|
}
|
|
169
|
-
|
|
170
|
-
// If not stable, we still need to proceed, but mark as no longer initial load
|
|
171
|
-
isInitialLoadRef.current = false;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// For subsequent changes, only trigger onChange if there's a meaningful difference
|
|
175
|
-
const normalizedNewHtml = normalizeHtmlForComparison(html);
|
|
176
|
-
const normalizedOriginalHtml = normalizeHtmlForComparison(originalValueRef.current);
|
|
177
|
-
|
|
178
|
-
if (normalizedNewHtml !== normalizedOriginalHtml) {
|
|
179
|
-
// Update the original value reference to the new HTML
|
|
180
|
-
originalValueRef.current = html;
|
|
181
|
-
onChange(html);
|
|
182
195
|
}
|
|
183
|
-
}
|
|
184
|
-
|
|
196
|
+
},
|
|
197
|
+
[onChange, simplifiedProfile],
|
|
198
|
+
);
|
|
185
199
|
|
|
186
200
|
const getOption = (type: string, id: string): CustomOption | undefined => {
|
|
187
201
|
switch (type) {
|
|
188
|
-
case
|
|
202
|
+
case "mark":
|
|
189
203
|
const markConfig = SLATE_MARKS[id];
|
|
190
|
-
return markConfig
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
204
|
+
return markConfig
|
|
205
|
+
? {
|
|
206
|
+
id,
|
|
207
|
+
label: markConfig.label,
|
|
208
|
+
icon: markConfig.icon,
|
|
209
|
+
isActive: (editor: Editor) => editor.isMarkActive(id),
|
|
210
|
+
toggle: (editor: Editor, event: React.MouseEvent) => {
|
|
211
|
+
event.preventDefault();
|
|
212
|
+
editor.toggleMark(id);
|
|
213
|
+
},
|
|
214
|
+
}
|
|
215
|
+
: undefined;
|
|
216
|
+
case "block":
|
|
201
217
|
const blockConfig = SLATE_BLOCKS[id];
|
|
202
|
-
return blockConfig
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
218
|
+
return blockConfig
|
|
219
|
+
? {
|
|
220
|
+
id,
|
|
221
|
+
label: blockConfig.label,
|
|
222
|
+
icon: blockConfig.icon,
|
|
223
|
+
isActive: (editor: Editor) => editor.isBlockActive(id),
|
|
224
|
+
toggle: (editor: Editor, event: React.MouseEvent) => {
|
|
225
|
+
event.preventDefault();
|
|
226
|
+
editor.toggleBlock(id);
|
|
227
|
+
},
|
|
228
|
+
}
|
|
229
|
+
: undefined;
|
|
230
|
+
case "alignment":
|
|
213
231
|
const alignConfig = SLATE_ALIGNMENTS[id];
|
|
214
|
-
return alignConfig
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
232
|
+
return alignConfig
|
|
233
|
+
? {
|
|
234
|
+
id,
|
|
235
|
+
label: alignConfig.label,
|
|
236
|
+
icon: alignConfig.icon,
|
|
237
|
+
isActive: (editor: Editor) =>
|
|
238
|
+
editor.isAlignActive(alignConfig.value),
|
|
239
|
+
toggle: (editor: Editor, event: React.MouseEvent) => {
|
|
240
|
+
event.preventDefault();
|
|
241
|
+
editor.toggleAlign(alignConfig.value);
|
|
242
|
+
},
|
|
243
|
+
}
|
|
244
|
+
: undefined;
|
|
245
|
+
case "link":
|
|
225
246
|
return {
|
|
226
|
-
id:
|
|
227
|
-
label:
|
|
228
|
-
icon:
|
|
247
|
+
id: "link",
|
|
248
|
+
label: "Link",
|
|
249
|
+
icon: "🔗",
|
|
229
250
|
isActive: (editor: Editor) => editor.isLinkActive(),
|
|
230
251
|
toggle: (editor: Editor, event: React.MouseEvent) => {
|
|
231
252
|
event.preventDefault();
|
|
232
253
|
handleLinkButtonClick();
|
|
233
|
-
}
|
|
254
|
+
},
|
|
234
255
|
};
|
|
235
|
-
case
|
|
256
|
+
case "list":
|
|
236
257
|
return {
|
|
237
258
|
id,
|
|
238
|
-
label: id ===
|
|
239
|
-
icon: id ===
|
|
240
|
-
isActive: (editor: Editor) =>
|
|
259
|
+
label: id === "unordered-list" ? "Bulleted List" : "Numbered List",
|
|
260
|
+
icon: id === "unordered-list" ? "•" : "1.",
|
|
261
|
+
isActive: (editor: Editor) =>
|
|
262
|
+
editor.isListActive(
|
|
263
|
+
id === "unordered-list" ? "unordered" : "ordered",
|
|
264
|
+
),
|
|
241
265
|
toggle: (editor: Editor, event: React.MouseEvent) => {
|
|
242
266
|
event.preventDefault();
|
|
243
|
-
editor.toggleList(
|
|
244
|
-
|
|
267
|
+
editor.toggleList(
|
|
268
|
+
id === "unordered-list" ? "unordered" : "ordered",
|
|
269
|
+
);
|
|
270
|
+
},
|
|
245
271
|
};
|
|
246
|
-
case
|
|
247
|
-
return { id:
|
|
272
|
+
case "divider":
|
|
273
|
+
return { id: "divider", label: "Divider" };
|
|
248
274
|
default:
|
|
249
275
|
return undefined;
|
|
250
276
|
}
|
|
@@ -253,11 +279,11 @@ export const ReactSlate = forwardRef<any, ReactSlateProps>((props, ref) => {
|
|
|
253
279
|
const optionHandlers = {
|
|
254
280
|
mark: {
|
|
255
281
|
isActive: (id: string) => editor.isMarkActive(id),
|
|
256
|
-
toggle: (id: string) => editor.toggleMark(id)
|
|
282
|
+
toggle: (id: string) => editor.toggleMark(id),
|
|
257
283
|
},
|
|
258
284
|
block: {
|
|
259
285
|
isActive: (id: string) => editor.isBlockActive(id),
|
|
260
|
-
toggle: (id: string) => editor.toggleBlock(id)
|
|
286
|
+
toggle: (id: string) => editor.toggleBlock(id),
|
|
261
287
|
},
|
|
262
288
|
alignment: {
|
|
263
289
|
isActive: (id: string) => {
|
|
@@ -269,22 +295,22 @@ export const ReactSlate = forwardRef<any, ReactSlateProps>((props, ref) => {
|
|
|
269
295
|
if (alignConfig) {
|
|
270
296
|
editor.toggleAlign(alignConfig.value);
|
|
271
297
|
}
|
|
272
|
-
}
|
|
298
|
+
},
|
|
273
299
|
},
|
|
274
300
|
link: {
|
|
275
301
|
isActive: () => editor.isLinkActive(),
|
|
276
|
-
toggle: () => handleLinkButtonClick()
|
|
302
|
+
toggle: () => handleLinkButtonClick(),
|
|
277
303
|
},
|
|
278
304
|
list: {
|
|
279
305
|
isActive: (id: string) => {
|
|
280
|
-
const listType = id ===
|
|
306
|
+
const listType = id === "unordered-list" ? "unordered" : "ordered";
|
|
281
307
|
return editor.isListActive(listType);
|
|
282
308
|
},
|
|
283
309
|
toggle: (id: string, event: React.MouseEvent<HTMLButtonElement>) => {
|
|
284
|
-
const listType = id ===
|
|
310
|
+
const listType = id === "unordered-list" ? "unordered" : "ordered";
|
|
285
311
|
editor.toggleList(listType);
|
|
286
|
-
}
|
|
287
|
-
}
|
|
312
|
+
},
|
|
313
|
+
},
|
|
288
314
|
};
|
|
289
315
|
|
|
290
316
|
const isOptionActive = (type: string, id: string): boolean => {
|
|
@@ -292,18 +318,22 @@ export const ReactSlate = forwardRef<any, ReactSlateProps>((props, ref) => {
|
|
|
292
318
|
return handler ? handler.isActive(id) : false;
|
|
293
319
|
};
|
|
294
320
|
|
|
295
|
-
const handleOptionSelect =
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
+
};
|
|
303
331
|
|
|
304
|
-
const createDropdownOptions = (
|
|
332
|
+
const createDropdownOptions = (
|
|
333
|
+
options: ToolbarOptionConfig[],
|
|
334
|
+
): DropdownOption<any>[] => {
|
|
305
335
|
return options
|
|
306
|
-
.map(option => {
|
|
336
|
+
.map((option) => {
|
|
307
337
|
const optionObj = getOption(option.type, option.id);
|
|
308
338
|
if (!optionObj) return null;
|
|
309
339
|
|
|
@@ -313,20 +343,25 @@ export const ReactSlate = forwardRef<any, ReactSlateProps>((props, ref) => {
|
|
|
313
343
|
icon: optionObj.icon,
|
|
314
344
|
style: (optionObj as any).style,
|
|
315
345
|
isActive: (editor: Editor) => isOptionActive(option.type, option.id),
|
|
316
|
-
onSelect: (editor: Editor, event: React.MouseEvent) =>
|
|
317
|
-
handleOptionSelect(
|
|
346
|
+
onSelect: (editor: Editor, event: React.MouseEvent) =>
|
|
347
|
+
handleOptionSelect(
|
|
348
|
+
option.type,
|
|
349
|
+
option.id,
|
|
350
|
+
)(event as React.MouseEvent<HTMLButtonElement>),
|
|
318
351
|
};
|
|
319
352
|
})
|
|
320
353
|
.filter(Boolean) as DropdownOption<any>[];
|
|
321
354
|
};
|
|
322
355
|
|
|
323
356
|
// Helper function to split options by dividers into sub-groups
|
|
324
|
-
const splitOptionsByDividers = (
|
|
357
|
+
const splitOptionsByDividers = (
|
|
358
|
+
options: ToolbarOptionConfig[],
|
|
359
|
+
): ToolbarOptionConfig[][] => {
|
|
325
360
|
const subGroups: ToolbarOptionConfig[][] = [];
|
|
326
361
|
let currentGroup: ToolbarOptionConfig[] = [];
|
|
327
|
-
|
|
328
|
-
options.forEach(option => {
|
|
329
|
-
if (option.type ===
|
|
362
|
+
|
|
363
|
+
options.forEach((option) => {
|
|
364
|
+
if (option.type === "divider") {
|
|
330
365
|
if (currentGroup.length > 0) {
|
|
331
366
|
subGroups.push(currentGroup);
|
|
332
367
|
currentGroup = [];
|
|
@@ -335,319 +370,358 @@ export const ReactSlate = forwardRef<any, ReactSlateProps>((props, ref) => {
|
|
|
335
370
|
currentGroup.push(option);
|
|
336
371
|
}
|
|
337
372
|
});
|
|
338
|
-
|
|
373
|
+
|
|
339
374
|
// Add the last group if it has options
|
|
340
375
|
if (currentGroup.length > 0) {
|
|
341
376
|
subGroups.push(currentGroup);
|
|
342
377
|
}
|
|
343
|
-
|
|
378
|
+
|
|
344
379
|
return subGroups;
|
|
345
380
|
};
|
|
346
381
|
|
|
347
382
|
const renderToolbarGroup = (group: ToolbarGroupConfig, index: number) => {
|
|
348
|
-
const validOptions = group.options.filter(
|
|
349
|
-
|
|
383
|
+
const validOptions = group.options.filter(
|
|
384
|
+
(option) =>
|
|
385
|
+
getOption(option.type, option.id) || option.type === "divider",
|
|
386
|
+
);
|
|
387
|
+
|
|
350
388
|
if (validOptions.length === 0) return null;
|
|
351
389
|
|
|
352
390
|
const groupStyle: React.CSSProperties = {
|
|
353
|
-
display:
|
|
354
|
-
alignItems:
|
|
355
|
-
gap:
|
|
356
|
-
flexWrap:
|
|
391
|
+
display: "flex",
|
|
392
|
+
alignItems: "center",
|
|
393
|
+
gap: "8px",
|
|
394
|
+
flexWrap: "wrap",
|
|
357
395
|
};
|
|
358
396
|
|
|
359
|
-
return (
|
|
397
|
+
return (
|
|
360
398
|
<div key={`group-${group.id || index}`} style={groupStyle}>
|
|
361
|
-
{group.display ===
|
|
362
|
-
(() => {
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
// If there's only one block option, render it as a disabled-style button
|
|
391
|
-
if (blockOptions.length === 1) {
|
|
392
|
-
const singleOption = dropdownOptions.find(opt =>
|
|
393
|
-
blockOptions.some(blockOpt =>
|
|
394
|
-
blockOpt.type === 'block' &&
|
|
395
|
-
getOption(blockOpt.type, blockOpt.id) === opt.value
|
|
396
|
-
)
|
|
399
|
+
{group.display === "buttons"
|
|
400
|
+
? (() => {
|
|
401
|
+
const subGroups = splitOptionsByDividers(validOptions);
|
|
402
|
+
return subGroups.map((subGroup, subGroupIndex) => (
|
|
403
|
+
<div
|
|
404
|
+
key={`subgroup-${subGroupIndex}`}
|
|
405
|
+
className="toolbar-button-group"
|
|
406
|
+
>
|
|
407
|
+
{subGroup.map((option, optionIndex) => {
|
|
408
|
+
const optionObj = getOption(option.type, option.id);
|
|
409
|
+
if (!optionObj) return null;
|
|
410
|
+
|
|
411
|
+
return (
|
|
412
|
+
<ToolbarButton
|
|
413
|
+
key={`${option.type}-${option.id}`}
|
|
414
|
+
icon={optionObj.icon}
|
|
415
|
+
active={isOptionActive(option.type, option.id)}
|
|
416
|
+
onMouseDown={handleOptionSelect(option.type, option.id)}
|
|
417
|
+
/>
|
|
418
|
+
);
|
|
419
|
+
})}
|
|
420
|
+
</div>
|
|
421
|
+
));
|
|
422
|
+
})()
|
|
423
|
+
: (() => {
|
|
424
|
+
const dropdownOptions = createDropdownOptions(validOptions);
|
|
425
|
+
const blockOptions = validOptions.filter(
|
|
426
|
+
(option) => option.type === "block",
|
|
397
427
|
);
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
<span className="toolbar-dropdown-icon">
|
|
415
|
-
{singleOption?.icon}
|
|
416
|
-
</span>
|
|
417
|
-
<span className="toolbar-dropdown-arrow">▼</span>
|
|
418
|
-
</>
|
|
419
|
-
) : (
|
|
420
|
-
<>
|
|
421
|
-
<span className="toolbar-dropdown-icon">
|
|
422
|
-
{singleOption?.icon && (
|
|
423
|
-
<span className="toolbar-dropdown-icon">{singleOption.icon}</span>
|
|
424
|
-
)}
|
|
428
|
+
|
|
429
|
+
// If there's only one block option, render it as a disabled-style button
|
|
430
|
+
if (blockOptions.length === 1) {
|
|
431
|
+
const singleOption = dropdownOptions.find((opt) =>
|
|
432
|
+
blockOptions.some(
|
|
433
|
+
(blockOpt) =>
|
|
434
|
+
blockOpt.type === "block" &&
|
|
435
|
+
getOption(blockOpt.type, blockOpt.id) === opt.value,
|
|
436
|
+
),
|
|
437
|
+
);
|
|
438
|
+
|
|
439
|
+
return (
|
|
440
|
+
<div className="toolbar-dropdown-container">
|
|
441
|
+
<button className="toolbar-dropdown-button" disabled>
|
|
442
|
+
{group.label ? (
|
|
443
|
+
<>
|
|
425
444
|
<span className="toolbar-dropdown-content">
|
|
426
|
-
{singleOption?.label}
|
|
445
|
+
{group.label}: {singleOption?.label}
|
|
427
446
|
</span>
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
447
|
+
<span className="toolbar-dropdown-arrow">▼</span>
|
|
448
|
+
</>
|
|
449
|
+
) : group.showIconsOnly ? (
|
|
450
|
+
<>
|
|
451
|
+
<span className="toolbar-dropdown-icon">
|
|
452
|
+
{singleOption?.icon}
|
|
453
|
+
</span>
|
|
454
|
+
<span className="toolbar-dropdown-arrow">▼</span>
|
|
455
|
+
</>
|
|
456
|
+
) : (
|
|
457
|
+
<>
|
|
458
|
+
<span className="toolbar-dropdown-icon">
|
|
459
|
+
{singleOption?.icon && (
|
|
460
|
+
<span className="toolbar-dropdown-icon">
|
|
461
|
+
{singleOption.icon}
|
|
462
|
+
</span>
|
|
463
|
+
)}
|
|
464
|
+
<span className="toolbar-dropdown-content">
|
|
465
|
+
{singleOption?.label}
|
|
466
|
+
</span>
|
|
467
|
+
</span>
|
|
468
|
+
<span className="toolbar-dropdown-arrow">▼</span>
|
|
469
|
+
</>
|
|
470
|
+
)}
|
|
471
|
+
</button>
|
|
472
|
+
</div>
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Multiple options - render normally wrapped in grey container
|
|
477
|
+
const isActive = dropdownOptions.some((option) =>
|
|
478
|
+
option.isActive(editor),
|
|
479
|
+
);
|
|
480
|
+
return (
|
|
481
|
+
<div className="toolbar-dropdown-container">
|
|
482
|
+
<EditorDropdown
|
|
483
|
+
options={dropdownOptions}
|
|
484
|
+
editor={editor}
|
|
485
|
+
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
|
+
}}
|
|
497
|
+
/>
|
|
433
498
|
</div>
|
|
434
499
|
);
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
// Multiple options - render normally wrapped in grey container
|
|
438
|
-
const isActive = dropdownOptions.some(option => option.isActive(editor));
|
|
439
|
-
return (
|
|
440
|
-
<div className="toolbar-dropdown-container">
|
|
441
|
-
<EditorDropdown
|
|
442
|
-
options={dropdownOptions}
|
|
443
|
-
editor={editor}
|
|
444
|
-
label={group.label}
|
|
445
|
-
buttonStyle={{
|
|
446
|
-
padding: '5px 10px',
|
|
447
|
-
margin: '0',
|
|
448
|
-
background: isActive ? '#ffffff' : 'transparent',
|
|
449
|
-
border: 'none',
|
|
450
|
-
borderRadius: '3px',
|
|
451
|
-
cursor: 'pointer',
|
|
452
|
-
boxShadow: isActive ? '0 1px 2px rgba(0,0,0,0.1)' : 'none'
|
|
453
|
-
}}
|
|
454
|
-
/>
|
|
455
|
-
</div>
|
|
456
|
-
);
|
|
457
|
-
})()
|
|
458
|
-
)}
|
|
500
|
+
})()}
|
|
459
501
|
</div>
|
|
460
502
|
);
|
|
461
503
|
};
|
|
462
504
|
|
|
463
505
|
const [showLinkDialog, setShowLinkDialog] = useState(false);
|
|
464
506
|
const [selectedLink, setSelectedLink] = useState<any>(null);
|
|
465
|
-
const [linkDialogCallback, setLinkDialogCallback] = useState<
|
|
507
|
+
const [linkDialogCallback, setLinkDialogCallback] = useState<
|
|
508
|
+
((link: any) => void) | null
|
|
509
|
+
>(null);
|
|
466
510
|
|
|
467
511
|
const editLink = useCallback((element: any) => {
|
|
468
|
-
const linkType = element.link?.type ||
|
|
512
|
+
const linkType = element.link?.type || "external";
|
|
469
513
|
let linkData;
|
|
470
|
-
|
|
471
|
-
if (linkType ===
|
|
514
|
+
|
|
515
|
+
if (linkType === "internal") {
|
|
472
516
|
linkData = {
|
|
473
|
-
type:
|
|
474
|
-
itemId:
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
517
|
+
type: "internal",
|
|
518
|
+
itemId:
|
|
519
|
+
element.link?.itemId ||
|
|
520
|
+
element.link?.targetItemLongId?.split("/").pop() ||
|
|
521
|
+
"",
|
|
522
|
+
targetItemLongId: element.link?.targetItemLongId || "",
|
|
523
|
+
target: element.link?.target || "",
|
|
524
|
+
queryString: element.link?.queryString || "",
|
|
478
525
|
};
|
|
479
526
|
} else {
|
|
480
527
|
linkData = {
|
|
481
|
-
type:
|
|
482
|
-
url: element.url || element.link?.url ||
|
|
483
|
-
target: element.link?.target ||
|
|
484
|
-
queryString: element.link?.queryString ||
|
|
528
|
+
type: "external",
|
|
529
|
+
url: element.url || element.link?.url || "",
|
|
530
|
+
target: element.link?.target || "_blank",
|
|
531
|
+
queryString: element.link?.queryString || "",
|
|
485
532
|
};
|
|
486
533
|
}
|
|
487
|
-
|
|
534
|
+
|
|
488
535
|
setSelectedLink(linkData);
|
|
489
536
|
setShowLinkDialog(true);
|
|
490
537
|
}, []);
|
|
491
538
|
|
|
492
|
-
const handleLinkUpdate = useCallback(
|
|
493
|
-
|
|
494
|
-
linkDialogCallback
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
type: 'external',
|
|
539
|
+
const handleLinkUpdate = useCallback(
|
|
540
|
+
(link: any) => {
|
|
541
|
+
if (linkDialogCallback) {
|
|
542
|
+
linkDialogCallback(link);
|
|
543
|
+
setLinkDialogCallback(null);
|
|
544
|
+
} else {
|
|
545
|
+
const [linkNode] = Editor.nodes(editor, {
|
|
546
|
+
match: (n) =>
|
|
547
|
+
!Editor.isEditor(n) && Element.isElement(n) && n.type === "link",
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
if (linkNode) {
|
|
551
|
+
const [node, path] = linkNode;
|
|
552
|
+
let newProperties: Partial<CustomElement>;
|
|
553
|
+
|
|
554
|
+
if (link.type === "internal") {
|
|
555
|
+
newProperties = {
|
|
556
|
+
url: "",
|
|
557
|
+
link: {
|
|
558
|
+
type: "internal",
|
|
559
|
+
targetItemLongId: link.targetItemLongId,
|
|
560
|
+
itemId: link.itemId,
|
|
561
|
+
target: link.target,
|
|
562
|
+
queryString: link.queryString,
|
|
563
|
+
},
|
|
564
|
+
};
|
|
565
|
+
} else {
|
|
566
|
+
newProperties = {
|
|
521
567
|
url: link.url,
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
568
|
+
link: {
|
|
569
|
+
type: "external",
|
|
570
|
+
url: link.url,
|
|
571
|
+
target: link.target,
|
|
572
|
+
queryString: link.queryString,
|
|
573
|
+
},
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
Transforms.setNodes(editor, newProperties, { at: path });
|
|
578
|
+
|
|
579
|
+
// Move cursor after the link
|
|
580
|
+
const after = Editor.after(editor, path);
|
|
581
|
+
if (after) {
|
|
582
|
+
Transforms.select(editor, after);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
ReactEditor.focus(editor);
|
|
534
586
|
}
|
|
535
|
-
|
|
536
|
-
ReactEditor.focus(editor);
|
|
537
587
|
}
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
588
|
+
|
|
589
|
+
setShowLinkDialog(false);
|
|
590
|
+
setSelectedLink(null);
|
|
591
|
+
},
|
|
592
|
+
[editor, linkDialogCallback],
|
|
593
|
+
);
|
|
543
594
|
|
|
544
595
|
const handleLinkButtonClick = useCallback(() => {
|
|
545
596
|
editor.insertLink({
|
|
546
597
|
onOpenLinkDialog: (callback: (link: any) => void) => {
|
|
547
|
-
setSelectedLink({
|
|
548
|
-
type:
|
|
549
|
-
url:
|
|
550
|
-
target:
|
|
598
|
+
setSelectedLink({
|
|
599
|
+
type: "external",
|
|
600
|
+
url: "",
|
|
601
|
+
target: "_blank",
|
|
551
602
|
});
|
|
552
603
|
setShowLinkDialog(true);
|
|
553
|
-
|
|
604
|
+
|
|
554
605
|
setLinkDialogCallback(() => callback);
|
|
555
|
-
}
|
|
606
|
+
},
|
|
556
607
|
});
|
|
557
608
|
}, [editor]);
|
|
558
609
|
|
|
559
|
-
const handleKeyDown = useCallback(
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
event.
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
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
|
+
}
|
|
586
642
|
}
|
|
587
643
|
}
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
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
|
+
}
|
|
613
678
|
}
|
|
614
679
|
}
|
|
615
680
|
}
|
|
616
|
-
}
|
|
617
|
-
|
|
681
|
+
},
|
|
682
|
+
[editor],
|
|
683
|
+
);
|
|
618
684
|
|
|
619
685
|
return (
|
|
620
686
|
<div className={`slate-editor ${props.className}`}>
|
|
621
|
-
<Slate
|
|
622
|
-
key={remountKey}
|
|
623
|
-
editor={editor}
|
|
624
|
-
initialValue={internalValue}
|
|
687
|
+
<Slate
|
|
688
|
+
key={remountKey}
|
|
689
|
+
editor={editor}
|
|
690
|
+
initialValue={internalValue}
|
|
625
691
|
onChange={handleChange}
|
|
626
692
|
>
|
|
627
693
|
{!readOnly && (
|
|
628
694
|
<div className="toolbar">
|
|
629
695
|
{(() => {
|
|
630
696
|
// Group toolbar items by row
|
|
631
|
-
const groupsByRow = editorProfile.toolbar.groups.reduce(
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
697
|
+
const groupsByRow = editorProfile.toolbar.groups.reduce(
|
|
698
|
+
(acc, group, index) => {
|
|
699
|
+
const row = group.row !== undefined ? group.row : index;
|
|
700
|
+
if (!acc[row]) acc[row] = [];
|
|
701
|
+
acc[row].push(group);
|
|
702
|
+
return acc;
|
|
703
|
+
},
|
|
704
|
+
{} as Record<number, typeof editorProfile.toolbar.groups>,
|
|
705
|
+
);
|
|
706
|
+
|
|
638
707
|
// Render each row
|
|
639
708
|
return Object.entries(groupsByRow)
|
|
640
709
|
.sort(([a], [b]) => parseInt(a) - parseInt(b))
|
|
641
710
|
.map(([rowIndex, rowGroups]) => (
|
|
642
711
|
<div key={`row-${rowIndex}`} className="toolbar-row">
|
|
643
|
-
{rowGroups.map(group =>
|
|
712
|
+
{rowGroups.map((group) =>
|
|
713
|
+
renderToolbarGroup(group, parseInt(rowIndex)),
|
|
714
|
+
)}
|
|
644
715
|
</div>
|
|
645
716
|
));
|
|
646
717
|
})()}
|
|
647
718
|
</div>
|
|
648
719
|
)}
|
|
649
720
|
<Editable
|
|
650
|
-
className={classNames(
|
|
721
|
+
className={classNames(
|
|
722
|
+
readOnly ? "bg-gray-4" : "bg-gray-5",
|
|
723
|
+
"focus-shadow p-2",
|
|
724
|
+
)}
|
|
651
725
|
readOnly={readOnly}
|
|
652
726
|
placeholder={placeholder}
|
|
653
727
|
onFocus={onFocus}
|
|
@@ -655,19 +729,21 @@ export const ReactSlate = forwardRef<any, ReactSlateProps>((props, ref) => {
|
|
|
655
729
|
onKeyDown={handleKeyDown}
|
|
656
730
|
renderElement={({ attributes, children, element }) => {
|
|
657
731
|
const style: React.CSSProperties = {
|
|
658
|
-
textAlign: element.align ||
|
|
732
|
+
textAlign: element.align || "left",
|
|
659
733
|
};
|
|
660
|
-
|
|
661
|
-
if (element.type ===
|
|
662
|
-
const isInternal = element.link?.type ===
|
|
663
|
-
const url = isInternal
|
|
664
|
-
|
|
734
|
+
|
|
735
|
+
if (element.type === "link") {
|
|
736
|
+
const isInternal = element.link?.type === "internal";
|
|
737
|
+
const url = isInternal
|
|
738
|
+
? "#"
|
|
739
|
+
: element.url || element.link?.url || "#";
|
|
740
|
+
|
|
665
741
|
return (
|
|
666
|
-
<a
|
|
667
|
-
{...attributes}
|
|
742
|
+
<a
|
|
743
|
+
{...attributes}
|
|
668
744
|
href={url}
|
|
669
745
|
style={style}
|
|
670
|
-
className={`slate-link ${isInternal ?
|
|
746
|
+
className={`slate-link ${isInternal ? "internal-link" : "external-link"}`}
|
|
671
747
|
onClick={(e) => {
|
|
672
748
|
if (!readOnly) {
|
|
673
749
|
e.preventDefault();
|
|
@@ -679,94 +755,104 @@ export const ReactSlate = forwardRef<any, ReactSlateProps>((props, ref) => {
|
|
|
679
755
|
</a>
|
|
680
756
|
);
|
|
681
757
|
}
|
|
682
|
-
|
|
683
|
-
if (element.type ===
|
|
758
|
+
|
|
759
|
+
if (element.type === "list-item") {
|
|
684
760
|
const indent = element.indent || 0;
|
|
685
|
-
const listType = element.listType ||
|
|
686
|
-
const isOrdered = listType ===
|
|
687
|
-
|
|
761
|
+
const listType = element.listType || "unordered";
|
|
762
|
+
const isOrdered = listType === "ordered";
|
|
763
|
+
|
|
688
764
|
// Calculate proper numbering for ordered lists
|
|
689
765
|
let listNumber = 1;
|
|
690
766
|
if (isOrdered) {
|
|
691
767
|
// Find the position of this item within its level
|
|
692
768
|
const allElements = editor.children as CustomElement[];
|
|
693
|
-
const currentIndex = allElements.findIndex(
|
|
694
|
-
|
|
769
|
+
const currentIndex = allElements.findIndex(
|
|
770
|
+
(el) => el === element,
|
|
771
|
+
);
|
|
772
|
+
|
|
695
773
|
// Count preceding list items at the same indent level and list type
|
|
696
774
|
let count = 0;
|
|
697
775
|
for (let i = 0; i < currentIndex; i++) {
|
|
698
776
|
const prevElement = allElements[i];
|
|
699
|
-
if (
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
777
|
+
if (
|
|
778
|
+
prevElement &&
|
|
779
|
+
prevElement.type === "list-item" &&
|
|
780
|
+
prevElement.listType === "ordered" &&
|
|
781
|
+
(prevElement.indent || 0) === indent
|
|
782
|
+
) {
|
|
703
783
|
count++;
|
|
704
|
-
} else if (
|
|
705
|
-
|
|
706
|
-
|
|
784
|
+
} else if (
|
|
785
|
+
prevElement &&
|
|
786
|
+
prevElement.type === "list-item" &&
|
|
787
|
+
(prevElement.indent || 0) < indent
|
|
788
|
+
) {
|
|
707
789
|
// Reset count when we encounter a parent-level item
|
|
708
790
|
count = 0;
|
|
709
791
|
}
|
|
710
792
|
}
|
|
711
793
|
listNumber = count + 1;
|
|
712
794
|
}
|
|
713
|
-
|
|
795
|
+
|
|
714
796
|
const listStyle: React.CSSProperties = {
|
|
715
797
|
...style,
|
|
716
|
-
position:
|
|
717
|
-
listStyleType:
|
|
798
|
+
position: "relative",
|
|
799
|
+
listStyleType: "none",
|
|
718
800
|
};
|
|
719
|
-
|
|
720
|
-
const bulletContent = isOrdered ? `${listNumber}.` :
|
|
721
|
-
|
|
801
|
+
|
|
802
|
+
const bulletContent = isOrdered ? `${listNumber}.` : "";
|
|
803
|
+
|
|
722
804
|
return (
|
|
723
|
-
<div
|
|
724
|
-
{...attributes}
|
|
805
|
+
<div
|
|
806
|
+
{...attributes}
|
|
725
807
|
style={listStyle}
|
|
726
808
|
className={`slate-list-item slate-list-${listType}`}
|
|
727
809
|
data-indent={indent}
|
|
728
810
|
>
|
|
729
|
-
<span className="slate-list-bullet">
|
|
730
|
-
|
|
731
|
-
</span>
|
|
732
|
-
<div className="slate-list-content">
|
|
733
|
-
{children}
|
|
734
|
-
</div>
|
|
811
|
+
<span className="slate-list-bullet">{bulletContent}</span>
|
|
812
|
+
<div className="slate-list-content">{children}</div>
|
|
735
813
|
</div>
|
|
736
814
|
);
|
|
737
815
|
}
|
|
738
|
-
|
|
816
|
+
|
|
739
817
|
// Handle different block types using built-in SLATE_BLOCKS configuration
|
|
740
818
|
const blockConfig = SLATE_BLOCKS[element.type];
|
|
741
|
-
if (blockConfig && element.type ===
|
|
819
|
+
if (blockConfig && element.type === "no-tag") {
|
|
742
820
|
// Special handling for no-tag blocks (plain text without wrapper)
|
|
743
|
-
return
|
|
821
|
+
return (
|
|
822
|
+
<span {...attributes} style={style}>
|
|
823
|
+
{children}
|
|
824
|
+
</span>
|
|
825
|
+
);
|
|
744
826
|
}
|
|
745
|
-
|
|
827
|
+
|
|
746
828
|
// For standard blocks, use the appropriate HTML tag
|
|
747
829
|
if (blockConfig) {
|
|
748
830
|
const tagName = blockConfig.htmlTag;
|
|
749
|
-
return React.createElement(
|
|
831
|
+
return React.createElement(
|
|
832
|
+
tagName,
|
|
833
|
+
{ ...attributes, style },
|
|
834
|
+
children,
|
|
835
|
+
);
|
|
750
836
|
}
|
|
751
|
-
|
|
837
|
+
|
|
752
838
|
// Default fallback to paragraph
|
|
753
|
-
return
|
|
839
|
+
return (
|
|
840
|
+
<p {...attributes} style={style}>
|
|
841
|
+
{children}
|
|
842
|
+
</p>
|
|
843
|
+
);
|
|
754
844
|
}}
|
|
755
845
|
renderLeaf={({ attributes, children, leaf }) => {
|
|
756
846
|
let el = <span {...attributes}>{children}</span>;
|
|
757
|
-
|
|
847
|
+
|
|
758
848
|
// Apply marks using the built-in SLATE_MARKS configuration
|
|
759
|
-
simplifiedProfile.marks.forEach(markId => {
|
|
849
|
+
simplifiedProfile.marks.forEach((markId) => {
|
|
760
850
|
if ((leaf as any)[markId]) {
|
|
761
851
|
const markConfig = SLATE_MARKS[markId];
|
|
762
852
|
if (markConfig) {
|
|
763
|
-
if (markId ===
|
|
853
|
+
if (markId === "extrabold") {
|
|
764
854
|
// Special handling for extrabold with CSS class
|
|
765
|
-
el =
|
|
766
|
-
<span className="extrabold">
|
|
767
|
-
{el}
|
|
768
|
-
</span>
|
|
769
|
-
);
|
|
855
|
+
el = <span className="extrabold">{el}</span>;
|
|
770
856
|
} else {
|
|
771
857
|
// Standard HTML tag rendering
|
|
772
858
|
const tagName = markConfig.htmlTag;
|
|
@@ -775,12 +861,12 @@ export const ReactSlate = forwardRef<any, ReactSlateProps>((props, ref) => {
|
|
|
775
861
|
}
|
|
776
862
|
}
|
|
777
863
|
});
|
|
778
|
-
|
|
864
|
+
|
|
779
865
|
return el;
|
|
780
866
|
}}
|
|
781
867
|
/>
|
|
782
868
|
</Slate>
|
|
783
|
-
|
|
869
|
+
|
|
784
870
|
{showLinkDialog && selectedLink && (
|
|
785
871
|
<LinkEditorDialog
|
|
786
872
|
linkValue={selectedLink}
|
|
@@ -796,6 +882,6 @@ export const ReactSlate = forwardRef<any, ReactSlateProps>((props, ref) => {
|
|
|
796
882
|
);
|
|
797
883
|
});
|
|
798
884
|
|
|
799
|
-
ReactSlate.displayName =
|
|
885
|
+
ReactSlate.displayName = "ReactSlate";
|
|
800
886
|
|
|
801
887
|
export default ReactSlate;
|