TEC-2515: editor chrome + flat schema v2 - #552
Merged
Merged
Conversation
Docs now carry an optional schemaVersion in a ddocMeta Y.Map (absent = v1). When a doc declares a version newer than this build supports, no editor is created at all (no y-sync binding) and a refresh prompt replaces the editor surface in both DdocEditor and PreviewDdocEditor. Dormant until v2 docs exist; shipping early so stale tabs are protected by launch.
defaultExtensions gains a schemaVersion param: v2 registers FlatDocument (content 'block|columns|pageBreak') and FlatColumn instead of dBlock, TrailingNode and the wrapped Document. All four assembly paths respect it (main, per-tab, headless, AI re-fork). useDocSchemaVersion resolves a doc's version from its ddocMeta marker and stamps genuinely new docs with preferredSchemaVersion (new DdocProps field, creation-time only). Demo gets a 'New Document (flat schema v2)' action driven by a v2 URL param. Verified in the running demo via headless Chrome: v2 doc creates flat, types with stock keymaps, survives reload without the URL param (marker rules), v1 docs unchanged, template overlay auto-suppressed on v2.
Switches the dBlock node view from the old per-block gutter/content-shell flex layout to container-level padding on .ProseMirror.main-doc-editor, with DBlockDragHandle (the Task 2 floating control cluster) as the sole block chrome. Deletes DBlockToolbar, the dblock-view-registry portal resolution machinery, and the associated hover/focus listeners.
Editor#setOptions only shallow-merges top-level options, so passing a partial editorProps object (as the toolbar's keyboard-shortcut effect did) silently replaced the whole thing -- wiping construction-time attributes (including the main-doc-editor class), clipboardTextSerializer, and handleDOMEvents. Adds a mergeEditorProps helper that spreads the current editorProps before patching, used at both call sites in editor-utils.tsx.
…itorProps
createEditorForTab's new Editor({ editorProps: { ...DdocEditorProps, ...,
attributes: { spellCheck: 'true' } } }) placed a duplicate attributes: key
after the DdocEditorProps spread in the same object literal. A later
duplicate key in a JS object literal replaces the earlier one outright
rather than merging, so this silently dropped DdocEditorProps' whole
attributes record (main-doc-editor/prose classes, spellcheck,
suppressContentEditableWarning) down to just { spellCheck: 'true' } --
predates this branch, but the new container padding is keyed on
main-doc-editor so it's now load-bearing.
Extracts the merged attributes into an exported TAB_EDITOR_ATTRIBUTES
constant used at the single call site instead of a second inline
attributes key, and drops the redundant camelCase spellCheck duplicate
(DdocEditorProps' lowercase spellcheck already covers it -- setAttribute
normalizes case on HTML elements anyway).
Every code path that emitted dBlock-shaped content or walked to a dBlock ancestor now detects the live schema (schema.nodes.dBlock) and produces flat content under v2: resizable-media and media-caption Enter, setColumns and buildNColumns, sanitize-content's corrupt-block fallback, list <-> text conversion in the bubble menu (incl. the mixed-selection guard), code-block Mod-Enter escape, the === page-break input rule and paste post-process, and TOC heading expansion. New shared util: utils/block-schema.ts (schemaHasDBlock / wrapBlockNode). Demo exposes window.__ddoc for smoke tests. Verified by driving real editor commands in the demo on both schemas: v2 doc holds paragraph/hr/pageBreak/codeBlock/table/callout/columns as flat top-level nodes with a dblock-free Yjs fragment, survives reopen; v1 control produces identical dBlock-wrapped structure; zero page exceptions.
The collapse engine in dblock-collapse.ts is generalized around one resolver: getBlockHeading(doc, node) returns the heading of a top-level block (the dBlock's first child in v1, the node itself in flat v2), with heading positions and caret offsets derived from schema presence. All exports keep their signatures, so v1 callers (dblock extension, node view, toolbar, content-item actions) are untouched. v2 registers the same plugin via a new FlatHeadingCollapse extension since it has no dBlock extension to host it. Preserved v1 quirk: non-dBlock top nodes (columns, pageBreak) are never hidden; flat v2 hides every block in a collapsed region. Verified in the demo on both schemas: collapsing an H1 hides exactly its section (paragraph, child H2, its paragraph) while the sibling H1 stays visible, state survives reopen via the Yjs attr, Enter at the collapsed heading end expands and re-anchors the caret, v1 behavior unchanged, zero page exceptions.
Cmd+Shift+D now shows which schema a doc uses: the ddocMeta marker vs the extension set the editor actually loaded, with a red MISMATCH state when the fork picked the wrong set. Reads through window.__ddoc, no prop threading.
The JSON button in the DevBar opens a panel showing editor.getJSON() pretty-printed and refreshed every second while open, with a copy button. For eyeballing v1 vs v2 document shapes while testing.
New BlockId extension, v2-only: every top-level block carries a blockId uuid as a global attribute (data-block-id in DOM), assigned by an appendTransaction plugin that does a shallow top-level pass on doc changes. keepOnSplit false so the original half of a split keeps its identity; the same pass re-ids duplicates that arrive via paste. Not undoable on its own (addToHistory false). Nested blocks declare the attr but stay null. Verified in the demo: fresh blocks get distinct ids, split keeps the original id and mints one for the new half, inserting a block with a stolen id gets re-identified, ids persist across reopen, and v1 docs contain no blockId anywhere.
v1 keeps the custom dBlock-aware TrailingNode (its detection reads the wrapper's inner child and would loop on a flat doc). v2 enables StarterKit's stock trailing node instead: schema-agnostic, appends a paragraph whenever the doc ends with a non-paragraph block. Known nuance for the fonts follow-up: stock trailing paragraphs carry no trailing-node class, so typography-persistence does not skip them in v2. Verified in the demo: v2 docs ending with a table or code block get a trailing paragraph, typing does not multiply paragraphs, v1 trailing dBlock with its class is unchanged.
New AiWriterSpaceTrigger extension: a single space in an empty top-level paragraph (or column cell) replaces it with the aiWriter node, matching the v1 behavior that lives inside the dBlock extension. Registered in the v2 set behind the same hasAvailableModels gate as v1, and self-disables when aiWriter is absent from the schema. The two dBlock checks inside ai-autocomplete needed no change: both are shadowed by schema-neutral guards in either schema. Verified by unit tests driving the plugin headlessly: trigger fires in an empty paragraph, skips non-empty ones, refuses a second writer while one is active, and no-ops without the aiWriter node type. (The demo has no AI models configured, so end-to-end needs ddocs.new, same as v1 today.)
unwrapDBlocksInJSON recursively hoists dBlock children (recursing into columns; empty wrappers degrade to empty paragraphs) so templates and any legacy JSON load into flat v2 docs. The v1 template JSONs stay untouched as the single source of truth. M2 parity exit test passed: all 6 package templates loaded into a live v2 editor with zero dBlock nodes, text fully preserved, and top-level block counts exactly matching the v1 baseline (including identical trailing-node behavior). Unit tests cover the transform invariants per template plus nesting and edge cases.
The slash-menu columns commands chained .focus(selection.head - 1), but that argument evaluated at chain-build time, before setColumns ran, so the caret aimed at a pre-insert position. setColumns now places the caret in the first column cell within the same transaction, with schema-aware offsets and TextSelection.near for the final descent. docs/POSITION_AUDIT.md records the verdict for every audited call site: these two were the only stale ones in scope; the caption-flow arithmetic is correct-by-construction (single chains against the post-mutation doc); createInputRule is dead code with zero consumers. Verified live in both schemas: caret lands in column index 0 after setColumns, typing goes into the first cell, keepContent variant included.
Three measured spacing problems from the Wikipedia-paste report: 1. Pasted HTML kept stacks of empty <p> (ProseMirror's native paste path, not the markdown handler). transformPasted now collapses interior runs of empty paragraphs to one, both schemas; slice edges never touched. The markdown-file path got the same collapse in its post-process. 2. The v1 heading translateY compensation leaked into v2, shifting headings off their caret line (40/56px asymmetric gaps). Scoped to :not([data-schema-version='2']) via a new data-schema-version attribute on the editor root. 3. v2 headings inherited prose-lg's 48px margins that v1 never shows (margins collapse through its wrapper rows). v2 pins headings to the same 1.5rem as paragraphs: uniform 24px rhythm, identical to v1. Documented while here: the editorProps attributes literal shadows DdocEditorProps.attributes, so the prose-p:my-2 class list in types.ts never reaches the main editor; today's real rhythm is 24px everywhere. Changing that is a product-visible design call, left for the layout track. Verified by DOM measurement in the demo, before and after, both schemas: v1 pixel-identical, v2 uniform 24px gaps, empty run collapsed 8 to 7 blocks.
Slices with bare top-level blocks (copied from a flat v2 doc, or parsed from any external site's HTML) silently demoted headings on paste into v1: prosemirror's fitter finds no legal closed placement under (dBlock|columns|pageBreak)+ - canReplaceWith never considers wrapping and the caret's dBlock already holds its one allowed child - so it falls back to merging the heading's inline content into the caret paragraph. v1-native copies never hit this because their slices carry the dBlock wrappers. New v1-only transformPasted plugin rewraps every doc-illegal top-level block of the pasted slice in a dBlock and grows the open depths across added wrappers, so foreign slices arrive exactly v1-native-shaped. Also fixes the Cmd+A stray-empty-paragraph paste symptom and external HTML heading pastes. Inline pastes still merge into the caret block. Verified against the real clipboard in Chrome across the full v1/v2 x copy-mode matrix; unit tests prove the repro fails without the plugin.
The diff pipeline is schema-agnostic end to end - schema-less blob decode via yDocToProsemirrorJSON, type-generic LCS differ, and the renderer's dBlock case is a v1-only refinement flat content never enters. Locked by characterization tests in the app repo (utils/diff/__tests__/node-diff-flat.test.ts). Persistent blockIds give v2 diffs better block alignment than v1's attr-less wrappers.
bhaveshxrawat
force-pushed
the
integration/tec2515-x-v2
branch
from
August 12, 2026 12:48
44b292e to
555622e
Compare
mbj36
marked this pull request as ready for review
August 13, 2026 07:27
PreviewDdocEditor (blog publish preview, version history) mounted the DragHandle chrome even though the surface is statically read-only. The upstream DragHandle plugin relocates its rendered element outside React's tree, so when a late-arriving blob flips the schema marker and the editor rebuilds (v1 -> v2), React re-commits EditorContent against the relocated .drag-handle sibling anchor and insertBefore throws NotFoundError - killing the blog publish preview for flat-schema docs. DBlockToolbarProvider gains a STATIC isPreviewEditor flag (the dynamic isPreviewMode would reintroduce the unmount crash on every edit/preview toggle in the main editor) that skips the DragHandle and the template overlay entirely: read-only heading affordances live inside the node view, the cluster is hard-hidden for non-editable editors anyway, and the relocated element also leaked per version switch.
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.
Both TEC-2515 tracks in one branch. #551 is closed and fully absorbed here —
every commit from
TEC-2515is an ancestor of this branch, with authorshipintact.
What's in here
container padding, template overlay portaled out of the editable DOM,
Fixes 1-3, to-do checkbox alignment, gutter toolbar shrink.
schemaVersionmarker plus theunsupported-version guard, the flat document/column schema behind
preferredSchemaVersion, block IDs, collapse generalised to both schemas,template unwrap, position-mapping audit.
Version:
4.4.0, merged with main through4.3.9(keeps@fileverse/ui5.2.2).Two of these are v1 bugs, not v2 bugs
Worth knowing regardless of the schema work — both were live on main:
.ProseMirror..drag-handleand.ProseMirrorare positioned siblingswith
z-index: auto, so DOM insertion order alone decided which painted ontop. The cluster stayed visible while every click landed on the editor
behind it. Fixed with an explicit z-index.
toggle scrolled to the caret unconditionally, and the caret is usually
somewhere else entirely. It now scrolls only when the transaction actually
moved the caret.
Other fixes
v1 builds it in the node view, which flat blocks don't have)
getHeadingLinkSlugresolved through the wrapper, so copy-link was dead in v2extractTitleFromContentwalked two levels, so every v2 document came outuntitled — it feeds export filenames from seven call sites
trailing node has none, so it's identified by position too
use the
'self'origin so they never reachonChange, leaving the saved blob'sactiveTabIdstale while IndexedDB has the true onefull editor on every switch — the main source of transient memory spikes on
large multi-tab documents
landed on code this PR deletes)
Verification
tscclean, 148 tests, build passes. Two probe scripts are committed so this isrepeatable:
scripts/parity-sweep.cjs— drives v1 and v2 through the same 20 features andcompares results: export markdown/HTML, title extraction, counts, TOC,
comments, search and replace, undo/redo, copy-paste, lists, slides, read-only
render, node commands, block duplicate/delete, tables, page break, suggestion
marks, split-view markdown, media, columns. 20/20 match, 0 page errors.
scripts/chrome-parity-probe.cjs— trusted mouse input, because a coveredbutton still passes a synthetic click: cluster hit-testability, collapse
toggle, options menu, viewport stability, template insert, and first paint
after a multi-tab reload. Identical in both schemas.
All six templates were also inserted through the real overlay and compared node
by node; the only difference is the absent wrapper.
Rollout constraint
The version guard must be live in a published release before any v2 document
exists. A client without the guard that opens a flat document parses it with
dBlock rules and writes that structure back through Yjs — corruption with no
undo, and it cannot be fixed retroactively for code already running in a
browser. Merging and releasing is dormant and safe; the ddocs.new flag that
creates the first v2 document is the step that must wait.
Not covered
Collaboration has not been tested against v2 — it can't produce a v2 document
today, but it must be tested before the flag flips. Chrome desktop only. Binary
exports (PDF/docx/odt) were not compared, nor comment/suggestion flows beyond
the marks. The v1 keymap shrink never happened, so
dblock.tsis still ~1090lines and the original v1 list bugs remain.