Skip to content

fix(core): render text boxes anchored inside table cells in the editor#975

Draft
sorenlouv wants to merge 12 commits into
eigenpal:mainfrom
sorenlouv:feat/textbox-in-table-cell
Draft

fix(core): render text boxes anchored inside table cells in the editor#975
sorenlouv wants to merge 12 commits into
eigenpal:mainfrom
sorenlouv:feat/textbox-in-table-cell

Conversation

@sorenlouv

@sorenlouv sorenlouv commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Summary

A text box anchored inside a table cell didn't appear in the editor and was lost on save — unlike text boxes in the body, headers, or footers, which work. They now render and round-trip from inside a table cell like anywhere else.

Before/after — text box in a table cell, in the editor

Fixture: e2e/fixtures/textbox-in-table-cell.docx (synthetic), covered by regression tests.

A floating box renders in-flow inside the cell for now; exact in-cell float positioning is a follow-up. The text-box serializer change here is identical to #967's and merges cleanly with it.

Opening as a draft for review.

A text box anchored from a run inside a table cell was dropped: the parser's
text-box enrichment ran only on block-level paragraphs (not cell paragraphs),
the serializer emitted text-box content only for `textBox`-geometry shapes
(parsed boxes are always `rect`), and the ProseMirror conversion never surfaced
cell boxes — so the box was lost on save and never rendered in the editor.

- parser: enrich cell paragraphs; serializer: emit a shape's text body on text
  presence rather than geometry
- schema: allow a `textBox` inside `tableCell`/`tableHeader`
- toProseDoc/fromProseDoc: promote a cell paragraph's anchored boxes to sibling
  `textBox` nodes and re-anchor them back on save
- layout-bridge + painter: render the cell box in-flow inside the cell

Anchored/floating boxes render in-flow; true in-cell float positioning is a
follow-up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@eigenpal-release-pal

eigenpal-release-pal Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

All contributors have signed the CLA ✍️ ✅

Posted by the CLA bot.

jacobjove and others added 9 commits June 23, 2026 08:38
* feat(core): footnote-PM-doc layout seam + data-footnote-id + note-ref marker on serialize

Adds getFootnotePmDoc seam (mirrors getHfPmDoc) so the painter can read a
live footnote ProseMirror doc; stamps data-footnote-id on .layout-footnote-content
for per-footnote position disambiguation; re-inserts the w:footnoteRef/w:endnoteRef
marker run when serializing a normal note from content (parser drops it). Groundwork
for editable footnotes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(react): lazy hidden footnote ProseMirror view + writeback

Mounts one off-screen EditorView for the actively-edited footnote (lazy single
slot keyed by activeFootnoteId), mirroring HiddenHeaderFooterPMs. Transactions
sync proseDocToBlocks back to Document.package.footnotes and clear verbatimXml;
useLayoutPipeline feeds the live doc through getFootnotePmDoc so the painter
reflects edits. Footnote-edit wiring lives in a new useFootnoteEditState hook
(keeps PagedEditor under the line cap). Click routing/overlay follow.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(react): route clicks into footnotes for editing

Clicking a painted footnote enters footnote-edit mode for that data-footnote-id,
places the caret in its hidden view (scoped span-snap so colliding per-footnote
PM positions never cross-match), focuses it, and blurs+read-onlys the body;
clicking outside exits. activeSurface() now routes body|HF|footnote. Adds e2e
spec (run via playwright-runner) and a unit test for the hit-resolver.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(react): restore body focus when a click exits footnote-edit mode

Exiting footnote mode via a body click now hands focus + caret back to the
body in the same gesture. The restore is deferred to a parent-level effect
(exitFootnoteToBody) that runs after HiddenFootnotePM tears its view down, so
the teardown's destroy-blur can't steal the focus, and the body PM is already
editable by then. Verified by the footnote-editing e2e (click→type→exit→type).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(react): visible caret + selection overlay for footnote editing

Draws the caret and range-selection rects over the actively-edited footnote,
mirroring the header/footer overlay. New framework-free core helpers
(computeFootnoteCaretRectFromView/SelectionRectsFromView) scope the DOM walk to
the active .layout-footnote-content[data-footnote-id] so colliding per-footnote
PM positions never cross-match; DocxEditorPagedArea portals the overlay and
recomputes on painter:painted + resize. e2e asserts the caret renders within
the clicked footnote bounds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(vue): editable footnotes at parity with React

Mirrors the React footnote-editing feature in the Vue adapter: lazy single
hidden footnote EditorView + writeback (useFootnotePM), click routing into
footnotes (usePagesPointer), and caret/selection overlay (useFootnoteOverlay +
FootnoteOverlay.vue) reusing the framework-free core helpers. getFootnotePmDoc
feeds the live doc to the painter. No public API change; parity contract holds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore: changeset for editable footnotes

* chore: update API report for footnote-editing exports

* fix(react): exit footnote-edit mode when the edited footnote is gone

