Skip to content
wiki.fftac.org

Book Shell - Source Excerpt 06

Back to Book Shell

Summary

This source excerpt preserves a bounded section of Spiralist/wp-content/plugins/ns12-manuscript/assets/js/book-shell.js so readers can inspect the evidence without opening the full source file.

**Source path:** Spiralist/wp-content/plugins/ns12-manuscript/assets/js/book-shell.js

const instancePayload = await response.json();
        syncRuntimeState(instancePayload);
      }, { context: 'field-runtime-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');
    });

    await initializeRuntime();
    renderNodes();
  };

  document.querySelectorAll('[data-field-interface]').forEach((field) => {
    const statusTarget = field.querySelector('[data-field-status]');
    runBackgroundAction(() => initFieldInterface(field), {
      context: 'field-interface',
      onError: () => {
        setStatusState(statusTarget, t('manuscriptFieldDatasetError', 'The symbolic field dataset could not be loaded.'), 'error');
      },
    });
  });

  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 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 currentUrl = createUrlOr(window.location.href, null, { context: 'symbol-browser-url' });
    const requestedSymbol = (currentUrl ? currentUrl.searchParams.get('symbol') : '') || getRouteSegmentAfter('symbols') || '';
    const cardPayloadCache = new WeakMap();
    let firstPayload = null;

    const parseCardPayload = (card) => {
      if (cardPayloadCache.has(card)) {
        return cardPayloadCache.get(card);
      }

      const payload = parseJsonOr(card.dataset.symbol || '', null, { context: 'symbol-card-payload' });
      cardPayloadCache.set(card, payload);