Seed the mouse editor from its target, and mark the target on the first edit (improvise-ugj4, improvise-jibp)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
This commit is contained in:
Edward Langley
2026-09-23 08:33:40 -07:00
parent c4877456a4
commit 8747074e52
7 changed files with 105 additions and 31 deletions
+9 -8
View File
@@ -546,20 +546,19 @@ function gridTable(model) {
/** Open an inline editor. Committing sends a frame; the value that comes
* back is the server's, never the string typed here. */
function edit(td, row, col, seed) {
function edit(td, row, col) {
if (editing) return;
// WHICH cell, captured now: rows renumber while the editor is open, so the
// position is not an address by commit time (improvise-2wu3).
const target = client.edit_target(row, col);
let target = client.edit_target(row, col);
if (!target) return;
// `seed` is `RenderCell::edit_text` -- the STORED value, not the drawn one:
// `td.textContent` has been through the number format.
const original = seed;
// What was on the screen, kept so closing can put it back: `close` no longer
// always repaints, so restoring `seed` would leave `1200` where `1,200` was.
// always repaints, so restoring the seed would leave `1200` where `1,200` was.
const drawn = td.textContent;
const input = document.createElement("input");
input.value = original;
// The seed the target captured, never the paint, which may be stale while
// another editor is open: one capture shown and compared (improvise-ugj4).
input.value = client.edit_seed(target);
td.replaceChildren(input);
input.focus();
input.select();
@@ -590,6 +589,8 @@ function edit(td, row, col, seed) {
// These two stay DIRECT while the five surfaces above are delegated: one
// transient node, one at a time, and `blur` does not bubble (improvise-b8ot).
// The first edit marks the target, so retyping the seed is still a write.
input.addEventListener("input", () => (target = client.edited_target(target)), { once: true });
input.addEventListener("keydown", (e) => {
if (e.key === "Enter") dismiss(true);
else if (e.key === "Escape") dismiss(false);
@@ -745,7 +746,7 @@ el.grid.addEventListener("mousedown", (event) => {
if (frame) transport.send(frame);
// Every cell moves the selection; an editable one also opens its editor.
// NOT repainted in that branch: a repaint would take the <input> away.
if (action.editable) edit(td, action.row, action.col, action.editText);
if (action.editable) edit(td, action.row, action.col);
else repaint();
});
+2 -3
View File
@@ -222,7 +222,6 @@ export function gridPlan(model) {
row: dataRow,
col,
text: typing ? `${model.editing}▏` : cell.text,
editText: cell.edit_text,
editable: cell.editable,
classes: {
numeric: cell.numeric && !typing,
@@ -260,7 +259,7 @@ export function gridPlan(model) {
/** What a data cell carries on its node, so one delegated handler can read
* back the answers `gridPlan` already made (improvise-b8ot, clients.md). */
export function cellAttrs(cell) {
return { row: String(cell.row), col: String(cell.col), edit: cell.editText };
return { row: String(cell.row), col: String(cell.col) };
}
/** …and the inverse: what a delegated mousedown MEANS. `null` where the event
@@ -271,7 +270,7 @@ export function cellAction(dataset, editable) {
// A node that carries no coordinates is not a cell: every header and the
// corner reach here, and none of them means anything.
if (row === null || col === null) return null;
return { row, col, editable, editText: dataset.edit ?? "" };
return { row, col, editable };
}
/** The same, for the surfaces whose rows are named by one index: the panels,
+2 -3
View File
@@ -16,7 +16,6 @@ import {
const cell = (text, extra = {}) => ({
text,
edit_text: text,
numeric: true,
error: false,
selected: false,
@@ -40,7 +39,7 @@ function grouped() {
{
kind: "data",
labels: ["Cold Brew"],
cells: [cell("3", { editable: false }), cell("4", { edit_text: "4000" })],
cells: [cell("3", { editable: false }), cell("4")],
},
],
};
@@ -50,7 +49,7 @@ test("a cell's attributes carry the plan's answers back to the handler", () => {
const plan = gridPlan(grouped());
const last = plan.body.at(-1).cells[1];
const action = cellAction(cellAttrs(last), last.editable);
assert.deepEqual(action, { row: 1, col: 1, editable: true, editText: "4000" });
assert.deepEqual(action, { row: 1, col: 1, editable: true });
});
// improvise-b8ot: the DATA row index is what a click sends, and `gridPlan`
-9
View File
@@ -8,7 +8,6 @@ import { gridPlan } from "../shell.js";
const cell = (text, extra = {}) => ({
text,
edit_text: text,
numeric: true,
error: false,
selected: false,
@@ -102,14 +101,6 @@ test("an empty edit buffer still means an editor is open", () => {
assert.equal(plan.body[1].cells[0].text, "▏");
});
test("the editor is seeded from the stored value, not the drawn one", () => {
const model = grouped();
model.rows[1].cells[0] = cell("20,000", { edit_text: "20000" });
const plan = gridPlan(model);
assert.equal(plan.body[1].cells[0].text, "20,000");
assert.equal(plan.body[1].cells[0].editText, "20000");
});
test("an empty totals strip is no strip at all, not a row of blanks", () => {
// Gotcha 33: an EMPTY strip means this grid has no totals — records mode,
// or an empty grid — so there is nothing to draw and nothing to pad.