@alpaca-editor/core 1.0.4083 → 1.0.4085

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 (85) hide show
  1. package/dist/components/ui/card.js +1 -1
  2. package/dist/components/ui/card.js.map +1 -1
  3. package/dist/editor/FieldListField.d.ts +3 -2
  4. package/dist/editor/FieldListField.js +8 -8
  5. package/dist/editor/FieldListField.js.map +1 -1
  6. package/dist/editor/FieldListFieldWithFallbacks.js +2 -2
  7. package/dist/editor/FieldListFieldWithFallbacks.js.map +1 -1
  8. package/dist/editor/LinkEditorDialog.d.ts +1 -0
  9. package/dist/editor/LinkEditorDialog.js +4 -2
  10. package/dist/editor/LinkEditorDialog.js.map +1 -1
  11. package/dist/editor/client/EditorShell.js +11 -3
  12. package/dist/editor/client/EditorShell.js.map +1 -1
  13. package/dist/editor/client/editContext.d.ts +1 -0
  14. package/dist/editor/client/editContext.js.map +1 -1
  15. package/dist/editor/client/hooks/useWorkbox.js +6 -7
  16. package/dist/editor/client/hooks/useWorkbox.js.map +1 -1
  17. package/dist/editor/client/ui/EditorChrome.js +2 -1
  18. package/dist/editor/client/ui/EditorChrome.js.map +1 -1
  19. package/dist/editor/component-designer/ComponentDesigner.d.ts +1 -1
  20. package/dist/editor/component-designer/ComponentDesigner.js +48 -53
  21. package/dist/editor/component-designer/ComponentDesigner.js.map +1 -1
  22. package/dist/editor/control-center/About.js +1 -1
  23. package/dist/editor/control-center/About.js.map +1 -1
  24. package/dist/editor/field-types/RichTextEditor.d.ts +2 -1
  25. package/dist/editor/field-types/RichTextEditor.js +4 -4
  26. package/dist/editor/field-types/RichTextEditor.js.map +1 -1
  27. package/dist/editor/field-types/RichTextEditorComponent.d.ts +2 -1
  28. package/dist/editor/field-types/RichTextEditorComponent.js +2 -2
  29. package/dist/editor/field-types/RichTextEditorComponent.js.map +1 -1
  30. package/dist/editor/field-types/richtext/components/ReactSlate.js +36 -25
  31. package/dist/editor/field-types/richtext/components/ReactSlate.js.map +1 -1
  32. package/dist/editor/field-types/richtext/components/ToolbarButton.d.ts +2 -2
  33. package/dist/editor/field-types/richtext/components/ToolbarButton.js +3 -10
  34. package/dist/editor/field-types/richtext/components/ToolbarButton.js.map +1 -1
  35. package/dist/editor/field-types/richtext/types.d.ts +1 -0
  36. package/dist/editor/field-types/richtext/types.js.map +1 -1
  37. package/dist/editor/field-types/richtext/utils/conversion.js +180 -21
  38. package/dist/editor/field-types/richtext/utils/conversion.js.map +1 -1
  39. package/dist/editor/field-types/richtext/utils/plugins.d.ts +1 -0
  40. package/dist/editor/field-types/richtext/utils/plugins.js +19 -4
  41. package/dist/editor/field-types/richtext/utils/plugins.js.map +1 -1
  42. package/dist/editor/page-editor-chrome/FieldActionIndicator.js +11 -2
  43. package/dist/editor/page-editor-chrome/FieldActionIndicator.js.map +1 -1
  44. package/dist/editor/page-editor-chrome/FieldActionIndicators.js +1 -1
  45. package/dist/editor/page-editor-chrome/FieldActionIndicators.js.map +1 -1
  46. package/dist/editor/page-viewer/EditorForm.js +1 -1
  47. package/dist/editor/page-viewer/PageViewerFrame.js +2 -1
  48. package/dist/editor/page-viewer/PageViewerFrame.js.map +1 -1
  49. package/dist/editor/utils/urlUtils.d.ts +9 -0
  50. package/dist/editor/utils/urlUtils.js +25 -0
  51. package/dist/editor/utils/urlUtils.js.map +1 -0
  52. package/dist/page-wizard/steps/SelectStep.js +28 -12
  53. package/dist/page-wizard/steps/SelectStep.js.map +1 -1
  54. package/dist/revision.d.ts +2 -2
  55. package/dist/revision.js +2 -2
  56. package/dist/styles.css +24 -0
  57. package/package.json +1 -2
  58. package/src/components/ui/card.tsx +1 -1
  59. package/src/editor/FieldListField.tsx +11 -4
  60. package/src/editor/FieldListFieldWithFallbacks.tsx +3 -1
  61. package/src/editor/LinkEditorDialog.tsx +6 -2
  62. package/src/editor/client/EditorShell.tsx +20 -4
  63. package/src/editor/client/editContext.ts +1 -0
  64. package/src/editor/client/hooks/useWorkbox.ts +6 -7
  65. package/src/editor/client/ui/EditorChrome.tsx +4 -1
  66. package/src/editor/component-designer/ComponentDesigner.tsx +49 -53
  67. package/src/editor/control-center/About.tsx +0 -15
  68. package/src/editor/field-types/RichTextEditor.tsx +13 -10
  69. package/src/editor/field-types/RichTextEditorComponent.tsx +3 -0
  70. package/src/editor/field-types/richtext/components/ReactSlate.tsx +503 -437
  71. package/src/editor/field-types/richtext/components/ToolbarButton.tsx +12 -14
  72. package/src/editor/field-types/richtext/types.ts +18 -10
  73. package/src/editor/field-types/richtext/utils/conversion.ts +204 -23
  74. package/src/editor/field-types/richtext/utils/plugins.ts +24 -4
  75. package/src/editor/page-editor-chrome/FieldActionIndicator.tsx +63 -7
  76. package/src/editor/page-editor-chrome/FieldActionIndicators.tsx +1 -1
  77. package/src/editor/page-viewer/EditorForm.tsx +2 -2
  78. package/src/editor/page-viewer/PageViewerFrame.tsx +2 -1
  79. package/src/editor/utils/urlUtils.ts +24 -0
  80. package/src/page-wizard/steps/SelectStep.tsx +47 -18
  81. package/src/revision.ts +2 -2
  82. package/dist/editor/ui/StackedPanels.d.ts +0 -5
  83. package/dist/editor/ui/StackedPanels.js +0 -67
  84. package/dist/editor/ui/StackedPanels.js.map +0 -1
  85. package/src/editor/ui/StackedPanels.tsx +0 -134
