╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ──────────╮ │src/embed/script.js│ ╰───────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────╮ │254 // bring the focused widget into view within its own .scroll div. it's │ │255 // several spans (one per row) sharing this id, in top-to-bottom order,│ │256 // so the first gives its top/left edge and the last its bottom. │ │178 - const els = grid.querySelectorAll(`[data-focus-id="${id}"]`); │ │257 + const els = focusElements(id); │ │258 if (els.length === 0) return; │ │259 const container = els[0].closest(".scroll"); │ │260 if (!container) return; │ ╰────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │393 wasmInstance.exports._tick(minRows(), maxCols()); │ │394 }); │ │395 │ │317 - // submit on mousedown rather than waiting for click: the focusin this │ │318 - // same mousedown fires ticks the wasm, and a resulting overlay rebuild │ │319 - // would detach the button before its click could land. the form is │ │320 - // guaranteed live at mousedown time. │ │321 - document.addEventListener("mousedown", (event) => { │ │322 - if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return; │ │323 - const btn = event.target.closest ? event.target.closest("button[type=submit][data-focus-id]") : null;│ │324 - if (!btn || !btn.form) return; │ │325 - // empty-action forms are handled by their onsubmit on the click path │ │326 - if (!btn.form.getAttribute("action")) return; │ │327 - // if the button survives to receive the click, don't submit twice │ │328 - btn.addEventListener("click", (e) => e.preventDefault(), { once: true }); │ │329 - btn.form.requestSubmit(btn); │ │330 - }); │ │331 - │ │332 - // listeners on document (not #grid) since the form lives outside #grid; │ │333 - // input/focus events from form elements still bubble up to document. │ │396 + // delegated listeners survive cell-layer replacement. the controls │ │397 + // themselves are retained inside #grid. │ │398 document.addEventListener("input", (event) => { │ │399 const t = event.target; │ │400 if (!t || (t.tagName !== "INPUT" && t.tagName !== "TEXTAREA") || !t.dataset.focusId) return; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────╮ │406 if (!t || !t.dataset || !t.dataset.focusId) return; │ │407 // sync the wasm focus to wherever the browser moved focus │ │408 wasmInstance.exports._setFocus(Number(t.dataset.focusId)); │ │345 - // a click's mousedown fires this focusin before the click event. ticking │ │346 - // here would rebuild #grid and destroy the node before its click lands, │ │347 - // swallowing the navigation and forcing a second click — so don't │ │348 - // re-render for a grid element. a file input is also skipped because a │ │349 - // changed overlay would replace it between mousedown and click, before │ │350 - // the browser opens its picker. │ │351 - if (!grid.contains(t) && !(t.tagName === "INPUT" && t.type === "file")) { │ │352 - wasmInstance.exports._tick(minRows(), maxCols()); │ │353 - } │ │409 + // keyed controls survive a redraw before click. cell links live in the │ │410 + // replaceable backdrop, so their click handler redraws after activation. │ │411 + if (nodeKey(t)?.startsWith("control:")) wasmInstance.exports._tick(minRows(), maxCols());│ │412 }); │ │413 │ │414 grid.addEventListener("click", (event) => { │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ────────╮ │src/main_wasm.zig│ ╰─────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ │56 // min == max height fills it exactly, and each Scroll clips to its │ │57 // viewport while handing its full content to a native-scrollable element. │ │58 // a full-height page instead grows to its content and the browser scrolls │ │59 - // the whole page, which the overlay's absolute positions track. │ │59 + // the whole page. │ │60 const max_height: ?u32 = if (session.data.current_page.fullHeight()) null else min_height;│ │61 try root_ptr.build(allocator, .{ │ │62 .min_size = .{ .width = null, .height = min_height }, │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────╮ │77 defer allocator.free(html); │ │78 _setHtml(html.ptr, @intCast(html.len)); │ │79 │ │80 - // emit the editable form overlay on every tick so the wasm layout drives│ │81 - // positions, not just the server's initial render. JS │ │82 - // diffs the result against the previous overlay; an unchanged overlay │ │83 - // leaves the live <form> alone — crucial since wiping it mid-click │ │84 - // would detach the submit button before the browser can dispatch the │ │85 - // form submission. │ │86 - const overlay = try web.generateOverlay(allocator, root_ptr, &session); │ │87 - defer allocator.free(overlay); │ │88 - _setOverlay(overlay.ptr, @intCast(overlay.len)); │ │89 - │ │80 const root_focus = root_ptr.getFocus(); │ │81 if (root_focus.grandchild_id) |gid| { │ │82 if (root_focus.children.get(gid)) |child| { │ ╰─────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────╮ │170 │ │171 extern fn _consoleLog(arg: [*]const u8, len: u32) void; │ │172 extern fn _setHtml(arg: [*]const u8, len: u32) void; │ │183 -extern fn _setOverlay(arg: [*]const u8, len: u32) void; │ │173 extern fn _replaceState(arg: [*]const u8, len: u32) void;│ │174 extern fn _focusInput(focus_id: u32) void; │ │175 extern fn _navigate(arg: [*]const u8, len: u32) void; │ ╰──────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────╮ │224 }; │ │225 } │ │226 │ │238 -// js calls this when the value of an overlay <input> changes; we replace │ │227 +// js calls this when the value of a native <input> changes; we replace │ │228 // the matching TextInput's content with the supplied utf-8 bytes. │ │229 export fn _setTextInputValue(focus_id: u32, value_ptr: [*]const u8, value_len: u32) void {│ │230 setTextInputValue(focus_id, value_ptr[0..value_len]) catch |err| { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1380 // focus id -> the live TextInput, refreshed each frame by the views that own │ │1381 // inputs. web/wasm form handling looks widgets up here by focus id. │ │1382 text_inputs: std.AutoHashMapUnmanaged(usize, *wgt.TextInput) = .empty, │ │1383 - // focus id -> the page-constant text the web renderer puts in an input │ │1384 - // initially, refreshed each frame alongside `text_inputs`. never an │ │1385 - // editable input's live content: the overlay html must not change as the │ │1386 - // user types, or the diff in _setOverlay would rebuild the element. │ │1387 - input_values: std.AutoHashMapUnmanaged(usize, []const u8) = .empty, │ │1383 nav_back: bool = false, // set by input (escape) to request the native TUI pop a page; see Nav│ │1384 refresh_requested: bool = false, // set by input (ctrl+r) │ │1385 // a requested forward navigation. set this (via navigate) to move to a new │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1732 return std.fmt.allocPrint(session.page_arena.allocator(), "{s}{s}", .{ in_page_link_prefix, try target.toUrl(session.page_arena) });│ │1733 } │ │1734 │ │1740 -// a button the web overlay covers with a file picker that posts the chosen │ │1735 +// a button the web renderer covers with a file picker that posts the chosen │ │1736 // file to the url after the prefix │ │1737 pub const file_input_prefix = "file:"; │ │1738 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────╮ │2037 // input-owning views build their TextInputs in init, so reset their│ │2038 // registrations here │ │2039 session.text_inputs.clearRetainingCapacity(); │ │2045 - session.input_values.clearRetainingCapacity(); │ │2040 │ │2041 try root.build(allocator, .{ │ │2042 .min_size = .{ .width = null, .height = 40 }, │ ╰──────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