Skip to content
wiki.fftac.org

Manuscript Workstation - Source Excerpt 09

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

});

      this.toggleButtons.forEach((button) => {
        const toggle = `${button.dataset.workstationToggle || ''}`.trim();
        const active =
          (toggle === 'raw' && this.state.rawMode) ||
          (toggle === 'guides' && this.state.guidesVisible) ||
          (toggle === 'overlay' && this.state.overlayVisible);
        button.classList.toggle('is-active', active);
        if (toggle === 'overlay') {
          button.disabled = !overlayAvailable;
          button.classList.toggle('is-disabled', !overlayAvailable);
          button.setAttribute('aria-disabled', overlayAvailable ? 'false' : 'true');
        }
        button.setAttribute('aria-pressed', active ? 'true' : 'false');
      });

      this.requestViewerStateSync();
    }

    updateDirtyState() {
      const dirtyCount = this.dirtyFolios.size;
      this.root.dataset.dirtyCount = `${dirtyCount}`;

      this.saveButtons.forEach((button) => {
        const pending = dirtyCount > 0;
        button.classList.toggle('is-pending', pending);
        if (this.canEdit) {
          button.disabled = this.saveInFlight || !pending;
        }

        const copy = button.querySelector('[data-workstation-save-copy]');
        if (copy) {
          copy.textContent = !this.canEdit
            ? 'Editor access required'
            : this.saveInFlight
              ? 'Saving crop data'
              : pending
                ? `Persist ${dirtyCount} unsaved crop${dirtyCount === 1 ? '' : 's'}`
                : 'All visible crops saved';
        }
      });
    }

    setViewMode(nextMode, options = {}) {
      const announce = options.announce !== false;
      const userInitiated = options.userInitiated !== false;
      this.state.viewMode = nextMode === 'single' ? 'single' : 'spread';
      if (userInitiated) {
        this.userSelectedViewMode = true;
      }
      this.syncButtons();
      this.swapImageSources().finally(() => {
        this.ensureActiveImagesReady(() => {
          this.refreshViewportState();
        });
      });
      if (announce) {
        this.updateStatus(
          this.state.viewMode === 'single'
            ? 'Single-page view active. The current folio stays centered for close reading.'
            : 'Facing-page view active. Adjacent folios stay visible together for book-style reading.'
        );
      }
    }

    toggleGuides() {
      this.state.guidesVisible = !this.state.guidesVisible;
      this.syncButtons();
      this.updateStatus(this.state.guidesVisible ? 'Crop guides shown.' : 'Crop guides hidden for clean inspection.');
    }

    toggleRawMode() {
      this.state.rawMode = !this.state.rawMode;
      this.syncButtons();
      this.swapImageSources().finally(() => {
        this.ensureActiveImagesReady(() => {
          this.refreshViewportState();
        });
      });
      this.updateStatus(
        this.state.rawMode
          ? 'Raw inspection mode active. Full-resolution sources are prioritized for crisp zooming.'
          : 'Reading mode active. Standard viewer sources restored.'
      );
    }

    async toggleOverlay() {
      const hasStudyLayer = this.folios.some((folio) => folio.studyLayer);
      if (!hasStudyLayer) {
        this.updateStatus('This folio route does not expose the study overlay.');
        return;
      }

      if (!this.state.overlayVisible) {
        await this.ensureStudyOverlay();
      }

      this.applyOverlayVisibility(!this.state.overlayVisible, { announce: true });
    }

    openAnchoredStudyTools() {
      if (this.studyTools && window.location.hash === '#study-surface-tools') {
        this.studyTools.open = true;
      }
    }

    bindControls() {
      this.fitButtons.forEach((button) => {
        button.addEventListener('click', () => {
          const mode = `${button.dataset.workstationFit || ''}`.trim() || 'width';
          this.fitTo(mode);
        });
      });

      this.viewModeButtons.forEach((button) => {
        button.addEventListener('click', () => {
          const mode = `${button.dataset.workstationViewMode || ''}`.trim() || 'spread';
          this.setViewMode(mode, { announce: true, userInitiated: true });
        });
      });

      this.toggleButtons.forEach((button) => {
        button.addEventListener('click', async () => {
          const toggle = `${button.dataset.workstationToggle || ''}`.trim();

          if (toggle === 'guides') {
            this.toggleGuides();
          }

          if (toggle === 'raw') {
            this.toggleRawMode();
          }

          if (toggle === 'overlay') {
            await this.toggleOverlay();
          }
        });
      });

      this.toolButtons.forEach((button) => {
        button.addEventListener('click', async () => {
          const action = `${button.dataset.workstationAction || ''}`.trim();

          if (action === 'copy-link') {
            await this.copyDeepLink();
          }

          if (action === 'reset-view') {
            this.resetViewerState();
          }

          if (action === 'calibrate') {
            await this.calibrateVisibleFolios();
          }

          if (action === 'auto-crop') {
            await this.autoCropVisibleFolios();
          }

          if (action === 'refresh') {
            await this.refreshVisibleFolios();
          }

          if (action === 'save') {
            await this.saveDirtyFolios();
          }

          if (action === 'apply') {
            this.focusCurrentCrop();
          }
        });
      });

      this.bindFolioActionControls();

      const handleViewportResize = () => {
        this.syncViewportLayoutMetrics();
        const responsiveViewMode = this.getResponsiveViewMode(`${this.root.dataset.initialViewMode || 'spread'}`.trim() || 'spread');
        if (!this.userSelectedViewMode && responsiveViewMode !== this.state.viewMode) {
          this.setViewMode(responsiveViewMode, { announce: false, userInitiated: false });
          return;
        }

        if (this.state.fitMode === 'actual' || this.state.fitMode === 'custom') {
          this.renderSurface();
          return;
        }

        this.fitTo(this.state.fitMode, false);
      };

      window.addEventListener('resize', handleViewportResize);
      if (window.visualViewport) {
        window.visualViewport.addEventListener('resize', handleViewportResize);
      }
    }

    getFullscreenElement() {
      return document.fullscreenElement || document.webkitFullscreenElement || null;
    }

    isViewportFullscreen() {
      return this.getFullscreenElement() === this.viewport;
    }

    isFullscreenSupported() {
      return Boolean(
        this.viewport &&
        (
          document.fullscreenEnabled ||
          document.webkitFullscreenEnabled ||
          this.viewport.requestFullscreen ||
          this.viewport.webkitRequestFullscreen
        )
      );
    }

    syncFullscreenButton(options = {}) {
      const { announce = false } = options;
      if (!this.fullscreenButton) {
        return;
      }

      if (!this.isFullscreenSupported()) {
        this.fullscreenButton.hidden = true;
        return;
      }

      const active = this.isViewportFullscreen();
      const label = active ? 'Exit fullscreen' : 'Enter fullscreen';
      this.fullscreenButton.hidden = false;
      this.fullscreenButton.classList.toggle('is-active', active);
      this.fullscreenButton.setAttribute('aria-pressed', active ? 'true' : 'false');
      this.fullscreenButton.setAttribute('aria-label', label);
      this.fullscreenButton.setAttribute('title', label);
      this.root.dataset.fullscreenActive = active ? 'true' : 'false';

      if (!announce) {
        return;
      }

      this.updateStatus(active ? 'Fullscreen viewer active.' : 'Viewer returned to normal size.');
    }

    async requestViewportFullscreen() {
      if (!this.viewport) {
        return false;
      }

      if (typeof this.viewport.requestFullscreen === 'function') {
        await this.viewport.requestFullscreen();
        return true;
      }

      if (typeof this.viewport.webkitRequestFullscreen === 'function') {
        this.viewport.webkitRequestFullscreen();
        return true;
      }

      return false;
    }

    async exitViewportFullscreen() {
      if (typeof document.exitFullscreen === 'function') {
        await document.exitFullscreen();
        return true;
      }

      if (typeof document.webkitExitFullscreen === 'function') {
        document.webkitExitFullscreen();
        return true;
      }

      return false;
    }

    bindFullscreenControl() {
      if (!this.fullscreenButton) {
        return;
      }

      if (!this.isFullscreenSupported()) {