refactor(drag-drop): move logic out of directives (#14350)

Moves the logic for `CdkDrag` and `CdkDropList` into separate, non-Angular-specific classes. This is a first step towards allowing consumers to attach drag&drop functionality to arbitrary DOM nodes.
This commit is contained in:
Kristiyan Kostadinov
2018-12-07 18:07:09 +01:00
committed by mmalerba
parent e0eb3dfb64
commit 19f9bca6fb
18 changed files with 1375 additions and 779 deletions
@@ -8,8 +8,8 @@
import {Directive, ElementRef, Inject, Optional, Input} from '@angular/core';
import {coerceBooleanProperty} from '@angular/cdk/coercion';
import {CDK_DRAG_PARENT} from './drag-parent';
import {toggleNativeDragInteractions} from './drag-styling';
import {CDK_DRAG_PARENT} from '../drag-parent';
import {toggleNativeDragInteractions} from '../drag-styling';
/** Handle that can be used to drag and CdkDrag instance. */
@Directive({
@@ -12,7 +12,7 @@ import {
ChangeDetectionStrategy,
} from '@angular/core';
import {TestBed, ComponentFixture, fakeAsync, flush, tick} from '@angular/core/testing';
import {DragDropModule} from './drag-drop-module';
import {DragDropModule} from '../drag-drop-module';
import {
createMouseEvent,
dispatchEvent,
@@ -21,13 +21,14 @@ import {
createTouchEvent,
} from '@angular/cdk/testing';
import {Directionality} from '@angular/cdk/bidi';
import {CdkDrag, CDK_DRAG_CONFIG, CdkDragConfig} from './drag';
import {CdkDragDrop} from './drag-events';
import {moveItemInArray} from './drag-utils';
import {CdkDrag, CDK_DRAG_CONFIG} from './drag';
import {CdkDragDrop} from '../drag-events';
import {moveItemInArray} from '../drag-utils';
import {CdkDropList} from './drop-list';
import {CdkDragHandle} from './drag-handle';
import {CdkDropListGroup} from './drop-list-group';
import {extendStyles} from './drag-styling';
import {extendStyles} from '../drag-styling';
import {DragRefConfig} from '../drag-ref';
const ITEM_HEIGHT = 25;
const ITEM_WIDTH = 75;
@@ -47,7 +48,7 @@ describe('CdkDrag', () => {
// have to deal with thresholds.
dragStartThreshold: dragDistance,
pointerDirectionChangeThreshold: 5
} as CdkDragConfig
} as DragRefConfig
},
...providers
],
+305
View File
@@ -0,0 +1,305 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Directionality} from '@angular/cdk/bidi';
import {ViewportRuler} from '@angular/cdk/scrolling';
import {DOCUMENT} from '@angular/common';
import {
AfterViewInit,
ContentChild,
ContentChildren,
Directive,
ElementRef,
EventEmitter,
Inject,
InjectionToken,
Input,
NgZone,
OnDestroy,
Optional,
Output,
QueryList,
SkipSelf,
ViewContainerRef,
} from '@angular/core';
import {coerceBooleanProperty} from '@angular/cdk/coercion';
import {Observable, Subscription, Observer} from 'rxjs';
import {startWith, take, map} from 'rxjs/operators';
import {DragDropRegistry} from '../drag-drop-registry';
import {
CdkDragDrop,
CdkDragEnd,
CdkDragEnter,
CdkDragExit,
CdkDragMove,
CdkDragStart,
} from '../drag-events';
import {CdkDragHandle} from './drag-handle';
import {CdkDragPlaceholder} from './drag-placeholder';
import {CdkDragPreview} from './drag-preview';
import {CDK_DROP_LIST} from '../drop-list-container';
import {CDK_DRAG_PARENT} from '../drag-parent';
import {DragRef, DragRefConfig} from '../drag-ref';
import {DropListRef} from '../drop-list-ref';
import {CdkDropListInternal as CdkDropList} from './drop-list';
/** Injection token that can be used to configure the behavior of `CdkDrag`. */
export const CDK_DRAG_CONFIG = new InjectionToken<DragRefConfig>('CDK_DRAG_CONFIG', {
providedIn: 'root',
factory: CDK_DRAG_CONFIG_FACTORY
});
/** @docs-private */
export function CDK_DRAG_CONFIG_FACTORY(): DragRefConfig {
return {dragStartThreshold: 5, pointerDirectionChangeThreshold: 5};
}
/** Element that can be moved inside a CdkDropList container. */
@Directive({
selector: '[cdkDrag]',
exportAs: 'cdkDrag',
host: {
'class': 'cdk-drag',
'[class.cdk-drag-dragging]': '_dragRef.isDragging()',
},
providers: [{provide: CDK_DRAG_PARENT, useExisting: CdkDrag}]
})
export class CdkDrag<T = any> implements AfterViewInit, OnDestroy {
/** Subscription to the stream that initializes the root element. */
private _rootElementInitSubscription = Subscription.EMPTY;
/** Reference to the underlying drag instance. */
_dragRef: DragRef<CdkDrag<T>>;
/** Elements that can be used to drag the draggable item. */
@ContentChildren(CdkDragHandle, {descendants: true}) _handles: QueryList<CdkDragHandle>;
/** Element that will be used as a template to create the draggable item's preview. */
@ContentChild(CdkDragPreview) _previewTemplate: CdkDragPreview;
/** Template for placeholder element rendered to show where a draggable would be dropped. */
@ContentChild(CdkDragPlaceholder) _placeholderTemplate: CdkDragPlaceholder;
/** Arbitrary data to attach to this drag instance. */
@Input('cdkDragData') data: T;
/** 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;
/**
* Selector that will be used to determine the element to which the draggable's position will
* be constrained. Matching starts from the element's parent and goes up the DOM until a matching
* element has been found.
*/
@Input('cdkDragBoundary') boundaryElementSelector: string;
/** Whether starting to drag this element is disabled. */
@Input('cdkDragDisabled')
get disabled(): boolean {
return this._disabled || (this.dropContainer && this.dropContainer.disabled);
}
set disabled(value: boolean) {
this._disabled = coerceBooleanProperty(value);
}
private _disabled = false;
/** Emits when the user starts dragging the item. */
@Output('cdkDragStarted') started: EventEmitter<CdkDragStart> = new EventEmitter<CdkDragStart>();
/** Emits when the user stops dragging an item in the container. */
@Output('cdkDragEnded') ended: EventEmitter<CdkDragEnd> = new EventEmitter<CdkDragEnd>();
/** Emits when the user has moved the item into a new container. */
@Output('cdkDragEntered') entered: EventEmitter<CdkDragEnter<any>> =
new EventEmitter<CdkDragEnter<any>>();
/** Emits when the user removes the item its container by dragging it into another container. */
@Output('cdkDragExited') exited: EventEmitter<CdkDragExit<any>> =
new EventEmitter<CdkDragExit<any>>();
/** Emits when the user drops the item inside a container. */
@Output('cdkDragDropped') dropped: EventEmitter<CdkDragDrop<any>> =
new EventEmitter<CdkDragDrop<any>>();
/**
* Emits as the user is dragging the item. Use with caution,
* because this event will fire for every pixel that the user has dragged.
*/
@Output('cdkDragMoved') moved: Observable<CdkDragMove<T>> =
Observable.create((observer: Observer<CdkDragMove<T>>) => {
const subscription = this._dragRef.moved.pipe(map(movedEvent => ({
source: this,
pointerPosition: movedEvent.pointerPosition,
event: movedEvent.event,
delta: movedEvent.delta
}))).subscribe(observer);
return () => {
subscription.unsubscribe();
};
});
constructor(
/** Element that the draggable is attached to. */
public element: ElementRef<HTMLElement>,
/** Droppable container that the draggable is a part of. */
@Inject(CDK_DROP_LIST) @Optional() @SkipSelf()
public dropContainer: CdkDropList,
@Inject(DOCUMENT) private _document: any,
private _ngZone: NgZone,
private _viewContainerRef: ViewContainerRef,
private _viewportRuler: ViewportRuler,
private _dragDropRegistry: DragDropRegistry<DragRef, DropListRef>,
@Inject(CDK_DRAG_CONFIG) private _config: DragRefConfig,
@Optional() private _dir: Directionality) {
const ref = this._dragRef = new DragRef(element, this._document, this._ngZone,
this._viewContainerRef, this._viewportRuler, this._dragDropRegistry,
this._config, this.dropContainer ? this.dropContainer._dropListRef : undefined,
this._dir);
ref.data = this;
ref.beforeStarted.subscribe(() => {
if (!ref.isDragging()) {
ref.disabled = this.disabled;
ref.lockAxis = this.lockAxis;
ref.withBoundaryElement(this._getBoundaryElement());
}
});
this._proxyEvents(ref);
}
/**
* Returns the element that is being used as a placeholder
* while the current element is being dragged.
*/
getPlaceholderElement(): HTMLElement {
return this._dragRef.getPlaceholderElement();
}
/** Returns the root draggable element. */
getRootElement(): HTMLElement {
return this._dragRef.getRootElement();
}
/** Resets a standalone drag item to its initial position. */
reset(): void {
this._dragRef.reset();
}
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._rootElementInitSubscription = this._ngZone.onStable.asObservable()
.pipe(take(1))
.subscribe(() => {
const rootElement = this._getRootElement();
if (rootElement.nodeType !== this._document.ELEMENT_NODE) {
throw Error(`cdkDrag must be attached to an element node. ` +
`Currently attached to "${rootElement.nodeName}".`);
}
this._dragRef
.withRootElement(rootElement)
.withPlaceholderTemplate(this._placeholderTemplate)
.withPreviewTemplate(this._previewTemplate);
this._handles.changes
.pipe(startWith(this._handles))
.subscribe((handleList: QueryList<CdkDragHandle>) => {
this._dragRef.withHandles(handleList.filter(handle => handle._parentDrag === this));
});
});
}
ngOnDestroy() {
this._rootElementInitSubscription.unsubscribe();
this._dragRef.dispose();
}
/** Gets the root draggable element, based on the `rootElementSelector`. */
private _getRootElement(): HTMLElement {
const element = this.element.nativeElement;
const rootElement = this.rootElementSelector ?
getClosestMatchingAncestor(element, this.rootElementSelector) : null;
return rootElement || element;
}
/** Gets the boundary element, based on the `boundaryElementSelector`. */
private _getBoundaryElement() {
const selector = this.boundaryElementSelector;
return selector ? getClosestMatchingAncestor(this.element.nativeElement, selector) : null;
}
/**
* Proxies the events from a DragRef to events that
* match the interfaces of the CdkDrag outputs.
*/
private _proxyEvents(ref: DragRef<CdkDrag<T>>) {
ref.started.subscribe(() => {
this.started.emit({source: this});
});
ref.ended.subscribe(() => {
this.ended.emit({source: this});
});
ref.entered.subscribe(event => {
this.entered.emit({
container: event.container.data,
item: this
});
});
ref.exited.subscribe(event => {
this.exited.emit({
container: event.container.data,
item: this
});
});
ref.dropped.subscribe(event => {
this.dropped.emit({
previousIndex: event.previousIndex,
currentIndex: event.currentIndex,
previousContainer: event.previousContainer.data,
container: event.container.data,
isPointerOverContainer: event.isPointerOverContainer,
item: this
});
});
}
}
/** Gets the closest ancestor of an element that matches a selector. */
function getClosestMatchingAncestor(element: HTMLElement, selector: string) {
let currentElement = element.parentElement as HTMLElement | null;
while (currentElement) {
// IE doesn't support `matches` so we have to fall back to `msMatchesSelector`.
if (currentElement.matches ? currentElement.matches(selector) :
(currentElement as any).msMatchesSelector(selector)) {
return currentElement;
}
currentElement = currentElement.parentElement;
}
return null;
}
+318
View File
@@ -0,0 +1,318 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {coerceArray, coerceBooleanProperty} from '@angular/cdk/coercion';
import {
ContentChildren,
ElementRef,
EventEmitter,
forwardRef,
Input,
OnDestroy,
Output,
QueryList,
Optional,
Directive,
ChangeDetectorRef,
SkipSelf,
Inject,
} from '@angular/core';
import {DOCUMENT} from '@angular/common';
import {Directionality} from '@angular/cdk/bidi';
import {CdkDrag} from './drag';
import {DragDropRegistry} from '../drag-drop-registry';
import {CdkDragDrop, CdkDragEnter, CdkDragExit, CdkDragSortEvent} from '../drag-events';
import {CDK_DROP_LIST_CONTAINER, CdkDropListContainer} from '../drop-list-container';
import {CdkDropListGroup} from './drop-list-group';
import {DropListRef} from '../drop-list-ref';
import {DragRef} from '../drag-ref';
/** Counter used to generate unique ids for drop zones. */
let _uniqueIdCounter = 0;
/**
* Internal compile-time-only representation of a `CdkDropList`.
* Used to avoid circular import issues between the `CdkDropList` and the `CdkDrag`.
* @docs-private
*/
export interface CdkDropListInternal extends CdkDropList {}
// @breaking-change 8.0.0 `CdkDropList` implements `CdkDropListContainer` for backwards
// compatiblity. The implements clause, as well as all the methods that it enforces can
// be removed when `CdkDropListContainer` is deleted.
/** Container that wraps a set of draggable items. */
@Directive({
selector: '[cdkDropList], cdk-drop-list',
exportAs: 'cdkDropList',
providers: [
// Prevent child drop lists from picking up the same group as their parent.
{provide: CdkDropListGroup, useValue: undefined},
{provide: CDK_DROP_LIST_CONTAINER, useExisting: CdkDropList},
],
host: {
'class': 'cdk-drop-list',
'[id]': 'id',
'[class.cdk-drop-list-dragging]': '_dropListRef.isDragging()'
}
})
export class CdkDropList<T = any> implements CdkDropListContainer, OnDestroy {
/** Keeps track of the drop lists that are currently on the page. */
private static _dropLists: CdkDropList[] = [];
/** Reference to the underlying drop list instance. */
_dropListRef: DropListRef<CdkDropList<T>>;
/** Draggable items in the container. */
@ContentChildren(forwardRef(() => CdkDrag)) _draggables: QueryList<CdkDrag>;
/**
* Other draggable containers that this container is connected to and into which the
* container's items can be transferred. Can either be references to other drop containers,
* or their unique IDs.
*/
@Input('cdkDropListConnectedTo')
connectedTo: (CdkDropList | string)[] | CdkDropList | string = [];
/** Arbitrary data to attach to this container. */
@Input('cdkDropListData') data: T;
/** Direction in which the list is oriented. */
@Input('cdkDropListOrientation') orientation: 'horizontal' | 'vertical' = 'vertical';
/**
* Unique ID for the drop zone. Can be used as a reference
* in the `connectedTo` of another `CdkDropList`.
*/
@Input() id: string = `cdk-drop-list-${_uniqueIdCounter++}`;
/** Locks the position of the draggable elements inside the container along the specified axis. */
@Input('cdkDropListLockAxis') lockAxis: 'x' | 'y';
/** Whether starting a dragging sequence from this container is disabled. */
@Input('cdkDropListDisabled')
get disabled(): boolean { return this._dropListRef.disabled; }
set disabled(value: boolean) {
this._dropListRef.disabled = coerceBooleanProperty(value);
}
/**
* Function that is used to determine whether an item
* is allowed to be moved into a drop container.
*/
@Input('cdkDropListEnterPredicate')
enterPredicate: (drag: CdkDrag, drop: CdkDropList) => boolean = () => true
/** Emits when the user drops an item inside the container. */
@Output('cdkDropListDropped')
dropped: EventEmitter<CdkDragDrop<T, any>> = new EventEmitter<CdkDragDrop<T, any>>();
/**
* Emits when the user has moved a new drag item into this container.
*/
@Output('cdkDropListEntered')
entered: EventEmitter<CdkDragEnter<T>> = new EventEmitter<CdkDragEnter<T>>();
/**
* Emits when the user removes an item from the container
* by dragging it into another container.
*/
@Output('cdkDropListExited')
exited: EventEmitter<CdkDragExit<T>> = new EventEmitter<CdkDragExit<T>>();
/** Emits as the user is swapping items while actively dragging. */
@Output('cdkDropListSorted')
sorted: EventEmitter<CdkDragSortEvent<T>> = new EventEmitter<CdkDragSortEvent<T>>();
constructor(
public element: ElementRef<HTMLElement>,
dragDropRegistry: DragDropRegistry<DragRef, DropListRef>,
private _changeDetectorRef: ChangeDetectorRef,
@Optional() dir?: Directionality,
@Optional() @SkipSelf() private _group?: CdkDropListGroup<CdkDropList>,
// @breaking-change 8.0.0 `_document` parameter to be made required.
@Optional() @Inject(DOCUMENT) _document?: any) {
// @breaking-change 8.0.0 Remove || once `_document` parameter is required.
const ref = this._dropListRef = new DropListRef(element, dragDropRegistry,
_document || document, dir);
ref.data = this;
ref.enterPredicate = (drag: DragRef<CdkDrag>, drop: DropListRef<CdkDropList>) => {
return this.enterPredicate(drag.data, drop.data);
};
this._syncInputs(ref);
this._proxyEvents(ref);
CdkDropList._dropLists.push(this);
if (_group) {
_group._items.add(this);
}
}
ngOnDestroy() {
const index = CdkDropList._dropLists.indexOf(this);
this._dropListRef.dispose();
if (index > -1) {
CdkDropList._dropLists.splice(index, 1);
}
if (this._group) {
this._group._items.delete(this);
}
}
/** Starts dragging an item. */
start(): void {
this._dropListRef.start();
}
/**
* Drops an item into this container.
* @param item Item being dropped into the container.
* @param currentIndex Index at which the item should be inserted.
* @param previousContainer Container from which the item got dragged in.
* @param isPointerOverContainer Whether the user's pointer was over the
* container when the item was dropped.
*/
drop(item: CdkDrag, currentIndex: number, previousContainer: Partial<CdkDropListContainer>,
isPointerOverContainer: boolean): void {
this._dropListRef.drop(item._dragRef, currentIndex,
(previousContainer as CdkDropList)._dropListRef, isPointerOverContainer);
}
/**
* Emits an event to indicate that the user moved an item into the container.
* @param item Item that was moved into the container.
* @param pointerX Position of the item along the X axis.
* @param pointerY Position of the item along the Y axis.
*/
enter(item: CdkDrag, pointerX: number, pointerY: number): void {
this._dropListRef.enter(item._dragRef, pointerX, pointerY);
}
/**
* Removes an item from the container after it was dragged into another container by the user.
* @param item Item that was dragged out.
*/
exit(item: CdkDrag): void {
this._dropListRef.exit(item._dragRef);
}
/**
* Figures out the index of an item in the container.
* @param item Item whose index should be determined.
*/
getItemIndex(item: CdkDrag): number {
return this._dropListRef.getItemIndex(item._dragRef);
}
/**
* Sorts an item inside the container based on its position.
* @param item Item to be sorted.
* @param pointerX Position of the item along the X axis.
* @param pointerY Position of the item along the Y axis.
* @param pointerDelta Direction in which the pointer is moving along each axis.
*/
_sortItem(item: CdkDrag, pointerX: number, pointerY: number,
pointerDelta: {x: number, y: number}): void {
return this._dropListRef._sortItem(item._dragRef, pointerX, pointerY, pointerDelta);
}
/**
* Figures out whether an item should be moved into a sibling
* drop container, based on its current position.
* @param item Drag item that is being moved.
* @param x Position of the item along the X axis.
* @param y Position of the item along the Y axis.
*/
_getSiblingContainerFromPosition(item: CdkDrag, x: number, y: number):
CdkDropListContainer | null {
const result = this._dropListRef._getSiblingContainerFromPosition(item._dragRef, x, y);
return result ? result.data : null;
}
/**
* Checks whether the user's pointer is positioned over the container.
* @param x Pointer position along the X axis.
* @param y Pointer position along the Y axis.
*/
_isOverContainer(x: number, y: number): boolean {
return this._dropListRef._isOverContainer(x, y);
}
/** Syncs the inputs of the CdkDropList with the options of the underlying DropListRef. */
private _syncInputs(ref: DropListRef<CdkDropList>) {
ref.beforeStarted.subscribe(() => {
const siblings = coerceArray(this.connectedTo).map(drop => {
return typeof drop === 'string' ?
CdkDropList._dropLists.find(list => list.id === drop)! : drop;
});
if (this._group) {
this._group._items.forEach(drop => {
if (siblings.indexOf(drop) === -1) {
siblings.push(drop);
}
});
}
ref.lockAxis = this.lockAxis;
ref
.connectedTo(siblings.filter(drop => drop && drop !== this).map(list => list._dropListRef))
.withOrientation(this.orientation)
.withItems(this._draggables.map(drag => drag._dragRef));
});
}
/**
* Proxies the events from a DropListRef to events that
* match the interfaces of the CdkDropList outputs.
*/
private _proxyEvents(ref: DropListRef<CdkDropList>) {
ref.beforeStarted.subscribe(() => {
this._changeDetectorRef.markForCheck();
});
ref.entered.subscribe(event => {
this.entered.emit({
container: this,
item: event.item.data
});
});
ref.exited.subscribe(event => {
this.exited.emit({
container: this,
item: event.item.data
});
});
ref.sorted.subscribe(event => {
this.sorted.emit({
previousIndex: event.previousIndex,
currentIndex: event.currentIndex,
container: this,
item: event.item.data
});
});
ref.dropped.subscribe(event => {
this.dropped.emit({
previousIndex: event.previousIndex,
currentIndex: event.currentIndex,
previousContainer: event.previousContainer.data,
container: event.container.data,
item: event.item.data,
isPointerOverContainer: event.isPointerOverContainer
});
});
}
}
+6 -6
View File
@@ -7,12 +7,12 @@
*/
import {NgModule} from '@angular/core';
import {CdkDropList} from './drop-list';
import {CdkDropListGroup} from './drop-list-group';
import {CdkDrag} from './drag';
import {CdkDragHandle} from './drag-handle';
import {CdkDragPreview} from './drag-preview';
import {CdkDragPlaceholder} from './drag-placeholder';
import {CdkDropList} from './directives/drop-list';
import {CdkDropListGroup} from './directives/drop-list-group';
import {CdkDrag} from './directives/drag';
import {CdkDragHandle} from './directives/drag-handle';
import {CdkDragPreview} from './directives/drag-preview';
import {CdkDragPlaceholder} from './directives/drag-placeholder';
@NgModule({
declarations: [
+2 -14
View File
@@ -9,8 +9,8 @@ import {
} from '@angular/cdk/testing';
import {DragDropRegistry} from './drag-drop-registry';
import {DragDropModule} from './drag-drop-module';
import {CdkDrag} from './drag';
import {CdkDropList} from './drop-list';
import {CdkDrag} from './directives/drag';
import {CdkDropList} from './directives/drop-list';
describe('DragDropRegistry', () => {
let fixture: ComponentFixture<SimpleDropZone>;
@@ -160,18 +160,6 @@ describe('DragDropRegistry', () => {
expect(() => registry.registerDropContainer(testComponent.dropInstances.first)).not.toThrow();
});
it('should throw when trying to register a different container with the same id', () => {
expect(() => {
testComponent.showDuplicateContainer = true;
fixture.detectChanges();
}).toThrowError(/Drop instance with id \"items\" has already been registered/);
});
it('should be able to get a drop container by its id', () => {
expect(registry.getDropContainer('items')).toBe(testComponent.dropInstances.first);
expect(registry.getDropContainer('does-not-exist')).toBeFalsy();
});
it('should not prevent the default `touchmove` actions when nothing is being dragged', () => {
expect(dispatchTouchEvent(document, 'touchmove').defaultPrevented).toBe(false);
});
+5 -1
View File
@@ -166,7 +166,11 @@ export class DragDropRegistry<I, C extends {id: string}> implements OnDestroy {
return this._activeDragInstances.has(drag);
}
/** Gets a drop container by its id. */
/**
* Gets a drop container by its id.
* @deprecated No longer being used. To be removed.
* @breaking-change 8.0.0
*/
getDropContainer(id: string): C | undefined {
return Array.from(this._dropInstances).find(instance => instance.id === id);
}
+7 -7
View File
@@ -6,8 +6,8 @@
* found in the LICENSE file at https://angular.io/license
*/
import {CdkDrag} from './drag';
import {CdkDropListContainer} from './drop-list-container';
import {CdkDrag} from './directives/drag';
import {CdkDropList} from './directives/drop-list';
/** Event emitted when the user starts dragging a draggable. */
export interface CdkDragStart<T = any> {
@@ -24,7 +24,7 @@ export interface CdkDragEnd<T = any> {
/** Event emitted when the user moves an item into a new drop container. */
export interface CdkDragEnter<T = any, I = T> {
/** Container into which the user has moved the item. */
container: CdkDropListContainer<T>;
container: CdkDropList<T>;
/** Item that was removed from the container. */
item: CdkDrag<I>;
}
@@ -35,7 +35,7 @@ export interface CdkDragEnter<T = any, I = T> {
*/
export interface CdkDragExit<T = any, I = T> {
/** Container from which the user has a removed an item. */
container: CdkDropListContainer<T>;
container: CdkDropList<T>;
/** Item that was removed from the container. */
item: CdkDrag<I>;
}
@@ -50,9 +50,9 @@ export interface CdkDragDrop<T, O = T> {
/** Item that is being dropped. */
item: CdkDrag;
/** Container in which the item was dropped. */
container: CdkDropListContainer<T>;
container: CdkDropList<T>;
/** Container from which the item was picked up. Can be the same as the `container`. */
previousContainer: CdkDropListContainer<O>;
previousContainer: CdkDropList<O>;
/** Whether the user's pointer was over the container when the item was dropped. */
isPointerOverContainer: boolean;
}
@@ -81,7 +81,7 @@ export interface CdkDragSortEvent<T = any, I = T> {
/** Index that the item is currently in. */
currentIndex: number;
/** Container that the item belongs to. */
container: CdkDropListContainer<T>;
container: CdkDropList<T>;
/** Item that is being sorted. */
item: CdkDrag<I>;
}
File diff suppressed because it is too large Load Diff
+19 -4
View File
@@ -7,9 +7,16 @@
*/
import {InjectionToken, QueryList, ElementRef} from '@angular/core';
import {CdkDrag} from './drag';
import {CdkDrag} from './directives/drag';
/**
* @deprecated To be removed. No longer being used. Previously the interface was used to avoid
* circular imports between `CdkDrag` and `CdkDropList`, however now we're using the
* `CdkDropListInternal` interface to achieve the same result, without having to maintain
* this large of an interface.
* @breaking-change 8.0.0
*/
export interface CdkDropListContainer<T = any> {
/** DOM node that corresponds to the drop container. */
element: ElementRef<HTMLElement>;
@@ -40,7 +47,7 @@ export interface CdkDropListContainer<T = any> {
* @param isPointerOverContainer Whether the user's pointer was over the
* container when the item was dropped.
*/
drop(item: CdkDrag, currentIndex: number, previousContainer: CdkDropListContainer,
drop(item: CdkDrag, currentIndex: number, previousContainer: Partial<CdkDropListContainer>,
isPointerOverContainer: boolean): void;
/**
@@ -73,5 +80,13 @@ export interface CdkDropListContainer<T = any> {
* Injection token that is used to provide a CdkDropList instance to CdkDrag.
* Used for avoiding circular imports.
*/
export const CDK_DROP_LIST_CONTAINER =
new InjectionToken<CdkDropListContainer>('CDK_DROP_LIST_CONTAINER');
export const CDK_DROP_LIST = new InjectionToken<CdkDropListContainer>('CDK_DROP_LIST');
/**
* Injection token that is used to provide a CdkDropList instance to CdkDrag.
* Used for avoiding circular imports.
* @deprecated Use `CDK_DROP_LIST` instead.
* @breaking-change 8.0.0
*/
export const CDK_DROP_LIST_CONTAINER = CDK_DROP_LIST;
@@ -6,34 +6,15 @@
* found in the LICENSE file at https://angular.io/license
*/
import {coerceArray, coerceBooleanProperty} from '@angular/cdk/coercion';
import {
ContentChildren,
ElementRef,
EventEmitter,
forwardRef,
Input,
OnDestroy,
OnInit,
Output,
QueryList,
Optional,
Directive,
ChangeDetectorRef,
SkipSelf,
Inject,
} from '@angular/core';
import {Directionality} from '@angular/cdk/bidi';
import {DOCUMENT} from '@angular/common';
import {CdkDrag} from './drag';
import {ElementRef} from '@angular/core';
import {DragDropRegistry} from './drag-drop-registry';
import {CdkDragDrop, CdkDragEnter, CdkDragExit, CdkDragSortEvent} from './drag-events';
import {Directionality} from '@angular/cdk/bidi';
import {Subject} from 'rxjs';
import {moveItemInArray} from './drag-utils';
import {CDK_DROP_LIST_CONTAINER} from './drop-list-container';
import {CdkDropListGroup} from './drop-list-group';
import {DragRefInternal as DragRef} from './drag-ref';
/** Counter used to generate unique ids for drop zones. */
/** Counter used to generate unique ids for drop refs. */
let _uniqueIdCounter = 0;
/**
@@ -61,7 +42,7 @@ interface PositionCache {
*/
interface ItemPositionCacheEntry {
/** Instance of the drag item. */
drag: CdkDrag;
drag: DragRef;
/** Dimensions of the item. */
clientRect: ClientRect;
/** Amount by which the item has been moved since dragging started. */
@@ -74,126 +55,81 @@ interface ItemPositionCacheEntry {
*/
interface ListPositionCacheEntry {
/** Instance of the drop list. */
drop: CdkDropList;
drop: DropListRef;
/** Dimensions of the list. */
clientRect: ClientRect;
}
/** Container that wraps a set of draggable items. */
@Directive({
selector: '[cdkDropList], cdk-drop-list',
exportAs: 'cdkDropList',
providers: [
// Prevent child drop lists from picking up the same group as their parent.
{provide: CdkDropListGroup, useValue: undefined},
{provide: CDK_DROP_LIST_CONTAINER, useExisting: CdkDropList},
],
host: {
'class': 'cdk-drop-list',
'[id]': 'id',
'[class.cdk-drop-list-dragging]': '_dragging'
}
})
export class CdkDropList<T = any> implements OnInit, OnDestroy {
private _document: Document | undefined;
/**
* Internal compile-time-only representation of a `DropListRef`.
* Used to avoid circular import issues between the `DropListRef` and the `DragRef`.
* @docs-private
*/
export interface DropListRefInternal extends DropListRef {}
/** Draggable items in the container. */
@ContentChildren(forwardRef(() => CdkDrag)) _draggables: QueryList<CdkDrag>;
/**
* Reference to a drop list. Used to manipulate or dispose of the container.
* @docs-private
*/
export class DropListRef<T = any> {
private _document: Document;
/**
* Other draggable containers that this container is connected to and into which the
* container's items can be transferred. Can either be references to other drop containers,
* or their unique IDs.
* Unique ID for the drop list.
* @deprecated No longer being used. To be removed.
* @breaking-change 8.0.0
*/
@Input('cdkDropListConnectedTo')
connectedTo: (CdkDropList | string)[] | CdkDropList | string = [];
/** Arbitrary data to attach to this container. */
@Input('cdkDropListData') data: T;
/** Direction in which the list is oriented. */
@Input('cdkDropListOrientation') orientation: 'horizontal' | 'vertical' = 'vertical';
/**
* Unique ID for the drop zone. Can be used as a reference
* in the `connectedTo` of another `CdkDropList`.
*/
@Input() id: string = `cdk-drop-list-${_uniqueIdCounter++}`;
/** Locks the position of the draggable elements inside the container along the specified axis. */
@Input('cdkDropListLockAxis') lockAxis: 'x' | 'y';
id = `cdk-drop-list-ref-${_uniqueIdCounter++}`;
/** Whether starting a dragging sequence from this container is disabled. */
@Input('cdkDropListDisabled')
get disabled(): boolean { return this._disabled; }
set disabled(value: boolean) {
this._disabled = coerceBooleanProperty(value);
}
private _disabled = false;
disabled: boolean = false;
/** Locks the position of the draggable elements inside the container along the specified axis. */
lockAxis: 'x' | 'y';
/**
* Function that is used to determine whether an item
* is allowed to be moved into a drop container.
*/
@Input('cdkDropListEnterPredicate')
enterPredicate: (drag: CdkDrag, drop: CdkDropList) => boolean = () => true
enterPredicate: (drag: DragRef, drop: DropListRef) => boolean = () => true;
/** Emits when the user drops an item inside the container. */
@Output('cdkDropListDropped')
dropped: EventEmitter<CdkDragDrop<T, any>> = new EventEmitter<CdkDragDrop<T, any>>();
/** Emits right before dragging has started. */
beforeStarted = new Subject<void>();
/**
* Emits when the user has moved a new drag item into this container.
*/
@Output('cdkDropListEntered')
entered: EventEmitter<CdkDragEnter<T>> = new EventEmitter<CdkDragEnter<T>>();
entered = new Subject<{item: DragRef, container: DropListRef}>();
/**
* Emits when the user removes an item from the container
* by dragging it into another container.
*/
@Output('cdkDropListExited')
exited: EventEmitter<CdkDragExit<T>> = new EventEmitter<CdkDragExit<T>>();
exited = new Subject<{item: DragRef, container: DropListRef}>();
/** Emits when the user drops an item inside the container. */
dropped = new Subject<{
item: DragRef,
currentIndex: number,
previousIndex: number,
container: DropListRef,
previousContainer: DropListRef,
isPointerOverContainer: boolean
}>();
/** Emits as the user is swapping items while actively dragging. */
@Output('cdkDropListSorted')
sorted: EventEmitter<CdkDragSortEvent<T>> = new EventEmitter<CdkDragSortEvent<T>>();
sorted = new Subject<{
previousIndex: number,
currentIndex: number,
container: DropListRef,
item: DragRef
}>();
constructor(
public element: ElementRef<HTMLElement>,
private _dragDropRegistry: DragDropRegistry<CdkDrag, CdkDropList<T>>,
private _changeDetectorRef: ChangeDetectorRef,
@Optional() private _dir?: Directionality,
@Optional() @SkipSelf() private _group?: CdkDropListGroup<CdkDropList>,
// @breaking-change 8.0.0 `_document` parameter to be made required.
@Optional() @Inject(DOCUMENT) _document?: any) {
/** Arbitrary data that can be attached to the drop list. */
data: T;
// @breaking-change 8.0.0 Remove null checks once `_document` parameter is required.
if (_document) {
this._document = _document;
} else if (typeof document !== 'undefined') {
this._document = document;
}
}
ngOnInit() {
this._dragDropRegistry.registerDropContainer(this);
if (this._group) {
this._group._items.add(this);
}
}
ngOnDestroy() {
this._dragDropRegistry.removeDropContainer(this);
if (this._group) {
this._group._items.delete(this);
}
}
/** Whether an item in the container is being dragged. */
_dragging = false;
/** Whether an item in the list is being dragged. */
private _isDragging = false;
/** Cache of the dimensions of all the items and the sibling containers. */
private _positionCache: PositionCache = {items: [], siblings: [], self: {} as ClientRect};
@@ -203,41 +139,55 @@ export class CdkDropList<T = any> implements OnInit, OnDestroy {
* from `_draggables`, as well as any items that have been dragged in, but haven't
* been dropped yet.
*/
private _activeDraggables: CdkDrag[];
private _activeDraggables: DragRef[];
/**
* Keeps track of the item that was last swapped with the dragged item, as
* well as what direction the pointer was moving in when the swap occured.
*/
private _previousSwap = {drag: null as CdkDrag | null, delta: 0};
private _previousSwap = {drag: null as DragRef | null, delta: 0};
/**
* Draggable items in the container.
* TODO(crisbeto): support arrays.
*/
private _draggables: DragRef[];
private _siblings: DropListRef[] = [];
/** Direction in which the list is oriented. */
private _orientation: 'horizontal' | 'vertical' = 'vertical';
constructor(
public element: ElementRef<HTMLElement>,
private _dragDropRegistry: DragDropRegistry<DragRef, DropListRef>,
_document: any,
private _dir?: Directionality) {
_dragDropRegistry.registerDropContainer(this);
this._document = _document;
}
/** Removes the drop list functionality from the DOM element. */
dispose() {
this.beforeStarted.complete();
this.entered.complete();
this.exited.complete();
this.dropped.complete();
this.sorted.complete();
this._dragDropRegistry.removeDropContainer(this);
}
/** Whether an item from this list is currently being dragged. */
isDragging() {
return this._isDragging;
}
/** Starts dragging an item. */
start(): void {
this._dragging = true;
this._activeDraggables = this._draggables.toArray();
this.beforeStarted.next();
this._isDragging = true;
this._activeDraggables = this._draggables.slice();
this._cachePositions();
this._changeDetectorRef.markForCheck();
}
/**
* Drops an item into this container.
* @param item Item being dropped into the container.
* @param currentIndex Index at which the item should be inserted.
* @param previousContainer Container from which the item got dragged in.
* @param isPointerOverContainer Whether the user's pointer was over the
* container when the item was dropped.
*/
drop(item: CdkDrag, currentIndex: number, previousContainer: CdkDropList,
isPointerOverContainer: boolean): void {
this._reset();
this.dropped.emit({
item,
currentIndex,
previousIndex: previousContainer.getItemIndex(item),
container: this,
previousContainer,
isPointerOverContainer
});
}
/**
@@ -246,8 +196,8 @@ export class CdkDropList<T = any> implements OnInit, OnDestroy {
* @param pointerX Position of the item along the X axis.
* @param pointerY Position of the item along the Y axis.
*/
enter(item: CdkDrag, pointerX: number, pointerY: number): void {
this.entered.emit({item, container: this});
enter(item: DragRef, pointerX: number, pointerY: number): void {
this.entered.next({item, container: this});
this.start();
// We use the coordinates of where the item entered the drop
@@ -286,24 +236,73 @@ export class CdkDropList<T = any> implements OnInit, OnDestroy {
* Removes an item from the container after it was dragged into another container by the user.
* @param item Item that was dragged out.
*/
exit(item: CdkDrag): void {
exit(item: DragRef): void {
this._reset();
this.exited.emit({item, container: this});
this.exited.next({item, container: this});
}
/**
* Drops an item into this container.
* @param item Item being dropped into the container.
* @param currentIndex Index at which the item should be inserted.
* @param previousContainer Container from which the item got dragged in.
* @param isPointerOverContainer Whether the user's pointer was over the
* container when the item was dropped.
*/
drop(item: DragRef, currentIndex: number, previousContainer: DropListRef,
isPointerOverContainer: boolean): void {
this._reset();
this.dropped.next({
item,
currentIndex,
previousIndex: previousContainer.getItemIndex(item),
container: this,
previousContainer,
isPointerOverContainer
});
}
/**
* Sets the draggable items that are a part of this list.
* @param items Items that are a part of this list.
*/
withItems(items: DragRef[]): this {
this._draggables = items.slice();
return this;
}
/**
* Sets the containers that are connected to this one. When two or more containers are
* connected, the user will be allowed to transfer items between them.
* @param connectedTo Other containers that the current containers should be connected to.
*/
connectedTo(connectedTo: DropListRef[]): this {
this._siblings = connectedTo.slice();
return this;
}
/**
* Sets the orientation of the container.
* @param orientation New orientation for the container.
*/
withOrientation(orientation: 'vertical' | 'horizontal'): this {
this._orientation = orientation;
return this;
}
/**
* Figures out the index of an item in the container.
* @param item Item whose index should be determined.
*/
getItemIndex(item: CdkDrag): number {
if (!this._dragging) {
return this._draggables.toArray().indexOf(item);
getItemIndex(item: DragRef): number {
if (!this._isDragging) {
return this._draggables.indexOf(item);
}
// Items are sorted always by top/left in the cache, however they flow differently in RTL.
// The rest of the logic still stands no matter what orientation we're in, however
// we need to invert the array when determining the index.
const items = this.orientation === 'horizontal' && this._dir && this._dir.value === 'rtl' ?
const items = this._orientation === 'horizontal' && this._dir && this._dir.value === 'rtl' ?
this._positionCache.items.slice().reverse() : this._positionCache.items;
return findIndex(items, currentItem => currentItem.drag === item);
@@ -314,9 +313,9 @@ export class CdkDropList<T = any> implements OnInit, OnDestroy {
* @param item Item to be sorted.
* @param pointerX Position of the item along the X axis.
* @param pointerY Position of the item along the Y axis.
* @param pointerDeta Direction in which the pointer is moving along each axis.
* @param pointerDelta Direction in which the pointer is moving along each axis.
*/
_sortItem(item: CdkDrag, pointerX: number, pointerY: number,
_sortItem(item: DragRef, pointerX: number, pointerY: number,
pointerDelta: {x: number, y: number}): void {
// Don't sort the item if it's out of range.
if (!this._isPointerNearDropContainer(pointerX, pointerY)) {
@@ -330,7 +329,7 @@ export class CdkDropList<T = any> implements OnInit, OnDestroy {
return;
}
const isHorizontal = this.orientation === 'horizontal';
const isHorizontal = this._orientation === 'horizontal';
const currentIndex = findIndex(siblings, currentItem => currentItem.drag === item);
const siblingAtNewPosition = siblings[newIndex];
const currentPosition = siblings[currentIndex].clientRect;
@@ -353,7 +352,7 @@ export class CdkDropList<T = any> implements OnInit, OnDestroy {
// Shuffle the array in place.
moveItemInArray(siblings, currentIndex, newIndex);
this.sorted.emit({
this.sorted.next({
previousIndex: currentIndex,
currentIndex: newIndex,
container: this,
@@ -382,71 +381,17 @@ export class CdkDropList<T = any> implements OnInit, OnDestroy {
// Round the transforms since some browsers will
// blur the elements, for sub-pixel transforms.
elementToOffset.style.transform = `translate3d(${Math.round(sibling.offset)}px, 0, 0)`;
this._adjustClientRect(sibling.clientRect, 0, offset);
adjustClientRect(sibling.clientRect, 0, offset);
} else {
elementToOffset.style.transform = `translate3d(0, ${Math.round(sibling.offset)}px, 0)`;
this._adjustClientRect(sibling.clientRect, offset, 0);
adjustClientRect(sibling.clientRect, offset, 0);
}
});
}
/**
* Figures out whether an item should be moved into a sibling
* drop container, based on its current position.
* @param item Drag item that is being moved.
* @param x Position of the item along the X axis.
* @param y Position of the item along the Y axis.
*/
_getSiblingContainerFromPosition(item: CdkDrag, x: number, y: number): CdkDropList | null {
const results = this._positionCache.siblings.filter(sibling => {
return isInsideClientRect(sibling.clientRect, x, y);
});
// No drop containers are intersecting with the pointer.
if (!results.length) {
return null;
}
let result: ListPositionCacheEntry | undefined = results[0];
// @breaking-change 8.0.0 remove null check once the
// `_document` is made into a required parameter.
if (this._document) {
const elementFromPoint = this._document.elementFromPoint(x, y);
// If there's no element at the pointer position, then
// the client rect is probably scrolled out of the view.
if (!elementFromPoint) {
return null;
}
// The `ClientRect`, that we're using to find the container over which the user is
// hovering, doesn't give us any information on whether the element has been scrolled
// out of the view or whether it's overlapping with other containers. This means that
// we could end up transferring the item into a container that's invisible or is positioned
// below another one. We use the result from `elementFromPoint` to get the top-most element
// at the pointer position and to find whether it's one of the intersecting drop containers.
result = results.find(sibling => {
const element = sibling.drop.element.nativeElement;
return element === elementFromPoint || element.contains(elementFromPoint);
});
}
return result && result.drop.enterPredicate(item, result.drop) ? result.drop : null;
}
/**
* Checks whether the user's pointer is positioned over the container.
* @param x Pointer position along the X axis.
* @param y Pointer position along the Y axis.
*/
_isOverContainer(x: number, y: number): boolean {
return isInsideClientRect(this._positionCache.self, x, y);
}
/** Refreshes the position cache of the items and sibling containers. */
private _cachePositions() {
const isHorizontal = this.orientation === 'horizontal';
const isHorizontal = this._orientation === 'horizontal';
this._positionCache.self = this.element.nativeElement.getBoundingClientRect();
this._positionCache.items = this._activeDraggables
@@ -480,7 +425,7 @@ export class CdkDropList<T = any> implements OnInit, OnDestroy {
a.clientRect.top - b.clientRect.top;
});
this._positionCache.siblings = this._getConnectedLists().map(drop => ({
this._positionCache.siblings = this._siblings.map(drop => ({
drop,
clientRect: drop.element.nativeElement.getBoundingClientRect()
}));
@@ -488,7 +433,7 @@ export class CdkDropList<T = any> implements OnInit, OnDestroy {
/** Resets the container to its initial state. */
private _reset() {
this._dragging = false;
this._isDragging = false;
// TODO(crisbeto): may have to wait for the animations to finish.
this._activeDraggables.forEach(item => item.getRootElement().style.transform = '');
@@ -500,17 +445,70 @@ export class CdkDropList<T = any> implements OnInit, OnDestroy {
}
/**
* Updates the top/left positions of a `ClientRect`, as well as their bottom/right counterparts.
* @param clientRect `ClientRect` that should be updated.
* @param top Amount to add to the `top` position.
* @param left Amount to add to the `left` position.
* Gets the offset in pixels by which the items that aren't being dragged should be moved.
* @param currentIndex Index of the item currently being dragged.
* @param siblings All of the items in the list.
* @param delta Direction in which the user is moving.
*/
private _adjustClientRect(clientRect: ClientRect, top: number, left: number) {
clientRect.top += top;
clientRect.bottom = clientRect.top + clientRect.height;
private _getSiblingOffsetPx(currentIndex: number,
siblings: ItemPositionCacheEntry[],
delta: 1 | -1) {
clientRect.left += left;
clientRect.right = clientRect.left + clientRect.width;
const isHorizontal = this._orientation === 'horizontal';
const currentPosition = siblings[currentIndex].clientRect;
const immediateSibling = siblings[currentIndex + delta * -1];
let siblingOffset = currentPosition[isHorizontal ? 'width' : 'height'] * delta;
if (immediateSibling) {
const start = isHorizontal ? 'left' : 'top';
const end = isHorizontal ? 'right' : 'bottom';
// Get the spacing between the start of the current item and the end of the one immediately
// after it in the direction in which the user is dragging, or vice versa. We add it to the
// offset in order to push the element to where it will be when it's inline and is influenced
// by the `margin` of its siblings.
if (delta === -1) {
siblingOffset -= immediateSibling.clientRect[start] - currentPosition[end];
} else {
siblingOffset += currentPosition[start] - immediateSibling.clientRect[end];
}
}
return siblingOffset;
}
/**
* Checks whether the pointer coordinates are close to the drop container.
* @param pointerX Coordinates along the X axis.
* @param pointerY Coordinates along the Y axis.
*/
private _isPointerNearDropContainer(pointerX: number, pointerY: number): boolean {
const {top, right, bottom, left, width, height} = this._positionCache.self;
const xThreshold = width * DROP_PROXIMITY_THRESHOLD;
const yThreshold = height * DROP_PROXIMITY_THRESHOLD;
return pointerY > top - yThreshold && pointerY < bottom + yThreshold &&
pointerX > left - xThreshold && pointerX < right + xThreshold;
}
/**
* Gets the offset in pixels by which the item that is being dragged should be moved.
* @param currentPosition Current position of the item.
* @param newPosition Position of the item where the current item should be moved.
* @param delta Direction in which the user is moving.
*/
private _getItemOffsetPx(currentPosition: ClientRect, newPosition: ClientRect, delta: 1 | -1) {
const isHorizontal = this._orientation === 'horizontal';
let itemOffset = isHorizontal ? newPosition.left - currentPosition.left :
newPosition.top - currentPosition.top;
// Account for differences in the item width/height.
if (delta === -1) {
itemOffset += isHorizontal ? newPosition.width - currentPosition.width :
newPosition.height - currentPosition.height;
}
return itemOffset;
}
/**
@@ -520,10 +518,10 @@ export class CdkDropList<T = any> implements OnInit, OnDestroy {
* @param pointerY Position of the user's pointer along the Y axis.
* @param delta Direction in which the user is moving their pointer.
*/
private _getItemIndexFromPointerPosition(item: CdkDrag, pointerX: number, pointerY: number,
private _getItemIndexFromPointerPosition(item: DragRef, pointerX: number, pointerY: number,
delta?: {x: number, y: number}) {
const isHorizontal = this.orientation === 'horizontal';
const isHorizontal = this._orientation === 'horizontal';
return findIndex(this._positionCache.items, ({drag, clientRect}, _, array) => {
if (drag === item) {
@@ -551,88 +549,68 @@ export class CdkDropList<T = any> implements OnInit, OnDestroy {
}
/**
* Checks whether the pointer coordinates are close to the drop container.
* @param pointerX Coordinates along the X axis.
* @param pointerY Coordinates along the Y axis.
* Checks whether the user's pointer is positioned over the container.
* @param x Pointer position along the X axis.
* @param y Pointer position along the Y axis.
*/
private _isPointerNearDropContainer(pointerX: number, pointerY: number): boolean {
const {top, right, bottom, left, width, height} = this._positionCache.self;
const xThreshold = width * DROP_PROXIMITY_THRESHOLD;
const yThreshold = height * DROP_PROXIMITY_THRESHOLD;
return pointerY > top - yThreshold && pointerY < bottom + yThreshold &&
pointerX > left - xThreshold && pointerX < right + xThreshold;
_isOverContainer(x: number, y: number): boolean {
return isInsideClientRect(this._positionCache.self, x, y);
}
/**
* Gets the offset in pixels by which the item that is being dragged should be moved.
* @param currentPosition Current position of the item.
* @param newPosition Position of the item where the current item should be moved.
* @param delta Direction in which the user is moving.
* Figures out whether an item should be moved into a sibling
* drop container, based on its current position.
* @param item Drag item that is being moved.
* @param x Position of the item along the X axis.
* @param y Position of the item along the Y axis.
*/
private _getItemOffsetPx(currentPosition: ClientRect, newPosition: ClientRect, delta: 1 | -1) {
const isHorizontal = this.orientation === 'horizontal';
let itemOffset = isHorizontal ? newPosition.left - currentPosition.left :
newPosition.top - currentPosition.top;
// Account for differences in the item width/height.
if (delta === -1) {
itemOffset += isHorizontal ? newPosition.width - currentPosition.width :
newPosition.height - currentPosition.height;
}
return itemOffset;
}
/**
* Gets the offset in pixels by which the items that aren't being dragged should be moved.
* @param currentIndex Index of the item currently being dragged.
* @param siblings All of the items in the list.
* @param delta Direction in which the user is moving.
*/
private _getSiblingOffsetPx(currentIndex: number,
siblings: ItemPositionCacheEntry[],
delta: 1 | -1) {
const isHorizontal = this.orientation === 'horizontal';
const currentPosition = siblings[currentIndex].clientRect;
const immediateSibling = siblings[currentIndex + delta * -1];
let siblingOffset = currentPosition[isHorizontal ? 'width' : 'height'] * delta;
if (immediateSibling) {
const start = isHorizontal ? 'left' : 'top';
const end = isHorizontal ? 'right' : 'bottom';
// Get the spacing between the start of the current item and the end of the one immediately
// after it in the direction in which the user is dragging, or vice versa. We add it to the
// offset in order to push the element to where it will be when it's inline and is influenced
// by the `margin` of its siblings.
if (delta === -1) {
siblingOffset -= immediateSibling.clientRect[start] - currentPosition[end];
} else {
siblingOffset += currentPosition[start] - immediateSibling.clientRect[end];
}
}
return siblingOffset;
}
/** Gets an array of unique drop lists that the current list is connected to. */
private _getConnectedLists(): CdkDropList[] {
const siblings = coerceArray(this.connectedTo).map(drop => {
return typeof drop === 'string' ? this._dragDropRegistry.getDropContainer(drop)! : drop;
_getSiblingContainerFromPosition(item: DragRef, x: number, y: number): DropListRef | null {
const results = this._positionCache.siblings.filter(sibling => {
return isInsideClientRect(sibling.clientRect, x, y);
});
if (this._group) {
this._group._items.forEach(drop => {
if (siblings.indexOf(drop) === -1) {
siblings.push(drop);
}
});
// No drop containers are intersecting with the pointer.
if (!results.length) {
return null;
}
return siblings.filter(drop => drop && drop !== this);
const elementFromPoint = this._document.elementFromPoint(x, y);
// If there's no element at the pointer position, then
// the client rect is probably scrolled out of the view.
if (!elementFromPoint) {
return null;
}
// The `ClientRect`, that we're using to find the container over which the user is
// hovering, doesn't give us any information on whether the element has been scrolled
// out of the view or whether it's overlapping with other containers. This means that
// we could end up transferring the item into a container that's invisible or is positioned
// below another one. We use the result from `elementFromPoint` to get the top-most element
// at the pointer position and to find whether it's one of the intersecting drop containers.
const result = results.find(sibling => {
const element = sibling.drop.element.nativeElement;
return element === elementFromPoint || element.contains(elementFromPoint);
});
return result && result.drop.enterPredicate(item, result.drop) ? result.drop : null;
}
}
/**
* Updates the top/left positions of a `ClientRect`, as well as their bottom/right counterparts.
* @param clientRect `ClientRect` that should be updated.
* @param top Amount to add to the `top` position.
* @param left Amount to add to the `left` position.
*/
function adjustClientRect(clientRect: ClientRect, top: number, left: number) {
clientRect.top += top;
clientRect.bottom = clientRect.top + clientRect.height;
clientRect.left += left;
clientRect.right = clientRect.left + clientRect.width;
}
+19 -6
View File
@@ -6,14 +6,27 @@
* found in the LICENSE file at https://angular.io/license
*/
export * from './drop-list';
export * from './drop-list-group';
// TODO(crisbeto): export once API is finalized
// export * from './drag-ref';
// export * from './drop-list-ref';
export * from './drop-list-container';
export * from './drag';
export * from './drag-handle';
export * from './drag-events';
export * from './drag-utils';
export * from './drag-preview';
export * from './drag-placeholder';
export * from './drag-drop-module';
export * from './drag-drop-registry';
export * from './directives/drop-list';
export * from './directives/drop-list-group';
export * from './directives/drag';
export * from './directives/drag-handle';
export * from './directives/drag-preview';
export * from './directives/drag-placeholder';
import {DragRefConfig} from './drag-ref';
/**
* @deprecated Use `DragRefConfig` instead.
* @breaking-change 8.0.0
*/
export interface CdkDragConfig extends DragRefConfig {}
@@ -2,9 +2,6 @@
<div class="demo-list">
<h2>To do</h2>
<mat-slide-toggle color="primary" [(ngModel)]="cloneMode">
Clone Mode
</mat-slide-toggle>
<div
cdkDropList
(cdkDropListDropped)="drop($event)"
+6 -21
View File
@@ -9,12 +9,7 @@
import {Component, ViewEncapsulation} from '@angular/core';
import {MatIconRegistry} from '@angular/material/icon';
import {DomSanitizer} from '@angular/platform-browser';
import {
CdkDragDrop,
moveItemInArray,
transferArrayItem,
copyArrayItem
} from '@angular/cdk/drag-drop';
import {CdkDragDrop, moveItemInArray, transferArrayItem} from '@angular/cdk/drag-drop';
@Component({
moduleId: module.id,
@@ -24,8 +19,6 @@ import {
encapsulation: ViewEncapsulation.None,
})
export class DragAndDropDemo {
cloneMode = false;
axisLock: 'x' | 'y';
todo = [
'Go out for Lunch',
@@ -63,19 +56,11 @@ export class DragAndDropDemo {
if (event.previousContainer === event.container) {
moveItemInArray(event.container.data, event.previousIndex, event.currentIndex);
} else {
if (this.cloneMode) {
copyArrayItem(
event.previousContainer.data,
event.container.data,
event.previousIndex,
event.currentIndex);
} else {
transferArrayItem(
event.previousContainer.data,
event.container.data,
event.previousIndex,
event.currentIndex);
}
transferArrayItem(
event.previousContainer.data,
event.container.data,
event.previousIndex,
event.currentIndex);
}
}
}
+22 -22
View File
@@ -1,19 +1,20 @@
export declare const CDK_DRAG_CONFIG: InjectionToken<CdkDragConfig>;
export declare const CDK_DRAG_CONFIG: InjectionToken<DragRefConfig>;
export declare function CDK_DRAG_CONFIG_FACTORY(): CdkDragConfig;
export declare function CDK_DRAG_CONFIG_FACTORY(): DragRefConfig;
export declare const CDK_DROP_LIST: InjectionToken<CdkDropListContainer<any>>;
export declare const CDK_DROP_LIST_CONTAINER: InjectionToken<CdkDropListContainer<any>>;
export declare class CdkDrag<T = any> implements AfterViewInit, OnDestroy {
_dragRef: DragRef<CdkDrag<T>>;
_handles: QueryList<CdkDragHandle>;
_hasStartedDragging: boolean;
_placeholderTemplate: CdkDragPlaceholder;
_pointerDown: (event: TouchEvent | MouseEvent) => void;
_previewTemplate: CdkDragPreview;
boundaryElementSelector: string;
data: T;
disabled: boolean;
dropContainer: CdkDropListContainer;
dropContainer: CdkDropList;
dropped: EventEmitter<CdkDragDrop<any>>;
element: ElementRef<HTMLElement>;
ended: EventEmitter<CdkDragEnd>;
@@ -25,8 +26,7 @@ export declare class CdkDrag<T = any> implements AfterViewInit, OnDestroy {
started: EventEmitter<CdkDragStart>;
constructor(
element: ElementRef<HTMLElement>,
dropContainer: CdkDropListContainer, document: any, _ngZone: NgZone, _viewContainerRef: ViewContainerRef, _viewportRuler: ViewportRuler, _dragDropRegistry: DragDropRegistry<CdkDrag<T>, CdkDropListContainer>, _config: CdkDragConfig, _dir: Directionality);
_isDragging(): boolean;
dropContainer: CdkDropList, _document: any, _ngZone: NgZone, _viewContainerRef: ViewContainerRef, _viewportRuler: ViewportRuler, _dragDropRegistry: DragDropRegistry<DragRef, DropListRef>, _config: DragRefConfig, _dir: Directionality);
getPlaceholderElement(): HTMLElement;
getRootElement(): HTMLElement;
ngAfterViewInit(): void;
@@ -34,17 +34,15 @@ export declare class CdkDrag<T = any> implements AfterViewInit, OnDestroy {
reset(): void;
}
export interface CdkDragConfig {
dragStartThreshold: number;
pointerDirectionChangeThreshold: number;
export interface CdkDragConfig extends DragRefConfig {
}
export interface CdkDragDrop<T, O = T> {
container: CdkDropListContainer<T>;
container: CdkDropList<T>;
currentIndex: number;
isPointerOverContainer: boolean;
item: CdkDrag;
previousContainer: CdkDropListContainer<O>;
previousContainer: CdkDropList<O>;
previousIndex: number;
}
@@ -53,12 +51,12 @@ export interface CdkDragEnd<T = any> {
}
export interface CdkDragEnter<T = any, I = T> {
container: CdkDropListContainer<T>;
container: CdkDropList<T>;
item: CdkDrag<I>;
}
export interface CdkDragExit<T = any, I = T> {
container: CdkDropListContainer<T>;
container: CdkDropList<T>;
item: CdkDrag<I>;
}
@@ -95,7 +93,7 @@ export declare class CdkDragPreview<T = any> {
}
export interface CdkDragSortEvent<T = any, I = T> {
container: CdkDropListContainer<T>;
container: CdkDropList<T>;
currentIndex: number;
item: CdkDrag<I>;
previousIndex: number;
@@ -105,9 +103,9 @@ export interface CdkDragStart<T = any> {
source: CdkDrag<T>;
}
export declare class CdkDropList<T = any> implements OnInit, OnDestroy {
export declare class CdkDropList<T = any> implements CdkDropListContainer, OnDestroy {
_draggables: QueryList<CdkDrag>;
_dragging: boolean;
_dropListRef: DropListRef<CdkDropList<T>>;
connectedTo: (CdkDropList | string)[] | CdkDropList | string;
data: T;
disabled: boolean;
@@ -120,19 +118,18 @@ export declare class CdkDropList<T = any> implements OnInit, OnDestroy {
lockAxis: 'x' | 'y';
orientation: 'horizontal' | 'vertical';
sorted: EventEmitter<CdkDragSortEvent<T>>;
constructor(element: ElementRef<HTMLElement>, _dragDropRegistry: DragDropRegistry<CdkDrag, CdkDropList<T>>, _changeDetectorRef: ChangeDetectorRef, _dir?: Directionality | undefined, _group?: CdkDropListGroup<CdkDropList<any>> | undefined, _document?: any);
_getSiblingContainerFromPosition(item: CdkDrag, x: number, y: number): CdkDropList | null;
constructor(element: ElementRef<HTMLElement>, dragDropRegistry: DragDropRegistry<DragRef, DropListRef>, _changeDetectorRef: ChangeDetectorRef, dir?: Directionality, _group?: CdkDropListGroup<CdkDropList<any>> | undefined, _document?: any);
_getSiblingContainerFromPosition(item: CdkDrag, x: number, y: number): CdkDropListContainer | null;
_isOverContainer(x: number, y: number): boolean;
_sortItem(item: CdkDrag, pointerX: number, pointerY: number, pointerDelta: {
x: number;
y: number;
}): void;
drop(item: CdkDrag, currentIndex: number, previousContainer: CdkDropList, isPointerOverContainer: boolean): void;
drop(item: CdkDrag, currentIndex: number, previousContainer: Partial<CdkDropListContainer>, isPointerOverContainer: boolean): void;
enter(item: CdkDrag, pointerX: number, pointerY: number): void;
exit(item: CdkDrag): void;
getItemIndex(item: CdkDrag): number;
ngOnDestroy(): void;
ngOnInit(): void;
start(): void;
}
@@ -150,7 +147,7 @@ export interface CdkDropListContainer<T = any> {
x: number;
y: number;
}): void;
drop(item: CdkDrag, currentIndex: number, previousContainer: CdkDropListContainer, isPointerOverContainer: boolean): void;
drop(item: CdkDrag, currentIndex: number, previousContainer: Partial<CdkDropListContainer>, isPointerOverContainer: boolean): void;
enter(item: CdkDrag, pointerX: number, pointerY: number): void;
exit(item: CdkDrag): void;
getItemIndex(item: CdkDrag): number;
@@ -162,6 +159,9 @@ export declare class CdkDropListGroup<T> implements OnDestroy {
ngOnDestroy(): void;
}
export interface CdkDropListInternal extends CdkDropList {
}
export declare function copyArrayItem<T = any>(currentArray: T[], targetArray: T[], currentIndex: number, targetIndex: number): void;
export declare class DragDropModule {