PageSourceSearch

https://www.badgebox.com/desk/javascript_2_0/bbx-tabs.js?v=1791095907

js badgebox.com collected 2026-10-05 09:52:51 UTC 24,298 bytes, 504 lines download raw bytes

1/*!
2 * bbx-tabs.js — sliding indicator + auto-init per tutti i container tab v2.
3 *
4 * Copre 4 markup:
5 *   - .bbx-header__tabs  (header globale, gestito da bbx-header.js)
6 *   - .bbx-tabs          (componente custom riusabile)
7 *   - .bbx-segment       (segmented control v2 — primitives §E.SEG)
8 *   - .nav-tabs          (Bootstrap legacy: agency.php, dialog_*.php, ecc.)
9 *
10 * Il motore è lo stesso per tutti: misura l'item attivo e dimensiona/trasla un
11 * indicatore assoluto; a cambiare è solo la VESTE dell'indicatore (linea sotto
12 * le tab, riquadro bianco dietro l'opzione nel segmented). Per questo il
13 * segmented non ha uno script suo.
14 *
15 * NON tocca il markup esistente. Inietta solo un <div class="bbx-tabs__indicator">
16 * dentro il container e ne anima width/translateX in base al tab attivo.
17 *
18 * E quando le tab non entrano nella riga (overflow orizzontale: molte voci,
19 * voci lunghe, dialog stretto, telefono) segna i BORDI: scrive
20 * data-overflow-start / data-overflow-end sul container e inietta due figli
21 * sticky `.bbx-tabs__edge` con la sfumatura e un chevron che scorre di un
22 * tratto (primitives §TABS). Al cambio di tab, la tab attiva viene portata
23 * in vista. Vale per ogni riga che scorre: le tab, e il segmented quando le
24 * opzioni non ci stanno (primitives §E.SEG lo fa scorrere).
25 *
26 * La misura si rifa' quando la geometria cambia davvero (ResizeObserver
27 * sulla riga e sulle sue voci, voci aggiunte o tolte, il font che arriva):
28 * i bordi ci sono dal primo frame in cui la riga ha la sua larghezza, non
29 * dal primo scroll.
30 *
31 * Bootstrap continua a gestire show/hide dei .tab-pane via data-bs-toggle/
32 * data-toggle="tab" — nessuna logica funzionale viene riscritta.
33 *
34 * Caricato globalmente via bbxV2IconsOnly() (server/bbx_v2_assets.php).
35 */
36(function () {
37    'use strict';
38    if (typeof window === 'undefined' || typeof document === 'undefined') return;
39    /* Caricato globalmente via bbxV2IconsOnly()/bbxV2Head(): guard per evitare
40     * doppia inizializzazione (osservatori/listener) se incluso più volte. */
41    if (window.__bbxTabsLoaded) return;
42    window.__bbxTabsLoaded = true;
43
44    var INDICATOR_CLASS_GENERIC = 'bbx-tabs__indicator';
45    var INDICATOR_CLASS_HEADER = 'bbx-header__tab-indicator';
46    var INDICATOR_CLASS_SEGMENT = 'bbx-segment__indicator';
47    /* Selettori container supportati. NB: per nav-tabs limitiamo al SOLO
48     * scope #body-content/#md-content/#md-modal/.modal (dentro card/modal),
49     * per non interferire con eventuali .nav-tabs in contesti speciali.
50     *
51     * Segmented: due famiglie, ENTRAMBE con la veste v2 (fondo gray-100, thumb
52     * bianco) e quindi entrambe animate —
53     *   - `#widget .people-switch .ui-segment` → il people-switch del widget
54     *     (Interni/Esterni/Tutti), costruito da segment-people.js: è IL
55     *     componente centralizzato, presente su badge/employee/requests/…
56     *   - `.bbx-segment` → la veste v2 opt-in del medesimo markup (§E.SEG),
57     *     usata dalla pagina messaggi.
58     * NON `.ui-segment` nuda: i segmented ancora stilati da segment*.css hanno
59     * la veste legacy, dove un thumb bianco non avrebbe senso. Chi migra una
60     * pagina aggiunge `.bbx-segment` e si porta dietro anche l'animazione. */
61    var CONTAINER_SELECTOR =
62        '.bbx-header__tabs, .bbx-tabs, .bbx-segment, #widget .people-switch .ui-segment, ' +
63        '#body-content .nav-tabs, #md-content .nav-tabs, #md-modal .nav-tabs, .modal .nav-tabs';
64    /* Item selectors dentro un container. */
65    var ITEM_SELECTOR =
66        '.bbx-header__tab, .bbx-tabs__item, .bbx-segment > .option, ' +
67        '#widget .people-switch .ui-segment > .option, ' +
68        '.nav-link, .nav-tabs > li > a, .nav-tabs > li.nav-item > a';
69    /* Active state. NB: nei dialog il markup è <li class="nav-tabs-link active">
70     * <a data-toggle="tab">: Bootstrap muove .active/aria-selected sull'<a>
71     * (NON sul <li>, che resta sul primo dal render PHP). Quindi l'active vero
72     * è l'<a> (.active o aria-selected), sia col vero .nav-link (agency) sia
73     * col semplice <li> > <a> (dialog). NON usare > li.active > a (bloccato). */
74    var ACTIVE_SELECTOR =
75        '.bbx-header__tab.is-active, .bbx-tabs__item.is-active, ' +
76        '.bbx-segment > .option.active, ' +
77        '#widget .people-switch .ui-segment > .option.active, ' +
78        '.nav-link.active, .nav-link[aria-selected="true"], ' +
79        '.nav-tabs > li > a.active, .nav-tabs > li > a[aria-selected="true"]';
80
81    function isHeaderContainer(container) {
82        return container.classList && container.classList.contains('bbx-header__tabs');
83    }
84
85    function isSegmentContainer(container) {
86        if (!container.classList) return false;
87        return container.classList.contains('bbx-segment') ||
88               container.classList.contains('ui-segment');
89    }
90
91    function indicatorClassFor(container) {
92        if (isHeaderContainer(container)) return INDICATOR_CLASS_HEADER;
93        if (isSegmentContainer(container)) return INDICATOR_CLASS_SEGMENT;
94        return INDICATOR_CLASS_GENERIC;
95    }
96
97    function getActiveItem(container) {
98        return container.querySelector(ACTIVE_SELECTOR);
99    }
100
101    function getIndicator(container) {
102        /* Cerco l'indicator esistente (header / generic / segment). */
103        var ind = container.querySelector(':scope > .' + INDICATOR_CLASS_HEADER) ||
104                  container.querySelector(':scope > .' + INDICATOR_CLASS_SEGMENT) ||
105                  container.querySelector(':scope > .' + INDICATOR_CLASS_GENERIC);
106        if (ind) { markThumb(container, ind); return ind; }
107
108        /* Iniettato dinamicamente. Per il container bbx-header uso la classe
109         * legacy così le regole CSS già esistenti continuano a matchare; per il
110         * segmented una classe sua, perché la veste è diversa (riquadro dietro
111         * l'opzione, non una linea sotto). */
112        ind = document.createElement('div');
113        ind.className = indicatorClassFor(container);
114        /* Forza position:relative sul container se è static (necessario per
115         * l'indicator absolute). */
116        var pos = getComputedStyle(container).position;
117        if (pos === 'static') container.style.position = 'relative';
118        container.appendChild(ind);
119        markThumb(container, ind);
120        return ind;
121    }
122
123    /* A segmented that has its sliding thumb says so with .has-thumb: the
124     * CSS stops drawing the active option's own box (the thumb draws it).
125     * It was a :not(:has(> .bbx-segment__indicator)), which the target
126     * browser does not apply. Only when missing: classList.add writes the
127     * attribute even when the class is there, the observer below reads that
128     * as a class change and positions the indicator again — a loop that
129     * froze the page. */
130    function markThumb(container, ind) {
131        if (ind.className === INDICATOR_CLASS_SEGMENT && !container.classList.contains('has-thumb'))
132            container.classList.add('has-thumb');
133    }
134
135    /* ---- I bordi di una riga che scorre --------------------------------- */
136    var EDGE_CLASS = 'bbx-tabs__edge';
137
138    function makeEdge(container, side) {
139        var edge = document.createElement('div');
140        edge.className = EDGE_CLASS + ' ' + EDGE_CLASS + '--' + side;
141        var btn = document.createElement('button');
142        btn.type = 'button';
143        btn.className = 'bbx-tabs__edge-btn';
144        btn.setAttribute('aria-label', side === 'end' ? 'Scorri avanti' : 'Scorri indietro');
145        btn.addEventListener('click', function (e) {
146            e.preventDefault();
147            e.stopPropagation();
148            scrollByStep(container, side === 'end' ? 1 : -1);
149        });
150        edge.appendChild(btn);
151        return edge;
152    }
153
154    /* The edges are the first and the last child of the row, and stay so: a
155     * builder that rewrites the row (bbx-header's renderTabs on every section
156     * change) throws them away with innerHTML, or appends tabs after the
157     * last one. Kept only in a property, they were attributes saying «there
158     * is more» with nothing on screen. */
159    function getEdges(container) {
160        var e = container.__bbxEdges;
161        if (!e) {
162            e = container.__bbxEdges = { start: makeEdge(container, 'start'), end: makeEdge(container, 'end') };
163        }
164        if (container.firstChild !== e.start) container.insertBefore(e.start, container.firstChild);
165        if (container.lastChild !== e.end) container.appendChild(e.end);
166        return e;
167    }
168
169    function scrollByStep(container, dir) {
170        var step = Math.max(80, Math.round(container.clientWidth * 0.6)) * dir;
171        if (typeof container.scrollBy === 'function') {
172            try { container.scrollBy({ left: step, behavior: 'smooth' }); return; } catch (_) { /* vecchio motore */ }
173        }
174        container.scrollLeft += step;
175    }
176
177    /* Misura lo scroll e scrive lo stato sul container. I bordi nascono la
178     * prima volta che servono: una riga che entra non porta nodi in piu'. */
179    function updateEdges(container) {
180        if (!container || !container.offsetParent) return;
181        var cs = getComputedStyle(container);
182        /* only a row that scrolls has edges: a segmented that fits is not
183         * a scroller (primitives §E.SEG makes it one only when it can) */
184        if (cs.overflowX !== 'auto' && cs.overflowX !== 'scroll') return;
185        var max = container.scrollWidth - container.clientWidth;
186        var atStart = container.scrollLeft <= 1;
187        var atEnd = container.scrollLeft >= max - 1;
188        var overflows = max > 1;
189        var edges = null;
190        if (overflows) edges = getEdges(container);
191        else if (!container.__bbxEdges) return;
192        else edges = container.__bbxEdges;
193        /* il padding della riga, perche' i bordi arrivino al filo vero
194         * (lo sticky si ferma al box interno: primitives §TABS) */
195        setVar(container, '--bbx-tabs-pad-start', cs.paddingLeft || '0px');
196        setVar(container, '--bbx-tabs-pad-end', cs.paddingRight || '0px');
197        /* the gap of THIS row: each edge is a flex item, and its negative
198         * margin gives back exactly the gap it adds (24 in the tabs, 2 in a
199         * segmented) — a fixed 24 ate the end of a narrow-gap row */
200        setVar(container, '--bbx-tabs-gap', (parseFloat(cs.columnGap
200) || 0) + 'px');
201        if (overflows) setVar(container, '--bbx-tabs-mid', buttonMiddle(container, edges.end) + 'px');
202        setAttr(container, 'data-overflow-start', (overflows && !atStart) ? 'true' : 'false');
203        setAttr(container, 'data-overflow-end', (overflows && !atEnd) ? 'true' : 'false');
204    }
205
206    /* Writes only on a change: the edges are re-measured by observers, and
207     * an unchanged write is still a mutation somebody may be listening to. */
208    function setVar(el, name, value) {
209        if (el.style.getPropertyValue(name) !== value) el.style.setProperty(name, value);
210    }
211    function setAttr(el, name, value) {
212        if (el.getAttribute(name) !== value) el.setAttribute(name, value);
213    }
214
215    /* Where the chevron's centre goes, from the top of the edge: on the
216     * middle of the tabs' TEXT (a tab row has room under the text for the
217     * indicator, so the middle of the box is too low), and never so low
218     * that the circle touches the rule under the row or so high that the
219     * row clips it. */
220    function buttonMiddle(container, edge) {
221        /* the edge stretches over the content box of the row: measured on
222         * the row, because a hidden edge has no box */
223        var cs = getComputedStyle(container);
224        var padTop = parseFloat(cs.paddingTop) || 0;
225        var top = container.getBoundingClientRect().top + container.clientTop + padTop;
226        var h = container.clientHeight - padTop - (parseFloat(cs.paddingBottom) || 0);
227        var item = getActiveItem(container) || container.querySelector(ITEM_SELECTOR);
228        var mid = h / 2;
229        if (item) {
230            var r = null;
231            try {
232                var range = document.createRange();
233                range.selectNodeContents(item);
234                r = range.getBoundingClientRect();
235            } catch (_) { /* old engine */ }
236            if (!r || !r.height) r = item.getBoundingClientRect();
237            mid = r.top + r.height / 2 - top;
238        }
239        var btn = edge.firstElementChild;
240        var half = (btn && btn.offsetHeight ? btn.offsetHeight : 24) / 2;
241        var air = 4;                                          /* above the rule */
242        mid = Math.min(mid, h - half - air);
243        mid = Math.max(mid, half);
244        return Math.round(mid);
245    }
246
247    /* La tab attiva, se e' fuori dalla parte visibile, viene portata dentro
248     * (solo in orizzontale: scrollIntoView muoverebbe anche la pagina). */
249    function revealActive(container, active) {
250        var max = container.scrollWidth - container.clientWidth;
251        if (max <= 1) return;
252        var rectC = container.getBoundingClientRect();
253        var rectA = active.getBoundingClientRect();
254        var pad = 40;                                         /* spazio per il bordo sfumato */
255        var left = null;
256        if (rectA.left < rectC.left + pad) left = container.scrollLeft - (rectC.left + pad - rectA.left);
257        else if (rectA.right > rectC.right - pad) left = container.scrollLeft + (rectA.right - (rectC.right - pad));
258        if (left === null) return;
259        left = Math.max(0, Math.min(max, left));
260        if (typeof container.scrollTo === 'function') {
261            try { container.scrollTo({ left: left, behavior: 'smooth' }); return; } catch (_) { /* vecchio motore */ }
262        }
263        container.scrollLeft = left;
264    }
265
266    function positionIndicator(container) {
267        if (!container || !container.offsetParent) return;       /* container nascosto */
268        updateEdges(container);
269        var active = getActiveItem(container);
270        if (!active) return;
271        var ind = getIndicator(container);
272        /* Per nav-tabs Bootstrap, l'active è la <a> dentro <li>: usa la <a> stessa
273         * (è ciò che otteniamo da ACTIVE_SELECTOR). Bounding rect sulla <a>. */
274        var rectC = container.getBoundingClientRect();
275        var rectA = active.getBoundingClientRect();
276        ind.style.width = rectA.width + 'px';
277        /* + scrollLeft: il container può scorrere orizzontalmente (molte tab);
278         * l'indicator è figlio assoluto del contenuto scrollabile, quindi va
279         * posizionato rispetto all'origine del contenuto, non al bordo visibile. */
280        ind.style.transform = 'translateX(' + (rectA.left - rectC.left + container.scrollLeft) + 'px)';
281        ind.style.opacity = '1';
282    }
283
284    function attachContainer(container) {
285        if (container.__bbxTabsAttached) return;
286        container.__bbxTabsAttached = true;
287
288        /* Listener click delegato sui figli. Bootstrap aggiorna .active in
289         * modo sincrono dopo il default click handler — usiamo doppio
290         * setTimeout per coprire sia render immediato che eventuale
291         * transition (fade) di Bootstrap. */
292        container.addEventListener('click', function (e) {
293            var target = e.target && e.target.closest && e.target.closest(ITEM_SELECTOR);
294            if (!target || !container.contains(target)) return;
295            setTimeout(function () { positionIndicator(container); }, 0);
296            setTimeout(function () {
297                positionIndicator(container);
298                var active = getActiveItem(container);
299                if (active) revealActive(container, active);
300            }, 200);
301        });
302
303        /* Lo scroll (dito, trackpad, chevron) aggiorna i bordi. */
304        var scrollRaf = null;
305        container.addEventListener('scroll', function () {
306            if (scrollRaf) return;
307            scrollRaf = requestAnimationFrame(function () {
308                scrollRaf = null;
309                updateEdges(container);
310            });
311        }, { passive: true });
312
313        /* Eventi Bootstrap (BS4 + BS5 emettono entrambi shown.bs.tab).
314         * Attacchiamo via jQuery se disponibile (Bootstrap richiede jQuery
315         * fino a BS4; BS5 emette eventi nativi pure). Per BS5 nativo, è già
316         * coperto dal listener click sopra. */
317        if (typeof window.jQuery !== 'undefined') {
318            try {
319                window.jQuery(container).on('shown.bs.tab', function () {
320                    positionIndicator(container);
321                });
322            } catch (_) { /* swallow */ }
323        }
324
325        /* The first measure, and every one after it, happens when the
326         * geometry really changes: the row and each of its items are under a
327         * ResizeObserver, so a row that is laid out late — hidden under the
328         * page loader, in a popup still behind its curtain, at width 0 in a
329         * collapsed panel, filled by a builder after the init, re-measured
330         * when the web font arrives — gets its edges in the frame it gets its
331         * width, without waiting for a scroll or a click. A container that
332         * is hidden (offsetParent null) is skipped and picked up when it
333         * shows: going from display:none to a box is a resize. */
334        watch(container);
335        schedule(container);
336    }
337
338    /* ---- Re-measure when the geometry changes --------------------------- */
339    var queue = [];
340    var queueRaf = null;
341
342    /* Coalesced on the next frame: a burst of resizes and mutations (a
343     * dialog that opens, a header that re-renders its tabs) measures once. */
344    function schedule(container) {
345        if (!container || container.__bbxQueued) return;
346        container.__bbxQueued = true;
347        queue.push(container);
348        if (queueRaf) return;
349        queueRaf = (typeof requestAnimationFrame === 'function')
350            ? requestAnimationFrame(flush)
351            : setTimeout(flush, 16);
352    }
353
354    function flush() {
355        queueRaf = null;
356        var list = queue;
357        queue = [];
358        for (var i = 0; i < list.length; i++) {
359            var c = list[i];
360            c.__bbxQueued = false;
361            if (!c.isConnected) continue;
362            positionIndicator(c);
363            /* the active tab is brought into view once per active tab: when
364             * it first becomes visible, and again when the row is rebuilt
365             * with another one (the header re-renders on a section change) */
366            if (!c.offsetParent) continue;
367            var active = getActiveItem(c);
368            if (active && c.__bbxRevealed !== active) {
369                c.__bbxRevealed = active;
370                revealActive(c, active);
371            }
372        }
373    }
374
375    var resizer = (typeof ResizeObserver !== 'undefined')
376        ? new ResizeObserver(function (entries) {
377            for (var i = 0; i < entries.length; i++) {
378                var t = entries[i].target;
379                schedule(t.__bbxTabsOf || t);
380            }
381            /* measured right here, between layout and paint: what this
382             * writes is painted in the SAME frame the row got its width
383             * (through a rAF it came one frame after the row showed) */
384            if (queueRaf) {
385                if (typeof cancelAnimationFrame === 'function') cancelAnimationFrame(queueRaf);
386                flush();
387            }
388        })
389        : null;
390
391    /* the row (its own box) and its items (the content: a label that
392     * changes, a font that arrives, a tab added) */
393    function watch(container) {
394        if (!resizer) return;
395        resizer.observe(container);
396        var kids = container.children;
397        for (var i = 0; i < kids.length; i++) {
398            var k = kids[i];
399            /* not the parts this engine injects: the indicator animates its
400             * width, and every frame of it would be a resize */
401            if (k.__bbxTabsOf || k.classList.contains(EDGE_CLASS) ||
402                k.classList.contains(INDICATOR_CLASS_GENERIC) ||
403                k.classList.contains(INDICATOR_CLASS_HEADER) ||
404                k.classList.contains(INDICATOR_CLASS_SEGMENT)) continue;
405            k.__bbxTabsOf = container;
406            resizer.observe(k);
407        }
408    }
409
410    function scanAndAttach(root) {
411        var scope = root && root.querySelectorAll ? root : document;
412        var nodes = scope.querySelectorAll(CONTAINER_SELECTOR);
413        for (var i = 0; i < nodes.length; i++) attachContainer(nodes[i]);
414    }
415
416    function isTabContainer(el) {
417        if (!el || el.nodeType !== 1) return false;
418        try { return el.matches && el.matches(CONTAINER_SELECTOR); }
419        catch (_) { return false; }
420    }
421
422    function init() {
423        scanAndAttach(document);
424
425        /* MutationObserver: copre tab inseriti dinamicamente (dialog modali
426         * caricati via AJAX dentro #md-modal, sezioni iframe che cambiano,
427         * ecc.). Osserva l'intero body in subtree. */
428        if (typeof MutationObserver !== 'undefined') {
429            var observer = new MutationObserver(function (mutations) {
430                for (var i = 0; i < mutations.length; i++) {
431                    var added = mutations[i].addedNodes;
432                    /* items added to or removed from a row already attached
433                     * (a builder that rewrites the tabs inside the same
434                     * container, bbx-header's renderTabs): the content
435                     * changed, measure again */
436                    if (mutations[i].type === 'childList') {
437                        var row = mutations[i].target;
438                        if (row && row.__bbxTabsAttached) {
439                            watch(row);
440                            schedule(row);
441                        }
442                    }
443                    for (var j = 0; j < added.length; j++) {
444                        var node = added[j];
445                        if (node.nodeType !== 1) continue;
446                        if (isTabContainer(node)) attachContainer(node);
447                        else if (node.querySelectorAll) scanAndAttach(node);
448                    }
449                    /* Class attr mutation: se .active/.is-active cambia su un item,
450                     * riposiziona l'indicator del container che lo contiene. */
451                    if (mutations[i].type === 'attributes' && mutations[i].attributeName === 'class') {
452                        var t = mutations[i].target;
453                        if (t && t.closest) {
454                            var c = t.closest(CONTAINER_SELECTOR);
455                            if (c) positionIndicator(c);
456                        }
457                    }
458                }
459            });
460            observer.observe(document.body, {
461                childList: true,
462                subtree: true,
463                attributes: true,
464                attributeFilter: ['class', 'aria-selected']
465            });
466        }
467
468        /* Resize: riposiziona tutti gli indicator (le tab cambiano width
469         * in base al container). Debounced via rAF. */
470        /* the web font changes every width in the row: measure again when
471         * it is in (the items' ResizeObserver sees it too, this is the
472         * engine without one) */
473        if (document.fonts && document.fonts.ready && typeof document.fonts.ready.then === 'function') {
474            document.fonts.ready.then(function () {
475                document.querySelectorAll(CONTAINER_SELECTOR).forEach(schedule);
476            });
477        }
478
479        var resizeRaf = null;
480        window.addEventListener('resize', function () {
481            if (resizeRaf) cancelAnimationFrame(resizeRaf);
482            resizeRaf = requestAnimationFrame(function () {
483                document.querySelectorAll(CONTAINER_SELECTOR).forEach(positionIndicator);
484                resizeRaf = null;
485            });
486        });
487    }
488
489    if (document.readyState === 'loading') {
490        document.addEventListener('DOMContentLoaded', init);
491    } else {
492        init();
493    }
494
495    /* API pubblica minimale per casi edge (es. tab inseriti senza
496     * triggerare childList — raro). */
497    window.BBX = window.BBX || {};
498    window.BBX.Tabs = {
499        attach: attachContainer,
500        scan: scanAndAttach,
501        reposition: positionIndicator,
502        edges: updateEdges
503    };
504})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.