// /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();
// --- 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__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]');
let slides = [];
let slideEls = [];
let current = 0;
let opener = null;
// --- 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);
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';
// Index can stay; if you want to hide on single too, uncomment next line:
// indexEl.style.display = single ? 'none' : '';
// Keyboard left/right still work; if you want to disable when arrows hidden,
// you can check config.showArrows in the key handler (left as-is for now).
}
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');
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(); });
window.addEventListener('keydown', (e)=>{
if (!overlay.classList.contains('open')) return;
if (e.key === 'Escape') { e.preventDefault(); 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 };
})();