ux4g-components-web 1.1.2 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,1092 +3,41 @@
3
3
  /**
4
4
  * UX4G Runtime Module
5
5
  *
6
- * Framework-agnostic runtime that encapsulates ALL interactive behaviors
7
- * from the vendor CDN scripts (ux4g.js + ux4g-custom.js).
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
- * initialization happens exactly once.
14
+ * the scripts are injected exactly once.
11
15
  */
12
16
  const isBrowser = typeof window !== 'undefined' && typeof document !== 'undefined';
13
- let activeObservers = [];
14
- const Registry = new WeakMap();
15
- // ─── Utilities ───────────────────────────────────────────────────────────────
16
- const U = {
17
- qs(sel, root = document) { return root.querySelector(sel); },
18
- qsa(sel, root = document) { return Array.from(root.querySelectorAll(sel)); },
19
- on(el, evt, cb, opts) { if (!el)
20
- return; el.addEventListener(evt, cb, opts); },
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#039;'); }
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
- init(document);
1089
- initDelegatedToggles();
1090
- initCustomBehaviors();
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 = __UX4G_VENDOR_MAIN__;
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 = __UX4G_VENDOR_CUSTOM__;
40
+ document.head.appendChild(script2);
1092
41
  }
1093
42
 
1094
43
  /**