Skip to content

Commit 42ca449

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 31f0e6d commit 42ca449

18 files changed

+1364
-768
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

+7-6
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,13 +21,14 @@ 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 {extendStyles} from './drag-styling';
30+
import {extendStyles} from '../drag-styling';
31+
import {DragRefConfig} from '../drag-ref';
3132

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

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

+305
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,305 @@
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+
/**
102+
* Selector that will be used to determine the element to which the draggable's position will
103+
* be constrained. Matching starts from the element's parent and goes up the DOM until a matching
104+
* element has been found.
105+
*/
106+
@Input('cdkDragBoundary') boundaryElementSelector: string;
107+
108+
/** Whether starting to drag this element is disabled. */
109+
@Input('cdkDragDisabled')
110+
get disabled(): boolean {
111+
return this._disabled || (this.dropContainer && this.dropContainer.disabled);
112+
}
113+
set disabled(value: boolean) {
114+
this._disabled = coerceBooleanProperty(value);
115+
}
116+
private _disabled = false;
117+
118+
/** Emits when the user starts dragging the item. */
119+
@Output('cdkDragStarted') started: EventEmitter<CdkDragStart> = new EventEmitter<CdkDragStart>();
120+
121+
/** Emits when the user stops dragging an item in the container. */
122+
@Output('cdkDragEnded') ended: EventEmitter<CdkDragEnd> = new EventEmitter<CdkDragEnd>();
123+
124+
/** Emits when the user has moved the item into a new container. */
125+
@Output('cdkDragEntered') entered: EventEmitter<CdkDragEnter<any>> =
126+
new EventEmitter<CdkDragEnter<any>>();
127+
128+
/** Emits when the user removes the item its container by dragging it into another container. */
129+
@Output('cdkDragExited') exited: EventEmitter<CdkDragExit<any>> =
130+
new EventEmitter<CdkDragExit<any>>();
131+
132+
/** Emits when the user drops the item inside a container. */
133+
@Output('cdkDragDropped') dropped: EventEmitter<CdkDragDrop<any>> =
134+
new EventEmitter<CdkDragDrop<any>>();
135+
136+
/**
137+
* Emits as the user is dragging the item. Use with caution,
138+
* because this event will fire for every pixel that the user has dragged.
139+
*/
140+
@Output('cdkDragMoved') moved: Observable<CdkDragMove<T>> =
141+
Observable.create((observer: Observer<CdkDragMove<T>>) => {
142+
const subscription = this._dragRef.moved.pipe(map(movedEvent => ({
143+
source: this,
144+
pointerPosition: movedEvent.pointerPosition,
145+
event: movedEvent.event,
146+
delta: movedEvent.delta
147+
}))).subscribe(observer);
148+
149+
return () => {
150+
subscription.unsubscribe();
151+
};
152+
});
153+
154+
constructor(
155+
/** Element that the draggable is attached to. */
156+
public element: ElementRef<HTMLElement>,
157+
/** Droppable container that the draggable is a part of. */
158+
@Inject(CDK_DROP_LIST) @Optional() @SkipSelf()
159+
public dropContainer: CdkDropList,
160+
@Inject(DOCUMENT) private _document: any,
161+
private _ngZone: NgZone,
162+
private _viewContainerRef: ViewContainerRef,
163+
private _viewportRuler: ViewportRuler,
164+
private _dragDropRegistry: DragDropRegistry<DragRef, DropListRef>,
165+
@Inject(CDK_DRAG_CONFIG) private _config: DragRefConfig,
166+
@Optional() private _dir: Directionality) {
167+
168+
const ref = this._dragRef = new DragRef(element, this._document, this._ngZone,
169+
this._viewContainerRef, this._viewportRuler, this._dragDropRegistry,
170+
this._config, this.dropContainer ? this.dropContainer._dropListRef : undefined,
171+
this._dir);
172+
ref.data = this;
173+
ref.beforeStarted.subscribe(() => {
174+
if (!ref.isDragging()) {
175+
ref.disabled = this.disabled;
176+
ref.lockAxis = this.lockAxis;
177+
ref.withBoundaryElement(this._getBoundaryElement());
178+
}
179+
});
180+
this._proxyEvents(ref);
181+
}
182+
183+
/**
184+
* Returns the element that is being used as a placeholder
185+
* while the current element is being dragged.
186+
*/
187+
getPlaceholderElement(): HTMLElement {
188+
return this._dragRef.getPlaceholderElement();
189+
}
190+
191+
/** Returns the root draggable element. */
192+
getRootElement(): HTMLElement {
193+
return this._dragRef.getRootElement();
194+
}
195+
196+
/** Resets a standalone drag item to its initial position. */
197+
reset(): void {
198+
this._dragRef.reset();
199+
}
200+
201+
ngAfterViewInit() {
202+
// We need to wait for the zone to stabilize, in order for the reference
203+
// element to be in the proper place in the DOM. This is mostly relevant
204+
// for draggable elements inside portals since they get stamped out in
205+
// their original DOM position and then they get transferred to the portal.
206+
this._rootElementInitSubscription = this._ngZone.onStable.asObservable()
207+
.pipe(take(1))
208+
.subscribe(() => {
209+
const rootElement = this._getRootElement();
210+
211+
if (rootElement.nodeType !== this._document.ELEMENT_NODE) {
212+
throw Error(`cdkDrag must be attached to an element node. ` +
213+
`Currently attached to "${rootElement.nodeName}".`);
214+
}
215+
216+
this._dragRef
217+
.withRootElement(rootElement)
218+
.withPlaceholderTemplate(this._placeholderTemplate)
219+
.withPreviewTemplate(this._previewTemplate);
220+
221+
this._handles.changes
222+
.pipe(startWith(this._handles))
223+
.subscribe((handleList: QueryList<CdkDragHandle>) => {
224+
this._dragRef.withHandles(handleList.filter(handle => handle._parentDrag === this));
225+
});
226+
});
227+
}
228+
229+
ngOnDestroy() {
230+
this._rootElementInitSubscription.unsubscribe();
231+
this._dragRef.dispose();
232+
}
233+
234+
/** Gets the root draggable element, based on the `rootElementSelector`. */
235+
private _getRootElement(): HTMLElement {
236+
const element = this.element.nativeElement;
237+
const rootElement = this.rootElementSelector ?
238+
getClosestMatchingAncestor(element, this.rootElementSelector) : null;
239+
240+
return rootElement || element;
241+
}
242+
243+
/** Gets the boundary element, based on the `boundaryElementSelector`. */
244+
private _getBoundaryElement() {
245+
const selector = this.boundaryElementSelector;
246+
return selector ? getClosestMatchingAncestor(this.element.nativeElement, selector) : null;
247+
}
248+
249+
/**
250+
* Proxies the events from a DragRef to events that
251+
* match the interfaces of the CdkDrag outputs.
252+
*/
253+
private _proxyEvents(ref: DragRef<CdkDrag<T>>) {
254+
ref.started.subscribe(() => {
255+
this.started.emit({source: this});
256+
});
257+
258+
ref.ended.subscribe(() => {
259+
this.ended.emit({source: this});
260+
});
261+
262+
ref.entered.subscribe(event => {
263+
this.entered.emit({
264+
container: event.container.data,
265+
item: this
266+
});
267+
});
268+
269+
ref.exited.subscribe(event => {
270+
this.exited.emit({
271+
container: event.container.data,
272+
item: this
273+
});
274+
});
275+
276+
ref.dropped.subscribe(event => {
277+
this.dropped.emit({
278+
previousIndex: event.previousIndex,
279+
currentIndex: event.currentIndex,
280+
previousContainer: event.previousContainer.data,
281+
container: event.container.data,
282+
isPointerOverContainer: event.isPointerOverContainer,
283+
item: this
284+
});
285+
});
286+
}
287+
}
288+
289+
/** Gets the closest ancestor of an element that matches a selector. */
290+
function getClosestMatchingAncestor(element: HTMLElement, selector: string) {
291+
let currentElement = element.parentElement as HTMLElement | null;
292+
293+
while (currentElement) {
294+
// IE doesn't support `matches` so we have to fall back to `msMatchesSelector`.
295+
if (currentElement.matches ? currentElement.matches(selector) :
296+
(currentElement as any).msMatchesSelector(selector)) {
297+
return currentElement;
298+
}
299+
300+
currentElement = currentElement.parentElement;
301+
}
302+
303+
return null;
304+
}
305+

0 commit comments

Comments
 (0)