@widgetic/editor 4.0.10 → 4.1.0

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 (84) hide show
  1. package/README.md +242 -0
  2. package/dist/components/WidgetPreview.svelte +123 -86
  3. package/dist/components/WidgetPreview.svelte.d.ts +8 -0
  4. package/dist/components/color-select/ColorSelect.svelte +4 -4
  5. package/dist/components/editors/CompositionEditorApp.svelte +562 -0
  6. package/dist/components/editors/CompositionEditorApp.svelte.d.ts +95 -0
  7. package/dist/components/editors/EmbedEditor.svelte +33 -22
  8. package/dist/components/editors/EmbedEditor.svelte.d.ts +1 -1
  9. package/dist/components/editors/PropsEditor.svelte +267 -850
  10. package/dist/components/editors/PropsEditor.svelte.d.ts +10 -3
  11. package/dist/components/ic/AngleInputController.svelte +1 -1
  12. package/dist/components/ic/BrowserInputController.svelte +58 -31
  13. package/dist/components/ic/ColorPickerInputController.svelte +55 -21
  14. package/dist/components/ic/DateFieldInputController.svelte +34 -14
  15. package/dist/components/ic/DatePickerInputController.svelte +37 -21
  16. package/dist/components/ic/DropdownInputController.svelte +21 -9
  17. package/dist/components/ic/DropdownInputController.svelte.d.ts +1 -1
  18. package/dist/components/ic/FixedPositionInputController.svelte +12 -6
  19. package/dist/components/ic/FontInputController.svelte +100 -76
  20. package/dist/components/ic/InputControllerHeader.svelte +16 -9
  21. package/dist/components/ic/NumberControls.svelte +4 -4
  22. package/dist/components/ic/NumberInputController.svelte +6 -4
  23. package/dist/components/ic/PositionInputController.svelte +23 -15
  24. package/dist/components/ic/RangeCalendarInputController.svelte +36 -20
  25. package/dist/components/ic/RoundedCornersInputController.svelte +39 -26
  26. package/dist/components/ic/SliderInputController.svelte +7 -2
  27. package/dist/components/ic/SwitchInputController.svelte +12 -3
  28. package/dist/components/ic/TagsInputController.svelte +19 -4
  29. package/dist/components/ic/TextAreaInputController.svelte +79 -51
  30. package/dist/components/ic/TextAreaPreviewInputController.svelte +27 -72
  31. package/dist/components/ic/TextInputController.svelte +17 -1
  32. package/dist/components/ic/TimeInputController.svelte +11 -4
  33. package/dist/components/ic/VectorInputController.svelte +101 -70
  34. package/dist/components/ic/shared/ItemGrid.svelte +12 -4
  35. package/dist/components/ic/shared/RecentItemsSection.svelte +16 -11
  36. package/dist/components/ic/shared/RecentItemsSection.svelte.d.ts +1 -1
  37. package/dist/composition/compositionApi.d.ts +95 -0
  38. package/dist/composition/compositionApi.js +289 -0
  39. package/dist/composition/compositionEditorController.d.ts +263 -0
  40. package/dist/composition/compositionEditorController.js +746 -0
  41. package/dist/composition/compositionHelpers.d.ts +74 -0
  42. package/dist/composition/compositionHelpers.js +405 -0
  43. package/dist/i18n/index.d.ts +4 -0
  44. package/dist/i18n/index.js +4 -0
  45. package/dist/i18n/labels.d.ts +15 -0
  46. package/dist/i18n/labels.js +57 -0
  47. package/dist/i18n/language.d.ts +228 -0
  48. package/dist/i18n/language.js +34 -0
  49. package/dist/i18n/languageKit.d.ts +54 -0
  50. package/dist/i18n/languageKit.js +102 -0
  51. package/dist/i18n/messages.d.ts +241 -0
  52. package/dist/i18n/messages.js +253 -0
  53. package/dist/i18n/translate.d.ts +6 -0
  54. package/dist/i18n/translate.js +8 -0
  55. package/dist/index.d.ts +4 -0
  56. package/dist/index.js +8 -0
  57. package/dist/mediaLibraryContext.d.ts +10 -0
  58. package/dist/mediaLibraryContext.js +30 -0
  59. package/dist/utils/FontManager.d.ts +21 -5
  60. package/dist/utils/FontManager.js +62 -67
  61. package/dist/utils/VectorManager.d.ts +0 -1
  62. package/dist/utils/VectorManager.js +10 -12
  63. package/dist/utils/dockPlacement.d.ts +12 -0
  64. package/dist/utils/dockPlacement.js +13 -0
  65. package/dist/utils/editorStorage.d.ts +23 -0
  66. package/dist/utils/editorStorage.js +95 -0
  67. package/dist/utils/fieldDirection.d.ts +13 -0
  68. package/dist/utils/fieldDirection.js +25 -0
  69. package/dist/utils/fontUtils.js +5 -3
  70. package/dist/utils/htmlSafety.d.ts +21 -0
  71. package/dist/utils/htmlSafety.js +38 -0
  72. package/dist/utils/itemReorder.d.ts +90 -0
  73. package/dist/utils/itemReorder.js +354 -0
  74. package/dist/utils/recentColors.js +4 -2
  75. package/dist/utils/richTextPreview.d.ts +38 -0
  76. package/dist/utils/richTextPreview.js +207 -0
  77. package/dist/utils/scrollActivity.d.ts +14 -0
  78. package/dist/utils/scrollActivity.js +63 -0
  79. package/dist/utils/shadowDom.d.ts +67 -0
  80. package/dist/utils/shadowDom.js +211 -0
  81. package/dist/utils/vectorUtils.js +5 -3
  82. package/dist/utils/visibilityCondition.d.ts +40 -0
  83. package/dist/utils/visibilityCondition.js +535 -0
  84. package/package.json +7 -6
