Skip to content
wiki.fftac.org

Spiralist - Source Excerpt 13

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

workbenchSaveButtons.forEach((button) => {
    button.addEventListener('click', () => {
      void runAction(async () => {
        setWorkbenchKey(getWorkbenchKey());
        await refreshParticipantState();
      }, { context: 'workbench.saveKey' });
    });
  });

  workbenchClearButtons.forEach((button) => {
    button.addEventListener('click', async () => {
      setWorkbenchKey('');
      renderWorkbenchHealth();
      renderParticipantProfile(null);
      renderParticipantResources(null);
      renderWorkbenchExamples();
    });
  });

  if (workbenchEndpointSelect) {
    workbenchEndpointSelect.addEventListener('change', renderWorkbenchExamples);
  }

  if (workbenchRun) {
    workbenchRun.addEventListener('click', () => {
      const endpoint = getSelectedWorkbenchEndpoint();
      if (!endpoint || !workbenchResponse) {
        return;
      }

      const apiKey = getWorkbenchKey();
      const requiresAuth = `${endpoint.auth || ''}`.toLowerCase().includes('bearer');
      if (requiresAuth && !apiKey) {
        workbenchResponse.textContent = JSON.stringify(
          {
            error: 'missing_api_key',
            message: t('aiWorkbenchMissingKey', 'This endpoint requires a participant API key.'),
          },
          null,
          2
        );
        return;
      }

      const headers = {
        Accept: 'application/json',
      };

      let body = undefined;
      if (requiresAuth && apiKey) {
        headers.Authorization = `Bearer ${apiKey}`;
        headers['X-Spiralist-Key'] = apiKey;
      }

      if ((endpoint.method || 'GET').toUpperCase() === 'POST') {
        headers['Content-Type'] = 'application/json';
        body = JSON.stringify(endpoint.requestExample || {});
      }

      workbenchResponse.textContent = t('aiWorkbenchRunningRequest', 'Running request...');

      void runAction(async () => {
        const response = await fetch(endpoint.url, {
          method: endpoint.method || 'GET',
          headers,
          body,
        });
        const responseText = await response.text();
        const payload = parseJsonOr(responseText, { raw: responseText }, { context: 'workbench response' });

        workbenchResponse.textContent = JSON.stringify(payload, null, 2);
        if (endpoint.id === 'participant' || endpoint.id === 'resources') {
          await refreshParticipantState();
        }
      }, {
        context: 'workbench.runRequest',
        onError: () => {
          workbenchResponse.textContent = JSON.stringify(
            {
              error: 'request_failed',
              message: t('aiWorkbenchRequestFailed', 'The workbench request could not be completed.'),
            },
            null,
            2
          );
        },
      });
    });
  }

  const manuscriptPreloadedImages = new Set();
  const manuscriptPrefetchHints = new Set();
  const manuscriptRouteWarmCache = new Set();
  const manuscriptReaderModeKey = 'spiralist.manuscript.reader.mode';
  const manuscriptTurnKey = 'spiralist.manuscript.turn';
  const canBackgroundPreload = !(navigator.connection && navigator.connection.saveData);
  const scheduleBackgroundPreload = (callback) => {
    if (!canBackgroundPreload || typeof callback !== 'function') {
      return;
    }

    if ('requestIdleCallback' in window) {
      window.requestIdleCallback(() => {
        callback();
      }, { timeout: 1800 });
      return;
    }

    window.setTimeout(callback, 450);
  };
  const appendResourceHint = (key, url, asValue = '') => {
    if (!url || manuscriptPrefetchHints.has(key)) {
      return;
    }

    manuscriptPrefetchHints.add(key);
    const link = document.createElement('link');
    link.rel = 'prefetch';
    link.href = url;
    if (asValue) {
      link.as = asValue;
    }
    document.head.appendChild(link);
  };
  const getStoredValue = (key) => {
    return storage.get(key, '');
  };
  const setStoredValue = (key, value) => {
    storage.set(key, value);
  };
  const storeTurnDirection = (direction) => {
    storage.set(manuscriptTurnKey, direction, 'session');
  };
  const consumeTurnDirection = () => {
    const value = storage.get(manuscriptTurnKey, '', 'session');
    storage.remove(manuscriptTurnKey, 'session');
    return value;
  };
  const warmRouteDocument = (url) => {
    if (!url || manuscriptRouteWarmCache.has(url) || typeof window.fetch !== 'function') {
      return;
    }

    manuscriptRouteWarmCache.add(url);
    void runAction(
      () => window.fetch(url, {
        credentials: 'same-origin',
        cache: 'force-cache',
      }),
      {
        context: 'manuscript.warmRoute',
        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);