Skip to content

Commit d4825e8

Browse files
committed
feat(drag-drop): add support for multiple handles and handles that are added after init
* Fixes a TODO about supporting handles that are added after init. * Adds the ability to have more than one handle in a draggable. * Moves some parameters around in the event faking utils to allow for the event target to be set.
1 parent 5c41410 commit d4825e8

File tree

5 files changed

+134
-32
lines changed

5 files changed

+134
-32
lines changed

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

Lines changed: 80 additions & 6 deletions
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

@@ -136,9 +138,40 @@ describe('CdkDrag', () => {
136138
const handle = fixture.componentInstance.handleElement.nativeElement;
137139

138140
expect(dragElement.style.transform).toBeFalsy();
139-
dragElementViaMouse(fixture, handle, 50, 100);
141+
dragElementViaMouse(fixture, dragElement, 50, 100, handle);
140142
expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)');
141143
}));
144+
145+
it('should be able to use a handle that was added after init', fakeAsync(() => {
146+
const fixture = createComponent(StandaloneDraggableWithDelayedHandle);
147+
148+
fixture.detectChanges();
149+
fixture.componentInstance.showHandle = true;
150+
fixture.detectChanges();
151+
152+
const dragElement = fixture.componentInstance.dragElement.nativeElement;
153+
const handle = fixture.componentInstance.handleElement.nativeElement;
154+
155+
expect(dragElement.style.transform).toBeFalsy();
156+
dragElementViaMouse(fixture, dragElement, 50, 100, handle);
157+
expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)');
158+
}));
159+
160+
it('should be able to use more than one handle to drag the element', fakeAsync(() => {
161+
const fixture = createComponent(StandaloneDraggableWithMultipleHandles);
162+
fixture.detectChanges();
163+
164+
const dragElement = fixture.componentInstance.dragElement.nativeElement;
165+
const handles = fixture.componentInstance.handles.map(handle => handle.element.nativeElement);
166+
167+
expect(dragElement.style.transform).toBeFalsy();
168+
dragElementViaMouse(fixture, dragElement, 50, 100, handles[1]);
169+
expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)');
170+
171+
dragElementViaMouse(fixture, dragElement, 100, 200, handles[0]);
172+
expect(dragElement.style.transform).toBe('translate3d(150px, 300px, 0px)');
173+
}));
174+
142175
});
143176

