Web frame: programmatic zoom (toolbar / keyboard / fit) leaves the iframe overlay at the old size #106

Open
opened 2026-04-29 10:52:53 +00:00 by AhmedHanafy725 · 3 comments
Member

Summary

Zooming the canvas via any path other than the mouse wheel leaves the iframe DOM overlay at its old size and screen position. The Konva placeholder card rescales correctly; the iframe content stays at the original 1.0 scale, often stranded inside a now-smaller-or-larger placeholder until a full page reload.

Steps to reproduce

  1. Open a board, place a webframe and let it load.
  2. Click the toolbar Zoom Out button (or press - / use the zoom slider).
  3. The Konva placeholder shrinks; the iframe overlay does not.

Expected

The iframe overlay tracks the new stage scale and position on every zoom change, just like it does for mouse-wheel zoom.

Actual

The iframe stays at the previous screen rect; only the placeholder is rescaled. A page reload — or any subsequent mouse-wheel zoom — brings them back into alignment.

Root cause

WhiteboardCanvas.setZoom(newScale) (in canvas.js) calls stage.scale({...}) and stage.position({...}) programmatically. Neither call fires Konva's wheel event, which is the only zoom hook webframe.js installs:

stage.on('wheel.wf_<id>', function() { hideOverlay; setTimeout(showOverlay, 150) });

Every programmatic zoom path therefore bypasses the overlay refresh:

  • Toolbar Zoom In / Out / 100% buttons (app.js:621/626/636)
  • Keyboard + / - shortcuts (per CLAUDE.md)
  • Fit-to-content / minimap navigation (if any)
  • Presentation focusFrame (frames.js:164) — already worked around in #102 by hiding all overlays during presentation, but the underlying bug is the same

#101 fixed the receiver-side overlay tracking on remote position/size updates, but the local programmatic-zoom path was never wired.

Suggested fix

Smallest reliable fix: at the end of setZoom in canvas.js, call a new WhiteboardWebframe.refreshAllOverlays() helper that walks every registered overlay and re-runs updateOverlayPosition (essentially Object.keys(overlays).forEach(refreshOverlay)).

Add the helper to webframe.js next to hideAllOverlays / showAllOverlays:

function refreshAllOverlays() {
    Object.keys(overlays).forEach(function(id) { refreshOverlay(id); });
}

…and export it. Then in canvas.js::setZoom, after saveView(), call:

if (typeof WhiteboardWebframe !== 'undefined' && WhiteboardWebframe.refreshAllOverlays) {
    WhiteboardWebframe.refreshAllOverlays();
}

refreshOverlay doesn't toggle _hiddenForInteraction, so any overlay that's intentionally hidden (e.g. during a remote drag in flight) stays hidden — only its target screen rect is refreshed for when it's next shown.

Optional follow-up: have setZoom stage.fire('zoomchange') and let webframe.js install a stage.on('zoomchange.wf_<id>') listener, decoupling the modules. Out of scope for this issue.

