╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ script.js ────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
292 // submit button does 293 event.preventDefault(); 294 return; 295 } else if (tag === "INPUT" && (event.key === "ArrowUp" || event.key === "ArrowDown")) { 296 // up/down moves between widgets; left/right stay in the input 297 document.activeElement.blur(); 298 } else if (tag === "BUTTON" && isArrow) { 299 // arrows navigate away from a focused submit button 300 document.activeElement.blur(); 301 } else if (tag === "INPUT" || tag === "BUTTON" || tag === "TEXTAREA") { 302 // Enter (submit / newline), typing, Tab, and a textarea's 303 // arrows — handled natively by the browser 304 return; 305 } 306 } 307 if ((event.key === "ArrowUp" || event.key === "ArrowDown") && scrollFocusedWidget(event.key === "ArrowDown")) { 308 event.preventDefault(); 309 return; 310 } 311 if ((event.key === "PageUp" || event.key === "PageDown") && pageFocusedScroll(event.key === "PageDown")) { 312 event.preventDefault(); 313 return; 314 } 315 if (["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "PageUp", "PageDown", "Home", "End"].includes(event.key)) { 316 event.preventDefault(); 317 } 318 wasmInstance.exports._onKeyDown(event.keyCode); 319 wasmInstance.exports._tick(minRows(), maxCols()); 320 }); 321 322 // submit on mousedown rather than waiting for click: the focusin this 323 // same mousedown fires ticks the wasm, and a resulting overlay rebuild 324 // would detach the button before its click could land. the form is 325 // guaranteed live at mousedown time. 326 document.addEventListener("mousedown", (event) => { 327 if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return; 328 const btn = event.target.closest ? event.target.closest("button[type=submit][data-focus-id]") : null; 329 if (!btn || !btn.form) return; 330 // empty-action forms are handled by their onsubmit on the click path 331 if (!btn.form.getAttribute("action")) return; 332 // if the button survives to receive the click, don't submit twice 333 btn.addEventListener("click", (e) => e.preventDefault(), { once: true }); 334 btn.form.requestSubmit(btn); 335 }); 336 337 // listeners on document (not #grid) since the form lives outside #grid; 338 // input/focus events from form elements still bubble up to document. 339 document.addEventListener("input", (event) => { 340 const t = event.target; 341 if (!t || (t.tagName !== "INPUT" && t.tagName !== "TEXTAREA") || !t.dataset.focusId) return; 342 sendTextInputValue(Number(t.dataset.focusId), t.value); 343 }); 344 345 document.addEventListener("click", (event) => { 346 const t = event.target; 347 if (t && t.tagName === "INPUT" && t.readOnly) t.select(); 348 }); 349 350 document.addEventListener("focusin", (event) => { 351 const t = event.target; 352 if (!t || !t.dataset || !t.dataset.focusId) return; 353 // sync the wasm focus to wherever the browser moved focus 354 wasmInstance.exports._setFocus(Number(t.dataset.focusId)); 355 // a click's mousedown fires this focusin before the click event. ticking 356 // here would rebuild #grid and destroy the node before its click lands, 357 // swallowing the navigation and forcing a second click — so don't 358 // re-render for a grid element. a file input is also skipped because a 359 // changed overlay would replace it between mousedown and click, before 360 // the browser opens its picker. 361 if (!grid.contains(t) && !(t.tagName === "INPUT" && t.type === "file")) { 362 wasmInstance.exports._tick(minRows(), maxCols()); 363 } 364 }); 365 366 grid.addEventListener("click", (event) => { 367 const span = event.target.closest(".clickable"); 368 if (!span) return; 369 // leave modified / non-left clicks (open-in-new-tab, etc.) to the browser 370 if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return; 371 // wasm routes the click: an in-page link just moves focus and re-renders, 372 // a cross-page one calls _navigate. suppress the <a>'s own navigation so 373 // an in-page link doesn't also trigger a redundant full page load. 374 event.preventDefault(); 375 const focusId = Number(span.dataset.focusId); 376 wasmInstance.exports._onMouseClick(focusId); 377 wasmInstance.exports._tick(minRows(), maxCols()); 378 }); 379 380 let resizeTimer = null; 381 window.addEventListener("resize", () => { 382 clearTimeout(resizeTimer); 383 resizeTimer = setTimeout(() => { 384 wasmInstance.exports._tick(minRows(), maxCols()); 385 }, 100); 386 }); 387 }); 388