// eo-modal — unified modal helper. CSS in modal.css under ".eo-modal". // // API: // const m = openEoModal({ // id, size, className, // 'sm' | 'md' (default) | 'lg' // icon, iconAlt, eyebrow, title, desc, // header // body, // string | HTMLElement | array of either // actions, // [{label, variant, onClick(modal)}] // closeOnBackdrop = true, // onClose, // }); // m.close(); // remove from DOM, fires onClose // m.bodyEl; // the .eo-modal-body element (mutate as needed) // m.contentEl; // the .eo-modal-content element // m.el; // the backdrop .eo-modal element // // Section primitives (return HTML strings; pass as part of body): // eoSection(title, content) // eoBadgeRow([{icon, label, variant}]) variant: success|info|purple|warning|danger // eoUrlList([{url, label}]) // eoCredList([{title, username, password}]) // eoEmpty(text) (function () { function escHtml(s) { return String(s == null ? '' : s) .replace(/&/g, '&').replace(//g, '>'); } function escAttr(s) { return escHtml(s).replace(/"/g, '"').replace(/'/g, '''); } function toBodyNode(input) { if (input == null) return document.createTextNode(''); if (typeof input === 'string') { const t = document.createElement('div'); t.style.display = 'contents'; t.innerHTML = input; return t; } if (Array.isArray(input)) { const f = document.createDocumentFragment(); input.forEach((p) => f.appendChild(toBodyNode(p))); return f; } if (input instanceof HTMLElement || input instanceof DocumentFragment) return input; return document.createTextNode(String(input)); } window.openEoModal = function openEoModal(opts) { opts = opts || {}; const id = opts.id || `eo-modal-${Math.random().toString(36).slice(2, 9)}`; const size = opts.size || 'md'; const existing = document.getElementById(id); if (existing) existing.remove(); const root = document.createElement('div'); root.className = `eo-modal ${opts.className || ''}`.trim(); root.id = id; root.dataset.size = size; const headerHasIcon = !!opts.icon; const titleHtml = `
${escHtml(opts.desc)}
` : ''}${escHtml(c.username)}
${copyBtn(c.username)}
••••••••
${copyBtn(c.password)}
${escHtml(text || '')}
`; }; document.addEventListener('click', (e) => { const btn = e.target.closest && e.target.closest('.eo-modal-cred-toggle'); if (!btn) return; const code = btn.parentElement && btn.parentElement.querySelector('.eo-cred-pass'); if (!code) return; const revealed = code.dataset.revealed === 'true'; code.textContent = revealed ? '••••••••' : code.dataset.value; code.dataset.revealed = revealed ? 'false' : 'true'; btn.textContent = revealed ? 'Show' : 'Hide'; }); // Copy-to-clipboard for cred rows. Briefly swaps the icon for a check. document.addEventListener('click', (e) => { const btn = e.target.closest && e.target.closest('.eo-modal-cred-copy'); if (!btn) return; const value = btn.dataset.copy || ''; const done = () => { btn.classList.add('copied'); const original = btn.innerHTML; btn.innerHTML = ``; setTimeout(() => { btn.classList.remove('copied'); btn.innerHTML = original; }, 1100); }; if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(value).then(done).catch(() => done()); } else { const ta = document.createElement('textarea'); ta.value = value; ta.style.position = 'fixed'; ta.style.opacity = '0'; document.body.appendChild(ta); ta.select(); try { document.execCommand('copy'); } catch (_) {} document.body.removeChild(ta); done(); } }); })();