From 8850ddfd79f60bd5efd469ec5f99650fb02199e0 Mon Sep 17 00:00:00 2001 From: Craig Anderson Date: Mon, 28 Dec 2020 16:24:03 +0000 Subject: [PATCH] feat: add validateDrop to mwlDroppable directive (#111) --- .../src/lib/draggable-helper.provider.ts | 1 + .../src/lib/draggable.directive.ts | 13 ++++- .../src/lib/droppable.directive.spec.ts | 56 ++++++++++++++++++- .../src/lib/droppable.directive.ts | 32 ++++++++++- .../src/public_api.ts | 6 +- src/demo/demo.component.css | 28 ++++++++++ src/demo/demo.component.html | 19 +++++++ src/demo/demo.component.ts | 20 ++++++- 8 files changed, 169 insertions(+), 6 deletions(-) diff --git a/projects/angular-draggable-droppable/src/lib/draggable-helper.provider.ts b/projects/angular-draggable-droppable/src/lib/draggable-helper.provider.ts index 5966ae9..913300b 100644 --- a/projects/angular-draggable-droppable/src/lib/draggable-helper.provider.ts +++ b/projects/angular-draggable-droppable/src/lib/draggable-helper.provider.ts @@ -5,6 +5,7 @@ export interface CurrentDragData { clientX: number; clientY: number; dropData: any; + target: EventTarget; } @Injectable({ diff --git a/projects/angular-draggable-droppable/src/lib/draggable.directive.ts b/projects/angular-draggable-droppable/src/lib/draggable.directive.ts index 658fa2b..d42bac1 100644 --- a/projects/angular-draggable-droppable/src/lib/draggable.directive.ts +++ b/projects/angular-draggable-droppable/src/lib/draggable.directive.ts @@ -316,6 +316,7 @@ export class DraggableDirective implements OnInit, OnChanges, OnDestroy { clientY: pointerMoveEvent.clientY, scrollLeft: scroll.left, scrollTop: scroll.top, + target: pointerMoveEvent.event.target, }; }), map((moveData) => { @@ -518,7 +519,16 @@ export class DraggableDirective implements OnInit, OnChanges, OnDestroy { map(([previous, next]) => next) ) .subscribe( - ({ x, y, currentDrag$, clientX, clientY, transformX, transformY }) => { + ({ + x, + y, + currentDrag$, + clientX, + clientY, + transformX, + transformY, + target, + }) => { this.zone.run(() => { this.dragging.next({ x, y }); }); @@ -538,6 +548,7 @@ export class DraggableDirective implements OnInit, OnChanges, OnDestroy { clientX, clientY, dropData: this.dropData, + target, }); } ); diff --git a/projects/angular-draggable-droppable/src/lib/droppable.directive.spec.ts b/projects/angular-draggable-droppable/src/lib/droppable.directive.spec.ts index f169bae..88230ca 100644 --- a/projects/angular-draggable-droppable/src/lib/droppable.directive.spec.ts +++ b/projects/angular-draggable-droppable/src/lib/droppable.directive.spec.ts @@ -5,7 +5,7 @@ import * as sinon from 'sinon'; import { triggerDomEvent } from '../test-utils'; import { DragAndDropModule } from 'angular-draggable-droppable'; import { DraggableDirective } from './draggable.directive'; -import { DroppableDirective } from './droppable.directive'; +import { DroppableDirective, ValidateDrop } from './droppable.directive'; import { DraggableScrollContainerDirective } from './draggable-scroll-container.directive'; import { By } from '@angular/platform-browser'; @@ -29,6 +29,7 @@ describe('droppable directive', () => { (drop)="drop($event)" [dragOverClass]="dragOverClass" [dragActiveClass]="dragActiveClass" + [validateDrop]="validateDrop" > Drop here @@ -69,6 +70,7 @@ describe('droppable directive', () => { }; dragOverClass: string; dragActiveClass: string; + validateDrop: ValidateDrop; } @Component({ @@ -411,4 +413,56 @@ describe('droppable directive', () => { }); expect(scrollFixture.componentInstance.drop).not.to.have.been.called; }); + + it('should fire drop events when validateDrop returns true', () => { + const draggableElement = + fixture.componentInstance.draggableElement.nativeElement; + const droppableElement = + fixture.componentInstance.droppableElement.nativeElement; + const elementInsideDroppableArea = document.createElement('div'); + droppableElement.appendChild(elementInsideDroppableArea); + fixture.componentInstance.validateDrop = () => true; + fixture.detectChanges(); + triggerDomEvent('mousedown', draggableElement, { + clientX: 5, + clientY: 10, + button: 0, + }); + triggerDomEvent('mousemove', elementInsideDroppableArea, { + clientX: 5, + clientY: 120, + }); + triggerDomEvent('mouseup', draggableElement, { + clientX: 5, + clientY: 120, + button: 0, + }); + expect(fixture.componentInstance.drop).to.have.been.called; + }); + + it('should not fire drop events when validateDrop returns false', () => { + const draggableElement = + fixture.componentInstance.draggableElement.nativeElement; + const droppableElement = + fixture.componentInstance.droppableElement.nativeElement; + const elementOutsideDroppableArea = document.createElement('div'); + document.body.appendChild(elementOutsideDroppableArea); + fixture.componentInstance.validateDrop = () => false; + fixture.detectChanges(); + triggerDomEvent('mousedown', draggableElement, { + clientX: 5, + clientY: 10, + button: 0, + }); + triggerDomEvent('mousemove', elementOutsideDroppableArea, { + clientX: 5, + clientY: 120, + }); + triggerDomEvent('mouseup', draggableElement, { + clientX: 5, + clientY: 120, + button: 0, + }); + expect(fixture.componentInstance.drop).not.to.have.been.called; + }); }); diff --git a/projects/angular-draggable-droppable/src/lib/droppable.directive.ts b/projects/angular-draggable-droppable/src/lib/droppable.directive.ts index 6303270..7a1b25f 100644 --- a/projects/angular-draggable-droppable/src/lib/droppable.directive.ts +++ b/projects/angular-draggable-droppable/src/lib/droppable.directive.ts @@ -33,6 +33,23 @@ export interface DropEvent { dropData: T; } +export interface ValidateDropParams { + /** + * ClientX value of the mouse location where the drop occurred + */ + clientX: number; + /** + * ClientY value of the mouse location where the drop occurred + */ + clientY: number; + /** + * The target of the event where the drop occurred + */ + target: EventTarget; +} + +export type ValidateDrop = (params: ValidateDropParams) => boolean; + @Directive({ selector: '[mwlDroppable]', }) @@ -47,6 +64,11 @@ export class DroppableDirective implements OnInit, OnDestroy { */ @Input() dragActiveClass: string; + /** + * Allow custom behaviour to control when the element is dropped + */ + @Input() validateDrop: ValidateDrop; + /** * Called when a draggable element starts overlapping the element */ @@ -101,7 +123,7 @@ export class DroppableDirective implements OnInit, OnDestroy { let currentDragDropData: any; const overlaps$ = drag$.pipe( - map(({ clientX, clientY, dropData }) => { + map(({ clientX, clientY, dropData, target }) => { currentDragDropData = dropData; if (droppableElement.updateCache) { droppableElement.rect = this.element.nativeElement.getBoundingClientRect(); @@ -115,9 +137,15 @@ export class DroppableDirective implements OnInit, OnDestroy { clientY, droppableElement.rect as ClientRect ); + + const isDropAllowed = + !this.validateDrop || + this.validateDrop({ clientX, clientY, target }); + if (droppableElement.scrollContainerRect) { return ( isWithinElement && + isDropAllowed && isCoordinateWithinRectangle( clientX, clientY, @@ -125,7 +153,7 @@ export class DroppableDirective implements OnInit, OnDestroy { ) ); } else { - return isWithinElement; + return isWithinElement && isDropAllowed; } }) ); diff --git a/projects/angular-draggable-droppable/src/public_api.ts b/projects/angular-draggable-droppable/src/public_api.ts index 232f06a..6aced63 100644 --- a/projects/angular-draggable-droppable/src/public_api.ts +++ b/projects/angular-draggable-droppable/src/public_api.ts @@ -3,7 +3,11 @@ */ export * from './lib/drag-and-drop.module'; -export { DropEvent } from './lib/droppable.directive'; +export { + DropEvent, + ValidateDrop, + ValidateDropParams, +} from './lib/droppable.directive'; export { DragPointerDownEvent, DragStartEvent, diff --git a/src/demo/demo.component.css b/src/demo/demo.component.css index e491be7..5df93ae 100644 --- a/src/demo/demo.component.css +++ b/src/demo/demo.component.css @@ -1,3 +1,7 @@ +:host { + display: flex; +} + [mwlDraggable] { background-color: red; width: 200px; @@ -19,6 +23,10 @@ left: 100px; } +.validate-drop { + left: 150px; +} + [mwlDraggable], [mwlDroppable] { color: white; @@ -36,3 +44,23 @@ .drag-active { z-index: 3; } + +.floating-toolbar { + position: absolute; + top: 140px; + z-index: 2; + width: 250px; + height: 75px; + background: yellow; + display: flex; + align-items: center; + justify-content: center; +} + +.floating-toolbar-1 { + left: 600px; +} + +.floating-toolbar-2 { + left: 1050px; +} diff --git a/src/demo/demo.component.html b/src/demo/demo.component.html index 6fdd78c..e2ee91c 100644 --- a/src/demo/demo.component.html +++ b/src/demo/demo.component.html @@ -21,3 +21,22 @@ >Item dropped here with data: "{{ droppedData }}"! +
+ Drop here + Item dropped here with data: "{{ droppedData2 }}"! +
+ +
+ Floating toolbar, drop here too! +
+
+ Floating toolbar, can't drop here! +
diff --git a/src/demo/demo.component.ts b/src/demo/demo.component.ts index 1002ded..202465b 100644 --- a/src/demo/demo.component.ts +++ b/src/demo/demo.component.ts @@ -1,5 +1,9 @@ -import { Component } from '@angular/core'; +import { Component, ElementRef, ViewChild } from '@angular/core'; import { DropEvent } from 'angular-draggable-droppable'; +import { + DroppableDirective, + ValidateDrop, +} from 'projects/angular-draggable-droppable/src/lib/droppable.directive'; @Component({ selector: 'mwl-demo-app', @@ -8,6 +12,10 @@ import { DropEvent } from 'angular-draggable-droppable'; }) export class DemoComponent { droppedData: string = ''; + droppedData2: string = ''; + + @ViewChild(DroppableDirective, { read: ElementRef }) + droppableElement: ElementRef; onDrop({ dropData }: DropEvent): void { this.droppedData = dropData; @@ -15,4 +23,14 @@ export class DemoComponent { this.droppedData = ''; }, 2000); } + + onDrop2({ dropData }: DropEvent): void { + this.droppedData2 = dropData; + setTimeout(() => { + this.droppedData2 = ''; + }, 2000); + } + + validateDrop: ValidateDrop = ({ target }) => + this.droppableElement.nativeElement.contains(target as Node); }