Whiteboard: frame body is not interactive + slide-reorder buttons always look enabled #192

Open
opened 2026-05-14 12:50:54 +00:00 by AhmedHanafy725 · 2 comments
Member

Two related frame-UX bugs.

A. Frame body is not click/dblclick-targetable

The frame Konva group contains a Rect (dashed border, name: "bg") and a Text label, both inside a draggable: true group (static/web/js/whiteboard/objects.js:1108-1224). The bg Rect has only a stroke — no fill — so the EMPTY interior of a frame isnt hit-testable. Two visible consequences:

  • Dragging a frame requires grabbing the thin dashed border or the title label; clicking inside the empty body does nothing.
  • Double-clicking inside the body to rename does nothing. Only label.on("dblclick dbltap", …) (line 1211) is wired — sticky / text / shape allow dblclick anywhere on the objects body to enter text edit, so the affordance is inconsistent.

Children inside the frame (those whose parent_frame_id points at it) sit on the same Konva layer above the frame and grab their own hits, so adding hit-testing to the bg only triggers on TRULY empty interior space — no regression to child interaction.

B. "Move slide up/down" always appears enabled

The selection toolbar adds two chevron buttons (static/web/js/whiteboard/selection_toolbar.js:1215-1224) and the right-click menu adds two items (static/web/js/whiteboard/contextmenu.js:80-82) unconditionally for every frame, even when the frame is the first / last / only frame in the deck. Clicking them at the edge is a no-op (the moveFrameUp / moveFrameDown early-exit in frames.js:50-64 checks idx <= 0 / idx >= length-1) but the UI gives no signal — buttons look enabled, menu items look clickable.

Approach

A. Hit-test the frame body

  • Add fill: "rgba(0,0,0,0)" to the frames bg Rect at creation. Konva treats fully-transparent fills as hit-testable but invisible — the interior becomes clickable / draggable.
  • Add a dblclick dbltap handler on the bg that calls the same editText(group, label, null) already used by the label.

B. Edge-aware enable state

  • In _renderFrame (selection_toolbar.js), compute the current frames position via WhiteboardFrames.getFrames() and call _buildIconBtn(...).disabled = isFirst / isLast. If theres only one frame, both are disabled.
  • In contextmenu.js, omit the menu items when at the edge (or when theres only one frame). Right-click menus shouldnt carry disabled items — just dont insert them.

Acceptance

  • Click an empty area inside a frame → frame is selected; click-and-drag → frame moves.
  • Double-click empty frame body → title editor opens (same as double-clicking the label).
  • Click a child object inside the frame → child is selected (NOT the frame).
  • Selection toolbar Move-slide-up / Move-slide-down disabled (greyed) when frame is first / last.
  • Right-click on a frame at the top of the deck → no "Move slide up" item; bottom → no "Move slide down". Single-frame board → neither item appears.
  • No regression to existing label dblclick, drag, snap guides, or frame children dragging-with-the-frame.
Two related frame-UX bugs. ## A. Frame body is not click/dblclick-targetable The frame Konva group contains a Rect (dashed border, `name: "bg"`) and a Text label, both inside a `draggable: true` group (`static/web/js/whiteboard/objects.js:1108-1224`). The bg Rect has only a stroke — no fill — so the EMPTY interior of a frame isnt hit-testable. Two visible consequences: - Dragging a frame requires grabbing the thin dashed border or the title label; clicking inside the empty body does nothing. - Double-clicking inside the body to rename does nothing. Only `label.on("dblclick dbltap", …)` (line 1211) is wired — sticky / text / shape allow dblclick anywhere on the objects body to enter text edit, so the affordance is inconsistent. Children inside the frame (those whose `parent_frame_id` points at it) sit on the same Konva layer above the frame and grab their own hits, so adding hit-testing to the bg only triggers on TRULY empty interior space — no regression to child interaction. ## B. "Move slide up/down" always appears enabled The selection toolbar adds two chevron buttons (`static/web/js/whiteboard/selection_toolbar.js:1215-1224`) and the right-click menu adds two items (`static/web/js/whiteboard/contextmenu.js:80-82`) unconditionally for every frame, even when the frame is the first / last / only frame in the deck. Clicking them at the edge is a no-op (the `moveFrameUp` / `moveFrameDown` early-exit in `frames.js:50-64` checks `idx <= 0` / `idx >= length-1`) but the UI gives no signal — buttons look enabled, menu items look clickable. ## Approach ### A. Hit-test the frame body - Add `fill: "rgba(0,0,0,0)"` to the frames bg Rect at creation. Konva treats fully-transparent fills as hit-testable but invisible — the interior becomes clickable / draggable. - Add a `dblclick dbltap` handler on the bg that calls the same `editText(group, label, null)` already used by the label. ### B. Edge-aware enable state - In `_renderFrame` (`selection_toolbar.js`), compute the current frames position via `WhiteboardFrames.getFrames()` and call `_buildIconBtn(...).disabled = isFirst` / `isLast`. If theres only one frame, both are disabled. - In `contextmenu.js`, omit the menu items when at the edge (or when theres only one frame). Right-click menus shouldnt carry disabled items — just dont insert them. ## Acceptance - [ ] Click an empty area inside a frame → frame is selected; click-and-drag → frame moves. - [ ] Double-click empty frame body → title editor opens (same as double-clicking the label). - [ ] Click a child object inside the frame → child is selected (NOT the frame). - [ ] Selection toolbar Move-slide-up / Move-slide-down disabled (greyed) when frame is first / last. - [ ] Right-click on a frame at the top of the deck → no "Move slide up" item; bottom → no "Move slide down". Single-frame board → neither item appears. - [ ] No regression to existing label dblclick, drag, snap guides, or frame children dragging-with-the-frame.
Author
Member

