Skip to content

Slides: text-edit + adjust drag entry on grouped elements#337

Merged
hackerwins merged 2 commits into
mainfrom
slides-grouped-text-edit-entry
Jun 3, 2026
Merged

Slides: text-edit + adjust drag entry on grouped elements#337
hackerwins merged 2 commits into
mainfrom
slides-grouped-text-edit-entry

Conversation

@hackerwins

@hackerwins hackerwins commented Jun 3, 2026

Copy link
Copy Markdown
Collaborator

Summary

Two view-layer paths in the slides editor assumed slide.elements is flat, so any text/shape nested inside a group silently failed to enter the relevant interaction:

  • Text-edit entry (slide 22 of the Yorkie deck, "웹/모바일…" textbox). Double-click drilled in via Selection.doubleClick, but Editor.enterEditMode resolved the click target via Array.prototype.find and bailed — and the canvas mask for the in-edit element used the same flat pattern, so even an isolated entry fix would have ghost-painted the original underneath.
  • Adjustment-handle drag (slide 31 "Dogfooding" pentagonArrow). The yellow diamond armed but never wrote because startAdjustmentDrag had the same flat find AND used the group-local frame for the world↔local conversion. paintLiveAdjustments had the matching flat-map bug.

Both paths now resolve elements via the existing recursive helpers (findElement, buildElementWorldLookup) and pass world-frame elements where downstream code expects world coords. Store-side mutation APIs already walk the tree, so writes are transparent.

