(() => { "use strict"; // -------------------------------------------------- // QF SECTOR UI CONFIG // -------------------------------------------------- const CONFIG = { tabGroupSelector: ".switchs-tab-wrapper", switchSelector: "[data-sector-switch]", activeClass: "is-active", backgroundSelector: "[data-sector-will-change]", backgroundAttribute: "data-background-color", publicBackgroundValue: "dark", sectorFilterSelector: 'select[fs-list-element="select"][fs-list-field="category"]', badgeGroupSelector: '[fs-list-nest="category"].badge-sector-item, ' + '.collection-list-badge .badge-sector-item, ' + '.badge-sector-item_wrap .badge-sector-item', badgeItemSelector: '[role="listitem"], .collection-item, .w-dyn-item', badgeValueSelector: '[fs-list-field="category"]', sectorValues: { "financial-services": "Financial Services", "public-sector": "Public Sector" }, removeEmptySectorOption: true, debug: false }; // -------------------------------------------------- // STATE // -------------------------------------------------- const originalBackgroundValues = new WeakMap(); const patchedListInstances = new WeakSet(); let initialized = false; let finsweetReady = false; let isSyncingSectorFilters = false; let suppressFinsweetScroll = false; let suppressScrollReleaseTimer = null; let unsubscribeSector = null; const LIST_CONTROL_SELECTOR = '[fs-list-element="filters"], ' + '[fs-list-element="page-button"], ' + '[fs-list-element="page-dots"], ' + '[fs-list-element="pagination-next"], ' + '[fs-list-element="pagination-previous"], ' + '.w-pagination-wrapper, ' + '[fs-list-element="clear"], ' + '.empty-state'; const LIST_SCROLL_WINDOW_MS = 5000; let lastListControlUse = -Infinity; function noteListControlUse(event) { if (!event.isTrusted) return; if (event.target?.closest?.(LIST_CONTROL_SELECTOR)) { lastListControlUse = performance.now(); } } ["pointerdown", "keydown", "change", "input"].forEach( (type) => { document.addEventListener( type, noteListControlUse, true ); } ); function visitorJustUsedListControls() { return ( performance.now() - lastListControlUse < LIST_SCROLL_WINDOW_MS ); } // -------------------------------------------------- // HELPERS // -------------------------------------------------- function log(...args) { if (!CONFIG.debug) return; console.log("[qfSectorUI]", ...args); } function getSector() { return window.qfSector?.get?.() || null; } function getFilterValue(sector) { return CONFIG.sectorValues[sector] || ""; } function getSectorFromFilterValue(value) { for (const [sector, filterValue] of Object.entries( CONFIG.sectorValues )) { if (filterValue === value) { return sector; } } return null; } // -------------------------------------------------- // TABS // -------------------------------------------------- function syncTabs(sector) { const groups = document.querySelectorAll( CONFIG.tabGroupSelector ); groups.forEach((group) => { const switches = group.querySelectorAll( CONFIG.switchSelector ); switches.forEach((element) => { const elementSector = element.getAttribute( "data-sector-switch" ); const isActive = elementSector === sector; element.classList.toggle( CONFIG.activeClass, isActive ); const isLink = element.matches("a[href]"); if (isLink) { element.removeAttribute("aria-pressed"); } else { element.setAttribute( "aria-pressed", String(isActive) ); } const isNativeInteractive = element.matches( "a, button, input, select, textarea" ); if (!isNativeInteractive) { element.setAttribute("role", "button"); element.setAttribute("tabindex", "0"); } }); }); } // -------------------------------------------------- // BACKGROUND SECTIONS // -------------------------------------------------- function rememberBackgroundValue(element) { if (originalBackgroundValues.has(element)) { return; } originalBackgroundValues.set( element, element.hasAttribute(CONFIG.backgroundAttribute) ? element.getAttribute(CONFIG.backgroundAttribute) : null ); } function syncBackgrounds(sector) { const sections = document.querySelectorAll( CONFIG.backgroundSelector ); sections.forEach((section) => { rememberBackgroundValue(section); if (sector === "public-sector") { section.setAttribute( CONFIG.backgroundAttribute, CONFIG.publicBackgroundValue ); return; } const originalValue = originalBackgroundValues.get( section ); if (originalValue === null) { section.removeAttribute( CONFIG.backgroundAttribute ); } else { section.setAttribute( CONFIG.backgroundAttribute, originalValue ); } }); } // -------------------------------------------------- // SECTOR BADGES // -------------------------------------------------- function normalizeLabel(value) { return String(value || "") .trim() .toLowerCase(); } /* Resource Hub has its own local filter state. "all" means showing the first available sector badge in each CMS card. Other pages continue using the global sector. */ function getBadgeMode(group, globalSector) { const resourceList = group.closest( ".block-resource-index-wrapper .block-list-cms" ); const hubRoot = resourceList?.closest( '.block-index-wrapper[data-qf-hub-badge-mode]' ); const hubMode = hubRoot?.getAttribute( "data-qf-hub-badge-mode" ); if ( hubMode === "all" || Object.prototype.hasOwnProperty.call( CONFIG.sectorValues, hubMode ) ) { return hubMode; } return globalSector; } function syncSectorBadges(sector) { const groups = document.querySelectorAll( CONFIG.badgeGroupSelector ); const allowedLabels = new Set( Object.values(CONFIG.sectorValues).map( normalizeLabel ) ); groups.forEach((group) => { const mode = getBadgeMode(group, sector); const showFirstAvailable = mode === "all"; const activeValue = normalizeLabel( getFilterValue(mode) ); if (!showFirstAvailable && !activeValue) { return; } const items = Array.from( group.querySelectorAll( CONFIG.badgeItemSelector ) ); let visibleItemFound = false; items.forEach((item) => { const valueElement = item.querySelector( CONFIG.badgeValueSelector ) || item; const itemValue = normalizeLabel( valueElement.textContent ); // Leave unrelated badges untouched. if (!allowedLabels.has(itemValue)) { return; } /* All sectors: Show the first available sector badge. Filtered: Show only the matching sector badge. Maximum one visible sector badge per card. */ const shouldShow = !visibleItemFound && ( showFirstAvailable || itemValue === activeValue ); item.style.display = shouldShow ? "" : "none"; item.setAttribute( "aria-hidden", String(!shouldShow) ); if (shouldShow) { visibleItemFound = true; } }); }); } // -------------------------------------------------- // FINSWEET SCROLL CONTROL // -------------------------------------------------- function patchFinsweetScroll(listInstances) { if (!Array.isArray(listInstances)) { return; } listInstances.forEach((listInstance) => { if ( !listInstance || patchedListInstances.has(listInstance) || typeof listInstance.scrollToAnchor !== "function" ) { return; } const originalScrollToAnchor = listInstance.scrollToAnchor.bind( listInstance ); listInstance.scrollToAnchor = (...args) => { if ( suppressFinsweetScroll || !visitorJustUsedListControls() ) { return; } return originalScrollToAnchor(...args); }; if (typeof listInstance.addHook === "function") { listInstance.addHook( "afterRender", (items) => { const sector = getSector(); if (sector) { syncSectorBadges(sector); } return items; } ); } patchedListInstances.add(listInstance); }); } function withoutFinsweetScroll(callback) { if (suppressScrollReleaseTimer !== null) { window.clearTimeout( suppressScrollReleaseTimer ); } suppressFinsweetScroll = true; try { callback(); } finally { suppressScrollReleaseTimer = window.setTimeout( () => { suppressFinsweetScroll = false; suppressScrollReleaseTimer = null; }, 0 ); } } // -------------------------------------------------- // FINSWEET SECTOR FILTERS // -------------------------------------------------- function getSectorFilters() { return Array.from( document.querySelectorAll( CONFIG.sectorFilterSelector ) ); } function removeEmptyOption(select) { if (!CONFIG.removeEmptySectorOption) { return; } Array.from(select.options) .filter((option) => { return option.value.trim() === ""; }) .forEach((option) => { option.remove(); }); } function prepareSectorFilters() { getSectorFilters().forEach( removeEmptyOption ); } function hasOption(select, value) { return Array.from(select.options).some( (option) => option.value === value ); } function getFinsweetTriggerEvent(select) { const form = select.closest( '[fs-list-element="filters"]' ); const filterOn = form?.getAttribute( "fs-list-filteron" ); if (filterOn === "change") { return "change"; } return "input"; } function triggerFinsweetFilter(select) { const eventName = getFinsweetTriggerEvent(select); select.dispatchEvent( new Event(eventName, { bubbles: true }) ); } function runSectorFilterSync( sector, forceFilter ) { const value = getFilterValue(sector); if (!value) return; const selects = getSectorFilters(); if (!selects.length) return; isSyncingSectorFilters = true; try { selects.forEach((select) => { removeEmptyOption(select); if (!hasOption(select, value)) { return; } const changed = select.value !== value; if (changed) { select.value = value; } if (changed || forceFilter) { triggerFinsweetFilter(select); } }); } finally { isSyncingSectorFilters = false; } } function syncSectorFilters( sector, options = {} ) { const { forceFilter = false, suppressScroll = false } = options; const sync = () => { runSectorFilterSync( sector, forceFilter ); }; if (suppressScroll) { withoutFinsweetScroll(sync); return; } sync(); } function handleSectorFilterChange(event) { const select = event.target.closest?.( CONFIG.sectorFilterSelector ); if (!select) return; if (isSyncingSectorFilters) { return; } const nextSector = getSectorFromFilterValue( select.value ); if (!nextSector) return; const currentSector = getSector(); if (nextSector !== currentSector) { window.qfSector?.set?.( nextSector, { source: "sector-filter", updateDocument: true } ); return; } window.setTimeout(() => { syncSectorFilters( nextSector, { suppressScroll: true } ); }, 0); } function registerFinsweetReady() { window.FinsweetAttributes ||= []; window.FinsweetAttributes.push([ "list", (listInstances) => { finsweetReady = true; patchFinsweetScroll( listInstances ); requestAnimationFrame(() => { prepareSectorFilters(); const sector = getSector(); if (!sector) return; syncSectorBadges(sector); syncSectorFilters( sector, { forceFilter: true, suppressScroll: true } ); log("Finsweet List ready."); }); } ]); } // -------------------------------------------------- // GLOBAL UI SYNC // -------------------------------------------------- function syncSectorUI( sector, options = {} ) { if (!sector) return; const { syncFilters = true, forceFilter = false, suppressFilterScroll = false } = options; syncTabs(sector); syncBackgrounds(sector); syncSectorBadges(sector); if (!syncFilters) { return; } syncSectorFilters( sector, { forceFilter, suppressScroll: suppressFilterScroll } ); log( `UI synced to "${sector}".` ); } // -------------------------------------------------- // ACCESSIBILITY // -------------------------------------------------- function handleTabKeydown(event) { if ( event.key !== "Enter" && event.key !== " " ) { return; } const target = event.target.closest( `${CONFIG.tabGroupSelector} ${CONFIG.switchSelector}` ); if (!target) return; if ( target.matches( "a, button, input, select, textarea" ) ) { return; } event.preventDefault(); target.click(); } // -------------------------------------------------- // SECTOR CHANGES // -------------------------------------------------- function handleSectorChange( nextSector, detail = {} ) { const source = detail.source || "unknown"; if (source === "switch") { syncSectorUI( nextSector, { suppressFilterScroll: true } ); return; } if (source === "sector-filter") { syncSectorUI( nextSector, { syncFilters: false } ); window.setTimeout(() => { syncSectorFilters( nextSector, { suppressScroll: true } ); }, 0); return; } syncSectorUI( nextSector, { suppressFilterScroll: true } ); } // -------------------------------------------------- // INITIALIZE // -------------------------------------------------- function init() { if (initialized) return; const sector = getSector(); if (!sector) return; initialized = true; prepareSectorFilters(); syncSectorUI( sector, { forceFilter: finsweetReady, suppressFilterScroll: true } ); unsubscribeSector = window.qfSector.onChange( handleSectorChange ); document.addEventListener( "change", handleSectorFilterChange ); document.addEventListener( "keydown", handleTabKeydown ); } function refresh() { const sector = getSector(); if (!sector) return; prepareSectorFilters(); syncSectorUI( sector, { forceFilter: finsweetReady, suppressFilterScroll: true } ); } function destroy() { if (unsubscribeSector) { unsubscribeSector(); unsubscribeSector = null; } document.removeEventListener( "change", handleSectorFilterChange ); document.removeEventListener( "keydown", handleTabKeydown ); if (suppressScrollReleaseTimer !== null) { window.clearTimeout( suppressScrollReleaseTimer ); suppressScrollReleaseTimer = null; } suppressFinsweetScroll = false; initialized = false; } // -------------------------------------------------- // REGISTER // -------------------------------------------------- registerFinsweetReady(); window.qfSectorUI = { refresh, // Refresh only badges without triggering filters. refreshBadges: () => { syncSectorBadges(getSector()); }, destroy, config: CONFIG }; if (document.readyState === "loading") { document.addEventListener( "DOMContentLoaded", init, { once: true } ); } else { init(); } })();