feat(drag-drop): add the ability to set an alternate root element (#12895)

Adds the `cdkDragRootElement` input which allows consumers to pass in a selector that will be used to determine which elements is going to become draggable, starting from the `cdkDrag` element and going up the DOM.
This commit is contained in:
Kristiyan Kostadinov
2018-08-30 16:03:14 +02:00
committed by Jeremy Elbourn
parent 4e419855ad
commit 5d0ec23b1d
8 changed files with 192 additions and 32 deletions
+9
View File
@@ -116,3 +116,12 @@ specific axis, you can set `cdkDragLockAxis` on `cdkDrag` or `lockAxis` on `<cdk
to either `"x"` or `"y"`.
<!-- example(cdk-drag-drop-axis-lock) -->
### Alternate drag root element
If there's an element that you want to make draggable, but you don't have direct access to it, you
can use the `cdkDragRootElement` attribute. The attribute works by accepting a selector and looking
up the DOM until it finds an element that matches the selector. If an element is found, it'll become
the element that is moved as the user is dragging. This is useful for cases like making a dialog
draggable.
<!-- example(cdk-drag-drop-root-element) -->
+33
View File
@@ -322,6 +322,22 @@ describe('CdkDrag', () => {
expect(element.classList).not.toContain('cdk-drag-dragging');
}));
it('should be able to set an alternate drag root element', fakeAsync(() => {
const fixture = createComponent(DraggableWithAlternateRoot);
fixture.detectChanges();
const dragRoot = fixture.componentInstance.dragRoot.nativeElement;
const dragElement = fixture.componentInstance.dragElement.nativeElement;
expect(dragRoot.style.transform).toBeFalsy();
expect(dragElement.style.transform).toBeFalsy();
dragElementViaMouse(fixture, dragRoot, 50, 100);
expect(dragRoot.style.transform).toBe('translate3d(50px, 100px, 0px)');
expect(dragElement.style.transform).toBeFalsy();
}));
});
describe('draggable with a handle', () => {
@@ -1597,6 +1613,23 @@ class ConnectedDropZones implements AfterViewInit {
}
@Component({
template: `
<div #dragRoot class="alternate-root" style="width: 200px; height: 200px; background: hotpink">
<div
cdkDrag
cdkDragRootElement=".alternate-root"
#dragElement
style="width: 100px; height: 100px; background: red;"></div>
</div>
`
})
class DraggableWithAlternateRoot {
@ViewChild('dragElement') dragElement: ElementRef<HTMLElement>;
@ViewChild('dragRoot') dragRoot: ElementRef<HTMLElement>;
@ViewChild(CdkDrag) dragInstance: CdkDrag;
}
/**
* Drags an element to a position on the page using the mouse.
+64 -20
View File
@@ -10,6 +10,7 @@ import {Directionality} from '@angular/cdk/bidi';
import {ViewportRuler} from '@angular/cdk/scrolling';
import {DOCUMENT} from '@angular/common';
import {
AfterViewInit,
ContentChild,
ContentChildren,
Directive,
@@ -27,7 +28,7 @@ import {
ViewContainerRef,
} from '@angular/core';
import {merge, Observable, Subject} from 'rxjs';
import {takeUntil} from 'rxjs/operators';
import {takeUntil, take} from 'rxjs/operators';
import {DragDropRegistry} from './drag-drop-registry';
import {
CdkDragDrop,
@@ -60,11 +61,9 @@ const POINTER_DIRECTION_CHANGE_THRESHOLD = 5;
host: {
'class': 'cdk-drag',
'[class.cdk-drag-dragging]': '_isDragging()',
'(mousedown)': '_startDragging($event)',
'(touchstart)': '_startDragging($event)',
}
})
export class CdkDrag<T = any> implements OnDestroy {
export class CdkDrag<T = any> implements AfterViewInit, OnDestroy {
private _document: Document;
private _destroyed = new Subject<void>();
@@ -127,6 +126,9 @@ export class CdkDrag<T = any> implements OnDestroy {
/** Pointer position at which the last change in the delta occurred. */
private _pointerPositionAtLastDirectionChange: Point;
/** Root element that will be dragged by the user. */
private _rootElement: HTMLElement;
/** Elements that can be used to drag the draggable item. */
@ContentChildren(CdkDragHandle) _handles: QueryList<CdkDragHandle>;
@@ -142,6 +144,13 @@ export class CdkDrag<T = any> implements OnDestroy {
/** Locks the position of the dragged element along the specified axis. */
@Input('cdkDragLockAxis') lockAxis: 'x' | 'y';
/**
* Selector that will be used to determine the root draggable element, starting from
* the `cdkDrag` element and going up the DOM. Passing an alternate root element is useful
* when trying to enable dragging on an element that you might not have access to.
*/
@Input('cdkDragRootElement') rootElementSelector: string;
/** Emits when the user starts dragging the item. */
@Output('cdkDragStarted') started: EventEmitter<CdkDragStart> = new EventEmitter<CdkDragStart>();
@@ -197,7 +206,26 @@ export class CdkDrag<T = any> implements OnDestroy {
return this._placeholder;
}
/** Returns the root draggable element. */
getRootElement(): HTMLElement {
return this._rootElement;
}
ngAfterViewInit() {
// We need to wait for the zone to stabilize, in order for the reference
// element to be in the proper place in the DOM. This is mostly relevant
// for draggable elements inside portals since they get stamped out in
// their original DOM position and then they get transferred to the portal.
this._ngZone.onStable.asObservable().pipe(take(1)).subscribe(() => {
const rootElement = this._rootElement = this._getRootElement();
rootElement.addEventListener('mousedown', this._startDragging);
rootElement.addEventListener('touchstart', this._startDragging);
});
}
ngOnDestroy() {
this._rootElement.removeEventListener('mousedown', this._startDragging);
this._rootElement.removeEventListener('touchstart', this._startDragging);
this._destroyPreview();
this._destroyPlaceholder();
@@ -206,7 +234,7 @@ export class CdkDrag<T = any> implements OnDestroy {
if (this._isDragging()) {
// Since we move out the element to the end of the body while it's being
// dragged, we have to make sure that it's removed if it gets destroyed.
this._removeElement(this.element.nativeElement);
this._removeElement(this._rootElement);
}
this._nextSibling = null;
@@ -217,7 +245,7 @@ export class CdkDrag<T = any> implements OnDestroy {
}
/** Starts the dragging sequence. */
_startDragging(event: MouseEvent | TouchEvent) {
_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 => {
@@ -227,10 +255,10 @@ export class CdkDrag<T = any> implements OnDestroy {
});
if (targetHandle) {
this._pointerDown(targetHandle.element, event);
this._pointerDown(targetHandle.element.nativeElement, event);
}
} else {
this._pointerDown(this.element, event);
this._pointerDown(this._rootElement, event);
}
}
@@ -240,7 +268,7 @@ export class CdkDrag<T = any> implements OnDestroy {
}
/** Handler for when the pointer is pressed down on the element or the handle. */
private _pointerDown = (referenceElement: ElementRef<HTMLElement>,
private _pointerDown = (referenceElement: HTMLElement,
event: MouseEvent | TouchEvent) => {
const isDragging = this._isDragging();
@@ -277,7 +305,7 @@ export class CdkDrag<T = any> implements OnDestroy {
this.started.emit({source: this});
if (this.dropContainer) {
const element = this.element.nativeElement;
const element = this._rootElement;
// Grab the `nextSibling` before the preview and placeholder
// have been created so we don't get the preview by accident.
@@ -318,7 +346,7 @@ export class CdkDrag<T = any> implements OnDestroy {
pointerPosition.x - this._pickupPositionOnPage.x + this._passiveTransform.x;
activeTransform.y =
pointerPosition.y - this._pickupPositionOnPage.y + this._passiveTransform.y;
this._setTransform(this.element.nativeElement, activeTransform.x, activeTransform.y);
this._setTransform(this._rootElement, activeTransform.x, activeTransform.y);
}
// Since this event gets fired for every pixel while dragging, we only
@@ -362,12 +390,12 @@ export class CdkDrag<T = any> implements OnDestroy {
// It's important that we maintain the position, because moving the element around in the DOM
// can throw off `NgFor` which does smart diffing and re-creates elements only when necessary,
// while moving the existing elements in all other cases.
this.element.nativeElement.style.display = '';
this._rootElement.style.display = '';
if (this._nextSibling) {
this._nextSibling.parentNode!.insertBefore(this.element.nativeElement, this._nextSibling);
this._nextSibling.parentNode!.insertBefore(this._rootElement, this._nextSibling);
} else {
this._placeholder.parentNode!.appendChild(this.element.nativeElement);
this._placeholder.parentNode!.appendChild(this._rootElement);
}
this._destroyPreview();
@@ -430,7 +458,7 @@ export class CdkDrag<T = any> implements OnDestroy {
this._previewRef = viewRef;
this._setTransform(preview, this._pickupPositionOnPage.x, this._pickupPositionOnPage.y);
} else {
const element = this.element.nativeElement;
const element = this._rootElement;
const elementRect = element.getBoundingClientRect();
preview = element.cloneNode(true) as HTMLElement;
@@ -456,7 +484,7 @@ export class CdkDrag<T = any> implements OnDestroy {
);
placeholder = this._placeholderRef.rootNodes[0];
} else {
placeholder = this.element.nativeElement.cloneNode(true) as HTMLElement;
placeholder = this._rootElement.cloneNode(true) as HTMLElement;
}
placeholder.classList.add('cdk-drag-placeholder');
@@ -468,10 +496,10 @@ export class CdkDrag<T = any> implements OnDestroy {
* @param referenceElement Element that initiated the dragging.
* @param event Event that initiated the dragging.
*/
private _getPointerPositionInElement(referenceElement: ElementRef<HTMLElement>,
private _getPointerPositionInElement(referenceElement: HTMLElement,
event: MouseEvent | TouchEvent): Point {
const elementRect = this.element.nativeElement.getBoundingClientRect();
const handleElement = referenceElement === this.element ? null : referenceElement.nativeElement;
const elementRect = this._rootElement.getBoundingClientRect();
const handleElement = referenceElement === this._rootElement ? null : referenceElement;
const referenceRect = handleElement ? handleElement.getBoundingClientRect() : elementRect;
const x = this._isTouchEvent(event) ?
event.targetTouches[0].pageX - referenceRect.left - this._scrollPosition.left :
@@ -632,6 +660,23 @@ export class CdkDrag<T = any> implements OnDestroy {
positionSinceLastChange.y = y;
}
}
/** Gets the root draggable element, based on the `rootElementSelector`. */
private _getRootElement(): HTMLElement {
if (this.rootElementSelector) {
let currentElement = this.element.nativeElement.parentElement as HTMLElement | null;
while (currentElement) {
if (currentElement.matches(this.rootElementSelector)) {
return currentElement;
}
currentElement = currentElement.parentElement;
}
}
return this.element.nativeElement;
}
}
/** Parses a CSS time value to milliseconds. */
@@ -650,7 +695,6 @@ function getTransitionDurationInMs(element: HTMLElement): number {
return parseCssTimeUnitsToMs(rawDuration) + parseCssTimeUnitsToMs(rawDelay);
}
/** Point on the page or within an element. */
interface Point {
x: number;
+4 -4
View File
@@ -187,7 +187,7 @@ export class CdkDrop<T = any> implements OnInit, OnDestroy {
// Don't use items that are being dragged as a reference, because
// their element has been moved down to the bottom of the body.
if (newPositionReference && !this._dragDropRegistry.isDragging(newPositionReference)) {
const element = newPositionReference.element.nativeElement;
const element = newPositionReference.getRootElement();
element.parentElement!.insertBefore(placeholder, element);
this._activeDraggables.splice(newIndex, 0, item);
} else {
@@ -277,7 +277,7 @@ export class CdkDrop<T = any> implements OnInit, OnDestroy {
const isDraggedItem = sibling.drag === item;
const offset = isDraggedItem ? itemOffset : siblingOffset;
const elementToOffset = isDraggedItem ? item.getPlaceholderElement() :
sibling.drag.element.nativeElement;
sibling.drag.getRootElement();
// Update the offset to reflect the new position.
sibling.offset += offset;
@@ -320,7 +320,7 @@ export class CdkDrop<T = any> implements OnInit, OnDestroy {
// If the element is being dragged, we have to measure the
// placeholder, because the element is hidden.
drag.getPlaceholderElement() :
drag.element.nativeElement;
drag.getRootElement();
const clientRect = elementToMeasure.getBoundingClientRect();
return {
@@ -355,7 +355,7 @@ export class CdkDrop<T = any> implements OnInit, OnDestroy {
this._dragging = false;
// TODO(crisbeto): may have to wait for the animations to finish.
this._activeDraggables.forEach(item => item.element.nativeElement.style.transform = '');
this._activeDraggables.forEach(item => item.getRootElement().style.transform = '');
this._activeDraggables = [];
this._positionCache.items = [];
this._positionCache.siblings = [];
+11 -8
View File
@@ -90,16 +90,19 @@ export class DialogDemo {
@Component({
selector: 'demo-jazz-dialog',
template: `
<p>It's Jazz!</p>
<div cdkDrag cdkDragRootElement=".cdk-overlay-pane">
<p>It's Jazz!</p>
<mat-form-field>
<mat-label>How much?</mat-label>
<input matInput #howMuch>
</mat-form-field>
<mat-form-field>
<mat-label>How much?</mat-label>
<input matInput #howMuch>
</mat-form-field>
<p> {{ data.message }} </p>
<button type="button" (click)="dialogRef.close(howMuch.value)">Close dialog</button>
<button (click)="togglePosition()">Change dimensions</button>`
<p cdkDragHandle> {{ data.message }} </p>
<button type="button" (click)="dialogRef.close(howMuch.value)">Close dialog</button>
<button (click)="togglePosition()">Change dimensions</button>
</div>
`
})
export class JazzDialog {
private _dimesionToggle = false;
@@ -0,0 +1,21 @@
.dialog-content {
width: 200px;
height: 200px;
border: solid 1px #ccc;
cursor: move;
display: flex;
justify-content: center;
align-items: center;
background: #fff;
border-radius: 4px;
transition: box-shadow 200ms cubic-bezier(0, 0, 0.2, 1);
box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2),
0 2px 2px 0 rgba(0, 0, 0, 0.14),
0 1px 5px 0 rgba(0, 0, 0, 0.12);
}
.dialog-content:active {
box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2),
0 8px 10px 1px rgba(0, 0, 0, 0.14),
0 3px 14px 2px rgba(0, 0, 0, 0.12);
}
@@ -0,0 +1,7 @@
<button (click)="openDialog()">Open a draggable dialog</button>
<ng-template>
<div class="dialog-content" cdkDrag cdkDragRootElement=".cdk-overlay-pane">
Drag the dialog around!
</div>
</ng-template>
@@ -0,0 +1,43 @@
import {
Component,
ViewChild,
TemplateRef,
AfterViewInit,
ViewContainerRef,
OnDestroy,
} from '@angular/core';
import {Overlay, OverlayRef} from '@angular/cdk/overlay';
import {TemplatePortal} from '@angular/cdk/portal';
/**
* @title Drag&Drop with alternate root element
*/
@Component({
selector: 'cdk-drag-drop-root-element-example',
templateUrl: 'cdk-drag-drop-root-element-example.html',
styleUrls: ['cdk-drag-drop-root-element-example.css'],
})
export class CdkDragDropRootElementExample implements AfterViewInit, OnDestroy {
@ViewChild(TemplateRef) _dialogTemplate: TemplateRef<any>;
private _overlayRef: OverlayRef;
private _portal: TemplatePortal;
constructor(private _overlay: Overlay, private _viewContainerRef: ViewContainerRef) {}
ngAfterViewInit() {
this._portal = new TemplatePortal(this._dialogTemplate, this._viewContainerRef);
this._overlayRef = this._overlay.create({
positionStrategy: this._overlay.position().global().centerHorizontally().centerVertically(),
hasBackdrop: true
});
this._overlayRef.backdropClick().subscribe(() => this._overlayRef.detach());
}
ngOnDestroy() {
this._overlayRef.dispose();
}
openDialog() {
this._overlayRef.attach(this._portal);
}
}