Hide transcript editor markup behind view classes #74

Merged
mike merged 14 commits from transcript-editor-abstractions into master 2026-09-27 11:37:50 -04:00
Owner

Previously, the transcript editor script read the admin page's HTML directly: it selected
span[data-segment-id], walked up to the enclosing article to find a speaker
turn, and kept its state in module-level variables. Any change to the template
meant hunting through the controller for the selectors it broke.

This change splits the script in two. transcript-editor/view.js owns every selector
and DOM read. TranscriptSegment wraps one contenteditable segment and converts
between the code-point offsets the server uses and the UTF-16 offsets DOM ranges
use. SpeakerTurn holds one speaker's run of segments and decides where a
paragraph break lands. TranscriptEditorView wraps the editor root, and
SegmentMenuView wraps the context menu. transcript-editor.js becomes a
TranscriptEditorController that turns DOM events into saves and carries save
state across the htmx swap that answers each one. It no longer names a tag,
class, or attribute.

The template marks each speaker turn with data-speaker-turn so the view stops
depending on the article element.

Previously, the transcript editor script read the admin page's HTML directly: it selected `span[data-segment-id]`, walked up to the enclosing `article` to find a speaker turn, and kept its state in module-level variables. Any change to the template meant hunting through the controller for the selectors it broke. This change splits the script in two. `transcript-editor/view.js` owns every selector and DOM read. `TranscriptSegment` wraps one contenteditable segment and converts between the code-point offsets the server uses and the UTF-16 offsets DOM ranges use. `SpeakerTurn` holds one speaker's run of segments and decides where a paragraph break lands. `TranscriptEditorView` wraps the editor root, and `SegmentMenuView` wraps the context menu. `transcript-editor.js` becomes a `TranscriptEditorController` that turns DOM events into saves and carries save state across the htmx swap that answers each one. It no longer names a tag, class, or attribute. The template marks each speaker turn with `data-speaker-turn` so the view stops depending on the `article` element.
Hide transcript markup behind editor abstractions
All checks were successful
ci/crow/pr/build Pipeline was successful
de9c14ab74
Represent editor elements as transcript segments and speaker turns so controller logic no longer depends on HTML tags, CSS classes, or raw elements. Semantic data attributes keep rendering choices separate from behavior.
Use the existing segment ID attribute as the JavaScript hook and remove the unused paragraph-break markup and view property.
SpeakerTurn.nextSegment indexed segments[0] when findIndex returned -1,
so a paragraph break requested at the end of a segment from another turn
would have started a paragraph at this turn's first segment.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The controller rewrite made the htmx handlers return early when
saveInFlight was null, and the network error handler stopped writing
its message in that case. Every htmx event on the editor belongs to a
save, so a missing one is a bug the page should surface, and a failed
request should always tell the user to reload.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
segmentFromTarget dereferenced a null target, which the focused-segment
lookup passes whenever document.activeElement is null, and segmentById
wrapped every segment in the editor to find one. Guard the null and use
an attribute selector instead.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The view module added data-transcript-editor and data-segment-menu
attributes beside the ids the elements already carried, and nothing
else used either marker. Point the selectors at the ids and drop the
duplicate attributes from the template and the handler test fixture.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
segmentFromTarget and segmentById only return segments inside the live
editor, so the isConnected checks in focusout and onResponseError could
never fail. SegmentMenuView.on only wrapped addEventListener, and
actionFromTarget already receives events dispatched inside the menu, so
its contains check was always true. TranscriptSegment had no importer.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The fail-hard change destructured caret from saveInFlight but left one
read of the old save variable, so caret restore threw after every swap.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Explain why focusout may save text already in flight
All checks were successful
ci/crow/pr/build Pipeline was successful
1121d59dd1
A speaker change or paragraph break sends the segment's current text
without moving the baseline, so a focusout during that request starts a
second save. htmx queues it on the editor element the swap replaces and
drops it once that element leaves the document, so no duplicate request
reaches the server. Record that so nobody adds a guard for it later.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Document the transcript editor classes and methods
All checks were successful
ci/crow/pr/build Pipeline was successful
265e6a954d
Add doc comments to the controller, the segment, turn, editor, and
menu views, and to the methods whose names do not say what they do,
including why the two htmx swap-target checks differ.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
mike changed title from Hide transcript markup behind editor abstractions to Hide transcript editor markup behind view classes 2026-09-27 09:46:13 -04:00
The old name did not say what the method does, and "target" already
means an event or swap target throughout view.js (matchesTarget,
segmentFromTarget, willReplaceFrom, actionFromTarget). The new name says
the method works out the paragraph break from a segment and an offset.

Also rewrite the passive-voice comments in transcript-editor.js and
view.js in active voice, and add a test that pins a quirk: only the
end-of-segment path checks that the segment belongs to the turn, so a
break at offset 0 in a segment outside the turn returns that segment.
Callers always look the turn up from the segment, so the editor never
hits this, but the test records the behavior until membership is checked
on every path.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Explain each paragraph break assertion in its failure message
All checks were successful
ci/crow/pr/build Pipeline was successful
64afbb7707
The boundary test asserted five (segment, offset) pairs with nothing
saying which placement rule each one checks. Pass the reason as the
assert.equal message so it appears in the test output on failure.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
offsetFromPoint returned 0 when the point was not over the segment, which
callers could not tell apart from a caret at the start of the text. A
keyboard-opened context menu reports coordinates away from the segment, so
its Paragraph action broke the paragraph at the segment's start by
accident.

offsetFromPoint now returns null on a miss. Shift-click treats a miss as a
bug and throws, since its event target lies inside the segment. The menu
falls back to the focused segment's caret, and when there is no caret
either, its Paragraph action does nothing and a speaker change leaves the
caret where the swap finds it.

Also renames methods and fields so each name says what it does from the
call site: isTargetOf, isSwapTarget, open/close, breakParagraph,
saveParagraphBreak, queuedParagraphBreak, recordSave, menuOrigin, and
onBeforeSwap/onAfterSwap. Class and method comments now describe what each
does for its clients, in active voice, rather than how it works.

Testing: the full flake build, including the e2e suite, passes. No
automated test covers the keyboard-opened menu.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Ask for client-facing class and function comments
All checks were successful
ci/crow/pr/build Pipeline was successful
0ad07c86e2
Comments that narrate a method's implementation or name one particular
caller go stale and hide what the code is for. Ask instead for what the
code does for a client, in plain active English.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
mike scheduled this pull request to auto merge when all checks succeed 2026-09-27 11:35:20 -04:00
mike merged commit e84030feed into master 2026-09-27 11:37:50 -04:00
mike deleted branch transcript-editor-abstractions 2026-09-27 11:37:50 -04:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
oe/transcripts!74
No description provided.