(() => { "use strict"; const CONFIG = window.QF_SECTOR_CONFIG; if (!CONFIG) { console.error( "[qfSector] CONFIG was not found." ); return; } // -------------------------------------------------- // CONFIG // -------------------------------------------------- const NAV_SELECTOR = "[data-sector-nav]"; const SWITCH_SELECTOR = "[data-sector-switch]"; const WEBFLOW_HIDDEN_CLASS = "w-condition-invisible"; const PAGE_LOCK_ATTRIBUTE = CONFIG.lockAttribute || "data-sector-lock"; // Keep sector state synchronized between open tabs/windows. const SYNC_ACROSS_TABS = true; // -------------------------------------------------- // STATE // -------------------------------------------------- let currentSector = document.documentElement.getAttribute( CONFIG.htmlAttribute ) || CONFIG.defaultSector; let currentSource = window.QF_INITIAL_SECTOR?.source || "unknown"; const navbarCache = new Map(); let navbarMount = null; let navbarParent = null; let navbarInitialized = false; // -------------------------------------------------- // HELPERS // -------------------------------------------------- function log(...args) { if (!CONFIG.debug) return; console.log( "[qfSector]", ...args ); } function isValidSector(sector) { return CONFIG.sectors.includes( sector ); } function normalizePath(path) { if (!path) return "/"; const normalized = path.replace(/\/+$/, ""); return normalized || "/"; } function getPageLockSector() { const element = document.querySelector( `[${PAGE_LOCK_ATTRIBUTE}]` ); const domValue = element?.getAttribute( PAGE_LOCK_ATTRIBUTE ); if (isValidSector(domValue)) { return domValue; } const earlyValue = window.QF_PAGE_LOCK_SECTOR; return isValidSector(earlyValue) ? earlyValue : null; } function getForcedSector( pathname ) { const path = normalizePath(pathname); for ( const rule of CONFIG.forcedPaths ) { const rulePath = normalizePath(rule.path); if ( rule.type === "exact" && path === rulePath ) { return rule.sector; } if ( rule.type === "prefix" && ( path === rulePath || path.startsWith( `${rulePath}/` ) ) ) { return rule.sector; } } return null; } function getQuerySector() { const params = new URLSearchParams( window.location.search ); const sector = params.get( CONFIG.queryParam ); return isValidSector(sector) ? sector : null; } function clearStorage() { try { localStorage.removeItem( CONFIG.storageKey ); localStorage.removeItem( CONFIG.storageExpiryKey ); } catch (error) { log( "Unable to clear localStorage:", error ); } } function getStoredSector() { try { const sector = localStorage.getItem( CONFIG.storageKey ); if (!isValidSector(sector)) { if (sector !== null) { clearStorage(); } return null; } const expiryRaw = localStorage.getItem( CONFIG.storageExpiryKey ); if (!expiryRaw) { return sector; } const expiresAt = Number(expiryRaw); if ( !Number.isFinite(expiresAt) || Date.now() >= expiresAt ) { clearStorage(); log( "Stored sector expired." ); return null; } return sector; } catch (error) { log( "Unable to read localStorage:", error ); return null; } } function saveToStorage(sector) { if (!isValidSector(sector)) { console.warn( `[qfSector] Invalid sector: "${sector}".` ); return false; } try { localStorage.setItem( CONFIG.storageKey, sector ); if ( Number.isFinite( CONFIG.storageTtlMs ) && CONFIG.storageTtlMs > 0 ) { localStorage.setItem( CONFIG.storageExpiryKey, String( Date.now() + CONFIG.storageTtlMs ) ); } else { localStorage.removeItem( CONFIG.storageExpiryKey ); } return true; } catch (error) { console.warn( "[qfSector] Unable to write localStorage:", error ); return false; } } /* Final page resolution priority: 1. data-sector-lock 2. Forced URL path 3. ?sector= 4. localStorage 5. Financial Services default */ function resolveCurrentSector() { const pageLock = getPageLockSector(); if (pageLock) { return { sector: pageLock, source: "page-lock", }; } const forcedSector = getForcedSector( window.location.pathname ); if (forcedSector) { return { sector: forcedSector, source: "path", }; } const querySector = getQuerySector(); if (querySector) { return { sector: querySector, source: "query", }; } const storedSector = getStoredSector(); if (storedSector) { return { sector: storedSector, source: "storage", }; } return { sector: CONFIG.defaultSector, source: "default", }; } function dispatchChange( sector, previousSector, source, extra = {} ) { window.dispatchEvent( new CustomEvent( "qf-sector-change", { detail: { sector, previousSector, source, ...extra, }, } ) ); } // -------------------------------------------------- // NAVIGATION // -------------------------------------------------- function cacheSectorNavbars() { if (navbarInitialized) { return; } const navbars = Array.from( document.querySelectorAll( NAV_SELECTOR ) ); if (!navbars.length) { navbarInitialized = true; log( "No sector navbars found." ); return; } const firstNavbar = navbars[0]; navbarParent = firstNavbar.parentNode; if (!navbarParent) { navbarInitialized = true; return; } navbarMount = document.createComment( "qf-sector-nav-mount" ); navbarParent.insertBefore( navbarMount, firstNavbar ); navbars.forEach( (navbar) => { const sector = navbar.getAttribute( "data-sector-nav" ); if (!isValidSector(sector)) { return; } navbarCache.set( sector, navbar ); } ); navbarInitialized = true; } function mountNavbar(sector) { if (!navbarInitialized) { cacheSectorNavbars(); } if ( !navbarMount || !navbarParent ) { return; } const activeNavbar = navbarCache.get(sector); if (!activeNavbar) { console.warn( `[qfSector] Navbar for "${sector}" was not found.` ); return; } navbarCache.forEach( ( navbar, navbarSector ) => { if ( navbarSector !== sector && navbar.isConnected ) { // Keep CMS wrappers in the server-rendered order. Finsweet List // matches paginated CMS lists by their position in the document. navbar.style.setProperty("display", "none", "important"); navbar.setAttribute("aria-hidden", "true"); navbar.inert = true; } } ); activeNavbar.style.removeProperty("display"); activeNavbar.inert = false; activeNavbar.classList.remove( WEBFLOW_HIDDEN_CLASS ); activeNavbar.removeAttribute( "aria-hidden" ); if (!activeNavbar.isConnected) { navbarParent.insertBefore( activeNavbar, navbarMount.nextSibling ); } log( `Active navbar: "${sector}".` ); } function initNavbar() { cacheSectorNavbars(); mountNavbar(currentSector); } // -------------------------------------------------- // PUBLIC API // -------------------------------------------------- function getSector() { return currentSector; } function setSector( requestedSector, options = {} ) { if ( !isValidSector( requestedSector ) ) { console.warn( `[qfSector] Invalid sector: "${requestedSector}".` ); return false; } const { source = "api", updateDocument = true, persist = true, forceDispatch = false, } = options; const previousSector = currentSector; const pageLock = getPageLockSector(); const effectiveSector = pageLock || requestedSector; const effectiveSource = pageLock ? "page-lock" : source; const blockedByPageLock = Boolean( pageLock && requestedSector !== pageLock ); /* A locked page must never overwrite the visitor's saved preference through live/programmatic state changes. Normal navigation links are handled separately and can still save the destination sector before leaving the page. */ if ( persist && !pageLock ) { saveToStorage( requestedSector ); } currentSector = effectiveSector; currentSource = effectiveSource; if (updateDocument) { document.documentElement.setAttribute( CONFIG.htmlAttribute, effectiveSector ); } mountNavbar( effectiveSector ); if ( previousSector !== effectiveSector || forceDispatch || blockedByPageLock ) { dispatchChange( effectiveSector, previousSector, effectiveSource, { requestedSector, pageLockSector: pageLock, blockedByPageLock, } ); } log( `Sector set to "${effectiveSector}" from ${effectiveSource}.` ); return true; } function clearSector() { clearStorage(); log( "Stored sector cleared." ); } function getDebugInfo() { let storedSector = null; let expiresAt = null; try { storedSector = localStorage.getItem( CONFIG.storageKey ); const storedExpiry = localStorage.getItem( CONFIG.storageExpiryKey ); if (storedExpiry) { const expiryNumber = Number(storedExpiry); if ( Number.isFinite( expiryNumber ) ) { expiresAt = new Date( expiryNumber ).toISOString(); } } } catch (error) { // Ignore debug read errors. } return { sector: currentSector, source: currentSource, pageLockSector: getPageLockSector(), pathname: window.location.pathname, query: window.location.search, storedSector, expiresAt, syncAcrossTabs: SYNC_ACROSS_TABS, renderedNavbars: document.querySelectorAll( NAV_SELECTOR ).length, cachedNavbars: navbarCache.size, config: CONFIG, }; } function onChange(callback) { if ( typeof callback !== "function" ) { console.warn( "[qfSector] onChange expects a function." ); return () => {}; } const handler = (event) => { callback( event.detail.sector, event.detail ); }; window.addEventListener( "qf-sector-change", handler ); return () => { window.removeEventListener( "qf-sector-change", handler ); }; } // -------------------------------------------------- // FINAL DOM-AWARE INITIAL RESOLUTION // -------------------------------------------------- const initialResolved = resolveCurrentSector(); currentSector = initialResolved.sector; currentSource = initialResolved.source; document.documentElement.setAttribute( CONFIG.htmlAttribute, initialResolved.sector ); window.QF_INITIAL_SECTOR = initialResolved; if ( initialResolved.source === "page-lock" ) { window.QF_PAGE_LOCK_SECTOR = initialResolved.sector; } /* Only explicit path/query resolution persists. data-sector-lock never changes the saved preference. */ if ( initialResolved.source === "path" || initialResolved.source === "query" ) { saveToStorage( initialResolved.sector ); } window.qfSector = { get: getSector, set: setSector, clear: clearSector, onChange, debug: getDebugInfo, }; // -------------------------------------------------- // SECTOR SWITCHES // -------------------------------------------------- function isModifiedNavigation( event, link ) { const target = ( link.getAttribute( "target" ) || "" ).toLowerCase(); return Boolean( event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || target === "_blank" ); } document.addEventListener( "click", (event) => { if ( !( event.target instanceof Element ) ) { return; } const switchElement = event.target.closest( SWITCH_SELECTOR ); if (!switchElement) return; const sector = switchElement.getAttribute( "data-sector-switch" ); if (!isValidSector(sector)) { console.warn( `[qfSector] Invalid data-sector-switch value: "${sector}".` ); return; } const isLink = switchElement.matches( "a[href]" ); if (isLink) { /* Cmd/Ctrl/Shift/Alt-click and target="_blank" open another browsing context. Do not mutate the current tab's sector state before that navigation happens. */ if ( isModifiedNavigation( event, switchElement ) ) { return; } /* A normal navigation link can save the user's selected destination sector even when the current page itself is sector-locked. */ saveToStorage( sector ); log( `Sector "${sector}" saved before navigation.` ); return; } /* Live/non-link controls cannot override a page lock. setSector() will keep the effective page sector locked and re-sync subscribers if necessary. */ setSector( sector, { source: "switch", updateDocument: true, } ); } ); // -------------------------------------------------- // BFCache / BACK-FORWARD NAVIGATION // -------------------------------------------------- window.addEventListener( "pageshow", (event) => { if (!event.persisted) { return; } const resolved = resolveCurrentSector(); setSector( resolved.sector, { source: resolved.source, updateDocument: true, persist: false, forceDispatch: true, } ); } ); // -------------------------------------------------- // CROSS-TAB SYNCHRONIZATION // -------------------------------------------------- if (SYNC_ACROSS_TABS) { window.addEventListener( "storage", (event) => { if ( event.storageArea !== localStorage ) { return; } if ( event.key !== CONFIG.storageKey && event.key !== CONFIG.storageExpiryKey ) { return; } /* Re-resolve instead of blindly applying event.newValue so page locks, forced paths and ?sector= keep their precedence. */ const resolved = resolveCurrentSector(); setSector( resolved.sector, { source: resolved.source === "page-lock" ? "page-lock" : "storage-sync", updateDocument: true, persist: false, } ); } ); } // -------------------------------------------------- // INITIALIZE AFTER WEBFLOW // -------------------------------------------------- window.Webflow ||= []; window.Webflow.push(() => { initNavbar(); /* Force one final subscriber refresh after Webflow is ready. This is useful when an early page lock corrected the provisional sector before UI scripts fully initialized. */ dispatchChange( currentSector, currentSector, currentSource, { pageLockSector: getPageLockSector(), initial: true, } ); log( "Initialized:", window.qfSector.debug() ); }); })();