Skip to content
wiki.fftac.org

Manuscript Workstation - Source Excerpt 17

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.setCopyFeedback(true);
        this.updateStatus('Deep link copied. It preserves the current folio, view mode, zoom, overlay state, and manuscript search.');
      }, {
        context: 'workstation-copy-deep-link',
        onError: () => {
          this.setCopyFeedback(false);
          this.updateStatus('Deep link could not be copied automatically. Select the address bar to copy the viewer URL manually.');
        },
      });
    }

    resetViewerState() {
      const defaultViewMode = this.getDefaultViewMode();
      this.isApplyingViewerState = true;
      this.userSelectedViewMode = false;
      this.baseFitMode = 'width';
      this.state.viewMode = defaultViewMode;
      this.state.fitMode = 'width';
      this.state.rawMode = false;
      this.state.guidesVisible = true;
      this.state.panX = 0;
      this.state.panY = 0;
      this.applyOverlayVisibility(false, { announce: false });

      if (this.searchAbortController) {
        this.searchAbortController.abort();
      }

      this.lastSearchQuery = '';
      if (this.searchInput) {
        this.searchInput.value = '';
      }

      if (this.searchClear) {
        this.searchClear.hidden = true;
      }

      this.applyThumbFilter('');
      this.clearSearchResults();
      this.setSearchStatus(this.getSearchIdleCopy());

      this.swapImageSources().finally(() => {
        this.ensureActiveImagesReady(() => {
          this.fitTo('width', false);
          this.isApplyingViewerState = false;
          this.syncButtons();
          this.requestViewerStateSync({ immediate: true });
          this.updateStatus('Viewer state reset. The manuscript returned to its clean default reading surface.');
        });
      });
    }

    openDrawer(drawerName = '') {
      if (drawerName === 'dossier' && this.dossierDrawer) {
        this.dossierDrawer.open = true;
      }

      if (drawerName === 'context' && this.contextDrawer) {
        this.contextDrawer.open = true;
      }
    }

    syncModalBodyState() {
      document.body.classList.toggle('is-manuscript-modal-open', this.isAnyModalOpen());
    }

    isAnyModalOpen() {
      return this.isSearchModalOpen() || this.isTocModalOpen();
    }

    isSearchModalOpen() {
      return Boolean(this.searchModal && !this.searchModal.hidden);
    }

    getSearchFocusable() {
      if (!this.searchDialog) {
        return [];
      }

      return Array.from(
        this.searchDialog.querySelectorAll(
          'button:not([disabled]), [href], input:not([disabled]), [tabindex]:not([tabindex="-1"])'
        )
      ).filter(
        (element) =>
          element instanceof HTMLElement &&
          !element.hasAttribute('hidden') &&
          !element.closest('[hidden]')
      );
    }

    openSearchModal() {
      if (!this.searchModal) {
        return;
      }

      this.previousModalFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null;
      this.searchModal.hidden = false;
      this.searchModal.setAttribute('aria-hidden', 'false');
      this.syncModalBodyState();

      if (this.searchScroll) {
        this.searchScroll.scrollTop = 0;
      }

      window.requestAnimationFrame(() => {
        if (this.searchInput instanceof HTMLElement) {
          this.searchInput.focus({ preventScroll: true });
          return;
        }

        const fallbackFocus = this.getSearchFocusable()[0];
        if (fallbackFocus instanceof HTMLElement) {
          fallbackFocus.focus({ preventScroll: true });
        }
      });
    }

    closeSearchModal() {
      if (!this.searchModal || this.searchModal.hidden) {
        return;
      }

      this.searchModal.hidden = true;
      this.searchModal.setAttribute('aria-hidden', 'true');
      this.syncModalBodyState();

      if (this.previousModalFocus && typeof this.previousModalFocus.focus === 'function') {
        this.previousModalFocus.focus({ preventScroll: true });
      }
    }

    bindSearchModal() {
      if (!this.searchModal) {
        return;
      }

      this.searchOpeners.forEach((opener) => {
        opener.addEventListener('click', (event) => {
          event.preventDefault();
          this.openSearchModal();
        });
      });

      this.searchClosers.forEach((closer) => {
        closer.addEventListener('click', (event) => {
          event.preventDefault();
          this.closeSearchModal();
        });
      });

      document.addEventListener('keydown', (event) => {
        if (!this.isSearchModalOpen()) {
          return;
        }

        if (event.key === 'Escape') {
          event.preventDefault();
          this.closeSearchModal();
          return;
        }

        if (event.key !== 'Tab') {
          return;
        }

        const focusable = this.getSearchFocusable();
        if (!focusable.length) {
          event.preventDefault();
          return;
        }

        const first = focusable[0];
        const last = focusable[focusable.length - 1];

        if (event.shiftKey && document.activeElement === first) {
          event.preventDefault();
          last.focus({ preventScroll: true });
          return;
        }

        if (!event.shiftKey && document.activeElement === last) {
          event.preventDefault();
          first.focus({ preventScroll: true });
        }
      });
    }

    isTocModalOpen() {
      return Boolean(this.tocModal && !this.tocModal.hidden);
    }

    resetTocTree() {
      this.tocSections.forEach((section) => {
        if (!(section instanceof HTMLDetailsElement)) {
          return;
        }

        section.open = false;
      });
    }

    getTocFocusable() {
      if (!this.tocDialog) {
        return [];
      }

      return Array.from(
        this.tocDialog.querySelectorAll(
          'button:not([disabled]), [href], summary, [tabindex]:not([tabindex="-1"])'
        )
      ).filter(
        (element) =>
          element instanceof HTMLElement &&
          !element.hasAttribute('hidden') &&
          !element.closest('[hidden]')
      );
    }

    openTocModal() {
      if (!this.tocModal) {
        return;
      }

      this.previousModalFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null;
      this.resetTocTree();
      this.tocModal.hidden = false;
      this.tocModal.setAttribute('aria-hidden', 'false');
      this.syncModalBodyState();

      if (this.tocScroll) {
        this.tocScroll.scrollTop = 0;
      }

      const focusTarget =
        (this.tocDialog && this.tocDialog.querySelector('summary')) ||
        (this.tocDialog && this.tocDialog.querySelector('button, [href]'));

      window.requestAnimationFrame(() => {
        if (focusTarget instanceof HTMLElement) {
          focusTarget.focus({ preventScroll: true });
        }
      });
    }

    closeTocModal() {
      if (!this.tocModal || this.tocModal.hidden) {
        return;
      }

      this.tocModal.hidden = true;
      this.tocModal.setAttribute('aria-hidden', 'true');
      this.syncModalBodyState();

      if (this.previousModalFocus && typeof this.previousModalFocus.focus === 'function') {
        this.previousModalFocus.focus({ preventScroll: true });
      }
    }

    bindTocModal() {
      if (!this.tocModal) {
        return;
      }

      this.tocOpeners.forEach((opener) => {
        opener.addEventListener('click', (event) => {
          event.preventDefault();
          this.openTocModal();
        });
      });

      this.tocClosers.forEach((closer) => {
        closer.addEventListener('click', (event) => {
          event.preventDefault();
          this.closeTocModal();
        });
      });

      document.addEventListener('keydown', (event) => {
        if (!this.isTocModalOpen()) {
          return;
        }

        if (event.key === 'Escape') {
          event.preventDefault();
          this.closeTocModal();
          return;
        }

        if (event.key !== 'Tab') {
          return;
        }

        const focusable = this.getTocFocusable();
        if (!focusable.length) {
          event.preventDefault();
          return;
        }

        const first = focusable[0];
        const last = focusable[focusable.length - 1];

        if (event.shiftKey && document.activeElement === first) {
          event.preventDefault();
          last.focus({ preventScroll: true });
          return;
        }

        if (!event.shiftKey && document.activeElement === last) {
          event.preventDefault();
          first.focus({ preventScroll: true });
        }
      });
    }

    getNavigationHref(link) {
      if (!(link instanceof HTMLElement)) {
        return '';
      }