Update web/templates/entry_view.html
This commit is contained in:
parent
ca7a3543a0
commit
0dea62b686
@ -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>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user