Skip to content

Commit

Permalink
feat(draggable-scroll-container): Added input activeLongPressDrag (#79)
Browse files Browse the repository at this point in the history
Start drag after a long touch to be able to scroll the container without dragging any item

Closes #78
  • Loading branch information
Onyphlax authored and mattlewis92 committed Feb 17, 2019
1 parent 5bd76ce commit f98f586
Show file tree
Hide file tree
Showing 3 changed files with 217 additions and 10 deletions.
Original file line number Diff line number Diff line change
@@ -1,8 +1,71 @@
import { Directive, ElementRef } from '@angular/core';
import {
Directive,
ElementRef,
Input,
NgZone,
OnInit,
Renderer2
} from '@angular/core';

@Directive({
selector: '[mwlDraggableScrollContainer]'
})
export class DraggableScrollContainerDirective {
constructor(public elementRef: ElementRef<HTMLElement>) {}
export class DraggableScrollContainerDirective implements OnInit {
/**
* Trigger the DragStart after a long touch in scrollable container when true
*/
@Input()
activeLongPressDrag: boolean = false;

/**
* Configuration of a long touch
* Duration in ms of a long touch before activating DragStart
* Delta of the
*/
@Input()
longPressConfig = { duration: 300, delta: 30 };

private cancelledScroll = false;

constructor(
public elementRef: ElementRef<HTMLElement>,
private renderer: Renderer2,
private zone: NgZone
) {}

ngOnInit() {
this.zone.runOutsideAngular(() => {
this.renderer.listen(
this.elementRef.nativeElement,
'touchmove',
(event: TouchEvent) => {
if (this.cancelledScroll && event.cancelable) {
event.preventDefault();
}
}
);
});
}

disableScroll(): void {
this.cancelledScroll = true;
this.renderer.setStyle(this.elementRef.nativeElement, 'overflow', 'hidden');
}

enableScroll(): void {
this.cancelledScroll = false;
this.renderer.setStyle(this.elementRef.nativeElement, 'overflow', 'auto');
}

hasScrollbar(): boolean {
const containerHasHorizontalScroll =
this.elementRef.nativeElement.scrollWidth -
this.elementRef.nativeElement.clientWidth >
0;
const containerHasVerticalScroll =
this.elementRef.nativeElement.scrollHeight -
this.elementRef.nativeElement.clientHeight >
0;
return containerHasHorizontalScroll || containerHasVerticalScroll;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ describe('draggable directive', () => {
@Component({
// tslint:disable-line max-classes-per-file
template: `
<div mwlDraggableScrollContainer>
<div mwlDraggableScrollContainer [activeLongPressDrag]="true">
<div
#draggableElement
mwlDraggable
Expand Down Expand Up @@ -881,4 +881,69 @@ describe('draggable directive', () => {
expect(innerDragFixture.componentInstance.outerDrag).not.to.have.been
.called;
});

const clock = sinon.useFakeTimers();

it('should not start dragging with long touch', () => {
const scrollFixture = TestBed.createComponent(ScrollTestComponent);
scrollFixture.detectChanges();
document.body.appendChild(scrollFixture.nativeElement);
const draggableElement =
scrollFixture.componentInstance.draggableElement.nativeElement;
triggerDomEvent('touchstart', draggableElement, {
touches: [{ clientX: 5, clientY: 10 }]
});
clock.tick(200);

// Touch is too short
triggerDomEvent('touchmove', draggableElement, {
targetTouches: [{ clientX: 5, clientY: 10 }]
});
expect(scrollFixture.componentInstance.dragStart).not.to.have.been.called;

// Touch is too far from touchstart position
clock.tick(200);
triggerDomEvent('touchmove', draggableElement, {
targetTouches: [{ clientX: 30, clientY: 20 }]
});
expect(scrollFixture.componentInstance.dragStart).not.to.have.been.called;

// Scroll begin so drag can't start
clock.tick(400);
scrollFixture.componentInstance.scrollContainer.elementRef.nativeElement.scrollTop = 5;
triggerDomEvent('touchmove', draggableElement, {
targetTouches: [{ clientX: 5, clientY: 5 }]
});
expect(scrollFixture.componentInstance.dragStart).not.to.have.been.called;
triggerDomEvent('touchend', draggableElement, {
changedTouches: [{ clientX: 10, clientY: 18 }]
});
});

it('should start dragging with long touch', () => {
const scrollFixture = TestBed.createComponent(ScrollTestComponent);
scrollFixture.detectChanges();
document.body.appendChild(scrollFixture.nativeElement);
const draggableElement =
scrollFixture.componentInstance.draggableElement.nativeElement;
triggerDomEvent('touchstart', draggableElement, {
touches: [{ clientX: 5, clientY: 10 }]
});
clock.tick(400);
triggerDomEvent('touchmove', draggableElement, {
targetTouches: [{ clientX: 5, clientY: 10 }]
});
expect(scrollFixture.componentInstance.dragStart).to.have.been.calledOnce;

triggerDomEvent('touchmove', draggableElement, {
targetTouches: [{ clientX: 7, clientY: 12 }]
});
expect(scrollFixture.componentInstance.dragging).to.have.been.calledWith({
x: 2,
y: 2
});
triggerDomEvent('touchend', draggableElement, {
changedTouches: [{ clientX: 10, clientY: 18 }]
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ export interface PointerEvent {
event: MouseEvent | TouchEvent;
}

export interface TimeLongPress {
timerBegin: number;
timerEnd: number;
}

@Directive({
selector: '[mwlDraggable]'
})
Expand Down Expand Up @@ -194,6 +199,8 @@ export class DraggableDirective implements OnInit, OnChanges, OnDestroy {

private destroy$ = new Subject();

private timeLongPress: TimeLongPress = { timerBegin: 0, timerEnd: 0 };

/**
* @hidden
*/
Expand All @@ -215,7 +222,7 @@ export class DraggableDirective implements OnInit, OnChanges, OnDestroy {
mergeMap((pointerDownEvent: PointerEvent) => {
// fix for https://github.com/mattlewis92/angular-draggable-droppable/issues/61
// stop mouse events propagating up the chain
if (pointerDownEvent.event.stopPropagation) {
if (pointerDownEvent.event.stopPropagation && !this.scrollContainer) {
pointerDownEvent.event.stopPropagation();
}

Expand Down Expand Up @@ -601,16 +608,49 @@ export class DraggableDirective implements OnInit, OnChanges, OnDestroy {
}

private onTouchStart(event: TouchEvent): void {
if (!this.scrollContainer) {
try {
event.preventDefault();
} catch (e) {}
}
let hasContainerScrollbar: boolean;
let startScrollPosition: any;
let isDragActivated: boolean;
if (this.scrollContainer && this.scrollContainer.activeLongPressDrag) {
this.timeLongPress.timerBegin = Date.now();
isDragActivated = false;
hasContainerScrollbar = this.scrollContainer.hasScrollbar();
startScrollPosition = this.getScrollPosition();
}
if (!this.eventListenerSubscriptions.touchmove) {
this.eventListenerSubscriptions.touchmove = this.renderer.listen(
'document',
'touchmove',
(touchMoveEvent: TouchEvent) => {
this.pointerMove$.next({
event: touchMoveEvent,
clientX: touchMoveEvent.targetTouches[0].clientX,
clientY: touchMoveEvent.targetTouches[0].clientY
});
if (
this.scrollContainer &&
this.scrollContainer.activeLongPressDrag &&
!isDragActivated &&
hasContainerScrollbar
) {
isDragActivated = this.shouldBeginDrag(
event,
touchMoveEvent,
startScrollPosition
);
}
if (
!this.scrollContainer ||
!this.scrollContainer.activeLongPressDrag ||
!hasContainerScrollbar ||
isDragActivated
) {
this.pointerMove$.next({
event: touchMoveEvent,
clientX: touchMoveEvent.targetTouches[0].clientX,
clientY: touchMoveEvent.targetTouches[0].clientY
});
}
}
);
}
Expand All @@ -625,6 +665,9 @@ export class DraggableDirective implements OnInit, OnChanges, OnDestroy {
if (this.eventListenerSubscriptions.touchmove) {
this.eventListenerSubscriptions.touchmove();
delete this.eventListenerSubscriptions.touchmove;
if (this.scrollContainer && this.scrollContainer.activeLongPressDrag) {
this.scrollContainer.enableScroll();
}
}
this.pointerUp$.next({
event,
Expand Down Expand Up @@ -678,4 +721,40 @@ export class DraggableDirective implements OnInit, OnChanges, OnDestroy {
};
}
}

private shouldBeginDrag(
event: TouchEvent,
touchMoveEvent: TouchEvent,
startScrollPosition: any
): boolean {
const moveScrollPosition = this.getScrollPosition();
const deltaScroll = {
top: Math.abs(moveScrollPosition.top - startScrollPosition.top),
left: Math.abs(moveScrollPosition.left - startScrollPosition.left)
};
const deltaX =
Math.abs(
touchMoveEvent.targetTouches[0].clientX - event.touches[0].clientX
) - deltaScroll.left;
const deltaY =
Math.abs(
touchMoveEvent.targetTouches[0].clientY - event.touches[0].clientY
) - deltaScroll.top;
const deltaTotal = deltaX + deltaY;
if (
deltaTotal > this.scrollContainer.longPressConfig.delta ||
deltaScroll.top > 0 ||
deltaScroll.left > 0
) {
this.timeLongPress.timerBegin = Date.now();
}
this.timeLongPress.timerEnd = Date.now();
const duration =
this.timeLongPress.timerEnd - this.timeLongPress.timerBegin;
if (duration >= this.scrollContainer.longPressConfig.duration) {
this.scrollContainer.disableScroll();
return true;
}
return false;
}
}

0 comments on commit f98f586

Please sign in to comment.