@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.
Files changed (103) hide show
  1. package/dist/config/config.js +0 -12
  2. package/dist/config/config.js.map +1 -1
  3. package/dist/config/types.d.ts +6 -3
  4. package/dist/editor/ContentTree.d.ts +2 -1
  5. package/dist/editor/ContentTree.js +2 -2
  6. package/dist/editor/ContentTree.js.map +1 -1
  7. package/dist/editor/ItemInfo.js +1 -1
  8. package/dist/editor/ItemInfo.js.map +1 -1
  9. package/dist/editor/field-types/InternalLinkFieldEditor.js +20 -25
  10. package/dist/editor/field-types/InternalLinkFieldEditor.js.map +1 -1
  11. package/dist/editor/field-types/RichTextEditor.d.ts +2 -2
  12. package/dist/editor/field-types/RichTextEditor.js +15 -2
  13. package/dist/editor/field-types/RichTextEditor.js.map +1 -1
  14. package/dist/editor/field-types/RichTextEditorComponent.d.ts +5 -5
  15. package/dist/editor/field-types/RichTextEditorComponent.js +40 -51
  16. package/dist/editor/field-types/RichTextEditorComponent.js.map +1 -1
  17. package/dist/editor/field-types/TreeListEditor.js +7 -5
  18. package/dist/editor/field-types/TreeListEditor.js.map +1 -1
  19. package/dist/editor/field-types/richtext/components/EditorDropdown.d.ts +11 -0
  20. package/dist/editor/field-types/richtext/components/EditorDropdown.js +96 -0
  21. package/dist/editor/field-types/richtext/components/EditorDropdown.js.map +1 -0
  22. package/dist/editor/field-types/richtext/components/ReactSlate.d.ts +5 -0
  23. package/dist/editor/field-types/richtext/components/ReactSlate.js +558 -0
  24. package/dist/editor/field-types/richtext/components/ReactSlate.js.map +1 -0
  25. package/dist/editor/field-types/richtext/components/ToolbarButton.d.ts +3 -0
  26. package/dist/editor/field-types/richtext/components/ToolbarButton.js +13 -0
  27. package/dist/editor/field-types/richtext/components/ToolbarButton.js.map +1 -0
  28. package/dist/editor/field-types/richtext/config/pluginFactory.d.ts +17 -0
  29. package/dist/editor/field-types/richtext/config/pluginFactory.js +14 -0
  30. package/dist/editor/field-types/richtext/config/pluginFactory.js.map +1 -0
  31. package/dist/editor/field-types/richtext/hooks/useProfileCache.d.ts +68 -0
  32. package/dist/editor/field-types/richtext/hooks/useProfileCache.js +208 -0
  33. package/dist/editor/field-types/richtext/hooks/useProfileCache.js.map +1 -0
  34. package/dist/editor/field-types/richtext/hooks/useRichTextProfile.d.ts +25 -0
  35. package/dist/editor/field-types/richtext/hooks/useRichTextProfile.js +64 -0
  36. package/dist/editor/field-types/richtext/hooks/useRichTextProfile.js.map +1 -0
  37. package/dist/editor/field-types/richtext/index.d.ts +5 -0
  38. package/dist/editor/field-types/richtext/index.js +6 -0
  39. package/dist/editor/field-types/richtext/index.js.map +1 -0
  40. package/dist/editor/field-types/richtext/types.d.ts +139 -0
  41. package/dist/editor/field-types/richtext/types.js +107 -0
  42. package/dist/editor/field-types/richtext/types.js.map +1 -0
  43. package/dist/editor/field-types/richtext/utils/conversion.d.ts +5 -0
  44. package/dist/editor/field-types/richtext/utils/conversion.js +539 -0
  45. package/dist/editor/field-types/richtext/utils/conversion.js.map +1 -0
  46. package/dist/editor/field-types/richtext/utils/plugins.d.ts +97 -0
  47. package/dist/editor/field-types/richtext/utils/plugins.js +272 -0
  48. package/dist/editor/field-types/richtext/utils/plugins.js.map +1 -0
  49. package/dist/editor/field-types/richtext/utils/profileMapper.d.ts +38 -0
  50. package/dist/editor/field-types/richtext/utils/profileMapper.js +366 -0
  51. package/dist/editor/field-types/richtext/utils/profileMapper.js.map +1 -0
  52. package/dist/editor/field-types/richtext/utils/profileServiceCache.d.ts +37 -0
  53. package/dist/editor/field-types/richtext/utils/profileServiceCache.js +117 -0
  54. package/dist/editor/field-types/richtext/utils/profileServiceCache.js.map +1 -0
  55. package/dist/editor/services/contentService.d.ts +8 -0
  56. package/dist/editor/services/contentService.js +3 -0
  57. package/dist/editor/services/contentService.js.map +1 -1
  58. package/dist/editor/sidebar/ComponentTree.js +7 -1
  59. package/dist/editor/sidebar/ComponentTree.js.map +1 -1
  60. package/dist/editor/ui/PerfectTree.d.ts +4 -2
  61. package/dist/editor/ui/PerfectTree.js +16 -7
  62. package/dist/editor/ui/PerfectTree.js.map +1 -1
  63. package/dist/editor/utils/itemutils.js +3 -1
  64. package/dist/editor/utils/itemutils.js.map +1 -1
  65. package/dist/editor/views/ItemEditor.js +10 -3
  66. package/dist/editor/views/ItemEditor.js.map +1 -1
  67. package/dist/page-wizard/steps/ContentStep.js +1 -3
  68. package/dist/page-wizard/steps/ContentStep.js.map +1 -1
  69. package/dist/page-wizard/steps/usePageCreator.js +1 -1
  70. package/dist/page-wizard/steps/usePageCreator.js.map +1 -1
  71. package/dist/revision.d.ts +2 -2
  72. package/dist/revision.js +2 -2
  73. package/dist/styles.css +9 -0
  74. package/package.json +4 -1
  75. package/src/config/config.tsx +0 -12
  76. package/src/config/types.ts +6 -3
  77. package/src/editor/ContentTree.tsx +3 -0
  78. package/src/editor/ItemInfo.tsx +2 -2
  79. package/src/editor/field-types/InternalLinkFieldEditor.tsx +73 -69
  80. package/src/editor/field-types/RichTextEditor.tsx +31 -3
  81. package/src/editor/field-types/RichTextEditorComponent.tsx +52 -69
  82. package/src/editor/field-types/TreeListEditor.tsx +7 -7
  83. package/src/editor/field-types/richtext/components/EditorDropdown.tsx +180 -0
  84. package/src/editor/field-types/richtext/components/ReactSlate.css +163 -0
  85. package/src/editor/field-types/richtext/components/ReactSlate.tsx +792 -0
  86. package/src/editor/field-types/richtext/components/ToolbarButton.tsx +23 -0
  87. package/src/editor/field-types/richtext/config/pluginFactory.tsx +22 -0
  88. package/src/editor/field-types/richtext/hooks/useProfileCache.ts +270 -0
  89. package/src/editor/field-types/richtext/hooks/useRichTextProfile.ts +94 -0
  90. package/src/editor/field-types/richtext/index.ts +5 -0
  91. package/src/editor/field-types/richtext/types.ts +269 -0
  92. package/src/editor/field-types/richtext/utils/conversion.ts +589 -0
  93. package/src/editor/field-types/richtext/utils/plugins.ts +346 -0
  94. package/src/editor/field-types/richtext/utils/profileMapper.ts +424 -0
  95. package/src/editor/field-types/richtext/utils/profileServiceCache.ts +154 -0
  96. package/src/editor/services/contentService.ts +12 -0
  97. package/src/editor/sidebar/ComponentTree.tsx +12 -1
  98. package/src/editor/ui/PerfectTree.tsx +19 -6
  99. package/src/editor/utils/{itemutils.ts → itemutils.tsx} +12 -12
  100. package/src/editor/views/ItemEditor.tsx +22 -1
  101. package/src/page-wizard/steps/ContentStep.tsx +1 -3
  102. package/src/page-wizard/steps/usePageCreator.ts +1 -0
  103. 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: p.description || p.name,
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), clear the search filter and select the node
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
- [onToggleExpand, onLazyLoad, expandedKeys, onSelect],
622
+ [
623
+ onToggleExpand,
624
+ onLazyLoad,
625
+ expandedKeys,
626
+ onSelect,
627
+ searchTerm,
628
+ disableAutoSelectOnExpand,
629
+ ],
617
630
  );
618
631
 
619
632
  const handleSelect = useCallback(