╭ changed ──────────╮ │src/embed/script.js│ ╰───────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │152 // mousedown→focusin→tick→click sequence, which is what lets │ │153 // the very first click on the submit button actually submit. │ │154 if (html === currentOverlay) return; │ │155 + // the initial wasm render can finish while a native picker is open. │ │156 + // keep its input connected so its change event reaches document. │ │157 + if (overlay.contains(document.activeElement) && document.activeElement.type === "file") return;│ │158 currentOverlay = html; │ │159 overlay.innerHTML = html; │ │160 }, │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────╮ │216 wasmInstance.exports._onKeyDown(13); │ │217 wasmInstance.exports._tick(minRows(), maxCols()); │ │218 } │ │219 + │ │220 +// upload as soon as the file dialog returns, so attaching is one click. the │ │221 +// file is the whole request body, so its name travels in a header. │ │222 +document.addEventListener("change", (event) => { │ │223 + const t = event.target; │ │224 + if (!t || t.tagName !== "INPUT" || t.type !== "file" || !t.dataset.action) return; │ │225 + const file = t.files[0]; │ │226 + if (!file) return; │ │227 + fetch(t.dataset.action, { │ │228 + method: "POST", │ │229 + headers: { "x-attachment-name": encodeURIComponent(file.name) }, │ │230 + body: file, │ │231 + }).then((res) => { │ │232 + if (res.ok) window.location.reload(); │ │233 + else res.text().then((message) => console.error("upload failed:", message)); │ │234 + }); │ │235 +}); │ │236 │ │237 WebAssembly.instantiateStreaming(fetch("/haxy.wasm"), importObject).then(async (result) => {│ │238 wasmInstance = result.instance; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────╮ │263 const tag = document.activeElement.tagName; │ │264 const isArrow = event.key === "ArrowUp" || event.key === "ArrowDown" ||│ │265 event.key === "ArrowLeft" || event.key === "ArrowRight"; │ │246 - if (tag === "INPUT" && document.activeElement.readOnly && │ │266 + // a file picker is an input but behaves like a button: every key │ │267 + // but an arrow is native (enter opens its dialog), and an arrow │ │268 + // navigates away from it │ │269 + if (tag === "INPUT" && document.activeElement.type === "file") { │ │270 + if (!isArrow) return; │ │271 + document.activeElement.blur(); │ │272 + } else if (tag === "INPUT" && document.activeElement.readOnly && │ │273 ["Enter", "ArrowLeft", "ArrowRight"].includes(event.key)) { │ │274 event.preventDefault(); │ │275 if (event.key === "Enter") { │ ╰────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────╮ │279 wasmInstance.exports._onKeyDown(event.keyCode); │ │280 wasmInstance.exports._tick(minRows(), maxCols()); │ │281 return; │ │256 - } │ │257 - if (tag === "INPUT" && event.key === "Enter") { │ │282 + } else if (tag === "INPUT" && event.key === "Enter") { │ │283 // enter in a text input never submits the form; only the│ │284 // submit button does │ │285 event.preventDefault(); │ ╰──────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │286 return; │ │262 - } │ │263 - if (tag === "INPUT" && (event.key === "ArrowUp" || event.key === "ArrowDown")) { │ │287 + } else if (tag === "INPUT" && (event.key === "ArrowUp" || event.key === "ArrowDown")) {│ │288 // up/down moves between widgets; left/right stay in the input │ │289 document.activeElement.blur(); │ │290 } else if (tag === "BUTTON" && isArrow) { │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────╮ │347 // a click's mousedown fires this focusin before the click event. ticking│ │348 // here would rebuild #grid and destroy the node before its click lands, │ │349 // swallowing the navigation and forcing a second click — so don't │ │326 - // re-render for a grid element. overlay controls live outside #grid and │ │327 - // survive the diff, so they still tick. │ │328 - if (!grid.contains(t)) wasmInstance.exports._tick(minRows(), maxCols()); │ │350 + // re-render for a grid element. a file input is also skipped because a │ │351 + // changed overlay would replace it between mousedown and click, before │ │352 + // the browser opens its picker. │ │353 + if (!grid.contains(t) && !(t.tagName === "INPUT" && t.type === "file")) {│ │354 + wasmInstance.exports._tick(minRows(), maxCols()); │ │355 + } │ │356 }); │ │357 │ │358 grid.addEventListener("click", (event) => { │ ╰──────────────────────────────────────────────────────────────────────────────────────╯