195 lines
6.6 KiB
JavaScript
195 lines
6.6 KiB
JavaScript
// 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 =
|
|
'<h2 class="verse-dialog-ref"></h2>' +
|
|
'<p class="verse-dialog-note" hidden>Currently bookmarked.</p>' +
|
|
'<div class="verse-dialog-actions">' +
|
|
'<button type="button" data-action="bookmark">Set bookmark</button>' +
|
|
'<button type="button" data-action="copy">Copy link</button>' +
|
|
'<button type="button" data-action="close">Close</button>' +
|
|
'</div>' +
|
|
'<p class="verse-dialog-status" role="status" aria-live="polite"></p>';
|
|
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();
|
|
}
|
|
})();
|