feat(drag-drop): add support for multiple handles and handles that are added after init (#12102)

This commit is contained in:
Kristiyan Kostadinov
2018-07-09 19:40:59 +02:00
committed by Joey Perrott
parent a6e28db2a0
commit f2963bc48d
3 changed files with 105 additions and 13 deletions
+73 -1
View File
@@ -6,6 +6,7 @@ import {
ViewChildren,
QueryList,
AfterViewInit,
ViewEncapsulation,
} from '@angular/core';
import {TestBed, ComponentFixture, fakeAsync, flush} from '@angular/core/testing';
import {DragDropModule} from './drag-drop-module';
@@ -14,6 +15,7 @@ import {CdkDrag} from './drag';
import {CdkDragDrop} from './drag-events';
import {moveItemInArray, transferArrayItem} from './drag-utils';
import {CdkDrop} from './drop';
import {CdkDragHandle} from './drag-handle';
const ITEM_HEIGHT = 25;
@@ -209,6 +211,37 @@ describe('CdkDrag', () => {
dragElementViaMouse(fixture, handle, 50, 100);
expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)');
}));
it('should be able to use a handle that was added after init', fakeAsync(() => {
const fixture = createComponent(StandaloneDraggableWithDelayedHandle);
fixture.detectChanges();
fixture.componentInstance.showHandle = true;
fixture.detectChanges();
const dragElement = fixture.componentInstance.dragElement.nativeElement;
const handle = fixture.componentInstance.handleElement.nativeElement;
expect(dragElement.style.transform).toBeFalsy();
dragElementViaMouse(fixture, handle, 50, 100);
expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)');
}));
it('should be able to use more than one handle to drag the element', fakeAsync(() => {
const fixture = createComponent(StandaloneDraggableWithMultipleHandles);
fixture.detectChanges();
const dragElement = fixture.componentInstance.dragElement.nativeElement;
const handles = fixture.componentInstance.handles.map(handle => handle.element.nativeElement);
expect(dragElement.style.transform).toBeFalsy();
dragElementViaMouse(fixture, handles[1], 50, 100);
expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)');
dragElementViaMouse(fixture, handles[0], 100, 200);
expect(dragElement.style.transform).toBe('translate3d(150px, 300px, 0px)');
}));
});
describe('in a drop container', () => {
@@ -463,9 +496,48 @@ export class StandaloneDraggable {
export class StandaloneDraggableWithHandle {
@ViewChild('dragElement') dragElement: ElementRef<HTMLElement>;
@ViewChild('handleElement') handleElement: ElementRef<HTMLElement>;
@ViewChild(CdkDrag) dragInstance: CdkDrag;
}
@Component({
template: `
<div #dragElement cdkDrag
style="width: 100px; height: 100px; background: red; position: relative">
<div
#handleElement
*ngIf="showHandle"
cdkDragHandle style="width: 10px; height: 10px; background: green;"></div>
</div>
`
})
export class StandaloneDraggableWithDelayedHandle {
@ViewChild('dragElement') dragElement: ElementRef<HTMLElement>;
@ViewChild('handleElement') handleElement: ElementRef<HTMLElement>;
showHandle = false;
}
@Component({
encapsulation: ViewEncapsulation.None,
styles: [`
.cdk-drag-handle {
position: absolute;
top: 0;
background: green;
width: 10px;
height: 10px;
}
`],
template: `
<div #dragElement cdkDrag
style="width: 100px; height: 100px; background: red; position: relative">
<div cdkDragHandle style="left: 0;"></div>
<div cdkDragHandle style="right: 0;"></div>
</div>
`
})
export class StandaloneDraggableWithMultipleHandles {
@ViewChild('dragElement') dragElement: ElementRef<HTMLElement>;
@ViewChildren(CdkDragHandle) handles: QueryList<CdkDragHandle>;
}
@Component({
template: `
+31 -11
View File
@@ -20,6 +20,8 @@ import {
EventEmitter,
ViewContainerRef,
EmbeddedViewRef,
ContentChildren,
QueryList,
} from '@angular/core';
import {CdkDragHandle} from './drag-handle';
import {DOCUMENT} from '@angular/platform-browser';
@@ -42,6 +44,8 @@ const activeEventOptions = supportsPassiveEventListeners() ? {passive: false} :
exportAs: 'cdkDrag',
host: {
'class': 'cdk-drag',
'(mousedown)': '_startDragging($event)',
'(touchstart)': '_startDragging($event)',
}
})
export class CdkDrag implements AfterContentInit, OnDestroy {
@@ -88,8 +92,8 @@ export class CdkDrag implements AfterContentInit, OnDestroy {
/** Cached scroll position on the page when the element was picked up. */
private _scrollPosition: {top: number, left: number};
/** Element that can be used to drag the draggable item. */
@ContentChild(CdkDragHandle) _handle: CdkDragHandle;
/** Elements that can be used to drag the draggable item. */
@ContentChildren(CdkDragHandle) _handles: QueryList<CdkDragHandle>;
/** Element that will be used as a template to create the draggable item's preview. */
@ContentChild(CdkDragPreview) _previewTemplate: CdkDragPreview;
@@ -135,11 +139,6 @@ export class CdkDrag implements AfterContentInit, OnDestroy {
}
ngAfterContentInit() {
// TODO: doesn't handle (pun intended) the handle being destroyed
const dragElement = (this._handle ? this._handle.element : this.element).nativeElement;
dragElement.addEventListener('mousedown', this._pointerDown);
dragElement.addEventListener('touchstart', this._pointerDown);
// WebKit won't preventDefault on a dynamically-added `touchmove` listener, which means that
// we need to add one ahead of time. See https://bugs.webkit.org/show_bug.cgi?id=184250.
// TODO: move into a central registry.
@@ -162,8 +161,27 @@ export class CdkDrag implements AfterContentInit, OnDestroy {
}
}
/** Starts the dragging sequence. */
_startDragging(event: MouseEvent | TouchEvent) {
// Delegate the event based on whether it started from a handle or the element itself.
if (this._handles.length) {
const targetHandle = this._handles.find(handle => {
const element = handle.element.nativeElement;
const target = event.target;
return !!target && (target === element || element.contains(target as HTMLElement));
});
if (targetHandle) {
this._pointerDown(targetHandle.element, event);
}
} else {
this._pointerDown(this.element, event);
}
}
/** Handler for when the pointer is pressed down on the element or the handle. */
private _pointerDown = (event: MouseEvent | TouchEvent) => {
private _pointerDown = (referenceElement: ElementRef<HTMLElement>,
event: MouseEvent | TouchEvent) => {
if (this._isDragging) {
return;
}
@@ -175,7 +193,7 @@ export class CdkDrag implements AfterContentInit, OnDestroy {
// If we have a custom preview template, the element won't be visible anyway so we avoid the
// extra `getBoundingClientRect` calls and just move the preview next to the cursor.
this._pickupPositionInElement = this._previewTemplate ? {x: 0, y: 0} :
this._getPointerPositionInElement(event);
this._getPointerPositionInElement(referenceElement, event);
this._pickupPositionOnPage = this._getPointerPositionOnPage(event);
this._registerMoveListeners(event);
@@ -371,11 +389,13 @@ export class CdkDrag implements AfterContentInit, OnDestroy {
/**
* Figures out the coordinates at which an element was picked up.
* @param referenceElement Element that initiated the dragging.
* @param event Event that initiated the dragging.
*/
private _getPointerPositionInElement(event: MouseEvent | TouchEvent): Point {
private _getPointerPositionInElement(referenceElement: ElementRef<HTMLElement>,
event: MouseEvent | TouchEvent): Point {
const elementRect = this.element.nativeElement.getBoundingClientRect();
const handleElement = this._handle ? this._handle.element.nativeElement : null;
const handleElement = referenceElement === this.element ? null : referenceElement.nativeElement;
const referenceRect = handleElement ? handleElement.getBoundingClientRect() : elementRect;
const x = this._isTouchEvent(event) ?
event.targetTouches[0].pageX - referenceRect.left - this._scrollPosition.left :
+1 -1
View File
@@ -11,7 +11,7 @@ export function createMouseEvent(type: string, x = 0, y = 0) {
const event = document.createEvent('MouseEvent');
event.initMouseEvent(type,
false, /* canBubble */
true, /* canBubble */
false, /* cancelable */
window, /* view */
0, /* detail */