Skip to content
wiki.fftac.org

Spiralist - Source Excerpt 04

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

storage.set(runtimeStorageKey, JSON.stringify(runtimePayload));
      window.dispatchEvent(new CustomEvent('spiralist:runtime-path-updated', {
        detail: {
          ...runtimePayload,
          source: 'field',
        },
      }));
    };

    const getNodeLabelById = (nodeId = '') => {
      const node = nodesById.get(nodeId);
      return node ? (node.label || node.id || nodeId) : nodeId;
    };

    const syncRuntimeState = (payloadState = {}) => {
      if (!payloadState || typeof payloadState !== 'object') {
        return;
      }

      if (payloadState.current_state || payloadState.currentState) {
        runtimeState.currentState = payloadState.current_state || payloadState.currentState;
      }

      if (payloadState.current_node || payloadState.currentNode) {
        runtimeState.currentNode = payloadState.current_node || payloadState.currentNode;
      }

      if (Array.isArray(payloadState.history)) {
        runtimeState.history = payloadState.history;
      }

      if (Array.isArray(payloadState.active_symbols) || Array.isArray(payloadState.activeSymbols)) {
        runtimeState.activeSymbols = Array.isArray(payloadState.active_symbols)
          ? payloadState.active_symbols
          : payloadState.activeSymbols;
      }

      if (Array.isArray(payloadState.next_nodes) || Array.isArray(payloadState.nextNodes)) {
        const nextNodes = Array.isArray(payloadState.next_nodes)
          ? payloadState.next_nodes
          : payloadState.nextNodes;

        runtimeState.nextNodeIds = nextNodes
          .map((entry) => (entry && typeof entry === 'object' ? (entry.node_id || entry.nodeId || entry.id || '') : `${entry || ''}`))
          .filter(Boolean);
      }
    };

    const buildStatusMessage = () => {
      const visibleCount = getVisibleNodes().length;
      let message = `${formatText('manuscriptFieldModeStatus', '{mode} mode: {visibleCount} of {totalCount} nodes visible.', {
        mode: formatNodeType(currentMode),
        visibleCount,
        totalCount: nodes.length,
      })} ${
        currentMode === 'expert'
          ? t('manuscriptFieldRelationsVisible', 'Relations remain visible.')
          : t('manuscriptFieldRelationsHover', 'Relations appear on hover or selection.')
      }`;

      if (runtimeState.currentState) {
        message += ` ${formatText('manuscriptFieldCurrentState', 'Current state: {state}.', { state: runtimeState.currentState })}`;
      }

      if (runtimeState.nextNodeIds.length) {
        message += ` ${formatText('manuscriptFieldNextNodes', 'Next nodes: {nodes}.', {
          nodes: runtimeState.nextNodeIds.map((nodeId) => getNodeLabelById(nodeId)).join(', '),
        })}`;
      }

      if (runtimeState.history.length) {
        const trail = runtimeState.history
          .slice(-3)
          .map((entry) => getNodeLabelById(entry.node_id || entry.current_node || ''))
          .filter(Boolean);

        if (trail.length) {
          message += ` ${formatText('manuscriptFieldHistory', 'History: {history}.', { history: trail.join(' -> ') })}`;
        }
      }

      return message;
    };

    const updateFieldStatus = (state = 'ready') => {
      setStatusState(statusTarget, buildStatusMessage(), state);
    };

    const getConnectedIds = (nodeId) => {
      const ids = new Set();

      getVisibleRelations().forEach((relation) => {
        if (relation.from === nodeId) {
          ids.add(relation.to);
        }
        if (relation.to === nodeId) {
          ids.add(relation.from);
        }
      });

      const node = nodesById.get(nodeId);
      if (node && Array.isArray(node.related)) {
        node.related.forEach((relatedId) => {
          const relatedNode = resolveNodeByIdentifier(relatedId);
          ids.add(relatedNode ? relatedNode.id : relatedId);
        });
      }

      return ids;
    };

    const renderRelations = (focusId = selectedNodeId) => {
      relationsTarget.replaceChildren();

      const visibleRelations = getVisibleRelations();
      const relationsToRender = currentMode === 'expert'
        ? visibleRelations
        : visibleRelations.filter((relation) => focusId && (relation.from === focusId || relation.to === focusId));

      relationsToRender.forEach((relation) => {
        const fromNode = nodesById.get(relation.from);
        const toNode = nodesById.get(relation.to);

        if (!fromNode || !toNode) {
          return;
        }

        const line = createRelationLine(relation, fromNode, toNode);
        relationsTarget.appendChild(line);
        window.requestAnimationFrame(() => line.classList.add('is-visible'));
      });
    };

    const markNodeState = (focusId = selectedNodeId) => {
      const connectedIds = focusId ? getConnectedIds(focusId) : new Set();
      runtimeState.nextNodeIds.forEach((nodeId) => connectedIds.add(nodeId));

      nodeButtons.forEach((button, nodeId) => {
        const isActive = nodeId === selectedNodeId;
        button.classList.toggle('is-active', isActive);
        button.classList.toggle('is-related', connectedIds.has(nodeId) && !isActive);
        button.setAttribute('aria-pressed', isActive ? 'true' : 'false');
      });

      listTarget.querySelectorAll('[data-field-list-node]').forEach((button) => {
        const nodeId = button.dataset.fieldListNode || '';
        const isActive = nodeId === selectedNodeId;
        button.classList.toggle('is-active', isActive);
        button.classList.toggle('is-related', connectedIds.has(nodeId) && !isActive);
        button.setAttribute('aria-pressed', isActive ? 'true' : 'false');
      });
    };

    const setRelatedNodes = (node) => {
      if (!related) {
        return;
      }

      related.replaceChildren();

      const relatedIds = Array.isArray(node.related) ? node.related : [];
      if (!relatedIds.length) {
        related.textContent = t('symbolsRelatedEmpty', 'No related symbols declared.');
        return;
      }

      relatedIds.forEach((relatedId) => {
        const relatedNode = resolveNodeByIdentifier(relatedId);

        const button = document.createElement('button');
        button.type = 'button';
        button.className = 'spiralist-related-node';
        button.textContent = relatedNode ? relatedNode.label : relatedId;
        button.addEventListener('click', () => {
          if (!relatedNode) {
            return;
          }

          if (!isVisibleForMode(relatedNode, currentMode)) {
            setMode(getNodeMode(relatedNode), false);
          }

          selectNode(relatedNode.id, true);
          executeSelectedNode(relatedNode.id);
        });
        related.appendChild(button);
      });
    };

    const setCrosswalkLinks = (node) => {
      if (!crosswalk) {
        return;
      }

      crosswalk.replaceChildren();

      const links = [];
      const canonicalId = node.canonicalId || '';
      const promptQuery = getPromptSearchQuery(node);

      if (node.type === 'symbol' && themeConfig.symbolsPageUrl && canonicalId) {
        links.push({
          label: t('symbolsCrosswalkSymbols', 'Open in symbols explorer'),
          url: buildChildRouteUrl(themeConfig.symbolsPageUrl, [canonicalId]),
        });
      }

      if (themeConfig.promptsPageUrl && promptQuery) {
        links.push({
          label: t('symbolsCrosswalkPrompts', 'Search prompts'),
          url: buildPromptSearchUrl(promptQuery),
        });
      }

      if (node.type === 'transformation' && themeConfig.symbolsPageUrl) {
        links.push({
          label: t('symbolsCrosswalkTransformations', 'View transformations'),
          url: `${themeConfig.symbolsPageUrl}#transformations`,
        });
      }

      if ((node.type === 'axiom' || node.type === 'structure') && themeConfig.systemUrl) {
        links.push({
          label: t('symbolsCrosswalkSystem', 'System concepts'),
          url: themeConfig.systemUrl,
        });
      }

      if (!links.length) {
        const empty = document.createElement('span');
        empty.className = 'spiralist-muted';
        empty.textContent = t('symbolsCrosswalkEmpty', 'No crosswalk links available.');
        crosswalk.appendChild(empty);
        return;
      }

      links.forEach((link) => {
        const anchor = document.createElement('a');
        anchor.className = 'spiralist-chip spiralist-chip--link';
        anchor.href = link.url;
        anchor.textContent = link.label;
        crosswalk.appendChild(anchor);
      });
    };

    const setInspectorNode = (node) => {
      if (type) type.textContent = `${formatNodeType(node.type)} Node`;
      if (glyph) glyph.textContent = node.type === 'symbol' ? (node.glyph || '\u25CB') : '';