Skip to content

Commit

Permalink
feat: allow setting drag start delay on touch devices
Browse files Browse the repository at this point in the history
  • Loading branch information
mattlewis92 committed Apr 18, 2020
1 parent aaaffe6 commit c9b28a5
Show file tree
Hide file tree
Showing 4 changed files with 110 additions and 112 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,74 +18,23 @@ import {
@Directive({
selector: '[mwlDraggableScrollContainer]'
})
export class DraggableScrollContainerDirective implements OnInit {
export class DraggableScrollContainerDirective {
/**
* Trigger the DragStart after a long touch in scrollable container when true
* @deprecated will be removed in v5 (use [touchStartLongPress]="{delay: 300, delta: 30}" on the mwlDraggable element instead)
*/
@Input()
activeLongPressDrag: boolean = false;
@Input() activeLongPressDrag: boolean = false;

/**
* Configuration of a long touch
* Duration in ms of a long touch before activating DragStart
* Delta of the
* @deprecated will be removed in v5 (use [touchStartLongPress]="{delay: 300, delta: 30}" on the mwlDraggable element instead)
*/
@Input()
longPressConfig = { duration: 300, delta: 30 };

private cancelledScroll = false;

/**
* @hidden
*/
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();
}
}
);
});
}

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

/**
* @hidden
*/
enableScroll(): void {
this.cancelledScroll = false;
this.renderer.setStyle(this.elementRef.nativeElement, 'overflow', 'auto');
}
@Input() longPressConfig = { duration: 300, delta: 30 };

