jsswift 2.0.1 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,333 @@
1
+ UI.Dialog = (...args) => {
2
+ const { props, children } = JSswift.uiNormalizeArgs(args);
3
+ const slots = props.slots || {};
4
+ const stateList = ["primary", "secondary", "warning", "danger", "success", "info", "light", "dark"];
5
+ const sizeList = ["xs", "sm", "md", "lg", "xl", "full"];
6
+ let currentProps = { ...props };
7
+ let entry = null;
8
+ let lastActive = null;
9
+ const resolveRender = (value, ctx) => typeof value === "function" ? value(ctx) : value;
10
+ const splitClasses = (value) => String(value || "").split(/\s+/).filter(Boolean);
11
+ const setTrackedClasses = (target, key, classes) => {
12
+ if (!target) return;
13
+ const prev = target[key] || [];
14
+ if (prev.length) target.classList.remove(...prev);
15
+ const next = (classes || []).filter(Boolean);
16
+ if (next.length) target.classList.add(...next);
17
+ target[key] = next;
18
+ };
19
+ const setStyleValue = (target, name, value, formatter = (v) => v) => {
20
+ if (!target) return;
21
+ if (value == null || value === "") {
22
+ target.style.removeProperty(name);
23
+ return;
24
+ }
25
+ target.style.setProperty(name, formatter(value));
26
+ };
27
+ const fullscreenViewportSize = () => {
28
+ const vv = window.visualViewport;
29
+ const left = Math.max(0, Math.round(vv?.offsetLeft ?? 0));
30
+ const top = Math.max(0, Math.round(vv?.offsetTop ?? 0));
31
+ const width = Math.max(0, Math.round(vv?.width ?? window.innerWidth ?? 0));
32
+ const height = Math.max(0, Math.round(vv?.height ?? window.innerHeight ?? 0));
33
+ return { left, top, width, height };
34
+ };
35
+ const syncFullscreenViewport = (currentEntry) => {
36
+ if (!currentEntry?.panel) return;
37
+ const { left, top, width, height } = fullscreenViewportSize();
38
+ setStyleValue(currentEntry.panel, "position", "fixed");
39
+ setStyleValue(currentEntry.panel, "inset", 0, (v) => `${v}px`);
40
+ setStyleValue(currentEntry.panel, "top", top, (v) => `${v}px`);
41
+ setStyleValue(currentEntry.panel, "left", left, (v) => `${v}px`);
42
+ setStyleValue(currentEntry.panel, "right", "auto");
43
+ setStyleValue(currentEntry.panel, "bottom", "auto");
44
+ setStyleValue(currentEntry.panel, "width", width, (v) => `${v}px`);
45
+ setStyleValue(currentEntry.panel, "height", height, (v) => `${v}px`);
46
+ setStyleValue(currentEntry.panel, "min-height", height, (v) => `${v}px`);
47
+ setStyleValue(currentEntry.panel, "max-width", width, (v) => `${v}px`);
48
+ setStyleValue(currentEntry.panel, "max-height", height, (v) => `${v}px`);
49
+ setStyleValue(currentEntry.panel, "min-width", 0, (v) => `${v}px`);
50
+ setStyleValue(currentEntry.panel, "margin", 0, (v) => `${v}px`);
51
+ setStyleValue(currentEntry.panel, "box-sizing", "border-box");
52
+ setStyleValue(currentEntry.panel, "border-width", 0, (v) => `${v}px`);
53
+ setStyleValue(currentEntry.panel, "border-radius", 0, (v) => `${v}px`);
54
+ setStyleValue(currentEntry.panel, "transform", "none");
55
+ setStyleValue(currentEntry.panel, "overflow", "hidden");
56
+ setStyleValue(currentEntry.panel, "--jss-dialog-fullscreen-left", left, (v) => `${v}px`);
57
+ setStyleValue(currentEntry.panel, "--jss-dialog-fullscreen-top", top, (v) => `${v}px`);
58
+ setStyleValue(currentEntry.panel, "--jss-dialog-fullscreen-width", width, (v) => `${v}px`);
59
+ setStyleValue(currentEntry.panel, "--jss-dialog-fullscreen-height", height, (v) => `${v}px`);
60
+ };
61
+ const clearFullscreenViewport = (currentEntry) => {
62
+ if (!currentEntry?.panel) return;
63
+ setStyleValue(currentEntry.panel, "position", null);
64
+ setStyleValue(currentEntry.panel, "inset", null);
65
+ setStyleValue(currentEntry.panel, "top", null);
66
+ setStyleValue(currentEntry.panel, "left", null);
67
+ setStyleValue(currentEntry.panel, "right", null);
68
+ setStyleValue(currentEntry.panel, "bottom", null);
69
+ setStyleValue(currentEntry.panel, "width", null);
70
+ setStyleValue(currentEntry.panel, "height", null);
71
+ setStyleValue(currentEntry.panel, "min-height", null);
72
+ setStyleValue(currentEntry.panel, "max-width", null);
73
+ setStyleValue(currentEntry.panel, "max-height", null);
74
+ setStyleValue(currentEntry.panel, "min-width", null);
75
+ setStyleValue(currentEntry.panel, "margin", null);
76
+ setStyleValue(currentEntry.panel, "box-sizing", null);
77
+ setStyleValue(currentEntry.panel, "border-width", null);
78
+ setStyleValue(currentEntry.panel, "border-radius", null);
79
+ setStyleValue(currentEntry.panel, "transform", null);
80
+ setStyleValue(currentEntry.panel, "overflow", null);
81
+ setStyleValue(currentEntry.panel, "--jss-dialog-fullscreen-left", null);
82
+ setStyleValue(currentEntry.panel, "--jss-dialog-fullscreen-top", null);
83
+ setStyleValue(currentEntry.panel, "--jss-dialog-fullscreen-width", null);
84
+ setStyleValue(currentEntry.panel, "--jss-dialog-fullscreen-height", null);
85
+ };
86
+ const ensureFullscreenViewportTracking = (currentEntry, enabled) => {
87
+ if (!currentEntry) return;
88
+ currentEntry._dialogViewportCleanup?.();
89
+ currentEntry._dialogViewportCleanup = null;
90
+ if (!enabled) {
91
+ clearFullscreenViewport(currentEntry);
92
+ return;
93
+ }
94
+ let frameId = null;
95
+ const raf = globalThis.requestAnimationFrame || ((fn) => setTimeout(() => fn(Date.now()), 0));
96
+ const caf = globalThis.cancelAnimationFrame || clearTimeout;
97
+ const sync = () => syncFullscreenViewport(currentEntry);
98
+ const scheduleSync = () => {
99
+ if (frameId != null) caf(frameId);
100
+ frameId = raf(() => {
101
+ frameId = null;
102
+ sync();
103
+ });
104
+ };
105
+ sync();
106
+ scheduleSync();
107
+ window.addEventListener("resize", sync);
108
+ window.addEventListener("orientationchange", scheduleSync);
109
+ window.visualViewport?.addEventListener?.("resize", sync);
110
+ window.visualViewport?.addEventListener?.("scroll", scheduleSync);
111
+ currentEntry._dialogViewportCleanup = () => {
112
+ if (frameId != null) {
113
+ caf(frameId);
114
+ frameId = null;
115
+ }
116
+ window.removeEventListener("resize", sync);
117
+ window.removeEventListener("orientationchange", scheduleSync);
118
+ window.visualViewport?.removeEventListener?.("resize", sync);
119
+ window.visualViewport?.removeEventListener?.("scroll", scheduleSync);
120
+ };
121
+ };
122
+ const getOptions = () => currentProps;
123
+ const getStateClass = (opts) => {
124
+ const value = uiUnwrap(opts.state ?? opts.color);
125
+ if (!value) return "";
126
+ return stateList.includes(value) ? value : String(value);
127
+ };
128
+ const getSizeClass = (opts) => {
129
+ const value = uiUnwrap(opts.size);
130
+ return (typeof value === "string" && sizeList.includes(value)) ? value : "";
131
+ };
132
+ const getAlignClass = (opts) => {
133
+ const raw = String(uiUnwrap(opts.actionsAlign ?? opts.footerAlign ?? opts.alignActions ?? "end")).toLowerCase();
134
+ if (["start", "left"].includes(raw)) return "start";
135
+ if (["center", "middle"].includes(raw)) return "center";
136
+ if (["between", "space-between"].includes(raw)) return "between";
137
+ if (["stretch", "full"].includes(raw)) return "stretch";
138
+ return "end";
139
+ };
140
+ const getVerticalAlignClass = (opts) => {
141
+ const raw = String(uiUnwrap(opts.align ?? opts.verticalAlign ?? opts.position ?? "center")).toLowerCase();
142
+ if (["top", "start", "flex-start"].includes(raw)) return "jss-dialog-align-top";
143
+ if (["bottom", "end", "flex-end"].includes(raw)) return "jss-dialog-align-bottom";
144
+ return "";
145
+ };
146
+ const isClosable = (opts) => {
147
+ const value = opts.closable ?? opts.dismissible ?? opts.closeButton;
148
+ return value !== false;
149
+ };
150
+ const buildContent = () => {
151
+ const opts = getOptions();
152
+ const ctx = {
153
+ close,
154
+ dismiss: close,
155
+ open,
156
+ toggle,
157
+ update,
158
+ isOpen,
159
+ entry: () => entry,
160
+ props: opts
161
+ };
162
+ const iconFallback = opts.icon != null
163
+ ? (typeof opts.icon === "string"
164
+ ? UI.Icon({ name: opts.icon, size: opts.iconSize || "md" })
165
+ : JSswift.ui.slot(opts.icon, { as: "icon" }))
166
+ : null;
167
+ const eyebrowNodes = renderSlotToArray(slots, "eyebrow", ctx, resolveRender(opts.eyebrow, ctx));
168
+ const titleNodes = renderSlotToArray(slots, "title", ctx, resolveRender(opts.title ?? opts.heading ?? opts.header, ctx));
169
+ const subtitleNodes = renderSlotToArray(slots, "subtitle", ctx, resolveRender(opts.subtitle ?? opts.description, ctx));
170
+ const iconNodes = renderSlotToArray(slots, "icon", ctx, iconFallback);
171
+ const customHeaderNodes = renderSlotToArray(slots, "header", ctx, resolveRender(opts.headerContent ?? opts.head, ctx));
172
+ const closeSlotNodes = isClosable(opts)
173
+ ? renderSlotToArray(slots, "close", ctx, UI.Btn({
174
+ class: "jss-dialog-close",
175
+ size: "sm",
176
+ outline: true,
177
+ "aria-label": opts.closeLabel || "Chiudi dialog",
178
+ "data-dialog-close": true
179
+ }, UI.Icon({ name: opts.closeIcon || "close", size: "sm" })))
180
+ : [];
181
+ const bodyRaw = opts.content ?? opts.body ?? opts.message ?? (children && children.length ? children : null);
182
+ let contentNodes = renderSlotToArray(slots, "content", ctx, resolveRender(bodyRaw, ctx));
183
+ if (!contentNodes.length) contentNodes = renderSlotToArray(slots, "body", ctx, resolveRender(bodyRaw, ctx));
184
+ if (!contentNodes.length) contentNodes = renderSlotToArray(slots, "default", ctx, resolveRender(bodyRaw, ctx));
185
+ const footerRaw = resolveRender(opts.footer ?? opts.actions, ctx);
186
+ let footerNodes = renderSlotToArray(slots, "footer", ctx, footerRaw);
187
+ if (!footerNodes.length) footerNodes = renderSlotToArray(slots, "actions", ctx, footerRaw);
188
+
189
+ let headerEl = null;
190
+ if (customHeaderNodes.length) {
191
+ headerEl = _.div({ class: "jss-dialog-head jss-dialog-head-custom" }, ...customHeaderNodes);
192
+ } else if (eyebrowNodes.length || titleNodes.length || subtitleNodes.length || iconNodes.length || closeSlotNodes.length) {
193
+ headerEl = _.div(
194
+ { class: "jss-dialog-head" },
195
+ iconNodes.length ? _.div({ class: "jss-dialog-icon" }, ...iconNodes) : null,
196
+ _.div(
197
+ { class: "jss-dialog-head-main" },
198
+ eyebrowNodes.length ? _.div({ class: "jss-dialog-eyebrow" }, ...eyebrowNodes) : null,
199
+ titleNodes.length ? _.div({ class: "jss-dialog-title" }, ...titleNodes) : null,
200
+ subtitleNodes.length ? _.div({ class: "jss-dialog-subtitle" }, ...subtitleNodes) : null
201
+ ),
202
+ closeSlotNodes.length ? _.div({ class: "jss-dialog-close-wrap" }, ...closeSlotNodes) : null
203
+ );
204
+ }
205
+
206
+ const bodyEl = _.div(
207
+ { class: uiClass(["jss-dialog-body", opts.bodyClass]) },
208
+ ...contentNodes
209
+ );
210
+ const footerEl = footerNodes.length
211
+ ? _.div({
212
+ class: uiClass([
213
+ "jss-dialog-actions",
214
+ `is-${getAlignClass(opts)}`,
215
+ uiWhen(opts.stackActions, "is-stacked"),
216
+ opts.actionsClass,
217
+ opts.footerClass
218
+ ])
219
+ }, ...footerNodes)
220
+ : null;
221
+
222
+ return _.div({
223
+ class: uiClass([
224
+ "jss-dialog-shell",
225
+ uiWhen(!!headerEl, "has-head"),
226
+ uiWhen(!!footerEl, "has-footer"),
227
+ uiWhen(opts.divider !== false, "with-divider")
228
+ ])
229
+ }, ...[headerEl, bodyEl, footerEl].filter(Boolean));
230
+ };
231
+ const applyEntryOptions = (currentEntry) => {
232
+ if (!currentEntry?.panel) return;
233
+ const opts = getOptions();
234
+ const stateClass = getStateClass(opts);
235
+ const sizeClass = getSizeClass(opts);
236
+ setTrackedClasses(currentEntry.panel, "_dialogClassTokens", [
237
+ "jss-dialog",
238
+ "jss-singularity",
239
+ "jss-clear-set",
240
+ sizeClass ? `jss-dialog-${sizeClass}` : "",
241
+ stateClass,
242
+ stateClass ? `jss-state-${stateClass}` : "",
243
+ uiUnwrap(opts.fullscreen) ? "fullscreen" : "",
244
+ uiUnwrap(opts.scrollable) ? "scrollable" : "",
245
+ uiUnwrap(opts.stickyHeader) ? "sticky-head" : "",
246
+ uiUnwrap(opts.stickyActions) ? "sticky-actions" : "",
247
+ uiUnwrap(opts.borderless) ? "borderless" : "",
248
+ ...splitClasses(opts.class),
249
+ ...splitClasses(opts.panelClass)
250
+ ]);
251
+ setTrackedClasses(currentEntry.overlay, "_dialogOverlayClassTokens", [
252
+ getVerticalAlignClass(opts),
253
+ uiUnwrap(opts.backdropBlur) ? "jss-dialog-overlay-blur" : "",
254
+ ...splitClasses(opts.overlayClass)
255
+ ]);
256
+ setStyleValue(currentEntry.panel, "--jss-dialog-width", uiUnwrap(opts.width), toCssSize);
257
+ setStyleValue(currentEntry.panel, "--jss-dialog-min-width", uiUnwrap(opts.minWidth), toCssSize);
258
+ setStyleValue(currentEntry.panel, "--jss-dialog-max-width", uiUnwrap(opts.maxWidth), toCssSize);
259
+ setStyleValue(currentEntry.panel, "--jss-dialog-max-height", uiUnwrap(opts.maxHeight), toCssSize);
260
+ setStyleValue(currentEntry.panel, "--jss-dialog-body-max-height", uiUnwrap(opts.bodyMaxHeight ?? opts.contentMaxHeight), toCssSize);
261
+ if (opts.style) Object.assign(currentEntry.panel.style, opts.style);
262
+ setPropertyProps(currentEntry.panel, opts);
263
+ ensureFullscreenViewportTracking(currentEntry, uiUnwrap(opts.fullscreen));
264
+ currentEntry.panel.setAttribute("role", opts.role || "dialog");
265
+ currentEntry.panel.setAttribute("aria-modal", opts.modal === false ? "false" : "true");
266
+ if (opts.ariaLabel) currentEntry.panel.setAttribute("aria-label", opts.ariaLabel);
267
+ else currentEntry.panel.removeAttribute("aria-label");
268
+ };
269
+ const renderOpenContent = () => {
270
+ if (!entry?.panel) return;
271
+ entry.panel.replaceChildren(buildContent());
272
+ };
273
+ const update = (nextProps = {}) => {
274
+ if (nextProps && typeof nextProps === "object") currentProps = { ...currentProps, ...nextProps };
275
+ if (entry) {
276
+ applyEntryOptions(entry);
277
+ renderOpenContent();
278
+ }
279
+ return api;
280
+ };
281
+ const open = (nextProps = null) => {
282
+ if (nextProps && typeof nextProps === "object") currentProps = { ...currentProps, ...nextProps };
283
+ if (entry) {
284
+ applyEntryOptions(entry);
285
+ renderOpenContent();
286
+ return entry;
287
+ }
288
+ lastActive = document.activeElement;
289
+ const opts = getOptions();
290
+ const persistent = opts.persistent === true;
291
+ entry = JSswift.overlay.open(() => buildContent(), {
292
+ type: "dialog",
293
+ backdrop: opts.backdrop !== false,
294
+ lockScroll: opts.lockScroll !== false,
295
+ trapFocus: opts.trapFocus !== false,
296
+ autoFocus: opts.autoFocus !== false,
297
+ closeOnOutside: opts.closeOnOutside ?? !persistent,
298
+ closeOnBackdrop: opts.closeOnBackdrop ?? !persistent,
299
+ closeOnEsc: opts.closeOnEsc ?? !persistent,
300
+ className: uiClassStatic(["jss-dialog"]),
301
+ onClose: () => {
302
+ entry?._dialogViewportCleanup?.();
303
+ entry?.panel?.removeEventListener("click", onPanelClick);
304
+ entry = null;
305
+ getOptions().onClose?.();
306
+ if (lastActive && typeof lastActive.focus === "function") {
307
+ setTimeout(() => lastActive.focus(), 0);
308
+ }
309
+ }
310
+ });
311
+ const onPanelClick = (e) => {
312
+ const target = e.target;
313
+ if (target && target.closest && target.closest("[data-dialog-close]")) close();
314
+ };
315
+ entry.panel.addEventListener("click", onPanelClick);
316
+ applyEntryOptions(entry);
317
+ overlayEnter(entry);
318
+ getOptions().onOpen?.(entry);
319
+ return entry;
320
+ };
321
+
322
+ const close = () => {
323
+ if (!entry) return;
324
+ const toClose = entry;
325
+ overlayLeave(toClose, () => JSswift.overlay.close(toClose.id));
326
+ };
327
+
328
+ const isOpen = () => !!entry;
329
+ const toggle = (nextProps = null) => isOpen() ? (close(), null) : open(nextProps);
330
+ const api = { open, close, toggle, update, isOpen, entry: () => entry, props: () => ({ ...currentProps }) };
331
+
332
+ return api;
333
+ };