╭ changed ──────────╮ │src/embed/script.js│ ╰───────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────╮ │57 const html = readWasmString(ptr, len); │ │58 if (html === currentHtml) return; │ │59 currentHtml = html; │ │60 - // the grid only holds non-focusable TUI cell spans now; the │ │61 - // form (inputs + submit button) lives in #overlay and is │ │62 - // never touched by this wipe, so we don't need to save and │ │63 - // restore focus across the swap. │ │60 + // each web-native Scroll renders as a .scroll div; replacing │ │61 + // innerHTML recreates them and would reset their scroll position │ │62 + // every tick, so snapshot scrollTop/Left by their stable id and │ │63 + // restore it after the swap. (mouse-wheel scrolling happens with no │ │64 + // tick, so it isn't affected.) │ │65 + const positions = {}; │ │66 + for (const el of grid.querySelectorAll(".scroll[data-scroll-id]")) { │ │67 + positions[el.dataset.scrollId] = { top: el.scrollTop, left: el.scrollLeft };│ │68 + } │ │69 grid.innerHTML = html; │ │70 + for (const el of grid.querySelectorAll(".scroll[data-scroll-id]")) { │ │71 + const p = positions[el.dataset.scrollId]; │ │72 + if (p) { el.scrollTop = p.top; el.scrollLeft = p.left; } │ │73 + } │ │74 }, │ │75 _replaceState: function (ptr, len) { │ │76 const url = readWasmString(ptr, len); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ │103 // can't render it client-side without server data). │ │104 window.location.assign(readWasmString(ptr, len)); │ │105 }, │ │ 97 - _scrollToRect: function (x, y, w, h) { │ │ 98 - // the rect arrives in grid cells; map it to document coordinates │ │ 99 - // via the measured cell size and the grid origin, then scroll the │ │100 - // minimum needed to bring the widget fully into view │ │101 - if (!cellHeight) measureCell(); │ │102 - const gridTop = grid.getBoundingClientRect().top + window.scrollY; │ │103 - const top = gridTop + y * cellHeight; │ │104 - const bottom = top + h * cellHeight; │ │105 - const viewTop = window.scrollY; │ │106 - const viewBottom = viewTop + document.documentElement.clientHeight; │ │107 - if (top < viewTop) { │ │108 - window.scrollTo({ top }); │ │109 - } else if (bottom > viewBottom) { │ │110 - window.scrollTo({ top: bottom - document.documentElement.clientHeight });│ │106 + _scrollToFocus: function (id) { │ │107 + // bring the focused widget into view within its own .scroll div. it's │ │108 + // several spans (one per row) sharing this id, in top-to-bottom order, │ │109 + // so the first gives its top/left edge and the last its bottom. │ │110 + const els = grid.querySelectorAll(`[data-focus-id="${id}"]`); │ │111 + if (els.length === 0) return; │ │112 + const container = els[0].closest(".scroll"); │ │113 + if (!container) return; │ │114 + │ │115 + const c = container.getBoundingClientRect(); │ │116 + const first = els[0].getBoundingClientRect(); │ │117 + const last = els[els.length - 1].getBoundingClientRect(); │ │118 + const top = first.top - c.top + container.scrollTop; │ │119 + const bottom = last.bottom - c.top + container.scrollTop; │ │120 + const left = first.left - c.left + container.scrollLeft; │ │121 + │ │122 + // vertical is the priority axis: fit the widget, preferring its top │ │123 + // when it's taller than the viewport. │ │124 + if (top < container.scrollTop || bottom - top > container.clientHeight) { │ │125 + container.scrollTop = top; │ │126 + } else if (bottom > container.scrollTop + container.clientHeight) { │ │127 + container.scrollTop = bottom - container.clientHeight; │ │128 } │ │129 + // horizontal: only reveal the row's start, never chase the right edge, │ │130 + // so moving down through varying-width rows doesn't yank the view │ │131 + // sideways. (a hidden-overflow x axis stays at 0, so this no-ops.) │ │132 + if (left < container.scrollLeft) container.scrollLeft = left; │ │133 }, │ │134 }, │ │135 }; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