From 5d0ec23b1d258504ab786e76f80bd9a1a76cd052 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 30 Aug 2018 16:03:14 +0200 Subject: [PATCH] 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. --- src/cdk/drag-drop/drag-drop.md | 9 ++ src/cdk/drag-drop/drag.spec.ts | 33 ++++++++ src/cdk/drag-drop/drag.ts | 84 ++++++++++++++----- src/cdk/drag-drop/drop.ts | 8 +- src/demo-app/dialog/dialog-demo.ts | 19 +++-- .../cdk-drag-drop-root-element-example.css | 21 +++++ .../cdk-drag-drop-root-element-example.html | 7 ++ .../cdk-drag-drop-root-element-example.ts | 43 ++++++++++ 8 files changed, 192 insertions(+), 32 deletions(-) create mode 100644 src/material-examples/cdk-drag-drop-root-element/cdk-drag-drop-root-element-example.css create mode 100644 src/material-examples/cdk-drag-drop-root-element/cdk-drag-drop-root-element-example.html create mode 100644 src/material-examples/cdk-drag-drop-root-element/cdk-drag-drop-root-element-example.ts diff --git a/src/cdk/drag-drop/drag-drop.md b/src/cdk/drag-drop/drag-drop.md index c1f4fa7696d..28b3a19d3c5 100644 --- a/src/cdk/drag-drop/drag-drop.md +++ b/src/cdk/drag-drop/drag-drop.md @@ -116,3 +116,12 @@ specific axis, you can set `cdkDragLockAxis` on `cdkDrag` or `lockAxis` on ` + +### 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. + + diff --git a/src/cdk/drag-drop/drag.spec.ts b/src/cdk/drag-drop/drag.spec.ts index 46662d837d9..25caeac081d 100644 --- a/src/cdk/drag-drop/drag.spec.ts +++ b/src/cdk/drag-drop/drag.spec.ts @@ -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: ` +
+
+
+ ` +}) +class DraggableWithAlternateRoot { + @ViewChild('dragElement') dragElement: ElementRef; + @ViewChild('dragRoot') dragRoot: ElementRef; + @ViewChild(CdkDrag) dragInstance: CdkDrag; +} + /** * Drags an element to a position on the page using the mouse. diff --git a/src/cdk/drag-drop/drag.ts b/src/cdk/drag-drop/drag.ts index fafcbc562a3..e0715703aeb 100644 --- a/src/cdk/drag-drop/drag.ts +++ b/src/cdk/drag-drop/drag.ts @@ -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 implements OnDestroy { +export class CdkDrag implements AfterViewInit, OnDestroy { private _document: Document; private _destroyed = new Subject(); @@ -127,6 +126,9 @@ export class CdkDrag 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; @@ -142,6 +144,13 @@ export class CdkDrag 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 = new EventEmitter(); @@ -197,7 +206,26 @@ export class CdkDrag 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 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 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 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 implements OnDestroy { } /** Handler for when the pointer is pressed down on the element or the handle. */ - private _pointerDown = (referenceElement: ElementRef, + private _pointerDown = (referenceElement: HTMLElement, event: MouseEvent | TouchEvent) => { const isDragging = this._isDragging(); @@ -277,7 +305,7 @@ export class CdkDrag 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 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 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 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 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 implements OnDestroy { * @param referenceElement Element that initiated the dragging. * @param event Event that initiated the dragging. */ - private _getPointerPositionInElement(referenceElement: ElementRef, + 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 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; diff --git a/src/cdk/drag-drop/drop.ts b/src/cdk/drag-drop/drop.ts index 9e7dc3da914..14ebe364092 100644 --- a/src/cdk/drag-drop/drop.ts +++ b/src/cdk/drag-drop/drop.ts @@ -187,7 +187,7 @@ export class CdkDrop 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 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 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 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 = []; diff --git a/src/demo-app/dialog/dialog-demo.ts b/src/demo-app/dialog/dialog-demo.ts index d9e54bd2288..b3640fa356b 100644 --- a/src/demo-app/dialog/dialog-demo.ts +++ b/src/demo-app/dialog/dialog-demo.ts @@ -90,16 +90,19 @@ export class DialogDemo { @Component({ selector: 'demo-jazz-dialog', template: ` -

It's Jazz!

+
+

It's Jazz!

- - How much? - - + + How much? + + -

{{ data.message }}

- - ` +

{{ data.message }}

+ + +
+ ` }) export class JazzDialog { private _dimesionToggle = false; diff --git a/src/material-examples/cdk-drag-drop-root-element/cdk-drag-drop-root-element-example.css b/src/material-examples/cdk-drag-drop-root-element/cdk-drag-drop-root-element-example.css new file mode 100644 index 00000000000..7304d815acd --- /dev/null +++ b/src/material-examples/cdk-drag-drop-root-element/cdk-drag-drop-root-element-example.css @@ -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); +} diff --git a/src/material-examples/cdk-drag-drop-root-element/cdk-drag-drop-root-element-example.html b/src/material-examples/cdk-drag-drop-root-element/cdk-drag-drop-root-element-example.html new file mode 100644 index 00000000000..005f6de708b --- /dev/null +++ b/src/material-examples/cdk-drag-drop-root-element/cdk-drag-drop-root-element-example.html @@ -0,0 +1,7 @@ + + + +
+ Drag the dialog around! +
+
diff --git a/src/material-examples/cdk-drag-drop-root-element/cdk-drag-drop-root-element-example.ts b/src/material-examples/cdk-drag-drop-root-element/cdk-drag-drop-root-element-example.ts new file mode 100644 index 00000000000..b9be4c47596 --- /dev/null +++ b/src/material-examples/cdk-drag-drop-root-element/cdk-drag-drop-root-element-example.ts @@ -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; + 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); + } +}