Acceptance criteria

  • Click Zoom In / Zoom Out / 100% — the iframe overlay rescales and repositions to match the placeholder, no flicker, no reload needed.
  • Keyboard + / - zoom keeps the overlay tracked.
  • Mouse-wheel zoom (existing path) continues to work as before — overlay hidden during gesture, restored after idle.
  • Presentation mode: webframes inside the focused frame remain correctly placed across zoom changes (composes with #102's hide-non-members behaviour).
  • No regression to drag, resize, rotation, sync, delete, or remote-update paths.
  • No new console errors.
## Summary Zooming the canvas via any path other than the mouse wheel leaves the iframe DOM overlay at its old size and screen position. The Konva placeholder card rescales correctly; the iframe content stays at the original 1.0 scale, often stranded inside a now-smaller-or-larger placeholder until a full page reload. ## Steps to reproduce 1. Open a board, place a webframe and let it load. 2. Click the toolbar Zoom Out button (or press `-` / use the zoom slider). 3. The Konva placeholder shrinks; the iframe overlay does not. ## Expected The iframe overlay tracks the new stage scale and position on every zoom change, just like it does for mouse-wheel zoom. ## Actual The iframe stays at the previous screen rect; only the placeholder is rescaled. A page reload — or any subsequent mouse-wheel zoom — brings them back into alignment. ## Root cause `WhiteboardCanvas.setZoom(newScale)` (in `canvas.js`) calls `stage.scale({...})` and `stage.position({...})` programmatically. Neither call fires Konva's `wheel` event, which is the only zoom hook `webframe.js` installs: ```js stage.on('wheel.wf_<id>', function() { hideOverlay; setTimeout(showOverlay, 150) }); ``` Every programmatic zoom path therefore bypasses the overlay refresh: - Toolbar Zoom In / Out / 100% buttons (`app.js:621/626/636`) - Keyboard `+` / `-` shortcuts (per CLAUDE.md) - Fit-to-content / minimap navigation (if any) - Presentation `focusFrame` (`frames.js:164`) — already worked around in #102 by hiding all overlays during presentation, but the underlying bug is the same #101 fixed the receiver-side overlay tracking on remote position/size updates, but the local programmatic-zoom path was never wired. ## Suggested fix Smallest reliable fix: at the end of `setZoom` in `canvas.js`, call a new `WhiteboardWebframe.refreshAllOverlays()` helper that walks every registered overlay and re-runs `updateOverlayPosition` (essentially `Object.keys(overlays).forEach(refreshOverlay)`). Add the helper to `webframe.js` next to `hideAllOverlays` / `showAllOverlays`: ```js function refreshAllOverlays() { Object.keys(overlays).forEach(function(id) { refreshOverlay(id); }); } ``` …and export it. Then in `canvas.js::setZoom`, after `saveView()`, call: ```js if (typeof WhiteboardWebframe !== 'undefined' && WhiteboardWebframe.refreshAllOverlays) { WhiteboardWebframe.refreshAllOverlays(); } ``` `refreshOverlay` doesn't toggle `_hiddenForInteraction`, so any overlay that's intentionally hidden (e.g. during a remote drag in flight) stays hidden — only its target screen rect is refreshed for when it's next shown. Optional follow-up: have `setZoom` `stage.fire('zoomchange')` and let webframe.js install a `stage.on('zoomchange.wf_<id>')` listener, decoupling the modules. Out of scope for this issue. ## Acceptance criteria - [ ] Click Zoom In / Zoom Out / 100% — the iframe overlay rescales and repositions to match the placeholder, no flicker, no reload needed. - [ ] Keyboard `+` / `-` zoom keeps the overlay tracked. - [ ] Mouse-wheel zoom (existing path) continues to work as before — overlay hidden during gesture, restored after idle. - [ ] Presentation mode: webframes inside the focused frame remain correctly placed across zoom changes (composes with #102's hide-non-members behaviour). - [ ] No regression to drag, resize, rotation, sync, delete, or remote-update paths. - [ ] No new console errors.
Author
Member

Spec — Issue #106: Programmatic zoom must refresh webframe iframe overlays

Objective

Make every programmatic zoom path (WhiteboardCanvas.setZoom) update the webframe iframe DOM overlay so it tracks the new stage transform, just like wheel zoom does today.

Root cause

canvas.js::setZoom(newScale) calls stage.scale(...) and stage.position(...) directly — neither fires Konva's wheel event, which is the only zoom hook installed by webframe.js. Every non-wheel zoom path (toolbar buttons, keyboard +/-, presentation focusFrame, etc.) therefore leaves overlays at their old screen rect.

Fix — add refreshAllOverlays() and call it from setZoom

Smallest reliable approach. webframe.js already exposes a per-id refreshOverlay(id) that calls updateOverlayPosition without toggling _hiddenForInteraction. Add a sibling that walks all overlays.

Files to Modify

  • crates/hero_whiteboard_ui/static/web/js/whiteboard/webframe.js — add refreshAllOverlays() helper and export it.
  • crates/hero_whiteboard_ui/static/web/js/whiteboard/canvas.js — call it at the end of setZoom.

Step-by-Step Plan

Step 1 — Add refreshAllOverlays to webframe.js

Next to hideAllOverlays / showAllOverlays:

function refreshAllOverlays() {
    Object.keys(overlays).forEach(function(id) { refreshOverlay(id); });
}

Add refreshAllOverlays: refreshAllOverlays, to the public API export block.

Step 2 — Call it from canvas.js::setZoom

After saveView() at the end of setZoom:

if (typeof WhiteboardWebframe !== 'undefined' && WhiteboardWebframe.refreshAllOverlays) {
    WhiteboardWebframe.refreshAllOverlays();
}

Acceptance Criteria

  • Toolbar Zoom In / Out / 100% buttons keep the iframe overlay tracked with the placeholder.
  • Keyboard + / - zoom keeps overlays tracked.
  • Mouse-wheel zoom (pre-existing path) continues to work as before.
  • Presentation focusFrame still works correctly — overlays inside the focused frame land at the new viewport coordinates (composes with #102).
  • No regression to drag, resize, rotation, sync, delete, or remote-update paths.

Notes

  • refreshOverlay does not toggle _hiddenForInteraction, so an overlay that's intentionally hidden (drag in flight, presentation hide) stays hidden — only its underlying screen rect is refreshed for when it's next shown.
  • Decoupling via stage.fire('zoomchange') would be cleaner long-term but is out of scope for this fix.
# Spec — Issue #106: Programmatic zoom must refresh webframe iframe overlays ## Objective Make every programmatic zoom path (`WhiteboardCanvas.setZoom`) update the webframe iframe DOM overlay so it tracks the new stage transform, just like wheel zoom does today. ## Root cause `canvas.js::setZoom(newScale)` calls `stage.scale(...)` and `stage.position(...)` directly — neither fires Konva's `wheel` event, which is the only zoom hook installed by `webframe.js`. Every non-wheel zoom path (toolbar buttons, keyboard `+`/`-`, presentation `focusFrame`, etc.) therefore leaves overlays at their old screen rect. ## Fix — add `refreshAllOverlays()` and call it from `setZoom` Smallest reliable approach. `webframe.js` already exposes a per-id `refreshOverlay(id)` that calls `updateOverlayPosition` without toggling `_hiddenForInteraction`. Add a sibling that walks all overlays. ## Files to Modify - `crates/hero_whiteboard_ui/static/web/js/whiteboard/webframe.js` — add `refreshAllOverlays()` helper and export it. - `crates/hero_whiteboard_ui/static/web/js/whiteboard/canvas.js` — call it at the end of `setZoom`. ## Step-by-Step Plan ### Step 1 — Add `refreshAllOverlays` to `webframe.js` Next to `hideAllOverlays` / `showAllOverlays`: ```js function refreshAllOverlays() { Object.keys(overlays).forEach(function(id) { refreshOverlay(id); }); } ``` Add `refreshAllOverlays: refreshAllOverlays,` to the public API export block. ### Step 2 — Call it from `canvas.js::setZoom` After `saveView()` at the end of `setZoom`: ```js if (typeof WhiteboardWebframe !== 'undefined' && WhiteboardWebframe.refreshAllOverlays) { WhiteboardWebframe.refreshAllOverlays(); } ``` ## Acceptance Criteria - [ ] Toolbar Zoom In / Out / 100% buttons keep the iframe overlay tracked with the placeholder. - [ ] Keyboard `+` / `-` zoom keeps overlays tracked. - [ ] Mouse-wheel zoom (pre-existing path) continues to work as before. - [ ] Presentation `focusFrame` still works correctly — overlays inside the focused frame land at the new viewport coordinates (composes with #102). - [ ] No regression to drag, resize, rotation, sync, delete, or remote-update paths. ## Notes - `refreshOverlay` does not toggle `_hiddenForInteraction`, so an overlay that's intentionally hidden (drag in flight, presentation hide) stays hidden — only its underlying screen rect is refreshed for when it's next shown. - Decoupling via `stage.fire('zoomchange')` would be cleaner long-term but is out of scope for this fix.
Author
Member

Test Results

  • cargo fmt --all -- --check: pass
  • cargo check --workspace: pass
  • node --check webframe.js: pass
  • node --check canvas.js: pass
## Test Results - cargo fmt --all -- --check: pass - cargo check --workspace: pass - node --check webframe.js: pass - node --check canvas.js: pass
Author
Member

Implementation Summary

Two files changed, +8/-0.

crates/hero_whiteboard_ui/static/web/js/whiteboard/webframe.js

  • Added refreshAllOverlays(): walks every registered overlay id and calls the existing per-id refreshOverlay(id). Like refreshOverlay, it does not toggle _hiddenForInteraction, so overlays that are intentionally hidden (drag in flight, presentation hide) stay hidden — only their underlying screen rect is refreshed for when they're next shown.
  • Exported it on the public API.

crates/hero_whiteboard_ui/static/web/js/whiteboard/canvas.js

  • At the end of setZoom, after saveView():
    if (typeof WhiteboardWebframe !== 'undefined' && WhiteboardWebframe.refreshAllOverlays) {
        WhiteboardWebframe.refreshAllOverlays();
    }
    
    Covers every programmatic zoom path (toolbar buttons, keyboard + / -, presentation focusFrame, fit-to-content, minimap navigation).

Verification

  • cargo fmt --all -- --check: clean
  • cargo check --workspace: clean
  • node --check webframe.js: clean
  • node --check canvas.js: clean

Manual smoke test

  1. Place a webframe and let the iframe load.
  2. Click toolbar Zoom Out / Zoom In / 100% — the iframe overlay rescales and repositions to match the placeholder, no flicker, no reload.
  3. Press + / - keyboard shortcuts — same.
  4. Mouse-wheel zoom (pre-existing path) still works.
  5. Enter presentation mode — webframes inside the focused frame land at the new viewport coordinates (composes with #102's hide-non-members behaviour).

Notes

  • refreshOverlay is a no-op for ids without a registered overlay, so calling refreshAllOverlays on a board with zero webframes is free.
  • Decoupling via a custom stage.fire('zoomchange') event would be cleaner long-term but is out of scope for this fix.
## Implementation Summary Two files changed, +8/-0. ### `crates/hero_whiteboard_ui/static/web/js/whiteboard/webframe.js` - Added `refreshAllOverlays()`: walks every registered overlay id and calls the existing per-id `refreshOverlay(id)`. Like `refreshOverlay`, it does not toggle `_hiddenForInteraction`, so overlays that are intentionally hidden (drag in flight, presentation hide) stay hidden — only their underlying screen rect is refreshed for when they're next shown. - Exported it on the public API. ### `crates/hero_whiteboard_ui/static/web/js/whiteboard/canvas.js` - At the end of `setZoom`, after `saveView()`: ```js if (typeof WhiteboardWebframe !== 'undefined' && WhiteboardWebframe.refreshAllOverlays) { WhiteboardWebframe.refreshAllOverlays(); } ``` Covers every programmatic zoom path (toolbar buttons, keyboard `+` / `-`, presentation `focusFrame`, fit-to-content, minimap navigation). ### Verification - `cargo fmt --all -- --check`: clean - `cargo check --workspace`: clean - `node --check webframe.js`: clean - `node --check canvas.js`: clean ### Manual smoke test 1. Place a webframe and let the iframe load. 2. Click toolbar Zoom Out / Zoom In / 100% — the iframe overlay rescales and repositions to match the placeholder, no flicker, no reload. 3. Press `+` / `-` keyboard shortcuts — same. 4. Mouse-wheel zoom (pre-existing path) still works. 5. Enter presentation mode — webframes inside the focused frame land at the new viewport coordinates (composes with #102's hide-non-members behaviour). ### Notes - `refreshOverlay` is a no-op for ids without a registered overlay, so calling `refreshAllOverlays` on a board with zero webframes is free. - Decoupling via a custom `stage.fire('zoomchange')` event would be cleaner long-term but is out of scope for this fix.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lhumina_code/hero_whiteboard#106
No description provided.