Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 30, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/cdk/drag-drop/drag-drop.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 33 additions & 0 deletions src/cdk/drag-drop/drag.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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.
Expand Down
84 changes: 64 additions & 20 deletions src/cdk/drag-drop/drag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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>();

Expand Down Expand Up @@ -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>;

Expand All @@ -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>();

Expand Down Expand Up @@ -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();

Expand All @@ -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;
Expand All @@ -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 => {
Expand All @@ -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);
}
}

Expand All @@ -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();
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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;
Expand All @@ -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');
Expand All @@ -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 :
Expand Down Expand Up @@ -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. */
Expand All @@ -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;
Expand Down
8 changes: 4 additions & 4 deletions src/cdk/drag-drop/drop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 = [];
Expand Down
23 changes: 13 additions & 10 deletions src/demo-app/dialog/dialog-demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,16 +90,19 @@ export class DialogDemo {
@Component({
selector: 'demo-jazz-dialog',
template: `
<p>It's Jazz!</p>

<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>`
<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>

<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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
@@ -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>
Loading