Skip to content
wiki.fftac.org

Book Shell - Source Excerpt 14

Back to Book Shell

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;