about summary refs log tree commit diff stats
path: root/js/scripting-lang/web/src/app.js
blob: 086cba1e707128f2a77e62722d8ee5a65bb0a622 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
// app.js
// Entrypoint for the app

import { initialState, cloneState } from './state.js';
import { update } from './update.js';
import { view } from './view.js';
import { fetchPokemon, fetchEvolutionData, executeBabaYagaScript, getExampleScripts, getCurrentHarness } from './api.js';
import { initDevMode } from './dev.js';

const root = document.getElementById('app');
let state = cloneState(initialState);
let dev;

/**
 * Entrypoint for the app.
 *
 * This file implements a minimal Elm-style architecture using only browser APIs and ES modules.
 * - All state is immutable and updated by a pure update function.
 * - The entire UI is re-rendered as a string on each state change for simplicity and predictability.
 * - Event delegation is used to keep wiring minimal and functional.
 * - No 3rd party code: everything is browser-native for cozy portability and clarity.
 *
 * Why this approach?
 * - Functional, pure update/view logic is easier for me to reason about and test.
 * - Re-rendering the whole UI avoids bugs from manual DOM updates and keeps state/UI in sync.
 * - Minimal code and clear data flow make it easy to extend or adapt for new projects.
 */

// Enable devMode if ?dev=1 is in the URL
/**
 * devMode enables logging of all actions and state transitions for debugging.
 *
 * Why? This makes the app's state flow transparent, helping you understand and debug the app without extra tooling.
 */
const devMode = window.location.search.includes('dev=1');

/**
 * Generalized render function for Elm-style apps.
 *
 * @param {Object} config - Render configuration
 * @param {HTMLElement} config.root - Root DOM element
 * @param {any} config.state - Current app state
 * @param {Function} config.view - View function (state => HTML string)
 * @param {Array} [config.events] - Array of { selector, event, handler }
 * @param {Function} [config.postRender] - Optional function({ root, state }) for post-render logic
 */
function render({ root, state, view, events = [], postRender }) {
  root.innerHTML = view(state);
  events.forEach(({ selector, event, handler }) => {
    const el = root.querySelector(selector);
    if (el) el.addEventListener(event, handler);
  });
  if (typeof postRender === 'function') {
    postRender({ root, state });
  }
}

// --- App-specific config for render ---
function postRender({ root, state }) {
  // Preserve scroll position
  const scrollPosition = window.scrollY;
  
  const input = root.querySelector('#pokemon-query');
  const error = root.querySelector('.error');
  
  // Only handle error focus - don't interfere with user typing
  if (error) {
    error.focus();
  } else if (input && !document.activeElement) {
    // Only auto-focus search input if nothing is currently focused
    input.focus();
    input.value = state.query;
    input.setSelectionRange(input.value.length, input.value.length);
  }
  
  // Restore scroll position
  window.scrollTo(0, scrollPosition);
}

function doRender() {
  render({
    root,
    state,
    view,
    events: [
      { selector: '#search-form', event: 'submit', handler: handleSubmit },
      { selector: '#pokemon-query', event: 'input', handler: handleInput },
      { selector: '#execute-script', event: 'click', handler: handleExecuteScript },
      { selector: '#clear-script', event: 'click', handler: handleClearScript },
      { selector: '#example-scripts', event: 'change', handler: handleLoadExample },
      { selector: '#baba-yaga-script', event: 'input', handler: handleScriptInput },
    ],
    postRender,
  });
}

/**
 * Dispatches an action to update state and re-render.
 *
 * Why centralize dispatch? This enforces a single source of truth for state changes, making the app predictable and easy to debug.
 *
 * Why log actions/state in devMode? This provides a transparent, time-travel-like view of app logic without needing any extra tooling.
 */
function dispatch(action) {
  const prevState = state;
  state = update(state, action);
  
  if (devMode && dev && typeof dev.pushState === 'function') {
    dev.pushState(state);
    console.groupCollapsed(`Action: ${action.type}`);
    console.log('Payload:', action.payload);
    console.log('Prev state:', prevState);
    console.log('Next state:', state);
    console.groupEnd();
  }
  
  // Only re-render for actions that actually change the UI
  const shouldRender = [
    'FETCH_SUCCESS',
    'FETCH_ERROR', 
    'FETCH_EVOLUTION_SUCCESS',
    'FETCH_EVOLUTION_ERROR',
    'EXECUTE_SCRIPT_SUCCESS',
    'EXECUTE_SCRIPT_ERROR',
    'CLEAR_SCRIPT_OUTPUT',
    'UPDATE_BABA_YAGA_SCRIPT' // Only when loading examples
  ].includes(action.type);
  
  if (shouldRender) {
    doRender();
  }
}

