Spiralist - Source Excerpt 15
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
if (zoomInButton) {
zoomInButton.disabled = atMaxScale;
}
if (percent) {
percent.textContent = `${Math.round(state.scale * 100)}%`;
}
if (message) {
message.textContent = atBaseScale
? defaultMessage
: t('manuscriptZoomPanInstructions', 'Drag or pinch to pan across the page. Use the arrow keys to nudge the view and press 0 to reset.');
}
};
const render = () => {
clampPan();
viewport.classList.toggle('is-zoomed', state.scale > state.minScale + 0.01);
viewport.classList.toggle('is-dragging', state.dragging);
image.style.transform = `translate(-50%, -50%) translate(${state.x}px, ${state.y}px) scale(${state.scale})`;
updateControls();
};
const fitToViewport = () => {
stopPinching();
state.scale = state.minScale;
state.x = 0;
state.y = 0;
render();
};
const zoomTo = (nextScale, clientX = null, clientY = null) => {
const clampedScale = clamp(nextScale, state.minScale, state.maxScale);
if (Math.abs(clampedScale - state.scale) < 0.001) {
return;
}
const rect = viewport.getBoundingClientRect();
const originX = typeof clientX === 'number' ? clientX - rect.left - rect.width / 2 : 0;
const originY = typeof clientY === 'number' ? clientY - rect.top - rect.height / 2 : 0;
const ratio = clampedScale / state.scale;
state.x = originX - (originX - state.x) * ratio;
state.y = originY - (originY - state.y) * ratio;
state.scale = clampedScale;
render();
};
const stopDragging = (event = null) => {
if (!state.dragging) {
return;
}
state.dragging = false;
if (event) {
releasePointerCapture(viewport, event.pointerId);
}
render();
};
const ensureImageReady = (callback) => {
if (image.complete && image.naturalWidth > 0) {
callback();
return;
}
image.addEventListener(
'load',
() => {
callback();
},
{ once: true }
);
};
const openZoom = () => {
state.previousFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null;
zoom.hidden = false;
zoom.setAttribute('aria-hidden', 'false');
document.body.classList.add('is-manuscript-zoom-open');
ensureImageReady(() => {
fitToViewport();
viewport.focus({ preventScroll: true });
});
};
const closeZoom = () => {
stopDragging();
stopPinching();
fitToViewport();
zoom.hidden = true;
zoom.setAttribute('aria-hidden', 'true');
document.body.classList.remove('is-manuscript-zoom-open');
if (state.previousFocus && typeof state.previousFocus.focus === 'function') {
state.previousFocus.focus({ preventScroll: true });
}
};
openers.forEach((opener) => {
opener.addEventListener('click', (event) => {
event.preventDefault();
openZoom();
});
const tagName = opener.tagName ? `${opener.tagName}`.toUpperCase() : '';
if (tagName !== 'BUTTON' && tagName !== 'A') {
opener.addEventListener('keydown', (event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
openZoom();
}
});
}
});
closers.forEach((closer) => {
closer.addEventListener('click', (event) => {
event.preventDefault();
closeZoom();
});
});
if (zoomInButton) {
zoomInButton.addEventListener('click', () => {
zoomTo(state.scale + 0.25);
});
}
if (zoomOutButton) {
zoomOutButton.addEventListener('click', () => {
zoomTo(state.scale - 0.25);
});
}
if (resetButton) {
resetButton.addEventListener('click', () => {
fitToViewport();
});
}
viewport.addEventListener(
'wheel',
(event) => {
if (zoom.hidden) {
return;
}
event.preventDefault();
const step = event.deltaY < 0 ? 0.2 : -0.2;
zoomTo(state.scale + step, event.clientX, event.clientY);
},
{ passive: false }
);
viewport.addEventListener('dblclick', (event) => {
if (zoom.hidden) {
return;
}
event.preventDefault();
if (state.scale <= state.minScale + 0.01) {
zoomTo(2, event.clientX, event.clientY);
} else {
fitToViewport();
}
});
viewport.addEventListener(
'touchstart',
(event) => {
if (zoom.hidden || event.touches.length !== 2) {
if (event.touches.length < 2) {
stopPinching();
}
return;
}
const distance = getTouchDistance(event.touches);
if (!distance) {
return;
}
state.dragging = false;
state.pinching = true;
state.pinchDistance = distance;
state.pinchScale = state.scale;
},
{ passive: true }
);
viewport.addEventListener(
'touchmove',
(event) => {
if (zoom.hidden || !state.pinching || event.touches.length !== 2) {
return;
}
const distance = getTouchDistance(event.touches);
if (!distance) {
return;
}
const midpoint = getTouchMidpoint(event.touches);
zoomTo(state.pinchScale * (distance / state.pinchDistance), midpoint.clientX, midpoint.clientY);
event.preventDefault();
},
{ passive: false }
);
viewport.addEventListener('touchend', (event) => {
if (event.touches.length < 2) {
stopPinching();
}
});
viewport.addEventListener('touchcancel', () => {
stopPinching();
});
viewport.addEventListener('pointerdown', (event) => {
if (zoom.hidden || state.scale <= state.minScale + 0.01) {
return;
}
if (state.pinching) {
return;
}
if (event.pointerType === 'mouse' && event.button !== 0) {
return;
}
state.dragging = true;
state.lastPointerX = event.clientX;
state.lastPointerY = event.clientY;
setPointerCapture(viewport, event.pointerId);
render();
event.preventDefault();
});
viewport.addEventListener('pointermove', (event) => {
if (!state.dragging) {
return;
}
state.x += event.clientX - state.lastPointerX;
state.y += event.clientY - state.lastPointerY;
state.lastPointerX = event.clientX;
state.lastPointerY = event.clientY;
render();
});
viewport.addEventListener('pointerup', (event) => {
stopDragging(event);
});
viewport.addEventListener('pointercancel', (event) => {
stopDragging(event);
});
window.addEventListener('resize', () => {
if (zoom.hidden) {
return;
}
render();
});
document.addEventListener('keydown', (event) => {
if (zoom.hidden) {
return;
}
if (event.key === 'Escape') {
event.preventDefault();
closeZoom();
return;
}
if (event.altKey || event.ctrlKey || event.metaKey) {
return;
}
if (event.key === '+' || event.key === '=') {
event.preventDefault();
zoomTo(state.scale + 0.25);
return;
}
if (event.key === '-' || event.key === '_') {
event.preventDefault();
zoomTo(state.scale - 0.25);
return;
}
if (event.key === '0') {
event.preventDefault();
fitToViewport();
return;
}
const step = event.shiftKey ? 120 : 60;
if (event.key === 'ArrowLeft') {
event.preventDefault();
state.x += step;
render();
}
if (event.key === 'ArrowRight') {
event.preventDefault();
state.x -= step;
render();
}
if (event.key === 'ArrowUp') {
event.preventDefault();
state.y += step;
render();
}
if (event.key === 'ArrowDown') {
event.preventDefault();
state.y -= step;
render();
}
});
render();
});
if (hasAiWorkbenchSurface) {
renderWorkbenchExamples();
refreshParticipantState();
}
});