Skip to content

Commit 6016f12

Browse files
author
Matt Lewis
committed
feat(droppable): add the mwlDroppable directive
Closes #1
1 parent 4e22912 commit 6016f12

10 files changed

+341
-28
lines changed

README.md

+12-2
Original file line numberDiff line numberDiff line change
@@ -43,18 +43,28 @@ class DemoModule {}
4343

4444
@Component({
4545
selector: 'demo-app',
46-
template: '<div mwlDraggable (dragEnd)="dragEnd($event)">Drag me!</div>'
46+
template: `
47+
<div mwlDraggable (dragEnd)="dragEnd($event)">Drag me!</div>
48+
<div
49+
mwlDroppable
50+
(drop)="this.droppedData = $event.dropData">
51+
<span [hidden]="droppedData">Drop here</span>
52+
<span [hidden]="!droppedData">Item dropped here with data: "{{ droppedData }}"!</span>
53+
</div>
54+
`
4755
})
4856
class DemoApp {
4957

58+
droppedData: string;
59+
5060
dragEnd(event) {
5161
console.log('Element was dragged', event);
5262
}
5363

5464
}
5565
```
5666

57-
You may also find it useful to view the [demo source](https://github.com/mattlewis92/angular-draggable-droppable/blob/master/demo/demo.ts).
67+
You may also find it useful to view the [demo source](https://github.com/mattlewis92/angular-draggable-droppable/blob/master/demo/demo.component.ts).
5868

5969
### Usage without a module bundler
6070
```

demo/demo.component.ts

+53-4
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,68 @@ import {Component} from '@angular/core';
22

33
@Component({
44
selector: 'demo-app',
5-
template: '<div mwlDraggable>Drag me!</div>',
5+
template: `
6+
<div mwlDraggable dropData="foo">
7+
Drag me!
8+
</div>
9+
<div mwlDraggable dropData="bar">
10+
Or drag me!
11+
</div>
12+
<div
13+
[class.dropOverActive]="dropOverActive"
14+
mwlDroppable
15+
(dragEnter)="dropOverActive = true"
16+
(dragLeave)="dropOverActive = false"
17+
(drop)="onDrop($event)">
18+
<span [hidden]="droppedData">Drop here</span>
19+
<span [hidden]="!droppedData">Item dropped here with data: "{{ droppedData }}"!</span>
20+
</div>
21+
`,
622
styles: [`
723
[mwlDraggable] {
824
background-color: red;
925
width: 200px;
10-
color: white;
1126
height: 200px;
27+
cursor: move;
28+
position: relative;
29+
z-index: 1;
30+
float: left;
31+
margin-right: 10px;
32+
}
33+
[mwlDroppable] {
34+
background-color: green;
35+
width: 400px;
36+
height: 400px;
37+
position: relative;
38+
top: 50px;
39+
left: 100px;
40+
}
41+
[mwlDraggable],
42+
[mwlDroppable] {
43+
color: white;
1244
text-align: center;
1345
display: flex;
1446
align-items: center;
1547
justify-content: center;
16-
cursor: move;
48+
}
49+
.dropOverActive {
50+
border: dashed 1px black;
51+
background-color: lightgreen;
1752
}
1853
`]
1954
})
20-
export class Demo {}
55+
export class Demo {
56+
57+
dropOverActive: boolean = false;
58+
59+
droppedData: string = '';
60+
61+
onDrop({dropData}: {dropData: any}): void {
62+
this.dropOverActive = false;
63+
this.droppedData = dropData;
64+
setTimeout(() => {
65+
this.droppedData = '';
66+
}, 2000);
67+
}
68+
69+
}

src/dragAndDrop.module.ts

+9-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11
import {NgModule} from '@angular/core';
22
import {Draggable} from './draggable.directive';
3+
import {Droppable} from './droppable.directive';
4+
import {DraggableHelper} from './draggableHelper.provider';
35

46
@NgModule({
57
declarations: [
6-
Draggable
8+
Draggable,
9+
Droppable
710
],
811
exports: [
9-
Draggable
12+
Draggable,
13+
Droppable
14+
],
15+
providers: [
16+
DraggableHelper
1017
]
1118
})
1219
export class DragAndDropModule {}

src/draggable.directive.ts

