(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 existingSection = picker ? picker.querySelector("[data-reaction-existing]") : null; const existingList = picker ? picker.querySelector("[data-reaction-existing-list]") : null; const readonlyNote = picker ? picker.querySelector("[data-reaction-readonly]") : null; const chooserElements = picker ? Array.from(picker.querySelectorAll(".reaction-picker-tabs, .reaction-search, .reaction-grid-wrap")) : []; 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); }); }); document.addEventListener("click", (event) => { const pill = event.target.closest("[data-reaction-pill]"); if (pill) { event.preventDefault(); event.stopPropagation(); const card = pill.closest("[data-reaction-card]") || activeCard; if (!card) { return; } if (card.dataset.reacted === "true") { openPickerForCard(card, pill); return; } submitReactionForCard(card, pill.dataset.reactionEmojiId); return; } const more = event.target.closest("[data-reaction-more]"); if (!more) { return; } event.preventDefault(); event.stopPropagation(); const card = more.closest("[data-reaction-card]"); if (card) { openPickerForCard(card, more); } }); 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 || !activeCard || activeCard.dataset.reacted === "true") { return; } await submitReactionForCard(activeCard, emoji.dataset.emojiId); }); 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; } if (event.target.closest("[data-reaction-more]") || event.target.closest("[data-reaction-pill]")) { return; } closePicker(); }); document.addEventListener("keydown", (event) => { if (event.key === "Escape") { closePicker(); } }); window.addEventListener("resize", () => { if (activeButton && !picker.hidden) { positionPicker(activeButton); } }); function openPicker(button) { openPickerForCard(button.closest("[data-reaction-card]"), button); } function openPickerForCard(card, trigger) { if (!card) { return; } activeButton = trigger || card.querySelector("[data-reaction-button]"); activeCard = card; populateExistingReactions(card); setPickerReadonly(card.dataset.reacted === "true"); picker.hidden = false; picker.classList.add("is-open"); if (search) { search.value = ""; filterEmoji(""); } positionPicker(activeButton || card); } 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 = ""; setPickerReadonly(false); 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 submitReactionForCard(card, emojiID) { if (!card || !emojiID || card.dataset.reacted === "true") { return; } const body = new URLSearchParams(); body.set("emoji_id", emojiID); const reactButton = card.querySelector("[data-reaction-button]"); if (reactButton) { reactButton.disabled = true; } const response = await fetch(card.dataset.reactUrl, { method: "POST", headers: { "Accept": "application/json", "Content-Type": "application/x-www-form-urlencoded", }, body, }); if (!response.ok) { if (reactButton) { reactButton.disabled = false; } closePicker(); return; } const payload = await response.json(); renderReactions(card, payload.reactions || []); card.dataset.reacted = "true"; if (reactButton) { reactButton.remove(); } closePicker(); } function renderReactions(card, reactions) { const list = card.querySelector("[data-reaction-list]"); if (!list) { return; } list.replaceChildren(); reactions.forEach((reaction) => { const pill = buildReactionPill(reaction); if (!reaction.visible) { pill.classList.add("is-hidden-summary"); } list.append(pill); }); const hiddenCount = reactions.length > 2 ? reactions.length - 2 : 0; if (hiddenCount > 0) { const more = document.createElement("button"); more.className = "reaction-more"; more.type = "button"; more.dataset.reactionMore = ""; more.textContent = `+${hiddenCount}`; more.setAttribute("aria-label", `Show ${hiddenCount} more reactions`); list.append(more); } } function buildReactionPill(reaction) { const pill = document.createElement("button"); pill.className = "reaction-pill"; pill.type = "button"; pill.title = reaction.label || reaction.emojiId; pill.dataset.reactionPill = ""; pill.dataset.reactionEmojiId = reaction.emojiId; pill.dataset.reactionLabel = reaction.label || reaction.emojiId; pill.dataset.reactionUrl = reaction.url; pill.dataset.reactionCount = reaction.count; pill.setAttribute("aria-label", `React with ${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); return pill; } function populateExistingReactions(card) { if (!existingSection || !existingList) { return; } existingList.replaceChildren(); card.querySelectorAll("[data-reaction-pill]").forEach((pill) => { const clone = pill.cloneNode(true); clone.classList.remove("is-hidden-summary"); existingList.append(clone); }); existingSection.hidden = existingList.children.length === 0; } function setPickerReadonly(readonly) { picker.classList.toggle("is-readonly", readonly); chooserElements.forEach((element) => { element.hidden = readonly; }); if (readonlyNote) { readonlyNote.hidden = !readonly; } } })();