docs(block-editor): spec for the embedded-contentlet selection guard (#36985) - #37285
Conversation
…36985) Issue-resolution spec for the reopened #36985. Clicking an embedded contentlet card does create a NodeSelection; a byte-wise change-guard then rebuilds the document and destroys it, leaving the card unselectable and the toolbar's contentlet actions disabled. The guard compares JSON.stringify of the incoming value against JSON.stringify of editor.getJSON(). TipTap normalizes on parse, so any stored document that does not round-trip byte-identically through the current schema fails it permanently -- which is why the defect looks random across a site. It is a per-row data condition, not per content type or per site. Records the measured evidence: - three confirmed triggers: the legacy chartCount typo (written until 3cd5549, #26025), missing indent on heading/paragraph/blockquote, and the string branch skipping preserveUnknownNodesInDocument for unknown node types; - the host-to-branch map explaining why only the legacy JSP contentlet editor is affected -- it is the only host where the value effect stays live after load; - AC-001..AC-008, including the deliberate node/mark split on AC-004. Also carries data-model.md (value shapes and the normalization fromJSON applies) and contracts/content-match.contract.md (the comparator's input-to-verdict table, which is the source for the Red-phase tests). Spec only -- no implementation. Per the Spec-Kit flow this needs another dev's approval before /speckit-plan output is acted on. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Claude finished @rjvelazco's task in 1m 34s —— View job Spec review — #36985 selection-guardThis is a documentation-only PR (three Spec-Kit artifacts, no implementation). I reviewed it for technical accuracy against the current source and internal consistency, since a wrong claim here propagates into the implementation PR.
Verification resultsEvery load-bearing code claim I could check against the repo holds:
Notably, the existing docstring on Notes (non-blocking)
VerdictNo blocking issues. The spec is accurate against the code, internally consistent, and honest about its unverified assumptions — appropriate for a PR-1 spec gated on dev approval. The one factual nit (
· |
rjvelazco
left a comment
There was a problem hiding this comment.
Analyze my comments; don't take anything for granted, be critical, and respond as concisely as possible.
| That rebuild is guarded by a check that asks "has the content actually changed?" The check | ||
| compares two JSON strings. For content saved by today's editor the strings match, the rebuild | ||
| is skipped, and everything works. For older content the strings don't match — even though the | ||
| content is identical — so the rebuild fires every time. |
There was a problem hiding this comment.
Why is the JSON different? Is there a change in the Schema? It's because of a change in the way we add the charCount, wordCount and readingTime? See:
and
We should always preserve the same schema so if something is failing because of this, we should fix the schema.
| **Actual Behavior**: the selection is created and immediately discarded. The document is | ||
| rebuilt, the cursor collapses to the end, no ring, "Edit Contentlet" stays disabled. |
There was a problem hiding this comment.
Is there a script we can add to the editor/JSP so we can ensure the Block Editor is rebuilding? This can be done locally while testing when you read this comment. Post here the result of the check
| enough. Never, for content last saved by the new editor. Note that dragging a text *range* | ||
| across the card still works, which is what distinguishes this from the original #36985. | ||
|
|
||
| ### Confirming it in the browser |
There was a problem hiding this comment.
Looks like this is a manual confirmation that we should do in the browser console which it's not the best practice. Is there a script we can add either in the block editor or in the JSP?
| Meanwhile TipTap normalizes heavily when it parses: it fills in schema defaults, drops | ||
| attributes the schema doesn't declare, and reorders keys to schema order. So the editor's side | ||
| of the comparison is always the normalized shape, and the stored side is whatever happened to be | ||
| written years ago. |
There was a problem hiding this comment.
Do we have a list of attr/marks/node/extension that were dropped? For compatibility we should support what the old editor supported when possible
| **Legacy JSP contentlet editor** — assigns `blockEditor.value` on the custom element | ||
| (`edit_field.jsp:316`), so the effect at `:619` *is* the loader and stays live on `value()` and | ||
| `editor()`. This is the only host where a re-push can reach `setContent` after load, and | ||
| therefore the only host where a failed check destroys a click-made selection. |
There was a problem hiding this comment.
Why we allowed JSP to re-push the value/setContent? I think content should be loaded once because after that the Block Editor has control over the content and, if I'm not wrong (we should double check), JSP or the legacy form shouldn't modify the Block editor content outside itself.
|
|
||
| ### What we're changing | ||
|
|
||
| - Replace the string comparison in `editorContentMatchesParsed` with a structural ProseMirror |
There was a problem hiding this comment.
How heavy can this be? Considering that the Block Editor is a Tree object and can has a lot of nesting?
If this happens to be a heavy check, can we skip comparison and always send the new object to the JSP? If we do that, how can we skip temporal state that we don't want to push? Ex: The highlight we add on text selection or the class we add to the dotContentlet?
There was a problem hiding this comment.
Is there a ProseMirror/Tiptap util for this?
There was a problem hiding this comment.
Before going with the fix you propose, I want you to check this: Why we keep listening to the this.value();? Is there a way the content of the block editor changes outside the block editor? Is there any issue/ticket that back up this behavior?
|
|
||
| --- | ||
|
|
||
| ## Reproduction *(mandatory)* |
There was a problem hiding this comment.
Another way to reproduce this issue without the script:
- Set the
FEATURE_FLAG_NEW_BLOCK_EDITORtofalse - Create a Block Editor content with this structure:
# Heading 1
text testing
[Insert a `dotContent`]
Text testing
- Save and Publish
- Verify that the
dotContentis clickable - Remove the
FEATURE_FLAG_NEW_BLOCK_EDITORor set it totrue - Go back to the just created content
- See the error when attending to click the
dotContent
Proposed Changes
Spec-Kit PR 1 of 2 for the reopened #36985 — specification only, no implementation.
Clicking an embedded contentlet card in the new Block Editor does create a
NodeSelection. A byte-wise change-guard downstream then rebuilds the whole document and destroys it, so the card shows no selection ring and the toolbar's edit / delete / reorder actions stay disabled.Why it looked random
The guard compares
JSON.stringifyof the incoming value againstJSON.stringifyofeditor.getJSON(). TipTap normalizes on parse — fills schema defaults, drops undeclared attrs, reorders keys — so any stored document that does not round-trip byte-identically through the current schema fails the check permanently. It is a per-row data condition, not per content type or per site, which is exactly what authors were reporting.Three confirmed triggers, all measured
chartCounttypoSetDocAttrStep('chartCount', …)until3cd5549d35(#26025, 2023-09-11).stripDocStatsstripscharCount, notchartCount.indentIndentExtensiondeclaresindent(default0) on heading / paragraph / blockquote. Older content has no such key; the editor adds it on parse.preserveUnknownNodesInDocument, so a custom block is compared placeholder-against-original.Supplying a missing key is not enough — it has to be in the right position. That is the measured Case B in the spec, and it is what makes this a byte-comparison bug rather than a missing-attribute bug.
Why only one screen
The new Angular Edit Content screen binds with
[formControlName], so the value arrives once throughwriteValueand the effect short-circuits forever after. The legacy JSP contentlet editor assignsblockEditor.value, so its effect is the loader and stays live — the only host where a re-push can reachsetContentafter load. The spec carries the full host-to-branch map.What's in this PR
specs/36985-block-editor-selection-guard/spec.mdspecs/36985-block-editor-selection-guard/data-model.mdNode.fromJSONappliesspecs/36985-block-editor-selection-guard/contracts/content-match.contract.mdplan.md,research.md,quickstart.mdandchecklists/are Spec-Kit working artifacts and are gitignored by design (.gitignore:229-233).Reproduction
No customer data required. The spec gives two recipes: a schema round-trip that needs no server, and an API-seeded BROKEN/CONTROL pair on a local instance. The legacy JSP contentlet editor is required — the content type needs
CONTENT_EDITOR2_ENABLED=false, and the new Angular screen will not reproduce it.A convenience seeding script is attached to the QA note on the issue rather than committed; the spec's steps are the source of truth.
Still open, recorded in the spec rather than hidden
dot-contentlet-editor.service.ts:204. If CONTROL also fails locally, "Why only one screen is affected" needs rewriting before anyone implements against it.commitEditorafter remotecustomBlocksextensions is excluded for the local fixture. This does not block the fix — once the comparison is structural the verdict becomes "unchanged", so a re-push is harmless rather than merely suppressed.Checklist
specs/36985-block-editor-selection-guard/checklists/requirements.md[NEEDS CLARIFICATION]markersgetSchema(createEditorExtensions(...)), not inferredNotes for reviewers
/speckit-planand/speckit-taskskeep resolving.chartCount→charCountdata migration, no port of click-to-select to the legacy editor (contentlet-block.node.ts:23-24has none, so theFEATURE_FLAG_NEW_BLOCK_EDITOR=falseworkaround still reproduces the original symptom — separate ticket), and no apply-once memo in the effect while the re-push is unidentified.falsetoday and must betrue— expect it to fail at Red.Spec for #36985
🤖 Generated with Claude Code
This PR fixes: #36985