+32-13
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import {Directive, HostListener, OnInit, ElementRef, Renderer, Output, EventEmitter} from '@angular/core';
1+
import {Directive, HostListener, OnInit, ElementRef, Renderer, Output, EventEmitter, Input} from '@angular/core';
22
import {Subject} from 'rxjs/Subject';
33
import {Observable} from 'rxjs/Observable';
44
import 'rxjs/add/observable/merge';
55
import 'rxjs/add/operator/map';
66
import 'rxjs/add/operator/mergeMap';
77
import 'rxjs/add/operator/takeUntil';
88
import 'rxjs/add/operator/takeLast';
9+
import {DraggableHelper} from './draggableHelper.provider';
910

1011
type Coordinates = {x: number, y: number};
1112

@@ -14,47 +15,57 @@ type Coordinates = {x: number, y: number};
1415
})
1516
export class Draggable implements OnInit {
1617

17-
public mouseDown: Subject<any> = new Subject();
18-
19-
public mouseMove: Subject<any> = new Subject();
20-
21-
public mouseUp: Subject<any> = new Subject();
18+
@Input() dropData: any;
2219

2320
@Output() dragStart: EventEmitter<Coordinates> = new EventEmitter<Coordinates>();
2421

2522
@Output() dragging: EventEmitter<Coordinates> = new EventEmitter<Coordinates>();
2623

2724
@Output() dragEnd: EventEmitter<Coordinates> = new EventEmitter<Coordinates>();
2825

29-
constructor(public element: ElementRef, private renderer: Renderer) {}
26+
public mouseDown: Subject<any> = new Subject();
27+
28+
public mouseMove: Subject<any> = new Subject();
29+
30+
public mouseUp: Subject<any> = new Subject();
31+
32+
constructor(public element: ElementRef, private renderer: Renderer, private draggableHelper: DraggableHelper) {}
3033

3134
ngOnInit(): void {
3235

33-
const mouseDrag: Observable<Coordinates> = this.mouseDown.flatMap((mouseDownEvent: MouseEvent) => {
36+
const mouseDrag: Observable<any> = this.mouseDown.flatMap((mouseDownEvent: MouseEvent) => {
3437

3538
this.dragStart.next({x: 0, y: 0});
3639

40+
const currentDrag: Subject<any> = new Subject();
41+
42+
this.draggableHelper.currentDrag.next(currentDrag);
43+
3744
const mouseMove: Observable<Coordinates> = this.mouseMove
3845
.map((mouseMoveEvent: MouseEvent) => {
3946
return {
47+
currentDrag,
4048
x: mouseMoveEvent.clientX - mouseDownEvent.clientX,
4149
y: mouseMoveEvent.clientY - mouseDownEvent.clientY
4250
};
4351
})
4452
.takeUntil(Observable.merge(this.mouseUp, this.mouseDown));
4553

46-
mouseMove.takeLast(1).subscribe((finalCoords: Coordinates) => {
47-
this.dragEnd.next(finalCoords);
48-
this.renderer.setElementStyle(this.element.nativeElement, 'transform', '');
54+
mouseMove.takeLast(1).subscribe(({x, y}) => {
55+
this.dragEnd.next({x, y});
56+
currentDrag.complete();
57+
this.setCssTransform('');
4958
});
5059

5160
return mouseMove;
5261

5362
});
5463

55-
mouseDrag.subscribe(({x, y}: Coordinates) => {
64+
// TODO - unsubscribe from this on destroy
65+
mouseDrag.subscribe(({x, y, currentDrag}) => {
5666
this.dragging.next({x, y});
57-
this.renderer.setElementStyle(this.element.nativeElement, 'transform', `translate(${x}px, ${y}px)`);
67+
this.setCssTransform(`translate(${x}px, ${y}px)`);
68+
currentDrag.next({rectangle: this.element.nativeElement.getBoundingClientRect(), dropData: this.dropData});
5869
});
5970

6071
}
@@ -83,4 +94,12 @@ export class Draggable implements OnInit {
8394
this.mouseUp.next(event);
8495
}
8596

97+
private setCssTransform(value: string): void {
98+
this.renderer.setElementStyle(this.element.nativeElement, 'transform', value);
99+
this.renderer.setElementStyle(this.element.nativeElement, '-webkit-transform', value);
100+
this.renderer.setElementStyle(this.element.nativeElement, '-ms-transform', value);
101+
this.renderer.setElementStyle(this.element.nativeElement, '-moz-transform', value);
102+
this.renderer.setElementStyle(this.element.nativeElement, '-o-transform', value);
103+
}
104+
86105
}

src/draggableHelper.provider.ts

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import {Subject} from 'rxjs/Subject';
2+
3+
export class DraggableHelper {
4+
5+
currentDrag: Subject<any> = new Subject();
6+
7+
}

src/droppable.directive.ts

+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import {Directive, OnInit, ElementRef, OnDestroy, Output, EventEmitter} from '@angular/core';
2+
import {Subject} from 'rxjs/Subject';
3+
import {Subscription} from 'rxjs/Subscription';
4+
import {Observable} from 'rxjs/Observable';
5+
import 'rxjs/add/operator/distinctUntilChanged';
6+
import 'rxjs/add/operator/pairwise';
7+
import 'rxjs/add/operator/filter';
8+
import {DraggableHelper} from './draggableHelper.provider';
9+
10+
function isOverlapping(rect1: ClientRect, rect2: ClientRect): boolean {
11+
return !(
12+
rect1.right < rect2.left ||
13+
rect1.left > rect2.right ||
14+
rect1.bottom < rect2.top ||
15+
rect1.top > rect2.bottom
16+
);
17+
}
18+
19+
type DropData = {dropData: any};
20+
21+
@Directive({
22+
selector: '[mwlDroppable]'
23+
})
24+
export class Droppable implements OnInit, OnDestroy {
25+
26+
@Output() dragEnter: EventEmitter<DropData> = new EventEmitter<DropData>();
27+
28+
@Output() dragLeave: EventEmitter<DropData> = new EventEmitter<DropData>();
29+
30+
@Output() dragOver: EventEmitter<DropData> = new EventEmitter<DropData>();
31+
32+
@Output() drop: EventEmitter<DropData> = new EventEmitter<DropData>();
33+
34+
currentDragSubscription: Subscription;
35+
36+
constructor(private element: ElementRef, private draggableHelper: DraggableHelper) {}
37+
38+
ngOnInit(): void {
39+
40+
this.currentDragSubscription = this.draggableHelper.currentDrag.subscribe((drag: Subject<{rectangle: ClientRect, dropData: any}>) => {
41+
42+
const droppableRectangle: ClientRect = this.element.nativeElement.getBoundingClientRect();
43+
44+
let currentDragDropData: any;
45+
const overlaps: Observable<boolean> = drag.map(({rectangle: draggableRectangle, dropData}) => {
46+
currentDragDropData = dropData;
47+
return isOverlapping(draggableRectangle, droppableRectangle);
48+
});
49+
50+
const overlapsChanged: Observable<boolean> = overlaps.distinctUntilChanged();
51+
52+
let dragOverActive: boolean; // TODO - see if there's a way of doing this via rxjs
53+
54+
overlapsChanged.filter(overlapsNow => overlapsNow).subscribe(() => {
55+
dragOverActive = true;
56+
this.dragEnter.next({
57+
dropData: currentDragDropData
58+
});
59+
});
60+
61+
overlaps.filter(overlapsNow => overlapsNow).subscribe(() => {
62+
this.dragOver.next({
63+
dropData: currentDragDropData
64+
});
65+
});
66+
67+
overlapsChanged
68+
.pairwise()
69+
.filter(([didOverlap, overlapsNow]) => didOverlap && !overlapsNow)
70+
.subscribe(() => {
71+
dragOverActive = false;
72+
this.dragLeave.next({
73+
dropData: currentDragDropData
74+
});
75+
});
76+
77+
drag.flatMap(() => overlaps).subscribe({
78+
complete: () => {
79+
if (dragOverActive) {
80+
this.drop.next({
81+
dropData: currentDragDropData
82+
});
83+
}
84+
}
85+
});
86+
87+
});
88+
89+
}
90+
91+
ngOnDestroy(): void {
92+
this.currentDragSubscription.unsubscribe();
93+
}
94+
95+
}

test/draggable.directive.spec.ts

+1-7
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,10 @@ import {Component, ViewChild} from '@angular/core';
22
import {TestBed, ComponentFixture} from '@angular/core/testing';
33
import {expect} from 'chai';
44
import * as sinon from 'sinon';
5+
import {triggerDomEvent} from './util';
56
import {DragAndDropModule} from '../src/index';
67
import {Draggable} from '../src/draggable.directive';
78

8-
const triggerDomEvent: Function = (eventType: string, target: HTMLElement | Element, eventData: Object = {}) => {
9-
const event: Event = document.createEvent('Event');
10-
Object.assign(event, eventData);
11-
event.initEvent(eventType, true, true);
12-
target.dispatchEvent(event);
13-
};
14-
159
describe('draggable directive', () => {
1610

1711
@Component({

0 commit comments

Comments
 (0)