Skip to content
wiki.fftac.org

Manuscript Workstation - Source Excerpt 01

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 config = window.spiralistManuscriptWorkstation || {};
  const DEFAULT_CROP = Object.freeze({
    x: 0.08,
    y: 0.06,
    width: 0.84,
    height: 0.88,
  });
  const MIN_CROP_SIZE = 0.05;
  const warmCache = new Set();
  const turnStateKey = 'spiralistManuscriptWorkstationTurn';
  const runtimePathStateKey = 'spiralistManuscriptRuntimePath';
  const viewerStateParams = Object.freeze({
    viewMode: 'm_view',
    fitMode: 'm_fit',
    rawMode: 'm_raw',
    guidesVisible: 'm_guides',
    overlayVisible: 'm_overlay',
    scale: 'm_zoom',
    panX: 'm_x',
    panY: 'm_y',
    searchQuery: 'm_query',
    currentSequence: 'm_page',
  });
  const viewerStateParamKeys = Object.freeze(Object.values(viewerStateParams));
  const prefersReducedMotion =
    typeof window.matchMedia === 'function' &&
    window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  const core = window.SpiralistCore || {};
  const handleCoreError = typeof core.handleError === 'function'
    ? core.handleError
    : () => '';
  const parseJsonOr = typeof core.parseJsonOr === 'function'
    ? core.parseJsonOr
    : (text, fallback = null, options = {}) => {
      if (typeof text !== 'string' || text === '') {
        return fallback;
      }

      try {
        return JSON.parse(text);
      } catch (error) {
        handleCoreError(error, { context: options.context || 'JSON.parse' });
        return fallback;
      }
    };
  const createUrlOr = typeof core.createUrlOr === 'function'
    ? core.createUrlOr
    : (value, fallback = null, options = {}) => {
      try {
        return new URL(value, options.base || window.location.href);
      } catch (error) {
        handleCoreError(error, { context: options.context || 'URL' });
        return fallback;
      }
    };
  const storage = core.storage || {
    get(key, fallback = '', area = 'local') {
      try {
        return (area === 'session' ? window.sessionStorage : window.localStorage).getItem(key) || fallback;
      } catch (error) {
        handleCoreError(error, { context: `${area}Storage.getItem` });
        return fallback;
      }
    },
    set(key, value, area = 'local') {
      try {
        (area === 'session' ? window.sessionStorage : window.localStorage).setItem(key, value);
        return true;
      } catch (error) {
        handleCoreError(error, { context: `${area}Storage.setItem` });
        return false;
      }
    },
    remove(key, area = 'local') {
      try {
        (area === 'session' ? window.sessionStorage : window.localStorage).removeItem(key);
      } catch (error) {
        handleCoreError(error, { context: `${area}Storage.removeItem` });
      }
    },
  };
  const copyText = typeof core.copyText === 'function'
    ? core.copyText
    : async (text) => {
      if (window.navigator?.clipboard && typeof window.navigator.clipboard.writeText === 'function') {
        await window.navigator.clipboard.writeText(text);
        return;
      }

      const textarea = document.createElement('textarea');
      textarea.value = text;
      textarea.setAttribute('readonly', 'readonly');
      textarea.style.position = 'fixed';
      textarea.style.top = '0';
      textarea.style.left = '-9999px';
      document.body.appendChild(textarea);
      textarea.focus();
      textarea.select();

      let copied = false;
      try {
        copied = Boolean(document.execCommand('copy'));
      } finally {
        textarea.remove();
      }

      if (!copied) {
        throw new Error('Clipboard copy failed.');
      }
    };
  const runWorkstationAction = (action, options = {}) => Promise.resolve()
    .then(() => action())
    .catch((error) => {
      if (typeof options.shouldIgnoreError === 'function' && options.shouldIgnoreError(error)) {
        return;
      }

      handleCoreError(error, options);
      if (typeof options.onError === 'function') {
        options.onError(error, error && error.message ? error.message : '');
      }

      throw error;
    })
    .finally(() => {
      if (typeof options.onFinally === 'function') {
        options.onFinally();
      }
    })
    .catch(() => {});
  const setPointerCapture = typeof core.setPointerCapture === 'function'
    ? core.setPointerCapture
    : (element, pointerId) => {
      if (!element || typeof element.setPointerCapture !== 'function') {
        return;
      }

      try {
        element.setPointerCapture(pointerId);
      } catch (error) {
        handleCoreError(error, { context: 'Element.setPointerCapture' });
      }
    };
  const dispatchEventSafely = (eventName, detail = {}) => {
    try {
      window.dispatchEvent(new CustomEvent(eventName, { detail }));
    } catch (error) {
      handleCoreError(error, { context: eventName });
    }
  };

  const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
  const parseBooleanParam = (value) => {
    const normalized = `${value ?? ''}`.trim().toLowerCase();
    if (!normalized) {
      return null;
    }

    if (['1', 'true', 'yes', 'on'].includes(normalized)) {
      return true;
    }

    if (['0', 'false', 'no', 'off'].includes(normalized)) {
      return false;
    }

    return null;
  };
  const formatStateNumber = (value, precision = 4) => {
    if (!Number.isFinite(value)) {
      return '';
    }

    return Number(value).toFixed(precision).replace(/\.?0+$/, '');
  };
  const escapeHtml = (value) =>
    `${value ?? ''}`
      .replace(/&/g, '&')
      .replace(/</g, '<')
      .replace(/>/g, '>')
      .replace(/"/g, '"')
      .replace(/'/g, ''');
  const escapeRegExp = (value) => `${value ?? ''}`.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  const slugifyPathSegment = (value = '') => {
    const normalized = `${value ?? ''}`
      .trim()
      .normalize('NFKD')
      .replace(/[\u0300-\u036f]/g, '')
      .toLowerCase()
      .replace(/[^a-z0-9]+/g, '-')
      .replace(/^-+|-+$/g, '');

    return normalized || 'item';
  };

  const buildChildRouteUrl = (baseUrl, segments = [], hash = '') => {
    const url = createUrlOr(baseUrl, null, { context: 'workstation-child-route' });

    if (!url) {
      return baseUrl || window.location.href;
    }

    const basePath = url.pathname.replace(/\/+$/, '');
    const childPath = segments
      .map((segment) => slugifyPathSegment(segment))
      .filter(Boolean)
      .map(encodeURIComponent)
      .join('/');

    url.pathname = `${basePath || ''}/${childPath ? `${childPath}/` : ''}`.replace(/\/{2,}/g, '/');
    url.search = '';
    url.hash = hash || '';

    return url.toString();
  };

  const isEditableTarget = (target) => {
    if (!(target instanceof HTMLElement)) {
      return false;
    }

    const tagName = target.tagName.toUpperCase();
    return target.isContentEditable || tagName === 'INPUT' || tagName === 'TEXTAREA' || tagName === 'SELECT';
  };

  const normalizeCropRect = (rect = {}) => {
    const x = clamp(Number(rect.x) || DEFAULT_CROP.x, 0, 1);
    const y = clamp(Number(rect.y) || DEFAULT_CROP.y, 0, 1);
    let width = clamp(Number(rect.width) || DEFAULT_CROP.width, MIN_CROP_SIZE, 1);
    let height = clamp(Number(rect.height) || DEFAULT_CROP.height, MIN_CROP_SIZE, 1);
    let nextX = x;
    let nextY = y;

    if (nextX + width > 1) {
      nextX = Math.max(0, 1 - width);
    }

    if (nextY + height > 1) {
      nextY = Math.max(0, 1 - height);
    }

    width = clamp(width, MIN_CROP_SIZE, 1 - nextX);
    height = clamp(height, MIN_CROP_SIZE, 1 - nextY);

    return {
      x: Number(nextX.toFixed(6)),
      y: Number(nextY.toFixed(6)),
      width: Number(width.toFixed(6)),
      height: Number(height.toFixed(6)),
    };
  };

  const parseCrop = (raw) => {
    if (!raw) {
      return { ...DEFAULT_CROP };
    }

    return normalizeCropRect(parseJsonOr(raw, DEFAULT_CROP, { context: 'workstation-crop' }));
  };
  const parseOverlayData = (raw) => {
    if (!raw) {
      return {};
    }

    const parsed = parseJsonOr(raw, {}, { context: 'workstation-overlay-data' });
    return parsed && typeof parsed === 'object' ? parsed : {};
  };
  const getOverlayNodesUrl = (overlayData = {}) =>
    `${overlayData.nodes_url || overlayData.nodesUrl || ''}`.trim();
  const overlayIncludesGlobalNodes = (overlayData = {}) =>
    Boolean(overlayData.include_global_nodes || overlayData.includeGlobalNodes);

  const buildNodeUrl = (baseUrl, nodeId) => {
    if (!baseUrl || !nodeId) {
      return '#study-surface-tools';
    }

    return buildChildRouteUrl(config.manuscriptPageUrl || baseUrl, ['node', nodeId], '#study-surface-tools');
  };

  const addCacheBuster = (url) => {
    if (!url) {
      return '';
    }

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