@@ -68,9 +68,16 @@
68
68
  // Import Shad Editor
69
69
  import { ShadEditor, type TipTapEditor, type TipTapEditorLevel } from '@widgetic/design-system/shad-editor';
70
70
 
71
+ // The words of the editor in the language of the page, and the share of them that belongs to the rich text editor below.
72
+ import { useEditorLanguage } from '../../i18n/language';
73
+ import type { EditorMessageKey } from '../../i18n/messages';
74
+
71
75
  // Add local ColorPickerInputController import
72
76
  import ColorPickerInputController from './ColorPickerInputController.svelte';
73
77
 
78
+ // Looking things up and listening for scrolling in the tree the editor is in (a module has a shadow root)
79
+ import { isEventInside, listenForScroll, queryAllInTreeOf, queryInTreeOf } from '../../utils/shadowDom';
80
+
74
81
  // Add this constant for the decoration type
75
82
  const TRANSITION_DURATION = 250;
76
83
 
@@ -91,6 +98,8 @@
91
98
  // Use config values with fallbacks
92
99
  const textColor = $derived(config.textColor || '#1a1a1a');
93
100
 
101
+ const { t, shadEditor: shadEditorLanguage } = useEditorLanguage();
102
+
94
103
  /**
95
104
  * State Variables
96
105
  */
@@ -108,7 +117,7 @@
108
117
  // Add type definition for formats
109
118
  type FormatType = 'bold' | 'italic' | 'underline' | 'link';
110
119
  type ActiveFormats = Record<FormatType, boolean>;
111
- type FormatMapping = Record<FormatType, { value: string; icon: any }>;
120
+ type FormatMapping = Record<FormatType, { value: string; icon: any; messageKey: EditorMessageKey }>;
112
121
 
113
122
  let activeFormats = $state<ActiveFormats>({
114
123
  bold: false,
@@ -118,10 +127,10 @@
118
127
  });
119
128
 
120
129
  const formatMapping: FormatMapping = {
121
- bold: { value: 'b', icon: IconBold },
122
- italic: { value: 'i', icon: IconItalic },
123
- underline: { value: 'u', icon: IconUnderline },
124
- link: { value: 'link', icon: IconLink }
130
+ bold: { value: 'b', icon: IconBold, messageKey: 'editor.textArea.toggleBold' },
131
+ italic: { value: 'i', icon: IconItalic, messageKey: 'editor.textArea.toggleItalic' },
132
+ underline: { value: 'u', icon: IconUnderline, messageKey: 'editor.textArea.toggleUnderline' },
133
+ link: { value: 'link', icon: IconLink, messageKey: 'editor.textArea.toggleLink' }
125
134
  };
126
135
 
127
136
  let currentTextStyle = $state('text'); // Track current text style
@@ -130,17 +139,23 @@
130
139
  let currentColor = $derived(textColor);
131
140
  let selectedTextRange: { from: number, to: number } | null = $state(null);
132
141
 
