// 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 = `
${opts.eyebrow ? `
${escHtml(opts.eyebrow)}
` : ''} ${opts.title ? `

${escHtml(opts.title)}

` : ''} ${opts.desc ? `

${escHtml(opts.desc)}

` : ''}
`; const endIconHtml = opts.endIcon ? `` : ''; root.innerHTML = `
${(opts.icon || opts.title || opts.eyebrow) ? `
${headerHasIcon ? `${escAttr(opts.iconAlt || '')}` : ''} ${titleHtml}
${endIconHtml}
` : ''}
${(opts.actions && opts.actions.length) ? '' : ''}
`; const contentEl = root.querySelector('.eo-modal-content'); const bodyEl = root.querySelector('.eo-modal-body'); const footerEl = root.querySelector('.eo-modal-footer'); const closeBtn = root.querySelector('.eo-modal-close'); bodyEl.appendChild(toBodyNode(opts.body)); const m = { el: root, contentEl, bodyEl, close: () => { root.remove(); if (typeof opts.onClose === 'function') { try { opts.onClose(); } catch (e) { console.error(e); } } } }; if (closeBtn) closeBtn.addEventListener('click', m.close); if (opts.closeOnBackdrop !== false) { root.addEventListener('click', (e) => { if (e.target === root) m.close(); }); } if (footerEl && Array.isArray(opts.actions)) { opts.actions.forEach((a) => { const btn = document.createElement('button'); btn.type = 'button'; btn.className = `btn btn-${a.variant || 'secondary'}`; btn.textContent = a.label || 'OK'; btn.addEventListener('click', () => { if (typeof a.onClick === 'function') a.onClick(m); else m.close(); }); footerEl.appendChild(btn); }); } document.body.appendChild(root); return m; }; // ----- Section primitives ----- window.eoSection = function (title, content) { return `
${title ? `
${escHtml(title)}
` : ''} ${content || ''}
`; }; window.eoBadgeRow = function (badges) { if (!Array.isArray(badges) || badges.length === 0) return ''; const html = badges.map((b) => { const variant = b.variant ? ` ${b.variant}` : ''; return `${b.icon ? escHtml(b.icon) + ' ' : ''}${escHtml(b.label)}`; }).join(''); return `
${html}
`; }; window.eoUrlList = function (urls) { if (!Array.isArray(urls) || urls.length === 0) return ''; const arrow = ``; const rows = urls.map((u) => ` ${escHtml(u.label)} ${escHtml(u.url)} ${arrow} `).join(''); return `
${rows}
`; }; window.eoCredList = function (creds) { if (!Array.isArray(creds) || creds.length === 0) return ''; const copyBtn = (val) => ``; return creds.map((c) => { const userLabel = c.userLabel || (typeof c.username === 'string' && c.username.includes('@') ? 'Email' : 'User'); const passLabel = c.passLabel || 'Pass'; return `
${c.title ? `
${escHtml(c.title)}
` : ''} ${c.username != null ? `
${escHtml(userLabel)} ${escHtml(c.username)} ${copyBtn(c.username)}
` : ''} ${c.password != null ? `
${escHtml(passLabel)} •••••••• ${copyBtn(c.password)}
` : ''}
`; }).join(''); }; window.eoEmpty = function (text) { return `

${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(); } }); })();