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.