Skip to content

Commit f98f586

Browse files
Onyphlaxmattlewis92
authored andcommitted
feat(draggable-scroll-container): Added input activeLongPressDrag (#79)
Start drag after a long touch to be able to scroll the container without dragging any item Closes #78
1 parent 5bd76ce commit f98f586

File tree

3 files changed

+217
-10
lines changed

3 files changed

+217
-10
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,71 @@
1-
import { Directive, ElementRef } from '@angular/core';
1+
import {
2+
Directive,
3+
ElementRef,
4+
Input,
5+
NgZone,
6+
OnInit,
7+
Renderer2
8+
} from '@angular/core';
29

310
@Directive({
411
selector: '[mwlDraggableScrollContainer]'
512
})
6-
export class DraggableScrollContainerDirective {
7-
constructor(public elementRef: ElementRef<HTMLElement>) {}
13+
export class DraggableScrollContainerDirective implements OnInit {
14+
/**
15+
* Trigger the DragStart after a long touch in scrollable container when true
16+
*/
17+
@Input()
18+
activeLongPressDrag: boolean = false;
19+
20+
/**
21+
* Configuration of a long touch
22+
* Duration in ms of a long touch before activating DragStart
23+
* Delta of the
24+
*/
25+
@Input()
26+
longPressConfig = { duration: 300, delta: 30 };
27+
28+
private cancelledScroll = false;
29+
30+
constructor(
31+
public elementRef: ElementRef<HTMLElement>,
32+
private renderer: Renderer2,
33+
private zone: NgZone
34+
) {}
35+
36+
ngOnInit() {
37+
this.zone.runOutsideAngular(() => {
38+
this.renderer.listen(
39+
this.elementRef.nativeElement,
40+
'touchmove',
41+
(event: TouchEvent) => {
42+
if (this.cancelledScroll && event.cancelable) {
43+
event.preventDefault();
44+
}
45+
}
46+
);
47+
});
48+
}
49+
50+
disableScroll(): void {
51+
this.cancelledScroll = true;
52+
this.renderer.setStyle(this.elementRef.nativeElement, 'overflow', 'hidden');
53+
}
54+
55+
enableScroll(): void {
56+
this.cancelledScroll = false;
57+
this.renderer.setStyle(this.elementRef.nativeElement, 'overflow', 'auto');
58+
}
59+
60+
hasScrollbar(): boolean {
61+
const containerHasHorizontalScroll =
62+
this.elementRef.nativeElement.scrollWidth -
63+
this.elementRef.nativeElement.clientWidth >
64+
0;
65+
const containerHasVerticalScroll =
66+
this.elementRef.nativeElement.scrollHeight -
67+
this.elementRef.nativeElement.clientHeight >
68+
0;
69+
return containerHasHorizontalScroll || containerHasVerticalScroll;
70+
}
871
}

projects/angular-draggable-droppable/src/lib/draggable.directive.spec.ts

+66-1
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ describe('draggable directive', () => {
6161
@Component({
6262
// tslint:disable-line max-classes-per-file
6363
template: `
64-
<div mwlDraggableScrollContainer>
64+
<div mwlDraggableScrollContainer [activeLongPressDrag]="true">
6565
<div
6666
#draggableElement
6767
mwlDraggable
@@ -881,4 +881,69 @@ describe('draggable directive', () => {
881881
expect(innerDragFixture.componentInstance.outerDrag).not.to.have.been
882882
.called;
883883
});
884+
885+
const clock = sinon.useFakeTimers();
886+
887+
it('should not start dragging with long touch', () => {
888+
const scrollFixture = TestBed.createComponent(ScrollTestComponent);
889+
scrollFixture.detectChanges();
890+
document.body.appendChild(scrollFixture.nativeElement);
891+
const draggableElement =
892+
scrollFixture.componentInstance.draggableElement.nativeElement;
893+
triggerDomEvent('touchstart', draggableElement, {
894+
touches: [{ clientX: 5, clientY: 10 }]
895+
});
896+
clock.tick(200);
897+
898+
// Touch is too short
899+
triggerDomEvent('touchmove', draggableElement, {
900+
targetTouches: [{ clientX: 5, clientY: 10 }]
901+
});
902+
expect(scrollFixture.componentInstance.dragStart).not.to.have.been.called;
903+
904+
// Touch is too far from touchstart position
905+
clock.tick(200);
906+
triggerDomEvent('touchmove', draggableElement, {
907+
targetTouches: [{ clientX: 30, clientY: 20 }]
908+
});
909+
expect(scrollFixture.componentInstance.dragStart).not.to.have.been.called;
910+
911+
// Scroll begin so drag can't start
912+
clock.tick(400);
913+
scrollFixture.componentInstance.scrollContainer.elementRef.nativeElement.scrollTop = 5;
914+
triggerDomEvent('touchmove', draggableElement, {
915+
targetTouches: [{ clientX: 5, clientY: 5 }]
916+
});
917+
expect(scrollFixture.componentInstance.dragStart).not.to.have.been.called;
918+
triggerDomEvent('touchend', draggableElement, {
919+
changedTouches: [{ clientX: 10, clientY: 18 }]
920+
});
921+
});
922+
923+
it('should start dragging with long touch', () => {
924+
const scrollFixture = TestBed.createComponent(ScrollTestComponent);
925+
scrollFixture.detectChanges();
926+
document.body.appendChild(scrollFixture.nativeElement);
927+
const draggableElement =
928+
scrollFixture.componentInstance.draggableElement.nativeElement;
929+
triggerDomEvent('touchstart', draggableElement, {
930+
touches: [{ clientX: 5, clientY: 10 }]
931+
});
932+
clock.tick(400);
933+
triggerDomEvent('touchmove', draggableElement, {
934+
targetTouches: [{ clientX: 5, clientY: 10 }]
935+
});
936+
expect(scrollFixture.componentInstance.dragStart).to.have.been.calledOnce;
937+
938+
triggerDomEvent('touchmove', draggableElement, {
939+
targetTouches: [{ clientX: 7, clientY: 12 }]
940+
});
941+
expect(scrollFixture.componentInstance.dragging).to.have.been.calledWith({
942+
x: 2,
943+
y: 2
944+
});
945+
triggerDomEvent('touchend', draggableElement, {
946+
changedTouches: [{ clientX: 10, clientY: 18 }]
947+
});
948+
});
884949
});

projects/angular-draggable-droppable/src/lib/draggable.directive.ts

+85-6
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,11 @@ export interface PointerEvent {
6767
event: MouseEvent | TouchEvent;
6868
}
6969

70+
export interface TimeLongPress {
71+
timerBegin: number;
72+
timerEnd: number;
73+
}
74+
7075
@Directive({
7176
selector: '[mwlDraggable]'
7277
})
@@ -194,6 +199,8 @@ export class DraggableDirective implements OnInit, OnChanges, OnDestroy {
194199

195200
private destroy$ = new Subject();
196201

202+
private timeLongPress: TimeLongPress = { timerBegin: 0, timerEnd: 0 };
203+
197204
/**
198205
* @hidden
199206
*/
@@ -215,7 +222,7 @@ export class DraggableDirective implements OnInit, OnChanges, OnDestroy {
215222
mergeMap((pointerDownEvent: PointerEvent) => {
216223
// fix for https://github.com/mattlewis92/angular-draggable-droppable/issues/61
217224
// stop mouse events propagating up the chain
218-
if (pointerDownEvent.event.stopPropagation) {
225+
if (pointerDownEvent.event.stopPropagation && !this.scrollContainer) {
219226
pointerDownEvent.event.stopPropagation();
220227
}
221228

@@ -601,16 +608,49 @@ export class DraggableDirective implements OnInit, OnChanges, OnDestroy {
601608
}
602609

603610
private onTouchStart(event: TouchEvent): void {
611+
if (!this.scrollContainer) {
612+
try {
613+
event.preventDefault();
614+
} catch (e) {}
615+
}
616+
let hasContainerScrollbar: boolean;
617+
let startScrollPosition: any;
618+
let isDragActivated: boolean;
619+
if (this.scrollContainer && this.scrollContainer.activeLongPressDrag) {
620+
this.timeLongPress.timerBegin = Date.now();
621+
isDragActivated = false;
622+
hasContainerScrollbar = this.scrollContainer.hasScrollbar();
623+
startScrollPosition = this.getScrollPosition();
624+
}
604625
if (!this.eventListenerSubscriptions.touchmove) {
605626
this.eventListenerSubscriptions.touchmove = this.renderer.listen(
606627
'document',
607628
'touchmove',
608629
(touchMoveEvent: TouchEvent) => {
609-
this.pointerMove$.next({
610-
event: touchMoveEvent,
611-
clientX: touchMoveEvent.targetTouches[0].clientX,
612-
clientY: touchMoveEvent.targetTouches[0].clientY
613-
});
630+
if (
631+
this.scrollContainer &&
632+
this.scrollContainer.activeLongPressDrag &&
633+
!isDragActivated &&
634+
hasContainerScrollbar
635+
) {
636+
isDragActivated = this.shouldBeginDrag(
637+
event,
638+
touchMoveEvent,
639+
startScrollPosition
640+
);
641+
}
642+
if (
643+
!this.scrollContainer ||
644+
!this.scrollContainer.activeLongPressDrag ||
645+
!hasContainerScrollbar ||
646+
isDragActivated
647+
) {
648+
this.pointerMove$.next({
649+
event: touchMoveEvent,
650+
clientX: touchMoveEvent.targetTouches[0].clientX,
651+
clientY: touchMoveEvent.targetTouches[0].clientY
652+
});
653+
}
614654
}
615655
);
616656
}
@@ -625,6 +665,9 @@ export class DraggableDirective implements OnInit, OnChanges, OnDestroy {
625665
if (this.eventListenerSubscriptions.touchmove) {
626666
this.eventListenerSubscriptions.touchmove();
627667
delete this.eventListenerSubscriptions.touchmove;
668+
if (this.scrollContainer && this.scrollContainer.activeLongPressDrag) {
669+
this.scrollContainer.enableScroll();
670+
}
628671
}
629672
this.pointerUp$.next({
630673
event,
@@ -678,4 +721,40 @@ export class DraggableDirective implements OnInit, OnChanges, OnDestroy {
678721
};
679722
}
680723
}
724+
725+
private shouldBeginDrag(
726+
event: TouchEvent,
727+
touchMoveEvent: TouchEvent,
728+
startScrollPosition: any
729+
): boolean {
730+
const moveScrollPosition = this.getScrollPosition();
731+
const deltaScroll = {
732+
top: Math.abs(moveScrollPosition.top - startScrollPosition.top),
733+
left: Math.abs(moveScrollPosition.left - startScrollPosition.left)
734+
};
735+
const deltaX =
736+
Math.abs(
737+
touchMoveEvent.targetTouches[0].clientX - event.touches[0].clientX
738+
) - deltaScroll.left;
739+
const deltaY =
740+
Math.abs(
741+
touchMoveEvent.targetTouches[0].clientY - event.touches[0].clientY
742+
) - deltaScroll.top;
743+
const deltaTotal = deltaX + deltaY;
744+
if (
745+
deltaTotal > this.scrollContainer.longPressConfig.delta ||
746+
deltaScroll.top > 0 ||
747+
deltaScroll.left > 0
748+
) {
749+
this.timeLongPress.timerBegin = Date.now();
750+
}
751+
this.timeLongPress.timerEnd = Date.now();
752+
const duration =
753+
this.timeLongPress.timerEnd - this.timeLongPress.timerBegin;
754+
if (duration >= this.scrollContainer.longPressConfig.duration) {
755+
this.scrollContainer.disableScroll();
756+
return true;
757+
}
758+
return false;
759+
}
681760
}

0 commit comments

Comments
 (0)