@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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ContentSchemaView, DesignSchemaView } from '../../schemaViews.js';
|
|
2
|
+
import type { MediaLibraryProvider } from '@widgetic/file-browser';
|
|
2
3
|
/**
|
|
3
4
|
* STYLES from Widgetic Design System
|
|
4
5
|
* Note: props-editor.css global rules (body, *) moved to app.css (standalone only).
|
|
@@ -28,8 +29,8 @@ declare const PropsEditor: $$__sveltets_2_IsomorphicComponent<{
|
|
|
28
29
|
designSchema?: DesignSchemaView | null;
|
|
29
30
|
contentSchema?: ContentSchemaView | undefined;
|
|
30
31
|
itemTemplate?: any;
|
|
31
|
-
designTabTitle?: string;
|
|
32
|
-
contentTabTitle?: string;
|
|
32
|
+
designTabTitle?: string | undefined;
|
|
33
|
+
contentTabTitle?: string | undefined;
|
|
33
34
|
defaultTab?: "design" | "content";
|
|
34
35
|
defaultOpen?: boolean;
|
|
35
36
|
showDragHandle?: boolean;
|
|
@@ -40,11 +41,17 @@ declare const PropsEditor: $$__sveltets_2_IsomorphicComponent<{
|
|
|
40
41
|
/** Optional content below the shared toolbar, before property fields. */ headerContent?: any;
|
|
41
42
|
headerClass?: string;
|
|
42
43
|
dragHandleClass?: string;
|
|
43
|
-
title?: string;
|
|
44
|
+
title?: string | undefined;
|
|
44
45
|
/** Hide the default missing-schema message for header-only editors. */ showSchemaError?: boolean;
|
|
45
46
|
/** Optional layout wrapper moved by this editor's built-in drag and position API. */ positionTarget?: any;
|
|
46
47
|
initialDesignValues?: Record<string, any>;
|
|
47
48
|
initialContentValues?: Record<string, any>;
|
|
49
|
+
/**
|
|
50
|
+
* Where the file controls (images, video, audio, documents) get their files from. Without it they show the
|
|
51
|
+
* Widgetic account's uploads, as they always have (configured with `setFileBrowserUploadConfig`). With a
|
|
52
|
+
* `MediaLibraryProvider` they list, upload, rename and delete through its functions instead, so a partner's own
|
|
53
|
+
* storage (a CMS, an S3 bucket, a DAM) can be used with no Widgetic storage at all.
|
|
54
|
+
*/ mediaLibrary?: MediaLibraryProvider | undefined;
|
|
48
55
|
updateDesignValue?: (propertyId: string, value: any) => void;
|
|
49
56
|
setPosition?: (left: number, top: number) => void;
|
|
50
57
|
getPosition?: () => {
|
|
@@ -234,7 +234,7 @@
|
|
|
234
234
|
<InputControllerHeader {label} {helpText} />
|
|
235
235
|
</div>
|
|
236
236
|
|
|
237
|
-
<div class="absolute
|
|
237
|
+
<div class="absolute end-0 h-[48px] flex items-center">
|
|
238
238
|
<div class="flex items-center justify-end gap-2">
|
|
239
239
|
<!-- Circle Indicator -->
|
|
240
240
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
@@ -35,12 +35,17 @@
|
|
|
35
35
|
import { Portal, Tooltip } from '@widgetic/design-system/components';
|
|
36
36
|
import { ThumbHashImage } from "@widgetic/design-system/components";
|
|
37
37
|
import { FileBrowser, setFileBrowserUploadConfig, persistentSelectStore } from '@widgetic/file-browser';
|
|
38
|
+
import { useEditorMediaLibrary } from '../../mediaLibraryContext';
|
|
39
|
+
import { useEditorLanguage } from '../../i18n/language';
|
|
40
|
+
import type { EditorMessageKey } from '../../i18n/messages';
|
|
41
|
+
import type { Translate } from '@widgetic/design-system/i18n';
|
|
38
42
|
import X from '../icons/x.svelte';
|
|
39
43
|
// console.log("BrowserInputController - Tooltip:", Tooltip);
|
|
40
44
|
|
|
41
45
|
|
|
42
46
|
// utils imports
|
|
43
47
|
import MediaManager from '../../utils/MediaManager';
|
|
48
|
+
import { queryInTreeOf } from '../../utils/shadowDom';
|
|
44
49
|
|
|
45
50
|
// Static asset imports (bundled via Vite, works when imported as library)
|
|
46
51
|
import icAudio from '../../assets/icons/ic-browser/audio.svg?url';
|
|
@@ -65,10 +70,20 @@
|
|
|
65
70
|
(portalTarget instanceof HTMLElement &&
|
|
66
71
|
portalTarget.id === 'props-editor-file-browser-portal');
|
|
67
72
|
export let config: BrowserInputControllerProps['config'] = {};
|
|
73
|
+
|
|
74
|
+
// The media library the editor above was given (a partner's own storage, for instance), read through a getter
|
|
75
|
+
// because it reaches this component through context.
|
|
76
|
+
const getEditorMediaLibrary = useEditorMediaLibrary();
|
|
77
|
+
|
|
78
|
+
// The words of the editor in the language of the page, and the share of them that belongs to the File Browser inside it.
|
|
79
|
+
const { t, fileBrowser: fileBrowserLanguage } = useEditorLanguage();
|
|
68
80
|
|
|
69
81
|
// Extract allowed file types from config or default to all files
|
|
70
82
|
const fileTypes = config.fileTypes || ['*'];
|
|
71
83
|
|
|
84
|
+
// The control's own element, to find the editor's panel from (see getPortalHost)
|
|
85
|
+
let controllerElement: HTMLDivElement | undefined;
|
|
86
|
+
|
|
72
87
|
// Media element references for audio/video playback
|
|
73
88
|
let audioElement: HTMLAudioElement | null = null;
|
|
74
89
|
let videoElement: HTMLVideoElement | null = null;
|
|
@@ -166,6 +181,9 @@
|
|
|
166
181
|
/** Only embedded iframe mode (URL params) sets upload config here; standalone uses parent +page sync. */
|
|
167
182
|
function syncFileBrowserUploadConfigIfEmbedded() {
|
|
168
183
|
if (typeof window === 'undefined') return;
|
|
184
|
+
// With a media library the File Browser takes its files from it and never reads the upload configuration, and the
|
|
185
|
+
// address of the page this editor sits in (which can be a partner's) is none of the editor's business.
|
|
186
|
+
if (getEditorMediaLibrary()) return;
|
|
169
187
|
const params = new URLSearchParams(window.location.search);
|
|
170
188
|
const apiUrl = params.get('apiUrl');
|
|
171
189
|
const token = params.get('token');
|
|
@@ -187,7 +205,9 @@
|
|
|
187
205
|
function getPortalHost(): HTMLElement | null {
|
|
188
206
|
if (portalTarget instanceof HTMLElement) return portalTarget;
|
|
189
207
|
if (typeof document === 'undefined') return null;
|
|
190
|
-
|
|
208
|
+
// Looked for in the tree this control is in: inside a module that is a shadow root, which `document` cannot see into
|
|
209
|
+
const portalHost = queryInTreeOf(controllerElement, '#props-editor-file-browser-portal');
|
|
210
|
+
return portalHost instanceof HTMLElement ? portalHost : null;
|
|
191
211
|
}
|
|
192
212
|
|
|
193
213
|
function setPortalHostInteractive(active: boolean) {
|
|
@@ -377,15 +397,15 @@
|
|
|
377
397
|
$: if (videoElement) setupMediaElement(videoElement);
|
|
378
398
|
|
|
379
399
|
// Add placeholder text logic
|
|
380
|
-
function getAddToEditorLabelSingular(fileTypes: string[]): string {
|
|
381
|
-
if (!fileTypes || fileTypes.includes('*')) return '
|
|
400
|
+
function getAddToEditorLabelSingular(fileTypes: string[], translate: Translate<EditorMessageKey>): string {
|
|
401
|
+
if (!fileTypes || fileTypes.includes('*')) return translate('editor.file.useFile');
|
|
382
402
|
const type = fileTypes[0].split('/')[0];
|
|
383
403
|
switch (type) {
|
|
384
|
-
case 'image': return '
|
|
385
|
-
case 'video': return '
|
|
386
|
-
case 'audio': return '
|
|
387
|
-
case 'document': return '
|
|
388
|
-
default: return '
|
|
404
|
+
case 'image': return translate('editor.file.useImage');
|
|
405
|
+
case 'video': return translate('editor.file.useVideo');
|
|
406
|
+
case 'audio': return translate('editor.file.useAudio');
|
|
407
|
+
case 'document': return translate('editor.file.useDocument');
|
|
408
|
+
default: return translate('editor.file.useFile');
|
|
389
409
|
}
|
|
390
410
|
}
|
|
391
411
|
|
|
@@ -405,15 +425,15 @@
|
|
|
405
425
|
}
|
|
406
426
|
}
|
|
407
427
|
|
|
408
|
-
function getPlaceholderText(fileTypes: string[]): string {
|
|
409
|
-
if (!fileTypes || fileTypes.includes('*')) return '
|
|
428
|
+
function getPlaceholderText(fileTypes: string[], translate: Translate<EditorMessageKey>): string {
|
|
429
|
+
if (!fileTypes || fileTypes.includes('*')) return translate('editor.file.addFile');
|
|
410
430
|
const type = fileTypes[0].split('/')[0];
|
|
411
431
|
switch (type) {
|
|
412
|
-
case 'image': return '
|
|
413
|
-
case 'video': return '
|
|
414
|
-
case 'audio': return '
|
|
415
|
-
case 'document': return '
|
|
416
|
-
default: return '
|
|
432
|
+
case 'image': return translate('editor.file.addImage');
|
|
433
|
+
case 'video': return translate('editor.file.addVideo');
|
|
434
|
+
case 'audio': return translate('editor.file.addAudio');
|
|
435
|
+
case 'document': return translate('editor.file.addDocument');
|
|
436
|
+
default: return translate('editor.file.addFile');
|
|
417
437
|
}
|
|
418
438
|
}
|
|
419
439
|
|
|
@@ -429,7 +449,7 @@
|
|
|
429
449
|
|
|
430
450
|
$: fileBrowserFilter = getInitialFileBrowserFilter(fileTypes);
|
|
431
451
|
$: fileBrowserRestrictUpload = fileBrowserFilter !== 'all';
|
|
432
|
-
$: fileBrowserAddToEditorLabelSingular = getAddToEditorLabelSingular(fileTypes);
|
|
452
|
+
$: fileBrowserAddToEditorLabelSingular = getAddToEditorLabelSingular(fileTypes, $t);
|
|
433
453
|
|
|
434
454
|
function handleContentRemoval(e: MouseEvent) {
|
|
435
455
|
e.stopPropagation();
|
|
@@ -461,13 +481,13 @@
|
|
|
461
481
|
</script>
|
|
462
482
|
|
|
463
483
|
<!-- Main container with updated styling -->
|
|
464
|
-
<div class="flex flex-col gap-2 widgetic-colors">
|
|
484
|
+
<div bind:this={controllerElement} class="flex flex-col gap-2 widgetic-colors">
|
|
465
485
|
<div class="flex items-center w-full relative">
|
|
466
486
|
<div class="flex-1 min-w-0">
|
|
467
487
|
<InputControllerHeader label={header?.label || label} helpText={header?.helpText} />
|
|
468
488
|
</div>
|
|
469
489
|
|
|
470
|
-
<div class="absolute
|
|
490
|
+
<div class="absolute end-0">
|
|
471
491
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
472
492
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
473
493
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
@@ -482,12 +502,12 @@
|
|
|
482
502
|
<!-- File name or placeholder -->
|
|
483
503
|
<div class="browser-input-controller-label flex-1 min-w-0 px-4 flex items-center h-full">
|
|
484
504
|
<span class="subtext {value ? 'text-grey7' : 'text-grey3'} truncate inline-block">
|
|
485
|
-
{value ? getFileName(value) : getPlaceholderText(fileTypes)}
|
|
505
|
+
{value ? getFileName(value) : getPlaceholderText(fileTypes, $t)}
|
|
486
506
|
</span>
|
|
487
507
|
</div>
|
|
488
508
|
|
|
489
509
|
<!-- Preview area -->
|
|
490
|
-
<div class="flex-none h-full flex items-center
|
|
510
|
+
<div class="flex-none h-full flex items-center pe-[16px]">
|
|
491
511
|
{#if isChecking}
|
|
492
512
|
<!-- Loading state -->
|
|
493
513
|
<div class="w-8 h-7 bg-gray-100 rounded-lg"></div>
|
|
@@ -564,7 +584,7 @@
|
|
|
564
584
|
>
|
|
565
585
|
<img
|
|
566
586
|
src={isPlaying ? icPlayerPause : icPlayerPlay}
|
|
567
|
-
alt={isPlaying ?
|
|
587
|
+
alt={isPlaying ? $t('editor.file.pause') : $t('editor.file.play')}
|
|
568
588
|
class="w-5 h-5 {isPlaying ? 'opacity-0 hover:opacity-100' : 'opacity-100'} transition-opacity"
|
|
569
589
|
/>
|
|
570
590
|
</div>
|
|
@@ -600,7 +620,7 @@
|
|
|
600
620
|
>
|
|
601
621
|
<img
|
|
602
622
|
src={isPlaying ? icPlayerPause : icPlayerPlay}
|
|
603
|
-
alt={isPlaying ?
|
|
623
|
+
alt={isPlaying ? $t('editor.file.pause') : $t('editor.file.play')}
|
|
604
624
|
class="w-5 h-5"
|
|
605
625
|
/>
|
|
606
626
|
</div>
|
|
@@ -629,14 +649,14 @@
|
|
|
629
649
|
<!-- Error state -->
|
|
630
650
|
<img
|
|
631
651
|
src={icError}
|
|
632
|
-
alt=
|
|
652
|
+
alt={$t('editor.file.notFound')}
|
|
633
653
|
class="w-8 h-7 object-contain"
|
|
634
654
|
/>
|
|
635
655
|
{:else}
|
|
636
656
|
<!-- Empty state -->
|
|
637
657
|
<img
|
|
638
658
|
src={icFolder}
|
|
639
|
-
alt=
|
|
659
|
+
alt={$t('editor.file.emptyFolder')}
|
|
640
660
|
class="w-8 h-7 object-contain"
|
|
641
661
|
/>
|
|
642
662
|
{/if}
|
|
@@ -645,9 +665,9 @@
|
|
|
645
665
|
{#if value}
|
|
646
666
|
<button
|
|
647
667
|
type="button"
|
|
648
|
-
class="browser-ic-clear-btn absolute top-[-4px]
|
|
668
|
+
class="browser-ic-clear-btn absolute top-[-4px] end-[-4px] w-4 h-4 rounded-full hover:bg-grey2 transition-colors flex items-center justify-center"
|
|
649
669
|
on:click={handleContentRemoval}
|
|
650
|
-
aria-label=
|
|
670
|
+
aria-label={$t('editor.file.clear')}
|
|
651
671
|
>
|
|
652
672
|
<div class="w-4 h-4 flex items-center justify-center">
|
|
653
673
|
<X class="w-4! h-4!" />
|
|
@@ -685,31 +705,38 @@
|
|
|
685
705
|
class:rounded-large={!fillEditorPanel}
|
|
686
706
|
class:shadow-2xl={!fillEditorPanel}
|
|
687
707
|
>
|
|
688
|
-
<div class="browser-input-file-browser-header relative flex h-[48px] shrink-0 items-center border-b border-grey2
|
|
689
|
-
<span class="body-medium text-grey7">
|
|
708
|
+
<div class="browser-input-file-browser-header relative flex h-[48px] shrink-0 items-center border-b border-grey2 ps-4 pe-8">
|
|
709
|
+
<span class="body-medium text-grey7">{$t('editor.file.dialogTitle')}</span>
|
|
690
710
|
<button
|
|
691
711
|
type="button"
|
|
692
712
|
class="browser-input-file-browser-close inline-flex h-6 w-6 items-center justify-center p-0"
|
|
693
|
-
style="position:absolute;top:4px;
|
|
713
|
+
style="position:absolute;top:4px;inset-inline-end:4px;"
|
|
694
714
|
on:click={closeFileBrowser}
|
|
695
|
-
aria-label=
|
|
715
|
+
aria-label={$t('editor.file.closeDialog')}
|
|
696
716
|
>
|
|
697
717
|
<X class="h-6! w-6!" />
|
|
698
718
|
</button>
|
|
699
719
|
</div>
|
|
700
720
|
|
|
701
721
|
<div class="browser-input-file-browser-body flex min-h-0 flex-1 flex-col overflow-hidden">
|
|
722
|
+
<!-- With a media library the File Browser keeps nothing about this visit (the filter it was left on, the
|
|
723
|
+
folder it was left in) in the page's localStorage: that storage belongs to the page this editor sits in,
|
|
724
|
+
which can be a partner's. Without one (the Widgetic site) it remembers them, as it always did. -->
|
|
702
725
|
<FileBrowser
|
|
703
726
|
class="h-full min-h-0 flex-1"
|
|
704
727
|
instanceId={`props-editor-browser-input-${fileBrowserFilter}`}
|
|
705
728
|
initialFilter={fileBrowserFilter}
|
|
706
729
|
restrictUpload={fileBrowserRestrictUpload}
|
|
707
730
|
useDemoContent={false}
|
|
731
|
+
mediaLibrary={getEditorMediaLibrary()}
|
|
732
|
+
persistHistory={!getEditorMediaLibrary()}
|
|
708
733
|
allowSelecting={true}
|
|
709
734
|
showAddToEditor={true}
|
|
710
735
|
maxSelections={1}
|
|
711
736
|
activeFileId={getCurrentFileId()}
|
|
712
737
|
addToEditorLabel={fileBrowserAddToEditorLabelSingular}
|
|
738
|
+
locale={$fileBrowserLanguage.locale ?? undefined}
|
|
739
|
+
messages={$fileBrowserLanguage.messages ?? null}
|
|
713
740
|
on:select={handleFileBrowserSelect}
|
|
714
741
|
/>
|
|
715
742
|
</div>
|
|
@@ -722,7 +749,7 @@
|
|
|
722
749
|
.browser-input-file-browser-close {
|
|
723
750
|
position: absolute;
|
|
724
751
|
top: 4px;
|
|
725
|
-
|
|
752
|
+
inset-inline-end: 4px;
|
|
726
753
|
}
|
|
727
754
|
|
|
728
755
|
.browser-input-file-browser-dialog-animate {
|
|
@@ -69,6 +69,15 @@
|
|
|
69
69
|
import { colorNames } from '../../utils/colorNames';
|
|
70
70
|
import { getColorSuggestions, getNextSuggestion, type AutocompleteResult } from '../../utils/colorAutocomplete';
|
|
71
71
|
import { recentColorsUtil } from '../../utils/recentColors';
|
|
72
|
+
import { listenForScroll, lockPointerStyle } from '../../utils/shadowDom';
|
|
73
|
+
import { useEditorLanguage } from '../../i18n/language';
|
|
74
|
+
import { useTextDirection } from '@widgetic/design-system/components/textDirection';
|
|
75
|
+
|
|
76
|
+
// The words of the editor in the language of the page.
|
|
77
|
+
const { t } = useEditorLanguage();
|
|
78
|
+
|
|
79
|
+
// The way the screen is read: the pop-up opens from the end of the field, which is its left edge on a right to left screen.
|
|
80
|
+
const textDirection = useTextDirection();
|
|
72
81
|
|
|
73
82
|
|
|
74
83
|
|
|
@@ -544,8 +553,10 @@
|
|
|
544
553
|
* @param event - Scroll event
|
|
545
554
|
*/
|
|
546
555
|
function handleScroll(event: Event) {
|
|
547
|
-
// Don't close if scrolling happens within allowed elements
|
|
548
|
-
|
|
556
|
+
// Don't close if scrolling happens within allowed elements. The page itself scrolling has the document as its
|
|
557
|
+
// target (it has no closest()), so only an element can be one of the allowed ones.
|
|
558
|
+
const scrollTarget = event.target;
|
|
559
|
+
if (scrollTarget instanceof Element && scrollTarget.closest('[data-allow-scroll="true"]')) {
|
|
549
560
|
return;
|
|
550
561
|
}
|
|
551
562
|
|
|
@@ -557,6 +568,9 @@
|
|
|
557
568
|
}
|
|
558
569
|
}
|
|
559
570
|
|
|
571
|
+
// Puts the pointer back after an opacity drag, however the drag ends (the picker can be removed in the middle of one).
|
|
572
|
+
let releaseOpacityPointerStyle: (() => void) | null = null;
|
|
573
|
+
|
|
560
574
|
/**
|
|
561
575
|
* Manages opacity slider interactions
|
|
562
576
|
* - Handles mouse drag events
|
|
@@ -567,7 +581,12 @@
|
|
|
567
581
|
*/
|
|
568
582
|
function handleOpacityMouseDown(event: MouseEvent) {
|
|
569
583
|
isDragging = true;
|
|
570
|
-
|
|
584
|
+
// The cursor is shown on the host of the shadow root the picker is in, and on <body> only on a normal page: the
|
|
585
|
+
// <body> of a customer's page is not the editor's to change.
|
|
586
|
+
releaseOpacityPointerStyle?.();
|
|
587
|
+
releaseOpacityPointerStyle = lockPointerStyle(event.currentTarget ?? event.target, 'grabbing', {
|
|
588
|
+
blockTextSelection: false
|
|
589
|
+
});
|
|
571
590
|
updateOpacity(event);
|
|
572
591
|
|
|
573
592
|
const handleMouseMove = (e: MouseEvent) => {
|
|
@@ -578,7 +597,8 @@
|
|
|
578
597
|
|
|
579
598
|
const handleMouseUp = () => {
|
|
580
599
|
isDragging = false;
|
|
581
|
-
|
|
600
|
+
releaseOpacityPointerStyle?.();
|
|
601
|
+
releaseOpacityPointerStyle = null;
|
|
582
602
|
window.removeEventListener('mousemove', handleMouseMove);
|
|
583
603
|
window.removeEventListener('mouseup', handleMouseUp);
|
|
584
604
|
};
|
|
@@ -947,6 +967,10 @@
|
|
|
947
967
|
}
|
|
948
968
|
}
|
|
949
969
|
|
|
970
|
+
// Stops listening for scrolling (the page and, inside a module, the module's own panels: a scroll never leaves a
|
|
971
|
+
// shadow root, so a listener on the window alone does not hear about those)
|
|
972
|
+
let stopListeningForScroll: () => void = () => {};
|
|
973
|
+
|
|
950
974
|
// Sets up event listeners when the component mounts
|
|
951
975
|
onMount(() => {
|
|
952
976
|
initializeColor();
|
|
@@ -954,17 +978,19 @@
|
|
|
954
978
|
window.addEventListener('mousemove', handleGlobalMouseMove);
|
|
955
979
|
window.addEventListener('keydown', handlePopupKeyDown);
|
|
956
980
|
window.addEventListener('keyup', handlePopupKeyUp);
|
|
957
|
-
|
|
981
|
+
stopListeningForScroll = listenForScroll(inputRef, handleScroll);
|
|
958
982
|
window.addEventListener('widgetic:close-color-pickers', onCloseOtherPickers as EventListener);
|
|
959
983
|
});
|
|
960
984
|
|
|
961
985
|
// Cleans up event listeners when the component is destroyed
|
|
962
986
|
onDestroy(() => {
|
|
987
|
+
releaseOpacityPointerStyle?.();
|
|
988
|
+
releaseOpacityPointerStyle = null;
|
|
963
989
|
window.removeEventListener('mouseup', handleGlobalMouseUp);
|
|
964
990
|
window.removeEventListener('mousemove', handleGlobalMouseMove);
|
|
965
991
|
window.removeEventListener('keydown', handlePopupKeyDown);
|
|
966
992
|
window.removeEventListener('keyup', handlePopupKeyUp);
|
|
967
|
-
|
|
993
|
+
stopListeningForScroll();
|
|
968
994
|
window.removeEventListener('widgetic:close-color-pickers', onCloseOtherPickers as EventListener);
|
|
969
995
|
});
|
|
970
996
|
|
|
@@ -1172,11 +1198,16 @@
|
|
|
1172
1198
|
const top = openAbove
|
|
1173
1199
|
? Math.round(rect.top - popoverHeight - 4)
|
|
1174
1200
|
: Math.round(rect.bottom + 4);
|
|
1175
|
-
// Align popup's
|
|
1201
|
+
// Align the popup's end edge with the end edge of the full input container: the right edge on a left to right screen,
|
|
1202
|
+
// the left edge on a right to left one (the popup itself is a drawing, drawn left to right, and opens toward the inside)
|
|
1176
1203
|
const inputContainer = triggerRef.closest('.color-input-main-container') as HTMLElement | null;
|
|
1177
|
-
const
|
|
1178
|
-
|
|
1179
|
-
|
|
1204
|
+
const containerRect = inputContainer ? inputContainer.getBoundingClientRect() : rect;
|
|
1205
|
+
if ($textDirection === 'rtl') {
|
|
1206
|
+
popoverStyle = `top: ${top}px; left: ${Math.round(containerRect.left)}px;`; // rtl-ok: the end edge of a right to left field is its left edge
|
|
1207
|
+
} else {
|
|
1208
|
+
const right = Math.round(window.innerWidth - containerRect.right);
|
|
1209
|
+
popoverStyle = `top: ${top}px; right: ${right}px;`; // rtl-ok: the end edge of a left to right field is its right edge
|
|
1210
|
+
}
|
|
1180
1211
|
}
|
|
1181
1212
|
|
|
1182
1213
|
// Position is calculated in handleToggle after tick()
|
|
@@ -1225,17 +1256,19 @@
|
|
|
1225
1256
|
</div>
|
|
1226
1257
|
|
|
1227
1258
|
<!-- Input Controller Container -->
|
|
1228
|
-
<div class="input-controller-ct absolute
|
|
1259
|
+
<div class="input-controller-ct absolute end-0">
|
|
1229
1260
|
<div class="color-input-main-container rounded-large flex items-center transition-colors duration-200"
|
|
1230
1261
|
style="height: 48px; width: 185px; background: hsl(var(--grey1)); {showRing ? 'border: 1px solid hsl(var(--grey2));' : ''}">
|
|
1231
|
-
<div class="flex items-center w-full justify-between" style="padding-
|
|
1262
|
+
<div class="flex items-center w-full justify-between" style="padding-inline-end: 12px;">
|
|
1232
1263
|
|
|
1233
1264
|
<!-- Hex Input Container -->
|
|
1234
1265
|
<div class="hex-input-container flex-1" bind:this={inputRef}>
|
|
1266
|
+
<!-- A colour code (#FF00AA) is read left to right on every screen: in a right to left line its "#" would end up at the wrong end -->
|
|
1235
1267
|
<Input
|
|
1236
1268
|
bind:this={inputComponent}
|
|
1237
1269
|
type="text"
|
|
1238
|
-
|
|
1270
|
+
dir="ltr"
|
|
1271
|
+
class="h-9 border-0 bg-transparent subtext text-grey7 placeholder:text-grey3 focus-visible:ring-0 focus-visible:ring-offset-0 ps-4"
|
|
1239
1272
|
value={hexInput}
|
|
1240
1273
|
onfocus={(e) => {
|
|
1241
1274
|
dispatch('focus', e);
|
|
@@ -1299,7 +1332,7 @@
|
|
|
1299
1332
|
z-index: 99999;
|
|
1300
1333
|
max-height: 12rem;
|
|
1301
1334
|
top: {inputRect.bottom + 4}px;
|
|
1302
|
-
left: {inputRect.left}px;
|
|
1335
|
+
left: {inputRect.left}px; /* rtl-ok: the list is as wide as the field it opens from, so its left edge is the field's */
|
|
1303
1336
|
width: {inputRect.width}px;
|
|
1304
1337
|
"
|
|
1305
1338
|
data-allow-scroll="true"
|
|
@@ -1348,7 +1381,8 @@
|
|
|
1348
1381
|
|
|
1349
1382
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
1350
1383
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
1351
|
-
<div class="color-picker-popover-content w-auto pt-2 pb-2
|
|
1384
|
+
<div class="color-picker-popover-content w-auto pt-2 pb-2 ps-2 pe-0 widgetic-colors widgetic-radius modal-shadow"
|
|
1385
|
+
dir="ltr"
|
|
1352
1386
|
style="{popoverStyle} border: 1px solid hsl(var(--grey2)); z-index: 2;"
|
|
1353
1387
|
onmousedown={(e) => e.stopPropagation()}
|
|
1354
1388
|
onclick={(e) => e.stopPropagation()}
|
|
@@ -1358,7 +1392,7 @@
|
|
|
1358
1392
|
|
|
1359
1393
|
<!-- Recent Colors Container -->
|
|
1360
1394
|
{#if saveRecentColors && (isShiftPressed || isSpacePressed)}
|
|
1361
|
-
<div class="recent-colors-container" style="width: {pickerSize + sliderWidth + 14}px; margin-
|
|
1395
|
+
<div class="recent-colors-container" style="width: {pickerSize + sliderWidth + 14}px; margin-inline-start: 10px; padding-top: 11px; padding-bottom: 8px;">
|
|
1362
1396
|
{#if recentColors.length > 0}
|
|
1363
1397
|
<div class="recent-colors-grid" style="display: grid; grid-template-columns: repeat(5, 1fr); width: 100%; gap: {swatchGap}px;">
|
|
1364
1398
|
{#each recentColors.slice(0, 5) as {color, alpha}, i}
|
|
@@ -1392,7 +1426,7 @@
|
|
|
1392
1426
|
</div>
|
|
1393
1427
|
{:else}
|
|
1394
1428
|
<div class="text-sm text-gray-500 text-center py-2">
|
|
1395
|
-
|
|
1429
|
+
{$t('editor.color.noRecent')}
|
|
1396
1430
|
</div>
|
|
1397
1431
|
{/if}
|
|
1398
1432
|
</div>
|
|
@@ -1403,7 +1437,7 @@
|
|
|
1403
1437
|
<div
|
|
1404
1438
|
class="color-picker-wrapper relative flex flex-col"
|
|
1405
1439
|
role="application"
|
|
1406
|
-
aria-label=
|
|
1440
|
+
aria-label={$t('editor.color.picker')}
|
|
1407
1441
|
onmousedown={handleDragStart}
|
|
1408
1442
|
style="padding: 0;"
|
|
1409
1443
|
>
|
|
@@ -1428,7 +1462,7 @@
|
|
|
1428
1462
|
<div
|
|
1429
1463
|
bind:this={opacitySliderRef}
|
|
1430
1464
|
class="relative cursor-pointer opacity-slider"
|
|
1431
|
-
style="width: {pickerSize}px; height: {sliderWidth}px; margin-
|
|
1465
|
+
style="width: {pickerSize}px; height: {sliderWidth}px; margin-inline-start: 10px;"
|
|
1432
1466
|
onmousedown={handleOpacityMouseDown}
|
|
1433
1467
|
>
|
|
1434
1468
|
<div class="absolute inset-0 overflow-hidden">
|
|
@@ -1449,13 +1483,13 @@
|
|
|
1449
1483
|
</div>
|
|
1450
1484
|
</div>
|
|
1451
1485
|
<!-- Opacity number input — square, matching swatch size -->
|
|
1452
|
-
<div class="opacity-number-ct flex items-center justify-center" style="width: {swatchSize + 6}px; min-width: {swatchSize + 6}px; height: {swatchSize}px; margin-
|
|
1486
|
+
<div class="opacity-number-ct flex items-center justify-center" style="width: {swatchSize + 6}px; min-width: {swatchSize + 6}px; height: {swatchSize}px; margin-inline-start: 8px;">
|
|
1453
1487
|
<Input
|
|
1454
1488
|
type="number"
|
|
1455
1489
|
min="0"
|
|
1456
1490
|
max="100"
|
|
1457
1491
|
class="w-full h-full text-center [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none ring-0 focus:ring-0 shadow-none widgetic-colors subtext"
|
|
1458
|
-
style="border: 1px solid hsl(var(--forest-green)); color: hsl(var(--grey7)); padding-
|
|
1492
|
+
style="border: 1px solid hsl(var(--forest-green)); color: hsl(var(--grey7)); padding-inline-start: 8px; padding-inline-end: 0;"
|
|
1459
1493
|
value={opacityPercentage}
|
|
1460
1494
|
oninput={(e) => {
|
|
1461
1495
|
e.stopPropagation();
|
|
@@ -29,6 +29,16 @@
|
|
|
29
29
|
<script lang="ts">
|
|
30
30
|
import { onMount, tick } from 'svelte';
|
|
31
31
|
import InputControllerHeader from './InputControllerHeader.svelte';
|
|
32
|
+
import { activeElementIn } from '../../utils/shadowDom';
|
|
33
|
+
import { useEditorLanguage } from '../../i18n/language';
|
|
34
|
+
import type { EditorMessageKey } from '../../i18n/messages';
|
|
35
|
+
import { useTextDirection } from '@widgetic/design-system/components/textDirection';
|
|
36
|
+
|
|
37
|
+
// The words of the editor in the language of the page.
|
|
38
|
+
const { t } = useEditorLanguage();
|
|
39
|
+
|
|
40
|
+
// The way the screen is read: the row of fields turns around with it, and so do the keys that move from one field to the next.
|
|
41
|
+
const textDirection = useTextDirection();
|
|
32
42
|
|
|
33
43
|
// Constants
|
|
34
44
|
const MIN_DAY = 1;
|
|
@@ -92,27 +102,30 @@
|
|
|
92
102
|
}
|
|
93
103
|
});
|
|
94
104
|
|
|
95
|
-
// Get field configuration
|
|
105
|
+
// Get field configuration. The hint in a box and its name for a screen reader are messages (`hintKey`, `labelKey`).
|
|
96
106
|
function getFieldConfig(field: 'day' | 'month' | 'year') {
|
|
97
|
-
const configs = {
|
|
107
|
+
const configs: Record<'day' | 'month' | 'year', { min: number; max: number; hintKey: EditorMessageKey; labelKey: EditorMessageKey; width: string; maxLength: number }> = {
|
|
98
108
|
day: {
|
|
99
109
|
min: MIN_DAY,
|
|
100
110
|
max: MAX_DAY,
|
|
101
|
-
|
|
111
|
+
hintKey: 'editor.dateField.dayHint',
|
|
112
|
+
labelKey: 'editor.dateField.dayLabel',
|
|
102
113
|
width: '40px',
|
|
103
114
|
maxLength: 2
|
|
104
115
|
},
|
|
105
116
|
month: {
|
|
106
117
|
min: MIN_MONTH,
|
|
107
118
|
max: MAX_MONTH,
|
|
108
|
-
|
|
119
|
+
hintKey: 'editor.dateField.monthHint',
|
|
120
|
+
labelKey: 'editor.dateField.monthLabel',
|
|
109
121
|
width: '40px',
|
|
110
122
|
maxLength: 2
|
|
111
123
|
},
|
|
112
124
|
year: {
|
|
113
125
|
min: MIN_YEAR,
|
|
114
126
|
max: MAX_YEAR,
|
|
115
|
-
|
|
127
|
+
hintKey: 'editor.dateField.yearHint',
|
|
128
|
+
labelKey: 'editor.dateField.yearLabel',
|
|
116
129
|
width: '60px',
|
|
117
130
|
maxLength: 4
|
|
118
131
|
}
|
|
@@ -311,9 +324,12 @@
|
|
|
311
324
|
|
|
312
325
|
// Reset focused field after a short delay to allow for focus changes
|
|
313
326
|
setTimeout(() => {
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
327
|
+
// The focused element of the tree this control is in: inside a module `document.activeElement` is the module's
|
|
328
|
+
// host element, which is never one of the fields
|
|
329
|
+
const focusedElement = activeElementIn(input);
|
|
330
|
+
if (!focusedElement ||
|
|
331
|
+
!(focusedElement instanceof HTMLInputElement) ||
|
|
332
|
+
!Object.values(inputRefs).includes(focusedElement)) {
|
|
317
333
|
focusedField = null;
|
|
318
334
|
}
|
|
319
335
|
}, 100);
|
|
@@ -333,11 +349,14 @@
|
|
|
333
349
|
return;
|
|
334
350
|
}
|
|
335
351
|
|
|
336
|
-
// Handle arrow navigation
|
|
352
|
+
// Handle arrow navigation. The digits of a field run left to right on every screen, so the key that leaves a field at the
|
|
353
|
+
// right end of its digits is ArrowRight on every screen, and what lies on that side is the next field on a left to right
|
|
354
|
+
// screen and the one before it on a right to left one (where the row of fields starts on the right).
|
|
337
355
|
if (e.key === 'ArrowRight') {
|
|
338
356
|
if (input.selectionStart === input.value.length) {
|
|
339
357
|
e.preventDefault();
|
|
340
|
-
|
|
358
|
+
if ($textDirection === 'rtl') focusPreviousField(field);
|
|
359
|
+
else focusNextField(field);
|
|
341
360
|
}
|
|
342
361
|
return;
|
|
343
362
|
}
|
|
@@ -345,7 +364,8 @@
|
|
|
345
364
|
if (e.key === 'ArrowLeft') {
|
|
346
365
|
if (input.selectionStart === 0) {
|
|
347
366
|
e.preventDefault();
|
|
348
|
-
|
|
367
|
+
if ($textDirection === 'rtl') focusNextField(field);
|
|
368
|
+
else focusPreviousField(field);
|
|
349
369
|
}
|
|
350
370
|
return;
|
|
351
371
|
}
|
|
@@ -399,7 +419,7 @@
|
|
|
399
419
|
<InputControllerHeader {label} {helpText} />
|
|
400
420
|
</div>
|
|
401
421
|
|
|
402
|
-
<div class="absolute
|
|
422
|
+
<div class="absolute end-0">
|
|
403
423
|
<div class="h-[48px] w-[185px] rounded-large border border-grey2 bg-grey1 flex items-center transition-colors duration-200 hover:border-forest-green">
|
|
404
424
|
<div class="flex items-center justify-between px-4 w-full">
|
|
405
425
|
{#each fieldOrder as field}
|
|
@@ -412,14 +432,14 @@
|
|
|
412
432
|
pattern="[0-9]*"
|
|
413
433
|
bind:this={inputRefs[field]}
|
|
414
434
|
value={field === 'day' ? day : field === 'month' ? month : year}
|
|
415
|
-
placeholder={config.
|
|
435
|
+
placeholder={$t(config.hintKey)}
|
|
416
436
|
on:input={field === 'day' ? handleDayInput : field === 'month' ? handleMonthInput : handleYearInput}
|
|
417
437
|
on:change={handleChange}
|
|
418
438
|
on:focus={() => handleFocus(field)}
|
|
419
439
|
on:blur={() => handleBlur(field)}
|
|
420
440
|
on:keydown={(e) => handleKeyDown(field, e)}
|
|
421
441
|
class={getFieldClasses(field).input}
|
|
422
|
-
aria-label={
|
|
442
|
+
aria-label={$t(config.labelKey)}
|
|
423
443
|
maxlength={config.maxLength}
|
|
424
444
|
/>
|
|
425
445
|
</div>
|