feat(super-editor): disable toolbar mutations inside content-locked SDTs (SD-3274)#3534
Merged
Conversation
…locked SDTs Add a hasContentLockedStructuredContentSelection helper that walks the selection's ancestor chain, NodeSelection target, and range, then gate formatting, paragraph, list, table, link, image, and track-changes derivers through a new isMutationCommandDisabled. View controls (undo/redo, ruler, zoom, document-mode, formatting marks) keep their existing rules. Covered by unit tests across lock modes and selection shapes plus a behavior spec.
There was a problem hiding this comment.
cubic analysis
1 issue found across 14 files
Linked issue analysis
Linked issue: SD-3274: Disable styling toolbar when selection includes a locked SDT
| Status | Acceptance criteria | Notes |
|---|---|---|
| ✅ | Inline text-styling controls (bold, italic, underline, font size/family, text color, highlight, link, copy-format) are reported disabled when the selection includes a content-locked SDT | Formatting derivers were routed through the new mutation-gate and now return disabled when a content-locked SDT is present. Unit tests and an e2e UI test assert the buttons/states are disabled. |
| ✅ | Other mutation controls (lists, table actions, copy/clear-formatting, linked styles, track-changes accept/reject, etc.) are reported disabled when selection includes a content-locked SDT | Registry entries for mutation commands are now gated by the same predicate; tests cover representative mutation commands (table-insert, copy-format, clear-formatting, linked-style, track-changes acceptance) and assert they are disabled. |
| ✅ | Commands that are reported disabled cannot be executed (execute() refuses disabled commands and does not call underlying command implementations) | createHeadlessToolbar now checks command execution eligibility (including content-lock) before invoking; unit test ensures controller.execute returns false and underlying command mocks are not called. |
| ✅ | Selections that are NodeSelections, collapsed cursors inside locked block SDTs, or range selections that span into locked SDT content are detected and cause mutation controls to disable | A single predicate (hasContentLockedStructuredContentSelection) walks selection ancestry, node selection target, and nodesBetween range to detect locked SDTs; tests cover node selection, collapsed cursor in block SDT, and span selections crossing into locked SDT. |
| ✅ | View-only / document-level controls remain governed by existing rules and are not blocked by content-lock (e.g., undo/redo, ruler, formatting-marks, zoom, document-mode) | createHeadlessToolbar exempts these IDs from content-lock execution blocking and tests assert these controls remain available per their normal rules. |
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
Text alignment is a read-only state indicator that should remain visible even when the selection sits inside a content-locked SDT. Switch the text-align state deriver to isCommandDisabled so it only disables on a truly read-only context, not on the mutation-blocked one.
Structured-content chrome labels share pm-start/pm-end ranges with their underlying content. Without exclusion, DomPositionIndex could return the label element for caret lookups, breaking hover and click-to-place interactions over block SDTs.
Adds moveIntoBlockSdtBeforeTextBlockStart to the backspace chain so that Backspace at the start of a textblock following a block SDT moves the caret to the last text position inside the SDT instead of deleting into protected content. Lifts findFirstTextPosInNode / findLastTextPosInNode out of the table boundary navigation plugin into a shared helper module.
Adds moveIntoBlockSdtAfterTextBlockEnd to the delete chain so that Delete at the end of a textblock preceding a block SDT moves the caret to the first text position inside the SDT instead of deleting into protected content. Mirrors the existing backspace-side handler.
resolveInsertionBoundary now sorts candidate boundaries by distance from the requested target and falls back to side-of-bias only as a tie-break. The drop path passes the mapped source start as a position to avoid, so the moved node lands on the next nearest valid boundary instead of snapping back to where it came from.
… labels Consolidates structured content label rules so block and inline SDT labels share a single declaration for size, padding, border, background, and a new drag-handle ::before indicator. Scope-specific rules now only carry positioning, border-radius, and the inline display: inline-flex override.
Undo/redo transactions were being blocked by the structured-content lock plugin, preventing recovery of SDT content after deletion. Bypass the lock guard when the transaction is a history undo or redo.
When backspace or delete targets an sdtContentLocked structured-content SDT, collapse the selection to the wrapper boundary instead of letting the keystroke fall through. Prevents the locked inline content from being mutated while keeping the caret in a usable position for the next edit.
Replace the invisible 8px spacer with a full "Click or tap here to enter text" placeholder for both inline and block structured-content controls. The placeholder is layout-only (no document text), styled via CSS pseudo-element, and selected-node highlight inherits the system Highlight color. Wire the new chrome into pointer mapping, caret geometry, and the input manager so clicks on the placeholder land inside the SDT instead of snapping to the wrapper boundary. ArrowLeft from the trailing boundary now re-enters an empty inline SDT, and Backspace/Delete inside an unlocked inline SDT no longer escapes into surrounding text.
Route empty block-SDT paragraphs through paragraphToFlowBlocks so the placeholder run picks up the paragraph's resolved font, size, and color instead of falling back to the document defaults. The painter now applies those run styles to the placeholder span, keeping "Click or tap here to enter text" visually consistent with the surrounding paragraph chrome.
Use `max-content` with `max-width: 130px` instead of stretching block SDT labels to the chrome width, so short labels no longer span the entire block. Inner span now flexes with `min-width: 0` to keep ellipsis behavior.
Stop maxing the measured placeholder width with the fallback — for empty SDTs we now trust the measured value and only fall back when measurement returns zero. Also treat empty-SDT placeholder runs as visible content so chrome geometry (--sd-sdt-chrome-left/width) is emitted for them.
…int modes Remove CSS rules that blanked the ::before content for empty SDT placeholders in viewing and print modes so the placeholder prompt stays visible. Update tests to assert the rules are absent.
…ments fix(sdt): empty SDT placeholder text + cursor/keyboard interactions (SD-3237)
fix(super-editor): repair caret, keyboard, and drag interactions around block SDTs (SD-3237)
20fdcea
into
luccas/sd-3116-bug-image-set-as-presetcontent-in-sdt-field-does-not-persist
5 checks passed
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.
Summary
lockMode: contentLockedorsdtContentLocked, the headless toolbar now reports every mutation control (bold,italic,underline,strike,font-size,font-family,text-color,highlight,link,copy-format, alignment, line-height, linked-styles, lists, table actions, document operations, track-changes accept/reject) asdisabledand their state derivers short-circuit before reading editor state.execute()oncreateHeadlessToolbar: any command the current snapshot reports asdisabledis refused, and for commands not present in the configured registry the controller still evaluates the registry entry against the live snapshot so callers can't bypass the lock by invoking an id that isn't surfaced as a button. View-only controls (undo,redo,ruler,formatting-marks,zoom,document-mode) are explicitly exempted so they remain usable inside locked content.hasContentLockedStructuredContentSelectionpredicate inhelpers/context.tsthat walks the selection ancestry, NodeSelection target, andnodesBetweenrange so range selections that cross into a content-locked SDT are also caught.STRUCTURED_CONTENT_NODE_TYPES/isStructuredContentNodeTypeinto a shared module underextensions/structured-content/nodeTypes.js, reused by the view-mode selection helper and by the new toolbar context predicate to keep the set of recognized SDT node types in one place.isCommandDisabled(viewing-mode gate) from a newisMutationCommandDisabled(viewing-mode or content-locked SDT selection), and routes every mutation deriver through the new helper while leaving non-mutation derivers on the original gate.