(function () { const picker = document.querySelector("[data-reaction-picker]"); const panel = picker ? picker.querySelector(".reaction-picker-panel") : null; const search = picker ? picker.querySelector("[data-reaction-search]") : null; const closeButton = picker ? picker.querySelector("[data-reaction-close]") : null; const tabs = picker ? Array.from(picker.querySelectorAll("[data-reaction-tab]")) : []; const panels = picker ? Array.from(picker.querySelectorAll("[data-reaction-panel]")) : []; let activeButton = null; let activeCard = null; document.querySelectorAll("[data-reaction-button]").forEach((button) => { button.addEventListener("click", (event) => { event.preventDefault(); event.stopPropagation(); openPicker(button); }); }); if (!picker || !panel) { return; } // Aurora's glass card uses backdrop-filter, and the main content animates // with transform. Both can create a containing block for fixed descendants, // so keep the floating picker at body level where viewport coordinates mean // what they say. document.body.appendChild(picker); picker.addEventListener("click", (event) => { if (event.target === picker) { closePicker(); } }); panel.addEventListener("click", async (event) => { const emoji = event.target.closest("[data-emoji-id]"); if (!emoji || !activeButton || !activeCard) { return; } await submitReaction(emoji); }); tabs.forEach((tab) => { tab.addEventListener("click", () => { setActiveTab(tab.dataset.reactionTab); }); }); if (search) { search.addEventListener("input", () => filterEmoji(search.value)); } if (closeButton) { closeButton.addEventListener("click", closePicker); } document.addEventListener("click", (event) => { if (picker.hidden) { return; } if (panel.contains(event.target) || event.target.closest("[data-reaction-button]")) { return; } closePicker(); }); document.addEventListener("keydown", (event) => { if (event.key === "Escape") { closePicker(); } }); window.addEventListener("resize", () => { if (activeButton && !picker.hidden) { positionPicker(activeButton); } }); function openPicker(button) { activeButton = button; activeCard = button.closest("[data-reaction-card]"); picker.hidden = false; picker.classList.add("is-open"); if (search) { search.value = ""; filterEmoji(""); } positionPicker(button); } function closePicker() { picker.hidden = true; picker.classList.remove("is-open", "is-mobile"); document.documentElement.classList.remove("reaction-picker-open"); picker.style.left = ""; picker.style.top = ""; activeButton = null; activeCard = null; } function positionPicker(button) { if (isMobilePicker()) { picker.classList.add("is-mobile"); document.documentElement.classList.add("reaction-picker-open"); picker.style.left = "0px"; picker.style.top = "0px"; return; } picker.classList.remove("is-mobile"); document.documentElement.classList.remove("reaction-picker-open"); picker.style.left = "0px"; picker.style.top = "0px"; const buttonRect = button.getBoundingClientRect(); const pickerRect = panel.getBoundingClientRect(); const margin = 10; const preferredLeft = buttonRect.left + (buttonRect.width / 2) - (pickerRect.width / 2); const preferredTop = buttonRect.bottom + 8; const left = Math.min(Math.max(margin, preferredLeft), window.innerWidth - pickerRect.width - margin); const top = Math.min(Math.max(margin, preferredTop), window.innerHeight - pickerRect.height - margin); picker.style.left = `${left}px`; picker.style.top = `${top}px`; } function isMobilePicker() { return window.matchMedia("(max-width: 820px), (pointer: coarse)").matches; } function setActiveTab(tabID) { tabs.forEach((tab) => { const active = tab.dataset.reactionTab === tabID; tab.classList.toggle("is-active", active); tab.setAttribute("aria-selected", active ? "true" : "false"); }); panels.forEach((item) => { item.classList.toggle("is-active", item.dataset.reactionPanel === tabID); }); } function filterEmoji(value) { const query = value.trim().toLowerCase(); picker.querySelectorAll("[data-emoji-id]").forEach((button) => { const haystack = `${button.dataset.emojiId} ${button.dataset.emojiLabel}`.toLowerCase(); button.hidden = query !== "" && !haystack.includes(query); }); } async function submitReaction(emoji) { const body = new URLSearchParams(); body.set("emoji_id", emoji.dataset.emojiId); activeButton.disabled = true; const response = await fetch(activeButton.dataset.reactUrl, { method: "POST", headers: { "Accept": "application/json", "Content-Type": "application/x-www-form-urlencoded", }, body, }); if (!response.ok) { activeButton.disabled = false; closePicker(); return; } const payload = await response.json(); renderReactions(activeCard, payload.reactions || []); activeButton.remove(); closePicker(); } function renderReactions(card, reactions) { const list = card.querySelector("[data-reaction-list]"); if (!list) { return; } list.replaceChildren(); reactions.forEach((reaction) => { const pill = document.createElement("span"); pill.className = "reaction-pill"; pill.title = reaction.label || reaction.emojiId; const image = document.createElement("img"); image.src = reaction.url; image.alt = reaction.label || reaction.emojiId; image.loading = "lazy"; const count = document.createElement("span"); count.textContent = reaction.count; pill.append(image, count); list.append(pill); }); } })();