@jsswift/ui 1.0.41
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/dist/css/animation.css +121 -0
- package/dist/css/base.css +621 -0
- package/dist/css/docs.css +216 -0
- package/dist/css/min-ui.css +1 -0
- package/dist/css/responsive.css +3259 -0
- package/dist/css/ui-components.css +8757 -0
- package/dist/css/ui.css +12765 -0
- package/dist/css/ui.min.css +1 -0
- package/dist/fonts/Nunito-Black.ttf +0 -0
- package/dist/fonts/Nunito-BlackItalic.ttf +0 -0
- package/dist/fonts/Nunito-Bold.ttf +0 -0
- package/dist/fonts/Nunito-BoldItalic.ttf +0 -0
- package/dist/fonts/Nunito-ExtraBold.ttf +0 -0
- package/dist/fonts/Nunito-ExtraBoldItalic.ttf +0 -0
- package/dist/fonts/Nunito-ExtraLight.ttf +0 -0
- package/dist/fonts/Nunito-ExtraLightItalic.ttf +0 -0
- package/dist/fonts/Nunito-Italic.ttf +0 -0
- package/dist/fonts/Nunito-Light.ttf +0 -0
- package/dist/fonts/Nunito-LightItalic.ttf +0 -0
- package/dist/fonts/Nunito-Medium.ttf +0 -0
- package/dist/fonts/Nunito-MediumItalic.ttf +0 -0
- package/dist/fonts/Nunito-Regular.ttf +0 -0
- package/dist/fonts/Nunito-SemiBold.ttf +0 -0
- package/dist/fonts/Nunito-SemiBoldItalic.ttf +0 -0
- package/dist/img/logo.svg +131 -0
- package/dist/img/svg/tabler-icons-sprite.svg +39534 -0
- package/dist/min-ui.js +34 -0
- package/dist/ui.js +21027 -0
- package/dist/ui.min.js +34 -0
- package/package.json +40 -0
- package/src/00-bootstrap.js +330 -0
- package/src/01-foundation-helpers.js +1208 -0
- package/src/10-primitives-layout.js +5972 -0
- package/src/20-display-content.js +1014 -0
- package/src/21-list-content.js +341 -0
- package/src/22-banner-content.js +489 -0
- package/src/23-tooltip.js +326 -0
- package/src/31-form-advanced.js +3149 -0
- package/src/40-navigation.js +1454 -0
- package/src/41-tab-panel.js +1090 -0
- package/src/50-feedback.js +1209 -0
- package/src/51-feedback-service.js +166 -0
- package/src/60-shell-app.js +1483 -0
- package/src/61-form-service.js +450 -0
- package/src/70-dialog.js +401 -0
- package/src/71-menu-overlays.js +1442 -0
- package/src/80-data.js +503 -0
- package/src/README.md +159 -0
- package/src/css/README.md +17 -0
- package/src/css/animation.css +121 -0
- package/src/css/base.css +621 -0
- package/src/css/docs.css +216 -0
- package/src/css/responsive.css +3259 -0
- package/src/css/ui-components.css +8757 -0
- package/src/modules.json +176 -0
|
@@ -0,0 +1,1090 @@
|
|
|
1
|
+
UI.TabPanel = (...args) => {
|
|
2
|
+
const { props, children } = JSswift.uiNormalizeArgs(args);
|
|
3
|
+
const slots = props.slots || {};
|
|
4
|
+
const rawTabs = Array.isArray(props.tabs)
|
|
5
|
+
? props.tabs
|
|
6
|
+
: (Array.isArray(props.items) ? props.items : []);
|
|
7
|
+
const model = resolveModel(props.model, "UI.TabPanel:model");
|
|
8
|
+
const componentId = props.id || (`cms-tabpanel-` + Math.random().toString(36).slice(2, 9));
|
|
9
|
+
const normalizeOrientation = (value) => String(uiUnwrap(value) || "vertical").toLowerCase() === "horizontal" ? "horizontal" : "vertical";
|
|
10
|
+
const normalizePosition = (value) => String(uiUnwrap(value) || "before").toLowerCase() === "after" ? "after" : "before";
|
|
11
|
+
const normalizeVariant = (value) => {
|
|
12
|
+
const raw = String(uiUnwrap(value) || "").toLowerCase();
|
|
13
|
+
if (raw === "pills" || raw === "pill") return "pills";
|
|
14
|
+
if (raw === "soft" || raw === "card") return "soft";
|
|
15
|
+
return "line";
|
|
16
|
+
};
|
|
17
|
+
const resolveAccent = (value) => {
|
|
18
|
+
const raw = uiUnwrap(value);
|
|
19
|
+
if (raw == null || raw === "") return null;
|
|
20
|
+
return JSswift.uiColors.includes(raw) ? `var(--cms-${raw})` : String(raw);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const orientation = normalizeOrientation(props.orientation || props.orient || props.direction);
|
|
24
|
+
const navPosition = normalizePosition(props.navPosition || props.barPosition || props.position);
|
|
25
|
+
const variant = normalizeVariant(props.variant || (props.pills ? "pills" : (props.soft ? "soft" : "line")));
|
|
26
|
+
const wrapTabs = !!props.wrap;
|
|
27
|
+
const swipeable = !!props.swipeable;
|
|
28
|
+
const infinite = !!props.infinite;
|
|
29
|
+
const animated = !!props.animated;
|
|
30
|
+
const navFill = !!(props.navFill ?? props.fill ?? props.stretch);
|
|
31
|
+
const disabledAll = !!props.disabled;
|
|
32
|
+
|
|
33
|
+
const transitionDurationRaw = uiUnwrap(props.transitionDuration ?? props["transition-duration"]);
|
|
34
|
+
const transitionDuration = (() => {
|
|
35
|
+
if (transitionDurationRaw == null) return 220;
|
|
36
|
+
if (typeof transitionDurationRaw === "number") return transitionDurationRaw;
|
|
37
|
+
const parsed = parseFloat(transitionDurationRaw);
|
|
38
|
+
return Number.isFinite(parsed) ? parsed : 220;
|
|
39
|
+
})();
|
|
40
|
+
const transitionEasing = uiUnwrap(props.transitionEasing ?? props["transition-easing"]) || "ease";
|
|
41
|
+
const transitionPrev = props.transitionPrev ?? props["transition-prev"] ?? null;
|
|
42
|
+
const transitionNext = props.transitionNext ?? props["transition-next"] ?? null;
|
|
43
|
+
const accentColor = resolveAccent(props.color ?? props.state);
|
|
44
|
+
|
|
45
|
+
const tabs = rawTabs.map((tab, index) => {
|
|
46
|
+
if (tab == null) return null;
|
|
47
|
+
if (typeof tab !== "object") {
|
|
48
|
+
return {
|
|
49
|
+
name: tab,
|
|
50
|
+
labelFallback: tab,
|
|
51
|
+
panelFallback: null,
|
|
52
|
+
noteFallback: null,
|
|
53
|
+
iconFallback: null,
|
|
54
|
+
badgeFallback: null,
|
|
55
|
+
hidden: false,
|
|
56
|
+
raw: tab,
|
|
57
|
+
index
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const name = tab.name ?? tab.value ?? tab.id ?? tab.key ?? tab.label ?? tab.title ?? `tab-${index}`;
|
|
61
|
+
const hasLabel = tab.label != null || tab.title != null;
|
|
62
|
+
const labelFallback = hasLabel
|
|
63
|
+
? (tab.label ?? tab.title)
|
|
64
|
+
: (tab.children != null && tab.content == null && tab.panel == null ? tab.children : (tab.name ?? tab.value ?? `Tab ${index + 1}`));
|
|
65
|
+
const panelFallback = tab.content ?? tab.panel ?? tab.body ?? (hasLabel ? tab.children : null);
|
|
66
|
+
const noteFallback = tab.note ?? tab.subtitle ?? tab.description ?? null;
|
|
67
|
+
const iconFallback = tab.icon ?? null;
|
|
68
|
+
const badgeFallback = Object.prototype.hasOwnProperty.call(tab, "badge")
|
|
69
|
+
? tab.badge
|
|
70
|
+
: (Object.prototype.hasOwnProperty.call(tab, "counter") ? tab.counter : null);
|
|
71
|
+
return {
|
|
72
|
+
...tab,
|
|
73
|
+
name,
|
|
74
|
+
labelFallback,
|
|
75
|
+
panelFallback,
|
|
76
|
+
noteFallback,
|
|
77
|
+
iconFallback,
|
|
78
|
+
badgeFallback,
|
|
79
|
+
index
|
|
80
|
+
};
|
|
81
|
+
}).filter(Boolean).filter((tab) => tab.hidden !== true && tab.visible !== false);
|
|
82
|
+
|
|
83
|
+
const cls = uiClass([
|
|
84
|
+
"cms-clear-set",
|
|
85
|
+
"cms-tabpanel",
|
|
86
|
+
"cms-singularity",
|
|
87
|
+
orientation,
|
|
88
|
+
`variant-${variant}`,
|
|
89
|
+
uiWhen(wrapTabs, "wrap"),
|
|
90
|
+
uiWhen(animated, "animated"),
|
|
91
|
+
uiWhen(navFill, "nav-fill"),
|
|
92
|
+
uiWhen(disabledAll, "disabled"),
|
|
93
|
+
uiWhen(navPosition === "after", "nav-after"),
|
|
94
|
+
props.class
|
|
95
|
+
]);
|
|
96
|
+
const wrapProps = JSswift.omit(props, [
|
|
97
|
+
"tabs", "items", "value", "default", "defaultValue", "model",
|
|
98
|
+
"orientation", "orient", "direction",
|
|
99
|
+
"navPosition", "barPosition", "position",
|
|
100
|
+
"variant", "pills", "soft",
|
|
101
|
+
"navFill", "fill", "stretch",
|
|
102
|
+
"wrap", "swipeable", "infinite", "animated",
|
|
103
|
+
"transitionDuration", "transition-duration",
|
|
104
|
+
"transitionEasing", "transition-easing",
|
|
105
|
+
"transitionPrev", "transition-prev",
|
|
106
|
+
"transitionNext", "transition-next",
|
|
107
|
+
"slots", "empty",
|
|
108
|
+
"tabClass", "tabStyle",
|
|
109
|
+
"panelClass", "panelStyle",
|
|
110
|
+
"navClass", "panelsClass",
|
|
111
|
+
"class", "style"
|
|
112
|
+
]);
|
|
113
|
+
wrapProps.class = cls;
|
|
114
|
+
wrapProps.style = props.style;
|
|
115
|
+
const wrap = _.div(wrapProps);
|
|
116
|
+
|
|
117
|
+
if (animated) {
|
|
118
|
+
wrap.style.setProperty("--cms-tabpanel-duration", `${transitionDuration}ms`);
|
|
119
|
+
wrap.style.setProperty("--cms-tabpanel-easing", transitionEasing);
|
|
120
|
+
}
|
|
121
|
+
if (accentColor) {
|
|
122
|
+
wrap.style.setProperty("--cms-tabpanel-accent", accentColor);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const nav = _.div({
|
|
126
|
+
class: uiClass(["cms-tabpanel-nav", props.navClass]),
|
|
127
|
+
role: "tablist",
|
|
128
|
+
"aria-orientation": orientation
|
|
129
|
+
});
|
|
130
|
+
const panelsWrap = _.div({ class: uiClass(["cms-tabpanel-panels", props.panelsClass]) });
|
|
131
|
+
|
|
132
|
+
const tabButtons = [];
|
|
133
|
+
const panelNodes = [];
|
|
134
|
+
let activeIndex = -1;
|
|
135
|
+
|
|
136
|
+
const isHorizontal = () => orientation === "horizontal";
|
|
137
|
+
const createCtx = (tab, index, active) => ({
|
|
138
|
+
tab,
|
|
139
|
+
name: tab.name,
|
|
140
|
+
index,
|
|
141
|
+
active,
|
|
142
|
+
value: tab.name,
|
|
143
|
+
select: () => setActiveByIndex(index),
|
|
144
|
+
next: () => goNext(),
|
|
145
|
+
prev: () => goPrev()
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const findEnabledIndex = (start, step, allowWrap = false) => {
|
|
149
|
+
if (!tabs.length) return -1;
|
|
150
|
+
let idx = start;
|
|
151
|
+
for (let i = 0; i < tabs.length; i += 1) {
|
|
152
|
+
idx += step;
|
|
153
|
+
if (idx >= tabs.length) {
|
|
154
|
+
if (!allowWrap) return -1;
|
|
155
|
+
idx = 0;
|
|
156
|
+
} else if (idx < 0) {
|
|
157
|
+
if (!allowWrap) return -1;
|
|
158
|
+
idx = tabs.length - 1;
|
|
159
|
+
}
|
|
160
|
+
if (!tabs[idx]?.disabled) return idx;
|
|
161
|
+
}
|
|
162
|
+
return -1;
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const focusIndex = (index) => {
|
|
166
|
+
const btn = tabButtons[index]?.btn;
|
|
167
|
+
if (!btn || typeof btn.focus !== "function") return;
|
|
168
|
+
requestAnimationFrame(() => btn.focus());
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const makeLabelNodes = (tab, index, isActive) => {
|
|
172
|
+
const ctx = createCtx(tab, index, isActive);
|
|
173
|
+
const labelNode = JSswift.ui.renderSlot(slots, "label", ctx, tab.labelFallback);
|
|
174
|
+
return renderSlotToArray(null, "default", {}, labelNode);
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const makeTabNode = (tab, index) => {
|
|
178
|
+
const isActive = index === activeIndex;
|
|
179
|
+
const tabId = `${componentId}-tab-${index}`;
|
|
180
|
+
const panelId = `${componentId}-panel-${index}`;
|
|
181
|
+
const ctx = createCtx(tab, index, isActive);
|
|
182
|
+
const labelNodes = makeLabelNodes(tab, index, isActive);
|
|
183
|
+
const iconFallback = tab.iconFallback != null
|
|
184
|
+
? (typeof tab.iconFallback === "string"
|
|
185
|
+
? UI.Icon({ name: tab.iconFallback, size: tab.iconSize ?? tab.size ?? props.size ?? null })
|
|
186
|
+
: tab.iconFallback)
|
|
187
|
+
: null;
|
|
188
|
+
const iconNodes = renderSlotToArray(slots, "icon", ctx, iconFallback);
|
|
189
|
+
const noteNodes = renderSlotToArray(slots, "note", ctx, tab.noteFallback);
|
|
190
|
+
const badgeNodes = renderSlotToArray(slots, "badge", ctx, tab.badgeFallback);
|
|
191
|
+
const fallbackTabContent = _.span({ class: "cms-tabpanel-tab-inner" },
|
|
192
|
+
iconNodes.length ? _.span({ class: "cms-tabpanel-tab-icon" }, ...iconNodes) : null,
|
|
193
|
+
_.span({ class: "cms-tabpanel-tab-copy" },
|
|
194
|
+
_.span({ class: "cms-tabpanel-tab-label" }, ...(labelNodes.length ? labelNodes : [""])),
|
|
195
|
+
noteNodes.length ? _.span({ class: "cms-tabpanel-tab-note" }, ...noteNodes) : null
|
|
196
|
+
),
|
|
197
|
+
badgeNodes.length ? _.span({ class: "cms-tabpanel-tab-badge" }, ...badgeNodes) : null
|
|
198
|
+
);
|
|
199
|
+
const contentNodes = renderSlotToArray(slots, "tab", {
|
|
200
|
+
...ctx,
|
|
201
|
+
label: labelNodes,
|
|
202
|
+
icon: iconNodes,
|
|
203
|
+
note: noteNodes,
|
|
204
|
+
badge: badgeNodes,
|
|
205
|
+
tabId,
|
|
206
|
+
panelId
|
|
207
|
+
}, fallbackTabContent);
|
|
208
|
+
const onKeydown = (e) => {
|
|
209
|
+
if (disabledAll || tab.disabled) return;
|
|
210
|
+
const key = e.key;
|
|
211
|
+
if (key === "Enter" || key === " ") {
|
|
212
|
+
e.preventDefault();
|
|
213
|
+
setActiveByIndex(index);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (key === "Home") {
|
|
217
|
+
e.preventDefault();
|
|
218
|
+
const first = tabs.findIndex((item) => !item.disabled);
|
|
219
|
+
if (first >= 0) {
|
|
220
|
+
setActiveByIndex(first);
|
|
221
|
+
focusIndex(first);
|
|
222
|
+
}
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
if (key === "End") {
|
|
226
|
+
e.preventDefault();
|
|
227
|
+
const last = (() => {
|
|
228
|
+
for (let i = tabs.length - 1; i >= 0; i -= 1) {
|
|
229
|
+
if (!tabs[i]?.disabled) return i;
|
|
230
|
+
}
|
|
231
|
+
return -1;
|
|
232
|
+
})();
|
|
233
|
+
if (last >= 0) {
|
|
234
|
+
setActiveByIndex(last);
|
|
235
|
+
focusIndex(last);
|
|
236
|
+
}
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const prevKeys = isHorizontal() ? ["ArrowLeft"] : ["ArrowUp"];
|
|
240
|
+
const nextKeys = isHorizontal() ? ["ArrowRight"] : ["ArrowDown"];
|
|
241
|
+
if (prevKeys.includes(key)) {
|
|
242
|
+
e.preventDefault();
|
|
243
|
+
const prev = findEnabledIndex(index, -1, infinite);
|
|
244
|
+
if (prev >= 0) {
|
|
245
|
+
setActiveByIndex(prev, { dir: "prev" });
|
|
246
|
+
focusIndex(prev);
|
|
247
|
+
}
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (nextKeys.includes(key)) {
|
|
251
|
+
e.preventDefault();
|
|
252
|
+
const next = findEnabledIndex(index, 1, infinite);
|
|
253
|
+
if (next >= 0) {
|
|
254
|
+
setActiveByIndex(next, { dir: "next" });
|
|
255
|
+
focusIndex(next);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
const defaultBtn = UI.Btn({
|
|
260
|
+
class: uiClass([
|
|
261
|
+
"cms-tabpanel-tab",
|
|
262
|
+
uiWhen(isActive, "active"),
|
|
263
|
+
uiWhen(tab.disabled || disabledAll, "disabled"),
|
|
264
|
+
props.tabClass,
|
|
265
|
+
tab.tabClass || tab.class
|
|
266
|
+
]),
|
|
267
|
+
type: "button",
|
|
268
|
+
id: tabId,
|
|
269
|
+
role: "tab",
|
|
270
|
+
disabled: !!(tab.disabled || disabledAll),
|
|
271
|
+
"aria-selected": isActive ? "true" : "false",
|
|
272
|
+
"aria-controls": panelId,
|
|
273
|
+
tabindex: isActive ? "0" : "-1",
|
|
274
|
+
outline: variant === "pills" && !isActive,
|
|
275
|
+
color: variant === "pills" && isActive ? (tab.color || props.color || props.state || "primary") : null,
|
|
276
|
+
size: tab.size ?? props.size ?? null,
|
|
277
|
+
style: {
|
|
278
|
+
...(props.tabStyle || {}),
|
|
279
|
+
...(tab.tabStyle || {})
|
|
280
|
+
},
|
|
281
|
+
onClick: () => {
|
|
282
|
+
if (tab.disabled || disabledAll) return;
|
|
283
|
+
setActiveByIndex(index);
|
|
284
|
+
},
|
|
285
|
+
onKeydown
|
|
286
|
+
}, ...contentNodes);
|
|
287
|
+
const navBtn = _.div({
|
|
288
|
+
class: uiClass([
|
|
289
|
+
"cms-tabpanel-nav-btn",
|
|
290
|
+
uiWhen(isActive, "active"),
|
|
291
|
+
tab.navClass
|
|
292
|
+
]),
|
|
293
|
+
"data-name": tab.name
|
|
294
|
+
}, defaultBtn, _.span({ class: "tab-indicator" }));
|
|
295
|
+
tabButtons[index] = { btn: defaultBtn, wrap: navBtn, index };
|
|
296
|
+
return navBtn;
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
const makePanelNode = (tab, index) => {
|
|
300
|
+
const isActive = index === activeIndex;
|
|
301
|
+
const tabId = `${componentId}-tab-${index}`;
|
|
302
|
+
const panelId = `${componentId}-panel-${index}`;
|
|
303
|
+
const ctx = createCtx(tab, index, isActive);
|
|
304
|
+
const panelNode = JSswift.ui.renderSlot(slots, "panel", ctx, tab.panelFallback);
|
|
305
|
+
const panel = _.div({
|
|
306
|
+
class: uiClass([
|
|
307
|
+
"cms-tabpanel-panel",
|
|
308
|
+
uiWhen(isActive, "active"),
|
|
309
|
+
props.panelClass,
|
|
310
|
+
tab.panelClass
|
|
311
|
+
]),
|
|
312
|
+
style: {
|
|
313
|
+
...(props.panelStyle || {}),
|
|
314
|
+
...(tab.panelStyle || {})
|
|
315
|
+
},
|
|
316
|
+
"data-name": tab.name,
|
|
317
|
+
role: "tabpanel",
|
|
318
|
+
id: panelId,
|
|
319
|
+
tabindex: 0,
|
|
320
|
+
"aria-labelledby": tabId,
|
|
321
|
+
"aria-hidden": isActive ? "false" : "true"
|
|
322
|
+
}, ...renderSlotToArray(null, "default", {}, panelNode));
|
|
323
|
+
panelNodes[index] = panel;
|
|
324
|
+
return panel;
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
const defaultNavNodes = tabs.map((tab, index) => makeTabNode(tab, index));
|
|
328
|
+
const navContent = JSswift.ui.renderSlot(slots, "nav", {
|
|
329
|
+
tabs,
|
|
330
|
+
active: () => (activeIndex >= 0 ? tabs[activeIndex]?.name : null),
|
|
331
|
+
activeIndex: () => activeIndex,
|
|
332
|
+
activeTab: () => (activeIndex >= 0 ? tabs[activeIndex] : null),
|
|
333
|
+
select: (nameOrIndex) => {
|
|
334
|
+
if (typeof nameOrIndex === "number") setActiveByIndex(nameOrIndex);
|
|
335
|
+
else setActiveByValue(nameOrIndex);
|
|
336
|
+
},
|
|
337
|
+
next: () => goNext(),
|
|
338
|
+
prev: () => goPrev(),
|
|
339
|
+
nodes: defaultNavNodes,
|
|
340
|
+
orientation,
|
|
341
|
+
position: navPosition,
|
|
342
|
+
variant
|
|
343
|
+
}, defaultNavNodes);
|
|
344
|
+
|
|
345
|
+
renderSlotToArray(null, "default", {}, navContent).forEach(n => nav.appendChild(n));
|
|
346
|
+
if (tabs.length) {
|
|
347
|
+
tabs.forEach((tab, index) => panelsWrap.appendChild(makePanelNode(tab, index)));
|
|
348
|
+
} else {
|
|
349
|
+
const emptyNodes = renderSlotToArray(slots, "empty", { tabs }, props.empty ?? _.div({ class: "cms-tabpanel-empty" }, "Nessun contenuto disponibile."));
|
|
350
|
+
emptyNodes.forEach((node) => panelsWrap.appendChild(node));
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const setDirectionVars = (dir) => {
|
|
354
|
+
if (!animated) return;
|
|
355
|
+
const enter = dir === "next" ? "18px" : "-18px";
|
|
356
|
+
const leave = dir === "next" ? "-18px" : "18px";
|
|
357
|
+
wrap.style.setProperty("--cms-tabpanel-enter", enter);
|
|
358
|
+
wrap.style.setProperty("--cms-tabpanel-leave", leave);
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
const cleanCustomTransitions = () => {
|
|
362
|
+
const classes = [];
|
|
363
|
+
if (transitionPrev) classes.push(...String(transitionPrev).split(/\s+/));
|
|
364
|
+
if (transitionNext) classes.push(...String(transitionNext).split(/\s+/));
|
|
365
|
+
if (classes.length) panelsWrap.classList.remove(...classes.filter(Boolean));
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
const applyCustomTransition = (dir) => {
|
|
369
|
+
cleanCustomTransitions();
|
|
370
|
+
const cls = dir === "prev" ? transitionPrev : transitionNext;
|
|
371
|
+
if (!cls) return;
|
|
372
|
+
const parts = String(cls).split(/\s+/).filter(Boolean);
|
|
373
|
+
if (!parts.length) return;
|
|
374
|
+
panelsWrap.classList.add(...parts);
|
|
375
|
+
setTimeout(() => panelsWrap.classList.remove(...parts), transitionDuration + 40);
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
const updateNavButtons = (nextIndex) => {
|
|
379
|
+
const active = tabButtons.find(({ btn }) => btn.classList.contains("active"));
|
|
380
|
+
const rectPrev = active?.btn.parentNode?.getBoundingClientRect();
|
|
381
|
+
tabButtons.forEach(({ btn, index }) => {
|
|
382
|
+
const isActive = index === nextIndex;
|
|
383
|
+
const parent = btn.parentNode;
|
|
384
|
+
if (!parent) return;
|
|
385
|
+
const rect = parent.getBoundingClientRect();
|
|
386
|
+
if (parent.isConnected && rectPrev) {
|
|
387
|
+
parent.style.setProperty("--nav-pos-x", (rectPrev.left - rect.left) + "px");
|
|
388
|
+
parent.style.setProperty("--nav-pos-y", (rectPrev.top - rect.top) + "px");
|
|
389
|
+
}
|
|
390
|
+
parent.classList.toggle("active", isActive);
|
|
391
|
+
btn.classList.toggle("active", isActive);
|
|
392
|
+
btn.setAttribute("aria-selected", isActive ? "true" : "false");
|
|
393
|
+
btn.setAttribute("tabindex", isActive ? "0" : "-1");
|
|
394
|
+
});
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
const updatePanels = (prevIndex, nextIndex, dir) => {
|
|
398
|
+
setDirectionVars(dir);
|
|
399
|
+
panelNodes.forEach((panel, index) => {
|
|
400
|
+
if (!panel) return;
|
|
401
|
+
const isNext = index === nextIndex;
|
|
402
|
+
const isPrev = index === prevIndex;
|
|
403
|
+
if (isNext) {
|
|
404
|
+
panel.classList.add("active");
|
|
405
|
+
panel.classList.remove("leaving");
|
|
406
|
+
panel.setAttribute("aria-hidden", "false");
|
|
407
|
+
} else {
|
|
408
|
+
panel.classList.remove("active");
|
|
409
|
+
panel.setAttribute("aria-hidden", "true");
|
|
410
|
+
if (animated && isPrev) {
|
|
411
|
+
panel.classList.add("leaving");
|
|
412
|
+
setTimeout(() => panel.classList.remove("leaving"), transitionDuration + 40);
|
|
413
|
+
} else {
|
|
414
|
+
panel.classList.remove("leaving");
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
const computeDir = (prevIndex, nextIndex) => {
|
|
421
|
+
if (prevIndex < 0 || prevIndex === nextIndex) return "next";
|
|
422
|
+
if (infinite && prevIndex === tabs.length - 1 && nextIndex === 0) return "next";
|
|
423
|
+
if (infinite && prevIndex === 0 && nextIndex === tabs.length - 1) return "prev";
|
|
424
|
+
return nextIndex > prevIndex ? "next" : "prev";
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
const setActiveByIndex = (nextIndex, opts = {}) => {
|
|
428
|
+
if (nextIndex == null || nextIndex < 0 || nextIndex >= tabs.length) return;
|
|
429
|
+
if (tabs[nextIndex]?.disabled) return;
|
|
430
|
+
const prevIndex = activeIndex;
|
|
431
|
+
if (prevIndex === nextIndex) return;
|
|
432
|
+
activeIndex = nextIndex;
|
|
433
|
+
const nextTab = tabs[nextIndex];
|
|
434
|
+
const dir = opts.dir || computeDir(prevIndex, nextIndex);
|
|
435
|
+
if (model && !opts.fromModel) model.set(nextTab.name);
|
|
436
|
+
updateNavButtons(nextIndex);
|
|
437
|
+
updatePanels(prevIndex, nextIndex, dir);
|
|
438
|
+
applyCustomTransition(dir);
|
|
439
|
+
wrap.dataset.active = String(nextTab.name);
|
|
440
|
+
props.onChange?.(nextTab.name, nextTab, nextIndex);
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
const setActiveByValue = (value, opts = {}) => {
|
|
444
|
+
if (!tabs.length) return;
|
|
445
|
+
const idx = tabs.findIndex(t => t.name == value);
|
|
446
|
+
if (idx === -1) return;
|
|
447
|
+
setActiveByIndex(idx, opts);
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
const goNext = () => {
|
|
451
|
+
if (!tabs.length) return;
|
|
452
|
+
const nextIndex = activeIndex < 0
|
|
453
|
+
? tabs.findIndex((tab) => !tab.disabled)
|
|
454
|
+
: findEnabledIndex(activeIndex, 1, infinite);
|
|
455
|
+
if (nextIndex < 0) return;
|
|
456
|
+
setActiveByIndex(nextIndex, { dir: "next" });
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
const goPrev = () => {
|
|
460
|
+
if (!tabs.length) return;
|
|
461
|
+
const nextIndex = activeIndex < 0
|
|
462
|
+
? (() => {
|
|
463
|
+
for (let i = tabs.length - 1; i >= 0; i -= 1) {
|
|
464
|
+
if (!tabs[i]?.disabled) return i;
|
|
465
|
+
}
|
|
466
|
+
return -1;
|
|
467
|
+
})()
|
|
468
|
+
: findEnabledIndex(activeIndex, -1, infinite);
|
|
469
|
+
if (nextIndex < 0) return;
|
|
470
|
+
setActiveByIndex(nextIndex, { dir: "prev" });
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
if (swipeable) {
|
|
474
|
+
let startX = 0;
|
|
475
|
+
let startY = 0;
|
|
476
|
+
let tracking = false;
|
|
477
|
+
const threshold = 42;
|
|
478
|
+
panelsWrap.addEventListener("pointerdown", (e) => {
|
|
479
|
+
if (e.button != null && e.button !== 0) return;
|
|
480
|
+
tracking = true;
|
|
481
|
+
startX = e.clientX;
|
|
482
|
+
startY = e.clientY;
|
|
483
|
+
});
|
|
484
|
+
panelsWrap.addEventListener("pointerup", (e) => {
|
|
485
|
+
if (!tracking) return;
|
|
486
|
+
tracking = false;
|
|
487
|
+
const dx = e.clientX - startX;
|
|
488
|
+
const dy = e.clientY - startY;
|
|
489
|
+
if (Math.abs(dx) < threshold || Math.abs(dx) < Math.abs(dy)) return;
|
|
490
|
+
if (dx < 0) goNext();
|
|
491
|
+
else goPrev();
|
|
492
|
+
});
|
|
493
|
+
panelsWrap.addEventListener("pointercancel", () => { tracking = false; });
|
|
494
|
+
panelsWrap.addEventListener("pointerleave", () => { tracking = false; });
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const initialValue = model ? model.get() : (props.value ?? props.defaultValue ?? props.default ?? null);
|
|
498
|
+
if (tabs.length) {
|
|
499
|
+
const initialIndex = tabs.findIndex(t => t.name == initialValue);
|
|
500
|
+
if (initialIndex >= 0) {
|
|
501
|
+
setActiveByIndex(initialIndex, { fromModel: true });
|
|
502
|
+
} else {
|
|
503
|
+
const firstEnabled = tabs.findIndex((tab) => !tab.disabled);
|
|
504
|
+
if (firstEnabled >= 0) {
|
|
505
|
+
setActiveByIndex(firstEnabled, { fromModel: true });
|
|
506
|
+
if (model) model.set(tabs[firstEnabled].name);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (model) {
|
|
512
|
+
model.watch((v) => setActiveByValue(v, { fromModel: true }), "UI.TabPanel:watch");
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
if (navPosition === "after") {
|
|
516
|
+
wrap.appendChild(panelsWrap);
|
|
517
|
+
wrap.appendChild(nav);
|
|
518
|
+
} else {
|
|
519
|
+
wrap.appendChild(nav);
|
|
520
|
+
wrap.appendChild(panelsWrap);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const extra = renderSlotToArray(slots, "default", {}, children);
|
|
524
|
+
extra.forEach((n) => wrap.appendChild(n));
|
|
525
|
+
setPropertyProps(wrap, props);
|
|
526
|
+
wrap._tabs = () => tabs.slice();
|
|
527
|
+
wrap._active = () => (activeIndex >= 0 ? tabs[activeIndex] : null);
|
|
528
|
+
wrap._getValue = () => (activeIndex >= 0 ? tabs[activeIndex]?.name : null);
|
|
529
|
+
wrap._setValue = (value) => setActiveByValue(value);
|
|
530
|
+
wrap._select = (value) => {
|
|
531
|
+
if (typeof value === "number") setActiveByIndex(value);
|
|
532
|
+
else setActiveByValue(value);
|
|
533
|
+
};
|
|
534
|
+
wrap._next = goNext;
|
|
535
|
+
wrap._prev = goPrev;
|
|
536
|
+
return wrap;
|
|
537
|
+
};
|
|
538
|
+
if (JSswift.isDev?.()) {
|
|
539
|
+
UI.meta = UI.meta || {};
|
|
540
|
+
UI.meta.TabPanel = {
|
|
541
|
+
signature: "UI.TabPanel(props) | UI.TabPanel(props, ...children)",
|
|
542
|
+
props: {
|
|
543
|
+
tabs: {
|
|
544
|
+
type: "Array<{ name?, value?, label?, title?, note?, subtitle?, icon?, badge?, content?, panel?, body?, children?, disabled?, hidden?, tabClass?, panelClass? }>",
|
|
545
|
+
description: "Tab definition. Supports multiple aliases for label and content.",
|
|
546
|
+
category: "data"
|
|
547
|
+
},
|
|
548
|
+
items: {
|
|
549
|
+
type: "Array",
|
|
550
|
+
description: "Alias of `tabs`.",
|
|
551
|
+
category: "data"
|
|
552
|
+
},
|
|
553
|
+
value: {
|
|
554
|
+
type: "any",
|
|
555
|
+
description: "Initial or controlled value of the active tab.",
|
|
556
|
+
category: "data"
|
|
557
|
+
},
|
|
558
|
+
defaultValue: {
|
|
559
|
+
type: "any",
|
|
560
|
+
description: "Explicit alias for the initial tab when `model` is not used.",
|
|
561
|
+
category: "data"
|
|
562
|
+
},
|
|
563
|
+
model: {
|
|
564
|
+
type: "[get,set] signal",
|
|
565
|
+
description: "Reactive binding for the active tab.",
|
|
566
|
+
category: "data"
|
|
567
|
+
},
|
|
568
|
+
orientation: {
|
|
569
|
+
type: "vertical|horizontal",
|
|
570
|
+
description: "Navigation orientation.",
|
|
571
|
+
values: ["vertical", "horizontal"],
|
|
572
|
+
default: "vertical",
|
|
573
|
+
category: "layout"
|
|
574
|
+
},
|
|
575
|
+
navPosition: {
|
|
576
|
+
type: "before|after",
|
|
577
|
+
description: "Tab bar position relative to the panels.",
|
|
578
|
+
values: ["before", "after"],
|
|
579
|
+
default: "before",
|
|
580
|
+
category: "layout"
|
|
581
|
+
},
|
|
582
|
+
variant: {
|
|
583
|
+
type: "line|pills|soft",
|
|
584
|
+
description: "Visual style of the tab navigation.",
|
|
585
|
+
values: ["line", "pills", "soft"],
|
|
586
|
+
default: "line",
|
|
587
|
+
category: "style"
|
|
588
|
+
},
|
|
589
|
+
wrap: {
|
|
590
|
+
type: "boolean",
|
|
591
|
+
description: "Allows the nav to wrap when space is limited.",
|
|
592
|
+
default: false,
|
|
593
|
+
category: "layout"
|
|
594
|
+
},
|
|
595
|
+
navFill: {
|
|
596
|
+
type: "boolean",
|
|
597
|
+
description: "Distributes tabs across the available width.",
|
|
598
|
+
default: false,
|
|
599
|
+
category: "layout"
|
|
600
|
+
},
|
|
601
|
+
swipeable: {
|
|
602
|
+
type: "boolean",
|
|
603
|
+
description: "Enables swipe gestures on panels.",
|
|
604
|
+
default: false,
|
|
605
|
+
category: "behavior"
|
|
606
|
+
},
|
|
607
|
+
infinite: {
|
|
608
|
+
type: "boolean",
|
|
609
|
+
description: "When enabled, next/prev cycles from the first to the last tab.",
|
|
610
|
+
default: false,
|
|
611
|
+
category: "behavior"
|
|
612
|
+
},
|
|
613
|
+
animated: {
|
|
614
|
+
type: "boolean",
|
|
615
|
+
description: "Enables the transition between panels.",
|
|
616
|
+
default: false,
|
|
617
|
+
category: "behavior"
|
|
618
|
+
},
|
|
619
|
+
transitionDuration: {
|
|
620
|
+
type: "number",
|
|
621
|
+
description: "Animation duration in milliseconds.",
|
|
622
|
+
default: 220,
|
|
623
|
+
category: "behavior"
|
|
624
|
+
},
|
|
625
|
+
transitionEasing: {
|
|
626
|
+
type: "string",
|
|
627
|
+
description: "CSS timing function for the animation.",
|
|
628
|
+
default: "ease",
|
|
629
|
+
category: "behavior"
|
|
630
|
+
},
|
|
631
|
+
transitionPrev: {
|
|
632
|
+
type: "string",
|
|
633
|
+
description: "Custom classes applied during the transition to the previous tab.",
|
|
634
|
+
category: "behavior"
|
|
635
|
+
},
|
|
636
|
+
transitionNext: {
|
|
637
|
+
type: "string",
|
|
638
|
+
description: "Custom classes applied during the transition to the next tab.",
|
|
639
|
+
category: "behavior"
|
|
640
|
+
},
|
|
641
|
+
tabClass: {
|
|
642
|
+
type: "string",
|
|
643
|
+
description: "Additional classes for all tab buttons.",
|
|
644
|
+
category: "style"
|
|
645
|
+
},
|
|
646
|
+
tabStyle: {
|
|
647
|
+
type: "object",
|
|
648
|
+
description: "Inline style applied to all tab buttons.",
|
|
649
|
+
category: "style"
|
|
650
|
+
},
|
|
651
|
+
navClass: {
|
|
652
|
+
type: "string",
|
|
653
|
+
description: "Additional classes for the nav wrapper.",
|
|
654
|
+
category: "style"
|
|
655
|
+
},
|
|
656
|
+
panelsClass: {
|
|
657
|
+
type: "string",
|
|
658
|
+
description: "Additional classes for the panels wrapper.",
|
|
659
|
+
category: "style"
|
|
660
|
+
},
|
|
661
|
+
panelClass: {
|
|
662
|
+
type: "string",
|
|
663
|
+
description: "Common additional classes for each panel.",
|
|
664
|
+
category: "style"
|
|
665
|
+
},
|
|
666
|
+
panelStyle: {
|
|
667
|
+
type: "object",
|
|
668
|
+
description: "Common inline style for each panel.",
|
|
669
|
+
category: "style"
|
|
670
|
+
},
|
|
671
|
+
empty: {
|
|
672
|
+
type: "Node|Function|Array",
|
|
673
|
+
description: "Visual fallback when `tabs` and `items` are empty.",
|
|
674
|
+
category: "state"
|
|
675
|
+
},
|
|
676
|
+
disabled: {
|
|
677
|
+
type: "boolean",
|
|
678
|
+
description: "Disables the entire component.",
|
|
679
|
+
default: false,
|
|
680
|
+
category: "state"
|
|
681
|
+
},
|
|
682
|
+
slots: {
|
|
683
|
+
type: "{ nav?, tab?, label?, icon?, note?, badge?, panel?, empty?, default? }",
|
|
684
|
+
description: "Structured slots for customizing nav, label, badge, and content.",
|
|
685
|
+
category: "general"
|
|
686
|
+
}
|
|
687
|
+
},
|
|
688
|
+
slots: {
|
|
689
|
+
nav: {
|
|
690
|
+
type: "Function|Node|Array",
|
|
691
|
+
description: "Full navigation renderer. Receives `tabs`, `active()`, `activeTab()`, `select()`, `next()`, `prev()`, `nodes`, `orientation`, `position`, `variant`."
|
|
692
|
+
},
|
|
693
|
+
tab: {
|
|
694
|
+
type: "Function|Node|Array",
|
|
695
|
+
description: "Inner content of the tab button. Receives `tab`, `name`, `index`, `active`, `label`, `icon`, `note`, `badge`, `select()`."
|
|
696
|
+
},
|
|
697
|
+
label: {
|
|
698
|
+
type: "Function|Node|Array",
|
|
699
|
+
description: "Tab label."
|
|
700
|
+
},
|
|
701
|
+
icon: {
|
|
702
|
+
type: "Function|Node|Array",
|
|
703
|
+
description: "Tab icon."
|
|
704
|
+
},
|
|
705
|
+
note: {
|
|
706
|
+
type: "Function|Node|Array",
|
|
707
|
+
description: "Note, subtitle, or short description below the label."
|
|
708
|
+
},
|
|
709
|
+
badge: {
|
|
710
|
+
type: "Function|Node|Array",
|
|
711
|
+
description: "Badge or counter aligned to the right of the tab."
|
|
712
|
+
},
|
|
713
|
+
panel: {
|
|
714
|
+
type: "Function|Node|Array",
|
|
715
|
+
description: "Active/inactive panel renderer. Receives `tab`, `name`, `index`, `active`, `select()`, `next()`, `prev()`."
|
|
716
|
+
},
|
|
717
|
+
empty: {
|
|
718
|
+
type: "Function|Node|Array",
|
|
719
|
+
description: "Fallback when there are no tabs."
|
|
720
|
+
},
|
|
721
|
+
default: {
|
|
722
|
+
type: "Node|Array|Function",
|
|
723
|
+
description: "Extra content appended after the component."
|
|
724
|
+
}
|
|
725
|
+
},
|
|
726
|
+
events: {
|
|
727
|
+
onChange: "(name, tab, index)"
|
|
728
|
+
},
|
|
729
|
+
returns: "HTMLDivElement with API `_getValue()`, `_setValue(value)`, `_select(value)`, `_next()`, `_prev()`, `_active()`, `_tabs()`",
|
|
730
|
+
description: "Standardized tab panel with accessible nav, structured slots, reactive model, swipe, and animations."
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function closeBackdrop(backdrop) {
|
|
735
|
+
if (backdrop && backdrop.parentNode) backdrop.parentNode.removeChild(backdrop);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function renderDialog({ title, message, contentNode, okText, cancelText, showCancel, onOk, onCancel, persistent }) {
|
|
739
|
+
ensureModalRoot();
|
|
740
|
+
|
|
741
|
+
const backdrop = _.div({
|
|
742
|
+
class: "cms-dialog-backdrop", onClick: (e) => {
|
|
743
|
+
if (e.target === backdrop && persistent !== true) onCancel?.();
|
|
744
|
+
}
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
const titleNodes = renderSlotToArray(null, "default", {}, title);
|
|
748
|
+
const messageNodes = renderSlotToArray(null, "default", {}, message);
|
|
749
|
+
const contentNodes = renderSlotToArray(null, "default", {}, contentNode);
|
|
750
|
+
|
|
751
|
+
const dialog = _.div({ class: "cms-dialog cms-panel" },
|
|
752
|
+
titleNodes.length ? _.h3(...titleNodes) : null,
|
|
753
|
+
messageNodes.length ? _.p(...messageNodes) : null,
|
|
754
|
+
...contentNodes,
|
|
755
|
+
_.div({ class: "cms-dialog-actions" },
|
|
756
|
+
showCancel ? JSswift.ui.Btn({ onClick: onCancel }, cancelText || "Annulla") : null,
|
|
757
|
+
JSswift.ui.Btn({ color: "primary", onClick: onOk }, okText || "OK")
|
|
758
|
+
)
|
|
759
|
+
);
|
|
760
|
+
|
|
761
|
+
backdrop.appendChild(dialog);
|
|
762
|
+
ensureModalRoot().appendChild(backdrop);
|
|
763
|
+
|
|
764
|
+
// escape to close
|
|
765
|
+
const onKey = (e) => {
|
|
766
|
+
if (e.key === "Escape") onCancel?.();
|
|
767
|
+
};
|
|
768
|
+
document.addEventListener("keydown", onKey, { once: true });
|
|
769
|
+
|
|
770
|
+
return { backdrop, close: () => closeBackdrop(backdrop) };
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
app.dialog.alert = function (message, opts = {}) {
|
|
774
|
+
return new Promise((resolve) => {
|
|
775
|
+
const { backdrop, close } = renderDialog({
|
|
776
|
+
title: opts.title || "Info",
|
|
777
|
+
message,
|
|
778
|
+
okText: opts.okText || "OK",
|
|
779
|
+
showCancel: false,
|
|
780
|
+
persistent: opts.persistent,
|
|
781
|
+
onOk: () => { close(); resolve(true); },
|
|
782
|
+
onCancel: () => { close(); resolve(true); }
|
|
783
|
+
});
|
|
784
|
+
});
|
|
785
|
+
};
|
|
786
|
+
|
|
787
|
+
app.dialog.confirm = function (opts = {}) {
|
|
788
|
+
return new Promise((resolve) => {
|
|
789
|
+
const { close } = renderDialog({
|
|
790
|
+
title: opts.title || "Conferma",
|
|
791
|
+
message: opts.message || "",
|
|
792
|
+
okText: opts.okText || "Conferma",
|
|
793
|
+
cancelText: opts.cancelText || "Annulla",
|
|
794
|
+
showCancel: true,
|
|
795
|
+
persistent: opts.persistent,
|
|
796
|
+
onOk: () => { close(); resolve(true); },
|
|
797
|
+
onCancel: () => { close(); resolve(false); }
|
|
798
|
+
});
|
|
799
|
+
});
|
|
800
|
+
};
|
|
801
|
+
|
|
802
|
+
app.dialog.prompt = function (opts = {}) {
|
|
803
|
+
return new Promise((resolve) => {
|
|
804
|
+
const input = JSswift.ui.Input({
|
|
805
|
+
class: "cms-dialog-input",
|
|
806
|
+
type: opts.type || "text",
|
|
807
|
+
placeholder: opts.placeholder || ""
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
// default value
|
|
811
|
+
if (opts.value != null) input.value = String(opts.value);
|
|
812
|
+
|
|
813
|
+
const { close } = renderDialog({
|
|
814
|
+
title: opts.title || "Inserisci valore",
|
|
815
|
+
message: opts.message || "",
|
|
816
|
+
okText: opts.okText || "OK",
|
|
817
|
+
cancelText: opts.cancelText || "Annulla",
|
|
818
|
+
showCancel: true,
|
|
819
|
+
contentNode: _.div(input),
|
|
820
|
+
persistent: opts.persistent,
|
|
821
|
+
onOk: () => { const v = input.value; close(); resolve(v); },
|
|
822
|
+
onCancel: () => { close(); resolve(null); }
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
// focus
|
|
826
|
+
setTimeout(() => input.focus(), 0);
|
|
827
|
+
});
|
|
828
|
+
};
|
|
829
|
+
|
|
830
|
+
// ===============================
|
|
831
|
+
// Loading Service
|
|
832
|
+
// ===============================
|
|
833
|
+
|
|
834
|
+
app.services = app.services || {};
|
|
835
|
+
app.services.loading = app.services.loading || {};
|
|
836
|
+
app.loading = app.services.loading;
|
|
837
|
+
|
|
838
|
+
let overlay = null;
|
|
839
|
+
let countLoading = 0;
|
|
840
|
+
|
|
841
|
+
function ensureOverlay() {
|
|
842
|
+
if (overlay) return overlay;
|
|
843
|
+
|
|
844
|
+
overlay = document.createElement("div");
|
|
845
|
+
overlay.className = "cms-loading-backdrop";
|
|
846
|
+
overlay.style.cssText = `
|
|
847
|
+
position: fixed; inset: 0;
|
|
848
|
+
background: rgba(0,0,0,0.45);
|
|
849
|
+
display: none;
|
|
850
|
+
align-items: center; justify-content: center;
|
|
851
|
+
z-index: 10001;
|
|
852
|
+
padding: 14px;
|
|
853
|
+
`;
|
|
854
|
+
const box = document.createElement("div");
|
|
855
|
+
box.className = "cms-panel cms-loading-card";
|
|
856
|
+
box.style.cssText = `
|
|
857
|
+
padding: 14px 16px;
|
|
858
|
+
border-radius: var(--cms-r-lg);
|
|
859
|
+
min-width: 260px;
|
|
860
|
+
display:flex; gap: 12px; align-items:center;
|
|
861
|
+
flex-direction: column;
|
|
862
|
+
`;
|
|
863
|
+
|
|
864
|
+
const spinner = document.createElement("div");
|
|
865
|
+
spinner.className = "cms-loading-spinner";
|
|
866
|
+
spinner.style.cssText = `
|
|
867
|
+
width: 18px; height: 18px;
|
|
868
|
+
border-radius: 50%;
|
|
869
|
+
border: 2px solid rgba(255,255,255,0.25);
|
|
870
|
+
border-top-color: var(--cms-primary);
|
|
871
|
+
animation: cmsSpin 0.9s linear infinite;
|
|
872
|
+
`;
|
|
873
|
+
|
|
874
|
+
const text = document.createElement("div");
|
|
875
|
+
text.className = "cms-muted cms-loading-text";
|
|
876
|
+
text.textContent = "Loading...";
|
|
877
|
+
|
|
878
|
+
const progressWrap = document.createElement("div");
|
|
879
|
+
progressWrap.className = "cms-loading-progress";
|
|
880
|
+
progressWrap.style.cssText = `
|
|
881
|
+
width: 100%;
|
|
882
|
+
height: 6px;
|
|
883
|
+
border-radius: 999px;
|
|
884
|
+
overflow: hidden;
|
|
885
|
+
background: rgba(255,255,255,0.08);
|
|
886
|
+
display: none;
|
|
887
|
+
`;
|
|
888
|
+
const progressBar = document.createElement("div");
|
|
889
|
+
progressBar.className = "cms-loading-progress-bar";
|
|
890
|
+
progressBar.style.cssText = `
|
|
891
|
+
height: 100%;
|
|
892
|
+
width: 0%;
|
|
893
|
+
background: var(--cms-primary);
|
|
894
|
+
transition: width 200ms ease;
|
|
895
|
+
`;
|
|
896
|
+
progressWrap.appendChild(progressBar);
|
|
897
|
+
|
|
898
|
+
const row = document.createElement("div");
|
|
899
|
+
row.style.cssText = "display:flex; gap: 12px; align-items:center; width: 100%;";
|
|
900
|
+
row.appendChild(spinner);
|
|
901
|
+
row.appendChild(text);
|
|
902
|
+
|
|
903
|
+
box.appendChild(row);
|
|
904
|
+
box.appendChild(progressWrap);
|
|
905
|
+
overlay.appendChild(box);
|
|
906
|
+
document.body.appendChild(overlay);
|
|
907
|
+
|
|
908
|
+
const style = document.createElement("style");
|
|
909
|
+
style.textContent = `
|
|
910
|
+
@keyframes cmsSpin { to { transform: rotate(360deg); } }
|
|
911
|
+
`;
|
|
912
|
+
document.head.appendChild(style);
|
|
913
|
+
|
|
914
|
+
overlay._textNode = text;
|
|
915
|
+
overlay._spinner = spinner;
|
|
916
|
+
overlay._progressWrap = progressWrap;
|
|
917
|
+
overlay._progressBar = progressBar;
|
|
918
|
+
overlay._progressActive = false;
|
|
919
|
+
return overlay;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
function normalizeLoadingOptions(message, opts) {
|
|
923
|
+
if (typeof message === "object" && message !== null) return { ...message };
|
|
924
|
+
const base = (opts && typeof opts === "object") ? opts : {};
|
|
925
|
+
if (typeof opts === "string" && (message == null || typeof message === "number")) {
|
|
926
|
+
return { ...base, message: opts };
|
|
927
|
+
}
|
|
928
|
+
return { ...base, message: message };
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
function setLoadingMode(ov, mode) {
|
|
932
|
+
const showProgress = mode === "progress";
|
|
933
|
+
ov._spinner.style.display = showProgress ? "none" : "";
|
|
934
|
+
ov._progressWrap.style.display = showProgress ? "block" : "none";
|
|
935
|
+
ov._mode = mode;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
app.loading.show = function (message = "Loading...", opts = {}) {
|
|
939
|
+
const ov = ensureOverlay();
|
|
940
|
+
const o = normalizeLoadingOptions(message, opts);
|
|
941
|
+
countLoading++;
|
|
942
|
+
ov._progressActive = false;
|
|
943
|
+
ov._textNode.textContent = o.message || "Loading...";
|
|
944
|
+
setLoadingMode(ov, "spinner");
|
|
945
|
+
if (o.spinner === false) ov._spinner.style.display = "none";
|
|
946
|
+
ov.style.display = "flex";
|
|
947
|
+
};
|
|
948
|
+
|
|
949
|
+
app.loading.hide = function () {
|
|
950
|
+
if (!overlay) return;
|
|
951
|
+
countLoading = Math.max(0, countLoading - 1);
|
|
952
|
+
if (countLoading === 0) {
|
|
953
|
+
overlay.style.display = "none";
|
|
954
|
+
overlay._progressActive = false;
|
|
955
|
+
}
|
|
956
|
+
};
|
|
957
|
+
|
|
958
|
+
app.loading.progress = function (value = 0, opts = {}) {
|
|
959
|
+
const ov = ensureOverlay();
|
|
960
|
+
if (typeof value === "object" && value !== null) {
|
|
961
|
+
opts = value;
|
|
962
|
+
value = value.value ?? 0;
|
|
963
|
+
}
|
|
964
|
+
const o = normalizeLoadingOptions(null, opts);
|
|
965
|
+
if (!ov._progressActive) {
|
|
966
|
+
countLoading++;
|
|
967
|
+
ov._progressActive = true;
|
|
968
|
+
}
|
|
969
|
+
const v = Math.max(0, Math.min(100, Number(value ?? 0)));
|
|
970
|
+
ov._textNode.textContent = o.message || "Loading...";
|
|
971
|
+
ov._progressBar.style.width = v + "%";
|
|
972
|
+
setLoadingMode(ov, "progress");
|
|
973
|
+
ov.style.display = "flex";
|
|
974
|
+
};
|
|
975
|
+
|
|
976
|
+
// helper: wrapper async
|
|
977
|
+
app.loading.wrap = async function (message, fn) {
|
|
978
|
+
app.loading.show(message);
|
|
979
|
+
try { return await fn(); }
|
|
980
|
+
finally { app.loading.hide(); }
|
|
981
|
+
};
|
|
982
|
+
|
|
983
|
+
// ===============================
|
|
984
|
+
// UI.Table
|
|
985
|
+
// ===============================
|
|
986
|
+
function toValue(rows) {
|
|
987
|
+
if (typeof rows === "function") return rows();
|
|
988
|
+
return rows || [];
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
function tableGetByPath(obj, path) {
|
|
992
|
+
if (obj == null || path == null) return undefined;
|
|
993
|
+
if (typeof path !== "string" || path.indexOf(".") < 0) return obj?.[path];
|
|
994
|
+
return path.split(".").reduce((acc, key) => acc == null ? acc : acc[key], obj);
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
function defaultCompare(a, b) {
|
|
998
|
+
if (a == null && b == null) return 0;
|
|
999
|
+
if (a == null) return -1;
|
|
1000
|
+
if (b == null) return 1;
|
|
1001
|
+
if (typeof a === "number" && typeof b === "number") return a - b;
|
|
1002
|
+
return String(a).localeCompare(String(b), undefined, { numeric: true, sensitivity: "base" });
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
function tableToArray(value) {
|
|
1006
|
+
const rows = toValue(value);
|
|
1007
|
+
return Array.isArray(rows) ? rows : [];
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
function tableColumnLabel(col) {
|
|
1011
|
+
return col?.label ?? col?.title ?? col?.header ?? col?.key ?? "";
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
function tableColumnSortKey(col, index) {
|
|
1015
|
+
return col?.sortKey ?? col?.key ?? `__col_${index}`;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
function tableFindColumn(columns, key) {
|
|
1019
|
+
return columns.find((col, index) => tableColumnSortKey(col, index) === key || col?.key === key) || null;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
function tableResolveValue(col, row, rowIndex) {
|
|
1023
|
+
if (!col) return row;
|
|
1024
|
+
if (typeof col.get === "function") return col.get(row, { row, rowIndex, col });
|
|
1025
|
+
if (typeof col.value === "function") return col.value(row, { row, rowIndex, col });
|
|
1026
|
+
if (typeof col.key === "string") return tableGetByPath(row, col.key);
|
|
1027
|
+
return col.key != null ? row?.[col.key] : row;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
function tableResolveStyle(style, ctx) {
|
|
1031
|
+
if (typeof style === "function") return style(ctx) || {};
|
|
1032
|
+
return style || {};
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
function tableTextValue(value) {
|
|
1036
|
+
if (value == null) return "";
|
|
1037
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return String(value);
|
|
1038
|
+
if (Array.isArray(value)) return value.map(tableTextValue).filter(Boolean).join(" ");
|
|
1039
|
+
if (typeof Node !== "undefined" && value instanceof Node) return value.textContent || "";
|
|
1040
|
+
if (typeof value === "object") {
|
|
1041
|
+
if (typeof value.textContent === "string") return value.textContent;
|
|
1042
|
+
try {
|
|
1043
|
+
return JSON.stringify(value);
|
|
1044
|
+
} catch {
|
|
1045
|
+
return "";
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
return String(value);
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
function tableNormalizePageSizes(options, fallback) {
|
|
1052
|
+
const list = Array.isArray(options) ? options : fallback;
|
|
1053
|
+
const normalized = list
|
|
1054
|
+
.map((item) => Number(item))
|
|
1055
|
+
.filter((item) => Number.isFinite(item) && item > 0);
|
|
1056
|
+
return normalized.length ? Array.from(new Set(normalized)) : fallback;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
function tableMatchesQuery(row, columns, query, props) {
|
|
1060
|
+
const predicate = props.searchBy || props.searchPredicate;
|
|
1061
|
+
if (typeof predicate === "function") return !!predicate(row, { query, columns });
|
|
1062
|
+
|
|
1063
|
+
const terms = String(query || "")
|
|
1064
|
+
.trim()
|
|
1065
|
+
.toLowerCase()
|
|
1066
|
+
.split(/\s+/)
|
|
1067
|
+
.filter(Boolean);
|
|
1068
|
+
if (!terms.length) return true;
|
|
1069
|
+
|
|
1070
|
+
const searchKeys = Array.isArray(props.searchKeys) && props.searchKeys.length
|
|
1071
|
+
? props.searchKeys
|
|
1072
|
+
: columns.filter((col) => col?.searchable !== false).map((col) => col?.searchKey ?? col?.key).filter(Boolean);
|
|
1073
|
+
|
|
1074
|
+
const tokens = [];
|
|
1075
|
+
if (searchKeys.length) {
|
|
1076
|
+
for (const key of searchKeys) {
|
|
1077
|
+
if (typeof key === "function") tokens.push(tableTextValue(key(row)));
|
|
1078
|
+
else {
|
|
1079
|
+
const col = columns.find((item) => item?.key === key || item?.searchKey === key);
|
|
1080
|
+
tokens.push(tableTextValue(col ? tableResolveValue(col, row, -1) : tableGetByPath(row, key)));
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
} else {
|
|
1084
|
+
tokens.push(tableTextValue(row));
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
const haystack = tokens.join(" ").toLowerCase();
|
|
1088
|
+
return terms.every((term) => haystack.includes(term));
|
|
1089
|
+
}
|
|
1090
|
+
|