@widgetic/editor 4.0.10 → 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 -49
- 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,22 +266,20 @@
|
|
|
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
|
-
// The font popover looks up this node while it is created. onMount is too
|
|
267
|
-
// late: the child portal mounts first and throws if the host is missing,
|
|
268
|
-
// which takes the whole details panel (including chat) down with it.
|
|
269
|
-
ensureFontPickerPortal();
|
|
270
|
-
|
|
271
283
|
onMount(async () => {
|
|
272
284
|
try {
|
|
273
285
|
if (browser) {
|
|
@@ -282,13 +294,13 @@
|
|
|
282
294
|
}
|
|
283
295
|
|
|
284
296
|
// Load saved filter
|
|
285
|
-
const savedFilter =
|
|
297
|
+
const savedFilter = readEditorStorageValue(FONT_FILTER_KEY(controllerId));
|
|
286
298
|
if (savedFilter) {
|
|
287
299
|
currentFilter = savedFilter as FilterType;
|
|
288
300
|
}
|
|
289
301
|
|
|
290
302
|
// Load saved weight
|
|
291
|
-
const savedWeight =
|
|
303
|
+
const savedWeight = readEditorStorageValue(FONT_WEIGHT_KEY(controllerId));
|
|
292
304
|
if (savedWeight) {
|
|
293
305
|
selectedWeight = savedWeight;
|
|
294
306
|
}
|
|
@@ -790,7 +802,7 @@
|
|
|
790
802
|
|
|
791
803
|
selectedWeight = `${controllerId}-${newWeight}`;
|
|
792
804
|
if (browser) {
|
|
793
|
-
|
|
805
|
+
writeEditorStorageValue(FONT_WEIGHT_KEY(controllerId), selectedWeight);
|
|
794
806
|
}
|
|
795
807
|
|
|
796
808
|
// Load the full font when selected
|
|
@@ -852,7 +864,7 @@
|
|
|
852
864
|
if (!searchTerm && value) {
|
|
853
865
|
lastActiveFilter = currentFilter;
|
|
854
866
|
currentFilter = 'All';
|
|
855
|
-
|
|
867
|
+
writeEditorStorageValue(FONT_FILTER_KEY(controllerId), 'All');
|
|
856
868
|
}
|
|
857
869
|
|
|
858
870
|
// Apply filtering with the new search term
|
|
@@ -1064,7 +1076,7 @@
|
|
|
1064
1076
|
currentFilter = filter;
|
|
1065
1077
|
|
|
1066
1078
|
if (browser) {
|
|
1067
|
-
|
|
1079
|
+
writeEditorStorageValue(FONT_FILTER_KEY(controllerId), filter);
|
|
1068
1080
|
}
|
|
1069
1081
|
|
|
1070
1082
|
if (fontManagerInstance) {
|
|
@@ -1482,7 +1494,8 @@
|
|
|
1482
1494
|
* @param term Search term
|
|
1483
1495
|
* @returns HTML string with highlighted match
|
|
1484
1496
|
*
|
|
1485
|
-
* 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).
|
|
1486
1499
|
*/
|
|
1487
1500
|
function highlightMatch(name: string, term: string) {
|
|
1488
1501
|
// Check if this font is the selected one
|
|
@@ -1491,8 +1504,8 @@
|
|
|
1491
1504
|
if (!term) {
|
|
1492
1505
|
// If no search term, just return the name with appropriate color
|
|
1493
1506
|
return isSelected
|
|
1494
|
-
? `<span style="color: white">${name}</span>`
|
|
1495
|
-
: `<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>`;
|
|
1496
1509
|
}
|
|
1497
1510
|
|
|
1498
1511
|
const nameLower = name.toLowerCase();
|
|
@@ -1501,9 +1514,9 @@
|
|
|
1501
1514
|
// If the name contains the search term, highlight that part
|
|
1502
1515
|
if (nameLower.includes(termLower)) {
|
|
1503
1516
|
const index = nameLower.indexOf(termLower);
|
|
1504
|
-
const beforeMatch = name.slice(0, index);
|
|
1505
|
-
const match = name.slice(index, index + termLower.length);
|
|
1506
|
-
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));
|
|
1507
1520
|
|
|
1508
1521
|
if (isSelected) {
|
|
1509
1522
|
// For selected items: text is white, highlight is grey7
|
|
@@ -1516,8 +1529,8 @@
|
|
|
1516
1529
|
|
|
1517
1530
|
// Default case - just return the name with appropriate color
|
|
1518
1531
|
return isSelected
|
|
1519
|
-
? `<span style="color: white">${name}</span>`
|
|
1520
|
-
: `<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>`;
|
|
1521
1534
|
}
|
|
1522
1535
|
|
|
1523
1536
|
/**
|
|
@@ -1550,28 +1563,31 @@
|
|
|
1550
1563
|
}
|
|
1551
1564
|
}
|
|
1552
1565
|
|
|
1553
|
-
//
|
|
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
|
|
1554
1571
|
onMount(() => {
|
|
1555
1572
|
// Add scroll listener with passive option
|
|
1556
|
-
|
|
1557
|
-
capture: true,
|
|
1558
|
-
passive: true
|
|
1559
|
-
});
|
|
1573
|
+
stopListeningForScroll = listenForScroll(controllerElement, handleGlobalScroll, { passive: true });
|
|
1560
1574
|
window.addEventListener('resize', handleFamilyPopoverViewportChange);
|
|
1561
1575
|
window.visualViewport?.addEventListener('resize', handleFamilyPopoverViewportChange);
|
|
1562
1576
|
|
|
1563
|
-
//
|
|
1564
|
-
|
|
1565
|
-
|
|
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.)
|
|
1566
1580
|
});
|
|
1567
1581
|
|
|
1568
1582
|
// Update onDestroy to remove all listeners
|
|
1569
1583
|
onDestroy(() => {
|
|
1570
|
-
|
|
1584
|
+
stopListeningForScroll();
|
|
1571
1585
|
window.removeEventListener('resize', handleFamilyPopoverViewportChange);
|
|
1572
1586
|
window.visualViewport?.removeEventListener('resize', handleFamilyPopoverViewportChange);
|
|
1573
|
-
|
|
1574
|
-
|
|
1587
|
+
|
|
1588
|
+
// Give the popover layer back (the last input to leave removes it from the page).
|
|
1589
|
+
releasePortalHost(fontPickerPortalHost);
|
|
1590
|
+
fontPickerPortalHost = undefined;
|
|
1575
1591
|
|
|
1576
1592
|
if (fontObserver) {
|
|
1577
1593
|
fontObserver.disconnect();
|
|
@@ -1653,7 +1669,7 @@
|
|
|
1653
1669
|
function handleWeightSelect(weight: string) {
|
|
1654
1670
|
selectedWeight = `${controllerId}-${weight}`;
|
|
1655
1671
|
if (browser) {
|
|
1656
|
-
|
|
1672
|
+
writeEditorStorageValue(FONT_WEIGHT_KEY(controllerId), selectedWeight);
|
|
1657
1673
|
}
|
|
1658
1674
|
const current = coerceFontValue(value);
|
|
1659
1675
|
if (onChange) {
|
|
@@ -1679,7 +1695,8 @@
|
|
|
1679
1695
|
}
|
|
1680
1696
|
|
|
1681
1697
|
// Ensure the selected item is visible
|
|
1682
|
-
|
|
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)}"]`);
|
|
1683
1700
|
selectedElement?.scrollIntoView({ block: 'nearest' });
|
|
1684
1701
|
}
|
|
1685
1702
|
|
|
@@ -1691,14 +1708,16 @@
|
|
|
1691
1708
|
|
|
1692
1709
|
// Functions for RecentItemsSection
|
|
1693
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.
|
|
1694
1713
|
return {
|
|
1695
1714
|
html: `
|
|
1696
1715
|
<div class="flex items-center flex-1 min-w-0">
|
|
1697
1716
|
<span
|
|
1698
1717
|
class="text-[24px] truncate"
|
|
1699
|
-
style="font-family:
|
|
1718
|
+
style="font-family: ${escapeHtml(cssQuotedString(font.family))}, sans-serif; font-display: swap; color: inherit; -webkit-text-fill-color: currentColor;"
|
|
1700
1719
|
>
|
|
1701
|
-
${font.family}
|
|
1720
|
+
${escapeHtml(font.family)}
|
|
1702
1721
|
</span>
|
|
1703
1722
|
</div>
|
|
1704
1723
|
`
|
|
@@ -1727,7 +1746,7 @@
|
|
|
1727
1746
|
</script>
|
|
1728
1747
|
|
|
1729
1748
|
<!-- Dual IC: header zone always on the left (even if label is empty); 185px inputs on the right. -->
|
|
1730
|
-
<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}>
|
|
1731
1750
|
<div class="font-family-row flex items-center justify-between w-full min-w-0 gap-2" style="min-height: 48px;">
|
|
1732
1751
|
<div class="font-family-label-ct flex-1 min-w-0">
|
|
1733
1752
|
<InputControllerHeader label={label || ''} />
|
|
@@ -1779,7 +1798,7 @@
|
|
|
1779
1798
|
>
|
|
1780
1799
|
<span
|
|
1781
1800
|
class="truncate flex-1 text-left subtext text-grey7"
|
|
1782
|
-
style="font-family:
|
|
1801
|
+
style="font-family: {cssQuotedString(resolvedFont?.family || 'system-ui')}, sans-serif; font-display: swap;"
|
|
1783
1802
|
>
|
|
1784
1803
|
{resolvedFont?.family || "Select font..."}
|
|
1785
1804
|
</span>
|
|
@@ -1790,7 +1809,7 @@
|
|
|
1790
1809
|
collisionPadding={12}
|
|
1791
1810
|
side={familyPopoverSide}
|
|
1792
1811
|
sideOffset={4}
|
|
1793
|
-
portalProps={{ to:
|
|
1812
|
+
portalProps={{ to: fontPickerPortalHost }}
|
|
1794
1813
|
class="font-input-popover !p-0 widgetic-colors widgetic-radius rounded-medium modal-shadow border border-grey2"
|
|
1795
1814
|
style={familyPopoverStyle}
|
|
1796
1815
|
>
|
|
@@ -1835,7 +1854,7 @@
|
|
|
1835
1854
|
// Restore the previous filter when clearing search
|
|
1836
1855
|
currentFilter = lastActiveFilter;
|
|
1837
1856
|
if (browser) {
|
|
1838
|
-
|
|
1857
|
+
writeEditorStorageValue(FONT_FILTER_KEY(controllerId), lastActiveFilter);
|
|
1839
1858
|
}
|
|
1840
1859
|
|
|
1841
1860
|
// ... rest of your onClear code ...
|
|
@@ -1937,7 +1956,7 @@
|
|
|
1937
1956
|
<Check class="mr-2 h-6 w-6 flex-shrink-0 {isFontSelected(font) ? 'text-white' : 'text-sage-green opacity-0'}"/>
|
|
1938
1957
|
<span
|
|
1939
1958
|
class="text-[24px] truncate {index === activeSearchIndex && searchTerm ? 'font-medium' : ''}"
|
|
1940
|
-
style="font-family:
|
|
1959
|
+
style="font-family: {cssQuotedString(font.family)}, sans-serif; font-display: swap; color: inherit; -webkit-text-fill-color: currentColor;"
|
|
1941
1960
|
>
|
|
1942
1961
|
{@html highlightMatch(font.family, searchTerm)}
|
|
1943
1962
|
</span>
|
|
@@ -1996,7 +2015,7 @@
|
|
|
1996
2015
|
</span>
|
|
1997
2016
|
<IconCaretDownFilled class="w-5 h-5 shrink-0 text-grey5 group-hover:text-grey7" />
|
|
1998
2017
|
</Popover.Trigger>
|
|
1999
|
-
<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 }}>
|
|
2000
2019
|
<Command.Root>
|
|
2001
2020
|
<Command.List>
|
|
2002
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 === '-') {
|