Skip to content

Commit 566bf78

Browse files
committed
fix: account for the window being scrolled whilst dragging
1 parent d72e16b commit 566bf78

File tree

3 files changed

+169
-35
lines changed

3 files changed

+169
-35
lines changed

src/draggable-scroll-container.directive.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@ import { Directive, ElementRef } from '@angular/core';
44
selector: '[mwlDraggableScrollContainer]'
55
})
66
export class DraggableScrollContainerDirective {
7-
constructor(public elementRef: ElementRef) {}
7+
constructor(public elementRef: ElementRef<HTMLElement>) {}
88
}

src/draggable.directive.ts

+86-33
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@ import {
1212
SimpleChanges,
1313
Inject,
1414
TemplateRef,
15-
ViewContainerRef
15+
ViewContainerRef,
16+
Optional
1617
} from '@angular/core';
17-
import { Subject, Observable, merge, ReplaySubject } from 'rxjs';
18+
import { Subject, Observable, merge, ReplaySubject, combineLatest } from 'rxjs';
1819
import {
1920
map,
2021
mergeMap,
@@ -24,10 +25,12 @@ import {
2425
pairwise,
2526
share,
2627
filter,
27-
count
28+
count,
29+
startWith
2830
} from 'rxjs/operators';
2931
import { CurrentDragData, DraggableHelper } from './draggable-helper.provider';
3032
import { DOCUMENT } from '@angular/common';
33+
import { DraggableScrollContainerDirective } from './draggable-scroll-container.directive';
3134

3235
export interface Coordinates {
3336
x: number;
@@ -174,6 +177,8 @@ export class DraggableDirective implements OnInit, OnChanges, OnDestroy {
174177

175178
private ghostElement: HTMLElement | null;
176179

180+
private destroy$ = new Subject();
181+
177182
/**
178183
* @hidden
179184
*/
@@ -183,6 +188,7 @@ export class DraggableDirective implements OnInit, OnChanges, OnDestroy {
183188
private draggableHelper: DraggableHelper,
184189
private zone: NgZone,
185190
private vcr: ViewContainerRef,
191+
@Optional() private scrollContainer: DraggableScrollContainerDirective,
186192
@Inject(DOCUMENT) private document: any
187193
) {}
188194

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

219+
const startScrollPosition = this.getScrollPosition();
220+
221+
const scrollContainerScroll$ = new Observable(observer => {
222+
const scrollContainer = this.scrollContainer
223+
? this.scrollContainer.elementRef.nativeElement
224+
: 'window';
225+
return this.renderer.listen(scrollContainer, 'scroll', e =>
226+
observer.next(e)
227+
);
228+
}).pipe(
229+
startWith(startScrollPosition),
230+
map(() => this.getScrollPosition())
231+
);
232+
213233
const currentDrag$ = new Subject<CurrentDragData>();
214234
const cancelDrag$ = new ReplaySubject<void>();
215235

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

220-
const pointerMove = this.pointerMove.pipe(
221-
map((pointerMoveEvent: PointerEvent) => {
240+
const pointerMove = combineLatest<
241+
PointerEvent,
242+
{ top: number; left: number }
243+
>(this.pointerMove, scrollContainerScroll$).pipe(
244+
map(([pointerMoveEvent, scroll]) => {
222245
return {
223246
currentDrag$,
224-
x: pointerMoveEvent.clientX - pointerDownEvent.clientX,
225-
y: pointerMoveEvent.clientY - pointerDownEvent.clientY,
247+
transformX: pointerMoveEvent.clientX - pointerDownEvent.clientX,
248+
transformY: pointerMoveEvent.clientY - pointerDownEvent.clientY,
226249
clientX: pointerMoveEvent.clientX,
227-
clientY: pointerMoveEvent.clientY
250+
clientY: pointerMoveEvent.clientY,
251+
scrollLeft: scroll.left,
252+
scrollTop: scroll.top
228253
};
229254
}),
230255
map(moveData => {
231256
if (this.dragSnapGrid.x) {
232-
moveData.x =
233-
Math.round(moveData.x / this.dragSnapGrid.x) *
257+
moveData.transformX =
258+
Math.round(moveData.transformX / this.dragSnapGrid.x) *
234259
this.dragSnapGrid.x;
235260
}
236261

237262
if (this.dragSnapGrid.y) {
238-
moveData.y =
239-
Math.round(moveData.y / this.dragSnapGrid.y) *
263+
moveData.transformY =
264+
Math.round(moveData.transformY / this.dragSnapGrid.y) *
240265
this.dragSnapGrid.y;
241266
}
242267

243268
return moveData;
244269
}),
245270
map(moveData => {
246271
if (!this.dragAxis.x) {
247-
moveData.x = 0;
272+
moveData.transformX = 0;
248273
}
249274

250275
if (!this.dragAxis.y) {
251-
moveData.y = 0;
276+
moveData.transformY = 0;
252277
}
253278

254279
return moveData;
255280
}),
281+
map(moveData => {
282+
const scrollX = moveData.scrollLeft - startScrollPosition.left;
283+
const scrollY = moveData.scrollTop - startScrollPosition.top;
284+
return {
285+
...moveData,
286+
x: moveData.transformX + scrollX,
287+
y: moveData.transformY + scrollY
288+
};
289+
}),
256290
filter(
257291
({ x, y }) => !this.validateDrag || this.validateDrag({ x, y })
258292
),
259-
takeUntil(merge(this.pointerUp, this.pointerDown, cancelDrag$)),
293+
takeUntil(
294+
merge(this.pointerUp, this.pointerDown, cancelDrag$, this.destroy$)
295+
),
260296
share()
261297
);
262298

@@ -394,26 +430,28 @@ export class DraggableDirective implements OnInit, OnChanges, OnDestroy {
394430
}),
395431
map(([previous, next]) => next)
396432
)
397-
.subscribe(({ x, y, currentDrag$, clientX, clientY }) => {
398-
this.zone.run(() => {
399-
this.dragging.next({ x, y });
400-
});
401-
if (this.ghostElement) {
402-
const transform = `translate(${x}px, ${y}px)`;
403-
this.setElementStyles(this.ghostElement, {
404-
transform,
405-
'-webkit-transform': transform,
406-
'-ms-transform': transform,
407-
'-moz-transform': transform,
408-
'-o-transform': transform
433+
.subscribe(
434+
({ x, y, currentDrag$, clientX, clientY, transformX, transformY }) => {
435+
this.zone.run(() => {
436+
this.dragging.next({ x, y });
437+
});
438+
if (this.ghostElement) {
439+
const transform = `translate(${transformX}px, ${transformY}px)`;
440+
this.setElementStyles(this.ghostElement, {
441+
transform,
442+
'-webkit-transform': transform,
443+
'-ms-transform': transform,
444+
'-moz-transform': transform,
445+
'-o-transform': transform
446+
});
447+
}
448+
currentDrag$.next({
449+
clientX,
450+
clientY,
451+
dropData: this.dropData
409452
});
410453
}
411-
currentDrag$.next({
412-
clientX,
413-
clientY,
414-
dropData: this.dropData
415-
});
416-
});
454+
);
417455
}
418456

419457
ngOnChanges(changes: SimpleChanges): void {
@@ -427,6 +465,7 @@ export class DraggableDirective implements OnInit, OnChanges, OnDestroy {
427465
this.pointerDown.complete();
428466
this.pointerMove.complete();
429467
this.pointerUp.complete();
468+
this.destroy$.next();
430469
}
431470

432471
private checkEventListeners(): void {
@@ -594,4 +633,18 @@ export class DraggableDirective implements OnInit, OnChanges, OnDestroy {
594633
this.renderer.setStyle(element, key, styles[key]);
595634
});
596635
}
636+
637+
private getScrollPosition() {
638+
if (this.scrollContainer) {
639+
return {
640+
top: this.scrollContainer.elementRef.nativeElement.scrollTop,
641+
left: this.scrollContainer.elementRef.nativeElement.scrollLeft
642+
};
643+
} else {
644+
return {
645+
top: window.pageYOffset || document.documentElement.scrollTop,
646+
left: window.pageXOffset || document.documentElement.scrollLeft
647+
};
648+
}
649+
}
597650
}

test/draggable.directive.spec.ts

+82-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import * as sinon from 'sinon';
55
import { triggerDomEvent } from './util';
66
import { DragAndDropModule } from '../src/index';
77
import { DraggableDirective, ValidateDrag } from '../src/draggable.directive';
8+
import { DraggableScrollContainerDirective } from '../src/draggable-scroll-container.directive';
9+
import { By } from '@angular/platform-browser';
810

911
describe('draggable directive', () => {
1012
@Component({
@@ -54,10 +56,50 @@ describe('draggable directive', () => {
5456
ghostElementTemplate: TemplateRef<any>;
5557
}
5658

59+
@Component({
60+
// tslint:disable-line max-classes-per-file
61+
template: `
62+
<div mwlDraggableScrollContainer>
63+
<div
64+
#draggableElement
65+
mwlDraggable
66+
[dragAxis]="{x: true, y: true}"
67+
(dragPointerDown)="dragPointerDown($event)"
68+
(dragStart)="dragStart($event)"
69+
(ghostElementCreated)="ghostElementCreated($event)"
70+
(dragging)="dragging($event)"
71+
(dragEnd)="dragEnd($event)">
72+
Drag me!
73+
</div>
74+
</div>
75+
`,
76+
styles: [
77+
`
78+
[mwlDraggableScrollContainer] {
79+
height: 25px;
80+
overflow: scroll;
81+
position: fixed;
82+
top: 0;
83+
left: 0;
84+
}
85+
[mwlDraggable] {
86+
position: relative;
87+
width: 50px;
88+
height: 50px;
89+
z-index: 1;
90+
}
91+
`
92+
]
93+
})
94+
class ScrollTestComponent extends TestComponent {
95+
@ViewChild(DraggableScrollContainerDirective)
96+
scrollContainer: DraggableScrollContainerDirective;
97+
}
98+
5799
beforeEach(() => {
58100
TestBed.configureTestingModule({
59101
imports: [DragAndDropModule],
60-
declarations: [TestComponent]
102+
declarations: [TestComponent, ScrollTestComponent]
61103
});
62104
});
63105

@@ -723,4 +765,43 @@ describe('draggable directive', () => {
723765
const ghostElement = draggableElement.nextSibling as HTMLElement;
724766
expect(ghostElement.innerHTML).to.equal('<span>2 test</span>');
725767
});
768+
769+
it('should handle the parent element being scrolled while dragging', () => {
770+
const scrollFixture = TestBed.createComponent(ScrollTestComponent);
771+
scrollFixture.detectChanges();
772+
document.body.appendChild(scrollFixture.nativeElement);
773+
const draggableElement =
774+
scrollFixture.componentInstance.draggableElement.nativeElement;
775+
triggerDomEvent('mousedown', draggableElement, { clientX: 5, clientY: 10 });
776+
expect(
777+
scrollFixture.componentInstance.dragPointerDown
778+
).to.have.been.calledWith({
779+
x: 0,
780+
y: 0
781+
});
782+
triggerDomEvent('mousemove', draggableElement, { clientX: 5, clientY: 12 });
783+
expect(scrollFixture.componentInstance.dragStart).to.have.been.calledOnce;
784+
expect(scrollFixture.componentInstance.dragging).to.have.been.calledWith({
785+
x: 0,
786+
y: 2
787+
});
788+
const ghostElement = draggableElement.nextSibling as HTMLElement;
789+
expect(ghostElement.style.transform).to.equal('translate(0px, 2px)');
790+
scrollFixture.componentInstance.scrollContainer.elementRef.nativeElement.scrollTop = 5;
791+
scrollFixture.debugElement
792+
.query(By.directive(DraggableScrollContainerDirective))
793+
.triggerEventHandler('scroll', {});
794+
triggerDomEvent('mousemove', draggableElement, { clientX: 5, clientY: 14 });
795+
expect(scrollFixture.componentInstance.dragging).to.have.been.calledWith({
796+
x: 0,
797+
y: 9
798+
});
799+
expect(ghostElement.style.transform).to.equal('translate(0px, 4px)');
800+
triggerDomEvent('mouseup', draggableElement, { clientX: 5, clientY: 14 });
801+
expect(scrollFixture.componentInstance.dragEnd).to.have.been.calledWith({
802+
x: 0,
803+
y: 9,
804+
dragCancelled: false
805+
});
806+
});
726807
});

0 commit comments

Comments
 (0)