Skip to content

Commit f2963bc

Browse files
crisbetojosephperrott
authored andcommitted
feat(drag-drop): add support for multiple handles and handles that are added after init (#12102)
1 parent a6e28db commit f2963bc

File tree

3 files changed

+105
-13
lines changed

3 files changed

+105
-13
lines changed

src/cdk-experimental/drag-drop/drag.spec.ts

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
ViewChildren,
77
QueryList,
88
AfterViewInit,
9+
ViewEncapsulation,
910
} from '@angular/core';
1011
import {TestBed, ComponentFixture, fakeAsync, flush} from '@angular/core/testing';
1112
import {DragDropModule} from './drag-drop-module';
@@ -14,6 +15,7 @@ import {CdkDrag} from './drag';
1415
import {CdkDragDrop} from './drag-events';
1516
import {moveItemInArray, transferArrayItem} from './drag-utils';
1617
import {CdkDrop} from './drop';
18+
import {CdkDragHandle} from './drag-handle';
1719

1820
const ITEM_HEIGHT = 25;
1921

@@ -209,6 +211,37 @@ describe('CdkDrag', () => {
209211
dragElementViaMouse(fixture, handle, 50, 100);
210212
expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)');
211213
}));
214+
215+
it('should be able to use a handle that was added after init', fakeAsync(() => {
216+
const fixture = createComponent(StandaloneDraggableWithDelayedHandle);
217+
218+
fixture.detectChanges();
219+
fixture.componentInstance.showHandle = true;
220+
fixture.detectChanges();
221+
222+
const dragElement = fixture.componentInstance.dragElement.nativeElement;
223+
const handle = fixture.componentInstance.handleElement.nativeElement;
224+
225+
expect(dragElement.style.transform).toBeFalsy();
226+
dragElementViaMouse(fixture, handle, 50, 100);
227+
expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)');
228+
}));
229+
230+
it('should be able to use more than one handle to drag the element', fakeAsync(() => {
231+
const fixture = createComponent(StandaloneDraggableWithMultipleHandles);
232+
fixture.detectChanges();
233+
234+
const dragElement = fixture.componentInstance.dragElement.nativeElement;
235+
const handles = fixture.componentInstance.handles.map(handle => handle.element.nativeElement);
236+
237+
expect(dragElement.style.transform).toBeFalsy();
238+
dragElementViaMouse(fixture, handles[1], 50, 100);
239+
expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)');
240+
241+
dragElementViaMouse(fixture, handles[0], 100, 200);
242+
expect(dragElement.style.transform).toBe('translate3d(150px, 300px, 0px)');
243+
}));
244+
212245
});
213246