@@ -8,7 +8,13 @@ import React, {
8
8
  memo,
9
9
  } from "react";
10
10
  import { createEditor, Descendant, Editor, Element, Transforms } from "slate";
11
- import { Slate, Editable, withReact, ReactEditor, useSlateSelector } from "slate-react";
11
+ import {
12
+ Slate,
13
+ Editable,
14
+ withReact,
15
+ ReactEditor,
16
+ useSlateSelector,
17
+ } from "slate-react";
12
18
  import { withHistory } from "slate-history";
13
19
  import "./ReactSlate.css";
14
20
 
@@ -34,7 +40,9 @@ import { LinkEditorDialog } from "../../../LinkEditorDialog";
34
40
 
35
41
  import { htmlToSlate, slateToHtml } from "../utils/conversion";
36
42
  import { createPluginsFromConfig } from "../config/pluginFactory";
37
- import { createKeyboardHandler } from "../utils/plugins";
43
+ import { createKeyboardHandler, generateInternalLinkUrl } from "../utils/plugins";
44
+ import { normalizeUrl } from "../../../utils/urlUtils";
45
+
38
46
  import { useCachedSimplifiedProfile } from "../hooks/useProfileCache";
39
47
  import { classNames } from "primereact/utils";
40
48
 
@@ -56,7 +64,8 @@ const ToolbarButtonWrapper: React.FC<{
56
64
  case "link":
57
65
  return editor.isLinkActive();
58
66
  case "list":
59
- const listType = option.id === "unordered-list" ? "unordered" : "ordered";
67
+ const listType =
68
+ option.id === "unordered-list" ? "unordered" : "ordered";
60
69
  return editor.isListActive(listType);
61
70
  case "insertion":
62
71
  return false; // Insertion buttons are never active
@@ -66,11 +75,7 @@ const ToolbarButtonWrapper: React.FC<{
66
75
  });
67
76
 
68
77
  return (
69
- <ToolbarButton
70
- icon={icon}
71
- active={isActive}
72
- onMouseDown={onMouseDown}
73
- />
78
+ <ToolbarButton icon={icon} active={isActive} onMouseDown={onMouseDown} />
74
79
  );
75
80
  });
76
81
 
