(async () => { // Helper to get dynamic context const getContext = () => { const idLink = document.querySelector("a.id-link"); if (!idLink) return null; return { postid: +idLink.innerText, poster: document.querySelector("a#a_username")?.innerText, tags: [...document.querySelectorAll("#tags > .badge")].map(t => t.innerText.slice(0, -2)) }; }; const queryapi = async (url, data, method = 'GET') => { let req; if (method == 'POST') { req = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data) }); } else { let s = []; for (const [key, val] of Object.entries(data)) s.push(encodeURIComponent(key) + "=" + encodeURIComponent(val)); req = await fetch(url + '?' + s.join('&')); } return await req.json(); }; const get = async (url, data) => queryapi(url, data, 'GET'); const post = async (url, data) => queryapi(url, data, 'POST'); const renderTags = _tags => { [...document.querySelectorAll("#tags > .badge")].forEach(tag => tag.parentElement.removeChild(tag)); _tags.reverse().forEach(tag => { const a = document.createElement("a"); a.href = `/tag/${tag.normalized}`; a.style = "color: inherit !important"; a.innerHTML = tag.tag; // Admin specific: edit event // Note: delegation handles this now if we set it up, OR we can attach here since elements are new. // But delegation is cleaner if possible. However, editTagEvent relies on 'e.target'. const span = document.createElement("span"); span.classList.add("badge", "mr-2"); span.setAttribute('tooltip', tag.user); tag.badge.split(" ").forEach(b => span.classList.add(b)); const delbutton = document.createElement("a"); delbutton.innerHTML = " ×"; delbutton.href = "#"; // Class for delegation delbutton.classList.add("admin-deltag"); span.insertAdjacentElement("beforeend", a); span.innerHTML += ' '; span.insertAdjacentElement("beforeend", delbutton); document.querySelector("#tags").insertAdjacentElement("afterbegin", span); }); }; const deleteEvent = async e => { e.preventDefault(); const ctx = getContext(); if (!ctx) return; const { postid } = ctx; if (!confirm("Do you really want to delete this tag?")) return; const tagname = e.target.parentElement.querySelector('a:first-child').innerText; const res = await (await fetch("/api/v2/admin/" + postid + "/tags/" + encodeURIComponent(tagname), { method: 'DELETE' })).json(); if (!res.success) return alert("uff"); renderTags(res.tags); }; const addtagClick = (e) => { if (e) e.preventDefault(); const ctx = getContext(); if (!ctx) return; const { postid, tags } = ctx; const insert = document.querySelector("a#a_addtag"); if (insert.previousElementSibling && insert.previousElementSibling.querySelector('input')) { insert.previousElementSibling.querySelector('input').focus(); return; } const span = document.createElement("span"); span.classList.add("badge", "badge-light", "mr-2"); const input = document.createElement("input"); input.size = "10"; input.value = ""; input.setAttribute("list", "testlist"); input.setAttribute("autoComplete", "off"); span.insertAdjacentElement("afterbegin", input); insert.insertAdjacentElement("beforebegin", span); input.focus(); let tt = null; let lastInput = ''; const testList = document.querySelector('#testlist'); input.addEventListener("keyup", async e => { if (e.key === "Enter") { const tmptag = input.value?.trim(); // We should re-check tags from DOM? Or trust captured tags? // Captured 'tags' is safe for immediate check. if (tags.includes(tmptag)) return alert("tag already exists"); const res = await post("/api/v2/admin/" + postid + "/tags", { tagname: tmptag }); if (!res.success) { alert(res.msg); return false; } renderTags(res.tags); span.parentElement.removeChild(span); testList.innerText = ""; addtagClick(); } else if (e.key === "Escape") { span.parentElement.removeChild(span); testList.innerText = ""; } else { if (tt != null) clearTimeout(tt); tt = setTimeout(async () => { tt = null; const tmptag = input.value?.trim(); if (tmptag == lastInput || tmptag.length <= 1) return false; testList.innerText = ""; lastInput = tmptag; const res = await get('/api/v2/admin/tags/suggest', { q: tmptag }); for (const entry of res.suggestions) { const option = document.createElement('option'); option.value = entry.tag; if (!/fox/.test(navigator.userAgent)) option.innerText = `tagged ${entry.tagged} times (score: ${entry.score.toFixed(2)})`; testList.insertAdjacentElement('beforeEnd', option); }; }, 500); } return true; }); input.addEventListener("focusout", ie => { if (input.value.length === 0) input.parentElement.parentElement.removeChild(input.parentElement); }); }; const toggleEvent = async (e) => { if (e) e.preventDefault(); const ctx = getContext(); if (!ctx) return; const { postid } = ctx; const res = await (await fetch('/api/v2/admin/' + encodeURIComponent(postid) + '/tags/toggle', { method: 'PUT' })).json(); renderTags(res.tags); }; const deleteButtonEvent = async e => { if (e) e.preventDefault(); const ctx = getContext(); if (!ctx) return; const { postid, poster } = ctx; if (!confirm(`Reason for deleting f0ckpost ${postid} by ${poster} (Weihnachten™)`)) return; const res = await post("/api/v2/admin/deletepost", { postid: postid }); if (!res.success) { alert(res.msg); } }; const toggleFavEvent = async (e) => { const ctx = getContext(); if (!ctx) return; const { postid } = ctx; const res = await post('/api/v2/admin/togglefav', { postid: postid }); if (res.success) { const fav = document.querySelector("svg#a_favo > use").href; fav.baseVal = '/s/img/iconset.svg#heart_' + (fav.baseVal.match(/heart_(regular|solid)$/)[1] == "solid" ? "regular" : "solid"); const favcontainer = document.querySelector('span#favs'); favcontainer.innerHTML = ""; favcontainer.hidden = !(favcontainer.hidden || res.favs.length > 0); res.favs.forEach(f => { const a = document.createElement('a'); a.href = `/user/${f.user}/favs`; a.setAttribute('tooltip', f.user); a.setAttribute('flow', 'up'); const img = document.createElement('img'); img.src = `/t/${f.avatar}.webp`; img.style.height = "32px"; img.style.width = "32px"; a.insertAdjacentElement('beforeend', img); favcontainer.insertAdjacentElement('beforeend', a); favcontainer.innerHTML += " "; }); } }; let tmptt = null; const editTagEvent = async e => { e.preventDefault(); if (e.detail === 2) { // Double click clearTimeout(tmptt); const old = e.target; const parent = e.target.parentElement; const oldtag = e.target.innerText; const textfield = document.createElement('input'); textfield.value = e.target.innerText; textfield.size = 10; parent.insertAdjacentElement('afterbegin', textfield); textfield.focus(); parent.removeChild(e.target); // Hide delete button while editing const delBtn = parent.querySelector('a:last-child'); if (delBtn) delBtn.style.display = 'none'; textfield.addEventListener("keyup", async e => { if (e.key === 'Enter') { parent.removeChild(textfield); let res = await fetch('/api/v2/admin/tags/' + encodeURIComponent(oldtag), { method: 'PUT', headers: { "Content-Type": "application/json" }, body: JSON.stringify({ newtag: textfield.value }) }); const status = res.status; res = await res.json(); switch (status) { case 200: case 201: parent.insertAdjacentElement('afterbegin', old); if (delBtn) delBtn.style.display = ''; old.href = `/tag/${res.tag}`; old.innerText = res.tag.trim(); break; default: console.log(res); break; } } else if (e.key === 'Escape') { parent.removeChild(textfield); parent.insertAdjacentElement('afterbegin', old); if (delBtn) delBtn.style.display = ''; } }); } else tmptt = setTimeout(() => location.href = e.target.href, 250); return false; }; // Event Delegation document.addEventListener("click", e => { if (e.target.matches("a#a_addtag")) { addtagClick(e); } else if (e.target.matches("a#a_toggle")) { toggleEvent(e); } else if (e.target.closest("svg#a_favo")) { toggleFavEvent(e); } else if (e.target.closest("svg#a_delete")) { deleteButtonEvent(e); } else if (e.target.matches("#tags > .badge > a:first-child")) { editTagEvent(e); } else if (e.target.innerText === " \u00d7" && e.target.closest(".badge")) { // check text " x" or similar for delete? // Original was " ×" which is Ă— (\u00d7). // Logic in deleteEvent expects match. // Let's rely on class or structure. // In renderTags I added class 'admin-deltag'. // Existing tags in HTML might NOT have this class unless rendered by JS? // But existing tags are just HTML. We should match structure. // selector: "#tags > .badge > a:last-child" if (e.target.matches("#tags > .badge > a:last-child")) { deleteEvent(e); } } }); document.addEventListener("keyup", e => { if (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA") return; const ctx = getContext(); if (!ctx) return; if (e.key === "p") toggleEvent(); else if (e.key === "i") addtagClick(); else if (e.key === "x") deleteButtonEvent(); else if (e.key === "f") toggleFavEvent(); }); // Settings page if (document.location.pathname === '/settings') { const saveAvatar = async e => { e.preventDefault(); const avatar = +document.querySelector('input[name="i_avatar"]').value; let res = await fetch('/api/v2/settings/setAvatar', { method: 'PUT', headers: { "Content-Type": "application/json" }, body: JSON.stringify({ avatar }) }); const code = res.status; res = await res.json(); if (code === 200) { document.querySelector('#img_avatar').src = `/t/${avatar}.webp`; document.querySelector('img.avatar').src = `/t/${avatar}.webp`; } }; const sAvatar = document.querySelector('input#s_avatar'); if (sAvatar) sAvatar.addEventListener('click', saveAvatar); const iAvatar = document.querySelector('input[name="i_avatar"]'); if (iAvatar) iAvatar.addEventListener('keyup', async e => { if (e.key === 'Enter') await saveAvatar(e); }); } })();