Skip to content
wiki.fftac.org

Manuscript Workstation - Source Excerpt 19

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

${resetButton}
              <a class="spiralist-manuscript-workstation__folio-action" href="${escapeHtml(fullUrl)}" target="_blank" rel="noopener noreferrer">Open Raw</a>
            </div>
          </div>`
        : '';
      const figcaption = this.isDetailRoute && record.caption
        ? `<figcaption class="spiralist-manuscript-stage-panel__caption spiralist-muted">${escapeHtml(record.caption)}</figcaption>`
        : '';

      figure.innerHTML = `
        ${folioHead}
        <div class="spiralist-manuscript-workstation__folio-frame" data-workstation-folio-frame>
          <span class="spiralist-manuscript-workstation__folio-chip" aria-hidden="true">${escapeHtml(record.label || 'Page')}</span>
          <img src="${escapeHtml(previewUrl)}" alt="${escapeHtml(alt)}" loading="eager" decoding="async" ${isCurrent ? 'fetchpriority="high"' : ''} class="is-preview-source" data-current-level="preview" data-loaded-src="${escapeHtml(previewUrl)}" data-workstation-image />
          ${cropLayer}
          ${studyLayer}
        </div>
        ${figcaption}
      `;

      return figure;
    }

    renderFolioWindow(currentIndex) {
      const records = this.getFolioWindowRecords(currentIndex);
      if (!records.length) {
        return null;
      }

      this.content.replaceChildren(
        ...records.map((record, index) => {
          const hasContext = records.length >= 3;
          const slot = index === 0 ? 'left' : index === 1 && hasContext ? 'center' : 'right';
          return this.createFolioElement(record, {
            slot,
            isCurrent: index === 0,
            isContext: index >= 2,
          });
        })
      );
      this.content.dataset.spreadCount = `${Math.max(1, records.length)}`;
      this.folios = this.readRenderedFolios();
      this.renderAllCrops();
      this.bindFolioActionControls();
      this.bindCropHandles();
      this.applyOverlayVisibility(this.state.overlayVisible, { announce: false });
      this.swapImageSources().finally(() => {
        this.ensureActiveImagesReady(() => {
          this.refreshViewportState();
        });
      });

      return records[0];
    }

    updateEdgeLink(link, record, direction) {
      if (!(link instanceof HTMLElement)) {
        return;
      }

      if (!record) {
        link.hidden = true;
        link.setAttribute('aria-hidden', 'true');
        link.setAttribute('tabindex', '-1');
        link.setAttribute('href', '#');
        link.dataset.baseHref = '#';
        link.dataset.spreadHref = '';
        return;
      }

      const href = record.routeUrl || '#';
      link.hidden = false;
      link.removeAttribute('aria-hidden');
      link.removeAttribute('tabindex');
      link.setAttribute('href', href);
      link.dataset.baseHref = href;
      link.dataset.spreadHref = '';
      link.dataset.baseSpreadHref = '';
      link.dataset.pageSequence = `${record.sequence}`;
      link.dataset.imageUrl = record.prefetchUrl || record.standardUrl || record.previewUrl || record.imageUrl || '';
      link.setAttribute('aria-label', `Turn to ${direction === 'prev' ? 'previous' : 'next'} folio, ${record.title || record.label || 'manuscript page'}`);

      const label = link.querySelector('.spiralist-manuscript-workstation__edge-copy span');
      if (label) {
        label.textContent = record.label || record.title || (direction === 'prev' ? 'Previous folio' : 'Next folio');
      }
    }

    syncCurrentFolioChrome(record, currentIndex) {
      this.currentSequence = record.sequence;
      this.root.dataset.currentSequence = `${record.sequence}`;
      this.root.dataset.currentSlug = record.slug;
      this.nodeTargetUrl = record.routeUrl || this.nodeTargetUrl;
      this.root.dataset.nodeTargetUrl = this.nodeTargetUrl;

      if (this.isDetailRoute) {
        const headline = this.root.querySelector('.spiralist-manuscript-workstation__headline');
        if (headline) {
          headline.textContent = record.title || 'Manuscript folio';
        }

        const pageLabel = this.root.querySelector('.spiralist-manuscript-reader__page-label');
        if (pageLabel) {
          pageLabel.textContent = `${record.label || 'Page'} | ${record.eyebrow || 'Book Page'}`;
        }

        const viewerTitle = this.root.querySelector('#manuscript-workstation-viewer');
        if (viewerTitle) {
          viewerTitle.textContent = record.title || 'Manuscript folio';
        }

        const routeChip = this.root.querySelector('.spiralist-manuscript-workstation__meta-chips .spiralist-manuscript-workstation__chip');
        if (routeChip) {
          routeChip.textContent = `Route ${record.sequence}`;
        }
      }

      this.thumbLinks.forEach((link) => {
        const isCurrent = Number.parseInt(`${link.dataset.pageSequence || ''}`.trim(), 10) === record.sequence;
        link.classList.toggle('is-current', isCurrent);
        if (isCurrent) {
          link.setAttribute('aria-current', 'page');
        } else {
          link.removeAttribute('aria-current');
        }
      });

      this.updateEdgeLink(this.prevLink, this.folioRecords[currentIndex - 1] || null, 'prev');
      this.updateEdgeLink(this.nextLink, this.folioRecords[currentIndex + 1] || null, 'next');
    }

    updateBrowserRoute(record) {
      if (!record || this.isDetailRoute) {
        return;
      }

      this.requestViewerStateSync({ immediate: true });
    }

    turnFolio(direction = 'next') {
      if (this.isDetailRoute || this.isTurningPage || !this.folioRecords.length) {
        return false;
      }

      const currentIndex = this.getCurrentFolioRecordIndex();
      const targetIndex = direction === 'prev' ? currentIndex - 1 : currentIndex + 1;
      const targetRecord = this.folioRecords[targetIndex] || null;
      if (!targetRecord) {
        return false;
      }

      this.warmFolioRecord(targetRecord);
      this.warmFolioRecord(this.folioRecords[targetIndex + 1] || null);
      this.warmFolioRecord(this.folioRecords[targetIndex + 2] || null);
      this.isTurningPage = true;
      this.root.classList.remove('is-entering-prev', 'is-entering-next', 'is-turning-prev', 'is-turning-next', 'is-sliding-prev', 'is-sliding-next');
      this.root.classList.add(direction === 'prev' ? 'is-sliding-prev' : 'is-sliding-next');

      const finish = () => {
        const currentRecord = this.renderFolioWindow(targetIndex);
        if (currentRecord) {
          this.syncCurrentFolioChrome(currentRecord, targetIndex);
          this.updateBrowserRoute(currentRecord);
          this.updateStatus('Manuscript browser updated.');
        }

        this.isTurningPage = false;
        this.root.classList.remove('is-sliding-prev', 'is-sliding-next');
      };

      window.setTimeout(finish, prefersReducedMotion ? 0 : 220);
      return true;
    }

    turnPage(url, direction = 'next') {
      if (this.turnFolio(direction)) {
        return;
      }

      this.navigateToPage(url, direction);
    }

    navigateToPage(url, direction = 'next') {
      if (!url || this.isTurningPage) {
        return;
      }

      const targetUrl = createUrlOr(url, null, { context: 'workstation-page-target-url' });
      const currentUrl = createUrlOr(window.location.href, null, { context: 'workstation-page-current-url' });
      if (!targetUrl || !currentUrl) {
        window.location.assign(url);
        return;
      }

      const nextHref = this.buildViewerStateUrl(targetUrl.href, { mode: 'minimal' });
      const currentHref = this.buildViewerStateUrl(currentUrl.href, { mode: 'minimal' });

      if (nextHref === currentHref) {
        return;
      }

      storeTurnDirection(direction);
      this.isTurningPage = true;

      if (prefersReducedMotion) {
        window.location.assign(nextHref);
        window.setTimeout(() => {
          if (window.location.href === currentUrl.href) {
            this.isTurningPage = false;
            clearTurnDirection();
          }
        }, 480);
        return;
      }

      this.root.classList.remove('is-entering-prev', 'is-entering-next', 'is-turning-prev', 'is-turning-next');
      this.root.classList.add(direction === 'prev' ? 'is-turning-prev' : 'is-turning-next');
      window.setTimeout(() => {
        window.location.assign(nextHref);
      }, 220);
      window.setTimeout(() => {
        if (window.location.href === currentUrl.href) {
          this.isTurningPage = false;
          clearTurnDirection();
          this.root.classList.remove('is-turning-prev', 'is-turning-next');
        }
      }, 900);
    }

    activateEntryMotion() {
      const direction = consumeTurnDirection();
      if (!direction || prefersReducedMotion) {
        return;
      }