// Client-side comic navigation using the API
(function() {
'use strict';
let totalComics = 0;
let comicName = ''; // Will be extracted from initial page title
let currentComicNumber = 0;
// Fetch and display a comic
async function loadComic(comicId) {
try {
const response = await fetch(`/api/comics/${comicId}`);
if (!response.ok) {
if (response.status === 404) {
window.location.href = '/404';
return;
}
throw new Error('Failed to load comic');
}
const comic = response.json ? await response.json() : comic;
displayComic(comic);
} catch (error) {
console.error('Error loading comic:', error);
}
}
// Update the page with comic data
function displayComic(comic) {
const title = comic.title || `#${comic.number}`;
currentComicNumber = comic.number;
// Announce comic change to screen readers
const announcer = document.getElementById('comic-announcer');
if (announcer) {
announcer.textContent = `Loaded comic ${title}, dated ${comic.formatted_date || comic.date}`;
}
// Update container class for full-width option
const container = document.querySelector('.comic-container');
if (comic.full_width) {
container.classList.add('comic-container-fullwidth');
} else {
container.classList.remove('comic-container-fullwidth');
}
// Update container class for plain mode
if (comic.plain) {
container.classList.add('comic-container-plain');
} else {
container.classList.remove('comic-container-plain');
}
// Show/hide header based on plain mode
let header = document.querySelector('.comic-header');
if (comic.plain) {
if (header) {
header.style.display = 'none';
}
} else {
if (header) {
header.style.display = 'block';
} else {
// Create header if it doesn't exist
const newHeader = document.createElement('div');
newHeader.className = 'comic-header';
newHeader.innerHTML = '
';
container.insertBefore(newHeader, container.firstChild);
header = newHeader; // Update reference to the newly created element
}
// Update title and date using the header reference
header.querySelector('h1').textContent = title;
header.querySelector('.comic-date').textContent = comic.date;
}
// Update image and its link
const comicImageDiv = document.querySelector('.comic-image');
updateComicImage(comicImageDiv, comic, title);
// Update or create/remove the link wrapper
updateComicImageLink(comic.number);
// Update author note
let transcriptDiv = document.querySelector('.comic-transcript');
if (comic.author_note) {
if (!transcriptDiv) {
const container = document.querySelector('.comic-container');
const newDiv = document.createElement('div');
newDiv.className = 'comic-transcript';
newDiv.innerHTML = 'Author Note
';
container.appendChild(newDiv);
transcriptDiv = newDiv; // Update reference to the newly created element
}
// Clear existing content after the h3
const h3 = transcriptDiv.querySelector('h3');
while (h3.nextSibling) {
h3.nextSibling.remove();
}
// Add content based on whether it's HTML or plain text
if (comic.author_note_is_html) {
const contentDiv = document.createElement('div');
contentDiv.innerHTML = comic.author_note;
transcriptDiv.appendChild(contentDiv);
} else {
const contentP = document.createElement('p');
contentP.textContent = comic.author_note;
transcriptDiv.appendChild(contentP);
}
transcriptDiv.style.display = 'block';
} else if (transcriptDiv) {
transcriptDiv.style.display = 'none';
}
// Update navigation buttons
updateNavButtons(comic.number, comic.formatted_date);
// Update page title
document.title = `${title} - ${comicName}`;
// Update URL without reload
history.pushState({ comicId: comic.number }, '', `/comic/${comic.number}`);
// Move focus to comic image for keyboard navigation accessibility
const comicImageFocus = document.getElementById('comic-image-focus');
if (comicImageFocus) {
comicImageFocus.focus();
}
}
// Update or create comic image with optional mobile version
function updateComicImage(comicImageDiv, comic, title) {
// Clear all existing content
comicImageDiv.innerHTML = '';
// Create new image element(s)
if (comic.mobile_filename) {
// Create picture element with mobile source
const picture = document.createElement('picture');
const source = document.createElement('source');
source.media = '(max-width: 768px)';
source.srcset = `/static/images/comics/${comic.mobile_filename}`;
const img = document.createElement('img');
img.src = `/static/images/comics/${comic.filename}`;
img.alt = title;
img.title = comic.alt_text;
picture.appendChild(source);
picture.appendChild(img);
comicImageDiv.appendChild(picture);
} else {
// Create regular img element
const img = document.createElement('img');
img.src = `/static/images/comics/${comic.filename}`;
img.alt = title;
img.title = comic.alt_text;
comicImageDiv.appendChild(img);
}
}
// Update comic image link for click navigation
function updateComicImageLink(currentNumber) {
const comicImageDiv = document.querySelector('.comic-image');
const imgOrPicture = comicImageDiv.querySelector('picture') || comicImageDiv.querySelector('img');
// Remove existing link if present
const existingLink = comicImageDiv.querySelector('a');
if (existingLink) {
existingLink.replaceWith(imgOrPicture);
}
// Add link if there's a next comic
if (currentNumber < totalComics) {
const link = document.createElement('a');
link.href = `/comic/${currentNumber + 1}`;
link.onclick = (e) => {
e.preventDefault();
loadComic(currentNumber + 1);
};
imgOrPicture.parentNode.insertBefore(link, imgOrPicture);
link.appendChild(imgOrPicture);
}
}
// Update navigation button states
function updateNavButtons(currentNumber, formattedDate) {
const navButtons = document.querySelector('.nav-buttons');
// Detect if using icon navigation
const isIconNav = navButtons.children[0].classList.contains('btn-icon-nav');
// First button
const firstBtn = navButtons.children[0];
if (currentNumber > 1) {
firstBtn.className = isIconNav ? 'btn-icon-nav' : 'btn btn-nav';
firstBtn.onclick = (e) => { e.preventDefault(); loadComic(1); };
firstBtn.removeAttribute('aria-disabled');
} else {
firstBtn.className = isIconNav ? 'btn-icon-nav btn-icon-disabled' : 'btn btn-nav btn-disabled';
firstBtn.onclick = null;
firstBtn.setAttribute('aria-disabled', 'true');
}
// Previous button
const prevBtn = navButtons.children[1];
if (currentNumber > 1) {
prevBtn.className = isIconNav ? 'btn-icon-nav' : 'btn btn-nav';
prevBtn.onclick = (e) => { e.preventDefault(); loadComic(currentNumber - 1); };
prevBtn.removeAttribute('aria-disabled');
} else {
prevBtn.className = isIconNav ? 'btn-icon-nav btn-icon-disabled' : 'btn btn-nav btn-disabled';
prevBtn.onclick = null;
prevBtn.setAttribute('aria-disabled', 'true');
}
// Comic date display
if (formattedDate) {
navButtons.children[2].textContent = formattedDate;
}
// Next button
const nextBtn = navButtons.children[3];
if (currentNumber < totalComics) {
nextBtn.className = isIconNav ? 'btn-icon-nav' : 'btn btn-nav';
nextBtn.onclick = (e) => { e.preventDefault(); loadComic(currentNumber + 1); };
nextBtn.removeAttribute('aria-disabled');
} else {
nextBtn.className = isIconNav ? 'btn-icon-nav btn-icon-disabled' : 'btn btn-nav btn-disabled';
nextBtn.onclick = null;
nextBtn.setAttribute('aria-disabled', 'true');
}
// Latest button
const latestBtn = navButtons.children[4];
if (currentNumber < totalComics) {
latestBtn.className = isIconNav ? 'btn-icon-nav' : 'btn btn-nav';
latestBtn.onclick = (e) => { e.preventDefault(); loadComic(totalComics); };
latestBtn.removeAttribute('aria-disabled');
} else {
latestBtn.className = isIconNav ? 'btn-icon-nav btn-icon-disabled' : 'btn btn-nav btn-disabled';
latestBtn.onclick = null;
latestBtn.setAttribute('aria-disabled', 'true');
}
}
// Handle keyboard navigation
function handleKeyboardNavigation(event) {
// Don't interfere if user is typing in an input field
if (event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA') {
return;
}
const announcer = document.getElementById('comic-announcer');
switch(event.key) {
case 'ArrowLeft':
// Previous comic
if (currentComicNumber > 1) {
event.preventDefault();
loadComic(currentComicNumber - 1);
} else if (announcer) {
announcer.textContent = 'Already at the first comic';
}
break;
case 'ArrowRight':
// Next comic
if (currentComicNumber < totalComics) {
event.preventDefault();
loadComic(currentComicNumber + 1);
} else if (announcer) {
announcer.textContent = 'Already at the latest comic';
}
break;
case 'Home':
// First comic
if (currentComicNumber > 1) {
event.preventDefault();
loadComic(1);
} else if (announcer) {
announcer.textContent = 'Already at the first comic';
}
break;
case 'End':
// Latest comic
if (currentComicNumber < totalComics) {
event.preventDefault();
loadComic(totalComics);
} else if (announcer) {
announcer.textContent = 'Already at the latest comic';
}
break;
}
}
// Initialize on page load
async function init() {
// Only run on comic pages
if (!document.querySelector('.comic-container')) {
return;
}
// Extract comic name from initial page title (e.g., "Latest Comic - Sunday Comics" -> "Sunday Comics")
const titleParts = document.title.split(' - ');
if (titleParts.length > 1) {
comicName = titleParts[titleParts.length - 1];
}
// Get total comics count from the page
totalComics = parseInt(document.querySelector('.comic-container').dataset.totalComics || 0);
// Get current comic number
const currentNumber = parseInt(document.querySelector('.comic-container').dataset.comicNumber || 0);
currentComicNumber = currentNumber;
if (currentNumber && totalComics) {
// Get the formatted date from the DOM (already rendered by server)
const dateDisplay = document.querySelector('.comic-date-display');
const formattedDate = dateDisplay ? dateDisplay.textContent : null;
updateNavButtons(currentNumber, formattedDate);
updateComicImageLink(currentNumber);
}
// Handle browser back/forward
window.addEventListener('popstate', (event) => {
if (event.state && event.state.comicId) {
loadComic(event.state.comicId);
}
});
// Handle keyboard navigation
document.addEventListener('keydown', handleKeyboardNavigation);
// Set initial state
history.replaceState({ comicId: currentNumber }, '', window.location.pathname);
}
// Run when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();