@alpaca-editor/core 1.0.3992 → 1.0.3993
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/config/config.js +0 -12
- package/dist/config/config.js.map +1 -1
- package/dist/config/types.d.ts +6 -3
- package/dist/editor/ContentTree.d.ts +2 -1
- package/dist/editor/ContentTree.js +2 -2
- package/dist/editor/ContentTree.js.map +1 -1
- package/dist/editor/ItemInfo.js +1 -1
- package/dist/editor/ItemInfo.js.map +1 -1
- package/dist/editor/field-types/InternalLinkFieldEditor.js +20 -25
- package/dist/editor/field-types/InternalLinkFieldEditor.js.map +1 -1
- package/dist/editor/field-types/RichTextEditor.d.ts +2 -2
- package/dist/editor/field-types/RichTextEditor.js +15 -2
- package/dist/editor/field-types/RichTextEditor.js.map +1 -1
- package/dist/editor/field-types/RichTextEditorComponent.d.ts +5 -5
- package/dist/editor/field-types/RichTextEditorComponent.js +40 -51
- package/dist/editor/field-types/RichTextEditorComponent.js.map +1 -1
- package/dist/editor/field-types/TreeListEditor.js +7 -5
- package/dist/editor/field-types/TreeListEditor.js.map +1 -1
- package/dist/editor/field-types/richtext/components/EditorDropdown.d.ts +11 -0
- package/dist/editor/field-types/richtext/components/EditorDropdown.js +96 -0
- package/dist/editor/field-types/richtext/components/EditorDropdown.js.map +1 -0
- package/dist/editor/field-types/richtext/components/ReactSlate.d.ts +5 -0
- package/dist/editor/field-types/richtext/components/ReactSlate.js +558 -0
- package/dist/editor/field-types/richtext/components/ReactSlate.js.map +1 -0
- package/dist/editor/field-types/richtext/components/ToolbarButton.d.ts +3 -0
- package/dist/editor/field-types/richtext/components/ToolbarButton.js +13 -0
- package/dist/editor/field-types/richtext/components/ToolbarButton.js.map +1 -0
- package/dist/editor/field-types/richtext/config/pluginFactory.d.ts +17 -0
- package/dist/editor/field-types/richtext/config/pluginFactory.js +14 -0
- package/dist/editor/field-types/richtext/config/pluginFactory.js.map +1 -0
- package/dist/editor/field-types/richtext/hooks/useProfileCache.d.ts +68 -0
- package/dist/editor/field-types/richtext/hooks/useProfileCache.js +208 -0
- package/dist/editor/field-types/richtext/hooks/useProfileCache.js.map +1 -0
- package/dist/editor/field-types/richtext/hooks/useRichTextProfile.d.ts +25 -0
- package/dist/editor/field-types/richtext/hooks/useRichTextProfile.js +64 -0
- package/dist/editor/field-types/richtext/hooks/useRichTextProfile.js.map +1 -0
- package/dist/editor/field-types/richtext/index.d.ts +5 -0
- package/dist/editor/field-types/richtext/index.js +6 -0
- package/dist/editor/field-types/richtext/index.js.map +1 -0
- package/dist/editor/field-types/richtext/types.d.ts +139 -0
- package/dist/editor/field-types/richtext/types.js +107 -0
- package/dist/editor/field-types/richtext/types.js.map +1 -0
- package/dist/editor/field-types/richtext/utils/conversion.d.ts +5 -0
- package/dist/editor/field-types/richtext/utils/conversion.js +539 -0
- package/dist/editor/field-types/richtext/utils/conversion.js.map +1 -0
- package/dist/editor/field-types/richtext/utils/plugins.d.ts +97 -0
- package/dist/editor/field-types/richtext/utils/plugins.js +272 -0
- package/dist/editor/field-types/richtext/utils/plugins.js.map +1 -0
- package/dist/editor/field-types/richtext/utils/profileMapper.d.ts +38 -0
- package/dist/editor/field-types/richtext/utils/profileMapper.js +366 -0
- package/dist/editor/field-types/richtext/utils/profileMapper.js.map +1 -0
- package/dist/editor/field-types/richtext/utils/profileServiceCache.d.ts +37 -0
- package/dist/editor/field-types/richtext/utils/profileServiceCache.js +117 -0
- package/dist/editor/field-types/richtext/utils/profileServiceCache.js.map +1 -0
- package/dist/editor/services/contentService.d.ts +8 -0
- package/dist/editor/services/contentService.js +3 -0
- package/dist/editor/services/contentService.js.map +1 -1
- package/dist/editor/sidebar/ComponentTree.js +7 -1
- package/dist/editor/sidebar/ComponentTree.js.map +1 -1
- package/dist/editor/ui/PerfectTree.d.ts +4 -2
- package/dist/editor/ui/PerfectTree.js +16 -7
- package/dist/editor/ui/PerfectTree.js.map +1 -1
- package/dist/editor/utils/itemutils.js +3 -1
- package/dist/editor/utils/itemutils.js.map +1 -1
- package/dist/editor/views/ItemEditor.js +10 -3
- package/dist/editor/views/ItemEditor.js.map +1 -1
- package/dist/page-wizard/steps/ContentStep.js +1 -3
- package/dist/page-wizard/steps/ContentStep.js.map +1 -1
- package/dist/page-wizard/steps/usePageCreator.js +1 -1
- package/dist/page-wizard/steps/usePageCreator.js.map +1 -1
- package/dist/revision.d.ts +2 -2
- package/dist/revision.js +2 -2
- package/dist/styles.css +9 -0
- package/package.json +4 -1
- package/src/config/config.tsx +0 -12
- package/src/config/types.ts +6 -3
- package/src/editor/ContentTree.tsx +3 -0
- package/src/editor/ItemInfo.tsx +2 -2
- package/src/editor/field-types/InternalLinkFieldEditor.tsx +73 -69
- package/src/editor/field-types/RichTextEditor.tsx +31 -3
- package/src/editor/field-types/RichTextEditorComponent.tsx +52 -69
- package/src/editor/field-types/TreeListEditor.tsx +7 -7
- package/src/editor/field-types/richtext/components/EditorDropdown.tsx +180 -0
- package/src/editor/field-types/richtext/components/ReactSlate.css +163 -0
- package/src/editor/field-types/richtext/components/ReactSlate.tsx +792 -0
- package/src/editor/field-types/richtext/components/ToolbarButton.tsx +23 -0
- package/src/editor/field-types/richtext/config/pluginFactory.tsx +22 -0
- package/src/editor/field-types/richtext/hooks/useProfileCache.ts +270 -0
- package/src/editor/field-types/richtext/hooks/useRichTextProfile.ts +94 -0
- package/src/editor/field-types/richtext/index.ts +5 -0
- package/src/editor/field-types/richtext/types.ts +269 -0
- package/src/editor/field-types/richtext/utils/conversion.ts +589 -0
- package/src/editor/field-types/richtext/utils/plugins.ts +346 -0
- package/src/editor/field-types/richtext/utils/profileMapper.ts +424 -0
- package/src/editor/field-types/richtext/utils/profileServiceCache.ts +154 -0
- package/src/editor/services/contentService.ts +12 -0
- package/src/editor/sidebar/ComponentTree.tsx +12 -1
- package/src/editor/ui/PerfectTree.tsx +19 -6
- package/src/editor/utils/{itemutils.ts → itemutils.tsx} +12 -12
- package/src/editor/views/ItemEditor.tsx +22 -1
- package/src/page-wizard/steps/ContentStep.tsx +1 -3
- package/src/page-wizard/steps/usePageCreator.ts +1 -0
- package/src/revision.ts +2 -2
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
import { RichTextEditorProfile, ToolbarGroupConfig, ToolbarOptionConfig } from '../types';
|
|
2
|
+
|
|
3
|
+
// Define the mapping from Sitecore button names to Slate options
|
|
4
|
+
const SITECORE_TO_SLATE_MAPPING: Record<string, ToolbarOptionConfig | null> = {
|
|
5
|
+
// Text formatting marks
|
|
6
|
+
'Bold': { type: 'mark', id: 'bold' },
|
|
7
|
+
'Italic': { type: 'mark', id: 'italic' },
|
|
8
|
+
'Underline': { type: 'mark', id: 'underline' },
|
|
9
|
+
'StrikeThrough': { type: 'mark', id: 'strikethrough' },
|
|
10
|
+
'Subscript': { type: 'mark', id: 'subscript' },
|
|
11
|
+
'Superscript': { type: 'mark', id: 'superscript' },
|
|
12
|
+
|
|
13
|
+
// Custom headline marks
|
|
14
|
+
'Extrabold': { type: 'mark', id: 'extrabold' },
|
|
15
|
+
|
|
16
|
+
// Alignment options
|
|
17
|
+
'JustifyLeft': { type: 'alignment', id: 'left' },
|
|
18
|
+
'JustifyCenter': { type: 'alignment', id: 'center' },
|
|
19
|
+
'JustifyRight': { type: 'alignment', id: 'right' },
|
|
20
|
+
'JustifyFull': { type: 'alignment', id: 'justify' },
|
|
21
|
+
'JustifyNone': { type: 'alignment', id: 'left' }, // Default to left
|
|
22
|
+
|
|
23
|
+
// Block formats
|
|
24
|
+
'InsertOrderedList': { type: 'list', id: 'ordered-list' },
|
|
25
|
+
'InsertUnorderedList': { type: 'list', id: 'unordered-list' },
|
|
26
|
+
'Indent': { type: 'list', id: 'indent' },
|
|
27
|
+
'Outdent': { type: 'list', id: 'outdent' },
|
|
28
|
+
'FormatBlock': { type: 'block', id: 'paragraph' },
|
|
29
|
+
|
|
30
|
+
// Additional block format options that might come from Sitecore
|
|
31
|
+
'Paragraph': { type: 'block', id: 'paragraph' },
|
|
32
|
+
'PlainText': { type: 'block', id: 'no-tag' },
|
|
33
|
+
'plaintext': { type: 'block', id: 'no-tag' }, // Handle lowercase version from option headers
|
|
34
|
+
'NoTag': { type: 'block', id: 'no-tag' },
|
|
35
|
+
'Heading1': { type: 'block', id: 'heading-1' },
|
|
36
|
+
'Heading2': { type: 'block', id: 'heading-2' },
|
|
37
|
+
'Heading3': { type: 'block', id: 'heading-3' },
|
|
38
|
+
'Heading4': { type: 'block', id: 'heading-4' },
|
|
39
|
+
'Heading5': { type: 'block', id: 'heading-5' },
|
|
40
|
+
'Heading6': { type: 'block', id: 'heading-6' },
|
|
41
|
+
|
|
42
|
+
// Links
|
|
43
|
+
'InsertSitecoreLink': { type: 'link', id: 'link' }, // Internal/External link
|
|
44
|
+
'InsertCustomLink': { type: 'link', id: 'link' }, // Basic external link
|
|
45
|
+
'Unlink': { type: 'link', id: 'unlink' },
|
|
46
|
+
|
|
47
|
+
// Line breaks - handled by Shift+Enter, not as a button
|
|
48
|
+
'ManualLineBreak': null, // Handled by keyboard shortcut
|
|
49
|
+
|
|
50
|
+
// Colors (future enhancement - not implemented yet)
|
|
51
|
+
'ForeColor': null, // { type: 'color', id: 'text-color' },
|
|
52
|
+
'BackColor': null, // { type: 'color', id: 'background-color' },
|
|
53
|
+
|
|
54
|
+
// Typography (future enhancement - not implemented yet)
|
|
55
|
+
'FontName': null, // { type: 'font', id: 'font-family' },
|
|
56
|
+
'FontSize': null, // { type: 'font', id: 'font-size' },
|
|
57
|
+
|
|
58
|
+
// Special UI elements
|
|
59
|
+
'Divider': { type: 'divider', id: 'divider' },
|
|
60
|
+
'Print': null,
|
|
61
|
+
'FindAndReplace': null,
|
|
62
|
+
'Cut': null,
|
|
63
|
+
'Copy': null,
|
|
64
|
+
'Paste': null,
|
|
65
|
+
'PasteFromWord': null,
|
|
66
|
+
'PasteFromWordNoFontsNoSizes': null,
|
|
67
|
+
'PastePlainText': null,
|
|
68
|
+
'PasteAsHtml': null,
|
|
69
|
+
'FormatStripper': null,
|
|
70
|
+
'Undo': null,
|
|
71
|
+
'Redo': null,
|
|
72
|
+
'InsertSitecoreMedia': null,
|
|
73
|
+
'LinkManager': null,
|
|
74
|
+
'InsertTable': null,
|
|
75
|
+
'InsertFormElement': null,
|
|
76
|
+
'InsertParagraph': null,
|
|
77
|
+
'InsertDate': null,
|
|
78
|
+
'InsertTime': null,
|
|
79
|
+
'InsertSymbol': null,
|
|
80
|
+
'InsertSnippet': null,
|
|
81
|
+
'InsertHorizontalRule': null,
|
|
82
|
+
'ToggleTableBorder': null,
|
|
83
|
+
'ModuleManager': null,
|
|
84
|
+
'AjaxSpellCheck': null,
|
|
85
|
+
'XhtmlValidator': null,
|
|
86
|
+
'Help': null,
|
|
87
|
+
'ApplyClass': null,
|
|
88
|
+
'SelectAll': null,
|
|
89
|
+
'AbsolutePosition': null,
|
|
90
|
+
'MediaManager': null,
|
|
91
|
+
'FlashManager': null,
|
|
92
|
+
'Zoom': null,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export interface SitecoreButton {
|
|
96
|
+
name: string;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface SitecoreToolbar {
|
|
100
|
+
buttons: SitecoreButton[];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export type SitecoreProfile = SitecoreToolbar[];
|
|
104
|
+
|
|
105
|
+
export interface SitecoreOption {
|
|
106
|
+
header: string;
|
|
107
|
+
value: string;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface SitecoreOptionFolder {
|
|
111
|
+
name: string;
|
|
112
|
+
options: SitecoreOption[];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface SitecoreProfileWrapper {
|
|
116
|
+
toolbars: SitecoreToolbar[];
|
|
117
|
+
options: SitecoreOptionFolder[];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Cleans up dividers in an options array by removing consecutive dividers and dividers at start/end
|
|
122
|
+
*/
|
|
123
|
+
function cleanupDividers(options: ToolbarOptionConfig[]): ToolbarOptionConfig[] {
|
|
124
|
+
if (options.length === 0) return options;
|
|
125
|
+
|
|
126
|
+
const cleaned: ToolbarOptionConfig[] = [];
|
|
127
|
+
|
|
128
|
+
for (let i = 0; i < options.length; i++) {
|
|
129
|
+
const option = options[i];
|
|
130
|
+
if (!option) continue;
|
|
131
|
+
|
|
132
|
+
// Skip dividers at the start
|
|
133
|
+
if (option.type === 'divider' && cleaned.length === 0) {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Skip consecutive dividers
|
|
138
|
+
const lastOption = cleaned[cleaned.length - 1];
|
|
139
|
+
if (option.type === 'divider' && cleaned.length > 0 && lastOption && lastOption.type === 'divider') {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
cleaned.push(option);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Remove divider at the end if present
|
|
147
|
+
const lastOption = cleaned[cleaned.length - 1];
|
|
148
|
+
if (cleaned.length > 0 && lastOption && lastOption.type === 'divider') {
|
|
149
|
+
cleaned.pop();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return cleaned;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Maps a Sitecore rich text profile to a Slate editor profile
|
|
157
|
+
*/
|
|
158
|
+
export function mapSitecoreProfileToSlate(sitecoreProfile: SitecoreProfile, optionFolders: SitecoreOptionFolder[] = []): RichTextEditorProfile {
|
|
159
|
+
const groups: ToolbarGroupConfig[] = [];
|
|
160
|
+
const specialButtons = {
|
|
161
|
+
alignment: new Set<string>(),
|
|
162
|
+
block: new Set<string>()
|
|
163
|
+
};
|
|
164
|
+
const unsupportedButtons = new Set<string>();
|
|
165
|
+
|
|
166
|
+
sitecoreProfile.forEach((toolbar, toolbarIndex) => {
|
|
167
|
+
const rowGroups: ToolbarGroupConfig[] = [];
|
|
168
|
+
const rawOptions: ToolbarOptionConfig[] = [];
|
|
169
|
+
|
|
170
|
+
toolbar.buttons.forEach(button => {
|
|
171
|
+
// Handle special "Paragraphs" button
|
|
172
|
+
if (button.name === 'Paragraphs') {
|
|
173
|
+
const paragraphsFolder = optionFolders.find(folder => folder.name === 'Paragraphs');
|
|
174
|
+
if (paragraphsFolder && paragraphsFolder.options.length > 0) {
|
|
175
|
+
const paragraphOptions = paragraphsFolder.options
|
|
176
|
+
.map(option => {
|
|
177
|
+
// Try to find mapping for the header (first try as-is, then lowercase)
|
|
178
|
+
const mappedOption = SITECORE_TO_SLATE_MAPPING[option.header] ||
|
|
179
|
+
SITECORE_TO_SLATE_MAPPING[option.header.toLowerCase()];
|
|
180
|
+
return mappedOption ? { type: mappedOption.type, id: mappedOption.id } : null;
|
|
181
|
+
})
|
|
182
|
+
.filter(option => option !== null) as ToolbarOptionConfig[];
|
|
183
|
+
|
|
184
|
+
if (paragraphOptions.length > 0) {
|
|
185
|
+
// Add paragraphs dropdown to the row
|
|
186
|
+
rowGroups.push({
|
|
187
|
+
id: `paragraphs-${toolbarIndex}`,
|
|
188
|
+
label: 'Format',
|
|
189
|
+
display: 'dropdown',
|
|
190
|
+
showIconsOnly: false,
|
|
191
|
+
options: paragraphOptions,
|
|
192
|
+
row: toolbarIndex
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return; // Skip normal processing for Paragraphs button
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const mappedOption = SITECORE_TO_SLATE_MAPPING[button.name];
|
|
200
|
+
if (mappedOption) {
|
|
201
|
+
if (mappedOption.type === 'alignment') {
|
|
202
|
+
specialButtons.alignment.add(mappedOption.id);
|
|
203
|
+
} else if (mappedOption.type === 'block') {
|
|
204
|
+
specialButtons.block.add(mappedOption.id);
|
|
205
|
+
} else {
|
|
206
|
+
rawOptions.push(mappedOption);
|
|
207
|
+
}
|
|
208
|
+
} else if (!SITECORE_TO_SLATE_MAPPING.hasOwnProperty(button.name)) {
|
|
209
|
+
unsupportedButtons.add(button.name);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const cleanedOptions = cleanupDividers(rawOptions);
|
|
214
|
+
|
|
215
|
+
// Add buttons group if there are any buttons
|
|
216
|
+
if (cleanedOptions.length > 0) {
|
|
217
|
+
rowGroups.push({
|
|
218
|
+
id: `toolbar-${toolbarIndex}`,
|
|
219
|
+
label: `Toolbar ${toolbarIndex + 1}`,
|
|
220
|
+
display: 'buttons',
|
|
221
|
+
showIconsOnly: true,
|
|
222
|
+
options: cleanedOptions,
|
|
223
|
+
row: toolbarIndex
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Add all groups for this toolbar row
|
|
228
|
+
groups.push(...rowGroups);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// Create dropdown groups for special button types
|
|
232
|
+
const dropdownGroups = [
|
|
233
|
+
{
|
|
234
|
+
condition: specialButtons.block.size > 1 || (specialButtons.block.size === 1 && !specialButtons.block.has('paragraph')),
|
|
235
|
+
id: 'format-group',
|
|
236
|
+
label: '',
|
|
237
|
+
showIconsOnly: false,
|
|
238
|
+
order: ['paragraph', 'no-tag', 'heading-1', 'heading-2', 'heading-3', 'heading-4', 'heading-5', 'heading-6'],
|
|
239
|
+
buttons: specialButtons.block,
|
|
240
|
+
type: 'block' as const,
|
|
241
|
+
prepend: true
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
condition: specialButtons.alignment.size > 0,
|
|
245
|
+
id: 'alignment-group',
|
|
246
|
+
label: '',
|
|
247
|
+
showIconsOnly: true,
|
|
248
|
+
order: ['left', 'center', 'right', 'justify'],
|
|
249
|
+
buttons: specialButtons.alignment,
|
|
250
|
+
type: 'alignment' as const,
|
|
251
|
+
prepend: false
|
|
252
|
+
}
|
|
253
|
+
];
|
|
254
|
+
|
|
255
|
+
dropdownGroups.forEach(group => {
|
|
256
|
+
if (group.condition) {
|
|
257
|
+
const options = group.order
|
|
258
|
+
.filter(item => group.buttons.has(item))
|
|
259
|
+
.map(item => ({ type: group.type, id: item }));
|
|
260
|
+
|
|
261
|
+
const groupConfig: ToolbarGroupConfig = {
|
|
262
|
+
id: group.id,
|
|
263
|
+
label: group.label,
|
|
264
|
+
display: 'dropdown',
|
|
265
|
+
showIconsOnly: group.showIconsOnly,
|
|
266
|
+
options: options,
|
|
267
|
+
row: group.prepend ? -1 : 1000 // Put prepend groups first, append groups last
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
if (group.prepend) {
|
|
271
|
+
groups.unshift(groupConfig);
|
|
272
|
+
} else {
|
|
273
|
+
groups.push(groupConfig);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
if (unsupportedButtons.size > 0) {
|
|
279
|
+
console.warn('Unsupported Sitecore buttons found:', Array.from(unsupportedButtons).join(', '));
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
toolbar: {
|
|
284
|
+
groups: groups
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Maps a Sitecore profile JSON string to a Slate editor profile
|
|
291
|
+
*/
|
|
292
|
+
export function mapSitecoreProfileJsonToSlate(profileJson: string): RichTextEditorProfile | null {
|
|
293
|
+
// Add input validation
|
|
294
|
+
if (!profileJson || typeof profileJson !== 'string') {
|
|
295
|
+
console.error('Invalid profile JSON input:', profileJson);
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (profileJson.trim().length === 0) {
|
|
300
|
+
console.error('Empty profile JSON string');
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
try {
|
|
305
|
+
const parsed = JSON.parse(profileJson);
|
|
306
|
+
|
|
307
|
+
// Handle both formats: direct array or wrapped in toolbars property
|
|
308
|
+
let sitecoreProfile: SitecoreProfile;
|
|
309
|
+
let optionFolders: SitecoreOptionFolder[] = [];
|
|
310
|
+
|
|
311
|
+
if (Array.isArray(parsed)) {
|
|
312
|
+
// Old format: direct array
|
|
313
|
+
sitecoreProfile = parsed;
|
|
314
|
+
} else if (parsed.toolbars && Array.isArray(parsed.toolbars)) {
|
|
315
|
+
// New format: wrapped in toolbars property
|
|
316
|
+
sitecoreProfile = parsed.toolbars;
|
|
317
|
+
optionFolders = parsed.options || [];
|
|
318
|
+
} else {
|
|
319
|
+
console.error('Invalid Sitecore profile format:', parsed);
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Validate the structure
|
|
324
|
+
if (!sitecoreProfile.every(toolbar => toolbar.buttons && Array.isArray(toolbar.buttons))) {
|
|
325
|
+
console.error('Invalid toolbar structure in Sitecore profile');
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return mapSitecoreProfileToSlate(sitecoreProfile, optionFolders);
|
|
330
|
+
} catch (error) {
|
|
331
|
+
console.error('Failed to parse Sitecore profile JSON:', error);
|
|
332
|
+
console.error('Raw profile JSON length:', profileJson.length);
|
|
333
|
+
console.error('Raw profile JSON preview:', profileJson.substring(0, 200) + (profileJson.length > 200 ? '...' : ''));
|
|
334
|
+
console.error('Raw profile JSON ending:', profileJson.length > 200 ? '...' + profileJson.substring(profileJson.length - 200) : profileJson);
|
|
335
|
+
|
|
336
|
+
// Try to determine if it's a truncation issue
|
|
337
|
+
if (error instanceof SyntaxError && error.message.includes('Unexpected end of JSON input')) {
|
|
338
|
+
console.error('This appears to be a truncated JSON string. Check the caching implementation.');
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Creates a debug profile with ALL available options for testing
|
|
347
|
+
* - All text formatting marks (including custom ones)
|
|
348
|
+
* - All block formats
|
|
349
|
+
* - All list options
|
|
350
|
+
* - All alignment options
|
|
351
|
+
* - All link options
|
|
352
|
+
* - Visual dividers for grouping
|
|
353
|
+
*/
|
|
354
|
+
export function getDebugProfile(): RichTextEditorProfile {
|
|
355
|
+
return {
|
|
356
|
+
toolbar: {
|
|
357
|
+
groups: [
|
|
358
|
+
{
|
|
359
|
+
id: 'all-marks',
|
|
360
|
+
label: '',
|
|
361
|
+
display: 'buttons',
|
|
362
|
+
showIconsOnly: false,
|
|
363
|
+
options: [
|
|
364
|
+
{ type: 'mark', id: 'bold' },
|
|
365
|
+
{ type: 'mark', id: 'italic' },
|
|
366
|
+
{ type: 'mark', id: 'underline' },
|
|
367
|
+
{ type: 'mark', id: 'strikethrough' },
|
|
368
|
+
{ type: 'mark', id: 'subscript' },
|
|
369
|
+
{ type: 'mark', id: 'superscript' },
|
|
370
|
+
{ type: 'divider', id: 'divider' },
|
|
371
|
+
{ type: 'mark', id: 'extrabold' }
|
|
372
|
+
]
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
id: 'all-blocks',
|
|
376
|
+
label: '',
|
|
377
|
+
display: 'dropdown',
|
|
378
|
+
showIconsOnly: false,
|
|
379
|
+
options: [
|
|
380
|
+
{ type: 'block', id: 'no-tag' },
|
|
381
|
+
{ type: 'block', id: 'paragraph' },
|
|
382
|
+
{ type: 'block', id: 'heading-1' },
|
|
383
|
+
{ type: 'block', id: 'heading-2' },
|
|
384
|
+
{ type: 'block', id: 'heading-3' },
|
|
385
|
+
{ type: 'block', id: 'heading-4' },
|
|
386
|
+
{ type: 'block', id: 'heading-5' },
|
|
387
|
+
{ type: 'block', id: 'heading-6' }
|
|
388
|
+
]
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
id: 'all-lists',
|
|
392
|
+
label: '',
|
|
393
|
+
display: 'buttons',
|
|
394
|
+
showIconsOnly: false,
|
|
395
|
+
options: [
|
|
396
|
+
{ type: 'list', id: 'unordered-list' },
|
|
397
|
+
{ type: 'list', id: 'ordered-list' }
|
|
398
|
+
]
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
id: 'all-alignment',
|
|
402
|
+
label: '',
|
|
403
|
+
display: 'dropdown',
|
|
404
|
+
showIconsOnly: false,
|
|
405
|
+
options: [
|
|
406
|
+
{ type: 'alignment', id: 'left' },
|
|
407
|
+
{ type: 'alignment', id: 'center' },
|
|
408
|
+
{ type: 'alignment', id: 'right' },
|
|
409
|
+
{ type: 'alignment', id: 'justify' }
|
|
410
|
+
]
|
|
411
|
+
},
|
|
412
|
+
{
|
|
413
|
+
id: 'all-links',
|
|
414
|
+
label: '',
|
|
415
|
+
display: 'buttons',
|
|
416
|
+
showIconsOnly: false,
|
|
417
|
+
options: [
|
|
418
|
+
{ type: 'link', id: 'link' }
|
|
419
|
+
]
|
|
420
|
+
}
|
|
421
|
+
]
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { RichTextEditorProfile } from '../types';
|
|
2
|
+
import { mapSitecoreProfileJsonToSlate } from './profileMapper';
|
|
3
|
+
|
|
4
|
+
// Single cache for parsed profiles
|
|
5
|
+
const parsedProfileCache = new Map<string, RichTextEditorProfile | null>();
|
|
6
|
+
|
|
7
|
+
// Cache for in-flight requests to prevent duplicate calls
|
|
8
|
+
const inflightRequests = new Map<string, Promise<RichTextEditorProfile | null>>();
|
|
9
|
+
|
|
10
|
+
// Cache statistics
|
|
11
|
+
interface ServiceCacheStats {
|
|
12
|
+
hits: number;
|
|
13
|
+
misses: number;
|
|
14
|
+
cacheSize: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let serviceStats: ServiceCacheStats = {
|
|
18
|
+
hits: 0,
|
|
19
|
+
misses: 0,
|
|
20
|
+
cacheSize: 0
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Get a parsed profile with service-level caching
|
|
25
|
+
* This combines service calls and parsing into a single cached operation
|
|
26
|
+
*/
|
|
27
|
+
export async function getCachedParsedProfile(
|
|
28
|
+
profilePath: string,
|
|
29
|
+
serviceCall: (path: string) => Promise<any>
|
|
30
|
+
): Promise<RichTextEditorProfile | null> {
|
|
31
|
+
console.log(`[ProfileServiceCache] Getting parsed profile for path: ${profilePath}`);
|
|
32
|
+
|
|
33
|
+
// Check if we already have a cached result
|
|
34
|
+
if (parsedProfileCache.has(profilePath)) {
|
|
35
|
+
serviceStats.hits++;
|
|
36
|
+
const cachedResult = parsedProfileCache.get(profilePath)!;
|
|
37
|
+
console.log(`[ProfileServiceCache] Cache hit for ${profilePath}`);
|
|
38
|
+
return cachedResult;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Check if there's already an in-flight request for this profile
|
|
42
|
+
if (inflightRequests.has(profilePath)) {
|
|
43
|
+
serviceStats.hits++;
|
|
44
|
+
console.log(`[ProfileServiceCache] In-flight request found for ${profilePath}`);
|
|
45
|
+
return inflightRequests.get(profilePath)!;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Make the service call, parse, and cache the result
|
|
49
|
+
serviceStats.misses++;
|
|
50
|
+
console.log(`[ProfileServiceCache] Making service call for ${profilePath}`);
|
|
51
|
+
|
|
52
|
+
const servicePromise = serviceCall(profilePath)
|
|
53
|
+
.then(response => {
|
|
54
|
+
console.log(`[ProfileServiceCache] Service call completed for ${profilePath}`, response);
|
|
55
|
+
|
|
56
|
+
let parsedProfile: RichTextEditorProfile | null = null;
|
|
57
|
+
|
|
58
|
+
if (response?.data) {
|
|
59
|
+
const jsonString = JSON.stringify(response.data);
|
|
60
|
+
console.log(`[ProfileServiceCache] Parsing profile for ${profilePath}, JSON length: ${jsonString.length}`);
|
|
61
|
+
|
|
62
|
+
parsedProfile = mapSitecoreProfileJsonToSlate(jsonString);
|
|
63
|
+
|
|
64
|
+
if (parsedProfile) {
|
|
65
|
+
console.log(`[ProfileServiceCache] Successfully parsed profile for ${profilePath}`);
|
|
66
|
+
} else {
|
|
67
|
+
console.warn(`[ProfileServiceCache] Failed to parse profile for ${profilePath}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Cache the parsed result (even if null)
|
|
72
|
+
parsedProfileCache.set(profilePath, parsedProfile);
|
|
73
|
+
serviceStats.cacheSize = parsedProfileCache.size;
|
|
74
|
+
|
|
75
|
+
// Remove from in-flight requests
|
|
76
|
+
inflightRequests.delete(profilePath);
|
|
77
|
+
|
|
78
|
+
console.log(`[ProfileServiceCache] Cached parsed profile for ${profilePath}, cache size: ${serviceStats.cacheSize}`);
|
|
79
|
+
return parsedProfile;
|
|
80
|
+
})
|
|
81
|
+
.catch(error => {
|
|
82
|
+
console.error(`[ProfileServiceCache] Service call failed for ${profilePath}:`, error);
|
|
83
|
+
// Remove from in-flight requests on error
|
|
84
|
+
inflightRequests.delete(profilePath);
|
|
85
|
+
throw error;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Cache the promise to prevent duplicate requests
|
|
89
|
+
inflightRequests.set(profilePath, servicePromise);
|
|
90
|
+
|
|
91
|
+
return servicePromise;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Legacy function for backward compatibility
|
|
96
|
+
* @deprecated Use getCachedParsedProfile instead
|
|
97
|
+
*/
|
|
98
|
+
export async function getCachedRichTextProfile(
|
|
99
|
+
profilePath: string,
|
|
100
|
+
serviceCall: (path: string) => Promise<any>
|
|
101
|
+
): Promise<string | null> {
|
|
102
|
+
const parsedProfile = await getCachedParsedProfile(profilePath, serviceCall);
|
|
103
|
+
return parsedProfile ? JSON.stringify(parsedProfile) : null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Clear all caches
|
|
108
|
+
*/
|
|
109
|
+
export function clearServiceCache(): void {
|
|
110
|
+
console.log('[ProfileServiceCache] Clearing all caches');
|
|
111
|
+
parsedProfileCache.clear();
|
|
112
|
+
inflightRequests.clear();
|
|
113
|
+
serviceStats = {
|
|
114
|
+
hits: 0,
|
|
115
|
+
misses: 0,
|
|
116
|
+
cacheSize: 0
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Remove a specific profile from cache
|
|
122
|
+
*/
|
|
123
|
+
export function evictProfileFromServiceCache(profilePath: string): boolean {
|
|
124
|
+
console.log(`[ProfileServiceCache] Evicting profile: ${profilePath}`);
|
|
125
|
+
const evicted = parsedProfileCache.delete(profilePath);
|
|
126
|
+
inflightRequests.delete(profilePath);
|
|
127
|
+
|
|
128
|
+
if (evicted) {
|
|
129
|
+
serviceStats.cacheSize = parsedProfileCache.size;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return evicted;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Get service cache statistics
|
|
137
|
+
*/
|
|
138
|
+
export function getServiceCacheStats(): Readonly<ServiceCacheStats> {
|
|
139
|
+
return { ...serviceStats };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Get all cached profile paths
|
|
144
|
+
*/
|
|
145
|
+
export function getCachedProfilePaths(): string[] {
|
|
146
|
+
return Array.from(parsedProfileCache.keys());
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Check if a profile path is cached
|
|
151
|
+
*/
|
|
152
|
+
export function isProfilePathCached(profilePath: string): boolean {
|
|
153
|
+
return parsedProfileCache.has(profilePath);
|
|
154
|
+
}
|
|
@@ -226,3 +226,15 @@ export async function exportItems(request: ExportItemsRequest) {
|
|
|
226
226
|
export async function importItems(request: ImportItemsRequest) {
|
|
227
227
|
return await post<ImportItemsResult>("/alpaca/editor/importItems", request);
|
|
228
228
|
}
|
|
229
|
+
|
|
230
|
+
export type RichTextProfileResponse = {
|
|
231
|
+
toolbars: Array<{
|
|
232
|
+
buttons: Array<{
|
|
233
|
+
name: string;
|
|
234
|
+
}>;
|
|
235
|
+
}>;
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
export async function getRichTextProfile(itemPath: string) {
|
|
239
|
+
return await post<RichTextProfileResponse>(`/alpaca/editor/RichTextProfile`, {profile: itemPath});
|
|
240
|
+
}
|
|
@@ -46,6 +46,14 @@ export function ComponentTree({}) {
|
|
|
46
46
|
|
|
47
47
|
const treeRef = useRef<HTMLDivElement>(null);
|
|
48
48
|
|
|
49
|
+
// Helper function to clean placeholder labels by removing _{guid} pattern
|
|
50
|
+
function cleanPlaceholderLabel(label: string): string {
|
|
51
|
+
return label.replace(
|
|
52
|
+
/_{[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}}$/,
|
|
53
|
+
"",
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
49
57
|
// Helper function to check if a component has editable descendants
|
|
50
58
|
// function hasEditableDescendants(component: Component): boolean {
|
|
51
59
|
// if (!component.placeholders) return false;
|
|
@@ -148,10 +156,13 @@ export function ComponentTree({}) {
|
|
|
148
156
|
p: Placeholder,
|
|
149
157
|
parent: CustomTreeNode,
|
|
150
158
|
): CustomTreeNode {
|
|
159
|
+
const rawLabel = p.description || p.name;
|
|
160
|
+
const cleanedLabel = cleanPlaceholderLabel(rawLabel);
|
|
161
|
+
|
|
151
162
|
const node: CustomTreeNode = {
|
|
152
163
|
key: p.key,
|
|
153
164
|
componentId: p.key,
|
|
154
|
-
label:
|
|
165
|
+
label: cleanedLabel,
|
|
155
166
|
icon: "pi pi-folder",
|
|
156
167
|
data: p,
|
|
157
168
|
parent: parent,
|
|
@@ -81,6 +81,8 @@ export interface TreeProps<T = any> {
|
|
|
81
81
|
enableKeyboardSearch?: boolean;
|
|
82
82
|
/** Time in ms before search is cleared (default: 1500) */
|
|
83
83
|
searchClearDelay?: number;
|
|
84
|
+
/** Whether to disable automatic selection when expanding nodes (even during search) */
|
|
85
|
+
disableAutoSelectOnExpand?: boolean;
|
|
84
86
|
}
|
|
85
87
|
|
|
86
88
|
/**
|
|
@@ -580,6 +582,7 @@ export const PerfectTree = <T,>({
|
|
|
580
582
|
scrollToSelected = false,
|
|
581
583
|
enableKeyboardSearch = false,
|
|
582
584
|
searchClearDelay = 1500,
|
|
585
|
+
disableAutoSelectOnExpand = false,
|
|
583
586
|
}: TreeProps<T>) => {
|
|
584
587
|
const [searchTerm, setSearchTerm] = useState("");
|
|
585
588
|
const [isFocused, setIsFocused] = useState(false);
|
|
@@ -590,18 +593,21 @@ export const PerfectTree = <T,>({
|
|
|
590
593
|
(node: TreeNode<T>) => {
|
|
591
594
|
const isCurrentlyExpanded = expandedKeys.includes(node.key);
|
|
592
595
|
|
|
593
|
-
// If the node is being expanded (not collapsed)
|
|
596
|
+
// If the node is being expanded (not collapsed) and there's an active search filter
|
|
594
597
|
if (!isCurrentlyExpanded) {
|
|
598
|
+
// Only select the node for quick navigation if there's an active search filter
|
|
599
|
+
// and auto-selection is not disabled
|
|
600
|
+
if (searchTerm && onSelect && !disableAutoSelectOnExpand) {
|
|
601
|
+
onSelect(node.key, {} as React.MouseEvent);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Clear the search filter
|
|
595
605
|
setSearchTerm("");
|
|
596
606
|
// Clear any pending search timeout
|
|
597
607
|
if (searchTimeoutRef.current) {
|
|
598
608
|
clearTimeout(searchTimeoutRef.current);
|
|
599
609
|
searchTimeoutRef.current = null;
|
|
600
610
|
}
|
|
601
|
-
// Select the node being expanded for quick navigation
|
|
602
|
-
if (onSelect) {
|
|
603
|
-
onSelect(node.key, {} as React.MouseEvent);
|
|
604
|
-
}
|
|
605
611
|
}
|
|
606
612
|
|
|
607
613
|
if (onToggleExpand) {
|
|
@@ -613,7 +619,14 @@ export const PerfectTree = <T,>({
|
|
|
613
619
|
onLazyLoad(node);
|
|
614
620
|
}
|
|
615
621
|
},
|
|
616
|
-
[
|
|
622
|
+
[
|
|
623
|
+
onToggleExpand,
|
|
624
|
+
onLazyLoad,
|
|
625
|
+
expandedKeys,
|
|
626
|
+
onSelect,
|
|
627
|
+
searchTerm,
|
|
628
|
+
disableAutoSelectOnExpand,
|
|
629
|
+
],
|
|
617
630
|
);
|
|
618
631
|
|
|
619
632
|
const handleSelect = useCallback(
|