Implementation Spec for Issue #192

Files to Modify

  • static/web/js/whiteboard/objects.js
  • static/web/js/whiteboard/selection_toolbar.js
  • static/web/js/whiteboard/contextmenu.js

Plan

A. Hit-test the frame body

File: static/web/js/whiteboard/objects.js (~line 1108)

Add a transparent fill on the bg Rect and a dblclick handler that opens the title editor.

var rect = new Konva.Rect({
    width: width, height: height,
    stroke: WhiteboardThemes.get('frame-stroke') || '#495057',
    strokeWidth: 1,
    dash: [8, 4],
    cornerRadius: 8,
    // Transparent fill keeps the interior invisible but hit-testable, so
    // empty frame body can be clicked / dragged / dblclicked. Child
    // objects on the same layer (parent_frame_id pointing here) sit
    // above and continue to grab their own clicks.
    fill: 'rgba(0,0,0,0)',
    name: 'bg',
});

Right after the label dblclick handler (~line 1211-1215), add:

rect.on('dblclick dbltap', function(e) {
    e.cancelBubble = true;
    if (e.evt) e.evt.stopPropagation();
    editText(group, label, null);
});

B. Disable / hide slide-reorder controls at deck edge

Selection toolbar (selection_toolbar.js:1215-1224)

Compute the frame's index in the deck. Disable the chevron when at the edge.

var frames = (typeof WhiteboardFrames !== 'undefined' && WhiteboardFrames.getFrames)
    ? WhiteboardFrames.getFrames()
    : [];
var idx = frames.indexOf(node);
var isFirst = idx <= 0;
var isLast = idx < 0 || idx >= frames.length - 1;

var upBtn = _buildIconBtn('bi bi-arrow-up-square', 'Move slide earlier (in presentation order)', function() {
    if (typeof WhiteboardFrames !== 'undefined' && WhiteboardFrames.moveFrameUp) {
        WhiteboardFrames.moveFrameUp(node);
    }
});
upBtn.disabled = isFirst;
propsEl.appendChild(upBtn);

var downBtn = _buildIconBtn('bi bi-arrow-down-square', 'Move slide later (in presentation order)', function() {
    if (typeof WhiteboardFrames !== 'undefined' && WhiteboardFrames.moveFrameDown) {
        WhiteboardFrames.moveFrameDown(node);
    }
});
downBtn.disabled = isLast;
propsEl.appendChild(downBtn);

The CSS already greys disabled buttons via Bootstrap's default :disabled styling (or whatever the existing rule does). Verify visually after rebuild.

Right-click menu (contextmenu.js:78-83)

Skip the items at the edge:

if (objData && objData.type === 'frame' && typeof WhiteboardFrames !== 'undefined') {
    var frames = WhiteboardFrames.getFrames ? WhiteboardFrames.getFrames() : [];
    var idx = frames.indexOf(targetNode);
    if (idx > 0) {
        items.push({ label: 'Move slide up', icon: 'bi-arrow-up', action: function() { WhiteboardFrames.moveFrameUp(targetNode); } });
    }
    if (idx >= 0 && idx < frames.length - 1) {
        items.push({ label: 'Move slide down', icon: 'bi-arrow-down', action: function() { WhiteboardFrames.moveFrameDown(targetNode); } });
    }
    if (idx > 0 || (idx >= 0 && idx < frames.length - 1)) {
        items.push({ divider: true });
    }
}

Single-frame board: frames.length === 1, idx === 0 → neither item appears, no divider.