144177
describe('in a drop container', () => {
@@ -415,9 +448,48 @@ export class StandaloneDraggable {
415448
export class StandaloneDraggableWithHandle {
416449
@ViewChild('dragElement') dragElement: ElementRef<HTMLElement>;
417450
@ViewChild('handleElement') handleElement: ElementRef<HTMLElement>;
418-
@ViewChild(CdkDrag) dragInstance: CdkDrag;
419451
}
420452

453+
@Component({
454+
template: `
455+
<div #dragElement cdkDrag
456+
style="width: 100px; height: 100px; background: red; position: relative">
457+
<div
458+
#handleElement
459+
*ngIf="showHandle"
460+
cdkDragHandle style="width: 10px; height: 10px; background: green;"></div>
461+
</div>
462+
`
463+
})
464+
export class StandaloneDraggableWithDelayedHandle {
465+
@ViewChild('dragElement') dragElement: ElementRef<HTMLElement>;
466+
@ViewChild('handleElement') handleElement: ElementRef<HTMLElement>;
467+
showHandle = false;
468+
}
469+
470+
@Component({
471+
encapsulation: ViewEncapsulation.None,
472+
styles: [`
473+
.cdk-drag-handle {
474+
position: absolute;
475+
top: 0;
476+
background: green;
477+
width: 10px;
478+
height: 10px;
479+
}
480+
`],
481+
template: `
482+
<div #dragElement cdkDrag
483+
style="width: 100px; height: 100px; background: red; position: relative">
484+
<div cdkDragHandle style="left: 0;"></div>
485+
<div cdkDragHandle style="right: 0;"></div>
486+
</div>
487+
`
488+
})
489+
export class StandaloneDraggableWithMultipleHandles {
490+
@ViewChild('dragElement') dragElement: ElementRef<HTMLElement>;
491+
@ViewChildren(CdkDragHandle) handles: QueryList<CdkDragHandle>;
492+
}
421493

422494
@Component({
423495
template: `
@@ -542,11 +614,12 @@ export class ConnectedDropZones implements AfterViewInit {
542614
* @param element Element which is being dragged.
543615
* @param x Position along the x axis to which to drag the element.
544616
* @param y Position along the y axis to which to drag the element.
617+
* @param eventTarget Event to be passed as the target to the event handler.
545618
*/
546619
function dragElementViaMouse(fixture: ComponentFixture<any>,
547-
element: HTMLElement, x: number, y: number) {
620+
element: HTMLElement, x: number, y: number, eventTarget = element) {
548621

549-
dispatchMouseEvent(element, 'mousedown');
622+
dispatchMouseEvent(element, 'mousedown', undefined, undefined, eventTarget);
550623
fixture.detectChanges();
551624

552625
dispatchMouseEvent(document, 'mousemove', x, y);
@@ -562,11 +635,12 @@ function dragElementViaMouse(fixture: ComponentFixture<any>,
562635
* @param element Element which is being dragged.
563636
* @param x Position along the x axis to which to drag the element.
564637
* @param y Position along the y axis to which to drag the element.
638+
* @param eventTarget Event to be passed as the target to the event handler.
565639
*/
566640
function dragElementViaTouch(fixture: ComponentFixture<any>,
567-
element: HTMLElement, x: number, y: number) {
641+
element: HTMLElement, x: number, y: number, eventTarget = element) {
568642

569-
dispatchTouchEvent(element, 'touchstart');
643+
dispatchTouchEvent(element, 'touchstart', undefined, undefined, eventTarget);
570644
fixture.detectChanges();
571645

572646
dispatchTouchEvent(document, 'touchmove', x, y);

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';
@@ -41,6 +43,8 @@ const activeEventOptions = supportsPassiveEventListeners() ? {passive: false} :
4143
exportAs: 'cdkDrag',
4244
host: {
4345
'class': 'cdk-drag',
46+
'(mousedown)': '_startDragging($event)',
47+
'(touchstart)': '_startDragging($event)',
4448
}
4549
})
4650
export class CdkDrag implements AfterContentInit, OnDestroy {
@@ -84,8 +88,8 @@ export class CdkDrag implements AfterContentInit, OnDestroy {
8488
/** Drop container in which the CdkDrag resided when dragging began. */
8589
private _initialContainer: CdkDropContainer;
8690

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

9094
/** Element that will be used as a template to create the draggable item's preview. */
9195
@ContentChild(CdkDragPreview) _previewTemplate: CdkDragPreview;
@@ -130,11 +134,6 @@ export class CdkDrag implements AfterContentInit, OnDestroy {
130134
}
131135

132136
ngAfterContentInit() {
133-
// TODO: doesn't handle (pun intended) the handle being destroyed
134-
const dragElement = (this._handle ? this._handle.element : this.element).nativeElement;
135-
dragElement.addEventListener('mousedown', this._pointerDown);
136-
dragElement.addEventListener('touchstart', this._pointerDown);
137-
138137
// Webkit won't preventDefault on a dynamically-added `touchmove` listener, which means that
139138
// we need to add one ahead of time. See https://bugs.webkit.org/show_bug.cgi?id=184250.
140139
// TODO: move into a central registry.
@@ -157,8 +156,27 @@ export class CdkDrag implements AfterContentInit, OnDestroy {
157156
}
158157
}
159158

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

@@ -365,11 +383,13 @@ export class CdkDrag implements AfterContentInit, OnDestroy {
365383

366384
/**
367385
* Figures out the coordinates at which an element was picked up.
386+
* @param referenceElement Element that initiated the dragging.
368387
* @param event Event that initiated the dragging.
369388
*/
370-
private _getPointerPositionInElement(event: MouseEvent | TouchEvent): Point {
389+
private _getPointerPositionInElement(referenceElement: ElementRef<HTMLElement>,
390+
event: MouseEvent | TouchEvent): Point {
371391
const elementRect = this.element.nativeElement.getBoundingClientRect();
372-
const handleElement = this._handle ? this._handle.element.nativeElement : null;
392+
const handleElement = referenceElement === this.element ? null : referenceElement.nativeElement;
373393
const referenceRect = handleElement ? handleElement.getBoundingClientRect() : elementRect;
374394
const x = this._isTouchEvent(event) ? event.targetTouches[0].pageX - referenceRect.left :
375395
event.offsetX;

src/cdk/testing/dispatch-events.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,18 +25,18 @@ export function dispatchFakeEvent(node: Node | Window, type: string, canBubble?:
2525
}
2626

2727
/** Shorthand to dispatch a keyboard event with a specified key code. */
28-
export function dispatchKeyboardEvent(node: Node, type: string, keyCode: number, target?: Element):
29-
KeyboardEvent {
28+
export function dispatchKeyboardEvent(node: Node, type: string, keyCode: number,
29+
target?: EventTarget): KeyboardEvent {
3030
return dispatchEvent(node, createKeyboardEvent(type, keyCode, target)) as KeyboardEvent;
3131
}
3232

3333
/** Shorthand to dispatch a mouse event on the specified coordinates. */
34-
export function dispatchMouseEvent(node: Node, type: string, x = 0, y = 0,
35-
event = createMouseEvent(type, x, y)): MouseEvent {
36-
return dispatchEvent(node, event) as MouseEvent;
34+
export function dispatchMouseEvent(node: Node, type: string, x = 0, y = 0, target = node):
35+
MouseEvent {
36+
return dispatchEvent(node, createMouseEvent(type, x, y, target)) as MouseEvent;
3737
}
3838

3939
/** Shorthand to dispatch a touch event on the specified coordinates. */
40-
export function dispatchTouchEvent(node: Node, type: string, x = 0, y = 0) {
41-
return dispatchEvent(node, createTouchEvent(type, x, y));
40+
export function dispatchTouchEvent(node: Node, type: string, x = 0, y = 0, target = node) {
41+
return dispatchEvent(node, createTouchEvent(type, x, y, target));
4242
}

src/cdk/testing/event-objects.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
/** Creates a browser MouseEvent with the specified options. */
10-
export function createMouseEvent(type: string, x = 0, y = 0) {
10+
export function createMouseEvent(type: string, x = 0, y = 0, target?: EventTarget) {
1111
const event = document.createEvent('MouseEvent');
1212

1313
event.initMouseEvent(type,
@@ -26,11 +26,17 @@ export function createMouseEvent(type: string, x = 0, y = 0) {
2626
0, /* button */
2727
null /* relatedTarget */);
2828

29+
if (target) {
30+
Object.defineProperty(event, 'target', {
31+
get: () => target
32+
});
33+
}
34+
2935
return event;
3036
}
3137

3238
/** Creates a browser TouchEvent with the specified pointer coordinates. */
33-
export function createTouchEvent(type: string, pageX = 0, pageY = 0) {
39+
export function createTouchEvent(type: string, pageX = 0, pageY = 0, target?: EventTarget) {
3440
// In favor of creating events that work for most of the browsers, the event is created
3541
// as a basic UI Event. The necessary details for the event will be set manually.
3642
const event = document.createEvent('UIEvent');
@@ -42,14 +48,16 @@ export function createTouchEvent(type: string, pageX = 0, pageY = 0) {
4248
// the touch details.
4349
Object.defineProperties(event, {
4450
touches: {value: [touchDetails]},
45-
targetTouches: {value: [touchDetails]}
51+
targetTouches: {value: [touchDetails]},
52+
target: {value: target}
4653
});
4754

4855
return event;
4956
}
5057

5158
/** Dispatches a keydown event from an element. */
52-
export function createKeyboardEvent(type: string, keyCode: number, target?: Element, key?: string) {
59+
export function createKeyboardEvent(type: string, keyCode: number, target?: EventTarget,
60+
key?: string) {
5361
let event = document.createEvent('KeyboardEvent') as any;
5462
// Firefox does not support `initKeyboardEvent`, but supports `initKeyEvent`.
5563
let initEventFn = (event.initKeyEvent || event.initKeyboardEvent).bind(event);
@@ -60,9 +68,9 @@ export function createKeyboardEvent(type: string, keyCode: number, target?: Elem
6068
// Webkit Browsers don't set the keyCode when calling the init function.
6169
// See related bug https://bugs.webkit.org/show_bug.cgi?id=16735
6270
Object.defineProperties(event, {
63-
keyCode: { get: () => keyCode },
64-
key: { get: () => key },
65-
target: { get: () => target }
71+
keyCode: {get: () => keyCode},
72+
key: {get: () => key},
73+
target: {get: () => target}
6674
});
6775

6876
// IE won't set `defaultPrevented` on synthetic events so we need to do it manually.

src/lib/menu/menu.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1512,7 +1512,7 @@ describe('MatMenu', () => {
15121512
Object.defineProperty(event, 'buttons', {get: () => 1});
15131513
event.preventDefault = jasmine.createSpy('preventDefault spy');
15141514

1515-
dispatchMouseEvent(overlay.querySelector('[mat-menu-item]')!, 'mousedown', 0, 0, event);
1515+
dispatchEvent(overlay.querySelector('[mat-menu-item]')!, event);
15161516
expect(event.preventDefault).toHaveBeenCalled();
15171517
});
15181518

0 commit comments

Comments
 (0)