deleteAdjacentChip and insert did getSelection()/Range work inside the component, which pushed it over the max-lines budget under a disable comment. rich-text-dom.ts already owns the DOM boundary, so the surgery moves there as two new exports, chipAtCaret and insertChipAtCaret, and the component keeps only its event-handling and output concerns. adjacentChip stays exported with its own spec case. The component disable comment is gone, since the file is now under the line budget. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
7.5 KiB
RD-21 — Move the selection surgery into rich-text-dom.ts, and delete the disable
Status: done Source: PLAN.md 3h, order step 3
Why
rich-text-editor.component.ts measures ~256 effective lines against a limit of 250, so it
carries /* eslint-disable max-lines */. The lines that put it over are not component
concerns: deleteAdjacentChip and insert do getSelection()/Range surgery inside a
component whose job is the toolbar and the contenteditable host.
rich-text-dom.ts already exists beside it, already owns the DOM boundary, and already has a
spec. The seam is built. This is the cheapest of Phase 3's seven splits, and it converts two
untested imperative branches into spec cases.
Read first
libs/shared/src/ui/rich-text-editor/rich-text-dom.ts— the four exports today (renderInto,createChip,readBlock,adjacentChip) and the file's header comment, which already states the contract this ticket extends.rich-text-editor.component.ts:257-293—deleteAdjacentChipandinsert, the two bodies that move.rich-text-dom.spec.ts— 8 cases, plain jsdom, no TestBed. The new cases join it.
Decisions (pre-made, don't relitigate)
-
Two new exports in
rich-text-dom.ts, both taking the editor root:/** The chip a collapsed caret sits next to, or null. `direction` is -1 for Backspace and 1 for Delete. Returns null when the selection is absent, is a range rather than a caret, or sits outside `root`. */ export function chipAtCaret(root: HTMLElement, direction: -1 | 1): HTMLElement | null; /** Insert `chip` at the caret when the selection is inside `root`, and leave the caret after it. With no usable selection, append to the last line instead. */ export function insertChipAtCaret(root: HTMLElement, chip: HTMLElement): void; -
chipAtCaretwrapsadjacentChip; it does not replace it.adjacentChipstays exported and keeps its own spec case, which tests the node/offset arithmetic directly.chipAtCaretadds the selection guards around it. The component stops importingadjacentChipand importschipAtCaretinstead. -
The two component methods reduce to their component concerns:
private deleteAdjacentChip(e: KeyboardEvent) { const el = this.editorEl()?.nativeElement; if (!el) return; const chip = chipAtCaret(el, e.key === 'Backspace' ? -1 : 1); if (!chip) return; e.preventDefault(); chip.remove(); this.emit(); } protected insert(key: string) { const el = this.editorEl()?.nativeElement; if (!key || !el) return; el.focus(); insertChipAtCaret(el, createChip(el.ownerDocument, key, this.labelFor(key), this.autoFor(key))); this.emit(); }e.preventDefault(),chip.remove(),el.focus()andthis.emit()stay in the component: they are event handling and output, not DOM boundary work. -
Delete
/* eslint-disable max-lines */from line 1 of the component. This is not optional bookkeeping —reportUnusedDisableDirectivesiserror, so leaving a directive that is no longer needed fails the build. The two checks pin each other: if the file is still over budget, lint fails onmax-lines; if it is under and the directive stays, lint fails on the unused directive. -
No new file, no new folder.
rich-text-dom.tsis the right home and already carries the header comment that describes exactly this responsibility. -
No story changes.
rich-text-editor.stories.tsexercises the component through the same public surface; nothing it renders changes.
Files
libs/shared/src/ui/rich-text-editor/rich-text-dom.ts— two new exportslibs/shared/src/ui/rich-text-editor/rich-text-dom.spec.ts— new caseslibs/shared/src/ui/rich-text-editor/rich-text-editor.component.ts— two shrunken methods, changed import, and the disable deletedlibs/shared/docs/behaviour-spec.mdx(regenerated, never hand-edited)
Steps
- Add
chipAtCaretandinsertChipAtCaretper decision 1, moving the bodies out of the component rather than rewriting them. - Add spec cases. Cover, at minimum:
chipAtCaretreturns the chip before a Backspace caret; returns null for a non-collapsed selection; returns null for a caret outsideroot;insertChipAtCaretsplices at the caret and leaves the caret after the chip;insertChipAtCaretappends when there is no selection insideroot. - Rewrite the two component methods per decision 3 and fix the import line.
- Delete the disable (decision 4).
- Run
npm run gen:behaviour-spec. git add -A, then run the acceptance commands.- Update this ticket's
Status:todoneand the README's RD-21 row todone. - Commit all of it together.
Acceptance criteria
Measured against the tree before handover.
git grep -c "^export function" libs/shared/src/ui/rich-text-editor/rich-text-dom.ts # is 4 -> MUST be 6
git grep -c "eslint-disable max-lines" -- libs/shared/src/ui/rich-text-editor/rich-text-editor.component.ts # is 1 -> MUST be 0
The selection surgery has left the component entirely:
git grep -c "getSelection" -- libs/shared/src/ui/rich-text-editor/rich-text-editor.component.ts # is 2 -> MUST be 0
git grep -c "getSelection" -- libs/shared/src/ui/rich-text-editor/rich-text-dom.ts # is 0 -> MUST be 2
git grep -c "adjacentChip" -- libs/shared/src/ui/rich-text-editor/rich-text-editor.component.ts # is 2 -> MUST be 0
adjacentChip survives with its spec case (decision 2):
git grep -c "export function adjacentChip" libs/shared/src/ui/rich-text-editor/rich-text-dom.ts # MUST be 1
git grep -c " it(" libs/shared/src/ui/rich-text-editor/rich-text-dom.spec.ts # is 8 -> MUST be >= 13
npm run ci --full # exits 0
Verification
Do not add a line-count command. npm run lint, inside the gate, is the exact check and a
hand-rolled grep -v | wc -l is not: it cannot reproduce eslint's skipComments for a trailing
comment or for the component's inline template. Decision 4 explains why lint alone pins both
directions.
--full is required. This edits libs/shared/src/ui/**, which the README's rule names
explicitly.
jsdom supports document.getSelection(), Range.deleteContents(), insertNode,
removeAllRanges and addRange, so every new case runs in the existing plain-vitest setup. No
TestBed, no browser.
Out of scope
- Splitting the component further. It is over by a handful of lines, not structurally wrong.
- Touching
renderInto,readBlockorcreateChip. - The
ponytail:note inrich-text-dom.ts's header about exotic pasted markup. That is a recorded limitation, not this ticket's work.
Risks
- Deleting the disable is mandatory, not cosmetic (decision 4). Forgetting it fails lint
with
Unused eslint-disable directive, which reads like an unrelated error. - Keep
adjacentChipexported. Its spec case imports it directly; folding it intochipAtCaretdeletes a test that covers node/offset arithmetic the wrapper does not. - Move the bodies, do not rewrite them. The caret placement after insert
(
setStartAfter→collapse(true)→removeAllRanges→addRange) is the part users feel; a "cleaner" rewrite is where a regression hides, and no story catches it. behaviour-spec.mdxdrift from the new spec titles. Regenerate in the same commit. A name used in adescribeorittitle also lands in that generated file — count it if you add a grep for one.