(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(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;">☰</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>
`;
// --- Check for fullscreen mode ---
const isFullscreen = options.fullscreen === true;
// --- Determine alignment (defaults to center) - ignored if fullscreen
const align = options.align || 'center'; // Options: 'center', 'top', 'bottom', 'left', 'right', 'top-left', 'top-right', 'bottom-left', 'bottom-right'
let justifyContent = 'center';
let alignItems = 'center';
if (!isFullscreen) {
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';
}
// --- Overlay Styles
overlay.style.cssText = `
position: fixed;
inset: 0;
display: none;
flex-direction: column;
align-items: ${alignItems};
justify-content: ${justifyContent};
background: rgba(0,0,0,0.8);
z-index: 2147483647;
overflow-y: auto;
padding: ${isFullscreen ? '0' : '20px'};
-webkit-overflow-scrolling: touch;
`;
// --- Dialog container adjustments
const dialog = overlay.querySelector('.app-dialog');
// Get border color option
const borderColor = options.borderColor || '#3a3a3a';
// Default dimensions (overridden by fullscreen)
let width, maxWidth, height, maxHeight, minHeight, borderRadius;
if (isFullscreen) {
width = '100vw';
maxWidth = '100vw';
height = '100vh';
maxHeight = '100vh';
minHeight = '100vh';
borderRadius = '0';
} 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: relative;
`;
const bodyEl = overlay.querySelector('.app-dialog__body');
bodyEl.style.overflowY = 'auto';
bodyEl.style.flex = '1';
bodyEl.style.minHeight = '200px';
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(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;
// 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)) {
// Remove old overlay
if (overlay.parentElement) overlay.parentElement.removeChild(overlay);
// Create new overlay with options
const newOverlay = createOverlayDOM(options);
// Update references
Object.assign(window, {
overlay: newOverlay,
titleEl: newOverlay.querySelector('#appDialogTitle'),
bodyEl: newOverlay.querySelector('.app-dialog__body'),
toolbarEl: newOverlay.querySelector('.app-dialog__toolbar'),
closeEl: newOverlay.querySelector('.app-dialog__close'),
prevEl: newOverlay.querySelector('[data-prev]'),
nextEl: newOverlay.querySelector('[data-next]'),
menuEl: newOverlay.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');
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 };
})();