Update web/templates/entry_view.html

This commit is contained in:
Joshua Laymon 2025-08-23 16:07:23 +00:00
parent fd12a809de
commit bfd78b2d1b

View File

@ -467,11 +467,11 @@ function showToast(message, duration = 3000) {
} }
</script> </script>
<!-- Highlighter: robust triggers (DOMContentLoaded + pageshow + readyState) --> <!-- Highlighter: robust (readyState + DOMContentLoaded + pageshow) + wait for content + run-once guard -->
<script> <script>
(function(){ (function(){
// EXACT field -> selector mapping for your template IDs // exact field -> selector mapping for your template IDs
const fieldToSelector = { const FIELD_TO_SELECTOR = {
subject: "#subject-list", subject: "#subject-list",
illustration: "#illustration-text", illustration: "#illustration-text",
application: "#application-text", application: "#application-text",
@ -481,31 +481,47 @@ function showToast(message, duration = 3000) {
talk_number: "#talk_title-text", talk_number: "#talk_title-text",
}; };
// Kickoff with multiple safety nets so it also runs after bfcache restores // prevent double-runs on the same page
if (window.__HL_DONE__) return;
window.__HL_DONE__ = false;
// Kick off under ALL the common scenarios
if (document.readyState === "complete" || document.readyState === "interactive") { if (document.readyState === "complete" || document.readyState === "interactive") {
run(); scheduleRun();
} else { } else {
document.addEventListener("DOMContentLoaded", run, { once: true }); document.addEventListener("DOMContentLoaded", scheduleRun, { once: true });
}
window.addEventListener("pageshow", scheduleRun);
function scheduleRun(){
// Run now, and also a couple of short retries in case the DOM fills a tad later.
run();
setTimeout(run, 50);
setTimeout(run, 150);
setTimeout(run, 400);
} }
// pageshow fires on bfcache restores and some soft navigations
window.addEventListener("pageshow", run);
async function run() { async function run() {
// Respect per-user toggle if (window.__HL_DONE__) return;
// 1) respect per-user toggle
let enabled = true; let enabled = true;
try { try {
const res = await fetch("/api/get-prefs/"); const res = await fetch("/api/get-prefs/", { cache: "no-store", credentials: "same-origin" });
if (res.ok) {
const prefs = await res.json(); const prefs = await res.json();
if (prefs && typeof prefs.highlight_search_hits !== "undefined") { if (prefs && typeof prefs.highlight_search_hits !== "undefined") {
enabled = !!prefs.highlight_search_hits; enabled = !!prefs.highlight_search_hits;
} }
} catch (_) {} }
if (!enabled) return; } catch (_) { /* default true */ }
if (!enabled) { window.__HL_DONE__ = true; return; }
// Read last search (prefer JS-literal fallback, then JSON blob) // 2) read last search (prefer JS-literal fallback you already added)
let q = (window.__lastSearchQ || "").trim(); let q = (window.__lastSearchQ || "").trim();
let fields = Array.isArray(window.__lastSearchFields) ? window.__lastSearchFields : []; let fields = Array.isArray(window.__lastSearchFields) ? window.__lastSearchFields : [];
// fall back to the JSON <script> only if needed
if ((!q || !fields.length)) { if ((!q || !fields.length)) {
const dataEl = document.getElementById("last-search-data"); const dataEl = document.getElementById("last-search-data");
if (dataEl) { if (dataEl) {
@ -516,23 +532,65 @@ function showToast(message, duration = 3000) {
} catch (_) {} } catch (_) {}
} }
} }
if (!q || !fields.length) return; if (!q || !fields.length) { window.__HL_DONE__ = true; return; }
// Tokenize like your search (keep quoted phrases, strip * and ?) // 3) determine which containers to target based on searched fields
const selectors = fields
.map(f => FIELD_TO_SELECTOR[f])
.filter(Boolean);
if (!selectors.length) { window.__HL_DONE__ = true; return; }
// 4) wait until all target containers exist & have text (covers late paint / soft navs)
const containers = await waitForContainers(selectors, { timeout: 1000 }); // up to ~1s
if (!containers || !containers.length) { window.__HL_DONE__ = true; return; }
// 5) tokenize like your search: keep quoted phrases; strip wildcards
const tokens = tokenize(q).map(t => t.replaceAll("*","").replaceAll("?","")).filter(Boolean); const tokens = tokenize(q).map(t => t.replaceAll("*","").replaceAll("?","")).filter(Boolean);
if (!tokens.length) return; if (!tokens.length) { window.__HL_DONE__ = true; return; }
// Highlight only within fields that were searched // 6) highlight
for (const f of fields) { for (const el of containers) {
const sel = fieldToSelector[f]; for (const tok of tokens) {
if (!sel) continue; highlightAll(el, tok);
const container = document.querySelector(sel);
if (!container) continue;
for (const tok of tokens) highlightAll(container, tok);
} }
} }
window.__HL_DONE__ = true; // mark done so refreshes dont re-wrap
}
// ---- helpers ---- // ---- helpers ----
// Wait until all selectors exist and have any meaningful text
function waitForContainers(selectors, { timeout = 1000 } = {}) {
const start = performance.now();
return new Promise((resolve) => {
const tryNow = () => {
const els = selectors
.map(sel => document.querySelector(sel))
.filter(Boolean);
if (els.length === selectors.length && els.some(hasAnyText)) {
return resolve(els);
}
if (performance.now() - start > timeout) {
return resolve(els); // return what we have (may be empty)
}
requestAnimationFrame(tryNow);
};
tryNow();
});
}
function hasAnyText(node) {
// Consider it "ready" if there is at least one visible text node (ignoring whitespace)
const walker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT, null, false);
while (walker.nextNode()) {
if ((walker.currentNode.nodeValue || "").trim()) return true;
}
return false;
}
function tokenize(s) { function tokenize(s) {
const out = []; const out = [];
let i = 0, buf = "", inQ = false; let i = 0, buf = "", inQ = false;
@ -558,9 +616,16 @@ function showToast(message, duration = 3000) {
function highlightAll(root, needle) { function highlightAll(root, needle) {
if (!needle) return; if (!needle) return;
const re = new RegExp(escapeRegExp(needle), "gi"); const re = new RegExp(escapeRegExp(needle), "gi");
walkTextNodes(root, (textNode) => {
// Collect first; TreeWalker snapshot avoids modifying while walking
const nodes = [];
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null, false);
while (walker.nextNode()) nodes.push(walker.currentNode);
for (const textNode of nodes) {
const text = textNode.nodeValue; const text = textNode.nodeValue;
if (!re.test(text)) return; if (!text || !re.test(text)) continue;
const frag = document.createDocumentFragment(); const frag = document.createDocumentFragment();
let lastIndex = 0; let lastIndex = 0;
text.replace(re, (m, idx) => { text.replace(re, (m, idx) => {
@ -573,18 +638,13 @@ function showToast(message, duration = 3000) {
return m; return m;
}); });
if (lastIndex < text.length) frag.appendChild(document.createTextNode(text.slice(lastIndex))); if (lastIndex < text.length) frag.appendChild(document.createTextNode(text.slice(lastIndex)));
textNode.parentNode.replaceChild(frag, textNode);
});
}
function walkTextNodes(node, cb) { if (textNode.parentNode) textNode.parentNode.replaceChild(frag, textNode);
const walker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT, null, false); }
const nodes = [];
while (walker.nextNode()) nodes.push(walker.currentNode);
nodes.forEach(cb);
} }
})(); })();
</script> </script>
{% endblock %} {% endblock %}