Skip to content

Commit 219beb1

Browse files
committed
refactor(drag-drop): move logic out of directives
Moves the logic for `CdkDrag` and `CdkDropList` into separate, non-Angular-specific classes. This is a first step towards allowing consumers to attach drag&drop functionality to arbitrary DOM nodes.
1 parent 3982e9e commit 219beb1

18 files changed

+1292
-702
lines changed

src/cdk/drag-drop/drag-handle.ts renamed to src/cdk/drag-drop/directives/drag-handle.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88

99
import {Directive, ElementRef, Inject, Optional, Input} from '@angular/core';
1010
import {coerceBooleanProperty} from '@angular/cdk/coercion';
11-
import {CDK_DRAG_PARENT} from './drag-parent';
12-
import {toggleNativeDragInteractions} from './drag-styling';
11+
import {CDK_DRAG_PARENT} from '../drag-parent';
12+
import {toggleNativeDragInteractions} from '../drag-styling';
1313

1414
/** Handle that can be used to drag and CdkDrag instance. */
1515
@Directive({

src/cdk/drag-drop/drag.spec.ts renamed to src/cdk/drag-drop/directives/drag.spec.ts

+6-5
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
ChangeDetectionStrategy,
1313
} from '@angular/core';
1414
import {TestBed, ComponentFixture, fakeAsync, flush, tick} from '@angular/core/testing';
15-
import {DragDropModule} from './drag-drop-module';
15+
import {DragDropModule} from '../drag-drop-module';
1616
import {
1717
createMouseEvent,
1818
dispatchEvent,
@@ -21,12 +21,13 @@ import {
2121
createTouchEvent,
2222
} from '@angular/cdk/testing';
2323
import {Directionality} from '@angular/cdk/bidi';
24-
import {CdkDrag, CDK_DRAG_CONFIG, CdkDragConfig} from './drag';
25-
import {CdkDragDrop} from './drag-events';
26-
import {moveItemInArray} from './drag-utils';
24+
import {CdkDrag, CDK_DRAG_CONFIG} from './drag';
25+
import {CdkDragDrop} from '../drag-events';
26+
import {moveItemInArray} from '../drag-utils';
2727
import {CdkDropList} from './drop-list';
2828
import {CdkDragHandle} from './drag-handle';
2929
import {CdkDropListGroup} from './drop-list-group';
30+
import {DragRefConfig} from '../drag-ref';
3031

3132
const ITEM_HEIGHT = 25;
3233
const ITEM_WIDTH = 75;
@@ -46,7 +47,7 @@ describe('CdkDrag', () => {
4647
// have to deal with thresholds.
4748
dragStartThreshold: dragDistance,
4849
pointerDirectionChangeThreshold: 5
49-
} as CdkDragConfig
50+
} as DragRefConfig
5051
},
5152
...providers
5253
],

src/cdk/drag-drop/directives/drag.ts

