/* * Annotator search-and-claim sidebar. * * Self-gating: on init it probes GET /api/search with no query. The * endpoint returns 400 ("q required") when annotator search-and-claim * is enabled and the user is authenticated, and 403/401/503 otherwise. * The toggle is only revealed on 400, so the feature stays invisible * unless search.annotator_claim is on. */ (function () { "use strict"; var API = "/api/search"; function el(id) { return document.getElementById(id); } function esc(s) { var d = document.createElement("div"); d.textContent = s == null ? "" : String(s); return d.innerHTML; } // FTS5 marks the matched term with the STX/ETX sentinels (see // potato/search/fts5.py). Escape the whole snippet FIRST so any real // markup in the text is neutralised, THEN swap our own sentinels for a // highlight — keeping it XSS-safe while showing where it matched. var SNIP_OPEN = String.fromCharCode(2); // STX, matches fts5.py var SNIP_CLOSE = String.fromCharCode(3); // ETX function highlightSnippet(s) { return esc(s) .split(SNIP_OPEN).join('') .split(SNIP_CLOSE).join(""); } function setStatus(html) { var box = el("search-results"); if (box) box.innerHTML = html; } function renderResults(data) { var box = el("search-results"); if (!box) return; var hits = (data && data.results) || []; if (!hits.length) { box.innerHTML = '
No matches. Try a ' + "different word or phrase.
"; return; } box.innerHTML = hits.map(function (h) { return '
' + '
' + highlightSnippet(h.snippet) + "
" + '
' + '' + esc(h.instance_id) + "" + '' + "
"; }).join(""); box.querySelectorAll(".search-claim").forEach(function (b) { b.addEventListener("click", function () { claim(b); }); }); } function claim(btn) { var id = btn.getAttribute("data-id"); btn.disabled = true; var prev = btn.textContent; btn.textContent = "Claiming…"; fetch(API + "/claim", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ instance_id: id }), }).then(function (r) { if (!r.ok) { failClaim(btn, prev); return; } var span = document.createElement("span"); span.className = "search-claimed"; span.textContent = "✓ In your queue"; btn.replaceWith(span); }).catch(function () { failClaim(btn, prev); }); } function failClaim(btn, label) { var note = document.createElement("span"); note.className = "search-claim-error"; note.textContent = "Couldn't claim — retry"; note.setAttribute("role", "button"); note.setAttribute("tabindex", "0"); function retry() { var b = document.createElement("button"); b.type = "button"; b.className = "search-claim"; b.setAttribute("data-id", btn.getAttribute("data-id")); b.textContent = label; b.addEventListener("click", function () { claim(b); }); note.replaceWith(b); b.focus(); } note.addEventListener("click", retry); note.addEventListener("keydown", function (e) { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); retry(); } }); btn.replaceWith(note); } function doSearch() { var input = el("search-q"); var go = el("search-go"); var q = (input && input.value || "").trim(); if (!q) { if (input) input.focus(); return; } if (go) go.disabled = true; setStatus('
Searching…
'); fetch(API + "?q=" + encodeURIComponent(q) + "&limit=50") .then(function (r) { return r.ok ? r.json() : { results: [] }; }) .then(renderResults) .catch(function () { renderResults({ results: [] }); }) .finally(function () { if (go) go.disabled = false; }); } function closePanel() { var panel = el("search-panel"); var toggle = el("search-panel-toggle"); if (panel) panel.hidden = true; if (toggle) { toggle.hidden = false; toggle.focus(); } } function wire() { var toggle = el("search-panel-toggle"); var panel = el("search-panel"); var close = el("search-panel-close"); if (toggle && panel) { toggle.addEventListener("click", function () { panel.hidden = false; toggle.hidden = true; var q = el("search-q"); if (q) q.focus(); }); } if (close) close.addEventListener("click", closePanel); if (panel) { panel.addEventListener("keydown", function (e) { if (e.key === "Escape") { e.preventDefault(); closePanel(); } }); } var go = el("search-go"); if (go) go.addEventListener("click", doSearch); var input = el("search-q"); if (input) { input.addEventListener("keydown", function (e) { if (e.key === "Enter") { e.preventDefault(); doSearch(); } }); } } function init() { if (!window.config || !window.config.is_annotation_page) return; if (!el("search-panel")) return; // Enable-probe: 400 => enabled+authed; anything else => stay hidden. fetch(API).then(function (r) { if (r.status === 400) { var t = el("search-panel-toggle"); if (t) t.hidden = false; wire(); } }).catch(function () { /* leave hidden */ }); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", init); } else { init(); } })();