import { onBeforeUnmount, onMounted, ref, watch, type Ref } from "vue"; const VISIBILITY_ATTR = "data-ccb-mobile-summary-visibility"; const CENTER_DIST_ATTR = "data-ccb-mobile-summary-center-dist"; const ACTIVE_EVENT = "ccb-mobile-summary-active-calc"; const STICKY_MODAL_Z_INDEX = 1060; const INACTIVE_EARLY_OFFSET_PX = 350; const isElementVisible = (element: HTMLElement): boolean => { return ( !element.classList.contains("ccb-calc-hidden") && element.getClientRects().length > 0 ); }; const getCalculatorWidgets = (): HTMLElement[] => { return Array.from( document.querySelectorAll(".ccb-main-widget[data-calc-id]"), ).filter(isElementVisible); }; const getStickyModalElement = (calcId: number): HTMLElement | null => { return document.getElementById(`ccb-sticky-calc-modal-${calcId}`); }; const isStickyModalOpen = (calcId: number): boolean => { return getStickyModalElement(calcId)?.classList.contains("show") ?? false; }; const getVisibleCalculatorRoot = (calcId: number): HTMLElement | null => { const widgets = Array.from( document.querySelectorAll( `.ccb-main-widget[data-calc-id="${calcId}"]`, ), ); if (!widgets.length) { return document.getElementById(`ccb_app_${calcId}`); } const stickyModal = getStickyModalElement(calcId); if (stickyModal?.classList.contains("show")) { const widgetInModal = widgets.find((widget) => stickyModal.contains(widget), ); if (widgetInModal && isElementVisible(widgetInModal)) { return widgetInModal; } } return widgets.find(isElementVisible) ?? widgets[0] ?? null; }; const getOpenStickyModalCalcId = (): string | null => { const openModals = document.querySelectorAll( ".ccb-sticky-modal.show", ); for (const modal of openModals) { const calcId = modal.id.replace("ccb-sticky-calc-modal-", ""); const widget = modal.querySelector( `.ccb-main-widget[data-calc-id="${calcId}"]`, ); if (widget && isElementVisible(widget)) { return calcId; } } return null; }; const pickActiveCalcId = (): string | null => { const openModalCalcId = getOpenStickyModalCalcId(); if (openModalCalcId) { return openModalCalcId; } const widgets = getCalculatorWidgets(); if (widgets.length <= 1) { const widget = widgets[0]; if (!widget) { return null; } const ratioAttr = widget.getAttribute(VISIBILITY_ATTR); if (ratioAttr === null) { return widget.getAttribute("data-calc-id"); } const ratio = Number.parseFloat(ratioAttr); return ratio > 0 ? widget.getAttribute("data-calc-id") : null; } let winner: string | null = null; let bestRatio = -1; let bestCenterDist = Infinity; widgets.forEach((widget) => { const ratio = Number.parseFloat( widget.getAttribute(VISIBILITY_ATTR) ?? "0", ); const centerDist = Number.parseFloat( widget.getAttribute(CENTER_DIST_ATTR) ?? "Infinity", ); if (ratio <= 0) { return; } if ( ratio > bestRatio || (ratio === bestRatio && centerDist < bestCenterDist) ) { bestRatio = ratio; bestCenterDist = centerDist; winner = widget.getAttribute("data-calc-id"); } }); return winner; }; const broadcastActiveCalc = (): void => { window.dispatchEvent( new CustomEvent(ACTIVE_EVENT, { detail: { calcId: pickActiveCalcId() }, }), ); }; let scrollListenerCount = 0; let scrollRafId: number | null = null; const scheduleRecalculate = (): void => { if (scrollRafId !== null) { return; } scrollRafId = window.requestAnimationFrame(() => { scrollRafId = null; broadcastActiveCalc(); }); }; const ensureGlobalListeners = (): void => { if (scrollListenerCount !== 0) { return; } window.addEventListener("scroll", scheduleRecalculate, { passive: true }); window.addEventListener("resize", scheduleRecalculate, { passive: true }); }; const releaseGlobalListeners = (): void => { if (scrollListenerCount !== 0) { return; } window.removeEventListener("scroll", scheduleRecalculate); window.removeEventListener("resize", scheduleRecalculate); }; export const useMobileSummaryActiveTrigger = ( calcId: Ref, isSummaryOpen: Ref, ) => { const isTriggerActive = ref(true); const isInStickyModal = ref(false); let observer: IntersectionObserver | null = null; let stickyModalElement: HTMLElement | null = null; const updateStickyModalState = (): void => { if (calcId.value === null) { isInStickyModal.value = false; return; } const modalOpen = isStickyModalOpen(calcId.value); isInStickyModal.value = modalOpen && !isSummaryOpen.value; }; const syncTriggerVisibility = (activeCalcId: string | null): void => { updateStickyModalState(); if (isSummaryOpen.value || isInStickyModal.value) { isTriggerActive.value = true; return; } if (!activeCalcId || calcId.value === null) { isTriggerActive.value = false; return; } isTriggerActive.value = String(calcId.value) === activeCalcId; }; const handleActiveChange = (event: Event): void => { const detail = (event as CustomEvent<{ calcId: string | null }>).detail; syncTriggerVisibility(detail?.calcId ?? null); }; const handleStickyModalShown = (): void => { updateStickyModalState(); isTriggerActive.value = true; broadcastActiveCalc(); }; const handleStickyModalHidden = (): void => { updateStickyModalState(); broadcastActiveCalc(); }; watch(isSummaryOpen, (isOpen) => { updateStickyModalState(); if (isOpen) { isTriggerActive.value = true; return; } broadcastActiveCalc(); }); onMounted(() => { if (typeof window === "undefined" || calcId.value === null) { return; } const root = getVisibleCalculatorRoot(calcId.value); if (!root) { return; } updateStickyModalState(); stickyModalElement = getStickyModalElement(calcId.value); stickyModalElement?.addEventListener( "shown.bs.modal", handleStickyModalShown, ); stickyModalElement?.addEventListener( "hidden.bs.modal", handleStickyModalHidden, ); if (isStickyModalOpen(calcId.value)) { isTriggerActive.value = true; } observer = new IntersectionObserver( (entries) => { entries.forEach((entry) => { const target = entry.target as HTMLElement; target.setAttribute(VISIBILITY_ATTR, String(entry.intersectionRatio)); const rect = entry.boundingClientRect; const centerDist = Math.abs( (rect.top + rect.bottom) / 2 - window.innerHeight / 2, ); target.setAttribute(CENTER_DIST_ATTR, String(centerDist)); }); scheduleRecalculate(); }, { threshold: [0, 0.25, 0.5, 0.75, 1], rootMargin: `-${INACTIVE_EARLY_OFFSET_PX}px 0px -${INACTIVE_EARLY_OFFSET_PX}px 0px`, }, ); observer.observe(root); window.addEventListener(ACTIVE_EVENT, handleActiveChange); ensureGlobalListeners(); scrollListenerCount += 1; broadcastActiveCalc(); }); onBeforeUnmount(() => { observer?.disconnect(); window.removeEventListener(ACTIVE_EVENT, handleActiveChange); stickyModalElement?.removeEventListener( "shown.bs.modal", handleStickyModalShown, ); stickyModalElement?.removeEventListener( "hidden.bs.modal", handleStickyModalHidden, ); scrollListenerCount = Math.max(0, scrollListenerCount - 1); releaseGlobalListeners(); scheduleRecalculate(); }); return { isTriggerActive, isInStickyModal, stickyModalZIndex: STICKY_MODAL_Z_INDEX, }; };