📜
overlay_copy2.js
Back
📝 Javascript ⚡ Executable Ctrl+S: Save • Ctrl+R: Run • Ctrl+F: Find
// /core/js/overlay.js (function(){ const SWIPE_THRESHOLD = 50; // px horizontal const VERTICAL_LIMIT = 40; // px vertical // --- Config (persisted) --- const LS_KEY = 'AppOverlayConfig'; const defaultConfig = { showArrows: true, enableSwipe: true }; function loadConfig(){ try { return Object.assign({}, defaultConfig, JSON.parse(localStorage.getItem(LS_KEY) || '{}')); } catch { return { ...defaultConfig }; } } function saveConfig(cfg){ try { localStorage.setItem(LS_KEY, JSON.stringify(cfg)); } catch {} } let config = loadConfig(); // --- Global menu items array --- window.AppOverlayMenuItems = window.AppOverlayMenuItems || []; // --- DOM creation (once) --- function createOverlayDOM(){ const overlay = document.createElement('div'); overlay.className = 'app-overlay'; overlay.setAttribute('role','dialog'); overlay.setAttribute('aria-modal','true'); overlay.setAttribute('aria-hidden','true'); overlay.innerHTML = ` <section class="app-dialog" aria-labelledby="appDialogTitle"> <header class="app-dialog__header"> <div class="app-dialog__menu" data-menu style="display:none;">&#9776;</div> <div class="app-dialog__title" id="appDialogTitle">Title</div> <button class="app-dialog__close" type="button" aria-label="Close overlay">✕</button> </header> <div class="app-dialog__body"></div> <footer class="app-dialog__footer" data-footer> <button class="app-navbtn" data-prev type="button">← Prev</button> <div class="app-index" data-index>1 / 1</div> <button class="app-navbtn" data-next type="button">Next →</button> </footer> </section> `; document.body.appendChild(overlay); return overlay; } const overlay = createOverlayDOM(); const titleEl = overlay.querySelector('#appDialogTitle'); const bodyEl = overlay.querySelector('.app-dialog__body'); const closeEl = overlay.querySelector('.app-dialog__close'); const prevEl = overlay.querySelector('[data-prev]'); const nextEl = overlay.querySelector('[data-next]'); const indexEl = overlay.querySelector('[data-index]'); const footerEl= overlay.querySelector('[data-footer]'); const menuEl = overlay.querySelector('[data-menu]'); let slides = []; let slideEls = []; let current = 0; let opener = null; // --- Menu toggle --- let menuOpen = false; menuEl.addEventListener('click', (e) => { e.stopPropagation(); menuOpen = !menuOpen; if (menuOpen) showMenu(); else hideMenu(); }); let currentMenuStack = []; // Track menu hierarchy function showMenu(items = window.AppOverlayMenuItems, parentDropdown = null) { const rect = menuEl.getBoundingClientRect(); // Create dropdown let dropdown = document.createElement('div'); dropdown.className = 'app-menu-dropdown'; dropdown.style.position = 'fixed'; dropdown.style.background = '#fff'; dropdown.style.border = '1px solid #ddd'; dropdown.style.borderRadius = '4px'; dropdown.style.boxShadow = '0 2px 8px rgba(0,0,0,0.15)'; dropdown.style.minWidth = '180px'; dropdown.style.zIndex = '2147483647'; dropdown.style.transition = 'all 0.2s ease'; dropdown.style.overflow = 'hidden'; // Start position (main vs submenu) if (!parentDropdown) { dropdown.style.top = (rect.bottom + 4) + 'px'; dropdown.style.left = rect.left + 'px'; currentMenuStack = []; // Reset stack for root menu } else { const r = parentDropdown.getBoundingClientRect(); dropdown.style.top = r.top + 'px'; dropdown.style.left = (r.right - 2) + 'px'; // AUTO-ADD BACK BUTTON FOR SUBMENUS items = [{ label: "◀ Back", type: "back" }, ...items]; } // Build buttons items.forEach(item => { const btn = document.createElement('button'); btn.textContent = item.label; btn.className = 'app-menu-item'; btn.style.display = 'flex'; btn.style.alignItems = 'center'; btn.style.justifyContent = 'space-between'; btn.style.width = '100%'; btn.style.padding = '8px 12px'; btn.style.border = 'none'; btn.style.background = 'transparent'; btn.style.cursor = 'pointer'; btn.style.fontSize = '14px'; btn.style.color = '#333'; btn.onmouseenter = () => btn.style.background = '#f0f0f0'; btn.onmouseleave = () => btn.style.background = 'transparent'; if (item.type === 'toggle') { const toggle = document.createElement('span'); toggle.textContent = item.state ? '✅' : '⬜'; btn.appendChild(toggle); btn.onclick = (e) => { e.stopPropagation(); // Prevent menu close item.state = !item.state; toggle.textContent = item.state ? '✅' : '⬜'; if (item.onToggle) item.onToggle(item.state); hideMenu(); }; } else if (item.submenu) { const arrow = document.createElement('span'); arrow.textContent = '▶'; btn.appendChild(arrow); btn.onclick = (e) => { e.stopPropagation(); // Prevent menu close // Save current menu to stack currentMenuStack.push(dropdown); // Hide current dropdown dropdown.style.display = 'none'; // Show submenu const sub = showMenu(item.submenu, dropdown); document.body.appendChild(sub); }; } else if (item.type === 'back') { btn.textContent = '◀ Back'; btn.onclick = (e) => { e.stopPropagation(); // Prevent menu close // Remove current dropdown dropdown.remove(); // Show previous menu from stack if (currentMenuStack.length > 0) { const prevMenu = currentMenuStack.pop(); prevMenu.style.display = 'block'; } }; } else { btn.onclick = (e) => { e.stopPropagation(); // Prevent menu close if (item.action) item.action(); hideMenu(); }; } dropdown.appendChild(btn); }); // Handle root menu if (!parentDropdown) { hideMenu(); document.body.appendChild(dropdown); // CLOSE MENU WHEN CLICKING OUTSIDE setTimeout(() => { const closeOnOutsideClick = (e) => { // Check if click is on ANY menu dropdown or the menu button const clickedOnMenu = e.target.closest('.app-menu-dropdown') || e.target === menuEl || e.target.closest('.app-menu-trigger'); if (!clickedOnMenu) { hideMenu(); document.removeEventListener('click', closeOnOutsideClick); document.removeEventListener('touchstart', closeOnOutsideClick); } }; document.addEventListener('click', closeOnOutsideClick); document.addEventListener('touchstart', closeOnOutsideClick); }, 10); } dropdown.style.display = 'block'; return dropdown; } function hideMenu() { // Remove all menu dropdowns document.querySelectorAll('.app-menu-dropdown').forEach(m => m.remove()); currentMenuStack = []; } // --- Render / Update --- function renderSlides(){ bodyEl.innerHTML = ''; slideEls = slides.map(s => { const el = document.createElement('article'); el.className = 'app-slide'; el.innerHTML = s.html || ''; bodyEl.appendChild(el); // Call onRender callback if provided if (typeof s.onRender === 'function') { requestAnimationFrame(() => s.onRender(el)); } return el; }); } function applyNavVisibility(){ const single = slides.length <= 1; // Hide arrows when: (a) single item OR (b) user turned them off const showArrows = !single && !!config.showArrows; prevEl.style.display = showArrows ? '' : 'none'; nextEl.style.display = showArrows ? '' : 'none'; // Show menu only if items exist menuEl.style.display = window.AppOverlayMenuItems.length > 0 ? 'flex' : 'none'; } function update(){ titleEl.textContent = slides[current]?.title || ''; slideEls.forEach((el,i)=>el.classList.toggle('is-active', i===current)); indexEl.textContent = `${current+1} / ${slides.length}`; applyNavVisibility(); } function mountOnTop(){ if (overlay.parentElement !== document.body || document.body.lastElementChild !== overlay) { document.body.appendChild(overlay); } } // --- Controls --- function open(items, startIndex=0, openerEl=null){ slides = Array.isArray(items) ? items : []; if (slides.length === 0) return; opener = openerEl || document.activeElement; renderSlides(); current = Math.max(0, Math.min(startIndex, slides.length - 1)); mountOnTop(); overlay.classList.add('open'); overlay.setAttribute('aria-hidden','false'); document.body.classList.add('body-lock'); // block background scroll/P2R update(); closeEl.focus(); } function close(){ overlay.classList.remove('open'); overlay.setAttribute('aria-hidden','true'); document.body.classList.remove('body-lock'); hideMenu(); if (opener && typeof opener.focus === 'function') opener.focus(); } function next(){ if (slides.length > 1) { current = (current + 1) % slides.length; update(); } } function prev(){ if (slides.length > 1) { current = (current - 1 + slides.length) % slides.length; update(); } } // --- Events --- closeEl.addEventListener('click', close); nextEl.addEventListener('click', next); prevEl.addEventListener('click', prev); overlay.addEventListener('click', (e)=>{ if (e.target === overlay) close(); // Close menu if clicking outside if (menuOpen && !menuEl.contains(e.target) && !e.target.closest('.app-menu-dropdown')) { hideMenu(); } }); window.addEventListener('keydown', (e)=>{ if (!overlay.classList.contains('open')) return; if (e.key === 'Escape') { e.preventDefault(); if (menuOpen) hideMenu(); else close(); } if (e.key === 'ArrowRight') { e.preventDefault(); next(); } if (e.key === 'ArrowLeft') { e.preventDefault(); prev(); } }); // --- Swipe (obeys enableSwipe and slide count) --- let startX = 0, startY = 0, tracking = false; bodyEl.addEventListener('touchstart', (e)=>{ if (!overlay.classList.contains('open')) return; if (!config.enableSwipe || slides.length <= 1) return; if (e.touches.length !== 1) return; tracking = true; startX = e.touches[0].clientX; startY = e.touches[0].clientY; }, { passive: true }); bodyEl.addEventListener('touchend', (e)=>{ if (!tracking) return; tracking = false; if (!config.enableSwipe || slides.length <= 1) return; const endX = e.changedTouches[0].clientX; const endY = e.changedTouches[0].clientY; const dx = endX - startX; const dy = endY - startY; if (Math.abs(dx) >= SWIPE_THRESHOLD && Math.abs(dy) <= VERTICAL_LIMIT) { if (dx > 0) prev(); else next(); } }, { passive: true }); // --- Public config API --- function configure(partial){ if (!partial || typeof partial !== 'object') return; config = Object.assign({}, config, partial); saveConfig(config); // Re-apply current UI state without closing overlay applyNavVisibility(); } function getConfig(){ return Object.assign({}, config); } // --- Export API --- window.AppOverlay = { open, close, next, prev, configure, getConfig }; })();