Skip to content
wiki.fftac.org

Manuscript Workstation - Source Excerpt 12

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.viewport.addEventListener('pointermove', (event) => {
        if (!this.viewportDrag || event.pointerId !== this.viewportDrag.pointerId) {
          this.updateEdgeHintFromPointer(event);
          return;
        }

        const deltaX = event.clientX - this.viewportDrag.startX;
        const deltaY = event.clientY - this.viewportDrag.startY;
        const movedEnough = Math.abs(deltaX) > 6 || Math.abs(deltaY) > 6;

        if (movedEnough) {
          this.suppressViewportClick = true;
        }

        const canSwipeTurn = this.canUseSwipeTurnGesture();
        if (
          (this.viewportDrag.mode === 'turn' || (canSwipeTurn && this.isHorizontalTurnGesture(deltaX, deltaY))) &&
          movedEnough
        ) {
          const lockedTurnDirection = `${this.viewportDrag.lockedTurnDirection || 'none'}`.trim();
          let intendedDirection = deltaX > 0 ? 'prev' : 'next';
          let effectiveDistance = deltaX;

          if (lockedTurnDirection === 'prev') {
            intendedDirection = 'prev';
            effectiveDistance = Math.max(0, deltaX);
          } else if (lockedTurnDirection === 'next') {
            intendedDirection = 'next';
            effectiveDistance = Math.min(0, deltaX);
          }

          const hasTarget = this.hasTurnTarget(intendedDirection);
          const canCommitTurn = hasTarget && Math.abs(effectiveDistance) > 0;
          const resistedOffset = hasTarget ? effectiveDistance : effectiveDistance * 0.28;

          this.viewportDrag.activated = true;
          this.viewportDrag.mode = 'turn';
          this.viewportDrag.turnDistance = effectiveDistance;
          this.viewportDrag.turnDirection = canCommitTurn ? intendedDirection : 'none';
          this.setEdgeHint(canCommitTurn ? intendedDirection : 'none');
          this.setDragTurnOffset(resistedOffset);
          return;
        }

        if (movedEnough && !this.viewportDrag.activated) {
          this.viewportDrag.activated = true;
          this.viewportDrag.mode = 'pan';
          if (this.ensurePannableScale()) {
            this.renderSurface();
            this.viewportDrag.startX = event.clientX;
            this.viewportDrag.startY = event.clientY;
            this.viewportDrag.startPanX = this.state.panX;
            this.viewportDrag.startPanY = this.state.panY;
            return;
          }
        }

        this.state.panX = this.viewportDrag.startPanX + deltaX;
        this.state.panY = this.viewportDrag.startPanY + deltaY;
        this.renderSurface();
      });

      this.viewport.addEventListener('click', (event) => {
        if (this.suppressViewportClick) {
          this.suppressViewportClick = false;
          return;
        }

        const target = event.target;
        if (
          target instanceof HTMLElement &&
          target.closest('a, button, input, textarea, select, label')
        ) {
          return;
        }

        const edgeIntent = this.getViewportEdgeIntent(event.clientX);
        if (edgeIntent === 'prev' && this.prevLink && !this.prevLink.hidden) {
          event.preventDefault();
          this.turnPage(this.getNavigationHref(this.prevLink), 'prev');
          return;
        }

        if (edgeIntent === 'next' && this.nextLink && !this.nextLink.hidden) {
          event.preventDefault();
          this.turnPage(this.getNavigationHref(this.nextLink), 'next');
        }
      });

      const stopViewportDrag = (event) => {
        if (!this.viewportDrag) {
          return;
        }

        if (event && event.pointerId !== this.viewportDrag.pointerId) {
          return;
        }

        const dragState = this.viewportDrag;
        this.viewportDrag = null;
        this.viewport.classList.remove('is-dragging');
        this.resetDragTurnOffset();
        this.setEdgeHint('none');

        if (dragState.mode === 'turn') {
          const direction = dragState.turnDirection;
          const crossedThreshold =
            direction !== 'none' && Math.abs(dragState.turnDistance || 0) >= this.getDragTurnThreshold();

          if (crossedThreshold) {
            const link = direction === 'prev' ? this.prevLink : this.nextLink;
            const href = this.getNavigationHref(link);
            if (href) {
              this.turnPage(href, direction);
            }
          }
          return;
        }
      };

      this.viewport.addEventListener('pointerup', stopViewportDrag);
      this.viewport.addEventListener('pointercancel', stopViewportDrag);
      this.viewport.addEventListener('pointerleave', () => {
        this.setEdgeHint('none');
      });
      this.viewport.addEventListener('pointerleave', (event) => {
        if (this.viewportDrag && event.pointerId === this.viewportDrag.pointerId) {
          stopViewportDrag(event);
        }
      });
    }

    bindCropHandles() {
      if (!this.canEdit) {
        return;
      }

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

        folio.cropLayer.querySelectorAll('[data-workstation-crop-handle]').forEach((handle) => {
          if (handle.dataset.workstationCropBound === 'true') {
            return;
          }

          handle.dataset.workstationCropBound = 'true';
          handle.addEventListener('pointerdown', (event) => {
            if (event.button !== 0) {
              return;
            }

            event.preventDefault();
            event.stopPropagation();

            if (!folio.frame || !folio.cropLayer) {
              return;
            }

            this.cropDrag = {
              pointerId: event.pointerId,
              folio,
              handle: `${handle.dataset.workstationCropHandle || 'move'}`.trim(),
              startRect: { ...folio.crop },
              bounds: folio.cropLayer.getBoundingClientRect(),
              startX: event.clientX,
              startY: event.clientY,
            };
            this.viewport.classList.add('is-cropping');

            setPointerCapture(handle, event.pointerId);
          });

          handle.addEventListener('keydown', (event) => {
            const step = event.shiftKey ? 0.02 : 0.005;
            const rect = { ...folio.crop };
            const handleType = `${handle.dataset.workstationCropHandle || 'move'}`.trim();
            let changed = false;

            if (event.key === 'ArrowLeft') {
              changed = this.adjustCropByKeyboard(rect, handleType, -step, 0);
            }

            if (event.key === 'ArrowRight') {
              changed = this.adjustCropByKeyboard(rect, handleType, step, 0);
            }

            if (event.key === 'ArrowUp') {
              changed = this.adjustCropByKeyboard(rect, handleType, 0, -step);
            }

            if (event.key === 'ArrowDown') {
              changed = this.adjustCropByKeyboard(rect, handleType, 0, step);
            }

            if (!changed) {
              return;
            }

            event.preventDefault();
            this.setCrop(folio, rect, { dirty: true, autoDetected: false });
          });
        });
      });

      if (this.cropGlobalBound) {
        return;
      }

      this.cropGlobalBound = true;
      window.addEventListener('pointermove', (event) => {
        if (!this.cropDrag || event.pointerId !== this.cropDrag.pointerId) {
          return;
        }

        const { folio, startRect, handle, bounds, startX, startY } = this.cropDrag;
        const deltaX = bounds.width > 0 ? (event.clientX - startX) / bounds.width : 0;
        const deltaY = bounds.height > 0 ? (event.clientY - startY) / bounds.height : 0;
        const nextRect = { ...startRect };

        if (handle === 'move') {
          nextRect.x += deltaX;
          nextRect.y += deltaY;
        }

        if (handle.includes('n')) {
          nextRect.y += deltaY;
          nextRect.height -= deltaY;
        }

        if (handle.includes('s')) {
          nextRect.height += deltaY;
        }

        if (handle.includes('e')) {
          nextRect.width += deltaX;
        }

        if (handle.includes('w')) {
          nextRect.x += deltaX;
          nextRect.width -= deltaX;
        }

        this.setCrop(folio, nextRect, { dirty: true, autoDetected: false });
      });

      const stopCropDrag = (event) => {
        if (!this.cropDrag || event.pointerId !== this.cropDrag.pointerId) {
          return;
        }

        this.cropDrag = null;
        this.viewport.classList.remove('is-cropping');
      };

      window.addEventListener('pointerup', stopCropDrag);
      window.addEventListener('pointercancel', stopCropDrag);
    }

    adjustCropByKeyboard(rect, handle, deltaX, deltaY) {
      const nextRect = { ...rect };