Files
kjvstudy.org/kjvstudy_org/static/base.js
T
kennethreitz 7ce3f6ab89 Add Strong's number support to search
- Sidebar search box now recognizes Strong's numbers (H1, G3056, etc.)
  and shows a "Go to" option to navigate directly to the concordance
- Main search page redirects Strong's number queries to /strongs/{number}
- Supports both uppercase and lowercase prefixes (H/h, G/g)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-05 15:46:12 -05:00

1790 lines
64 KiB
JavaScript

// Dark mode functionality
(function() {
// Check for saved theme preference or default to light mode
const currentTheme = localStorage.getItem('theme') || 'light';
document.documentElement.setAttribute('data-theme', currentTheme);
})();
function toggleDarkMode() {
const currentTheme = document.documentElement.getAttribute('data-theme');
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
}
// Font size functionality
(function() {
const sizes = ['small', 'normal', 'large', 'x-large'];
const savedSize = localStorage.getItem('fontSize') || 'normal';
if (savedSize !== 'normal') {
document.documentElement.setAttribute('data-font-size', savedSize);
}
})();
function toggleFontSize() {
const currentSize = document.documentElement.getAttribute('data-font-size');
if (currentSize === 'large') {
document.documentElement.removeAttribute('data-font-size');
localStorage.removeItem('fontSize');
} else {
document.documentElement.setAttribute('data-font-size', 'large');
localStorage.setItem('fontSize', 'large');
}
}
// Bookmark functionality
function getBookmarks() {
try {
return JSON.parse(localStorage.getItem('kjvBookmarks') || '[]');
} catch (e) {
return [];
}
}
function saveBookmarks(bookmarks) {
localStorage.setItem('kjvBookmarks', JSON.stringify(bookmarks));
}
function isBookmarked(url) {
var bookmarks = getBookmarks();
return bookmarks.some(function(b) { return b.url === url; });
}
function getPageExcerpt() {
var article = document.querySelector('article');
if (!article) return '';
var clone = article.cloneNode(true);
clone.querySelectorAll('.breadcrumb, .breadcrumb-actions, .sidenote, .marginnote, .toc, script, style, nav, button, h1, h2, h3').forEach(function(el) {
el.remove();
});
var text = (clone.textContent || clone.innerText || '').trim();
// Get first meaningful chunk
text = text.replace(/\s+/g, ' ').substring(0, 300);
return text;
}
function getPageDescription() {
var meta = document.querySelector('meta[name="description"]');
return meta ? meta.getAttribute('content') : '';
}
function getPageBreadcrumbs() {
var breadcrumb = document.querySelector('.breadcrumb');
if (!breadcrumb) return [];
var crumbs = [];
breadcrumb.querySelectorAll('a, span:not(.breadcrumb-separator):not(.breadcrumb-actions)').forEach(function(el) {
if (el.classList.contains('breadcrumb-actions')) return;
if (el.closest('.breadcrumb-actions')) return;
var text = el.textContent.trim();
if (text && text !== '>' && text.length > 0) {
crumbs.push({
text: text,
url: el.tagName === 'A' ? el.getAttribute('href') : null
});
}
});
// Drop the last item (it's the current page title)
if (crumbs.length > 0) {
crumbs.pop();
}
return crumbs;
}
function toggleBookmark() {
var btn = document.getElementById('bookmark-btn');
var url = window.location.pathname;
var title = document.title.replace(' - KJV Study', '').replace(' - KJV Bible', '');
var bookmarks = getBookmarks();
var existingIndex = bookmarks.findIndex(function(b) { return b.url === url; });
if (existingIndex >= 0) {
// Remove bookmark
bookmarks.splice(existingIndex, 1);
if (btn) btn.classList.remove('bookmarked');
showBookmarkToast(false);
} else {
// Add bookmark with description, breadcrumbs, and excerpt
bookmarks.unshift({
url: url,
title: title,
description: getPageDescription(),
breadcrumbs: getPageBreadcrumbs(),
excerpt: getPageExcerpt(),
date: new Date().toISOString()
});
if (btn) btn.classList.add('bookmarked');
showBookmarkToast(true);
}
saveBookmarks(bookmarks);
updateStarsBadge();
}
function showBookmarkToast(added) {
var toast = document.getElementById('bookmark-toast');
if (!toast) return;
if (added) {
toast.innerHTML = 'Added to <a href="/stars">Starred Pages</a>';
} else {
toast.innerHTML = 'Removed from Starred Pages';
}
toast.classList.add('show');
// Hide after 3 seconds
setTimeout(function() {
toast.classList.remove('show');
}, 3000);
}
// Check bookmark state on page load and update nav badges
(function() {
document.addEventListener('DOMContentLoaded', function() {
var btn = document.getElementById('bookmark-btn');
if (btn && isBookmarked(window.location.pathname)) {
btn.classList.add('bookmarked');
}
// Update nav badges
updateStarsBadge();
updateReadingPlansBadge();
});
})();
function updateStarsBadge() {
var badge = document.getElementById('stars-badge');
if (!badge) return;
var bookmarks = getBookmarks();
badge.textContent = bookmarks.length > 0 ? bookmarks.length : '';
}
// Reading plans progress badge
function updateReadingPlansBadge() {
var badge = document.getElementById('reading-plans-badge');
if (!badge) return;
// Reading plan IDs and their total days
var planDays = {
'chronological': 365,
'one-year': 365,
'new-testament': 90,
'gospels-acts': 30,
'psalms-proverbs': 31,
'pentateuch': 40,
'prophets': 60,
'paul-epistles': 30,
'minor-prophets': 14,
'wisdom': 30,
'historical': 45,
'general-epistles': 14
};
var totalCompleted = 0;
var totalDays = 0;
var activePlans = 0;
// Check each plan
for (var planId in planDays) {
var storageKey = 'reading-plan-' + planId;
var saved = localStorage.getItem(storageKey);
if (saved) {
try {
var data = JSON.parse(saved);
if (data.completed && data.completed.length > 0) {
activePlans++;
totalCompleted += data.completed.length;
totalDays += planDays[planId];
}
} catch (e) {
// Invalid JSON, skip
}
}
}
if (activePlans === 0 || totalDays === 0) {
badge.textContent = '';
return;
}
var percent = Math.round((totalCompleted / totalDays) * 100);
badge.textContent = percent + '%';
}
// Page speech toggle (for breadcrumb button) - triggers spacebar speech
function togglePageSpeech() {
// Clear suppressSpace flag since this is a button click, not a keyboard repeat
if (window.KJVResourceSpeech) {
window.KJVResourceSpeech.suppressSpace = false;
}
// Simulate spacebar press to use existing speech system
var event = new KeyboardEvent('keydown', { key: ' ', code: 'Space', bubbles: true });
document.dispatchEvent(event);
// Update button state after a tick
setTimeout(updateSpeechButtonState, 50);
}
function updateSpeechButtonState() {
var btn = document.getElementById('speech-toggle-btn');
if (!btn) return;
var isSpeaking = (window.KJVResourceSpeech && window.KJVResourceSpeech.speaking) ||
(window.KJVSpeech && window.KJVSpeech.speaking) ||
('speechSynthesis' in window && (speechSynthesis.speaking || speechSynthesis.pending));
if (isSpeaking) {
btn.classList.add('speaking');
} else {
btn.classList.remove('speaking');
}
}
// Poll for speech state changes to keep button in sync
setInterval(updateSpeechButtonState, 500);
// Red letter toggle functionality
(function() {
// Check for saved red letter preference or default to enabled
const redLettersEnabled = localStorage.getItem('redLetters') !== 'disabled';
if (!redLettersEnabled) {
document.documentElement.setAttribute('data-red-letters', 'disabled');
}
})();
function toggleRedLetters() {
const currentState = document.documentElement.getAttribute('data-red-letters');
const newState = currentState === 'disabled' ? 'enabled' : 'disabled';
if (newState === 'disabled') {
document.documentElement.setAttribute('data-red-letters', 'disabled');
localStorage.setItem('redLetters', 'disabled');
} else {
document.documentElement.removeAttribute('data-red-letters');
localStorage.removeItem('redLetters');
}
}
// Sticky breadcrumb detection
(function() {
var breadcrumb = document.getElementById('breadcrumb');
if (!breadcrumb) return;
var ticking = false;
function updateStickyState() {
// Check if breadcrumb is at the top (stuck)
var rect = breadcrumb.getBoundingClientRect();
if (rect.top <= 0) {
breadcrumb.classList.add('stuck');
} else {
breadcrumb.classList.remove('stuck');
}
ticking = false;
}
window.addEventListener('scroll', function() {
if (!ticking) {
window.requestAnimationFrame(updateStickyState);
ticking = true;
}
}, { passive: true });
})();
// Sidebar collapse state persistence (mobile only)
(function() {
var toggle = document.getElementById('sidebar-toggle');
if (!toggle) return;
// On mobile, default to collapsed (don't restore from localStorage - always start closed)
var isMobile = window.innerWidth <= 1200;
if (isMobile) {
toggle.checked = false;
// Close sidebar when clicking any link inside it (for mobile navigation)
var sidebar = document.querySelector('.nav-sidebar');
if (sidebar) {
sidebar.addEventListener('click', function(e) {
var link = e.target.closest('a');
if (link && link.href) {
// Close sidebar on navigation
toggle.checked = false;
}
});
}
}
})();
// Details elements (subsections) collapse state persistence
(function() {
// Get all details elements in sidebar
var detailsElements = document.querySelectorAll('.nav-sidebar details');
// Restore saved states
detailsElements.forEach(function(details) {
var id = details.id || details.querySelector('summary')?.textContent.trim();
if (id) {
var savedState = localStorage.getItem('sidebar-details-' + id);
if (savedState === 'open') {
details.open = true;
} else if (savedState === 'closed') {
details.open = false;
}
}
});
// Save state on toggle
detailsElements.forEach(function(details) {
details.addEventListener('toggle', function() {
var id = this.id || this.querySelector('summary')?.textContent.trim();
if (id) {
localStorage.setItem('sidebar-details-' + id, this.open ? 'open' : 'closed');
}
});
});
})();
// Universal search functionality with smart verse navigation
(function() {
var searchInput = document.getElementById('sidebar-search-input');
var dropdown = document.getElementById('search-results-dropdown');
if (!searchInput || !dropdown) return;
var searchTimeout = null;
var selectedIndex = -1;
var currentResults = [];
// Book name mapping (same as homepage)
var bookMap = {
'genesis': 'Genesis', 'exodus': 'Exodus', 'leviticus': 'Leviticus', 'numbers': 'Numbers',
'deuteronomy': 'Deuteronomy', 'joshua': 'Joshua', 'judges': 'Judges', 'ruth': 'Ruth',
'1 samuel': '1 Samuel', '2 samuel': '2 Samuel', '1 kings': '1 Kings', '2 kings': '2 Kings',
'1 chronicles': '1 Chronicles', '2 chronicles': '2 Chronicles', 'ezra': 'Ezra', 'nehemiah': 'Nehemiah',
'esther': 'Esther', 'job': 'Job', 'psalms': 'Psalms', 'psalm': 'Psalms', 'proverbs': 'Proverbs',
'ecclesiastes': 'Ecclesiastes', 'song of solomon': 'Song of Solomon', 'isaiah': 'Isaiah',
'jeremiah': 'Jeremiah', 'lamentations': 'Lamentations', 'ezekiel': 'Ezekiel', 'daniel': 'Daniel',
'hosea': 'Hosea', 'joel': 'Joel', 'amos': 'Amos', 'obadiah': 'Obadiah', 'jonah': 'Jonah',
'micah': 'Micah', 'nahum': 'Nahum', 'habakkuk': 'Habakkuk', 'zephaniah': 'Zephaniah',
'haggai': 'Haggai', 'zechariah': 'Zechariah', 'malachi': 'Malachi', 'matthew': 'Matthew',
'mark': 'Mark', 'luke': 'Luke', 'john': 'John', 'acts': 'Acts', 'romans': 'Romans',
'1 corinthians': '1 Corinthians', '2 corinthians': '2 Corinthians', 'galatians': 'Galatians',
'ephesians': 'Ephesians', 'philippians': 'Philippians', 'colossians': 'Colossians',
'1 thessalonians': '1 Thessalonians', '2 thessalonians': '2 Thessalonians',
'1 timothy': '1 Timothy', '2 timothy': '2 Timothy', 'titus': 'Titus', 'philemon': 'Philemon',
'hebrews': 'Hebrews', 'james': 'James', '1 peter': '1 Peter', '2 peter': '2 Peter',
'1 john': '1 John', '2 john': '2 John', '3 john': '3 John', 'jude': 'Jude', 'revelation': 'Revelation',
'gen': 'Genesis', 'ex': 'Exodus', 'lev': 'Leviticus', 'num': 'Numbers', 'deut': 'Deuteronomy',
'josh': 'Joshua', 'judg': 'Judges', 'ru': 'Ruth', '1sam': '1 Samuel', '2sam': '2 Samuel',
'1ki': '1 Kings', '2ki': '2 Kings', '1chr': '1 Chronicles', '2chr': '2 Chronicles',
'neh': 'Nehemiah', 'est': 'Esther', 'ps': 'Psalms', 'prov': 'Proverbs', 'eccl': 'Ecclesiastes',
'isa': 'Isaiah', 'jer': 'Jeremiah', 'lam': 'Lamentations', 'ezek': 'Ezekiel', 'dan': 'Daniel',
'hos': 'Hosea', 'mic': 'Micah', 'hab': 'Habakkuk', 'zech': 'Zechariah', 'mal': 'Malachi',
'matt': 'Matthew', 'mk': 'Mark', 'lk': 'Luke', 'jn': 'John', 'rom': 'Romans',
'1cor': '1 Corinthians', '2cor': '2 Corinthians', 'gal': 'Galatians', 'eph': 'Ephesians',
'phil': 'Philippians', 'col': 'Colossians', '1thess': '1 Thessalonians', '2thess': '2 Thessalonians',
'1tim': '1 Timothy', '2tim': '2 Timothy', 'tit': 'Titus', 'heb': 'Hebrews', 'jas': 'James',
'1pet': '1 Peter', '2pet': '2 Peter', '1jn': '1 John', '2jn': '2 John', '3jn': '3 John', 'rev': 'Revelation'
};
function capitalizeBook(name) {
return bookMap[name.toLowerCase()] || name;
}
// Try to parse as Strong's number and return URL, or null
function parseStrongsNumber(input) {
// Strong's number format: H1234, G5678, h1234, g5678
var match = input.match(/^([HhGg])(\d+)$/);
if (match) {
var prefix = match[1].toUpperCase();
var number = match[2];
return '/strongs/' + prefix + number;
}
return null;
}
// Try to parse as verse reference and return URL, or null
function parseVerseReference(input) {
// Book Chapter:Verse (e.g., "John 3:16")
var match = input.match(/^(.+)\s+(\d+):(\d+)$/i);
if (match) {
var book = capitalizeBook(match[1].trim());
return '/book/' + encodeURIComponent(book) + '/chapter/' + match[2] + '/verse/' + match[3];
}
// Book Chapter Verse (e.g., "Rev 22 20" or "John 3 16")
match = input.match(/^(.+)\s+(\d+)\s+(\d+)$/i);
if (match) {
var book = capitalizeBook(match[1].trim());
return '/book/' + encodeURIComponent(book) + '/chapter/' + match[2] + '/verse/' + match[3];
}
// Book Chapter (e.g., "Genesis 1")
match = input.match(/^(.+)\s+(\d+)$/i);
if (match) {
var book = capitalizeBook(match[1].trim());
return '/book/' + encodeURIComponent(book) + '/chapter/' + match[2];
}
return null;
}
// Category labels
var categoryLabels = {
books: 'Books',
verses: 'Verses',
topics: 'Topics',
resources: 'Resources',
stories: 'Stories',
plans: 'Reading Plans'
};
// Render search results
function renderResults(data) {
var results = data.results;
var html = '';
currentResults = [];
// Check if query looks like a Strong's number
var strongsUrl = parseStrongsNumber(data.query.trim());
if (strongsUrl) {
html += '<div class="search-results-category">';
html += '<div class="search-results-category-title">Go to</div>';
currentResults.push(strongsUrl);
html += '<a href="' + strongsUrl + '" class="search-result-item selected">';
html += '<span class="result-title">' + data.query.trim().toUpperCase() + '</span>';
html += '<span class="result-meta">Strong\'s Concordance</span>';
html += '</a></div>';
selectedIndex = 0;
}
// Check if query looks like a verse reference
var verseUrl = parseVerseReference(data.query);
if (verseUrl && !strongsUrl) {
html += '<div class="search-results-category">';
html += '<div class="search-results-category-title">Go to</div>';
currentResults.push(verseUrl);
html += '<a href="' + verseUrl + '" class="search-result-item selected">';
html += '<span class="result-title">' + data.query + '</span>';
html += '<span class="result-meta">Press Enter to navigate</span>';
html += '</a></div>';
selectedIndex = 0;
}
if (Object.keys(results).length === 0 && !verseUrl && !strongsUrl) {
html = '<div class="search-no-results">No results found</div>';
} else {
// Render categories in specific order: books, topics, resources, stories, plans, verses
var categoryOrder = ['books', 'topics', 'resources', 'stories', 'plans', 'verses'];
categoryOrder.forEach(function(category) {
if (results[category] && results[category].length > 0) {
html += '<div class="search-results-category">';
html += '<div class="search-results-category-title">' + (categoryLabels[category] || category) + '</div>';
results[category].forEach(function(item) {
var title = item.name || item.title || item.reference;
var meta = '';
if (item.text) meta = item.text;
else if (item.category) meta = item.category;
currentResults.push(item.url);
html += '<a href="' + item.url + '" class="search-result-item">';
html += '<span class="result-title">' + title + '</span>';
if (meta) html += '<span class="result-meta">' + meta + '</span>';
html += '</a>';
});
html += '</div>';
}
});
// Add "View all results" link
html += '<a href="/search?q=' + encodeURIComponent(data.query) + '" class="search-view-all">View all verse results →</a>';
}
dropdown.innerHTML = html;
dropdown.classList.add('show');
if (!verseUrl) selectedIndex = -1;
}
// Perform search
function doSearch(query) {
if (query.length < 2) {
dropdown.classList.remove('show');
return;
}
dropdown.innerHTML = '<div class="search-loading">Searching...</div>';
dropdown.classList.add('show');
fetch('/api/universal-search?q=' + encodeURIComponent(query) + '&limit=4')
.then(function(r) { return r.json(); })
.then(renderResults)
.catch(function() {
dropdown.innerHTML = '<div class="search-no-results">Search error</div>';
});
}
// Input handler with debounce
searchInput.addEventListener('input', function() {
var query = this.value.trim();
clearTimeout(searchTimeout);
if (query.length < 2) {
dropdown.classList.remove('show');
return;
}
searchTimeout = setTimeout(function() {
doSearch(query);
}, 150);
});
// Keyboard navigation
searchInput.addEventListener('keydown', function(e) {
var items = dropdown.querySelectorAll('.search-result-item');
if (e.key === 'ArrowDown') {
e.preventDefault();
selectedIndex = Math.min(selectedIndex + 1, items.length - 1);
updateSelection(items);
} else if (e.key === 'ArrowUp') {
e.preventDefault();
selectedIndex = Math.max(selectedIndex - 1, -1);
updateSelection(items);
} else if (e.key === 'Enter') {
e.preventDefault();
if (selectedIndex >= 0 && currentResults[selectedIndex]) {
window.location.href = currentResults[selectedIndex];
} else {
// Try Strong's number first, then verse reference, then search
var strongsUrl = parseStrongsNumber(this.value.trim());
if (strongsUrl) {
window.location.href = strongsUrl;
} else {
var verseUrl = parseVerseReference(this.value.trim());
if (verseUrl) {
window.location.href = verseUrl;
} else if (this.value.trim()) {
window.location.href = '/search?q=' + encodeURIComponent(this.value.trim());
}
}
}
} else if (e.key === 'Escape') {
dropdown.classList.remove('show');
this.blur();
}
});
function updateSelection(items) {
items.forEach(function(item, i) {
item.classList.toggle('selected', i === selectedIndex);
});
}
// Close dropdown when clicking outside
document.addEventListener('click', function(e) {
if (!e.target.closest('.sidebar-search')) {
dropdown.classList.remove('show');
}
});
// Reopen on focus if there's a query
searchInput.addEventListener('focus', function() {
if (this.value.trim().length >= 2) {
doSearch(this.value.trim());
}
});
})();
// Global viewport helpers for keyboard navigation
window.KJVNav = {
isInViewport: function(el) {
if (!el) return false;
var rect = el.getBoundingClientRect();
return rect.top < window.innerHeight && rect.bottom > 0;
},
findFirstVisibleIndex: function(elements) {
for (var i = 0; i < elements.length; i++) {
if (this.isInViewport(elements[i])) return i;
}
return 0;
},
isSelectionOffScreen: function(elements, selectedIndex) {
if (selectedIndex < 0) return true;
if (selectedIndex >= elements.length) return true;
return !this.isInViewport(elements[selectedIndex]);
},
// Sidebar navigation state
sidebarActive: false,
sidebarIndex: -1,
sidebarLinks: [],
currentPageNav: null,
// Activate sidebar navigation with 'n' key
initSidebarNav: function() {
var self = this;
var sidebar = document.querySelector('.nav-sidebar');
if (!sidebar) return;
// Get all navigable links in sidebar
self.sidebarLinks = Array.from(sidebar.querySelectorAll('a'));
function clearSidebarSelection() {
self.sidebarLinks.forEach(function(link) {
link.style.outline = '';
link.style.background = '';
});
}
function selectSidebarLink(index) {
clearSidebarSelection();
if (self.sidebarLinks.length === 0) return;
self.sidebarIndex = Math.max(0, Math.min(index, self.sidebarLinks.length - 1));
var link = self.sidebarLinks[self.sidebarIndex];
link.style.outline = '2px solid #4a7c59';
link.style.background = 'rgba(74, 124, 89, 0.1)';
link.scrollIntoView({ behavior: 'auto', block: 'center' });
// Expand parent details if collapsed
var details = link.closest('details');
if (details && !details.open) {
details.open = true;
}
}
function exitSidebar() {
clearSidebarSelection();
self.sidebarActive = false;
self.sidebarIndex = -1;
// Close sidebar on mobile when exiting nav mode
var sidebarToggle = document.getElementById('sidebar-toggle');
if (sidebarToggle && sidebarToggle.checked) {
sidebarToggle.checked = false;
}
// Clear page nav selection if exists
if (self.currentPageNav && self.currentPageNav.clearSelection) {
self.currentPageNav.clearSelection();
}
}
document.addEventListener('keydown', function(e) {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
// 'n' to enter sidebar nav mode
if (e.key === 'n' && !self.sidebarActive) {
e.preventDefault();
// Open sidebar if it's not visible (mobile/collapsed state)
var sidebarToggle = document.getElementById('sidebar-toggle');
if (sidebarToggle && !sidebarToggle.checked) {
sidebarToggle.checked = true;
}
self.sidebarActive = true;
// Clear any page content selection
if (self.currentPageNav && self.currentPageNav.clearSelection) {
self.currentPageNav.clearSelection();
}
// Start at first link or current page link
var currentLink = sidebar.querySelector('a.current');
var startIndex = currentLink ? self.sidebarLinks.indexOf(currentLink) : 0;
selectSidebarLink(startIndex >= 0 ? startIndex : 0);
return;
}
// Handle sidebar navigation when active
if (self.sidebarActive) {
if (e.key === 'ArrowDown' || e.key === 'j') {
e.preventDefault();
e.stopPropagation();
selectSidebarLink(self.sidebarIndex + 1);
return;
} else if (e.key === 'ArrowUp' || e.key === 'k') {
e.preventDefault();
e.stopPropagation();
selectSidebarLink(self.sidebarIndex - 1);
return;
} else if (e.key === 'Enter') {
e.preventDefault();
e.stopPropagation();
if (self.sidebarIndex >= 0 && self.sidebarLinks[self.sidebarIndex]) {
window.location.href = self.sidebarLinks[self.sidebarIndex].href;
}
return;
} else if (e.key === 'Escape') {
e.preventDefault();
e.stopPropagation();
exitSidebar();
return;
} else if (e.key === 'ArrowRight' || e.key === 'l') {
// Expand details or go to link
e.preventDefault();
e.stopPropagation();
if (self.sidebarIndex >= 0) {
var link = self.sidebarLinks[self.sidebarIndex];
var details = link.closest('details');
var summary = link.closest('summary');
if (summary && details && !details.open) {
details.open = true;
} else {
window.location.href = link.href;
}
}
return;
} else if (e.key === 'ArrowLeft' || e.key === 'h') {
// Collapse details or exit sidebar
e.preventDefault();
e.stopPropagation();
if (self.sidebarIndex >= 0) {
var link = self.sidebarLinks[self.sidebarIndex];
var details = link.closest('details');
if (details && details.open) {
details.open = false;
} else {
exitSidebar();
}
} else {
exitSidebar();
}
return;
}
}
});
},
// Simple linear keyboard navigation - just pass a CSS selector
initSimpleNav: function(selector, options) {
options = options || {};
var elements = Array.from(document.querySelectorAll(selector));
var selectedIndex = -1;
var pdfSelector = options.pdfSelector || '[class*="-download-btn"]';
var self = this;
function clearSelection() {
if (selectedIndex >= 0 && selectedIndex < elements.length) {
elements[selectedIndex].style.outline = '';
elements[selectedIndex].style.outlineOffset = '';
elements[selectedIndex].classList.remove('selected');
}
}
function selectElement(index) {
// Exit sidebar mode if active
if (self.sidebarActive) {
self.sidebarActive = false;
self.sidebarIndex = -1;
document.querySelectorAll('.nav-sidebar a').forEach(function(link) {
link.style.outline = '';
link.style.background = '';
});
}
clearSelection();
if (elements.length === 0) return;
selectedIndex = Math.max(0, Math.min(index, elements.length - 1));
elements[selectedIndex].style.outline = '2px solid #4a7c59';
elements[selectedIndex].style.outlineOffset = '8px';
elements[selectedIndex].classList.add('selected');
elements[selectedIndex].scrollIntoView({ behavior: 'auto', block: 'center' });
}
// Store reference for sidebar to clear
var navInstance = { elements: elements, selectElement: selectElement, clearSelection: clearSelection };
self.currentPageNav = navInstance;
document.addEventListener('keydown', function(e) {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
// Don't handle if sidebar is active (except 'n' is handled above)
if (self.sidebarActive) return;
if (e.key === 'ArrowDown' || e.key === 'j') {
e.preventDefault();
if (selectedIndex < 0 || KJVNav.isSelectionOffScreen(elements, selectedIndex)) {
selectElement(KJVNav.findFirstVisibleIndex(elements));
} else {
selectElement(selectedIndex + 1);
}
} else if (e.key === 'ArrowUp' || e.key === 'k') {
e.preventDefault();
if (selectedIndex < 0 || KJVNav.isSelectionOffScreen(elements, selectedIndex)) {
selectElement(KJVNav.findFirstVisibleIndex(elements));
} else {
selectElement(selectedIndex - 1);
}
} else if (e.key === 'ArrowLeft' || e.key === 'h') {
e.preventDefault();
history.back();
} else if (e.key === 'Enter') {
e.preventDefault();
if (selectedIndex >= 0) {
var el = elements[selectedIndex];
var link = el.tagName === 'A' ? el : el.querySelector('a');
if (link) window.location.href = link.href;
}
} else if (e.key === 'Escape') {
e.preventDefault();
clearSelection();
selectedIndex = -1;
} else if (e.key === 'p') {
e.preventDefault();
var pdfBtn = document.querySelector(pdfSelector);
if (pdfBtn) window.location.href = pdfBtn.href;
} else if (e.key === ' ') {
e.preventDefault();
if (selectedIndex >= 0 && window.KJVSpeech) {
var text = elements[selectedIndex].textContent || elements[selectedIndex].innerText;
KJVSpeech.speak(text);
}
}
});
return navInstance;
},
// 2D Grid navigation for card grids - handles up/down by row, left/right within row
initGridNav: function(selector, options) {
options = options || {};
var gridSelector = options.gridSelector || '.book-grid, .topic-grid, .resource-grid, .story-grid, .card-grid';
var elements = Array.from(document.querySelectorAll(selector));
var selectedIndex = -1;
var self = this;
function getGridColumns() {
var grid = document.querySelector(gridSelector);
if (!grid) return 1;
var style = getComputedStyle(grid);
var cols = style.gridTemplateColumns.split(' ').length;
return cols || 1;
}
function clearSelection() {
if (selectedIndex >= 0 && selectedIndex < elements.length) {
elements[selectedIndex].style.outline = '';
elements[selectedIndex].style.outlineOffset = '';
elements[selectedIndex].classList.remove('selected');
}
}
function selectElement(index) {
// Exit sidebar mode if active
if (self.sidebarActive) {
self.sidebarActive = false;
self.sidebarIndex = -1;
document.querySelectorAll('.nav-sidebar a').forEach(function(link) {
link.style.outline = '';
link.style.background = '';
});
}
clearSelection();
if (elements.length === 0) return;
selectedIndex = Math.max(0, Math.min(index, elements.length - 1));
elements[selectedIndex].style.outline = '2px solid #4a7c59';
elements[selectedIndex].style.outlineOffset = '4px';
elements[selectedIndex].classList.add('selected');
elements[selectedIndex].scrollIntoView({ behavior: 'auto', block: 'center' });
}
// Store reference for sidebar to clear
var navInstance = { elements: elements, selectElement: selectElement, clearSelection: clearSelection };
self.currentPageNav = navInstance;
document.addEventListener('keydown', function(e) {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
if (self.sidebarActive) return;
var cols = getGridColumns();
if (e.key === 'ArrowDown' || e.key === 'j') {
e.preventDefault();
if (selectedIndex < 0) {
selectElement(0);
} else {
selectElement(selectedIndex + cols);
}
} else if (e.key === 'ArrowUp' || e.key === 'k') {
e.preventDefault();
if (selectedIndex < 0) {
selectElement(0);
} else if (selectedIndex - cols >= 0) {
selectElement(selectedIndex - cols);
}
} else if (e.key === 'ArrowRight' || e.key === 'l') {
e.preventDefault();
if (selectedIndex < 0) {
selectElement(0);
} else {
selectElement(selectedIndex + 1);
}
} else if (e.key === 'ArrowLeft' || e.key === 'h') {
e.preventDefault();
if (selectedIndex <= 0) {
history.back();
} else {
selectElement(selectedIndex - 1);
}
} else if (e.key === 'Enter') {
e.preventDefault();
if (selectedIndex >= 0) {
var el = elements[selectedIndex];
var link = el.tagName === 'A' ? el : el.querySelector('a');
if (link) window.location.href = link.href;
}
} else if (e.key === 'Escape') {
e.preventDefault();
clearSelection();
selectedIndex = -1;
} else if (e.key === ' ') {
e.preventDefault();
if (selectedIndex >= 0 && window.KJVSpeech) {
var text = elements[selectedIndex].textContent || elements[selectedIndex].innerText;
KJVSpeech.speak(text);
}
}
});
return navInstance;
}
};
// Initialize sidebar navigation globally
document.addEventListener('DOMContentLoaded', function() {
KJVNav.initSidebarNav();
});
// Text-to-speech for any selected content
window.KJVSpeech = {
utterance: null,
speaking: false,
speak: function(text) {
if (!('speechSynthesis' in window)) {
console.log('Speech synthesis not supported');
return;
}
// Stop any current speech
this.stop();
// Clean up the text - remove verse numbers at start, collapse whitespace
text = text.replace(/^\s*\d+\s*/, '').replace(/\s+/g, ' ').trim();
if (!text) return;
this.utterance = new SpeechSynthesisUtterance(text);
this.utterance.rate = 0.9;
this.utterance.pitch = 1;
// Try to use a good English voice
var voices = speechSynthesis.getVoices();
var englishVoice = voices.find(function(v) {
return v.lang.startsWith('en') && v.name.includes('Daniel');
}) || voices.find(function(v) {
return v.lang.startsWith('en-GB');
}) || voices.find(function(v) {
return v.lang.startsWith('en');
});
if (englishVoice) {
this.utterance.voice = englishVoice;
}
this.speaking = true;
this.utterance.onend = function() {
KJVSpeech.speaking = false;
};
this.utterance.onerror = function() {
KJVSpeech.speaking = false;
};
speechSynthesis.speak(this.utterance);
},
stop: function() {
if ('speechSynthesis' in window) {
speechSynthesis.cancel();
}
this.speaking = false;
},
toggle: function(text) {
if (this.speaking) {
this.stop();
} else {
this.speak(text);
}
},
// Get text from the currently selected element (green box)
getSelectedText: function() {
// Find element with our green selection outline
var selected = document.querySelector('[style*="outline: 2px solid"]') ||
document.querySelector('[style*="outline:2px solid"]');
if (selected) {
return selected.textContent || selected.innerText;
}
// Fallback: Try specific verse selectors
var verseEl = document.querySelector('.verse-text-content') ||
document.querySelector('.verse-display .text') ||
document.querySelector('.verse-text') ||
document.querySelector('[data-verse-text]');
if (verseEl) {
return verseEl.textContent || verseEl.innerText;
}
return null;
}
};
// Simple resource reader state (for resource pages)
window.KJVResourceSpeech = {
speaking: false,
utterance: null,
suppressSpace: false
};
// Load voices (they may not be available immediately)
if ('speechSynthesis' in window) {
speechSynthesis.getVoices();
speechSynthesis.onvoiceschanged = function() {
speechSynthesis.getVoices();
};
}
// Default resource reader unless explicitly disabled
document.addEventListener('DOMContentLoaded', function() {
if (document.body && !document.body.dataset.resourceReader) {
document.body.dataset.resourceReader = 'true';
}
});
// Keyboard shortcuts
document.addEventListener('keydown', function(e) {
// Don't trigger if user is typing in an input field
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') {
// Allow Escape to clear focus
if (e.key === 'Escape') {
e.target.blur();
}
return;
}
// Cmd/Ctrl + D: Toggle dark mode
if ((e.metaKey || e.ctrlKey) && e.key === 'd') {
e.preventDefault();
toggleDarkMode();
}
// Cmd/Ctrl + S: Toggle star (overrides browser save)
if ((e.metaKey || e.ctrlKey) && e.key === 's') {
e.preventDefault();
toggleBookmark();
}
// Cmd/Ctrl + K or /: Focus search
if (((e.metaKey || e.ctrlKey) && e.key === 'k') || e.key === '/') {
e.preventDefault();
window.location.href = '/search';
}
// Single key shortcuts for navigation (only work without modifiers)
if (!e.metaKey && !e.ctrlKey && !e.altKey) {
switch(e.key) {
case '0':
e.preventDefault();
window.location.href = '/';
break;
case '1':
e.preventDefault();
window.location.href = '/book/Genesis';
break;
case '2':
e.preventDefault();
window.location.href = '/book/Matthew';
break;
case '3':
e.preventDefault();
window.location.href = '/book/John';
break;
case '4':
e.preventDefault();
window.location.href = '/book/Romans';
break;
case '7':
e.preventDefault();
window.location.href = '/book/Psalms';
break;
case '8':
e.preventDefault();
window.location.href = '/book/Proverbs';
break;
case '9':
e.preventDefault();
window.location.href = '/book/Revelation';
break;
case 'g':
e.preventDefault();
showVerseLookup();
break;
case 'r':
e.preventDefault();
window.location.href = '/resources';
break;
case 'b':
e.preventDefault();
window.location.href = '/books';
break;
case 's':
e.preventDefault();
window.location.href = '/stars';
break;
case '/':
e.preventDefault();
window.location.href = '/search';
break;
case 't':
e.preventDefault();
window.location.href = '/topics';
break;
case 'p':
e.preventDefault();
var pdfBtn = document.querySelector('.pdf-btn, a[href$="/pdf"]');
if (pdfBtn) window.location.href = pdfBtn.href;
break;
case 'f':
e.preventDefault();
window.location.href = '/family-tree';
break;
case 'c':
e.preventDefault();
window.location.href = '/strongs';
break;
case 'v':
e.preventDefault();
window.location.href = '/verse-of-the-day';
break;
case '.':
e.preventDefault();
window.location.href = '/random-verse';
break;
case '?':
showKeyboardHelp();
break;
case 'R':
e.preventDefault();
toggleRedLetters();
break;
case ' ':
// Space: Read aloud selected text (with optional resource-reader handling)
if (document.body && document.body.dataset && document.body.dataset.resourceReader === 'false') {
// explicitly disabled
} else if (document.body) {
e.preventDefault();
if (window.KJVResourceSpeech.suppressSpace) return;
if (!('speechSynthesis' in window)) return;
// If something is currently speaking or queued, stop it instead of starting a new read
if (window.KJVResourceSpeech.utterance && (speechSynthesis.speaking || speechSynthesis.pending || speechSynthesis.paused)) {
speechSynthesis.cancel();
window.KJVResourceSpeech.speaking = false;
window.KJVResourceSpeech.utterance = null;
window.KJVResourceSpeech.suppressSpace = true;
return;
}
if (window.KJVResourceSpeech.speaking) {
speechSynthesis.cancel();
window.KJVResourceSpeech.speaking = false;
window.KJVResourceSpeech.utterance = null;
window.KJVResourceSpeech.suppressSpace = true;
return;
}
var highlighted = document.querySelector('[style*=\"outline: 2px solid\"]:not(.toc):not(.breadcrumb):not(.chapters-section)') ||
document.querySelector('[style*=\"outline:2px solid\"]:not(.toc):not(.breadcrumb):not(.chapters-section)') ||
document.querySelector('.selected:not(.toc):not(.breadcrumb):not(.chapters-section)');
var combined = '';
if (highlighted) {
var hclone = highlighted.cloneNode(true);
hclone.querySelectorAll('.sidenote, .marginnote, .sidenote-number, .margin-toggle, .breadcrumb, .toc, .chapters-section, script, style').forEach(function(el) { el.remove(); });
combined = (hclone.textContent || hclone.innerText || '').trim();
} else {
var article = document.querySelector('article');
if (article) {
// If this is a card grid, concatenate the highlighted or first card
var selectedCard = article.querySelector('.resource-card[style*=\"outline\"]') || article.querySelector('.resource-card');
if (selectedCard) {
var cclone = selectedCard.cloneNode(true);
cclone.querySelectorAll('.sidenote, .marginnote, .sidenote-number, .margin-toggle, .breadcrumb, .toc, .chapters-section, script, style').forEach(function(el) { el.remove(); });
combined = (cclone.textContent || cclone.innerText || '').trim();
} else {
// Prefer reading substantive text blocks instead of the whole article
var textBlocks = Array.from(article.querySelectorAll('h1, h2, h3, p, li, blockquote, .intro-text, .verse-text, .resource-description, .resource-description-body, .resource-item-description, .parable-description, .angel-description, .covenant-description, .festival-description, .prophet-description, .fruit-description, .name-description, .woman-description, .apostle-description'))
.filter(function(el) {
if (el.closest('.toc') || el.closest('.chapters-section') || el.closest('.breadcrumb')) return false;
var txt = (el.textContent || el.innerText || '').trim();
return txt.length > 120;
})
.map(function(el) {
var clone = el.cloneNode(true);
clone.querySelectorAll('.sidenote, .marginnote, .sidenote-number, .margin-toggle, .breadcrumb, .toc, .chapters-section, script, style').forEach(function(x) { x.remove(); });
return (clone.textContent || clone.innerText || '').trim();
})
.filter(Boolean);
if (textBlocks.length > 0) {
combined = textBlocks.join('\\n\\n');
} else {
var aclone = article.cloneNode(true);
aclone.querySelectorAll('.sidenote, .marginnote, .sidenote-number, .margin-toggle, .breadcrumb, .toc, .chapters-section, script, style').forEach(function(el) { el.remove(); });
combined = (aclone.textContent || aclone.innerText || '').trim();
}
}
}
}
if (combined) {
var utter = new SpeechSynthesisUtterance(combined);
var voices = speechSynthesis.getVoices();
var englishVoice = voices.find(function(v) { return v.lang && v.lang.toLowerCase().startsWith('en') && v.name.includes('Daniel'); }) ||
voices.find(function(v) { return v.lang && v.lang.toLowerCase().startsWith('en-gb'); }) ||
voices.find(function(v) { return v.lang && v.lang.toLowerCase().startsWith('en'); });
if (englishVoice) utter.voice = englishVoice;
utter.onend = function() {
window.KJVResourceSpeech.speaking = false;
window.KJVResourceSpeech.utterance = null;
};
utter.onerror = function() {
window.KJVResourceSpeech.speaking = false;
window.KJVResourceSpeech.utterance = null;
};
window.KJVResourceSpeech.speaking = true;
window.KJVResourceSpeech.utterance = utter;
window.KJVResourceSpeech.suppressSpace = false;
speechSynthesis.cancel();
speechSynthesis.speak(utter);
}
} else {
var selectedText = KJVSpeech.getSelectedText();
if (selectedText) {
e.preventDefault();
KJVSpeech.toggle(selectedText);
}
}
break;
}
}
});
// Clear suppression after spacebar is released
document.addEventListener('keyup', function(e) {
if (e.key === ' ') {
window.KJVResourceSpeech.suppressSpace = false;
}
});
// Quick verse lookup
function showVerseLookup() {
var reference = prompt('Enter verse reference (e.g., John 3:16, Rev 22 20, Psalm 23, Genesis 1):');
if (!reference) return;
reference = reference.trim();
// Try to match: Book Chapter:Verse (with colon)
var match = reference.match(/^(.+?)\s+(\d+):(\d+)$/i);
if (match) {
var book = match[1].trim();
var chapter = match[2];
var verse = match[3];
window.location.href = '/book/' + encodeURIComponent(book) + '/chapter/' + chapter + '/verse/' + verse;
return;
}
// Try to match: Book Chapter Verse (space instead of colon)
match = reference.match(/^(.+?)\s+(\d+)\s+(\d+)$/i);
if (match) {
var book = match[1].trim();
var chapter = match[2];
var verse = match[3];
window.location.href = '/book/' + encodeURIComponent(book) + '/chapter/' + chapter + '/verse/' + verse;
return;
}
// Try to match: Book Chapter
match = reference.match(/^(.+?)\s+(\d+)$/i);
if (match) {
var book = match[1].trim();
var chapter = match[2];
window.location.href = '/book/' + encodeURIComponent(book) + '/chapter/' + chapter;
return;
}
// Try to match: Book
match = reference.match(/^(.+)$/i);
if (match) {
var book = match[1].trim();
window.location.href = '/book/' + encodeURIComponent(book);
return;
}
}
// Keyboard shortcuts help modal
function showKeyboardHelp() {
// Remove existing modal if present
var existingModal = document.getElementById('keyboard-help-modal');
if (existingModal) {
existingModal.remove();
return;
}
var modal = document.createElement('div');
modal.id = 'keyboard-help-modal';
modal.innerHTML =
'<div class="keyboard-help-backdrop"></div>' +
'<div class="keyboard-help-content">' +
'<button class="keyboard-help-close" aria-label="Close">&times;</button>' +
'<h2>Keyboard Shortcuts</h2>' +
'<div class="keyboard-help-columns">' +
'<div class="keyboard-help-section">' +
'<h3>Navigation</h3>' +
'<div class="shortcut"><kbd>0</kbd><span>Homepage</span></div>' +
'<div class="shortcut"><kbd>1</kbd><span>Genesis (OT)</span></div>' +
'<div class="shortcut"><kbd>2</kbd><span>Matthew (NT)</span></div>' +
'<div class="shortcut"><kbd>3</kbd><span>John</span></div>' +
'<div class="shortcut"><kbd>4</kbd><span>Romans</span></div>' +
'<div class="shortcut"><kbd>7</kbd><span>Psalms</span></div>' +
'<div class="shortcut"><kbd>8</kbd><span>Proverbs</span></div>' +
'<div class="shortcut"><kbd>9</kbd><span>Revelation</span></div>' +
'<div class="shortcut"><kbd>b</kbd><span>Books</span></div>' +
'<div class="shortcut"><kbd>s</kbd><span>Stars</span></div>' +
'<div class="shortcut"><kbd>r</kbd><span>Resources</span></div>' +
'<div class="shortcut"><kbd>t</kbd><span>Topics</span></div>' +
'<div class="shortcut"><kbd>p</kbd><span>PDF (when available)</span></div>' +
'<div class="shortcut"><kbd>f</kbd><span>Family Tree</span></div>' +
'<div class="shortcut"><kbd>c</kbd><span>Strong\'s Concordance</span></div>' +
'<div class="shortcut"><kbd>v</kbd><span>Verse of the Day</span></div>' +
'<div class="shortcut"><kbd>.</kbd><span>Random Verse</span></div>' +
'<div class="shortcut"><kbd>g</kbd><span>Quick verse lookup</span></div>' +
'</div>' +
'<div class="keyboard-help-section">' +
'<h3>Page Navigation</h3>' +
'<div class="shortcut"><kbd>↑</kbd> / <kbd>k</kbd><span>Previous item</span></div>' +
'<div class="shortcut"><kbd>↓</kbd> / <kbd>j</kbd><span>Next item</span></div>' +
'<div class="shortcut"><kbd>←</kbd> / <kbd>h</kbd><span>Go back</span></div>' +
'<div class="shortcut"><kbd>→</kbd> / <kbd>l</kbd><span>Next page</span></div>' +
'<div class="shortcut"><kbd>Enter</kbd><span>Select / Open</span></div>' +
'</div>' +
'<div class="keyboard-help-section">' +
'<h3>Other</h3>' +
'<div class="shortcut"><kbd>Space</kbd><span>Read aloud</span></div>' +
'<div class="shortcut"><kbd>n</kbd><span>Navigate sidebar</span></div>' +
'<div class="shortcut"><kbd>⌘</kbd>+<kbd>D</kbd><span>Toggle dark mode</span></div>' +
'<div class="shortcut"><kbd>R</kbd><span>Toggle red letters</span></div>' +
'<div class="shortcut"><kbd>⌘</kbd>+<kbd>S</kbd><span>Toggle star</span></div>' +
'<div class="shortcut"><kbd>/</kbd><span>Search</span></div>' +
'<div class="shortcut"><kbd>?</kbd><span>Show this help</span></div>' +
'<div class="shortcut"><kbd>Esc</kbd><span>Close / Clear focus</span></div>' +
'</div>' +
'</div>' +
'</div>';
// Add styles
var style = document.createElement('style');
style.textContent =
'#keyboard-help-modal { position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: 10000; display: flex; align-items: center; justify-content: center; }' +
'.keyboard-help-backdrop { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); }' +
'.keyboard-help-content { position: relative; background: var(--bg-color); border-radius: 8px; padding: 1.5rem 2rem; max-width: 700px; max-height: 90vh; overflow-y: auto; box-shadow: 0 10px 40px rgba(0,0,0,0.3); }' +
'.keyboard-help-content h2 { margin: 0 0 1rem 0; font-size: 1.4rem; font-weight: 600; }' +
'.keyboard-help-close { position: absolute; top: 0.75rem; right: 0.75rem; background: none; border: none; font-size: 1.5rem; cursor: pointer; color: var(--text-secondary); padding: 0.25rem 0.5rem; line-height: 1; }' +
'.keyboard-help-close:hover { color: var(--text-color); }' +
'.keyboard-help-columns { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1.5rem; }' +
'.keyboard-help-section h3 { font-size: 0.85rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-secondary); margin: 0 0 0.75rem 0; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border-color); }' +
'.shortcut { display: flex; align-items: center; gap: 0.5rem; margin: 0.4rem 0; font-size: 0.9rem; }' +
'.shortcut kbd { display: inline-block; padding: 0.15rem 0.4rem; font-family: inherit; font-size: 0.8rem; background: var(--code-bg); border: 1px solid var(--border-color-darker); border-radius: 3px; min-width: 1.5rem; text-align: center; }' +
'.shortcut span { color: var(--text-secondary); }' +
'@media (max-width: 700px) { .keyboard-help-columns { grid-template-columns: 1fr; } .keyboard-help-content { margin: 1rem; padding: 1rem 1.5rem; } }';
modal.appendChild(style);
document.body.appendChild(modal);
// Close handlers
modal.querySelector('.keyboard-help-backdrop').addEventListener('click', function() {
modal.remove();
});
modal.querySelector('.keyboard-help-close').addEventListener('click', function() {
modal.remove();
});
document.addEventListener('keydown', function closeOnEsc(e) {
if (e.key === 'Escape') {
modal.remove();
document.removeEventListener('keydown', closeOnEsc);
}
});
}
// Verse tooltip functionality
(function() {
// Remove any existing tooltip (handles bfcache restoration)
var existingTooltip = document.querySelector('.verse-tooltip');
if (existingTooltip) {
existingTooltip.remove();
}
// Create tooltip element
var tooltip = document.createElement('div');
tooltip.className = 'verse-tooltip';
document.body.appendChild(tooltip);
// Cache for fetched verses
var verseCache = {};
// Parse verse URL to extract book, chapter, and verse
function parseVerseUrl(url) {
// Try to match single verse URL (old format): /book/John/chapter/3/verse/16
var match = url.match(/\/book\/([^\/]+)\/chapter\/(\d+)\/verse\/(\d+)/);
if (match) {
return {
book: decodeURIComponent(match[1]),
chapter: match[2],
verse: match[3],
verseEnd: null,
cacheKey: match[1] + '_' + match[2] + '_' + match[3],
isRange: false
};
}
// Try to match verse range URL: /book/Proverbs/chapter/31#verse-26-28
match = url.match(/\/book\/([^\/]+)\/chapter\/(\d+)#verse-(\d+)-(\d+)/);
if (match) {
return {
book: decodeURIComponent(match[1]),
chapter: match[2],
verse: match[3],
verseEnd: match[4],
cacheKey: match[1] + '_' + match[2] + '_' + match[3] + '-' + match[4],
isRange: true
};
}
// Try to match single verse anchor URL (new format): /book/John/chapter/3#verse-16
match = url.match(/\/book\/([^\/]+)\/chapter\/(\d+)#verse-(\d+)$/);
if (match) {
return {
book: decodeURIComponent(match[1]),
chapter: match[2],
verse: match[3],
verseEnd: null,
cacheKey: match[1] + '_' + match[2] + '_' + match[3],
isRange: false
};
}
return null;
}
// Fetch verse text from server using API
async function fetchVerseText(book, chapter, verse, verseEnd, cacheKey) {
// Check cache first
if (verseCache[cacheKey]) {
return verseCache[cacheKey];
}
try {
var url;
if (verseEnd) {
// Use verse range API endpoint
url = '/api/verse-range/' + encodeURIComponent(book) + '/' + chapter + '/' + verse + '/' + verseEnd;
} else {
// Use single verse API endpoint
url = '/api/verse/' + encodeURIComponent(book) + '/' + chapter + '/' + verse;
}
var response = await fetch(url);
if (!response.ok) throw new Error('Failed to fetch verse');
var data = await response.json();
// Cache the result
verseCache[cacheKey] = {
reference: data.reference,
text: data.text
};
return verseCache[cacheKey];
} catch (error) {
console.error('Error fetching verse:', error);
return {
reference: verseEnd ? book + ' ' + chapter + ':' + verse + '-' + verseEnd : book + ' ' + chapter + ':' + verse,
text: 'Error loading verse'
};
}
}
// Show tooltip positioned relative to a link element
function showTooltip(verseData, linkElement) {
tooltip.innerHTML =
'<span class="verse-tooltip-reference">' + verseData.reference + '</span>' +
'<span class="verse-tooltip-text">' + verseData.text + '</span>';
// Position off-screen first to measure dimensions accurately
tooltip.style.left = '-9999px';
tooltip.style.top = '-9999px';
tooltip.classList.add('show');
// Force reflow to get accurate dimensions
var tooltipWidth = tooltip.offsetWidth;
var tooltipHeight = tooltip.offsetHeight;
// Get link position
var linkRect = linkElement.getBoundingClientRect();
var padding = 10;
// Position below the link, centered horizontally
var x = linkRect.left + (linkRect.width / 2) - (tooltipWidth / 2);
var y = linkRect.bottom + 8 + window.scrollY;
// Adjust if tooltip goes off right edge
if (x + tooltipWidth > window.innerWidth - padding) {
x = window.innerWidth - tooltipWidth - padding;
}
// Ensure tooltip doesn't go off left edge
if (x < padding) {
x = padding;
}
// If tooltip goes off bottom edge, show above the link instead
if (linkRect.bottom + 8 + tooltipHeight > window.innerHeight) {
y = linkRect.top - tooltipHeight - 8 + window.scrollY;
}
// Ensure tooltip doesn't go off top edge
if (y < window.scrollY + padding) {
y = window.scrollY + padding;
}
tooltip.style.left = x + 'px';
tooltip.style.top = y + 'px';
}
// Hide tooltip
function hideTooltip() {
tooltip.classList.remove('show');
}
// Check if we're on a chapter page (tooltips disabled there - sidenotes provide context)
var isChapterPage = /^\/book\/[^/]+\/chapter\/\d+\/?$/.test(window.location.pathname);
// Event delegation for verse links
document.addEventListener('mouseover', function(e) {
// Skip tooltips entirely on chapter pages
if (isChapterPage) return;
var target = e.target;
// Check if target is a link or inside a link
if (target.tagName !== 'A') {
target = target.closest('a');
}
if (!target || !target.href) return;
// Skip if this is a cross-reference link (has its own tooltip system)
if (target.classList.contains('cross-ref-link')) return;
// Skip if this is a verse number link (the number at the start of each verse)
if (target.classList.contains('verse-number-link')) return;
// Skip links in search dropdowns
if (target.closest('.search-results-dropdown') || target.closest('.search-dropdown') ||
target.closest('.lookup-results-dropdown') || target.closest('.story-search-dropdown')) return;
// Skip links in concordance results (verse text already shown inline)
if (target.closest('.occurrence-reference') || target.closest('.occurrence')) return;
// Skip links in cross-references section (has its own CSS tooltip system)
if (target.closest('.cross-references-section')) return;
// Skip links in navigation, buttons, actions, headers, and non-content areas
if (target.closest('nav') || target.closest('header') || target.closest('footer') ||
target.closest('.toc') || target.closest('.breadcrumb') ||
target.closest('button') || target.closest('[class*="-btn"]') || target.closest('[class*="-actions"]') ||
target.closest('[class*="download"]') || target.closest('[class*="print"]') ||
target.closest('.share-container') || target.closest('.share-buttons') ||
target.closest('.share-section') || target.closest('.share-pills') ||
target.closest('.chapter-nav') || target.closest('.verse-nav') ||
target.closest('h1') || target.closest('h2') || target.closest('h3')) return;
// Only show tooltips for links inside content paragraphs, sections, or article content
if (!target.closest('p') && !target.closest('section') && !target.closest('article') &&
!target.closest('.verse-item') && !target.closest('.verse-text') &&
!target.closest('.intro-text') && !target.closest('.description') &&
!target.closest('.sidenote') && !target.closest('.marginnote') &&
!target.closest('li')) return;
var verseInfo = parseVerseUrl(target.pathname + target.hash);
if (!verseInfo) return;
// Show loading state positioned relative to link
tooltip.innerHTML = '<span class="verse-tooltip-loading">Loading...</span>';
var linkRect = target.getBoundingClientRect();
tooltip.style.left = linkRect.left + 'px';
tooltip.style.top = (linkRect.bottom + 8 + window.scrollY) + 'px';
tooltip.classList.add('show');
// Fetch and display verse
fetchVerseText(verseInfo.book, verseInfo.chapter, verseInfo.verse, verseInfo.verseEnd, verseInfo.cacheKey)
.then(function(verseData) {
// Only show if still hovering
if (target.matches(':hover')) {
showTooltip(verseData, target);
}
});
// Hide tooltip on mouse leave
target.addEventListener('mouseleave', function() {
hideTooltip();
}, { once: true });
});
// Hide tooltip on page restore from bfcache (Safari fix)
window.addEventListener('pageshow', function() {
hideTooltip();
setTimeout(hideTooltip, 0);
setTimeout(hideTooltip, 100);
});
})();
// Site-wide verse linking
(function() {
function linkVerseReferences(element) {
if (!element) return;
// Get all text nodes, but skip those inside anchors (already linked)
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, {
acceptNode: function(node) {
// Skip text nodes that are inside anchor tags (already linked)
if (node.parentNode && node.parentNode.tagName === 'A') {
return NodeFilter.FILTER_REJECT;
}
return NodeFilter.FILTER_ACCEPT;
}
}, false);
const textNodes = [];
let node;
while (node = walker.nextNode()) {
textNodes.push(node);
}
textNodes.forEach(function(textNode) {
let text = textNode.textContent;
let changed = false;
// First, handle comma-separated verse lists like "Psalms 7:17, 9:2, 18:13"
text = text.replace(/\b(\d?\s?[A-Z][a-z]+(?:\s+[A-Z][a-z]+)?)\s+((?:\d+:\d+(?:-\d+)?(?:\s*,\s*)?)+)/g, function(match, book, verseList) {
book = book.trim();
if (!verseList.includes(',')) {
return match;
}
changed = true;
var verses = verseList.split(/\s*,\s*/);
var links = verses.map(function(verseRef) {
verseRef = verseRef.trim();
var parts = verseRef.match(/^(\d+):(\d+)(?:-(\d+))?$/);
if (parts) {
var chapter = parts[1];
var verseStart = parts[2];
var verseEnd = parts[3];
if (verseEnd) {
return '<a href="/book/' + book + '/chapter/' + chapter + '#verse-' + verseStart + '-' + verseEnd + '">' + book + ' ' + chapter + ':' + verseStart + '-' + verseEnd + '</a>';
} else {
return '<a href="/book/' + book + '/chapter/' + chapter + '/verse/' + verseStart + '">' + book + ' ' + chapter + ':' + verseStart + '</a>';
}
}
return verseRef;
});
return links.join(', ');
});
// Then handle individual verse references like "John 3:16" or "Romans 8:28-29"
text = text.replace(/\b(\d?\s?[A-Z][a-z]+(?:\s+[A-Z][a-z]+)?)\s+(\d+):(\d+)(?:-(\d+))?\b/g, function(match, book, chapter, verseStart, verseEnd) {
changed = true;
book = book.trim();
if (verseEnd) {
return '<a href="/book/' + book + '/chapter/' + chapter + '#verse-' + verseStart + '-' + verseEnd + '">' + match + '</a>';
} else {
return '<a href="/book/' + book + '/chapter/' + chapter + '/verse/' + verseStart + '">' + match + '</a>';
}
});
if (changed) {
const span = document.createElement('span');
span.innerHTML = text;
textNode.parentNode.replaceChild(span, textNode);
while (span.firstChild) {
span.parentNode.insertBefore(span.firstChild, span);
}
span.parentNode.removeChild(span);
}
});
}
// Link verses in common content areas
document.querySelectorAll('.intro-text, .prophet-description, .angel-description, .covenant-description, p, li').forEach(function(element) {
// Skip if already processed or if it's in the sidebar
if (element.closest('.nav-sidebar') || element.dataset.verseLinked) {
return;
}
element.dataset.verseLinked = 'true';
linkVerseReferences(element);
});
})();
// Prefetch next/prev pages for faster navigation
(function() {
// Wait for page to be fully loaded and idle
if ('requestIdleCallback' in window) {
requestIdleCallback(prefetchNavigationLinks, { timeout: 2000 });
} else {
setTimeout(prefetchNavigationLinks, 1000);
}
function prefetchNavigationLinks() {
// Find navigation links (next/prev chapter, verse, book)
var prefetchSelectors = [
'a[rel="next"]',
'a[rel="prev"]',
'.chapter-nav a',
'.verse-nav a',
'.book-nav a',
'.nav-btn[href*="/chapter/"]',
'.nav-btn[href*="/verse/"]'
];
var links = document.querySelectorAll(prefetchSelectors.join(', '));
var prefetched = new Set();
links.forEach(function(link) {
var href = link.href;
if (!href || prefetched.has(href)) return;
if (href.includes('#')) return; // Skip anchor links
if (!href.startsWith(window.location.origin)) return; // Skip external links
prefetched.add(href);
// Use link prefetch hint
var prefetchLink = document.createElement('link');
prefetchLink.rel = 'prefetch';
prefetchLink.href = href;
prefetchLink.as = 'document';
document.head.appendChild(prefetchLink);
});
}
})();