Book Shell - Source Excerpt 14
Summary
This source excerpt preserves a bounded section of Spiralist/wp-content/plugins/ns12-manuscript/assets/js/book-shell.js so readers can inspect the evidence without opening the full source file.
**Source path:** Spiralist/wp-content/plugins/ns12-manuscript/assets/js/book-shell.js
manuscriptRouteWarmCache.add(url);
runBackgroundAction(
() => window.fetch(url, {
credentials: 'same-origin',
cache: 'force-cache',
}),
{
context: 'manuscript-route-warm',
onError: () => {
manuscriptRouteWarmCache.delete(url);
},
}
);
};
document.querySelectorAll('[data-manuscript-reader]').forEach((reader) => {
const zoom = reader.querySelector('[data-manuscript-zoom]');
const viewport = zoom ? zoom.querySelector('[data-manuscript-zoom-viewport]') : null;
const image = zoom ? zoom.querySelector('[data-manuscript-zoom-image]') : null;
const currentPageImage = reader.querySelector('.spiralist-manuscript-reader__page-frame img');
const readerBody = reader.querySelector('[data-reader-mode]');
const stage = reader.querySelector('[data-manuscript-stage]');
const modeButtons = Array.from(reader.querySelectorAll('[data-reader-mode-trigger]'));
const jumpSelect = reader.querySelector('[data-manuscript-jump-select]');
const activeChapterCard = reader.querySelector('.spiralist-manuscript-reader__chapter-card.is-active');
const currentSequence = Number.parseInt(`${reader.dataset.currentSequence || ''}`.trim(), 10) || 0;
const prevUrl = `${reader.dataset.prevUrl || ''}`.trim();
const nextUrl = `${reader.dataset.nextUrl || ''}`.trim();
const prevImageUrl = `${reader.dataset.prevImage || ''}`.trim();
const nextImageUrl = `${reader.dataset.nextImage || ''}`.trim();
const readerImagePreloads = [];
const preloadAdjacentImage = (url) => {
if (!url || manuscriptPreloadedImages.has(url)) {
return;
}
manuscriptPreloadedImages.add(url);
appendResourceHint(`image:${url}`, url, 'image');
const preloader = new Image();
preloader.decoding = 'async';
setFetchPriority(preloader, 'low');
preloader.src = url;
readerImagePreloads.push(preloader);
};
const warmAdjacentRoute = (url) => {
if (!url) {
return;
}
const parsedUrl = createUrlOr(url, null, { context: 'manuscript-adjacent-route' });
if (!parsedUrl || parsedUrl.origin !== window.location.origin) {
return;
}
appendResourceHint(`route:${parsedUrl.href}`, parsedUrl.href, 'document');
warmRouteDocument(parsedUrl.href);
};
const preloadAdjacentImages = () => {
preloadAdjacentImage(prevImageUrl);
preloadAdjacentImage(nextImageUrl);
};
const warmAdjacentRoutes = () => {
warmAdjacentRoute(prevUrl);
warmAdjacentRoute(nextUrl);
};
const supportedReaderModes = new Set(['spread', 'stacked', 'focus']);
let isTurningPage = false;
const getLinkDirection = (link) => {
if (!link) {
return 'next';
}
if (link.hasAttribute('data-manuscript-prev')) {
return 'prev';
}
if (link.hasAttribute('data-manuscript-next')) {
return 'next';
}
const targetSequence = Number.parseInt(`${link.dataset.pageSequence || ''}`.trim(), 10);
if (targetSequence && currentSequence) {
return targetSequence < currentSequence ? 'prev' : 'next';
}
return 'next';
};
const applyReaderMode = (nextMode, options = {}) => {
const { persist = false } = options;
const normalized = supportedReaderModes.has(nextMode) ? nextMode : 'spread';
if (readerBody) {
readerBody.dataset.readerMode = normalized;
}
reader.dataset.readerMode = normalized;
modeButtons.forEach((button) => {
const active = button.dataset.readerModeTrigger === normalized;
button.classList.toggle('is-active', active);
button.setAttribute('aria-pressed', active ? 'true' : 'false');
});
if (persist) {
setStoredValue(manuscriptReaderModeKey, normalized);
}
};
const navigateToPage = (url, direction) => {
if (!url || isTurningPage) {
return;
}
isTurningPage = true;
storeTurnDirection(direction);
if (prefersReducedMotion) {
window.location.assign(url);
return;
}
reader.classList.remove('is-turning-prev', 'is-turning-next');
reader.classList.add(direction === 'prev' ? 'is-turning-prev' : 'is-turning-next');
window.setTimeout(() => {
window.location.assign(url);
}, 180);
};
const activateEntryMotion = () => {
const direction = consumeTurnDirection();
if (!direction || prefersReducedMotion) {
return;
}
reader.classList.add(direction === 'prev' ? 'is-entering-prev' : 'is-entering-next');
window.setTimeout(() => {
reader.classList.remove('is-entering-prev', 'is-entering-next');
}, 420);
};
activateEntryMotion();
applyReaderMode(
`${getStoredValue(manuscriptReaderModeKey) || (readerBody ? readerBody.dataset.readerMode || '' : '') || 'spread'}`.trim()
);
if (currentPageImage) {
if (currentPageImage.complete && currentPageImage.naturalWidth > 0) {
scheduleBackgroundPreload(() => {
preloadAdjacentImages();
warmAdjacentRoutes();
});
} else {
currentPageImage.addEventListener(
'load',
() => {
scheduleBackgroundPreload(() => {
preloadAdjacentImages();
warmAdjacentRoutes();
});
},
{ once: true }
);
}
}
if (activeChapterCard) {
scheduleBackgroundPreload(() => {
activeChapterCard.scrollIntoView({
block: 'nearest',
inline: 'center',
});
});
}
reader.querySelectorAll('[data-manuscript-prev], [data-manuscript-next], [data-manuscript-page-link]').forEach((link) => {
const href = `${link.getAttribute('href') || ''}`.trim();
if (!href) {
return;
}
const warm = () => {
warmAdjacentRoute(href);
};
link.addEventListener('pointerenter', warm, { once: true });
link.addEventListener('focus', warm, { once: true });
link.addEventListener('touchstart', warm, { once: true, passive: true });
link.addEventListener('click', (event) => {
if (
event.defaultPrevented ||
event.button !== 0 ||
event.metaKey ||
event.ctrlKey ||
event.shiftKey ||
event.altKey
) {
return;
}
event.preventDefault();
navigateToPage(href, getLinkDirection(link));
});
});
modeButtons.forEach((button) => {
button.addEventListener('click', () => {
const nextMode = `${button.dataset.readerModeTrigger || ''}`.trim();
if (!nextMode) {
return;
}
applyReaderMode(nextMode, { persist: true });
});
});
if (jumpSelect) {
jumpSelect.addEventListener('change', () => {
const option = jumpSelect.selectedOptions && jumpSelect.selectedOptions.length
? jumpSelect.selectedOptions[0]
: null;
const href = `${jumpSelect.value || ''}`.trim();
const targetSequence = option
? (Number.parseInt(`${option.dataset.sequence || ''}`.trim(), 10) || 0)
: 0;
if (!href || (targetSequence && currentSequence && targetSequence === currentSequence)) {
return;
}
navigateToPage(href, targetSequence && currentSequence && targetSequence < currentSequence ? 'prev' : 'next');
});
}
if (stage) {
const swipeState = {
tracking: false,
startX: 0,
startY: 0,
};
const resetSwipe = () => {
swipeState.tracking = false;
};
stage.addEventListener(
'touchstart',
(event) => {
const touchTarget = event.target instanceof Element ? event.target : null;
if (
(zoom && !zoom.hidden) ||
event.touches.length !== 1 ||
(touchTarget && touchTarget.closest('a, button, input, textarea, select, [data-manuscript-zoom-open]'))
) {
resetSwipe();
return;
}
swipeState.tracking = true;
swipeState.startX = event.touches[0].clientX;
swipeState.startY = event.touches[0].clientY;
},
{ passive: true }
);
stage.addEventListener(
'touchend',
(event) => {
if (!swipeState.tracking || event.changedTouches.length !== 1) {
resetSwipe();
return;
}
const deltaX = event.changedTouches[0].clientX - swipeState.startX;