📜
overlay.js
Back
📝 Javascript ⚡ Executable Ctrl+S: Save • Ctrl+R: Run • Ctrl+F: Find
(function(){ const SWIPE_THRESHOLD = 50; const VERTICAL_LIMIT = 40; // --- Config --- const LS_KEY = 'AppOverlayConfig'; const defaultConfig = { showArrows: false, enableSwipe: false }; 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 --- window.AppOverlayMenuItems = window.AppOverlayMenuItems || []; // --- Track all overlay instances --- const overlayInstances = []; let zIndexCounter = 2147483647; // --- Layout Templates --- const layouts = { grid: { container: 'display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 15px; padding: 20px;', button: 'width: 100%; padding: 20px; font-size: 18px;' }, list: { container: 'display: flex; flex-direction: column; gap: 10px; padding: 20px; max-width: 600px; margin: 0 auto;', button: 'width: 100%; padding: 16px 24px; font-size: 16px; text-align: left;' }, compact: { container: 'display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 10px; padding: 20px;', button: 'width: 100%; padding: 12px; font-size: 14px;' }, hero: { container: 'display: flex; flex-direction: column; gap: 20px; padding: 40px; align-items: center; justify-content: center;', button: 'min-width: 300px; padding: 24px 48px; font-size: 20px;' }, sidebar: { container: 'display: flex; flex-direction: column; gap: 8px; padding: 20px; max-width: 300px;', button: 'width: 100%; padding: 14px 20px; font-size: 15px; text-align: left;' }, tiles: { container: 'display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 20px; padding: 20px;', button: 'width: 100%; aspect-ratio: 1; padding: 20px; font-size: 16px; display: flex; flex-direction: column; align-items: center; justify-content: center;' }, topbar: { container: 'display: flex; flex-direction: row; gap: 10px; padding: 12px 15px; overflow-x: auto; flex-wrap: nowrap; align-items: center; background: #0a0a0a; border-bottom: 1px solid #1a1a1a; scroll-snap-type: x mandatory; -webkit-overflow-scrolling: touch; scrollbar-width: none;', button: 'padding: 10px 20px; font-size: 14px; white-space: nowrap; flex-shrink: 0; scroll-snap-align: start;' }, bottombar: { container: 'display: flex; flex-direction: row; gap: 10px; padding: 12px 15px; overflow-x: auto; flex-wrap: nowrap; align-items: center; background: #0a0a0a; border-top: 1px solid #1a1a1a; position: sticky; bottom: 0; scroll-snap-type: x mandatory; -webkit-overflow-scrolling: touch; scrollbar-width: none;', button: 'padding: 10px 20px; font-size: 14px; white-space: nowrap; flex-shrink: 0; scroll-snap-align: start;' } }; // --- Button Renderer --- function renderButtons(buttons, layout = 'grid', buttonCSS = {}, containerCSS = {}) { const layoutStyle = layouts[layout] || layouts.grid; const container = document.createElement('div'); container.style.cssText = layoutStyle.container; container.className = 'button-container'; // Apply container-level CSS overrides (spacing, padding, etc.) Object.entries(containerCSS).forEach(([prop, value]) => { container.style[prop] = value; }); buttons.forEach(btn => { const button = document.createElement('button'); button.textContent = btn.label || 'Button'; // Apply layout-specific button styles first button.style.cssText = layoutStyle.button; // Apply global buttonCSS overrides (from slide config) Object.entries(buttonCSS).forEach(([prop, value]) => { button.style[prop] = value; }); // Apply individual button styles (highest priority) const btnStyles = { background: btn.background || '#1a1a1a', color: btn.color || '#ffffff', border: btn.border || '1px solid #2a2a2a', borderRadius: btn.borderRadius || '0', cursor: 'pointer', transition: 'all 0.2s', fontWeight: btn.fontWeight || '600' }; Object.entries(btnStyles).forEach(([prop, value]) => { button.style[prop] = value; }); // Apply any additional custom CSS from button object if (btn.css) { Object.entries(btn.css).forEach(([prop, value]) => { button.style[prop] = value; }); } if (btn.icon) { const icon = document.createElement('span'); icon.textContent = btn.icon + ' '; icon.style.marginRight = '8px'; button.insertBefore(icon, button.firstChild); } button.onmouseenter = () => { button.style.transform = btn.hoverTransform || 'scale(1.05)'; button.style.opacity = btn.hoverOpacity || '0.9'; }; button.onmouseleave = () => { button.style.transform = 'scale(1)'; button.style.opacity = '1'; }; // Handle onclick - can be function or overlay config // Support both onClick (new) and action (old) for backwards compatibility const clickHandler = btn.onClick || btn.action; if (typeof clickHandler === 'function') { button.onclick = clickHandler; } else if (clickHandler && typeof clickHandler === 'object') { // If onClick/action is an object, treat it as overlay config button.onclick = () => { AppOverlay.open(clickHandler.slides || [clickHandler], clickHandler.startIndex); }; } container.appendChild(button); }); return container; } // --- DOM creation --- 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'); // Add cache-busting data attribute overlay.setAttribute('data-overlay-id', Date.now() + '-' + Math.random()); // Add styles for hiding scrollbars const style = document.createElement('style'); style.textContent = ` .button-container::-webkit-scrollbar { display: none; } .button-container { -ms-overflow-style: none; scrollbar-width: none; } `; document.head.appendChild(style); overlay.innerHTML = ` <section class="app-dialog" aria-labelledby="appDialogTitle"> <header class="app-dialog__header" style="display:flex;align-items:center;justify-content:space-between;padding:12px 16px;background:#0a0a0a;border-bottom:1px solid #1a1a1a;"> <div style="display:flex;align-items:center;gap:10px;"> <div class="app-dialog__menu" data-menu style="font-size:20px;cursor:pointer;color:#e0e0e0;">&#9776;</div> <div class="app-dialog__title" id="appDialogTitle" style="color:#e0e0e0;font-size:16px;font-weight:500;">Title</div> </div> <div style="display:flex;align-items:center;gap:8px;flex-shrink:0;"> <button class="app-navbtn" data-prev type="button" style="display:none;background:#3a3a3a;color:#e0e0e0;border:none;padding:6px 12px;border-radius:4px;cursor:pointer;">←</button> <button class="app-navbtn" data-next type="button" style="display:none;background:#3a3a3a;color:#e0e0e0;border:none;padding:6px 12px;border-radius:4px;cursor:pointer;">→</button> <button class="app-dialog__close" type="button" aria-label="Close overlay" style="background:#3a3a3a;color:#e0e0e0;border:none;padding:6px 12px;border-radius:4px;cursor:pointer;">✕</button> </div> </header> <div class="app-dialog__body" style="background:#000;padding:0;min-height:100%;overflow:auto;color:#e0e0e0;"></div> </section> `; // --- Overlay Styles (full screen, no padding) overlay.style.cssText = ` position: fixed; inset: 0; display: none; flex-direction: column; align-items: center; justify-content: flex-start; background: rgba(0,0,0,0.8); z-index: ${zIndexCounter++}; overflow-y: auto; padding: 0; -webkit-overflow-scrolling: touch; `; // --- Dialog container adjustments (full screen) const dialog = overlay.querySelector('.app-dialog'); dialog.style.cssText = ` background: #000; border: none; border-radius: 0; width: 100%; max-width: 100%; height: 100vh; max-height: 100vh; display: flex; flex-direction: column; box-shadow: none; overflow: hidden; position: relative; margin: 0; `; const bodyEl = overlay.querySelector('.app-dialog__body'); bodyEl.style.overflowY = 'auto'; bodyEl.style.maxHeight = 'calc(100vh - 60px)'; bodyEl.style.flex = '1'; document.body.appendChild(overlay); return overlay; } // --- Create Overlay Instance --- function createOverlayInstance() { 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 menuEl = overlay.querySelector('[data-menu]'); let slides = []; let slideEls = []; let current = 0; let opener = null; // --- Menu toggle --- let menuOpen = false; let currentMenuStack = []; menuEl.addEventListener('click', (e) => { e.stopPropagation(); menuOpen = !menuOpen; if (menuOpen) showMenu(); else hideMenu(); }); function showMenu(items = window.AppOverlayMenuItems, parentDropdown = null) { const rect = menuEl.getBoundingClientRect(); 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 = (parseInt(overlay.style.zIndex) + 1).toString(); dropdown.style.transition = 'all 0.2s ease'; dropdown.style.overflow = 'hidden'; if (!parentDropdown) { dropdown.style.top = (rect.bottom + 4) + 'px'; dropdown.style.left = rect.left + 'px'; currentMenuStack = []; } else { const r = parentDropdown.getBoundingClientRect(); dropdown.style.top = r.top + 'px'; dropdown.style.left = (r.right - 2) + 'px'; items = [{ label: "◀ Back", type: "back" }, ...items]; } 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(); 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(); currentMenuStack.push(dropdown); dropdown.style.display = 'none'; const sub = showMenu(item.submenu, dropdown); document.body.appendChild(sub); }; } else if (item.type === 'back') { btn.textContent = '◀ Back'; btn.onclick = (e) => { e.stopPropagation(); dropdown.remove(); if (currentMenuStack.length > 0) { const prevMenu = currentMenuStack.pop(); prevMenu.style.display = 'block'; } }; } else { btn.onclick = (e) => { e.stopPropagation(); if (item.action) item.action(); hideMenu(); }; } dropdown.appendChild(btn); }); if (!parentDropdown) { hideMenu(); document.body.appendChild(dropdown); setTimeout(() => { const closeOnOutsideClick = (e) => { const clickedOnMenu = e.target.closest('.app-menu-dropdown') || e.target === menuEl; if (!clickedOnMenu) { hideMenu(); document.removeEventListener('click', closeOnOutsideClick); } }; document.addEventListener('click', closeOnOutsideClick); }, 10); } dropdown.style.display = 'block'; return dropdown; } function hideMenu() { document.querySelectorAll('.app-menu-dropdown').forEach(m => m.remove()); currentMenuStack = []; menuOpen = false; } // --- Render / Update --- function renderSlides(){ bodyEl.innerHTML = ''; slideEls = slides.map(s => { const el = document.createElement('article'); el.className = 'app-slide'; el.style.height = '100%'; // If slide has buttons, render them with slide-level buttonCSS and containerCSS if (s.buttons && Array.isArray(s.buttons)) { const buttonContainer = renderButtons( s.buttons, s.layout || 'grid', s.buttonCSS || {}, s.containerCSS || {} ); el.appendChild(buttonContainer); } // If slide has HTML, add it if (s.html) { const htmlContainer = document.createElement('div'); htmlContainer.innerHTML = s.html; el.appendChild(htmlContainer); } bodyEl.appendChild(el); if (typeof s.onRender === 'function') requestAnimationFrame(() => s.onRender(el)); return el; }); } function update(){ titleEl.textContent = slides[current]?.title || ''; slideEls.forEach((el,i)=>el.classList.toggle('is-active', i===current)); const showArrows = config.showArrows && slides.length > 1; prevEl.style.display = showArrows ? '' : 'none'; nextEl.style.display = showArrows ? '' : 'none'; menuEl.style.display = window.AppOverlayMenuItems.length > 0 ? 'block' : 'none'; } 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; // Apply overlay-level styling if present const firstSlide = slides[0]; if (firstSlide.overlayCSS) { const allowedOverlayProps = [ 'background', 'backgroundColor', 'backdropFilter', 'border', 'borderRadius', 'outline', 'boxShadow', 'opacity' ]; Object.entries(firstSlide.overlayCSS).forEach(([prop, value]) => { if (allowedOverlayProps.includes(prop)) { overlay.style[prop] = value; } }); } // Apply dialog-level styling if present const dialog = overlay.querySelector('.app-dialog'); if (firstSlide.dialogCSS) { const allowedDialogProps = [ 'background', 'backgroundColor', 'backdropFilter', 'border', 'borderColor', 'borderWidth', 'borderStyle', 'outline', 'boxShadow', 'color' ]; Object.entries(firstSlide.dialogCSS).forEach(([prop, value]) => { if (allowedDialogProps.includes(prop)) { dialog.style[prop] = value; } }); } renderSlides(); current = Math.max(0, Math.min(startIndex, slides.length - 1)); mountOnTop(); overlay.classList.add('open'); overlay.setAttribute('aria-hidden','false'); overlay.style.display = 'flex'; update(); closeEl.focus(); } function close(){ overlay.classList.remove('open'); overlay.setAttribute('aria-hidden','true'); overlay.style.display = 'none'; hideMenu(); // Remove from instances array const idx = overlayInstances.indexOf(instance); if (idx > -1) overlayInstances.splice(idx, 1); // Remove from DOM overlay.remove(); 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(); if (menuOpen && !menuEl.contains(e.target) && !e.target.closest('.app-menu-dropdown')) { hideMenu(); } }); const keyHandler = (e) => { if (!overlay.classList.contains('open')) return; // Only handle keys for the topmost overlay if (overlayInstances[overlayInstances.length - 1] !== instance) 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(); } }; window.addEventListener('keydown', keyHandler); const instance = { open, close, next, prev, overlay, keyHandler }; overlayInstances.push(instance); return instance; } // --- Public API --- function configure(partial){ if (!partial || typeof partial !== 'object') return; config = Object.assign({}, config, partial); saveConfig(config); } function getConfig(){ return Object.assign({}, config); } // Main API - creates new overlay instances function openNew(items, startIndex=0, openerEl=null) { const instance = createOverlayInstance(); instance.open(items, startIndex, openerEl); return instance; } function closeAll() { [...overlayInstances].forEach(inst => inst.close()); } window.AppOverlay = { open: openNew, closeAll, configure, getConfig, instances: overlayInstances, layouts: Object.keys(layouts) // Expose available layouts }; })();