Reset footnoteEditId when the actively-edited footnote is no longer present in
the document (e.g. a new document loaded mid-edit) — otherwise the body PM
stays readOnly and the next writeback resolves the stale id against the new
document. Keyed on footnote existence, not document identity (history.state
gets a fresh object per body edit, so a bare [document] reset would exit
mid-edit). Mirrors the Vue adapter's destroyFootnotePM-on-load behavior.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(footnotes): black caret + glyph-accurate click placement

The footnote caret rendered blue (HF chrome color) — make it black to match the
body caret in both adapters. Footnote clicks resolved position via a coarse
estimate that drifted (clicking between letters landed at the word end); React
now reuses the body's glyph-accurate findPositionInSpan (exported from core) on
the span under the cursor, scoped to the active footnote. Vue keeps the coarse
snap for now (precise caret exposes a separate Vue typing issue).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(footnotes): enable click-drag text selection

The footnote click handler placed the caret and returned without arming drag
state, so click-drag selected nothing. Arm dragAnchorRef + isDraggingRef from the
clicked position on mousedown (DOM-resolved, no view needed); the existing move
handler already extends the selection through the footnote surface. e2e covers it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Jacob Jove <jacobjove@Jacobs-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…genpal#991)

* fix(core): keep a footnote reference on the same line as its word

The line-breaker created a wrap opportunity at every run boundary, so a
footnote/endnote reference run (a separate superscript with no space
before it, e.g. copyright.¹) could split onto the next line. Fold the
width of the unbreakable content that follows a run's last word into the
wrap decision so adjacent runs with no whitespace between them wrap as a
single cluster, matching Word.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(core): derive footnote-glue maxWidth from measureTextWidth

The hardcoded maxWidth assumed this file's 8px/char canvas stub, but the
guard only installs it when no global document exists; run after another
test that sets one, measureTextWidth returned different widths and the
control case failed. Derive the threshold from measureTextWidth so the
test is self-consistent under whichever stub is active.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Jacob Jove <jacobjove@Jacobs-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…igenpal#994)

* fix: superscript footnote/endnote refs only when the style says so (Word parity)

A bare anchor run (no FootnoteReference rStyle, e.g. Pandoc output) was
force-raised by the layout bridge, diverging from Word, which renders an
unstyled anchor at the baseline. Drop the implicit superscript; it now flows
solely from the resolved character-style chain or the run's own vertAlign.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(core): note basedOn limitation + endnote-mark intent in footnote-ref parity test

Addresses review feedback on PR eigenpal#994 — clarify that getRunStyleOwnProperties
resolves only own (not basedOn-inherited) vertAlign, and that endnoteRef content
maps to the footnoteRef mark.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(core): correct basedOn note — styleParser pre-merges the chain

The prior comment (and a PR eigenpal#994 review reply) wrongly claimed a basedOn-inherited
vertAlign renders at the baseline. styleParser.resolveStyleInheritance merges the
parent rPr into the child before getRunStyleOwnProperties reads it, so production
honors inherited superscript; the own-only behavior is the test mock's, not the
real resolver's.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Jacob Jove <jacobjove@Jacobs-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…igenpal#988)

* fix(core): grow a section's footer/header band per its own margins

extendMarginsForHeaderFooter decided whether to grow the header/footer band
once, from the body section's margins, then applied that decision to every
section. A section whose own margins are thin — e.g. a landscape table section
with a 0.5in bottom margin (≈ the footer distance) embedded in a 1in-margin
portrait body — never grew its footer band, so the footer overlapped the
footnote area and the page number rode up beside the last footnote instead of
sitting below it. Decide the overflow per margin set, using that set's own
top/bottom and header/footer distances.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore: changeset for header/footer band fix

---------

Co-authored-by: Jacob Jove <jacobjove@Jacobs-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…, hyperlinks) (eigenpal#987)

* feat(core): options-aware generateTableOfContents (level range, title, hyperlinks)

* Address review: clamp/order TOC level range + test empty-string title
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…al#996)

The editable-footnotes change (eigenpal#995) pushed the Vue composable to 1003 lines,
past the default 1000-line cap, failing the lint job on main. Add a per-file
override with modest headroom, matching how DocxEditor.vue (bumped to 1250 in
the same PR) and the other cohesive orchestrators are handled.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Resolves conflicts from eigenpal#967 (text boxes + block-level bookmarks on
round-trip), which landed an identical drawing.ts 'gate on text presence,
not geometry' fix plus overlapping textbox/table conversion changes:

- runSerializer/drawing.ts: kept eigenpal#967's text-presence gating (code was
  identical on both sides; took its more detailed comments)
- tableParser.ts: kept eigenpal#967's BlockMarkerCollector/parseBlockMarker import
- toProseDoc/textbox.ts: unioned the type imports (Paragraph,
  ParagraphContent, Run, TextBox, Shape, TextFormatting)

typecheck passes; textbox + block-bookmark round-trip tests (61) green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 23, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
docx-editor Ready Ready Preview, Comment Jun 23, 2026 9:14am

Request Review

Compress the multi-line comments added for the cell text-box path to one or
two lines and drop the dangling "Option A" references. No behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants