╭ changed ──────────╮ │src/embed/script.js│ ╰───────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭─────────────────────────────────────╮ │3 │ │4 let wasmInstance; │ │5 const decoder = new TextDecoder();│ │6 +const encoder = new TextEncoder();│ │7 let currentHtml = ""; │ │8 │ │9 const MIN_COLS = 30; │ ╰─────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ │41 }, │ │42 _setHtml: function (ptr, len) { │ │43 const html = readWasmString(ptr, len); │ │43 - if (html !== currentHtml) { │ │44 - currentHtml = html; │ │45 - grid.innerHTML = html; │ │44 + if (html === currentHtml) return; │ │45 + currentHtml = html; │ │46 + // capture which focusable element had focus + (for inputs) its │ │47 + // cursor position so we can restore it after innerHTML wipes │ │48 + // and re-creates the elements. covers both overlay inputs AND │ │49 + // tabbable submit-button spans; otherwise focus on the latter │ │50 + // would be lost on every re-render, and Enter would fall │ │51 + // through to the wasm key dispatch instead of being intercepted │ │52 + // as a form submission. │ │53 + const active = document.activeElement; │ │54 + let savedFocusId = null; │ │55 + let savedStart = null; │ │56 + let savedEnd = null; │ │57 + if (active && active.dataset && active.dataset.focusId) { │ │58 + savedFocusId = active.dataset.focusId; │ │59 + if (active.tagName === "INPUT") { │ │60 + savedStart = active.selectionStart; │ │61 + savedEnd = active.selectionEnd; │ │62 + } │ │63 + } │ │64 + grid.innerHTML = html; │ │65 + if (savedFocusId !== null) { │ │66 + const restored = grid.querySelector(`[data-focus-id="${savedFocusId}"]`); │ │67 + if (restored && typeof restored.focus === "function") { │ │68 + restored.focus(); │ │69 + if (savedStart !== null && restored.setSelectionRange) { │ │70 + try { restored.setSelectionRange(savedStart, savedEnd); } catch (_) {}│ │71 + } │ │72 + } │ │73 } │ │74 }, │ │75 }, │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 76 }; │ │ 77 │ │ 78 +function sendTextInputValue(focusId, value) { │ │ 79 + const bytes = encoder.encode(value); │ │ 80 + const ptr = wasmInstance.exports._alloc(bytes.length); │ │ 81 + new Uint8Array(wasmInstance.exports.memory.buffer, ptr, bytes.length).set(bytes); │ │ 82 + wasmInstance.exports._setTextInputValue(focusId, ptr, bytes.length); │ │ 83 + wasmInstance.exports._tick(maxCols()); │ │ 84 +} │ │ 85 + │ │ 86 +// POST the current input values to the server form route. on response (any │ │ 87 +// status) we navigate to / so the server can render the page with whatever │ │ 88 +// session cookie it just set. │ │ 89 +function submitForm(url) { │ │ 90 + const params = new URLSearchParams(); │ │ 91 + const inputs = grid.querySelectorAll("input"); │ │ 92 + for (const input of inputs) { │ │ 93 + if (input.name) params.append(input.name, input.value); │ │ 94 + } │ │ 95 + fetch(url, { method: "POST", body: params, redirect: "manual", credentials: "same-origin" })│ │ 96 + .finally(() => { window.location.href = "/"; }); │ │ 97 +} │ │ 98 + │ │ 99 WebAssembly.instantiateStreaming(fetch("haxy.wasm"), importObject).then(async (result) => { │ │100 wasmInstance = result.instance; │ │101 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │114 wasmInstance.exports._start(ptr, jsonBytes.length, maxCols()); │ │115 │ │116 document.addEventListener("keydown", (event) => { │ │117 + // when an overlay input owns focus, the browser handles typing & │ │118 + // cursor movement natively; wasm gets the new value via input events │ │119 + // instead of per-key dispatch. don't forward, and don't intercept │ │120 + // arrow / home / end which the input needs for cursor movement. │ │121 + // exception: Enter triggers form submission when one is on the page. │ │122 + if (document.activeElement && document.activeElement.tagName === "INPUT") { │ │123 + if (event.key === "Enter") { │ │124 + event.preventDefault(); │ │125 + const submitSpan = grid.querySelector('[data-action="submit"]'); │ │126 + if (submitSpan) { │ │127 + submitForm(submitSpan.dataset.url); │ │128 + } │ │129 + } │ │130 + return; │ │131 + } │ │132 + // a focused submit-button span (Tab can land here now that it has a │ │133 + // tabindex) should submit on Enter the same as a click does. │ │134 + if (event.key === "Enter" && document.activeElement && document.activeElement.dataset && document.activeElement.dataset.action === "submit") {│ │135 + event.preventDefault(); │ │136 + submitForm(document.activeElement.dataset.url); │ │137 + return; │ │138 + } │ │139 if (["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "PageUp", "PageDown", "Home", "End"].includes(event.key)) { │ │140 event.preventDefault(); │ │141 } │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────╮ │143 wasmInstance.exports._tick(maxCols()); │ │144 }); │ │145 │ │146 + // event delegation: each render rebuilds the input elements (innerHTML│ │147 + // wipe), so we can't attach listeners per-input. delegating on grid │ │148 + // catches inputs created at any point. │ │149 + grid.addEventListener("input", (event) => { │ │150 + const t = event.target; │ │151 + if (!t || t.tagName !== "INPUT" || !t.dataset.focusId) return; │ │152 + sendTextInputValue(Number(t.dataset.focusId), t.value); │ │153 + }); │ │154 + │ │155 + grid.addEventListener("focusin", (event) => { │ │156 + const t = event.target; │ │157 + if (!t || !t.dataset || !t.dataset.focusId) return; │ │158 + wasmInstance.exports._onMouseClick(Number(t.dataset.focusId)); │ │159 + wasmInstance.exports._tick(maxCols()); │ │160 + }); │ │161 + │ │162 grid.addEventListener("click", (event) => { │ │163 const span = event.target.closest(".clickable"); │ │164 if (!span) return; │ ╰────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────╮ │165 + if (span.dataset.action === "submit") { │ │166 + event.preventDefault(); │ │167 + submitForm(span.dataset.url); │ │168 + return; │ │169 + } │ │170 const focusId = Number(span.dataset.focusId); │ │171 wasmInstance.exports._onMouseClick(focusId); │ │172 + // synthesize Enter so the focused widget's "activate" handler fires│ │173 + // (e.g. a button click runs its submit handler). widgets that don't│ │174 + // care about Enter just ignore it. │ │175 + wasmInstance.exports._onKeyDown(13); │ │176 wasmInstance.exports._tick(maxCols()); │ │177 }); │ │178 │ ╰─────────────────────────────────────────────────────────────────────────────────╯