📜
overlay_extra.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 || []; // --- Nested overlay stack --- let nestedOverlays = []; // --- 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'); 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:#2d2d2d;border-bottom:1px solid #3a3a3a;"> <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__toolbar" style="display:none;background:#252525;padding:8px 16px;border-bottom:1px solid #3a3a3a;gap:8px;flex-wrap:wrap;"></div> <div class="app-dialog__body" style="background:#1e1e1e;padding:20px;min-height:60vh;overflow:auto;color:#e0e0e0;"></div> </section> `; 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: 2147483647; overflow-y: auto; padding: 20px 0; -webkit-overflow-scrolling: touch; `; const dialog = overlay.querySelector('.app-dialog'); dialog.style.cssText = ` background: #1e1e1e; border: 1px solid #3a3a3a; border-radius: 8px; width: 94%; max-width: 900px; max-height: 90vh; display: flex; flex-direction: column; box-shadow: 0 10px 40px rgba(0,0,0,0.5); overflow: hidden; position: relative; `; const bodyEl = overlay.querySelector('.app-dialog__body'); bodyEl.style.overflowY = 'auto'; bodyEl.style.maxHeight = 'calc(90vh - 60px)'; document.body.appendChild(overlay); return overlay; } const overlay = createOverlayDOM(); const titleEl = overlay.querySelector('#appDialogTitle'); const bodyEl = overlay.querySelector('.app-dialog__body'); const toolbarEl = overlay.querySelector('.app-dialog__toolbar'); 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; let toolbarButtons = []; // --- 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 = '2147483647'; 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; } // --- Toolbar Buttons --- function renderToolbar(buttons = []) { toolbarButtons = Array.isArray(buttons) ? buttons : []; toolbarEl.innerHTML = ''; if (toolbarButtons.length === 0) { toolbarEl.style.display = 'none'; return; } toolbarEl.style.display = 'flex'; toolbarButtons.forEach(btn => { const button = document.createElement('button'); button.textContent = btn.label || 'Button'; button.type = 'button'; button.style.cssText = ` background: #3a3a3a; color: #e0e0e0; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer; font-size: 14px; transition: background 0.2s; `; button.onmouseenter = () => button.style.background = '#4a4a4a'; button.onmouseleave = () => button.style.background = '#3a3a3a'; button.onclick = (e) => { e.stopPropagation(); if (btn.overlay) { openNestedOverlay(btn.overlay); } else if (btn.action) { btn.action(); } }; toolbarEl.appendChild(button); }); } // --- Nested Overlay --- function openNestedOverlay(overlayConfig) { const nestedOverlay = createOverlayDOM(); nestedOverlay.classList.add('nested-overlay'); const baseZIndex = 2147483647; nestedOverlay.style.zIndex = baseZIndex + nestedOverlays.length + 1; const nestedTitleEl = nestedOverlay.querySelector('#appDialogTitle'); const nestedBodyEl = nestedOverlay.querySelector('.app-dialog__body'); const nestedCloseEl = nestedOverlay.querySelector('.app-dialog__close'); const nestedToolbarEl = nestedOverlay.querySelector('.app-dialog__toolbar'); const nestedMenuEl = nestedOverlay.querySelector('[data-menu]'); const nestedPrevEl = nestedOverlay.querySelector('[data-prev]'); const nestedNextEl = nestedOverlay.querySelector('[data-next]'); nestedPrevEl.style.display = 'none'; nestedNextEl.style.display = 'none'; nestedMenuEl.style.display = 'none'; nestedTitleEl.textContent = overlayConfig.title || 'Nested View'; if (overlayConfig.buttons) { renderNestedToolbar(nestedToolbarEl, overlayConfig.buttons, nestedOverlay); } nestedBodyEl.innerHTML = overlayConfig.html || ''; if (typeof overlayConfig.onRender === 'function') { requestAnimationFrame(() => overlayConfig.onRender(nestedBodyEl)); } nestedCloseEl.onclick = () => closeNestedOverlay(nestedOverlay); nestedOverlay.onclick = (e) => { if (e.target === nestedOverlay) { closeNestedOverlay(nestedOverlay); } }; nestedOverlays.push(nestedOverlay); nestedOverlay.classList.add('open'); nestedOverlay.setAttribute('aria-hidden', 'false'); nestedOverlay.style.display = 'flex'; nestedCloseEl.focus(); } function renderNestedToolbar(toolbarEl, buttons, overlayEl) { toolbarEl.innerHTML = ''; if (!buttons || buttons.length === 0) { toolbarEl.style.display = 'none'; return; } toolbarEl.style.display = 'flex'; buttons.forEach(btn => { const button = document.createElement('button'); button.textContent = btn.label || 'Button'; button.type = 'button'; button.style.cssText = ` background: #3a3a3a; color: #e0e0e0; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer; font-size: 14px; transition: background 0.2s; `; button.onmouseenter = () => button.style.background = '#4a4a4a'; button.onmouseleave = () => button.style.background = '#3a3a3a'; button.onclick = (e) => { e.stopPropagation(); if (btn.overlay) { openNestedOverlay(btn.overlay); } else if (btn.action) { btn.action(); } }; toolbarEl.appendChild(button); }); } function closeNestedOverlay(nestedOverlay) { nestedOverlay.classList.remove('open'); nestedOverlay.setAttribute('aria-hidden', 'true'); nestedOverlay.style.display = 'none'; const index = nestedOverlays.indexOf(nestedOverlay); if (index > -1) { nestedOverlays.splice(index, 1); } setTimeout(() => { if (nestedOverlay.parentElement) { nestedOverlay.parentElement.removeChild(nestedOverlay); } }, 300); if (nestedOverlays.length > 0) { const lastNested = nestedOverlays[nestedOverlays.length - 1]; const closeBtn = lastNested.querySelector('.app-dialog__close'); if (closeBtn) closeBtn.focus(); } else { closeEl.focus(); } } // --- 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); 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'; const currentSlide = slides[current]; renderToolbar(currentSlide?.buttons || []); } 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'); overlay.style.display = 'flex'; update(); closeEl.focus(); } function close(){ overlay.classList.remove('open'); overlay.setAttribute('aria-hidden','true'); overlay.style.display = 'none'; hideMenu(); nestedOverlays.forEach(nested => { nested.style.display = 'none'; if (nested.parentElement) { nested.parentElement.removeChild(nested); } }); nestedOverlays = []; 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(); } }); window.addEventListener('keydown', (e)=>{ if (!overlay.classList.contains('open') && nestedOverlays.length === 0) return; if (e.key === 'Escape') { e.preventDefault(); if (nestedOverlays.length > 0) { const topNested = nestedOverlays[nestedOverlays.length - 1]; closeNestedOverlay(topNested); } else if (menuOpen) { hideMenu(); } else { close(); } } if (nestedOverlays.length === 0) { if (e.key === 'ArrowRight') { e.preventDefault(); next(); } if (e.key === 'ArrowLeft') { e.preventDefault(); prev(); } } }); // --- Public API --- function configure(partial){ if (!partial || typeof partial !== 'object') return; config = Object.assign({}, config, partial); saveConfig(config); update(); } function getConfig(){ return Object.assign({}, config); } window.AppOverlay = { open, close, next, prev, configure, getConfig }; })();