Skip to content

Resize handle leaks document mouse listeners when destroyed before the 3px threshold (throws on every mouse event until reload) #3345

Description

@patrykskrzyniarz

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

  1. Init a grid with resizable widgets.
  2. mousedown on a .ui-resizable-handle, without moving more than ~3px.
  3. 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.
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions