diff options
Diffstat (limited to 'js/scripting-lang/web/src/app.js')
-rw-r--r-- | js/scripting-lang/web/src/app.js | 286 |
1 files changed, 286 insertions, 0 deletions
diff --git a/js/scripting-lang/web/src/app.js b/js/scripting-lang/web/src/app.js new file mode 100644 index 0000000..086cba1 --- /dev/null +++ b/js/scripting-lang/web/src/app.js @@ -0,0 +1,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(); + } +} |