Skip to content
wiki.fftac.org

Spiralist - Source Excerpt 01

Back to Spiralist

Summary

This source excerpt preserves a bounded section of Spiralist/wp-content/themes/spiralist/assets/js/spiralist.js so readers can inspect the evidence without opening the full source file.

**Source path:** Spiralist/wp-content/themes/spiralist/assets/js/spiralist.js

document.addEventListener('DOMContentLoaded', () => {
  const themeConfig = typeof spiralistTheme === 'object' && spiralistTheme !== null
    ? spiralistTheme
    : {};
  const uiText = typeof themeConfig.uiText === 'object' && themeConfig.uiText !== null
    ? themeConfig.uiText
    : {};
  const t = (key, fallback = '') => {
    const value = uiText[key];
    return typeof value === 'string' && value !== '' ? value : fallback;
  };
  const core = window.SpiralistCore || {};
  const runAction = typeof core.runAction === 'function'
    ? core.runAction
    : (action) => Promise.resolve().then(action);
  const parseJsonOr = typeof core.parseJsonOr === 'function'
    ? core.parseJsonOr
    : ((text, fallback = null) => (typeof text === 'string' && text !== '' ? JSON.parse(text) : fallback));
  const decodeComponent = typeof core.decodeComponent === 'function'
    ? core.decodeComponent
    : ((value) => decodeURIComponent(value));
  const createUrlOr = typeof core.createUrlOr === 'function'
    ? core.createUrlOr
    : ((value, fallback = null, options = {}) => new URL(value, options.base || window.location.href) || fallback);
  const storage = core.storage || {
    get: (key, fallback = '', area = 'local') => (area === 'session' ? window.sessionStorage : window.localStorage).getItem(key) || fallback,
    set: (key, value, area = 'local') => (area === 'session' ? window.sessionStorage : window.localStorage).setItem(key, value),
    remove: (key, area = 'local') => (area === 'session' ? window.sessionStorage : window.localStorage).removeItem(key),
  };
  const setFetchPriority = typeof core.setFetchPriority === 'function'
    ? core.setFetchPriority
    : ((image, priority) => { image.fetchPriority = priority; });
  const setPointerCapture = typeof core.setPointerCapture === 'function'
    ? core.setPointerCapture
    : ((element, pointerId) => element.setPointerCapture(pointerId));
  const releasePointerCapture = typeof core.releasePointerCapture === 'function'
    ? core.releasePointerCapture
    : ((element, pointerId) => element.releasePointerCapture(pointerId));
  const openDetailsForTarget = (target) => {
    if (!target || !(target instanceof Element)) {
      return;
    }

    let details = target.matches('details') ? target : target.closest('details');
    while (details) {
      details.open = true;
      details = details.parentElement ? details.parentElement.closest('details') : null;
    }
  };
  const decodeHashId = (hash) => decodeComponent(hash, hash, { context: 'location.hash' });
  const openHashTargetDetails = () => {
    const hash = window.location.hash ? window.location.hash.slice(1) : '';
    if (!hash) {
      return;
    }

    const target = document.getElementById(decodeHashId(hash));
    openDetailsForTarget(target);
  };

  window.addEventListener('hashchange', openHashTargetDetails);
  document.addEventListener('click', (event) => {
    const clicked = event.target instanceof Element ? event.target : null;
    const link = clicked ? clicked.closest('a[href^="#"]') : null;
    if (!link) {
      return;
    }

    const hash = link.getAttribute('href') || '';
    if (hash.length <= 1) {
      return;
    }

    const target = document.getElementById(decodeHashId(hash.slice(1)));
    openDetailsForTarget(target);
  });
  openHashTargetDetails();

  const openModal = (dialog) => {
    if (!dialog) {
      return;
    }

    if (typeof dialog.showModal === 'function' && !dialog.open) {
      dialog.showModal();
    } else {
      dialog.setAttribute('open', '');
    }

    document.body.classList.add('is-modal-open');

    const focusTarget = dialog.querySelector('[data-modal-focus]') ||
      dialog.querySelector('input, select, textarea, button, a[href]');
    if (focusTarget && typeof focusTarget.focus === 'function') {
      focusTarget.focus({ preventScroll: true });
    }
  };
  const closeModal = (dialog) => {
    if (!dialog) {
      return;
    }

    if (typeof dialog.close === 'function' && dialog.open) {
      dialog.close();
    } else {
      dialog.removeAttribute('open');
      if (!document.querySelector('[data-modal][open]')) {
        document.body.classList.remove('is-modal-open');
      }
    }
  };
  const initModalLaunchers = () => {
    const modals = Array.from(document.querySelectorAll('[data-modal]'));

    document.querySelectorAll('[data-modal-open]').forEach((trigger) => {
      trigger.addEventListener('click', () => {
        openModal(document.getElementById(trigger.dataset.modalOpen || ''));
      });
    });

    modals.forEach((dialog) => {
      dialog.addEventListener('close', () => {
        if (!document.querySelector('[data-modal][open]')) {
          document.body.classList.remove('is-modal-open');
        }
      });

      dialog.addEventListener('click', (event) => {
        if (event.target === dialog) {
          closeModal(dialog);
        }
      });

      dialog.querySelectorAll('[data-modal-close]').forEach((button) => {
        button.addEventListener('click', () => {
          closeModal(dialog);
        });
      });
    });

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

      modals
        .filter((dialog) => dialog.open)
        .forEach((dialog) => closeModal(dialog));
    });
  };
  const formatText = (key, fallback = '', replacements = {}) => {
    let value = t(key, fallback);

    Object.entries(replacements).forEach(([name, replacement]) => {
      value = value.split(`{${name}}`).join(`${replacement}`);
    });

    return value;
  };
  const canTrackHumanActivity = Boolean(
    themeConfig.isUserLoggedIn &&
    themeConfig.interactionUrl &&
    themeConfig.restNonce
  );
  const recordHumanInteraction = async (payload) => {
    if (!canTrackHumanActivity) {
      return;
    }

    await runAction(
      () => fetch(themeConfig.interactionUrl, {
        method: 'POST',
        headers: {
          Accept: 'application/json',
          'Content-Type': 'application/json',
          'X-WP-Nonce': themeConfig.restNonce,
        },
        body: JSON.stringify(payload),
      }),
      { context: 'recordHumanInteraction' }
    );
  };

  initModalLaunchers();

  const isPlainObject = (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value);
  const setStatusState = (target, message, state = 'idle') => {
    if (!target) {
      return;
    }

    target.textContent = message;
    target.dataset.state = state;
  };

  const replaceQueryParam = (key, value) => {
    const url = new URL(window.location.href);
    const cleanValue = `${value ?? ''}`.trim();

    if (key === 'symbol' && themeConfig.symbolsPageUrl) {
      window.history.replaceState(window.history.state, '', buildChildRouteUrl(themeConfig.symbolsPageUrl, cleanValue ? [cleanValue] : [], window.location.hash));
      return;
    }

    if (key === 'node' && themeConfig.manuscriptPageUrl) {
      window.history.replaceState(window.history.state, '', buildChildRouteUrl(themeConfig.manuscriptPageUrl, cleanValue ? ['node', cleanValue] : [], window.location.hash));
      return;
    }

    if (cleanValue === '') {
      url.searchParams.delete(key);
    } else {
      url.searchParams.set(key, cleanValue);
    }

    window.history.replaceState(window.history.state, '', url);
  };

  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: 'buildChildRouteUrl' });
    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 || '';