133
- const textOptions = [
134
- { value: 'text', label: 'Paragraph', icon: IconPilcrow },
135
- { value: 'h1', label: 'H1' },
136
- { value: 'h2', label: 'H2' },
137
- { value: 'h3', label: 'H3' }
142
+ // Each option is shown by its message (`messageKey`), in the language of the page.
143
+ const textOptions: { value: string; messageKey: EditorMessageKey; icon?: any }[] = [
144
+ { value: 'text', messageKey: 'editor.textArea.paragraph', icon: IconPilcrow },
145
+ { value: 'h1', messageKey: 'editor.textArea.heading1' },
146
+ { value: 'h2', messageKey: 'editor.textArea.heading2' },
147
+ { value: 'h3', messageKey: 'editor.textArea.heading3' }
138
148
  ];
139
149
 
140
- const listOptions = [
141
- { value: 'bulletList', label: 'Bullet List', icon: IconList },
142
- { value: 'orderedList', label: 'Numbered List', icon: IconListNumbers },
143
- { value: 'checkList', label: 'Check List', icon: IconListCheck }
150
+ /** The message of the style the toolbar button shows: the plain text's when the style is none of the options. */
151
+ let currentTextStyleMessageKey = $derived<EditorMessageKey>(
152
+ textOptions.find((option) => option.value === currentTextStyle)?.messageKey ?? 'editor.textArea.text'
153
+ );
154
+
155
+ const listOptions: { value: string; messageKey: EditorMessageKey; icon?: any }[] = [
156
+ { value: 'bulletList', messageKey: 'editor.textArea.bulletList', icon: IconList },
157
+ { value: 'orderedList', messageKey: 'editor.textArea.numberedList', icon: IconListNumbers },
158
+ { value: 'checkList', messageKey: 'editor.textArea.checkList', icon: IconListCheck }
144
159
  ];
145
160
 
146
161
  const content = writable(value);
@@ -392,7 +407,7 @@
392
407
  break;
393
408
  case 'link':
394
409
  if (isAdding) {
395
- const href = window.prompt('Enter URL');
410
+ const href = window.prompt($t('editor.textArea.enterUrl'));
396
411
  if (href) {
397
412
  chain.setLink({ href }).run();
398
413
  }
@@ -533,7 +548,9 @@
533
548
  });
534
549
 
535
550
  updateActiveFormats();
551
+ // Choosing from either menu (text style or list style) closes it: the list style menu used to stay open.
536
552
  textStyleDropdownOpen = false;
553
+ listStyleDropdownOpen = false;
537
554
  } catch (error) {
538
555
  console.error('Error changing text style:', error);
539
556
  }
@@ -843,8 +860,11 @@
843
860
  if (isDestroyed) return;
