Add TOC drill-down keyboard navigation on resource index pages

- TOC is now a selectable block that can be navigated to with arrow keys
- Press Enter or Right arrow to drill into individual TOC entries
- Navigate TOC entries with Up/Down, Enter or Right to follow link
- Left arrow exits TOC items back to TOC block

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-30 00:27:26 -05:00
parent 58b9c316a6
commit 44bfa73157
+112 -24
View File
@@ -281,6 +281,7 @@
<script>
document.addEventListener('DOMContentLoaded', function() {
const tocList = document.getElementById('toc-list');
const toc = document.getElementById('toc');
const headings = document.querySelectorAll('section h2, section h3, section article h3');
headings.forEach((heading, index) => {
@@ -298,21 +299,59 @@ document.addEventListener('DOMContentLoaded', function() {
tocList.appendChild(li);
});
// Keyboard navigation for intro, description paragraphs, and verses
const elements = Array.from(document.querySelectorAll('.intro-text, .resource-item-description p, .verse-item'));
if (elements.length === 0) return;
// Two-section navigation: TOC block -> content elements
// Or when inside TOC: individual TOC entries
const contentElements = Array.from(document.querySelectorAll('.intro-text, .resource-item-description p, .verse-item'));
const tocItems = Array.from(tocList.querySelectorAll('li a'));
let currentSection = 'content'; // 'content' or 'toc'
let selectedIndex = -1;
let selectedTocIndex = -1;
let tocBlockSelected = false;
function selectElement(index) {
if (selectedIndex >= 0 && selectedIndex < elements.length) {
elements[selectedIndex].style.outline = '';
elements[selectedIndex].style.outlineOffset = '';
function clearAllSelections() {
if (selectedIndex >= 0 && selectedIndex < contentElements.length) {
contentElements[selectedIndex].style.outline = '';
contentElements[selectedIndex].style.outlineOffset = '';
}
selectedIndex = Math.max(0, Math.min(index, elements.length - 1));
elements[selectedIndex].style.outline = '2px solid #4a7c59';
elements[selectedIndex].style.outlineOffset = '8px';
elements[selectedIndex].scrollIntoView({ behavior: 'smooth', block: 'center' });
if (tocBlockSelected) {
toc.style.outline = '';
toc.style.outlineOffset = '';
tocBlockSelected = false;
}
tocItems.forEach(item => {
item.style.outline = '';
item.style.background = '';
});
}
function selectTocBlock() {
clearAllSelections();
currentSection = 'toc';
tocBlockSelected = true;
selectedTocIndex = -1;
toc.style.outline = '2px solid #4a7c59';
toc.style.outlineOffset = '8px';
toc.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
function selectTocItem(index) {
clearAllSelections();
currentSection = 'toc';
tocBlockSelected = false;
selectedTocIndex = Math.max(0, Math.min(index, tocItems.length - 1));
tocItems[selectedTocIndex].style.background = 'rgba(74, 124, 89, 0.15)';
tocItems[selectedTocIndex].style.outline = '2px solid #4a7c59';
tocItems[selectedTocIndex].scrollIntoView({ behavior: 'smooth', block: 'center' });
}
function selectContentElement(index) {
clearAllSelections();
currentSection = 'content';
selectedIndex = Math.max(0, Math.min(index, contentElements.length - 1));
contentElements[selectedIndex].style.outline = '2px solid #4a7c59';
contentElements[selectedIndex].style.outlineOffset = '8px';
contentElements[selectedIndex].scrollIntoView({ behavior: 'smooth', block: 'center' });
}
document.addEventListener('keydown', function(e) {
@@ -320,27 +359,76 @@ document.addEventListener('DOMContentLoaded', function() {
if (e.key === 'ArrowDown' || e.key === 'j') {
e.preventDefault();
selectElement(selectedIndex < 0 ? 0 : selectedIndex + 1);
if (currentSection === 'toc') {
if (tocBlockSelected) {
// Move from TOC block to first content element
selectContentElement(0);
} else {
// Navigate within TOC items
selectTocItem(selectedTocIndex + 1);
}
} else {
// Navigate content elements
selectContentElement(selectedIndex < 0 ? 0 : selectedIndex + 1);
}
} else if (e.key === 'ArrowUp' || e.key === 'k') {
e.preventDefault();
if (selectedIndex <= 0) selectElement(0);
else selectElement(selectedIndex - 1);
if (currentSection === 'toc') {
if (tocBlockSelected) {
// Already at TOC block, stay there
selectTocBlock();
} else if (selectedTocIndex <= 0) {
// At first TOC item, go back to TOC block
selectTocBlock();
} else {
selectTocItem(selectedTocIndex - 1);
}
} else {
if (selectedIndex <= 0) {
// At first content element, go to TOC block
selectTocBlock();
} else {
selectContentElement(selectedIndex - 1);
}
}
} else if (e.key === 'ArrowLeft' || e.key === 'h') {
e.preventDefault();
history.back();
} else if (e.key === 'Enter' && selectedIndex >= 0) {
if (currentSection === 'toc' && !tocBlockSelected) {
// Exit TOC items back to TOC block
selectTocBlock();
} else {
history.back();
}
} else if (e.key === 'ArrowRight' || e.key === 'l') {
e.preventDefault();
var el = elements[selectedIndex];
// Check for link in resource entry title or verse reference
var link = el.querySelector('.resource-name a') || el.querySelector('.verse-ref a') || el.querySelector('a');
if (link) window.location.href = link.href;
if (tocBlockSelected) {
// Drill into TOC items
selectTocItem(0);
} else if (currentSection === 'toc' && selectedTocIndex >= 0) {
// Navigate to the linked section
var href = tocItems[selectedTocIndex].getAttribute('href');
if (href) window.location.href = href;
}
} else if (e.key === 'Enter') {
e.preventDefault();
if (tocBlockSelected) {
// Drill into TOC items
selectTocItem(0);
} else if (currentSection === 'toc' && selectedTocIndex >= 0) {
// Navigate to the linked section
var href = tocItems[selectedTocIndex].getAttribute('href');
if (href) window.location.href = href;
} else if (selectedIndex >= 0) {
var el = contentElements[selectedIndex];
var link = el.querySelector('.resource-name a') || el.querySelector('.verse-ref a') || el.querySelector('a');
if (link) window.location.href = link.href;
}
} else if (e.key === 'Escape') {
e.preventDefault();
if (selectedIndex >= 0 && selectedIndex < elements.length) {
elements[selectedIndex].style.outline = '';
elements[selectedIndex].style.outlineOffset = '';
}
clearAllSelections();
currentSection = 'content';
selectedIndex = -1;
selectedTocIndex = -1;
}
});
});