/**
* @hidden
*/
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;
}
constructor(public elementRef: ElementRef<HTMLElement>) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,13 @@ describe('draggable directive', () => {
@Component({
// tslint:disable-line max-classes-per-file
template: `
<div mwlDraggableScrollContainer [activeLongPressDrag]="true">
<div mwlDraggableScrollContainer>
<div
#draggableElement
mwlDraggable
[dragAxis]="{ x: true, y: true }"
[validateDrag]="validateDrag"
[touchStartLongPress]="{ delay: 300, delta: 30 }"
(dragPointerDown)="dragPointerDown($event)"
(dragStart)="dragStart($event)"
(ghostElementCreated)="ghostElementCreated($event)"
Expand Down Expand Up @@ -1227,4 +1228,30 @@ describe('draggable directive', () => {
});
expect(fixture.componentInstance.dragEnd).not.to.have.been.called;
});

it('should disable right click events on touch events', () => {
const scrollFixture = TestBed.createComponent(ScrollTestComponent);
scrollFixture.detectChanges();
document.body.appendChild(scrollFixture.nativeElement);
const draggableElement =
scrollFixture.componentInstance.draggableElement.nativeElement;

const preventDefault = sinon.spy();

triggerDomEvent('contextmenu', document.body, {
preventDefault
});

expect(preventDefault).not.to.have.been.called;

triggerDomEvent('touchstart', draggableElement, {
touches: [{ clientX: 5, clientY: 10 }]
});

triggerDomEvent('contextmenu', document.body, {
preventDefault
});

expect(preventDefault).to.have.been.calledOnce;
});
});
106 changes: 59 additions & 47 deletions projects/angular-draggable-droppable/src/lib/draggable.directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ import {
merge,
ReplaySubject,
combineLatest,
animationFrameScheduler
animationFrameScheduler,
fromEvent
} from 'rxjs';
import {
map,
Expand Down Expand Up @@ -159,6 +160,12 @@ export class DraggableDirective implements OnInit, OnChanges, OnDestroy {
@Input()
ghostElementTemplate: TemplateRef<any>;

/**
* Amount of milliseconds to wait on touch devices before starting to drag the element (so that you can scroll the page by touching a draggable element)
*/
@Input()
touchStartLongPress: { delay: number; delta: number };

/**
* Called when the element can be dragged along one axis and has the mouse or pointer device pressed on it
*/
Expand Down Expand Up @@ -649,51 +656,56 @@ 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) {
if (
(this.scrollContainer && this.scrollContainer.activeLongPressDrag) ||
this.touchStartLongPress
) {
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) => {
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
});
}
const contextMenuListener = fromEvent(document, 'contextmenu').subscribe(
e => {
e.preventDefault();
}
);

const touchMoveListener = fromEvent<TouchEvent>(document, 'touchmove', {
passive: false
}).subscribe(touchMoveEvent => {
if (
((this.scrollContainer && this.scrollContainer.activeLongPressDrag) ||
this.touchStartLongPress) &&
!isDragActivated
) {
isDragActivated = this.shouldBeginDrag(
event,
touchMoveEvent,
startScrollPosition
);
}
if (
((!this.scrollContainer ||
!this.scrollContainer.activeLongPressDrag) &&
!this.touchStartLongPress) ||
isDragActivated
) {
touchMoveEvent.preventDefault();
this.pointerMove$.next({
event: touchMoveEvent,
clientX: touchMoveEvent.targetTouches[0].clientX,
clientY: touchMoveEvent.targetTouches[0].clientY
});
}
});

this.eventListenerSubscriptions.touchmove = () => {
contextMenuListener.unsubscribe();
touchMoveListener.unsubscribe();
};
}
this.pointerDown$.next({
event,
Expand All @@ -706,9 +718,6 @@ 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 @@ -768,7 +777,7 @@ export class DraggableDirective implements OnInit, OnChanges, OnDestroy {
private shouldBeginDrag(
event: TouchEvent,
touchMoveEvent: TouchEvent,
startScrollPosition: any
startScrollPosition: { top: number; left: number }
): boolean {
const moveScrollPosition = this.getScrollPosition();
const deltaScroll = {
Expand All @@ -784,8 +793,15 @@ export class DraggableDirective implements OnInit, OnChanges, OnDestroy {
touchMoveEvent.targetTouches[0].clientY - event.touches[0].clientY
) - deltaScroll.top;
const deltaTotal = deltaX + deltaY;
const longPressConfig = this.touchStartLongPress
? this.touchStartLongPress
: /* istanbul ignore next */
{
delta: this.scrollContainer.longPressConfig.delta,
delay: this.scrollContainer.longPressConfig.duration
};
if (
deltaTotal > this.scrollContainer.longPressConfig.delta ||
deltaTotal > longPressConfig.delta ||
deltaScroll.top > 0 ||
deltaScroll.left > 0
) {
Expand All @@ -794,10 +810,6 @@ export class DraggableDirective implements OnInit, OnChanges, OnDestroy {
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;
return duration >= longPressConfig.delay;
}
}
24 changes: 17 additions & 7 deletions src/demo/demo.component.html
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
<div mwlDraggable dropData="foo" dragActiveClass="drag-active">
<div
mwlDraggable
dropData="foo"
dragActiveClass="drag-active"
[touchStartLongPress]="{ delay: 300, delta: 30 }"
>
Drag me!
</div>
<div mwlDraggable dropData="bar" dragActiveClass="drag-active" [dragSnapGrid]="{x: 100, y: 100}">
<div
mwlDraggable
dropData="bar"
dragActiveClass="drag-active"
[dragSnapGrid]="{ x: 100, y: 100 }"
[touchStartLongPress]="{ delay: 300, delta: 30 }"
>
I snap to a 100 x 100 grid
</div>
<div
mwlDroppable
(drop)="onDrop($event)"
dragOverClass="drop-over-active">
<div mwlDroppable (drop)="onDrop($event)" dragOverClass="drop-over-active">
<span [hidden]="droppedData">Drop here</span>
<span [hidden]="!droppedData">Item dropped here with data: "{{ droppedData }}"!</span>
<span [hidden]="!droppedData"
>Item dropped here with data: "{{ droppedData }}"!</span
>
</div>

0 comments on commit c9b28a5

Please sign in to comment.