@marianmeres/stuic 2.3.2 → 2.5.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 +0 -1
- package/dist/README.md +17 -11
- package/dist/actions/autogrow.svelte.js +4 -4
- package/dist/actions/autoscroll.d.ts +2 -2
- package/dist/actions/autoscroll.js +17 -8
- package/dist/actions/file-dropzone.svelte.d.ts +1 -1
- package/dist/actions/file-dropzone.svelte.js +1 -1
- package/dist/actions/focus-trap.js +33 -24
- package/dist/actions/highlight-dragover.svelte.js +6 -5
- package/dist/actions/index.d.ts +1 -0
- package/dist/actions/index.js +1 -0
- package/dist/actions/on-submit-validity-check.svelte.js +2 -2
- package/dist/actions/popover/PopoverContent.svelte +15 -0
- package/dist/actions/popover/PopoverContent.svelte.d.ts +7 -0
- package/dist/actions/popover/index.css +78 -0
- package/dist/actions/popover/index.d.ts +1 -0
- package/dist/actions/popover/index.js +1 -0
- package/dist/actions/popover/popover.svelte.d.ts +112 -0
- package/dist/actions/popover/popover.svelte.js +449 -0
- package/dist/actions/resizable-width.svelte.d.ts +1 -1
- package/dist/actions/resizable-width.svelte.js +7 -5
- package/dist/actions/tooltip/index.css +2 -7
- package/dist/actions/tooltip/tooltip.svelte.js +5 -4
- package/dist/actions/trim.svelte.d.ts +1 -1
- package/dist/actions/trim.svelte.js +2 -2
- package/dist/actions/validate.svelte.d.ts +4 -4
- package/dist/actions/validate.svelte.js +9 -9
- package/dist/components/AlertConfirmPrompt/alert-confirm-prompt-stack.svelte.d.ts +7 -6
- package/dist/components/AlertConfirmPrompt/alert-confirm-prompt-stack.svelte.js +1 -2
- package/dist/components/AlertConfirmPrompt/index.d.ts +1 -1
- package/dist/components/AlertConfirmPrompt/index.js +1 -1
- package/dist/components/AnimatedElipsis/index.d.ts +1 -1
- package/dist/components/AnimatedElipsis/index.js +1 -1
- package/dist/components/Button/index.d.ts +1 -1
- package/dist/components/Button/index.js +1 -1
- package/dist/components/ButtonGroupRadio/ButtonGroupRadio.svelte +4 -1
- package/dist/components/ButtonGroupRadio/index.d.ts +1 -1
- package/dist/components/ButtonGroupRadio/index.js +1 -1
- package/dist/components/ColorScheme/index.d.ts +2 -2
- package/dist/components/ColorScheme/index.js +2 -2
- package/dist/components/CommandMenu/CommandMenu.svelte +1 -1
- package/dist/components/CommandMenu/index.d.ts +1 -1
- package/dist/components/CommandMenu/index.js +1 -1
- package/dist/components/DismissibleMessage/index.d.ts +1 -1
- package/dist/components/DismissibleMessage/index.js +1 -1
- package/dist/components/HoverExpandableWidth/index.d.ts +1 -1
- package/dist/components/HoverExpandableWidth/index.js +1 -1
- package/dist/components/Input/FieldAssets.svelte +7 -3
- package/dist/components/Input/FieldLikeButton.svelte +1 -1
- package/dist/components/Input/FieldOptions.svelte +1 -1
- package/dist/components/Input/index.d.ts +7 -7
- package/dist/components/Input/index.js +7 -7
- package/dist/components/KbdShortcut/index.d.ts +1 -1
- package/dist/components/KbdShortcut/index.js +1 -1
- package/dist/components/ModalDialog/index.d.ts +1 -1
- package/dist/components/ModalDialog/index.js +1 -1
- package/dist/components/Notifications/index.d.ts +1 -1
- package/dist/components/Notifications/index.js +1 -1
- package/dist/components/Notifications/notifications-stack.svelte.d.ts +5 -5
- package/dist/components/Notifications/notifications-stack.svelte.js +8 -7
- package/dist/components/SlidingPanels/index.d.ts +1 -1
- package/dist/components/SlidingPanels/index.js +1 -1
- package/dist/components/Spinner/index.d.ts +1 -1
- package/dist/components/Spinner/index.js +1 -1
- package/dist/components/Switch/Switch.svelte +5 -2
- package/dist/components/Switch/index.d.ts +1 -1
- package/dist/components/Switch/index.js +1 -1
- package/dist/components/TypeaheadInput/index.d.ts +1 -1
- package/dist/components/TypeaheadInput/index.js +1 -1
- package/dist/utils/body-scroll-locker.js +4 -3
- package/dist/utils/breakpoint.svelte.js +0 -2
- package/dist/utils/colors.js +3 -3
- package/dist/utils/debounce.d.ts +1 -1
- package/dist/utils/debounce.js +1 -2
- package/dist/utils/escape-regex.js +1 -1
- package/dist/utils/event-emitter.d.ts +2 -3
- package/dist/utils/event-emitter.js +1 -2
- package/dist/utils/event-modifiers.d.ts +4 -4
- package/dist/utils/event-modifiers.js +4 -6
- package/dist/utils/get-file-type-label.js +1 -1
- package/dist/utils/is-image.js +2 -2
- package/dist/utils/is-nullish.d.ts +1 -1
- package/dist/utils/is-plain-object.d.ts +1 -1
- package/dist/utils/is-plain-object.js +4 -1
- package/dist/utils/maybe-json-parse.d.ts +1 -1
- package/dist/utils/maybe-json-parse.js +1 -1
- package/dist/utils/maybe-json-stringify.d.ts +1 -1
- package/dist/utils/move-array-item.d.ts +1 -1
- package/dist/utils/preload-img.js +2 -1
- package/dist/utils/sleep.d.ts +1 -1
- package/dist/utils/storage-abstraction.d.ts +13 -13
- package/dist/utils/storage-abstraction.js +2 -0
- package/dist/utils/svg-circle.js +2 -1
- package/dist/utils/switch.svelte.d.ts +1 -1
- package/dist/utils/switch.svelte.js +1 -1
- package/dist/utils/throttle.d.ts +1 -1
- package/dist/utils/throttle.js +7 -8
- package/dist/utils/to-integer.d.ts +1 -1
- package/package.json +6 -2
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
import { mount, unmount } from "svelte";
|
|
2
|
+
import { twMerge } from "../../utils/tw-merge.js";
|
|
3
|
+
import PopoverContent from "./PopoverContent.svelte";
|
|
4
|
+
//
|
|
5
|
+
import "./index.css";
|
|
6
|
+
// Registry of open popover hide functions for closeOthers feature
|
|
7
|
+
const openPopovers = new Set();
|
|
8
|
+
const SHOW_DELAY = 100;
|
|
9
|
+
const HIDE_DELAY = 200;
|
|
10
|
+
const TRANSITION = 200;
|
|
11
|
+
/**
|
|
12
|
+
* Checks if the browser supports CSS Anchor Positioning for popovers.
|
|
13
|
+
*
|
|
14
|
+
* Tests for support of `anchor-name`, `position-area`, `position-try`,
|
|
15
|
+
* and `position-try-fallbacks` CSS properties.
|
|
16
|
+
*
|
|
17
|
+
* @returns `true` if CSS Anchor Positioning is fully supported
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```ts
|
|
21
|
+
* if (isPopoverSupported()) {
|
|
22
|
+
* // Use native anchor positioning
|
|
23
|
+
* } else {
|
|
24
|
+
* // Fall back to centered modal
|
|
25
|
+
* }
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export function isPopoverSupported() {
|
|
29
|
+
return (CSS.supports("anchor-name", "--anchor") &&
|
|
30
|
+
CSS.supports("position-area", "top") &&
|
|
31
|
+
CSS.supports("position-try", "top") &&
|
|
32
|
+
CSS.supports("position-try-fallbacks", "top"));
|
|
33
|
+
}
|
|
34
|
+
const POSITION_MAP = {
|
|
35
|
+
top: "top",
|
|
36
|
+
"top-left": "top left",
|
|
37
|
+
"top-right": "top right",
|
|
38
|
+
"top-span-left": "top span-left",
|
|
39
|
+
"top-span-right": "top span-right",
|
|
40
|
+
bottom: "bottom",
|
|
41
|
+
"bottom-left": "bottom left",
|
|
42
|
+
"bottom-right": "bottom right",
|
|
43
|
+
"bottom-span-left": "bottom span-left",
|
|
44
|
+
"bottom-span-right": "bottom span-right",
|
|
45
|
+
left: "left",
|
|
46
|
+
right: "right",
|
|
47
|
+
};
|
|
48
|
+
const _classPopover = `
|
|
49
|
+
bg-popover-bg dark:bg-popover-bg-dark text-popover-text dark:text-popover-text-dark
|
|
50
|
+
shadow-lg rounded-md
|
|
51
|
+
border border-popover-border dark:border-popover-border-dark
|
|
52
|
+
z-50
|
|
53
|
+
`;
|
|
54
|
+
const _classBackdrop = `
|
|
55
|
+
fixed inset-0 bg-black/25
|
|
56
|
+
z-40
|
|
57
|
+
`;
|
|
58
|
+
/**
|
|
59
|
+
* Checks if content is simple (string/html) vs complex (component/snippet).
|
|
60
|
+
*/
|
|
61
|
+
function isSimpleContent(content) {
|
|
62
|
+
if (!content)
|
|
63
|
+
return true;
|
|
64
|
+
if (typeof content === "string")
|
|
65
|
+
return true;
|
|
66
|
+
if (typeof content === "object") {
|
|
67
|
+
if ("text" in content || "html" in content)
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Extracts string content for simple THC types.
|
|
74
|
+
*/
|
|
75
|
+
function getStringContent(content) {
|
|
76
|
+
if (!content)
|
|
77
|
+
return "";
|
|
78
|
+
if (typeof content === "string")
|
|
79
|
+
return content;
|
|
80
|
+
if (typeof content === "object") {
|
|
81
|
+
if ("html" in content)
|
|
82
|
+
return content.html;
|
|
83
|
+
if ("text" in content)
|
|
84
|
+
return content.text;
|
|
85
|
+
}
|
|
86
|
+
return "";
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* A Svelte action that displays a popover anchored to an element using CSS Anchor Positioning.
|
|
90
|
+
*
|
|
91
|
+
* The popover appears on click or hover (configurable) and supports multiple positions
|
|
92
|
+
* with automatic fallback. When CSS Anchor Positioning is not supported, falls back
|
|
93
|
+
* to a centered modal overlay.
|
|
94
|
+
*
|
|
95
|
+
* @param anchorEl - The element to attach the popover to
|
|
96
|
+
* @param fn - Function returning popover options (reactive)
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* ```svelte
|
|
100
|
+
* <!-- Click trigger (default) -->
|
|
101
|
+
* <button use:popover={() => ({ content: "Hello World" })}>
|
|
102
|
+
* Open Popover
|
|
103
|
+
* </button>
|
|
104
|
+
* ```
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* ```svelte
|
|
108
|
+
* <!-- Hover trigger -->
|
|
109
|
+
* <button use:popover={() => ({
|
|
110
|
+
* content: "Hover content",
|
|
111
|
+
* trigger: "hover",
|
|
112
|
+
* position: "top"
|
|
113
|
+
* })}>
|
|
114
|
+
* Hover Me
|
|
115
|
+
* </button>
|
|
116
|
+
* ```
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* ```svelte
|
|
120
|
+
* <!-- With component content -->
|
|
121
|
+
* <button use:popover={() => ({
|
|
122
|
+
* content: { component: MyComponent, props: { foo: "bar" } }
|
|
123
|
+
* })}>
|
|
124
|
+
* With Component
|
|
125
|
+
* </button>
|
|
126
|
+
* ```
|
|
127
|
+
*
|
|
128
|
+
* @remarks
|
|
129
|
+
* - Falls back to centered modal when CSS Anchor Positioning is not supported
|
|
130
|
+
* - Closes on click outside (for click trigger) and Escape key by default
|
|
131
|
+
* - For hover trigger, popover persists when hovering over it
|
|
132
|
+
* - Automatically cleans up DOM elements on unmount
|
|
133
|
+
*/
|
|
134
|
+
export function popover(anchorEl, fn) {
|
|
135
|
+
const isSupported = isPopoverSupported();
|
|
136
|
+
// State
|
|
137
|
+
let popoverEl = null;
|
|
138
|
+
let backdropEl = null;
|
|
139
|
+
let wrapperEl = null;
|
|
140
|
+
let mountedComponent = null;
|
|
141
|
+
let showTimer = null;
|
|
142
|
+
let hideTimer = null;
|
|
143
|
+
let isVisible = false;
|
|
144
|
+
let do_debug = false;
|
|
145
|
+
// Unique identifiers
|
|
146
|
+
const rnd = Math.random().toString(36).slice(2);
|
|
147
|
+
const id = `popover-${rnd}`;
|
|
148
|
+
const anchorName = `--anchor-popover-${rnd}`;
|
|
149
|
+
// Current options (updated reactively)
|
|
150
|
+
let currentOptions = {};
|
|
151
|
+
// Initialize anchor element - anchor-name is always set
|
|
152
|
+
// In forceFallback mode, the CSS is just ignored
|
|
153
|
+
anchorEl.style.cssText += `anchor-name: ${anchorName};`;
|
|
154
|
+
anchorEl.setAttribute("aria-haspopup", "dialog");
|
|
155
|
+
anchorEl.setAttribute("aria-expanded", "false");
|
|
156
|
+
anchorEl.setAttribute("aria-controls", id);
|
|
157
|
+
// Debug helper
|
|
158
|
+
const debug = (...args) => {
|
|
159
|
+
if (do_debug)
|
|
160
|
+
console.debug("[popover]", rnd, ...args);
|
|
161
|
+
};
|
|
162
|
+
// Timer helpers
|
|
163
|
+
function clearTimers() {
|
|
164
|
+
if (showTimer)
|
|
165
|
+
clearTimeout(showTimer);
|
|
166
|
+
if (hideTimer)
|
|
167
|
+
clearTimeout(hideTimer);
|
|
168
|
+
showTimer = null;
|
|
169
|
+
hideTimer = null;
|
|
170
|
+
}
|
|
171
|
+
function cancelHide() {
|
|
172
|
+
if (hideTimer)
|
|
173
|
+
clearTimeout(hideTimer);
|
|
174
|
+
hideTimer = null;
|
|
175
|
+
}
|
|
176
|
+
// Event handlers
|
|
177
|
+
function onEscape(e) {
|
|
178
|
+
if (e.key === "Escape") {
|
|
179
|
+
e.preventDefault();
|
|
180
|
+
e.stopPropagation();
|
|
181
|
+
e.stopImmediatePropagation();
|
|
182
|
+
hide();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
function onClickOutside(e) {
|
|
186
|
+
if (popoverEl &&
|
|
187
|
+
!popoverEl.contains(e.target) &&
|
|
188
|
+
!anchorEl.contains(e.target)) {
|
|
189
|
+
hide();
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
function onClickTrigger(e) {
|
|
193
|
+
e.stopPropagation();
|
|
194
|
+
if (isVisible) {
|
|
195
|
+
hide();
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
show();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// Render content into popover element
|
|
202
|
+
function renderContent() {
|
|
203
|
+
if (!popoverEl || !currentOptions.content)
|
|
204
|
+
return;
|
|
205
|
+
debug("renderContent()", currentOptions.content);
|
|
206
|
+
// Cleanup previous mounted component
|
|
207
|
+
if (mountedComponent) {
|
|
208
|
+
unmount(mountedComponent);
|
|
209
|
+
mountedComponent = null;
|
|
210
|
+
}
|
|
211
|
+
popoverEl.innerHTML = "";
|
|
212
|
+
const content = currentOptions.content;
|
|
213
|
+
if (isSimpleContent(content)) {
|
|
214
|
+
// Simple string/html content
|
|
215
|
+
popoverEl.innerHTML = getStringContent(content);
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
// Complex content (component/snippet) - use mount()
|
|
219
|
+
mountedComponent = mount(PopoverContent, {
|
|
220
|
+
target: popoverEl,
|
|
221
|
+
props: { thc: content },
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
// Show popover
|
|
226
|
+
function show() {
|
|
227
|
+
debug("show()", { enabled: currentOptions.enabled, content: currentOptions.content });
|
|
228
|
+
clearTimers();
|
|
229
|
+
if (!currentOptions.enabled || !currentOptions.content)
|
|
230
|
+
return;
|
|
231
|
+
if (isVisible)
|
|
232
|
+
return;
|
|
233
|
+
// Close other popovers if requested
|
|
234
|
+
if (currentOptions.closeOthers) {
|
|
235
|
+
openPopovers.forEach((hideFn) => {
|
|
236
|
+
if (hideFn !== hide)
|
|
237
|
+
hideFn();
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
// Register this popover
|
|
241
|
+
openPopovers.add(hide);
|
|
242
|
+
isVisible = true;
|
|
243
|
+
anchorEl.setAttribute("aria-expanded", "true");
|
|
244
|
+
const offsetValue = currentOptions.offset || "0.25rem";
|
|
245
|
+
const useAnchorPositioning = isSupported && !currentOptions.forceFallback;
|
|
246
|
+
// Create elements
|
|
247
|
+
if (useAnchorPositioning) {
|
|
248
|
+
// CSS Anchor Positioning mode
|
|
249
|
+
popoverEl = document.createElement("div");
|
|
250
|
+
popoverEl.setAttribute("id", id);
|
|
251
|
+
popoverEl.setAttribute("role", "dialog");
|
|
252
|
+
popoverEl.style.cssText = `
|
|
253
|
+
position: fixed;
|
|
254
|
+
position-anchor: ${anchorName};
|
|
255
|
+
position-area: ${POSITION_MAP[currentOptions.position || "bottom"] || "bottom"};
|
|
256
|
+
transition-duration: ${TRANSITION}ms;
|
|
257
|
+
margin: ${offsetValue};
|
|
258
|
+
`;
|
|
259
|
+
popoverEl.classList.add(...twMerge("stuic-popover", _classPopover, currentOptions.class).split(/\s/));
|
|
260
|
+
document.body.appendChild(popoverEl);
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
// Fallback centered modal mode
|
|
264
|
+
if (currentOptions.showBackdrop !== false) {
|
|
265
|
+
backdropEl = document.createElement("div");
|
|
266
|
+
backdropEl.classList.add(...twMerge("stuic-popover-backdrop", _classBackdrop).split(/\s/));
|
|
267
|
+
backdropEl.style.cssText = `transition-duration: ${TRANSITION}ms;`;
|
|
268
|
+
document.body.appendChild(backdropEl);
|
|
269
|
+
// Backdrop click closes popover
|
|
270
|
+
if (currentOptions.closeOnClickOutside !== false) {
|
|
271
|
+
backdropEl.addEventListener("click", hide);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
// Create wrapper for centering
|
|
275
|
+
wrapperEl = document.createElement("div");
|
|
276
|
+
wrapperEl.classList.add("stuic-popover-wrapper");
|
|
277
|
+
wrapperEl.style.cssText = `
|
|
278
|
+
position: fixed;
|
|
279
|
+
inset: 0;
|
|
280
|
+
display: flex;
|
|
281
|
+
align-items: center;
|
|
282
|
+
justify-content: center;
|
|
283
|
+
z-index: 50;
|
|
284
|
+
pointer-events: none;
|
|
285
|
+
`;
|
|
286
|
+
// Create popover element
|
|
287
|
+
popoverEl = document.createElement("div");
|
|
288
|
+
popoverEl.setAttribute("id", id);
|
|
289
|
+
popoverEl.setAttribute("role", "dialog");
|
|
290
|
+
popoverEl.style.cssText = `
|
|
291
|
+
position: relative;
|
|
292
|
+
max-width: 90vw;
|
|
293
|
+
max-height: 90vh;
|
|
294
|
+
overflow: auto;
|
|
295
|
+
transition-duration: ${TRANSITION}ms;
|
|
296
|
+
pointer-events: auto;
|
|
297
|
+
`;
|
|
298
|
+
popoverEl.classList.add(...twMerge("stuic-popover-fallback", _classPopover, currentOptions.class).split(/\s/));
|
|
299
|
+
wrapperEl.appendChild(popoverEl);
|
|
300
|
+
document.body.appendChild(wrapperEl);
|
|
301
|
+
// Click on wrapper (outside popover) closes
|
|
302
|
+
if (currentOptions.closeOnClickOutside !== false) {
|
|
303
|
+
wrapperEl.addEventListener("click", (e) => {
|
|
304
|
+
if (e.target === wrapperEl)
|
|
305
|
+
hide();
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
// Render content
|
|
310
|
+
renderContent();
|
|
311
|
+
// Transition in
|
|
312
|
+
popoverEl.classList.add("pop-block");
|
|
313
|
+
requestAnimationFrame(() => {
|
|
314
|
+
popoverEl?.classList.add("pop-visible");
|
|
315
|
+
backdropEl?.classList.add("pop-visible");
|
|
316
|
+
currentOptions.onShow?.();
|
|
317
|
+
});
|
|
318
|
+
// Add event listeners
|
|
319
|
+
if (currentOptions.closeOnEscape !== false) {
|
|
320
|
+
document.addEventListener("keydown", onEscape);
|
|
321
|
+
}
|
|
322
|
+
// For hover mode, allow hovering over the popover itself
|
|
323
|
+
if (currentOptions.trigger === "hover" && popoverEl) {
|
|
324
|
+
popoverEl.addEventListener("mouseenter", cancelHide);
|
|
325
|
+
popoverEl.addEventListener("mouseleave", scheduleHide);
|
|
326
|
+
}
|
|
327
|
+
// For click mode with closeOnClickOutside
|
|
328
|
+
if (currentOptions.trigger === "click" &&
|
|
329
|
+
currentOptions.closeOnClickOutside !== false &&
|
|
330
|
+
useAnchorPositioning) {
|
|
331
|
+
// Delay adding click listener to avoid immediate close
|
|
332
|
+
setTimeout(() => {
|
|
333
|
+
document.addEventListener("click", onClickOutside);
|
|
334
|
+
}, 0);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
// Hide popover
|
|
338
|
+
function hide() {
|
|
339
|
+
debug("hide()");
|
|
340
|
+
clearTimers();
|
|
341
|
+
if (!isVisible)
|
|
342
|
+
return;
|
|
343
|
+
isVisible = false;
|
|
344
|
+
// Unregister from open popovers
|
|
345
|
+
openPopovers.delete(hide);
|
|
346
|
+
anchorEl.setAttribute("aria-expanded", "false");
|
|
347
|
+
// Remove event listeners
|
|
348
|
+
document.removeEventListener("keydown", onEscape);
|
|
349
|
+
document.removeEventListener("click", onClickOutside);
|
|
350
|
+
// Transition out
|
|
351
|
+
popoverEl?.classList.remove("pop-visible");
|
|
352
|
+
backdropEl?.classList.remove("pop-visible");
|
|
353
|
+
setTimeout(() => {
|
|
354
|
+
// Cleanup mounted component
|
|
355
|
+
if (mountedComponent) {
|
|
356
|
+
unmount(mountedComponent);
|
|
357
|
+
mountedComponent = null;
|
|
358
|
+
}
|
|
359
|
+
// Remove elements
|
|
360
|
+
popoverEl?.remove();
|
|
361
|
+
backdropEl?.remove();
|
|
362
|
+
wrapperEl?.remove();
|
|
363
|
+
popoverEl = null;
|
|
364
|
+
backdropEl = null;
|
|
365
|
+
wrapperEl = null;
|
|
366
|
+
currentOptions.onHide?.();
|
|
367
|
+
}, TRANSITION);
|
|
368
|
+
}
|
|
369
|
+
function scheduleShow() {
|
|
370
|
+
debug("scheduleShow()");
|
|
371
|
+
clearTimers();
|
|
372
|
+
const delay = currentOptions.showDelay ?? SHOW_DELAY;
|
|
373
|
+
showTimer = setTimeout(show, delay);
|
|
374
|
+
}
|
|
375
|
+
function scheduleHide() {
|
|
376
|
+
debug("scheduleHide()");
|
|
377
|
+
clearTimers();
|
|
378
|
+
const delay = currentOptions.hideDelay ?? HIDE_DELAY;
|
|
379
|
+
hideTimer = setTimeout(hide, delay);
|
|
380
|
+
}
|
|
381
|
+
// Reactive params effect
|
|
382
|
+
$effect(() => {
|
|
383
|
+
const opts = fn?.() || {};
|
|
384
|
+
currentOptions = {
|
|
385
|
+
enabled: opts.enabled ?? true,
|
|
386
|
+
content: opts.content,
|
|
387
|
+
position: opts.position || "bottom",
|
|
388
|
+
trigger: opts.trigger || "click",
|
|
389
|
+
showDelay: opts.showDelay ?? SHOW_DELAY,
|
|
390
|
+
hideDelay: opts.hideDelay ?? HIDE_DELAY,
|
|
391
|
+
class: opts.class,
|
|
392
|
+
offset: opts.offset,
|
|
393
|
+
closeOthers: opts.closeOthers ?? false,
|
|
394
|
+
closeOnClickOutside: opts.closeOnClickOutside ?? true,
|
|
395
|
+
closeOnEscape: opts.closeOnEscape ?? true,
|
|
396
|
+
showBackdrop: opts.showBackdrop ?? true,
|
|
397
|
+
forceFallback: opts.forceFallback ?? false,
|
|
398
|
+
onShow: opts.onShow,
|
|
399
|
+
onHide: opts.onHide,
|
|
400
|
+
debug: opts.debug,
|
|
401
|
+
};
|
|
402
|
+
do_debug = !!opts.debug;
|
|
403
|
+
// Update popover if visible
|
|
404
|
+
if (isVisible && popoverEl) {
|
|
405
|
+
// Update position (only in anchor positioning mode)
|
|
406
|
+
if (isSupported && !currentOptions.forceFallback) {
|
|
407
|
+
popoverEl.style.setProperty("position-area", POSITION_MAP[currentOptions.position || "bottom"] || "bottom");
|
|
408
|
+
}
|
|
409
|
+
// Re-render content
|
|
410
|
+
renderContent();
|
|
411
|
+
}
|
|
412
|
+
// Note: trigger mode change while visible is not fully handled
|
|
413
|
+
// User should close and reopen for trigger mode change to take effect
|
|
414
|
+
});
|
|
415
|
+
// Event listeners effect
|
|
416
|
+
$effect(() => {
|
|
417
|
+
const trigger = currentOptions.trigger || "click";
|
|
418
|
+
if (trigger === "click") {
|
|
419
|
+
anchorEl.addEventListener("click", onClickTrigger);
|
|
420
|
+
}
|
|
421
|
+
else {
|
|
422
|
+
// hover mode
|
|
423
|
+
anchorEl.addEventListener("mouseenter", scheduleShow);
|
|
424
|
+
anchorEl.addEventListener("mouseleave", scheduleHide);
|
|
425
|
+
anchorEl.addEventListener("focus", scheduleShow);
|
|
426
|
+
anchorEl.addEventListener("blur", scheduleHide);
|
|
427
|
+
}
|
|
428
|
+
return () => {
|
|
429
|
+
anchorEl.removeEventListener("click", onClickTrigger);
|
|
430
|
+
anchorEl.removeEventListener("mouseenter", scheduleShow);
|
|
431
|
+
anchorEl.removeEventListener("mouseleave", scheduleHide);
|
|
432
|
+
anchorEl.removeEventListener("focus", scheduleShow);
|
|
433
|
+
anchorEl.removeEventListener("blur", scheduleHide);
|
|
434
|
+
// Cleanup popover on unmount
|
|
435
|
+
if (mountedComponent) {
|
|
436
|
+
unmount(mountedComponent);
|
|
437
|
+
mountedComponent = null;
|
|
438
|
+
}
|
|
439
|
+
popoverEl?.remove();
|
|
440
|
+
backdropEl?.remove();
|
|
441
|
+
wrapperEl?.remove();
|
|
442
|
+
clearTimers();
|
|
443
|
+
// Unregister from open popovers
|
|
444
|
+
openPopovers.delete(hide);
|
|
445
|
+
document.removeEventListener("keydown", onEscape);
|
|
446
|
+
document.removeEventListener("click", onClickOutside);
|
|
447
|
+
};
|
|
448
|
+
});
|
|
449
|
+
}
|
|
@@ -16,7 +16,7 @@ export interface ResizableWidthOptions {
|
|
|
16
16
|
units: "px" | "%";
|
|
17
17
|
container: number;
|
|
18
18
|
}) => void;
|
|
19
|
-
debug?: (...args:
|
|
19
|
+
debug?: (...args: unknown[]) => void;
|
|
20
20
|
}
|
|
21
21
|
/**
|
|
22
22
|
* A Svelte action that makes an element's width resizable via drag handle.
|
|
@@ -73,7 +73,8 @@ export function resizableWidth(el, fn) {
|
|
|
73
73
|
return handle;
|
|
74
74
|
}
|
|
75
75
|
$effect(() => {
|
|
76
|
-
|
|
76
|
+
const { enabled = true, initial: initialValue = 0, min = 0, max = 0, units = "px", key, storage = "session", handleClass = "", handleDragClass = "", onResize, debug, } = fn?.() || {};
|
|
77
|
+
let initial = initialValue;
|
|
77
78
|
const _debug = (...args) => debug?.("[resizable-width]", ...args);
|
|
78
79
|
_debug("$effect");
|
|
79
80
|
if (!enabled)
|
|
@@ -108,7 +109,8 @@ export function resizableWidth(el, fn) {
|
|
|
108
109
|
value = Math.max(min, value);
|
|
109
110
|
if (max)
|
|
110
111
|
value = Math.min(max, value);
|
|
111
|
-
_initial !== value
|
|
112
|
+
if (_initial !== value)
|
|
113
|
+
_debug("clamped", value, units);
|
|
112
114
|
return value;
|
|
113
115
|
};
|
|
114
116
|
let width;
|
|
@@ -132,7 +134,7 @@ export function resizableWidth(el, fn) {
|
|
|
132
134
|
e.preventDefault(); // prevent scrolling on touch devices
|
|
133
135
|
isResizing = true;
|
|
134
136
|
//
|
|
135
|
-
const clientX =
|
|
137
|
+
const clientX = "touches" in e ? e.touches[0].clientX : e.clientX;
|
|
136
138
|
startX = clientX;
|
|
137
139
|
startWidth = parseInt(getComputedStyle(el).width, 10);
|
|
138
140
|
containerW = container.offsetWidth;
|
|
@@ -145,9 +147,9 @@ export function resizableWidth(el, fn) {
|
|
|
145
147
|
return;
|
|
146
148
|
e.preventDefault(); // prevent scrolling on touch devices
|
|
147
149
|
//
|
|
148
|
-
const clientX =
|
|
150
|
+
const clientX = "touches" in e ? e.touches[0].clientX : e.clientX;
|
|
149
151
|
const deltaX = clientX - startX;
|
|
150
|
-
|
|
152
|
+
const width = startWidth + deltaX;
|
|
151
153
|
set_width_px(width);
|
|
152
154
|
}
|
|
153
155
|
function resize_stop() {
|
|
@@ -41,13 +41,8 @@
|
|
|
41
41
|
/* position-area is set via inline style based on position param */
|
|
42
42
|
/* fallbacks ensure tooltip stays within viewport */
|
|
43
43
|
position-try-fallbacks:
|
|
44
|
-
--tt-top-span-right,
|
|
45
|
-
--tt-
|
|
46
|
-
flip-block,
|
|
47
|
-
--tt-bottom-span-right,
|
|
48
|
-
--tt-bottom-span-left,
|
|
49
|
-
--tt-left,
|
|
50
|
-
--tt-right;
|
|
44
|
+
--tt-top-span-right, --tt-top-span-left, flip-block, --tt-bottom-span-right,
|
|
45
|
+
--tt-bottom-span-left, --tt-left, --tt-right;
|
|
51
46
|
|
|
52
47
|
&.tt-block {
|
|
53
48
|
display: block;
|
|
@@ -103,7 +103,8 @@ export function tooltip(anchorEl, fn) {
|
|
|
103
103
|
anchorEl.setAttribute("aria-describedby", id);
|
|
104
104
|
anchorEl.setAttribute("aria-expanded", "false");
|
|
105
105
|
const debug = (...args) => {
|
|
106
|
-
|
|
106
|
+
if (do_debug)
|
|
107
|
+
console.debug("[tooltip]", rnd, ...args);
|
|
107
108
|
};
|
|
108
109
|
function ensure_tooltip() {
|
|
109
110
|
debug("ensure_tooltip()", content, classTooltip);
|
|
@@ -132,7 +133,7 @@ export function tooltip(anchorEl, fn) {
|
|
|
132
133
|
// update position-area in case it changed
|
|
133
134
|
tooltipEl.style.setProperty("position-area", POSITION_MAP[position] || "top");
|
|
134
135
|
if (classTooltip) {
|
|
135
|
-
|
|
136
|
+
const old = tooltipEl.className;
|
|
136
137
|
tooltipEl.className = ""; // reset
|
|
137
138
|
tooltipEl.classList.add(...twMerge(old, classTooltip).split(/\s/));
|
|
138
139
|
}
|
|
@@ -201,12 +202,12 @@ export function tooltip(anchorEl, fn) {
|
|
|
201
202
|
}
|
|
202
203
|
// "reactive" params re/set
|
|
203
204
|
$effect(() => {
|
|
204
|
-
|
|
205
|
+
const { enabled: _enabled, content: _content, position: _position, debug: debugParam, class: _classTooltip, onShow, onHide, } = fn?.() || {};
|
|
205
206
|
// re/assign new params
|
|
206
207
|
do_debug = !!debugParam;
|
|
207
208
|
on_show = onShow;
|
|
208
209
|
on_hide = onHide;
|
|
209
|
-
content = _content
|
|
210
|
+
content = _content || anchorEl.getAttribute("aria-label");
|
|
210
211
|
classTooltip = _classTooltip;
|
|
211
212
|
enabled = _enabled ?? true;
|
|
212
213
|
position = _position || "top";
|
|
@@ -24,8 +24,8 @@ export function trim(el, fn) {
|
|
|
24
24
|
// the node has been mounted in the DOM
|
|
25
25
|
$effect(() => {
|
|
26
26
|
// setup goes here
|
|
27
|
-
|
|
28
|
-
function trim(
|
|
27
|
+
const { enabled, setValue } = fn?.() || { enabled: true };
|
|
28
|
+
function trim() {
|
|
29
29
|
if (enabled && typeof el.value === "string") {
|
|
30
30
|
el.value = el.value.trim();
|
|
31
31
|
setValue?.(el.value);
|
|
@@ -71,14 +71,14 @@ export interface ValidationResult {
|
|
|
71
71
|
valid: boolean;
|
|
72
72
|
message: string;
|
|
73
73
|
}
|
|
74
|
-
type ReasonTranslate = (reason: keyof ValidityStateFlags, value:
|
|
74
|
+
type ReasonTranslate = (reason: keyof ValidityStateFlags, value: unknown, fallback: string) => string;
|
|
75
75
|
/**
|
|
76
76
|
* Options for the validate action.
|
|
77
77
|
*/
|
|
78
78
|
export interface ValidateOptions {
|
|
79
79
|
enabled?: boolean;
|
|
80
|
-
context?: Record<string,
|
|
81
|
-
customValidator?: (value:
|
|
80
|
+
context?: Record<string, unknown>;
|
|
81
|
+
customValidator?: (value: unknown, context: Record<string, unknown> | undefined, el: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement) => string | undefined;
|
|
82
82
|
on?: "input" | "change";
|
|
83
83
|
setValidationResult?: (res: ValidationResult) => void;
|
|
84
84
|
t?: false | ReasonTranslate;
|
|
@@ -148,6 +148,6 @@ export interface ValidateOptions {
|
|
|
148
148
|
*/
|
|
149
149
|
export declare function validate(el: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement, fn?: () => boolean | ValidateOptions): void;
|
|
150
150
|
export declare namespace validate {
|
|
151
|
-
var t: null;
|
|
151
|
+
var t: ReasonTranslate | null;
|
|
152
152
|
}
|
|
153
153
|
export {};
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { tick } from "svelte";
|
|
2
|
-
import { waitForNextRepaint, waitForTwoRepaints } from "../utils/paint.js";
|
|
3
1
|
/**
|
|
4
2
|
* Creates a ValidityStateLike object for testing/mocking validation states.
|
|
5
3
|
*
|
|
@@ -133,8 +131,8 @@ const KNOWN_REASONS = [
|
|
|
133
131
|
export function validate(el, fn) {
|
|
134
132
|
$effect(() => {
|
|
135
133
|
//
|
|
136
|
-
|
|
137
|
-
|
|
134
|
+
const fnResult = fn?.() ?? {};
|
|
135
|
+
const { enabled, context, customValidator, on = "change", setValidationResult, t, } = typeof fnResult === "boolean" ? { enabled: !!fnResult } : fnResult;
|
|
138
136
|
//
|
|
139
137
|
const _t = (reason, value, fallback) => {
|
|
140
138
|
// explicit false
|
|
@@ -187,10 +185,13 @@ export function validate(el, fn) {
|
|
|
187
185
|
el.addEventListener(on, _doValidate);
|
|
188
186
|
//
|
|
189
187
|
let _touchCount = 0;
|
|
190
|
-
const onFocus = (
|
|
188
|
+
const onFocus = () => _touchCount++;
|
|
191
189
|
el.addEventListener("focus", onFocus);
|
|
192
190
|
// also validate on first blur
|
|
193
|
-
const onBlur = (
|
|
191
|
+
const onBlur = () => {
|
|
192
|
+
if (_touchCount === 1)
|
|
193
|
+
_doValidate();
|
|
194
|
+
};
|
|
194
195
|
el.addEventListener("blur", onBlur);
|
|
195
196
|
return () => {
|
|
196
197
|
el.removeEventListener(on, _doValidate);
|
|
@@ -199,6 +200,5 @@ export function validate(el, fn) {
|
|
|
199
200
|
};
|
|
200
201
|
});
|
|
201
202
|
}
|
|
202
|
-
//
|
|
203
|
-
|
|
204
|
-
validate.t = t;
|
|
203
|
+
// Global translation function - can be set by consumers
|
|
204
|
+
validate.t = null;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Component } from "svelte";
|
|
1
2
|
import type { THC } from "../Thc/Thc.svelte";
|
|
2
3
|
/**
|
|
3
4
|
* Types of alert/confirm/prompt dialogs.
|
|
@@ -12,13 +13,13 @@ export declare enum AlertConfirmPromptType {
|
|
|
12
13
|
*/
|
|
13
14
|
export type AlertConfirmPromptVariant = "info" | "success" | "warn" | "error";
|
|
14
15
|
/** Callback type for OK button click. */
|
|
15
|
-
export type FnOnOK = (value: any) =>
|
|
16
|
+
export type FnOnOK = (value: any) => void;
|
|
16
17
|
/** Callback type for Cancel button click. */
|
|
17
|
-
export type FnOnCancel = (value: false) =>
|
|
18
|
+
export type FnOnCancel = (value: false) => void;
|
|
18
19
|
/** Callback type for Escape key press. */
|
|
19
20
|
export type FnOnEscape = () => void;
|
|
20
21
|
/** Callback type for custom button click. */
|
|
21
|
-
export type FnOnCustom = (value: any) =>
|
|
22
|
+
export type FnOnCustom = (value: any) => void;
|
|
22
23
|
/**
|
|
23
24
|
* Configuration object for an alert/confirm/prompt dialog.
|
|
24
25
|
*/
|
|
@@ -38,9 +39,9 @@ export interface AlertConfirmPromptObj extends Record<string, any> {
|
|
|
38
39
|
variant: AlertConfirmPromptVariant;
|
|
39
40
|
iconFn: (() => string) | boolean;
|
|
40
41
|
forceAsHtml?: boolean;
|
|
41
|
-
CmpButtonOk?:
|
|
42
|
-
CmpButtonCancel?:
|
|
43
|
-
CmpButtonCustom?:
|
|
42
|
+
CmpButtonOk?: Component;
|
|
43
|
+
CmpButtonCancel?: Component;
|
|
44
|
+
CmpButtonCustom?: Component;
|
|
44
45
|
}
|
|
45
46
|
/**
|
|
46
47
|
* A reactive stack manager for alert, confirm, and prompt dialogs.
|