/**
 * Handles input events by updating state without re-rendering.
 */
function handleInput(e) {
  // Update state directly without triggering re-render
  state.query = e.target.value;
}

/**
 * Handles script input events by updating state without re-rendering.
 */
function handleScriptInput(e) {
  // Update state directly without triggering re-render
  state.babaYagaScript = e.target.value;
}

/**
 * Handles form submission, triggers async fetch, and dispatches state updates.
 *
 * Why handle async here? Keeps update/view pure and centralizes side-effect.
 */
async function handleSubmit(e) {
  e.preventDefault();
  if (!state.query.trim()) return;
  dispatch({ type: 'FETCH_START' });
  try {
    const data = await fetchPokemon(state.query.trim());
    dispatch({ type: 'FETCH_SUCCESS', payload: data });
    
    // Automatically fetch evolution chain after successful Pokémon search
    try {
      dispatch({ type: 'FETCH_EVOLUTION_START' });
      const evolutionData = await fetchEvolutionData(data.name);
      dispatch({ type: 'FETCH_EVOLUTION_SUCCESS', payload: evolutionData });
    } catch (evolutionErr) {
      dispatch({ type: 'FETCH_EVOLUTION_ERROR', payload: evolutionErr.message });
    }
  } catch (err) {
    dispatch({ type: 'FETCH_ERROR', payload: err.message });
  }
}



/**
 * Handles Baba Yaga script execution.
 */
async function handleExecuteScript(e) {
  e.preventDefault();
  if (!state.evolutionChain || !state.babaYagaScript.trim()) return;
  
  dispatch({ type: 'EXECUTE_SCRIPT_START' });
  try {
    // state.evolutionChain contains the wrapper object, pass it directly
    const result = await executeBabaYagaScript(state.babaYagaScript, state.evolutionChain);
    dispatch({ type: 'EXECUTE_SCRIPT_SUCCESS', payload: result });
    
    // Update dev mode with the harness instance for enhanced debugging
    console.log('[App] Checking dev mode integration:', {
      devMode,
      dev: !!dev,
      updateHarness: dev ? typeof dev.updateHarness : 'no dev',
      updateHarnessValue: dev ? dev.updateHarness : 'no dev'
    });
    
    if (devMode && dev && typeof dev.updateHarness === 'function') {
      const currentHarness = getCurrentHarness();
      console.log('[App] Script executed, current harness:', !!currentHarness);
      try {
        dev.updateHarness(currentHarness);
        console.log('[App] updateHarness called successfully');
      } catch (error) {
        console.error('[App] Error calling updateHarness:', error);
      }
    } else {
      console.log('[App] Dev mode or updateHarness not available:', {
        devMode,
        dev: !!dev,
        updateHarness: dev ? typeof dev.updateHarness : false
      });
      
      // Try to access the function directly from window.dev
      if (window.dev && typeof window.dev.updateHarness === 'function') {
        console.log('[App] Found updateHarness on window.dev, trying direct call');
        const currentHarness = getCurrentHarness();
        try {
          window.dev.updateHarness(currentHarness);
          console.log('[App] Direct updateHarness call successful');
        } catch (error) {
          console.error('[App] Error in direct updateHarness call:', error);
        }
      }
    }
  } catch (err) {
    dispatch({ type: 'EXECUTE_SCRIPT_ERROR', payload: err.message });
  }
}

/**
 * Handles script clearing.
 */
function handleClearScript(e) {
  e.preventDefault();
  dispatch({ type: 'UPDATE_BABA_YAGA_SCRIPT', payload: '' });
  dispatch({ type: 'CLEAR_SCRIPT_OUTPUT' });
}

/**
 * Handles loading example scripts.
 */
function handleLoadExample(e) {
  const selectedExample = e.target.value;
  if (!selectedExample) return;
  
  const examples = getExampleScripts();
  if (examples[selectedExample]) {
    // Update state and trigger re-render to show the example
    state.babaYagaScript = examples[selectedExample];
    doRender();
  }
  
  // Reset the select
  e.target.value = '';
}

// Initialize dev mode before first render
if (devMode) {
  dev = initDevMode({
    getState: () => state,
    setState: s => { state = s; },
    render: doRender,
    harness: null // Will be updated when harness is created
  });
}

// Initial render
doRender();

function updateHistoryInfo() {
  if (!devMode || !dev) return;
  dev.update();
}

function setHistoryPointer(idx) {
  const info = dev.getHistoryInfo();
  if (idx < 1 || idx > info.length) return;
  const newState = dev.setPointer(idx - 1);
  if (newState) {
    state = newState;
    doRender();
    updateHistoryInfo();
  }
}