// 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'; import React from 'react'; import { createRoot } from 'react-dom/client'; 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 mountStoryEditor() { var storyEditorRoot = document.getElementById('story-editor-react-root'); if (!storyEditorRoot) return; if (storyEditorRoot.dataset.reactMounted === 'true') return; var mode = storyEditorRoot.getAttribute('data-mode') || 'create'; var storyRaw = storyEditorRoot.getAttribute('data-story') || '{}'; var storyTypesRaw = storyEditorRoot.getAttribute('data-story-types') || '[]'; var endpointsRaw = storyEditorRoot.getAttribute('data-endpoints') || '{}'; var csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''; var initialStory = {}; var storyTypes = []; var endpoints = {}; try { initialStory = JSON.parse(storyRaw); storyTypes = JSON.parse(storyTypesRaw); endpoints = JSON.parse(endpointsRaw); } catch (_error) { // If parsing fails, the editor falls back to component defaults. } storyEditorRoot.dataset.reactMounted = 'true'; void import('./components/editor/StoryEditor') .then(function (module) { var StoryEditor = module.default; createRoot(storyEditorRoot).render( React.createElement(StoryEditor, { mode: mode, initialStory: initialStory, storyTypes: storyTypes, endpoints: endpoints, csrfToken: csrfToken, }) ); }) .catch(function () { storyEditorRoot.dataset.reactMounted = 'false'; storyEditorRoot.innerHTML = '
Failed to load editor. Please refresh the page.
'; }); } mountStoryEditor(); (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 setMobileToggleVisual(isOpen) { var toggle = document.querySelector('[data-mobile-toggle]') || document.getElementById('btnSidebar'); if (!toggle) return; setExpanded(toggle, !!isOpen); var hamburgerIcon = toggle.querySelector('[data-mobile-icon-hamburger]'); var closeIcon = toggle.querySelector('[data-mobile-icon-close]'); if (hamburgerIcon) hamburgerIcon.classList.toggle('hidden', !!isOpen); if (closeIcon) closeIcon.classList.toggle('hidden', !isOpen); } function closeMobileMenu() { var menu = getMobileMenu(); if (!menu) return; menu.classList.add('hidden'); setMobileToggleVisual(false); } function toggleMobileMenu() { var menu = getMobileMenu(); if (!menu) return; var isOpen = !menu.classList.contains('hidden'); if (isOpen) { closeMobileMenu(); } else { menu.classList.remove('hidden'); setMobileToggleVisual(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; } var mobileSectionToggle = closest(e.target, '[data-mobile-section-toggle]'); if (mobileSectionToggle) { e.preventDefault(); var panelId = mobileSectionToggle.getAttribute('aria-controls'); var panel = panelId ? document.getElementById(panelId) : null; if (!panel) return; var wasOpen = !panel.classList.contains('hidden'); var menuRoot = getMobileMenu(); // Keep mobile navigation tidy: close all sections first. if (menuRoot) { menuRoot.querySelectorAll('[data-mobile-section-panel]').forEach(function (el) { el.classList.add('hidden'); }); menuRoot.querySelectorAll('[data-mobile-section-toggle]').forEach(function (btn) { setExpanded(btn, false); var icon = btn.querySelector('[data-mobile-section-icon]'); if (icon) icon.classList.remove('rotate-180'); }); } // If it was closed, open it. If it was open, it stays closed (toggle behavior). if (!wasOpen) { panel.classList.remove('hidden'); setExpanded(mobileSectionToggle, true); var currentIcon = mobileSectionToggle.querySelector('[data-mobile-section-icon]'); if (currentIcon) currentIcon.classList.add('rotate-180'); } 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(); } // Close mobile menu when tapping outside of it and outside the hamburger toggle. var mobileMenu = getMobileMenu(); var mobileToggle = closest(e.target, '[data-mobile-toggle]') || closest(e.target, '#btnSidebar'); if (mobileMenu && !mobileMenu.classList.contains('hidden') && !mobileToggle && !closest(e.target, '#mobileMenu')) { closeMobileMenu(); } }); // 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(); } }); })();