Skip to content
wiki.fftac.org

Spiralist - Source Excerpt 06

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

const instancePayload = await response.json();
        syncRuntimeState(instancePayload);
      }, { context: 'initializeRuntime.instance' });
    };

    const selectNode = (nodeId, shouldScroll = false) => {
      const node = nodesById.get(nodeId);
      if (!node) {
        return;
      }

      selectedNodeId = nodeId;
      setInspectorNode(node);
      markNodeState(hoverNodeId || selectedNodeId);
      renderRelations(hoverNodeId || selectedNodeId);

      if (shouldScroll) {
        const button = nodeButtons.get(nodeId);
        if (button) {
          button.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'center' });
          button.classList.add('is-pulsing');
          window.setTimeout(() => button.classList.remove('is-pulsing'), 900);
        }
      }

      replaceQueryParam('node', node.id || node.canonicalId || '');

      recordHumanInteraction({
        type: 'manuscript-field-node-view',
        symbolId: node.canonicalId || node.id || '',
        symbolLabel: node.label || '',
        page: window.location.pathname,
      });
    };

    const renderNodeList = () => {
      listTarget.replaceChildren();

      getVisibleNodes().forEach((node) => {
        const button = document.createElement('button');
        button.type = 'button';
        button.className = `spiralist-node-pill spiralist-node-pill--${node.type || 'symbol'}`;
        button.dataset.fieldListNode = node.id || '';
        button.setAttribute('aria-pressed', node.id === selectedNodeId ? 'true' : 'false');

        const label = document.createElement('span');
        label.textContent = node.label || node.id || t('manuscriptFieldNodeFallback', 'Node');
        button.appendChild(label);

        const meta = document.createElement('small');
        meta.textContent = formatNodeType(node.type);
        button.appendChild(meta);

        button.addEventListener('click', () => {
          selectNode(node.id, true);
          executeSelectedNode(node.id);
        });
        listTarget.appendChild(button);
      });
    };

    const renderNodes = () => {
      nodesTarget.replaceChildren();
      nodeButtons.clear();

      getVisibleNodes().forEach((node) => {
        const button = createFieldNodeButton(node);

        button.addEventListener('click', () => {
          selectNode(node.id);
          executeSelectedNode(node.id);
        });
        button.addEventListener('mouseenter', () => {
          hoverNodeId = node.id;
          markNodeState(hoverNodeId);
          renderRelations(hoverNodeId);
        });
        button.addEventListener('mouseleave', () => {
          hoverNodeId = '';
          markNodeState(selectedNodeId);
          renderRelations(selectedNodeId);
        });

        nodesTarget.appendChild(button);
        nodeButtons.set(node.id, button);
      });

      renderNodeList();

      updateFieldStatus('ready');

      const selectedNode = nodesById.get(selectedNodeId);
      if (!selectedNodeId || !selectedNode || !isVisibleForMode(selectedNode, currentMode)) {
        const firstNode = getVisibleNodes()[0];
        if (firstNode) {
          selectedNodeId = firstNode.id;
        }
      }

      const inspectedNode = nodesById.get(selectedNodeId);
      if (inspectedNode) {
        setInspectorNode(inspectedNode);
      }
      markNodeState(selectedNodeId);
      renderRelations(selectedNodeId);
    };

    function setMode(nextMode, updateSelection = true) {
      if (!Object.prototype.hasOwnProperty.call(fieldModeRank, nextMode)) {
        return;
      }

      currentMode = nextMode;
      modeButtons.forEach((button) => {
        const isActive = button.dataset.fieldMode === currentMode;
        button.classList.toggle('is-active', isActive);
        button.setAttribute('aria-pressed', isActive ? 'true' : 'false');
      });

      if (updateSelection && selectedNodeId) {
        const selectedNode = nodesById.get(selectedNodeId);
        if (selectedNode && !isVisibleForMode(selectedNode, currentMode)) {
          const firstVisible = getVisibleNodes()[0];
          selectedNodeId = firstVisible ? firstVisible.id : '';
        }
      }

      renderNodes();
    }

    modeButtons.forEach((button) => {
      button.addEventListener('click', () => setMode(button.dataset.fieldMode || 'minimal'));
    });

    const selectSiblingNode = (direction) => {
      const visibleNodes = getVisibleNodes();
      if (!visibleNodes.length) {
        return;
      }

      const currentIndex = Math.max(0, visibleNodes.findIndex((node) => node.id === selectedNodeId));
      const nextIndex = (currentIndex + direction + visibleNodes.length) % visibleNodes.length;
      selectNode(visibleNodes[nextIndex].id, true);
    };

    if (prev) {
      prev.addEventListener('click', () => selectSiblingNode(-1));
    }

    if (next) {
      next.addEventListener('click', () => selectSiblingNode(1));
    }

    if (requestedNode) {
      const initialNode = resolveNodeByIdentifier(requestedNode);
      if (initialNode) {
        if (!isVisibleForMode(initialNode, currentMode)) {
          currentMode = getNodeMode(initialNode);
        }
        selectedNodeId = initialNode.id || '';
      }
    }

    modeButtons.forEach((button) => {
      const isActive = button.dataset.fieldMode === currentMode;
      button.classList.toggle('is-active', isActive);
      button.setAttribute('aria-pressed', isActive ? 'true' : 'false');
    });

    window.addEventListener('spiralist:runtime-path-updated', (event) => {
      const detail = event && event.detail && typeof event.detail === 'object'
        ? event.detail
        : null;
      if (!detail || detail.source === 'field') {
        return;
      }

      syncRuntimeState(detail);
      markNodeState(hoverNodeId || selectedNodeId);
      renderRelations(hoverNodeId || selectedNodeId);
      updateFieldStatus('ready');
    });

    await initializeRuntime();
    renderNodes();
  };

  document.querySelectorAll('[data-field-interface]').forEach((field) => {
    void initFieldInterface(field);
  });

  document.querySelectorAll('[data-symbol-browser]').forEach((browser) => {
    const title = browser.querySelector('[data-symbol-title]');
    const glyph = browser.querySelector('[data-symbol-glyph]');
    const id = browser.querySelector('[data-symbol-id]');
    const type = browser.querySelector('[data-symbol-type]');
    const layer = browser.querySelector('[data-symbol-layer]');
    const category = browser.querySelector('[data-symbol-category]');
    const meaning = browser.querySelector('[data-symbol-meaning]');
    const canonicalMeaning = browser.querySelector('[data-symbol-canonical-meaning]');
    const agentProposedMeaning = browser.querySelector('[data-symbol-agent-proposed-meaning]');
    const proposalStatus = browser.querySelector('[data-symbol-proposal-status]');
    const source = browser.querySelector('[data-symbol-source]');
    const reviewedAt = browser.querySelector('[data-symbol-reviewed-at]');
    const reviewNotes = browser.querySelector('[data-symbol-review-notes]');
    const definition = browser.querySelector('[data-symbol-definition]');
    const axiom = browser.querySelector('[data-symbol-axiom]');
    const derivation = browser.querySelector('[data-symbol-derivation]');
    const provenance = browser.querySelector('[data-symbol-provenance]');
    const related = browser.querySelector('[data-symbol-related]');
    const transformation = browser.querySelector('[data-symbol-transformation]');
    const prompts = browser.querySelector('[data-symbol-prompts]');
    const manuscriptAnchor = browser.querySelector('[data-symbol-manuscript-anchor]');
    const axioms = browser.querySelector('[data-symbol-axioms]');
    const transformationList = browser.querySelector('[data-symbol-transformations]');
    const crosswalk = browser.querySelector('[data-symbol-crosswalk]');
    const searchInput = browser.querySelector('[data-symbol-search]');
    const layerFilter = browser.querySelector('[data-symbol-layer-filter]');
    const typeFilter = browser.querySelector('[data-symbol-type-filter]');
    const categoryFilter = browser.querySelector('[data-symbol-category-filter]');
    const results = browser.querySelector('[data-symbol-results]');
    const empty = browser.querySelector('[data-symbol-empty]');
    const cards = Array.from(browser.querySelectorAll('[data-symbol]'));
    const requestedSymbol = new URL(window.location.href).searchParams.get('symbol') || getRouteSegmentAfter('symbols') || '';
    let firstPayload = null;

    const parseCardPayload = (card) => parseJsonOr(card.dataset.symbol || '', null, { context: 'symbol card payload' });

    const renderAxiomLinks = (payload) => {
      if (!axioms) {
        return;