+275
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {Directionality} from '@angular/cdk/bidi';
10+
import {ViewportRuler} from '@angular/cdk/scrolling';
11+
import {DOCUMENT} from '@angular/common';
12+
import {
13+
AfterViewInit,
14+
ContentChild,
15+
ContentChildren,
16+
Directive,
17+
ElementRef,
18+
EventEmitter,
19+
Inject,
20+
InjectionToken,
21+
Input,
22+
NgZone,
23+
OnDestroy,
24+
Optional,
25+
Output,
26+
QueryList,
27+
SkipSelf,
28+
ViewContainerRef,
29+
} from '@angular/core';
30+
import {coerceBooleanProperty} from '@angular/cdk/coercion';
31+
import {Observable, Subscription, Observer} from 'rxjs';
32+
import {startWith, take, map} from 'rxjs/operators';
33+
import {DragDropRegistry} from '../drag-drop-registry';
34+
import {
35+
CdkDragDrop,
36+
CdkDragEnd,
37+
CdkDragEnter,
38+
CdkDragExit,
39+
CdkDragMove,
40+
CdkDragStart,
41+
} from '../drag-events';
42+
import {CdkDragHandle} from './drag-handle';
43+
import {CdkDragPlaceholder} from './drag-placeholder';
44+
import {CdkDragPreview} from './drag-preview';
45+
import {CDK_DROP_LIST} from '../drop-list-container';
46+
import {CDK_DRAG_PARENT} from '../drag-parent';
47+
import {DragRef, DragRefConfig} from '../drag-ref';
48+
import {DropListRef} from '../drop-list-ref';
49+
import {CdkDropListInternal as CdkDropList} from './drop-list';
50+
51+
/** Injection token that can be used to configure the behavior of `CdkDrag`. */
52+
export const CDK_DRAG_CONFIG = new InjectionToken<DragRefConfig>('CDK_DRAG_CONFIG', {
53+
providedIn: 'root',
54+
factory: CDK_DRAG_CONFIG_FACTORY
55+
});
56+
57+
/** @docs-private */
58+
export function CDK_DRAG_CONFIG_FACTORY(): DragRefConfig {
59+
return {dragStartThreshold: 5, pointerDirectionChangeThreshold: 5};
60+
}
61+
62+
/** Element that can be moved inside a CdkDropList container. */
63+
@Directive({
64+
selector: '[cdkDrag]',
65+
exportAs: 'cdkDrag',
66+
host: {
67+
'class': 'cdk-drag',
68+
'[class.cdk-drag-dragging]': '_dragRef.isDragging()',
69+
},
70+
providers: [{provide: CDK_DRAG_PARENT, useExisting: CdkDrag}]
71+
})
72+
export class CdkDrag<T = any> implements AfterViewInit, OnDestroy {
73+
/** Subscription to the stream that initializes the root element. */
74+
private _rootElementInitSubscription = Subscription.EMPTY;
75+
76+
/** Reference to the underlying drag instance. */
77+
_dragRef: DragRef<CdkDrag<T>>;
78+
79+
/** Elements that can be used to drag the draggable item. */
80+
@ContentChildren(CdkDragHandle, {descendants: true}) _handles: QueryList<CdkDragHandle>;
81+
82+
/** Element that will be used as a template to create the draggable item's preview. */
83+
@ContentChild(CdkDragPreview) _previewTemplate: CdkDragPreview;
84+
85+
/** Template for placeholder element rendered to show where a draggable would be dropped. */
86+
@ContentChild(CdkDragPlaceholder) _placeholderTemplate: CdkDragPlaceholder;
87+
88+
/** Arbitrary data to attach to this drag instance. */
89+
@Input('cdkDragData') data: T;
90+
91+
/** Locks the position of the dragged element along the specified axis. */
92+
@Input('cdkDragLockAxis') lockAxis: 'x' | 'y';
93+
94+
/**
95+
* Selector that will be used to determine the root draggable element, starting from
96+
* the `cdkDrag` element and going up the DOM. Passing an alternate root element is useful
97+
* when trying to enable dragging on an element that you might not have access to.
98+
*/
99+
@Input('cdkDragRootElement') rootElementSelector: string;
100+
101+
/** Whether starting to drag this element is disabled. */
102+
@Input('cdkDragDisabled')
103+
get disabled(): boolean {
104+
return this._disabled || (this.dropContainer && this.dropContainer.disabled);
105+
}
106+
set disabled(value: boolean) {
107+
this._disabled = coerceBooleanProperty(value);
108+
}
109+
private _disabled = false;
110+
111+
/** Emits when the user starts dragging the item. */
112+
@Output('cdkDragStarted') started: EventEmitter<CdkDragStart> = new EventEmitter<CdkDragStart>();
113+
114+
/** Emits when the user stops dragging an item in the container. */
115+
@Output('cdkDragEnded') ended: EventEmitter<CdkDragEnd> = new EventEmitter<CdkDragEnd>();
116+
117+
/** Emits when the user has moved the item into a new container. */
118+
@Output('cdkDragEntered') entered: EventEmitter<CdkDragEnter<any>> =
119+
new EventEmitter<CdkDragEnter<any>>();
120+
121+
/** Emits when the user removes the item its container by dragging it into another container. */
122+
@Output('cdkDragExited') exited: EventEmitter<CdkDragExit<any>> =
123+
new EventEmitter<CdkDragExit<any>>();
124+
125+
/** Emits when the user drops the item inside a container. */
126+
@Output('cdkDragDropped') dropped: EventEmitter<CdkDragDrop<any>> =
127+
new EventEmitter<CdkDragDrop<any>>();
128+
129+
/**
130+
* Emits as the user is dragging the item. Use with caution,
131+
* because this event will fire for every pixel that the user has dragged.
132+
*/
133+
@Output('cdkDragMoved') moved: Observable<CdkDragMove<T>> =
134+
Observable.create((observer: Observer<CdkDragMove<T>>) => {
135+
const subscription = this._dragRef.moved.pipe(map(movedEvent => ({
136+
source: this,
137+
pointerPosition: movedEvent.pointerPosition,
138+
event: movedEvent.event,
139+
delta: movedEvent.delta
140+
}))).subscribe(observer);
141+
142+
return () => {
143+
subscription.unsubscribe();
144+
};
145+
});
146+
147+
constructor(
148+
/** Element that the draggable is attached to. */
149+
public element: ElementRef<HTMLElement>,
150+
/** Droppable container that the draggable is a part of. */
151+
@Inject(CDK_DROP_LIST) @Optional() @SkipSelf()
152+
public dropContainer: CdkDropList,
153+
@Inject(DOCUMENT) private _document: any,
154+
private _ngZone: NgZone,
155+
private _viewContainerRef: ViewContainerRef,
156+
private _viewportRuler: ViewportRuler,
157+
private _dragDropRegistry: DragDropRegistry<DragRef, DropListRef>,
158+
@Inject(CDK_DRAG_CONFIG) private _config: DragRefConfig,
159+
@Optional() private _dir: Directionality) {
160+
161+
const ref = this._dragRef = new DragRef(element, this._document, this._ngZone,
162+
this._viewContainerRef, this._viewportRuler, this._dragDropRegistry,
163+
this._config, this.dropContainer ? this.dropContainer._dropListRef : undefined,
164+
this._dir);
165+
ref.data = this;
166+
ref.beforeStarted.subscribe(() => {
167+
ref.disabled = this.disabled;
168+
ref.lockAxis = this.lockAxis;
169+
});
170+
this._proxyEvents(ref);
171+
}
172+
173+
/**
174+
* Returns the element that is being used as a placeholder
175+
* while the current element is being dragged.
176+
*/
177+
getPlaceholderElement(): HTMLElement {
178+
return this._dragRef.getPlaceholderElement();
179+
}
180+
181+
/** Returns the root draggable element. */
182+
getRootElement(): HTMLElement {
183+
return this._dragRef.getRootElement();
184+
}
185+
186+
/** Resets a standalone drag item to its initial position. */
187+
reset(): void {
188+
this._dragRef.reset();
189+
}
190+
191+
ngAfterViewInit() {
192+
// We need to wait for the zone to stabilize, in order for the reference
193+
// element to be in the proper place in the DOM. This is mostly relevant
194+
// for draggable elements inside portals since they get stamped out in
195+
// their original DOM position and then they get transferred to the portal.
196+
this._rootElementInitSubscription = this._ngZone.onStable.asObservable()
197+
.pipe(take(1))
198+
.subscribe(() => {
199+
this._dragRef
200+
.withRootElement(this._getRootElement())
201+
.withPlaceholderTemplate(this._placeholderTemplate)
202+
.withPreviewTemplate(this._previewTemplate);
203+
204+
this._handles.changes
205+
.pipe(startWith(this._handles))
206+
.subscribe((handleList: QueryList<CdkDragHandle>) => {
207+
this._dragRef.withHandles(handleList.filter(handle => handle._parentDrag === this));
208+
});
209+
});
210+
}
211+
212+
ngOnDestroy() {
213+
this._rootElementInitSubscription.unsubscribe();
214+
this._dragRef.dispose();
215+
}
216+
217+
/** Gets the root draggable element, based on the `rootElementSelector`. */
218+
private _getRootElement(): HTMLElement {
219+
if (this.rootElementSelector) {
220+
const selector = this.rootElementSelector;
221+
let currentElement = this.element.nativeElement.parentElement as HTMLElement | null;
222+
223+
while (currentElement) {
224+
// IE doesn't support `matches` so we have to fall back to `msMatchesSelector`.
225+
if (currentElement.matches ? currentElement.matches(selector) :
226+
(currentElement as any).msMatchesSelector(selector)) {
227+
return currentElement;
228+
}
229+
230+
currentElement = currentElement.parentElement;
231+
}
232+
}
233+
234+
return this.element.nativeElement;
235+
}
236+
237+
/**
238+
* Proxies the events from a DragRef to events that
239+
* match the interfaces of the CdkDrag outputs.
240+
*/
241+
private _proxyEvents(ref: DragRef<CdkDrag<T>>) {
242+
ref.started.subscribe(() => {
243+
this.started.emit({source: this});
244+
});
245+
246+
ref.ended.subscribe(() => {
247+
this.ended.emit({source: this});
248+
});
249+
250+
ref.entered.subscribe(event => {
251+
this.entered.emit({
252+
container: event.container.data,
253+
item: this
254+
});
255+
});
256+
257+
ref.exited.subscribe(event => {
258+
this.exited.emit({
259+
container: event.container.data,
260+
item: this
261+
});
262+
});
263+
264+
ref.dropped.subscribe(event => {
265+
this.dropped.emit({
266+
previousIndex: event.previousIndex,
267+
currentIndex: event.currentIndex,
268+
previousContainer: event.previousContainer.data,
269+
container: event.container.data,
270+
isPointerOverContainer: event.isPointerOverContainer,
271+
item: this
272+
});
273+
});
274+
}
275+
}

0 commit comments

Comments
 (0)