ux4g-components-web 1.1.2 → 1.2.1
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/runtime/bootstrap.cjs +25 -1076
- package/dist/runtime/bootstrap.mjs +25 -1076
- package/dist/runtime/index.cjs +36 -1091
- package/dist/runtime/index.d.ts +20 -4
- package/dist/runtime/index.mjs +36 -1091
- package/package.json +2 -1
|
@@ -3,1092 +3,41 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* UX4G Runtime Module
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* Injects the vendor JS (ux4g.js + ux4g-custom.js) into the page as inline scripts.
|
|
7
|
+
* These files handle all interactive behaviors: dropdowns, modals, tooltips,
|
|
8
|
+
* accordions, carousels, drawers, tabs, popovers, toasts, scrollspy, etc.
|
|
9
|
+
*
|
|
10
|
+
* The vendor JS is inlined at build time by the Rollup virtual plugin.
|
|
11
|
+
* At runtime, initRuntime() creates <script> elements with the inlined code.
|
|
8
12
|
*
|
|
9
13
|
* Uses a singleton guard (window.__UX4G_RUNTIME_INITIALIZED__) to ensure
|
|
10
|
-
*
|
|
14
|
+
* the scripts are injected exactly once.
|
|
11
15
|
*/
|
|
12
16
|
const isBrowser = typeof window !== 'undefined' && typeof document !== 'undefined';
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
off(el, evt, cb, opts) { if (!el)
|
|
22
|
-
return; el.removeEventListener(evt, cb, opts); },
|
|
23
|
-
closest(el, sel) { return el ? el.closest(sel) : null; },
|
|
24
|
-
isVisible(el) { const h = el; return !!(el && (h.offsetWidth || h.offsetHeight || el.getClientRects().length)); },
|
|
25
|
-
reflow(el) { return el.offsetHeight; },
|
|
26
|
-
attr(el, name, fallback = null) { if (!el)
|
|
27
|
-
return fallback; const v = el.getAttribute(name); return v == null ? fallback : v; },
|
|
28
|
-
data(el, key, fallback = null) {
|
|
29
|
-
if (!el)
|
|
30
|
-
return fallback;
|
|
31
|
-
const ux = el.getAttribute(`data-ux-${key}`);
|
|
32
|
-
if (ux != null)
|
|
33
|
-
return ux;
|
|
34
|
-
const bs = el.getAttribute(`data-bs-${key}`);
|
|
35
|
-
if (bs != null)
|
|
36
|
-
return bs;
|
|
37
|
-
const u4 = el.getAttribute(`ux4g-${key}`);
|
|
38
|
-
if (u4 != null)
|
|
39
|
-
return u4;
|
|
40
|
-
return fallback;
|
|
41
|
-
},
|
|
42
|
-
bool(v, fallback = false) { if (v == null)
|
|
43
|
-
return fallback; if (typeof v === 'boolean')
|
|
44
|
-
return v; const s = String(v).trim().toLowerCase(); if (s === '' || s === 'true' || s === '1')
|
|
45
|
-
return true; if (s === 'false' || s === '0')
|
|
46
|
-
return false; return fallback; },
|
|
47
|
-
num(v, fallback = 0) { const n = Number(v); return Number.isFinite(n) ? n : fallback; },
|
|
48
|
-
dispatch(el, name, detail) { if (!el)
|
|
49
|
-
return; el.dispatchEvent(new CustomEvent(name, { bubbles: true, cancelable: true, detail })); },
|
|
50
|
-
focusables(root) { if (!root)
|
|
51
|
-
return []; return U.qsa('a[href],area[href],button:not([disabled]),input:not([disabled]):not([type="hidden"]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"]),[contenteditable="true"]', root).filter(el => U.isVisible(el)); },
|
|
52
|
-
lockBody(lock) { if (lock)
|
|
53
|
-
document.body.classList.add('ux4g-scroll-lock');
|
|
54
|
-
else
|
|
55
|
-
document.body.classList.remove('ux4g-scroll-lock'); },
|
|
56
|
-
ensureBackdrop(kind) { let bd = U.qs(`.ux4g-backdrop[data-kind="${kind}"]`); if (!bd) {
|
|
57
|
-
bd = document.createElement('div');
|
|
58
|
-
bd.className = 'ux4g-backdrop';
|
|
59
|
-
bd.setAttribute('data-kind', kind);
|
|
60
|
-
document.body.appendChild(bd);
|
|
61
|
-
} return bd; },
|
|
62
|
-
removeBackdrop(kind) { const bd = U.qs(`.ux4g-backdrop[data-kind="${kind}"]`); if (bd)
|
|
63
|
-
bd.remove(); },
|
|
64
|
-
placeFloating(target, floating, placement = 'bottom', offset = 8) {
|
|
65
|
-
if (!target || !floating)
|
|
66
|
-
return;
|
|
67
|
-
const rect = target.getBoundingClientRect();
|
|
68
|
-
const od = floating.style.display;
|
|
69
|
-
const ov = floating.style.visibility;
|
|
70
|
-
floating.style.display = 'block';
|
|
71
|
-
floating.style.visibility = 'hidden';
|
|
72
|
-
const fr = floating.getBoundingClientRect();
|
|
73
|
-
floating.style.display = od;
|
|
74
|
-
floating.style.visibility = ov;
|
|
75
|
-
const vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0);
|
|
76
|
-
const vh = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0);
|
|
77
|
-
const clamp = (v, min, max) => Math.min(max, Math.max(min, v));
|
|
78
|
-
const ws = placement.split('-')[0];
|
|
79
|
-
const opp = { top: 'bottom', bottom: 'top', left: 'right', right: 'left' };
|
|
80
|
-
const tries = [placement];
|
|
81
|
-
if (opp[ws])
|
|
82
|
-
tries.push(placement.replace(ws, opp[ws]));
|
|
83
|
-
['bottom', 'top', 'right', 'left'].forEach(s => { if (!tries.includes(s))
|
|
84
|
-
tries.push(s); });
|
|
85
|
-
const compute = (p) => {
|
|
86
|
-
let t = 0, l = 0;
|
|
87
|
-
const [side, align] = p.split('-');
|
|
88
|
-
if (side === 'top') {
|
|
89
|
-
t = rect.top - fr.height - offset;
|
|
90
|
-
l = align === 'start' ? rect.left : align === 'end' ? rect.right - fr.width : rect.left + (rect.width - fr.width) / 2;
|
|
91
|
-
}
|
|
92
|
-
else if (side === 'bottom') {
|
|
93
|
-
t = rect.bottom + offset;
|
|
94
|
-
l = align === 'start' ? rect.left : align === 'end' ? rect.right - fr.width : rect.left + (rect.width - fr.width) / 2;
|
|
95
|
-
}
|
|
96
|
-
else if (side === 'left') {
|
|
97
|
-
l = rect.left - fr.width - offset;
|
|
98
|
-
t = align === 'start' ? rect.top : align === 'end' ? rect.bottom - fr.height : rect.top + (rect.height - fr.height) / 2;
|
|
99
|
-
}
|
|
100
|
-
else if (side === 'right') {
|
|
101
|
-
l = rect.right + offset;
|
|
102
|
-
t = align === 'start' ? rect.top : align === 'end' ? rect.bottom - fr.height : rect.top + (rect.height - fr.height) / 2;
|
|
103
|
-
}
|
|
104
|
-
else {
|
|
105
|
-
t = rect.bottom + offset;
|
|
106
|
-
l = rect.left + (rect.width - fr.width) / 2;
|
|
107
|
-
p = 'bottom';
|
|
108
|
-
}
|
|
109
|
-
return { t, l, p };
|
|
110
|
-
};
|
|
111
|
-
let chosen = null;
|
|
112
|
-
for (const p of tries) {
|
|
113
|
-
const c = compute(p);
|
|
114
|
-
if (c.t >= 0 && (c.t + fr.height) <= vh && c.l >= 0 && (c.l + fr.width) <= vw) {
|
|
115
|
-
chosen = c;
|
|
116
|
-
break;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
if (!chosen)
|
|
120
|
-
chosen = compute(placement);
|
|
121
|
-
const top = clamp(chosen.t, 8, Math.max(8, vh - fr.height - 8));
|
|
122
|
-
const left = clamp(chosen.l, 8, Math.max(8, vw - fr.width - 8));
|
|
123
|
-
floating.style.position = 'fixed';
|
|
124
|
-
floating.style.top = `${top}px`;
|
|
125
|
-
floating.style.left = `${left}px`;
|
|
126
|
-
floating.setAttribute('data-placement', chosen.p);
|
|
127
|
-
const isTooltip = floating.classList.contains('ux4g-tooltip');
|
|
128
|
-
const base = isTooltip ? 'ux4g-tooltip' : 'ux4g-popover';
|
|
129
|
-
Array.from(floating.classList).forEach(cls => { if (cls.startsWith(`${base}-`) && cls !== base)
|
|
130
|
-
floating.classList.remove(cls); });
|
|
131
|
-
floating.classList.add(`${base}-${chosen.p}`);
|
|
132
|
-
},
|
|
133
|
-
repositionMenu(container, menu) {
|
|
134
|
-
if (!menu)
|
|
135
|
-
return;
|
|
136
|
-
menu.style.top = '';
|
|
137
|
-
menu.style.bottom = '';
|
|
138
|
-
menu.style.left = '';
|
|
139
|
-
menu.style.right = '';
|
|
140
|
-
const vh = window.innerHeight;
|
|
141
|
-
const vw = window.innerWidth;
|
|
142
|
-
const mr = menu.getBoundingClientRect();
|
|
143
|
-
const cr = container.getBoundingClientRect();
|
|
144
|
-
if (mr.bottom > vh && cr.top > mr.height) {
|
|
145
|
-
menu.style.top = 'auto';
|
|
146
|
-
menu.style.bottom = '100%';
|
|
147
|
-
}
|
|
148
|
-
const ur = menu.getBoundingClientRect();
|
|
149
|
-
if (ur.right > vw) {
|
|
150
|
-
menu.style.left = 'auto';
|
|
151
|
-
menu.style.right = '0';
|
|
152
|
-
}
|
|
153
|
-
if (ur.left < 0) {
|
|
154
|
-
menu.style.left = '0';
|
|
155
|
-
menu.style.right = 'auto';
|
|
156
|
-
}
|
|
157
|
-
},
|
|
158
|
-
};
|
|
159
|
-
function getI(el, key) { return Registry.get(el)?.[key] || null; }
|
|
160
|
-
function setI(el, key, inst) { let m = Registry.get(el); if (!m) {
|
|
161
|
-
m = {};
|
|
162
|
-
Registry.set(el, m);
|
|
163
|
-
} m[key] = inst; }
|
|
164
|
-
function escapeHtml(s) { return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, '''); }
|
|
165
|
-
class DropdownComponent {
|
|
166
|
-
constructor(toggle) {
|
|
167
|
-
this._open = false;
|
|
168
|
-
this.toggle = toggle;
|
|
169
|
-
this.menu = this._findMenu(toggle);
|
|
170
|
-
U.on(this.toggle, 'click', ((e) => { e.preventDefault(); this.toggleDropdown(); }));
|
|
171
|
-
U.on(document, 'click', ((e) => { if (!this._open)
|
|
172
|
-
return; const t = e.target; if (this.menu && (this.menu.contains(t) || this.toggle.contains(t)))
|
|
173
|
-
return; this.hide(); }));
|
|
174
|
-
U.on(document, 'keydown', ((e) => { if (!this._open)
|
|
175
|
-
return; if (e.key === 'Escape') {
|
|
176
|
-
this.hide();
|
|
177
|
-
this.toggle.focus();
|
|
178
|
-
} }));
|
|
179
|
-
}
|
|
180
|
-
_findMenu(toggle) { const p = toggle.parentElement; let m = p ? p.querySelector('.dropdown-menu') : null; if (!m) {
|
|
181
|
-
const t = U.data(toggle, 'target') || U.attr(toggle, 'aria-controls');
|
|
182
|
-
if (t && t.startsWith('#'))
|
|
183
|
-
m = U.qs(t);
|
|
184
|
-
} return m; }
|
|
185
|
-
show() { if (!this.menu)
|
|
186
|
-
return; this._open = true; this.toggle.classList.add('show'); this.menu.classList.add('show'); this.toggle.setAttribute('aria-expanded', 'true'); U.placeFloating(this.toggle, this.menu, U.data(this.toggle, 'placement', 'bottom') || 'bottom', U.num(U.data(this.toggle, 'offset', '6'), 6)); U.dispatch(this.toggle, 'ux4g.dropdown.shown', { menu: this.menu }); }
|
|
187
|
-
hide() { if (!this.menu)
|
|
188
|
-
return; this._open = false; this.toggle.classList.remove('show'); this.menu.classList.remove('show'); this.toggle.setAttribute('aria-expanded', 'false'); U.dispatch(this.toggle, 'ux4g.dropdown.hidden', { menu: this.menu }); }
|
|
189
|
-
toggleDropdown() { this._open ? this.hide() : this.show(); }
|
|
190
|
-
static getOrCreate(el) { let i = getI(el, 'dropdown'); if (!i) {
|
|
191
|
-
i = new DropdownComponent(el);
|
|
192
|
-
setI(el, 'dropdown', i);
|
|
193
|
-
} return i; }
|
|
194
|
-
}
|
|
195
|
-
class CollapseComponent {
|
|
196
|
-
constructor(trigger) {
|
|
197
|
-
this.trigger = trigger;
|
|
198
|
-
this.target = this._resolveTarget(trigger);
|
|
199
|
-
this.parentSel = U.data(this.target, 'parent') || U.data(this.trigger, 'parent');
|
|
200
|
-
this.duration = this._readDur(this.target, 200);
|
|
201
|
-
U.on(this.trigger, 'click', ((e) => { e.preventDefault(); this.toggle(); }));
|
|
202
|
-
}
|
|
203
|
-
_resolveTarget(tr) { const s = U.data(tr, 'target') || U.attr(tr, 'href') || U.attr(tr, 'aria-controls') || U.attr(tr, 'ux4g-target'); if (s && s.startsWith('#'))
|
|
204
|
-
return U.qs(s); if (s)
|
|
205
|
-
return U.qs('#' + s); return null; }
|
|
206
|
-
_readDur(el, fb) { if (!el)
|
|
207
|
-
return fb; const d = getComputedStyle(el).transitionDuration || ''; const ms = d.includes('ms') ? parseFloat(d) : (d.includes('s') ? parseFloat(d) * 1000 : NaN); return Number.isFinite(ms) && ms > 0 ? ms : fb; }
|
|
208
|
-
show() {
|
|
209
|
-
if (!this.target)
|
|
210
|
-
return;
|
|
211
|
-
const t = this.target;
|
|
212
|
-
if (this.parentSel) {
|
|
213
|
-
const p = U.qs(this.parentSel);
|
|
214
|
-
if (p)
|
|
215
|
-
U.qsa('.collapse.show', p).forEach(el => { if (el === this.target)
|
|
216
|
-
return; el.classList.remove('show'); if (el.id) {
|
|
217
|
-
const id = el.id;
|
|
218
|
-
U.qsa(`[data-bs-target="#${id}"],[data-ux-target="#${id}"],[ux4g-target="#${id}"],a[href="#${id}"]`).forEach(x => { x.classList.add('collapsed'); x.setAttribute('aria-expanded', 'false'); });
|
|
219
|
-
} });
|
|
220
|
-
}
|
|
221
|
-
t.classList.add('collapsing');
|
|
222
|
-
t.classList.remove('collapse');
|
|
223
|
-
t.style.height = '0px';
|
|
224
|
-
U.reflow(t);
|
|
225
|
-
t.style.height = t.scrollHeight + 'px';
|
|
226
|
-
this.trigger.classList.remove('collapsed');
|
|
227
|
-
this.trigger.setAttribute('aria-expanded', 'true');
|
|
228
|
-
setTimeout(() => { t.classList.remove('collapsing'); t.classList.add('collapse', 'show'); t.style.height = ''; U.dispatch(t, 'ux4g.collapse.shown', {}); }, this.duration);
|
|
229
|
-
}
|
|
230
|
-
hide() {
|
|
231
|
-
if (!this.target)
|
|
232
|
-
return;
|
|
233
|
-
const t = this.target;
|
|
234
|
-
t.style.height = t.getBoundingClientRect().height + 'px';
|
|
235
|
-
U.reflow(t);
|
|
236
|
-
t.classList.add('collapsing');
|
|
237
|
-
t.classList.remove('collapse', 'show');
|
|
238
|
-
this.trigger.classList.add('collapsed');
|
|
239
|
-
this.trigger.setAttribute('aria-expanded', 'false');
|
|
240
|
-
setTimeout(() => { t.style.height = '0px'; }, 10);
|
|
241
|
-
setTimeout(() => { t.classList.remove('collapsing'); t.classList.add('collapse'); t.style.height = ''; U.dispatch(t, 'ux4g.collapse.hidden', {}); }, this.duration);
|
|
242
|
-
}
|
|
243
|
-
toggle() { if (!this.target)
|
|
244
|
-
return; this.target.classList.contains('show') ? this.hide() : this.show(); }
|
|
245
|
-
static getOrCreate(el) { let i = getI(el, 'collapse'); if (!i) {
|
|
246
|
-
i = new CollapseComponent(el);
|
|
247
|
-
setI(el, 'collapse', i);
|
|
248
|
-
} return i; }
|
|
249
|
-
}
|
|
250
|
-
class ModalComponent {
|
|
251
|
-
constructor(el) {
|
|
252
|
-
this._shown = false;
|
|
253
|
-
this._bdKind = 'modal';
|
|
254
|
-
this._lastFocus = null;
|
|
255
|
-
this.el = el;
|
|
256
|
-
this.duration = 250;
|
|
257
|
-
U.on(this.el, 'click', ((e) => { const d = U.closest(e.target, '[data-bs-dismiss="modal"],[data-ux-dismiss="modal"],.close-modal'); if (d) {
|
|
258
|
-
e.preventDefault();
|
|
259
|
-
this.hide();
|
|
260
|
-
} }));
|
|
261
|
-
U.on(document, 'keydown', ((e) => { if (!this._shown)
|
|
262
|
-
return; if (e.key === 'Escape') {
|
|
263
|
-
if (U.bool(U.data(this.el, 'keyboard', 'true'), true))
|
|
264
|
-
this.hide();
|
|
265
|
-
}
|
|
266
|
-
else if (e.key === 'Tab') {
|
|
267
|
-
this._trapTab(e);
|
|
268
|
-
} }));
|
|
269
|
-
}
|
|
270
|
-
_trapTab(e) { const f = U.focusables(this.el); if (!f.length)
|
|
271
|
-
return; const first = f[0], last = f[f.length - 1]; if (e.shiftKey && document.activeElement === first) {
|
|
272
|
-
e.preventDefault();
|
|
273
|
-
last.focus();
|
|
274
|
-
}
|
|
275
|
-
else if (!e.shiftKey && document.activeElement === last) {
|
|
276
|
-
e.preventDefault();
|
|
277
|
-
first.focus();
|
|
278
|
-
} }
|
|
279
|
-
show(trigger) {
|
|
280
|
-
if (this._shown)
|
|
281
|
-
return;
|
|
282
|
-
this._shown = true;
|
|
283
|
-
this._lastFocus = document.activeElement;
|
|
284
|
-
const bo = U.data(this.el, 'backdrop', 'true');
|
|
285
|
-
if (bo !== 'false') {
|
|
286
|
-
const bd = U.ensureBackdrop(this._bdKind);
|
|
287
|
-
bd.classList.add('show');
|
|
288
|
-
U.on(bd, 'click', (() => { if (bo === 'static')
|
|
289
|
-
return; this.hide(); }));
|
|
290
|
-
}
|
|
291
|
-
U.lockBody(true);
|
|
292
|
-
this.el.style.display = 'block';
|
|
293
|
-
this.el.removeAttribute('aria-hidden');
|
|
294
|
-
this.el.setAttribute('aria-modal', 'true');
|
|
295
|
-
this.el.setAttribute('role', this.el.getAttribute('role') || 'dialog');
|
|
296
|
-
U.reflow(this.el);
|
|
297
|
-
this.el.classList.add('show');
|
|
298
|
-
if (U.bool(U.data(this.el, 'focus', 'true'), true)) {
|
|
299
|
-
const f = U.focusables(this.el);
|
|
300
|
-
(f[0] || this.el).focus({ preventScroll: true });
|
|
301
|
-
}
|
|
302
|
-
U.dispatch(this.el, 'ux4g.modal.shown', { relatedTarget: trigger || null });
|
|
303
|
-
}
|
|
304
|
-
hide() {
|
|
305
|
-
if (!this._shown)
|
|
306
|
-
return;
|
|
307
|
-
this._shown = false;
|
|
308
|
-
this.el.classList.remove('show');
|
|
309
|
-
this.el.setAttribute('aria-hidden', 'true');
|
|
310
|
-
this.el.removeAttribute('aria-modal');
|
|
311
|
-
setTimeout(() => { this.el.style.display = 'none'; U.lockBody(false); U.removeBackdrop(this._bdKind); if (this._lastFocus?.focus)
|
|
312
|
-
this._lastFocus.focus({ preventScroll: true }); U.dispatch(this.el, 'ux4g.modal.hidden', {}); }, this.duration);
|
|
313
|
-
}
|
|
314
|
-
toggle(trigger) { this._shown ? this.hide() : this.show(trigger); }
|
|
315
|
-
static getOrCreate(el) { let i = getI(el, 'modal'); if (!i) {
|
|
316
|
-
i = new ModalComponent(el);
|
|
317
|
-
setI(el, 'modal', i);
|
|
318
|
-
} return i; }
|
|
319
|
-
}
|
|
320
|
-
class OffcanvasComponent {
|
|
321
|
-
constructor(el) {
|
|
322
|
-
this._shown = false;
|
|
323
|
-
this._bdKind = 'offcanvas';
|
|
324
|
-
this._lastFocus = null;
|
|
325
|
-
this.duration = 250;
|
|
326
|
-
this.el = el;
|
|
327
|
-
U.on(this.el, 'click', ((e) => { const d = U.closest(e.target, '[data-bs-dismiss="offcanvas"],[data-ux-dismiss="offcanvas"]'); if (d) {
|
|
328
|
-
e.preventDefault();
|
|
329
|
-
this.hide();
|
|
330
|
-
} }));
|
|
331
|
-
U.on(document, 'keydown', ((e) => { if (!this._shown)
|
|
332
|
-
return; if (e.key === 'Escape') {
|
|
333
|
-
if (U.bool(U.data(this.el, 'keyboard', 'true'), true))
|
|
334
|
-
this.hide();
|
|
335
|
-
}
|
|
336
|
-
else if (e.key === 'Tab') {
|
|
337
|
-
const f = U.focusables(this.el);
|
|
338
|
-
if (!f.length)
|
|
339
|
-
return;
|
|
340
|
-
const first = f[0], last = f[f.length - 1];
|
|
341
|
-
if (e.shiftKey && document.activeElement === first) {
|
|
342
|
-
e.preventDefault();
|
|
343
|
-
last.focus();
|
|
344
|
-
}
|
|
345
|
-
else if (!e.shiftKey && document.activeElement === last) {
|
|
346
|
-
e.preventDefault();
|
|
347
|
-
first.focus();
|
|
348
|
-
}
|
|
349
|
-
} }));
|
|
350
|
-
}
|
|
351
|
-
show(trigger) {
|
|
352
|
-
if (this._shown)
|
|
353
|
-
return;
|
|
354
|
-
this._shown = true;
|
|
355
|
-
this._lastFocus = document.activeElement;
|
|
356
|
-
const bo = U.data(this.el, 'backdrop', 'true');
|
|
357
|
-
if (bo !== 'false') {
|
|
358
|
-
const bd = U.ensureBackdrop(this._bdKind);
|
|
359
|
-
bd.classList.add('show');
|
|
360
|
-
U.on(bd, 'click', (() => { if (bo === 'static')
|
|
361
|
-
return; this.hide(); }));
|
|
362
|
-
}
|
|
363
|
-
U.lockBody(true);
|
|
364
|
-
this.el.style.visibility = 'visible';
|
|
365
|
-
this.el.classList.add('show');
|
|
366
|
-
this.el.setAttribute('aria-modal', 'true');
|
|
367
|
-
if (U.bool(U.data(this.el, 'focus', 'true'), true)) {
|
|
368
|
-
const f = U.focusables(this.el);
|
|
369
|
-
(f[0] || this.el).focus({ preventScroll: true });
|
|
370
|
-
}
|
|
371
|
-
U.dispatch(this.el, 'ux4g.offcanvas.shown', { relatedTarget: trigger || null });
|
|
372
|
-
}
|
|
373
|
-
hide() {
|
|
374
|
-
if (!this._shown)
|
|
375
|
-
return;
|
|
376
|
-
this._shown = false;
|
|
377
|
-
this.el.classList.remove('show');
|
|
378
|
-
this.el.removeAttribute('aria-modal');
|
|
379
|
-
setTimeout(() => { this.el.style.visibility = ''; U.lockBody(false); U.removeBackdrop(this._bdKind); if (this._lastFocus?.focus)
|
|
380
|
-
this._lastFocus.focus({ preventScroll: true }); U.dispatch(this.el, 'ux4g.offcanvas.hidden', {}); }, this.duration);
|
|
381
|
-
}
|
|
382
|
-
toggle(trigger) { this._shown ? this.hide() : this.show(trigger); }
|
|
383
|
-
static getOrCreate(el) { let i = getI(el, 'offcanvas'); if (!i) {
|
|
384
|
-
i = new OffcanvasComponent(el);
|
|
385
|
-
setI(el, 'offcanvas', i);
|
|
386
|
-
} return i; }
|
|
387
|
-
}
|
|
388
|
-
class FloatingComponent {
|
|
389
|
-
constructor(el, kind) {
|
|
390
|
-
this._open = false;
|
|
391
|
-
this._floating = null;
|
|
392
|
-
this._onWin = null;
|
|
393
|
-
this._raf = null;
|
|
394
|
-
this.el = el;
|
|
395
|
-
this.kind = kind;
|
|
396
|
-
this.placement = U.data(el, 'placement', kind === 'tooltip' ? 'top' : 'right') || (kind === 'tooltip' ? 'top' : 'right');
|
|
397
|
-
this.offset = U.num(U.data(el, 'offset', '8'), 8);
|
|
398
|
-
this.triggerMode = U.data(el, 'trigger', kind === 'tooltip' ? 'hover focus' : 'click') || (kind === 'tooltip' ? 'hover focus' : 'click');
|
|
399
|
-
this.html = U.bool(U.data(el, 'html', 'false'), false);
|
|
400
|
-
this._bind();
|
|
401
|
-
}
|
|
402
|
-
_getContent() {
|
|
403
|
-
const content = U.data(this.el, 'content');
|
|
404
|
-
if (this.kind === 'popover') {
|
|
405
|
-
const title = U.data(this.el, 'title') || this.el.getAttribute('title') || '';
|
|
406
|
-
const body = content || this.el.getAttribute('data-content') || '';
|
|
407
|
-
const t = this.html ? String(title) : escapeHtml(title);
|
|
408
|
-
const b = this.html ? String(body) : escapeHtml(body);
|
|
409
|
-
return `<div class="ux4g-popover-header">${t}</div><div class="ux4g-popover-body">${b}</div>`;
|
|
410
|
-
}
|
|
411
|
-
const t = content != null ? content : (this.el.getAttribute('title') || '');
|
|
412
|
-
return this.html ? String(t) : escapeHtml(t);
|
|
413
|
-
}
|
|
414
|
-
_create() { if (this._floating)
|
|
415
|
-
return; const div = document.createElement('div'); div.className = this.kind === 'tooltip' ? 'ux4g-tooltip' : 'ux4g-popover'; div.setAttribute('role', this.kind === 'tooltip' ? 'tooltip' : 'dialog'); div.innerHTML = this._getContent() || ''; document.body.appendChild(div); this._floating = div; }
|
|
416
|
-
show() {
|
|
417
|
-
if (this._open)
|
|
418
|
-
return;
|
|
419
|
-
this._open = true;
|
|
420
|
-
if (this.kind === 'tooltip') {
|
|
421
|
-
const t = this.el.getAttribute('title');
|
|
422
|
-
if (t != null) {
|
|
423
|
-
this.el.setAttribute('data-ux-original-title', t);
|
|
424
|
-
this.el.removeAttribute('title');
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
this._create();
|
|
428
|
-
this._floating.style.display = 'block';
|
|
429
|
-
this._floating.classList.add('show');
|
|
430
|
-
const update = () => { if (!this._open)
|
|
431
|
-
return; U.placeFloating(this.el, this._floating, this.placement, this.offset); this._raf = requestAnimationFrame(update); };
|
|
432
|
-
this._onWin = update;
|
|
433
|
-
this._raf = requestAnimationFrame(update);
|
|
434
|
-
U.on(window, 'scroll', this._onWin, { capture: true, passive: true });
|
|
435
|
-
U.on(window, 'resize', this._onWin);
|
|
436
|
-
U.dispatch(this.el, `ux4g.${this.kind}.shown`, {});
|
|
437
|
-
}
|
|
438
|
-
hide() {
|
|
439
|
-
if (!this._open)
|
|
440
|
-
return;
|
|
441
|
-
this._open = false;
|
|
442
|
-
if (this._floating) {
|
|
443
|
-
this._floating.classList.remove('show');
|
|
444
|
-
this._floating.style.display = 'none';
|
|
445
|
-
}
|
|
446
|
-
if (this.kind === 'tooltip') {
|
|
447
|
-
const ot = this.el.getAttribute('data-ux-original-title');
|
|
448
|
-
if (ot != null) {
|
|
449
|
-
this.el.setAttribute('title', ot);
|
|
450
|
-
this.el.removeAttribute('data-ux-original-title');
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
if (this._raf) {
|
|
454
|
-
cancelAnimationFrame(this._raf);
|
|
455
|
-
this._raf = null;
|
|
456
|
-
}
|
|
457
|
-
if (this._onWin) {
|
|
458
|
-
U.off(window, 'scroll', this._onWin, { capture: true });
|
|
459
|
-
U.off(window, 'resize', this._onWin);
|
|
460
|
-
this._onWin = null;
|
|
461
|
-
}
|
|
462
|
-
U.dispatch(this.el, `ux4g.${this.kind}.hidden`, {});
|
|
463
|
-
}
|
|
464
|
-
toggle() { this._open ? this.hide() : this.show(); }
|
|
465
|
-
_bind() {
|
|
466
|
-
let triggers = String(this.triggerMode).split(/\s+/).filter(Boolean);
|
|
467
|
-
if (this.kind === 'popover') {
|
|
468
|
-
triggers = triggers.filter(t => t !== 'hover');
|
|
469
|
-
if (!triggers.length)
|
|
470
|
-
triggers = ['click'];
|
|
471
|
-
}
|
|
472
|
-
if (triggers.includes('hover')) {
|
|
473
|
-
U.on(this.el, 'mouseenter', (() => this.show()));
|
|
474
|
-
U.on(this.el, 'mouseleave', (() => this.hide()));
|
|
475
|
-
}
|
|
476
|
-
if (triggers.includes('focus')) {
|
|
477
|
-
U.on(this.el, 'focus', (() => this.show()));
|
|
478
|
-
U.on(this.el, 'blur', (() => this.hide()));
|
|
479
|
-
}
|
|
480
|
-
if (triggers.includes('click')) {
|
|
481
|
-
U.on(this.el, 'click', ((e) => { e.preventDefault(); this.toggle(); }));
|
|
482
|
-
U.on(document, 'click', ((e) => { if (!this._open)
|
|
483
|
-
return; const t = e.target; if (this.el.contains(t) || (this._floating && this._floating.contains(t)))
|
|
484
|
-
return; this.hide(); }));
|
|
485
|
-
U.on(document, 'keydown', ((e) => { if (!this._open)
|
|
486
|
-
return; if (e.key === 'Escape')
|
|
487
|
-
this.hide(); }));
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
static getOrCreate(el, kind) { let i = getI(el, kind); if (!i) {
|
|
491
|
-
i = new FloatingComponent(el, kind);
|
|
492
|
-
setI(el, kind, i);
|
|
493
|
-
} return i; }
|
|
494
|
-
}
|
|
495
|
-
class ToastComponent {
|
|
496
|
-
constructor(el) {
|
|
497
|
-
this._timer = null;
|
|
498
|
-
this.el = el;
|
|
499
|
-
U.on(this.el, 'click', ((e) => { const d = U.closest(e.target, '[data-bs-dismiss="toast"],[data-ux-dismiss="toast"],.close-toast'); if (d) {
|
|
500
|
-
e.preventDefault();
|
|
501
|
-
this.hide();
|
|
502
|
-
} }));
|
|
503
|
-
}
|
|
504
|
-
show() { this.el.classList.add('show'); this.el.classList.remove('hide'); const ah = U.bool(U.data(this.el, 'autohide', 'true'), true); const delay = U.num(U.data(this.el, 'delay', '5000'), 5000); if (ah) {
|
|
505
|
-
if (this._timer)
|
|
506
|
-
clearTimeout(this._timer);
|
|
507
|
-
this._timer = setTimeout(() => this.hide(), delay);
|
|
508
|
-
} U.dispatch(this.el, 'ux4g.toast.shown', {}); }
|
|
509
|
-
hide() { this.el.classList.remove('show'); this.el.classList.add('hide'); if (this._timer)
|
|
510
|
-
clearTimeout(this._timer); U.dispatch(this.el, 'ux4g.toast.hidden', {}); }
|
|
511
|
-
static getOrCreate(el) { let i = getI(el, 'toast'); if (!i) {
|
|
512
|
-
i = new ToastComponent(el);
|
|
513
|
-
setI(el, 'toast', i);
|
|
514
|
-
} return i; }
|
|
515
|
-
}
|
|
516
|
-
class CarouselComponent {
|
|
517
|
-
constructor(el) {
|
|
518
|
-
this._timer = null;
|
|
519
|
-
this.el = el;
|
|
520
|
-
this.items = U.qsa('.carousel-item', el);
|
|
521
|
-
this.indicators = U.qsa('[data-bs-slide-to],[data-ux-slide-to]', el);
|
|
522
|
-
this.interval = U.num(U.data(el, 'interval', '5000'), 5000);
|
|
523
|
-
this.wrap = U.bool(U.data(el, 'wrap', 'true'), true);
|
|
524
|
-
U.on(el, 'click', ((e) => { const t = e.target; if (U.closest(t, '[data-bs-slide="prev"],[data-ux-slide="prev"]')) {
|
|
525
|
-
e.preventDefault();
|
|
526
|
-
this.prev();
|
|
527
|
-
} if (U.closest(t, '[data-bs-slide="next"],[data-ux-slide="next"]')) {
|
|
528
|
-
e.preventDefault();
|
|
529
|
-
this.next();
|
|
530
|
-
} const ind = U.closest(t, '[data-bs-slide-to],[data-ux-slide-to]'); if (ind) {
|
|
531
|
-
e.preventDefault();
|
|
532
|
-
this.to(U.num(ind.getAttribute('data-bs-slide-to') ?? ind.getAttribute('data-ux-slide-to'), 0));
|
|
533
|
-
} }));
|
|
534
|
-
if (U.data(el, 'pause', 'hover') === 'hover') {
|
|
535
|
-
U.on(el, 'mouseenter', (() => this._stop()));
|
|
536
|
-
U.on(el, 'mouseleave', (() => this._start()));
|
|
537
|
-
}
|
|
538
|
-
if (U.data(el, 'ride') === 'carousel')
|
|
539
|
-
this._start();
|
|
540
|
-
}
|
|
541
|
-
_activeIndex() { const idx = this.items.findIndex(i => i.classList.contains('active')); return idx >= 0 ? idx : 0; }
|
|
542
|
-
_setActive(ni) {
|
|
543
|
-
if (!this.items.length)
|
|
544
|
-
return;
|
|
545
|
-
const cur = this._activeIndex();
|
|
546
|
-
if (ni < 0)
|
|
547
|
-
ni = this.wrap ? this.items.length - 1 : 0;
|
|
548
|
-
if (ni >= this.items.length)
|
|
549
|
-
ni = this.wrap ? 0 : this.items.length - 1;
|
|
550
|
-
if (cur === ni)
|
|
551
|
-
return;
|
|
552
|
-
this.items[cur]?.classList.remove('active');
|
|
553
|
-
this.items[ni]?.classList.add('active');
|
|
554
|
-
this.indicators.forEach(ind => ind.classList.remove('active'));
|
|
555
|
-
const mi = this.indicators.find(x => U.num(x.getAttribute('data-bs-slide-to') ?? x.getAttribute('data-ux-slide-to'), -1) === ni);
|
|
556
|
-
if (mi)
|
|
557
|
-
mi.classList.add('active');
|
|
558
|
-
U.dispatch(this.el, 'ux4g.carousel.slid', { from: cur, to: ni });
|
|
559
|
-
}
|
|
560
|
-
next() { this._setActive(this._activeIndex() + 1); }
|
|
561
|
-
prev() { this._setActive(this._activeIndex() - 1); }
|
|
562
|
-
to(i) { this._setActive(i); }
|
|
563
|
-
_start() { if (this._timer || this.interval <= 0)
|
|
564
|
-
return; this._timer = setInterval(() => this.next(), this.interval); }
|
|
565
|
-
_stop() { if (this._timer) {
|
|
566
|
-
clearInterval(this._timer);
|
|
567
|
-
this._timer = null;
|
|
568
|
-
} }
|
|
569
|
-
static getOrCreate(el) { let i = getI(el, 'carousel'); if (!i) {
|
|
570
|
-
i = new CarouselComponent(el);
|
|
571
|
-
setI(el, 'carousel', i);
|
|
572
|
-
} return i; }
|
|
573
|
-
}
|
|
574
|
-
class TabComponent {
|
|
575
|
-
constructor(el) {
|
|
576
|
-
this.el = el;
|
|
577
|
-
U.on(el, 'click', ((e) => { e.preventDefault(); this.show(); }));
|
|
578
|
-
U.on(el, 'keydown', ((e) => { if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight')
|
|
579
|
-
return; const list = U.closest(this.el, ".nav, [role='tablist']"); if (!list)
|
|
580
|
-
return; const tabs = U.qsa("[data-bs-toggle='tab'],[data-ux-toggle='tab'],[role='tab']", list); const idx = tabs.indexOf(this.el); if (idx < 0)
|
|
581
|
-
return; e.preventDefault(); const n = e.key === 'ArrowRight' ? idx + 1 : idx - 1; const wi = (n + tabs.length) % tabs.length; tabs[wi].focus(); TabComponent.getOrCreate(tabs[wi]).show(); }));
|
|
582
|
-
}
|
|
583
|
-
_target() { const s = U.data(this.el, 'target') || U.attr(this.el, 'href') || U.attr(this.el, 'data-target'); if (s && s.startsWith('#'))
|
|
584
|
-
return U.qs(s); const c = this.el.getAttribute('aria-controls'); if (c)
|
|
585
|
-
return U.qs('#' + c); return null; }
|
|
586
|
-
show() {
|
|
587
|
-
const list = U.closest(this.el, ".nav, [role='tablist']");
|
|
588
|
-
const pane = this._target();
|
|
589
|
-
if (!list || !pane)
|
|
590
|
-
return;
|
|
591
|
-
U.qsa("[data-bs-toggle='tab'],[data-ux-toggle='tab'],[role='tab']", list).forEach(t => { t.classList.remove('active'); t.setAttribute('aria-selected', 'false'); t.setAttribute('tabindex', '-1'); });
|
|
592
|
-
this.el.classList.add('active');
|
|
593
|
-
this.el.setAttribute('aria-selected', 'true');
|
|
594
|
-
this.el.setAttribute('tabindex', '0');
|
|
595
|
-
const container = U.closest(pane, '.tab-content') || pane.parentElement;
|
|
596
|
-
if (container)
|
|
597
|
-
U.qsa('.tab-pane', container).forEach(p => p.classList.remove('active', 'show'));
|
|
598
|
-
pane.classList.add('active', 'show');
|
|
599
|
-
U.dispatch(this.el, 'ux4g.tab.shown', { relatedTarget: pane });
|
|
600
|
-
}
|
|
601
|
-
static getOrCreate(el) { let i = getI(el, 'tab'); if (!i) {
|
|
602
|
-
i = new TabComponent(el);
|
|
603
|
-
setI(el, 'tab', i);
|
|
604
|
-
} return i; }
|
|
605
|
-
}
|
|
606
|
-
class ScrollSpyComponent {
|
|
607
|
-
constructor(el) {
|
|
608
|
-
this._links = [];
|
|
609
|
-
this._sections = [];
|
|
610
|
-
this._io = null;
|
|
611
|
-
this.el = el;
|
|
612
|
-
this.targetSel = U.data(el, 'target');
|
|
613
|
-
this.offset = U.num(U.data(el, 'offset', '10'), 10);
|
|
614
|
-
this.refresh();
|
|
615
|
-
this._bind();
|
|
616
|
-
}
|
|
617
|
-
refresh() { const nav = this.targetSel ? U.qs(this.targetSel) : null; if (!nav)
|
|
618
|
-
return; this._links = U.qsa('a[href^="#"]', nav).filter(a => (a.getAttribute('href') || '').length > 1); this._sections = this._links.map(a => U.qs(a.getAttribute('href'))).filter(Boolean); }
|
|
619
|
-
_activate(id) { const nav = this.targetSel ? U.qs(this.targetSel) : null; if (!nav)
|
|
620
|
-
return; this._links.forEach(a => a.classList.remove('active')); const link = this._links.find(a => a.getAttribute('href') === '#' + id); if (link)
|
|
621
|
-
link.classList.add('active'); }
|
|
622
|
-
_bind() {
|
|
623
|
-
const container = (this.el === document.body || this.el === document.documentElement) ? window : this.el;
|
|
624
|
-
if ('IntersectionObserver' in window) {
|
|
625
|
-
const root = container === window ? null : this.el;
|
|
626
|
-
this._io = new IntersectionObserver((entries) => { const v = entries.filter(e => e.isIntersecting).sort((a, b) => b.intersectionRatio - a.intersectionRatio)[0]; if (v?.target?.id)
|
|
627
|
-
this._activate(v.target.id); }, { root, rootMargin: `-${this.offset}px 0px -60% 0px`, threshold: [0.1, 0.25, 0.5, 0.75] });
|
|
628
|
-
this._sections.forEach(s => this._io.observe(s));
|
|
629
|
-
activeObservers.push(this._io);
|
|
630
|
-
return;
|
|
631
|
-
}
|
|
632
|
-
const onScroll = () => { const st = container === window ? window.pageYOffset : this.el.scrollTop; let active = null; for (const s of this._sections) {
|
|
633
|
-
if (st + this.offset >= s.getBoundingClientRect().top + window.pageYOffset)
|
|
634
|
-
active = s;
|
|
635
|
-
} if (active?.id)
|
|
636
|
-
this._activate(active.id); };
|
|
637
|
-
U.on(container, 'scroll', onScroll, { passive: true });
|
|
638
|
-
onScroll();
|
|
639
|
-
}
|
|
640
|
-
static getOrCreate(el) { let i = getI(el, 'scrollspy'); if (!i) {
|
|
641
|
-
i = new ScrollSpyComponent(el);
|
|
642
|
-
setI(el, 'scrollspy', i);
|
|
643
|
-
} return i; }
|
|
644
|
-
}
|
|
645
|
-
class TableComponent {
|
|
646
|
-
constructor(el) { this.el = el; this._bindSort(); this._bindSelection(); }
|
|
647
|
-
_bindSort() { const cols = U.qsa('.ux4g-table-sortable th[data-sort]', this.el); cols.forEach(th => { U.on(th, 'click', ((e) => { if (U.closest(e.target, '.ux4g-table-filter-icon,.ux4g-search-input,.ux4g-search-clear'))
|
|
648
|
-
return; const cur = U.attr(th, 'data-sort', 'none'); const next = cur === 'asc' ? 'desc' : 'asc'; cols.forEach(o => { if (o !== th)
|
|
649
|
-
o.setAttribute('data-sort', 'none'); }); th.setAttribute('data-sort', next); const tbody = U.qs('tbody', this.el); if (tbody) {
|
|
650
|
-
const trs = Array.from(tbody.querySelectorAll('tr'));
|
|
651
|
-
const ci = Array.from(th.parentNode.children).indexOf(th);
|
|
652
|
-
trs.sort((a, b) => { const at = (a.children[ci]?.textContent || '').trim(); const bt = (b.children[ci]?.textContent || '').trim(); const an = Number(at.replace(/[₹$,\s]/g, '')); const bn = Number(bt.replace(/[₹$,\s]/g, '')); if (!isNaN(an) && !isNaN(bn))
|
|
653
|
-
return next === 'asc' ? an - bn : bn - an; const c = at.localeCompare(bt, undefined, { numeric: true, sensitivity: 'base' }); return next === 'asc' ? c : -c; });
|
|
654
|
-
trs.forEach(tr => tbody.appendChild(tr));
|
|
655
|
-
} U.dispatch(this.el, 'ux4g.table.sort', { column: th, direction: next }); })); }); }
|
|
656
|
-
_bindSelection() {
|
|
657
|
-
const sa = U.qs('thead .ux4g-checkbox', this.el);
|
|
658
|
-
if (!sa)
|
|
659
|
-
return;
|
|
660
|
-
const rcs = U.qsa('tbody .ux4g-checkbox', this.el);
|
|
661
|
-
if (!rcs.length)
|
|
662
|
-
return;
|
|
663
|
-
const upd = () => { let c = 0; rcs.forEach(cb => { const tr = U.closest(cb, 'tr'); if (cb.checked) {
|
|
664
|
-
c++;
|
|
665
|
-
if (tr)
|
|
666
|
-
tr.classList.add('ux4g-is-selected');
|
|
667
|
-
}
|
|
668
|
-
else {
|
|
669
|
-
if (tr)
|
|
670
|
-
tr.classList.remove('ux4g-is-selected');
|
|
671
|
-
} }); sa.checked = c === rcs.length; sa.indeterminate = c > 0 && c < rcs.length; };
|
|
672
|
-
U.on(sa, 'change', (() => { rcs.forEach(cb => { cb.checked = sa.checked; }); upd(); }));
|
|
673
|
-
rcs.forEach(cb => U.on(cb, 'change', upd));
|
|
674
|
-
upd();
|
|
675
|
-
}
|
|
676
|
-
static getOrCreate(el) { let i = getI(el, 'table'); if (!i) {
|
|
677
|
-
i = new TableComponent(el);
|
|
678
|
-
setI(el, 'table', i);
|
|
679
|
-
} return i; }
|
|
680
|
-
}
|
|
681
|
-
class ListComponent {
|
|
682
|
-
constructor(el) {
|
|
683
|
-
this.el = el;
|
|
684
|
-
U.on(this.el, 'click', ((e) => {
|
|
685
|
-
const t = e.target;
|
|
686
|
-
const item = U.closest(t, '.ux4g-list-item-row') || U.closest(t, '.ux4g-list-select-item');
|
|
687
|
-
if (!item)
|
|
688
|
-
return;
|
|
689
|
-
const isMulti = this.el.classList.contains('ux4g-multiselect') || this.el.classList.contains('ux4g-list-multiselect');
|
|
690
|
-
if (t.tagName === 'INPUT')
|
|
691
|
-
return;
|
|
692
|
-
const cb = U.qs('input[type="checkbox"]', item);
|
|
693
|
-
if (isMulti) {
|
|
694
|
-
const active = item.classList.toggle('active');
|
|
695
|
-
if (cb) {
|
|
696
|
-
cb.checked = active;
|
|
697
|
-
cb.dispatchEvent(new Event('change', { bubbles: true }));
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
else {
|
|
701
|
-
const was = item.classList.contains('active');
|
|
702
|
-
U.qsa('.ux4g-list-item-row,.ux4g-list-select-item', this.el).forEach(i => { i.classList.remove('active'); const inp = U.qs('input', i); if (inp)
|
|
703
|
-
inp.checked = false; });
|
|
704
|
-
if (!was) {
|
|
705
|
-
item.classList.add('active');
|
|
706
|
-
if (cb) {
|
|
707
|
-
cb.checked = true;
|
|
708
|
-
cb.dispatchEvent(new Event('change', { bubbles: true }));
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
U.dispatch(this.el, 'ux4g.list.change', { item, active: item.classList.contains('active') });
|
|
713
|
-
}));
|
|
714
|
-
}
|
|
715
|
-
static getOrCreate(el) { let i = getI(el, 'list'); if (!i) {
|
|
716
|
-
i = new ListComponent(el);
|
|
717
|
-
setI(el, 'list', i);
|
|
718
|
-
} return i; }
|
|
719
|
-
}
|
|
720
|
-
// ─── Init + Custom Behaviors + Public API ────────────────────────────────────
|
|
721
|
-
function init(root = document) {
|
|
722
|
-
U.qsa('[data-bs-toggle="dropdown"],[data-ux-toggle="dropdown"]', root).forEach(el => DropdownComponent.getOrCreate(el));
|
|
723
|
-
U.qsa('[data-bs-toggle="collapse"],[data-ux-toggle="collapse"],[ux4g-toggle="collapse"]', root).forEach(el => CollapseComponent.getOrCreate(el));
|
|
724
|
-
U.qsa('[data-bs-toggle="tab"],[data-ux-toggle="tab"]', root).forEach(el => TabComponent.getOrCreate(el));
|
|
725
|
-
U.qsa('[data-bs-toggle="tooltip"],[data-ux-toggle="tooltip"]', root).forEach(el => FloatingComponent.getOrCreate(el, 'tooltip'));
|
|
726
|
-
U.qsa('[data-bs-toggle="popover"],[data-ux-toggle="popover"]', root).forEach(el => FloatingComponent.getOrCreate(el, 'popover'));
|
|
727
|
-
U.qsa('.toast', root).forEach(el => ToastComponent.getOrCreate(el));
|
|
728
|
-
U.qsa('.carousel', root).forEach(el => CarouselComponent.getOrCreate(el));
|
|
729
|
-
U.qsa('[data-bs-spy="scroll"],[data-ux-spy="scroll"]', root).forEach(el => ScrollSpyComponent.getOrCreate(el));
|
|
730
|
-
U.qsa('.ux4g-table', root).forEach(el => TableComponent.getOrCreate(el));
|
|
731
|
-
U.qsa('.ux4g-list', root).forEach(el => ListComponent.getOrCreate(el));
|
|
732
|
-
}
|
|
733
|
-
function initDelegatedToggles() {
|
|
734
|
-
U.on(document, 'click', ((e) => {
|
|
735
|
-
const t = e.target;
|
|
736
|
-
const mb = U.closest(t, '[data-bs-toggle="modal"],[data-ux-toggle="modal"]');
|
|
737
|
-
if (mb) {
|
|
738
|
-
e.preventDefault();
|
|
739
|
-
const s = U.data(mb, 'target') || U.attr(mb, 'href');
|
|
740
|
-
const el = s && s.startsWith('#') ? U.qs(s) : null;
|
|
741
|
-
if (el)
|
|
742
|
-
ModalComponent.getOrCreate(el).toggle(mb);
|
|
743
|
-
return;
|
|
744
|
-
}
|
|
745
|
-
const om = U.closest(t, '.open-modal');
|
|
746
|
-
if (om) {
|
|
747
|
-
e.preventDefault();
|
|
748
|
-
const el = U.qs('#exampleModal');
|
|
749
|
-
if (el)
|
|
750
|
-
ModalComponent.getOrCreate(el).show(om);
|
|
751
|
-
return;
|
|
752
|
-
}
|
|
753
|
-
const ob = U.closest(t, '[data-bs-toggle="offcanvas"],[data-ux-toggle="offcanvas"]');
|
|
754
|
-
if (ob) {
|
|
755
|
-
e.preventDefault();
|
|
756
|
-
const s = U.data(ob, 'target') || U.attr(ob, 'href');
|
|
757
|
-
const el = s && s.startsWith('#') ? U.qs(s) : null;
|
|
758
|
-
if (el)
|
|
759
|
-
OffcanvasComponent.getOrCreate(el).toggle(ob);
|
|
760
|
-
return;
|
|
761
|
-
}
|
|
762
|
-
const ct = U.closest(t, '.close-toast');
|
|
763
|
-
if (ct) {
|
|
764
|
-
e.preventDefault();
|
|
765
|
-
const el = U.closest(ct, '.toast');
|
|
766
|
-
if (el)
|
|
767
|
-
ToastComponent.getOrCreate(el).hide();
|
|
768
|
-
return;
|
|
769
|
-
}
|
|
770
|
-
const cm = U.closest(t, '.close-modal');
|
|
771
|
-
if (cm) {
|
|
772
|
-
e.preventDefault();
|
|
773
|
-
const el = U.closest(cm, '.modal');
|
|
774
|
-
if (el)
|
|
775
|
-
ModalComponent.getOrCreate(el).hide();
|
|
776
|
-
return;
|
|
777
|
-
}
|
|
778
|
-
}));
|
|
779
|
-
}
|
|
780
|
-
function initCustomBehaviors() {
|
|
781
|
-
// Manual tooltip repositioning
|
|
782
|
-
U.on(document.body, 'mouseover', ((e) => { const w = U.closest(e.target, '.ux4g-tooltip-wrapper'); if (w) {
|
|
783
|
-
const tt = U.qs('.ux4g-tooltip', w);
|
|
784
|
-
if (tt && tt.dataset.uxAdjusted !== 'true') {
|
|
785
|
-
const vw = window.innerWidth;
|
|
786
|
-
const vh = window.innerHeight;
|
|
787
|
-
tt.style.transition = 'none';
|
|
788
|
-
tt.style.display = 'flex';
|
|
789
|
-
tt.style.opacity = '0';
|
|
790
|
-
tt.offsetHeight;
|
|
791
|
-
const r = tt.getBoundingClientRect();
|
|
792
|
-
let sx = 0, sy = 0;
|
|
793
|
-
if (r.left < 18)
|
|
794
|
-
sx = 18 - r.left;
|
|
795
|
-
else if (r.right > vw - 18)
|
|
796
|
-
sx = (vw - 18) - r.right;
|
|
797
|
-
if (r.top < 18)
|
|
798
|
-
sy = 18 - r.top;
|
|
799
|
-
else if (r.bottom > vh - 18)
|
|
800
|
-
sy = (vh - 18) - r.bottom;
|
|
801
|
-
if (sx || sy) {
|
|
802
|
-
tt.style.transform = `translate(${sx}px, ${sy}px)`;
|
|
803
|
-
tt.dataset.uxAdjusted = 'true';
|
|
804
|
-
}
|
|
805
|
-
tt.style.display = '';
|
|
806
|
-
tt.style.opacity = '';
|
|
807
|
-
setTimeout(() => { tt.style.transition = ''; }, 50);
|
|
808
|
-
}
|
|
809
|
-
} }));
|
|
810
|
-
U.on(document.body, 'mouseout', ((e) => { const w = U.closest(e.target, '.ux4g-tooltip-wrapper'); if (w && !w.contains(e.relatedTarget)) {
|
|
811
|
-
const tt = U.qs('.ux4g-tooltip', w);
|
|
812
|
-
if (tt) {
|
|
813
|
-
tt.style.transform = '';
|
|
814
|
-
delete tt.dataset.uxAdjusted;
|
|
815
|
-
}
|
|
816
|
-
} }));
|
|
817
|
-
// Breadcrumb dropdowns
|
|
818
|
-
const bds = U.qsa('.ux4g-breadcrumb-dropdown');
|
|
819
|
-
if (bds.length) {
|
|
820
|
-
const closeBd = (d) => { const tg = U.qs('.ux4g-breadcrumb-toggle', d); const mn = U.qs('.ux4g-breadcrumb-menu', d); if (tg && mn) {
|
|
821
|
-
tg.classList.remove('show');
|
|
822
|
-
mn.classList.remove('show');
|
|
823
|
-
tg.setAttribute('aria-expanded', 'false');
|
|
824
|
-
} };
|
|
825
|
-
bds.forEach(d => {
|
|
826
|
-
const tg = U.qs('.ux4g-breadcrumb-toggle', d);
|
|
827
|
-
const mn = U.qs('.ux4g-breadcrumb-menu', d);
|
|
828
|
-
if (!tg || !mn)
|
|
829
|
-
return;
|
|
830
|
-
U.on(tg, 'click', ((e) => { e.preventDefault(); e.stopPropagation(); const isOpen = mn.classList.contains('show'); bds.forEach(x => closeBd(x)); if (!isOpen) {
|
|
831
|
-
tg.classList.add('show');
|
|
832
|
-
mn.classList.add('show');
|
|
833
|
-
tg.setAttribute('aria-expanded', 'true');
|
|
834
|
-
U.repositionMenu(d, mn);
|
|
835
|
-
} }));
|
|
836
|
-
U.on(mn, 'click', (() => closeBd(d)));
|
|
837
|
-
});
|
|
838
|
-
U.on(document, 'click', (() => bds.forEach(x => closeBd(x))));
|
|
839
|
-
U.on(document, 'keydown', ((e) => { if (e.key === 'Escape')
|
|
840
|
-
bds.forEach(x => closeBd(x)); }));
|
|
841
|
-
}
|
|
842
|
-
// Drawer
|
|
843
|
-
const closeDrawer = () => { const od = document.querySelector('.ux4g-drawer.ux4g-drawer-open'); const oo = document.querySelector('.ux4g-drawer-overlay.ux4g-drawer-open'); if (!od || !oo)
|
|
844
|
-
return; od.classList.remove('ux4g-drawer-open'); oo.classList.remove('ux4g-drawer-open'); document.body.classList.remove('ux4g-drawer-lock'); };
|
|
845
|
-
U.qsa('[data-drawer]').forEach(btn => { U.on(btn, 'click', (() => { const dr = document.getElementById(btn.getAttribute('data-drawer') || ''); if (!dr)
|
|
846
|
-
return; const ov = dr.closest('.ux4g-drawer-overlay'); document.querySelectorAll('.ux4g-drawer-open').forEach(el => el.classList.remove('ux4g-drawer-open')); if (ov)
|
|
847
|
-
ov.classList.add('ux4g-drawer-open'); dr.classList.add('ux4g-drawer-open'); document.body.classList.add('ux4g-drawer-lock'); })); });
|
|
848
|
-
U.on(document, 'click', ((e) => { if (U.closest(e.target, '[data-drawer-close]')) {
|
|
849
|
-
closeDrawer();
|
|
850
|
-
return;
|
|
851
|
-
} const ov = U.closest(e.target, '.ux4g-drawer-overlay'); if (ov && !U.closest(e.target, '.ux4g-drawer'))
|
|
852
|
-
closeDrawer(); }));
|
|
853
|
-
U.on(document, 'keydown', ((e) => { if (e.key === 'Escape')
|
|
854
|
-
closeDrawer(); }));
|
|
855
|
-
// Modal helpers (data-modal-target)
|
|
856
|
-
U.qsa('[data-modal-target]').forEach(btn => { U.on(btn, 'click', (() => { const s = btn.getAttribute('data-modal-target'); const m = s ? document.querySelector(s) : null; if (m) {
|
|
857
|
-
m.classList.add('is-open');
|
|
858
|
-
document.body.style.overflow = 'hidden';
|
|
859
|
-
} })); });
|
|
860
|
-
U.qsa('[data-close-modal]').forEach(btn => { U.on(btn, 'click', ((e) => { const m = U.closest(e.target, '.ux4g-modal-backdrop'); if (m) {
|
|
861
|
-
m.classList.remove('is-open');
|
|
862
|
-
document.body.style.overflow = '';
|
|
863
|
-
} })); });
|
|
864
|
-
// Search/filter
|
|
865
|
-
U.qsa('.ux4g-search-container').forEach(sw => {
|
|
866
|
-
const inp = U.qs('.ux4g-search-input', sw);
|
|
867
|
-
const cb = U.qs('.ux4g-search-clear', sw);
|
|
868
|
-
if (!inp)
|
|
869
|
-
return;
|
|
870
|
-
const toggle = () => { sw.classList.toggle('ux4g-has-value', inp.value.trim() !== ''); };
|
|
871
|
-
U.on(inp, 'input', toggle);
|
|
872
|
-
if (cb)
|
|
873
|
-
U.on(cb, 'click', (() => { inp.value = ''; toggle(); inp.focus(); }));
|
|
874
|
-
toggle();
|
|
875
|
-
});
|
|
876
|
-
// Textarea counter
|
|
877
|
-
U.on(document, 'input', ((e) => { const t = e.target; if (t.matches && t.matches('.ux4g-textarea-input')) {
|
|
878
|
-
const w = t.closest('.ux4g-textarea');
|
|
879
|
-
if (!w)
|
|
880
|
-
return;
|
|
881
|
-
const c = w.querySelector('.ux4g-textarea-counter');
|
|
882
|
-
if (c)
|
|
883
|
-
c.textContent = `${t.value.length} / ${t.getAttribute('maxlength') || '0'}`;
|
|
884
|
-
} }));
|
|
885
|
-
// Switch keyboard
|
|
886
|
-
U.on(document, 'keydown', ((e) => { if (e.key === 'Enter') {
|
|
887
|
-
const inp = U.closest(e.target, '.ux4g-switch-input');
|
|
888
|
-
if (inp && !inp.disabled) {
|
|
889
|
-
e.preventDefault();
|
|
890
|
-
inp.checked = !inp.checked;
|
|
891
|
-
inp.dispatchEvent(new Event('change', { bubbles: true }));
|
|
892
|
-
}
|
|
893
|
-
} }));
|
|
894
|
-
// Custom dropdown (ux4g-dropdown)
|
|
895
|
-
const dds = Array.from(document.querySelectorAll('.ux4g-dropdown'));
|
|
896
|
-
if (dds.length) {
|
|
897
|
-
const closeDd = (d) => { d.classList.remove('is-open'); const c = U.qs('.ux4g-dropdown-control', d); if (c)
|
|
898
|
-
c.setAttribute('aria-expanded', 'false'); };
|
|
899
|
-
const openDd = (d) => { dds.forEach(x => { if (x !== d)
|
|
900
|
-
closeDd(x); }); d.classList.add('is-open'); const c = U.qs('.ux4g-dropdown-control', d); if (c)
|
|
901
|
-
c.setAttribute('aria-expanded', 'true'); const m = U.qs('.ux4g-dropdown-menu', d); if (m)
|
|
902
|
-
U.repositionMenu(d, m); };
|
|
903
|
-
dds.forEach(dd => {
|
|
904
|
-
const ctrl = U.qs('.ux4g-dropdown-control', dd);
|
|
905
|
-
const menu = U.qs('.ux4g-dropdown-menu', dd);
|
|
906
|
-
if (!ctrl || !menu)
|
|
907
|
-
return;
|
|
908
|
-
U.on(ctrl, 'click', ((ev) => { if (U.closest(ev.target, '[ux4g-dropdown-search]')) {
|
|
909
|
-
if (!dd.classList.contains('is-open'))
|
|
910
|
-
openDd(dd);
|
|
911
|
-
return;
|
|
912
|
-
} ev.preventDefault(); ev.stopPropagation(); dd.classList.contains('is-open') ? closeDd(dd) : openDd(dd); }));
|
|
913
|
-
U.on(menu, 'click', ((ev) => { const ch = U.closest(ev.target, '[ux4g-dropdown-choice]'); if (ch) {
|
|
914
|
-
ev.stopPropagation();
|
|
915
|
-
closeDd(dd);
|
|
916
|
-
} }));
|
|
917
|
-
U.on(menu, 'change', ((ev) => { const inp = U.closest(ev.target, '.ux4g-dropdown-option-input'); if (!inp)
|
|
918
|
-
return; if (!dd.classList.contains('ux4g-dropdown-multi'))
|
|
919
|
-
closeDd(dd); }));
|
|
920
|
-
});
|
|
921
|
-
U.on(document, 'click', ((ev) => { dds.forEach(d => { if (!d.contains(ev.target))
|
|
922
|
-
closeDd(d); }); }));
|
|
923
|
-
U.on(document, 'keydown', ((ev) => { if (ev.key === 'Escape')
|
|
924
|
-
dds.forEach(d => closeDd(d)); }));
|
|
925
|
-
}
|
|
926
|
-
// Combobox
|
|
927
|
-
const cbs = Array.from(document.querySelectorAll('.ux4g-combobox'));
|
|
928
|
-
if (cbs.length) {
|
|
929
|
-
const closeCb = (c) => { c.classList.remove('is-open'); const ctrl = U.qs('.ux4g-combobox-control', c); if (ctrl)
|
|
930
|
-
ctrl.setAttribute('aria-expanded', 'false'); };
|
|
931
|
-
const openCb = (c) => { cbs.forEach(x => { if (x !== c)
|
|
932
|
-
closeCb(x); }); c.classList.add('is-open'); const ctrl = U.qs('.ux4g-combobox-control', c); if (ctrl)
|
|
933
|
-
ctrl.setAttribute('aria-expanded', 'true'); const m = U.qs('.ux4g-combobox-menu', c); if (m)
|
|
934
|
-
U.repositionMenu(c, m); };
|
|
935
|
-
cbs.forEach(cb => {
|
|
936
|
-
const ctrl = U.qs('.ux4g-combobox-control', cb);
|
|
937
|
-
const menu = U.qs('.ux4g-combobox-menu', cb);
|
|
938
|
-
if (!ctrl || !menu)
|
|
939
|
-
return;
|
|
940
|
-
const inp = cb.querySelector('[ux4g-combobox-search]');
|
|
941
|
-
if (inp) {
|
|
942
|
-
U.on(inp, 'focus', (() => openCb(cb)));
|
|
943
|
-
U.on(inp, 'input', (() => openCb(cb)));
|
|
944
|
-
}
|
|
945
|
-
const caret = cb.querySelector('.ux4g-combobox-caret');
|
|
946
|
-
if (caret)
|
|
947
|
-
U.on(caret, 'click', ((e) => { e.stopPropagation(); cb.classList.contains('is-open') ? closeCb(cb) : openCb(cb); }));
|
|
948
|
-
U.on(ctrl, 'click', ((ev) => { if (U.closest(ev.target, '[ux4g-combobox-search]')) {
|
|
949
|
-
if (!cb.classList.contains('is-open'))
|
|
950
|
-
openCb(cb);
|
|
951
|
-
return;
|
|
952
|
-
} ev.preventDefault(); ev.stopPropagation(); cb.classList.contains('is-open') ? closeCb(cb) : openCb(cb); }));
|
|
953
|
-
U.on(menu, 'click', ((ev) => { const ch = U.closest(ev.target, '[ux4g-combobox-choice]'); if (ch) {
|
|
954
|
-
ev.stopPropagation();
|
|
955
|
-
closeCb(cb);
|
|
956
|
-
} }));
|
|
957
|
-
});
|
|
958
|
-
U.on(document, 'click', ((ev) => { cbs.forEach(c => { if (!c.contains(ev.target))
|
|
959
|
-
closeCb(c); }); }));
|
|
960
|
-
U.on(document, 'keydown', ((ev) => { if (ev.key === 'Escape')
|
|
961
|
-
cbs.forEach(c => closeCb(c)); }));
|
|
962
|
-
}
|
|
963
|
-
// UX4G Tabs
|
|
964
|
-
U.qsa('[data-ux4g-tab]').forEach(root => {
|
|
965
|
-
const list = U.qs('.ux4g-tab-list', root);
|
|
966
|
-
if (!list)
|
|
967
|
-
return;
|
|
968
|
-
const items = U.qsa('.ux4g-tab-item:not(.ux4g-tab-more)', list);
|
|
969
|
-
const panels = U.qsa('.ux4g-tab-panel', root);
|
|
970
|
-
const resetActive = () => { U.qsa('.ux4g-tab-item', list).forEach(i => i.classList.remove('is-active')); root.querySelectorAll('.ux4g-tab-dropdown-item').forEach(i => i.classList.remove('is-active')); };
|
|
971
|
-
const showPanel = (id) => { panels.forEach(p => p.classList.remove('is-active')); const t = root.querySelector('#' + id); if (t)
|
|
972
|
-
t.classList.add('is-active'); };
|
|
973
|
-
items.forEach(item => { U.on(item, 'click', (() => { if (item.classList.contains('ux4g-tab-item-disabled'))
|
|
974
|
-
return; resetActive(); item.classList.add('is-active'); const pid = item.dataset.panel; if (pid)
|
|
975
|
-
showPanel(pid); })); });
|
|
976
|
-
});
|
|
977
|
-
// Sliders
|
|
978
|
-
U.on(document, 'input', ((e) => {
|
|
979
|
-
const t = e.target;
|
|
980
|
-
if (!t.classList.contains('ux4g-slider-input'))
|
|
981
|
-
return;
|
|
982
|
-
const slider = t.closest('.ux4g-slider');
|
|
983
|
-
if (!slider)
|
|
984
|
-
return;
|
|
985
|
-
const fill = slider.querySelector('.ux4g-slider-fill');
|
|
986
|
-
const inp = t;
|
|
987
|
-
if (!slider.classList.contains('ux4g-slider-dual')) {
|
|
988
|
-
const thumb = slider.querySelector('.ux4g-slider-thumb');
|
|
989
|
-
const pct = ((parseFloat(inp.value) - parseFloat(inp.min)) / (parseFloat(inp.max) - parseFloat(inp.min))) * 100;
|
|
990
|
-
if (fill)
|
|
991
|
-
fill.style.width = pct + '%';
|
|
992
|
-
if (thumb)
|
|
993
|
-
thumb.style.left = pct + '%';
|
|
994
|
-
}
|
|
995
|
-
}));
|
|
996
|
-
// Custom carousel (.ux4g-carousel)
|
|
997
|
-
document.querySelectorAll('.ux4g-carousel').forEach(car => {
|
|
998
|
-
const sc = car.querySelector('.ux4g-carousel-slides');
|
|
999
|
-
const slides = car.querySelectorAll('.ux4g-carousel-slide');
|
|
1000
|
-
const dots = car.querySelectorAll('.ux4g-carousel-dot');
|
|
1001
|
-
if (!slides.length)
|
|
1002
|
-
return;
|
|
1003
|
-
let ci = 0;
|
|
1004
|
-
const upd = (i) => { if (i < 0)
|
|
1005
|
-
i = slides.length - 1; if (i >= slides.length)
|
|
1006
|
-
i = 0; ci = i; if (sc)
|
|
1007
|
-
sc.style.transform = `translateX(-${ci * 100}%)`; slides.forEach((s, idx) => { s.classList.toggle('is-active', idx === ci); }); dots.forEach((d, idx) => d.classList.toggle('is-active', idx === ci)); };
|
|
1008
|
-
const pb = car.querySelector('.ux4g-carousel-arrow-prev');
|
|
1009
|
-
const nb = car.querySelector('.ux4g-carousel-arrow-next');
|
|
1010
|
-
if (pb)
|
|
1011
|
-
U.on(pb, 'click', ((e) => { e.preventDefault(); upd(ci - 1); }));
|
|
1012
|
-
if (nb)
|
|
1013
|
-
U.on(nb, 'click', ((e) => { e.preventDefault(); upd(ci + 1); }));
|
|
1014
|
-
dots.forEach((d, idx) => U.on(d, 'click', ((e) => { e.preventDefault(); upd(idx); })));
|
|
1015
|
-
upd(ci);
|
|
1016
|
-
});
|
|
1017
|
-
// Context alerts
|
|
1018
|
-
U.qsa('[data-ux4g-toggle="toast"]').forEach(btn => {
|
|
1019
|
-
U.on(btn, 'click', (() => {
|
|
1020
|
-
const pos = btn.dataset.ux4gPosition || 'top-right';
|
|
1021
|
-
const variant = btn.dataset.ux4gVariant || 'info';
|
|
1022
|
-
const title = btn.dataset.ux4gTitle || variant;
|
|
1023
|
-
const body = btn.dataset.ux4gBody || '';
|
|
1024
|
-
const cid = `ux4g-alert-container-${pos}`;
|
|
1025
|
-
let cont = document.getElementById(cid);
|
|
1026
|
-
if (!cont) {
|
|
1027
|
-
cont = document.createElement('div');
|
|
1028
|
-
cont.id = cid;
|
|
1029
|
-
cont.className = `ux4g-alert-container ux4g-alert-${pos}`;
|
|
1030
|
-
document.body.appendChild(cont);
|
|
1031
|
-
}
|
|
1032
|
-
const al = document.createElement('div');
|
|
1033
|
-
al.className = `ux4g-context-alert ux4g-alert-${variant} ${pos.includes('left') ? 'ux4g-animate-left' : 'ux4g-animate-right'}`;
|
|
1034
|
-
al.innerHTML = `<span class="ux4g-alert-title">${title}</span><button class="ux4g-alert-close"><i class="ux4g-icon">close</i></button><div class="ux4g-alert-message">${body}</div>`;
|
|
1035
|
-
const cb = al.querySelector('.ux4g-alert-close');
|
|
1036
|
-
if (cb)
|
|
1037
|
-
cb.addEventListener('click', () => { al.style.opacity = '0'; setTimeout(() => al.remove(), 400); });
|
|
1038
|
-
cont.appendChild(al);
|
|
1039
|
-
setTimeout(() => { if (al.parentNode) {
|
|
1040
|
-
al.style.opacity = '0';
|
|
1041
|
-
setTimeout(() => al.remove(), 400);
|
|
1042
|
-
} }, 5000);
|
|
1043
|
-
}));
|
|
1044
|
-
});
|
|
1045
|
-
// Feedback (NPS, Emoji, Stars)
|
|
1046
|
-
U.qsa('.feedback-nps-button').forEach(btn => { U.on(btn, 'click', (() => { const c = btn.closest('.ux4g-feedback-nps-wrapper') || btn.parentElement; const sibs = Array.from(c.querySelectorAll('.feedback-nps-button')); const ci = sibs.indexOf(btn); sibs.forEach((s, i) => s.classList.toggle('active', i <= ci)); })); });
|
|
1047
|
-
U.qsa('.feedback-emoji-button').forEach(btn => { U.on(btn, 'click', (() => { const c = btn.closest('.ux4g-d-flex') || document; c.querySelectorAll('.feedback-emoji-button').forEach(b => b.classList.remove('active')); btn.classList.add('active'); })); });
|
|
1048
|
-
U.qsa('.ux4g-feedback-star').forEach(star => { U.on(star, 'click', (() => { const c = star.parentElement; const sibs = Array.from(c.querySelectorAll('.ux4g-feedback-star')); const ci = sibs.indexOf(star); sibs.forEach((s, i) => s.classList.toggle('active', i <= ci)); })); });
|
|
1049
|
-
// Mega menu
|
|
1050
|
-
const catItems = U.qsa('.ux4g-mega-menu__category-item');
|
|
1051
|
-
const contentBlocks = U.qsa('.ux4g-mega-menu__content');
|
|
1052
|
-
if (catItems.length && contentBlocks.length) {
|
|
1053
|
-
catItems.forEach((item, idx) => { U.on(item, 'click', ((e) => { e.preventDefault(); catItems.forEach(c => c.classList.remove('ux4g-mega-menu__category-item--active')); item.classList.add('ux4g-mega-menu__category-item--active'); contentBlocks.forEach(b => b.classList.remove('ux4g-mega-menu__content--active')); const tb = document.getElementById(`category-${idx + 1}`); if (tb)
|
|
1054
|
-
tb.classList.add('ux4g-mega-menu__content--active'); })); });
|
|
1055
|
-
}
|
|
1056
|
-
// Result list accordion
|
|
1057
|
-
U.qsa('.ux4g-result-list-accordion-toggle').forEach(btn => { U.on(btn, 'click', (() => { const exp = btn.getAttribute('aria-expanded') === 'true'; btn.setAttribute('aria-expanded', String(!exp)); btn.innerText = exp ? 'expand_more' : 'expand_less'; })); });
|
|
1058
|
-
// Resize handler
|
|
1059
|
-
U.on(window, 'resize', (() => { document.querySelectorAll('.ux4g-dropdown.is-open,.ux4g-combobox.is-open').forEach(el => { const m = el.querySelector('.ux4g-dropdown-menu,.ux4g-combobox-menu'); if (m)
|
|
1060
|
-
U.repositionMenu(el, m); }); }));
|
|
1061
|
-
}
|
|
1062
|
-
function initMutationObserver() {
|
|
1063
|
-
if (typeof MutationObserver === 'undefined')
|
|
1064
|
-
return;
|
|
1065
|
-
const obs = new MutationObserver((mutations) => { let shouldInit = false; for (const m of mutations) {
|
|
1066
|
-
if (m.addedNodes.length) {
|
|
1067
|
-
shouldInit = true;
|
|
1068
|
-
break;
|
|
1069
|
-
}
|
|
1070
|
-
} if (shouldInit) {
|
|
1071
|
-
if (window.__ux4gInitRaf)
|
|
1072
|
-
cancelAnimationFrame(window.__ux4gInitRaf);
|
|
1073
|
-
window.__ux4gInitRaf = requestAnimationFrame(() => init(document));
|
|
1074
|
-
} });
|
|
1075
|
-
const start = () => { if (document.body)
|
|
1076
|
-
obs.observe(document.body, { childList: true, subtree: true });
|
|
1077
|
-
else
|
|
1078
|
-
setTimeout(start, 50); };
|
|
1079
|
-
start();
|
|
1080
|
-
activeObservers.push(obs);
|
|
1081
|
-
}
|
|
17
|
+
/**
|
|
18
|
+
* Initialize the UX4G runtime by injecting the vendor JS scripts into the page.
|
|
19
|
+
*
|
|
20
|
+
* - Safe for SSR — no-ops when window/document are unavailable.
|
|
21
|
+
* - Singleton guard prevents double-injection.
|
|
22
|
+
* - The vendor JS handles its own DOMContentLoaded timing internally.
|
|
23
|
+
* - MutationObserver in the vendor JS auto-initializes dynamically added elements.
|
|
24
|
+
*/
|
|
1082
25
|
function initRuntime() {
|
|
1083
26
|
if (!isBrowser)
|
|
1084
27
|
return;
|
|
1085
28
|
if (window.__UX4G_RUNTIME_INITIALIZED__)
|
|
1086
29
|
return;
|
|
1087
30
|
window.__UX4G_RUNTIME_INITIALIZED__ = true;
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
initMutationObserver();
|
|
31
|
+
// Inject ux4g.js (sets up window.ux4g with all core components)
|
|
32
|
+
const script1 = document.createElement('script');
|
|
33
|
+
script1.setAttribute('data-ux4g-runtime', 'main');
|
|
34
|
+
script1.textContent = "/*!\r\n * UX4G.js\r\n * Components: Dropdown, Collapse/Accordion, Modal, Offcanvas, Tooltip, Popover, Toast, Carousel, Tabs, Scrollspy\r\n * Compatibility: UX4G markup (data-ux-*)\r\n * Extras: supports .open-modal/.close-modal, .close-toast, #liveToastBtn (your sample HTML)\r\n * Version: 1.1.0\r\n */\r\n\r\n(function (global) {\r\n \"use strict\";\r\n\r\n // -----------------------------\r\n // Utilities\r\n // -----------------------------\r\n const U = {\r\n qs(sel, root = document) { return root.querySelector(sel); },\r\n qsa(sel, root = document) { return Array.from(root.querySelectorAll(sel)); },\r\n on(el, evt, cb, opts) { el && el.addEventListener(evt, cb, opts); },\r\n off(el, evt, cb, opts) { el && el.removeEventListener(evt, cb, opts); },\r\n closest(el, sel) { return el ? el.closest(sel) : null; },\r\n isVisible(el) { return !!(el && (el.offsetWidth || el.offsetHeight || el.getClientRects().length)); },\r\n reflow(el) { return el && el.offsetHeight; },\r\n attr(el, name, fallback = null) {\r\n if (!el) return fallback;\r\n const v = el.getAttribute(name);\r\n return v == null ? fallback : v;\r\n },\r\n // Prefer UX4G attributes\r\n data(el, key, fallback = null) {\r\n if (!el) return fallback;\r\n const ux = el.getAttribute(`data-ux-${key}`);\r\n if (ux != null) return ux;\r\n const bs = el.getAttribute(`data-bs-${key}`);\r\n if (bs != null) return bs;\r\n const ux4g = el.getAttribute(`ux4g-${key}`);\r\n if (ux4g != null) return ux4g;\r\n return fallback;\r\n },\r\n bool(v, fallback = false) {\r\n if (v == null) return fallback;\r\n if (typeof v === \"boolean\") return v;\r\n const s = String(v).trim().toLowerCase();\r\n if (s === \"\" || s === \"true\" || s === \"1\") return true;\r\n if (s === \"false\" || s === \"0\") return false;\r\n return fallback;\r\n },\r\n num(v, fallback = 0) {\r\n const n = Number(v);\r\n return Number.isFinite(n) ? n : fallback;\r\n },\r\n dispatch(el, name, detail) {\r\n if (!el) return;\r\n el.dispatchEvent(new CustomEvent(name, { bubbles: true, cancelable: true, detail }));\r\n },\r\n focusables(root) {\r\n if (!root) return [];\r\n const sel = [\r\n \"a[href]\",\r\n \"area[href]\",\r\n \"button:not([disabled])\",\r\n \"input:not([disabled]):not([type='hidden'])\",\r\n \"select:not([disabled])\",\r\n \"textarea:not([disabled])\",\r\n \"[tabindex]:not([tabindex='-1'])\",\r\n \"[contenteditable='true']\"\r\n ].join(\",\");\r\n return U.qsa(sel, root).filter(U.isVisible);\r\n },\r\n // Body scroll lock (simple)\r\n lockBody(lock) {\r\n const cls = \"ux4g-scroll-lock\";\r\n if (lock) document.body.classList.add(cls);\r\n else document.body.classList.remove(cls);\r\n },\r\n ensureBackdrop(kind) {\r\n let bd = U.qs(`.ux4g-backdrop[data-kind=\"${kind}\"]`);\r\n if (!bd) {\r\n bd = document.createElement(\"div\");\r\n bd.className = \"ux4g-backdrop\";\r\n bd.setAttribute(\"data-kind\", kind);\r\n document.body.appendChild(bd);\r\n }\r\n return bd;\r\n },\r\n removeBackdrop(kind) {\r\n const bd = U.qs(`.ux4g-backdrop[data-kind=\"${kind}\"]`);\r\n if (bd) bd.remove();\r\n },\r\n // Lightweight positioning (NOT Popper-perfect)\r\n placeFloating(target, floating, placement = \"bottom\", offset = 8) {\r\n if (!target || !floating) return;\r\n\r\n const rect = target.getBoundingClientRect();\r\n const scrollX = window.pageXOffset || document.documentElement.scrollLeft;\r\n const scrollY = window.pageYOffset || document.documentElement.scrollTop;\r\n \r\n // Temporary show to measure\r\n const originalDisplay = floating.style.display;\r\n const originalVisibility = floating.style.visibility;\r\n floating.style.display = \"block\";\r\n floating.style.visibility = \"hidden\";\r\n \r\n const fr = floating.getBoundingClientRect();\r\n \r\n floating.style.display = originalDisplay;\r\n floating.style.visibility = originalVisibility;\r\n\r\n const vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0);\r\n const vh = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0);\r\n\r\n const clamp = (v, min, max) => Math.min(max, Math.max(min, v));\r\n\r\n const want = placement;\r\n const [wantedSide] = want.split(\"-\");\r\n const tries = [want];\r\n const opposite = { top: \"bottom\", bottom: \"top\", left: \"right\", right: \"left\" };\r\n const sideFallback = opposite[wantedSide];\r\n if (sideFallback) {\r\n tries.push(want.replace(wantedSide, sideFallback));\r\n }\r\n [\"bottom\", \"top\", \"right\", \"left\"].forEach(s => {\r\n if (!tries.includes(s)) tries.push(s);\r\n });\r\n\r\n const compute = (p) => {\r\n let t = 0, l = 0;\r\n const [side, align] = p.split(\"-\");\r\n \r\n if (side === \"top\") {\r\n t = rect.top - fr.height - offset;\r\n if (align === \"start\") l = rect.left;\r\n else if (align === \"end\") l = rect.right - fr.width;\r\n else l = rect.left + (rect.width - fr.width) / 2;\r\n } else if (side === \"bottom\") {\r\n t = rect.bottom + offset;\r\n if (align === \"start\") l = rect.left;\r\n else if (align === \"end\") l = rect.right - fr.width;\r\n else l = rect.left + (rect.width - fr.width) / 2;\r\n } else if (side === \"left\") {\r\n l = rect.left - fr.width - offset;\r\n if (align === \"start\") t = rect.top;\r\n else if (align === \"end\") t = rect.bottom - fr.height;\r\n else t = rect.top + (rect.height - fr.height) / 2;\r\n } else if (side === \"right\") {\r\n l = rect.right + offset;\r\n if (align === \"start\") t = rect.top;\r\n else if (align === \"end\") t = rect.bottom - fr.height;\r\n else t = rect.top + (rect.height - fr.height) / 2;\r\n } else {\r\n t = rect.bottom + offset;\r\n l = rect.left + (rect.width - fr.width) / 2;\r\n p = \"bottom\";\r\n }\r\n return { t, l, p };\r\n };\r\n\r\n let chosen = null;\r\n for (const p of tries) {\r\n const c = compute(p);\r\n const fitsH = c.t >= 0 && (c.t + fr.height) <= vh;\r\n const fitsW = c.l >= 0 && (c.l + fr.width) <= vw;\r\n if (fitsH && fitsW) { chosen = c; break; }\r\n }\r\n if (!chosen) chosen = compute(want);\r\n\r\n const top = clamp(chosen.t, 8, Math.max(8, vh - fr.height - 8));\r\n const left = clamp(chosen.l, 8, Math.max(8, vw - fr.width - 8));\r\n\r\n floating.style.position = \"fixed\";\r\n floating.style.top = `${top}px`;\r\n floating.style.left = `${left}px`;\r\n floating.setAttribute(\"data-placement\", chosen.p);\r\n \r\n const isTooltip = floating.classList.contains('ux4g-tooltip');\r\n const baseClass = isTooltip ? 'ux4g-tooltip' : 'ux4g-popover';\r\n \r\n const placementPrefix = `${baseClass}-`;\r\n for (const cls of Array.from(floating.classList)) {\r\n if (cls.startsWith(placementPrefix) && cls !== baseClass) {\r\n floating.classList.remove(cls);\r\n }\r\n }\r\n floating.classList.add(`${placementPrefix}${chosen.p}`);\r\n }\r\n };\r\n\r\n const Registry = new WeakMap();\r\n const getI = (el, key) => (Registry.get(el)?.[key]) || null;\r\n const setI = (el, key, inst) => {\r\n let map = Registry.get(el);\r\n if (!map) { map = {}; Registry.set(el, map); }\r\n map[key] = inst;\r\n };\r\n\r\n const escapeHtml = (s) => String(s)\r\n .replaceAll(\"&\", \"&\")\r\n .replaceAll(\"<\", \"<\")\r\n .replaceAll(\">\", \">\")\r\n .replaceAll('\"', \""\")\r\n .replaceAll(\"'\", \"'\");\r\n\r\n // -----------------------------\r\n // Dropdown\r\n // -----------------------------\r\n class Dropdown {\r\n constructor(toggle) {\r\n this.toggle = toggle;\r\n this.menu = this._findMenu(toggle);\r\n this._open = false;\r\n\r\n U.on(this.toggle, \"click\", (e) => {\r\n e.preventDefault();\r\n this.toggleDropdown();\r\n });\r\n\r\n U.on(document, \"click\", (e) => {\r\n if (!this._open) return;\r\n if (this.menu && (this.menu.contains(e.target) || this.toggle.contains(e.target))) return;\r\n this.hide();\r\n });\r\n\r\n U.on(document, \"keydown\", (e) => {\r\n if (!this._open) return;\r\n if (e.key === \"Escape\") {\r\n this.hide();\r\n this.toggle.focus();\r\n }\r\n });\r\n }\r\n\r\n _findMenu(toggle) {\r\n // Standard dropdown structure: .dropdown > [toggle] + .dropdown-menu\r\n const parent = toggle.parentElement;\r\n let menu = parent ? parent.querySelector(\".dropdown-menu\") : null;\r\n if (!menu) {\r\n const target = U.data(toggle, \"target\") || U.attr(toggle, \"aria-controls\");\r\n if (target && target.startsWith(\"#\")) menu = U.qs(target);\r\n }\r\n return menu;\r\n }\r\n\r\n show() {\r\n if (!this.menu) return;\r\n this._open = true;\r\n this.toggle.classList.add(\"show\");\r\n this.menu.classList.add(\"show\");\r\n this.toggle.setAttribute(\"aria-expanded\", \"true\");\r\n\r\n const placement = U.data(this.toggle, \"placement\", \"bottom\");\r\n const offset = U.num(U.data(this.toggle, \"offset\", 6), 6);\r\n U.placeFloating(this.toggle, this.menu, placement, offset);\r\n\r\n U.dispatch(this.toggle, \"ux4g.dropdown.shown\", { menu: this.menu });\r\n }\r\n\r\n hide() {\r\n if (!this.menu) return;\r\n this._open = false;\r\n this.toggle.classList.remove(\"show\");\r\n this.menu.classList.remove(\"show\");\r\n this.toggle.setAttribute(\"aria-expanded\", \"false\");\r\n U.dispatch(this.toggle, \"ux4g.dropdown.hidden\", { menu: this.menu });\r\n }\r\n\r\n toggleDropdown() { this._open ? this.hide() : this.show(); }\r\n\r\n static getOrCreate(el) {\r\n let inst = getI(el, \"dropdown\");\r\n if (!inst) { inst = new Dropdown(el); setI(el, \"dropdown\", inst); }\r\n return inst;\r\n }\r\n }\r\n\r\n // -----------------------------\r\n // Collapse / Accordion\r\n // -----------------------------\r\n class Collapse {\r\n constructor(trigger) {\r\n this.trigger = trigger;\r\n this.target = this._resolveTarget(trigger);\r\n // ux4g accordion uses data-bs-parent on .collapse\r\n this.parentSel = U.data(this.target, \"parent\") || U.data(this.trigger, \"parent\");\r\n this.duration = this._readDuration(this.target, 200);\r\n\r\n U.on(this.trigger, \"click\", (e) => {\r\n e.preventDefault();\r\n this.toggle();\r\n });\r\n }\r\n\r\n _resolveTarget(trigger) {\r\n const sel = U.data(trigger, \"target\") || U.attr(trigger, \"href\") || U.attr(trigger, \"aria-controls\") || U.attr(trigger, \"ux4g-target\");\r\n if (sel && sel.startsWith(\"#\")) return U.qs(sel);\r\n return U.qs(\"#\" + sel);\r\n }\r\n\r\n _readDuration(el, fallbackMs) {\r\n if (!el) return fallbackMs;\r\n const d = getComputedStyle(el).transitionDuration || \"\";\r\n const ms = d.includes(\"ms\") ? parseFloat(d) : (d.includes(\"s\") ? parseFloat(d) * 1000 : NaN);\r\n return Number.isFinite(ms) && ms > 0 ? ms : fallbackMs;\r\n }\r\n\r\n show() {\r\n if (!this.target) return;\r\n\r\n // Accordion: close other open within parent\r\n if (this.parentSel) {\r\n const parent = U.qs(this.parentSel);\r\n if (parent) {\r\n U.qsa(\".collapse.show\", parent).forEach((el) => {\r\n if (el === this.target) return;\r\n el.classList.remove(\"show\");\r\n // update triggers for that element\r\n if (el.id) {\r\n U.qsa(`[data-bs-target=\"#${el.id}\"],[data-ux-target=\"#${el.id}\"],[ux4g-target=\"#${el.id}\"],a[href=\"#${el.id}\"]`)\r\n .forEach(t => {\r\n t.classList.add(\"collapsed\");\r\n t.setAttribute(\"aria-expanded\", \"false\");\r\n });\r\n }\r\n });\r\n }\r\n }\r\n\r\n // Animate height\r\n this.target.classList.add(\"collapsing\");\r\n this.target.classList.remove(\"collapse\");\r\n this.target.style.height = \"0px\";\r\n U.reflow(this.target);\r\n\r\n const h = this.target.scrollHeight;\r\n this.target.style.height = h + \"px\";\r\n\r\n this.trigger.classList.remove(\"collapsed\");\r\n this.trigger.setAttribute(\"aria-expanded\", \"true\");\r\n\r\n window.setTimeout(() => {\r\n this.target.classList.remove(\"collapsing\");\r\n this.target.classList.add(\"collapse\", \"show\");\r\n this.target.style.height = \"\";\r\n U.dispatch(this.target, \"ux4g.collapse.shown\", {});\r\n }, this.duration);\r\n }\r\n\r\n hide() {\r\n if (!this.target) return;\r\n\r\n this.target.style.height = this.target.getBoundingClientRect().height + \"px\";\r\n U.reflow(this.target);\r\n\r\n this.target.classList.add(\"collapsing\");\r\n this.target.classList.remove(\"collapse\", \"show\");\r\n\r\n this.trigger.classList.add(\"collapsed\");\r\n this.trigger.setAttribute(\"aria-expanded\", \"false\");\r\n\r\n window.setTimeout(() => {\r\n this.target.style.height = \"0px\";\r\n }, 10);\r\n\r\n window.setTimeout(() => {\r\n this.target.classList.remove(\"collapsing\");\r\n this.target.classList.add(\"collapse\");\r\n this.target.style.height = \"\";\r\n U.dispatch(this.target, \"ux4g.collapse.hidden\", {});\r\n }, this.duration);\r\n }\r\n\r\n toggle() {\r\n if (!this.target) return;\r\n this.target.classList.contains(\"show\") ? this.hide() : this.show();\r\n }\r\n\r\n static getOrCreate(el) {\r\n let inst = getI(el, \"collapse\");\r\n if (!inst) { inst = new Collapse(el); setI(el, \"collapse\", inst); }\r\n return inst;\r\n }\r\n }\r\n\r\n // -----------------------------\r\n // Modal\r\n // -----------------------------\r\n class Modal {\r\n constructor(el) {\r\n this.el = el;\r\n this._shown = false;\r\n this._bdKind = \"modal\";\r\n this._lastFocus = null;\r\n this.duration = this._readDuration(el, 250);\r\n\r\n // Dismiss buttons inside\r\n U.on(this.el, \"click\", (e) => {\r\n const dismiss = U.closest(e.target, '[data-bs-dismiss=\"modal\"],[data-ux-dismiss=\"modal\"],.close-modal');\r\n if (dismiss) {\r\n e.preventDefault();\r\n this.hide();\r\n }\r\n });\r\n\r\n // ESC + trap\r\n U.on(document, \"keydown\", (e) => {\r\n if (!this._shown) return;\r\n if (e.key === \"Escape\") {\r\n const kb = U.bool(U.data(this.el, \"keyboard\", \"true\"), true);\r\n if (kb) this.hide();\r\n } else if (e.key === \"Tab\") {\r\n this._trapTab(e);\r\n }\r\n });\r\n }\r\n\r\n _readDuration(el, fallbackMs) {\r\n if (!el) return fallbackMs;\r\n const d = getComputedStyle(el).transitionDuration || \"\";\r\n const ms = d.includes(\"ms\") ? parseFloat(d) : (d.includes(\"s\") ? parseFloat(d) * 1000 : NaN);\r\n return Number.isFinite(ms) && ms > 0 ? ms : fallbackMs;\r\n }\r\n\r\n _trapTab(e) {\r\n const f = U.focusables(this.el);\r\n if (!f.length) return;\r\n const first = f[0], last = f[f.length - 1];\r\n if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }\r\n else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }\r\n }\r\n\r\n show(trigger) {\r\n if (this._shown) return;\r\n this._shown = true;\r\n this._lastFocus = document.activeElement;\r\n\r\n const backdropOpt = U.data(this.el, \"backdrop\", \"true\");\r\n const backdrop = backdropOpt !== \"false\";\r\n\r\n if (backdrop) {\r\n const bd = U.ensureBackdrop(this._bdKind);\r\n bd.classList.add(\"show\");\r\n U.on(bd, \"click\", () => {\r\n if (backdropOpt === \"static\") return;\r\n this.hide();\r\n });\r\n }\r\n\r\n U.lockBody(true);\r\n\r\n // ux4g expects display none -> block\r\n this.el.style.display = \"block\";\r\n this.el.removeAttribute(\"aria-hidden\");\r\n this.el.setAttribute(\"aria-modal\", \"true\");\r\n this.el.setAttribute(\"role\", this.el.getAttribute(\"role\") || \"dialog\");\r\n\r\n U.reflow(this.el);\r\n this.el.classList.add(\"show\");\r\n\r\n const focus = U.bool(U.data(this.el, \"focus\", \"true\"), true);\r\n if (focus) {\r\n const f = U.focusables(this.el);\r\n (f[0] || this.el).focus({ preventScroll: true });\r\n }\r\n\r\n U.dispatch(this.el, \"ux4g.modal.shown\", { relatedTarget: trigger || null });\r\n }\r\n\r\n hide() {\r\n if (!this._shown) return;\r\n this._shown = false;\r\n\r\n this.el.classList.remove(\"show\");\r\n this.el.setAttribute(\"aria-hidden\", \"true\");\r\n this.el.removeAttribute(\"aria-modal\");\r\n\r\n window.setTimeout(() => {\r\n this.el.style.display = \"none\";\r\n U.lockBody(false);\r\n U.removeBackdrop(this._bdKind);\r\n\r\n if (this._lastFocus && typeof this._lastFocus.focus === \"function\") {\r\n this._lastFocus.focus({ preventScroll: true });\r\n }\r\n U.dispatch(this.el, \"ux4g.modal.hidden\", {});\r\n }, this.duration);\r\n }\r\n\r\n toggle(trigger) { this._shown ? this.hide() : this.show(trigger); }\r\n\r\n static getOrCreate(el) {\r\n let inst = getI(el, \"modal\");\r\n if (!inst) { inst = new Modal(el); setI(el, \"modal\", inst); }\r\n return inst;\r\n }\r\n }\r\n\r\n // -----------------------------\r\n // Offcanvas\r\n // -----------------------------\r\n class Offcanvas {\r\n constructor(el) {\r\n this.el = el;\r\n this._shown = false;\r\n this._bdKind = \"offcanvas\";\r\n this._lastFocus = null;\r\n this.duration = this._readDuration(el, 250);\r\n\r\n U.on(this.el, \"click\", (e) => {\r\n const dismiss = U.closest(e.target, '[data-bs-dismiss=\"offcanvas\"],[data-ux-dismiss=\"offcanvas\"]');\r\n if (dismiss) {\r\n e.preventDefault();\r\n this.hide();\r\n }\r\n });\r\n\r\n U.on(document, \"keydown\", (e) => {\r\n if (!this._shown) return;\r\n if (e.key === \"Escape\") {\r\n const kb = U.bool(U.data(this.el, \"keyboard\", \"true\"), true);\r\n if (kb) this.hide();\r\n } else if (e.key === \"Tab\") {\r\n this._trapTab(e);\r\n }\r\n });\r\n }\r\n\r\n _readDuration(el, fallbackMs) {\r\n if (!el) return fallbackMs;\r\n const d = getComputedStyle(el).transitionDuration || \"\";\r\n const ms = d.includes(\"ms\") ? parseFloat(d) : (d.includes(\"s\") ? parseFloat(d) * 1000 : NaN);\r\n return Number.isFinite(ms) && ms > 0 ? ms : fallbackMs;\r\n }\r\n\r\n _trapTab(e) {\r\n const f = U.focusables(this.el);\r\n if (!f.length) return;\r\n const first = f[0], last = f[f.length - 1];\r\n if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }\r\n else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }\r\n }\r\n\r\n show(trigger) {\r\n if (this._shown) return;\r\n this._shown = true;\r\n this._lastFocus = document.activeElement;\r\n\r\n const backdropOpt = U.data(this.el, \"backdrop\", \"true\");\r\n const backdrop = backdropOpt !== \"false\";\r\n\r\n if (backdrop) {\r\n const bd = U.ensureBackdrop(this._bdKind);\r\n bd.classList.add(\"show\");\r\n U.on(bd, \"click\", () => {\r\n if (backdropOpt === \"static\") return;\r\n this.hide();\r\n });\r\n }\r\n\r\n U.lockBody(true);\r\n this.el.style.visibility = \"visible\";\r\n this.el.classList.add(\"show\");\r\n this.el.setAttribute(\"aria-modal\", \"true\");\r\n\r\n const focus = U.bool(U.data(this.el, \"focus\", \"true\"), true);\r\n if (focus) {\r\n const f = U.focusables(this.el);\r\n (f[0] || this.el).focus({ preventScroll: true });\r\n }\r\n\r\n U.dispatch(this.el, \"ux4g.offcanvas.shown\", { relatedTarget: trigger || null });\r\n }\r\n\r\n hide() {\r\n if (!this._shown) return;\r\n this._shown = false;\r\n\r\n this.el.classList.remove(\"show\");\r\n this.el.removeAttribute(\"aria-modal\");\r\n\r\n window.setTimeout(() => {\r\n this.el.style.visibility = \"\";\r\n U.lockBody(false);\r\n U.removeBackdrop(this._bdKind);\r\n\r\n if (this._lastFocus && typeof this._lastFocus.focus === \"function\") {\r\n this._lastFocus.focus({ preventScroll: true });\r\n }\r\n U.dispatch(this.el, \"ux4g.offcanvas.hidden\", {});\r\n }, this.duration);\r\n }\r\n\r\n toggle(trigger) { this._shown ? this.hide() : this.show(trigger); }\r\n\r\n static getOrCreate(el) {\r\n let inst = getI(el, \"offcanvas\");\r\n if (!inst) { inst = new Offcanvas(el); setI(el, \"offcanvas\", inst); }\r\n return inst;\r\n }\r\n }\r\n\r\n // -----------------------------\r\n // Tooltip / Popover (lightweight)\r\n // -----------------------------\r\n class Floating {\r\n constructor(el, kind) {\r\n this.el = el;\r\n this.kind = kind; // tooltip | popover\r\n this._open = false;\r\n this._floating = null;\r\n\r\n this.placement = U.data(el, \"placement\", kind === \"tooltip\" ? \"top\" : \"right\");\r\n this.offset = U.num(U.data(el, \"offset\", 8), 8);\r\n this.trigger = U.data(el, \"trigger\", kind === \"tooltip\" ? \"hover focus\" : \"click\");\r\n this.html = U.bool(U.data(el, \"html\", \"false\"), false);\r\n\r\n this._bind();\r\n }\r\n\r\n _getContent() {\r\n const content = U.data(this.el, \"content\");\r\n if (this.kind === \"popover\") {\r\n const title = U.data(this.el, \"title\") || this.el.getAttribute(\"title\") || \"\";\r\n const subtitle = U.data(this.el, \"subtitle\") || \"\";\r\n const icon = U.data(this.el, \"icon\") || \"\";\r\n const label = U.data(this.el, \"label\") || \"\";\r\n const actionHtml = U.data(this.el, \"action-html\") || \"\";\r\n\r\n const t = this.html ? String(title) : escapeHtml(title);\r\n const s = this.html ? String(subtitle) : escapeHtml(subtitle);\r\n const c = this.html ? String(content || \"\") : escapeHtml(content || \"\");\r\n const i = this.html ? String(icon) : escapeHtml(icon);\r\n const l = this.html ? String(label) : escapeHtml(label);\r\n\r\n let inner = \"\";\r\n if (t || s) {\r\n inner += `<div class=\"ux4g-popover-header\">\r\n <div class=\"ux4g-popover-title-row\">\r\n <div class=\"ux4g-popover-title\">\r\n ${i ? `<i class=\"ux4g-icon\">${i}</i>` : \"\"}\r\n <span>${t}</span>\r\n ${l ? `<span class=\"ux4g-tag-outline-brand\">${l}</span>` : \"\"}\r\n </div>\r\n ${actionHtml ? `\r\n ${actionHtml}\r\n ` : \"\"}\r\n </div>\r\n ${s ? `<div class=\"ux4g-popover-subtitle\">${s}</div>` : \"\"}\r\n </div>`;\r\n }\r\n inner += `<div class=\"ux4g-popover-body\">${c}</div>`;\r\n \r\n const hasArrow = U.bool(U.data(this.el, \"arrow\", \"true\"), true);\r\n if (hasArrow) {\r\n inner += '<div class=\"ux4g-popover-arrow\"><i class=\"ux4g-icon\">arrow_drop_up</i></div>';\r\n }\r\n \r\n return inner;\r\n }\r\n\r\n const t = content != null ? content : (this.el.getAttribute(\"title\") || \"\");\r\n return this.html ? String(t) : escapeHtml(t);\r\n }\r\n\r\n _create() {\r\n if (this._floating) return;\r\n const div = document.createElement(\"div\");\r\n div.className = this.kind === \"tooltip\" ? \"ux4g-tooltip\" : \"ux4g-popover\";\r\n div.setAttribute(\"role\", this.kind === \"tooltip\" ? \"tooltip\" : \"dialog\");\r\n \r\n const hasArrow = U.bool(U.data(this.el, \"arrow\", \"true\"), true);\r\n if (!hasArrow) {\r\n div.classList.add(\"ux4g-popover-no-arrow\");\r\n }\r\n\r\n div.innerHTML = this._getContent() || \"\";\r\n document.body.appendChild(div);\r\n this._floating = div;\r\n }\r\n\r\n show() {\r\n if (this._open) return;\r\n this._open = true;\r\n\r\n // Prevent native tooltip doubling\r\n if (this.kind === \"tooltip\") {\r\n const t = this.el.getAttribute(\"title\");\r\n if (t != null) {\r\n this.el.setAttribute(\"data-ux-original-title\", t);\r\n this.el.removeAttribute(\"title\");\r\n }\r\n }\r\n\r\n this._create();\r\n this._floating.style.display = \"block\";\r\n this._floating.classList.add(\"show\");\r\n \r\n init(this._floating);\r\n \r\n const update = () => {\r\n if (!this._open) return;\r\n U.placeFloating(this.el, this._floating, this.placement, this.offset);\r\n this._raf = requestAnimationFrame(update);\r\n };\r\n this._onWin = update;\r\n this._raf = requestAnimationFrame(update);\r\n\r\n U.on(window, \"scroll\", this._onWin, { capture: true, passive: true });\r\n U.on(window, \"resize\", this._onWin);\r\n\r\n U.dispatch(this.el, `ux4g.${this.kind}.shown`, {});\r\n }\r\n\r\n hide() {\r\n if (!this._open) return;\r\n this._open = false;\r\n\r\n if (this._floating) {\r\n this._floating.classList.remove(\"show\");\r\n this._floating.style.display = \"none\";\r\n }\r\n\r\n if (this.kind === \"tooltip\") {\r\n const ot = this.el.getAttribute(\"data-ux-original-title\");\r\n if (ot != null) {\r\n this.el.setAttribute(\"title\", ot);\r\n this.el.removeAttribute(\"data-ux-original-title\");\r\n }\r\n }\r\n\r\n if (this._raf) {\r\n cancelAnimationFrame(this._raf);\r\n this._raf = null;\r\n }\r\n\r\n if (this._onWin) {\r\n U.off(window, \"scroll\", this._onWin, { capture: true });\r\n U.off(window, \"resize\", this._onWin);\r\n this._onWin = null;\r\n }\r\n\r\n U.dispatch(this.el, `ux4g.${this.kind}.hidden`, {});\r\n }\r\n\r\n toggle() { this._open ? this.hide() : this.show(); }\r\n\r\n _bind() {\r\n let triggers = String(this.trigger).split(/\\s+/).filter(Boolean);\r\n \r\n if (this.kind === \"popover\") {\r\n triggers = triggers.filter(t => t !== \"hover\");\r\n if (!triggers.length) triggers = [\"click\"];\r\n }\r\n\r\n if (triggers.includes(\"hover\")) {\r\n U.on(this.el, \"mouseenter\", () => this.show());\r\n U.on(this.el, \"mouseleave\", () => this.hide());\r\n }\r\n if (triggers.includes(\"focus\")) {\r\n U.on(this.el, \"focus\", () => this.show());\r\n U.on(this.el, \"blur\", () => this.hide());\r\n }\r\n if (triggers.includes(\"click\")) {\r\n U.on(this.el, \"click\", (e) => { e.preventDefault(); this.toggle(); });\r\n\r\n U.on(document, \"click\", (e) => {\r\n if (!this._open) return;\r\n if (this.el.contains(e.target) || (this._floating && this._floating.contains(e.target))) return;\r\n this.hide();\r\n });\r\n\r\n U.on(document, \"keydown\", (e) => {\r\n if (!this._open) return;\r\n if (e.key === \"Escape\") this.hide();\r\n });\r\n }\r\n }\r\n\r\n static getOrCreate(el, kind) {\r\n const key = kind;\r\n let inst = getI(el, key);\r\n if (!inst) { inst = new Floating(el, kind); setI(el, key, inst); }\r\n return inst;\r\n }\r\n }\r\n\r\n // -----------------------------\r\n // Toast\r\n // -----------------------------\r\n class Toast {\r\n constructor(el) {\r\n this.el = el;\r\n this._timer = null;\r\n\r\n U.on(this.el, \"click\", (e) => {\r\n const dismiss = U.closest(e.target, '[data-bs-dismiss=\"toast\"],[data-ux-dismiss=\"toast\"],.close-toast');\r\n if (dismiss) {\r\n e.preventDefault();\r\n this.hide();\r\n }\r\n });\r\n }\r\n\r\n show() {\r\n this.el.classList.add(\"show\");\r\n this.el.classList.remove(\"hide\");\r\n\r\n const autohide = U.bool(U.data(this.el, \"autohide\", \"true\"), true);\r\n const delay = U.num(U.data(this.el, \"delay\", 5000), 5000);\r\n\r\n if (autohide) {\r\n clearTimeout(this._timer);\r\n this._timer = setTimeout(() => this.hide(), delay);\r\n }\r\n U.dispatch(this.el, \"ux4g.toast.shown\", {});\r\n }\r\n\r\n hide() {\r\n this.el.classList.remove(\"show\");\r\n this.el.classList.add(\"hide\");\r\n clearTimeout(this._timer);\r\n U.dispatch(this.el, \"ux4g.toast.hidden\", {});\r\n }\r\n\r\n static getOrCreate(el) {\r\n let inst = getI(el, \"toast\");\r\n if (!inst) { inst = new Toast(el); setI(el, \"toast\", inst); }\r\n return inst;\r\n }\r\n }\r\n\r\n // -----------------------------\r\n // Carousel\r\n // -----------------------------\r\n class Carousel {\r\n constructor(el) {\r\n this.el = el;\r\n this.items = U.qsa(\".carousel-item\", el);\r\n this.indicators = U.qsa(\"[data-bs-slide-to],[data-ux-slide-to]\", el);\r\n this.interval = U.num(U.data(el, \"interval\", 5000), 5000);\r\n this.ride = U.data(el, \"ride\");\r\n this.pause = U.data(el, \"pause\", \"hover\");\r\n this.wrap = U.bool(U.data(el, \"wrap\", \"true\"), true);\r\n this._timer = null;\r\n\r\n U.on(el, \"click\", (e) => {\r\n const prev = U.closest(e.target, '[data-bs-slide=\"prev\"],[data-ux-slide=\"prev\"]');\r\n const next = U.closest(e.target, '[data-bs-slide=\"next\"],[data-ux-slide=\"next\"]');\r\n if (prev) { e.preventDefault(); this.prev(); }\r\n if (next) { e.preventDefault(); this.next(); }\r\n\r\n const ind = U.closest(e.target, \"[data-bs-slide-to],[data-ux-slide-to]\");\r\n if (ind) {\r\n e.preventDefault();\r\n const v = ind.getAttribute(\"data-bs-slide-to\") ?? ind.getAttribute(\"data-ux-slide-to\");\r\n this.to(U.num(v, 0));\r\n }\r\n });\r\n\r\n if (this.pause === \"hover\") {\r\n U.on(el, \"mouseenter\", () => this._stop());\r\n U.on(el, \"mouseleave\", () => this._start());\r\n }\r\n\r\n if (this.ride === \"carousel\") this._start();\r\n }\r\n\r\n _activeIndex() {\r\n const idx = this.items.findIndex(i => i.classList.contains(\"active\"));\r\n return idx >= 0 ? idx : 0;\r\n }\r\n\r\n _setActive(nextIndex) {\r\n if (!this.items.length) return;\r\n const cur = this._activeIndex();\r\n\r\n if (nextIndex < 0) nextIndex = this.wrap ? this.items.length - 1 : 0;\r\n if (nextIndex >= this.items.length) nextIndex = this.wrap ? 0 : this.items.length - 1;\r\n if (cur === nextIndex) return;\r\n\r\n this.items[cur]?.classList.remove(\"active\");\r\n this.items[nextIndex]?.classList.add(\"active\");\r\n\r\n this.indicators.forEach(ind => ind.classList.remove(\"active\"));\r\n const ind = this.indicators.find(x => {\r\n const v = x.getAttribute(\"data-bs-slide-to\") ?? x.getAttribute(\"data-ux-slide-to\");\r\n return U.num(v, -1) === nextIndex;\r\n });\r\n if (ind) ind.classList.add(\"active\");\r\n\r\n U.dispatch(this.el, \"ux4g.carousel.slid\", { from: cur, to: nextIndex });\r\n }\r\n\r\n next() { this._setActive(this._activeIndex() + 1); }\r\n prev() { this._setActive(this._activeIndex() - 1); }\r\n to(i) { this._setActive(i); }\r\n\r\n _start() {\r\n if (this._timer || this.interval <= 0) return;\r\n this._timer = setInterval(() => this.next(), this.interval);\r\n }\r\n\r\n _stop() { clearInterval(this._timer); this._timer = null; }\r\n\r\n static getOrCreate(el) {\r\n let inst = getI(el, \"carousel\");\r\n if (!inst) { inst = new Carousel(el); setI(el, \"carousel\", inst); }\r\n return inst;\r\n }\r\n }\r\n\r\n // -----------------------------\r\n // Tabs\r\n // -----------------------------\r\n class Tab {\r\n constructor(el) {\r\n this.el = el;\r\n\r\n U.on(el, \"click\", (e) => { e.preventDefault(); this.show(); });\r\n\r\n U.on(el, \"keydown\", (e) => {\r\n if (e.key !== \"ArrowLeft\" && e.key !== \"ArrowRight\") return;\r\n const list = U.closest(this.el, \".nav, [role='tablist']\");\r\n if (!list) return;\r\n\r\n const tabs = U.qsa(\"[data-bs-toggle='tab'],[data-ux-toggle='tab'],[role='tab']\", list);\r\n const idx = tabs.indexOf(this.el);\r\n if (idx < 0) return;\r\n\r\n e.preventDefault();\r\n const next = (e.key === \"ArrowRight\") ? idx + 1 : idx - 1;\r\n const wrapIdx = (next + tabs.length) % tabs.length;\r\n tabs[wrapIdx].focus();\r\n Tab.getOrCreate(tabs[wrapIdx]).show();\r\n });\r\n }\r\n\r\n _target() {\r\n const sel = U.data(this.el, \"target\") || U.attr(this.el, \"href\") || U.attr(this.el, \"data-target\");\r\n if (sel && sel.startsWith(\"#\")) return U.qs(sel);\r\n const controls = this.el.getAttribute(\"aria-controls\");\r\n if (controls) return U.qs(\"#\" + controls);\r\n return null;\r\n }\r\n\r\n show() {\r\n const list = U.closest(this.el, \".nav, [role='tablist']\");\r\n const pane = this._target();\r\n if (!list || !pane) return;\r\n\r\n const tabs = U.qsa(\"[data-bs-toggle='tab'],[data-ux-toggle='tab'],[role='tab']\", list);\r\n tabs.forEach(t => {\r\n t.classList.remove(\"active\");\r\n t.setAttribute(\"aria-selected\", \"false\");\r\n t.setAttribute(\"tabindex\", \"-1\");\r\n });\r\n\r\n this.el.classList.add(\"active\");\r\n this.el.setAttribute(\"aria-selected\", \"true\");\r\n this.el.setAttribute(\"tabindex\", \"0\");\r\n\r\n const container = U.closest(pane, \".tab-content\") || pane.parentElement;\r\n const panes = container ? U.qsa(\".tab-pane\", container) : [];\r\n panes.forEach(p => p.classList.remove(\"active\", \"show\"));\r\n\r\n pane.classList.add(\"active\", \"show\");\r\n U.dispatch(this.el, \"ux4g.tab.shown\", { relatedTarget: pane });\r\n }\r\n\r\n static getOrCreate(el) {\r\n let inst = getI(el, \"tab\");\r\n if (!inst) { inst = new Tab(el); setI(el, \"tab\", inst); }\r\n return inst;\r\n }\r\n }\r\n\r\n // -----------------------------\r\n // Scrollspy\r\n // -----------------------------\r\n class ScrollSpy {\r\n constructor(el) {\r\n this.el = el; // element with data-bs-spy=\"scroll\"\r\n this.targetSel = U.data(el, \"target\");\r\n this.offset = U.num(U.data(el, \"offset\", 10), 10);\r\n this._links = [];\r\n this._sections = [];\r\n\r\n this.refresh();\r\n this._bind();\r\n }\r\n\r\n refresh() {\r\n const nav = this.targetSel ? U.qs(this.targetSel) : null;\r\n if (!nav) return;\r\n\r\n this._links = U.qsa('a[href^=\"#\"]', nav)\r\n .filter(a => a.getAttribute(\"href\").length > 1);\r\n\r\n this._sections = this._links\r\n .map(a => U.qs(a.getAttribute(\"href\")))\r\n .filter(Boolean);\r\n }\r\n\r\n _activate(id) {\r\n const nav = this.targetSel ? U.qs(this.targetSel) : null;\r\n if (!nav) return;\r\n\r\n this._links.forEach(a => a.classList.remove(\"active\"));\r\n const link = this._links.find(a => a.getAttribute(\"href\") === \"#\" + id);\r\n if (link) link.classList.add(\"active\");\r\n }\r\n\r\n _bind() {\r\n // If the spy is on body, use window; else use that element\r\n const container = (this.el === document.body || this.el === document.documentElement) ? window : this.el;\r\n\r\n if (\"IntersectionObserver\" in window) {\r\n const root = (container === window) ? null : this.el;\r\n\r\n const io = new IntersectionObserver((entries) => {\r\n const visible = entries\r\n .filter(e => e.isIntersecting)\r\n .sort((a, b) => b.intersectionRatio - a.intersectionRatio)[0];\r\n if (visible?.target?.id) this._activate(visible.target.id);\r\n }, {\r\n root,\r\n rootMargin: `-${this.offset}px 0px -60% 0px`,\r\n threshold: [0.1, 0.25, 0.5, 0.75]\r\n });\r\n\r\n this._sections.forEach(s => io.observe(s));\r\n this._io = io;\r\n return;\r\n }\r\n\r\n this._onScroll = () => {\r\n const scrollTop = (container === window) ? window.pageYOffset : this.el.scrollTop;\r\n let active = null;\r\n\r\n for (const s of this._sections) {\r\n const top = s.getBoundingClientRect().top + window.pageYOffset;\r\n if (scrollTop + this.offset >= top) active = s;\r\n }\r\n if (active?.id) this._activate(active.id);\r\n };\r\n\r\n U.on(container, \"scroll\", this._onScroll, { passive: true });\r\n this._onScroll();\r\n }\r\n\r\n static getOrCreate(el) {\r\n let inst = getI(el, \"scrollspy\");\r\n if (!inst) { inst = new ScrollSpy(el); setI(el, \"scrollspy\", inst); }\r\n return inst;\r\n }\r\n }\r\n\r\n // -----------------------------\r\n // Table\r\n // -----------------------------\r\n class Table {\r\n constructor(el) {\r\n this.el = el;\r\n this._bindSort();\r\n this._bindResize();\r\n this._bindSelection();\r\n this._bindFilter();\r\n }\r\n\r\n _bindFilter() {\r\n const filterBtns = U.qsa(\".ux4g-table-filter-icon\", this.el);\r\n filterBtns.forEach(btn => {\r\n U.on(btn, \"click\", (e) => {\r\n e.preventDefault();\r\n e.stopPropagation();\r\n const targetTh = U.closest(btn, \"th\");\r\n if (targetTh) {\r\n // Close other filters first if needed or just toggle this one\r\n targetTh.classList.toggle(\"ux4g-is-filtering\");\r\n if (targetTh.classList.contains(\"ux4g-is-filtering\")) {\r\n const input = U.qs(\".ux4g-search-input\", targetTh);\r\n if (input) input.focus();\r\n }\r\n }\r\n });\r\n });\r\n\r\n const closeBtns = U.qsa(\".ux4g-search-clear\", this.el);\r\n closeBtns.forEach(btn => {\r\n U.on(btn, \"click\", (e) => {\r\n // Only close if it's within a table filter\r\n const th = U.closest(btn, \"th.ux4g-is-filtering\");\r\n if (th) {\r\n e.preventDefault();\r\n e.stopPropagation();\r\n th.classList.remove(\"ux4g-is-filtering\");\r\n const input = U.qs(\".ux4g-search-input\", th);\r\n if (input) {\r\n input.value = '';\r\n const container = U.closest(input, '.ux4g-search-container');\r\n if (container) container.classList.remove('ux4g-has-value');\r\n }\r\n }\r\n });\r\n });\r\n\r\n const inputs = U.qsa(\"th .ux4g-search-input\", this.el);\r\n inputs.forEach(input => {\r\n U.on(input, \"input\", (e) => {\r\n const container = U.closest(input, '.ux4g-search-container');\r\n if (container) {\r\n if (input.value.length > 0) {\r\n container.classList.add('ux4g-has-value');\r\n } else {\r\n container.classList.remove('ux4g-has-value');\r\n }\r\n }\r\n });\r\n });\r\n }\r\n\r\n _bindSort() {\r\n const sortableCols = U.qsa(\".ux4g-table-sortable th[data-sort]\", this.el);\r\n sortableCols.forEach(th => {\r\n if (!U.qs(\".ux4g-table-sort-icon\", th)) {\r\n const content = U.qs(\".ux4g-table-th-content\", th) || th;\r\n const icon = document.createElement(\"i\");\r\n icon.className = \"ux4g-icon ux4g-table-sort-icon\";\r\n icon.innerHTML = \"arrow_downward\";\r\n content.appendChild(icon);\r\n }\r\n\r\n U.on(th, \"click\", (e) => {\r\n // If the click happened on a filter button or something else interactive, ignore sort\r\n if (U.closest(e.target, \".ux4g-table-filter-icon\") || U.closest(e.target, \".ux4g-search-input\") || U.closest(e.target, \".ux4g-search-clear\")) {\r\n return;\r\n }\r\n\r\n const currentSort = U.attr(th, \"data-sort\", \"none\");\r\n // Cycle: none -> asc -> desc -> asc... (skip none once sorted to avoid double clicks)\r\n const nextSort = currentSort === \"asc\" ? \"desc\" : \"asc\";\r\n \r\n // Reset other columns in the same table\r\n sortableCols.forEach(otherTh => {\r\n if (otherTh !== th) otherTh.setAttribute(\"data-sort\", \"none\");\r\n });\r\n\r\n th.setAttribute(\"data-sort\", nextSort);\r\n \r\n // Row sorting logic\r\n if (nextSort !== \"none\") {\r\n const tbody = U.qs(\"tbody\", this.el);\r\n if (tbody) {\r\n const trs = Array.from(tbody.querySelectorAll(\"tr\"));\r\n const thIndex = Array.from(th.parentNode.children).indexOf(th);\r\n \r\n trs.sort((a, b) => {\r\n const aCol = a.children[thIndex];\r\n const bCol = b.children[thIndex];\r\n if (!aCol || !bCol) return 0;\r\n \r\n const aText = (aCol.textContent || aCol.innerText).trim();\r\n const bText = (bCol.textContent || bCol.innerText).trim();\r\n \r\n const cleanV1 = aText.replace(/[₹$,\\s]/g, \"\");\r\n const cleanV2 = bText.replace(/[₹$,\\s]/g, \"\");\r\n const num1 = Number(cleanV1);\r\n const num2 = Number(cleanV2);\r\n \r\n const isNum = !isNaN(num1) && !isNaN(num2) && cleanV1 !== \"\" && cleanV2 !== \"\";\r\n \r\n if (isNum) {\r\n return nextSort === \"asc\" ? (num1 - num2) : (num2 - num1);\r\n } else {\r\n const comp = aText.localeCompare(bText, undefined, { numeric: true, sensitivity: 'base' });\r\n return nextSort === \"asc\" ? comp : -comp;\r\n }\r\n });\r\n \r\n trs.forEach(tr => tbody.appendChild(tr));\r\n }\r\n }\r\n\r\n U.dispatch(this.el, \"ux4g.table.sort\", { column: th, direction: nextSort });\r\n });\r\n });\r\n }\r\n\r\n _bindResize() {\r\n // Check if this table instance has resize enabled\r\n if (!this.el.classList.contains(\"ux4g-table-resizable\")) return;\r\n const resizableCols = U.qsa(\"th\", this.el);\r\n\r\n resizableCols.forEach(th => {\r\n // Skip last child to avoid out-of-bounds drag or provide a cleaner UX edge\r\n if (th === th.parentNode.lastElementChild) return;\r\n\r\n const handle = document.createElement(\"div\");\r\n handle.className = \"ux4g-table-resize-handle\";\r\n th.appendChild(handle);\r\n\r\n let startX, startWidth;\r\n const onMouseMove = (e) => {\r\n const newWidth = Math.max(40, startWidth + (e.clientX - startX));\r\n th.style.width = `${newWidth}px`;\r\n th.style.minWidth = `${newWidth}px`;\r\n };\r\n\r\n const onMouseUp = () => {\r\n handle.classList.remove(\"is-resizing\");\r\n handle.classList.remove(\"ux4g-is-resizing\");\r\n U.off(document, \"mousemove\", onMouseMove);\r\n U.off(document, \"mouseup\", onMouseUp);\r\n U.dispatch(this.el, \"ux4g.table.resize\", { column: th, width: th.offsetWidth });\r\n };\r\n\r\n U.on(handle, \"mousedown\", (e) => {\r\n e.preventDefault();\r\n e.stopPropagation();\r\n startX = e.clientX;\r\n startWidth = th.offsetWidth || th.getBoundingClientRect().width;\r\n handle.classList.add(\"ux4g-is-resizing\");\r\n U.on(document, \"mousemove\", onMouseMove);\r\n U.on(document, \"mouseup\", onMouseUp);\r\n });\r\n });\r\n }\r\n\r\n _bindSelection() {\r\n // Find a select-all checkbox in the head\r\n const selectAll = U.qs(\"thead .ux4g-checkbox\", this.el);\r\n if (!selectAll) return;\r\n\r\n const rowCheckboxes = U.qsa(\"tbody .ux4g-checkbox\", this.el);\r\n if (!rowCheckboxes.length) return;\r\n \r\n const updateState = () => {\r\n let checkedCount = 0;\r\n rowCheckboxes.forEach(cb => {\r\n const tr = U.closest(cb, \"tr\");\r\n if (cb.checked) {\r\n checkedCount++;\r\n if (tr) tr.classList.add(\"ux4g-is-selected\");\r\n } else {\r\n if (tr) tr.classList.remove(\"ux4g-is-selected\");\r\n }\r\n });\r\n\r\n if (checkedCount === 0) {\r\n selectAll.checked = false;\r\n selectAll.indeterminate = false;\r\n } else if (checkedCount === rowCheckboxes.length) {\r\n selectAll.checked = true;\r\n selectAll.indeterminate = false;\r\n } else {\r\n selectAll.checked = false;\r\n selectAll.indeterminate = true;\r\n }\r\n };\r\n\r\n // Listen to select-all\r\n U.on(selectAll, \"change\", (e) => {\r\n const isChecked = e.target.checked;\r\n rowCheckboxes.forEach(cb => {\r\n cb.checked = isChecked;\r\n });\r\n updateState();\r\n });\r\n\r\n // Listen to individual row checkboxes\r\n rowCheckboxes.forEach(cb => {\r\n U.on(cb, \"change\", updateState);\r\n \r\n // Also allow row click to toggle checkbox, skipping if clicking on interactable elements\r\n const tr = U.closest(cb, \"tr\");\r\n if (tr && tr.classList.contains(\"ux4g-table-interactive\")) {\r\n U.on(tr, \"click\", (e) => {\r\n if (e.target.tagName !== \"INPUT\" && e.target.tagName !== \"BUTTON\" && !U.closest(e.target, \"button\") && !U.closest(e.target, \"a\")) {\r\n cb.checked = !cb.checked;\r\n updateState();\r\n }\r\n });\r\n }\r\n });\r\n\r\n // Initialize state on load\r\n updateState();\r\n }\r\n\r\n static getOrCreate(el) {\r\n let inst = getI(el, \"table\");\r\n if (!inst) { inst = new Table(el); setI(el, \"table\", inst); }\r\n return inst;\r\n }\r\n }\r\n\r\n // -----------------------------\r\n // List\r\n // -----------------------------\r\n class List {\r\n constructor(el) {\r\n this.el = el;\r\n this._bind();\r\n }\r\n\r\n _bind() {\r\n U.on(this.el, \"click\", (e) => {\r\n const item = U.closest(e.target, \".ux4g-list-item-row\") || U.closest(e.target, \".ux4g-list-select-item\");\r\n if (!item || item.disabled) return;\r\n\r\n const isMulti = (this.el.id === \"ux4g-multiselect-list\") || \r\n this.el.classList.contains(\"ux4g-multiselect\") || \r\n this.el.classList.contains(\"ux4g-list-multiselect\");\r\n const checkbox = U.qs('input[type=\"checkbox\"]', item);\r\n const radio = U.qs('input[type=\"radio\"]', item);\r\n const switchInput = U.qs('.ux4g-switch-input', item);\r\n\r\n // If clicking on input directly, don't double toggle\r\n if (e.target.tagName === 'INPUT') {\r\n const inputChecked = e.target.checked;\r\n \r\n if (!isMulti) {\r\n const allItems = U.qsa(\".ux4g-list-item-row, .ux4g-list-select-item\", this.el);\r\n allItems.forEach(i => {\r\n if (i !== item) i.classList.remove(\"active\");\r\n // Also ensure other inputs are unchecked\r\n if (i !== item) {\r\n const otherInp = U.qs('input', i);\r\n if (otherInp) otherInp.checked = false;\r\n }\r\n });\r\n item.classList.toggle(\"active\", inputChecked);\r\n } else {\r\n item.classList.toggle(\"active\", inputChecked);\r\n }\r\n return;\r\n }\r\n\r\n if (isMulti) {\r\n const isActive = item.classList.toggle(\"active\");\r\n if (checkbox) {\r\n checkbox.checked = isActive;\r\n checkbox.dispatchEvent(new Event(\"change\", { bubbles: true }));\r\n }\r\n if (switchInput) {\r\n switchInput.checked = isActive;\r\n switchInput.dispatchEvent(new Event(\"change\", { bubbles: true }));\r\n }\r\n } else {\r\n // Single selection\r\n const wasActive = item.classList.contains(\"active\");\r\n const allItems = U.qsa(\".ux4g-list-item-row, .ux4g-list-select-item\", this.el);\r\n \r\n // Clear all first\r\n allItems.forEach(i => {\r\n i.classList.remove(\"active\");\r\n const cb = U.qs('input[type=\"checkbox\"]', i);\r\n const rb = U.qs('input[type=\"radio\"]', i);\r\n const sw = U.qs('.ux4g-switch-input', i);\r\n if (cb) cb.checked = false;\r\n if (rb) rb.checked = false;\r\n if (sw) sw.checked = false;\r\n });\r\n\r\n // Toggle: only add if it wasn't already active\r\n if (!wasActive) {\r\n item.classList.add(\"active\");\r\n if (checkbox) {\r\n checkbox.checked = true;\r\n checkbox.dispatchEvent(new Event(\"change\", { bubbles: true }));\r\n }\r\n if (radio) {\r\n radio.checked = true;\r\n radio.dispatchEvent(new Event(\"change\", { bubbles: true }));\r\n }\r\n if (switchInput) {\r\n switchInput.checked = true;\r\n switchInput.dispatchEvent(new Event(\"change\", { bubbles: true }));\r\n }\r\n }\r\n }\r\n \r\n U.dispatch(this.el, \"ux4g.list.change\", { item, active: item.classList.contains(\"active\") });\r\n });\r\n }\r\n\r\n static getOrCreate(el) {\r\n let inst = getI(el, \"list\");\r\n if (!inst) { inst = new List(el); setI(el, \"list\", inst); }\r\n return inst;\r\n }\r\n }\r\n\r\n // -----------------------------\r\n // Upload\r\n // -----------------------------\r\n class Upload {\r\n constructor(el) {\r\n this.el = el;\r\n this.input = el.querySelector('[data-ux-upload-input], .ux4g-upload-input');\r\n this.dropzone = el.querySelector('.ux4g-upload-panel');\r\n this.fileList = el.querySelector('.ux4g-upload-file-list');\r\n this.errorMsg = el.querySelector('.ux4g-upload-error-msg');\r\n this.errorText = el.querySelector('.ux4g-upload-error-text');\r\n this.moreButton = el.querySelector('.ux4g-upload-more');\r\n this.heading = el.querySelector('.ux4g-upload-heading');\r\n this.defaultHeading = this.heading ? this.heading.textContent.trim() : '';\r\n this.files = [];\r\n this.dragDepth = 0;\r\n this.stateClasses = [\r\n 'ux4g-upload-state-default',\r\n 'ux4g-upload-state-default-vle',\r\n 'ux4g-upload-state-selecting',\r\n 'ux4g-upload-state-scanning',\r\n 'ux4g-upload-state-uploaded',\r\n 'ux4g-upload-state-uploaded-vle',\r\n 'ux4g-upload-state-error'\r\n ];\r\n this.maxSizeMB = U.num(U.data(el, 'max-size', 5), 5);\r\n this.accept = (U.attr(this.input, 'accept', '') || '')\r\n .split(',')\r\n .map(s => s.trim().toLowerCase())\r\n .filter(Boolean);\r\n this._bind();\r\n this._syncInitialState();\r\n }\r\n\r\n _bind() {\r\n U.on(this.el, 'click', e => {\r\n if (U.closest(e.target, '[data-ux-upload-trigger]')) {\r\n this._openPicker();\r\n return;\r\n }\r\n if (U.closest(e.target, '.ux4g-upload-file-remove')) {\r\n const item = U.closest(e.target, '.ux4g-upload-file-item');\r\n this._removeFile(item);\r\n return;\r\n }\r\n if (U.closest(e.target, '.ux4g-upload-file-retry')) {\r\n this._clearError();\r\n this._openPicker();\r\n return;\r\n }\r\n if (U.closest(e.target, '.ux4g-upload-more')) {\r\n this._openPicker();\r\n }\r\n });\r\n\r\n U.on(this.dropzone, 'keydown', e => {\r\n if (e.key === ' ' || e.key === 'Enter') {\r\n e.preventDefault();\r\n this._openPicker();\r\n }\r\n });\r\n\r\n U.on(this.input, 'click', () => {\r\n this.input.setAttribute('data-clicked', 'true');\r\n });\r\n\r\n U.on(this.input, 'change', e => this._addFiles(Array.from(e.target.files || [])));\r\n\r\n U.on(this.dropzone, 'dragenter', e => {\r\n e.preventDefault();\r\n this.dragDepth += 1;\r\n this._setState('selecting');\r\n });\r\n\r\n U.on(this.dropzone, 'dragover', e => {\r\n e.preventDefault();\r\n this._setState('selecting');\r\n });\r\n\r\n U.on(this.dropzone, 'dragleave', e => {\r\n e.preventDefault();\r\n this.dragDepth = Math.max(0, this.dragDepth - 1);\r\n const nextTarget = e.relatedTarget;\r\n if (this.dragDepth === 0 || !nextTarget || !this.dropzone.contains(nextTarget)) {\r\n this._clearActive();\r\n }\r\n });\r\n\r\n U.on(this.dropzone, 'drop', e => {\r\n e.preventDefault();\r\n this.dragDepth = 0;\r\n this._clearActive();\r\n this._addFiles(Array.from((e.dataTransfer && e.dataTransfer.files) || []));\r\n });\r\n }\r\n\r\n _openPicker() {\r\n if (!this.input) return;\r\n this.input.setAttribute('data-clicked', 'true');\r\n this.input.click();\r\n }\r\n\r\n _setState(state) {\r\n this.el.classList.remove('ux4g-upload-state-selecting', 'ux4g-upload-state-error');\r\n if (state === 'selecting') this.el.classList.add('ux4g-upload-state-selecting');\r\n if (state === 'error') this.el.classList.add('ux4g-upload-state-error');\r\n this._syncDragHeading(state === 'selecting');\r\n }\r\n\r\n _clearActive() {\r\n this.el.classList.remove('ux4g-upload-state-selecting');\r\n this._syncDragHeading(false);\r\n }\r\n\r\n _syncDragHeading(isDragging) {\r\n if (!this.heading) return;\r\n this.heading.textContent = isDragging ? 'Drop file here' : this.defaultHeading;\r\n }\r\n\r\n _showError(msg, file) {\r\n this.el.classList.remove('ux4g-upload-state-selecting');\r\n this.el.classList.add('ux4g-upload-state-error');\r\n this._clearErrorRows();\r\n this._renderErrorFile(file, msg);\r\n if (this.errorMsg) this.errorMsg.classList.add('ux4g-d-none');\r\n }\r\n\r\n _clearError() {\r\n this.el.classList.remove('ux4g-upload-state-error');\r\n if (this.errorMsg) this.errorMsg.classList.add('ux4g-d-none');\r\n if (this.errorText) this.errorText.textContent = '';\r\n this._clearErrorRows();\r\n }\r\n\r\n _clearErrorRows() {\r\n if (!this.fileList) return;\r\n this.fileList.querySelectorAll('.ux4g-upload-file-item-error[data-ux-upload-error-row=\"true\"]').forEach(item => item.remove());\r\n }\r\n\r\n _validate(file) {\r\n const parts = file.name.split('.');\r\n const ext = parts.length > 1 ? `.${parts.pop().toLowerCase()}` : '';\r\n if (this.accept.length && !this.accept.includes(ext)) {\r\n return `File type not allowed: ${ext || 'unknown'}`;\r\n }\r\n if (file.size > this.maxSizeMB * 1024 * 1024) {\r\n return `File too large. Max size: ${this.maxSizeMB} MB`;\r\n }\r\n return null;\r\n }\r\n\r\n _addFiles(incoming) {\r\n let errorOccurred = false;\r\n\r\n incoming.forEach(file => {\r\n const err = this._validate(file);\r\n if (err) {\r\n errorOccurred = true;\r\n this._showError(err, file);\r\n U.dispatch(this.el, 'ux4g.upload.error', { file, reason: err });\r\n return;\r\n }\r\n\r\n this.files.push(file);\r\n this._renderFile(file);\r\n U.dispatch(this.el, 'ux4g.upload.added', { file });\r\n });\r\n\r\n if (!errorOccurred) this._clearError();\r\n this._syncHasFiles();\r\n this.input.value = '';\r\n }\r\n\r\n _renderFile(file) {\r\n if (!this.fileList) return;\r\n\r\n const sizeKB = file.size / 1024;\r\n const sizeLabel = sizeKB >= 1024\r\n ? `${(sizeKB / 1024).toFixed(1)} MB`\r\n : `${Math.max(1, Math.round(sizeKB))} KB`;\r\n\r\n const li = document.createElement('li');\r\n li.className = 'ux4g-upload-file-item';\r\n li.setAttribute('role', 'listitem');\r\n li.dataset.fileName = file.name;\r\n li.innerHTML = `\r\n <div class=\"ux4g-upload-file-row\">\r\n <span class=\"ux4g-upload-file-leading\" aria-hidden=\"true\">\r\n <span class=\"ux4g-icon-outlined ux4g-upload-file-icon\">token</span>\r\n </span>\r\n <span class=\"ux4g-upload-file-copy\">\r\n <span class=\"ux4g-body-m-strong ux4g-upload-file-name\">${this._escape(file.name)}</span>\r\n <span class=\"ux4g-body-s-default ux4g-upload-file-description\">${sizeLabel}</span>\r\n </span>\r\n <span class=\"ux4g-upload-file-statusbox\" aria-hidden=\"true\">\r\n <span class=\"ux4g-icon-outlined ux4g-upload-file-status\">done</span>\r\n </span>\r\n <button type=\"button\" class=\"ux4g-upload-file-remove\" aria-label=\"Remove ${this._escape(file.name)}\">\r\n <span class=\"ux4g-icon-outlined\" aria-hidden=\"true\">close</span>\r\n </button>\r\n </div>\r\n `;\r\n this.fileList.appendChild(li);\r\n }\r\n\r\n _renderErrorFile(file, reason) {\r\n if (!this.fileList) return;\r\n\r\n const label = file && file.name ? file.name : 'Document_name.pdf';\r\n const li = document.createElement('li');\r\n li.className = 'ux4g-upload-file-item ux4g-upload-file-item-error';\r\n li.setAttribute('role', 'listitem');\r\n li.dataset.uploadErrorRow = 'true';\r\n li.dataset.errorReason = reason || '';\r\n li.innerHTML = `\r\n <div class=\"ux4g-upload-file-row\">\r\n <span class=\"ux4g-upload-file-leading\" aria-hidden=\"true\">\r\n <span class=\"ux4g-icon-outlined ux4g-upload-file-icon\">error_outline</span>\r\n </span>\r\n <span class=\"ux4g-upload-file-copy\">\r\n <span class=\"ux4g-body-m-strong ux4g-upload-file-name\">${this._escape(label)}</span>\r\n <span class=\"ux4g-body-s-default ux4g-upload-file-description\">${this._escape(reason || 'Description')}</span>\r\n </span>\r\n <button type=\"button\" class=\"ux4g-upload-file-retry\" aria-label=\"Retry upload\">\r\n <span class=\"ux4g-icon-outlined\" aria-hidden=\"true\">replay</span>\r\n <span class=\"ux4g-label-l-default\">Retry</span>\r\n </button>\r\n </div>\r\n `;\r\n this.fileList.appendChild(li);\r\n }\r\n\r\n _removeFile(item) {\r\n const name = item && item.dataset.fileName;\r\n this.files = this.files.filter(f => f.name !== name);\r\n if (item) item.remove();\r\n this._syncHasFiles();\r\n U.dispatch(this.el, 'ux4g.upload.removed', { name });\r\n }\r\n\r\n _syncHasFiles() {\r\n const hasSuccessfulRows = this.fileList && this.fileList.querySelector('.ux4g-upload-file-item:not(.ux4g-upload-file-item-error)');\r\n const hasErrorRows = this.fileList && this.fileList.querySelector('.ux4g-upload-file-item-error');\r\n const hasFiles = this.files.length > 0 || !!hasSuccessfulRows || !!hasErrorRows;\r\n if (this.fileList) this.fileList.classList.toggle('ux4g-d-none', !hasFiles);\r\n if (this.moreButton) this.moreButton.classList.toggle('ux4g-d-none', !(this.files.length > 0 || !!hasSuccessfulRows));\r\n this._deriveBaseState(hasFiles);\r\n }\r\n\r\n _syncInitialState() {\r\n if (this.fileList) {\r\n this.files = Array.from(this.fileList.querySelectorAll('.ux4g-upload-file-item:not(.ux4g-upload-file-item-error)'))\r\n .map(item => ({ name: item.dataset.fileName || item.textContent.trim(), size: 0 }));\r\n }\r\n if (this.el.classList.contains('ux4g-upload-state-error')) {\r\n if (this.errorMsg) this.errorMsg.classList.remove('ux4g-d-none');\r\n } else {\r\n this._clearError();\r\n }\r\n this._syncHasFiles();\r\n }\r\n\r\n _deriveBaseState(hasFiles) {\r\n const variant = U.data(this.el, 'variant', 'default');\r\n this.stateClasses.forEach(cls => {\r\n if (cls !== 'ux4g-upload-state-selecting' && cls !== 'ux4g-upload-state-error') {\r\n this.el.classList.remove(cls);\r\n }\r\n });\r\n\r\n if (this.el.classList.contains('ux4g-upload-state-error')) return;\r\n if (variant === 'scanning') {\r\n this.el.classList.add('ux4g-upload-state-scanning');\r\n return;\r\n }\r\n if (variant === 'default-vle') {\r\n this.el.classList.add(hasFiles ? 'ux4g-upload-state-uploaded-vle' : 'ux4g-upload-state-default-vle');\r\n return;\r\n }\r\n this.el.classList.add(hasFiles ? 'ux4g-upload-state-uploaded' : 'ux4g-upload-state-default');\r\n }\r\n\r\n _escape(str) {\r\n return String(str).replace(/[&<>\"']/g, c => ({ '&': '&', '<': '<', '>': '>', '\"': '"', \"'\": ''' }[c]));\r\n }\r\n\r\n static getOrCreate(el) {\r\n let inst = getI(el, 'upload');\r\n if (!inst) { inst = new Upload(el); setI(el, 'upload', inst); }\r\n return inst;\r\n }\r\n }\r\n\r\n // -----------------------------\r\n // OTP\r\n // -----------------------------\r\n class OtpInput {\r\n constructor(el) {\r\n this.el = el;\r\n this.group = U.qs('.ux4g-otp-group', el);\r\n this.sourceInput = U.qs('.ux4g-otp-source', el);\r\n this.resend = U.qs('[data-ux-otp-resend]', el);\r\n this.status = U.qs('[data-ux-otp-status]', el);\r\n this.helper = U.qs('[data-ux-otp-helper]', el);\r\n this.timerTargets = U.qsa('[data-ux-otp-timer]', el);\r\n this.state = U.data(el, 'state', 'default');\r\n this.count = Math.max(1, U.num(U.data(el, 'count', 0), 0) || 1);\r\n this.placeholder = this.sourceInput?.getAttribute('placeholder') || '-';\r\n this.demoErrorOnComplete = U.bool(U.data(el, 'demo-error-on-complete', 'false'), false);\r\n this._timerId = null;\r\n this._shakeTimer = null;\r\n this._completedByUser = false;\r\n this._observer = null;\r\n this._demoErrorTimer = null;\r\n\r\n this._renderInputs();\r\n this.inputs = U.qsa('.ux4g-otp-input', el);\r\n this.length = this.count;\r\n this._syncInputs();\r\n this._observeErrorState();\r\n this._bind();\r\n this._applyVisualFocus();\r\n this._startTimers();\r\n }\r\n\r\n _renderInputs() {\r\n if (!this.group) return;\r\n\r\n const digits = this._getDigits();\r\n this.group.replaceChildren();\r\n if (this.sourceInput) this.group.append(this.sourceInput);\r\n\r\n for (let index = 0; index < this.count; index += 1) {\r\n const slot = document.createElement('div');\r\n slot.className = 'ux4g-input ux4g-otp-slot';\r\n\r\n const input = document.createElement('input');\r\n input.className = 'ux4g-input-input ux4g-otp-input ux4g-body-m-default';\r\n input.type = 'text';\r\n input.setAttribute('aria-label', `Digit ${index + 1}`);\r\n\r\n if (digits[index]) {\r\n input.value = digits[index];\r\n } else {\r\n input.placeholder = this.placeholder;\r\n }\r\n\r\n this._syncInputTone(input);\r\n slot.append(input);\r\n this.group.append(slot);\r\n\r\n if (index < this.count - 1) {\r\n const separator = document.createElement('span');\r\n separator.className = 'ux4g-otp-separator ux4g-icon-outlined';\r\n separator.setAttribute('aria-hidden', 'true');\r\n separator.textContent = 'horizontal_rule';\r\n this.group.append(separator);\r\n }\r\n }\r\n }\r\n\r\n _getDigits() {\r\n return ((this.sourceInput?.value || '').replace(/\\D/g, '').slice(0, this.count)).split('');\r\n }\r\n\r\n _bind() {\r\n this.inputs.forEach((input, index) => {\r\n U.on(input, 'focus', () => this._setFocused(index));\r\n U.on(input, 'click', () => this._setFocused(index));\r\n U.on(input, 'input', e => this._onInput(e, index));\r\n U.on(input, 'keydown', e => this._onKeydown(e, index));\r\n });\r\n }\r\n\r\n _isInteractive() {\r\n return this.state === 'default' || this.state === 'partial-filled' || this.state === 'all-filled';\r\n }\r\n\r\n _syncInputs() {\r\n this.inputs.forEach((input, index) => {\r\n input.setAttribute('inputmode', 'numeric');\r\n input.setAttribute('pattern', '[0-9]*');\r\n input.setAttribute('autocomplete', index === 0 ? 'one-time-code' : 'off');\r\n input.setAttribute('maxlength', '1');\r\n input.placeholder = input.value ? '' : this.placeholder;\r\n this._syncInputTone(input);\r\n\r\n if (!this._isInteractive()) {\r\n input.setAttribute('readonly', 'readonly');\r\n input.setAttribute('tabindex', '-1');\r\n }\r\n\r\n if (this.state === 'locked-out') {\r\n input.setAttribute('disabled', 'disabled');\r\n }\r\n });\r\n }\r\n\r\n _syncInputTone(input) {\r\n input.classList.toggle('ux4g-title-m-strong', !!input.value);\r\n input.classList.toggle('ux4g-body-m-default', !input.value);\r\n }\r\n\r\n _setFocused(index) {\r\n this.inputs.forEach((input, inputIndex) => {\r\n input.closest('.ux4g-otp-slot')?.classList.toggle('ux4g-otp-focus', inputIndex === index);\r\n input.classList.toggle('ux4g-otp-caret', inputIndex === index && !input.value);\r\n input.placeholder = input.value ? '' : (inputIndex === index ? '' : this.placeholder);\r\n });\r\n }\r\n\r\n _syncFocusClass() {\r\n const active = this.inputs.findIndex(input => input === document.activeElement);\r\n if (active >= 0) this._setFocused(active);\r\n }\r\n\r\n _clearFocused() {\r\n this.inputs.forEach(input => {\r\n input.closest('.ux4g-otp-slot')?.classList.remove('ux4g-otp-focus');\r\n input.classList.remove('ux4g-otp-caret');\r\n input.placeholder = input.value ? '' : this.placeholder;\r\n });\r\n }\r\n\r\n _applyVisualFocus() {\r\n if (this.state === 'default') {\r\n this._setFocused(0);\r\n return;\r\n }\r\n\r\n if (this.state === 'partial-filled') {\r\n const emptyIndex = this.inputs.findIndex(input => !input.value);\r\n this._setFocused(emptyIndex >= 0 ? emptyIndex : this.inputs.length - 1);\r\n return;\r\n }\r\n\r\n if (this.state === 'all-filled') {\r\n this._setFocused(this.inputs.length - 1);\r\n return;\r\n }\r\n\r\n this._syncFocusClass();\r\n }\r\n\r\n _onInput(e, index) {\r\n const input = e.target;\r\n const value = (input.value || '').replace(/\\D/g, '').slice(-1);\r\n input.value = value;\r\n input.placeholder = value ? '' : this.placeholder;\r\n this._syncInputTone(input);\r\n this._clearDemoErrorState();\r\n\r\n this._syncSourceValue();\r\n\r\n if (!value) return;\r\n\r\n if (index < this.inputs.length - 1) {\r\n this.inputs[index + 1].focus();\r\n }\r\n\r\n this._updateStateFromValue();\r\n }\r\n\r\n _onKeydown(e, index) {\r\n const input = e.target;\r\n\r\n if (e.key === 'Backspace') {\r\n if (input.value) {\r\n input.value = '';\r\n input.placeholder = this.placeholder;\r\n this._syncInputTone(input);\r\n this._clearDemoErrorState();\r\n this._syncSourceValue();\r\n this._completedByUser = false;\r\n this._updateStateFromValue();\r\n return;\r\n }\r\n\r\n if (index > 0) {\r\n const prev = this.inputs[index - 1];\r\n prev.value = '';\r\n prev.placeholder = this.placeholder;\r\n this._syncInputTone(prev);\r\n prev.focus();\r\n this._clearDemoErrorState();\r\n this._syncSourceValue();\r\n this._completedByUser = false;\r\n this._updateStateFromValue();\r\n }\r\n return;\r\n }\r\n\r\n if (e.key === 'ArrowLeft' && index > 0) {\r\n e.preventDefault();\r\n this.inputs[index - 1].focus();\r\n return;\r\n }\r\n\r\n if (e.key === 'ArrowRight' && index < this.inputs.length - 1) {\r\n e.preventDefault();\r\n this.inputs[index + 1].focus();\r\n }\r\n }\r\n\r\n _syncSourceValue() {\r\n if (!this.sourceInput) return;\r\n this.sourceInput.value = this.inputs.map(input => input.value).join('');\r\n }\r\n\r\n _updateStateFromValue() {\r\n if (!this._isInteractive()) return;\r\n const filled = this.inputs.filter(input => input.value).length;\r\n this._completedByUser = filled === this.length;\r\n const next = filled === 0 ? 'default' : (filled === this.length ? 'all-filled' : 'partial-filled');\r\n this.el.setAttribute('data-ux-state', next);\r\n if (this.demoErrorOnComplete && this._completedByUser) {\r\n this._scheduleDemoErrorState();\r\n }\r\n }\r\n\r\n _isErrorState() {\r\n return this.el.classList.contains('ux4g-otp-error') || U.data(this.el, 'state', '') === 'error';\r\n }\r\n\r\n _triggerShakeIfError() {\r\n if (!this.group) return;\r\n if (!this._completedByUser || this.inputs.some(input => !input.value) || !this._isErrorState()) return;\r\n\r\n this.group.classList.remove('ux4g-otp-shake');\r\n void this.group.offsetWidth;\r\n this.group.classList.add('ux4g-otp-shake');\r\n\r\n if (this._shakeTimer) global.clearTimeout(this._shakeTimer);\r\n this._shakeTimer = global.setTimeout(() => {\r\n this.group?.classList.remove('ux4g-otp-shake');\r\n this._shakeTimer = null;\r\n }, 400);\r\n }\r\n\r\n _observeErrorState() {\r\n this._observer = new MutationObserver(() => this._triggerShakeIfError());\r\n this._observer.observe(this.el, {\r\n attributes: true,\r\n attributeFilter: ['class', 'data-ux-state']\r\n });\r\n }\r\n\r\n _scheduleDemoErrorState() {\r\n if (this._demoErrorTimer) global.clearTimeout(this._demoErrorTimer);\r\n this._demoErrorTimer = global.setTimeout(() => {\r\n if (!this._completedByUser || this.inputs.some(input => !input.value)) return;\r\n this._clearFocused();\r\n if (document.activeElement && this.inputs.includes(document.activeElement)) {\r\n document.activeElement.blur();\r\n }\r\n this.el.classList.add('ux4g-otp-error');\r\n this.el.setAttribute('data-ux-state', 'error');\r\n this.el.setAttribute('aria-invalid', 'true');\r\n if (this.helper) {\r\n this.helper.outerHTML = '<span class=\"ux4g-otp-status\" data-ux-otp-status><span class=\"ux4g-icon-outlined\" aria-hidden=\"true\">error</span><span>Attempt 2 of 3</span></span>';\r\n this.helper = null;\r\n this.status = U.qs('[data-ux-otp-status]', this.el);\r\n }\r\n }, 300);\r\n }\r\n\r\n _clearDemoErrorState() {\r\n if (!this.demoErrorOnComplete) return;\r\n if (this._demoErrorTimer) {\r\n global.clearTimeout(this._demoErrorTimer);\r\n this._demoErrorTimer = null;\r\n }\r\n this.el.classList.remove('ux4g-otp-error');\r\n this.el.setAttribute('data-ux-state', this.inputs.some(input => input.value) ? 'partial-filled' : 'default');\r\n this.el.removeAttribute('aria-invalid');\r\n if (!this.helper && this.status) {\r\n this.status.outerHTML = '<span class=\"ux4g-otp-helper\" data-ux-otp-helper>Didn’t receive OTP?</span>';\r\n this.helper = U.qs('[data-ux-otp-helper]', this.el);\r\n this.status = null;\r\n }\r\n }\r\n\r\n _formatTime(totalSeconds) {\r\n const safe = Math.max(0, totalSeconds);\r\n const minutes = Math.floor(safe / 60);\r\n const seconds = safe % 60;\r\n return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;\r\n }\r\n\r\n _tickTimer(node) {\r\n const total = U.num(node.getAttribute('data-ux-otp-seconds'), 0);\r\n const prefix = node.getAttribute('data-ux-otp-prefix') || '';\r\n node.textContent = `${prefix}${this._formatTime(total)}`;\r\n if (total > 0) {\r\n node.setAttribute('data-ux-otp-seconds', String(total - 1));\r\n }\r\n }\r\n\r\n _startTimers() {\r\n if (!this.timerTargets.length) return;\r\n this.timerTargets.forEach(node => this._tickTimer(node));\r\n this._timerId = global.setInterval(() => {\r\n this.timerTargets.forEach(node => this._tickTimer(node));\r\n }, 1000);\r\n }\r\n\r\n static getOrCreate(el) {\r\n let inst = getI(el, 'otp');\r\n if (!inst) { inst = new OtpInput(el); setI(el, 'otp', inst); }\r\n return inst;\r\n }\r\n }\r\n\r\n // -----------------------------\r\n // SLA Progress\r\n // -----------------------------\r\n class SlaProgress {\r\n constructor(el) {\r\n this.el = el;\r\n this.valueTargets = U.qsa('.ux4g-sla-linear-value', el);\r\n this.circleValue = U.qs('.ux4g-sla-circle-value', el);\r\n this.circleMeta = U.qs('.ux4g-sla-circle-meta', el);\r\n this.sync();\r\n }\r\n\r\n sync() {\r\n const progress = Math.min(100, Math.max(0, U.num(U.data(this.el, 'progress', 0), 0)));\r\n this.el.style.setProperty('--ux4g-sla-progress', String(progress));\r\n\r\n if (this.el.hasAttribute('data-ux-sla-linear')) {\r\n this.valueTargets.forEach(node => {\r\n node.textContent = `${Math.round(progress)}%`;\r\n });\r\n this.el.setAttribute('aria-valuemin', '0');\r\n this.el.setAttribute('aria-valuemax', '100');\r\n this.el.setAttribute('aria-valuenow', String(Math.round(progress)));\r\n }\r\n\r\n if (this.el.hasAttribute('data-ux-sla-circle')) {\r\n const days = U.data(this.el, 'days', null);\r\n if (days != null && this.circleValue) {\r\n this.circleValue.textContent = String(days);\r\n }\r\n if (this.circleMeta) {\r\n this.circleMeta.textContent = Number(days) === 1 ? 'day left' : 'days left';\r\n }\r\n }\r\n }\r\n\r\n static getOrCreate(el) {\r\n let inst = getI(el, 'sla-progress');\r\n if (!inst) { inst = new SlaProgress(el); setI(el, 'sla-progress', inst); }\r\n return inst;\r\n }\r\n }\r\n\r\n class ProgressIndicator {\r\n constructor(el) {\r\n this.el = el;\r\n this.ensureStructure();\r\n this.labelTargets = U.qsa('[data-ux-progress-label]', el);\r\n this.descTargets = U.qsa('[data-ux-progress-desc]', el);\r\n this.endpointStart = U.qs('[data-ux-progress-start]', el);\r\n this.endpointEnd = U.qs('[data-ux-progress-end]', el);\r\n this.halfTrackTail = U.qs('.ux4g-progress-half-track-tail', el);\r\n this.halfRoundedProgress = U.qs('[data-ux-progress-half-svg-progress]', el);\r\n this.halfRoundedTrack = U.qs('[data-ux-progress-half-svg-track]', el);\r\n this.halfRoundedTrackCap = U.qs('[data-ux-progress-half-svg-track-cap]', el);\r\n this.halfSharpProgress = U.qs('[data-ux-progress-half-svg-progress-sharp]', el);\r\n this.halfSharpTrack = U.qs('[data-ux-progress-half-svg-track-sharp]', el);\r\n this.sync();\r\n }\r\n\r\n ensureStructure() {\r\n if (!this.el.hasAttribute('data-ux-progress-half')) return;\r\n if (U.qs('.ux4g-progress-half-arc', this.el)) return;\r\n\r\n const size = (this.el.getAttribute('data-ux-size') || 'm').toLowerCase();\r\n const shape = this.el.getAttribute('data-ux-shape') || 'sharp';\r\n const config = {\r\n s: { width: 80, height: 80, radius: 30, stroke: 10, roundedStroke: 8, labelClass: 'ux4g-label-l-strong', descriptionClass: 'ux4g-body-xs-default', endpoints: false },\r\n m: { width: 160, height: 160, radius: 70, stroke: 16, roundedStroke: 16, labelClass: 'ux4g-heading-m-strong', descriptionClass: 'ux4g-body-xs-default', endpoints: true },\r\n l: { width: 200, height: 200, radius: 90, stroke: 20, roundedStroke: 20, labelClass: 'ux4g-heading-m-strong', descriptionClass: 'ux4g-body-xs-default', endpoints: true },\r\n xl: { width: 240, height: 240, radius: 105, stroke: 24, roundedStroke: 24, labelClass: 'ux4g-heading-xl-strong', descriptionClass: 'ux4g-body-s-default', endpoints: true }\r\n }[size] || { width: 160, height: 160, radius: 70, stroke: 16, roundedStroke: 16, labelClass: 'ux4g-heading-m-strong', descriptionClass: 'ux4g-body-xs-default', endpoints: true };\r\n\r\n if (!this.el.hasAttribute('data-ux-radius')) {\r\n this.el.setAttribute('data-ux-radius', String(config.radius));\r\n }\r\n\r\n const description = this.el.getAttribute('data-ux-description') || 'Description';\r\n const start = this.el.getAttribute('data-ux-start-label') || '0%';\r\n const end = this.el.getAttribute('data-ux-end-label') || '100%';\r\n\r\n const roundedArcMarkup = `\r\n <svg class=\"ux4g-progress-half-svg\" viewBox=\"0 0 ${config.width} ${config.height}\" aria-hidden=\"true\" focusable=\"false\">\r\n <defs>\r\n <linearGradient id=\"ux4g-progress-half-gradient-${size}\" x1=\"0\" y1=\"${config.height / 2}\" x2=\"${config.width}\" y2=\"${config.height / 2}\" gradientUnits=\"userSpaceOnUse\">\r\n <stop offset=\"0%\" stop-color=\"var(--ux4g-progress-fill-start)\" />\r\n <stop offset=\"100%\" stop-color=\"var(--ux4g-progress-fill-end)\" />\r\n </linearGradient>\r\n </defs>\r\n <path class=\"ux4g-progress-half-svg-track\" d=\"${progressHalfRoundedArcPath(config.width, config.roundedStroke)}\" data-ux-progress-half-svg-track></path>\r\n <path class=\"ux4g-progress-half-svg-progress\" d=\"${progressHalfRoundedArcPath(config.width, config.roundedStroke)}\" pathLength=\"100\" data-ux-progress-half-svg-progress></path>\r\n <circle class=\"ux4g-progress-half-svg-track-cap\" cx=\"${config.width - (config.roundedStroke / 2)}\" cy=\"${config.height / 2}\" r=\"${config.roundedStroke / 2}\" data-ux-progress-half-svg-track-cap></circle>\r\n </svg>`;\r\n const sharpArcMarkup = `\r\n <svg class=\"ux4g-progress-half-svg ux4g-progress-half-svg-sharp\" viewBox=\"0 0 ${config.width} ${config.height}\" aria-hidden=\"true\" focusable=\"false\">\r\n <defs>\r\n <linearGradient id=\"ux4g-progress-half-gradient-sharp-${size}\" x1=\"0\" y1=\"${config.height / 2}\" x2=\"${config.width}\" y2=\"${config.height / 2}\" gradientUnits=\"userSpaceOnUse\">\r\n <stop offset=\"0%\" stop-color=\"var(--ux4g-progress-fill-start)\" />\r\n <stop offset=\"100%\" stop-color=\"var(--ux4g-progress-fill-end)\" />\r\n </linearGradient>\r\n </defs>\r\n <path class=\"ux4g-progress-half-svg-track ux4g-progress-half-svg-track-sharp\" d=\"${progressHalfRoundedArcPath(config.width, config.stroke)}\" data-ux-progress-half-svg-track-sharp></path>\r\n <path class=\"ux4g-progress-half-svg-progress ux4g-progress-half-svg-progress-sharp\" d=\"${progressHalfRoundedArcPath(config.width, config.stroke)}\" data-ux-progress-half-svg-progress-sharp></path>\r\n </svg>`;\r\n const arcMarkup = shape === 'rounded' ? roundedArcMarkup : sharpArcMarkup;\r\n\r\n this.el.innerHTML = `<div class=\"ux4g-progress-half-arc\" aria-hidden=\"true\">${arcMarkup}</div><div class=\"ux4g-progress-half-copy\"><span class=\"${config.labelClass}\" data-ux-progress-label>50%</span><p class=\"ux4g-progress-half-description ${config.descriptionClass}\" data-ux-progress-desc>${description}</p></div>${config.endpoints ? `<div class=\"ux4g-progress-half-endpoints\"><span class=\"ux4g-body-xs-default\" data-ux-progress-start>${start}</span><span class=\"ux4g-body-xs-default\" data-ux-progress-end>${end}</span></div>` : ''}`;\r\n }\r\n\r\n sync() {\r\n const progress = Math.min(100, Math.max(0, U.num(U.data(this.el, 'progress', 0), 0)));\r\n this.el.style.setProperty('--ux4g-progress-value', String(progress));\r\n\r\n this.labelTargets.forEach(node => {\r\n node.textContent = `${Math.round(progress)}%`;\r\n });\r\n\r\n if (this.el.hasAttribute('data-ux-progress-half')) {\r\n const radius = U.num(U.data(this.el, 'radius', 0), 0);\r\n this.el.style.setProperty('--ux4g-progress-half-angle', `${progress * 1.8}deg`);\r\n if (this.halfRoundedProgress) {\r\n const size = this.halfRoundedProgress.ownerSVGElement.viewBox.baseVal.width;\r\n const stroke = U.num(getComputedStyle(this.el).getPropertyValue('--ux4g-progress-half-stroke').replace('px', ''), 0);\r\n this.halfRoundedProgress.setAttribute('d', progressHalfRoundedArcPath(size, stroke, 180, 180 + (progress * 1.8)));\r\n this.halfRoundedProgress.style.strokeDasharray = '';\r\n if (this.halfRoundedTrack) {\r\n if (progress >= 100) {\r\n this.halfRoundedTrack.setAttribute('d', '');\r\n } else {\r\n const trackStartAngle = progress <= 0 ? 180 : 180 + (progress * 1.8);\r\n this.halfRoundedTrack.setAttribute('d', progressHalfRoundedArcPath(size, stroke, trackStartAngle, 360));\r\n }\r\n }\r\n if (this.halfRoundedTrackCap) {\r\n this.halfRoundedTrackCap.style.display = progress < 100 ? 'block' : 'none';\r\n }\r\n }\r\n if (this.halfSharpProgress) {\r\n const size = this.halfSharpProgress.ownerSVGElement.viewBox.baseVal.width;\r\n const stroke = U.num(getComputedStyle(this.el).getPropertyValue('--ux4g-progress-half-stroke').replace('px', ''), 0);\r\n this.halfSharpProgress.setAttribute('d', progressHalfRoundedArcPath(size, stroke, 180, 180 + (progress * 1.8)));\r\n if (this.halfSharpTrack) {\r\n if (progress >= 100) {\r\n this.halfSharpTrack.setAttribute('d', '');\r\n } else {\r\n const trackStartAngle = progress <= 0 ? 180 : 180 + (progress * 1.8);\r\n this.halfSharpTrack.setAttribute('d', progressHalfRoundedArcPath(size, stroke, trackStartAngle, 360));\r\n }\r\n }\r\n }\r\n if (this.halfTrackTail) {\r\n this.halfTrackTail.hidden = true;\r\n }\r\n }\r\n\r\n const description = this.el.getAttribute('data-ux-description');\r\n if (description) {\r\n this.descTargets.forEach(node => {\r\n node.textContent = description;\r\n });\r\n }\r\n\r\n const start = this.el.getAttribute('data-ux-start-label');\r\n const end = this.el.getAttribute('data-ux-end-label');\r\n if (this.endpointStart && start != null) this.endpointStart.textContent = start;\r\n if (this.endpointEnd && end != null) this.endpointEnd.textContent = end;\r\n\r\n this.el.setAttribute('aria-valuemin', '0');\r\n this.el.setAttribute('aria-valuemax', '100');\r\n this.el.setAttribute('aria-valuenow', String(Math.round(progress)));\r\n }\r\n\r\n static getOrCreate(el) {\r\n let inst = getI(el, 'progress-indicator');\r\n if (!inst) { inst = new ProgressIndicator(el); setI(el, 'progress-indicator', inst); }\r\n return inst;\r\n }\r\n }\r\n\r\n function progressCircleLabelClass(size, placement) {\r\n if (placement === 'outside') return 'ux4g-label-xl-strong';\r\n if (size === 'xs' || size === 's' || size === 'm') return 'ux4g-label-m-strong';\r\n if (size === 'l') return 'ux4g-label-l-strong';\r\n return 'ux4g-label-xl-strong';\r\n }\r\n\r\n function progressCircleDescriptionClass(size, placement) {\r\n if (placement === 'outside') return 'ux4g-body-s-default';\r\n return (size === 'xl' || size === '2xl' || size === '3xl') ? 'ux4g-body-s-default' : 'ux4g-body-xs-default';\r\n }\r\n\r\n function progressHalfLabelClass(size) {\r\n return size === 's' ? 'ux4g-label-l-strong' : 'ux4g-heading-m-strong';\r\n }\r\n\r\n function progressHalfRoundedArcPath(size, stroke, startAngle = 180, endAngle = 360) {\r\n const radius = (size / 2) - (stroke / 2);\r\n const center = size / 2;\r\n const start = polarPoint(center, radius, startAngle);\r\n const end = polarPoint(center, radius, endAngle);\r\n const largeArcFlag = Math.abs(endAngle - startAngle) > 180 ? 1 : 0;\r\n return `M ${start.x} ${start.y} A ${radius} ${radius} 0 ${largeArcFlag} 1 ${end.x} ${end.y}`;\r\n }\r\n\r\n function polarPoint(center, radius, angleDeg) {\r\n const angle = (angleDeg * Math.PI) / 180;\r\n return {\r\n x: center + (radius * Math.cos(angle)),\r\n y: center + (radius * Math.sin(angle))\r\n };\r\n }\r\n\r\n function buildProgressBarDemo(shape, placement, progress) {\r\n const inside = placement === 'inside';\r\n return `<article class=\"ux4g-progress-bar\" data-ux-progress-bar data-ux-shape=\"${shape}\" data-ux-label-placement=\"${placement}\" data-ux-progress=\"${progress}\" role=\"progressbar\" aria-label=\"${shape} ${placement} bar ${progress} percent\"><div class=\"ux4g-progress-bar-track\"><div class=\"ux4g-progress-bar-fill\">${inside ? `<span class=\"ux4g-progress-bar-label ux4g-progress-bar-label-inside ux4g-label-s-strong\" data-ux-progress-label>${progress}%</span>` : ''}</div></div>${inside ? '' : `<span class=\"ux4g-progress-bar-label ux4g-progress-bar-label-outside ux4g-label-s-strong\" data-ux-progress-label>${progress}%</span>`}</article>`;\r\n }\r\n\r\n function buildProgressCircleDemo(shape, size, placement) {\r\n const labelClass = progressCircleLabelClass(size, placement);\r\n const showInsideDescription = placement === 'inside' && (size === 'xl' || size === '2xl' || size === '3xl');\r\n const descClass = progressCircleDescriptionClass(size, placement);\r\n return `<div class=\"ux4g-progress-size-demo\"><article class=\"ux4g-progress-circle\" data-ux-progress-circle data-ux-shape=\"${shape}\" data-ux-size=\"${size}\" data-ux-label-placement=\"${placement}\" data-ux-progress=\"50\" ${(placement === 'outside' || showInsideDescription) ? 'data-ux-description=\"Description\"' : ''} role=\"progressbar\" aria-label=\"${size} ${shape} circle ${placement} 50 percent\"><div class=\"ux4g-progress-circle-indicator\"><span class=\"ux4g-progress-circle-ring\"></span>${placement === 'inside' ? `<div class=\"ux4g-progress-circle-value-wrap\"><span class=\"${labelClass}\" data-ux-progress-label>50%</span>${showInsideDescription ? `<p class=\"ux4g-progress-circle-description ${descClass}\" data-ux-progress-desc>Description</p>` : ''}</div>` : ''}</div>${placement === 'outside' ? `<div class=\"ux4g-progress-circle-copy\"><span class=\"${labelClass}\" data-ux-progress-label>50%</span><p class=\"ux4g-progress-circle-description ${descClass}\" data-ux-progress-desc>Description</p></div>` : ''}</article></div>`;\r\n }\r\n\r\n function renderProgressIndicatorDemos(root = document) {\r\n const steps = [0, 10];\r\n const barContainers = [\r\n ['sharp-outside', 'sharp', 'outside'],\r\n ['rounded-outside', 'rounded', 'outside'],\r\n ['sharp-inside', 'sharp', 'inside'],\r\n ['rounded-inside', 'rounded', 'inside']\r\n ];\r\n barContainers.forEach(([key, shape, placement]) => {\r\n const container = root.querySelector(`[data-ux-progress-demo-bars=\"${key}\"]`);\r\n if (!container) return;\r\n container.innerHTML = steps.map(progress => buildProgressBarDemo(shape, placement, progress)).join('');\r\n });\r\n\r\n const circleSizes = ['xs', 's', 'm', 'l', 'xl', '2xl', '3xl'];\r\n const circleHeadings = root.querySelector('[data-ux-progress-demo-circle-headings]');\r\n if (circleHeadings) circleHeadings.innerHTML = '';\r\n const circleRows = [\r\n ['sharp-inside', 'sharp', 'inside'],\r\n ['sharp-outside', 'sharp', 'outside'],\r\n ['rounded-inside', 'rounded', 'inside'],\r\n ['rounded-outside', 'rounded', 'outside']\r\n ];\r\n circleRows.forEach(([key, shape, placement]) => {\r\n const row = root.querySelector(`[data-ux-progress-demo-circles=\"${key}\"]`);\r\n if (!row) return;\r\n row.innerHTML = circleSizes.map(size => buildProgressCircleDemo(shape, size, placement)).join('');\r\n });\r\n\r\n }\r\n\r\n // -----------------------------\r\n // Data API init\r\n // -----------------------------\r\n function init(root = document) {\r\n renderProgressIndicatorDemos(root);\r\n\r\n // Dropdown\r\n U.qsa('[data-bs-toggle=\"dropdown\"],[data-ux-toggle=\"dropdown\"]', root).forEach(Dropdown.getOrCreate);\r\n\r\n // Collapse\r\n U.qsa('[data-bs-toggle=\"collapse\"],[data-ux-toggle=\"collapse\"],[ux4g-toggle=\"collapse\"]', root).forEach(Collapse.getOrCreate);\r\n\r\n // Tabs\r\n U.qsa('[data-bs-toggle=\"tab\"],[data-ux-toggle=\"tab\"]', root).forEach(Tab.getOrCreate);\r\n\r\n // Tooltips / Popovers\r\n U.qsa('[data-bs-toggle=\"tooltip\"],[data-ux-toggle=\"tooltip\"]', root).forEach(el => Floating.getOrCreate(el, \"tooltip\"));\r\n U.qsa('[data-bs-toggle=\"popover\"],[data-ux-toggle=\"popover\"]', root).forEach(el => Floating.getOrCreate(el, \"popover\"));\r\n\r\n // Toasts (wires dismiss; does not auto-show unless .show is already present)\r\n U.qsa(\".toast\", root).forEach(Toast.getOrCreate);\r\n\r\n // Carousels\r\n U.qsa(\".carousel\", root).forEach(Carousel.getOrCreate);\r\n\r\n // Scrollspy\r\n U.qsa('[data-bs-spy=\"scroll\"],[data-ux-spy=\"scroll\"]', root).forEach(ScrollSpy.getOrCreate);\r\n\r\n // Table Interactions\r\n U.qsa(\".ux4g-table\", root).forEach(Table.getOrCreate);\r\n\r\n // List Interactions\r\n U.qsa(\".ux4g-list\", root).forEach(List.getOrCreate);\r\n\r\n // Upload\r\n U.qsa('[data-ux-upload]', root).forEach(Upload.getOrCreate);\r\n\r\n // OTP\r\n U.qsa('[data-ux-otp]', root).forEach(OtpInput.getOrCreate);\r\n\r\n // SLA Progress\r\n U.qsa('[data-ux-sla-circle],[data-ux-sla-linear]', root).forEach(SlaProgress.getOrCreate);\r\n\r\n // Progress Indicators\r\n U.qsa('[data-ux-progress-bar],[data-ux-progress-circle],[data-ux-progress-half]', root).forEach(ProgressIndicator.getOrCreate);\r\n\r\n // Validation: Ensure ux4g-multiselect-list ID is only used on .ux4g-list elements\r\n const multiselectIdEls = root.querySelectorAll('#ux4g-multiselect-list');\r\n multiselectIdEls.forEach(el => {\r\n if (!el.classList.contains('ux4g-list')) {\r\n console.warn(`[UX4G Validation] The ID 'ux4g-multiselect-list' should only be used on elements with the 'ux4g-list' class. Found on:`, el);\r\n }\r\n });\r\n }\r\n\r\n // Delegated toggles for Modal & Offcanvas (+ your custom open/close classes)\r\n U.on(document, \"click\", (e) => {\r\n // UX4G modal toggle\r\n const modalBtn = U.closest(e.target, '[data-bs-toggle=\"modal\"],[data-ux-toggle=\"modal\"]');\r\n if (modalBtn) {\r\n e.preventDefault();\r\n const sel = U.data(modalBtn, \"target\") || U.attr(modalBtn, \"href\");\r\n const modalEl = sel && sel.startsWith(\"#\") ? U.qs(sel) : null;\r\n if (modalEl) Modal.getOrCreate(modalEl).toggle(modalBtn);\r\n return;\r\n }\r\n\r\n // YOUR custom modal open (.open-modal) — assumes #exampleModal (your HTML)\r\n const openModal = U.closest(e.target, \".open-modal\");\r\n if (openModal) {\r\n e.preventDefault();\r\n const modalEl = U.qs(\"#exampleModal\");\r\n if (modalEl) Modal.getOrCreate(modalEl).show(openModal);\r\n return;\r\n }\r\n\r\n // UX4G offcanvas toggle\r\n const offBtn = U.closest(e.target, '[data-bs-toggle=\"offcanvas\"],[data-ux-toggle=\"offcanvas\"]');\r\n if (offBtn) {\r\n e.preventDefault();\r\n const sel = U.data(offBtn, \"target\") || U.attr(offBtn, \"href\");\r\n const el = sel && sel.startsWith(\"#\") ? U.qs(sel) : null;\r\n if (el) Offcanvas.getOrCreate(el).toggle(offBtn);\r\n return;\r\n }\r\n\r\n // Toast close class bridge (your HTML)\r\n const closeToast = U.closest(e.target, \".close-toast\");\r\n if (closeToast) {\r\n e.preventDefault();\r\n const toastEl = U.closest(closeToast, \".toast\");\r\n if (toastEl) Toast.getOrCreate(toastEl).hide();\r\n return;\r\n }\r\n\r\n // Modal close class bridge (your HTML)\r\n const closeModal = U.closest(e.target, \".close-modal\");\r\n if (closeModal) {\r\n e.preventDefault();\r\n const modalEl = U.closest(closeModal, \".modal\");\r\n if (modalEl) Modal.getOrCreate(modalEl).hide();\r\n return;\r\n }\r\n });\r\n\r\n // Switch keyboard support (Enter to toggle)\r\n U.on(document, \"keydown\", (e) => {\r\n if (e.key === \"Enter\") {\r\n const input = U.closest(e.target, \".ux4g-switch-input\");\r\n if (input && !input.disabled) {\r\n e.preventDefault();\r\n input.checked = !input.checked;\r\n input.dispatchEvent(new Event(\"change\", { bubbles: true }));\r\n }\r\n }\r\n });\r\n\r\n // Your demo: #liveToastBtn shows #liveToast\r\n U.on(document, \"DOMContentLoaded\", () => {\r\n const btn = U.qs(\"#liveToastBtn\");\r\n if (btn) {\r\n U.on(btn, \"click\", () => {\r\n const toastEl = U.qs(\"#liveToast\");\r\n if (toastEl) Toast.getOrCreate(toastEl).show();\r\n });\r\n }\r\n });\r\n\r\n U.on(document, \"ux4g.upload.error\", (e) => {\r\n if (typeof global.showContextAlert === \"function\") {\r\n global.showContextAlert(\"top-right\", \"error\", \"Upload Failed\", e.detail.reason);\r\n }\r\n });\r\n\r\n // Auto-init\r\n if (document.readyState === \"loading\") {\r\n U.on(document, \"DOMContentLoaded\", () => init(document));\r\n } else {\r\n init(document);\r\n }\r\n\r\n // Expose API\r\n global.ux4g = {\r\n version: \"1.1.0\",\r\n U,\r\n getI,\r\n setI,\r\n escapeHtml,\r\n init,\r\n Dropdown,\r\n Collapse,\r\n Modal,\r\n Offcanvas,\r\n Toast,\r\n Carousel,\r\n Tab,\r\n ScrollSpy,\r\n Table,\r\n List,\r\n Upload,\r\n OtpInput,\r\n Theme: {\r\n get() {\r\n return document.documentElement.getAttribute(\"data-theme\") || (window.matchMedia(\"(prefers-color-scheme: dark)\").matches ? \"dark\" : \"light\");\r\n },\r\n set(theme) {\r\n document.documentElement.setAttribute(\"data-theme\", theme);\r\n window.dispatchEvent(new CustomEvent(\"ux4g.theme.changed\", { detail: { theme } }));\r\n },\r\n toggle() {\r\n const next = this.get() === \"dark\" ? \"light\" : \"dark\";\r\n this.set(next);\r\n }\r\n }\r\n };\r\n\r\n // Auto-init theme if not set\r\n U.on(document, \"DOMContentLoaded\", () => {\r\n if (!document.documentElement.hasAttribute(\"data-theme\")) {\r\n const preferred = window.matchMedia(\"(prefers-color-scheme: dark)\").matches ? \"dark\" : \"light\";\r\n document.documentElement.setAttribute(\"data-theme\", preferred);\r\n }\r\n });\r\n\r\nconst Backdrop = (() => {\r\n let count = 0;\r\n\r\n function create() {\r\n const bd = document.createElement(\"div\");\r\n bd.className = \"modal-backdrop fade\";\r\n document.body.appendChild(bd);\r\n // force reflow to enable transition\r\n bd.offsetHeight; // eslint-disable-line no-unused-expressions\r\n bd.classList.add(\"show\");\r\n return bd;\r\n }\r\n\r\n function show(onClick) {\r\n count += 1;\r\n const bd = create();\r\n if (typeof onClick === \"function\") {\r\n bd.addEventListener(\"click\", onClick);\r\n }\r\n return bd;\r\n }\r\n\r\n function hide(bd, duration = 250) {\r\n if (!bd) return;\r\n bd.classList.remove(\"show\");\r\n\r\n // Remove after transition\r\n window.setTimeout(() => {\r\n try { bd.remove(); } catch (_) {}\r\n }, duration);\r\n }\r\n\r\n function dec() {\r\n count = Math.max(0, count - 1);\r\n return count;\r\n }\r\n\r\n return { show, hide, dec, get count() { return count; } };\r\n})();\r\n\r\n\r\n})(typeof window !== \"undefined\" ? window : this);\r\n\r\n\r\n";
|
|
35
|
+
document.head.appendChild(script1);
|
|
36
|
+
// Inject ux4g-custom.js (extends window.ux4g with custom behaviors)
|
|
37
|
+
const script2 = document.createElement('script');
|
|
38
|
+
script2.setAttribute('data-ux4g-runtime', 'custom');
|
|
39
|
+
script2.textContent = "/* ========================================================= tooltips js ========================================================= */\r\n(function (ux4g) {\r\n if (!ux4g) return;\r\n const U = ux4g.U;\r\n const getI = ux4g.getI;\r\n const setI = ux4g.setI;\r\n const escapeHtml = ux4g.escapeHtml;\r\n\r\n class Floating {\r\n constructor(el, kind) {\r\n this.el = el;\r\n this.kind = kind; // tooltip | popover\r\n this._open = false;\r\n this._floating = null;\r\n\r\n this.placement = U.data(el, \"placement\", kind === \"tooltip\" ? \"top\" : \"right\");\r\n this.offset = U.num(U.data(el, \"offset\", 8), 8);\r\n this.trigger = U.data(el, \"trigger\", kind === \"tooltip\" ? \"hover focus\" : \"click\");\r\n this.html = U.bool(U.data(el, \"html\", \"false\"), false);\r\n\r\n this._bind();\r\n }\r\n\r\n _getContent() {\r\n const content = U.data(this.el, \"content\");\r\n if (content != null) return this.html ? String(content) : escapeHtml(content);\r\n\r\n if (this.kind === \"popover\") {\r\n const title = U.data(this.el, \"title\") || this.el.getAttribute(\"title\") || \"\";\r\n const body = this.el.getAttribute(\"data-content\") || \"\";\r\n const t = this.html ? String(title) : escapeHtml(title);\r\n const b = this.html ? String(body) : escapeHtml(body);\r\n return `<div class=\"ux4g-popover-header\">${t}</div><div class=\"ux4g-popover-body\">${b}</div>`;\r\n }\r\n\r\n const t = this.el.getAttribute(\"title\") || \"\";\r\n return this.html ? String(t) : escapeHtml(t);\r\n }\r\n\r\n _create() {\r\n if (this._floating) return;\r\n const div = document.createElement(\"div\");\r\n div.className = this.kind === \"tooltip\" ? \"ux4g-tooltip\" : \"ux4g-popover\";\r\n div.setAttribute(\"role\", this.kind === \"tooltip\" ? \"tooltip\" : \"dialog\");\r\n div.innerHTML = this._getContent() || \"\";\r\n document.body.appendChild(div);\r\n this._floating = div;\r\n }\r\n\r\n show() {\r\n if (this._open) return;\r\n this._open = true;\r\n\r\n // Prevent native tooltip doubling\r\n if (this.kind === \"tooltip\") {\r\n const t = this.el.getAttribute(\"title\");\r\n if (t != null) {\r\n this.el.setAttribute(\"data-ux-original-title\", t);\r\n this.el.removeAttribute(\"title\");\r\n }\r\n }\r\n\r\n this._create();\r\n this._floating.style.display = \"block\";\r\n this._floating.classList.add(\"show\");\r\n U.placeFloating(this.el, this._floating, this.placement, this.offset);\r\n\r\n this._onWin = () => U.placeFloating(this.el, this._floating, this.placement, this.offset);\r\n U.on(window, \"scroll\", this._onWin, { passive: true });\r\n U.on(window, \"resize\", this._onWin);\r\n\r\n U.dispatch(this.el, `ux4g.${this.kind}.shown`, {});\r\n }\r\n\r\n hide() {\r\n if (!this._open) return;\r\n this._open = false;\r\n\r\n if (this._floating) {\r\n this._floating.classList.remove(\"show\");\r\n this._floating.style.display = \"none\";\r\n }\r\n\r\n if (this.kind === \"tooltip\") {\r\n const ot = this.el.getAttribute(\"data-ux-original-title\");\r\n if (ot != null) {\r\n this.el.setAttribute(\"title\", ot);\r\n this.el.removeAttribute(\"data-ux-original-title\");\r\n }\r\n }\r\n\r\n if (this._onWin) {\r\n U.off(window, \"scroll\", this._onWin);\r\n U.off(window, \"resize\", this._onWin);\r\n this._onWin = null;\r\n }\r\n\r\n U.dispatch(this.el, `ux4g.${this.kind}.hidden`, {});\r\n }\r\n\r\n toggle() { this._open ? this.hide() : this.show(); }\r\n\r\n _bind() {\r\n const triggers = String(this.trigger).split(/\\s+/).filter(Boolean);\r\n\r\n if (triggers.includes(\"hover\")) {\r\n U.on(this.el, \"mouseenter\", () => this.show());\r\n U.on(this.el, \"mouseleave\", () => this.hide());\r\n }\r\n if (triggers.includes(\"focus\")) {\r\n U.on(this.el, \"focus\", () => this.show());\r\n U.on(this.el, \"blur\", () => this.hide());\r\n }\r\n if (triggers.includes(\"click\")) {\r\n U.on(this.el, \"click\", (e) => { e.preventDefault(); this.toggle(); });\r\n\r\n U.on(document, \"click\", (e) => {\r\n if (!this._open) return;\r\n if (this.el.contains(e.target) || (this._floating && this._floating.contains(e.target))) return;\r\n this.hide();\r\n });\r\n\r\n U.on(document, \"keydown\", (e) => {\r\n if (!this._open) return;\r\n if (e.key === \"Escape\") this.hide();\r\n });\r\n }\r\n }\r\n\r\n static getOrCreate(el, kind) {\r\n const key = kind;\r\n let inst = getI(el, key);\r\n if (!inst) { inst = new Floating(el, kind); setI(el, key, inst); }\r\n return inst;\r\n }\r\n }\r\n\r\n function repositionManualTooltip(tooltip) {\r\n if (!tooltip || tooltip.dataset.uxAdjusted === \"true\") return;\r\n\r\n // We need to know where it *will* be once it's fully hovered\r\n const vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0);\r\n const vh = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0);\r\n\r\n const originalTransition = tooltip.style.transition;\r\n const originalDisplay = tooltip.style.display;\r\n const originalOpacity = tooltip.style.opacity;\r\n\r\n // Temporarily disable transition and force show to measure target state\r\n tooltip.style.transition = 'none';\r\n tooltip.style.display = 'flex';\r\n tooltip.style.opacity = '0';\r\n \r\n // Force reflow\r\n tooltip.offsetHeight; \r\n \r\n const rect = tooltip.getBoundingClientRect();\r\n\r\n let shiftX = 0;\r\n let shiftY = 0;\r\n\r\n // Check boundaries with some padding\r\n const padding = 18; \r\n if (rect.left < padding) {\r\n shiftX = padding - rect.left;\r\n } else if (rect.right > vw - padding) {\r\n shiftX = (vw - padding) - rect.right;\r\n }\r\n\r\n if (rect.top < padding) {\r\n shiftY = padding - rect.top;\r\n } else if (rect.bottom > vh - padding) {\r\n shiftY = (vh - padding) - rect.bottom;\r\n }\r\n\r\n if (shiftX !== 0 || shiftY !== 0) {\r\n const computedStyle = window.getComputedStyle(tooltip);\r\n const currentTransform = computedStyle.transform === 'none' ? '' : computedStyle.transform;\r\n // Apply the fix\r\n tooltip.style.transform = `${currentTransform} translate(${shiftX}px, ${shiftY}px)`;\r\n tooltip.dataset.uxAdjusted = \"true\";\r\n }\r\n\r\n // Restore state (but keep the transform)\r\n tooltip.style.display = originalDisplay;\r\n tooltip.style.opacity = originalOpacity;\r\n // Delay restoring transition slightly to avoid \"flying\" into place from old position\r\n setTimeout(() => {\r\n tooltip.style.transition = originalTransition;\r\n }, 50);\r\n }\r\n\r\n // Initialize\r\n document.addEventListener(\"DOMContentLoaded\", () => {\r\n // Data API init\r\n U.qsa('[data-bs-toggle=\"tooltip\"],[data-ux-toggle=\"tooltip\"]').forEach(el => Floating.getOrCreate(el, \"tooltip\"));\r\n U.qsa('[data-bs-toggle=\"popover\"],[data-ux-toggle=\"popover\"]').forEach(el => Floating.getOrCreate(el, \"popover\"));\r\n\r\n // Auto-fix manual tooltips on hover\r\n document.body.addEventListener('mouseover', (e) => {\r\n const wrapper = e.target.closest('.ux4g-tooltip-wrapper');\r\n if (wrapper) {\r\n const tooltip = wrapper.querySelector('.ux4g-tooltip');\r\n if (tooltip) repositionManualTooltip(tooltip);\r\n }\r\n });\r\n\r\n document.body.addEventListener('mouseout', (e) => {\r\n const wrapper = e.target.closest('.ux4g-tooltip-wrapper');\r\n if (wrapper && !wrapper.contains(e.relatedTarget)) {\r\n const tooltip = wrapper.querySelector('.ux4g-tooltip');\r\n if (tooltip) {\r\n tooltip.style.transform = '';\r\n delete tooltip.dataset.uxAdjusted;\r\n }\r\n }\r\n });\r\n });\r\n\r\n // Assign to global\r\n ux4g.Tooltip = { getOrCreate(el) { return Floating.getOrCreate(el, \"tooltip\"); } };\r\n ux4g.Popover = { getOrCreate(el) { return Floating.getOrCreate(el, \"popover\"); } };\r\n\r\n /**\r\n * Helper to ensure menus stay within viewport\r\n */\r\n ux4g.repositionMenu = function(container, menu) {\r\n if (!menu) return;\r\n \r\n // Reset positions to let CSS natural flow work first for measurement\r\n menu.style.top = \"\";\r\n menu.style.bottom = \"\";\r\n menu.style.left = \"\";\r\n menu.style.right = \"\";\r\n \r\n const vh = window.innerHeight;\r\n const vw = window.innerWidth;\r\n const menuRect = menu.getBoundingClientRect();\r\n const containerRect = container.getBoundingClientRect();\r\n \r\n // Vertical Flip\r\n if (menuRect.bottom > vh && containerRect.top > menuRect.height) {\r\n menu.style.top = \"auto\";\r\n menu.style.bottom = \"100%\";\r\n const offset = getComputedStyle(menu).getPropertyValue('--ux4g-dropdown-menu-offset-y') || \r\n getComputedStyle(menu).getPropertyValue('--ux4g-combobox-menu-offset-y') || '6px';\r\n menu.style.marginBottom = offset;\r\n }\r\n\r\n // Horizontal Shift\r\n const updatedRect = menu.getBoundingClientRect();\r\n if (updatedRect.right > vw) {\r\n menu.style.left = \"auto\";\r\n menu.style.right = \"0\";\r\n }\r\n if (updatedRect.left < 0) {\r\n menu.style.left = \"0\";\r\n menu.style.right = \"auto\";\r\n }\r\n };\r\n\r\n // Update open menus on resize/scroll\r\n window.addEventListener('resize', () => {\r\n document.querySelectorAll('.ux4g-dropdown.is-open, .ux4g-combobox.is-open, .ux4g-breadcrumb-dropdown .show + .show').forEach(el => {\r\n let container = el;\r\n let menu = el.querySelector('.ux4g-dropdown-menu, .ux4g-combobox-menu, .ux4g-breadcrumb-menu');\r\n if (el.classList.contains('ux4g-breadcrumb-toggle')) {\r\n container = el.parentElement;\r\n menu = container.querySelector('.ux4g-breadcrumb-menu');\r\n }\r\n if (container && menu) ux4g.repositionMenu(container, menu);\r\n });\r\n });\r\n\r\n /**\r\n * Common Filter Core Logic\r\n */\r\n ux4g.filterCore = {\r\n /**\r\n * @param {string} text - The item text to check\r\n * @param {string} query - The search query\r\n * @param {string} mode - 'contains', 'starts-with', or 'starts-with-term'\r\n */\r\n matches: function (text, query, mode) {\r\n if (!query) return true;\r\n const t = String(text).toLowerCase();\r\n const q = String(query).toLowerCase().trim();\r\n\r\n switch (mode) {\r\n case \"starts-with\":\r\n return t.startsWith(q);\r\n case \"starts-with-term\":\r\n // Matches if any word in the text starts with the query\r\n return t.split(/\\s+/).some((w) => w.startsWith(q));\r\n case \"contains\":\r\n default:\r\n return t.includes(q);\r\n }\r\n },\r\n\r\n /**\r\n * @param {string} originalText - The text to highlight\r\n * @param {string} query - The search query\r\n * @param {string} mode - 'contains', 'starts-with', or 'starts-with-term'\r\n */\r\n highlight: function (originalText, query, mode) {\r\n if (!query) return originalText;\r\n const q = query.trim();\r\n const escaped = q.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\r\n\r\n let regex;\r\n switch (mode) {\r\n case \"starts-with\":\r\n regex = new RegExp(`^(${escaped})`, \"i\");\r\n break;\r\n case \"starts-with-term\":\r\n regex = new RegExp(`\\\\b(${escaped})`, \"i\");\r\n break;\r\n case \"contains\":\r\n default:\r\n regex = new RegExp(`(${escaped})`, \"i\");\r\n break;\r\n }\r\n\r\n return originalText.replace(regex, \"<strong>$1</strong>\");\r\n }\r\n };\r\n\r\n})(window.ux4g);\r\n\r\n/* ========================================================= breadcrumb js ========================================================= */\r\ndocument.addEventListener(\"DOMContentLoaded\", () => {\r\n const dropdowns = document.querySelectorAll(\".ux4g-breadcrumb-dropdown\");\r\n if (!dropdowns.length) return;\r\n\r\n const closeDropdown = (dropdown) => {\r\n const toggle = dropdown.querySelector(\".ux4g-breadcrumb-toggle\");\r\n const menu = dropdown.querySelector(\".ux4g-breadcrumb-menu\");\r\n\r\n if (!toggle || !menu) return;\r\n\r\n toggle.classList.remove(\"show\");\r\n menu.classList.remove(\"show\");\r\n toggle.setAttribute(\"aria-expanded\", \"false\");\r\n };\r\n\r\n const openDropdown = (dropdown) => {\r\n const toggle = dropdown.querySelector(\".ux4g-breadcrumb-toggle\");\r\n const menu = dropdown.querySelector(\".ux4g-breadcrumb-menu\");\r\n\r\n if (!toggle || !menu) return;\r\n\r\n toggle.classList.add(\"show\");\r\n menu.classList.add(\"show\");\r\n toggle.setAttribute(\"aria-expanded\", \"true\"); \r\n if (window.ux4g && window.ux4g.repositionMenu) {\r\n window.ux4g.repositionMenu(dropdown, menu);\r\n }\r\n };\r\n\r\n dropdowns.forEach((dropdown) => {\r\n const toggle = dropdown.querySelector(\".ux4g-breadcrumb-toggle\");\r\n const menu = dropdown.querySelector(\".ux4g-breadcrumb-menu\");\r\n\r\n if (!toggle || !menu) return;\r\n\r\n toggle.addEventListener(\"click\", (e) => {\r\n e.preventDefault();\r\n e.stopPropagation();\r\n\r\n const isOpen = menu.classList.contains(\"show\");\r\n\r\n dropdowns.forEach((d) => closeDropdown(d));\r\n\r\n if (!isOpen) openDropdown(dropdown);\r\n });\r\n\r\n menu.addEventListener(\"click\", () => closeDropdown(dropdown));\r\n });\r\n\r\n document.addEventListener(\"click\", () => {\r\n dropdowns.forEach((d) => closeDropdown(d));\r\n });\r\n\r\n document.addEventListener(\"keydown\", (e) => {\r\n if (e.key === \"Escape\") dropdowns.forEach((d) => closeDropdown(d));\r\n });\r\n});\r\n\r\n\r\n\r\n/* ========================================================= list js ========================================================= */\r\n\r\n// Draggable js\r\n\r\ndocument.addEventListener(\"DOMContentLoaded\", () => {\r\n\r\n const list = document.querySelector(\".ux4g-list-draggable\");\r\n let draggedItem = null;\r\n\r\n // enable drag only from icon\r\n document.querySelectorAll(\".ux4g-icon-outlined, .ux4g-icon\").forEach(icon => {\r\n\r\n icon.addEventListener(\"dragstart\", (e) => {\r\n draggedItem = icon.closest(\".ux4g-list-item\");\r\n draggedItem.classList.add(\"dragging\");\r\n\r\n e.dataTransfer.effectAllowed = \"move\";\r\n });\r\n\r\n });\r\n\r\n document.querySelectorAll(\".ux4g-list-item\").forEach(item => {\r\n\r\n item.addEventListener(\"dragover\", (e) => {\r\n e.preventDefault();\r\n const dragging = document.querySelector(\".dragging\");\r\n\r\n if (dragging === item) return;\r\n\r\n const rect = item.getBoundingClientRect();\r\n const offset = e.clientY - rect.top;\r\n\r\n if (offset > rect.height / 2) {\r\n item.after(dragging);\r\n } else {\r\n item.before(dragging);\r\n }\r\n });\r\n\r\n item.addEventListener(\"dragend\", () => {\r\n draggedItem.classList.remove(\"dragging\");\r\n draggedItem = null;\r\n });\r\n\r\n });\r\n\r\n});\r\n\r\n/* ========================================================= Modal js ========================================================= */\r\n\r\ndocument.addEventListener('DOMContentLoaded', () => {\r\n\r\n const openModalButtons = document.querySelectorAll('[data-modal-target]');\r\n\r\n openModalButtons.forEach(button => {\r\n button.addEventListener('click', () => {\r\n const targetSelector = button.getAttribute('data-modal-target');\r\n const targetModal = document.querySelector(targetSelector);\r\n\r\n if (targetModal) {\r\n targetModal.classList.add('is-open');\r\n document.body.style.overflow = 'hidden';\r\n }\r\n });\r\n });\r\n\r\n const closeModalButtons = document.querySelectorAll('[data-close-modal]');\r\n\r\n closeModalButtons.forEach(button => {\r\n button.addEventListener('click', (event) => {\r\n const modal = event.target.closest('.ux4g-modal-backdrop');\r\n if (modal) {\r\n modal.classList.remove('is-open');\r\n document.body.style.overflow = '';\r\n }\r\n });\r\n });\r\n\r\n window.addEventListener('click', (event) => {\r\n if (event.target.classList.contains('ux4g-modal-backdrop')) {\r\n event.target.classList.remove('is-open');\r\n document.body.style.overflow = '';\r\n }\r\n });\r\n\r\n window.addEventListener('keydown', (event) => {\r\n if (event.key === 'Escape') {\r\n const openModals = document.querySelectorAll('.ux4g-modal-backdrop.is-open');\r\n openModals.forEach(modal => {\r\n modal.classList.remove('is-open');\r\n });\r\n document.body.style.overflow = '';\r\n }\r\n });\r\n});\r\n\r\n/* ========================================================= clear seach btn js ========================================================= */\r\n\r\n// if (!input || !clearBtn) return;\r\n\r\n// input.addEventListener(\"input\", () => {\r\n// clearBtn.classList.toggle(\"ux4g-show-clear\", input.value.trim() !== \"\");\r\n// });\r\n\r\n// clearBtn.addEventListener(\"click\", () => {\r\n// input.value = \"\";\r\n// clearBtn.classList.remove(\"ux4g-show-clear\");\r\n// input.focus();\r\n// });\r\n// });\r\n// });\r\n\r\ndocument.addEventListener(\"DOMContentLoaded\", () => {\r\n document.querySelectorAll(\".ux4g-search-container\").forEach((searchWrap) => {\r\n const input = searchWrap.querySelector(\".ux4g-search-input\");\r\n const clearBtn = searchWrap.querySelector(\".ux4g-search-clear\");\r\n const list = searchWrap.querySelector(\".ux4g-search-list\");\r\n const filterMode = searchWrap.getAttribute(\"ux4g-search-filter\") || \"contains\";\r\n\r\n if (!input) return;\r\n\r\n const toggleSearchState = () => {\r\n searchWrap.classList.toggle(\"ux4g-has-value\", input.value.trim() !== \"\");\r\n handleSearchFilter();\r\n };\r\n\r\n const handleSearchFilter = () => {\r\n if (!list) return;\r\n\r\n // Only show list if there's a value, the component is active (focused),\r\n // or it's forced by 'show-empty' class\r\n const isActive = searchWrap.classList.contains(\"ux4g-is-active\");\r\n const hasValue = input.value.trim() !== \"\";\r\n const showEmpty = searchWrap.classList.contains(\"ux4g-search-show-empty\");\r\n\r\n if (isActive || hasValue || showEmpty) {\r\n list.style.display = \"block\";\r\n } else {\r\n list.style.display = \"none\";\r\n }\r\n\r\n const value = input.value;\r\n const options = list.querySelectorAll(\".ux4g-list-item\");\r\n const isSearchable = input.id && input.id.trim() !== \"\";\r\n let visibleCount = 0;\r\n\r\n options.forEach(option => {\r\n // Store original text\r\n const labelNode = option.querySelector(\".ux4g-list-item-start\");\r\n if (labelNode) {\r\n labelNode.style.gap = \"0\"; // Remove gap that causes space between highlight and text\r\n }\r\n const originalText = option.dataset.originalText || (labelNode ? labelNode.textContent.trim() : option.textContent.trim());\r\n if (!option.dataset.originalText) option.dataset.originalText = originalText;\r\n\r\n const isMatch = isSearchable ? window.ux4g.filterCore.matches(originalText, value, filterMode) : true;\r\n option.style.display = isMatch ? \"\" : \"none\";\r\n\r\n if (labelNode) {\r\n const trimmedQuery = value.trim();\r\n if (isSearchable && trimmedQuery && isMatch) {\r\n labelNode.innerHTML = window.ux4g.filterCore.highlight(originalText, trimmedQuery, filterMode);\r\n } else {\r\n labelNode.textContent = originalText;\r\n }\r\n }\r\n if (isMatch) visibleCount++;\r\n });\r\n \r\n // Toggle No Results Message\r\n let noResults = list.querySelector(\".ux4g-search-no-results\");\r\n if (visibleCount === 0 && value.trim() !== \"\") {\r\n if (!noResults) {\r\n noResults = document.createElement(\"li\");\r\n noResults.className = \"ux4g-search-no-results ux4g-p-s ux4g-text-center ux4g-text-muted\";\r\n noResults.style.listStyle = \"none\";\r\n noResults.textContent = \"No results found\";\r\n list.appendChild(noResults);\r\n }\r\n noResults.style.display = \"\";\r\n } else if (noResults) {\r\n noResults.style.display = \"none\";\r\n }\r\n\r\n // Optionally show list only when there's a value or interaction\r\n list.style.display = (value.trim() === \"\" && visibleCount === 0 && !searchWrap.classList.contains(\"ux4g-search-show-empty\")) ? \"none\" : \"\";\r\n \r\n // If list is hidden, don't show no results either\r\n if (list.style.display === \"none\" && noResults) noResults.style.display = \"none\";\r\n };\r\n\r\n input.addEventListener(\"input\", toggleSearchState);\r\n\r\n if (clearBtn) {\r\n clearBtn.addEventListener(\"click\", () => {\r\n input.value = \"\";\r\n toggleSearchState();\r\n input.focus();\r\n });\r\n }\r\n\r\n input.addEventListener(\"focus\", () => {\r\n searchWrap.classList.add(\"ux4g-is-active\");\r\n handleSearchFilter();\r\n });\r\n\r\n input.addEventListener(\"blur\", (e) => {\r\n // Small timeout to allow potential clicks on the list item itself\r\n setTimeout(() => {\r\n if (!searchWrap.contains(document.activeElement)) {\r\n searchWrap.classList.remove(\"ux4g-is-active\");\r\n handleSearchFilter();\r\n }\r\n }, 200);\r\n });\r\n\r\n // Close when clicking outside\r\n document.addEventListener(\"mousedown\", (e) => {\r\n if (!searchWrap.contains(e.target)) {\r\n searchWrap.classList.remove(\"ux4g-is-active\");\r\n handleSearchFilter();\r\n }\r\n });\r\n\r\n // Initial state\r\n toggleSearchState();\r\n });\r\n});\r\n\r\n// clear input text\r\ndocument.addEventListener(\"DOMContentLoaded\", () => {\r\n const toggle = (i) => i.closest(\".ux4g-input-container\")?.classList.toggle(\"ux4g-has-value\", i.value.length > 0);\r\n\r\n document.addEventListener(\"input\", (e) => e.target.tagName === \"INPUT\" && toggle(e.target));\r\n\r\n document.addEventListener(\"click\", (e) => {\r\n const btn = e.target.closest('[aria-label=\"Clear input\"], .ux4g-input-clear');\r\n if (btn) {\r\n const input = btn.closest(\".ux4g-input-container\")?.querySelector(\"input\");\r\n if (input && !input.disabled && !input.readOnly) {\r\n input.value = \"\";\r\n toggle(input);\r\n input.dispatchEvent(new Event(\"input\", { bubbles: true }));\r\n input.focus();\r\n }\r\n }\r\n });\r\n\r\n document.querySelectorAll(\".ux4g-input-container input\").forEach(toggle);\r\n});\r\n\r\n/* ========================================================= textarea counter js ========================================================= */\r\ndocument.addEventListener(\"DOMContentLoaded\", () => {\r\n const updateCounter = (textarea) => {\r\n const wrapper = textarea.closest(\".ux4g-textarea\");\r\n if (!wrapper) return;\r\n\r\n const counter = wrapper.querySelector(\".ux4g-textarea-counter\");\r\n if (!counter) return;\r\n\r\n const maxLength = textarea.getAttribute(\"maxlength\") || 0;\r\n const currentLength = textarea.value.length;\r\n\r\n // Update text content with standard \"0 / 200\" format \r\n // Uses textContent to avoid XSS vectors or accidental HTML\r\n counter.textContent = `${currentLength} / ${maxLength}`;\r\n };\r\n\r\n // Add listener for all input events globally (event delegation)\r\n document.addEventListener(\"input\", (e) => {\r\n if (e.target && e.target.matches && e.target.matches(\".ux4g-textarea-input\")) {\r\n updateCounter(e.target);\r\n }\r\n });\r\n\r\n // Initialize all textareas with counters on page load\r\n document.querySelectorAll(\".ux4g-textarea-input\").forEach((textarea) => {\r\n updateCounter(textarea);\r\n });\r\n});\r\n\r\n\r\n\r\n// // ux4g drawer js\r\n\r\ndocument.addEventListener(\"DOMContentLoaded\", () => {\r\n\r\n const buttons = document.querySelectorAll(\"[data-drawer]\");\r\n\r\n\r\n /* ---------------- */\r\n /* CLOSE DRAWER FUNCTION */\r\n /* ---------------- */\r\n\r\n function closeDrawer() {\r\n\r\n const openDrawer = document.querySelector(\".ux4g-drawer.ux4g-drawer-open\");\r\n const openOverlay = document.querySelector(\".ux4g-drawer-overlay.ux4g-drawer-open\");\r\n\r\n if (!openDrawer || !openOverlay) return;\r\n\r\n openDrawer.classList.remove(\"ux4g-drawer-open\");\r\n openOverlay.classList.remove(\"ux4g-drawer-open\");\r\n\r\n document.body.classList.remove(\"ux4g-drawer-lock\");\r\n }\r\n\r\n\r\n\r\n /* ---------------- */\r\n /* OPEN DRAWER */\r\n /* ---------------- */\r\n\r\n buttons.forEach(button => {\r\n\r\n button.addEventListener(\"click\", () => {\r\n\r\n const drawer = document.getElementById(button.dataset.drawer);\r\n const overlay = drawer.closest(\".ux4g-drawer-overlay\");\r\n\r\n document.querySelectorAll(\".ux4g-drawer-open\")\r\n .forEach(el => el.classList.remove(\"ux4g-drawer-open\"));\r\n\r\n overlay.classList.add(\"ux4g-drawer-open\");\r\n drawer.classList.add(\"ux4g-drawer-open\");\r\n\r\n document.body.classList.add(\"ux4g-drawer-lock\");\r\n\r\n });\r\n\r\n });\r\n\r\n\r\n\r\n /* ---------------- */\r\n /* CLOSE VIA BUTTONS */\r\n /* ---------------- */\r\n\r\n document.addEventListener(\"click\", (e) => {\r\n\r\n const closeTrigger = e.target.closest(\"[data-drawer-close]\");\r\n\r\n if (closeTrigger) {\r\n closeDrawer();\r\n return;\r\n }\r\n\r\n\r\n /* CLOSE VIA OVERLAY CLICK */\r\n\r\n const overlay = e.target.closest(\".ux4g-drawer-overlay\");\r\n\r\n if (overlay && !e.target.closest(\".ux4g-drawer\")) {\r\n closeDrawer();\r\n }\r\n\r\n });\r\n\r\n\r\n\r\n /* ---------------- */\r\n /* CLOSE VIA ESC KEY */\r\n /* ---------------- */\r\n\r\n document.addEventListener(\"keydown\", (e) => {\r\n\r\n if (e.key === \"Escape\") {\r\n closeDrawer();\r\n }\r\n\r\n });\r\n\r\n});\r\n\r\n/* ========================================================= dropdown js ========================================================= */\r\n\r\n/* ========================================================= dropdown js ========================================================= */\r\n\r\ndocument.addEventListener(\"DOMContentLoaded\", () => {\r\n const dropdowns = Array.from(document.querySelectorAll(\".ux4g-dropdown\"));\r\n if (!dropdowns.length) return;\r\n\r\n const closeDropdown = (dropdown) => {\r\n dropdown.classList.remove(\"is-open\");\r\n const control = dropdown.querySelector(\".ux4g-dropdown-control\");\r\n if (control) control.setAttribute(\"aria-expanded\", \"false\");\r\n };\r\n\r\n const openDropdown = (dropdown) => {\r\n dropdowns.forEach((item) => {\r\n if (item !== dropdown) closeDropdown(item);\r\n });\r\n dropdown.classList.add(\"is-open\");\r\n const control = dropdown.querySelector(\".ux4g-dropdown-control\");\r\n if (control) control.setAttribute(\"aria-expanded\", \"true\");\r\n\r\n const menu = dropdown.querySelector(\".ux4g-dropdown-menu\");\r\n if (menu && window.ux4g && window.ux4g.repositionMenu) {\r\n window.ux4g.repositionMenu(dropdown, menu);\r\n }\r\n };\r\n\r\n const setControlText = (dropdown, value) => {\r\n const hasValue = Boolean(value && String(value).trim());\r\n dropdown.classList.toggle(\"has-value\", hasValue);\r\n\r\n const searchInput = dropdown.querySelector(\"[ux4g-dropdown-search]\");\r\n if (searchInput) {\r\n if (dropdown.classList.contains(\"ux4g-dropdown-single\")) {\r\n searchInput.value = value || \"\";\r\n } else {\r\n searchInput.value = \"\";\r\n }\r\n // Trigger filter logic to stay in sync\r\n searchInput.dispatchEvent(new Event(\"input\", { bubbles: true }));\r\n return;\r\n }\r\n\r\n const valueNode = dropdown.querySelector(\"[ux4g-dropdown-value]\");\r\n if (!valueNode) return;\r\n const placeholder = valueNode.getAttribute(\"data-placeholder\") || \"Please select..\";\r\n valueNode.textContent = hasValue ? value : placeholder;\r\n valueNode.classList.toggle(\"is-placeholder\", !hasValue);\r\n };\r\n\r\n const getInputLabel = (input) => {\r\n if (!input) return \"\";\r\n if (input.dataset.label) return input.dataset.label;\r\n if (input.value) return input.value;\r\n const option = input.closest(\".ux4g-dropdown-option\");\r\n const text = option?.querySelector(\".ux4g-checkbox-label, .ux4g-dropdown-option-title\");\r\n return text?.textContent?.trim() || \"\";\r\n };\r\n\r\n const getChoiceLabel = (choice) => {\r\n if (!choice) return \"\";\r\n return (choice.getAttribute(\"ux4g-dropdown-choice\") || choice.textContent || \"\").trim();\r\n };\r\n\r\n const applySingleSelection = (dropdown, value) => {\r\n const chipsNode = dropdown.querySelector(\"[ux4g-dropdown-chips]\");\r\n dropdown.classList.remove(\"has-selection\");\r\n setControlText(dropdown, value);\r\n if (!chipsNode) return;\r\n chipsNode.innerHTML = \"\";\r\n };\r\n\r\n const setSingleSelectedOption = (dropdown, selectedOption) => {\r\n const options = dropdown.querySelectorAll(\".ux4g-dropdown-single-option\");\r\n options.forEach((option) => {\r\n const isSelected = option === selectedOption;\r\n if (isSelected) {\r\n option.classList.add(\"is-selected\", \"active\");\r\n option.setAttribute(\"aria-selected\", \"true\");\r\n } else {\r\n option.classList.remove(\"is-selected\", \"active\");\r\n option.setAttribute(\"aria-selected\", \"false\");\r\n }\r\n const listItem = option.closest(\".ux4g-list-item\");\r\n if (listItem) listItem.setAttribute(\"aria-selected\", String(isSelected));\r\n });\r\n };\r\n\r\n const renderMultiSelection = (dropdown) => {\r\n const chipsNode = dropdown.querySelector(\"[ux4g-dropdown-chips]\");\r\n const checkedInputs = Array.from(\r\n dropdown.querySelectorAll(\".ux4g-dropdown-option-input:checked\")\r\n );\r\n const options = Array.from(dropdown.querySelectorAll(\".ux4g-dropdown-option\"));\r\n\r\n const hasSelection = checkedInputs.length > 0;\r\n dropdown.classList.toggle(\"has-selection\", hasSelection && Boolean(chipsNode));\r\n options.forEach((option) => {\r\n const input = option.querySelector(\".ux4g-dropdown-option-input\");\r\n const isSelected = Boolean(input?.checked);\r\n option.classList.toggle(\"is-selected\", isSelected);\r\n option.classList.toggle(\"active\", isSelected);\r\n option.setAttribute(\"aria-selected\", String(isSelected));\r\n });\r\n\r\n setControlText(dropdown, \"\");\r\n if (!chipsNode) return;\r\n chipsNode.innerHTML = \"\";\r\n if (!chipsNode || !hasSelection) return;\r\n\r\n let chipSizeClass = \"ux4g-input-chip-sm\";\r\n if (dropdown.classList.contains(\"ux4g-dropdown-sm\")) chipSizeClass = \"ux4g-input-chip-xs\";\r\n if (dropdown.classList.contains(\"ux4g-dropdown-lg\")) chipSizeClass = \"ux4g-input-chip\";\r\n\r\n checkedInputs.forEach((input) => {\r\n const label = getInputLabel(input);\r\n if (!label) return;\r\n\r\n const chip = document.createElement(\"span\");\r\n chip.className = `${chipSizeClass} ux4g-dropdown-chip`;\r\n chip.setAttribute(\"role\", \"button\");\r\n chip.setAttribute(\"tabindex\", \"0\");\r\n chip.setAttribute(\"aria-label\", `Remove ${label}`);\r\n chip.setAttribute(\"data-input-id\", input.id);\r\n chip.innerHTML = `<span class=\"ux4g-icon-outlined\" aria-hidden=\"true\">token</span><span>${label}</span><span class=\"ux4g-icon-outlined\" aria-hidden=\"true\">close</span>`;\r\n\r\n const closeBtn = chip.querySelector(\".ux4g-icon-outlined:last-child\");\r\n\r\n const clearSelection = (event) => {\r\n event.preventDefault();\r\n event.stopPropagation();\r\n input.checked = false;\r\n renderMultiSelection(dropdown);\r\n };\r\n\r\n chip.addEventListener(\"mousedown\", (event) => {\r\n event.preventDefault();\r\n event.stopPropagation();\r\n });\r\n\r\n if (closeBtn) {\r\n closeBtn.addEventListener(\"click\", clearSelection);\r\n }\r\n\r\n chip.addEventListener(\"keydown\", (event) => {\r\n if (event.key === \"Enter\" || event.key === \" \") {\r\n clearSelection(event);\r\n }\r\n });\r\n\r\n chipsNode.appendChild(chip);\r\n });\r\n };\r\n\r\n dropdowns.forEach((dropdown, index) => {\r\n const control = dropdown.querySelector(\".ux4g-dropdown-control\");\r\n const menu = dropdown.querySelector(\".ux4g-dropdown-menu\");\r\n if (!control || !menu) return;\r\n\r\n if (!control.id) {\r\n control.id = `ux4g-dropdown-control-${index + 1}`;\r\n }\r\n\r\n const popupRole = menu.getAttribute(\"role\") === \"menu\" ? \"menu\" : \"listbox\";\r\n control.setAttribute(\"aria-haspopup\", popupRole);\r\n control.setAttribute(\"aria-expanded\", \"false\");\r\n menu.setAttribute(\"aria-labelledby\", control.id);\r\n\r\n const isSingle = dropdown.classList.contains(\"ux4g-dropdown-single\");\r\n const isMulti = dropdown.classList.contains(\"ux4g-dropdown-multi\");\r\n\r\n const dropdownInputs = Array.from(menu.querySelectorAll(\".ux4g-dropdown-option-input\"));\r\n dropdownInputs.forEach((input, inputIndex) => {\r\n if (!input.id) {\r\n input.id = `${control.id}-option-${inputIndex + 1}`;\r\n }\r\n });\r\n\r\n control.addEventListener(\"click\", (event) => {\r\n // Prevent interactions if disabled\r\n if (control.disabled || control.getAttribute(\"aria-disabled\") === \"true\") {\r\n event.preventDefault();\r\n event.stopPropagation();\r\n return;\r\n }\r\n\r\n if (event.target.closest(\"[ux4g-dropdown-search]\")) {\r\n if (!dropdown.classList.contains(\"is-open\")) {\r\n openDropdown(dropdown);\r\n }\r\n return;\r\n }\r\n event.preventDefault();\r\n event.stopPropagation();\r\n if (dropdown.classList.contains(\"is-open\")) closeDropdown(dropdown);\r\n else openDropdown(dropdown);\r\n });\r\n\r\n menu.addEventListener(\"click\", (event) => {\r\n const choice = event.target.closest(\"[ux4g-dropdown-choice]\");\r\n if (!choice) return;\r\n\r\n // Stop propagation to prevent global list JS from re-adding 'active' class\r\n event.stopPropagation();\r\n\r\n const value = getChoiceLabel(choice);\r\n if (!value) return;\r\n\r\n if (isSingle) {\r\n setSingleSelectedOption(dropdown, choice);\r\n applySingleSelection(dropdown, value);\r\n } else if (!isMulti) {\r\n setSingleSelectedOption(dropdown, choice); // Also set option for regular single selection\r\n applySingleSelection(dropdown, value);\r\n }\r\n\r\n closeDropdown(dropdown);\r\n });\r\n\r\n menu.addEventListener(\"change\", (event) => {\r\n const input = event.target.closest(\".ux4g-dropdown-option-input\");\r\n if (!input) return;\r\n\r\n if (isMulti) {\r\n renderMultiSelection(dropdown);\r\n return;\r\n }\r\n\r\n if (input.checked && !isMulti) {\r\n menu.querySelectorAll(\".ux4g-dropdown-option-input\").forEach((item) => {\r\n if (item !== input) item.checked = false;\r\n });\r\n }\r\n\r\n applySingleSelection(dropdown, input.checked ? getInputLabel(input) : \"\");\r\n closeDropdown(dropdown);\r\n });\r\n\r\n menu.addEventListener(\"click\", (event) => {\r\n const row = event.target.closest(\".ux4g-list-item-row\");\r\n if (!row) return;\r\n\r\n const isActionDropdown =\r\n dropdown.classList.contains(\"ux4g-dropdown-button\") ||\r\n dropdown.classList.contains(\"ux4g-dropdown-overflow\");\r\n\r\n if (isActionDropdown && !row.hasAttribute(\"ux4g-dropdown-choice\")) {\r\n closeDropdown(dropdown);\r\n }\r\n });\r\n\r\n if (isMulti) {\r\n const searchInput = dropdown.querySelector(\"[ux4g-dropdown-search]\");\r\n if (searchInput) {\r\n const icon = searchInput.previousElementSibling;\r\n if (icon && icon.classList.contains(\"ux4g-icon-outlined\") && !searchInput.closest(\".ux4g-dropdown-input-wrap\")) {\r\n const wrapper = document.createElement(\"span\");\r\n wrapper.className = \"ux4g-dropdown-input-wrap\";\r\n icon.parentNode.insertBefore(wrapper, icon);\r\n wrapper.appendChild(icon);\r\n wrapper.appendChild(searchInput);\r\n }\r\n }\r\n renderMultiSelection(dropdown);\r\n return;\r\n }\r\n\r\n const preselectedSingle = menu.querySelector(\r\n \".ux4g-dropdown-single-option.is-selected, .ux4g-dropdown-single-option[aria-selected='true']\"\r\n );\r\n if (preselectedSingle) {\r\n setSingleSelectedOption(dropdown, preselectedSingle);\r\n applySingleSelection(dropdown, getChoiceLabel(preselectedSingle));\r\n return;\r\n }\r\n\r\n const preselectedChoice = menu.querySelector(\"[ux4g-dropdown-choice][aria-selected='true']\");\r\n if (preselectedChoice) {\r\n applySingleSelection(dropdown, getChoiceLabel(preselectedChoice));\r\n return;\r\n }\r\n\r\n const preselectedInput = menu.querySelector(\".ux4g-dropdown-option-input:checked\");\r\n applySingleSelection(dropdown, preselectedInput ? getInputLabel(preselectedInput) : \"\");\r\n });\r\n\r\n document.addEventListener(\"click\", (event) => {\r\n dropdowns.forEach((dropdown) => {\r\n if (!dropdown.contains(event.target)) closeDropdown(dropdown);\r\n });\r\n });\r\n\r\n document.addEventListener(\"keydown\", (event) => {\r\n if (event.key === \"Escape\") dropdowns.forEach((dropdown) => closeDropdown(dropdown));\r\n });\r\n});\r\n\r\n\r\n/* =========================================\r\nDROPDOWN SEARCH\r\n========================================= */\r\n\r\ndocument.addEventListener(\"DOMContentLoaded\", () => {\r\n document.querySelectorAll(\"[ux4g-dropdown-search]\").forEach((input) => {\r\n\r\n const dropdown = input.closest(\".ux4g-dropdown\");\r\n const menu = dropdown.querySelector(\".ux4g-dropdown-menu\");\r\n\r\n input.addEventListener(\"focus\", () => {\r\n dropdown.classList.add(\"is-open\");\r\n if (window.ux4g && window.ux4g.repositionMenu) {\r\n window.ux4g.repositionMenu(dropdown, menu);\r\n }\r\n });\r\n\r\n input.addEventListener(\"input\", () => {\r\n const value = input.value;\r\n const filterMode = dropdown.getAttribute(\"ux4g-dropdown-filter\") || \"contains\";\r\n\r\n dropdown.classList.add(\"is-open\");\r\n if (window.ux4g && window.ux4g.repositionMenu) {\r\n window.ux4g.repositionMenu(dropdown, menu);\r\n }\r\n\r\n const options = dropdown.querySelectorAll(\r\n \".ux4g-dropdown-single-option, .ux4g-dropdown-option\"\r\n );\r\n\r\n options.forEach(option => {\r\n const originalText = option.dataset.originalText || option.textContent.trim();\r\n if (!option.dataset.originalText) option.dataset.originalText = originalText;\r\n\r\n const isMatch = window.ux4g.filterCore.matches(originalText, value, filterMode);\r\n option.style.display = isMatch ? \"\" : \"none\";\r\n\r\n // Optional: Add highlighting for Dropdown Search too\r\n const labelNode = option.querySelector(\".ux4g-checkbox-label, .ux4g-dropdown-option-title, .ux4g-list-item-start\");\r\n if (labelNode) {\r\n if (value && isMatch) {\r\n labelNode.innerHTML = window.ux4g.filterCore.highlight(originalText, value, filterMode);\r\n } else {\r\n labelNode.textContent = originalText;\r\n }\r\n }\r\n });\r\n });\r\n\r\n // Sync disabled state\r\n const control = dropdown.querySelector(\".ux4g-dropdown-control\");\r\n if (control && (control.disabled || control.getAttribute(\"aria-disabled\") === \"true\")) {\r\n input.disabled = true;\r\n }\r\n });\r\n});\r\n\r\n/* ========================================================= combobox js ========================================================= */\r\n\r\ndocument.addEventListener(\"DOMContentLoaded\", () => {\r\n const comboboxs = Array.from(document.querySelectorAll(\".ux4g-combobox\"));\r\n if (!comboboxs.length) return;\r\n\r\n const closeCombobox = (combobox) => {\r\n combobox.classList.remove(\"is-open\");\r\n const control = combobox.querySelector(\".ux4g-combobox-control\");\r\n if (control) control.setAttribute(\"aria-expanded\", \"false\");\r\n };\r\n\r\n const openCombobox = (combobox) => {\r\n comboboxs.forEach((item) => {\r\n if (item !== combobox) closeCombobox(item);\r\n });\r\n combobox.classList.add(\"is-open\");\r\n const control = combobox.querySelector(\".ux4g-combobox-control\");\r\n if (control) control.setAttribute(\"aria-expanded\", \"true\");\r\n\r\n const menu = combobox.querySelector(\".ux4g-combobox-menu\");\r\n if (menu && window.ux4g && window.ux4g.repositionMenu) {\r\n window.ux4g.repositionMenu(combobox, menu);\r\n }\r\n };\r\n\r\n const setControlText = (combobox, value) => {\r\n const searchInput = combobox.querySelector(\"[ux4g-combobox-search]\");\r\n if (searchInput) {\r\n if (combobox.classList.contains(\"ux4g-combobox-single\")) {\r\n searchInput.value = value || \"\";\r\n } else {\r\n searchInput.value = \"\";\r\n }\r\n return;\r\n }\r\n\r\n const valueNode = combobox.querySelector(\"[ux4g-combobox-value]\");\r\n const hasValue = Boolean(value && String(value).trim());\r\n if (!valueNode) return;\r\n const placeholder = valueNode.getAttribute(\"data-placeholder\") || \"Please select..\";\r\n valueNode.textContent = hasValue ? value : placeholder;\r\n valueNode.classList.toggle(\"is-placeholder\", !hasValue);\r\n };\r\n\r\n const getInputLabel = (input) => {\r\n if (!input) return \"\";\r\n if (input.dataset.label) return input.dataset.label;\r\n if (input.value) return input.value;\r\n const option = input.closest(\".ux4g-combobox-option\");\r\n const text = option?.querySelector(\".ux4g-checkbox-label, .ux4g-combobox-option-title\");\r\n return text?.textContent?.trim() || \"\";\r\n };\r\n\r\n const getChoiceLabel = (choice) => {\r\n if (!choice) return \"\";\r\n return (choice.getAttribute(\"ux4g-combobox-choice\") || choice.textContent || \"\").trim();\r\n };\r\n\r\n const applySingleSelection = (combobox, value) => {\r\n const chipsNode = combobox.querySelector(\"[ux4g-combobox-chips]\");\r\n combobox.classList.remove(\"has-selection\");\r\n setControlText(combobox, value);\r\n if (!chipsNode) return;\r\n chipsNode.innerHTML = \"\";\r\n };\r\n\r\n const setSingleSelectedOption = (combobox, selectedOption) => {\r\n const options = combobox.querySelectorAll(\".ux4g-combobox-single-option\");\r\n options.forEach((option) => {\r\n const isSelected = option === selectedOption;\r\n option.classList.toggle(\"is-selected\", isSelected);\r\n option.classList.toggle(\"active\", isSelected);\r\n option.setAttribute(\"aria-selected\", String(isSelected));\r\n const listItem = option.closest(\".ux4g-list-item\");\r\n if (listItem) listItem.setAttribute(\"aria-selected\", String(isSelected));\r\n });\r\n };\r\n\r\n const renderMultiSelection = (combobox) => {\r\n const chipsNode = combobox.querySelector(\"[ux4g-combobox-chips]\");\r\n const checkedInputs = Array.from(\r\n combobox.querySelectorAll(\".ux4g-combobox-option-input:checked\")\r\n );\r\n const options = Array.from(combobox.querySelectorAll(\".ux4g-combobox-option\"));\r\n\r\n const hasSelection = checkedInputs.length > 0;\r\n combobox.classList.toggle(\"has-selection\", hasSelection && Boolean(chipsNode));\r\n options.forEach((option) => {\r\n const input = option.querySelector(\".ux4g-combobox-option-input\");\r\n const isSelected = Boolean(input?.checked);\r\n option.classList.toggle(\"is-selected\", isSelected);\r\n option.classList.toggle(\"active\", isSelected);\r\n option.setAttribute(\"aria-selected\", String(isSelected));\r\n });\r\n\r\n setControlText(combobox, \"\");\r\n if (!chipsNode) return;\r\n chipsNode.style.display = \"contents\";\r\n chipsNode.innerHTML = \"\";\r\n if (!chipsNode || !hasSelection) return;\r\n\r\n let chipSizeClass = \"ux4g-input-chip-sm\";\r\n if (combobox.classList.contains(\"ux4g-combobox-sm\")) chipSizeClass = \"ux4g-input-chip-xs\";\r\n if (combobox.classList.contains(\"ux4g-combobox-lg\")) chipSizeClass = \"ux4g-input-chip\";\r\n\r\n checkedInputs.forEach((input) => {\r\n const label = getInputLabel(input);\r\n if (!label) return;\r\n\r\n const chip = document.createElement(\"span\");\r\n chip.className = `${chipSizeClass} ux4g-combobox-chip`;\r\n chip.setAttribute(\"role\", \"button\");\r\n chip.setAttribute(\"tabindex\", \"0\");\r\n chip.setAttribute(\"aria-label\", `Remove ${label}`);\r\n chip.setAttribute(\"data-input-id\", input.id);\r\n chip.innerHTML = `<span class=\"ux4g-icon-outlined\" aria-hidden=\"true\">token</span><span>${label}</span><span class=\"ux4g-icon-outlined\" aria-hidden=\"true\">close</span>`;\r\n\r\n const closeBtn = chip.querySelector(\".ux4g-icon-outlined:last-child\");\r\n\r\n const clearSelection = (event) => {\r\n event.preventDefault();\r\n event.stopPropagation();\r\n input.checked = false;\r\n renderMultiSelection(combobox);\r\n };\r\n\r\n chip.addEventListener(\"mousedown\", (event) => {\r\n event.preventDefault();\r\n event.stopPropagation();\r\n });\r\n\r\n if (closeBtn) {\r\n closeBtn.addEventListener(\"click\", clearSelection);\r\n }\r\n\r\n chip.addEventListener(\"keydown\", (event) => {\r\n if (event.key === \"Enter\" || event.key === \" \") {\r\n clearSelection(event);\r\n }\r\n });\r\n\r\n chipsNode.appendChild(chip);\r\n });\r\n };\r\n\r\n const DEBOUNCE_DELAY = 150;\r\n const debounce = (fn, delay) => {\r\n let t;\r\n return (...args) => {\r\n clearTimeout(t);\r\n t = setTimeout(() => fn(...args), delay);\r\n };\r\n };\r\n\r\n function toggleNoResults(menu, show) {\r\n let el = menu.querySelector(\".ux4g-combobox-no-results\");\r\n if (show) {\r\n if (!el) {\r\n el = document.createElement(\"div\");\r\n el.className = \"ux4g-combobox-no-results ux4g-p-s ux4g-text-center ux4g-text-muted\";\r\n el.textContent = \"No results found\";\r\n menu.appendChild(el);\r\n }\r\n el.style.display = \"\";\r\n } else if (el) {\r\n el.style.display = \"none\";\r\n }\r\n }\r\n\r\n comboboxs.forEach((combobox, index) => {\r\n const control = combobox.querySelector(\".ux4g-combobox-control\");\r\n const menu = combobox.querySelector(\".ux4g-combobox-menu\");\r\n const input = combobox.querySelector(\"[ux4g-combobox-search]\");\r\n const caret = combobox.querySelector(\".ux4g-combobox-caret\");\r\n\r\n if (!control || !menu) return;\r\n\r\n if (!control.id) {\r\n control.id = `ux4g-combobox-control-${index + 1}`;\r\n }\r\n\r\n const popupRole = menu.getAttribute(\"role\") === \"menu\" ? \"menu\" : \"listbox\";\r\n control.setAttribute(\"aria-haspopup\", popupRole);\r\n control.setAttribute(\"aria-expanded\", \"false\");\r\n menu.setAttribute(\"aria-labelledby\", control.id);\r\n\r\n const isSingle = combobox.classList.contains(\"ux4g-combobox-single\");\r\n const isMulti = combobox.classList.contains(\"ux4g-combobox-multi\");\r\n const filterMode = combobox.getAttribute(\"ux4g-combobox-filter\") || \"contains\";\r\n\r\n const comboboxInputs = Array.from(menu.querySelectorAll(\".ux4g-combobox-option-input\"));\r\n comboboxInputs.forEach((input, inputIndex) => {\r\n if (!input.id) {\r\n input.id = `${control.id}-option-${inputIndex + 1}`;\r\n }\r\n });\r\n\r\n const options = isMulti\r\n ? Array.from(combobox.querySelectorAll(\".ux4g-combobox-option\"))\r\n : Array.from(combobox.querySelectorAll(\".ux4g-combobox-single-option\"));\r\n\r\n // Store original text for filtering\r\n options.forEach((option) => {\r\n let labelNode = isMulti\r\n ? option.querySelector(\".ux4g-checkbox-label\")\r\n : option.querySelector(\".ux4g-list-item-start\");\r\n\r\n option.dataset.originalText = labelNode\r\n ? labelNode.textContent.trim()\r\n : option.textContent.trim();\r\n });\r\n\r\n const handleFilter = () => {\r\n if (!input) return;\r\n const value = input.value;\r\n let visibleCount = 0;\r\n\r\n options.forEach((option) => {\r\n const originalText = option.dataset.originalText;\r\n const isMatch = window.ux4g.filterCore.matches(originalText, value, filterMode);\r\n \r\n option.style.display = isMatch ? \"\" : \"none\";\r\n\r\n const labelNode = isMulti\r\n ? option.querySelector(\".ux4g-checkbox-label\")\r\n : option.querySelector(\".ux4g-list-item-start\");\r\n\r\n if (labelNode) {\r\n if (value && isMatch) {\r\n labelNode.innerHTML = window.ux4g.filterCore.highlight(originalText, value, filterMode);\r\n } else {\r\n labelNode.textContent = originalText;\r\n }\r\n }\r\n if (isMatch) visibleCount++;\r\n });\r\n toggleNoResults(menu, visibleCount === 0 && value.trim());\r\n };\r\n\r\n if (input) {\r\n input.addEventListener(\"focus\", () => openCombobox(combobox));\r\n input.addEventListener(\"input\", debounce(() => {\r\n openCombobox(combobox);\r\n if (isSingle && input.value.trim() === \"\") {\r\n setSingleSelectedOption(combobox, null);\r\n applySingleSelection(combobox, \"\");\r\n }\r\n handleFilter();\r\n }, DEBOUNCE_DELAY));\r\n }\r\n\r\n if (caret) {\r\n caret.addEventListener(\"click\", (e) => {\r\n e.stopPropagation();\r\n combobox.classList.contains(\"is-open\") ? closeCombobox(combobox) : openCombobox(combobox);\r\n });\r\n }\r\n\r\n control.addEventListener(\"click\", (event) => {\r\n if (control.disabled || control.getAttribute(\"aria-disabled\") === \"true\") {\r\n event.preventDefault();\r\n event.stopPropagation();\r\n return;\r\n }\r\n\r\n if (event.target.closest(\"[ux4g-combobox-search]\")) {\r\n if (!combobox.classList.contains(\"is-open\")) openCombobox(combobox);\r\n return;\r\n }\r\n event.preventDefault();\r\n event.stopPropagation();\r\n if (combobox.classList.contains(\"is-open\")) closeCombobox(combobox);\r\n else openCombobox(combobox);\r\n });\r\n\r\n menu.addEventListener(\"click\", (event) => {\r\n const choice = event.target.closest(\"[ux4g-combobox-choice]\");\r\n if (!choice) {\r\n // Handle Action Combobox closing\r\n const row = event.target.closest(\".ux4g-list-item-row\");\r\n if (row) {\r\n const isActionCombobox =\r\n combobox.classList.contains(\"ux4g-combobox-button\") ||\r\n combobox.classList.contains(\"ux4g-combobox-overflow\");\r\n if (isActionCombobox) {\r\n closeCombobox(combobox);\r\n }\r\n }\r\n return;\r\n }\r\n event.stopPropagation();\r\n\r\n const value = getChoiceLabel(choice);\r\n if (!value) return;\r\n\r\n if (isSingle) {\r\n setSingleSelectedOption(combobox, choice);\r\n applySingleSelection(combobox, value);\r\n handleFilter(); // Reset highlight\r\n } else if (!isMulti) {\r\n applySingleSelection(combobox, value);\r\n }\r\n closeCombobox(combobox);\r\n });\r\n\r\n menu.addEventListener(\"change\", (event) => {\r\n const inputEl = event.target.closest(\".ux4g-combobox-option-input\");\r\n if (!inputEl) return;\r\n\r\n if (isMulti) {\r\n renderMultiSelection(combobox);\r\n handleFilter();\r\n return;\r\n }\r\n\r\n if (inputEl.checked && !isMulti) {\r\n menu.querySelectorAll(\".ux4g-combobox-option-input\").forEach((item) => {\r\n if (item !== inputEl) item.checked = false;\r\n });\r\n }\r\n\r\n applySingleSelection(combobox, inputEl.checked ? getInputLabel(inputEl) : \"\");\r\n closeCombobox(combobox);\r\n });\r\n\r\n if (isMulti) {\r\n if (input) {\r\n const icon = input.previousElementSibling;\r\n if (icon && icon.classList.contains(\"ux4g-icon-outlined\") && !input.closest(\".ux4g-combobox-input-wrap\")) {\r\n const wrapper = document.createElement(\"span\");\r\n wrapper.className = \"ux4g-combobox-input-wrap\";\r\n icon.parentNode.insertBefore(wrapper, icon);\r\n wrapper.appendChild(icon);\r\n wrapper.appendChild(input);\r\n }\r\n }\r\n renderMultiSelection(combobox);\r\n return;\r\n }\r\n\r\n const preselectedSingle = menu.querySelector(\r\n \".ux4g-combobox-single-option.is-selected, .ux4g-combobox-single-option[aria-selected='true']\"\r\n );\r\n if (preselectedSingle) {\r\n setSingleSelectedOption(combobox, preselectedSingle);\r\n applySingleSelection(combobox, getChoiceLabel(preselectedSingle));\r\n return;\r\n }\r\n\r\n const preselectedChoice = menu.querySelector(\"[ux4g-combobox-choice][aria-selected='true']\");\r\n if (preselectedChoice) {\r\n applySingleSelection(combobox, getChoiceLabel(preselectedChoice));\r\n return;\r\n }\r\n\r\n const preselectedInput = menu.querySelector(\".ux4g-combobox-option-input:checked\");\r\n if (preselectedInput) {\r\n applySingleSelection(combobox, getInputLabel(preselectedInput));\r\n }\r\n });\r\n\r\n document.addEventListener(\"click\", (event) => {\r\n comboboxs.forEach((combobox) => {\r\n if (!combobox.contains(event.target)) closeCombobox(combobox);\r\n });\r\n });\r\n\r\n document.addEventListener(\"keydown\", (event) => {\r\n if (event.key === \"Escape\") comboboxs.forEach((combobox) => closeCombobox(combobox));\r\n });\r\n});\r\n\r\n\r\n/* ========================================================= ux4g Tabs ========================================================= */\r\n\r\n\r\n(function (global) {\r\n 'use strict';\r\n\r\n class UX4GTab {\r\n\r\n constructor(rootEl, options = {}) {\r\n if (!(rootEl instanceof HTMLElement)) {\r\n throw new Error('UX4GTab: rootEl must be an HTMLElement');\r\n }\r\n\r\n this.root = rootEl;\r\n this.options = Object.assign({ onChange: null }, options);\r\n\r\n this._detectConfig();\r\n this._cleanInitialState();\r\n this._bindEvents();\r\n }\r\n\r\n /* Detect elements */\r\n _detectConfig() {\r\n this.list = this.root.querySelector('.ux4g-tab-list');\r\n this.items = Array.from(\r\n this.list.querySelectorAll('.ux4g-tab-item:not(.ux4g-tab-more)')\r\n );\r\n this.moreBtns = Array.from(\r\n this.list.querySelectorAll('.ux4g-tab-more')\r\n );\r\n this.panels = Array.from(\r\n this.root.querySelectorAll('.ux4g-tab-panel')\r\n );\r\n }\r\n\r\n /* Clean any static open state */\r\n _cleanInitialState() {\r\n this.root.querySelectorAll('.ux4g-tab-dropdown-list.is-open')\r\n .forEach(d => d.classList.remove('is-open'));\r\n }\r\n\r\n /* Bind events */\r\n _bindEvents() {\r\n\r\n /* Regular tabs */\r\n this.items.forEach(item => {\r\n item.addEventListener('click', () => this._activateItem(item));\r\n item.addEventListener('keydown', (e) => {\r\n if (e.key === 'Enter') {\r\n e.preventDefault();\r\n this._activateItem(item);\r\n }\r\n });\r\n });\r\n\r\n /* More dropdown */\r\n this.moreBtns.forEach(moreBtn => {\r\n const dropdown = moreBtn.querySelector('.ux4g-tab-dropdown-list');\r\n if (!dropdown) return;\r\n\r\n moreBtn.addEventListener('click', (e) => {\r\n const isDropdownClick = e.target.closest('.ux4g-tab-dropdown-list');\r\n\r\n if (!isDropdownClick) {\r\n e.stopPropagation();\r\n this._toggleDropdown(dropdown);\r\n }\r\n });\r\n\r\n // Add Enter key support to open dropdown\r\n moreBtn.addEventListener('keydown', (e) => {\r\n if (e.key === 'Enter') {\r\n e.preventDefault();\r\n const isDropdownClick = e.target.closest('.ux4g-tab-dropdown-list');\r\n if (!isDropdownClick) {\r\n e.stopPropagation();\r\n this._toggleDropdown(dropdown);\r\n }\r\n }\r\n });\r\n\r\n dropdown.querySelectorAll('.ux4g-tab-dropdown-item')\r\n .forEach(dItem => {\r\n dItem.addEventListener('click', (e) => {\r\n e.stopPropagation();\r\n this._activateDropdownItem(dItem, moreBtn);\r\n });\r\n dItem.addEventListener('keydown', (e) => {\r\n if (e.key === 'Enter') {\r\n e.preventDefault();\r\n e.stopPropagation();\r\n this._activateDropdownItem(dItem, moreBtn);\r\n }\r\n });\r\n });\r\n });\r\n\r\n /* Outside click (scoped) */\r\n document.addEventListener('click', (e) => {\r\n if (!this.root.contains(e.target)) {\r\n this._closeAllDropdowns();\r\n }\r\n });\r\n }\r\n\r\n /* Activate normal tab */\r\n _activateItem(item) {\r\n if (item.classList.contains('ux4g-tab-item-disabled')) return;\r\n\r\n this._resetActive();\r\n item.classList.add('is-active');\r\n\r\n const panelId = item.dataset.panel;\r\n if (panelId) this._showPanel(panelId);\r\n\r\n this._closeAllDropdowns();\r\n this._emitChange(panelId);\r\n }\r\n\r\n /* Activate dropdown item */\r\n _activateDropdownItem(dItem, moreBtn) {\r\n this._resetActive();\r\n\r\n moreBtn.classList.add('is-active');\r\n dItem.classList.add('is-active');\r\n\r\n const panelId = dItem.dataset.panel;\r\n if (panelId) this._showPanel(panelId);\r\n\r\n this._closeAllDropdowns();\r\n this._emitChange(panelId);\r\n }\r\n\r\n /* Reset active states */\r\n _resetActive() {\r\n this.list.querySelectorAll('.ux4g-tab-item')\r\n .forEach(i => i.classList.remove('is-active'));\r\n\r\n this.root.querySelectorAll('.ux4g-tab-dropdown-item')\r\n .forEach(i => i.classList.remove('is-active'));\r\n }\r\n\r\n /* Show panel */\r\n _showPanel(panelId) {\r\n this.panels.forEach(p => p.classList.remove('is-active'));\r\n const target = this.root.querySelector('#' + panelId);\r\n if (target) target.classList.add('is-active');\r\n }\r\n\r\n /* Toggle dropdown */\r\n _toggleDropdown(dropdown) {\r\n const isOpen = dropdown.classList.contains('is-open');\r\n this._closeAllDropdowns();\r\n\r\n if (!isOpen) {\r\n dropdown.classList.add('is-open');\r\n }\r\n }\r\n\r\n /* Close dropdowns (scoped) */\r\n _closeAllDropdowns() {\r\n this.root.querySelectorAll('.ux4g-tab-dropdown-list.is-open')\r\n .forEach(d => d.classList.remove('is-open'));\r\n }\r\n\r\n /* Emit change */\r\n _emitChange(panelId) {\r\n if (typeof this.options.onChange === 'function') {\r\n this.options.onChange(panelId);\r\n }\r\n }\r\n\r\n /* Init all */\r\n static initAll(scope = document) {\r\n return Array.from(scope.querySelectorAll('[data-ux4g-tab]'))\r\n .map(el => new UX4GTab(el));\r\n }\r\n }\r\n\r\n global.UX4GTab = UX4GTab;\r\n\r\n})(window);\r\n\r\n/* Auto init */\r\ndocument.addEventListener('DOMContentLoaded', () => {\r\n UX4GTab.initAll();\r\n});\r\n\r\n/* ========================================================= slider js ========================================================= */\r\n\r\ndocument.addEventListener(\"DOMContentLoaded\", () => {\r\n\r\n /* =========================================================\r\n HELPERS\r\n ========================================================= */\r\n\r\n const pct = (val, min, max) => ((val - min) / (max - min)) * 100;\r\n\r\n const buildSteps = (container, min, max, step) => {\r\n if (!container) return;\r\n\r\n let html = \"\";\r\n\r\n for (let v = min; v <= max; v += step) {\r\n const p = pct(v, min, max);\r\n\r\n html += `\r\n <div class=\"ux4g-slider-step\" data-value=\"${v}\" style=\"left:${p}%\">\r\n <span class=\"ux4g-slider-step-mark\"></span>\r\n <span class=\"ux4g-slider-step-label\">${v}</span>\r\n </div>\r\n `;\r\n }\r\n\r\n container.innerHTML = html;\r\n };\r\n\r\n /* =========================================================\r\n SINGLE SLIDERS\r\n ========================================================= */\r\n\r\n const singleSliders = document.querySelectorAll(\r\n \".ux4g-slider:not(.ux4g-slider-dual)\"\r\n );\r\n\r\n singleSliders.forEach((slider) => {\r\n\r\n const input = slider.querySelector(\".ux4g-slider-input\");\r\n const fill = slider.querySelector(\".ux4g-slider-fill\");\r\n const thumb = slider.querySelector(\".ux4g-slider-thumb\");\r\n const steps = slider.querySelector(\".ux4g-slider-steps\");\r\n\r\n if (!input) return;\r\n\r\n const valueBox =\r\n slider.closest(\".ux4g-slider-field\")?.querySelector(\r\n \".ux4g-slider-range-box\"\r\n );\r\n const valueBadge =\r\n slider.closest(\".ux4g-slider-field\")?.querySelector(\r\n \".ux4g-slider-value-badge\"\r\n );\r\n\r\n const min = parseFloat(input.min) || 0;\r\n const max = parseFloat(input.max) || 100;\r\n const step = parseFloat(input.step) || 10;\r\n\r\n /* Helper: Make badge editable */\r\n const initBadgeEdit = (badge, targetInput) => {\r\n if (!badge) return;\r\n badge.setAttribute(\"contenteditable\", \"true\");\r\n badge.style.cursor = \"text\";\r\n\r\n // Strict number only entry\r\n badge.addEventListener(\"keypress\", (e) => {\r\n if (!/[0-9]/.test(e.key) && e.key !== \"Enter\") {\r\n e.preventDefault();\r\n }\r\n });\r\n \r\n badge.addEventListener(\"keydown\", (e) => {\r\n if (e.key === \"Enter\") {\r\n e.preventDefault();\r\n badge.blur();\r\n }\r\n });\r\n\r\n badge.addEventListener(\"blur\", () => {\r\n let raw = badge.textContent.replace(/[^0-9]/g, \"\");\r\n let val = parseInt(raw, 10);\r\n if (isNaN(val)) val = parseFloat(targetInput.value);\r\n \r\n // Clamp 0-100\r\n val = Math.min(max, Math.max(min, val));\r\n \r\n targetInput.value = val;\r\n // Trigger input event to update everything\r\n targetInput.dispatchEvent(new Event(\"input\"));\r\n \r\n // Re-apply suffix if needed\r\n const suffix = badge.textContent.includes(\"%\") ? \"%\" : \"\";\r\n badge.textContent = val + suffix;\r\n });\r\n };\r\n\r\n if (valueBox) initBadgeEdit(valueBox, input);\r\n\r\n /* Build Steps */\r\n\r\n buildSteps(steps, min, max, step);\r\n\r\n const stepEls = slider.querySelectorAll(\".ux4g-slider-step\");\r\n\r\n const update = () => {\r\n\r\n const val = parseFloat(input.value);\r\n const p = pct(val, min, max);\r\n\r\n if (fill) fill.style.width = p + \"%\";\r\n if (thumb) thumb.style.left = p + \"%\";\r\n\r\n // Only update text containers if not currently editing\r\n const containers = [valueBadge, valueBox];\r\n containers.forEach(container => {\r\n if (container && document.activeElement !== container) {\r\n const suffix = container.textContent.includes(\"%\") ? \"%\" : \"\";\r\n container.textContent = val + suffix;\r\n }\r\n });\r\n\r\n stepEls.forEach((el) => {\r\n const sv = parseFloat(el.dataset.value);\r\n el.classList.toggle(\"is-active\", sv <= val);\r\n });\r\n\r\n };\r\n\r\n input.addEventListener(\"input\", update);\r\n\r\n update();\r\n\r\n });\r\n\r\n /* =========================================================\r\n DUAL RANGE SLIDERS\r\n ========================================================= */\r\n\r\n const dualSliders = document.querySelectorAll(\".ux4g-slider-dual\");\r\n\r\n dualSliders.forEach((slider) => {\r\n\r\n const inputMin = slider.querySelector(\".ux4g-slider-input-min\");\r\n const inputMax = slider.querySelector(\".ux4g-slider-input-max\");\r\n\r\n const fill = slider.querySelector(\".ux4g-slider-fill\");\r\n const thumbMin = slider.querySelector(\".ux4g-slider-thumb-min\");\r\n const thumbMax = slider.querySelector(\".ux4g-slider-thumb-max\");\r\n\r\n const steps = slider.querySelector(\".ux4g-slider-steps\");\r\n\r\n const field = slider.closest(\".ux4g-slider-field\");\r\n\r\n const minBox = field?.querySelector(\".ux4g-slider-range-box:first-of-type, .ux4g-slider-range-box\");\r\n // Actually better to select all and differentiate if multiple exist\r\n const rangeBoxes = field?.querySelectorAll(\".ux4g-slider-range-box\");\r\n const valueBadges = field?.querySelectorAll(\".ux4g-slider-value-badge\");\r\n\r\n if (!inputMin || !inputMax) return;\r\n\r\n const min = parseFloat(inputMin.min) || 0;\r\n const max = parseFloat(inputMin.max) || 100;\r\n const step = parseFloat(inputMin.step) || 10;\r\n\r\n /* Helper: Make box editable */\r\n const initBoxEdit = (badge, targetInput) => {\r\n if (!badge) return;\r\n badge.setAttribute(\"contenteditable\", \"true\");\r\n badge.style.cursor = \"text\";\r\n \r\n // Strict number only entry\r\n badge.addEventListener(\"keypress\", (e) => {\r\n if (!/[0-9]/.test(e.key) && e.key !== \"Enter\") {\r\n e.preventDefault();\r\n }\r\n });\r\n\r\n badge.addEventListener(\"keydown\", (e) => {\r\n if (e.key === \"Enter\") {\r\n e.preventDefault();\r\n badge.blur();\r\n }\r\n });\r\n\r\n badge.addEventListener(\"blur\", () => {\r\n let raw = badge.textContent.replace(/[^0-9]/g, \"\");\r\n let val = parseInt(raw, 10);\r\n if (isNaN(val)) val = parseFloat(targetInput.value);\r\n \r\n // Clamp min-max\r\n val = Math.min(max, Math.max(min, val));\r\n \r\n targetInput.value = val;\r\n // Trigger change to validate cross limits\r\n targetInput.dispatchEvent(new Event(\"input\"));\r\n\r\n // Re-apply suffix\r\n const suffix = badge.textContent.includes(\"%\") ? \"%\" : \"\";\r\n badge.textContent = val + suffix;\r\n });\r\n };\r\n\r\n if (rangeBoxes?.[0]) initBoxEdit(rangeBoxes[0], inputMin);\r\n if (rangeBoxes?.[1]) initBoxEdit(rangeBoxes[1], inputMax);\r\n\r\n buildSteps(steps, min, max, step);\r\n\r\n const stepEls = slider.querySelectorAll(\".ux4g-slider-step\");\r\n\r\n const update = (e) => {\r\n\r\n let vMin = parseFloat(inputMin.value);\r\n let vMax = parseFloat(inputMax.value);\r\n\r\n /* Prevent crossing */\r\n\r\n if (e?.target === inputMin && vMin >= vMax) {\r\n inputMin.value = vMax - step;\r\n vMin = parseFloat(inputMin.value);\r\n }\r\n\r\n if (e?.target === inputMax && vMax <= vMin) {\r\n inputMax.value = vMin + step;\r\n vMax = parseFloat(inputMax.value);\r\n }\r\n\r\n const pMin = pct(vMin, min, max);\r\n const pMax = pct(vMax, min, max);\r\n\r\n if (fill) {\r\n fill.style.left = pMin + \"%\";\r\n fill.style.width = (pMax - pMin) + \"%\";\r\n }\r\n\r\n if (thumbMin) thumbMin.style.left = pMin + \"%\";\r\n if (thumbMax) thumbMax.style.left = pMax + \"%\";\r\n\r\n // Sync dual badges\r\n if (valueBadges?.[0] && document.activeElement !== valueBadges[0]) {\r\n valueBadges[0].textContent = vMin + (valueBadges[0].textContent.includes(\"%\") ? \"%\" : \"\");\r\n }\r\n if (valueBadges?.[1] && document.activeElement !== valueBadges[1]) {\r\n valueBadges[1].textContent = vMax + (valueBadges[1].textContent.includes(\"%\") ? \"%\" : \"\");\r\n }\r\n\r\n // Sync dual boxes\r\n if (rangeBoxes?.[0] && document.activeElement !== rangeBoxes[0]) {\r\n rangeBoxes[0].textContent = vMin + (rangeBoxes[0].textContent.includes(\"%\") ? \"%\" : \"\");\r\n }\r\n if (rangeBoxes?.[1] && document.activeElement !== rangeBoxes[1]) {\r\n rangeBoxes[1].textContent = vMax + (rangeBoxes[1].textContent.includes(\"%\") ? \"%\" : \"\");\r\n }\r\n \r\n /* Thumb overlap fix */\r\n\r\n if (pMax - pMin < 10) {\r\n\r\n inputMin.style.zIndex = pMin > 50 ? \"5\" : \"3\";\r\n inputMax.style.zIndex = pMin > 50 ? \"3\" : \"5\";\r\n\r\n } else {\r\n\r\n inputMin.style.zIndex = \"\";\r\n inputMax.style.zIndex = \"\";\r\n\r\n }\r\n\r\n /* Active steps */\r\n\r\n stepEls.forEach((el) => {\r\n\r\n const sv = parseFloat(el.dataset.value);\r\n\r\n el.classList.toggle(\r\n \"is-active\",\r\n sv >= vMin && sv <= vMax\r\n );\r\n\r\n });\r\n\r\n };\r\n\r\n inputMin.addEventListener(\"input\", update);\r\n inputMax.addEventListener(\"input\", update);\r\n\r\n update();\r\n\r\n });\r\n\r\n});\r\n\r\n\r\n/* ========================================================= context alert js ========================================================= */\r\n/* Trigger Toast alerts via event delegation (guard against duplicate handlers) */\r\nif (!window._ux4gToastInit) {\r\n window._ux4gToastInit = true;\r\n document.addEventListener('click', (e) => {\r\n const btn = e.target.closest('[data-ux4g-toggle=\"toast\"]');\r\n if (!btn) return;\r\n const position = btn.dataset.ux4gPosition || 'top-right';\r\n const variant = btn.dataset.ux4gVariant || btn.dataset.ux4gStatus;\r\n const title = btn.dataset.ux4gTitle;\r\n const body = btn.dataset.ux4gBody;\r\n showContextAlert(position, variant, title, body);\r\n });\r\n}\r\n\r\nlet alertCount = 0;\r\n\r\n/**\r\n * Shows a Context Alert (Toast) in the specified position with animation\r\n * @param {string} position - 'top-left', 'top-right', 'bottom-left', or 'bottom-right'\r\n * @param {string} [variant] - Optional: 'info', 'success', 'warning', 'error', 'none'\r\n * @param {string} [customTitle] - Optional custom title text\r\n * @param {string} [customBody] - Optional custom body text\r\n */\r\nfunction showContextAlert(position, variant, customTitle, customBody) {\r\n const statuses = {\r\n 'info': { icon: 'info', title: 'Info ' },\r\n 'success': { icon: 'check_circle', title: 'Success ' },\r\n 'warning': { icon: 'warning', title: 'Warning ' },\r\n 'error': { icon: 'error', title: 'Error ' },\r\n 'none': { icon: null, title: 'Alert Title' }\r\n };\r\n\r\n // Determine type (use variant, or cycle through if not provided)\r\n const types = Object.keys(statuses).filter(t => t !== 'none');\r\n const type = variant || types[alertCount++ % types.length];\r\n const status = statuses[type] || statuses['info'];\r\n\r\n const title = customTitle || status.title;\r\n const bodyText = customBody || `This is a ${type} alert shown at the ${position.replace('-', ' ')} corner.`;\r\n\r\n const containerId = `ux4g-alert-container-${position}`;\r\n let container = document.getElementById(containerId);\r\n \r\n if (!container) {\r\n container = document.createElement('div');\r\n container.id = containerId;\r\n container.className = `ux4g-alert-container ux4g-alert-${position}`;\r\n document.body.appendChild(container);\r\n }\r\n\r\n const alert = document.createElement('div');\r\n const animationClass = position.includes('left') ? 'ux4g-animate-left' : 'ux4g-animate-right';\r\n \r\n // Apply correct status class (fallback to info if variant is 'none' for styling)\r\n const statusClass = type === 'none' ? 'info' : type;\r\n alert.className = `ux4g-context-alert ux4g-alert-${statusClass} ${animationClass}`;\r\n \r\n // Icon Logic: None means no icon HTML\r\n const iconHtml = status.icon ? `<i class=\"ux4g-icon ux4g-alert-icon\">${status.icon}</i>` : '';\r\n\r\n alert.innerHTML = `\r\n ${iconHtml}\r\n <span class=\"ux4g-alert-title\">${title}</span>\r\n <div class=\"ux4g-alert-actions\">\r\n <button class=\"ux4g-alert-close\" onclick=\"closeContextAlert(this)\">\r\n <i class=\"ux4g-icon\">close</i>\r\n </button>\r\n </div>\r\n <div class=\"ux4g-alert-message\">${bodyText}</div>\r\n `;\r\n\r\n if (position.includes('bottom')) {\r\n container.insertBefore(alert, container.firstChild);\r\n } else {\r\n container.appendChild(alert);\r\n }\r\n\r\n setTimeout(() => {\r\n if (alert.parentNode) closeAlertWithAnimation(alert);\r\n }, 5000);\r\n}\r\n\r\n/**\r\n * Handles manual close click\r\n */\r\nfunction closeContextAlert(button) {\r\n const alert = button.closest('.ux4g-context-alert');\r\n if (alert) {\r\n closeAlertWithAnimation(alert);\r\n }\r\n}\r\n\r\n/**\r\n * Closes an alert with a slide-out animation\r\n */\r\nfunction closeAlertWithAnimation(alert) {\r\n if (!alert) return;\r\n \r\n const isLeft = alert.classList.contains('ux4g-animate-left');\r\n alert.style.transition = 'all 0.4s cubic-bezier(0.16, 1, 0.3, 1)';\r\n alert.style.transform = isLeft ? 'translateX(-100%)' : 'translateX(100%)';\r\n alert.style.opacity = '0';\r\n \r\n // Remove element after animation completes\r\n setTimeout(() => {\r\n if (alert.parentNode) {\r\n alert.parentNode.removeChild(alert);\r\n }\r\n }, 400);\r\n}\r\n\r\n\r\n/* ========================================================= pagination js ========================================================= */\r\ndocument.addEventListener(\"DOMContentLoaded\", () => {\r\n const pageSelects = document.querySelectorAll(\".ux4g-page-size select\");\r\n \r\n pageSelects.forEach(select => {\r\n const wrapper = select.closest(\".ux4g-page-size-select-wrapper\");\r\n \r\n const updateState = () => {\r\n if (wrapper) {\r\n // If there's a selected option that isn't empty (or for simplicity, any selection)\r\n if (select.value) {\r\n wrapper.classList.add(\"has-value\");\r\n } else {\r\n wrapper.classList.remove(\"has-value\");\r\n }\r\n }\r\n };\r\n\r\n select.addEventListener(\"change\", updateState);\r\n // Do not run on load, so we show the placeholder icon by default\r\n });\r\n});\r\n\r\n\r\n// NPS and Emoji Button interactions\r\ndocument.addEventListener('DOMContentLoaded', () => {\r\n // NPS interaction\r\n const npsButtons = document.querySelectorAll('.feedback-nps-button');\r\n npsButtons.forEach(btn => {\r\n btn.addEventListener('click', () => {\r\n const container = btn.closest('.ux4g-feedback-nps-wrapper') || btn.parentElement;\r\n const siblings = Array.from(container.querySelectorAll('.feedback-nps-button'));\r\n const clickedIndex = siblings.indexOf(btn);\r\n \r\n // If clicking the highest active button, reset it (toggle off)\r\n const isHighestActive = btn.classList.contains('active') && \r\n (clickedIndex === siblings.length - 1 || !siblings[clickedIndex + 1]?.classList.contains('active'));\r\n \r\n if (isHighestActive) {\r\n siblings.forEach(s => s.classList.remove('active'));\r\n container.removeAttribute('data-nps-rating');\r\n } else {\r\n container.setAttribute('data-nps-rating', clickedIndex);\r\n siblings.forEach((s, i) => {\r\n if (i <= clickedIndex) {\r\n s.classList.add('active');\r\n } else {\r\n s.classList.remove('active');\r\n }\r\n });\r\n }\r\n });\r\n });\r\n\r\n // Emoji interaction\r\n const emojiButtons = document.querySelectorAll('.feedback-emoji-button');\r\n emojiButtons.forEach(btn => {\r\n btn.addEventListener('click', () => {\r\n const wasActive = btn.classList.contains('active');\r\n const container = btn.closest('.ux4g-d-flex') || document;\r\n container.querySelectorAll('.feedback-emoji-button').forEach(b => b.classList.remove('active'));\r\n \r\n if (!wasActive) {\r\n btn.classList.add('active');\r\n }\r\n });\r\n });\r\n\r\n // Star interaction\r\n const stars = document.querySelectorAll('.ux4g-feedback-star');\r\n stars.forEach(star => {\r\n star.addEventListener('click', () => {\r\n const container = star.parentElement;\r\n const siblings = Array.from(container.querySelectorAll('.ux4g-feedback-star'));\r\n const clickedIndex = siblings.indexOf(star);\r\n \r\n // If clicking the only active star, reset it (toggle off)\r\n const isOnlyActive = star.classList.contains('active') && \r\n (clickedIndex === siblings.length - 1 || !siblings[clickedIndex + 1].classList.contains('active'));\r\n \r\n if (isOnlyActive) {\r\n siblings.forEach(s => s.classList.remove('active'));\r\n container.removeAttribute('data-rating');\r\n } else {\r\n container.setAttribute('data-rating', clickedIndex + 1);\r\n siblings.forEach((s, i) => {\r\n if (i <= clickedIndex) {\r\n s.classList.add('active');\r\n } else {\r\n s.classList.remove('active');\r\n }\r\n });\r\n }\r\n });\r\n });\r\n\r\n // Submit and Skip Reset interaction\r\n const resetButtons = document.querySelectorAll('.ux4g-feedback .ux4g-btn-primary, .ux4g-feedback .ux4g-btn-text-primary');\r\n resetButtons.forEach(btn => {\r\n btn.addEventListener('click', () => {\r\n const feedbackContainer = btn.closest('.ux4g-feedback');\r\n if (feedbackContainer) {\r\n // Clear textareas\r\n feedbackContainer.querySelectorAll('textarea').forEach(textarea => {\r\n textarea.value = '';\r\n });\r\n // Clear active states on all feedback interactive elements\r\n feedbackContainer.querySelectorAll('.active').forEach(activeEl => {\r\n activeEl.classList.remove('active');\r\n });\r\n }\r\n });\r\n });\r\n});\r\n\r\n\r\n\r\n/* ========================================================= carousel js ========================================================= */\r\ndocument.addEventListener(\"DOMContentLoaded\", () => {\r\n const carousels = document.querySelectorAll(\".ux4g-carousel\");\r\n\r\n carousels.forEach(carousel => {\r\n const slidesContainer = carousel.querySelector(\".ux4g-carousel-slides\");\r\n const slides = carousel.querySelectorAll(\".ux4g-carousel-slide\");\r\n const slideCount = slides.length;\r\n const prevBtn = carousel.querySelector(\".ux4g-carousel-arrow-prev\");\r\n const nextBtn = carousel.querySelector(\".ux4g-carousel-arrow-next\");\r\n const dots = carousel.querySelectorAll(\".ux4g-carousel-dot\");\r\n \r\n if (slideCount === 0) return;\r\n\r\n let currentIndex = 0;\r\n\r\n // Initialize: find if any slide is already marked as active\r\n slides.forEach((slide, index) => {\r\n if (slide.classList.contains(\"is-active\")) {\r\n currentIndex = index;\r\n }\r\n });\r\n\r\n const updateCarousel = (index) => {\r\n // Handle looping\r\n if (index < 0) {\r\n index = slideCount - 1;\r\n } else if (index >= slideCount) {\r\n index = 0;\r\n }\r\n \r\n currentIndex = index;\r\n \r\n // Move slides\r\n if (slidesContainer) {\r\n slidesContainer.style.transform = `translateX(-${currentIndex * 100}%)`;\r\n }\r\n \r\n // Update slides active state\r\n slides.forEach((slide, i) => {\r\n slide.classList.toggle(\"is-active\", i === currentIndex);\r\n slide.setAttribute(\"aria-hidden\", i !== currentIndex);\r\n });\r\n\r\n // Update dots active state\r\n dots.forEach((dot, i) => {\r\n dot.classList.toggle(\"is-active\", i === currentIndex);\r\n dot.setAttribute(\"aria-current\", i === currentIndex ? \"step\" : \"false\");\r\n });\r\n };\r\n\r\n // Event Listeners\r\n if (prevBtn) {\r\n prevBtn.addEventListener(\"click\", (e) => {\r\n e.preventDefault();\r\n updateCarousel(currentIndex - 1);\r\n });\r\n }\r\n\r\n if (nextBtn) {\r\n nextBtn.addEventListener(\"click\", (e) => {\r\n e.preventDefault();\r\n updateCarousel(currentIndex + 1);\r\n });\r\n }\r\n\r\n dots.forEach((dot, index) => {\r\n dot.addEventListener(\"click\", (e) => {\r\n e.preventDefault();\r\n updateCarousel(index);\r\n });\r\n });\r\n\r\n // Initial update to ensure everything is synced\r\n updateCarousel(currentIndex);\r\n });\r\n});\r\n\r\n/* ========================================================= range slider js ========================================================= */\r\ndocument.addEventListener(\"input\", (e) => {\r\n if (e.target.classList.contains(\"ux4g-slider-input\")) {\r\n const sliderField = e.target.closest(\".ux4g-slider-field\");\r\n const slider = e.target.closest(\".ux4g-slider\");\r\n if (!slider) return;\r\n\r\n const isDual = slider.classList.contains(\"ux4g-slider-dual\");\r\n const fill = slider.querySelector(\".ux4g-slider-fill\");\r\n \r\n if (!isDual) {\r\n const thumb = slider.querySelector(\".ux4g-slider-thumb\");\r\n const percent = ((e.target.value - e.target.min) / (e.target.max - e.target.min)) * 100;\r\n if (fill) fill.style.width = percent + \"%\";\r\n if (thumb) thumb.style.left = percent + \"%\";\r\n \r\n if (sliderField) {\r\n const badge = sliderField.querySelector(\".ux4g-slider-value-badge\");\r\n if (badge) badge.textContent = e.target.value + \"%\";\r\n }\r\n } else {\r\n const inputMin = slider.querySelector(\".ux4g-slider-input-min\");\r\n const inputMax = slider.querySelector(\".ux4g-slider-input-max\");\r\n const thumbMin = slider.querySelector(\".ux4g-slider-thumb-min\");\r\n const thumbMax = slider.querySelector(\".ux4g-slider-thumb-max\");\r\n \r\n let min = parseFloat(inputMin.value);\r\n let max = parseFloat(inputMax.value);\r\n const rangeMin = parseFloat(inputMin.min);\r\n const rangeMax = parseFloat(inputMin.max);\r\n \r\n if (e.target.classList.contains(\"ux4g-slider-input-min\")) {\r\n if (min > max) {\r\n min = max;\r\n inputMin.value = min;\r\n }\r\n } else {\r\n if (max < min) {\r\n max = min;\r\n inputMax.value = max;\r\n }\r\n }\r\n \r\n const left = ((min - rangeMin) / (rangeMax - rangeMin)) * 100;\r\n const width = ((max - min) / (rangeMax - rangeMin)) * 100;\r\n \r\n if (fill) {\r\n fill.style.left = left + \"%\";\r\n fill.style.width = width + \"%\";\r\n }\r\n if (thumbMin) thumbMin.style.left = left + \"%\";\r\n if (thumbMax) thumbMax.style.left = (left + width) + \"%\";\r\n \r\n if (sliderField) {\r\n const badges = sliderField.querySelectorAll(\".ux4g-slider-value-badge\");\r\n if (badges.length >= 2) {\r\n badges[0].textContent = min + \"%\";\r\n badges[1].textContent = max + \"%\";\r\n }\r\n }\r\n }\r\n }\r\n});\r\n\r\n\r\n/********************************* UX4G DatePicker & TimePicker JS ***********************************/\r\n\r\n(function (global) {\r\n \"use strict\";\r\n\r\n console.log('UX4G Components Script Loaded');\r\n\r\n const makeKeyboardClickable = (el) => {\r\n if (!el) return;\r\n if (el.tagName !== 'BUTTON' && el.tagName !== 'INPUT') {\r\n if (!el.hasAttribute('tabindex')) el.setAttribute('tabindex', '0');\r\n el.addEventListener('keydown', (e) => {\r\n if (e.key === 'Enter' || e.key === ' ') {\r\n e.preventDefault();\r\n el.click();\r\n }\r\n });\r\n }\r\n };\r\n\r\n // Shared Backdrop\r\n let backdrop = document.querySelector('.ux4g-date-picker-backdrop');\r\n const getBackdrop = () => {\r\n if (!backdrop) {\r\n backdrop = document.createElement('div');\r\n backdrop.className = 'ux4g-date-picker-backdrop'; \r\n document.body.appendChild(backdrop);\r\n }\r\n return backdrop;\r\n };\r\n\r\n const isMobile = () => window.innerWidth <= 576;\r\n\r\n class DatePicker {\r\n constructor(container) {\r\n this.container = container;\r\n this.input = container.querySelector('.ux4g-date-picker-input');\r\n this.dropdown = container.querySelector('.ux4g-date-picker-dropdown');\r\n this.calendarGrid = container.querySelector('.ux4g-date-picker-grid');\r\n this.monthLabel = container.querySelector('.ux4g-date-picker-current');\r\n \r\n const navBtns = container.querySelectorAll('.ux4g-date-picker-nav-btn');\r\n this.prevBtn = navBtns[0];\r\n this.nextBtn = navBtns[1];\r\n \r\n this.confirmBtn = container.querySelector('.ux4g-btn-primary');\r\n this.cancelBtn = container.querySelector('.ux4g-btn-outline-neutral');\r\n \r\n this.currentDate = new Date();\r\n this.viewDate = new Date(this.currentDate.getFullYear(), this.currentDate.getMonth(), 1);\r\n this.selectedDate = null;\r\n this.tempSelectedDate = null;\r\n \r\n this.isSelectingYearMonth = false;\r\n \r\n this._init();\r\n }\r\n\r\n _init() {\r\n if (!this.input || !this.dropdown) return;\r\n\r\n if (this.input.value) {\r\n const parts = this.input.value.split('/');\r\n if (parts.length === 3) {\r\n this.selectedDate = new Date(parts[2], parts[1] - 1, parts[0]);\r\n this.viewDate = new Date(parts[2], parts[1] - 1, 1);\r\n this.tempSelectedDate = new Date(this.selectedDate);\r\n }\r\n }\r\n\r\n // Keyboard accessibility\r\n this.input.addEventListener('keydown', (e) => {\r\n if (e.key === 'Enter' || e.key === ' ') {\r\n e.preventDefault();\r\n this.open();\r\n }\r\n });\r\n\r\n this.input.addEventListener('focus', (e) => {\r\n this.open();\r\n });\r\n\r\n this.input.addEventListener('click', (e) => {\r\n e.stopPropagation();\r\n this.open();\r\n });\r\n \r\n if (this.prevBtn) {\r\n makeKeyboardClickable(this.prevBtn);\r\n this.prevBtn.addEventListener('click', (e) => {\r\n e.stopPropagation();\r\n if (this.isSelectingYearMonth) {\r\n this.changeYearRange(-8);\r\n } else {\r\n this.changeMonth(-1);\r\n }\r\n });\r\n }\r\n \r\n if (this.nextBtn) {\r\n makeKeyboardClickable(this.nextBtn);\r\n this.nextBtn.addEventListener('click', (e) => {\r\n e.stopPropagation();\r\n if (this.isSelectingYearMonth) {\r\n this.changeYearRange(8);\r\n } else {\r\n this.changeMonth(1);\r\n }\r\n });\r\n }\r\n\r\n if (this.monthLabel) {\r\n makeKeyboardClickable(this.monthLabel);\r\n this.monthLabel.addEventListener('click', (e) => {\r\n e.stopPropagation();\r\n this.isSelectingYearMonth = !this.isSelectingYearMonth;\r\n this.render();\r\n setTimeout(() => {\r\n if (this.isSelectingYearMonth) {\r\n const calendarContainer = this.container.querySelector('.ux4g-date-picker-calendar');\r\n let el = calendarContainer.querySelector('.ux4g-date-picker-year-item.is-selected') || calendarContainer.querySelector('.ux4g-date-picker-year-item');\r\n if (el) el.focus();\r\n } else {\r\n let el = this.calendarGrid.querySelector('.is-selected') || this.calendarGrid.querySelector('.is-today') || this.calendarGrid.querySelector('.ux4g-date-picker-day:not(.is-muted)');\r\n if (el) el.focus();\r\n }\r\n }, 0);\r\n });\r\n }\r\n\r\n if (this.confirmBtn) {\r\n makeKeyboardClickable(this.confirmBtn);\r\n this.confirmBtn.addEventListener('click', (e) => {\r\n e.stopPropagation();\r\n if (this.isSelectingYearMonth) {\r\n this.isSelectingYearMonth = false;\r\n this.render();\r\n setTimeout(() => {\r\n let el = this.calendarGrid.querySelector('.is-selected') || this.calendarGrid.querySelector('.is-today') || this.calendarGrid.querySelector('.ux4g-date-picker-day:not(.is-muted)');\r\n if (el) el.focus();\r\n }, 0);\r\n } else {\r\n this.confirmSelection();\r\n }\r\n });\r\n }\r\n\r\n if (this.cancelBtn) {\r\n makeKeyboardClickable(this.cancelBtn);\r\n this.cancelBtn.addEventListener('click', (e) => {\r\n e.stopPropagation();\r\n this.cancelSelection();\r\n });\r\n }\r\n\r\n document.addEventListener('click', (e) => {\r\n if (!this.container.contains(e.target) && !getBackdrop().contains(e.target)) {\r\n this.close();\r\n }\r\n });\r\n\r\n getBackdrop().addEventListener('click', () => {\r\n this.close();\r\n });\r\n\r\n this.render();\r\n }\r\n\r\n open() {\r\n if (this.dropdown) {\r\n this.tempSelectedDate = this.selectedDate ? new Date(this.selectedDate) : null;\r\n this.isSelectingYearMonth = false;\r\n this.dropdown.classList.add('is-open');\r\n if (isMobile()) {\r\n getBackdrop().classList.add('is-active');\r\n document.body.style.overflow = 'hidden';\r\n }\r\n this.render();\r\n }\r\n }\r\n\r\n close() {\r\n if (this.dropdown) {\r\n this.dropdown.classList.remove('is-open');\r\n getBackdrop().classList.remove('is-active');\r\n document.body.style.overflow = '';\r\n }\r\n }\r\n\r\n confirmSelection() {\r\n this.selectedDate = this.tempSelectedDate ? new Date(this.tempSelectedDate) : null;\r\n if (this.selectedDate) {\r\n const day = String(this.selectedDate.getDate()).padStart(2, '0');\r\n const month = String(this.selectedDate.getMonth() + 1).padStart(2, '0');\r\n const year = this.selectedDate.getFullYear();\r\n this.input.value = `${day}/${month}/${year}`;\r\n } else {\r\n this.input.value = '';\r\n }\r\n this.close();\r\n }\r\n\r\n cancelSelection() {\r\n this.tempSelectedDate = this.selectedDate ? new Date(this.selectedDate) : null;\r\n this.close();\r\n }\r\n\r\n changeMonth(delta) {\r\n this.viewDate.setMonth(this.viewDate.getMonth() + delta);\r\n this.render();\r\n }\r\n\r\n changeYearRange(delta) {\r\n this.viewDate.setFullYear(this.viewDate.getFullYear() + delta);\r\n this.render();\r\n }\r\n\r\n render() {\r\n if (this.isSelectingYearMonth) {\r\n this.renderYearMonthSelection();\r\n } else {\r\n this.renderCalendar();\r\n }\r\n }\r\n\r\n renderCalendar() {\r\n const year = this.viewDate.getFullYear();\r\n const month = this.viewDate.getMonth();\r\n const monthNames = [\"January\", \"February\", \"March\", \"April\", \"May\", \"June\",\r\n \"July\", \"August\", \"September\", \"October\", \"November\", \"December\"\r\n ];\r\n \r\n if (this.monthLabel) {\r\n this.monthLabel.innerHTML = `${monthNames[month]} ${year} <span class=\"ux4g-icon-outlined ux4g-fs-18\">keyboard_arrow_down</span>`;\r\n }\r\n\r\n const calendarHtml = `\r\n <div class=\"ux4g-date-picker-weekdays\">\r\n <div class=\"ux4g-date-picker-weekday\">Mo</div>\r\n <div class=\"ux4g-date-picker-weekday\">Tu</div>\r\n <div class=\"ux4g-date-picker-weekday\">We</div>\r\n <div class=\"ux4g-date-picker-weekday\">Th</div>\r\n <div class=\"ux4g-date-picker-weekday\">Fr</div>\r\n <div class=\"ux4g-date-picker-weekday\">Sa</div>\r\n <div class=\"ux4g-date-picker-weekday\">Su</div>\r\n </div>\r\n <div class=\"ux4g-date-picker-grid\"></div>\r\n `;\r\n \r\n const calendarContainer = this.container.querySelector('.ux4g-date-picker-calendar');\r\n calendarContainer.innerHTML = calendarHtml;\r\n this.calendarGrid = calendarContainer.querySelector('.ux4g-date-picker-grid');\r\n\r\n const firstDayOfMonth = new Date(year, month, 1).getDay();\r\n const daysInMonth = new Date(year, month + 1, 0).getDate();\r\n let startDay = firstDayOfMonth === 0 ? 6 : firstDayOfMonth - 1;\r\n const prevMonthLastDay = new Date(year, month, 0).getDate();\r\n \r\n let html = '';\r\n for (let i = startDay - 1; i >= 0; i--) {\r\n html += `<div class=\"ux4g-date-picker-day is-muted\">${prevMonthLastDay - i}</div>`;\r\n }\r\n \r\n for (let i = 1; i <= daysInMonth; i++) {\r\n const date = new Date(year, month, i);\r\n const isToday = date.toDateString() === this.currentDate.toDateString();\r\n const isSelected = this.tempSelectedDate && date.toDateString() === this.tempSelectedDate.toDateString();\r\n \r\n let classes = 'ux4g-date-picker-day';\r\n if (isToday) classes += ' is-today';\r\n if (isSelected) classes += ' is-selected';\r\n \r\n html += `<div class=\"${classes}\" data-date=\"${i}\" tabindex=\"0\">${i}</div>`;\r\n }\r\n \r\n const totalCells = 42;\r\n const remainingCells = totalCells - (startDay + daysInMonth);\r\n for (let i = 1; i <= remainingCells; i++) {\r\n html += `<div class=\"ux4g-date-picker-day is-muted\">${i}</div>`;\r\n }\r\n \r\n this.calendarGrid.innerHTML = html;\r\n this.calendarGrid.querySelectorAll('.ux4g-date-picker-day:not(.is-muted)').forEach(dayEl => {\r\n makeKeyboardClickable(dayEl);\r\n dayEl.addEventListener('click', (e) => {\r\n e.stopPropagation();\r\n const day = e.target.dataset.date;\r\n this.selectDate(new Date(year, month, day));\r\n });\r\n });\r\n\r\n if (this.confirmBtn) {\r\n this.confirmBtn.innerHTML = 'Confirm';\r\n this.confirmBtn.disabled = !this.tempSelectedDate;\r\n }\r\n }\r\n\r\n renderYearMonthSelection() {\r\n const currentYear = this.viewDate.getFullYear();\r\n const startYear = Math.floor(currentYear / 8) * 8;\r\n const endYear = startYear + 7;\r\n \r\n if (this.monthLabel) {\r\n this.monthLabel.innerHTML = `${startYear}-${endYear} <span class=\"ux4g-icon-outlined ux4g-fs-18\">keyboard_arrow_down</span>`;\r\n }\r\n\r\n let html = '<div class=\"ux4g-date-picker-selection-view\">';\r\n html += '<div class=\"ux4g-date-picker-year-grid\">';\r\n for (let y = startYear; y <= endYear; y++) {\r\n const isSelected = y === this.viewDate.getFullYear();\r\n html += `<div class=\"ux4g-date-picker-year-item ${isSelected ? 'is-selected' : ''}\" data-year=\"${y}\" tabindex=\"0\">${y}</div>`;\r\n }\r\n html += '</div>';\r\n\r\n const monthNamesShort = [\"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\", \"Jul\", \"Aug\", \"Sep\", \"Oct\", \"Nov\", \"Dec\"];\r\n html += '<div class=\"ux4g-date-picker-month-grid\">';\r\n monthNamesShort.forEach((m, i) => {\r\n const isSelected = i === this.viewDate.getMonth();\r\n html += `<div class=\"ux4g-date-picker-month-item ${isSelected ? 'is-selected' : ''}\" data-month=\"${i}\" tabindex=\"0\">${m}</div>`;\r\n });\r\n html += '</div></div>';\r\n\r\n const calendarContainer = this.container.querySelector('.ux4g-date-picker-calendar');\r\n calendarContainer.innerHTML = html;\r\n\r\n calendarContainer.querySelectorAll('.ux4g-date-picker-year-item').forEach(el => {\r\n makeKeyboardClickable(el);\r\n el.addEventListener('click', (e) => {\r\n e.stopPropagation();\r\n this.viewDate.setFullYear(parseInt(e.target.dataset.year));\r\n this.renderYearMonthSelection();\r\n setTimeout(() => {\r\n const selectedMonth = this.container.querySelector('.ux4g-date-picker-month-item.is-selected');\r\n if (selectedMonth) selectedMonth.focus();\r\n }, 0);\r\n });\r\n });\r\n\r\n calendarContainer.querySelectorAll('.ux4g-date-picker-month-item').forEach(el => {\r\n makeKeyboardClickable(el);\r\n el.addEventListener('click', (e) => {\r\n e.stopPropagation();\r\n this.viewDate.setMonth(parseInt(e.target.dataset.month));\r\n this.renderYearMonthSelection();\r\n if (this.confirmBtn) setTimeout(() => this.confirmBtn.focus(), 0);\r\n });\r\n });\r\n\r\n if (this.confirmBtn) {\r\n this.confirmBtn.innerHTML = 'Select date';\r\n this.confirmBtn.disabled = false;\r\n }\r\n }\r\n\r\n selectDate(date) {\r\n this.tempSelectedDate = date;\r\n this.render();\r\n if (this.confirmBtn && !this.confirmBtn.disabled) {\r\n setTimeout(() => this.confirmBtn.focus(), 0);\r\n }\r\n }\r\n }\r\n\r\n class RangeDatePicker {\r\n constructor(container) {\r\n this.container = container;\r\n this.inputs = container.querySelectorAll('.ux4g-date-picker-input');\r\n this.dropdown = container.querySelector('.ux4g-date-picker-dropdown');\r\n this.calendarGrid = container.querySelector('.ux4g-date-picker-grid');\r\n this.monthLabel = container.querySelector('.ux4g-date-picker-current');\r\n \r\n const navBtns = container.querySelectorAll('.ux4g-date-picker-nav-btn');\r\n this.prevBtn = navBtns[0];\r\n this.nextBtn = navBtns[1];\r\n \r\n this.confirmBtn = container.querySelector('.ux4g-btn-primary');\r\n this.cancelBtn = container.querySelector('.ux4g-btn-outline-neutral');\r\n \r\n this.currentDate = new Date();\r\n this.viewDate = new Date(this.currentDate.getFullYear(), this.currentDate.getMonth(), 1);\r\n this.startDate = null;\r\n this.endDate = null;\r\n this.tempStartDate = null;\r\n this.tempEndDate = null;\r\n this.selectingEnd = false;\r\n this.isSelectingYearMonth = false;\r\n \r\n this._init();\r\n }\r\n\r\n _init() {\r\n if (!this.inputs.length || !this.dropdown) return;\r\n\r\n this.inputs.forEach(input => {\r\n // Keyboard accessibility\r\n input.addEventListener('keydown', (e) => {\r\n if (e.key === 'Enter' || e.key === ' ') {\r\n e.preventDefault();\r\n this.open();\r\n }\r\n });\r\n\r\n input.addEventListener('focus', (e) => {\r\n this.open();\r\n });\r\n\r\n input.addEventListener('click', (e) => {\r\n e.stopPropagation();\r\n this.open();\r\n });\r\n });\r\n \r\n if (this.prevBtn) {\r\n makeKeyboardClickable(this.prevBtn);\r\n this.prevBtn.addEventListener('click', (e) => {\r\n e.stopPropagation();\r\n if (this.isSelectingYearMonth) {\r\n this.changeYearRange(-8);\r\n } else {\r\n this.changeMonth(-1);\r\n }\r\n });\r\n }\r\n \r\n if (this.nextBtn) {\r\n makeKeyboardClickable(this.nextBtn);\r\n this.nextBtn.addEventListener('click', (e) => {\r\n e.stopPropagation();\r\n if (this.isSelectingYearMonth) {\r\n this.changeYearRange(8);\r\n } else {\r\n this.changeMonth(1);\r\n }\r\n });\r\n }\r\n\r\n if (this.monthLabel) {\r\n makeKeyboardClickable(this.monthLabel);\r\n this.monthLabel.addEventListener('click', (e) => {\r\n e.stopPropagation();\r\n this.isSelectingYearMonth = !this.isSelectingYearMonth;\r\n this.render();\r\n setTimeout(() => {\r\n if (this.isSelectingYearMonth) {\r\n const calendarContainer = this.container.querySelector('.ux4g-date-picker-calendar');\r\n let el = calendarContainer.querySelector('.ux4g-date-picker-year-item.is-selected') || calendarContainer.querySelector('.ux4g-date-picker-year-item');\r\n if (el) el.focus();\r\n } else {\r\n let el = this.calendarGrid.querySelector('.is-range-start') || this.calendarGrid.querySelector('.is-selected') || this.calendarGrid.querySelector('.is-today') || this.calendarGrid.querySelector('.ux4g-date-picker-day:not(.is-muted)');\r\n if (el) el.focus();\r\n }\r\n }, 0);\r\n });\r\n }\r\n\r\n if (this.confirmBtn) {\r\n makeKeyboardClickable(this.confirmBtn);\r\n this.confirmBtn.addEventListener('click', (e) => {\r\n e.stopPropagation();\r\n if (this.isSelectingYearMonth) {\r\n this.isSelectingYearMonth = false;\r\n this.render();\r\n setTimeout(() => {\r\n let el = this.calendarGrid.querySelector('.is-range-start') || this.calendarGrid.querySelector('.is-selected') || this.calendarGrid.querySelector('.is-today') || this.calendarGrid.querySelector('.ux4g-date-picker-day:not(.is-muted)');\r\n if (el) el.focus();\r\n }, 0);\r\n } else {\r\n this.confirmSelection();\r\n }\r\n });\r\n }\r\n\r\n if (this.cancelBtn) {\r\n this.cancelBtn.addEventListener('click', (e) => {\r\n e.stopPropagation();\r\n this.cancelSelection();\r\n });\r\n }\r\n\r\n document.addEventListener('click', (e) => {\r\n if (!this.container.contains(e.target) && !getBackdrop().contains(e.target)) {\r\n this.close();\r\n }\r\n });\r\n\r\n getBackdrop().addEventListener('click', () => {\r\n this.close();\r\n });\r\n\r\n this.render();\r\n }\r\n\r\n open() {\r\n if (this.dropdown) {\r\n this.tempStartDate = this.startDate ? new Date(this.startDate) : null;\r\n this.tempEndDate = this.endDate ? new Date(this.endDate) : null;\r\n this.selectingEnd = this.tempStartDate && !this.tempEndDate;\r\n this.isSelectingYearMonth = false;\r\n this.dropdown.classList.add('is-open');\r\n if (isMobile()) {\r\n getBackdrop().classList.add('is-active');\r\n document.body.style.overflow = 'hidden';\r\n }\r\n this.render();\r\n }\r\n }\r\n\r\n close() {\r\n if (this.dropdown) {\r\n this.dropdown.classList.remove('is-open');\r\n getBackdrop().classList.remove('is-active');\r\n document.body.style.overflow = '';\r\n }\r\n }\r\n\r\n confirmSelection() {\r\n this.startDate = this.tempStartDate ? new Date(this.tempStartDate) : null;\r\n this.endDate = this.tempEndDate ? new Date(this.tempEndDate) : null;\r\n this.updateInputs();\r\n this.close();\r\n }\r\n\r\n cancelSelection() {\r\n this.tempStartDate = this.startDate ? new Date(this.tempStartDate) : null;\r\n this.tempEndDate = this.endDate ? new Date(this.endDate) : null;\r\n this.close();\r\n }\r\n\r\n changeMonth(delta) {\r\n this.viewDate.setMonth(this.viewDate.getMonth() + delta);\r\n this.render();\r\n }\r\n\r\n changeYearRange(delta) {\r\n this.viewDate.setFullYear(this.viewDate.getFullYear() + delta);\r\n this.render();\r\n }\r\n\r\n render() {\r\n if (this.isSelectingYearMonth) {\r\n this.renderYearMonthSelection();\r\n } else {\r\n this.renderCalendar();\r\n }\r\n }\r\n\r\n renderCalendar() {\r\n const year = this.viewDate.getFullYear();\r\n const month = this.viewDate.getMonth();\r\n const monthNames = [\"January\", \"February\", \"March\", \"April\", \"May\", \"June\",\r\n \"July\", \"August\", \"September\", \"October\", \"November\", \"December\"\r\n ];\r\n \r\n if (this.monthLabel) {\r\n this.monthLabel.innerHTML = `${monthNames[month]} ${year} <span class=\"ux4g-icon-outlined ux4g-fs-18\">keyboard_arrow_down</span>`;\r\n }\r\n\r\n const calendarHtml = `\r\n <div class=\"ux4g-date-picker-weekdays\">\r\n <div class=\"ux4g-date-picker-weekday\">Mo</div>\r\n <div class=\"ux4g-date-picker-weekday\">Tu</div>\r\n <div class=\"ux4g-date-picker-weekday\">We</div>\r\n <div class=\"ux4g-date-picker-weekday\">Th</div>\r\n <div class=\"ux4g-date-picker-weekday\">Fr</div>\r\n <div class=\"ux4g-date-picker-weekday\">Sa</div>\r\n <div class=\"ux4g-date-picker-weekday\">Su</div>\r\n </div>\r\n <div class=\"ux4g-date-picker-grid\"></div>\r\n `;\r\n \r\n const calendarContainer = this.container.querySelector('.ux4g-date-picker-calendar');\r\n calendarContainer.innerHTML = calendarHtml;\r\n this.calendarGrid = calendarContainer.querySelector('.ux4g-date-picker-grid');\r\n\r\n const firstDayOfMonth = new Date(year, month, 1).getDay();\r\n const daysInMonth = new Date(year, month + 1, 0).getDate();\r\n let startDay = firstDayOfMonth === 0 ? 6 : firstDayOfMonth - 1;\r\n const prevMonthLastDay = new Date(year, month, 0).getDate();\r\n \r\n let html = '';\r\n for (let i = startDay - 1; i >= 0; i--) {\r\n html += `<div class=\"ux4g-date-picker-day is-muted\">${prevMonthLastDay - i}</div>`;\r\n }\r\n \r\n for (let i = 1; i <= daysInMonth; i++) {\r\n const date = new Date(year, month, i);\r\n const isToday = date.toDateString() === this.currentDate.toDateString();\r\n \r\n let classes = 'ux4g-date-picker-day';\r\n if (isToday) classes += ' is-today';\r\n \r\n if (this.tempStartDate && date.toDateString() === this.tempStartDate.toDateString()) {\r\n classes += ' is-selected is-range-start';\r\n } else if (this.tempEndDate && date.toDateString() === this.tempEndDate.toDateString()) {\r\n classes += ' is-selected is-range-end';\r\n } else if (this.tempStartDate && this.tempEndDate && date > this.tempStartDate && date < this.tempEndDate) {\r\n classes += ' is-in-range';\r\n }\r\n \r\n html += `<div class=\"${classes}\" data-date=\"${i}\" tabindex=\"0\">${i}</div>`;\r\n }\r\n \r\n const totalCells = 42;\r\n const remainingCells = totalCells - (startDay + daysInMonth);\r\n for (let i = 1; i <= remainingCells; i++) {\r\n html += `<div class=\"ux4g-date-picker-day is-muted\">${i}</div>`;\r\n }\r\n \r\n this.calendarGrid.innerHTML = html;\r\n this.calendarGrid.querySelectorAll('.ux4g-date-picker-day:not(.is-muted)').forEach(dayEl => {\r\n makeKeyboardClickable(dayEl);\r\n dayEl.addEventListener('click', (e) => {\r\n e.stopPropagation();\r\n const day = e.target.dataset.date;\r\n this.handleDateSelection(new Date(year, month, day));\r\n });\r\n });\r\n\r\n if (this.confirmBtn) {\r\n this.confirmBtn.innerHTML = 'Confirm';\r\n this.confirmBtn.disabled = !this.tempStartDate || !this.tempEndDate;\r\n }\r\n }\r\n\r\n renderYearMonthSelection() {\r\n const currentYear = this.viewDate.getFullYear();\r\n const startYear = Math.floor(currentYear / 8) * 8;\r\n const endYear = startYear + 7;\r\n \r\n if (this.monthLabel) {\r\n this.monthLabel.innerHTML = `${startYear}-${endYear} <span class=\"ux4g-icon-outlined ux4g-fs-18\">keyboard_arrow_down</span>`;\r\n }\r\n\r\n let html = '<div class=\"ux4g-date-picker-selection-view\">';\r\n html += '<div class=\"ux4g-date-picker-year-grid\">';\r\n for (let y = startYear; y <= endYear; y++) {\r\n const isSelected = y === this.viewDate.getFullYear();\r\n html += `<div class=\"ux4g-date-picker-year-item ${isSelected ? 'is-selected' : ''}\" data-year=\"${y}\" tabindex=\"0\">${y}</div>`;\r\n }\r\n html += '</div>';\r\n\r\n const monthNamesShort = [\"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\", \"Jul\", \"Aug\", \"Sep\", \"Oct\", \"Nov\", \"Dec\"];\r\n html += '<div class=\"ux4g-date-picker-month-grid\">';\r\n monthNamesShort.forEach((m, i) => {\r\n const isSelected = i === this.viewDate.getMonth();\r\n html += `<div class=\"ux4g-date-picker-month-item ${isSelected ? 'is-selected' : ''}\" data-month=\"${i}\" tabindex=\"0\">${m}</div>`;\r\n });\r\n html += '</div></div>';\r\n\r\n const calendarContainer = this.container.querySelector('.ux4g-date-picker-calendar');\r\n calendarContainer.innerHTML = html;\r\n\r\n calendarContainer.querySelectorAll('.ux4g-date-picker-year-item').forEach(el => {\r\n makeKeyboardClickable(el);\r\n el.addEventListener('click', (e) => {\r\n e.stopPropagation();\r\n this.viewDate.setFullYear(parseInt(e.target.dataset.year));\r\n this.renderYearMonthSelection();\r\n setTimeout(() => {\r\n const selectedMonth = this.container.querySelector('.ux4g-date-picker-month-item.is-selected');\r\n if (selectedMonth) selectedMonth.focus();\r\n }, 0);\r\n });\r\n });\r\n\r\n calendarContainer.querySelectorAll('.ux4g-date-picker-month-item').forEach(el => {\r\n makeKeyboardClickable(el);\r\n el.addEventListener('click', (e) => {\r\n e.stopPropagation();\r\n this.viewDate.setMonth(parseInt(e.target.dataset.month));\r\n this.renderYearMonthSelection();\r\n if (this.confirmBtn) setTimeout(() => this.confirmBtn.focus(), 0);\r\n });\r\n });\r\n\r\n if (this.confirmBtn) {\r\n this.confirmBtn.innerHTML = 'Select date';\r\n this.confirmBtn.disabled = false;\r\n }\r\n }\r\n\r\n handleDateSelection(date) {\r\n if (!this.tempStartDate || (this.tempStartDate && this.tempEndDate)) {\r\n this.tempStartDate = date;\r\n this.tempEndDate = null;\r\n this.selectingEnd = true;\r\n } else if (this.selectingEnd) {\r\n if (date < this.tempStartDate) {\r\n this.tempEndDate = this.tempStartDate;\r\n this.tempStartDate = date;\r\n } else {\r\n this.tempEndDate = date;\r\n }\r\n this.selectingEnd = false;\r\n }\r\n this.render();\r\n if (!this.selectingEnd && this.confirmBtn && !this.confirmBtn.disabled) {\r\n setTimeout(() => this.confirmBtn.focus(), 0);\r\n } else if (this.selectingEnd) {\r\n setTimeout(() => {\r\n const selectedEl = this.calendarGrid.querySelector('.is-range-start');\r\n if (selectedEl) selectedEl.focus();\r\n }, 0);\r\n }\r\n }\r\n\r\n updateInputs() {\r\n if (this.startDate) {\r\n const d = String(this.startDate.getDate()).padStart(2, '0');\r\n const m = String(this.startDate.getMonth() + 1).padStart(2, '0');\r\n const y = this.startDate.getFullYear();\r\n this.inputs[0].value = `${d}/${m}/${y}`;\r\n } else {\r\n this.inputs[0].value = '';\r\n }\r\n if (this.endDate) {\r\n const d = String(this.endDate.getDate()).padStart(2, '0');\r\n const m = String(this.endDate.getMonth() + 1).padStart(2, '0');\r\n const y = this.endDate.getFullYear();\r\n this.inputs[1].value = `${d}/${m}/${y}`;\r\n } else {\r\n this.inputs[1].value = '';\r\n }\r\n }\r\n }\r\n\r\n class TimePicker {\r\n constructor(container) {\r\n this.container = container;\r\n this.input = container.querySelector('.ux4g-time-picker-input');\r\n this.dropdown = container.querySelector('.ux4g-time-picker-dropdown');\r\n this.hhColumn = container.querySelector('[data-column=\"hh\"]');\r\n this.mmColumn = container.querySelector('[data-column=\"mm\"]');\r\n this.ampmBtns = container.querySelectorAll('.ux4g-time-picker-ampm-btn');\r\n this.confirmBtn = container.querySelector('.ux4g-btn-primary');\r\n this.cancelBtn = container.querySelector('.ux4g-btn-outline-neutral');\r\n\r\n this.selectedHH = null;\r\n this.selectedMM = null;\r\n this.selectedAMPM = \"PM\";\r\n\r\n this.tempHH = null;\r\n this.tempMM = null;\r\n\r\n this._init();\r\n }\r\n\r\n _init() {\r\n if (!this.input || !this.dropdown) return;\r\n\r\n // Keyboard accessibility\r\n this.input.addEventListener('keydown', (e) => {\r\n if (e.key === 'Enter' || e.key === ' ') {\r\n e.preventDefault();\r\n this.open();\r\n }\r\n });\r\n\r\n this.input.addEventListener('focus', (e) => {\r\n this.open();\r\n });\r\n\r\n this.input.addEventListener('click', (e) => {\r\n e.stopPropagation();\r\n this.open();\r\n });\r\n\r\n this.ampmBtns.forEach(btn => {\r\n makeKeyboardClickable(btn);\r\n btn.addEventListener('click', (e) => {\r\n e.stopPropagation();\r\n this.selectedAMPM = e.target.dataset.value;\r\n this.updateAMPMUI();\r\n });\r\n });\r\n\r\n if (this.confirmBtn) {\r\n makeKeyboardClickable(this.confirmBtn);\r\n this.confirmBtn.addEventListener('click', (e) => {\r\n e.stopPropagation();\r\n if (!this.confirmBtn.disabled) {\r\n this.confirmSelection();\r\n }\r\n });\r\n }\r\n\r\n if (this.cancelBtn) {\r\n makeKeyboardClickable(this.cancelBtn);\r\n this.cancelBtn.addEventListener('click', (e) => {\r\n e.stopPropagation();\r\n this.close();\r\n });\r\n }\r\n\r\n document.addEventListener('click', (e) => {\r\n if (!this.container.contains(e.target) && !getBackdrop().contains(e.target)) {\r\n this.close();\r\n }\r\n });\r\n\r\n this.renderColumns();\r\n this.updateAMPMUI();\r\n this.validate();\r\n }\r\n\r\n open() {\r\n this.tempHH = this.selectedHH;\r\n this.tempMM = this.selectedMM;\r\n \r\n this.dropdown.classList.add('is-open');\r\n if (isMobile()) {\r\n getBackdrop().classList.add('is-active');\r\n document.body.style.overflow = 'hidden';\r\n }\r\n \r\n this.renderColumns();\r\n this.scrollToSelected();\r\n this.validate();\r\n }\r\n\r\n close() {\r\n this.dropdown.classList.remove('is-open');\r\n getBackdrop().classList.remove('is-active');\r\n document.body.style.overflow = '';\r\n }\r\n\r\n validate() {\r\n if (this.confirmBtn) {\r\n this.confirmBtn.disabled = !(this.tempHH && this.tempMM);\r\n }\r\n }\r\n\r\n confirmSelection() {\r\n this.selectedHH = this.tempHH;\r\n this.selectedMM = this.tempMM;\r\n this.input.value = `${this.selectedHH} : ${this.selectedMM} ${this.selectedAMPM}`;\r\n this.close();\r\n }\r\n\r\n updateAMPMUI() {\r\n this.ampmBtns.forEach(btn => {\r\n btn.classList.toggle('is-active', btn.dataset.value === this.selectedAMPM);\r\n });\r\n }\r\n\r\n renderColumns() {\r\n // Hours (1-12)\r\n let hhHtml = '<div class=\"ux4g-time-picker-col-header\">HH</div>';\r\n for (let i = 1; i <= 12; i++) {\r\n const val = String(i).padStart(2, '0');\r\n const isSelected = val === this.tempHH;\r\n hhHtml += `<div class=\"ux4g-time-picker-item ${isSelected ? 'is-selected' : ''}\" data-value=\"${val}\" tabindex=\"0\">${val}</div>`;\r\n }\r\n this.hhColumn.innerHTML = hhHtml;\r\n\r\n // Minutes (0-55, step 5)\r\n let mmHtml = '<div class=\"ux4g-time-picker-col-header\">MM</div>';\r\n for (let i = 0; i < 60; i += 5) {\r\n const val = String(i).padStart(2, '0');\r\n const isSelected = val === this.tempMM;\r\n mmHtml += `<div class=\"ux4g-time-picker-item ${isSelected ? 'is-selected' : ''}\" data-value=\"${val}\" tabindex=\"0\">${val}</div>`;\r\n }\r\n this.mmColumn.innerHTML = mmHtml;\r\n\r\n // Click events\r\n this.hhColumn.querySelectorAll('.ux4g-time-picker-item').forEach(el => {\r\n makeKeyboardClickable(el);\r\n el.addEventListener('click', (e) => {\r\n e.stopPropagation();\r\n this.tempHH = e.target.dataset.value;\r\n this.updateColumnSelection(this.hhColumn, this.tempHH);\r\n this.validate();\r\n if (!this.confirmBtn.disabled) {\r\n setTimeout(() => this.confirmBtn.focus(), 0);\r\n } else if (!this.tempMM) {\r\n const firstMM = this.mmColumn.querySelector('.ux4g-time-picker-item');\r\n if (firstMM) setTimeout(() => firstMM.focus(), 0);\r\n }\r\n });\r\n });\r\n\r\n this.mmColumn.querySelectorAll('.ux4g-time-picker-item').forEach(el => {\r\n makeKeyboardClickable(el);\r\n el.addEventListener('click', (e) => {\r\n e.stopPropagation();\r\n this.tempMM = e.target.dataset.value;\r\n this.updateColumnSelection(this.mmColumn, this.tempMM);\r\n this.validate();\r\n if (!this.confirmBtn.disabled) {\r\n setTimeout(() => this.confirmBtn.focus(), 0);\r\n }\r\n });\r\n });\r\n }\r\n\r\n updateColumnSelection(column, value) {\r\n column.querySelectorAll('.ux4g-time-picker-item').forEach(el => {\r\n el.classList.toggle('is-selected', el.dataset.value === value);\r\n });\r\n }\r\n\r\n scrollToSelected() {\r\n const columns = [this.hhColumn, this.mmColumn];\r\n columns.forEach(col => {\r\n const selected = col.querySelector('.is-selected');\r\n if (selected) {\r\n col.scrollTop = selected.offsetTop - col.offsetTop - 80;\r\n }\r\n });\r\n }\r\n }\r\n\r\n const init = () => {\r\n document.querySelectorAll('.ux4g-date-picker-container').forEach(container => {\r\n if (!container.closest('.ux4g-date-range-picker')) new DatePicker(container);\r\n });\r\n document.querySelectorAll('.ux4g-date-range-picker').forEach(container => {\r\n new RangeDatePicker(container);\r\n });\r\n document.querySelectorAll('.ux4g-time-picker-container').forEach(container => {\r\n new TimePicker(container);\r\n });\r\n };\r\n\r\n if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);\r\n else init();\r\n\r\n global.DatePicker = DatePicker;\r\n global.RangeDatePicker = RangeDatePicker;\r\n global.TimePicker = TimePicker;\r\n\r\n})(window);\r\n\r\n\r\n/********************************* Time Slot JS ***********************************/ \r\n\r\nclass TimeSlotCalendar {\r\n constructor(container) {\r\n this.container = container;\r\n this.calendarGrid = container.querySelector('.ux4g-time-slot-compact-grid');\r\n this.monthLabel = container.querySelector('.ux4g-time-slot-compact-month');\r\n this.slotTitle = container.querySelector('.ux4g-time-slot-compact-desktop-header');\r\n this.slotsList = container.querySelector('.ux4g-time-slot-compact-list');\r\n this.confirmBtn = container.querySelector('.ux4g-btn-primary');\r\n \r\n const navBtns = container.querySelectorAll('.ux4g-btn-icon');\r\n this.prevBtn = navBtns[0];\r\n this.nextBtn = navBtns[1];\r\n\r\n this.currentDate = new Date();\r\n this.viewDate = new Date(this.currentDate.getFullYear(), this.currentDate.getMonth(), 1);\r\n this.selectedDate = new Date(2026, 3, 23); // Default from design: April 23, 2026\r\n\r\n // Mock Data for Statuses\r\n this.holidays = ['2026-04-09', '2026-04-21'];\r\n this.weeklyOffs = [0, 6]; // Sunday, Saturday\r\n this.noSlotsDates = ['2026-04-08', '2026-04-13'];\r\n\r\n this._init();\r\n }\r\n\r\n _init() {\r\n if (this.prevBtn) {\r\n this.prevBtn.addEventListener('click', () => this.changeMonth(-1));\r\n }\r\n if (this.nextBtn) {\r\n this.nextBtn.addEventListener('click', () => this.changeMonth(1));\r\n }\r\n\r\n // Cancel Button Reset Logic\r\n const cancelBtn = this.container.querySelector('.ux4g-btn-outline-neutral');\r\n if (cancelBtn) {\r\n cancelBtn.addEventListener('click', () => {\r\n this.selectedDate = null; // Reset selection\r\n this.render(); // Re-render calendar to clear highlights\r\n if (this.slotTitle) this.slotTitle.innerText = \"Select a Date\"; // Reset slot header\r\n this.resetSlots(); // Clear slot selection and disable confirm button\r\n });\r\n }\r\n\r\n this.render();\r\n }\r\n\r\n changeMonth(delta) {\r\n this.viewDate.setMonth(this.viewDate.getMonth() + delta);\r\n this.render();\r\n }\r\n\r\n render() {\r\n this.renderCalendar();\r\n }\r\n\r\n renderCalendar() {\r\n const year = this.viewDate.getFullYear();\r\n const month = this.viewDate.getMonth();\r\n const monthNames = [\"January\", \"February\", \"March\", \"April\", \"May\", \"June\",\r\n \"July\", \"August\", \"September\", \"October\", \"November\", \"December\"\r\n ];\r\n\r\n // Update Month Label\r\n if (this.monthLabel) {\r\n this.monthLabel.innerText = `${monthNames[month]} ${year}`;\r\n }\r\n\r\n const firstDayOfMonth = new Date(year, month, 1).getDay();\r\n const daysInMonth = new Date(year, month + 1, 0).getDate();\r\n \r\n // Adjust for Monday start\r\n let startDay = firstDayOfMonth === 0 ? 6 : firstDayOfMonth - 1;\r\n const prevMonthLastDay = new Date(year, month, 0).getDate();\r\n\r\n // Clear and rebuild grid\r\n this.calendarGrid.innerHTML = '';\r\n\r\n // Add Weekday Names\r\n const days = [\"Mo\", \"Tu\", \"We\", \"Th\", \"Fr\", \"Sa\", \"Su\"];\r\n days.forEach(day => {\r\n const dayEl = document.createElement('div');\r\n dayEl.className = 'ux4g-time-slot-day-name';\r\n dayEl.innerText = day;\r\n this.calendarGrid.appendChild(dayEl);\r\n });\r\n\r\n // Previous Month Days\r\n for (let i = startDay - 1; i >= 0; i--) {\r\n const dateEl = document.createElement('div');\r\n dateEl.className = 'ux4g-time-slot-date muted';\r\n dateEl.innerText = prevMonthLastDay - i;\r\n this.calendarGrid.appendChild(dateEl);\r\n }\r\n\r\n // Current Month Days\r\n for (let i = 1; i <= daysInMonth; i++) {\r\n const date = new Date(year, month, i);\r\n const dateStr = date.getFullYear() + '-' + String(date.getMonth() + 1).padStart(2, '0') + '-' + String(i).padStart(2, '0');\r\n const isToday = date.toDateString() === this.currentDate.toDateString();\r\n const isSelected = this.selectedDate && date.toDateString() === this.selectedDate.toDateString();\r\n const isHoliday = this.holidays.includes(dateStr);\r\n const isWeeklyOff = this.weeklyOffs.includes(date.getDay());\r\n const isNoSlots = this.noSlotsDates.includes(dateStr);\r\n\r\n const dateEl = document.createElement('div');\r\n dateEl.className = 'ux4g-time-slot-date';\r\n if (isToday) dateEl.classList.add('today');\r\n if (isSelected) dateEl.classList.add('selected');\r\n if (isHoliday) dateEl.classList.add('holiday');\r\n if (isWeeklyOff) dateEl.classList.add('weekly-off');\r\n if (isNoSlots) dateEl.classList.add('no-slots');\r\n \r\n dateEl.innerText = i;\r\n dateEl.dataset.date = i;\r\n\r\n dateEl.addEventListener('click', () => {\r\n this.selectedDate = new Date(year, month, i);\r\n this.render();\r\n this.updateSlotHeader();\r\n this.resetSlots();\r\n });\r\n\r\n this.calendarGrid.appendChild(dateEl);\r\n }\r\n\r\n // Next Month Days\r\n const totalCells = 42 + 7; // Including header row\r\n const currentCells = this.calendarGrid.children.length;\r\n const remainingCells = totalCells - currentCells;\r\n for (let i = 1; i <= remainingCells; i++) {\r\n const dateEl = document.createElement('div');\r\n dateEl.className = 'ux4g-time-slot-date muted';\r\n dateEl.innerText = i;\r\n this.calendarGrid.appendChild(dateEl);\r\n }\r\n }\r\n\r\n updateSlotHeader() {\r\n if (this.slotTitle && this.selectedDate) {\r\n const day = this.selectedDate.getDate();\r\n const monthNames = [\"January\", \"February\", \"March\", \"April\", \"May\", \"June\",\r\n \"July\", \"August\", \"September\", \"October\", \"November\", \"December\"\r\n ];\r\n const month = monthNames[this.selectedDate.getMonth()];\r\n const year = this.selectedDate.getFullYear();\r\n this.slotTitle.innerText = `${day}${this.getOrdinal(day)} ${month} ${year}`;\r\n }\r\n }\r\n\r\n resetSlots() {\r\n // Reset slot selection and disable confirm button\r\n const slots = this.container.querySelectorAll('.ux4g-time-slot-compact-slot-item:not(.disabled)');\r\n slots.forEach(s => s.style.backgroundColor = 'transparent');\r\n if (this.confirmBtn) this.confirmBtn.setAttribute('disabled', 'true');\r\n \r\n // Re-attach slot selection listeners\r\n slots.forEach(slot => {\r\n slot.addEventListener('click', () => {\r\n slots.forEach(s => s.style.backgroundColor = 'transparent');\r\n slot.style.backgroundColor = 'var(--ux4g-bg-neutral-subtle)';\r\n if (this.confirmBtn) this.confirmBtn.removeAttribute('disabled');\r\n });\r\n });\r\n }\r\n\r\n getOrdinal(n) {\r\n const s = [\"th\", \"st\", \"nd\", \"rd\"];\r\n const v = n % 100;\r\n return (s[(v - 20) % 10] || s[v] || s[0]);\r\n }\r\n}\r\n\r\n// Initialize on Load\r\ndocument.addEventListener('DOMContentLoaded', () => {\r\n // Initialize Compact Calendar\r\n const compactContainer = document.querySelector('.ux4g-time-slot-compact-container');\r\n if (compactContainer) {\r\n new TimeSlotCalendar(compactContainer);\r\n }\r\n\r\n // Initialize Weekly Grid Selection\r\n const weeklyGrid = document.querySelector('.ux4g-time-slot-weekly-grid');\r\n if (weeklyGrid) {\r\n const cells = weeklyGrid.querySelectorAll('.ux4g-time-slot-cell.available, .ux4g-time-slot-cell.limited');\r\n const confirmBtn = weeklyGrid.parentElement.querySelector('.ux4g-time-slot-weekly-actions .ux4g-btn-primary');\r\n const cancelBtn = weeklyGrid.parentElement.querySelector('.ux4g-time-slot-weekly-actions .ux4g-btn-outline-neutral');\r\n\r\n cells.forEach(cell => {\r\n // Store original content to restore later\r\n const originalHTML = cell.innerHTML;\r\n\r\n cell.addEventListener('click', () => {\r\n if (cell.classList.contains('selected')) return;\r\n\r\n // 1. Restore all other cells to their original state\r\n cells.forEach(c => {\r\n if (c.classList.contains('selected')) {\r\n c.classList.remove('selected');\r\n if (c._originalContent) {\r\n c.innerHTML = c._originalContent;\r\n }\r\n }\r\n });\r\n\r\n // 2. Select this cell\r\n cell.classList.add('selected');\r\n cell._originalContent = originalHTML;\r\n cell.innerHTML = `<span class=\"ux4g-icon-filled\">check_circle</span> Selected`;\r\n\r\n // 3. Enable Confirm Button\r\n if (confirmBtn) confirmBtn.removeAttribute('disabled');\r\n });\r\n });\r\n\r\n // Cancel Button logic for Weekly Grid\r\n if (cancelBtn) {\r\n cancelBtn.addEventListener('click', () => {\r\n cells.forEach(c => {\r\n if (c.classList.contains('selected')) {\r\n c.classList.remove('selected');\r\n if (c._originalContent) {\r\n c.innerHTML = c._originalContent;\r\n }\r\n }\r\n });\r\n if (confirmBtn) confirmBtn.setAttribute('disabled', 'true');\r\n });\r\n }\r\n }\r\n\r\n // Initialize Weekly Grid Mobile Navigation (Dynamic approach)\r\n const mobileNav = document.querySelector('.ux4g-time-slot-mobile-nav');\r\n if (weeklyGrid && mobileNav) {\r\n const mobileDateLabel = mobileNav.querySelector('.ux4g-time-slot-mobile-date');\r\n const navBtns = mobileNav.querySelectorAll('.ux4g-btn-icon');\r\n const prevBtn = navBtns[0];\r\n const nextBtn = navBtns[1];\r\n\r\n // 1. Dynamically assign data-day attributes to cells based on grid position\r\n // Grid has 8 columns: Time, Mon, Tue, Wed, Thu, Fri, Sat, Sun\r\n const children = weeklyGrid.children;\r\n for (let i = 0; i < children.length; i++) {\r\n const colIndex = i % 8;\r\n if (colIndex > 0) { // Skip Time column\r\n children[i].setAttribute('data-day', colIndex - 1);\r\n }\r\n }\r\n\r\n // 2. Navigation Logic\r\n let activeDay = 0;\r\n weeklyGrid.setAttribute('data-active-day', activeDay);\r\n\r\n const daysData = [\r\n { day: \"Mon 14 Apr\", status: \"Today\" },\r\n { day: \"Tue 15 Apr\", status: \"\" },\r\n { day: \"Wed 16 Apr\", status: \"\" },\r\n { day: \"Thu 17 Apr\", status: \"Public Holiday\" },\r\n { day: \"Fri 18 Apr\", status: \"\" },\r\n { day: \"Sat 19 Apr\", status: \"Weekly off\" },\r\n { day: \"Sun 20 Apr\", status: \"Weekly off\" }\r\n ];\r\n\r\n const updateMobileNav = (index) => {\r\n weeklyGrid.setAttribute('data-active-day', index);\r\n const data = daysData[index];\r\n if (mobileDateLabel) {\r\n mobileDateLabel.innerHTML = `\r\n <strong>${data.day}</strong>\r\n ${data.status ? `<span class=\"${data.status === 'Today' ? 'ux4g-text-success-600' : 'ux4g-text-neutral-secondary'}\">${data.status}</span>` : ''}\r\n `;\r\n }\r\n };\r\n\r\n if (prevBtn) {\r\n prevBtn.addEventListener('click', () => {\r\n activeDay = (activeDay > 0) ? activeDay - 1 : 6;\r\n updateMobileNav(activeDay);\r\n });\r\n }\r\n\r\n if (nextBtn) {\r\n nextBtn.addEventListener('click', () => {\r\n activeDay = (activeDay < 6) ? activeDay + 1 : 0;\r\n updateMobileNav(activeDay);\r\n });\r\n }\r\n }\r\n});\r\n\r\n\r\n/********************************* Result list JS ***********************************/ \r\n\r\n// Accordion Toggle\r\ndocument.addEventListener('DOMContentLoaded', () => {\r\n const toggleBtns = document.querySelectorAll('.ux4g-result-list-accordion-toggle');\r\n toggleBtns.forEach(btn => {\r\n btn.addEventListener('click', (e) => {\r\n const toggle = e.target;\r\n const card = toggle.closest('.ux4g-result-list');\r\n const content = card.querySelector('.ux4g-result-list-content');\r\n if (!content) return;\r\n \r\n const isExpanded = toggle.getAttribute('aria-expanded') === 'true';\r\n \r\n if (isExpanded) {\r\n toggle.setAttribute('aria-expanded', 'false');\r\n toggle.innerText = 'expand_more';\r\n } else {\r\n toggle.setAttribute('aria-expanded', 'true');\r\n toggle.innerText = 'expand_less';\r\n }\r\n });\r\n });\r\n});\r\n\r\n\r\n\r\n/********************************* Mega menu category list JS ***********************************/ \r\n\r\ndocument.addEventListener('DOMContentLoaded', () => {\r\n const categoryItems = document.querySelectorAll('.ux4g-mega-menu__category-item');\r\n const contentBlocks = document.querySelectorAll('.ux4g-mega-menu__content');\r\n\r\n if (!categoryItems.length || !contentBlocks.length) return;\r\n\r\n categoryItems.forEach((item, index) => {\r\n item.addEventListener('click', (e) => {\r\n e.preventDefault();\r\n \r\n // Remove active class from all categories\r\n categoryItems.forEach(cat => cat.classList.remove('ux4g-mega-menu__category-item--active'));\r\n // Add active class to clicked category\r\n item.classList.add('ux4g-mega-menu__category-item--active');\r\n \r\n // Hide all content blocks\r\n contentBlocks.forEach(block => block.classList.remove('ux4g-mega-menu__content--active'));\r\n \r\n // Show the corresponding content block by ID matching category-1, category-2, etc.\r\n const targetId = `category-${index + 1}`;\r\n const targetBlock = document.getElementById(targetId);\r\n \r\n if (targetBlock) {\r\n targetBlock.classList.add('ux4g-mega-menu__content--active');\r\n }\r\n });\r\n });\r\n});\r\n\r\n";
|
|
40
|
+
document.head.appendChild(script2);
|
|
1092
41
|
}
|
|
1093
42
|
|
|
1094
43
|
/**
|