Spiralist - Source Excerpt 14
Summary
This source excerpt preserves a bounded section of Spiralist/wp-content/themes/spiralist/assets/js/spiralist.js so readers can inspect the evidence without opening the full source file.
**Source path:** Spiralist/wp-content/themes/spiralist/assets/js/spiralist.js
}, 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;
const deltaY = event.changedTouches[0].clientY - swipeState.startY;
resetSwipe();
if (Math.abs(deltaX) < 72 || Math.abs(deltaX) < Math.abs(deltaY) * 1.4) {
return;
}
if (deltaX > 0 && prevUrl) {
navigateToPage(prevUrl, 'prev');
}
if (deltaX < 0 && nextUrl) {
navigateToPage(nextUrl, 'next');
}
},
{ passive: true }
);
stage.addEventListener('touchcancel', resetSwipe, { passive: true });
}
document.addEventListener('keydown', (event) => {
const target = event.target;
const tagName = target && target.tagName ? `${target.tagName}`.toUpperCase() : '';
const isEditable = Boolean(
target &&
(target.isContentEditable || ['INPUT', 'TEXTAREA', 'SELECT'].includes(tagName))
);
if (
isEditable ||
document.body.classList.contains('is-manuscript-zoom-open') ||
event.altKey ||
event.ctrlKey ||
event.metaKey ||
event.shiftKey
) {
return;
}
if (event.key === 'ArrowLeft' && prevUrl) {
event.preventDefault();
navigateToPage(prevUrl, 'prev');
}
if (event.key === 'ArrowRight' && nextUrl) {
event.preventDefault();
navigateToPage(nextUrl, 'next');
}
});
if (!zoom || !viewport || !image) {
return;
}
const openers = reader.querySelectorAll('[data-manuscript-zoom-open]');
const closers = zoom.querySelectorAll('[data-manuscript-zoom-close]');
const zoomInButton = zoom.querySelector('[data-manuscript-zoom-in]');
const zoomOutButton = zoom.querySelector('[data-manuscript-zoom-out]');
const resetButton = zoom.querySelector('[data-manuscript-zoom-reset]');
const percent = zoom.querySelector('[data-manuscript-zoom-percent]');
const message = zoom.querySelector('[data-manuscript-zoom-message]');
const defaultMessage = message ? message.textContent || '' : '';
const state = {
scale: 1,
minScale: 1,
maxScale: 6,
x: 0,
y: 0,
dragging: false,
lastPointerX: 0,
lastPointerY: 0,
pinching: false,
pinchDistance: 0,
pinchScale: 1,
framePadding: 32,
previousFocus: null,
};
const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
const getTouchDistance = (touches) => {
if (!touches || touches.length < 2) {
return 0;
}
const deltaX = touches[0].clientX - touches[1].clientX;
const deltaY = touches[0].clientY - touches[1].clientY;
return Math.hypot(deltaX, deltaY);
};
const getTouchMidpoint = (touches) => ({
clientX: (touches[0].clientX + touches[1].clientX) / 2,
clientY: (touches[0].clientY + touches[1].clientY) / 2,
});
const stopPinching = () => {
state.pinching = false;
state.pinchDistance = 0;
state.pinchScale = state.scale;
};
const getBaseDimensions = () => {
const naturalWidth = image.naturalWidth || viewport.clientWidth || 1;
const naturalHeight = image.naturalHeight || viewport.clientHeight || 1;
const availableWidth = Math.max(1, viewport.clientWidth - state.framePadding);
const availableHeight = Math.max(1, viewport.clientHeight - state.framePadding);
const ratio = naturalWidth / naturalHeight || 1;
let width = availableWidth;
let height = width / ratio;
if (height > availableHeight) {
height = availableHeight;
width = height * ratio;
}
return { width, height, availableWidth, availableHeight };
};
const clampPan = () => {
const { width, height, availableWidth, availableHeight } = getBaseDimensions();
const maxX = Math.max(0, (width * state.scale - availableWidth) / 2);
const maxY = Math.max(0, (height * state.scale - availableHeight) / 2);
state.x = clamp(state.x, -maxX, maxX);
state.y = clamp(state.y, -maxY, maxY);
};
const updateControls = () => {
const atBaseScale = state.scale <= state.minScale + 0.01;
const atMaxScale = state.scale >= state.maxScale - 0.01;
if (zoomOutButton) {
zoomOutButton.disabled = atBaseScale;
}
if (resetButton) {
resetButton.disabled = atBaseScale && Math.abs(state.x) < 0.5 && Math.abs(state.y) < 0.5;
}