mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
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:
committed by
Jeremy Elbourn
parent
4e419855ad
commit
5d0ec23b1d
@@ -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) -->
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -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;
|
||||
|
||||
+21
@@ -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);
|
||||
}
|
||||
+7
@@ -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>
|
||||
+43
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user