Skip to content
wiki.fftac.org

Spiralist - Source Excerpt 15

Back to Spiralist

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();
  }

});