Web frame outside the focused frame leaks into presentation mode #102

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

Summary

Starting presentation mode while a webframe exists OUTSIDE all frames leaves the iframe DOM overlay parked on top of the presented slide instead of being hidden / re-positioned. The presented slide ends up with an unrelated live website rendered over its content even though the webframe is not a member of the focused frame.

Steps to reproduce

  1. Open a board.
  2. Place a frame somewhere on the canvas.
  3. Place a webframe (iframe-backed object) anywhere OUTSIDE that frame, with a clearly visible loaded URL.
  4. Briefly look at the webframe so the iframe DOM overlay is shown (not hidden).
  5. Start presentation mode (focus the first frame).

Expected

In presentation mode, only the focused frame's contents are visible inside the spotlight. Objects outside the frame (including webframes) are masked by the dark backdrop, the same way other objects are.

Actual

The iframe overlay stays rendered at its pre-presentation screen coordinates — which, after the stage zoom/pan that focusFrame applies, often happen to land inside the spotlight area. The result is a live website appearing inside the slide, on top of whatever the slide's actual contents are.

Root cause (likely)

Two compounding issues in webframe.js:

  1. Stage transforms during presentation are not propagated to the iframe overlay. WhiteboardFrames.focusFrame calls WhiteboardCanvas.setZoom(...) and stage.position({ x, y }) programmatically. The webframe's only position-update hooks listen for stage.on('dragstart' | 'dragend' | 'wheel'), which don't fire for programmatic transform changes. So the overlay stays at its old screen position while the canvas viewport has moved underneath it.

  2. The presentation spotlight does not hide non-member webframe overlays. The #pres-spotlight element uses box-shadow: 0 0 0 9999px rgba(0,0,0,0.92) to dim everything outside the focused frame's screen rect. That CSS shadow paints over the Konva canvas (z-index lower than 9999) but the iframe overlay has its own stacking context — depending on z-index, the box-shadow may not visually mask it. The cleanest behaviour is to actively hide every iframe overlay whose underlying Konva object is not a child of the focused frame for the duration of the presentation.

Suggested fix

  • In WhiteboardFrames.startPresentation / focusFrame, iterate the webframe overlays and hide every overlay whose owning Konva group is not contained in the currently-focused frame's bounds (or use the new explicit parent_frame_id from issue #96 if applicable).
  • In WhiteboardFrames.stopPresentation, restore visibility of all overlays.
  • Independently: also call WhiteboardWebframe.updateOverlayPosition(id, group) after every programmatic stage transform (focusFrame, setZoom, …) so any webframe inside the focused frame stays in sync with the new viewport. This shares the same fix surface with the related drag-desync bug.

Acceptance criteria

  • During presentation, no webframe outside the focused frame is visible inside the spotlight.
  • Webframes inside the focused frame remain visible and at the correct screen position.
  • Stopping presentation restores every overlay (including ones that were hidden) at its correct screen position.
  • Pan/zoom while presenting keeps each visible overlay aligned with its Konva group.
  • No regression on the normal (non-presentation) zoom/pan path.
