Skip to content
wiki.fftac.org

Manuscript Workstation - Source Excerpt 14

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

const profile = this.calibrationProfiles.get(folio.slug) || (await this.sampleBorderProfile(folio.image));
        const detected = await this.detectCropRect(folio.image, profile);
        if (detected) {
          this.setCrop(folio, detected, { dirty: true, autoDetected: true });
        }
      }

      this.updateStatus('Auto-crop applied to the visible folios. Review the guides, then save if the boundaries look correct.');
    }

    async refreshVisibleFolios() {
      const activeFolios = this.getVisibleFolios();

      await Promise.all(
        activeFolios.map((folio) =>
          this.applyFolioImageSource(folio, this.state.rawMode ? 'full' : 'standard', {
            cacheBust: true,
            force: true,
          })
        )
      );

      this.ensureActiveImagesReady(() => {
        this.refreshViewportState();
      });
      this.updateStatus('Visible folios refreshed.');
    }

    async saveDirtyFolios() {
      if (!this.canEdit) {
        this.updateStatus('Crop saving requires editor access on this WordPress site.');
        return;
      }

      if (this.saveInFlight) {
        return;
      }

      const dirty = this.folios.filter((folio) => this.dirtyFolios.has(folio.slug));
      if (!dirty.length) {
        this.updateDirtyState();
        this.updateStatus('No crop changes are waiting to be saved.');
        return;
      }

      this.saveInFlight = true;
      this.updateDirtyState();

      await runWorkstationAction(async () => {
        await Promise.all(
          dirty.map(async (folio) => {
            const response = await fetch(`${config.saveCropBaseUrl || ''}${encodeURIComponent(folio.slug)}`, {
              method: 'POST',
              headers: {
                Accept: 'application/json',
                'Content-Type': 'application/json',
                'X-WP-Nonce': config.restNonce || '',
              },
              credentials: 'same-origin',
              body: JSON.stringify({
                crop: folio.crop,
                auto_detected: this.autoDetected.has(folio.slug),
              }),
            });

            if (!response.ok) {
              throw new Error(`Crop save failed with ${response.status}`);
            }

            this.dirtyFolios.delete(folio.slug);
          })
        );

        this.updateDirtyState();
        this.updateStatus(`Saved crop data for ${dirty.length} folio${dirty.length === 1 ? '' : 's'}.`);
      }, {
        context: 'workstation-save-crops',
        onError: () => {
          this.updateStatus('Crop save failed. Check permissions and try again.');
        },
        onFinally: () => {
          this.saveInFlight = false;
          this.updateDirtyState();
        },
      });
    }

    focusCurrentCrop() {
      const folio = this.getCurrentFolio();
      if (!folio || !folio.frame || !folio.cropLayer) {
        return;
      }

      const crop = normalizeCropRect(folio.crop);
      const cropWidth = Math.max(1, folio.cropLayer.offsetWidth * crop.width);
      const cropHeight = Math.max(1, folio.cropLayer.offsetHeight * crop.height);
      const viewportWidth = Math.max(1, this.viewport.clientWidth - 80);
      const viewportHeight = Math.max(1, this.viewport.clientHeight - 80);
      const targetScale = Math.min(
        viewportWidth / cropWidth,
        viewportHeight / cropHeight,
        this.getScaleBounds().max
      );
      const cropCenterX =
        folio.frame.offsetLeft +
        folio.cropLayer.offsetLeft +
        crop.x * folio.cropLayer.offsetWidth +
        cropWidth / 2;
      const cropCenterY =
        folio.frame.offsetTop +
        folio.cropLayer.offsetTop +
        crop.y * folio.cropLayer.offsetHeight +
        cropHeight / 2;
      const contentCenterX = this.content.offsetWidth / 2;
      const contentCenterY = this.content.offsetHeight / 2;

      this.state.fitMode = 'custom';
      this.state.scale = targetScale;
      this.state.panX = (contentCenterX - cropCenterX) * targetScale;
      this.state.panY = (contentCenterY - cropCenterY) * targetScale;
      this.syncButtons();
      this.renderSurface();
      this.updateStatus(`Focused ${folio.title || 'the current folio'} on its crop bounds.`);
    }

    async ensureStudyOverlay() {
      if (this.studyPayload || this.studyLoading) {
        this.renderStudyOverlay();
        return;
      }

      const overlaySource = this.folios.find(
        (folio) => overlayIncludesGlobalNodes(folio.overlayData) && getOverlayNodesUrl(folio.overlayData)
      );
      const nodesUrl = overlaySource ? getOverlayNodesUrl(overlaySource.overlayData) : '';
      if (!nodesUrl) {
        this.renderStudyOverlay();
        return;
      }

      this.studyLoading = true;

      await runWorkstationAction(async () => {
        const response = await fetch(nodesUrl, {
          headers: {
            Accept: 'application/json',
          },
        });

        if (!response.ok) {
          throw new Error(`Study overlay failed with ${response.status}`);
        }