@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.
- package/README.md +242 -0
- package/dist/components/WidgetPreview.svelte +123 -86
- package/dist/components/WidgetPreview.svelte.d.ts +8 -0
- package/dist/components/color-select/ColorSelect.svelte +4 -4
- package/dist/components/editors/CompositionEditorApp.svelte +562 -0
- package/dist/components/editors/CompositionEditorApp.svelte.d.ts +95 -0
- package/dist/components/editors/EmbedEditor.svelte +33 -22
- package/dist/components/editors/EmbedEditor.svelte.d.ts +1 -1
- package/dist/components/editors/PropsEditor.svelte +267 -850
- package/dist/components/editors/PropsEditor.svelte.d.ts +10 -3
- package/dist/components/ic/AngleInputController.svelte +1 -1
- package/dist/components/ic/BrowserInputController.svelte +58 -31
- package/dist/components/ic/ColorPickerInputController.svelte +55 -21
- package/dist/components/ic/DateFieldInputController.svelte +34 -14
- package/dist/components/ic/DatePickerInputController.svelte +37 -21
- package/dist/components/ic/DropdownInputController.svelte +21 -9
- package/dist/components/ic/DropdownInputController.svelte.d.ts +1 -1
- package/dist/components/ic/FixedPositionInputController.svelte +12 -6
- package/dist/components/ic/FontInputController.svelte +100 -76
- package/dist/components/ic/InputControllerHeader.svelte +16 -9
- package/dist/components/ic/NumberControls.svelte +4 -4
- package/dist/components/ic/NumberInputController.svelte +6 -4
- package/dist/components/ic/PositionInputController.svelte +23 -15
- package/dist/components/ic/RangeCalendarInputController.svelte +36 -20
- package/dist/components/ic/RoundedCornersInputController.svelte +39 -26
- package/dist/components/ic/SliderInputController.svelte +7 -2
- package/dist/components/ic/SwitchInputController.svelte +12 -3
- package/dist/components/ic/TagsInputController.svelte +19 -4
- package/dist/components/ic/TextAreaInputController.svelte +79 -51
- package/dist/components/ic/TextAreaPreviewInputController.svelte +27 -72
- package/dist/components/ic/TextInputController.svelte +17 -1
- package/dist/components/ic/TimeInputController.svelte +11 -4
- package/dist/components/ic/VectorInputController.svelte +101 -70
- package/dist/components/ic/shared/ItemGrid.svelte +12 -4
- package/dist/components/ic/shared/RecentItemsSection.svelte +16 -11
- package/dist/components/ic/shared/RecentItemsSection.svelte.d.ts +1 -1
- package/dist/composition/compositionApi.d.ts +95 -0
- package/dist/composition/compositionApi.js +289 -0
- package/dist/composition/compositionEditorController.d.ts +263 -0
- package/dist/composition/compositionEditorController.js +746 -0
- package/dist/composition/compositionHelpers.d.ts +74 -0
- package/dist/composition/compositionHelpers.js +405 -0
- package/dist/i18n/index.d.ts +4 -0
- package/dist/i18n/index.js +4 -0
- package/dist/i18n/labels.d.ts +15 -0
- package/dist/i18n/labels.js +57 -0
- package/dist/i18n/language.d.ts +228 -0
- package/dist/i18n/language.js +34 -0
- package/dist/i18n/languageKit.d.ts +54 -0
- package/dist/i18n/languageKit.js +102 -0
- package/dist/i18n/messages.d.ts +241 -0
- package/dist/i18n/messages.js +253 -0
- package/dist/i18n/translate.d.ts +6 -0
- package/dist/i18n/translate.js +8 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +8 -0
- package/dist/mediaLibraryContext.d.ts +10 -0
- package/dist/mediaLibraryContext.js +30 -0
- package/dist/utils/FontManager.d.ts +21 -5
- package/dist/utils/FontManager.js +62 -67
- package/dist/utils/VectorManager.d.ts +0 -1
- package/dist/utils/VectorManager.js +10 -12
- package/dist/utils/dockPlacement.d.ts +12 -0
- package/dist/utils/dockPlacement.js +13 -0
- package/dist/utils/editorStorage.d.ts +23 -0
- package/dist/utils/editorStorage.js +95 -0
- package/dist/utils/fieldDirection.d.ts +13 -0
- package/dist/utils/fieldDirection.js +25 -0
- package/dist/utils/fontUtils.js +5 -3
- package/dist/utils/htmlSafety.d.ts +21 -0
- package/dist/utils/htmlSafety.js +38 -0
- package/dist/utils/itemReorder.d.ts +90 -0
- package/dist/utils/itemReorder.js +354 -0
- package/dist/utils/recentColors.js +4 -2
- package/dist/utils/richTextPreview.d.ts +38 -0
- package/dist/utils/richTextPreview.js +207 -0
- package/dist/utils/scrollActivity.d.ts +14 -0
- package/dist/utils/scrollActivity.js +63 -0
- package/dist/utils/shadowDom.d.ts +67 -0
- package/dist/utils/shadowDom.js +211 -0
- package/dist/utils/vectorUtils.js +5 -3
- package/dist/utils/visibilityCondition.d.ts +40 -0
- package/dist/utils/visibilityCondition.js +535 -0
- 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
|
-
|
|
134
|
-
|
|
135
|
-
{ value: '
|
|
136
|
-
{ value: '
|
|
137
|
-
{ value: '
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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('
|
|
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
|
-
|
|
847
|
-
|
|
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
|
-
|
|
945
|
-
|
|
946
|
-
const
|
|
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
|
-
|
|
1059
|
+
stopListeningForScroll = listenForScroll(toolbarRef, handleScroll);
|
|
1035
1060
|
});
|
|
1036
1061
|
|
|
1037
1062
|
onDestroy(() => {
|
|
1038
|
-
|
|
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
|
|
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=
|
|
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
|
|
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={
|
|
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">{
|
|
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.
|
|
1254
|
+
<span>{$t(option.messageKey)}</span>
|
|
1230
1255
|
</div>
|
|
1231
1256
|
{:else}
|
|
1232
|
-
<span>{option.
|
|
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.
|
|
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
|
|
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-
|
|
1463
|
+
margin-inline-end: 2px;
|
|
1437
1464
|
}
|
|
1438
1465
|
|
|
1439
1466
|
:global(.ProseMirror::-webkit-scrollbar-track) {
|
|
1440
1467
|
background-color: transparent;
|
|
1441
|
-
margin-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
1496
|
-
margin-
|
|
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
|
|
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-
|
|
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-
|
|
1807
|
-
margin-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
1906
|
-
padding-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
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]
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 -->
|