Acceptance Criteria

  • Click an empty area inside a frame → frame is selected; click-and-drag moves the frame.
  • Double-click empty frame body → title editor opens.
  • Click a child object inside the frame → child is selected, frame is not.
  • Selection toolbar chevrons disabled when at deck edge; both disabled when only one frame.
  • Right-click on first / last / only frame omits the inapplicable menu items.
  • No regression to label dblclick, drag, snap guides, or child-of-frame drag-with-parent.

Notes

  • Konva treats fill: 'rgba(0,0,0,0)' as hit-testable but invisible. (Compared to fill: null which makes only the stroke hit-testable.) Verified by behavior on shapes elsewhere in the codebase.
  • The transformer's transformstart snapshot path (tools.js) still uses the normal snapshotBefore(node.id()) for frames since they're regular WhiteboardObjects. No change needed there.
  • The disabled selection-toolbar buttons rely on Bootstrap's default :disabled styling. If the visual isn't subtle enough, a small CSS rule in selection_toolbar.css can be added later — out of scope here.
## Implementation Spec for Issue #192 ### Files to Modify - `static/web/js/whiteboard/objects.js` - `static/web/js/whiteboard/selection_toolbar.js` - `static/web/js/whiteboard/contextmenu.js` ### Plan #### A. Hit-test the frame body File: `static/web/js/whiteboard/objects.js` (~line 1108) Add a transparent fill on the bg Rect and a dblclick handler that opens the title editor. ```js var rect = new Konva.Rect({ width: width, height: height, stroke: WhiteboardThemes.get('frame-stroke') || '#495057', strokeWidth: 1, dash: [8, 4], cornerRadius: 8, // Transparent fill keeps the interior invisible but hit-testable, so // empty frame body can be clicked / dragged / dblclicked. Child // objects on the same layer (parent_frame_id pointing here) sit // above and continue to grab their own clicks. fill: 'rgba(0,0,0,0)', name: 'bg', }); ``` Right after the label dblclick handler (~line 1211-1215), add: ```js rect.on('dblclick dbltap', function(e) { e.cancelBubble = true; if (e.evt) e.evt.stopPropagation(); editText(group, label, null); }); ``` #### B. Disable / hide slide-reorder controls at deck edge ##### Selection toolbar (`selection_toolbar.js:1215-1224`) Compute the frame's index in the deck. Disable the chevron when at the edge. ```js var frames = (typeof WhiteboardFrames !== 'undefined' && WhiteboardFrames.getFrames) ? WhiteboardFrames.getFrames() : []; var idx = frames.indexOf(node); var isFirst = idx <= 0; var isLast = idx < 0 || idx >= frames.length - 1; var upBtn = _buildIconBtn('bi bi-arrow-up-square', 'Move slide earlier (in presentation order)', function() { if (typeof WhiteboardFrames !== 'undefined' && WhiteboardFrames.moveFrameUp) { WhiteboardFrames.moveFrameUp(node); } }); upBtn.disabled = isFirst; propsEl.appendChild(upBtn); var downBtn = _buildIconBtn('bi bi-arrow-down-square', 'Move slide later (in presentation order)', function() { if (typeof WhiteboardFrames !== 'undefined' && WhiteboardFrames.moveFrameDown) { WhiteboardFrames.moveFrameDown(node); } }); downBtn.disabled = isLast; propsEl.appendChild(downBtn); ``` The CSS already greys disabled buttons via Bootstrap's default `:disabled` styling (or whatever the existing rule does). Verify visually after rebuild. ##### Right-click menu (`contextmenu.js:78-83`) Skip the items at the edge: ```js if (objData && objData.type === 'frame' && typeof WhiteboardFrames !== 'undefined') { var frames = WhiteboardFrames.getFrames ? WhiteboardFrames.getFrames() : []; var idx = frames.indexOf(targetNode); if (idx > 0) { items.push({ label: 'Move slide up', icon: 'bi-arrow-up', action: function() { WhiteboardFrames.moveFrameUp(targetNode); } }); } if (idx >= 0 && idx < frames.length - 1) { items.push({ label: 'Move slide down', icon: 'bi-arrow-down', action: function() { WhiteboardFrames.moveFrameDown(targetNode); } }); } if (idx > 0 || (idx >= 0 && idx < frames.length - 1)) { items.push({ divider: true }); } } ``` Single-frame board: `frames.length === 1, idx === 0` → neither item appears, no divider. ### Acceptance Criteria - [ ] Click an empty area inside a frame → frame is selected; click-and-drag moves the frame. - [ ] Double-click empty frame body → title editor opens. - [ ] Click a child object inside the frame → child is selected, frame is not. - [ ] Selection toolbar chevrons disabled when at deck edge; both disabled when only one frame. - [ ] Right-click on first / last / only frame omits the inapplicable menu items. - [ ] No regression to label dblclick, drag, snap guides, or child-of-frame drag-with-parent. ### Notes - Konva treats `fill: 'rgba(0,0,0,0)'` as hit-testable but invisible. (Compared to `fill: null` which makes only the stroke hit-testable.) Verified by behavior on shapes elsewhere in the codebase. - The transformer's `transformstart` snapshot path (`tools.js`) still uses the normal `snapshotBefore(node.id())` for frames since they're regular `WhiteboardObjects`. No change needed there. - The disabled selection-toolbar buttons rely on Bootstrap's default `:disabled` styling. If the visual isn't subtle enough, a small CSS rule in `selection_toolbar.css` can be added later — out of scope here.
Author
Member

