475 lines
15 KiB
JavaScript
475 lines
15 KiB
JavaScript
// 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 = '<div class="rounded-xl border border-rose-700 bg-rose-900/20 p-4 text-rose-200">Failed to load editor. Please refresh the page.</div>';
|
|
});
|
|
}
|
|
|
|
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();
|
|
}
|
|
});
|
|
})();
|