844
861
  const handleClickOutside = (event: MouseEvent) => {
845
862
  if (isDestroyed) return;
846
- const colorSelectModal = document.querySelector('[data-radix-popper-content-wrapper]');
847
- const isClickingColorSelect = colorSelectModal?.contains(event.target as Node);
863
+ // Looked for in the tree this editor is in (inside a module, `document` has none of the editor's elements and may
864
+ // have a popover of the customer's page), and checked with the event's path (the target the document sees is the
865
+ // module's host)
866
+ const colorSelectModals = queryAllInTreeOf(toolbarRef, '[data-radix-popper-content-wrapper]');
867
+ const isClickingColorSelect = colorSelectModals.some((colorSelectModal) => isEventInside(event, colorSelectModal));
848
868
 
849
869
  // Don't do anything if clicking within the color select modal
850
870
  if (isClickingColorSelect) {
@@ -941,9 +961,11 @@
941
961
  function handleEditorBlur(relatedTarget: Node | null) {
942
962
  if (!editorInstance) return;
943
963
 
944
- const colorSelectModal = document.querySelector('[data-radix-popper-content-wrapper]');
945
- const popoverContent = document.querySelector('[role="dialog"]');
946
- const commandMenu = document.querySelector('[cmdk-root]');
964
+ // Looked for in the tree the editor is in: `document` does not see inside a module's shadow root, and may have a
965
+ // dialog of the customer's page
966
+ const colorSelectModal = queryInTreeOf(toolbarRef, '[data-radix-popper-content-wrapper]');
967
+ const popoverContent = queryInTreeOf(toolbarRef, '[role="dialog"]');
968
+ const commandMenu = queryInTreeOf(toolbarRef, '[cmdk-root]');
947
969
 
948
970
  const isClickingInside = toolbarRef?.contains(relatedTarget) ||
949
971
  editorInstance.view.dom.contains(relatedTarget as Node) ||
@@ -1030,12 +1052,15 @@
1030
1052
  }
1031
1053
 
1032
1054
  // Add to lifecycle management
1055
+ // (listens to the page and, inside a module, to the module's own panels too: a scroll never leaves a shadow root)
1056
+ let stopListeningForScroll: () => void = () => {};
1057
+
1033
1058
  onMount(() => {
1034
- window.addEventListener('scroll', handleScroll, true);
1059
+ stopListeningForScroll = listenForScroll(toolbarRef, handleScroll);
1035
1060
  });
1036
1061
 
1037
1062
  onDestroy(() => {
1038
- window.removeEventListener('scroll', handleScroll, true);
1063
+ stopListeningForScroll();
1039
1064
  });
1040
1065
 
1041
1066
  // Add cleanup function
@@ -1103,17 +1128,17 @@
1103
1128
  <!-- Toolbar at the top -->
1104
1129
  <div
1105
1130
  bind:this={toolbarRef}
1106
- class="toolbar-container sticky top-0 left-0 right-0 h-[56px] bg-grey1 z-10 border-b border-grey2 overflow-visible select-none"
1131
+ class="toolbar-container sticky top-0 start-0 end-0 h-[56px] bg-grey1 z-10 border-b border-grey2 overflow-visible select-none"
1107
1132
  on:mousedown={handleToolbarInteraction}
1108
1133
  on:click={handleToolbarInteraction}
1109
1134
  on:keydown={handleToolbarKeyDown}
1110
1135
  role="toolbar"
1111
- aria-label="Text formatting toolbar"
1136
+ aria-label={$t('editor.textArea.toolbar')}
1112
1137
  tabindex="-1"
1113
1138
  >
1114
1139
  <div class="toolbar-items-row relative flex items-center h-full w-full">
1115
1140
  <!-- Format cluster centered; close stays on the right -->
1116
- <div class={`toolbar-items-container absolute inset-y-0 left-0 right-10 flex items-center justify-center gap-2 transition-transform duration-200 ease-out ${
1141
+ <div class={`toolbar-items-container absolute inset-y-0 start-0 end-10 flex items-center justify-center gap-2 transition-transform duration-200 ease-out ${
1117
1142
  isColorPickerAnimating ? 'translate-y-[-200%]' :
1118
1143
  showColorPicker ? 'translate-y-[-200%]' :
1119
1144
  'translate-y-0'
@@ -1151,10 +1176,10 @@
1151
1176
  handleToggleChange(value);
1152
1177
  }}
1153
1178
  >
1154
- {#each Object.entries(formatMapping) as [format, { value, icon: Icon }] (format)}
1179
+ {#each Object.entries(formatMapping) as [format, { value, icon: Icon, messageKey }] (format)}
1155
1180
  <ToggleGroup.Item
1156
1181
  {value}
1157
- aria-label={`Toggle ${format}`}
1182
+ aria-label={$t(messageKey)}
1158
1183
  data-state={activeFormats[format as FormatType] ? 'on' : 'off'}
1159
1184
  class="toolbar-button shrink-0 !h-8 !w-8 !min-w-8 !p-0 {activeFormats[format as FormatType] ? 'bg-muted' : ''}"
1160
1185
  >
@@ -1199,7 +1224,7 @@
1199
1224
  {#if currentTextStyle === 'text'}
1200
1225
  <IconPilcrow class="h-3.5 w-3.5 shrink-0" />
1201
1226
  {:else}
1202
- <span class="text-xs">{textOptions.find(opt => opt.value === currentTextStyle)?.label || 'Text'}</span>
1227
+ <span class="text-xs">{$t(currentTextStyleMessageKey)}</span>
1203
1228
  {/if}
1204
1229
  <ChevronDown class="h-3 w-3 shrink-0" />
1205
1230
  </div>
@@ -1210,7 +1235,7 @@
1210
1235
  class="w-[200px] p-0"
1211
1236
  >
1212
1237
  <Command.Root>
1213
- <Command.List>
1238
+ <Command.List aria-label={$t('editor.textArea.textStyles')}>
1214
1239
  {#each textOptions as option}
1215
1240
  <Command.Item
1216
1241
  value={option.value}
@@ -1226,10 +1251,10 @@
1226
1251
  {#if option.icon}
1227
1252
  <option.icon class="h-4 w-4" />
1228
1253
  {/if}
1229
- <span>{option.label}</span>
1254
+ <span>{$t(option.messageKey)}</span>
1230
1255
  </div>
1231
1256
  {:else}
1232
- <span>{option.label}</span>
1257
+ <span>{$t(option.messageKey)}</span>
1233
1258
  {/if}
1234
1259
  {#if option.value === currentTextStyle}
1235
1260
  <div class="h-4 w-4">✓</div>
@@ -1283,7 +1308,7 @@
1283
1308
  class="style-dropdown-container w-[300px] p-0"
1284
1309
  >
1285
1310
  <Command.Root>
1286
- <Command.List>
1311
+ <Command.List aria-label={$t('editor.textArea.listStyles')}>
1287
1312
  {#each listOptions as option}
1288
1313
  <Command.Item
1289
1314
  value={option.value}
@@ -1298,7 +1323,7 @@
1298
1323
  {#if option.icon}
1299
1324
  <option.icon class="h-4 w-4" />
1300
1325
  {/if}
1301
- <span>{option.label}</span>
1326
+ <span>{$t(option.messageKey)}</span>
1302
1327
  </div>
1303
1328
  {#if option.value === currentTextStyle}
1304
1329
  <div class="h-4 w-4">✓</div>
@@ -1315,7 +1340,7 @@
1315
1340
  {#if onClose && !showColorPicker && !isColorPickerAnimating}
1316
1341
  <Button
1317
1342
  variant="ghost"
1318
- class="toolbar-close-button ml-auto shrink-0 p-0 hover:bg-transparent h-8 w-8 mr-2 z-10"
1343
+ class="toolbar-close-button ms-auto shrink-0 p-0 hover:bg-transparent h-8 w-8 me-2 z-10"
1319
1344
  onclick={(e) => {
1320
1345
  e.stopPropagation();
1321
1346
  e.preventDefault();
@@ -1389,6 +1414,8 @@
1389
1414
  <ShadEditor
1390
1415
  bind:content={value}
1391
1416
  showToolbar={false}
1417
+ locale={$shadEditorLanguage.locale ?? undefined}
1418
+ messages={$shadEditorLanguage.messages ?? undefined}
1392
1419
  class="shad-editor subtext {editorClass} {showColorPicker || textStyleDropdownOpen ? 'color-picking-active' : ''} {editorHasFocus ? 'editor-focused' : ''} {isSelectionActive ? 'selection-active' : ''}"
1393
1420
  on:mount={handleEditorMount}
1394
1421
  on:update={handleEditorUpdate}
@@ -1433,18 +1460,18 @@
1433
1460
  /* Keep scrollbar styles */
1434
1461
  :global(.ProseMirror::-webkit-scrollbar) {
1435
1462
  width: 6px;
1436
- margin-right: 2px;
1463
+ margin-inline-end: 2px;
1437
1464
  }
1438
1465
 
1439
1466
  :global(.ProseMirror::-webkit-scrollbar-track) {
1440
1467
  background-color: transparent;
1441
- margin-right: 2px;
1468
+ margin-inline-end: 2px;
1442
1469
  }
1443
1470
 
1444
1471
  :global(.ProseMirror::-webkit-scrollbar-thumb) {
1445
1472
  background-color: hsl(var(--border)) !important;
1446
1473
  border-radius: 3px;
1447
- margin-right: 2px;
1474
+ margin-inline-end: 2px;
1448
1475
  }
1449
1476
 
1450
1477
  :global(.ProseMirror::-webkit-scrollbar-thumb:hover) {
@@ -1471,7 +1498,7 @@
1471
1498
  :global(.ProseMirror ul),
1472
1499
  :global(.ProseMirror ol),
1473
1500
  :global(.ProseMirror [data-type="taskList"]) {
1474
- padding-left: 1.5rem;
1501
+ padding-inline-start: 1.5rem;
1475
1502
  margin: 0;
1476
1503
  line-height: 1.4;
1477
1504
  }
@@ -1479,21 +1506,21 @@
1479
1506
  :global(.ProseMirror li),
1480
1507
  :global(.ProseMirror [data-type="taskItem"]) {
1481
1508
  position: relative;
1482
- padding-left: 0.5rem;
1509
+ padding-inline-start: 0.5rem;
1483
1510
  margin: 0;
1484
1511
  }
1485
1512
 
1486
1513
  /* Task list specific styles */
1487
1514
  :global(.ProseMirror [data-type="taskList"]) {
1488
1515
  list-style: none;
1489
- padding-left: 0rem !important;
1516
+ padding-inline-start: 0rem !important;
1490
1517
  }
1491
1518
 
1492
1519
  :global(.ProseMirror [data-type="taskItem"]) {
1493
1520
  display: flex;
1494
1521
  align-items: flex-start;
1495
- padding-left: 0 !important;
1496
- margin-left: 0 !important;
1522
+ padding-inline-start: 0 !important;
1523
+ margin-inline-start: 0 !important;
1497
1524
  gap: 0.5rem;
1498
1525
  }
1499
1526
 
@@ -1786,11 +1813,12 @@
1786
1813
  gap: 8px !important;
1787
1814
  height: 100% !important;
1788
1815
  position: absolute !important;
1789
- inset: 0 2.5rem 0 0 !important;
1816
+ inset-block: 0 !important;
1817
+ inset-inline: 0 2.5rem !important;
1790
1818
  }
1791
1819
 
1792
1820
  :global(.text-area #text-style-trigger) {
1793
- margin-right: 0 !important;
1821
+ margin-inline-end: 0 !important;
1794
1822
  }
1795
1823
 
1796
1824
  /* Color the brush icon based on selected color */
@@ -1803,12 +1831,12 @@
1803
1831
  }
1804
1832
  /* Button spacing */
1805
1833
  :global(.text-area .color-picker-preview-button) {
1806
- margin-left: 0 !important;
1807
- margin-right: 0 !important;
1834
+ margin-inline-start: 0 !important;
1835
+ margin-inline-end: 0 !important;
1808
1836
  }
1809
1837
 
1810
1838
  :global(.text-area .text-formatting-toggle-group) {
1811
- margin-right: 0 !important;
1839
+ margin-inline-end: 0 !important;
1812
1840
  display: flex !important;
1813
1841
  gap: 8px !important;
1814
1842
  }
@@ -1818,7 +1846,7 @@
1818
1846
  }
1819
1847
 
1820
1848
  :global(.text-area .text-formatting-toggle-group button:not(:last-child)) {
1821
- margin-right: 0 !important;
1849
+ margin-inline-end: 0 !important;
1822
1850
  }
1823
1851
 
1824
1852
  :global(.text-area #text-style-trigger),
@@ -1861,7 +1889,7 @@
1861
1889
 
1862
1890
  :global(.text-area .toolbar-text-style-ct),
1863
1891
  :global(.text-area .toolbar-list-style-ct) {
1864
- margin-left: 0 !important;
1892
+ margin-inline-start: 0 !important;
1865
1893
  }
1866
1894
 
1867
1895
  /* Update the color picker button to show selected color */
@@ -1902,8 +1930,8 @@
1902
1930
  width: 44px !important;
1903
1931
  min-width: 44px !important;
1904
1932
  max-width: 44px !important;
1905
- margin-left: 0 !important;
1906
- padding-left: 0 !important;
1933
+ margin-inline-start: 0 !important;
1934
+ padding-inline-start: 0 !important;
1907
1935
  }
1908
1936
 
1909
1937
  /* Update color picker container styles */
@@ -16,9 +16,15 @@
16
16
  <script lang="ts">
17
17
  import { Portal } from '@widgetic/design-system/components';
18
18
  import { IconArrowsDiagonal } from '@widgetic/design-system/icons';
19
+ import { dirForUserText, useTextDirection } from '@widgetic/design-system/components/textDirection';
19
20
  import { onMount, onDestroy } from 'svelte';
20
21
 
21
22
  import { TextAreaInputController } from './index';
23
+ import { createPreviewHtml } from '../../utils/richTextPreview';
24
+ import { escapeHtml } from '../../utils/htmlSafety';
25
+
26
+ // The way the screen is read: the text in the preview is what a person wrote, so on a right to left screen it is read by its own first letter.
27
+ const textDirection = useTextDirection();
22
28
 
23
29
  /**
24
30
  * Component props
@@ -42,7 +48,7 @@
42
48
  $: isShowingPlaceholder = !value && placeholder;
43
49
 
44
50
  // Create a preview version of the text that preserves formatting
45
- $: previewText = value ? createPreviewText(value) : placeholder;
51
+ $: previewText = value ? createPreviewText(value) : escapeHtml(placeholder);
46
52
 
47
53
  // Add this to track the current color
48
54
  let currentColor = '#1a1a1a'; // Default color
@@ -53,6 +59,10 @@
53
59
  // Add this to store the background rect ref
54
60
  let backgroundRectRef: HTMLDivElement;
55
61
 
62
+ // The box the text editor is shown in. The editor is found through it: a search of the whole `document` finds nothing
63
+ // inside a module's shadow root, and finds the customer's own text editor (and focuses it) when the page has one.
64
+ let portalContentRef: HTMLDivElement | undefined;
65
+
56
66
  // Add these near the top with other state declarations
57
67
  let isClosing = false;
58
68
 
@@ -60,73 +70,16 @@
60
70
  * Methods
61
71
  */
62
72
  function createPreviewText(text: string) {
63
- if (!text) return '';
64
-
65
- let html = text;
66
- if (!html.includes('<') && html.includes('&lt;')) {
67
- const decoded = document.createElement('textarea');
68
- decoded.innerHTML = html;
69
- html = decoded.value;
73
+ // The text is rich text somebody typed or pasted, and this editor can run inside a customer's page: the preview is
74
+ // not a copy of that HTML. It is built by a renderer that escapes every text and keeps only the tags and styles on
75
+ // its lists (see richTextPreview.ts). Block wrappers (p, div, br) are flattened there, so marks render inside the
76
+ // compact clamp box.
77
+ try {
78
+ return createPreviewHtml(text);
79
+ } catch (error) {
80
+ console.error('[TextAreaPreview] Could not build the preview of the text:', error);
81
+ return escapeHtml(text);
70
82
  }
71
-
72
- const parser = new DOMParser();
73
- const doc = parser.parseFromString(html, 'text/html');
74
-
75
- function processNode(node: Node): string {
76
- if (node.nodeType === Node.TEXT_NODE) {
77
- return node.textContent || '';
78
- }
79
-
80
- if (node.nodeType === Node.ELEMENT_NODE) {
81
- const element = node as Element;
82
- let tag = element.tagName.toLowerCase();
83
- const attributes: Record<string, string> = {};
84
- const children = Array.from(node.childNodes)
85
- .map(child => processNode(child))
86
- .join('');
87
-
88
- // Flatten block wrappers so marks render inside the compact clamp box
89
- if (tag === 'p' || tag === 'div' || tag === 'br') {
90
- return tag === 'br' ? ' ' : `${children} `;
91
- }
92
-
93
- const style = element.getAttribute('style');
94
- if (style) {
95
- const colorMatch = style.match(/color:\s*([^;]+)/);
96
- if (colorMatch) {
97
- attributes.style = `--color: ${colorMatch[1]}; ${style}`;
98
- } else {
99
- attributes.style = style;
100
- }
101
- }
102
-
103
- const textStyle = element.getAttribute('data-text-style');
104
- if (textStyle) {
105
- attributes['data-text-style'] = textStyle;
106
- attributes.class = (attributes.class || '') + ' preserve-color';
107
- }
108
-
109
- if (tag.match(/^h[1-4]$/)) {
110
- tag = 'span';
111
- attributes.class = (attributes.class || '') + ' tiptap-heading';
112
- }
113
-
114
- let startTag = `<${tag}`;
115
- for (const [key, attrValue] of Object.entries(attributes)) {
116
- startTag += ` ${key}="${attrValue.trim()}"`;
117
- }
118
- startTag += '>';
119
-
120
- return `${startTag}${children}</${tag}>`;
121
- }
122
-
123
- return '';
124
- }
125
-
126
- return Array.from(doc.body.childNodes)
127
- .map(node => processNode(node))
128
- .join('')
129
- .trim();
130
83
  }
131
84
 
132
85
  function handleFullEditorChange(html: string) {
@@ -141,7 +94,7 @@
141
94
 
142
95
  $: if (isShowingTextArea) {
143
96
  setTimeout(() => {
144
- const editor = document.querySelector('.ProseMirror');
97
+ const editor = portalContentRef?.querySelector('.ProseMirror');
145
98
  if (editor instanceof HTMLElement) {
146
99
  editor.focus();
147
100
  }
@@ -213,7 +166,7 @@
213
166
  isClosing = true;
214
167
 
215
168
  // Add animation end handler
216
- const content = document.querySelector('.text-area-portal-content') as HTMLDivElement;
169
+ const content = portalContentRef;
217
170
  const overlay = content?.previousElementSibling as HTMLDivElement;
218
171
 
219
172
  const handleAnimationEnd = () => {
@@ -246,15 +199,16 @@
246
199
  role="button"
247
200
  tabindex="0"
248
201
  >
249
- <div class="textarea-ic-preview-copy min-w-0 overflow-hidden pr-10 pt-[13px] pb-4 pl-4">
202
+ <div class="textarea-ic-preview-copy min-w-0 overflow-hidden pe-10 pt-[13px] pb-4 ps-4">
250
203
  <div
251
204
  class="textarea-ic-preview-html break-words text-grey7 text-sm leading-5"
252
205
  class:text-grey3={isShowingPlaceholder}
206
+ dir={dirForUserText($textDirection)}
253
207
  >
254
208
  {@html previewText}
255
209
  </div>
256
210
  </div>
257
- <div class="textarea-ic-expand-icon absolute top-[14px] right-[12px]">
211
+ <div class="textarea-ic-expand-icon absolute top-[14px] end-[12px]">
258
212
  <IconArrowsDiagonal
259
213
  class="w-5 h-5 text-grey5 group-hover:text-grey7 transition-colors duration-200"
260
214
  />
@@ -271,7 +225,7 @@
271
225
  {#if isShowingTextArea}
272
226
  <Portal
273
227
  target={portalTarget}
274
- class="text-area-portal absolute top-0 left-0 w-full h-full"
228
+ class="text-area-portal absolute top-0 start-0 w-full h-full"
275
229
  >
276
230
  <!-- Blocking overlay -->
277
231
  <div
@@ -280,6 +234,7 @@
280
234
  ></div>
281
235
  <!-- svelte-ignore a11y_no_static_element_interactions -->
282
236
  <div
237
+ bind:this={portalContentRef}
283
238
  class="text-area-portal-content absolute mt-4 mx-6 mb-6 inset-0 {isClosing ? 'scale-out-animation' : 'scale-in-animation'}"
284
239
  style="z-index: 51; transform: translateZ(0); backface-visibility: hidden;"
285
240
  on:mouseenter={handlePortalMouseEnter}
@@ -16,6 +16,8 @@
16
16
  <script lang="ts">
17
17
  // Import Shadcn UI components from Widgetic design-system package
18
18
  import { Input } from '@widgetic/design-system/components';
19
+ import { useTextDirection } from '@widgetic/design-system/components/textDirection';
20
+ import { directionOfFieldValue } from '../../utils/fieldDirection';
19
21
  import { InputControllerHeader } from './index';
20
22
 
21
23
  // Component props
@@ -35,6 +37,18 @@
35
37
  export let description: TextInputControllerProps['description'] = undefined;
36
38
  export let onFocus: TextInputControllerProps['onFocus'] = () => {};
37
39
 
40
+ // The way the screen is read. What a visitor types reads by its own first letter on a right to left screen; an address
41
+ // (an email, a link) is read left to right on every screen. A left to right screen is written no attribute, as it always was.
42
+ // (The `type` is the name of the kind of field in the schema, `textInput` for a plain text field: see fieldDirection.ts.)
43
+ const textDirection = useTextDirection();
44
+ // What is in the field right now: the value the page gave it, then what is typed (only an empty field has no first letter).
45
+ let typedText = '';
46
+ $: typedText = value ?? '';
47
+ $: hasTypedValue = typedText.length > 0;
48
+ let valueDirection: 'auto' | 'ltr' | undefined;
49
+ // (An empty field takes the direction of the screen, so that its hint and its caret start at the start of the line.)
50
+ $: valueDirection = directionOfFieldValue(type, $textDirection, hasTypedValue);
51
+
38
52
  /**
39
53
  * Methods
40
54
  */
@@ -56,7 +70,7 @@
56
70
  <InputControllerHeader {label} />
57
71
  </div>
58
72
 
59
- <div class="absolute right-0">
73
+ <div class="absolute end-0">
60
74
  <div
61
75
  class="h-[48px] w-[185px] rounded-large border border-grey2 flex items-center bg-grey1 transition-colors duration-200 hover:border-forest-green"
62
76
  on:focusin={onFocus}
@@ -66,9 +80,11 @@
66
80
  {type}
67
81
  {value}
68
82
  {placeholder}
83
+ dir={valueDirection}
69
84
  onfocus={onFocus}
70
85
  oninput={(e) => {
71
86
  const newValue = e.currentTarget.value;
87
+ typedText = newValue;
72
88
  if (validate(newValue)) {
73
89
  onChange(newValue);
74
90
  }
@@ -29,6 +29,7 @@
29
29
  import { Portal } from '@widgetic/design-system/components';
30
30
  import InputControllerHeader from './InputControllerHeader.svelte';
31
31
  import type { InputControllerConfig } from './index';
32
+ import { isEventInside, listenForScroll } from '../../utils/shadowDom';
32
33
 
33
34
  export let label: TimeInputControllerProps['label'];
34
35
  export let value: TimeInputControllerProps['value'] = "00:00";
@@ -239,6 +240,7 @@
239
240
  if (dropdownContainer && timeButton) {
240
241
  const rect = timeButton.getBoundingClientRect();
241
242
  dropdownContainer.style.top = `${rect.bottom + 4}px`;
243
+ // rtl-ok: the list is as wide as the button it opens from, so the left edge of the button is both of its edges
242
244
  dropdownContainer.style.left = `${rect.left}px`;
243
245
  isDropdownPositioned = true;
244
246
  }
@@ -256,7 +258,9 @@
256
258
  }
257
259
 
258
260
  function handleClickOutside(event: MouseEvent) {
259
- if (dropdownContainer && !dropdownContainer.contains(event.target as Node)) {
261
+ // Seen from the document, a click inside a shadow root has the module's host as its target, so the click's path
262
+ // is what says whether it was inside the dropdown
263
+ if (dropdownContainer && !isEventInside(event, dropdownContainer)) {
260
264
  closeDropdown();
261
265
  }
262
266
  }
@@ -290,11 +294,14 @@
290
294
  }
291
295
  }
292
296
 
297
+ // Stops the scroll listener (it listens to the page and, in a module, to the module's own panels)
298
+ let stopListeningForScroll: () => void = () => {};
299
+
293
300
  // Update onMount to add scroll listener with capture phase
294
301
  onMount(() => {
295
302
  parseTimeFromValue(value);
296
303
  updateValue();
297
- window.addEventListener('scroll', handleScroll, { capture: true });
304
+ stopListeningForScroll = listenForScroll(clockContainer, handleScroll);
298
305
  });
299
306
 
300
307
  // Update onDestroy to remove scroll listener
@@ -302,7 +309,7 @@
302
309
  document.removeEventListener('mousemove', handleMouseMove);
303
310
  document.removeEventListener('mouseup', handleMouseUp);
304
311
  document.removeEventListener('click', handleClickOutside);
305
- window.removeEventListener('scroll', handleScroll, true);
312
+ stopListeningForScroll();
306
313
  });
307
314
 
308
315
  // Watch for external value changes
@@ -333,7 +340,7 @@
333
340
  <InputControllerHeader {label} {helpText} />
334
341
  </div>
335
342
 
336
- <div class="absolute right-0 h-[48px] flex items-center">
343
+ <div class="absolute end-0 h-[48px] flex items-center">
337
344
  <div class="flex items-center justify-end gap-2">
338
345
  <!-- Clock Face -->
339
346
  <!-- svelte-ignore a11y_no_static_element_interactions -->