Code-review follow-up commit guards the post-commit autofit-grow write against silent miswrite under rotated / non-unit-scale ancestor groups (height would be stored as local even though it's measured in world). The guard skips the fit when local and world frame.{w,h,rotation} diverge — for slide 22's groups they don't, so behavior there is unchanged.

Test plan

  • pnpm verify:fast (lint + unit) green.
  • Vitest grouped-text-edit-entry.test.ts covers the world-frame mount contract.
  • Manual smoke: slide 22 of the reporter's deck — double-click "웹/모바일…" → text-edit enters and the in-place editor's dashed outline lines up with the dark rect.
  • Manual smoke: slide 31 — drill into the Dogfooding group, drag the yellow adjustment diamond → data.adjustments reflects the new value (verified live; the test mutation was undone via store.undo()).

Out of scope (deliberate)

  • Rotated / non-unit-scale groups still get the autofit-grow skip (guarded against corruption, not fully supported). Real-deck need would unlock the proper inverse-transform write-back as a follow-up.
  • startConnectorEndpointDrag has the same flat-find pattern. PPTX import can produce nested connectors but the editor-side group op forbids them; user-reachable failure mode is import-only and tracked in the todo's out-of-scope section.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Fixed text editing for elements nested inside groups. Double-click now correctly enters text-edit mode for grouped elements.
    • Resolved duplicate text rendering when editing text within grouped shapes.
    • Improved coordinate handling for overlay positioning when editing grouped elements.
  • Tests

    • Added test coverage for text editing in grouped elements.
  • Documentation

    • Added implementation notes and lessons for grouped element editing patterns.

hackerwins and others added 2 commits June 3, 2026 21:20
Two view-layer paths assumed `slide.elements` is flat:

- `enterEditMode` resolved the click target via
  `Array.prototype.find`, so double-click on text/shape elements
  nested inside a group silently no-op'd. Slide 22 of the
  Yorkie deck (text "웹/모바일..." in the right column) reproduced
  this — `Selection.doubleClick` drilled in correctly but the
  mount path bailed.

- `startAdjustmentDrag` used the same flat `find`, so the yellow
  adjustment diamond on a grouped shape armed but never wrote.
  Slide 31's Dogfooding pentagonArrow reproduced this.

The render-side mask for the editing element and the live
preview for adjustments had the matching flat `slide.elements.map`
bug — even if the entry paths were fixed in isolation, the
canvas would still ghost-paint the element under the in-place
editor / miss the live preview for grouped shapes.

Both paths now resolve elements via the existing recursive
helpers (`findElement`, `buildElementWorldLookup`) and pass
world-frame elements where downstream code expects world coords
(overlay text-box mount, world↔local adjustment conversion).
The store-side mutation APIs (`requireElement`,
`updateElementFrame`, `updateElementData`, `withTextElement`,
`withShapeText`) already walk the tree via `findElementPath`,
so the writes work transparently.

Live-verified on slide 22 (text edit enters at the correct
world position, no canvas ghost) and slide 31 (Dogfooding
arrow's adjustments commit after drag).

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Code review caught a silent miswrite hole in the prior commit:
text-edit autofit-grow at commit time writes the editor-reported
content height (world / canvas-logical coords) straight into
`frame.h` via `store.updateElementFrame`, which expects LOCAL
coords. For top-level elements local === world; for groups with
rotation 0 + unit scale, w/h/rotation also match. But for a
rotated or non-unit-scale group, the world height would be stored
as local height, scrambling the element's frame.

Detect transformed ancestor by comparing local vs world frame
(w/h/rotation) at enter time and skip the autofit-grow write when
they diverge. Properly composing the inverse ancestor transform is
the right long-term fix, but until then this guard prevents the
silent corruption. Scope is unchanged: slide 22's groups have
rotation 0 + refSize === frame, so the autofit-grow path still
runs there.

Also annotate the todo's out-of-scope section with the matching
`startConnectorEndpointDrag` flat-find pattern (editor.ts:3135)
so future audits don't miss it. PPTX import can produce
connectors inside groups; the editor-side group op forbids it, so
the user-reachable failure mode is import-only — deferred.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@coderabbitai

coderabbitai Bot commented Jun 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR fixes a bug where text and shape elements nested inside groups couldn't be edited or dragged because element lookups were non-recursive and frame math used group-local coordinates. The fix adds recursive element traversal, uses world-frame composition for rendering and positioning, and gates stored frame changes when ancestor transforms are present.

Changes

Grouped Element Edit Fixes

Layer / File(s) Summary
Task documentation and specification
docs/tasks/README.md, docs/tasks/active/20260603-slides-grouped-text-edit-entry-todo.md, docs/tasks/active/20260603-slides-grouped-text-edit-entry-lessons.md
Specification of the grouped-element editing bug (non-recursive lookup, group-local frame handling), implementation scope across enterEditMode, render masking, adjustment drag, and overlay preview, plus lessons learned on recursive traversal, world vs local frames, and jsdom testing constraints.
Recursive helper functions
packages/slides/src/view/editor/editor.ts
New internal helpers: maskEditingElement() recursively suppresses the in-edit element from render output, and replaceShapeAdjustments() rebuilds grouped shape adjustment data for preview painting.
Text editing flow with world-frame transforms
packages/slides/src/view/editor/editor.ts
enterEditMode() now resolves edit targets recursively and computes world-frame alignment; render() uses recursive masking so nested elements don't duplicate under the text-box editor; onCommit() skips height-fit writes when ancestor transforms are present.
Adjustment drag and preview with world-frame lookup
packages/slides/src/view/editor/editor.ts
startAdjustmentDrag() resolves grouped shapes via world-frame lookup; paintLiveAdjustments() uses recursive rebuild and world-frame lookup for overlay positioning instead of top-level-only filtering.
Regression test suite for nested element editing
packages/slides/test/view/editor/grouped-text-edit-entry.test.ts
JSDOM test suite with capturing mount that verifies nested text elements enter edit mode and receive world-frame positioning (composed through ancestor group transforms) via buildElementWorldLookup.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • wafflebase/wafflebase#298: Both PRs extend the text-box frame-height fit logic in enterEditMode and onCommit; this PR adds ancestor-transform detection to prevent corrupt heights for grouped elements.

Poem

A rabbit hops through nested slides,
Where groups once hid their text inside,
Now world-frames guide the pointer true,
And edits work in layers too! 🐰✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly addresses the main fix: enabling text-edit and adjustment-drag entry for elements nested inside groups, which is the core change across multiple code paths in the PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch slides-grouped-text-edit-entry

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

github-actions Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Verification: verify:self

Result: ✅ PASS in 258.2s

Lane Status Duration
tokens:build ✅ pass 2.5s
sheets:build ✅ pass 14.1s
docs:build ✅ pass 13.2s
slides:build ✅ pass 15.5s
verify:fast ✅ pass 166.5s
frontend:build ✅ pass 19.6s
verify:frontend:chunks ✅ pass 0.4s
backend:build ✅ pass 5.2s
cli:build ✅ pass 2.2s
verify:entropy ✅ pass 19.0s

Verification: verify:integration

Result: ✅ PASS

@codecov

codecov Bot commented Jun 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
docs/tasks/README.md (1)

41-41: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Update stale “Latest active task” value.

Line 41 still points to slides font load repaint, but Line 21 adds slides grouped text edit entry as the newest listed active task. Please align this footer line with the current index content.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/tasks/README.md` at line 41, Update the stale footer string "Latest
active task: slides font load repaint" so it matches the current newest task
entry; replace that value with "slides grouped text edit entry" (the updated
latest task referenced earlier in the file) so the footer and the index content
are consistent.
packages/slides/src/view/editor/editor.ts (2)

3404-3409: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Keep permanent guides visible during live adjustment preview.

This overlay repaint drops permanentGuides, so guides disappear on the first adjustment pointermove and only come back after mouseup. The other live-paint paths preserve that chrome.

Suggested patch
     renderOverlay(this.options.overlay, [liveEl], {
       scale: this.scale(),
       slideWidth: SLIDE_WIDTH,
       slideHeight: SLIDE_HEIGHT,
       allElements: synthetic.elements,
+      permanentGuides: this.options.store.read().guides,
+      pendingGuide: this.pendingGuide,
     });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/slides/src/view/editor/editor.ts` around lines 3404 - 3409,
renderOverlay call during live adjustment is dropping permanentGuides so they
disappear on pointermove; update the renderOverlay invocation to preserve and
pass the permanent guides into the overlay render. Specifically, when calling
renderOverlay(this.options.overlay, [liveEl], {...}), include the
permanentGuides (e.g. this.options.overlay.permanentGuides or
synthetic.permanentGuides) in the options object or merge them into allElements
so renderOverlay receives permanentGuides alongside
scale/slideWidth/slideHeight/allElements; ensure renderOverlay receives the same
permanentGuides the other live-paint paths do so guides remain visible during
pointermove.

2221-2224: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Sync the drill-in scope before selecting a grouped edit target.

The new recursive lookup lets enterTextEditing() open a nested element from root scope, but this.selection.set([elementId]) leaves Selection.getScope() unchanged. After commit/cancel, the overlay and drag paths in this file still convert frames through toWorldFrame(..., this.selection.getScope(), slide), so that grouped element comes back selected with root-scoped geometry and its handles/drags are misplaced.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/slides/src/view/editor/editor.ts` around lines 2221 - 2224, When
selecting a grouped/nested edit target in enterTextEditing(), also update the
selection's drill-in scope before calling this.selection.set([elementId]) so
Selection.getScope() matches the element’s actual nested scope; compute the
element’s scope (e.g. via the same recursive lookup used to locate the element)
and call the appropriate selection API to set that scope (for example
setScope(...) or the overload that accepts a scope with set([...], scope)) and
only then set this.selection and this.editingElementId so subsequent
toWorldFrame(..., this.selection.getScope(), slide) conversions use the correct
geometry.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@docs/tasks/README.md`:
- Line 41: Update the stale footer string "Latest active task: slides font load
repaint" so it matches the current newest task entry; replace that value with
"slides grouped text edit entry" (the updated latest task referenced earlier in
the file) so the footer and the index content are consistent.

In `@packages/slides/src/view/editor/editor.ts`:
- Around line 3404-3409: renderOverlay call during live adjustment is dropping
permanentGuides so they disappear on pointermove; update the renderOverlay
invocation to preserve and pass the permanent guides into the overlay render.
Specifically, when calling renderOverlay(this.options.overlay, [liveEl], {...}),
include the permanentGuides (e.g. this.options.overlay.permanentGuides or
synthetic.permanentGuides) in the options object or merge them into allElements
so renderOverlay receives permanentGuides alongside
scale/slideWidth/slideHeight/allElements; ensure renderOverlay receives the same
permanentGuides the other live-paint paths do so guides remain visible during
pointermove.
- Around line 2221-2224: When selecting a grouped/nested edit target in
enterTextEditing(), also update the selection's drill-in scope before calling
this.selection.set([elementId]) so Selection.getScope() matches the element’s
actual nested scope; compute the element’s scope (e.g. via the same recursive
lookup used to locate the element) and call the appropriate selection API to set
that scope (for example setScope(...) or the overload that accepts a scope with
set([...], scope)) and only then set this.selection and this.editingElementId so
subsequent toWorldFrame(..., this.selection.getScope(), slide) conversions use
the correct geometry.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 818cbf1b-9309-4478-a583-89aff86f5d43

📥 Commits

Reviewing files that changed from the base of the PR and between e84dffe and 764e33a.

📒 Files selected for processing (5)
  • docs/tasks/README.md
  • docs/tasks/active/20260603-slides-grouped-text-edit-entry-lessons.md
  • docs/tasks/active/20260603-slides-grouped-text-edit-entry-todo.md
  • packages/slides/src/view/editor/editor.ts
  • packages/slides/test/view/editor/grouped-text-edit-entry.test.ts

@hackerwins
hackerwins merged commit 86d0692 into main Jun 3, 2026
4 checks passed
@hackerwins
hackerwins deleted the slides-grouped-text-edit-entry branch June 3, 2026 14:20
@hackerwins hackerwins mentioned this pull request Jun 7, 2026
5 tasks
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.

1 participant