export default class AnchorNavigation { private static readonly MODAL_OPEN_BODY_CLASS = "has-modal"; private static readonly STICKY_FLOAT_TOLERANCE = 3; private static readonly SCROLLSPY_CENTER_BUFFER = 50; private static readonly TOP_SECTION_THRESHOLD = 100; private static readonly DRAG_START_THRESHOLD = 6; private static readonly SCROLL_END_DEBOUNCE_MS = 150; private static readonly SCROLL_EDGE_TOLERANCE = 1; private static readonly SCROLL_ALIGNMENT_TOLERANCE = 1; private static readonly MAX_SCROLL_CORRECTIONS = 2; private element: HTMLElement; private contentLeftElement: HTMLElement | null; private megamenuElement: HTMLElement | null; private resizeObserver: ResizeObserver | null; private supportsScrollEnd: boolean; private anchorClickHandler: (event: Event) => void; private scrollHandler: () => void; private scrollSpyHandler: () => void; private scrollEndHandler: () => void; private scrollEndFallbackHandler: () => void; private resizeHandler: () => void; private mouseDownHandler: (event: MouseEvent) => void; private dragStartHandler: (event: MouseEvent) => void; private dragMoveHandler: (event: MouseEvent) => void; private dragEndHandler: () => void; private horizontalScrollHandler: () => void; private nativeDragStartHandler: (event: DragEvent) => void; private isAutoScrolling: boolean = false; private isPointerDown: boolean = false; private isDraggingNav: boolean = false; private suppressClick: boolean = false; private dragStartX: number = 0; private dragStartScrollLeft: number = 0; private scrollTimeout: ReturnType | null = null; private scrollEndFallbackTimeout: ReturnType | null = null; private navLinks: NodeListOf | null = null; private sections: HTMLElement[] = []; private currentPath: string; private lastActiveIndex: number = 0; private autoScrollTargetId: string | null = null; private autoScrollCorrectionCount: number = 0; private lastStickyOffset: number = 0; private modalClassObserver: MutationObserver | null = null; private modalStateHandler: () => void; private isForcedFixed: boolean = false; private wasStickyFloating: boolean = false; private forcedFixedTop: number = 0; private forcedFixedLeft: number = 0; private fixedFlowPlaceholder: HTMLDivElement | null = null; private originalInlineStyles: { position: string; top: string; left: string; right: string; width: string; } | null = null; constructor(element: HTMLElement) { this.element = element; this.contentLeftElement = null; this.megamenuElement = null; this.resizeObserver = null; this.supportsScrollEnd = "onscrollend" in window; this.isAutoScrolling = false; this.currentPath = window.location.pathname; this.anchorClickHandler = this.handleAnchorClick.bind(this); this.scrollHandler = this.updateStickyPosition.bind(this); this.scrollSpyHandler = this.handleScrollSpy.bind(this); this.scrollEndHandler = this.handleScrollEnd.bind(this); this.scrollEndFallbackHandler = this.handleScrollEndFallback.bind(this); this.resizeHandler = this.handleResize.bind(this); this.mouseDownHandler = this.handleMouseDown.bind(this); this.dragStartHandler = this.handleDragStart.bind(this); this.dragMoveHandler = this.handleDragMove.bind(this); this.dragEndHandler = this.handleDragEnd.bind(this); this.horizontalScrollHandler = this.updateHorizontalOverflowState.bind(this); this.nativeDragStartHandler = this.handleNativeDragStart.bind(this); this.modalStateHandler = this.handleModalStateChange.bind(this); (this.element as any).ODS_AnchorNavigation = this; this.init(); return this; } static getInstance(el: HTMLElement): AnchorNavigation | null { return el && (el as any).ODS_AnchorNavigation ? (el as any).ODS_AnchorNavigation : null; } private findMegamenuElement(): HTMLElement | null { return document.querySelector("[data-megamenu]") as HTMLElement | null; } private getMegamenuOffsetHeight(): number { return this.megamenuElement?.offsetHeight || 0; } private updateStickyPosition(): void { if (this.isForcedFixed) return; if (!this.megamenuElement) return; const stickyOffset = this.getMegamenuOffsetHeight(); const hasStickyOffsetChanged = stickyOffset !== this.lastStickyOffset; this.lastStickyOffset = stickyOffset; this.element.style.top = `${stickyOffset}px`; this.updateStickyFloatingState(); if ( !hasStickyOffsetChanged || !this.isAutoScrolling || !this.autoScrollTargetId || this.autoScrollCorrectionCount >= AnchorNavigation.MAX_SCROLL_CORRECTIONS ) { return; } const targetElement = document.getElementById(this.autoScrollTargetId); if (!targetElement) return; // Re-target smoothly as soon as sticky offset changes to avoid a visible snap at scroll end. this.autoScrollCorrectionCount += 1; this.scrollToSection(targetElement, "smooth"); } private setupMegamenuObserver(): void { this.megamenuElement = this.findMegamenuElement(); if (!this.megamenuElement) { this.lastStickyOffset = 0; this.element.style.top = "0px"; return; } this.updateStickyPosition(); window.addEventListener("scroll", this.scrollHandler, { passive: true }); this.resizeObserver = new ResizeObserver( this.updateStickyPosition.bind(this), ); this.resizeObserver.observe(this.megamenuElement); } private setupScrollSpy(): void { // Set dynamic scroll margin for CSS document.documentElement.style.setProperty( "--extra-scroll-margin", this.element.offsetHeight + "px", ); // Get all anchor navigation links this.navLinks = this.element.querySelectorAll(".anchor-navigation__item"); // Get all sections that correspond to the navigation links this.sections = Array.from(this.navLinks || []) .map((link) => link.getAttribute("href")) .filter((href) => href?.includes("#")) .map((href) => document.getElementById(href!.split("#")[1])) .filter(Boolean) as HTMLElement[]; this.teardownScrollSpyListeners(); this.element.addEventListener("click", this.anchorClickHandler); this.element.addEventListener("mousedown", this.mouseDownHandler); window.addEventListener("scroll", this.scrollSpyHandler, { passive: true }); if (this.supportsScrollEnd) { window.addEventListener("scrollend", this.scrollEndHandler); } else { window.addEventListener("scroll", this.scrollEndFallbackHandler, { passive: true, }); } window.addEventListener("resize", this.resizeHandler); this.initScrollSpy(); this.alignInitialHashIfNeeded(); } private teardownScrollSpyListeners(): void { this.element.removeEventListener("click", this.anchorClickHandler); this.element.removeEventListener("mousedown", this.mouseDownHandler); window.removeEventListener("scroll", this.scrollSpyHandler); window.removeEventListener("resize", this.resizeHandler); if (this.supportsScrollEnd) { window.removeEventListener("scrollend", this.scrollEndHandler); } else { window.removeEventListener("scroll", this.scrollEndFallbackHandler); } if (this.scrollEndFallbackTimeout) { clearTimeout(this.scrollEndFallbackTimeout); this.scrollEndFallbackTimeout = null; } } private handleMouseDown(event: MouseEvent): void { if (event.button !== 0) return; const target = event.target as HTMLElement | null; if (!target || !this.element.contains(target)) return; const interactiveSelector = "a, button, input, select, textarea, label, [role='button'], [contenteditable='true']"; if (target.closest(interactiveSelector)) return; // Prevent native text-selection drag from triggering page autoscroll. event.preventDefault(); } private handleAnchorClick(event: Event): void { if (this.suppressClick) { event.preventDefault(); event.stopPropagation(); this.suppressClick = false; return; } const target = event.target as HTMLElement | null; const anchor = target?.closest( ".anchor-navigation__item", ) as HTMLAnchorElement | null; if (!anchor || !this.element.contains(anchor)) return; event.preventDefault(); const href = anchor.getAttribute("href"); if (!href || !href.includes("#")) return; const targetId = href.split("#")[1]; const targetElement = document.getElementById(targetId); if (!targetElement) return; this.startAutoScroll(targetId); anchor.blur(); this.scrollToSection(targetElement, "smooth"); const nextUrl = `${window.location.pathname}${window.location.search}#${targetId}`; window.history.pushState(null, "", nextUrl); this.initScrollSpy(targetId); } private getHashSectionId(): string | null { const hash = window.location.hash; if (!hash || hash.length <= 1) return null; let hashId = hash.slice(1); try { hashId = decodeURIComponent(hashId); } catch { // Keep raw hash when decoding fails. } return hashId || null; } private startAutoScroll(sectionId: string): void { this.isAutoScrolling = true; this.autoScrollTargetId = sectionId; this.autoScrollCorrectionCount = 0; } private resetAutoScrollState(): void { this.isAutoScrolling = false; this.autoScrollTargetId = null; this.autoScrollCorrectionCount = 0; } private tryCorrectAutoScrollAlignment(): boolean { if (!this.autoScrollTargetId) return true; const targetElement = document.getElementById(this.autoScrollTargetId); if (!targetElement) return true; const targetTop = this.getTargetTop(targetElement); const distance = Math.abs(window.scrollY - targetTop); if (distance <= AnchorNavigation.SCROLL_ALIGNMENT_TOLERANCE) { return true; } if ( this.autoScrollCorrectionCount >= AnchorNavigation.MAX_SCROLL_CORRECTIONS ) { return true; } this.autoScrollCorrectionCount += 1; const correctionBehavior: ScrollBehavior = this.autoScrollCorrectionCount === 1 ? "smooth" : "auto"; this.scrollToSection(targetElement, correctionBehavior); return false; } private getTotalStickyOffset(): number { const scrollOffset = this.megamenuElement ? this.megamenuElement.offsetHeight : 0; const anchorNavOffset = this.element.offsetHeight; return scrollOffset + anchorNavOffset; } private getConfiguredScrollPaddingTop(): number { const rootStyles = window.getComputedStyle(document.documentElement); const rawScrollPaddingTop = rootStyles.scrollPaddingTop; const parsedScrollPaddingTop = Number.parseFloat(rawScrollPaddingTop); if (Number.isFinite(parsedScrollPaddingTop) && parsedScrollPaddingTop > 0) { return parsedScrollPaddingTop; } return 0; } private getEffectiveScrollOffset(): number { const configuredScrollPaddingTop = this.getConfiguredScrollPaddingTop(); if (configuredScrollPaddingTop > 0) { return configuredScrollPaddingTop; } return this.getTotalStickyOffset(); } private getTargetTop(targetElement: HTMLElement): number { const targetTop = targetElement.getBoundingClientRect().top + window.scrollY - this.getEffectiveScrollOffset(); return Math.max(0, targetTop); } private scrollToSection( targetElement: HTMLElement, behavior: ScrollBehavior, ): void { window.scrollTo({ top: this.getTargetTop(targetElement), behavior, }); } private alignInitialHashIfNeeded(): void { const hashId = this.getHashSectionId(); if (!hashId) return; const targetElement = document.getElementById(hashId); if (!targetElement) return; this.startAutoScroll(hashId); requestAnimationFrame(() => { this.scrollToSection(targetElement, "auto"); requestAnimationFrame(() => { this.handleScrollEnd(); }); }); } private initScrollSpy(forcedSectionId: string | null = null): void { if (!this.navLinks || !this.sections.length) return; let targetSection: HTMLElement | undefined; let targetIndex: number = -1; // Remove active class from all links this.navLinks.forEach((link) => link.classList.remove("is-active")); if (forcedSectionId) { targetSection = document.getElementById(forcedSectionId) || undefined; if (targetSection) { // Find the index of the forced section targetIndex = this.sections.findIndex( (section) => section.id === forcedSectionId, ); } } else { const totalOffset = this.getEffectiveScrollOffset(); const effectiveCenter = window.scrollY + totalOffset + AnchorNavigation.SCROLLSPY_CENTER_BUFFER; // Find the section that's currently in view for (let i = 0; i < this.sections.length; i++) { const section = this.sections[i]; const { top: sectionTopRaw, height: sectionHeight } = section.getBoundingClientRect(); const sectionTop = sectionTopRaw + window.scrollY; const sectionBottom = sectionTop + sectionHeight; if (effectiveCenter >= sectionTop && effectiveCenter < sectionBottom) { targetSection = section; targetIndex = i; break; } } } // Fallback logic: if no section is found, use fallback rules if (!targetSection) { // If we're at the very top, activate first item if (window.scrollY <= AnchorNavigation.TOP_SECTION_THRESHOLD) { targetIndex = 0; targetSection = this.sections[0]; } else { // Keep the last active item targetIndex = this.lastActiveIndex; targetSection = this.sections[targetIndex]; } } // Update last active index if we found a valid target if (targetIndex >= 0) { this.lastActiveIndex = targetIndex; } if (targetSection) { const id = targetSection.getAttribute("id"); // Find the matching navigation link - try different href patterns let activeLink = this.element.querySelector( `.anchor-navigation__item[href="#${id}"]`, ) as HTMLElement; if (!activeLink) { activeLink = this.element.querySelector( `.anchor-navigation__item[href="${this.currentPath}#${id}"]`, ) as HTMLElement; } if (!activeLink) { // Try without current path for relative links activeLink = Array.from(this.navLinks).find((link) => { const href = link.getAttribute("href"); return href && href.endsWith(`#${id}`); }) as HTMLElement; } if (activeLink) { activeLink.classList.add("is-active"); // Scroll the navigation to center the active link const contentLeft = this.element.querySelector( ".anchor-navigation__content-left", ) as HTMLElement; if (contentLeft) { this.scrollActiveLinkIntoView( contentLeft, activeLink, Boolean(forcedSectionId), ); } } } } private scrollActiveLinkIntoView( contentLeft: HTMLElement, activeLink: HTMLElement, forceCenter: boolean = false, ): void { const maxScrollLeft = Math.max( 0, contentLeft.scrollWidth - contentLeft.clientWidth, ); const contentRect = contentLeft.getBoundingClientRect(); const itemRect = activeLink.getBoundingClientRect(); const itemCenterWithinContent = itemRect.left - contentRect.left + contentLeft.scrollLeft + itemRect.width / 2; const targetScrollLeft = itemCenterWithinContent - contentLeft.clientWidth / 2; const behavior = window.innerWidth < 768 ? "auto" : "smooth"; const nextScrollLeft = Math.min( maxScrollLeft, Math.max(0, targetScrollLeft), ); const isAlreadyAligned = Math.abs(contentLeft.scrollLeft - nextScrollLeft) <= AnchorNavigation.SCROLL_ALIGNMENT_TOLERANCE; if (!forceCenter && isAlreadyAligned) return; if (typeof contentLeft.scrollTo === "function") { contentLeft.scrollTo({ left: nextScrollLeft, behavior, }); return; } contentLeft.scrollLeft = nextScrollLeft; } private setupDragScroll(): void { this.teardownDragScroll(); this.contentLeftElement = this.element.querySelector( ".anchor-navigation__content-left", ) as HTMLElement | null; if (!this.contentLeftElement) return; this.contentLeftElement.addEventListener( "mousedown", this.dragStartHandler, ); this.contentLeftElement.addEventListener( "dragstart", this.nativeDragStartHandler, ); this.contentLeftElement.addEventListener( "scroll", this.horizontalScrollHandler, { passive: true, }, ); this.updateDragState(); this.updateHorizontalOverflowState(); } private teardownDragScroll(): void { if (this.contentLeftElement) { this.contentLeftElement.removeEventListener( "mousedown", this.dragStartHandler, ); this.contentLeftElement.removeEventListener( "dragstart", this.nativeDragStartHandler, ); this.contentLeftElement.removeEventListener( "scroll", this.horizontalScrollHandler, ); this.contentLeftElement.classList.remove("is-draggable", "is-dragging"); } window.removeEventListener("mousemove", this.dragMoveHandler); window.removeEventListener("mouseup", this.dragEndHandler); this.contentLeftElement = null; this.isPointerDown = false; this.isDraggingNav = false; this.suppressClick = false; this.dragStartX = 0; this.dragStartScrollLeft = 0; } private updateDragState(): void { if (!this.contentLeftElement) return; const pointerMediaQuery = typeof window.matchMedia === "function" ? window.matchMedia("(pointer: fine)") : null; const hasFinePointer = !pointerMediaQuery || pointerMediaQuery.matches; const isOverflowing = this.contentLeftElement.scrollWidth > this.contentLeftElement.clientWidth; const isDraggable = hasFinePointer && isOverflowing; this.contentLeftElement.classList.toggle("is-draggable", isDraggable); if (!isDraggable) { this.contentLeftElement.classList.remove("is-dragging"); this.isPointerDown = false; this.isDraggingNav = false; this.suppressClick = false; window.removeEventListener("mousemove", this.dragMoveHandler); window.removeEventListener("mouseup", this.dragEndHandler); } this.updateHorizontalOverflowState(); } private updateHorizontalOverflowState(): void { if (!this.contentLeftElement) return; const contentElement = this.element.querySelector( ".anchor-navigation__content", ) as HTMLElement | null; if (!contentElement) return; const maxScrollLeft = this.contentLeftElement.scrollWidth - this.contentLeftElement.clientWidth; const hasOverflow = maxScrollLeft > AnchorNavigation.SCROLL_EDGE_TOLERANCE; const atStart = this.contentLeftElement.scrollLeft <= AnchorNavigation.SCROLL_EDGE_TOLERANCE; const atEnd = this.contentLeftElement.scrollLeft >= maxScrollLeft - AnchorNavigation.SCROLL_EDGE_TOLERANCE; contentElement.classList.toggle( "has-left-overflow", hasOverflow && !atStart, ); contentElement.classList.toggle( "has-right-overflow", hasOverflow && !atEnd, ); } private handleDragStart(event: MouseEvent): void { if ( event.button !== 0 || !this.contentLeftElement || !this.contentLeftElement.classList.contains("is-draggable") ) { return; } this.isPointerDown = true; this.isDraggingNav = false; this.suppressClick = false; this.dragStartX = event.clientX; this.dragStartScrollLeft = this.contentLeftElement.scrollLeft; window.addEventListener("mousemove", this.dragMoveHandler); window.addEventListener("mouseup", this.dragEndHandler); } private handleDragMove(event: MouseEvent): void { if (!this.isPointerDown || !this.contentLeftElement) return; const deltaX = event.clientX - this.dragStartX; if ( !this.isDraggingNav && Math.abs(deltaX) >= AnchorNavigation.DRAG_START_THRESHOLD ) { this.isDraggingNav = true; this.suppressClick = true; this.contentLeftElement.classList.add("is-dragging"); } if (!this.isDraggingNav) return; event.preventDefault(); this.contentLeftElement.scrollLeft = this.dragStartScrollLeft - deltaX; } private handleDragEnd(): void { if (!this.isPointerDown) return; this.isPointerDown = false; window.removeEventListener("mousemove", this.dragMoveHandler); window.removeEventListener("mouseup", this.dragEndHandler); if (this.contentLeftElement) { this.contentLeftElement.classList.remove("is-dragging"); } const shouldSuppressClick = this.isDraggingNav; this.isDraggingNav = false; if (shouldSuppressClick) { window.setTimeout(() => { this.suppressClick = false; }, 0); } } private handleNativeDragStart(event: DragEvent): void { if (this.contentLeftElement?.classList.contains("is-draggable")) { event.preventDefault(); } } private handleScrollEndFallback(): void { if (this.scrollEndFallbackTimeout) { clearTimeout(this.scrollEndFallbackTimeout); } this.scrollEndFallbackTimeout = setTimeout(() => { this.handleScrollEnd(); }, AnchorNavigation.SCROLL_END_DEBOUNCE_MS); } private handleResize(): void { if (this.isForcedFixed) { const parentRect = this.element.parentElement?.getBoundingClientRect(); const megamenuHeight = this.getMegamenuOffsetHeight(); if (parentRect) { this.forcedFixedLeft = Math.round(parentRect.left); this.element.style.width = `${Math.round(parentRect.width)}px`; } if (megamenuHeight > 0) { this.forcedFixedTop = megamenuHeight; } this.element.style.top = `${this.forcedFixedTop}px`; this.element.style.left = `${this.forcedFixedLeft}px`; if (this.fixedFlowPlaceholder) { this.fixedFlowPlaceholder.style.height = `${this.element.offsetHeight}px`; } } else { this.updateStickyFloatingState(); } this.initScrollSpy(); this.updateDragState(); this.updateHorizontalOverflowState(); } private isAnyModalOpen(): boolean { return document.body.classList.contains( AnchorNavigation.MODAL_OPEN_BODY_CLASS, ); } private isCurrentlyStickyFloating(): boolean { if (!this.megamenuElement) return false; const stickyTop = this.getMegamenuOffsetHeight(); const currentTop = this.element.getBoundingClientRect().top; return currentTop <= stickyTop + AnchorNavigation.STICKY_FLOAT_TOLERANCE; } private updateStickyFloatingState(): void { if (this.isAnyModalOpen() || this.isForcedFixed) { return; } this.wasStickyFloating = this.isCurrentlyStickyFloating(); } private ensureFixedFlowPlaceholder(): void { if (!this.fixedFlowPlaceholder) { this.fixedFlowPlaceholder = document.createElement("div"); this.fixedFlowPlaceholder.setAttribute("aria-hidden", "true"); this.fixedFlowPlaceholder.style.width = "100%"; this.fixedFlowPlaceholder.style.pointerEvents = "none"; this.fixedFlowPlaceholder.style.visibility = "hidden"; this.element.insertAdjacentElement("afterend", this.fixedFlowPlaceholder); } this.fixedFlowPlaceholder.style.height = `${this.element.offsetHeight}px`; } private removeFixedFlowPlaceholder(): void { if (!this.fixedFlowPlaceholder) return; this.fixedFlowPlaceholder.remove(); this.fixedFlowPlaceholder = null; } private applyFixedPosition(): void { if (this.isForcedFixed) return; const rect = this.element.getBoundingClientRect(); const parentRect = this.element.parentElement?.getBoundingClientRect(); const megamenuHeight = this.getMegamenuOffsetHeight(); this.originalInlineStyles = { position: this.element.style.position, top: this.element.style.top, left: this.element.style.left, right: this.element.style.right, width: this.element.style.width, }; this.forcedFixedTop = megamenuHeight > 0 ? megamenuHeight : Math.max(0, Math.round(rect.top)); this.forcedFixedLeft = Math.max( 0, Math.round(parentRect?.left ?? rect.left), ); const fixedWidth = Math.round(parentRect?.width ?? rect.width); this.element.style.position = "fixed"; this.element.style.top = `${this.forcedFixedTop}px`; this.element.style.left = `${this.forcedFixedLeft}px`; this.element.style.right = ""; this.element.style.width = `${fixedWidth}px`; this.ensureFixedFlowPlaceholder(); this.isForcedFixed = true; } private restoreStickyPosition(shouldRecalculate: boolean = true): void { if (!this.isForcedFixed) return; const originalStyles = this.originalInlineStyles; this.element.style.position = originalStyles?.position || ""; this.element.style.top = originalStyles?.top || ""; this.element.style.left = originalStyles?.left || ""; this.element.style.right = originalStyles?.right || ""; this.element.style.width = originalStyles?.width || ""; this.resetFixedPositionState(); if (shouldRecalculate) { this.updateStickyPosition(); this.initScrollSpy(); } } private handleModalStateChange(): void { if (this.isAnyModalOpen()) { // Keep sticky in normal flow until it is actually pinned. if (this.isForcedFixed || this.wasStickyFloating) { this.applyFixedPosition(); } return; } this.restoreStickyPosition(false); this.updateStickyFloatingState(); } private setupModalObserver(): void { this.updateStickyFloatingState(); this.handleModalStateChange(); if (!document.body || typeof MutationObserver === "undefined") { return; } this.modalClassObserver = new MutationObserver(this.modalStateHandler); this.modalClassObserver.observe(document.body, { attributes: true, attributeFilter: ["class"], }); } private teardownModalObserver(): void { if (!this.modalClassObserver) return; this.modalClassObserver.disconnect(); this.modalClassObserver = null; } private teardownMegamenuObserver(): void { window.removeEventListener("scroll", this.scrollHandler); if (this.resizeObserver) { this.resizeObserver.disconnect(); this.resizeObserver = null; } } private clearAutoScrollTimers(): void { if (this.scrollTimeout) { clearTimeout(this.scrollTimeout); this.scrollTimeout = null; } } private resetFixedPositionState(): void { this.originalInlineStyles = null; this.isForcedFixed = false; this.wasStickyFloating = false; this.forcedFixedTop = 0; this.forcedFixedLeft = 0; this.removeFixedFlowPlaceholder(); } private resetNavigationState(): void { this.megamenuElement = null; this.lastStickyOffset = 0; this.navLinks = null; this.sections = []; this.lastActiveIndex = 0; this.autoScrollTargetId = null; this.autoScrollCorrectionCount = 0; this.resetFixedPositionState(); } private handleScrollSpy(): void { if (this.isAutoScrolling) { // Clear existing timeout and set a new one if (this.scrollTimeout) { clearTimeout(this.scrollTimeout); } // Set a timeout to detect when scrolling has ended this.scrollTimeout = setTimeout(() => { this.handleScrollEnd(); }, AnchorNavigation.SCROLL_END_DEBOUNCE_MS); } else { this.initScrollSpy(); } } private handleScrollEnd(): void { if (this.scrollTimeout) { clearTimeout(this.scrollTimeout); this.scrollTimeout = null; } if (!this.tryCorrectAutoScrollAlignment()) { return; } this.resetAutoScrollState(); this.initScrollSpy(); } private init(): void { this.setupMegamenuObserver(); this.setupScrollSpy(); this.setupDragScroll(); this.setupModalObserver(); } destroy(): void { this.clearAutoScrollTimers(); this.teardownMegamenuObserver(); this.teardownScrollSpyListeners(); this.teardownDragScroll(); this.teardownModalObserver(); this.restoreStickyPosition(false); this.element.style.top = ""; this.resetNavigationState(); (this.element as any).ODS_AnchorNavigation = null; } update(): void { this.clearAutoScrollTimers(); this.teardownMegamenuObserver(); this.teardownScrollSpyListeners(); this.teardownDragScroll(); this.teardownModalObserver(); this.restoreStickyPosition(false); this.resetNavigationState(); this.init(); } }