## Summary Starting presentation mode while a webframe exists OUTSIDE all frames leaves the iframe DOM overlay parked on top of the presented slide instead of being hidden / re-positioned. The presented slide ends up with an unrelated live website rendered over its content even though the webframe is not a member of the focused frame. ## Steps to reproduce 1. Open a board. 2. Place a frame somewhere on the canvas. 3. Place a webframe (iframe-backed object) anywhere OUTSIDE that frame, with a clearly visible loaded URL. 4. Briefly look at the webframe so the iframe DOM overlay is shown (not hidden). 5. Start presentation mode (focus the first frame). ## Expected In presentation mode, only the focused frame's contents are visible inside the spotlight. Objects outside the frame (including webframes) are masked by the dark backdrop, the same way other objects are. ## Actual The iframe overlay stays rendered at its pre-presentation screen coordinates — which, after the stage zoom/pan that focusFrame applies, often happen to land inside the spotlight area. The result is a live website appearing inside the slide, on top of whatever the slide's actual contents are. ## Root cause (likely) Two compounding issues in `webframe.js`: 1. **Stage transforms during presentation are not propagated to the iframe overlay.** `WhiteboardFrames.focusFrame` calls `WhiteboardCanvas.setZoom(...)` and `stage.position({ x, y })` programmatically. The webframe's only position-update hooks listen for `stage.on('dragstart' | 'dragend' | 'wheel')`, which don't fire for programmatic transform changes. So the overlay stays at its old screen position while the canvas viewport has moved underneath it. 2. **The presentation spotlight does not hide non-member webframe overlays.** The `#pres-spotlight` element uses `box-shadow: 0 0 0 9999px rgba(0,0,0,0.92)` to dim everything outside the focused frame's screen rect. That CSS shadow paints over the Konva canvas (z-index lower than 9999) but the iframe overlay has its own stacking context — depending on z-index, the box-shadow may not visually mask it. The cleanest behaviour is to actively hide every iframe overlay whose underlying Konva object is not a child of the focused frame for the duration of the presentation. ## Suggested fix - In `WhiteboardFrames.startPresentation` / `focusFrame`, iterate the webframe overlays and hide every overlay whose owning Konva group is not contained in the currently-focused frame's bounds (or use the new explicit `parent_frame_id` from issue #96 if applicable). - In `WhiteboardFrames.stopPresentation`, restore visibility of all overlays. - Independently: also call `WhiteboardWebframe.updateOverlayPosition(id, group)` after every programmatic stage transform (`focusFrame`, `setZoom`, …) so any webframe inside the focused frame stays in sync with the new viewport. This shares the same fix surface with the related drag-desync bug. ## Acceptance criteria - [ ] During presentation, no webframe outside the focused frame is visible inside the spotlight. - [ ] Webframes inside the focused frame remain visible and at the correct screen position. - [ ] Stopping presentation restores every overlay (including ones that were hidden) at its correct screen position. - [ ] Pan/zoom while presenting keeps each visible overlay aligned with its Konva group. - [ ] No regression on the normal (non-presentation) zoom/pan path.
Author
Member

Spec — Issue #102: Web frame outside the focused frame leaks into presentation mode

Objective

During presentation mode, the iframe DOM overlay attached to a webframe object that lives outside the focused frame must not be visible inside the spotlight. The webframe should be masked exactly like every other non-focused-slide object on the canvas.

Root Cause

  1. WhiteboardFrames.focusFrame reframes the stage by calling WhiteboardCanvas.setZoom(...) and stage.position({...}) — both programmatic mutations that do NOT fire Konva's dragstart/dragend/wheel events.
  2. WhiteboardWebframe.createWebframe only refreshes the iframe overlay's screen position in response to those three stage events. So when presentation reframes the canvas, every webframe's <div id="wf-overlay-*"> keeps its pre-presentation coordinates.
  3. The spotlight is built from box-shadow: 0 0 0 9999px rgba(0,0,0,0.92) painted over #whiteboard-container. The iframe overlay div has its own stacking context (z-index: 10) and lives outside the Konva paint surface, so the box-shadow mask cannot dim it. The existing body.wb-presenting #whiteboard-container * rule disables clicks but not visibility.

Net effect: the iframe stays where the user last saw it on screen, fully visible on top of (or beside) the spotlight.

Hide ALL webframe overlays for the entire duration of presentation mode and restore them on exit. The presentation lifecycle in frames.js owns the overlay-visibility behaviour. webframe.js already exports hideAllOverlays and showAllOverlays — no new helper needed.

Trade-off: webframes that happen to be inside the focused slide will appear as their static Konva placeholder (dark rect with header + URL label) rather than as a live iframe. Per the user's bug report this is acceptable; Option B (per-slide containment test) is deferred as a follow-up if/when live web content during presentation becomes a requirement.