214247
describe('in a drop container', () => {
@@ -463,9 +496,48 @@ export class StandaloneDraggable {
463496
export class StandaloneDraggableWithHandle {
464497
@ViewChild('dragElement') dragElement: ElementRef<HTMLElement>;
465498
@ViewChild('handleElement') handleElement: ElementRef<HTMLElement>;
466-
@ViewChild(CdkDrag) dragInstance: CdkDrag;
467499
}
468500

501+
@Component({
502+
template: `
503+
<div #dragElement cdkDrag
504+
style="width: 100px; height: 100px; background: red; position: relative">
505+
<div
506+
#handleElement
507+
*ngIf="showHandle"
508+
cdkDragHandle style="width: 10px; height: 10px; background: green;"></div>
509+
</div>
510+
`
511+
})
512+
export class StandaloneDraggableWithDelayedHandle {
513+
@ViewChild('dragElement') dragElement: ElementRef<HTMLElement>;
514+
@ViewChild('handleElement') handleElement: ElementRef<HTMLElement>;
515+
showHandle = false;
516+
}
517+
518+
@Component({
519+
encapsulation: ViewEncapsulation.None,
520+
styles: [`
521+
.cdk-drag-handle {
522+
position: absolute;
523+
top: 0;
524+
background: green;
525+
width: 10px;
526+
height: 10px;
527+
}
528+
`],
529+
template: `
530+
<div #dragElement cdkDrag
531+
style="width: 100px; height: 100px; background: red; position: relative">
532+
<div cdkDragHandle style="left: 0;"></div>
533+
<div cdkDragHandle style="right: 0;"></div>
534+
</div>
535+
`
536+
})
537+
export class StandaloneDraggableWithMultipleHandles {
538+
@ViewChild('dragElement') dragElement: ElementRef<HTMLElement>;
539+
@ViewChildren(CdkDragHandle) handles: QueryList<CdkDragHandle>;
540+
}
469541

470542
@Component({
471543
template: `

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

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import {
2020
EventEmitter,
2121
ViewContainerRef,
2222
EmbeddedViewRef,
23+
ContentChildren,
24+
QueryList,
2325
} from '@angular/core';
2426
import {CdkDragHandle} from './drag-handle';
2527
import {DOCUMENT} from '@angular/platform-browser';
@@ -42,6 +44,8 @@ const activeEventOptions = supportsPassiveEventListeners() ? {passive: false} :
4244
exportAs: 'cdkDrag',
4345
host: {
4446
'class': 'cdk-drag',
47+
'(mousedown)': '_startDragging($event)',
48+
'(touchstart)': '_startDragging($event)',
4549
}
4650
})
4751
export class CdkDrag implements AfterContentInit, OnDestroy {
@@ -88,8 +92,8 @@ export class CdkDrag implements AfterContentInit, OnDestroy {
8892
/** Cached scroll position on the page when the element was picked up. */
8993
private _scrollPosition: {top: number, left: number};
9094

91-
/** Element that can be used to drag the draggable item. */
92-
@ContentChild(CdkDragHandle) _handle: CdkDragHandle;
95+
/** Elements that can be used to drag the draggable item. */
96+
@ContentChildren(CdkDragHandle) _handles: QueryList<CdkDragHandle>;
9397

9498
/** Element that will be used as a template to create the draggable item's preview. */
9599
@ContentChild(CdkDragPreview) _previewTemplate: CdkDragPreview;
@@ -135,11 +139,6 @@ export class CdkDrag implements AfterContentInit, OnDestroy {
135139
}
136140

137141
ngAfterContentInit() {
138-
// TODO: doesn't handle (pun intended) the handle being destroyed
139-
const dragElement = (this._handle ? this._handle.element : this.element).nativeElement;
140-
dragElement.addEventListener('mousedown', this._pointerDown);
141-
dragElement.addEventListener('touchstart', this._pointerDown);
142-
143142
// WebKit won't preventDefault on a dynamically-added `touchmove` listener, which means that
144143
// we need to add one ahead of time. See https://bugs.webkit.org/show_bug.cgi?id=184250.
145144
// TODO: move into a central registry.
@@ -162,8 +161,27 @@ export class CdkDrag implements AfterContentInit, OnDestroy {
162161
}
163162
}
164163

164+
/** Starts the dragging sequence. */
165+
_startDragging(event: MouseEvent | TouchEvent) {
166+
// Delegate the event based on whether it started from a handle or the element itself.
167+
if (this._handles.length) {
168+
const targetHandle = this._handles.find(handle => {
169+
const element = handle.element.nativeElement;
170+
const target = event.target;
171+
return !!target && (target === element || element.contains(target as HTMLElement));
172+
});
173+
174+
if (targetHandle) {
175+
this._pointerDown(targetHandle.element, event);
176+
}
177+
} else {
178+
this._pointerDown(this.element, event);
179+
}
180+
}
181+
165182
/** Handler for when the pointer is pressed down on the element or the handle. */
166-
private _pointerDown = (event: MouseEvent | TouchEvent) => {
183+
private _pointerDown = (referenceElement: ElementRef<HTMLElement>,
184+
event: MouseEvent | TouchEvent) => {
167185
if (this._isDragging) {
168186
return;
169187
}
@@ -175,7 +193,7 @@ export class CdkDrag implements AfterContentInit, OnDestroy {
175193
// If we have a custom preview template, the element won't be visible anyway so we avoid the
176194
// extra `getBoundingClientRect` calls and just move the preview next to the cursor.
177195
this._pickupPositionInElement = this._previewTemplate ? {x: 0, y: 0} :
178-
this._getPointerPositionInElement(event);
196+
this._getPointerPositionInElement(referenceElement, event);
179197
this._pickupPositionOnPage = this._getPointerPositionOnPage(event);
180198
this._registerMoveListeners(event);
181199

@@ -371,11 +389,13 @@ export class CdkDrag implements AfterContentInit, OnDestroy {
371389

372390
/**
373391
* Figures out the coordinates at which an element was picked up.
392+
* @param referenceElement Element that initiated the dragging.
374393
* @param event Event that initiated the dragging.
375394
*/
376-
private _getPointerPositionInElement(event: MouseEvent | TouchEvent): Point {
395+
private _getPointerPositionInElement(referenceElement: ElementRef<HTMLElement>,
396+
event: MouseEvent | TouchEvent): Point {
377397
const elementRect = this.element.nativeElement.getBoundingClientRect();
378-
const handleElement = this._handle ? this._handle.element.nativeElement : null;
398+
const handleElement = referenceElement === this.element ? null : referenceElement.nativeElement;
379399
const referenceRect = handleElement ? handleElement.getBoundingClientRect() : elementRect;
380400
const x = this._isTouchEvent(event) ?
381401
event.targetTouches[0].pageX - referenceRect.left - this._scrollPosition.left :

src/cdk/testing/event-objects.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export function createMouseEvent(type: string, x = 0, y = 0) {
1111
const event = document.createEvent('MouseEvent');
1212

1313
event.initMouseEvent(type,
14-
false, /* canBubble */
14+
true, /* canBubble */
1515
false, /* cancelable */
1616
window, /* view */
1717
0, /* detail */

0 commit comments

Comments
 (0)