// interests.js — wires the tag-combobox on the profile edit form. // // Server-rendered structure (see profile_edit.templ): // //
//
// // coffee // // // … //
// // //
// // JS owns the in-memory list, re-renders chips, and keeps the hidden input // in sync (comma-joined) so the existing server-side parseInterests handler // keeps working unchanged. (function () { "use strict"; function init(root) { var chipsEl = root.querySelector("[data-tag-chips]"); var input = root.querySelector("[data-tag-input]"); var hidden = root.querySelector("[data-tag-hidden]"); if (!chipsEl || !input || !hidden) return; var maxTags = parseInt(root.getAttribute("data-tag-max"), 10) || 30; var maxLen = parseInt(root.getAttribute("data-tag-maxlen"), 10) || 64; // Seed from server-rendered chips so a JS-failed initial render still // matches what the user sees. var tags = []; var seen = Object.create(null); Array.prototype.forEach.call( chipsEl.querySelectorAll("[data-tag]"), function (el) { var v = (el.getAttribute("data-tag") || "").trim(); if (!v) return; var key = v.toLowerCase(); if (seen[key]) return; seen[key] = true; tags.push(v); } ); function render() { // Rebuild chips from the array — cheap, list is tiny. var frag = document.createDocumentFragment(); tags.forEach(function (t) { var chip = document.createElement("span"); chip.className = "tag-chip"; chip.setAttribute("data-tag", t); var label = document.createElement("span"); label.className = "tag-chip-label"; label.textContent = t; chip.appendChild(label); var btn = document.createElement("button"); btn.type = "button"; btn.className = "tag-chip-remove"; btn.setAttribute("aria-label", "remove " + t); btn.setAttribute("data-tag-remove", ""); btn.textContent = "\u00d7"; // × chip.appendChild(btn); frag.appendChild(chip); }); chipsEl.replaceChildren(frag); hidden.value = tags.join(", "); } function addTag(raw) { if (typeof raw !== "string") return false; var v = raw.trim(); if (!v) return false; // Reject embedded commas — splitting would be unexpected here. // The user can paste multiple values; we handle that in onInput. if (v.indexOf(",") !== -1) return false; if (v.length > maxLen) v = v.slice(0, maxLen); var key = v.toLowerCase(); if (tags.some(function (t) { return t.toLowerCase() === key; })) { return false; } if (tags.length >= maxTags) return false; tags.push(v); return true; } function commitFromInput() { var raw = input.value; if (!raw) return; // Allow paste-style "foo, bar, baz". var parts = raw.split(","); var any = false; parts.forEach(function (p) { if (addTag(p)) any = true; }); if (any) render(); input.value = ""; } input.addEventListener("keydown", function (e) { if (e.key === "Enter" || e.key === ",") { e.preventDefault(); commitFromInput(); return; } if (e.key === "Backspace" && input.value === "" && tags.length > 0) { // Pop the last chip for fast correction. tags.pop(); render(); } }); // The `change` event fires when the user picks a value from the // dropdown (Safari/Firefox/Chrome). Treat it as a commit. input.addEventListener("change", function () { commitFromInput(); }); input.addEventListener("blur", function () { commitFromInput(); }); chipsEl.addEventListener("click", function (e) { var btn = e.target.closest("[data-tag-remove]"); if (!btn) return; var chip = btn.closest(".tag-chip"); if (!chip) return; var v = chip.getAttribute("data-tag"); tags = tags.filter(function (t) { return t !== v; }); render(); input.focus(); }); // If the user clicks anywhere in the combobox shell, focus the input — // makes the whole field feel like one big input. root.addEventListener("click", function (e) { if (e.target === root || e.target === chipsEl) { input.focus(); } }); // Form submit: flush any pending text first so a half-typed tag isn't // silently dropped. var form = root.closest("form"); if (form) { form.addEventListener("submit", function () { commitFromInput(); }); } // Initial sync (hidden input may have been server-rendered from a // stale list — repaint to be safe). render(); } function boot() { document.querySelectorAll("[data-tag-combobox]").forEach(init); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", boot); } else { boot(); } })();