Skip to content
wiki.fftac.org

Manuscript Workstation - Source Excerpt 02

Back to Manuscript Workstation

Summary

This source excerpt preserves a bounded section of Spiralist/wp-content/plugins/ns12-manuscript/assets/js/manuscript-workstation.js so readers can inspect the evidence without opening the full source file.

**Source path:** Spiralist/wp-content/plugins/ns12-manuscript/assets/js/manuscript-workstation.js

nextUrl.searchParams.set('refresh', `${Date.now()}`);
    return nextUrl.toString();
  };
  const imageLoadCache = new Map();
  const preloadImageAsset = (url) => {
    if (!url) {
      return Promise.resolve('');
    }

    if (imageLoadCache.has(url)) {
      return imageLoadCache.get(url);
    }

    const promise = new Promise((resolve) => {
      const image = new Image();
      const finalize = () => resolve(url);

      image.decoding = 'async';
      image.addEventListener(
        'load',
        () => {
          if (typeof image.decode === 'function') {
            image.decode().catch(() => {}).finally(finalize);
            return;
          }

          finalize();
        },
        { once: true }
      );
      image.addEventListener('error', () => resolve(''), { once: true });
      image.src = url;
    }).finally(() => {
      imageLoadCache.delete(url);
    });

    imageLoadCache.set(url, promise);
    return promise;
  };

  const storeTurnDirection = (direction) => {
    storage.set(turnStateKey, direction, 'session');
  };

  const consumeTurnDirection = () => {
    const value = `${storage.get(turnStateKey, '', 'session') || ''}`.trim();
    storage.remove(turnStateKey, 'session');

    return value;
  };

  const clearTurnDirection = () => {
    storage.remove(turnStateKey, 'session');
  };

  const readStoredRuntimePath = () => {
    const raw = `${storage.get(runtimePathStateKey, '') || ''}`.trim();
    if (!raw) {
      return null;
    }

    const parsed = parseJsonOr(raw, null, { context: 'workstation-runtime-path-storage' });
    return parsed && typeof parsed === 'object' ? parsed : null;
  };

  const writeStoredRuntimePath = (path) => {
    return storage.set(runtimePathStateKey, JSON.stringify(path));
  };

  const clearStoredRuntimePath = () => {
    storage.remove(runtimePathStateKey);
  };
  const emitRuntimePathUpdate = (path, source = 'workstation') => {
    dispatchEventSafely('spiralist:runtime-path-updated', {
      ...(path && typeof path === 'object' ? path : {}),
      source,
    });
  };

  class ManuscriptWorkstationController {
    constructor(root) {
      this.root = root;
      this.page = root.closest('.spiralist-page--manuscript');
      this.viewport = root.querySelector('[data-workstation-viewport]');
      this.viewer = root.querySelector('.spiralist-manuscript-workstation__viewer');
      this.surface = root.querySelector('[data-workstation-surface]');
      this.content = root.querySelector('[data-workstation-content]');
      this.statusTarget = root.querySelector('[data-workstation-status]');
      this.runtimeDrawer = root.querySelector('[data-workstation-runtime-drawer]');
      this.runtimeStatus = root.querySelector('[data-runtime-status]');
      this.runtimeCurrentStateChip = root.querySelector('[data-runtime-current-state]');
      this.runtimeNextCountChip = root.querySelector('[data-runtime-next-count]');
      this.runtimeHistoryCountChip = root.querySelector('[data-runtime-history-count]');
      this.runtimeStateValue = root.querySelector('[data-runtime-state-value]');
      this.runtimeNodeValue = root.querySelector('[data-runtime-node-value]');
      this.runtimeStorageValue = root.querySelector('[data-runtime-storage-value]');
      this.runtimeActiveSymbols = root.querySelector('[data-runtime-active-symbols]');
      this.runtimeNextNodes = root.querySelector('[data-runtime-next-nodes]');
      this.runtimeHistory = root.querySelector('[data-runtime-history]');
      this.runtimeRefresh = root.querySelector('[data-runtime-refresh]');
      this.runtimeReset = root.querySelector('[data-runtime-reset]');
      this.studyTools = root.querySelector('#study-surface-tools');
      this.contextDrawer = root.querySelector('[data-workstation-context-drawer]');
      this.dossierDrawer = root.querySelector('[data-workstation-dossier-drawer]');
      this.fullscreenButton = root.querySelector('[data-workstation-fullscreen]');
      this.searchModal = root.querySelector('[data-workstation-search-modal]');
      this.searchDialog = root.querySelector('[data-workstation-search-dialog]');
      this.searchScroll = root.querySelector('[data-workstation-search-scroll]');
      this.searchOpeners = Array.from(root.querySelectorAll('[data-workstation-open-modal="search"]'));
      this.searchClosers = Array.from(root.querySelectorAll('[data-workstation-close-modal="search"]'));
      this.tocModal = root.querySelector('[data-workstation-toc-modal]');
      this.tocDialog = root.querySelector('[data-workstation-toc-dialog]');
      this.tocScroll = root.querySelector('[data-workstation-toc-scroll]');
      this.tocOpeners = Array.from(root.querySelectorAll('[data-workstation-open-modal="toc"]'));
      this.tocClosers = Array.from(root.querySelectorAll('[data-workstation-close-modal="toc"]'));
      this.tocSections = Array.from(root.querySelectorAll('[data-workstation-toc-section]'));
      this.prevLink = this.viewport ? this.viewport.querySelector('[data-workstation-prev]') : null;
      this.nextLink = this.viewport ? this.viewport.querySelector('[data-workstation-next]') : null;
      this.jumpSelect = root.querySelector('[data-workstation-jump]');
      this.fitButtons = Array.from(root.querySelectorAll('[data-workstation-fit]'));
      this.viewModeButtons = Array.from(root.querySelectorAll('[data-workstation-view-mode]'));
      this.toggleButtons = Array.from(root.querySelectorAll('[data-workstation-toggle]'));
      this.toolButtons = Array.from(root.querySelectorAll('[data-workstation-action]'));
      this.drawerButtons = Array.from(root.querySelectorAll('[data-workstation-open-drawer]'));
      this.pageLinks = Array.from(root.querySelectorAll('[data-workstation-page-link]'));
      this.thumbLinks = Array.from(root.querySelectorAll('.spiralist-manuscript-workstation__thumb[data-workstation-page-link]'));
      this.searchInput = root.querySelector('[data-workstation-search]');
      this.searchClear = root.querySelector('[data-workstation-search-clear]');
      this.searchStatus = root.querySelector('[data-workstation-search-status]');
      this.searchResults = root.querySelector('[data-workstation-search-results]');
      this.searchResultsScroll = root.querySelector('[data-workstation-search-results-scroll]');
      this.runSearch = null;
      this.saveButtons = this.toolButtons.filter(
        (button) => `${button.dataset.workstationAction || ''}`.trim() === 'save'
      );
      this.copyButtons = this.toolButtons.filter(
        (button) => `${button.dataset.workstationAction || ''}`.trim() === 'copy-link'
      );
      this.canEdit = root.dataset.canEdit === 'true' && Boolean(config.canEdit);
      this.routeMode = `${root.dataset.routeMode || 'overview'}`.trim() || 'overview';
      this.isDetailRoute = this.routeMode === 'folio';
      this.isBrowserRoute = !this.isDetailRoute;
      this.nodeTargetUrl = `${root.dataset.nodeTargetUrl || window.location.href}`.trim();
      this.enginePathUrl = `${config.enginePathUrl || ''}`.trim();
      this.engineExecuteUrl = `${config.engineExecuteUrl || ''}`.trim();
      this.currentSequence = Number.parseInt(`${root.dataset.currentSequence || ''}`.trim(), 10) || 0;
      this.initialSequence = this.currentSequence;
      this.folios = this.readRenderedFolios();
      this.folioRecords = this.readFolioRecords();
      this.initialViewMode = this.getResponsiveViewMode(`${root.dataset.initialViewMode || 'spread'}`.trim() || 'spread');
      this.userSelectedViewMode = false;

      this.state = {
        fitMode: 'width',
        viewMode: this.initialViewMode,
        rawMode: false,
        guidesVisible: true,
        overlayVisible: false,
        scale: 1,
        panX: 0,
        panY: 0,
        edgeHint: 'none',
      };

      this.viewportDrag = null;
      this.cropDrag = null;
      this.cropGlobalBound = false;
      this.studyPayload = null;
      this.studyLoading = false;
      this.runtimePath = {
        currentState: '',
        currentNode: '',
        activeSymbols: [],
        history: [],
        nextNodes: [],
        storage: '',
      };
      this.runtimeLoading = false;
      this.calibrationProfiles = new Map();
      this.autoDetected = new Set();
      this.dirtyFolios = new Set();
      this.saveInFlight = false;
      this.isTurningPage = false;
      this.suppressViewportClick = false;
      this.previousModalFocus = null;
      this.searchTimer = 0;
      this.searchAbortController = null;
      this.lastSearchQuery = '';
      this.copyFeedbackTimer = 0;
      this.viewerStateSyncTimer = 0;
      this.viewerStateReady = false;
      this.isApplyingViewerState = false;
      this.baseFitMode = 'width';
      this.initialViewerState = this.readViewerStateFromUrl();