@widgetic/editor 4.0.9 → 4.0.11
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 +118 -0
- package/dist/components/WidgetPreview.svelte +65 -27
- package/dist/components/WidgetPreview.svelte.d.ts +8 -0
- package/dist/components/editors/CompositionEditorApp.svelte +508 -0
- package/dist/components/editors/CompositionEditorApp.svelte.d.ts +75 -0
- package/dist/components/editors/PropsEditor.svelte +193 -792
- package/dist/components/editors/PropsEditor.svelte.d.ts +7 -0
- package/dist/components/ic/BrowserInputController.svelte +21 -2
- package/dist/components/ic/ColorPickerInputController.svelte +24 -6
- package/dist/components/ic/DateFieldInputController.svelte +7 -3
- package/dist/components/ic/DatePickerInputController.svelte +10 -4
- package/dist/components/ic/DropdownInputController.svelte +11 -4
- package/dist/components/ic/FontInputController.svelte +68 -44
- package/dist/components/ic/InputControllerHeader.svelte +11 -8
- package/dist/components/ic/NumberInputController.svelte +4 -2
- package/dist/components/ic/PositionInputController.svelte +14 -11
- package/dist/components/ic/RangeCalendarInputController.svelte +10 -4
- package/dist/components/ic/RoundedCornersInputController.svelte +17 -9
- package/dist/components/ic/TextAreaInputController.svelte +20 -7
- package/dist/components/ic/TextAreaPreviewInputController.svelte +19 -69
- package/dist/components/ic/TimeInputController.svelte +9 -3
- package/dist/components/ic/VectorInputController.svelte +80 -54
- package/dist/components/ic/shared/ItemGrid.svelte +5 -1
- package/dist/composition/compositionApi.d.ts +95 -0
- package/dist/composition/compositionApi.js +289 -0
- package/dist/composition/compositionEditorController.d.ts +254 -0
- package/dist/composition/compositionEditorController.js +734 -0
- package/dist/composition/compositionHelpers.d.ts +72 -0
- package/dist/composition/compositionHelpers.js +403 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +4 -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/editorStorage.d.ts +23 -0
- package/dist/utils/editorStorage.js +95 -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 +6 -5
|
@@ -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).
|
|
@@ -45,6 +46,12 @@ declare const PropsEditor: $$__sveltets_2_IsomorphicComponent<{
|
|
|
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?: () => {
|
|
@@ -35,12 +35,14 @@
|
|
|
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';
|
|
38
39
|
import X from '../icons/x.svelte';
|
|
39
40
|
// console.log("BrowserInputController - Tooltip:", Tooltip);
|
|
40
41
|
|
|
41
42
|
|
|
42
43
|
// utils imports
|
|
43
44
|
import MediaManager from '../../utils/MediaManager';
|
|
45
|
+
import { queryInTreeOf } from '../../utils/shadowDom';
|
|
44
46
|
|
|
45
47
|
// Static asset imports (bundled via Vite, works when imported as library)
|
|
46
48
|
import icAudio from '../../assets/icons/ic-browser/audio.svg?url';
|
|
@@ -65,10 +67,17 @@
|
|
|
65
67
|
(portalTarget instanceof HTMLElement &&
|
|
66
68
|
portalTarget.id === 'props-editor-file-browser-portal');
|
|
67
69
|
export let config: BrowserInputControllerProps['config'] = {};
|
|
70
|
+
|
|
71
|
+
// The media library the editor above was given (a partner's own storage, for instance), read through a getter
|
|
72
|
+
// because it reaches this component through context.
|
|
73
|
+
const getEditorMediaLibrary = useEditorMediaLibrary();
|
|
68
74
|
|
|
69
75
|
// Extract allowed file types from config or default to all files
|
|
70
76
|
const fileTypes = config.fileTypes || ['*'];
|
|
71
77
|
|
|
78
|
+
// The control's own element, to find the editor's panel from (see getPortalHost)
|
|
79
|
+
let controllerElement: HTMLDivElement | undefined;
|
|
80
|
+
|
|
72
81
|
// Media element references for audio/video playback
|
|
73
82
|
let audioElement: HTMLAudioElement | null = null;
|
|
74
83
|
let videoElement: HTMLVideoElement | null = null;
|
|
@@ -166,6 +175,9 @@
|
|
|
166
175
|
/** Only embedded iframe mode (URL params) sets upload config here; standalone uses parent +page sync. */
|
|
167
176
|
function syncFileBrowserUploadConfigIfEmbedded() {
|
|
168
177
|
if (typeof window === 'undefined') return;
|
|
178
|
+
// With a media library the File Browser takes its files from it and never reads the upload configuration, and the
|
|
179
|
+
// address of the page this editor sits in (which can be a partner's) is none of the editor's business.
|
|
180
|
+
if (getEditorMediaLibrary()) return;
|
|
169
181
|
const params = new URLSearchParams(window.location.search);
|
|
170
182
|
const apiUrl = params.get('apiUrl');
|
|
171
183
|
const token = params.get('token');
|
|
@@ -187,7 +199,9 @@
|
|
|
187
199
|
function getPortalHost(): HTMLElement | null {
|
|
188
200
|
if (portalTarget instanceof HTMLElement) return portalTarget;
|
|
189
201
|
if (typeof document === 'undefined') return null;
|
|
190
|
-
|
|
202
|
+
// Looked for in the tree this control is in: inside a module that is a shadow root, which `document` cannot see into
|
|
203
|
+
const portalHost = queryInTreeOf(controllerElement, '#props-editor-file-browser-portal');
|
|
204
|
+
return portalHost instanceof HTMLElement ? portalHost : null;
|
|
191
205
|
}
|
|
192
206
|
|
|
193
207
|
function setPortalHostInteractive(active: boolean) {
|
|
@@ -461,7 +475,7 @@
|
|
|
461
475
|
</script>
|
|
462
476
|
|
|
463
477
|
<!-- Main container with updated styling -->
|
|
464
|
-
<div class="flex flex-col gap-2 widgetic-colors">
|
|
478
|
+
<div bind:this={controllerElement} class="flex flex-col gap-2 widgetic-colors">
|
|
465
479
|
<div class="flex items-center w-full relative">
|
|
466
480
|
<div class="flex-1 min-w-0">
|
|
467
481
|
<InputControllerHeader label={header?.label || label} helpText={header?.helpText} />
|
|
@@ -699,12 +713,17 @@
|
|
|
699
713
|
</div>
|
|
700
714
|
|
|
701
715
|
<div class="browser-input-file-browser-body flex min-h-0 flex-1 flex-col overflow-hidden">
|
|
716
|
+
<!-- With a media library the File Browser keeps nothing about this visit (the filter it was left on, the
|
|
717
|
+
folder it was left in) in the page's localStorage: that storage belongs to the page this editor sits in,
|
|
718
|
+
which can be a partner's. Without one (the Widgetic site) it remembers them, as it always did. -->
|
|
702
719
|
<FileBrowser
|
|
703
720
|
class="h-full min-h-0 flex-1"
|
|
704
721
|
instanceId={`props-editor-browser-input-${fileBrowserFilter}`}
|
|
705
722
|
initialFilter={fileBrowserFilter}
|
|
706
723
|
restrictUpload={fileBrowserRestrictUpload}
|
|
707
724
|
useDemoContent={false}
|
|
725
|
+
mediaLibrary={getEditorMediaLibrary()}
|
|
726
|
+
persistHistory={!getEditorMediaLibrary()}
|
|
708
727
|
allowSelecting={true}
|
|
709
728
|
showAddToEditor={true}
|
|
710
729
|
maxSelections={1}
|
|
@@ -69,6 +69,7 @@
|
|
|
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';
|
|
72
73
|
|
|
73
74
|
|
|
74
75
|
|
|
@@ -544,8 +545,10 @@
|
|
|
544
545
|
* @param event - Scroll event
|
|
545
546
|
*/
|
|
546
547
|
function handleScroll(event: Event) {
|
|
547
|
-
// Don't close if scrolling happens within allowed elements
|
|
548
|
-
|
|
548
|
+
// Don't close if scrolling happens within allowed elements. The page itself scrolling has the document as its
|
|
549
|
+
// target (it has no closest()), so only an element can be one of the allowed ones.
|
|
550
|
+
const scrollTarget = event.target;
|
|
551
|
+
if (scrollTarget instanceof Element && scrollTarget.closest('[data-allow-scroll="true"]')) {
|
|
549
552
|
return;
|
|
550
553
|
}
|
|
551
554
|
|
|
@@ -557,6 +560,9 @@
|
|
|
557
560
|
}
|
|
558
561
|
}
|
|
559
562
|
|
|
563
|
+
// Puts the pointer back after an opacity drag, however the drag ends (the picker can be removed in the middle of one).
|
|
564
|
+
let releaseOpacityPointerStyle: (() => void) | null = null;
|
|
565
|
+
|
|
560
566
|
/**
|
|
561
567
|
* Manages opacity slider interactions
|
|
562
568
|
* - Handles mouse drag events
|
|
@@ -567,7 +573,12 @@
|
|
|
567
573
|
*/
|
|
568
574
|
function handleOpacityMouseDown(event: MouseEvent) {
|
|
569
575
|
isDragging = true;
|
|
570
|
-
|
|
576
|
+
// The cursor is shown on the host of the shadow root the picker is in, and on <body> only on a normal page: the
|
|
577
|
+
// <body> of a customer's page is not the editor's to change.
|
|
578
|
+
releaseOpacityPointerStyle?.();
|
|
579
|
+
releaseOpacityPointerStyle = lockPointerStyle(event.currentTarget ?? event.target, 'grabbing', {
|
|
580
|
+
blockTextSelection: false
|
|
581
|
+
});
|
|
571
582
|
updateOpacity(event);
|
|
572
583
|
|
|
573
584
|
const handleMouseMove = (e: MouseEvent) => {
|
|
@@ -578,7 +589,8 @@
|
|
|
578
589
|
|
|
579
590
|
const handleMouseUp = () => {
|
|
580
591
|
isDragging = false;
|
|
581
|
-
|
|
592
|
+
releaseOpacityPointerStyle?.();
|
|
593
|
+
releaseOpacityPointerStyle = null;
|
|
582
594
|
window.removeEventListener('mousemove', handleMouseMove);
|
|
583
595
|
window.removeEventListener('mouseup', handleMouseUp);
|
|
584
596
|
};
|
|
@@ -947,6 +959,10 @@
|
|
|
947
959
|
}
|
|
948
960
|
}
|
|
949
961
|
|
|
962
|
+
// Stops listening for scrolling (the page and, inside a module, the module's own panels: a scroll never leaves a
|
|
963
|
+
// shadow root, so a listener on the window alone does not hear about those)
|
|
964
|
+
let stopListeningForScroll: () => void = () => {};
|
|
965
|
+
|
|
950
966
|
// Sets up event listeners when the component mounts
|
|
951
967
|
onMount(() => {
|
|
952
968
|
initializeColor();
|
|
@@ -954,17 +970,19 @@
|
|
|
954
970
|
window.addEventListener('mousemove', handleGlobalMouseMove);
|
|
955
971
|
window.addEventListener('keydown', handlePopupKeyDown);
|
|
956
972
|
window.addEventListener('keyup', handlePopupKeyUp);
|
|
957
|
-
|
|
973
|
+
stopListeningForScroll = listenForScroll(inputRef, handleScroll);
|
|
958
974
|
window.addEventListener('widgetic:close-color-pickers', onCloseOtherPickers as EventListener);
|
|
959
975
|
});
|
|
960
976
|
|
|
961
977
|
// Cleans up event listeners when the component is destroyed
|
|
962
978
|
onDestroy(() => {
|
|
979
|
+
releaseOpacityPointerStyle?.();
|
|
980
|
+
releaseOpacityPointerStyle = null;
|
|
963
981
|
window.removeEventListener('mouseup', handleGlobalMouseUp);
|
|
964
982
|
window.removeEventListener('mousemove', handleGlobalMouseMove);
|
|
965
983
|
window.removeEventListener('keydown', handlePopupKeyDown);
|
|
966
984
|
window.removeEventListener('keyup', handlePopupKeyUp);
|
|
967
|
-
|
|
985
|
+
stopListeningForScroll();
|
|
968
986
|
window.removeEventListener('widgetic:close-color-pickers', onCloseOtherPickers as EventListener);
|
|
969
987
|
});
|
|
970
988
|
|
|
@@ -29,6 +29,7 @@
|
|
|
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';
|
|
32
33
|
|
|
33
34
|
// Constants
|
|
34
35
|
const MIN_DAY = 1;
|
|
@@ -311,9 +312,12 @@
|
|
|
311
312
|
|
|
312
313
|
// Reset focused field after a short delay to allow for focus changes
|
|
313
314
|
setTimeout(() => {
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
315
|
+
// The focused element of the tree this control is in: inside a module `document.activeElement` is the module's
|
|
316
|
+
// host element, which is never one of the fields
|
|
317
|
+
const focusedElement = activeElementIn(input);
|
|
318
|
+
if (!focusedElement ||
|
|
319
|
+
!(focusedElement instanceof HTMLInputElement) ||
|
|
320
|
+
!Object.values(inputRefs).includes(focusedElement)) {
|
|
317
321
|
focusedField = null;
|
|
318
322
|
}
|
|
319
323
|
}, 100);
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
<script lang="ts">
|
|
38
38
|
// Svelte imports
|
|
39
39
|
import { onMount, onDestroy } from 'svelte';
|
|
40
|
+
import { listenForScroll } from '../../utils/shadowDom';
|
|
40
41
|
|
|
41
42
|
// Shadcn UI Components
|
|
42
43
|
import { Button, Select } from '@widgetic/design-system/components';
|
|
@@ -209,19 +210,24 @@
|
|
|
209
210
|
}
|
|
210
211
|
}
|
|
211
212
|
|
|
212
|
-
//
|
|
213
|
+
// The control's own element, which says which tree (the page or a module's shadow root) to listen for scrolling in
|
|
214
|
+
let controllerElement: HTMLDivElement | undefined;
|
|
215
|
+
let stopListeningForScroll: () => void = () => {};
|
|
216
|
+
|
|
217
|
+
// Setup scroll event listener on mount (the page and, inside a module, the module's own panels: a scroll never
|
|
218
|
+
// leaves a shadow root, so a listener on the window alone does not hear about those)
|
|
213
219
|
onMount(() => {
|
|
214
|
-
|
|
220
|
+
stopListeningForScroll = listenForScroll(controllerElement, handleScroll);
|
|
215
221
|
});
|
|
216
222
|
|
|
217
223
|
// Cleanup scroll event listener on destroy
|
|
218
224
|
onDestroy(() => {
|
|
219
|
-
|
|
225
|
+
stopListeningForScroll();
|
|
220
226
|
});
|
|
221
227
|
</script>
|
|
222
228
|
|
|
223
229
|
<!-- Date Picker Container -->
|
|
224
|
-
<div class="flex flex-col gap-2 widgetic-colors">
|
|
230
|
+
<div bind:this={controllerElement} class="flex flex-col gap-2 widgetic-colors">
|
|
225
231
|
<div class="flex items-center w-full relative">
|
|
226
232
|
<div class="flex-1 min-w-0">
|
|
227
233
|
<InputControllerHeader {label} />
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
* Required for component setup and cleanup
|
|
32
32
|
*/
|
|
33
33
|
import { onMount, onDestroy } from 'svelte';
|
|
34
|
+
import { listenForScroll } from '../../utils/shadowDom';
|
|
34
35
|
|
|
35
36
|
// Local Components imports
|
|
36
37
|
import { InputControllerHeader } from './index';
|
|
@@ -113,13 +114,18 @@
|
|
|
113
114
|
* Lifecycle Hooks
|
|
114
115
|
* Setup and cleanup of scroll event listeners
|
|
115
116
|
*/
|
|
117
|
+
// The control's own element, which says which tree (the page or a module's shadow root) to listen for scrolling in
|
|
118
|
+
// (a scroll never leaves a shadow root, so a listener on the window alone only hears about the page scrolling)
|
|
119
|
+
let controllerElement: HTMLDivElement | undefined;
|
|
120
|
+
let stopListeningForScroll: () => void = () => {};
|
|
121
|
+
|
|
116
122
|
onMount(() => {
|
|
117
123
|
// console.log('DropdownInputController: onMount:', value);
|
|
118
|
-
|
|
124
|
+
stopListeningForScroll = listenForScroll(controllerElement, handleScroll);
|
|
119
125
|
});
|
|
120
126
|
|
|
121
127
|
onDestroy(() => {
|
|
122
|
-
|
|
128
|
+
stopListeningForScroll();
|
|
123
129
|
});
|
|
124
130
|
|
|
125
131
|
/**
|
|
@@ -147,14 +153,15 @@
|
|
|
147
153
|
/**
|
|
148
154
|
* The option label for the current selection. `value` accepts a bare string
|
|
149
155
|
* or an option object; normalize once instead of re-narrowing at each read.
|
|
156
|
+
* A missing selection is the empty string, as the select expects.
|
|
150
157
|
*/
|
|
151
|
-
$: selectedValue = typeof value === 'string' ? value : value?.value;
|
|
158
|
+
$: selectedValue = typeof value === 'string' ? value : (value?.value ?? '');
|
|
152
159
|
$: triggerContent =
|
|
153
160
|
options.find((o) => o.value === selectedValue)?.label ?? defaultSelectedLabel;
|
|
154
161
|
</script>
|
|
155
162
|
|
|
156
163
|
<!-- Dropdown Container -->
|
|
157
|
-
<div class="dropdown-ic flex flex-col gap-2 widgetic-colors">
|
|
164
|
+
<div bind:this={controllerElement} class="dropdown-ic flex flex-col gap-2 widgetic-colors">
|
|
158
165
|
<div class="flex items-center w-full relative">
|
|
159
166
|
<div class="flex-1 min-w-0">
|
|
160
167
|
<InputControllerHeader {label} />
|
|
@@ -63,6 +63,9 @@
|
|
|
63
63
|
import FontManager, { mapGoogleVariantIdsToWeightNames } from '../../utils/FontManager';
|
|
64
64
|
|
|
65
65
|
import { shouldSkipFont } from '../../utils/skipFonts';
|
|
66
|
+
import { escapeHtml, cssQuotedString } from '../../utils/htmlSafety';
|
|
67
|
+
import { acquirePortalHost, releasePortalHost, listenForScroll, queryInTreeOf } from '../../utils/shadowDom';
|
|
68
|
+
import { readEditorStorageValue, writeEditorStorageValue } from '../../utils/editorStorage';
|
|
66
69
|
import type { Writable } from 'svelte/store';
|
|
67
70
|
|
|
68
71
|
import { InputControllerHeader } from './index';
|
|
@@ -125,6 +128,8 @@
|
|
|
125
128
|
|
|
126
129
|
// Font loading optimization
|
|
127
130
|
let fontListContainer: HTMLElement;
|
|
131
|
+
// The control's own element: it says which tree (the page or a module's shadow root) to listen for scrolling in
|
|
132
|
+
let controllerElement: HTMLDivElement | undefined;
|
|
128
133
|
let familyTriggerEl: HTMLDivElement | undefined;
|
|
129
134
|
let familyPopoverStyle = '';
|
|
130
135
|
let familyPopoverSide: 'top' | 'bottom' = 'bottom';
|
|
@@ -191,6 +196,15 @@
|
|
|
191
196
|
// Add a timeout to track long-running initial loads
|
|
192
197
|
let initialLoadTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
193
198
|
|
|
199
|
+
/**
|
|
200
|
+
* The sticky header of the font list. It is looked up in the tree the list lives in (inside a module that is a
|
|
201
|
+
* shadow root, which `document.querySelector` cannot see into), and the id is escaped so that one with dots or
|
|
202
|
+
* brackets in it is found too.
|
|
203
|
+
*/
|
|
204
|
+
function findStickyHeader(): Element | null {
|
|
205
|
+
return queryInTreeOf(fontListContainer, `#${CSS.escape(`font-header-${controllerId}`)}`);
|
|
206
|
+
}
|
|
207
|
+
|
|
194
208
|
// Function to update scroll state
|
|
195
209
|
function updateScrollState() {
|
|
196
210
|
if (fontListContainer) {
|
|
@@ -199,7 +213,7 @@
|
|
|
199
213
|
|
|
200
214
|
// If we're transitioning from scrolled to not scrolled, remove transition
|
|
201
215
|
if (wasScrolled && !isScrolled) {
|
|
202
|
-
const stickyHeader =
|
|
216
|
+
const stickyHeader = findStickyHeader();
|
|
203
217
|
if (stickyHeader) {
|
|
204
218
|
stickyHeader.classList.remove('transition-border');
|
|
205
219
|
// Force a reflow to ensure the transition class removal takes effect
|
|
@@ -207,7 +221,7 @@
|
|
|
207
221
|
}
|
|
208
222
|
} else if (!wasScrolled && isScrolled) {
|
|
209
223
|
// Add transition class when starting to scroll
|
|
210
|
-
const stickyHeader =
|
|
224
|
+
const stickyHeader = findStickyHeader();
|
|
211
225
|
if (stickyHeader) {
|
|
212
226
|
stickyHeader.classList.add('transition-border');
|
|
213
227
|
}
|
|
@@ -252,15 +266,18 @@
|
|
|
252
266
|
|
|
253
267
|
let fontPickerReadyAt = 0;
|
|
254
268
|
|
|
269
|
+
/**
|
|
270
|
+
* The layer the font popovers are drawn in: one of their own, above everything else, in the tree this input lives
|
|
271
|
+
* in. On a page that is a node at the end of the body; inside a module it is a node in the module's portal layer,
|
|
272
|
+
* because a popover drawn in the customer's body would lose the module's styles and leave a node behind in the
|
|
273
|
+
* customer's page. The input has to be on the page to tell which tree it is in, so until it is (a short moment,
|
|
274
|
+
* and a popover is never open by then) the popovers go where the design system's portal configuration sends them.
|
|
275
|
+
*/
|
|
276
|
+
let fontPickerPortalHost: HTMLElement | undefined;
|
|
277
|
+
|
|
255
278
|
function ensureFontPickerPortal() {
|
|
256
|
-
if (!browser) return;
|
|
257
|
-
|
|
258
|
-
if (document.getElementById(portalId)) return;
|
|
259
|
-
const host = document.createElement('div');
|
|
260
|
-
host.id = portalId;
|
|
261
|
-
host.style.position = 'relative';
|
|
262
|
-
host.style.zIndex = '100000000';
|
|
263
|
-
document.body.appendChild(host);
|
|
279
|
+
if (!browser || fontPickerPortalHost) return;
|
|
280
|
+
fontPickerPortalHost = acquirePortalHost(familyTriggerEl, 'widgetic-font-picker-portal') ?? undefined;
|
|
264
281
|
}
|
|
265
282
|
|
|
266
283
|
onMount(async () => {
|
|
@@ -277,13 +294,13 @@
|
|
|
277
294
|
}
|
|
278
295
|
|
|
279
296
|
// Load saved filter
|
|
280
|
-
const savedFilter =
|
|
297
|
+
const savedFilter = readEditorStorageValue(FONT_FILTER_KEY(controllerId));
|
|
281
298
|
if (savedFilter) {
|
|
282
299
|
currentFilter = savedFilter as FilterType;
|
|
283
300
|
}
|
|
284
301
|
|
|
285
302
|
// Load saved weight
|
|
286
|
-
const savedWeight =
|
|
303
|
+
const savedWeight = readEditorStorageValue(FONT_WEIGHT_KEY(controllerId));
|
|
287
304
|
if (savedWeight) {
|
|
288
305
|
selectedWeight = savedWeight;
|
|
289
306
|
}
|
|
@@ -785,7 +802,7 @@
|
|
|
785
802
|
|
|
786
803
|
selectedWeight = `${controllerId}-${newWeight}`;
|
|
787
804
|
if (browser) {
|
|
788
|
-
|
|
805
|
+
writeEditorStorageValue(FONT_WEIGHT_KEY(controllerId), selectedWeight);
|
|
789
806
|
}
|
|
790
807
|
|
|
791
808
|
// Load the full font when selected
|
|
@@ -847,7 +864,7 @@
|
|
|
847
864
|
if (!searchTerm && value) {
|
|
848
865
|
lastActiveFilter = currentFilter;
|
|
849
866
|
currentFilter = 'All';
|
|
850
|
-
|
|
867
|
+
writeEditorStorageValue(FONT_FILTER_KEY(controllerId), 'All');
|
|
851
868
|
}
|
|
852
869
|
|
|
853
870
|
// Apply filtering with the new search term
|
|
@@ -1059,7 +1076,7 @@
|
|
|
1059
1076
|
currentFilter = filter;
|
|
1060
1077
|
|
|
1061
1078
|
if (browser) {
|
|
1062
|
-
|
|
1079
|
+
writeEditorStorageValue(FONT_FILTER_KEY(controllerId), filter);
|
|
1063
1080
|
}
|
|
1064
1081
|
|
|
1065
1082
|
if (fontManagerInstance) {
|
|
@@ -1477,7 +1494,8 @@
|
|
|
1477
1494
|
* @param term Search term
|
|
1478
1495
|
* @returns HTML string with highlighted match
|
|
1479
1496
|
*
|
|
1480
|
-
* Note:
|
|
1497
|
+
* Note: The result goes to {@html}, so every piece of the font name is escaped as it is put in (a name such as
|
|
1498
|
+
* `<img src=x onerror=...>` must show as text, not run in the page the editor is on).
|
|
1481
1499
|
*/
|
|
1482
1500
|
function highlightMatch(name: string, term: string) {
|
|
1483
1501
|
// Check if this font is the selected one
|
|
@@ -1486,8 +1504,8 @@
|
|
|
1486
1504
|
if (!term) {
|
|
1487
1505
|
// If no search term, just return the name with appropriate color
|
|
1488
1506
|
return isSelected
|
|
1489
|
-
? `<span style="color: white">${name}</span>`
|
|
1490
|
-
: `<span style="color: hsl(var(--grey7))">${name}</span>`;
|
|
1507
|
+
? `<span style="color: white">${escapeHtml(name)}</span>`
|
|
1508
|
+
: `<span style="color: hsl(var(--grey7))">${escapeHtml(name)}</span>`;
|
|
1491
1509
|
}
|
|
1492
1510
|
|
|
1493
1511
|
const nameLower = name.toLowerCase();
|
|
@@ -1496,9 +1514,9 @@
|
|
|
1496
1514
|
// If the name contains the search term, highlight that part
|
|
1497
1515
|
if (nameLower.includes(termLower)) {
|
|
1498
1516
|
const index = nameLower.indexOf(termLower);
|
|
1499
|
-
const beforeMatch = name.slice(0, index);
|
|
1500
|
-
const match = name.slice(index, index + termLower.length);
|
|
1501
|
-
const afterMatch = name.slice(index + termLower.length);
|
|
1517
|
+
const beforeMatch = escapeHtml(name.slice(0, index));
|
|
1518
|
+
const match = escapeHtml(name.slice(index, index + termLower.length));
|
|
1519
|
+
const afterMatch = escapeHtml(name.slice(index + termLower.length));
|
|
1502
1520
|
|
|
1503
1521
|
if (isSelected) {
|
|
1504
1522
|
// For selected items: text is white, highlight is grey7
|
|
@@ -1511,8 +1529,8 @@
|
|
|
1511
1529
|
|
|
1512
1530
|
// Default case - just return the name with appropriate color
|
|
1513
1531
|
return isSelected
|
|
1514
|
-
? `<span style="color: white">${name}</span>`
|
|
1515
|
-
: `<span style="color: hsl(var(--grey7))">${name}</span>`;
|
|
1532
|
+
? `<span style="color: white">${escapeHtml(name)}</span>`
|
|
1533
|
+
: `<span style="color: hsl(var(--grey7))">${escapeHtml(name)}</span>`;
|
|
1516
1534
|
}
|
|
1517
1535
|
|
|
1518
1536
|
/**
|
|
@@ -1545,28 +1563,31 @@
|
|
|
1545
1563
|
}
|
|
1546
1564
|
}
|
|
1547
1565
|
|
|
1548
|
-
//
|
|
1566
|
+
// Stops listening for scrolling (the page and, inside a module, the module's own panels: a scroll never leaves a
|
|
1567
|
+
// shadow root, so a listener on the window alone does not hear about those)
|
|
1568
|
+
let stopListeningForScroll: () => void = () => {};
|
|
1569
|
+
|
|
1570
|
+
// Update onMount to add the scroll listener with passive option
|
|
1549
1571
|
onMount(() => {
|
|
1550
1572
|
// Add scroll listener with passive option
|
|
1551
|
-
|
|
1552
|
-
capture: true,
|
|
1553
|
-
passive: true
|
|
1554
|
-
});
|
|
1573
|
+
stopListeningForScroll = listenForScroll(controllerElement, handleGlobalScroll, { passive: true });
|
|
1555
1574
|
window.addEventListener('resize', handleFamilyPopoverViewportChange);
|
|
1556
1575
|
window.visualViewport?.addEventListener('resize', handleFamilyPopoverViewportChange);
|
|
1557
1576
|
|
|
1558
|
-
//
|
|
1559
|
-
|
|
1560
|
-
|
|
1577
|
+
// (Two touchstart/touchmove listeners that did nothing used to be added to the window here, and could never be
|
|
1578
|
+
// removed: each input left two more on the customer's page every time it was shown. They did nothing, so they are
|
|
1579
|
+
// gone.)
|
|
1561
1580
|
});
|
|
1562
1581
|
|
|
1563
1582
|
// Update onDestroy to remove all listeners
|
|
1564
1583
|
onDestroy(() => {
|
|
1565
|
-
|
|
1584
|
+
stopListeningForScroll();
|
|
1566
1585
|
window.removeEventListener('resize', handleFamilyPopoverViewportChange);
|
|
1567
1586
|
window.visualViewport?.removeEventListener('resize', handleFamilyPopoverViewportChange);
|
|
1568
|
-
|
|
1569
|
-
|
|
1587
|
+
|
|
1588
|
+
// Give the popover layer back (the last input to leave removes it from the page).
|
|
1589
|
+
releasePortalHost(fontPickerPortalHost);
|
|
1590
|
+
fontPickerPortalHost = undefined;
|
|
1570
1591
|
|
|
1571
1592
|
if (fontObserver) {
|
|
1572
1593
|
fontObserver.disconnect();
|
|
@@ -1648,7 +1669,7 @@
|
|
|
1648
1669
|
function handleWeightSelect(weight: string) {
|
|
1649
1670
|
selectedWeight = `${controllerId}-${weight}`;
|
|
1650
1671
|
if (browser) {
|
|
1651
|
-
|
|
1672
|
+
writeEditorStorageValue(FONT_WEIGHT_KEY(controllerId), selectedWeight);
|
|
1652
1673
|
}
|
|
1653
1674
|
const current = coerceFontValue(value);
|
|
1654
1675
|
if (onChange) {
|
|
@@ -1674,7 +1695,8 @@
|
|
|
1674
1695
|
}
|
|
1675
1696
|
|
|
1676
1697
|
// Ensure the selected item is visible
|
|
1677
|
-
|
|
1698
|
+
// (The family goes through CSS.escape: a name with a quote in it must not end the selector.)
|
|
1699
|
+
const selectedElement = fontListContainer?.querySelector(`[data-font="${CSS.escape(filteredFonts[activeSearchIndex].family)}"]`);
|
|
1678
1700
|
selectedElement?.scrollIntoView({ block: 'nearest' });
|
|
1679
1701
|
}
|
|
1680
1702
|
|
|
@@ -1686,14 +1708,16 @@
|
|
|
1686
1708
|
|
|
1687
1709
|
// Functions for RecentItemsSection
|
|
1688
1710
|
function renderFontItem(font: FontItem) {
|
|
1711
|
+
// The result is used as HTML by RecentItemsSection: the family is quoted for CSS and escaped for HTML where it is
|
|
1712
|
+
// a rule, and escaped for HTML where it is the text.
|
|
1689
1713
|
return {
|
|
1690
1714
|
html: `
|
|
1691
1715
|
<div class="flex items-center flex-1 min-w-0">
|
|
1692
1716
|
<span
|
|
1693
1717
|
class="text-[24px] truncate"
|
|
1694
|
-
style="font-family:
|
|
1718
|
+
style="font-family: ${escapeHtml(cssQuotedString(font.family))}, sans-serif; font-display: swap; color: inherit; -webkit-text-fill-color: currentColor;"
|
|
1695
1719
|
>
|
|
1696
|
-
${font.family}
|
|
1720
|
+
${escapeHtml(font.family)}
|
|
1697
1721
|
</span>
|
|
1698
1722
|
</div>
|
|
1699
1723
|
`
|
|
@@ -1722,7 +1746,7 @@
|
|
|
1722
1746
|
</script>
|
|
1723
1747
|
|
|
1724
1748
|
<!-- Dual IC: header zone always on the left (even if label is empty); 185px inputs on the right. -->
|
|
1725
|
-
<div class="font-ic flex flex-col gap-2 w-full min-w-0 widgetic-colors {className}" style={style}>
|
|
1749
|
+
<div bind:this={controllerElement} class="font-ic flex flex-col gap-2 w-full min-w-0 widgetic-colors {className}" style={style}>
|
|
1726
1750
|
<div class="font-family-row flex items-center justify-between w-full min-w-0 gap-2" style="min-height: 48px;">
|
|
1727
1751
|
<div class="font-family-label-ct flex-1 min-w-0">
|
|
1728
1752
|
<InputControllerHeader label={label || ''} />
|
|
@@ -1774,7 +1798,7 @@
|
|
|
1774
1798
|
>
|
|
1775
1799
|
<span
|
|
1776
1800
|
class="truncate flex-1 text-left subtext text-grey7"
|
|
1777
|
-
style="font-family:
|
|
1801
|
+
style="font-family: {cssQuotedString(resolvedFont?.family || 'system-ui')}, sans-serif; font-display: swap;"
|
|
1778
1802
|
>
|
|
1779
1803
|
{resolvedFont?.family || "Select font..."}
|
|
1780
1804
|
</span>
|
|
@@ -1785,7 +1809,7 @@
|
|
|
1785
1809
|
collisionPadding={12}
|
|
1786
1810
|
side={familyPopoverSide}
|
|
1787
1811
|
sideOffset={4}
|
|
1788
|
-
portalProps={{ to:
|
|
1812
|
+
portalProps={{ to: fontPickerPortalHost }}
|
|
1789
1813
|
class="font-input-popover !p-0 widgetic-colors widgetic-radius rounded-medium modal-shadow border border-grey2"
|
|
1790
1814
|
style={familyPopoverStyle}
|
|
1791
1815
|
>
|
|
@@ -1830,7 +1854,7 @@
|
|
|
1830
1854
|
// Restore the previous filter when clearing search
|
|
1831
1855
|
currentFilter = lastActiveFilter;
|
|
1832
1856
|
if (browser) {
|
|
1833
|
-
|
|
1857
|
+
writeEditorStorageValue(FONT_FILTER_KEY(controllerId), lastActiveFilter);
|
|
1834
1858
|
}
|
|
1835
1859
|
|
|
1836
1860
|
// ... rest of your onClear code ...
|
|
@@ -1932,7 +1956,7 @@
|
|
|
1932
1956
|
<Check class="mr-2 h-6 w-6 flex-shrink-0 {isFontSelected(font) ? 'text-white' : 'text-sage-green opacity-0'}"/>
|
|
1933
1957
|
<span
|
|
1934
1958
|
class="text-[24px] truncate {index === activeSearchIndex && searchTerm ? 'font-medium' : ''}"
|
|
1935
|
-
style="font-family:
|
|
1959
|
+
style="font-family: {cssQuotedString(font.family)}, sans-serif; font-display: swap; color: inherit; -webkit-text-fill-color: currentColor;"
|
|
1936
1960
|
>
|
|
1937
1961
|
{@html highlightMatch(font.family, searchTerm)}
|
|
1938
1962
|
</span>
|
|
@@ -1991,7 +2015,7 @@
|
|
|
1991
2015
|
</span>
|
|
1992
2016
|
<IconCaretDownFilled class="w-5 h-5 shrink-0 text-grey5 group-hover:text-grey7" />
|
|
1993
2017
|
</Popover.Trigger>
|
|
1994
|
-
<Popover.Content class="font-input-popover p-0 w-[150px]" portalProps={{ to:
|
|
2018
|
+
<Popover.Content class="font-input-popover p-0 w-[150px]" portalProps={{ to: fontPickerPortalHost }}>
|
|
1995
2019
|
<Command.Root>
|
|
1996
2020
|
<Command.List>
|
|
1997
2021
|
<Command.Group>
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
// Svelte imports
|
|
3
3
|
import { onDestroy, onMount } from 'svelte';
|
|
4
4
|
|
|
5
|
+
// Shared listener for "the user is scrolling" (one per tree, not one per header)
|
|
6
|
+
import { onScrollActivity } from '../../utils/scrollActivity';
|
|
7
|
+
|
|
5
8
|
// Shadcn UI Components from design-system
|
|
6
9
|
import { Tooltip, Portal } from '@widgetic/design-system/components';
|
|
7
10
|
// console.log("InputControllerHeader - Tooltip:", Tooltip);
|
|
@@ -58,23 +61,23 @@ function handleMouseLeave() {
|
|
|
58
61
|
clearTimeout(tooltipTimeout);
|
|
59
62
|
}
|
|
60
63
|
|
|
64
|
+
let headerElement: HTMLElement | undefined;
|
|
65
|
+
let stopListeningForScrolling: () => void = () => {};
|
|
66
|
+
|
|
61
67
|
onMount(() => {
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
document.addEventListener('touchmove', handleScroll, { capture: true, passive: true });
|
|
68
|
+
// The listeners for scrolling are shared by all the headers in the same tree (the page or the module's shadow
|
|
69
|
+
// root): every header used to add three of its own to the window and the document.
|
|
70
|
+
stopListeningForScrolling = onScrollActivity(headerElement, handleScroll);
|
|
66
71
|
});
|
|
67
72
|
|
|
68
73
|
onDestroy(() => {
|
|
69
|
-
|
|
70
|
-
document.removeEventListener('wheel', handleScroll, { capture: true });
|
|
71
|
-
document.removeEventListener('touchmove', handleScroll, { capture: true });
|
|
74
|
+
stopListeningForScrolling();
|
|
72
75
|
clearTimeout(scrollTimeout);
|
|
73
76
|
clearTimeout(tooltipTimeout);
|
|
74
77
|
});
|
|
75
78
|
</script>
|
|
76
79
|
|
|
77
|
-
<div class="input-controller-header widgetic-colors {className ? className + ' ' : ''}flex group" {style}>
|
|
80
|
+
<div bind:this={headerElement} class="input-controller-header widgetic-colors {className ? className + ' ' : ''}flex group" {style}>
|
|
78
81
|
<div class="header-label-ct flex-1 min-w-0 flex items-center h-full">
|
|
79
82
|
<div class="tooltip-group flex items-center max-w-full" style="gap: 8px;">
|
|
80
83
|
<span class="subtext cursor-default select-none truncate !text-base" style="color: hsl(var(--grey5));">
|
|
@@ -203,11 +203,13 @@
|
|
|
203
203
|
* Blur Event Handler
|
|
204
204
|
* Validates and normalizes input when focus is lost
|
|
205
205
|
*/
|
|
206
|
-
function handleBlur() {
|
|
206
|
+
function handleBlur(event: FocusEvent) {
|
|
207
207
|
// console.log('handleBlur:', { value, isEditing, min, max });
|
|
208
208
|
|
|
209
209
|
isEditing = false;
|
|
210
|
-
|
|
210
|
+
// The input that lost the focus. (It used to be looked up by the id "number-input" in the whole document, which is
|
|
211
|
+
// another input when there are several, and nothing inside a module's shadow root.)
|
|
212
|
+
const input = event.currentTarget as HTMLInputElement;
|
|
211
213
|
|
|
212
214
|
// Handle empty or invalid input
|
|
213
215
|
if (input.value === '' || input.value === '-') {
|