Whiteboard: draw / eraser / rubber-band can get stuck "on" if mouseup happens outside the Konva canvas #185
Labels
No labels
prio_critical
prio_low
type_bug
type_contact
type_issue
type_lead
type_question
type_story
type_task
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
lhumina_code/hero_whiteboard#185
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem (critical)
When the user is in Draw mode and releases the mouse button while the cursor is over the floating subtoolbar (or any DOM element outside the Konva stage container, like the top header, the property panel, etc.), the stroke never ends:
mousedown→drawLineKonva preview line is created (tools.js:539-549).mouseupfires on the subtoolbar DOM element, not on Konva (they are sibling DOM nodes, not nested). Konvas stagemouseuphandler (tools.js:729) never runs.drawLineis stuck non-null. The orphan preview line is still on the object layer.stage.on("mousemove")fires unconditionally on hover — thecurrentTool === "draw" && drawLinebranch (tools.js:658-662) keeps appending points to the orphan line. The user appears to be drawing without holding the button.createDrawingis only called inonMouseUpat line 737), so a page refresh wipes it.Same class of bug also affects:
isErasingflag (tools.js:551, 748-754) stays true; subsequent cursor moves keep erasing.isSelectingflag (tools.js:480-482, 677-680); the rubber-band rectangle can be left visible and tracking the cursor.Root cause
Konvas
mousedown/mousemove/mouseupare attached to the stage container<div id="whiteboard-container">. The subtoolbar (<div id="subtoolbar">), main toolbar, and other UI overlays are siblings of that container. A button release over any of them does not propagate to Konva. Mid-stroke state (drawLine,isErasing,isSelecting) is never cleaned up.Expected
Releasing the mouse button outside the canvas while a stroke / eraser drag / rubber-band is in progress ends the gesture cleanly: persist the drawing (if it has points), reset state, hide the rubber-band rectangle, commit the eraser drag.
Acceptance
drawLineis reset.Approach (sketch)
Add a
document(orwindow) levelmouseupsafety net intools.js:setup()that calls the same end-gesture cleanup wheneverdrawLine/isErasing/isSelectingis non-null. Use a guard flag so the Konvamouseuphandler doesnt double-fire if both run for the same release.Out of scope
Mouse leaving the window entirely (
mouseleaveon document) — pointer capture handles that on most browsers; if it becomes an issue, file separately.Implementation Spec for Issue #185
Objective
Releasing the mouse button outside the Konva stage during an in-progress draw / eraser / rubber-band gesture must end the gesture cleanly: persist the drawing, commit the eraser drag, finalise the rubber-band selection. Subsequent cursor movement must not keep drawing / erasing / extending the selection rectangle.
Approach
Add a single
document-levelmouseupsafety net intools.js:setup(). It calls the existing KonvaonMouseUp(e)handler when any in-progress gesture flag is non-null (drawLine,isErasing,isSelecting). The Konva-level handler still runs first when the release happens inside the canvas; the document-level handler is a fallback when it doesn't.Use a one-shot guard so
onMouseUpdoesn't double-fire for a single release:_gestureEndedThisCycle = trueinsideonMouseUpafter it runs.File to modify
crates/hero_whiteboard_admin/static/web/js/whiteboard/tools.jsImplementation Plan
Step 1: Refactor
onMouseUpinto a reusable end-gesture functionAdd a guard flag and a thin wrapper that both the Konva
mouseupand the new document fallback call. Concretely:Replace the existing Konva binding:
with:
Step 2: Add the document-level safety net in
setup()After the existing stage event bindings (around
tools.js:378), add:onMouseUpalready handles the three branches correctly, so we don't need to duplicate the cleanup logic.Step 3: Build a fake Konva-shaped event for the rubber-band branch
onMouseUpreadse.evtfor things likelastClientX/Y— but only for the path that fires while mid-drag (within onMouseMove). The rubber-band end branch usesselectionRect.getClientRect(), not the event coords directly. The other branches (draw / eraser) don't read from the event at all. So passing the raw DOMMouseEventaseis safe —onMouseUpdoesn't accesse.evtfrommouseupitself.If a regression is observed, wrap
eas{ evt: e }before callingonMouseUp(e)to mimic the Konva shape — but a quick grep ofonMouseUpshows noe.evtaccess in the mouseup-side code paths.Acceptance Criteria
documentmouseup still fires for the latter case in most browsers) → same outcome._gestureEndedThisCycleguard prevents the document fallback from running after the stage handler already did).Notes
setup(). No teardown needed (the page lifetime owns it).mouseupondocument(some browsers / OS combinations) are out of scope; if they become an issue, add apointercancel/blurfallback.Test Results + Final Summary
Changes
static/web/js/whiteboard/tools.js:endActiveGesture(e)wrapper with a one-shot_gestureEndedThisCycleguard. Calls the existingonMouseUp(e)and clears the guard on the next tick.mouseuptoendActiveGestureinstead ofonMouseUpdirectly.document-levelmouseuplistener that callsendActiveGesture(e)whendrawLine || isErasing || isSelecting. The guard prevents double-fire when both the stage and document handlers run for the same release.Behaviour after fix
createDrawing) and resetsdrawLine. Subsequent cursor moves do NOT keep drawing.isErasing.Gates
node -c tools.js— JS syntax OKcargo fmt --check— passcargo clippy --workspace --all-targets -- -D warnings— passAcceptance Criteria
Manual verification still required
Rebuild + restart
hero_whiteboard_admin, hard-reload, exercise: