Update web/templates/entry_view.html

This commit is contained in:
Joshua Laymon 2025-09-02 03:04:27 +00:00
parent ca7a3543a0
commit 0dea62b686

View File

@ -102,19 +102,10 @@
<div class="meta-label">Source</div> <div class="meta-label">Source</div>
<div class="meta-value" id="source-text"> <div class="meta-value" id="source-text">
{% if entry.source %} {% if entry.source %}
{% with s=entry.source %} <a class="chip chip-link js-source-link"
{% with sl=s|lower %} data-source="{{ entry.source }}"
{% if sl|slice:":2" == "wp" or sl|slice:":2" == "ws" or sl|slice:":2" == "yb" or sl|slice:":3" == "mwb" or sl|slice:":1" == "w" or sl|slice:":1" == "g" or sl|slice:":2" == "ap" or sl|slice:":3" == "apf" or sl|slice:":2" == "be" or sl|slice:":2" == "bh" or sl|slice:":2" == "br" or sl|slice:":2" == "bt" or sl|slice:":3" == "btg" or sl|slice:":2" == "cf" or sl|slice:":2" == "cl" or sl|slice:":2" == "ct" or sl|slice:":2" == "dp" or sl|slice:":2" == "fg" or sl|slice:":2" == "fy" or sl|slice:":2" == "gt" or sl|slice:":2" == "hb" or sl|slice:":2" == "im" or sl|slice:":2" == "ip" or sl|slice:":2" == "it" or sl|slice:":2" == "jv" or sl|slice:":2" == "ka" or sl|slice:":2" == "kj" or sl|slice:":2" == "kl" or sl|slice:":2" == "lf" or sl|slice:":3" == "lff" or sl|slice:":2" == "ll" or sl|slice:":2" == "ly" or sl|slice:":2" == "my" or sl|slice:":2" == "od" or sl|slice:":2" == "pe" or sl|slice:":2" == "po" or sl|slice:":2" == "pt" or sl|slice:":2" == "rr" or sl|slice:":2" == "rs" or sl|slice:":2" == "sg" or sl|slice:":2" == "sh" or sl|slice:":2" == "si" or sl|slice:":2" == "td" or sl|slice:":2" == "tp" or sl|slice:":2" == "tr" or sl|slice:":2" == "ts" or sl|slice:":2" == "un" or sl|slice:":2" == "jy"%} href="https://www.google.com/search?q={{ entry.source|urlencode }}"
<a class="chip chip-link" target="_blank" rel="noopener noreferrer">{{ entry.source }}</a>
href="https://wol.jw.org/en/wol/l/r1/lp-e?q={{ s|urlencode }}"
target="_blank" rel="noopener noreferrer">{{ s }}</a>
{% else %}
<a class="chip chip-link"
href="https://www.google.com/search?q={{ s|urlencode }}"
target="_blank" rel="noopener noreferrer">{{ s }}</a>
{% endif %}
{% endwith %}
{% endwith %}
{% else %}—{% endif %} {% else %}—{% endif %}
</div> </div>
</div> </div>
@ -216,7 +207,7 @@
border-color:#c9def5; border-color:#c9def5;
} }
/* Subject chip gets a hit → color the whole pill */ /* Subject chip gets a hit → color the whole chip */
.chip-subject.chip-hit{ .chip-subject.chip-hit{
background:#f8e3c5; /* your chosen highlight color */ background:#f8e3c5; /* your chosen highlight color */
border-color:#e0b98e; border-color:#e0b98e;
@ -394,10 +385,9 @@
a.muted = false; a.muted = false;
try { try {
await a.play(); // <- may throw NotAllowedError on some iOS/Safari cases await a.play();
playing = true; playing = true;
} catch (err) { } catch (err) {
// Autoplay/permission style block (Safari/iOS or Chrome heuristics)
if (err && (err.name === 'NotAllowedError' || if (err && (err.name === 'NotAllowedError' ||
/not allowed|denied permission/i.test(err.message))) { /not allowed|denied permission/i.test(err.message))) {
showToast("Audio was blocked. Make sure the phone isn't on Silent and tap the speaker again."); showToast("Audio was blocked. Make sure the phone isn't on Silent and tap the speaker again.");
@ -407,9 +397,7 @@
} }
} }
// -------- button: toggle behavior --------
ttsBtn.addEventListener('click', async () => { ttsBtn.addEventListener('click', async () => {
// Toggle OFF if something is already playing (either engine)
if (playing) { if (playing) {
try { try {
stopAll(); stopAll();
@ -438,7 +426,6 @@
} }
}); });
// Safety: stop audio when navigating away
window.addEventListener('pagehide', stopAll); window.addEventListener('pagehide', stopAll);
})(); })();
</script> </script>
@ -469,10 +456,7 @@ function showToast(message, duration = 3000) {
toast.textContent = message; toast.textContent = message;
document.body.appendChild(toast); document.body.appendChild(toast);
// Trigger fade in
setTimeout(() => toast.classList.add("show"), 50); setTimeout(() => toast.classList.add("show"), 50);
// Fade out & remove
setTimeout(() => { setTimeout(() => {
toast.classList.remove("show"); toast.classList.remove("show");
setTimeout(() => document.body.removeChild(toast), 500); setTimeout(() => document.body.removeChild(toast), 500);
@ -483,7 +467,6 @@ function showToast(message, duration = 3000) {
<!-- Highlighter: apply to ALL fields; for Subjects, color the whole chip; highlight WHOLE WORDS --> <!-- Highlighter: apply to ALL fields; for Subjects, color the whole chip; highlight WHOLE WORDS -->
<script> <script>
(function(){ (function(){
// Target EVERY field we render on entry_view
const ALL_SELECTORS = [ const ALL_SELECTORS = [
"#subject-list", "#subject-list",
"#illustration-text", "#illustration-text",
@ -493,7 +476,6 @@ function showToast(message, duration = 3000) {
"#talk_title-text" "#talk_title-text"
]; ];
// Run on fresh load + bfcache restores
if (document.readyState === "complete" || document.readyState === "interactive") run(); if (document.readyState === "complete" || document.readyState === "interactive") run();
else document.addEventListener("DOMContentLoaded", run, { once: true }); else document.addEventListener("DOMContentLoaded", run, { once: true });
window.addEventListener("pageshow", run); window.addEventListener("pageshow", run);
@ -502,7 +484,6 @@ function showToast(message, duration = 3000) {
async function run(){ async function run(){
if (ran) return; if (ran) return;
// Respect per-user toggle (defaults ON if not present)
let enabled = true; let enabled = true;
try { try {
const res = await fetch("/api/get-prefs/", { cache: "no-store", credentials: "same-origin" }); const res = await fetch("/api/get-prefs/", { cache: "no-store", credentials: "same-origin" });
@ -513,27 +494,19 @@ function showToast(message, duration = 3000) {
} catch(_) {} } catch(_) {}
if (!enabled) { ran = true; return; } if (!enabled) { ran = true; return; }
// Get the query (ignore fields entirely)
let q = (window.__lastSearchQ || "").trim(); let q = (window.__lastSearchQ || "").trim();
// JSON script fallback
if (!q) { if (!q) {
const dataEl = document.getElementById("last-search-data"); const dataEl = document.getElementById("last-search-data");
if (dataEl) { if (dataEl) {
try { q = (JSON.parse(dataEl.textContent||"{}").q || "").trim(); } catch(_){} try { q = (JSON.parse(dataEl.textContent||"{}").q || "").trim(); } catch(_){}
} }
} }
// localStorage fallback
if (!q) q = (localStorage.getItem("lastSearchQ") || "").trim(); if (!q) q = (localStorage.getItem("lastSearchQ") || "").trim();
if (!q) { ran = true; return; } if (!q) { ran = true; return; }
// 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) { ran = true; return; } if (!tokens.length) { ran = true; return; }
// Highlight across ALL selectors
for (const sel of ALL_SELECTORS) { for (const sel of ALL_SELECTORS) {
const container = document.querySelector(sel); const container = document.querySelector(sel);
if (!container) continue; if (!container) continue;
@ -543,7 +516,6 @@ function showToast(message, duration = 3000) {
ran = true; ran = true;
} }
// ---- helpers ----
function tokenize(s) { function tokenize(s) {
const out = []; const out = [];
let i = 0, buf = "", inQ = false; let i = 0, buf = "", inQ = false;
@ -563,11 +535,9 @@ function showToast(message, duration = 3000) {
} }
function makeWordRegex() { function makeWordRegex() {
// Prefer full Unicode word matching (letters + marks + numbers + ' and -)
try { try {
return new RegExp("(\\p{L}[\\p{L}\\p{M}\\p{N}'-]*|\\d+)", "gu"); return new RegExp("(\\p{L}[\\p{L}\\p{M}\\p{N}'-]*|\\d+)", "gu");
} catch (_) { } catch (_) {
// Fallback for older browsers: best-effort ASCII-ish words
return /([A-Za-z][A-Za-z0-9'-]*|\d+)/g; return /([A-Za-z][A-Za-z0-9'-]*|\d+)/g;
} }
} }
@ -576,14 +546,13 @@ function showToast(message, duration = 3000) {
if (!needle) return; if (!needle) return;
const needleLower = needle.toLowerCase(); const needleLower = needle.toLowerCase();
// Special handling for SUBJECT chips: color the whole chip, no <mark> injection
if (root.id === "subject-list") { if (root.id === "subject-list") {
root.querySelectorAll(".chip-subject, .chip-muted").forEach(chip => { root.querySelectorAll(".chip-subject, .chip-muted").forEach(chip => {
if ((chip.textContent || "").toLowerCase().includes(needleLower)) { if ((chip.textContent || "").toLowerCase().includes(needleLower)) {
chip.classList.add("chip-hit"); chip.classList.add("chip-hit");
} }
}); });
return; // do not inject <mark> inside subject chips return;
} }
const nodes = []; const nodes = [];
@ -595,8 +564,6 @@ function showToast(message, duration = 3000) {
for (const textNode of nodes) { for (const textNode of nodes) {
const text = textNode.nodeValue; const text = textNode.nodeValue;
if (!text) continue; if (!text) continue;
// Fast skip: if token not present anywhere (case-insensitive), skip this node
if (!text.toLowerCase().includes(needleLower)) continue; if (!text.toLowerCase().includes(needleLower)) continue;
const frag = document.createDocumentFragment(); const frag = document.createDocumentFragment();
@ -608,14 +575,12 @@ function showToast(message, duration = 3000) {
const start = m.index; const start = m.index;
const end = start + m[0].length; const end = start + m[0].length;
// Append any non-word separator text before this word
if (start > lastIndex) { if (start > lastIndex) {
frag.appendChild(document.createTextNode(text.slice(lastIndex, start))); frag.appendChild(document.createTextNode(text.slice(lastIndex, start)));
} }
const word = m[0]; const word = m[0];
if (word.toLowerCase().includes(needleLower)) { if (word.toLowerCase().includes(needleLower)) {
// Highlight the ENTIRE word
const mark = document.createElement("mark"); const mark = document.createElement("mark");
mark.className = "mark-hit"; mark.className = "mark-hit";
mark.textContent = word; mark.textContent = word;
@ -627,7 +592,6 @@ function showToast(message, duration = 3000) {
lastIndex = end; lastIndex = end;
} }
// Remainder after the last word
if (lastIndex < text.length) { if (lastIndex < text.length) {
frag.appendChild(document.createTextNode(text.slice(lastIndex))); frag.appendChild(document.createTextNode(text.slice(lastIndex)));
} }
@ -640,8 +604,28 @@ function showToast(message, duration = 3000) {
})(); })();
</script> </script>
<!-- Include shared Scripture Validator and validate each Scripture pill individually --> <!-- Include shared validators -->
<script src="{% static 'js/scripture-validator.v1.js' %}"></script> <script src="{% static 'js/scripture-validator.v1.js' %}"></script>
<script src="{% static 'js/source-validator.v1.js' %}"></script>
<!-- Upgrade Source link to WOL if valid (no other logic changed) -->
<script>
(function () {
const link = document.querySelector('#source-text .js-source-link');
if (!link) return;
const srcText = link.getAttribute('data-source') || (link.textContent || '').trim();
if (SourceValidator.isWOLSource(srcText)) {
link.href = SourceValidator.buildWOLSearchURL(srcText);
link.title = (link.title ? link.title + ' • ' : '') + 'Open in WOL';
} else {
// already points to Google; keep it
link.title = (link.title ? link.title + ' • ' : '') + 'Search on Google';
}
})();
</script>
<!-- Scripture pill validation you already use -->
<script> <script>
(function () { (function () {
function validatePills() { function validatePills() {
@ -653,11 +637,9 @@ function showToast(message, duration = 3000) {
const txt = (pill.textContent || '').trim(); const txt = (pill.textContent || '').trim();
if (!txt) return; if (!txt) return;
// Create a temporary off-DOM input to reuse the shared validator
const tmp = document.createElement('input'); const tmp = document.createElement('input');
tmp.type = 'text'; tmp.type = 'text';
tmp.value = txt; tmp.value = txt;
// We attach, which triggers immediate validation (no need to keep the node)
ScriptureValidator.attach(tmp); ScriptureValidator.attach(tmp);
const isValid = tmp.classList.contains('scripture-valid'); const isValid = tmp.classList.contains('scripture-valid');
@ -665,7 +647,6 @@ function showToast(message, duration = 3000) {
pill.classList.add('scripture-pill-invalid'); pill.classList.add('scripture-pill-invalid');
pill.title = (pill.title ? pill.title + ' • ' : '') + 'Unrecognized scripture format'; pill.title = (pill.title ? pill.title + ' • ' : '') + 'Unrecognized scripture format';
} }
// Clean up
tmp.remove(); tmp.remove();
}); });
} }
@ -675,7 +656,6 @@ function showToast(message, duration = 3000) {
} else { } else {
validatePills(); validatePills();
} }
// Also re-run if page is restored from bfcache
window.addEventListener('pageshow', validatePills); window.addEventListener('pageshow', validatePills);
})(); })();
</script> </script>