@@ -104,6 +109,7 @@ export const ReactSlate = forwardRef<any, ReactSlateProps>((props, ref) => {
104
109
  readOnly = false,
105
110
  placeholder = "Enter some text...",
106
111
  profile,
112
+ showControls,
107
113
  } = props;
108
114
 
109
115
  const editorProfile = profile || {
@@ -118,12 +124,12 @@ export const ReactSlate = forwardRef<any, ReactSlateProps>((props, ref) => {
118
124
  // Create the Slate editor with plugins - only once
119
125
  const editor = useMemo(() => {
120
126
  const baseEditor = createEditor();
121
-
127
+
122
128
  // Apply plugins in correct order
123
129
  const enhancedEditor = createPluginsFromConfig(
124
- withHistory(withReact(baseEditor))
130
+ withHistory(withReact(baseEditor)),
125
131
  );
126
-
132
+
127
133
  return enhancedEditor;
128
134
  }, []);
129
135
 
@@ -164,21 +170,24 @@ export const ReactSlate = forwardRef<any, ReactSlateProps>((props, ref) => {
164
170
  // Only update if value actually changed from outside
165
171
  if (value !== previousValueRef.current) {
166
172
  previousValueRef.current = value;
167
-
173
+
168
174
  // Update editor children with new value
169
175
  const newChildren = slateValue;
170
-
176
+
171
177
  // Prevent infinite loops by temporarily removing selection
172
178
  const currentSelection = editor.selection;
173
-
179
+
174
180
  // Update the editor's children
175
181
  editor.children = newChildren;
176
-
182
+
177
183
  // Normalize the editor to ensure consistency
178
184
  Editor.normalize(editor, { force: true });
179
-
185
+
180
186
  // Restore selection if it was valid, otherwise set to end
181
- if (currentSelection && Editor.hasPath(editor, currentSelection.anchor.path)) {
187
+ if (
188
+ currentSelection &&
189
+ Editor.hasPath(editor, currentSelection.anchor.path)
190
+ ) {
182
191
  try {
183
192
  Transforms.select(editor, currentSelection);
184
193
  } catch {
@@ -198,7 +207,7 @@ export const ReactSlate = forwardRef<any, ReactSlateProps>((props, ref) => {
198
207
  if (onChange) {
199
208
  try {
200
209
  const html = slateToHtml(newValue, simplifiedProfile);
201
-
210
+
202
211
  // Only trigger onChange if content actually changed
203
212
  if (html !== value) {
204
213
  // Mark this as an internal change
@@ -235,19 +244,26 @@ export const ReactSlate = forwardRef<any, ReactSlateProps>((props, ref) => {
235
244
  });
236
245
  }, [editor]);
237
246
 
238
- const handleStripFormattingClick = useCallback((event: React.MouseEvent<HTMLButtonElement>) => {
239
- event.preventDefault();
240
- editor.stripFormatting();
241
- }, [editor]);
247
+ const handleStripFormattingClick = useCallback(
248
+ (event: React.MouseEvent<HTMLButtonElement>) => {
249
+ event.preventDefault();
250
+ editor.stripFormatting();
251
+ },
252
+ [editor],
253
+ );
242
254
 
243
255
  // Memoize option handlers to prevent creating new functions on every render
244
256
  const optionHandlers = useMemo(() => {
245
- const handlers: Record<string, (editor: Editor, event: React.MouseEvent) => void> = {};
246
-
247
- const handleOptionSelect = (option: ToolbarOptionConfig) =>
257
+ const handlers: Record<
258
+ string,
259
+ (editor: Editor, event: React.MouseEvent) => void
260
+ > = {};
261
+
262
+ const handleOptionSelect =
263
+ (option: ToolbarOptionConfig) =>
248
264
  (editor: Editor, event: React.MouseEvent) => {
249
265
  event.preventDefault();
250
-
266
+
251
267
  switch (option.type) {
252
268
  case "mark":
253
269
  editor.toggleMark(option.id);
@@ -263,7 +279,8 @@ export const ReactSlate = forwardRef<any, ReactSlateProps>((props, ref) => {
263
279
  handleLinkButtonClick();
264
280
  break;
265
281
  case "list":
266
- const listType = option.id === "unordered-list" ? "unordered" : "ordered";
282
+ const listType =
283
+ option.id === "unordered-list" ? "unordered" : "ordered";
267
284
  editor.toggleList(listType);
268
285
  break;
269
286
  case "insertion":
@@ -277,8 +294,8 @@ export const ReactSlate = forwardRef<any, ReactSlateProps>((props, ref) => {
277
294
  };
278
295
 
279
296
  // Pre-create handlers for all possible options
280
- editorProfile.toolbar.groups.forEach(group => {
281
- group.options.forEach(option => {
297
+ editorProfile.toolbar.groups.forEach((group) => {
298
+ group.options.forEach((option) => {
282
299
  const key = `${option.type}-${option.id}`;
283
300
  handlers[key] = handleOptionSelect(option);
284
301
  });
@@ -289,8 +306,11 @@ export const ReactSlate = forwardRef<any, ReactSlateProps>((props, ref) => {
289
306
 
290
307
  // Create button handlers that adapt the signature for toolbar buttons
291
308
  const buttonHandlers = useMemo(() => {
292
- const handlers: Record<string, (event: React.MouseEvent<HTMLButtonElement>) => void> = {};
293
-
309
+ const handlers: Record<
310
+ string,
311
+ (event: React.MouseEvent<HTMLButtonElement>) => void
312
+ > = {};
313
+
294
314
  Object.entries(optionHandlers).forEach(([key, handler]) => {
295
315
  handlers[key] = (event: React.MouseEvent<HTMLButtonElement>) => {
296
316
  handler(editor, event);
@@ -300,266 +320,288 @@ export const ReactSlate = forwardRef<any, ReactSlateProps>((props, ref) => {
300
320
  return handlers;
301
321
  }, [optionHandlers, editor]);
302
322
 
303
- const getOption = useCallback((option: ToolbarOptionConfig): CustomOption | undefined => {
304
- const handlerKey = `${option.type}-${option.id}`;
305
-
306
- switch (option.type) {
307
- case "mark":
308
- const markConfig = SLATE_MARKS[option.id];
309
- return {
310
- id: option.id,
311
- label: markConfig.label,
312
- icon: markConfig.icon,
313
- isActive: (editor: Editor) => editor.isMarkActive(option.id),
314
- toggle: optionHandlers[handlerKey],
315
- };
316
- case "block":
317
- const blockConfig = SLATE_BLOCKS[option.id];
318
- return {
319
- id: option.id,
320
- label: blockConfig.label,
321
- icon: blockConfig.icon,
322
- isActive: (editor: Editor) => editor.isBlockActive(option.id),
323
- toggle: optionHandlers[handlerKey],
324
- };
325
- case "alignment":
326
- const alignConfig = SLATE_ALIGNMENTS[option.id];
327
- return {
328
- id: option.id,
329
- label: alignConfig.label,
330
- icon: alignConfig.icon,
331
- isActive: (editor: Editor) => editor.isAlignActive(alignConfig.value),
332
- toggle: optionHandlers[handlerKey],
333
- };
334
- case "link":
335
- return {
336
- id: "link",
337
- label: "Link",
338
- icon: "🔗",
339
- isActive: (editor: Editor) => editor.isLinkActive(),
340
- toggle: optionHandlers[handlerKey],
341
- };
342
- case "list":
343
- return {
344
- id: option.id,
345
- label: option.id === "unordered-list" ? "Bulleted List" : "Numbered List",
346
- icon: option.id === "unordered-list" ? "•" : "1.",
347
- isActive: (editor: Editor) => editor.isListActive(
348
- option.id === "unordered-list" ? "unordered" : "ordered"
349
- ),
350
- toggle: optionHandlers[handlerKey],
351
- };
352
- case "divider":
353
- return { id: "divider", label: "Divider" };
354
- case "insertion":
355
- if (option.id === "horizontal-rule") {
323
+ const getOption = useCallback(
324
+ (option: ToolbarOptionConfig): CustomOption | undefined => {
325
+ const handlerKey = `${option.type}-${option.id}`;
326
+
327
+ switch (option.type) {
328
+ case "mark":
329
+ const markConfig = SLATE_MARKS[option.id];
356
330
  return {
357
331
  id: option.id,
358
- label: "Horizontal Rule",
359
- icon: "─",
360
- isActive: () => false, // Insertion buttons are never "active"
332
+ label: markConfig.label,
333
+ icon: markConfig.icon,
334
+ isActive: (editor: Editor) => editor.isMarkActive(option.id),
361
335
  toggle: optionHandlers[handlerKey],
362
336
  };
363
- }
364
- return undefined;
365
- default:
366
- return undefined;
367
- }
368
- }, [optionHandlers]);
337
+ case "block":
338
+ const blockConfig = SLATE_BLOCKS[option.id];
339
+ return {
340
+ id: option.id,
341
+ label: blockConfig.label,
342
+ icon: blockConfig.icon,
343
+ isActive: (editor: Editor) => editor.isBlockActive(option.id),
344
+ toggle: optionHandlers[handlerKey],
345
+ };
346
+ case "alignment":
347
+ const alignConfig = SLATE_ALIGNMENTS[option.id];
348
+ return {
349
+ id: option.id,
350
+ label: alignConfig.label,
351
+ icon: alignConfig.icon,
352
+ isActive: (editor: Editor) =>
353
+ editor.isAlignActive(alignConfig.value),
354
+ toggle: optionHandlers[handlerKey],
355
+ };
356
+ case "link":
357
+ return {
358
+ id: "link",
359
+ label: "Link",
360
+ icon: "🔗",
361
+ isActive: (editor: Editor) => editor.isLinkActive(),
362
+ toggle: optionHandlers[handlerKey],
363
+ };
364
+ case "list":
365
+ return {
366
+ id: option.id,
367
+ label:
368
+ option.id === "unordered-list"
369
+ ? "Bulleted List"
370
+ : "Numbered List",
371
+ icon: option.id === "unordered-list" ? "•" : "1.",
372
+ isActive: (editor: Editor) =>
373
+ editor.isListActive(
374
+ option.id === "unordered-list" ? "unordered" : "ordered",
375
+ ),
376
+ toggle: optionHandlers[handlerKey],
377
+ };
378
+ case "divider":
379
+ return { id: "divider", label: "Divider" };
380
+ case "insertion":
381
+ if (option.id === "horizontal-rule") {
382
+ return {
383
+ id: option.id,
384
+ label: "Horizontal Rule",
385
+ icon: "─",
386
+ isActive: () => false, // Insertion buttons are never "active"
387
+ toggle: optionHandlers[handlerKey],
388
+ };
389
+ }
390
+ return undefined;
391
+ default:
392
+ return undefined;
393
+ }
394
+ },
395
+ [optionHandlers],
396
+ );
369
397
 
370
398
  // Memoize dropdown options creation with strict type safety
371
- const createDropdownOptions = useCallback((
372
- options: readonly ToolbarOptionConfig[],
373
- ): DropdownOption<any>[] => {
374
- return options
375
- .map((option) => {
376
- const optionObj = getOption(option);
377
- if (!optionObj) return null;
378
-
379
- return {
380
- value: optionObj,
381
- label: optionObj.label || option.id,
382
- icon: optionObj.icon,
383
- style: (optionObj as any).style,
384
- isActive: (editor: Editor) => {
385
- switch (option.type) {
386
- case "mark":
387
- return editor.isMarkActive(option.id);
388
- case "block":
389
- return editor.isBlockActive(option.id);
390
- case "alignment":
391
- return editor.isAlignActive(SLATE_ALIGNMENTS[option.id].value);
392
- case "link":
393
- return editor.isLinkActive();
394
- case "list":
395
- const listType = option.id === "unordered-list" ? "unordered" : "ordered";
396
- return editor.isListActive(listType);
397
- default:
398
- return false;
399
- }
400
- },
401
- onSelect: optionHandlers[`${option.type}-${option.id}`],
402
- };
403
- })
404
- .filter(Boolean) as DropdownOption<any>[];
405
- }, [getOption, optionHandlers]);
399
+ const createDropdownOptions = useCallback(
400
+ (options: readonly ToolbarOptionConfig[]): DropdownOption<any>[] => {
401
+ return options
402
+ .map((option) => {
403
+ const optionObj = getOption(option);
404
+ if (!optionObj) return null;
405
+
406
+ return {
407
+ value: optionObj,
408
+ label: optionObj.label || option.id,
409
+ icon: optionObj.icon,
410
+ style: (optionObj as any).style,
411
+ isActive: (editor: Editor) => {
412
+ switch (option.type) {
413
+ case "mark":
414
+ return editor.isMarkActive(option.id);
415
+ case "block":
416
+ return editor.isBlockActive(option.id);
417
+ case "alignment":
418
+ return editor.isAlignActive(
419
+ SLATE_ALIGNMENTS[option.id].value,
420
+ );
421
+ case "link":
422
+ return editor.isLinkActive();
423
+ case "list":
424
+ const listType =
425
+ option.id === "unordered-list" ? "unordered" : "ordered";
426
+ return editor.isListActive(listType);
427
+ default:
428
+ return false;
429
+ }
430
+ },
431
+ onSelect: optionHandlers[`${option.type}-${option.id}`],
432
+ };
433
+ })
434
+ .filter(Boolean) as DropdownOption<any>[];
435
+ },
436
+ [getOption, optionHandlers],
437
+ );
406
438
 
407
439
  // Helper function to split options by dividers into sub-groups
408
- const splitOptionsByDividers = useCallback((
409
- options: ToolbarOptionConfig[],
410
- ): ToolbarOptionConfig[][] => {
411
- const subGroups: ToolbarOptionConfig[][] = [];
412
- let currentGroup: ToolbarOptionConfig[] = [];
413
-
414
- options.forEach((option) => {
415
- if (option.type === "divider") {
416
- if (currentGroup.length > 0) {
417
- subGroups.push(currentGroup);
418
- currentGroup = [];
440
+ const splitOptionsByDividers = useCallback(
441
+ (options: ToolbarOptionConfig[]): ToolbarOptionConfig[][] => {
442
+ const subGroups: ToolbarOptionConfig[][] = [];
443
+ let currentGroup: ToolbarOptionConfig[] = [];
444
+
445
+ options.forEach((option) => {
446
+ if (option.type === "divider") {
447
+ if (currentGroup.length > 0) {
448
+ subGroups.push(currentGroup);
449
+ currentGroup = [];
450
+ }
451
+ } else {
452
+ currentGroup.push(option);
419
453
  }
420
- } else {
421
- currentGroup.push(option);
422
- }
423
- });
454
+ });
424
455
 
425
- // Add the last group if it has options
426
- if (currentGroup.length > 0) {
427
- subGroups.push(currentGroup);
428
- }
456
+ // Add the last group if it has options
457
+ if (currentGroup.length > 0) {
458
+ subGroups.push(currentGroup);
459
+ }
429
460
 
430
- return subGroups;
431
- }, []);
461
+ return subGroups;
462
+ },
463
+ [],
464
+ );
432
465
 
433
466
  // Memoize toolbar group rendering
434
- const renderToolbarGroup = useCallback((group: ToolbarGroupConfig, index: number) => {
435
- const validOptions = group.options.filter(
436
- (option) =>
437
- getOption(option) || option.type === "divider",
438
- );
467
+ const renderToolbarGroup = useCallback(
468
+ (group: ToolbarGroupConfig, index: number) => {
469
+ const validOptions = group.options.filter(
470
+ (option) => getOption(option) || option.type === "divider",
471
+ );
439
472
 
440
- if (validOptions.length === 0) return null;
441
-
442
- // Skip rendering dropdown groups with only one option
443
- if (group.display === "dropdown" && validOptions.length === 1) return null;
444
-
445
- const groupStyle: React.CSSProperties = {
446
- display: "flex",
447
- alignItems: "center",
448
- gap: "8px",
449
- flexWrap: "wrap",
450
- };
451
-
452
- return (
453
- <div key={`group-${group.id || index}`} style={groupStyle}>
454
- {group.display === "buttons"
455
- ? (() => {
456
- const subGroups = splitOptionsByDividers(validOptions);
457
- return subGroups.map((subGroup, subGroupIndex) => (
458
- <div
459
- key={`subgroup-${subGroupIndex}`}
460
- className="toolbar-button-group"
461
- >
462
- {subGroup.map((option) => {
463
- const optionObj = getOption(option);
464
- const handler = buttonHandlers[`${option.type}-${option.id}`];
465
- if (!optionObj || !handler) return null;
466
-
467
- return (
468
- <ToolbarButtonWrapper
469
- key={`${option.type}-${option.id}`}
470
- option={option}
471
- icon={optionObj.icon}
472
- onMouseDown={handler}
473
- />
474
- );
475
- })}
476
- </div>
477
- ));
478
- })()
479
- : (() => {
480
- const dropdownOptions = createDropdownOptions(validOptions);
481
- const blockOptions = validOptions.filter(
482
- (option) => option.type === "block",
483
- );
484
-
485
- // If there's only one block option, render it as a disabled-style button
486
- if (blockOptions.length === 1) {
487
- const singleOption = dropdownOptions.find((opt) =>
488
- blockOptions.some(
489
- (blockOpt) =>
490
- blockOpt.type === "block" &&
491
- getOption(blockOpt) === opt.value,
492
- ),
473
+ if (validOptions.length === 0) return null;
474
+
475
+ // Skip rendering dropdown groups with only one option
476
+ if (group.display === "dropdown" && validOptions.length === 1)
477
+ return null;
478
+
479
+ const groupStyle: React.CSSProperties = {
480
+ display: "flex",
481
+ alignItems: "center",
482
+ gap: "8px",
483
+ flexWrap: "wrap",
484
+ };
485
+
486
+ return (
487
+ <div key={`group-${group.id || index}`} style={groupStyle}>
488
+ {group.display === "buttons"
489
+ ? (() => {
490
+ const subGroups = splitOptionsByDividers(validOptions);
491
+ return subGroups.map((subGroup, subGroupIndex) => (
492
+ <div
493
+ key={`subgroup-${subGroupIndex}`}
494
+ className="toolbar-button-group"
495
+ >
496
+ {subGroup.map((option) => {
497
+ const optionObj = getOption(option);
498
+ const handler =
499
+ buttonHandlers[`${option.type}-${option.id}`];
500
+ if (!optionObj || !handler) return null;
501
+
502
+ return (
503
+ <ToolbarButtonWrapper
504
+ key={`${option.type}-${option.id}`}
505
+ option={option}
506
+ icon={optionObj.icon}
507
+ onMouseDown={handler}
508
+ />
509
+ );
510
+ })}
511
+ </div>
512
+ ));
513
+ })()
514
+ : (() => {
515
+ const dropdownOptions = createDropdownOptions(validOptions);
516
+ const blockOptions = validOptions.filter(
517
+ (option) => option.type === "block",
493
518
  );
494
519
 
495
- return (
496
- <div className="toolbar-dropdown-container">
497
- <button className="toolbar-dropdown-button" disabled>
498
- {group.label ? (
499
- <>
500
- <span className="toolbar-dropdown-content">
501
- {group.label}: {singleOption?.label}
502
- </span>
503
- <span className="toolbar-dropdown-arrow">▼</span>
504
- </>
505
- ) : group.showIconsOnly ? (
506
- <>
507
- <span className="toolbar-dropdown-icon">
508
- {singleOption?.icon}
509
- </span>
510
- <span className="toolbar-dropdown-arrow">▼</span>
511
- </>
512
- ) : (
513
- <>
514
- <span className="toolbar-dropdown-icon">
515
- {singleOption?.icon && (
516
- <span className="toolbar-dropdown-icon">
517
- {singleOption.icon}
518
- </span>
519
- )}
520
+ // If there's only one block option, render it as a disabled-style button
521
+ if (blockOptions.length === 1) {
522
+ const singleOption = dropdownOptions.find((opt) =>
523
+ blockOptions.some(
524
+ (blockOpt) =>
525
+ blockOpt.type === "block" &&
526
+ getOption(blockOpt) === opt.value,
527
+ ),
528
+ );
529
+
530
+ return (
531
+ <div className="toolbar-dropdown-container">
532
+ <button className="toolbar-dropdown-button" disabled>
533
+ {group.label ? (
534
+ <>
520
535
  <span className="toolbar-dropdown-content">
521
- {singleOption?.label}
536
+ {group.label}: {singleOption?.label}
522
537
  </span>
523
- </span>
524
- <span className="toolbar-dropdown-arrow">▼</span>
525
- </>
526
- )}
527
- </button>
528
- </div>
538
+ <span className="toolbar-dropdown-arrow">▼</span>
539
+ </>
540
+ ) : group.showIconsOnly ? (
541
+ <>
542
+ <span className="toolbar-dropdown-icon">
543
+ {singleOption?.icon}
544
+ </span>
545
+ <span className="toolbar-dropdown-arrow">▼</span>
546
+ </>
547
+ ) : (
548
+ <>
549
+ <span className="toolbar-dropdown-icon">
550
+ {singleOption?.icon && (
551
+ <span className="toolbar-dropdown-icon">
552
+ {singleOption.icon}
553
+ </span>
554
+ )}
555
+ <span className="toolbar-dropdown-content">
556
+ {singleOption?.label}
557
+ </span>
558
+ </span>
559
+ <span className="toolbar-dropdown-arrow">▼</span>
560
+ </>
561
+ )}
562
+ </button>
563
+ </div>
564
+ );
565
+ }
566
+
567
+ // Multiple options - render normally wrapped in grey container
568
+ const isActive = dropdownOptions.some((option) =>
569
+ option.isActive(editor),
529
570
  );
530
- }
531
571
 
532
- // Multiple options - render normally wrapped in grey container
533
- const isActive = dropdownOptions.some((option) =>
534
- option.isActive(editor),
535
- );
536
-
537
- const buttonStyle = {
538
- padding: "5px 10px",
539
- margin: "0",
540
- background: isActive ? "#ffffff" : "transparent",
541
- border: "none",
542
- borderRadius: "3px",
543
- cursor: "pointer",
544
- boxShadow: isActive
545
- ? "0 1px 2px rgba(0,0,0,0.1)"
546
- : "none",
547
- };
548
-
549
- return (
550
- <div className="toolbar-dropdown-container">
551
- <MemoizedEditorDropdown
552
- options={dropdownOptions}
553
- editor={editor}
554
- label={group.label}
555
- buttonStyle={buttonStyle}
556
- />
557
- </div>
558
- );
559
- })()}
560
- </div>
561
- );
562
- }, [getOption, splitOptionsByDividers, createDropdownOptions, optionHandlers, buttonHandlers, editor]);
572
+ const buttonStyle = {
573
+ padding: "5px 10px",
574
+ margin: "0",
575
+ background: isActive ? "#ffffff" : "transparent",
576
+ border: "none",
577
+ borderRadius: "3px",
578
+ cursor: "pointer",
579
+ boxShadow: isActive ? "0 1px 2px rgba(0,0,0,0.1)" : "none",
580
+ };
581
+
582
+ return (
583
+ <div className="toolbar-dropdown-container">
584
+ <MemoizedEditorDropdown
585
+ options={dropdownOptions}
586
+ editor={editor}
587
+ label={group.label}
588
+ buttonStyle={buttonStyle}
589
+ />
590
+ </div>
591
+ );
592
+ })()}
593
+ </div>
594
+ );
595
+ },
596
+ [
597
+ getOption,
598
+ splitOptionsByDividers,
599
+ createDropdownOptions,
600
+ optionHandlers,
601
+ buttonHandlers,
602
+ editor,
603
+ ],
604
+ );
563
605
 
564
606
  // Memoize toolbar structure (expensive grouping operation)
565
607
  const toolbarStructure = useMemo(() => {
@@ -575,8 +617,9 @@ export const ReactSlate = forwardRef<any, ReactSlateProps>((props, ref) => {
575
617
  );
576
618
 
577
619
  // Return sorted entries for consistent rendering
578
- return Object.entries(groupsByRow)
579
- .sort(([a], [b]) => parseInt(a) - parseInt(b));
620
+ return Object.entries(groupsByRow).sort(
621
+ ([a], [b]) => parseInt(a) - parseInt(b),
622
+ );
580
623
  }, [editorProfile.toolbar.groups]);
581
624
 
582
625
  const editLink = useCallback((element: any) => {
@@ -634,11 +677,12 @@ export const ReactSlate = forwardRef<any, ReactSlateProps>((props, ref) => {
634
677
  },
635
678
  };
636
679
  } else {
680
+ const normalizedUrl = normalizeUrl(link.url || "");
637
681
  newProperties = {
638
- url: link.url,
682
+ url: normalizedUrl,
639
683
  link: {
640
684
  type: "external",
641
- url: link.url,
685
+ url: normalizedUrl,
642
686
  target: link.target,
643
687
  queryString: link.queryString,
644
688
  },
@@ -664,51 +708,59 @@ export const ReactSlate = forwardRef<any, ReactSlateProps>((props, ref) => {
664
708
  );
665
709
 
666
710
  // Use the keyboard handler from plugins
667
- const handleKeyDown = useMemo(() => createKeyboardHandler(editor, simplifiedProfile), [editor, simplifiedProfile]);
711
+ const handleKeyDown = useMemo(
712
+ () => createKeyboardHandler(editor, simplifiedProfile),
713
+ [editor, simplifiedProfile],
714
+ );
668
715
 
669
716
  // Calculate proper list numbering for ordered lists
670
- const calculateListNumbers = useCallback((elements: Descendant[]): Map<string, string> => {
671
- const numberMap = new Map<string, string>();
672
- const counters: number[] = [0, 0, 0, 0, 0, 0]; // Support up to 6 levels
673
- let wasInList = false; // Track if previous element was a list item
674
-
675
- elements.forEach((element, index) => {
676
- if (Element.isElement(element) &&
677
- element.type === 'list-item' &&
678
- (element as any).listType === 'ordered') {
679
- const indent = ((element as any).indent || 0);
680
-
681
- // Ensure indent is within bounds
682
- if (indent < 0 || indent >= counters.length) return;
683
-
684
- // If we weren't in a list before, this is a new list - reset all counters
685
- if (!wasInList) {
686
- for (let i = 0; i < counters.length; i++) {
717
+ const calculateListNumbers = useCallback(
718
+ (elements: Descendant[]): Map<string, string> => {
719
+ const numberMap = new Map<string, string>();
720
+ const counters: number[] = [0, 0, 0, 0, 0, 0]; // Support up to 6 levels
721
+ let wasInList = false; // Track if previous element was a list item
722
+
723
+ elements.forEach((element, index) => {
724
+ if (
725
+ Element.isElement(element) &&
726
+ element.type === "list-item" &&
727
+ (element as any).listType === "ordered"
728
+ ) {
729
+ const indent = (element as any).indent || 0;
730
+
731
+ // Ensure indent is within bounds
732
+ if (indent < 0 || indent >= counters.length) return;
733
+
734
+ // If we weren't in a list before, this is a new list - reset all counters
735
+ if (!wasInList) {
736
+ for (let i = 0; i < counters.length; i++) {
737
+ counters[i] = 0;
738
+ }
739
+ }
740
+
741
+ // Increment counter for current level
742
+ counters[indent] = (counters[indent] || 0) + 1;
743
+
744
+ // Reset all deeper level counters
745
+ for (let i = indent + 1; i < counters.length; i++) {
687
746
  counters[i] = 0;
688
747
  }
748
+
749
+ // Build number string for current level only (e.g., "3.")
750
+ const numberString = counters[indent] + ".";
751
+ numberMap.set(`${index}`, numberString);
752
+
753
+ wasInList = true;
754
+ } else {
755
+ // Not a list item, so we're no longer in a list
756
+ wasInList = false;
689
757
  }
690
-
691
- // Increment counter for current level
692
- counters[indent] = (counters[indent] || 0) + 1;
693
-
694
- // Reset all deeper level counters
695
- for (let i = indent + 1; i < counters.length; i++) {
696
- counters[i] = 0;
697
- }
698
-
699
- // Build number string for current level only (e.g., "3.")
700
- const numberString = counters[indent] + '.';
701
- numberMap.set(`${index}`, numberString);
702
-
703
- wasInList = true;
704
- } else {
705
- // Not a list item, so we're no longer in a list
706
- wasInList = false;
707
- }
708
- });
709
-
710
- return numberMap;
711
- }, []);
758
+ });
759
+
760
+ return numberMap;
761
+ },
762
+ [],
763
+ );
712
764
 
713
765
  // Calculate list numbers for the entire editor content (memoized for performance)
714
766
  const listNumbers = useMemo(() => {
@@ -716,146 +768,160 @@ export const ReactSlate = forwardRef<any, ReactSlateProps>((props, ref) => {
716
768
  }, [editor.children, calculateListNumbers]);
717
769
 
718
770
  // Memoize renderElement to prevent unnecessary re-renders as per Slate performance docs
719
- const renderElement = useCallback(({ attributes, children, element }: any) => {
720
- const style: React.CSSProperties = {
721
- textAlign: element.align || "left",
722
- };
771
+ const renderElement = useCallback(
772
+ ({ attributes, children, element }: any) => {
773
+ const style: React.CSSProperties = {
774
+ textAlign: element.align || "left",
775
+ };
723
776
 
724
777
  if (element.type === "link") {
725
778
  const isInternal = element.link?.type === "internal";
726
779
  const url = isInternal
727
- ? "#"
728
- : element.url || element.link?.url || "#";
729
-
730
- return (
731
- <a
732
- {...attributes}
733
- href={url}
734
- style={style}
735
- className={`slate-link ${isInternal ? "internal-link" : "external-link"}`}
736
- onClick={(e) => {
737
- if (!readOnly) {
738
- e.preventDefault();
739
- editLink(element);
740
- }
741
- }}
742
- >
743
- {children}
744
- </a>
745
- );
746
- }
780
+ ? generateInternalLinkUrl(element.link?.itemId, element.link?.targetItemLongId, element.link?.queryString)
781
+ : normalizeUrl(element.url || element.link?.url || "#");
782
+
783
+ return (
784
+ <a
785
+ {...attributes}
786
+ href={url}
787
+ style={style}
788
+ className={`slate-link ${isInternal ? "internal-link" : "external-link"}`}
789
+ onClick={(e) => {
790
+ if (!readOnly) {
791
+ e.preventDefault();
792
+ editLink(element);
793
+ }
794
+ }}
795
+ >
796
+ {children}
797
+ </a>
798
+ );
799
+ }
747
800
 
748
- if (element.type === "list-item") {
749
- const indent = element.indent || 0;
750
- const listType = element.listType || "unordered";
801
+ if (element.type === "list-item") {
802
+ const indent = element.indent || 0;
803
+ const listType = element.listType || "unordered";
751
804
 
752
- const listStyle: React.CSSProperties = {
753
- ...style,
754
- position: "relative",
755
- listStyleType: "none",
756
- };
805
+ const listStyle: React.CSSProperties = {
806
+ ...style,
807
+ position: "relative",
808
+ listStyleType: "none",
809
+ };
757
810
 
758
- // Find the element's index in the editor children to get the correct number
759
- const elementIndex = editor.children.findIndex((child: any) => child === element);
760
- const listNumber = listNumbers.get(`${elementIndex}`) || '';
811
+ // Find the element's index in the editor children to get the correct number
812
+ const elementIndex = editor.children.findIndex(
813
+ (child: any) => child === element,
814
+ );
815
+ const listNumber = listNumbers.get(`${elementIndex}`) || "";
816
+
817
+ return (
818
+ <div
819
+ {...attributes}
820
+ style={listStyle}
821
+ className={`slate-list-item slate-list-${listType}`}
822
+ data-indent={indent}
823
+ data-list-number={listNumber}
824
+ >
825
+ <span className="slate-list-bullet"></span>
826
+ <div className="slate-list-content">{children}</div>
827
+ </div>
828
+ );
829
+ }
761
830
 
762
- return (
763
- <div
764
- {...attributes}
765
- style={listStyle}
766
- className={`slate-list-item slate-list-${listType}`}
767
- data-indent={indent}
768
- data-list-number={listNumber}
769
- >
770
- <span className="slate-list-bullet"></span>
771
- <div className="slate-list-content">{children}</div>
772
- </div>
773
- );
774
- }
831
+ if (element.type === "horizontal-rule") {
832
+ return (
833
+ <div
834
+ {...attributes}
835
+ contentEditable={false}
836
+ style={{ ...style, userSelect: "none" }}
837
+ >
838
+ {children}
839
+ <hr
840
+ style={{
841
+ border: "none",
842
+ borderTop: "1px solid #ccc",
843
+ margin: "1em 0",
844
+ width: "100%",
845
+ }}
846
+ />
847
+ </div>
848
+ );
849
+ }
775
850
 
776
- if (element.type === "horizontal-rule") {
777
- return (
778
- <div {...attributes} contentEditable={false} style={{ ...style, userSelect: "none" }}>
779
- {children}
780
- <hr style={{
781
- border: "none",
782
- borderTop: "1px solid #ccc",
783
- margin: "1em 0",
784
- width: "100%"
785
- }} />
786
- </div>
787
- );
788
- }
851
+ if (element.type === "line-break") {
852
+ return (
853
+ <span {...attributes} contentEditable={false}>
854
+ {children}
855
+ <br />
856
+ </span>
857
+ );
858
+ }
789
859
 
790
- if (element.type === "line-break") {
791
- return (
792
- <span {...attributes} contentEditable={false}>
793
- {children}
794
- <br />
795
- </span>
796
- );
797
- }
860
+ // Handle different block types using built-in SLATE_BLOCKS configuration
861
+ const isValidBlockId = element.type in SLATE_BLOCKS;
862
+ const blockConfig = isValidBlockId
863
+ ? SLATE_BLOCKS[element.type as BlockId]
864
+ : undefined;
865
+ if (blockConfig && element.type === "no-tag") {
866
+ // Special handling for no-tag blocks (plain text without wrapper)
867
+ return (
868
+ <div {...attributes} style={style}>
869
+ {children}
870
+ </div>
871
+ );
872
+ }
798
873
 
799
- // Handle different block types using built-in SLATE_BLOCKS configuration
800
- const isValidBlockId = element.type in SLATE_BLOCKS;
801
- const blockConfig = isValidBlockId ? SLATE_BLOCKS[element.type as BlockId] : undefined;
802
- if (blockConfig && element.type === "no-tag") {
803
- // Special handling for no-tag blocks (plain text without wrapper)
874
+ // For standard blocks, use the appropriate HTML tag
875
+ if (blockConfig) {
876
+ const tagName = blockConfig.htmlTag;
877
+ return React.createElement(tagName, { ...attributes, style }, children);
878
+ }
879
+ // Default fallback to paragraph
804
880
  return (
805
- <div {...attributes} style={style}>
881
+ <p {...attributes} style={style}>
806
882
  {children}
807
- </div>
883
+ </p>
808
884
  );
809
- }
810
-
811
- // For standard blocks, use the appropriate HTML tag
812
- if (blockConfig) {
813
- const tagName = blockConfig.htmlTag;
814
- return React.createElement(
815
- tagName,
816
- { ...attributes, style },
817
- children,
818
- );
819
- }
820
- // Default fallback to paragraph
821
- return (
822
- <p {...attributes} style={style}>
823
- {children}
824
- </p>
825
- );
826
- }, [readOnly, editLink, editor.children, listNumbers]);
885
+ },
886
+ [readOnly, editLink, editor.children, listNumbers],
887
+ );
827
888
 
828
889
  // Memoize renderLeaf to prevent unnecessary re-renders as per Slate performance docs
829
- const renderLeaf = useCallback(({ attributes, children, leaf }: any) => {
830
-
831
- let el = <span {...attributes}>{children}</span>;
832
-
833
- // Apply marks using the built-in SLATE_MARKS configuration
834
- simplifiedProfile.marks.forEach((markId) => {
835
- if ((leaf as any)[markId]) {
836
- const markConfig = SLATE_MARKS[markId];
837
- if (markConfig) {
838
- if (markId === "extrabold") {
839
- // Special handling for extrabold with CSS class
840
- el = <span className="extrabold">{el}</span>;
841
- } else {
842
- // Standard HTML tag rendering
843
- const tagName = markConfig.htmlTag;
844
- el = React.createElement(tagName, {}, el);
890
+ const renderLeaf = useCallback(
891
+ ({ attributes, children, leaf }: any) => {
892
+ let el = <span {...attributes}>{children}</span>;
893
+
894
+ // Apply marks using the built-in SLATE_MARKS configuration
895
+ simplifiedProfile.marks.forEach((markId) => {
896
+ if ((leaf as any)[markId]) {
897
+ const markConfig = SLATE_MARKS[markId];
898
+ if (markConfig) {
899
+ if (markId === "extrabold") {
900
+ // Special handling for extrabold with CSS class
901
+ el = <span className="extrabold">{el}</span>;
902
+ } else {
903
+ // Standard HTML tag rendering
904
+ const tagName = markConfig.htmlTag;
905
+ el = React.createElement(tagName, {}, el);
906
+ }
845
907
  }
846
908
  }
847
- }
848
- });
909
+ });
849
910
 
850
- return el;
851
- }, [simplifiedProfile.marks]);
911
+ return el;
912
+ },
913
+ [simplifiedProfile.marks],
914
+ );
852
915
 
853
916
  // Memoize renderPlaceholder for consistency
854
- const renderPlaceholder = useCallback(({ attributes, children }: any) => (
855
- <span {...attributes} className="p-2 text-gray-500">
856
- {children}
857
- </span>
858
- ), []);
917
+ const renderPlaceholder = useCallback(
918
+ ({ attributes, children }: any) => (
919
+ <span {...attributes} className="p-2 text-gray-500">
920
+ {children}
921
+ </span>
922
+ ),
923
+ [],
924
+ );
859
925
 
860
926
  return (
861
927
  <div className={`slate-editor ${props.className}`}>
@@ -864,8 +930,8 @@ export const ReactSlate = forwardRef<any, ReactSlateProps>((props, ref) => {
864
930
  initialValue={initialSlateValue}
865
931
  onChange={handleChange}
866
932
  >
867
- {!readOnly && (
868
- <div className="toolbar">
933
+ {!readOnly && showControls && (
934
+ <div className="mb-4 flex flex-col gap-2">
869
935
  {toolbarStructure.map(([rowIndex, rowGroups], mapIndex) => (
870
936
  <div key={`row-${rowIndex}`} className="toolbar-row">
871
937
  {rowGroups.map((group) =>
@@ -901,7 +967,7 @@ export const ReactSlate = forwardRef<any, ReactSlateProps>((props, ref) => {
901
967
  className={classNames(
902
968
  readOnly ? "bg-gray-4" : "bg-gray-5",
903
969
  "focus-shadow p-2",
904
- "slate-editable"
970
+ "slate-editable",
905
971
  )}
906
972
  readOnly={readOnly}
907
973
  placeholder={placeholder}