Skip to content

Commit 53323cb

Browse files
committed
feat(drag-n-drop): wip, TODO Drag outside weird snap
1 parent 950ee44 commit 53323cb

File tree

2 files changed

+122
-70
lines changed

2 files changed

+122
-70
lines changed

projects/angular-grid-layout/src/lib/grid.component.ts

Lines changed: 105 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {
2121
import { coerceNumberProperty, NumberInput } from './coercion/number-property';
2222
import { KtdGridItemComponent} from './grid-item/grid-item.component';
2323
import { combineLatest, merge, NEVER, Observable, of, Subscription} from 'rxjs';
24-
import { exhaustMap, map, startWith, switchMap, takeUntil} from 'rxjs/operators';
24+
import {exhaustMap, map, startWith, switchMap, takeUntil} from 'rxjs/operators';
2525
import {
2626
ktdGetGridItemRowHeight,
2727
ktdGridItemDragging, ktdGridItemLayoutItemAreEqual,
@@ -40,7 +40,7 @@ import {
4040
} from './grid.definitions';
4141
import { ktdPointerClientX, ktdPointerClientY } from './utils/pointer.utils';
4242
import { KtdDictionary } from '../types';
43-
import { KtdGridService } from './grid.service';
43+
import {KtdGridService, PointerEventInfo} from './grid.service';
4444
import { getMutableClientRect, KtdClientRect } from './utils/client-rect';
4545
import { ktdGetScrollTotalRelativeDifference$, ktdScrollIfNearElementClientRect$ } from './utils/scroll';
4646
import { BooleanInput, coerceBooleanProperty } from './coercion/boolean-property';
@@ -50,6 +50,7 @@ import {KtdRegistryService} from "./ktd-registry.service";
5050
import {DragRef} from "./utils/drag-ref";
5151
import {KtdDrag} from "./directives/ktd-drag";
5252

53+
// region Types
5354

5455
interface KtdGridDrag {
5556
dragSubscription: Subscription;
@@ -178,6 +179,8 @@ const defaultBackgroundConfig: Required<Omit<KtdGridBackgroundCfg, 'show'>> = {
178179
borderWidth: 1,
179180
};
180181

182+
// endregion
183+
181184
@Component({
182185
selector: 'ktd-grid',
183186
templateUrl: './grid.component.html',
@@ -193,6 +196,7 @@ const defaultBackgroundConfig: Required<Omit<KtdGridBackgroundCfg, 'show'>> = {
193196
]
194197
})
195198
export class KtdGridComponent implements OnChanges, AfterContentInit, AfterContentChecked, OnDestroy {
199+
// region Parameters
196200
private static _nextUniqueId: number = 0;
197201

198202
/** Query list of grid items that are being rendered. */
@@ -368,6 +372,8 @@ export class KtdGridComponent implements OnChanges, AfterContentInit, AfterConte
368372

369373
private readonly gridElement: HTMLElement;
370374

375+
// endregion
376+
371377
constructor(private gridService: KtdGridService,
372378
private ktdRegistryService: KtdRegistryService,
373379
private elementRef: ElementRef,
@@ -499,7 +505,7 @@ export class KtdGridComponent implements OnChanges, AfterContentInit, AfterConte
499505
}
500506

501507
private initSubscriptions() {
502-
const connectedToItems$ = this.ktdRegistryService.getKtdDragItemsConnectedToGrid(this);
508+
const itemsConnectedToGrid$ = this.ktdRegistryService.getKtdDragItemsConnectedToGrid(this);
503509
this.subscriptions = [
504510
this._gridItems.changes.pipe(
505511
startWith(this._gridItems),
@@ -533,8 +539,8 @@ export class KtdGridComponent implements OnChanges, AfterContentInit, AfterConte
533539
return this.gridService.startDrag(event, gridItem.dragRef, type, this);
534540
}),
535541

536-
connectedToItems$.pipe(
537-
startWith(connectedToItems$.value),
542+
itemsConnectedToGrid$.pipe(
543+
startWith(itemsConnectedToGrid$.value),
538544
switchMap((draggableItems) => {
539545
return merge(
540546
...draggableItems.map((draggableItem) => draggableItem.dragStart.pipe(
@@ -547,27 +553,94 @@ export class KtdGridComponent implements OnChanges, AfterContentInit, AfterConte
547553
}),
548554

549555
this.dragEntered.subscribe(({event, }) => {
550-
this.startDragSequenceOld(event);
556+
this.startRestoreDragSequence(event);
551557
}),
552558
this.dragExited.subscribe(() => {
553-
this.stopDragSequence();
559+
this.pauseDragSequence();
560+
}),
561+
this.gridService.pointerBeforeEnd$.subscribe(({dragInfo}) => {
562+
if (this.drag !== null && dragInfo !== null) {
563+
this.updateLayout(dragInfo);
564+
this.stopDragSequence(dragInfo);
565+
}
566+
console.log(this.drag, dragInfo);
554567
}),
555568
this.gridService.pointerEnd$.subscribe(() => {
556-
this.stopDragSequence();
557-
})
569+
this.drag = null;
570+
}),
558571
];
559572
}
560573

561-
private startDragSequenceOld(event: PointingDeviceEvent): void {
574+
/**
575+
* Starts the drag sequence when a drag event is triggered. It will restore paused drag sequence if it's already started.
576+
* @param event The event that triggered the drag sequence.
577+
*/
578+
private startRestoreDragSequence(event: PointingDeviceEvent): void {
562579
const dragInfo = this.gridService.drag!;
580+
const scrollableParent = typeof this.scrollableParent === 'string' ? document.getElementById(this.scrollableParent) : this.scrollableParent;
581+
582+
// TODO (enhancement): consider move this 'side effect' observable inside the main drag loop.
583+
// - Pros are that we would not repeat subscriptions and takeUntil would shut down observables at the same time.
584+
// - Cons are that moving this functionality as a side effect inside the main drag loop would be confusing.
585+
const scrollSubscription = this.ngZone.runOutsideAngular(() =>
586+
(!scrollableParent ? NEVER : this.gridService.pointerMove$.pipe(
587+
map((event) => ({
588+
pointerX: ktdPointerClientX(event),
589+
pointerY: ktdPointerClientY(event)
590+
})),
591+
ktdScrollIfNearElementClientRect$(scrollableParent, {scrollStep: this.scrollSpeed})
592+
)).pipe(
593+
takeUntil(this.gridService.pointerEnd$),
594+
).subscribe());
595+
596+
if (this.drag != null) {
597+
this.drag.dragSubscription = this.createDragResizeLoop(scrollableParent, dragInfo);
598+
this.drag.scrollSubscription = scrollSubscription;
599+
return;
600+
}
601+
602+
this.drag = {
603+
dragSubscription: this.createDragResizeLoop(scrollableParent, dragInfo),
604+
scrollSubscription,
605+
startEvent: event,
606+
newLayout: null,
607+
newLayoutItem: dragInfo.dragRef.itemRef instanceof KtdDrag ? {
608+
id: dragInfo.dragRef.id,
609+
w: 1,
610+
h: 1,
611+
x: -1,
612+
y: -1,
613+
} : null,
614+
};
615+
}
616+
617+
private pauseDragSequence(): void {
618+
const dragInfo = this.gridService.drag!;
619+
620+
// If the drag is a resize, we don't need to pause the drag sequence.
621+
if (dragInfo.type === 'resize') {
622+
return;
623+
}
624+
625+
if (this.drag != null) {
626+
this.drag.dragSubscription.unsubscribe();
627+
this.drag.scrollSubscription.unsubscribe();
628+
this.destroyPlaceholder();
629+
}
630+
}
631+
632+
/**
633+
* Creates the drag loop. It listens for 'pointer move' and 'scroll' events and recalculates the layout on each emission.
634+
* @param scrollableParent The parent element that contains the scroll.
635+
* @param dragInfo The drag info.
636+
*/
637+
private createDragResizeLoop(scrollableParent: HTMLElement | Document | null, dragInfo: PointerEventInfo): Subscription {
563638
let renderData: KtdGridItemRenderData<number> | null = null;
564639

565640
// Retrieve grid (parent) and gridItem (draggedElem) client rects.
566641
const gridElemClientRect: KtdClientRect = getMutableClientRect(this.gridElement);
567642
const dragElemClientRect: KtdClientRect = getMutableClientRect(dragInfo.dragRef.elementRef.nativeElement as HTMLElement);
568643

569-
const scrollableParent = typeof this.scrollableParent === 'string' ? document.getElementById(this.scrollableParent) : this.scrollableParent;
570-
571644
this.renderer.addClass(dragInfo.dragRef.elementRef.nativeElement, 'no-transitions');
572645
this.renderer.addClass(dragInfo.dragRef.elementRef.nativeElement, 'ktd-grid-item-dragging');
573646

@@ -579,22 +652,8 @@ export class KtdGridComponent implements OnChanges, AfterContentInit, AfterConte
579652

580653
this.createPlaceholderElement(placeholderClientRect, dragInfo.dragRef.placeholder);
581654

582-
// TODO (enhancement): consider move this 'side effect' observable inside the main drag loop.
583-
// - Pros are that we would not repeat subscriptions and takeUntil would shut down observables at the same time.
584-
// - Cons are that moving this functionality as a side effect inside the main drag loop would be confusing.
585-
const scrollSubscription = this.ngZone.runOutsideAngular(() =>
586-
(!scrollableParent ? NEVER : this.gridService.pointerMove$.pipe(
587-
map((event) => ({
588-
pointerX: ktdPointerClientX(event),
589-
pointerY: ktdPointerClientY(event)
590-
})),
591-
ktdScrollIfNearElementClientRect$(scrollableParent, {scrollStep: this.scrollSpeed})
592-
)).pipe(
593-
takeUntil(this.gridService.pointerEnd$)
594-
).subscribe());
595-
596655
// Main subscription, it listens for 'pointer move' and 'scroll' events and recalculates the layout on each emission
597-
const dragSubscription = this.ngZone.runOutsideAngular(() =>
656+
return this.ngZone.runOutsideAngular(() =>
598657
merge(
599658
combineLatest([
600659
this.gridService.pointerMove$,
@@ -693,47 +752,33 @@ export class KtdGridComponent implements OnChanges, AfterContentInit, AfterConte
693752
}
694753
})
695754
);
696-
697-
this.drag = {
698-
dragSubscription,
699-
scrollSubscription,
700-
startEvent: event,
701-
newLayout: null,
702-
newLayoutItem: dragInfo.dragRef.itemRef instanceof KtdDrag ? {
703-
id: dragInfo.dragRef.id,
704-
w: 1,
705-
h: 1,
706-
x: -1,
707-
y: -1,
708-
} : null,
709-
};
710755
}
711756

712757
// TODO: Call this only when the drag ended, when the drag is paused do nothing.
713-
public stopDragSequence(): void {
714-
const dragInfo = this.gridService.drag!;
758+
private stopDragSequence(dragInfo: PointerEventInfo): void {
759+
if (this.drag === null) {
760+
return;
761+
}
715762

716-
if (this.drag != null) {
717-
// Remove drag classes
718-
this.renderer.removeClass(dragInfo.dragRef.elementRef.nativeElement, 'no-transitions');
719-
this.renderer.removeClass(dragInfo.dragRef.elementRef.nativeElement, 'ktd-grid-item-dragging');
763+
console.log('stopDragSequence');
720764

721-
this.ngZone.run(() => {
722-
(dragInfo.type === 'drag' ? this.dragEnded : this.resizeEnded).emit(getDragResizeEventData(dragInfo.dragRef, this.layout));
723-
});
765+
// Remove drag classes
766+
this.renderer.removeClass(dragInfo.dragRef.elementRef.nativeElement, 'no-transitions');
767+
this.renderer.removeClass(dragInfo.dragRef.elementRef.nativeElement, 'ktd-grid-item-dragging');
724768

725-
this.addGridItemAnimatingClass(dragInfo.dragRef).subscribe();
726-
// Consider destroying the placeholder after the animation has finished.
727-
this.destroyPlaceholder();
728-
this.drag.dragSubscription.unsubscribe();
729-
this.drag.scrollSubscription?.unsubscribe();
730-
this.drag = null;
731-
}
732-
}
769+
this.ngZone.run(() => {
770+
(dragInfo.type === 'drag' ? this.dragEnded : this.resizeEnded).emit(getDragResizeEventData(dragInfo.dragRef, this.layout));
771+
});
733772

734-
public updateLayout(): void {
735-
const dragInfo = this.gridService.drag!;
773+
this.addGridItemAnimatingClass(dragInfo.dragRef).subscribe();
774+
// Consider destroying the placeholder after the animation has finished.
775+
this.destroyPlaceholder();
776+
this.drag.dragSubscription.unsubscribe();
777+
this.drag.scrollSubscription?.unsubscribe();
778+
this.drag = null;
779+
}
736780

781+
public updateLayout(dragInfo: PointerEventInfo): void {
737782
if (this.drag != null && this.drag.newLayout) {
738783
const previousLayoutItem = this.layout.find(item => item.id === dragInfo.dragRef.id);
739784
const currentLayoutItem = this.drag!.newLayout.find(item => item.id === dragInfo.dragRef.id);
@@ -760,8 +805,6 @@ export class KtdGridComponent implements OnChanges, AfterContentInit, AfterConte
760805
});
761806
});
762807
}
763-
764-
this.stopDragSequence();
765808
}
766809

767810
public isPointerInsideGridElement(event: MouseEvent | TouchEvent): boolean {

projects/angular-grid-layout/src/lib/grid.service.ts

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ export class KtdGridService {
2929
private pointerEndSubject: Subject<MouseEvent | TouchEvent> = new Subject<MouseEvent | TouchEvent>();
3030
private pointerEndSubscription: Subscription;
3131

32+
pointerBeforeEnd$: Observable<{event: MouseEvent | TouchEvent, dragInfo: PointerEventInfo | null}>;
33+
private pointerBeforeEndSubject: Subject<{event: MouseEvent | TouchEvent, dragInfo: PointerEventInfo | null}> = new Subject<{event: MouseEvent | TouchEvent, dragInfo: PointerEventInfo | null}>();
34+
3235
drag: PointerEventInfo | null = null;
3336

3437
constructor(
@@ -37,6 +40,7 @@ export class KtdGridService {
3740
) {
3841
this.pointerMove$ = this.pointerMoveSubject.asObservable();
3942
this.pointerEnd$ = this.pointerEndSubject.asObservable();
43+
this.pointerBeforeEnd$ = this.pointerBeforeEndSubject.asObservable();
4044
this.initSubscriptions();
4145
}
4246

@@ -49,12 +53,23 @@ export class KtdGridService {
4953
this.pointerEndSubscription = this.ngZone.runOutsideAngular(() =>
5054
ktdPointerUp(document)
5155
.subscribe((mouseEvent: MouseEvent | TouchEvent) => {
52-
this.stopDrag();
56+
this.pointerBeforeEndSubject.next({
57+
event: mouseEvent,
58+
dragInfo: this.drag,
59+
});
60+
this.drag = null;
5361
this.pointerEndSubject.next(mouseEvent);
5462
})
5563
);
5664
}
5765

66+
/**
67+
* Start a drag sequence.
68+
* @param event The event that triggered the drag sequence.
69+
* @param dragRef The dragRef that started the drag sequence.
70+
* @param type The type of drag sequence.
71+
* @param grid The grid where the drag sequence started. It can be null if the drag sequence started outside a grid.
72+
*/
5873
public startDrag(event: MouseEvent | TouchEvent | PointerEvent, dragRef: DragRef, type: DragActionType, grid: KtdGridComponent | null = null): void {
5974
// Make sure, this function is only being called once
6075
if (this.drag !== null) {
@@ -76,22 +91,16 @@ export class KtdGridService {
7691
this.ngZone.run(() => (type === 'drag' ? grid.dragStarted : grid.resizeStarted).emit(getDragResizeEventData(dragRef, grid.layout)));
7792
}
7893

94+
const connectedToGrids = isKtdDrag ? (dragRef.itemRef as KtdDrag<any>).connectedTo : this.registryService._ktgGrids;
7995
this.pointerMove$.pipe(
8096
takeUntil(this.pointerEnd$),
8197
ktdOutsideZone(this.ngZone),
8298
).subscribe((moveEvent: MouseEvent | TouchEvent) => {
8399
this.drag!.moveEvent = moveEvent;
84-
const connectedToGrids = isKtdDrag ? (dragRef.itemRef as KtdDrag<any>).connectedTo : this.registryService._ktgGrids;
85100
this.handleGridInteraction(moveEvent, connectedToGrids);
86101
});
87102
}
88103

89-
public stopDrag(): void {
90-
this.drag?.currentGrid?.updateLayout();
91-
this.drag = null;
92-
}
93-
94-
95104
private handleGridInteraction(moveEvent: MouseEvent | TouchEvent, connectedToGrids: KtdGridComponent[]): void {
96105
for (const grid of connectedToGrids) {
97106
if (!grid.isPointerInsideGridElement(moveEvent)) {

0 commit comments

Comments
 (0)