Skip to content

feat(html): type into a sheet cell through an overlay - #863

Merged
andiwand merged 1 commit into
mainfrom
feat/sheet-overlay-editor
Sep 8, 2026
Merged

feat(html): type into a sheet cell through an overlay#863
andiwand merged 1 commit into
mainfrom
feat/sheet-overlay-editor

Conversation

@andiwand

@andiwand andiwand commented Sep 8, 2026

Copy link
Copy Markdown
Member

🤖 Generated with Claude Code

Steps 1.2 and 1.3 of docs/design/spreadsheet-editing.md, which land together: an editor that throws away what is typed is not one.

The overlay

Double-clicking a cell, or a key on the pinned one (Enter, F2, a printable character), opens an <input> placed over it. Enter and Tab commit and move the pin — down and right, up and left with shift — Escape cancels, blur commits, and the arrows walk the pin through odr.sheet.pin rather than through a pin of the editor's own. The sheet's dom is untouched until the commit patches the cell, so nothing is contenteditable, and the pin is put back down while the overlay is open, since the overlay shows what a raise would.

A cell holding a formula, shapes or several runs never opens one: it refuses on the click, ahead of the double click, and is outlined for the tap it answers. odr.editing.editAt(column, row) is the same thing for a host.

The commit

The type follows what was typed (decision 4): a strict grammar makes a number, a leading ' forces a string, and = is refused as formulaInput (code 6), since overwriting an input leaves every formula computed from it stale until step 4 — the refusal leaves the editor open.

The commit records the op beside the value it replaced, patches the cell through its own run so a styled one keeps its style, toggles odr-value-type-float for the alignment, and hands the log out coalesced per position as odr.editing.getOperations() — the envelope Document::edit takes. getOperations came forward from step 1.4: ops nothing hands out are ops nothing can check.

What the sheet script gained

odr.sheet owns the position map, so the value and geometry half sits beside it rather than being reached around (decision 8): valueAt / showValue for what the page shows at a position, reflow for the spill translate_sheet measured — which goes stale the moment a blank cell fills or a full one empties — and lower for the raise. The spill is measured again by the same rule the renderer used, off the geometry the browser has: a string runs over the blank cells beside it up to the next one showing something, and is cut at its own edge where a column states no width to run over.

spreadsheet_js outgrew msvc's 16380-byte literal and is written as two, the way pdf_annotation_js is; serve joins them back.

Checks

test/browser/sheet/editing.html drives all of it through the public surface — 34 checks, green in Chrome, beside the 14 + 20 + 8 of the pages already there. It also verifies on a real render: translate on ods/overflow.ods, then typing into C2 through odr.editing, cuts the two strings spilling into it exactly as a fresh translate would.

No markup changed, so the reference-output pins stay where they are.

@andiwand

andiwand commented Sep 8, 2026

Copy link
Copy Markdown
Member Author

Reviewed the branch and force-pushed the fixes; the checks are test/browser/sheet/editing.html, now 36 (each new one verified against a negative control).

Placement was wrong under a body zoom. viewport_js puts a zoom on the body of every view, a sheet included. Blink scales getBoundingClientRect by it and WebKit does not, while the overlay — a child of body — is laid out under that zoom either way, so at zoom:0.5 the overlay landed at half the cell's position. place now walks offsetLeft/offsetTop up the offset chain and uses offsetWidth/offsetHeight, which are layout pixels in both engines.

The same bug in the reflow, where it matters more: the spill was measured with rects and written into a clip-path, which is stated in layout pixels, so a zoomed page cut the string at the wrong place. It measures with offsets now.

The reflow was also quadratic, with a getComputedStyle and an isBlank per step of the inner scan — a row is up to 500 cells wide (spreadsheet_limit), so a row of blanks cost ~125k style resolutions on every commit. It is two linear passes now: each cell read once into rowState, then one backward pass that gives every cell what it sees to its right.

Smaller things:

  • an open editor is now committed rather than dropped when another editAt opens one elsewhere — blur already did that, and finish() is the one path both take;
  • write no longer records an op the page could not show (showValue returning false);
  • ' alone is an empty cell rather than a string of nothing, and parse says it in three branches instead of four;
  • the overlay takes the cell's colour, and follows it on a window resize;
  • keyoverlayKey, positionOf(event)targetPosition.

Comments cut throughout — the msvc rationale on the split literal (it sits on fits_a_literal already), the doubled zoom explanation, and the CHANGELOG entry down to what a consumer notices.

Double-clicking a cell, or a key on the pinned one, opens an `<input>` over it:
Enter and Tab commit and move the pin, Escape cancels, blur commits, and the
arrows walk the pin through `odr.sheet.pin` rather than a pin of the editor's
own. The sheet's dom is untouched until the commit patches the cell, so nothing
is `contenteditable` and a cell holding shapes or several runs never opens one -
a locked cell refuses on the click, ahead of the double click.

The type follows what was typed (decision 4): a strict grammar makes a number,
a leading `'` forces a string, and `=` is refused as `formulaInput`, since
overwriting an input leaves every formula computed from it stale until step 4.
The commit records the op beside the value it replaced, patches the cell through
its own run so a styled one keeps its style, and hands the log out coalesced per
position as `odr.editing.getOperations()` - the envelope `Document::edit` takes.

`odr.sheet` gained the half of this the sheet script owns: `valueAt` and
`showValue` for what the page shows at a position, `reflow` for the spill
`translate_sheet` measured, which goes stale the moment a blank cell fills or a
full one empties, and `lower` for the raise the overlay stands in for. The
spill is measured again by the same rule, off the geometry the browser has.

`spreadsheet_js` outgrew msvc's 16380-byte literal and is written as two, as
`pdf_annotation_js` is; `serve` joins them back.

Steps 1.2 and 1.3 of `docs/design/spreadsheet-editing.md`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012eNBvra2NSxLjUvVKzuXSY
@andiwand
andiwand force-pushed the feat/sheet-overlay-editor branch from 4603784 to b6e6328 Compare September 8, 2026 20:20
@andiwand
andiwand merged commit 39ccb72 into main Sep 8, 2026
8 checks passed
@andiwand
andiwand deleted the feat/sheet-overlay-editor branch September 8, 2026 20:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant