AI Generated Snippet
A code snippet generated by AI.
Code
/**
* Revealing Card Modal (Vanilla JS): click a card to open an accessible modal with details.
* Usage: add [data-modal-card] elements with data-title/data-content; call initRevealingCardModal().
*/
(() => {
const SELECTORS = {
card: "[data-modal-card]",
modal: "#card-modal",
title: "#card-modal-title",
body: "#card-modal-body",
close: "[data-modal-close]",
};
const escapeHtml = (s = "") =>
String(s).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]));
const ensureModal = () => {
const existing = document.querySelector(SELECTORS.modal);
if (existing) return existing;
const modal = document.createElement("div");
modal.id = "card-modal";
modal.setAttribute("role", "dialog");
modal.setAttribute("aria-modal", "true");
modal.setAttribute("aria-labelledby", "card-modal-title");
modal.hidden = true;
modal.style.cssText =
"position:fixed;inset:0;display:grid;place-items:center;background:rgba(0,0,0,.55);padding:16px;z-index:9999;";
modal.innerHTML = `
<div style="width:min(560px,100%);background:#fff;border-radius:12px;padding:18px 18px 14px;box-shadow:0 10px 30px rgba(0,0,0,.25);">
<div style="display:flex;justify-content:space-between;gap:12px;align-items:flex-start;">
<h2 id="card-modal-title" style="margin:0;font:600 18px/1.2 system-ui,-apple-system,Segoe UI,Roboto;">Details</h2>
<button type="button" data-modal-close aria-label="Close" style="border:0;background:transparent;font-size:22px;line-height:1;cursor:pointer;">×</button>
</div>
<div id="card-modal-body" style="margin-top:10px;font:400 14px/1.55 system-ui,-apple-system,Segoe UI,Roboto;color:#222;"></div>
</div>
`;
document.body.appendChild(modal);
return modal;
};
const fetchContentIfNeeded = async (card) => {
const { content = "", contentUrl = "" } = card.dataset;
if (!contentUrl) return content;
try {
const res = await fetch(contentUrl, { headers: { Accept: "text/plain" } });
if (!res.ok) throw new Error(`Fetch failed (${res.status})`);
return await res.text();
} catch (err) {
console.error(err);
return content || "Sorry, details are unavailable right now.";
}
};
const initRevealingCardModal = ({ root = document } = {}) => {
const modal = ensureModal();
const titleEl = modal.querySelector(SELECTORS.title);
const bodyEl = modal.querySelector(SELECTORS.body);
const closeBtn = modal.querySelector(SELECTORS.close);
let lastActiveEl = null;
const setModalOpen = (open) => {
modal.hidden = !open;
document.documentElement.style.overflow = open ? "hidden" : "";
if (!open && lastActiveEl) lastActiveEl.focus?.();
};
const openFromCard = async (card) => {
lastActiveEl = document.activeElement;
const { title = "Details" } = card.dataset;
titleEl.textContent = title;
bodyEl.textContent = "Loading…";
setModalOpen(true);
closeBtn.focus?.();
const content = await fetchContentIfNeeded(card);
bodyEl.innerHTML = escapeHtml(content).replace(/\n/g, "<br>");
};
// Event delegation for performance: one listener handles all cards, even dynamically added ones.
root.addEventListener("click", (e) => {
const card = e.target.closest(SELECTORS.card);
if (card) openFromCard(card);
});
// Close interactions: button, backdrop click, and Escape key.
closeBtn.addEventListener("click", () => setModalOpen(false));
modal.addEventListener("click", (e) => {
if (e.target === modal) setModalOpen(false);
});
window.addEventListener("keydown", (e) => {
if (!modal.hidden && e.key === "Escape") setModalOpen(false);
});
return { openFromCard, close: () => setModalOpen(false) };
};
// Example usage:
// <div data-modal-card data-title="Card 1" data-content="Details about Card 1">...</div>
// <div data-modal-card data-title="Card 2" data-content-url="/details/card-2.txt">...</div>
window.initRevealingCardModal = initRevealingCardModal;
})();