Skip to content
wiki.fftac.org

Manuscript Workstation - Source Excerpt 16

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

return null;
      }

      const paddingX = Math.max(2, Math.round(width * 0.01));
      const paddingY = Math.max(2, Math.round(height * 0.01));

      return normalizeCropRect({
        x: Math.max(0, (minX - paddingX) / width),
        y: Math.max(0, (minY - paddingY) / height),
        width: Math.min(width, maxX - minX + paddingX * 2) / width,
        height: Math.min(height, maxY - minY + paddingY * 2) / height,
      });
    }

    drawImageToCanvas(image) {
      if (!image || !image.naturalWidth || !image.naturalHeight) {
        return null;
      }

      const scale = Math.min(256 / image.naturalWidth, 256 / image.naturalHeight, 1);
      const width = Math.max(1, Math.round(image.naturalWidth * scale));
      const height = Math.max(1, Math.round(image.naturalHeight * scale));
      const canvas = document.createElement('canvas');
      const context = canvas.getContext('2d', { willReadFrequently: true });
      if (!context) {
        return null;
      }

      canvas.width = width;
      canvas.height = height;
      context.drawImage(image, 0, 0, width, height);
      return { canvas, context, width, height };
    }

    warmRoute(url) {
      if (!url) {
        return;
      }

      const nextUrl = createUrlOr(url, null, { context: 'workstation-warm-route' });
      if (!nextUrl) {
        return;
      }

      if (nextUrl.origin !== window.location.origin) {
        return;
      }

      const cacheKey = `route:${nextUrl.href}`;
      if (warmCache.has(cacheKey) || typeof window.fetch !== 'function') {
        return;
      }

      warmCache.add(cacheKey);
      runWorkstationAction(
        () => window.fetch(nextUrl.href, {
          credentials: 'same-origin',
          cache: 'force-cache',
        }),
        {
          context: 'workstation-route-warm',
          onError: () => {
            warmCache.delete(cacheKey);
          },
        }
      );
    }

    warmImage(url) {
      if (!url) {
        return;
      }

      const cacheKey = `image:${url}`;
      if (warmCache.has(cacheKey)) {
        return;
      }

      warmCache.add(cacheKey);
      preloadImageAsset(url);
    }

    applyOverlayVisibility(visible, options = {}) {
      const announce = options.announce !== false;
      this.state.overlayVisible = Boolean(visible);
      if (this.state.overlayVisible) {
        this.renderStudyOverlay();
      }

      this.folios.forEach((folio) => {
        if (!folio.studyLayer) {
          return;
        }

        folio.studyLayer.hidden = !this.state.overlayVisible;
        folio.studyLayer.setAttribute('aria-hidden', this.state.overlayVisible ? 'false' : 'true');
      });

      this.syncButtons();
      if (announce) {
        this.updateStatus(
          this.state.overlayVisible
            ? 'Study overlay shown. Visible folios now expose contextual notes, signal terms, and any linked symbolic nodes.'
            : 'Study overlay hidden.'
        );
      }
    }

    async applyInitialViewerState() {
      const seed = this.initialViewerState || {};
      if (this.isBrowserRoute && Number.isFinite(seed.currentSequence) && seed.currentSequence > 0) {
        const targetIndex = this.folioRecords.findIndex((record) => record.sequence === seed.currentSequence);
        if (targetIndex >= 0 && targetIndex !== this.getCurrentFolioRecordIndex()) {
          const currentRecord = this.renderFolioWindow(targetIndex);
          if (currentRecord) {
            this.syncCurrentFolioChrome(currentRecord, targetIndex);
          }
        }
      }

      const initialFitMode =
        seed.fitMode === 'height' || seed.fitMode === 'actual'
          ? seed.fitMode
          : 'width';

      this.isApplyingViewerState = true;
      this.fitTo(initialFitMode, false);

      if (Number.isFinite(seed.scale) && seed.scale > 0) {
        const bounds = this.getScaleBounds();
        this.state.fitMode = 'custom';
        this.state.scale = clamp(seed.scale, bounds.min, bounds.max);
      } else if (seed.fitMode === 'custom') {
        this.state.fitMode = 'custom';
      }

      if (Number.isFinite(seed.panRatioX) || Number.isFinite(seed.panRatioY)) {
        this.applyNormalizedPan(seed.panRatioX || 0, seed.panRatioY || 0);
      }

      this.renderSurface();

      if (seed.overlayVisible) {
        await runWorkstationAction(async () => {
          await this.ensureStudyOverlay();
          this.applyOverlayVisibility(true, { announce: false });
        }, {
          context: 'workstation-initial-overlay',
          onError: () => {
            this.applyOverlayVisibility(false, { announce: false });
          },
        });
      } else {
        this.applyOverlayVisibility(false, { announce: false });
      }

      this.viewerStateReady = true;
      this.isApplyingViewerState = false;
      this.syncNavigationStateUrls();
      this.requestViewerStateSync({ immediate: true });
    }

    async copyTextToClipboard(value) {
      const nextValue = `${value || ''}`.trim();
      if (!nextValue) {
        return false;
      }

      await copyText(nextValue);
      return true;
    }

    setCopyFeedback(copied = false) {
      window.clearTimeout(this.copyFeedbackTimer);

      this.copyButtons.forEach((button) => {
        const copy = button.querySelector('[data-workstation-copy-copy]');
        if (copy) {
          copy.textContent = copied ? 'Link Copied' : 'Copy Deep Link';
        }
      });

      if (!copied) {
        return;
      }

      this.copyFeedbackTimer = window.setTimeout(() => {
        this.setCopyFeedback(false);
      }, 1800);
    }

    async copyDeepLink() {
      const shareUrl = this.buildViewerStateUrl(window.location.href, { mode: 'share' });

      await runWorkstationAction(async () => {
        const copied = await this.copyTextToClipboard(shareUrl);
        if (!copied) {
          throw new Error('Clipboard copy failed.');
        }