feat(html): make editing one mode every format shares - #871
Merged
Conversation
The mode, the refusal channel and the dirty flag are what a host wires, and it wires them once per document - so they cannot live in the sheet's script. `editing.md` gains decisions 9 to 12: one generic `odr.editing` an editor attaches to, the frame stated on `<body>`, `HtmlConfig::editable` as the switch that writes the scaffolding, and a config for the keys a host may need back. `spreadsheet-editing.md` keeps the cell's own decisions and points at the frame. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KKFKbUVCYF2VhujdmjhhPW
The mode, the refusal channel and the dirty flag are what a host wires, and it wires them once per document, so they cannot sit in the sheet's script. `frontend/editing.js` owns `odr.editing` for every document view and each format attaches its own editor: the cell overlay for a sheet, the runs for a text document. A `.docx` view answers `isEditable()` the way an `.ods` already did, so a host can grey its edit button before a tap. `HtmlConfig::editable` writes the scaffolding the mode needs rather than `contenteditable`: the addressing an op names, the lock on a locked cell, the state on `<body>`, and the editor script. The mode writes `contenteditable` on the addressed runs when the host turns it on, so switching modes needs no second render, and a read-only render carries none of it. `keyboard_navigation` and `keyboard_shortcuts` let a host keep the arrow keys and the undo chord, which the sheet takes in the capture phase. The open editor's own keys are never taken away, because they are the way out of it. Also fixes two things the split exposed: a read-only text document took the Enter key from the reader and reported an error for it, and a sheet page swallowed Enter whenever no cell was pinned. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KKFKbUVCYF2VhujdmjhhPW
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KKFKbUVCYF2VhujdmjhhPW
Also restores `plain_text`'s doc comment, which sat above `writes_editable` and went with it, and corrects `write_spreadsheet_script`'s: the script beside it is the editing one now, not the document one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KKFKbUVCYF2VhujdmjhhPW
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KKFKbUVCYF2VhujdmjhhPW
A click on the pinned cell clears the pin, and the second click of a double click was taking it - so double-clicking a cell to select a word flashed the border on and off again. `detail` counts the clicks, and only a single one clears now. `positions.html` checks it, and fails without the fix. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KKFKbUVCYF2VhujdmjhhPW
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KKFKbUVCYF2VhujdmjhhPW
`contenteditable` per run made every run its own editing host, and a host is a wall: the caret could not cross it, a selection could not span two of them, and a reader who selected a sentence got nothing - silently. The mode now puts `contenteditable` on the body and the editor gates `beforeinput`, so a document behaves like one and every edit it cannot replay is refused with a reason a host can show. The whitelist is closed: only the input types that change the text of one addressed run pass. Refusing something we could have allowed costs a reader one gesture; allowing something no op can name costs them their document. Nothing is marked non-editable - an edit is allowed because it lands inside a `data-odr-path` run, so a picture, a table's furniture and the gap between two paragraphs are refused without an attribute of their own. New reasons: `unsupportedEdit` (7) and `range` (8). The observer reports code 9 where text changed outside every run, which is the hole a composition or an incomplete `beforeinput` would open. `test/browser/text` holds the cases, and `checks.js` moved up beside `serve.py` because two directories share it now. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KKFKbUVCYF2VhujdmjhhPW
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KKFKbUVCYF2VhujdmjhhPW
A `MutationObserver` cannot tell a reader's edit from a script rewriting the page, and `search.js` rewrites plenty: highlighting nine matches put nine no-op `setText` ops in the log and lit the host's save button. `input` is the browser saying it applied an edit, which a script never raises, so the two are no longer confused. The run is the one the caret sits in, falling back to the one `beforeinput` named; where neither answers, code 9 says the gate has a hole rather than dropping the edit in silence. Also records the two limits a reader meets in `editing.md` decision 13 - undo belongs to the browser until phase 3 gives the editor an inverse, and backspace at the start of a run is refused because merging two runs is not something `setText` can express - and adds `test/browser/text`, whose README says why no check may use `execCommand`: Chrome's scripted editing raises no cancelable `beforeinput` and dissolves a run whose whole text it replaces, neither of which trusted input does. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KKFKbUVCYF2VhujdmjhhPW
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KKFKbUVCYF2VhujdmjhhPW
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🤖 Generated with Claude Code
Editing was a sheet feature that happened to live in a script. This makes it a
mode every format shares, and answers the two questions
editing.mdleft open aboutHtmlConfig::editable.The mode is generic
frontend/editing.jsownsodr.editingfor every document view: the mode,the refusal table, the log a save reads, the
odr.onEdit*callbacks andodr.generateDiff. A format attaches its own editor to it:editing.jsdocument.jscontenteditableruns, aMutationObserver, no undosheet-editing.jssetCellSo a
.docxview answersisEditable()the way an.odsalready did, and ahost greys its edit button before a tap rather than after one. The public
surface a host wires does not change:
enable,disable,isEnabled,isEditable,undo,redo,getOperations,committed,lockAt,refuseAt,editAtand the three callbacks are all where they were.HtmlConfig::editablewrites the scaffolding, notcontenteditableIt writes what the mode cannot work out for itself —
data-odr-pathon everyeditable run, the lock on a locked cell, the state on
<body>, and the editorscript — and the mode writes
contenteditableon those runs when a hostcalls
enable(). Two consequences:spreadsheet-editing.mdasked for;editableoff carries no editing markup at all, so a read-onlyview pays nothing for an editor it cannot reach.
The document's editable state moved off the
.odr-sheettable onto<body>,which is where a text view can carry it too. The table keeps
data-odr-sheet.A host keeps the keys it needs
keyboard_navigationandkeyboard_shortcuts(both on by default) decidewhether the page takes the keys that move the selection and the undo chord. The
sheet registers its handler in the capture phase and calls
preventDefault, sobefore this an embedder with its own arrow-key bindings lost them with no way to
ask for them back. The open editor's own keys — Escape, Enter, Tab — are never
taken away, because they are the only way out of it.
Two bugs the split exposed
error 1 for it:
document.js's handler was global and unconditional. It nowfires only inside an editable run while the mode is on, and it goes through
onEditRefused(reasonnewLine, still code 1) because a refusal isexpected UX rather than a fault.
Checks
odr_testfull suite: 1633 passed, 6 skipped, none failed.test/browser/sheetheadless:tests14,positions20,sorting8,editing51, and the newkeyboard10 — all passing.keyboard.htmlis apage whose config took both key classes away.
test/browser/annotation: all passing.viewportis unchanged and needs areal window.
g++-15 -Wall -Wextra -Werror, andclang-tidy over them reports only a dead store that predates this branch.
Reference output
682 files, and every difference is one of: the two attributes added to
<body>,data-odr-editablegone from the sheet table,contenteditablegone from arun, the
editing.jslink, and — on the two-read-onlyvariants — the locksand the editor scripts gone. Proved by normalising both sides for exactly those
tokens and comparing the rest line by line: nothing else moved, so no pixel
does either.