const BASE = (document.querySelector('meta[name="zeyn-base"]')?.content ?? '').replace(/\/$/, '');
const HOVER_MS = 500;
const CLOSE_MS = 200;
const CACHE_MAX = 24;
const GAP = 8;
const fine = matchMedia('(hover: hover) and (pointer: fine)');
const cache = new Map();
let panel = null;
let owner = null;
let openTimer = 0;
let closeTimer = 0;
function load(url) {
const hit = cache.get(url);
if (hit) {
cache.delete(url);
cache.set(url, hit);
return hit;
}
const job = fetch(BASE + url, { credentials: 'same-origin', headers: { accept: 'text/html' } })
.then((res) => (res.ok ? res.text() : null))
.then((html) => {
if (html == null) return null;
const t = document.createElement('template');
t.innerHTML = html;
t.content.querySelectorAll('iframe, frame, object, embed, video, audio, script, template, [data-zeyn-grid]').forEach((el) => el.remove());
t.content.querySelectorAll('[id]').forEach((el) => el.removeAttribute('id'));
return t.content;
})
.catch(() => null);
cache.set(url, job);
while (cache.size > CACHE_MAX) cache.delete(cache.keys().next().value);
return job;
}
function panelEl() {
if (panel) return panel;
panel = document.createElement('div');
panel.className = 'z-preview';
panel.setAttribute('popover', 'manual');
panel.addEventListener('pointerenter', () => clearTimeout(closeTimer));
panel.addEventListener('pointerleave', () => closeSoon());
document.body.append(panel);
return panel;
}
function place(card) {
const p = panelEl();
const r = card.getBoundingClientRect();
const vw = document.documentElement.clientWidth;
const vh = document.documentElement.clientHeight;
const w = Math.min(p.offsetWidth, vw - 2 * GAP);
const h = Math.min(p.offsetHeight, vh - 2 * GAP);
const rtl = getComputedStyle(card).direction === 'rtl';
const after = vw - r.right - GAP;
const before = r.left - GAP;
let x;
if (after >= w && (!rtl || before < w)) x = r.right + GAP;
else if (before >= w) x = r.left - GAP - w;
else x = after >= before ? vw - GAP - w : GAP;
const y = Math.max(GAP, Math.min(r.top, vh - GAP - h));
p.style.left = `${Math.round(x)}px`;
p.style.top = `${Math.round(y)}px`;
}
async function open(card) {
clearTimeout(closeTimer);
owner = card;
const content = await load(card.dataset.zeynPreview);
if (owner !== card || !content || !card.isConnected || card.closest('.z-rail-dragging')) return;
const p = panelEl();
p.replaceChildren(content.cloneNode(true));
if (!p.matches(':popover-open')) p.showPopover();
place(card);
}
function close() {
clearTimeout(openTimer);
clearTimeout(closeTimer);
owner = null;
if (panel?.matches(':popover-open')) panel.hidePopover();
}
function closeSoon() {
clearTimeout(openTimer);
clearTimeout(closeTimer);
closeTimer = setTimeout(close, CLOSE_MS);
}
function cardOf(target) {
return target instanceof Element ? target.closest('[data-zeyn-preview]') : null;
}
function hover(card) {
if (!fine.matches || card.closest('.z-rail-dragging')) return;
clearTimeout(closeTimer);
if (owner === card && panel?.matches(':popover-open')) return;
clearTimeout(openTimer);
owner = card;
openTimer = setTimeout(() => open(card), HOVER_MS);
}
function focus(card) {
if (fine.matches && card.matches(':focus-visible')) open(card);
}
document.addEventListener('pointerover', (e) => {
const card = e.pointerType === 'mouse' || e.pointerType === 'pen' ? cardOf(e.target) : null;
if (card && !card.contains(e.relatedTarget)) hover(card);
});
document.addEventListener('pointerout', (e) => {
const card = cardOf(e.target);
if (card && !card.contains(e.relatedTarget) && !panel?.contains(e.relatedTarget)) closeSoon();
});
document.addEventListener('focusin', (e) => {
const card = cardOf(e.target);
if (card) focus(card);
else if (!panel?.contains(e.target)) close();
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && panel?.matches(':popover-open')) {
e.preventDefault();
close();
}
});
document.addEventListener('pointerdown', (e) => {
clearTimeout(openTimer);
if (panel?.matches(':popover-open') && !panel.contains(e.target)) close();
});
addEventListener('scroll', () => owner && panel?.matches(':popover-open') && place(owner), { passive: true, capture: true });
addEventListener('resize', () => close());
document.addEventListener('zeyn:leave', () => {
close();
cache.clear();
});
export function wake(card, type) {
if (type === 'focusin') focus(card);
else hover(card);
}