state = { activeDropdown: null, // element with [new_nav_dropdown] or null viewport: "desktop" | "mobile", }; class NavController { constructor(root) { this.root = root; this.overlay = document.getElementById("nav_overlay"); this.hamburger_icon = document.getElementById("new_hamburger_icon"); this.hamburger_menu = document.getElementById("new_hamburger_menu"); this.state = { activeDropdown: null, viewport: this.getViewport(), mobileMenuOpen: false, }; this.init(); } init() { document.addEventListener("keydown", this.onDocumentKeydown.bind(this)); this.root.addEventListener("click", this.onClick.bind(this)); this.root.addEventListener("keydown", this.onKeydown.bind(this)); window.addEventListener("resize", this.onResize.bind(this)); this.overlay.addEventListener("click", () => { if (this.state.activeDropdown) { this.onOutsideClick(this.state.activeDropdown); this.state.activeDropdown = null; } }); } onDocumentKeydown(e) { if (e.key === "Escape" && this.state.activeDropdown) { this.handleDropdownClose(); } } handleOpenReset(content, columns, trigger) { content.classList.remove("content_height_animation"); if (this.state.activeDropdown === trigger) { content.style.height = "auto"; } columns.forEach((col) => { col.classList.remove("column_show_animation"); }); } handleCloseReset(content, columns, trigger) { content.classList.remove("content_height_animation"); if (this.state.activeDropdown != trigger) { content.style.opacity = "0"; columns.forEach((col) => { col.classList.remove("column_show"); }); } } handleOverlayReset() { if (this.state.viewport === "desktop" && !this.state.activeDropdown) { this.overlay.classList.remove("overlay_show_height"); } } getDropdownContent(trigger) { if (!trigger) return null; const parent = trigger.parentElement; if (!parent) return null; return parent.querySelector("[new_nav_dropdown_content]"); } getDropdownText(trigger) { if (!trigger) return null; return trigger.querySelector("[new_nav_dropdown_text]"); } getDropdownUnderline(trigger) { if (!trigger) return null; return trigger.querySelector("[new_nav_dropdown_underline]"); } getDropdownIcon(trigger) { if (!trigger) return null; return trigger.querySelector("[new_nav_dropdown_arrow]"); } getDropdownColumns(content) { if (!content) return []; return Array.from( content.querySelectorAll("[new_nav_dropdown_content_col]") ); } getLeftDropdownColumns(content) { if (!content) return []; return Array.from(content.querySelectorAll("[main_pannel_links]")); } getRightDropdownColumns(content) { if (!content) return []; return Array.from(content.querySelectorAll("[right_pannel_links]")); } setDropdownStateOpen(trigger, content) { content.removeAttribute("inert"); content.setAttribute("aria-hidden", "false"); trigger.setAttribute("aria-expanded", "true"); } setDropdownStateClose(trigger, content) { content.setAttribute("inert", ""); content.setAttribute("aria-hidden", "true"); trigger.setAttribute("aria-expanded", "false"); } onClick(e) { const hamburger = e.target.closest("#new_hamburger_icon"); if (hamburger) { this.handlemMobileMenu(hamburger); return; } const trigger = e.target.closest("[new_nav_dropdown]"); if (trigger) { this.handleTrigger(trigger, "click"); return; } this.handleOutsideClick(e); } onKeydown(e) { if (e.key === "Enter" || e.key === " ") { const hamburger = e.target.closest("#new_hamburger_icon"); if (hamburger) { this.handlemMobileMenu(); return; } const trigger = e.target.closest("[new_nav_dropdown]"); if (trigger) { e.preventDefault(); this.handleTrigger(trigger, "enter"); } } if (e.key === "Tab") { this.handleTabNavigation(e); } } openMobileMenu() { this.mobileMenuOpen = true; this.hamburger_menu.style.transition = "transform 300ms"; this.hamburger_menu.classList.add("open_menu"); console.log("this.hamburger_menu", this.hamburger_menu); void this.hamburger_menu.offsetHeight; console.log("this.hamburger_menu", this.hamburger_menu); this.hamburger_menu.style.transition = "transform 0ms"; this.hamburger_menu.removeAttribute("inert"); this.hamburger_menu.setAttribute("aria-hidden", "false"); this.hamburger_icon.setAttribute("aria-expanded", "true"); const scrollY = window.scrollY; // document.body.style.position = "fixed"; document.body.style.top = `-${scrollY}px`; document.body.dataset.scrollY = scrollY; document.body.style.overflow = "hidden"; } closeMobileMenu() { this.mobileMenuOpen = false; this.hamburger_menu.style.transition = "transform 300ms"; this.hamburger_menu.classList.remove("open_menu"); console.log("this.hamburger_menu", this.hamburger_menu); void this.hamburger_menu.offsetHeight; console.log("this.hamburger_menu", this.hamburger_menu); this.hamburger_menu.style.transition = "transform 0ms"; this.hamburger_menu.setAttribute("inert", ""); this.hamburger_menu.setAttribute("aria-hidden", "true"); this.hamburger_icon.setAttribute("aria-expanded", "false"); const scrollY = document.body.dataset.scrollY; // document.body.style.position = ""; // document.body.style.top = ""; document.body.style.overflow = ""; window.scrollTo(0, parseInt(scrollY || "0")); } handlemMobileMenu() { if (!this.mobileMenuOpen) { this.openMobileMenu(); } else { this.closeMobileMenu(); } } handleTrigger(trigger, inputType) { const { activeDropdown } = this.state; if (!activeDropdown) { this.state.activeDropdown = trigger; this.onTriggerWithNoOpenDropdown(trigger, inputType); return; } if (activeDropdown && activeDropdown !== trigger) { this.state.activeDropdown = trigger; this.onTriggerWithAnotherDropdownOpen(trigger, activeDropdown, inputType); return; } if (activeDropdown === trigger) { this.onTriggerOnAlreadyOpenDropdown(trigger, inputType); // this.state.activeDropdown = null; } } handleOutsideClick(e) { const insideNav = e.target.closest("[new_nav_dropdown]") || e.target.closest("[new_nav_dropdown_content]"); if (!insideNav && this.state.activeDropdown) { this.onOutsideClick(this.state.activeDropdown); this.state.activeDropdown = null; } } handleTabNavigation(e) { if (!e.shiftKey) { if ( (this.state.viewport === "desktop" && e.target.hasAttribute("new_dropdown_last")) || (this.state.viewport === "mobile" && e.target.hasAttribute("new_dropdown_mobile_last") && !e.target.hasAttribute("new_last_mobile_menu_link")) ) { this.onTabFromLastItem(this.state.activeDropdown); } if ( this.state.viewport === "mobile" && e.target.hasAttribute("new_last_mobile_menu_link") ) { setTimeout(() => { document.getElementById("new_hamburger_icon")?.focus(); }, 0); this.closeMobileMenu(); } } if (e.shiftKey) { const first = e.target.closest("[new_dropdown_first]"); if (first) { // this.onShiftTabFromFirstItem(first); this.onShiftTabFromFirstItem(this.state.activeDropdown); } } } handleDropdownClose() { const trigger = this.state.activeDropdown; this.state.activeDropdown = null; const content = this.getDropdownContent(trigger); if (!content) return; const columns = this.getDropdownColumns(content); const dropdownText = this.getDropdownText(trigger); const dropdownUnderline = this.getDropdownUnderline(trigger); const dropdownIcon = this.getDropdownIcon(trigger); this.setDropdownStateClose(trigger, content); // 1️⃣ Measure current height const currentHeight = content.offsetHeight; // 2️⃣ Freeze that height content.style.height = `${currentHeight}px`; // 3️⃣ Force layout commit void content.offsetHeight; // 4️⃣ Add transition class content.classList.add("content_height_animation"); if (this.state.viewport === "desktop") { this.overlay.classList.remove("overlay_show_opacity"); const scrollY = document.body.dataset.scrollY; // document.body.style.position = ""; // document.body.style.top = ""; document.body.style.overflow = ""; window.scrollTo(0, parseInt(scrollY || "0")); // void this.overlay.offsetHeight; // // this.overlay.classList.remove("overlay_show_height"); } dropdownText.classList.remove("active_tab_color"); if (this.state.viewport === "desktop") { dropdownUnderline.classList.remove("underline_on", "active_tab_color"); } else { dropdownIcon.classList.remove("icon_up", "set_active_icon_color"); } // 5️⃣ Animate to 0 content.style.height = "0px"; // 6️⃣ After animation, move offscreen and reset content.addEventListener( "transitionend", this.handleCloseReset.bind(this, content, columns, trigger), // () => { // // content.style.left = "100%"; // // content.style.height = "auto"; // content.classList.remove("content_height_animation"); // columns.forEach((col) => { // col.classList.remove("column_show"); // }); // }, { once: true } ); this.overlay.addEventListener( "transitionend", this.handleOverlayReset.bind(this), // () => { // if (this.state.viewport === "desktop") { // console.log("remove overlay height"); // this.overlay.classList.remove("overlay_show_height"); // } // }, { once: true } ); } onResize() { const nextViewport = this.getViewport(); if (nextViewport !== this.state.viewport) { this.onViewportChange(this.state.viewport, nextViewport); this.state.viewport = nextViewport; } } getViewport() { return window.innerWidth >= 990 ? "desktop" : "mobile"; } onTriggerWithNoOpenDropdown(trigger, inputType) { console.log("OPEN_WITH_NONE_OPEN", { trigger, inputType }); const content = this.getDropdownContent(trigger); if (!content) return; const columns = this.getDropdownColumns(content); const dropdownText = this.getDropdownText(trigger); const dropdownUnderline = this.getDropdownUnderline(trigger); const dropdownIcon = this.getDropdownIcon(trigger); // content.style.height = "auto"; this.setDropdownStateOpen(trigger, content); // 1️⃣ Measure height while offscreen const targetHeight = content.scrollHeight; // 2️⃣ Prepare for animation content.style.height = "0px"; content.style.opacity = "1"; // content.style.left = "0%"; // 3️⃣ Force layout commit void content.offsetHeight; // 4️⃣ Add transition for height content.classList.add("content_height_animation"); // 5️⃣ Prepare columns for animation (same frame) columns.forEach((col) => { col.classList.add("column_show_animation"); col.classList.add("column_show"); }); if (this.state.viewport === "desktop") { this.overlay.classList.add("overlay_show_height"); void this.overlay.offsetHeight; this.overlay.classList.add("overlay_show_opacity"); const scrollY = window.scrollY; // document.body.style.position = "fixed"; document.body.style.top = `-${scrollY}px`; document.body.dataset.scrollY = scrollY; document.body.style.overflow = "hidden"; } dropdownText.classList.add("active_tab_color"); if (this.state.viewport === "desktop") { dropdownUnderline.classList.add("underline_on", "active_tab_color"); } else { dropdownIcon.classList.add("icon_up", "set_active_icon_color"); } // 6️⃣ Animate height content.style.height = `${targetHeight}px`; // 7️⃣ Cleanup after animation content.addEventListener( "transitionend", this.handleOpenReset.bind(this, content, columns, trigger), // () => { // content.classList.remove("content_height_animation"); // content.style.height = "auto"; // columns.forEach((col) => { // col.classList.remove("column_show_animation"); // }); // }, { once: true } ); } onTriggerWithAnotherDropdownOpen(trigger, previous, inputType) { console.log("OPEN_WITH_ANOTHER_OPEN", { trigger, previous, inputType }); const prevContent = this.getDropdownContent(previous); const currentContent = this.getDropdownContent(trigger); if (!prevContent || !currentContent) return; const prevColumns = this.getDropdownColumns(prevContent); const leftColumns = this.getLeftDropdownColumns(currentContent); const rightColumns = this.getRightDropdownColumns(currentContent); const prevDropdownText = this.getDropdownText(previous); const prevDropdownUnderline = this.getDropdownUnderline(previous); const prevDropdownIcon = this.getDropdownIcon(previous); const dropdownText = this.getDropdownText(trigger); const dropdownUnderline = this.getDropdownUnderline(trigger); const dropdownIcon = this.getDropdownIcon(trigger); rightColumns.forEach((col) => { col.classList.add("column_show"); }); this.setDropdownStateClose(previous, prevContent); this.setDropdownStateOpen(trigger, currentContent); // 1️⃣ Measure heights BEFORE moving anything const oldHeight = prevContent.scrollHeight; const newHeight = currentContent.scrollHeight; if (this.state.viewport === "desktop") { // 2️⃣ Lock the new content to the old height if (oldHeight != newHeight) { currentContent.style.height = `${oldHeight}px`; } // currentContent.style.left = "0%"; // bring into view } currentContent.style.opacity = "1"; // 3️⃣ Move previous out of view prevContent.classList.remove("content_height_animation"); prevContent.style.height = "0px"; prevContent.style.opacity = "0"; // prevContent.style.height = "auto"; // prevContent.style.left = "100%"; prevColumns.forEach((col) => { col.classList.remove("column_show"); }); // 4️⃣ Force layout so browser commits the old height void currentContent.offsetHeight; // 5️⃣ Add transition class if (oldHeight != newHeight) { currentContent.classList.add("content_height_animation"); currentContent.style.height = `${newHeight}px`; } else { currentContent.style.height = "auto"; } leftColumns.forEach((col) => { col.classList.add("column_show_animation"); col.classList.add("column_show"); }); prevDropdownText.classList.remove("active_tab_color"); if (this.state.viewport === "desktop") { prevDropdownUnderline.classList.remove( "underline_on", "active_tab_color" ); } else { prevDropdownIcon.classList.remove("icon_up", "set_active_icon_color"); } dropdownText.classList.add("active_tab_color"); if (this.state.viewport === "desktop") { dropdownUnderline.classList.add("underline_on", "active_tab_color"); } else { dropdownIcon.classList.add("icon_up", "set_active_icon_color"); } // 6️⃣ Animate to the real height // currentContent.style.height = `${newHeight}px`; // 7️⃣ Cleanup currentContent.addEventListener( "transitionend", () => { if (oldHeight != newHeight) { currentContent.classList.remove("content_height_animation"); currentContent.style.height = "auto"; } }, { once: true } ); } onTriggerOnAlreadyOpenDropdown(trigger, inputType) { console.log("TOGGLE_CLOSE_SAME", { trigger, inputType }); this.handleDropdownClose(); } onOutsideClick(activeDropdown) { console.log("OUTSIDE_CLICK", { activeDropdown }); this.handleDropdownClose(); } onTabFromLastItem(el) { console.log("TAB_FROM_LAST_ITEM", el); this.handleDropdownClose(); } onShiftTabFromFirstItem(el) { console.log("SHIFT_TAB_FROM_FIRST_ITEM", el); this.handleDropdownClose(); } onViewportChange(from, to) { console.log("VIEWPORT_CHANGE", { from, to }); if (this.state.activeDropdown) { this.handleDropdownClose(); } if (this.mobileMenuOpen) { this.closeMobileMenu(); } if (to === "desktop") { this.hamburger_menu.removeAttribute("inert"); } else { this.hamburger_menu.setAttribute("inert", ""); } } } const navRoot = document.getElementById("new-nav-container"); if (navRoot) { new NavController(navRoot); }