// Nova toolbar interactions // - dropdown menus via [data-dropdown] // - mobile menu toggle via [data-mobile-toggle] + #mobileMenu // Alpine.js — powers x-data/x-show/@click in Blade layouts (e.g. cookie banner, toasts). // Guard: don't start a second instance if app.js already loaded Alpine on this page. import Alpine from 'alpinejs'; if (!window.Alpine) { window.Alpine = Alpine; Alpine.start(); } // Gallery navigation context: stores artwork list for prev/next on artwork page import './lib/nav-context.js'; (function () { function initBlurPreviewImages() { var selector = 'img[data-blur-preview]'; function markLoaded(img) { if (!img) return; img.classList.remove('blur-sm', 'scale-[1.02]'); img.classList.add('is-loaded'); } document.querySelectorAll(selector).forEach(function (img) { if (img.complete && img.naturalWidth > 0) { markLoaded(img); return; } img.addEventListener('load', function () { markLoaded(img); }, { once: true }); img.addEventListener('error', function () { markLoaded(img); }, { once: true }); }); document.addEventListener('load', function (event) { var target = event.target; if (target && target.matches && target.matches(selector)) { markLoaded(target); } }, true); } initBlurPreviewImages(); function closest(el, selector) { while (el && el.nodeType === 1) { if (el.matches(selector)) return el; el = el.parentElement; } return null; } function canHover() { return window.matchMedia && window.matchMedia('(hover: hover) and (pointer: fine)').matches; } function setExpanded(toggleEl, expanded) { if (!toggleEl) return; toggleEl.setAttribute('aria-expanded', expanded ? 'true' : 'false'); } function closeAllDropdowns(except) { var dropdowns = document.querySelectorAll('[data-dropdown]'); dropdowns.forEach(function (dropdown) { if (except && dropdown === except) return; var menu = dropdown.querySelector('[data-dropdown-menu]'); var toggle = dropdown.querySelector('[data-dropdown-toggle]'); if (menu) menu.classList.remove('is-open'); setExpanded(toggle, false); // Close any submenus dropdown.querySelectorAll('[data-submenu-menu]').forEach(function (sm) { sm.classList.add('hidden'); }); dropdown.querySelectorAll('[data-submenu-toggle]').forEach(function (st) { setExpanded(st, false); }); }); } function openDropdown(dropdown) { var menu = dropdown.querySelector('[data-dropdown-menu]'); var toggle = dropdown.querySelector('[data-dropdown-toggle]'); if (!menu || !toggle) return; closeAllDropdowns(dropdown); menu.classList.add('is-open'); setExpanded(toggle, true); } function closeDropdown(dropdown) { var menu = dropdown.querySelector('[data-dropdown-menu]'); var toggle = dropdown.querySelector('[data-dropdown-toggle]'); if (menu) menu.classList.remove('is-open'); setExpanded(toggle, false); } function toggleDropdown(dropdown) { var menu = dropdown.querySelector('[data-dropdown-menu]'); var toggle = dropdown.querySelector('[data-dropdown-toggle]'); if (!menu || !toggle) return; var isOpen = menu.classList.contains('is-open'); closeAllDropdowns(isOpen ? null : dropdown); if (isOpen) { menu.classList.remove('is-open'); setExpanded(toggle, false); } else { menu.classList.add('is-open'); setExpanded(toggle, true); } } function getMobileMenu() { return document.getElementById('mobileMenu'); } function closeMobileMenu() { var menu = getMobileMenu(); if (!menu) return; menu.classList.add('hidden'); var toggle = document.querySelector('[data-mobile-toggle]'); setExpanded(toggle, false); } function toggleMobileMenu() { var menu = getMobileMenu(); if (!menu) return; var isOpen = !menu.classList.contains('hidden'); if (isOpen) { closeMobileMenu(); } else { menu.classList.remove('hidden'); var toggle = document.querySelector('[data-mobile-toggle]'); setExpanded(toggle, true); closeAllDropdowns(); } } document.addEventListener('click', function (e) { var dropdownToggle = closest(e.target, '[data-dropdown-toggle]'); // legacy shorthand toggles: data-dd="name" -> menu id = dd-name var legacyToggle = closest(e.target, '[data-dd]'); if (dropdownToggle) { // On pointer/hover-capable devices prefer hover; ignore mouse clicks if (canHover() && e.detail > 0) { // allow keyboard activation (e.detail === 0) to fall through return; } e.preventDefault(); var dropdown = closest(dropdownToggle, '[data-dropdown]'); if (dropdown) toggleDropdown(dropdown); return; } if (legacyToggle) { // On pointer/hover-capable devices prefer hover; ignore mouse clicks if (canHover() && e.detail > 0) { return; } e.preventDefault(); var ddName = legacyToggle.getAttribute('data-dd'); if (!ddName) return; var menu = document.getElementById('dd-' + ddName); if (!menu) return; // treat this pair (toggle + menu) similarly to our dropdown API var isOpen = menu.classList.contains('is-open'); // close other dropdowns closeAllDropdowns(); // also close other legacy (data-dd) menus document.querySelectorAll('[data-dd]').forEach(function (other) { if (other === legacyToggle) return; var otherId = other.getAttribute('data-dd'); var otherMenu = otherId ? document.getElementById('dd-' + otherId) : null; if (otherMenu) otherMenu.classList.remove('is-open'); setExpanded(other, false); }); if (isOpen) { menu.classList.remove('is-open'); setExpanded(legacyToggle, false); } else { menu.classList.add('is-open'); setExpanded(legacyToggle, true); } return; } var mobileToggle = closest(e.target, '[data-mobile-toggle]'); if (mobileToggle) { e.preventDefault(); toggleMobileMenu(); return; } // Submenu toggle (touch/click fallback) var submenuToggle = closest(e.target, '[data-submenu-toggle]'); if (submenuToggle) { if (canHover()) { // On desktop, submenu opens on hover via CSS. e.preventDefault(); return; } e.preventDefault(); var submenu = closest(submenuToggle, '[data-submenu]'); if (!submenu) return; var menu = submenu.querySelector('[data-submenu-menu]'); if (!menu) return; // Close other submenus within the same dropdown var dropdown = closest(submenuToggle, '[data-dropdown]'); if (dropdown) { dropdown.querySelectorAll('[data-submenu-menu]').forEach(function (sm) { if (sm !== menu) sm.classList.add('hidden'); }); dropdown.querySelectorAll('[data-submenu-toggle]').forEach(function (st) { if (st !== submenuToggle) setExpanded(st, false); }); } var isOpen = !menu.classList.contains('hidden'); if (isOpen) { menu.classList.add('hidden'); setExpanded(submenuToggle, false); } else { menu.classList.remove('hidden'); setExpanded(submenuToggle, true); } return; } if (!closest(e.target, '[data-dropdown]')) { closeAllDropdowns(); } }); // Hover-to-open for desktop pointers var hoverCloseTimers = new WeakMap(); function clearHoverTimer(dropdown) { var t = hoverCloseTimers.get(dropdown); if (t) window.clearTimeout(t); hoverCloseTimers.delete(dropdown); } function scheduleClose(dropdown) { clearHoverTimer(dropdown); hoverCloseTimers.set( dropdown, window.setTimeout(function () { closeMenuElement(dropdown); }, 140) ); } // Close a menu element or its parent dropdown wrapper. function closeMenuElement(el) { if (!el) return; // If this is a dropdown wrapper, find the menu inside if (el.hasAttribute && el.hasAttribute('data-dropdown')) { var menu = el.querySelector('[data-dropdown-menu]'); var toggle = el.querySelector('[data-dropdown-toggle]'); if (menu) menu.classList.remove('is-open'); setExpanded(toggle, false); // also close submenus inside el.querySelectorAll('[data-submenu-menu]').forEach(function (sm) { sm.classList.add('hidden'); }); el.querySelectorAll('[data-submenu-toggle]').forEach(function (st) { setExpanded(st, false); }); return; } // If it's a menu element (e.g., legacy id=dd-name) hide it and try to find its toggle var menuEl = el; if (!menuEl.id && el.getAttribute && el.getAttribute('data-dropdown-menu')) { // explicit menu element } // hide the element if possible try { menuEl.classList.remove('is-open'); } catch (e) {} // Try to map back to a toggle: id like dd-name -> data-dd="name" if (menuEl.id && menuEl.id.indexOf('dd-') === 0) { var name = menuEl.id.slice(3); var toggle = document.querySelector('[data-dd="' + name + '"]'); if (toggle) setExpanded(toggle, false); } else { // fallback: if menu is inside a [data-dropdown], handled above; nothing more to do } } function bindHoverHandlers() { if (!canHover()) return; document.querySelectorAll('[data-dropdown]').forEach(function (dropdown) { dropdown.addEventListener('mouseenter', function () { clearHoverTimer(dropdown); openDropdown(dropdown); }); dropdown.addEventListener('mouseleave', function () { scheduleClose(dropdown); }); }); // legacy hover binding for shorthand toggles (data-dd) document.querySelectorAll('[data-dd]').forEach(function (el) { var ddName = el.getAttribute('data-dd'); if (!ddName) return; var menu = document.getElementById('dd-' + ddName); if (!menu) return; // when pointer enters either toggle or menu, open function enter() { clearHoverTimer(menu); // Instantly close any other open legacy dropdown to prevent overlap document.querySelectorAll('[data-dd]').forEach(function (other) { if (other === el) return; var otherId = other.getAttribute('data-dd'); var otherMenu = otherId ? document.getElementById('dd-' + otherId) : null; if (otherMenu) otherMenu.classList.remove('is-open'); setExpanded(other, false); }); menu.classList.add('is-open'); setExpanded(el, true); } function leave() { scheduleClose(menu); } el.addEventListener('mouseenter', enter); el.addEventListener('mouseleave', leave); menu.addEventListener('mouseenter', enter); menu.addEventListener('mouseleave', leave); }); } bindHoverHandlers(); // Submenu hover handlers: ensure flyouts open on pointer devices if (canHover()) { document.querySelectorAll('[data-submenu]').forEach(function (group) { var toggle = group.querySelector('[data-submenu-toggle]'); var menu = group.querySelector('[data-submenu-menu]'); if (!menu) return; group.addEventListener('mouseenter', function () { menu.classList.remove('hidden'); if (toggle) setExpanded(toggle, true); }); group.addEventListener('mouseleave', function () { menu.classList.add('hidden'); if (toggle) setExpanded(toggle, false); }); }); } document.addEventListener('keydown', function (e) { if (e.key !== 'Escape') return; closeAllDropdowns(); closeMobileMenu(); }); window.addEventListener('resize', function () { if (window.matchMedia('(min-width: 768px)').matches) { closeMobileMenu(); } }); })();