// Bookmark UI. Deliberately small and dependency-free: the state is one // verse reference, so it lives in localStorage rather than IndexedDB -- // localStorage reads synchronously, which lets the index page paint the // bookmark link on first paint instead of flashing an empty slot. (function () { 'use strict'; var KEY = 'bible:bookmark'; var dialog = null; var current = null; // localStorage throws (not returns null) in some private-browsing modes, // so every access is guarded and the feature degrades to a no-op. function readBookmark() { try { return JSON.parse(localStorage.getItem(KEY) || 'null'); } catch (e) { return null; } } function writeBookmark(value) { try { localStorage.setItem(KEY, JSON.stringify(value)); return true; } catch (e) { return false; } } // -- index page --------------------------------------------------------- function renderBookmarkLink() { var slot = document.getElementById('bookmark'); if (!slot) return; var mark = readBookmark(); slot.textContent = ''; if (!mark || !mark.href) { slot.hidden = true; return; } var label = document.createElement('span'); label.className = 'bookmark-label'; label.textContent = 'Bookmark'; var link = document.createElement('a'); link.className = 'bookmark-link'; link.href = mark.href; link.textContent = mark.ref; slot.appendChild(label); slot.appendChild(document.createTextNode(' ')); slot.appendChild(link); slot.hidden = false; } // -- dialog ------------------------------------------------------------- function buildDialog() { var d = document.createElement('dialog'); d.id = 'verse-dialog'; d.className = 'verse-dialog'; d.innerHTML = '

' + '' + '
' + '' + '' + '' + '
' + '

'; document.body.appendChild(d); d.addEventListener('click', onDialogClick); return d; } function status(text) { var el = dialog.querySelector('.verse-dialog-status'); el.textContent = text || ''; return el; } function closeSoon() { setTimeout(function () { if (dialog.open) dialog.close(); }, 900); } function onDialogClick(event) { // A click landing on the dialog itself is the backdrop, not the content. if (event.target === dialog) { dialog.close(); return; } var button = event.target.closest('button[data-action]'); if (!button) return; var action = button.getAttribute('data-action'); if (action === 'close') { dialog.close(); } else if (action === 'bookmark') { var saved = writeBookmark({ ref: current.ref, href: current.href, savedAt: Date.now() }); status(saved ? 'Bookmark set.' : 'Could not save: storage unavailable.'); renderBookmarkLink(); dialog.querySelector('.verse-dialog-note').hidden = !saved; if (saved) closeSoon(); } else if (action === 'copy') { copyLink(current.url); } } // navigator.clipboard only exists in a secure context, so over plain http // this always falls through to a selectable field rather than failing. function copyLink(url) { if (navigator.clipboard && window.isSecureContext) { navigator.clipboard.writeText(url).then(function () { status('Link copied.'); closeSoon(); }, function () { offerManualCopy(url); }); } else { offerManualCopy(url); } } function offerManualCopy(url) { var el = status('Copy this link:'); var field = document.createElement('input'); field.type = 'text'; field.readOnly = true; field.value = url; field.className = 'verse-dialog-url'; el.appendChild(document.createTextNode(' ')); el.appendChild(field); field.focus(); field.select(); } // -- verse numbers ------------------------------------------------------ function verseInfo(span) { var article = document.querySelector('article.book'); var section = span.closest('.chapter'); var book = article ? (article.getAttribute('data-name') || article.getAttribute('data-book')) : ''; var chapter = section ? section.getAttribute('data-chapter') : ''; var page = location.pathname.split('/').pop() || 'index.html'; return { ref: book + ' ' + chapter + ':' + (span.textContent || '').trim(), // Relative, so a saved bookmark survives a change of host or port. href: page + '#' + span.id, url: location.origin + location.pathname + '#' + span.id }; } function openFor(span) { if (!dialog) dialog = buildDialog(); current = verseInfo(span); dialog.querySelector('.verse-dialog-ref').textContent = current.ref; var mark = readBookmark(); dialog.querySelector('.verse-dialog-note').hidden = !(mark && mark.href === current.href); status(''); if (typeof dialog.showModal === 'function') dialog.showModal(); else dialog.setAttribute('open', ''); } function initVerses() { var verses = document.querySelectorAll('.chapter .v'); if (!verses.length) return; // Applied here rather than baked into the HTML: without this script the // numbers are plain text, and announcing them as buttons would lie. for (var i = 0; i < verses.length; i++) { verses[i].setAttribute('role', 'button'); verses[i].setAttribute('tabindex', '0'); verses[i].setAttribute('aria-label', 'Verse ' + (verses[i].textContent || '').trim() + ', options'); } document.addEventListener('click', function (event) { var verse = event.target.closest ? event.target.closest('.v') : null; if (verse && verse.id) { event.preventDefault(); openFor(verse); } }); document.addEventListener('keydown', function (event) { if (event.key !== 'Enter' && event.key !== ' ') return; var el = document.activeElement; if (el && el.classList && el.classList.contains('v') && el.id) { event.preventDefault(); openFor(el); } }); } function init() { renderBookmarkLink(); initVerses(); } // Another tab changing the bookmark updates this one's index link. window.addEventListener('storage', function (event) { if (event.key === KEY) renderBookmarkLink(); }); if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();