Skip to content

Commit

Permalink
fix: account for the window being scrolled whilst dragging
Browse files Browse the repository at this point in the history
  • Loading branch information
mattlewis92 committed Jun 29, 2018
1 parent d72e16b commit 566bf78
Show file tree
Hide file tree
Showing 3 changed files with 169 additions and 35 deletions.
2 changes: 1 addition & 1 deletion src/draggable-scroll-container.directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ import { Directive, ElementRef } from '@angular/core';
selector: '[mwlDraggableScrollContainer]'
})
export class DraggableScrollContainerDirective {
constructor(public elementRef: ElementRef) {}
constructor(public elementRef: ElementRef<HTMLElement>) {}
}
119 changes: 86 additions & 33 deletions src/draggable.directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ import {
SimpleChanges,
Inject,
TemplateRef,
ViewContainerRef
ViewContainerRef,
Optional
} from '@angular/core';
import { Subject, Observable, merge, ReplaySubject } from 'rxjs';
import { Subject, Observable, merge, ReplaySubject, combineLatest } from 'rxjs';
import {
map,
mergeMap,
Expand All @@ -24,10 +25,12 @@ import {
pairwise,
share,
filter,
count
count,
startWith
} from 'rxjs/operators';
import { CurrentDragData, DraggableHelper } from './draggable-helper.provider';
import { DOCUMENT } from '@angular/common';
import { DraggableScrollContainerDirective } from './draggable-scroll-container.directive';

export interface Coordinates {
x: number;
Expand Down Expand Up @@ -174,6 +177,8 @@ export class DraggableDirective implements OnInit, OnChanges, OnDestroy {

private ghostElement: HTMLElement | null;

private destroy$ = new Subject();

/**
* @hidden
*/
Expand All @@ -183,6 +188,7 @@ export class DraggableDirective implements OnInit, OnChanges, OnDestroy {
private draggableHelper: DraggableHelper,
private zone: NgZone,
private vcr: ViewContainerRef,
@Optional() private scrollContainer: DraggableScrollContainerDirective,
@Inject(DOCUMENT) private document: any
) {}

Expand Down Expand Up @@ -210,53 +216,83 @@ export class DraggableDirective implements OnInit, OnChanges, OnDestroy {
);
this.document.head.appendChild(globalDragStyle);

const startScrollPosition = this.getScrollPosition();

const scrollContainerScroll$ = new Observable(observer => {
const scrollContainer = this.scrollContainer
? this.scrollContainer.elementRef.nativeElement
: 'window';
return this.renderer.listen(scrollContainer, 'scroll', e =>
observer.next(e)
);
}).pipe(
startWith(startScrollPosition),
map(() => this.getScrollPosition())
);

const currentDrag$ = new Subject<CurrentDragData>();
const cancelDrag$ = new ReplaySubject<void>();

this.zone.run(() => {
this.dragPointerDown.next({ x: 0, y: 0 });
});

const pointerMove = this.pointerMove.pipe(
map((pointerMoveEvent: PointerEvent) => {
const pointerMove = combineLatest<
PointerEvent,
{ top: number; left: number }
>(this.pointerMove, scrollContainerScroll$).pipe(
map(([pointerMoveEvent, scroll]) => {
return {
currentDrag$,
x: pointerMoveEvent.clientX - pointerDownEvent.clientX,
y: pointerMoveEvent.clientY - pointerDownEvent.clientY,
transformX: pointerMoveEvent.clientX - pointerDownEvent.clientX,
transformY: pointerMoveEvent.clientY - pointerDownEvent.clientY,
clientX: pointerMoveEvent.clientX,
clientY: pointerMoveEvent.clientY
clientY: pointerMoveEvent.clientY,
scrollLeft: scroll.left,
scrollTop: scroll.top
};
}),
map(moveData => {
if (this.dragSnapGrid.x) {
moveData.x =
Math.round(moveData.x / this.dragSnapGrid.x) *
moveData.transformX =
Math.round(moveData.transformX / this.dragSnapGrid.x) *
this.dragSnapGrid.x;
}

if (this.dragSnapGrid.y) {
moveData.y =
Math.round(moveData.y / this.dragSnapGrid.y) *
moveData.transformY =
Math.round(moveData.transformY / this.dragSnapGrid.y) *
this.dragSnapGrid.y;
}

return moveData;
}),
map(moveData => {
if (!this.dragAxis.x) {
moveData.x = 0;
moveData.transformX = 0;
}

if (!this.dragAxis.y) {
moveData.y = 0;
moveData.transformY = 0;
}

return moveData;
}),
map(moveData => {
const scrollX = moveData.scrollLeft - startScrollPosition.left;
const scrollY = moveData.scrollTop - startScrollPosition.top;
return {
...moveData,
x: moveData.transformX + scrollX,
y: moveData.transformY + scrollY
};
}),
filter(
({ x, y }) => !this.validateDrag || this.validateDrag({ x, y })
),
takeUntil(merge(this.pointerUp, this.pointerDown, cancelDrag$)),
takeUntil(
merge(this.pointerUp, this.pointerDown, cancelDrag$, this.destroy$)
),
share()
);

Expand Down Expand Up @@ -394,26 +430,28 @@ export class DraggableDirective implements OnInit, OnChanges, OnDestroy {
}),
map(([previous, next]) => next)
)
.subscribe(({ x, y, currentDrag$, clientX, clientY }) => {
this.zone.run(() => {
this.dragging.next({ x, y });
});
if (this.ghostElement) {
const transform = `translate(${x}px, ${y}px)`;
this.setElementStyles(this.ghostElement, {
transform,
'-webkit-transform': transform,
'-ms-transform': transform,
'-moz-transform': transform,
'-o-transform': transform
.subscribe(
({ x, y, currentDrag$, clientX, clientY, transformX, transformY }) => {
this.zone.run(() => {
this.dragging.next({ x, y });
});
if (this.ghostElement) {
const transform = `translate(${transformX}px, ${transformY}px)`;
this.setElementStyles(this.ghostElement, {
transform,
'-webkit-transform': transform,
'-ms-transform': transform,
'-moz-transform': transform,
'-o-transform': transform
});
}
currentDrag$.next({
clientX,
clientY,
dropData: this.dropData
});
}
currentDrag$.next({
clientX,
clientY,
dropData: this.dropData
});
});
);
}

ngOnChanges(changes: SimpleChanges): void {
Expand All @@ -427,6 +465,7 @@ export class DraggableDirective implements OnInit, OnChanges, OnDestroy {
this.pointerDown.complete();
this.pointerMove.complete();
this.pointerUp.complete();
this.destroy$.next();
}

private checkEventListeners(): void {
Expand Down Expand Up @@ -594,4 +633,18 @@ export class DraggableDirective implements OnInit, OnChanges, OnDestroy {
this.renderer.setStyle(element, key, styles[key]);
});
}

private getScrollPosition() {
if (this.scrollContainer) {
return {
top: this.scrollContainer.elementRef.nativeElement.scrollTop,
left: this.scrollContainer.elementRef.nativeElement.scrollLeft
};
} else {
return {
top: window.pageYOffset || document.documentElement.scrollTop,
left: window.pageXOffset || document.documentElement.scrollLeft
};
}
}
}
83 changes: 82 additions & 1 deletion test/draggable.directive.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import * as sinon from 'sinon';
import { triggerDomEvent } from './util';
import { DragAndDropModule } from '../src/index';
import { DraggableDirective, ValidateDrag } from '../src/draggable.directive';
import { DraggableScrollContainerDirective } from '../src/draggable-scroll-container.directive';
import { By } from '@angular/platform-browser';

describe('draggable directive', () => {
@Component({
Expand Down Expand Up @@ -54,10 +56,50 @@ describe('draggable directive', () => {
ghostElementTemplate: TemplateRef<any>;
}

@Component({
// tslint:disable-line max-classes-per-file
template: `
<div mwlDraggableScrollContainer>
<div
#draggableElement
mwlDraggable
[dragAxis]="{x: true, y: true}"
(dragPointerDown)="dragPointerDown($event)"
(dragStart)="dragStart($event)"
(ghostElementCreated)="ghostElementCreated($event)"
(dragging)="dragging($event)"
(dragEnd)="dragEnd($event)">
Drag me!
</div>
</div>
`,
styles: [
`
[mwlDraggableScrollContainer] {
height: 25px;
overflow: scroll;
position: fixed;
top: 0;
left: 0;
}
[mwlDraggable] {
position: relative;
width: 50px;
height: 50px;
z-index: 1;
}
`
]
})
class ScrollTestComponent extends TestComponent {
@ViewChild(DraggableScrollContainerDirective)
scrollContainer: DraggableScrollContainerDirective;
}

beforeEach(() => {
TestBed.configureTestingModule({
imports: [DragAndDropModule],
declarations: [TestComponent]
declarations: [TestComponent, ScrollTestComponent]
});
});

Expand Down Expand Up @@ -723,4 +765,43 @@ describe('draggable directive', () => {
const ghostElement = draggableElement.nextSibling as HTMLElement;
expect(ghostElement.innerHTML).to.equal('<span>2 test</span>');
});

it('should handle the parent element being scrolled while dragging', () => {
const scrollFixture = TestBed.createComponent(ScrollTestComponent);
scrollFixture.detectChanges();
document.body.appendChild(scrollFixture.nativeElement);
const draggableElement =
scrollFixture.componentInstance.draggableElement.nativeElement;
triggerDomEvent('mousedown', draggableElement, { clientX: 5, clientY: 10 });
expect(
scrollFixture.componentInstance.dragPointerDown
).to.have.been.calledWith({
x: 0,
y: 0
});
triggerDomEvent('mousemove', draggableElement, { clientX: 5, clientY: 12 });
expect(scrollFixture.componentInstance.dragStart).to.have.been.calledOnce;
expect(scrollFixture.componentInstance.dragging).to.have.been.calledWith({
x: 0,
y: 2
});
const ghostElement = draggableElement.nextSibling as HTMLElement;
expect(ghostElement.style.transform).to.equal('translate(0px, 2px)');
scrollFixture.componentInstance.scrollContainer.elementRef.nativeElement.scrollTop = 5;
scrollFixture.debugElement
.query(By.directive(DraggableScrollContainerDirective))
.triggerEventHandler('scroll', {});
triggerDomEvent('mousemove', draggableElement, { clientX: 5, clientY: 14 });
expect(scrollFixture.componentInstance.dragging).to.have.been.calledWith({
x: 0,
y: 9
});
expect(ghostElement.style.transform).to.equal('translate(0px, 4px)');
triggerDomEvent('mouseup', draggableElement, { clientX: 5, clientY: 14 });
expect(scrollFixture.componentInstance.dragEnd).to.have.been.calledWith({
x: 0,
y: 9,
dragCancelled: false
});
});
});

0 comments on commit 566bf78

Please sign in to comment.