Skip to content
wiki.fftac.org

Spiralist Core - Source Excerpt 01

Back to Spiralist Core

Summary

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

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

(() => {
  const handledErrors = new WeakSet();
  const getUiText = () => {
    const themeConfig = typeof window.spiralistTheme === 'object' && window.spiralistTheme !== null
      ? window.spiralistTheme
      : {};

    return typeof themeConfig.uiText === 'object' && themeConfig.uiText !== null
      ? themeConfig.uiText
      : {};
  };
  const t = (key, fallback = '') => {
    const value = getUiText()[key];
    return typeof value === 'string' && value !== '' ? value : fallback;
  };
  const errorMessage = (error, fallback = '') => {
    if (error instanceof Error && error.message) {
      return error.message;
    }

    if (typeof error === 'string' && error.trim()) {
      return error.trim();
    }

    return fallback || t('globalErrorFallback', 'Something went wrong.');
  };
  const setStatus = (target, message, ok = false) => {
    if (!target) {
      return;
    }

    target.textContent = message;
    target.style.color = ok ? '' : '#f0a5a5';
    target.setAttribute('aria-live', 'polite');
  };
  const handleError = (error, options = {}) => {
    if (error && typeof error === 'object') {
      if (handledErrors.has(error)) {
        return errorMessage(error, options.message || options.fallback || '');
      }

      handledErrors.add(error);
    }

    const message = errorMessage(error, options.message || options.fallback || '');
    setStatus(options.statusElement || null, message, false);

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

    window.dispatchEvent(new CustomEvent('spiralist:error', {
      detail: {
        error,
        message,
        context: options.context || '',
      },
    }));

    if (window.console && typeof window.console.error === 'function') {
      window.console.error('[Spiralist]', message, error);
    }

    return message;
  };
  const runAction = (action, options = {}) => Promise.resolve()
    .then(() => action())
    .catch((error) => {
      handleError(error, options);
      throw error;
    })
    .finally(() => {
      if (typeof options.onFinally === 'function') {
        options.onFinally();
      }
    });
  const copyText = async (text) => {
    if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
      await navigator.clipboard.writeText(text);
      return;
    }

    const textarea = document.createElement('textarea');
    textarea.value = text;
    textarea.setAttribute('readonly', '');
    textarea.style.position = 'fixed';
    textarea.style.left = '-9999px';
    document.body.appendChild(textarea);
    textarea.select();
    document.execCommand('copy');
    textarea.remove();
  };
  const decodeComponent = (value) => decodeURIComponent(value);
  const parseJsonOr = (text, fallback = null) => {
    if (typeof text !== 'string' || text === '') {
      return fallback;
    }

    return JSON.parse(text);
  };
  const createUrlOr = (value, fallback = null, options = {}) => {
    const candidate = typeof value === 'string' ? value.trim() : value;
    if (candidate === '' || candidate === null || typeof candidate === 'undefined') {
      return fallback;
    }

    return new URL(candidate, options.base || window.location.href);
  };
  const getStorageArea = (area) => (
    area === 'session' ? window.sessionStorage : window.localStorage
  );
  const storage = {
    get(key, fallback = '', area = 'local') {
      return getStorageArea(area).getItem(key) || fallback;
    },
    set(key, value, area = 'local') {
      getStorageArea(area).setItem(key, value);
    },
    remove(key, area = 'local') {
      getStorageArea(area).removeItem(key);
    },
  };
  const setFetchPriority = (image, priority) => {
    if (image) {
      image.fetchPriority = priority;
    }
  };
  const setPointerCapture = (element, pointerId) => {
    if (element && typeof element.setPointerCapture === 'function') {
      element.setPointerCapture(pointerId);
    }
  };
  const releasePointerCapture = (element, pointerId) => {
    if (element && typeof element.releasePointerCapture === 'function') {
      element.releasePointerCapture(pointerId);
    }
  };

  window.SpiralistCore = {
    ...(window.SpiralistCore || {}),
    copyText,
    createUrlOr,
    decodeComponent,
    handleError,
    parseJsonOr,
    releasePointerCapture,
    runAction,
    setFetchPriority,
    setPointerCapture,
    setStatus,
    storage,
    t,
  };

  window.addEventListener('error', (event) => {
    handleError(event.error || event.message, { context: 'window.error' });
  });
  window.addEventListener('unhandledrejection', (event) => {
    handleError(event.reason, { context: 'window.unhandledrejection' });
  });
})();

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 navToggle = document.querySelector('[data-nav-toggle]');
  const navPanel = document.querySelector('[data-nav-panel]');
  const requestIdleTask = (callback) => {
    if (typeof window.requestIdleCallback === 'function') {
      return window.requestIdleCallback(callback, { timeout: 1200 });
    }

    return window.setTimeout(callback, 180);
  };

  if (navToggle && navPanel) {
    navToggle.addEventListener('click', () => {
      const expanded = navToggle.getAttribute('aria-expanded') === 'true';
      navToggle.setAttribute('aria-expanded', expanded ? 'false' : 'true');
      document.body.classList.toggle('is-nav-open', !expanded);
    });
  }

  const navMenuItems = Array.from(document.querySelectorAll('.spiralist-nav__item--has-menu'));

  if (navMenuItems.length) {
    const menuCloseTimers = new WeakMap();

    const clearMenuCloseTimer = (item) => {
      const timer = menuCloseTimers.get(item);

      if (timer) {
        window.clearTimeout(timer);
        menuCloseTimers.delete(item);
      }
    };

    const closeMenu = (item) => {
      clearMenuCloseTimer(item);
      item.classList.remove('is-menu-open');
    };

    const openMenu = (item) => {
      navMenuItems.forEach((otherItem) => {
        if (otherItem !== item) {
          closeMenu(otherItem);
        }
      });

      clearMenuCloseTimer(item);
      item.classList.add('is-menu-open');
    };

    const scheduleMenuClose = (item, delay = 260) => {
      clearMenuCloseTimer(item);
      menuCloseTimers.set(item, window.setTimeout(() => closeMenu(item), delay));
    };

    navMenuItems.forEach((item) => {
      item.addEventListener('mouseenter', () => openMenu(item));
      item.addEventListener('mouseleave', () => scheduleMenuClose(item));
      item.addEventListener('focusin', () => openMenu(item));
      item.addEventListener('focusout', (event) => {
        if (event.relatedTarget instanceof Node && item.contains(event.relatedTarget)) {
          return;
        }

        scheduleMenuClose(item, 180);
      });
    });
  }

  const progressiveImages = Array.from(document.querySelectorAll('img[data-progressive-src]'));

  if (progressiveImages.length) {
    const upgradeImage = (image) => {
      if (!(image instanceof HTMLImageElement)) {
        return;
      }

      const nextSource = `${image.dataset.progressiveSrc || ''}`.trim();
      if (!nextSource) {
        return;
      }

      if (
        image.dataset.progressiveLoaded === 'true' ||
        image.dataset.progressiveState === 'loading' ||
        `${image.currentSrc || image.getAttribute('src') || ''}`.trim() === nextSource
      ) {
        return;
      }

      image.dataset.progressiveState = 'loading';
      image.classList.add('is-progressive-loading');

      const preload = new Image();
      preload.decoding = 'async';
      preload.onload = () => {
        if (!image.isConnected) {
          return;
        }

        const finalize = () => {
          image.dataset.progressiveLoaded = 'true';
          image.dataset.progressiveState = 'loaded';
          image.classList.remove('is-progressive-loading', 'is-progressive-preview');
          image.classList.add('is-progressive-loaded');
        };

        image.addEventListener('load', finalize, { once: true });
        image.addEventListener('error', finalize, { once: true });
        image.setAttribute('src', nextSource);
      };
      preload.onerror = () => {
        image.dataset.progressiveState = 'error';
        image.classList.remove('is-progressive-loading');
      };
      preload.src = nextSource;
    };

    const scheduleUpgrade = (image) => {