Test Results + Final Summary

Changes

  • static/web/js/whiteboard/objects.js:
    • Frame bg Rect now has fill: 'rgba(0,0,0,0)' — transparent fill, hit-testable. Empty frame body can be clicked / dragged / dblclicked.
    • Added a dblclick dbltap handler on the bg Rect that opens the same title editor as the label.
  • static/web/js/whiteboard/selection_toolbar.js:
    • Move-slide-up / Move-slide-down chevrons compute isFirst / isLast via WhiteboardFrames.getFrames() and set .disabled accordingly.
  • static/web/js/whiteboard/contextmenu.js:
    • Move-slide-up / Move-slide-down menu items are only inserted when the action is actually available; divider only appears when at least one of the two is shown.

Behaviour after fix

  • Clicking an empty area inside a frame selects the frame and drags it normally. Children inside the frame still grab their own clicks (they sit above the frame on the same Konva layer).
  • Double-click anywhere on empty frame body opens the title editor, matching sticky/text/shape affordance.
  • Selection toolbar greys out the chevron that isn't applicable; both grey on a single-frame board.
  • Right-click on a frame at the deck edge / on a single-frame board omits the inapplicable menu items entirely.

Gates

  • node -c objects.js / selection_toolbar.js / contextmenu.js — JS syntax OK
  • cargo fmt --check — pass
  • cargo clippy --workspace --all-targets -- -D warnings — pass

Manual verification still required

Rebuild + restart hero_whiteboard_admin, hard-reload. Create 2–3 frames, then:

  1. Click an empty area in a frame → frame is selected (not the canvas). Drag it from that empty area.
  2. Double-click empty frame body → title editor opens.
  3. Drop a sticky inside a frame, click the sticky → sticky is selected, frame is not.
  4. Open the selection toolbar for the first frame → up-chevron is disabled. Last frame → down-chevron disabled. Single-frame board → both disabled.
  5. Right-click each → only the applicable menu item appears; first/last/only frames omit the inapplicable one.
## Test Results + Final Summary ### Changes - `static/web/js/whiteboard/objects.js`: - Frame bg Rect now has `fill: 'rgba(0,0,0,0)'` — transparent fill, hit-testable. Empty frame body can be clicked / dragged / dblclicked. - Added a `dblclick dbltap` handler on the bg Rect that opens the same title editor as the label. - `static/web/js/whiteboard/selection_toolbar.js`: - Move-slide-up / Move-slide-down chevrons compute `isFirst` / `isLast` via `WhiteboardFrames.getFrames()` and set `.disabled` accordingly. - `static/web/js/whiteboard/contextmenu.js`: - Move-slide-up / Move-slide-down menu items are only inserted when the action is actually available; divider only appears when at least one of the two is shown. ### Behaviour after fix - Clicking an empty area inside a frame selects the frame and drags it normally. Children inside the frame still grab their own clicks (they sit above the frame on the same Konva layer). - Double-click anywhere on empty frame body opens the title editor, matching sticky/text/shape affordance. - Selection toolbar greys out the chevron that isn't applicable; both grey on a single-frame board. - Right-click on a frame at the deck edge / on a single-frame board omits the inapplicable menu items entirely. ### Gates - `node -c objects.js / selection_toolbar.js / contextmenu.js` — JS syntax OK - `cargo fmt --check` — pass - `cargo clippy --workspace --all-targets -- -D warnings` — pass ### Manual verification still required Rebuild + restart `hero_whiteboard_admin`, hard-reload. Create 2–3 frames, then: 1. Click an empty area in a frame → frame is selected (not the canvas). Drag it from that empty area. 2. Double-click empty frame body → title editor opens. 3. Drop a sticky inside a frame, click the sticky → sticky is selected, frame is not. 4. Open the selection toolbar for the first frame → up-chevron is disabled. Last frame → down-chevron disabled. Single-frame board → both disabled. 5. Right-click each → only the applicable menu item appears; first/last/only frames omit the inapplicable one.
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#192
No description provided.