Requirements

  • On startPresentation, every webframe iframe overlay DOM element is hidden before the first focusFrame runs.
  • On stopPresentation, every webframe iframe overlay is repositioned and shown again, matching the restored zoom/pan.
  • nextFrame / prevFrame need no extra plumbing — overlays remain hidden for the whole presentation; only entry and exit toggle visibility.
  • No change to RPC, sync, persistence, DOM structure of the overlays, or CSS.
  • No polling loop, no new transports.

Files to Modify

  • crates/hero_whiteboard_ui/static/web/js/whiteboard/frames.js — only file touched.

Files to Leave Alone

  • crates/hero_whiteboard_ui/static/web/js/whiteboard/webframe.jshideAllOverlays / showAllOverlays are already exported and do exactly what we need.
  • crates/hero_whiteboard_ui/templates/web/board.html — no CSS or markup changes.
  • All other modules — out of scope.

Step-by-Step Plan

Step 1 — Hide all webframe overlays at presentation start

File: frames.js, function startPresentation.

After document.body.classList.add('wb-presenting') and BEFORE focusFrame(frames[0]), add:

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

Hiding before focusFrame ensures the iframe never paints at the new (now-incorrect) coordinates between the zoom change and the restore.

Step 2 — Restore all webframe overlays at presentation end

File: frames.js, function stopPresentation.

After WhiteboardCanvas.drawGrid() (which finishes restoring the previous view) and BEFORE clearing previousView, add:

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

showAllOverlays walks every overlay id and calls showOverlay(id, group)updateOverlayPosition(id, group), which reads the current stage.scaleX() and stage.position() — which we just restored to previousView. Overlays land at the correct restored screen coordinates without any extra arithmetic.

Step 3 — No changes for nextFrame / prevFrame

Overlays are hidden for the whole presentation, so frame-to-frame navigation needs no extra hide/show cycle.

Acceptance Criteria

  • Webframe placed outside a frame is not visible during presentation mode (the user's bug).
  • Webframe placed inside the focused frame is also not visible as a live iframe; the audience sees the Konva placeholder (background + header + URL label). Trade-off accepted under Option A.
  • Exiting presentation restores every webframe overlay to its correct on-canvas position with no flicker, no leftover hidden state.
  • No regressions to: drag, pan, wheel-zoom, URL editing modal, create/destroy, remapOverlay (temp → server id), and the #101 refreshOverlay path.
  • No console errors when presentation is started on a board with zero webframes.

Notes / Caveats

  • Option B (per-slide membership test) is intentionally deferred. To enable it later we would extend the webframe registry entry to track parent_frame_id the same way other object types do post-#96, then iterate webframes in focusFrame and selectively hide/show + refresh based on whether their parent_frame_id matches the focused frame's id. Separate, larger change.
  • Do NOT add CSS like body.wb-presenting [id^="wf-overlay-"] { visibility: hidden }. It would visually mask the overlay but leave the iframe loaded and laid out at stale coordinates, papering over the underlying transform-tracking issue.
  • Do NOT add stage-level event hooks for programmatic zoom/pan. The same setZoom / stage.position shape is also used by minimap navigation and "fit to content"; making them fire synthetic dragend events would have non-local consequences.
  • Purely client-side; no server, RPC, or persistence changes.
# Spec — Issue #102: Web frame outside the focused frame leaks into presentation mode ## Objective During presentation mode, the iframe DOM overlay attached to a webframe object that lives outside the focused frame must not be visible inside the spotlight. The webframe should be masked exactly like every other non-focused-slide object on the canvas. ## Root Cause 1. `WhiteboardFrames.focusFrame` reframes the stage by calling `WhiteboardCanvas.setZoom(...)` and `stage.position({...})` — both programmatic mutations that do NOT fire Konva's `dragstart`/`dragend`/`wheel` events. 2. `WhiteboardWebframe.createWebframe` only refreshes the iframe overlay's screen position in response to those three stage events. So when presentation reframes the canvas, every webframe's `<div id="wf-overlay-*">` keeps its pre-presentation coordinates. 3. The spotlight is built from `box-shadow: 0 0 0 9999px rgba(0,0,0,0.92)` painted over `#whiteboard-container`. The iframe overlay div has its own stacking context (`z-index: 10`) and lives outside the Konva paint surface, so the box-shadow mask cannot dim it. The existing `body.wb-presenting #whiteboard-container *` rule disables clicks but not visibility. Net effect: the iframe stays where the user last saw it on screen, fully visible on top of (or beside) the spotlight. ## Recommended Fix — Option A (smallest, lowest risk) Hide ALL webframe overlays for the entire duration of presentation mode and restore them on exit. The presentation lifecycle in `frames.js` owns the overlay-visibility behaviour. `webframe.js` already exports `hideAllOverlays` and `showAllOverlays` — no new helper needed. **Trade-off:** webframes that happen to be inside the focused slide will appear as their static Konva placeholder (dark rect with header + URL label) rather than as a live iframe. Per the user's bug report this is acceptable; **Option B (per-slide containment test)** is deferred as a follow-up if/when live web content during presentation becomes a requirement. ## Requirements - On `startPresentation`, every webframe iframe overlay DOM element is hidden before the first `focusFrame` runs. - On `stopPresentation`, every webframe iframe overlay is repositioned and shown again, matching the restored zoom/pan. - `nextFrame` / `prevFrame` need no extra plumbing — overlays remain hidden for the whole presentation; only entry and exit toggle visibility. - No change to RPC, sync, persistence, DOM structure of the overlays, or CSS. - No polling loop, no new transports. ## Files to Modify - `crates/hero_whiteboard_ui/static/web/js/whiteboard/frames.js` — only file touched. ## Files to Leave Alone - `crates/hero_whiteboard_ui/static/web/js/whiteboard/webframe.js` — `hideAllOverlays` / `showAllOverlays` are already exported and do exactly what we need. - `crates/hero_whiteboard_ui/templates/web/board.html` — no CSS or markup changes. - All other modules — out of scope. ## Step-by-Step Plan ### Step 1 — Hide all webframe overlays at presentation start File: `frames.js`, function `startPresentation`. After `document.body.classList.add('wb-presenting')` and BEFORE `focusFrame(frames[0])`, add: ```js if (typeof WhiteboardWebframe !== 'undefined' && WhiteboardWebframe.hideAllOverlays) { WhiteboardWebframe.hideAllOverlays(); } ``` Hiding before `focusFrame` ensures the iframe never paints at the new (now-incorrect) coordinates between the zoom change and the restore. ### Step 2 — Restore all webframe overlays at presentation end File: `frames.js`, function `stopPresentation`. After `WhiteboardCanvas.drawGrid()` (which finishes restoring the previous view) and BEFORE clearing `previousView`, add: ```js if (typeof WhiteboardWebframe !== 'undefined' && WhiteboardWebframe.showAllOverlays) { WhiteboardWebframe.showAllOverlays(); } ``` `showAllOverlays` walks every overlay id and calls `showOverlay(id, group)` → `updateOverlayPosition(id, group)`, which reads the current `stage.scaleX()` and `stage.position()` — which we just restored to `previousView`. Overlays land at the correct restored screen coordinates without any extra arithmetic. ### Step 3 — No changes for `nextFrame` / `prevFrame` Overlays are hidden for the whole presentation, so frame-to-frame navigation needs no extra hide/show cycle. ## Acceptance Criteria - [ ] Webframe placed outside a frame is not visible during presentation mode (the user's bug). - [ ] Webframe placed inside the focused frame is also not visible as a live iframe; the audience sees the Konva placeholder (background + header + URL label). Trade-off accepted under Option A. - [ ] Exiting presentation restores every webframe overlay to its correct on-canvas position with no flicker, no leftover hidden state. - [ ] No regressions to: drag, pan, wheel-zoom, URL editing modal, create/destroy, remapOverlay (temp → server id), and the #101 `refreshOverlay` path. - [ ] No console errors when presentation is started on a board with zero webframes. ## Notes / Caveats - **Option B (per-slide membership test) is intentionally deferred.** To enable it later we would extend the webframe registry entry to track `parent_frame_id` the same way other object types do post-#96, then iterate webframes in `focusFrame` and selectively hide/show + refresh based on whether their `parent_frame_id` matches the focused frame's id. Separate, larger change. - Do NOT add CSS like `body.wb-presenting [id^="wf-overlay-"] { visibility: hidden }`. It would visually mask the overlay but leave the iframe loaded and laid out at stale coordinates, papering over the underlying transform-tracking issue. - Do NOT add stage-level event hooks for programmatic zoom/pan. The same `setZoom` / `stage.position` shape is also used by minimap navigation and "fit to content"; making them fire synthetic `dragend` events would have non-local consequences. - Purely client-side; no server, RPC, or persistence changes.
Author
Member

Test Results

  • cargo fmt --all -- --check: pass
  • cargo check --workspace: pass
  • cargo clippy --workspace -- -D warnings: pass
  • cargo test --workspace: pass
  • node --check frames.js: pass
## Test Results - cargo fmt --all -- --check: pass - cargo check --workspace: pass - cargo clippy --workspace -- -D warnings: pass - cargo test --workspace: pass - node --check frames.js: pass
Author
Member

Implementation Summary

One file changed (frames.js), +33/-0. Option B chosen: webframes inside the focused slide stay live; webframes outside are hidden for the duration of presentation mode.

Root cause

WhiteboardFrames.focusFrame reframes the stage with WhiteboardCanvas.setZoom(...) and stage.position({...}) — both programmatic mutations. Webframe's iframe-overlay listeners only react to stage.dragstart/dragend/wheel, so they never repositioned the iframe during presentation. The presentation spotlight's box-shadow mask paints over the Konva canvas but the iframe overlay has its own stacking context (z-index: 10), so the dimming doesn't reach it.

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

  • Added _applyWebframePresentationVisibility(frame) private helper. It iterates WhiteboardObjects.getAllObjects(), picks entries with type === 'webframe', computes the webframe group's canvas-coord box from its .bg rect, and tests fully-inside containment against the focused frame's .bg rect (canvas coords).
    • Inside the slide: WhiteboardWebframe.showOverlay(id, group) then refreshOverlay(id) (refresh runs after the new stage transform is applied, so the iframe lands at the correct screen position).
    • Outside: WhiteboardWebframe.hideOverlay(id).
  • focusFrame: at the end, when presentationMode is true, calls _applyWebframePresentationVisibility(frame). Covers startPresentation (which sets presentationMode = true then calls focusFrame(frames[0])) and the nextFrame / prevFrame paths.
  • stopPresentation: after restoring the previous zoom/pan, calls WhiteboardWebframe.showAllOverlays() which re-shows + repositions every overlay against the restored stage transform.

Verification

  • cargo fmt --all -- --check: clean
  • cargo check --workspace: clean
  • cargo clippy --workspace -- -D warnings: clean
  • cargo test --workspace: pass
  • node --check frames.js: clean

Manual smoke test

  1. Place a frame on the canvas. Place webframe A outside the frame, webframe B fully inside the frame.
  2. Start presentation:
    • A's iframe overlay must disappear.
    • B's iframe overlay must reposition to match the new zoomed/panned stage and remain visible inside the spotlight.
  3. Press Next/Prev between multiple frames. Webframes that are inside the newly-focused frame become visible at the new viewport coordinates; previously-visible ones outside the new frame disappear.
  4. Stop presentation. Every webframe overlay reappears at its correct on-canvas screen coordinates.
  5. Re-enter / exit several times to confirm idempotency.

Notes / scope

  • Containment is geometric (canvas coords): webframe's bounding box must be fully inside the focused frame's bg. Partial overlaps are treated as "outside" and hidden — they would visually clip against the spotlight edge anyway.
  • This does not depend on parent_frame_id from issue #96, so it works for webframes that pre-date that change. If the team later wants the explicit-membership semantics from #96 to govern presentation visibility too, swap the geometric test for a parent_frame_id === Number(frame.id()) check (and extend the webframe registry to track parent_frame_id, which is currently a follow-up).
  • During presentation, refreshing the overlay reads the current stage transform via stage.scaleX() / stage.position(), which are exactly what focusFrame just set. No extra arithmetic needed.
  • Pairs cleanly with #101's refreshOverlay call — both fixes share the same overlay-tracking infrastructure.
## Implementation Summary One file changed (`frames.js`), +33/-0. **Option B** chosen: webframes inside the focused slide stay live; webframes outside are hidden for the duration of presentation mode. ### Root cause `WhiteboardFrames.focusFrame` reframes the stage with `WhiteboardCanvas.setZoom(...)` and `stage.position({...})` — both programmatic mutations. Webframe's iframe-overlay listeners only react to `stage.dragstart/dragend/wheel`, so they never repositioned the iframe during presentation. The presentation spotlight's `box-shadow` mask paints over the Konva canvas but the iframe overlay has its own stacking context (`z-index: 10`), so the dimming doesn't reach it. ### `crates/hero_whiteboard_ui/static/web/js/whiteboard/frames.js` - Added `_applyWebframePresentationVisibility(frame)` private helper. It iterates `WhiteboardObjects.getAllObjects()`, picks entries with `type === 'webframe'`, computes the webframe group's canvas-coord box from its `.bg` rect, and tests fully-inside containment against the focused frame's `.bg` rect (canvas coords). - Inside the slide: `WhiteboardWebframe.showOverlay(id, group)` then `refreshOverlay(id)` (refresh runs after the new stage transform is applied, so the iframe lands at the correct screen position). - Outside: `WhiteboardWebframe.hideOverlay(id)`. - `focusFrame`: at the end, when `presentationMode` is true, calls `_applyWebframePresentationVisibility(frame)`. Covers `startPresentation` (which sets `presentationMode = true` then calls `focusFrame(frames[0])`) and the `nextFrame` / `prevFrame` paths. - `stopPresentation`: after restoring the previous zoom/pan, calls `WhiteboardWebframe.showAllOverlays()` which re-shows + repositions every overlay against the restored stage transform. ### Verification - `cargo fmt --all -- --check`: clean - `cargo check --workspace`: clean - `cargo clippy --workspace -- -D warnings`: clean - `cargo test --workspace`: pass - `node --check frames.js`: clean ### Manual smoke test 1. Place a frame on the canvas. Place webframe A outside the frame, webframe B fully inside the frame. 2. Start presentation: - A's iframe overlay must disappear. - B's iframe overlay must reposition to match the new zoomed/panned stage and remain visible inside the spotlight. 3. Press Next/Prev between multiple frames. Webframes that are inside the newly-focused frame become visible at the new viewport coordinates; previously-visible ones outside the new frame disappear. 4. Stop presentation. Every webframe overlay reappears at its correct on-canvas screen coordinates. 5. Re-enter / exit several times to confirm idempotency. ### Notes / scope - Containment is geometric (canvas coords): webframe's bounding box must be fully inside the focused frame's bg. Partial overlaps are treated as "outside" and hidden — they would visually clip against the spotlight edge anyway. - This does not depend on `parent_frame_id` from issue #96, so it works for webframes that pre-date that change. If the team later wants the explicit-membership semantics from #96 to govern presentation visibility too, swap the geometric test for a `parent_frame_id === Number(frame.id())` check (and extend the webframe registry to track `parent_frame_id`, which is currently a follow-up). - During presentation, refreshing the overlay reads the current stage transform via `stage.scaleX()` / `stage.position()`, which are exactly what `focusFrame` just set. No extra arithmetic needed. - Pairs cleanly with #101's `refreshOverlay` call — both fixes share the same overlay-tracking infrastructure.
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#102
No description provided.