Update web/templates/entry_view.html
This commit is contained in:
parent
bfd78b2d1b
commit
41da66b9b2
@ -467,11 +467,11 @@ function showToast(message, duration = 3000) {
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Highlighter: robust (readyState + DOMContentLoaded + pageshow) + wait for content + run-once guard -->
|
||||
<!-- Highlighter: robust triggers (DOMContentLoaded + pageshow + readyState) -->
|
||||
<script>
|
||||
(function(){
|
||||
// exact field -> selector mapping for your template IDs
|
||||
const FIELD_TO_SELECTOR = {
|
||||
// EXACT field -> selector mapping for your template IDs
|
||||
const fieldToSelector = {
|
||||
subject: "#subject-list",
|
||||
illustration: "#illustration-text",
|
||||
application: "#application-text",
|
||||
@ -481,47 +481,31 @@ function showToast(message, duration = 3000) {
|
||||
talk_number: "#talk_title-text",
|
||||
};
|
||||
|
||||
// prevent double-runs on the same page
|
||||
if (window.__HL_DONE__) return;
|
||||
window.__HL_DONE__ = false;
|
||||
|
||||
// Kick off under ALL the common scenarios
|
||||
// Kickoff with multiple safety nets so it also runs after bfcache restores
|
||||
if (document.readyState === "complete" || document.readyState === "interactive") {
|
||||
scheduleRun();
|
||||
} else {
|
||||
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);
|
||||
} else {
|
||||
document.addEventListener("DOMContentLoaded", run, { once: true });
|
||||
}
|
||||
// pageshow fires on bfcache restores and some soft navigations
|
||||
window.addEventListener("pageshow", run);
|
||||
|
||||
async function run() {
|
||||
if (window.__HL_DONE__) return;
|
||||
|
||||
// 1) respect per-user toggle
|
||||
// Respect per-user toggle
|
||||
let enabled = true;
|
||||
try {
|
||||
const res = await fetch("/api/get-prefs/", { cache: "no-store", credentials: "same-origin" });
|
||||
if (res.ok) {
|
||||
const prefs = await res.json();
|
||||
if (prefs && typeof prefs.highlight_search_hits !== "undefined") {
|
||||
enabled = !!prefs.highlight_search_hits;
|
||||
}
|
||||
const res = await fetch("/api/get-prefs/");
|
||||
const prefs = await res.json();
|
||||
if (prefs && typeof prefs.highlight_search_hits !== "undefined") {
|
||||
enabled = !!prefs.highlight_search_hits;
|
||||
}
|
||||
} catch (_) { /* default true */ }
|
||||
if (!enabled) { window.__HL_DONE__ = true; return; }
|
||||
} catch (_) {}
|
||||
if (!enabled) return;
|
||||
|
||||
// 2) read last search (prefer JS-literal fallback you already added)
|
||||
// Read last search (prefer JS-literal fallback, then JSON blob)
|
||||
let q = (window.__lastSearchQ || "").trim();
|
||||
let fields = Array.isArray(window.__lastSearchFields) ? window.__lastSearchFields : [];
|
||||
|
||||
// fall back to the JSON <script> only if needed
|
||||
if ((!q || !fields.length)) {
|
||||
const dataEl = document.getElementById("last-search-data");
|
||||
if (dataEl) {
|
||||
@ -532,65 +516,23 @@ function showToast(message, duration = 3000) {
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
if (!q || !fields.length) { window.__HL_DONE__ = true; return; }
|
||||
if (!q || !fields.length) return;
|
||||
|
||||
// 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
|
||||
// Tokenize like your search (keep quoted phrases, strip * and ?)
|
||||
const tokens = tokenize(q).map(t => t.replaceAll("*","").replaceAll("?","")).filter(Boolean);
|
||||
if (!tokens.length) { window.__HL_DONE__ = true; return; }
|
||||
if (!tokens.length) return;
|
||||
|
||||
// 6) highlight
|
||||
for (const el of containers) {
|
||||
for (const tok of tokens) {
|
||||
highlightAll(el, tok);
|
||||
}
|
||||
// Highlight only within fields that were searched
|
||||
for (const f of fields) {
|
||||
const sel = fieldToSelector[f];
|
||||
if (!sel) continue;
|
||||
const container = document.querySelector(sel);
|
||||
if (!container) continue;
|
||||
for (const tok of tokens) highlightAll(container, tok);
|
||||
}
|
||||
|
||||
window.__HL_DONE__ = true; // mark done so refreshes don’t re-wrap
|
||||
}
|
||||
|
||||
// ---- 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) {
|
||||
const out = [];
|
||||
let i = 0, buf = "", inQ = false;
|
||||
@ -616,16 +558,9 @@ function showToast(message, duration = 3000) {
|
||||
function highlightAll(root, needle) {
|
||||
if (!needle) return;
|
||||
const re = new RegExp(escapeRegExp(needle), "gi");
|
||||
|
||||
// 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) {
|
||||
walkTextNodes(root, (textNode) => {
|
||||
const text = textNode.nodeValue;
|
||||
if (!text || !re.test(text)) continue;
|
||||
|
||||
if (!re.test(text)) return;
|
||||
const frag = document.createDocumentFragment();
|
||||
let lastIndex = 0;
|
||||
text.replace(re, (m, idx) => {
|
||||
@ -638,13 +573,20 @@ function showToast(message, duration = 3000) {
|
||||
return m;
|
||||
});
|
||||
if (lastIndex < text.length) frag.appendChild(document.createTextNode(text.slice(lastIndex)));
|
||||
textNode.parentNode.replaceChild(frag, textNode);
|
||||
});
|
||||
}
|
||||
|
||||
if (textNode.parentNode) textNode.parentNode.replaceChild(frag, textNode);
|
||||
}
|
||||
function walkTextNodes(node, cb) {
|
||||
const walker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT, null, false);
|
||||
const nodes = [];
|
||||
while (walker.nextNode()) nodes.push(walker.currentNode);
|
||||
nodes.forEach(cb);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user