From 382e305107ac66291944462442800992d6f8cdac Mon Sep 17 00:00:00 2001 From: nexiumbiz-debug <265948173+nexiumbiz-debug@users.noreply.github.com> Date: Sun, 14 Jun 2026 04:13:33 -0400 Subject: [PATCH] Guard drag stop after destroy --- .../src/lib/gridsterDraggable.ts | 36 +++++++---- .../src/lib/gridsterResizable.ts | 28 +++++--- .../tests/gridsterInteractionDestroy.spec.ts | 64 +++++++++++++++++++ 3 files changed, 108 insertions(+), 20 deletions(-) create mode 100644 projects/angular-gridster2/src/lib/tests/gridsterInteractionDestroy.spec.ts diff --git a/projects/angular-gridster2/src/lib/gridsterDraggable.ts b/projects/angular-gridster2/src/lib/gridsterDraggable.ts index 7103bb6c..ff23595f 100644 --- a/projects/angular-gridster2/src/lib/gridsterDraggable.ts +++ b/projects/angular-gridster2/src/lib/gridsterDraggable.ts @@ -64,14 +64,12 @@ export class GridsterDraggable { ) {} destroy(): void { - if (this.gridster.previewStyle) { + if (this.gridster?.previewStyle) { this.gridster.previewStyle(true); } + this.removeStartListeners(); + this.removeDragListeners(); this.gridsterItem = this.gridster = this.collision = null!; - if (this.mousedown) { - this.mousedown(); - this.touchstart(); - } } dragStart(e: MouseEvent): void { @@ -232,15 +230,12 @@ export class GridsterDraggable { dragStop = (e: MouseEvent): void => { e.stopPropagation(); e.preventDefault(); + if (!this.gridster || !this.gridsterItem) { + return; + } cancelScroll(); - this.cancelOnBlur(); - this.mousemove(); - this.mouseup(); - this.mouseleave(); - this.touchmove(); - this.touchend(); - this.touchcancel(); + this.removeDragListeners(); this.gridsterItem.renderer.removeClass(this.gridsterItem.el, 'gridster-item-moving'); this.gridster.dragInProgress = false; this.gridster.updateGrid(); @@ -260,6 +255,23 @@ export class GridsterDraggable { }); }; + private removeStartListeners(): void { + this.mousedown?.(); + this.touchstart?.(); + this.mousedown = this.touchstart = null!; + } + + private removeDragListeners(): void { + this.cancelOnBlur?.(); + this.mousemove?.(); + this.mouseup?.(); + this.mouseleave?.(); + this.touchmove?.(); + this.touchend?.(); + this.touchcancel?.(); + this.cancelOnBlur = this.mousemove = this.mouseup = this.mouseleave = this.touchmove = this.touchend = this.touchcancel = null!; + } + cancelDrag = (): void => { this.gridsterItem.$item().x = this.gridsterItem.item().x || 0; this.gridsterItem.$item().y = this.gridsterItem.item().y || 0; diff --git a/projects/angular-gridster2/src/lib/gridsterResizable.ts b/projects/angular-gridster2/src/lib/gridsterResizable.ts index f475844b..a117c3bf 100644 --- a/projects/angular-gridster2/src/lib/gridsterResizable.ts +++ b/projects/angular-gridster2/src/lib/gridsterResizable.ts @@ -81,6 +81,7 @@ export class GridsterResizable { destroy(): void { this.gridster?.previewStyle(); + this.removeDragListeners(); this.gridster = this.gridsterItem = null!; } @@ -234,14 +235,12 @@ export class GridsterResizable { dragStop = (e: MouseEvent): void => { e.stopPropagation(); e.preventDefault(); + if (!this.gridster || !this.gridsterItem) { + return; + } + cancelScroll(); - this.mousemove(); - this.mouseup(); - this.mouseleave(); - this.cancelOnBlur(); - this.touchmove(); - this.touchend(); - this.touchcancel(); + this.removeDragListeners(); this.gridster.dragInProgress = false; this.resizeEventScrollType = { west: false, @@ -265,6 +264,17 @@ export class GridsterResizable { }); }; + private removeDragListeners(): void { + this.mousemove?.(); + this.mouseup?.(); + this.mouseleave?.(); + this.cancelOnBlur?.(); + this.touchmove?.(); + this.touchend?.(); + this.touchcancel?.(); + this.mousemove = this.mouseup = this.mouseleave = this.cancelOnBlur = this.touchmove = this.touchend = this.touchcancel = null!; + } + cancelResize = (): void => { const $item = this.gridsterItem.$item(); const item = this.gridsterItem.item(); @@ -293,7 +303,9 @@ export class GridsterResizable { }; private check = (direction: string): boolean => { - this.hasRatio && this.enforceAspectRatio(); + if (this.hasRatio) { + this.enforceAspectRatio(); + } this.pushResize.pushItems(direction); this.push.pushItems(direction, this.gridster.$options().disablePushOnResize); if (this.gridster.checkCollision(this.gridsterItem.$item(), true)) { diff --git a/projects/angular-gridster2/src/lib/tests/gridsterInteractionDestroy.spec.ts b/projects/angular-gridster2/src/lib/tests/gridsterInteractionDestroy.spec.ts new file mode 100644 index 00000000..bb4a3521 --- /dev/null +++ b/projects/angular-gridster2/src/lib/tests/gridsterInteractionDestroy.spec.ts @@ -0,0 +1,64 @@ +import type { ChangeDetectorRef, NgZone } from '@angular/core'; + +import type { Gridster } from '../gridster'; +import { GridsterDraggable } from '../gridsterDraggable'; +import type { GridsterItem } from '../gridsterItem'; +import { GridsterResizable } from '../gridsterResizable'; + +function makeEvent(): MouseEvent { + return { + stopPropagation: vi.fn(), + preventDefault: vi.fn() + } as unknown as MouseEvent; +} + +function makeGridster(): Gridster { + return { + previewStyle: vi.fn() + } as unknown as Gridster; +} + +function makeZone(): NgZone { + return { + runOutsideAngular: (fn: () => T) => fn(), + run: (fn: () => T) => fn() + } as unknown as NgZone; +} + +describe('gridster interaction teardown', () => { + it('ignores a late draggable stop event after destroy', () => { + const draggable = new GridsterDraggable({} as GridsterItem, makeGridster(), makeZone(), { markForCheck: vi.fn() } as unknown as ChangeDetectorRef); + const cleanup = [vi.fn(), vi.fn(), vi.fn(), vi.fn(), vi.fn(), vi.fn(), vi.fn(), vi.fn(), vi.fn()]; + draggable.mousedown = cleanup[0]; + draggable.touchstart = cleanup[1]; + draggable.mousemove = cleanup[2]; + draggable.mouseup = cleanup[3]; + draggable.mouseleave = cleanup[4]; + draggable.cancelOnBlur = cleanup[5]; + draggable.touchmove = cleanup[6]; + draggable.touchend = cleanup[7]; + draggable.touchcancel = cleanup[8]; + + draggable.destroy(); + + cleanup.forEach(listener => expect(listener).toHaveBeenCalledTimes(1)); + expect(() => draggable.dragStop(makeEvent())).not.toThrow(); + }); + + it('ignores a late resizable stop event after destroy', () => { + const resizable = new GridsterResizable({} as GridsterItem, makeGridster(), makeZone()); + const cleanup = [vi.fn(), vi.fn(), vi.fn(), vi.fn(), vi.fn(), vi.fn(), vi.fn()]; + resizable.mousemove = cleanup[0]; + resizable.mouseup = cleanup[1]; + resizable.mouseleave = cleanup[2]; + resizable.cancelOnBlur = cleanup[3]; + resizable.touchmove = cleanup[4]; + resizable.touchend = cleanup[5]; + resizable.touchcancel = cleanup[6]; + + resizable.destroy(); + + cleanup.forEach(listener => expect(listener).toHaveBeenCalledTimes(1)); + expect(() => resizable.dragStop(makeEvent())).not.toThrow(); + }); +});