📜
overlay_extra_copy2.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 = []; // --- Component Registry --- const componentRegistry = {}; // --- DOM creation --- function createOverlayDOM(options = {}) { 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> `; // detect fullscreen const isFullscreen = options.fullscreen === true; // detect side attachment mode const attachSide = options.attachSide; // 'left', 'right', 'top', 'bottom' const isSideAttached = ['left', 'right', 'top', 'bottom'].includes(attachSide); // determine alignment (only if not side-attached) const align = options.align || 'center'; let justifyContent = 'center'; let alignItems = 'center'; if (!isSideAttached) { if (align.includes('top')) justifyContent = 'flex-start'; else if (align.includes('bottom')) justifyContent = 'flex-end'; if (align.includes('left')) alignItems = 'flex-start'; else if (align.includes('right')) alignItems = 'flex-end'; } // container style if (isSideAttached) { overlay.style.cssText = ` position: fixed; inset: 0; display: block; background: rgba(0,0,0,0.8); z-index: 2147483647; overflow: hidden; padding: 0; margin: 0; -webkit-overflow-scrolling: touch; `; } else { overlay.style.cssText = ` position: fixed; inset: 0; display: flex; flex-direction: column; align-items: ${alignItems}; justify-content: ${justifyContent}; background: rgba(0,0,0,0.8); z-index: 2147483647; overflow: auto; padding: 0; margin: 0; -webkit-overflow-scrolling: touch; `; } const dialog = overlay.querySelector('.app-dialog'); const borderColor = options.borderColor || '#3a3a3a'; let width, maxWidth, height, maxHeight, minHeight, borderRadius; if (isFullscreen) { width = '100vw'; maxWidth = '100vw'; height = '100vh'; maxHeight = '100vh'; minHeight = '100vh'; borderRadius = '0'; } else if (isSideAttached) { // Side-attached mode - PERCENTAGE ONLY borderRadius = '0'; // No border radius for attached sides switch(attachSide) { case 'left': width = options.width || '30%'; maxWidth = '100vw'; height = '100vh'; maxHeight = '100vh'; minHeight = '100vh'; break; case 'right': width = options.width || '30%'; maxWidth = '100vw'; height = '100vh'; maxHeight = '100vh'; minHeight = '100vh'; break; case 'top': width = '100vw'; maxWidth = '100vw'; height = options.height || '30%'; maxHeight = '100vh'; minHeight = 'auto'; break; case 'bottom': width = '100vw'; maxWidth = '100vw'; height = options.height || '30%'; maxHeight = '100vh'; minHeight = 'auto'; break; } } else { width = options.width || '94%'; maxWidth = options.maxWidth || '900px'; height = options.height || 'auto'; maxHeight = options.maxHeight || '90vh'; minHeight = options.minHeight || 'auto'; borderRadius = options.borderRadius || '8px'; } dialog.style.cssText = ` background:#1e1e1e; border:${isFullscreen ? 'none' : `1px solid ${borderColor}`}; border-radius:${borderRadius}; width:${width}; max-width:${maxWidth}; height:${height}; max-height:${maxHeight}; min-height:${minHeight}; display:flex; flex-direction:column; box-shadow:${isFullscreen ? 'none' : '0 10px 40px rgba(0,0,0,0.5)'}; overflow:hidden; position:${isSideAttached || align.includes('top') || align.includes('bottom') ? 'absolute' : 'relative'}; box-sizing:border-box; `; // --- positioning for side-attached overlays --- if (isSideAttached) { dialog.style.margin = '0'; switch(attachSide) { case 'left': dialog.style.left = '0'; dialog.style.top = '0'; dialog.style.borderLeft = 'none'; break; case 'right': dialog.style.right = '0'; dialog.style.top = '0'; dialog.style.borderRight = 'none'; break; case 'top': dialog.style.top = '0'; dialog.style.left = '0'; dialog.style.borderTop = 'none'; break; case 'bottom': dialog.style.bottom = '0'; dialog.style.left = '0'; dialog.style.borderBottom = 'none'; break; } } else if (align.includes('top')) { dialog.style.top = '0'; dialog.style.marginTop = '0'; } else if (align.includes('bottom')) { dialog.style.bottom = '0'; dialog.style.marginBottom = '0'; } const bodyEl = overlay.querySelector('.app-dialog__body'); bodyEl.style.flex = '1'; bodyEl.style.overflowY = 'auto'; document.body.appendChild(overlay); return overlay; } const initialOverlay = createOverlayDOM(); initialOverlay.style.display = 'none'; // Hide by default let overlay = initialOverlay; let titleEl = overlay.querySelector('#appDialogTitle'); let bodyEl = overlay.querySelector('.app-dialog__body'); let toolbarEl = overlay.querySelector('.app-dialog__toolbar'); let closeEl = overlay.querySelector('.app-dialog__close'); let prevEl = overlay.querySelector('[data-prev]'); let nextEl = overlay.querySelector('[data-next]'); let 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(overlayConfig); 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, options={}){ slides = Array.isArray(items) ? items : []; if (slides.length === 0) return; opener = openerEl || document.activeElement; // Check if side attachment is requested const isSideAttached = ['left', 'right', 'top', 'bottom'].includes(options.attachSide); // If options are provided, recreate the overlay with new size/position if (options && (options.width || options.height || options.align || options.maxWidth || options.maxHeight || options.minHeight || options.fullscreen || options.borderColor || options.borderRadius || isSideAttached)) { // Remove old overlay if (overlay.parentElement) overlay.parentElement.removeChild(overlay); // Create new overlay with options overlay = createOverlayDOM(options); // Update local references titleEl = overlay.querySelector('#appDialogTitle'); bodyEl = overlay.querySelector('.app-dialog__body'); toolbarEl = overlay.querySelector('.app-dialog__toolbar'); closeEl = overlay.querySelector('.app-dialog__close'); prevEl = overlay.querySelector('[data-prev]'); nextEl = overlay.querySelector('[data-next]'); menuEl = overlay.querySelector('[data-menu]'); // Re-attach event listeners 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(); } }); menuEl.addEventListener('click', (e) => { e.stopPropagation(); menuOpen = !menuOpen; if (menuOpen) showMenu(); else hideMenu(); }); } renderSlides(); current = Math.max(0, Math.min(startIndex, slides.length - 1)); mountOnTop(); overlay.classList.add('open'); overlay.setAttribute('aria-hidden','false'); // Display mode is already set correctly by createOverlayDOM - don't override it if (overlay.style.display === 'none') { overlay.style.display = isSideAttached ? 'block' : '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); } // --- Schema Processor --- function processSection(section) { const slide = { title: section.title || '', html: '', buttons: section.buttons || [], onRender: null }; // Handle content - can be string, function, or HTML if (typeof section.content === 'function') { slide.onRender = section.content; slide.html = '<div class="section-content"></div>'; } else if (typeof section.content === 'string') { slide.html = section.content; } else if (section.html) { slide.html = section.html; } // Process buttons to handle action functions if (section.buttons) { slide.buttons = section.buttons.map(btn => { const processedBtn = { ...btn }; // If button has a nested overlay config if (btn.overlay) { const originalOverlay = btn.overlay; processedBtn.overlay = processOverlayConfig(originalOverlay); } return processedBtn; }); } return slide; } function processOverlayConfig(overlayConfig) { const processed = { title: overlayConfig.title || '', html: '', buttons: overlayConfig.buttons || [], // Pass through display options width: overlayConfig.width, maxWidth: overlayConfig.maxWidth, height: overlayConfig.height, maxHeight: overlayConfig.maxHeight, minHeight: overlayConfig.minHeight, align: overlayConfig.align, fullscreen: overlayConfig.fullscreen, borderColor: overlayConfig.borderColor, borderRadius: overlayConfig.borderRadius }; // Handle content if (typeof overlayConfig.content === 'function') { processed.onRender = overlayConfig.content; processed.html = '<div class="section-content"></div>'; } else if (overlayConfig.content) { processed.html = overlayConfig.content; } else if (overlayConfig.html) { processed.html = overlayConfig.html; } // Process nested buttons if (overlayConfig.buttons) { processed.buttons = overlayConfig.buttons.map(btn => { const processedBtn = { ...btn }; if (btn.overlay) { processedBtn.overlay = processOverlayConfig(btn.overlay); } return processedBtn; }); } return processed; } // --- High-Level Declarative API --- /** * Open an overlay using a declarative schema * @param {Object} schema - Overlay configuration * @param {string} schema.title - Overlay title * @param {Array} schema.sections - Array of section objects * @param {Array} schema.buttons - Top-level toolbar buttons * @param {boolean} schema.fullscreen - Fullscreen mode * @param {string} schema.width - Custom width * @param {string} schema.align - Alignment position * ... (other display options) */ function openOverlay(schema) { if (!schema) return; const options = { width: schema.width, maxWidth: schema.maxWidth, height: schema.height, maxHeight: schema.maxHeight, minHeight: schema.minHeight, align: schema.align, fullscreen: schema.fullscreen, borderColor: schema.borderColor, borderRadius: schema.borderRadius }; let slides = []; // If sections are provided, create slides from them if (schema.sections && Array.isArray(schema.sections)) { slides = schema.sections.map(section => processSection(section)); } // If single content provided, create single slide else if (schema.content || schema.html) { slides = [{ title: schema.title || '', html: schema.html || '', buttons: schema.buttons || [], onRender: typeof schema.content === 'function' ? schema.content : null }]; if (typeof schema.content === 'function') { slides[0].html = '<div class="section-content"></div>'; } else if (schema.content) { slides[0].html = schema.content; } } // Open with processed slides and options open(slides, 0, null, options); } /** * Define a reusable overlay component * @param {string} name - Component name * @param {Object} config - Component configuration */ function defineComponent(name, config) { if (!name || !config) return; componentRegistry[name] = config; } /** * Show a predefined overlay component * @param {string} name - Component name * @param {Object} overrides - Optional config overrides */ function show(name, overrides = {}) { const component = componentRegistry[name]; if (!component) { console.warn(`AppOverlay: Component "${name}" not found`); return; } // Merge component config with overrides const config = { ...component, ...overrides }; openOverlay(config); } /** * Get a list of all registered components */ function listComponents() { return Object.keys(componentRegistry); } /** * Remove a component from registry */ function undefineComponent(name) { delete componentRegistry[name]; } /** * Quick helper to open a simple dialog */ function alert(message, title = 'Alert') { openOverlay({ title: title, content: `<p>${message}</p>`, width: '400px', align: 'center', buttons: [ { label: 'OK', action: close } ] }); } /** * Quick helper to open a confirmation dialog */ function confirm(message, onConfirm, onCancel, title = 'Confirm') { openOverlay({ title: title, content: `<p>${message}</p>`, width: '450px', align: 'center', buttons: [ { label: 'Confirm', action: () => { close(); if (onConfirm) onConfirm(); } }, { label: 'Cancel', action: () => { close(); if (onCancel) onCancel(); } } ] }); } // Export both old and new APIs window.AppOverlay = { // Legacy API (backward compatible) open, close, next, prev, configure, getConfig, // New Declarative API openOverlay, defineComponent, show, listComponents, undefineComponent, alert, confirm }; })();