Subject of the issue
DDResizableHandle attaches mousemove/mouseup listeners to document on mousedown, but destroy() only removes them when a resize has already started (this.moving). If the handle is destroyed in the window between mousedown and the 3px move threshold, those document listeners leak onto a dead handle.
The next mouse event then throws Cannot read properties of undefined (reading 'gridstackNode'), and because the error aborts cleanup, it keeps throwing on every subsequent mouse event until a full page reload.
DDDraggable.destroy() already guards this correctly (if (this.mouseDownEvent)); DDResizableHandle.destroy() does not.
Your environment
- gridstack version: 12.4.2 — also confirmed on 13.1.2 and current
master (edb5f45a)
- browser / OS: any (logic bug, not browser-specific)
- framework: any (reproduced from a plain grid)
Steps to reproduce
- Init a grid with resizable widgets.
mousedown on a .ui-resizable-handle, without moving more than ~3px.
- While the button is still held, tear down the widget's drag/drop. Any of these trigger it, since they all reach
GridStack._removeDD: removeAll(), removeWidget(), disable(), setStatic(true), destroy(), or a framework re-render that rebuilds widgets.
- Move the mouse.
const grid = GridStack.init({ resizable: { handles: 'e, se, s, sw, w' } });
// ...add a widget...
const handle = document.querySelector('.ui-resizable-handle');
handle.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, clientX: 100, clientY: 100 }));
grid.removeAll(); // teardown BEFORE moving 3px
document.dispatchEvent(new MouseEvent('mousemove', { clientX: 101, clientY: 101 }));
// -> throws, and every subsequent mousemove/mouseup keeps throwing
Live reproduction
Minimal StackBlitz (forked from the official gridstack-demo template, pinned to gridstack@12.4.2):
Click Run automated repro (or hold a widget's SE resize handle without moving >3px, click removeAll while held / press R, then move the mouse). The on-page log and console show the repeating gridstackNode TypeError, which continues after mouseup and never self-heals until reload. Applying the fix below produces no errors.
Expected behavior
Destroying a resize handle detaches every listener it attached, regardless of whether a resize gesture had started. No errors after teardown.
Actual behavior
Uncaught TypeError: Cannot read properties of undefined (reading 'gridstackNode') from DDResizable._resizeStart, repeating on every mouse event until reload.
Root cause
_mouseDown registers the document listeners:
protected _mouseDown(e: MouseEvent): void {
this.mouseDownEvent = e;
document.addEventListener('mousemove', this._mouseMove, { capture: true, passive: true });
document.addEventListener('mouseup', this._mouseUp, true);
...
}
They're only removed inside _mouseUp, and destroy() only calls _mouseUp when this.moving is true — which requires moving >2px:
public destroy(): DDResizableHandle {
if (this.moving) this._mouseUp(this.mouseDownEvent!); // misses the mousedown-but-not-yet-moving case
...
}
The parent DDResizable.destroy() runs delete this.el, so the orphaned handle's start callback (a closure over that DDResizable) hits this.el.gridstackNode on undefined in _resizeStart. That throw also flips moving = true before crashing, so the eventual mouseup enters _resizeStop and throws again before the removeEventListener calls run — which is why the listeners are never cleaned up and the error repeats.
Suggested fix
Guard on the pointer-down state instead of the "already resizing" flag, mirroring DDDraggable.destroy():
public destroy(): DDResizableHandle {
if (this.mouseDownEvent) this._mouseUp(this.mouseDownEvent); // was: if (this.moving)
// ...unchanged...
}
Happy to open a PR.
Subject of the issue
DDResizableHandleattachesmousemove/mouseuplisteners todocumentonmousedown, butdestroy()only removes them when a resize has already started (this.moving). If the handle is destroyed in the window betweenmousedownand the 3px move threshold, thosedocumentlisteners leak onto a dead handle.The next mouse event then throws
Cannot read properties of undefined (reading 'gridstackNode'), and because the error aborts cleanup, it keeps throwing on every subsequent mouse event until a full page reload.DDDraggable.destroy()already guards this correctly (if (this.mouseDownEvent));DDResizableHandle.destroy()does not.Your environment
master(edb5f45a)Steps to reproduce
mousedownon a.ui-resizable-handle, without moving more than ~3px.GridStack._removeDD:removeAll(),removeWidget(),disable(),setStatic(true),destroy(), or a framework re-render that rebuilds widgets.Live reproduction
Minimal StackBlitz (forked from the official gridstack-demo template, pinned to
gridstack@12.4.2):Click Run automated repro (or hold a widget's SE resize handle without moving >3px, click removeAll while held / press
R, then move the mouse). The on-page log and console show the repeatinggridstackNodeTypeError, which continues aftermouseupand never self-heals until reload. Applying the fix below produces no errors.Expected behavior
Destroying a resize handle detaches every listener it attached, regardless of whether a resize gesture had started. No errors after teardown.
Actual behavior
Uncaught TypeError: Cannot read properties of undefined (reading 'gridstackNode')fromDDResizable._resizeStart, repeating on every mouse event until reload.Root cause
_mouseDownregisters thedocumentlisteners:They're only removed inside
_mouseUp, anddestroy()only calls_mouseUpwhenthis.movingistrue— which requires moving >2px:The parent
DDResizable.destroy()runsdelete this.el, so the orphaned handle'sstartcallback (a closure over thatDDResizable) hitsthis.el.gridstackNodeonundefinedin_resizeStart. That throw also flipsmoving = truebefore crashing, so the eventualmouseupenters_resizeStopand throws again before theremoveEventListenercalls run — which is why the listeners are never cleaned up and the error repeats.Suggested fix
Guard on the pointer-down state instead of the "already resizing" flag, mirroring
DDDraggable.destroy():Happy to open a PR.