feat: add experimental drag-drop package (#11864)

This commit is contained in:
Kristiyan Kostadinov
2018-06-28 17:46:44 +02:00
committed by Joey Perrott
parent 0ca08b78aa
commit 27811a3329
31 changed files with 1836 additions and 1 deletions
+2
View File
@@ -87,6 +87,7 @@
/src/cdk-experimental/** @jelbourn
/src/cdk-experimental/dialog/** @jelbourn @josephperrott @crisbeto
/src/cdk-experimental/scrolling/** @mmalerba
/src/cdk-experimental/drag-drop/** @crisbeto
# Docs examples & guides
/guides/** @amcdnl @jelbourn
@@ -110,6 +111,7 @@
/src/demo-app/demo-app/** @jelbourn
/src/demo-app/dialog/** @jelbourn @crisbeto
/src/demo-app/drawer/** @mmalerba
/src/demo-app/drag-drop/** @crisbeto
/src/demo-app/example/** @andrewseguin
/src/demo-app/examples-page/** @andrewseguin
/src/demo-app/expansion/** @josephperrott
+1
View File
@@ -24,6 +24,7 @@ CDK_TARGETS = ["//src/cdk"] + ["//src/cdk/%s" % p for p in CDK_PACKAGES]
CDK_EXPERIMENTAL_PACKAGES = [
"dialog",
"scrolling",
"drag-drop",
]
CDK_EXPERIMENTAL_TARGETS = ["//src/cdk-experimental"] + [
@@ -0,0 +1,51 @@
package(default_visibility=["//visibility:public"])
load("@angular//:index.bzl", "ng_module")
load("@build_bazel_rules_typescript//:defs.bzl", "ts_library", "ts_web_test")
load("@io_bazel_rules_sass//sass:sass.bzl", "sass_binary")
ng_module(
name = "drag-drop",
srcs = glob(["**/*.ts"], exclude=["**/*.spec.ts"]),
module_name = "@angular/cdk-experimental/drag-drop",
assets = [":drop.css"],
deps = [
"@rxjs",
"//src/cdk/platform",
],
tsconfig = "//src/cdk-experimental:tsconfig-build.json",
)
ts_library(
name = "drag_and_drop_test_sources",
testonly = 1,
srcs = glob(["**/*.spec.ts"]),
deps = [
":drag-drop",
"//src/cdk/testing",
],
tsconfig = "//src/cdk-experimental:tsconfig-build.json",
)
sass_binary(
name = "drop_scss",
src = "drop.scss",
)
ts_web_test(
name = "unit_tests",
bootstrap = [
"//:web_test_bootstrap_scripts",
],
tags = ["manual"],
# Do not sort
deps = [
"//:tslib_bundle",
"//:angular_bundles",
"//:angular_test_bundles",
"//test:angular_test_init",
":drag_and_drop_test_sources",
],
)
@@ -0,0 +1,32 @@
/**
* @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 {NgModule} from '@angular/core';
import {CdkDrop} from './drop';
import {CdkDrag} from './drag';
import {CdkDragHandle} from './drag-handle';
import {CdkDragPreview} from './drag-preview';
import {CdkDragPlaceholder} from './drag-placeholder';
@NgModule({
declarations: [
CdkDrop,
CdkDrag,
CdkDragHandle,
CdkDragPreview,
CdkDragPlaceholder,
],
exports: [
CdkDrop,
CdkDrag,
CdkDragHandle,
CdkDragPreview,
CdkDragPlaceholder,
],
})
export class DragDropModule {}
@@ -0,0 +1 @@
# TODO
@@ -0,0 +1,57 @@
/**
* @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 {CdkDrag} from './drag';
import {CdkDropContainer} from './drop-container';
/** Event emitted when the user starts dragging a draggable. */
export interface CdkDragStart {
/** Draggable that emitted the event. */
source: CdkDrag;
}
/** Event emitted when the user stops dragging a draggable. */
export interface CdkDragEnd {
/** Draggable that emitted the event. */
source: CdkDrag;
}
/** Event emitted when the user moves an item into a new drop container. */
export interface CdkDragEnter<T> {
/** Container into which the user has moved the item. */
container: CdkDropContainer<T>;
/** Item that was removed from the container. */
item: CdkDrag;
}
/**
* Event emitted when the user removes an item from a
* drop container by moving it into another one.
*/
export interface CdkDragExit<T> {
/** Container from which the user has a removed an item. */
container: CdkDropContainer<T>;
/** Item that was removed from the container. */
item: CdkDrag;
}
/** Event emitted when the user drops a draggable item inside a drop container. */
export interface CdkDragDrop<T, O = T> {
/** Index of the item when it was picked up. */
previousIndex: number;
/** Current index of the item. */
currentIndex: number;
/** Item that is being dropped. */
item: CdkDrag;
/** Container in which the item was dropped. */
container: CdkDropContainer<T>;
/** Container from which the item was picked up. Can be the same as the `container`. */
previousContainer: CdkDropContainer<O>;
}
@@ -0,0 +1,20 @@
/**
* @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 {Directive, ElementRef} from '@angular/core';
/** Handle that can be used to drag and CdkDrag instance. */
@Directive({
selector: '[cdkDragHandle]',
host: {
'class': 'cdk-drag-handle'
}
})
export class CdkDragHandle {
constructor(public element: ElementRef<HTMLElement>) {}
}
@@ -0,0 +1,22 @@
/**
* @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 {Directive, TemplateRef, Input} from '@angular/core';
/**
* Element that will be used as a template for the placeholder of a CdkDrag when
* it is being dragged. The placeholder is displayed in place of the element being dragged.
*/
@Directive({
selector: 'ng-template[cdkDragPlaceholder]'
})
export class CdkDragPlaceholder<T = any> {
/** Context data to be added to the placeholder template instance. */
@Input() data: T;
constructor(public templateRef: TemplateRef<T>) {}
}
@@ -0,0 +1,22 @@
/**
* @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 {Directive, TemplateRef, Input} from '@angular/core';
/**
* Element that will be used as a template for the preview
* of a CdkDrag when it is being dragged.
*/
@Directive({
selector: 'ng-template[cdkDragPreview]'
})
export class CdkDragPreview<T = any> {
/** Context data to be added to the preview template instance. */
@Input() data: T;
constructor(public templateRef: TemplateRef<T>) {}
}
@@ -0,0 +1,43 @@
/**
* @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
*/
/**
* Moves an item one index in an array to another.
* @param array Array in which to move the item.
* @param fromIndex Starting index of the item.
* @param toIndex Index to which the item should be moved.
*/
export function moveItemInArray<T = any>(array: T[], fromIndex: number, toIndex: number): void {
if (fromIndex === toIndex) {
return;
}
const target = array[fromIndex];
const delta = toIndex < fromIndex ? -1 : 1;
for (let i = fromIndex; i !== toIndex; i += delta) {
array[i] = array[i + delta];
}
array[toIndex] = target;
}
/**
* Moves an item from one array to another.
* @param currentArray Array from which to transfer the item.
* @param targetArray Array into which to put the item.
* @param currentIndex Index of the item in its current array.
* @param targetIndex Index at which to insert the item.
*/
export function transferArrayItem<T = any>(currentArray: T[],
targetArray: T[],
currentIndex: number,
targetIndex: number): void {
targetArray.splice(targetIndex, 0, currentArray.splice(currentIndex, 1)[0]);
}
+582
View File
@@ -0,0 +1,582 @@
import {
Component,
Type,
ViewChild,
ElementRef,
ViewChildren,
QueryList,
AfterViewInit,
} from '@angular/core';
import {TestBed, ComponentFixture, fakeAsync, flush} from '@angular/core/testing';
import {DragDropModule} from './drag-drop-module';
import {dispatchMouseEvent, dispatchTouchEvent} from '@angular/cdk/testing';
import {CdkDrag} from './drag';
import {CdkDragDrop} from './drag-events';
import {moveItemInArray, transferArrayItem} from './drag-utils';
import {CdkDrop} from './drop';
const ITEM_HEIGHT = 25;
describe('CdkDrag', () => {
function createComponent<T>(componentType: Type<T>): ComponentFixture<T> {
TestBed.configureTestingModule({
imports: [DragDropModule],
declarations: [componentType],
}).compileComponents();
return TestBed.createComponent<T>(componentType);
}
describe('standalone draggable', () => {
describe('mouse dragging', () => {
it('should drag an element freely to a particular position', fakeAsync(() => {
const fixture = createComponent(StandaloneDraggable);
fixture.detectChanges();
const dragElement = fixture.componentInstance.dragElement.nativeElement;
expect(dragElement.style.transform).toBeFalsy();
dragElementViaMouse(fixture, dragElement, 50, 100);
expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)');
}));
it('should continue dragging the element from where it was left off', fakeAsync(() => {
const fixture = createComponent(StandaloneDraggable);
fixture.detectChanges();
const dragElement = fixture.componentInstance.dragElement.nativeElement;
expect(dragElement.style.transform).toBeFalsy();
dragElementViaMouse(fixture, dragElement, 50, 100);
expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)');
dragElementViaMouse(fixture, dragElement, 100, 200);
expect(dragElement.style.transform).toBe('translate3d(150px, 300px, 0px)');
}));
});
describe('touch dragging', () => {
it('should drag an element freely to a particular position', fakeAsync(() => {
const fixture = createComponent(StandaloneDraggable);
fixture.detectChanges();
const dragElement = fixture.componentInstance.dragElement.nativeElement;
expect(dragElement.style.transform).toBeFalsy();
dragElementViaTouch(fixture, dragElement, 50, 100);
expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)');
}));
it('should continue dragging the element from where it was left off', fakeAsync(() => {
const fixture = createComponent(StandaloneDraggable);
fixture.detectChanges();
const dragElement = fixture.componentInstance.dragElement.nativeElement;
expect(dragElement.style.transform).toBeFalsy();
dragElementViaTouch(fixture, dragElement, 50, 100);
expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)');
dragElementViaTouch(fixture, dragElement, 100, 200);
expect(dragElement.style.transform).toBe('translate3d(150px, 300px, 0px)');
}));
it('should prevent the default `touchmove` action on the page while dragging',
fakeAsync(() => {
const fixture = createComponent(StandaloneDraggable);
fixture.detectChanges();
dispatchTouchEvent(fixture.componentInstance.dragElement.nativeElement, 'touchstart');
fixture.detectChanges();
expect(dispatchTouchEvent(document, 'touchmove').defaultPrevented).toBe(true);
dispatchTouchEvent(document, 'touchend');
fixture.detectChanges();
}));
});
it('should dispatch an event when the user has started dragging', fakeAsync(() => {
const fixture = createComponent(StandaloneDraggable);
fixture.detectChanges();
dispatchMouseEvent(fixture.componentInstance.dragElement.nativeElement, 'mousedown');
fixture.detectChanges();
expect(fixture.componentInstance.startedSpy).toHaveBeenCalledWith(jasmine.objectContaining({
source: fixture.componentInstance.dragInstance
}));
}));
it('should dispatch an event when the user has stopped dragging', fakeAsync(() => {
const fixture = createComponent(StandaloneDraggable);
fixture.detectChanges();
dragElementViaMouse(fixture, fixture.componentInstance.dragElement.nativeElement, 5, 10);
expect(fixture.componentInstance.endedSpy).toHaveBeenCalledWith(jasmine.objectContaining({
source: fixture.componentInstance.dragInstance
}));
}));
});
describe('draggable with a handle', () => {
it('should not be able to drag the entire element if it has a handle', fakeAsync(() => {
const fixture = createComponent(StandaloneDraggableWithHandle);
fixture.detectChanges();
const dragElement = fixture.componentInstance.dragElement.nativeElement;
expect(dragElement.style.transform).toBeFalsy();
dragElementViaMouse(fixture, dragElement, 50, 100);
expect(dragElement.style.transform).toBeFalsy();
}));
it('should be able to drag an element using its handle', fakeAsync(() => {
const fixture = createComponent(StandaloneDraggableWithHandle);
fixture.detectChanges();
const dragElement = fixture.componentInstance.dragElement.nativeElement;
const handle = fixture.componentInstance.handleElement.nativeElement;
expect(dragElement.style.transform).toBeFalsy();
dragElementViaMouse(fixture, handle, 50, 100);
expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)');
}));
});
describe('in a drop container', () => {
it('should be able to attach data to the drop container', () => {
const fixture = createComponent(DraggableInDropZone);
fixture.detectChanges();
expect(fixture.componentInstance.dropInstance.data).toBe(fixture.componentInstance.items);
});
it('should toggle a class when the user starts dragging an item', fakeAsync(() => {
const fixture = createComponent(DraggableInDropZone);
fixture.detectChanges();
const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement;
const dropZone = fixture.componentInstance.dropInstance;
expect(dropZone.element.nativeElement.classList).not.toContain('cdk-drop-dragging');
dispatchMouseEvent(item, 'mousedown');
fixture.detectChanges();
expect(dropZone.element.nativeElement.classList).toContain('cdk-drop-dragging');
dispatchMouseEvent(document, 'mouseup');
fixture.detectChanges();
flush();
fixture.detectChanges();
expect(dropZone.element.nativeElement.classList).not.toContain('cdk-drop-dragging');
}));
it('should dispatch the `dropped` event when an item has been dropped', fakeAsync(() => {
const fixture = createComponent(DraggableInDropZone);
fixture.detectChanges();
const dragItems = fixture.componentInstance.dragItems;
expect(dragItems.map(drag => drag.element.nativeElement.textContent!.trim()))
.toEqual(['Zero', 'One', 'Two', 'Three']);
const firstItem = dragItems.first;
const thirdItemRect = dragItems.toArray()[2].element.nativeElement.getBoundingClientRect();
dragElementViaMouse(fixture, firstItem.element.nativeElement,
thirdItemRect.left + 1, thirdItemRect.top + 1);
flush();
fixture.detectChanges();
expect(fixture.componentInstance.droppedSpy).toHaveBeenCalledTimes(1);
expect(fixture.componentInstance.droppedSpy).toHaveBeenCalledWith(jasmine.objectContaining({
previousIndex: 0,
currentIndex: 2,
item: firstItem,
container: fixture.componentInstance.dropInstance,
previousContainer: fixture.componentInstance.dropInstance
}));
expect(dragItems.map(drag => drag.element.nativeElement.textContent!.trim()))
.toEqual(['One', 'Two', 'Zero', 'Three']);
}));
it('should create a preview element while the item is dragged', fakeAsync(() => {
const fixture = createComponent(DraggableInDropZone);
fixture.detectChanges();
const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement;
const itemRect = item.getBoundingClientRect();
const initialParent = item.parentNode;
dispatchMouseEvent(item, 'mousedown');
fixture.detectChanges();
const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement;
const previewRect = preview.getBoundingClientRect();
expect(item.parentNode).toBe(document.body, 'Expected element to be moved out into the body');
expect(item.style.display).toBe('none', 'Expected element to be hidden');
expect(preview).toBeTruthy('Expected preview to be in the DOM');
expect(preview.textContent!.trim())
.toContain('One', 'Expected preview content to match element');
expect(previewRect.width).toBe(itemRect.width, 'Expected preview width to match element');
expect(previewRect.height).toBe(itemRect.height, 'Expected preview height to match element');
dispatchMouseEvent(document, 'mouseup');
fixture.detectChanges();
flush();
expect(item.parentNode)
.toBe(initialParent, 'Expected element to be moved back into its old parent');
expect(item.style.display).toBeFalsy('Expected element to be visible');
expect(preview.parentNode).toBeFalsy('Expected preview to be removed from the DOM');
}));
it('should create a placeholder element while the item is dragged', fakeAsync(() => {
const fixture = createComponent(DraggableInDropZone);
fixture.detectChanges();
const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement;
const initialParent = item.parentNode;
dispatchMouseEvent(item, 'mousedown');
fixture.detectChanges();
const placeholder = document.querySelector('.cdk-drag-placeholder')! as HTMLElement;
expect(placeholder).toBeTruthy('Expected placeholder to be in the DOM');
expect(placeholder.parentNode)
.toBe(initialParent, 'Expected placeholder to be inserted into the same parent');
expect(placeholder.textContent!.trim())
.toContain('One', 'Expected placeholder content to match element');
dispatchMouseEvent(document, 'mouseup');
fixture.detectChanges();
flush();
expect(placeholder.parentNode).toBeFalsy('Expected placeholder to be removed from the DOM');
}));
it('should move the placeholder as an item is being sorted down', fakeAsync(() => {
const fixture = createComponent(DraggableInDropZone);
fixture.detectChanges();
const items = fixture.componentInstance.dragItems.toArray();
const draggedItem = items[0].element.nativeElement;
const {top, left} = draggedItem.getBoundingClientRect();
dispatchMouseEvent(draggedItem, 'mousedown', left, top);
fixture.detectChanges();
const placeholder = document.querySelector('.cdk-drag-placeholder')! as HTMLElement;
// Drag over each item one-by-one going downwards.
for (let i = 0; i < items.length; i++) {
const elementRect = items[i].element.nativeElement.getBoundingClientRect();
// Add a few pixels to the top offset so we get some overlap.
dispatchMouseEvent(document, 'mousemove', elementRect.left, elementRect.top + 5);
fixture.detectChanges();
expect(getElementIndex(placeholder)).toBe(i);
}
dispatchMouseEvent(document, 'mouseup');
fixture.detectChanges();
flush();
}));
it('should move the placeholder as an item is being sorted up', fakeAsync(() => {
const fixture = createComponent(DraggableInDropZone);
fixture.detectChanges();
const items = fixture.componentInstance.dragItems.toArray();
const draggedItem = items[items.length - 1].element.nativeElement;
const {top, left} = draggedItem.getBoundingClientRect();
dispatchMouseEvent(draggedItem, 'mousedown', left, top);
fixture.detectChanges();
const placeholder = document.querySelector('.cdk-drag-placeholder')! as HTMLElement;
// Drag over each item one-by-one going upwards.
for (let i = items.length - 1; i > -1; i--) {
const elementRect = items[i].element.nativeElement.getBoundingClientRect();
// Remove a few pixels from the bottom offset so we get some overlap.
dispatchMouseEvent(document, 'mousemove', elementRect.left, elementRect.bottom - 5);
fixture.detectChanges();
expect(getElementIndex(placeholder)).toBe(Math.min(i + 1, items.length - 1));
}
dispatchMouseEvent(document, 'mouseup');
fixture.detectChanges();
flush();
}));
it('should clean up the preview element if the item is destroyed mid-drag', fakeAsync(() => {
const fixture = createComponent(DraggableInDropZone);
fixture.detectChanges();
const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement;
dispatchMouseEvent(item, 'mousedown');
fixture.detectChanges();
const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement;
expect(preview.parentNode).toBeTruthy('Expected preview to be in the DOM');
expect(item.parentNode).toBeTruthy('Expected drag item to be in the DOM');
fixture.destroy();
expect(preview.parentNode).toBeFalsy('Expected preview to be removed from the DOM');
expect(item.parentNode).toBeFalsy('Expected drag item to be removed from the DOM');
}));
it('should be able to customize the preview element', fakeAsync(() => {
const fixture = createComponent(DraggableInDropZoneWithCustomPreview);
fixture.detectChanges();
const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement;
dispatchMouseEvent(item, 'mousedown');
fixture.detectChanges();
const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement;
expect(preview).toBeTruthy();
expect(preview.classList).toContain('custom-preview');
expect(preview.textContent!.trim()).toContain('Custom preview');
}));
it('should position custom previews next to the pointer', fakeAsync(() => {
const fixture = createComponent(DraggableInDropZoneWithCustomPreview);
fixture.detectChanges();
const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement;
dispatchMouseEvent(item, 'mousedown', 50, 50);
fixture.detectChanges();
const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement;
expect(preview.style.transform).toBe('translate3d(50px, 50px, 0px)');
}));
it('should be able to customize the placeholder', fakeAsync(() => {
const fixture = createComponent(DraggableInDropZoneWithCustomPlaceholder);
fixture.detectChanges();
const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement;
dispatchMouseEvent(item, 'mousedown');
fixture.detectChanges();
const placeholder = document.querySelector('.cdk-drag-placeholder')! as HTMLElement;
expect(placeholder).toBeTruthy();
expect(placeholder.classList).toContain('custom-placeholder');
expect(placeholder.textContent!.trim()).toContain('Custom placeholder');
}));
});
describe('in a connected drop container', () => {
it('should dispatch the `dropped` event when an item has been dropped into a new container',
fakeAsync(() => {
const fixture = createComponent(ConnectedDropZones);
fixture.detectChanges();
// TODO
// console.log(fixture.componentInstance.groupedDragItems.map(d => d.length));
}));
});
});
@Component({
template: `
<div
cdkDrag
(cdkDragStarted)="startedSpy($event)"
(cdkDragEnded)="endedSpy($event)"
#dragElement
style="width: 100px; height: 100px; background: red;"></div>
`
})
export class StandaloneDraggable {
@ViewChild('dragElement') dragElement: ElementRef<HTMLElement>;
@ViewChild(CdkDrag) dragInstance: CdkDrag;
startedSpy = jasmine.createSpy('started spy');
endedSpy = jasmine.createSpy('ended spy');
}
@Component({
template: `
<div #dragElement cdkDrag
style="width: 100px; height: 100px; background: red; position: relative">
<div #handleElement cdkDragHandle style="width: 10px; height: 10px; background: green;"></div>
</div>
`
})
export class StandaloneDraggableWithHandle {
@ViewChild('dragElement') dragElement: ElementRef<HTMLElement>;
@ViewChild('handleElement') handleElement: ElementRef<HTMLElement>;
@ViewChild(CdkDrag) dragInstance: CdkDrag;
}
@Component({
template: `
<cdk-drop
style="display: block; width: 100px; background: pink;"
[data]="items"
(dropped)="droppedSpy($event)">
<div
*ngFor="let item of items"
cdkDrag
style="width: 100%; height: ${ITEM_HEIGHT}px; background: red;">{{item}}</div>
</cdk-drop>
`
})
export class DraggableInDropZone {
@ViewChildren(CdkDrag) dragItems: QueryList<CdkDrag>;
@ViewChild(CdkDrop) dropInstance: CdkDrop;
items = ['Zero', 'One', 'Two', 'Three'];
droppedSpy = jasmine.createSpy('dropped spy').and.callFake((event: CdkDragDrop<string[]>) => {
moveItemInArray(this.items, event.previousIndex, event.currentIndex);
});
}
@Component({
template: `
<cdk-drop style="display: block; width: 100px; background: pink;">
<div *ngFor="let item of items" cdkDrag
style="width: 100%; height: ${ITEM_HEIGHT}px; background: red;">
{{item}}
<div class="custom-preview" *cdkDragPreview>Custom preview</div>
</div>
</cdk-drop>
`
})
export class DraggableInDropZoneWithCustomPreview {
@ViewChildren(CdkDrag) dragItems: QueryList<CdkDrag>;
items = ['Zero', 'One', 'Two', 'Three'];
}
@Component({
template: `
<cdk-drop style="display: block; width: 100px; background: pink;">
<div *ngFor="let item of items" cdkDrag
style="width: 100%; height: ${ITEM_HEIGHT}px; background: red;">
{{item}}
<div class="custom-placeholder" *cdkDragPlaceholder>Custom placeholder</div>
</div>
</cdk-drop>
`
})
export class DraggableInDropZoneWithCustomPlaceholder {
@ViewChildren(CdkDrag) dragItems: QueryList<CdkDrag>;
items = ['Zero', 'One', 'Two', 'Three'];
}
@Component({
template: `
<cdk-drop
#todoZone
style="display: block; width: 100px; background: pink;"
[data]="todo"
[connectedTo]="[doneZone]"
(dropped)="droppedSpy($event)">
<div
*ngFor="let item of todo"
cdkDrag
style="width: 100%; height: ${ITEM_HEIGHT}px; background: red;">{{item}}</div>
</cdk-drop>
<cdk-drop
#doneZone
style="display: block; width: 100px; background: purple;"
[data]="done"
[connectedTo]="[todoZone]"
(dropped)="droppedSpy($event)">
<div
*ngFor="let item of done"
cdkDrag
style="width: 100%; height: ${ITEM_HEIGHT}px; background: green;">{{item}}</div>
</cdk-drop>
`
})
export class ConnectedDropZones implements AfterViewInit {
@ViewChildren(CdkDrag) rawDragItems: QueryList<CdkDrag>;
@ViewChildren(CdkDrop) dropInstances: QueryList<CdkDrop>;
groupedDragItems: CdkDrag[][] = [];
todo = ['Zero', 'One', 'Two', 'Three'];
done = ['Four', 'Five', 'Six'];
droppedSpy = jasmine.createSpy('dropped spy').and.callFake((event: CdkDragDrop<string[]>) => {
if (event.previousContainer === event.container) {
moveItemInArray(event.container.data, event.previousIndex, event.currentIndex);
} else {
transferArrayItem(event.previousContainer.data,
event.container.data,
event.previousIndex,
event.currentIndex);
}
});
ngAfterViewInit() {
this.dropInstances.forEach((dropZone, index) => {
if (!this.groupedDragItems[index]) {
this.groupedDragItems.push([]);
}
this.groupedDragItems[index].push(...dropZone._draggables.toArray());
});
}
}
/**
* Drags an element to a position on the page using the mouse.
* @param fixture Fixture on which to run change detection.
* @param element Element which is being dragged.
* @param x Position along the x axis to which to drag the element.
* @param y Position along the y axis to which to drag the element.
*/
function dragElementViaMouse(fixture: ComponentFixture<any>,
element: HTMLElement, x: number, y: number) {
dispatchMouseEvent(element, 'mousedown');
fixture.detectChanges();
dispatchMouseEvent(document, 'mousemove', x, y);
fixture.detectChanges();
dispatchMouseEvent(document, 'mouseup');
fixture.detectChanges();
}
/**
* Drags an element to a position on the page using a touch device.
* @param fixture Fixture on which to run change detection.
* @param element Element which is being dragged.
* @param x Position along the x axis to which to drag the element.
* @param y Position along the y axis to which to drag the element.
*/
function dragElementViaTouch(fixture: ComponentFixture<any>,
element: HTMLElement, x: number, y: number) {
dispatchTouchEvent(element, 'touchstart');
fixture.detectChanges();
dispatchTouchEvent(document, 'touchmove', x, y);
fixture.detectChanges();
dispatchTouchEvent(document, 'touchend');
fixture.detectChanges();
}
/** Gets the index of a DOM element inside its parent. */
function getElementIndex(element: HTMLElement) {
return element.parentElement ? Array.from(element.parentElement.children).indexOf(element) : -1;
}
+524
View File
@@ -0,0 +1,524 @@
/**
* @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 {
Directive,
ContentChild,
Inject,
Optional,
ElementRef,
AfterContentInit,
NgZone,
SkipSelf,
OnDestroy,
Output,
EventEmitter,
ViewContainerRef,
EmbeddedViewRef,
} from '@angular/core';
import {CdkDragHandle} from './drag-handle';
import {DOCUMENT} from '@angular/platform-browser';
import {CdkDropContainer, CDK_DROP_CONTAINER} from './drop-container';
import {supportsPassiveEventListeners} from '@angular/cdk/platform';
import {CdkDragStart, CdkDragEnd, CdkDragExit, CdkDragEnter, CdkDragDrop} from './drag-events';
import {CdkDragPreview} from './drag-preview';
import {CdkDragPlaceholder} from './drag-placeholder';
/** Event options that can be used to bind an active event. */
const activeEventOptions = supportsPassiveEventListeners() ? {passive: false} : false;
// TODO: add an API for moving a draggable up/down the
// list programmatically. Useful for keyboard controls.
/** Element that can be moved inside a CdkDrop container. */
@Directive({
selector: '[cdkDrag]',
exportAs: 'cdkDrag',
host: {
'class': 'cdk-drag',
}
})
export class CdkDrag implements AfterContentInit, OnDestroy {
private _document: Document;
/** Element displayed next to the user's pointer while the element is dragged. */
private _preview: HTMLElement;
/** Reference to the view of the preview element. */
private _previewRef: EmbeddedViewRef<any> | null;
/** Reference to the view of the placeholder element. */
private _placeholderRef: EmbeddedViewRef<any> | null;
/** Element that is rendered instead of the draggable item while it is being sorted. */
private _placeholder: HTMLElement;
/** Coordinates within the element at which the user picked up the element. */
private _pickupPositionInElement: Point;
/** Coordinates on the page at which the user picked up the element. */
private _pickupPositionOnPage: Point;
/**
* CSS `transform` applied to the element when it isn't being dragged. We need a
* passive transform in order for the dragged element to retain its new position
* after the user has stopped dragging and because we need to know the relative
* position in case they start dragging again. This corresponds to `element.style.transform`.
*/
private _passiveTransform: Point = {x: 0, y: 0};
/** CSS `transform` that is applied to the element while it's being dragged. */
private _activeTransform: Point = {x: 0, y: 0};
/** Whether the element is being dragged. */
private _isDragging = false;
/** Whether the element has moved since the user started dragging it. */
private _hasMoved = false;
/** Drop container in which the CdkDrag resided when dragging began. */
private _initialContainer: CdkDropContainer;
/** Element that can be used to drag the draggable item. */
@ContentChild(CdkDragHandle) _handle: 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;
/** Emits when the user starts dragging the item. */
@Output('cdkDragStarted') started = new EventEmitter<CdkDragStart>();
/** Emits when the user stops dragging an item in the container. */
@Output('cdkDragEnded') ended = new EventEmitter<CdkDragEnd>();
/** Emits when the user has moved the item into a new container. */
@Output('cdkDragEntered') entered = new EventEmitter<CdkDragEnter<any>>();
/** Emits when the user removes the item its container by dragging it into another container. */
@Output('cdkDragExited') exited = new EventEmitter<CdkDragExit<any>>();
/** Emits when the user drops the item inside a container. */
@Output('cdkDragDropped') dropped = new EventEmitter<CdkDragDrop<any>>();
constructor(
/** Element that the draggable is attached to. */
public element: ElementRef<HTMLElement>,
/** Droppable container that the draggable is a part of. */
@Inject(CDK_DROP_CONTAINER) @Optional() @SkipSelf() public dropContainer: CdkDropContainer,
@Inject(DOCUMENT) document: any,
private _ngZone: NgZone,
private _viewContainerRef: ViewContainerRef) {
this._document = document;
}
/**
* Returns the element that is being used as a placeholder
* while the current element is being dragged.
*/
getPlaceholderElement(): HTMLElement {
return this._placeholder;
}
ngAfterContentInit() {
// TODO: doesn't handle (pun intended) the handle being destroyed
const dragElement = (this._handle ? this._handle.element : this.element).nativeElement;
dragElement.addEventListener('mousedown', this._pointerDown);
dragElement.addEventListener('touchstart', this._pointerDown);
// Webkit won't preventDefault on a dynamically-added `touchmove` listener, which means that
// we need to add one ahead of time. See https://bugs.webkit.org/show_bug.cgi?id=184250.
// TODO: move into a central registry.
this._ngZone.runOutsideAngular(() => {
this._document.addEventListener('touchmove', this._preventScrollListener, activeEventOptions);
});
}
ngOnDestroy() {
this._removeDocumentEvents();
this._destroyPreview();
this._destroyPlaceholder();
this._document.removeEventListener('touchmove', this._preventScrollListener,
activeEventOptions as any);
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);
}
}
/** Handler for when the pointer is pressed down on the element or the handle. */
private _pointerDown = (event: MouseEvent | TouchEvent) => {
if (this._isDragging) {
return;
}
this._isDragging = true;
this._initialContainer = this.dropContainer;
// If we have a custom preview template, the element won't be visible anyway so we avoid the
// extra `getBoundingClientRect` calls and just move the preview next to the cursor.
this._pickupPositionInElement = this._previewTemplate ? {x: 0, y: 0} :
this._getPointerPositionInElement(event);
this._pickupPositionOnPage = this._getPointerPositionOnPage(event);
this._registerMoveListeners(event);
// Emit the event on the item before the one on the container.
this.started.emit({source: this});
if (this.dropContainer) {
const element = this.element.nativeElement;
const preview = this._preview = this._createPreviewElement();
const placeholder = this._placeholder = this._createPlaceholderElement();
// We move the element out at the end of the body and we make it hidden, because keeping it in
// place will throw off the consumer's `:last-child` selectors. We can't remove the element
// from the DOM completely, because iOS will stop firing all subsequent events in the chain.
element.style.display = 'none';
this._document.body.appendChild(element.parentNode!.replaceChild(placeholder, element));
this._document.body.appendChild(preview);
this.dropContainer.start();
}
}
/** Handler that is invoked when the user moves their pointer after they've initiated a drag. */
private _pointerMove = (event: MouseEvent | TouchEvent) => {
if (!this._isDragging) {
return;
}
this._hasMoved = true;
event.preventDefault();
if (this.dropContainer) {
this._updateActiveDropContainer(event);
} else {
const activeTransform = this._activeTransform;
const {x: pageX, y: pageY} = this._getPointerPositionOnPage(event);
activeTransform.x = pageX - this._pickupPositionOnPage.x + this._passiveTransform.x;
activeTransform.y = pageY - this._pickupPositionOnPage.y + this._passiveTransform.y;
this._setTransform(this.element.nativeElement, activeTransform.x, activeTransform.y);
}
}
/** Handler that is invoked when the user lifts their pointer up, after initiating a drag. */
private _pointerUp = () => {
if (!this._isDragging) {
return;
}
this._removeDocumentEvents();
this._isDragging = false;
if (!this.dropContainer) {
// Convert the active transform into a passive one. This means that next time
// the user starts dragging the item, its position will be calculated relatively
// to the new passive transform.
this._passiveTransform.x = this._activeTransform.x;
this._passiveTransform.y = this._activeTransform.y;
this._ngZone.run(() => this.ended.emit({source: this}));
return;
}
this._animatePreviewToPlaceholder().then(() => this._cleanupDragArtifacts());
}
/** Cleans up the DOM artifacts that were added to facilitate the element being dragged. */
private _cleanupDragArtifacts() {
this._destroyPreview();
this._placeholder.parentNode!.insertBefore(this.element.nativeElement, this._placeholder);
this._destroyPlaceholder();
this.element.nativeElement.style.display = '';
// Re-enter the NgZone since we bound `document` events on the outside.
this._ngZone.run(() => {
const currentIndex = this._getElementIndexInDom();
this.ended.emit({source: this});
this.dropped.emit({
item: this,
currentIndex,
previousIndex: this._initialContainer.getItemIndex(this),
container: this.dropContainer,
previousContainer: this._initialContainer
});
this.dropContainer.drop(this, currentIndex, this._initialContainer);
});
}
/**
* Updates the item's position in its drop container, or moves it
* into a new one, depending on its current drag position.
*/
private _updateActiveDropContainer(event: MouseEvent | TouchEvent) {
const {x, y} = this._getPointerPositionOnPage(event);
// Drop container that draggable has been moved into.
const newContainer = this.dropContainer._getSiblingContainerFromPosition(x, y);
if (newContainer) {
this._ngZone.run(() => {
// Notify the old container that the item has left.
this.exited.emit({ item: this, container: this.dropContainer });
this.dropContainer.exit(this);
// Notify the new container that the item has entered.
this.entered.emit({ item: this, container: newContainer });
this.dropContainer = newContainer;
this.dropContainer.enter(this);
});
}
this.dropContainer._sortItem(this, y);
this._setTransform(this._preview,
x - this._pickupPositionInElement.x,
y - this._pickupPositionInElement.y);
}
/**
* Creates the element that will be rendered next to the user's pointer
* and will be used as a preview of the element that is being dragged.
*/
private _createPreviewElement(): HTMLElement {
let preview: HTMLElement;
if (this._previewTemplate) {
const viewRef = this._viewContainerRef.createEmbeddedView(this._previewTemplate.templateRef,
this._previewTemplate.data);
preview = viewRef.rootNodes[0];
this._previewRef = viewRef;
this._setTransform(preview, this._pickupPositionOnPage.x, this._pickupPositionOnPage.y);
} else {
const element = this.element.nativeElement;
const elementRect = element.getBoundingClientRect();
preview = element.cloneNode(true) as HTMLElement;
preview.style.width = `${elementRect.width}px`;
preview.style.height = `${elementRect.height}px`;
this._setTransform(preview, elementRect.left, elementRect.top);
}
preview.classList.add('cdk-drag-preview');
return preview;
}
/** Creates an element that will be shown instead of the current element while dragging. */
private _createPlaceholderElement(): HTMLElement {
let placeholder: HTMLElement;
if (this._placeholderTemplate) {
this._placeholderRef = this._viewContainerRef.createEmbeddedView(
this._placeholderTemplate.templateRef,
this._placeholderTemplate.data
);
placeholder = this._placeholderRef.rootNodes[0];
} else {
placeholder = this.element.nativeElement.cloneNode(true) as HTMLElement;
}
placeholder.classList.add('cdk-drag-placeholder');
return placeholder;
}
/** Gets the index of the dragable element, based on its index in the DOM. */
private _getElementIndexInDom(): number {
// Note: we may be able to figure this in memory while sorting, but doing so won't be very
// reliable when transferring between containers, because the new container doesn't have
// the proper indices yet. Also this will work better for the case where the consumer
// isn't using an `ngFor` to render the list.
const element = this.element.nativeElement;
if (!element.parentElement) {
return -1;
}
// Avoid accessing `children` and `children.length` too much since they're a "live collection".
let index = 0;
const siblings = element.parentElement.children;
const siblingsLength = siblings.length;
const draggableElements = this.dropContainer._draggables
.filter(item => item !== this)
.map(item => item.element.nativeElement);
// Loop through the sibling elements to find out the index of the
// current one, while skipping any elements that aren't draggable.
for (let i = 0; i < siblingsLength; i++) {
if (siblings[i] === element) {
return index;
} else if (draggableElements.indexOf(siblings[i] as HTMLElement) > -1) {
index++;
}
}
return -1;
}
/**
* Figures out the coordinates at which an element was picked up.
* @param event Event that initiated the dragging.
*/
private _getPointerPositionInElement(event: MouseEvent | TouchEvent): Point {
const elementRect = this.element.nativeElement.getBoundingClientRect();
const handleElement = this._handle ? this._handle.element.nativeElement : null;
const referenceRect = handleElement ? handleElement.getBoundingClientRect() : elementRect;
const x = this._isTouchEvent(event) ? event.targetTouches[0].pageX - referenceRect.left :
event.offsetX;
const y = this._isTouchEvent(event) ? event.targetTouches[0].pageY - referenceRect.top :
event.offsetY;
return {
x: referenceRect.left - elementRect.left + x,
y: referenceRect.top - elementRect.top + y
};
}
/**
* Animates the preview element from its current position to the location of the drop placeholder.
* @returns Promise that resolves when the animation completes.
*/
private _animatePreviewToPlaceholder(): Promise<void> {
// If the user hasn't moved yet, the transitionend event won't fire.
if (!this._hasMoved) {
return Promise.resolve();
}
const placeholderRect = this._placeholder.getBoundingClientRect();
// Apply the class that adds a transition to the preview.
this._preview.classList.add('cdk-drag-animating');
// Move the preview to the placeholder position.
this._setTransform(this._preview, placeholderRect.left, placeholderRect.top);
// If the element doesn't have a `transition`, the `transitionend` event won't fire. Since
// we need to trigger a style recalculation in order for the `cdk-drag-animating` class to
// apply its style, we take advantage of the available info to figure out whether we need to
// bind the event in the first place.
const duration = getComputedStyle(this._preview).getPropertyValue('transition-duration');
if (parseFloat(duration) === 0) {
return Promise.resolve();
}
return this._ngZone.runOutsideAngular(() => {
return new Promise(resolve => {
const handler = (event: Event) => {
if (event.target === this._preview) {
this._preview.removeEventListener('transitionend', handler);
resolve();
}
};
this._preview.addEventListener('transitionend', handler);
});
});
}
/**
* Sets the `transform` style on an element.
* @param element Element on which to set the transform.
* @param x Desired position of the element along the X axis.
* @param y Desired position of the element along the Y axis.
*/
private _setTransform(element: HTMLElement, x: number, y: number) {
element.style.transform = `translate3d(${x}px, ${y}px, 0)`;
}
/**
* Helper to remove an element from the DOM and to do all the necessary null checks.
* @param element Element to be removed.
*/
private _removeElement(element: HTMLElement | null) {
if (element && element.parentNode) {
element.parentNode.removeChild(element);
}
}
/** Removes the global event listeners that were bound by this draggable. */
private _removeDocumentEvents() {
this._document.removeEventListener('mousemove', this._pointerMove,
activeEventOptions as any);
this._document.removeEventListener('touchmove', this._pointerMove,
activeEventOptions as any);
this._document.removeEventListener('mouseup', this._pointerUp);
this._document.removeEventListener('touchend', this._pointerUp);
}
/** Determines the point of the page that was touched by the user. */
private _getPointerPositionOnPage(event: MouseEvent | TouchEvent): Point {
return this._isTouchEvent(event) ? {x: event.touches[0].pageX, y: event.touches[0].pageY} :
{x: event.pageX, y: event.pageY};
}
/** Listener used to prevent `touchmove` events while the element is being dragged. */
private _preventScrollListener = (event: TouchEvent) => {
if (this._isDragging) {
event.preventDefault();
}
}
/** Determines whether an event is a touch event. */
private _isTouchEvent(event: MouseEvent | TouchEvent): event is TouchEvent {
return event.type.startsWith('touch');
}
/** Destroys the preview element and its ViewRef. */
private _destroyPreview() {
if (this._preview) {
this._removeElement(this._preview);
}
if (this._previewRef) {
this._previewRef.destroy();
}
this._preview = this._previewRef = null!;
}
/** Destroys the placeholder element and its ViewRef. */
private _destroyPlaceholder() {
if (this._placeholder) {
this._removeElement(this._placeholder);
}
if (this._placeholderRef) {
this._placeholderRef.destroy();
}
this._placeholder = this._placeholderRef = null!;
}
/**
* Registers global event listeners that are used for moving the element.
* @param event Event that initiated the dragging.
*/
private _registerMoveListeners(event: MouseEvent | TouchEvent) {
this._ngZone.runOutsideAngular(() => {
const isTouchEvent = this._isTouchEvent(event);
// We explicitly bind __active__ listeners here, because newer browsers
// will default to passive ones for `mousemove` and `touchmove`.
// TODO: this should be bound in `mousemove` and after a certain threshold,
// otherwise it'll interfere with clicks on the element.
this._document.addEventListener(isTouchEvent ? 'touchmove' : 'mousemove', this._pointerMove,
activeEventOptions);
this._document.addEventListener(isTouchEvent ? 'touchend' : 'mouseup', this._pointerUp);
});
}
}
/** Point on the page or within an element. */
interface Point {
x: number;
y: number;
}
@@ -0,0 +1,55 @@
/**
* @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 {InjectionToken, QueryList} from '@angular/core';
import {CdkDrag} from './drag';
/** @docs-private */
export interface CdkDropContainer<T = any> {
/** Arbitrary data to attach to all events emitted by this container. */
data: T;
/** Starts dragging an item. */
start(): void;
/**
* 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.
*/
drop(item: CdkDrag, currentIndex: number, previousContainer?: CdkDropContainer): void;
/**
* Emits an event to indicate that the user moved an item into the container.
* @param item Item that was moved into the container.
*/
enter(item: CdkDrag): void;
/**
* 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;
/**
* Figures out the index of an item in the container.
* @param item Item whose index should be determined.
*/
getItemIndex(item: CdkDrag): number;
_sortItem(item: CdkDrag, yOffset: number): void;
_draggables: QueryList<CdkDrag>;
_getSiblingContainerFromPosition(x: number, y: number): CdkDropContainer | null;
}
/**
* Injection token that is used to provide a CdkDrop instance to CdkDrag.
* Used for avoiding circular imports.
* @docs-private
*/
export const CDK_DROP_CONTAINER = new InjectionToken<CdkDropContainer>('CDK_DROP_CONTAINER');
+8
View File
@@ -0,0 +1,8 @@
$cdk-z-index-drag-preview: 1000;
.cdk-drag-preview {
position: absolute;
top: 0;
left: 0;
z-index: $cdk-z-index-drag-preview;
}
+194
View File
@@ -0,0 +1,194 @@
/**
* @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 {
Component,
ContentChildren,
forwardRef,
Input,
ViewEncapsulation,
ChangeDetectionStrategy,
Output,
EventEmitter,
ElementRef,
QueryList,
} from '@angular/core';
import {CdkDrag} from './drag';
import {CdkDragExit, CdkDragEnter, CdkDragDrop} from './drag-events';
import {CDK_DROP_CONTAINER} from './drop-container';
/** Container that wraps a set of draggable items. */
@Component({
moduleId: module.id,
selector: 'cdk-drop',
exportAs: 'cdkDrop',
template: '<ng-content></ng-content>',
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
styleUrls: ['drop.css'],
providers: [
{provide: CDK_DROP_CONTAINER, useExisting: CdkDrop},
],
host: {
'class': 'cdk-drop',
'[class.cdk-drop-dragging]': '_dragging'
}
})
export class CdkDrop<T = any> {
/** 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.
*/
@Input() connectedTo: CdkDrop[] = [];
/** Arbitrary data to attach to all events emitted by this container. */
@Input() data: T;
/** Emits when the user drops an item inside the container. */
@Output() dropped = new EventEmitter<CdkDragDrop<T, any>>();
/**
* Emits when the user has moved a new drag item into this container.
*/
@Output() entered = new EventEmitter<CdkDragEnter<T>>();
/**
* Emits when the user removes an item from the container
* by dragging it into another container.
*/
@Output() exited = new EventEmitter<CdkDragExit<T>>();
constructor(public element: ElementRef<HTMLElement>) {}
/** Whether an item in the container is being dragged. */
_dragging = false;
/** Cache of the dimensions of all the items and the sibling containers. */
private _positionCache = {
items: [] as {drag: CdkDrag, clientRect: ClientRect}[],
siblings: [] as {drop: CdkDrop, clientRect: ClientRect}[]
};
/** Starts dragging an item. */
start(): void {
this._dragging = true;
this._refreshPositions();
}
/**
* 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.
*/
drop(item: CdkDrag, currentIndex: number, previousContainer: CdkDrop): void {
this.dropped.emit({
item,
currentIndex,
previousIndex: previousContainer.getItemIndex(item),
container: this,
// TODO: reconsider whether to make this null if the containers are the same.
previousContainer
});
this._reset();
}
/**
* Emits an event to indicate that the user moved an item into the container.
* @param item Item that was moved into the container.
*/
enter(item: CdkDrag): void {
this.entered.emit({item, container: this});
this.start();
}
/**
* 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._reset();
this.exited.emit({item, container: this});
}
/**
* Figures out the index of an item in the container.
* @param item Item whose index should be determined.
*/
getItemIndex(item: CdkDrag): number {
return this._draggables.toArray().indexOf(item);
}
/**
* Sorts an item inside the container based on its position.
* @param item Item to be sorted.
* @param yOffset Position of the item along the Y axis.
*/
_sortItem(item: CdkDrag, yOffset: number): void {
// TODO: only covers Y axis sorting.
const siblings = this._positionCache.items;
const newPosition = siblings.find(({drag, clientRect}) => {
return drag !== item && yOffset > clientRect.top && yOffset < clientRect.bottom;
});
if (!newPosition && siblings.length > 0) {
return;
}
const element = newPosition ? newPosition.drag.element.nativeElement : null;
const next = element ? element!.nextSibling : null;
const parent = element ? element.parentElement! : this.element.nativeElement;
const placeholder = item.getPlaceholderElement();
if (next) {
parent.insertBefore(placeholder, next === placeholder ? element : next);
} else {
parent.appendChild(placeholder);
}
this._refreshPositions();
}
/**
* Figures out whether an item should be moved into a sibling
* drop container, based on its current position.
* @param x Position of the item along the X axis.
* @param y Position of the item along the Y axis.
*/
_getSiblingContainerFromPosition(x: number, y: number): CdkDrop | null {
const result = this._positionCache.siblings.find(({clientRect}) => {
const {top, bottom, left, right} = clientRect;
return y >= top && y <= bottom && x >= left && x <= right;
});
return result ? result.drop : null;
}
/** Refreshes the position cache of the items and sibling containers. */
private _refreshPositions() {
this._positionCache.items = this._draggables
.map(drag => ({drag, clientRect: drag.element.nativeElement.getBoundingClientRect()}))
.sort((a, b) => a.clientRect.top - b.clientRect.top);
// TODO: add filter here that ensures that the current container isn't being passed to itself.
this._positionCache.siblings = this.connectedTo
.map(drop => ({drop, clientRect: drop.element.nativeElement.getBoundingClientRect()}));
}
/** Resets the container to its initial state. */
private _reset() {
this._dragging = false;
this._positionCache.items = [];
this._positionCache.siblings = [];
}
}
+9
View File
@@ -0,0 +1,9 @@
/**
* @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
*/
export * from './public-api';
@@ -0,0 +1,16 @@
/**
* @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
*/
export * from './drop';
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';
@@ -0,0 +1,15 @@
{
"extends": "../tsconfig-build",
"files": [
"public-api.ts",
"./typings.d.ts"
],
"angularCompilerOptions": {
"annotateForClosureCompiler": true,
"strictMetadataEmit": true,
"flatModuleOutFile": "index.js",
"flatModuleId": "@angular/cdk-experimental/drag-drop",
"skipTemplateCodegen": true,
"fullTemplateTypeCheck": true
}
}
+1
View File
@@ -0,0 +1 @@
declare var module: {id: string};
+2 -1
View File
@@ -41,7 +41,8 @@ export function createTouchEvent(type: string, pageX = 0, pageY = 0) {
// Most of the browsers don't have a "initTouchEvent" method that can be used to define
// the touch details.
Object.defineProperties(event, {
touches: {value: [touchDetails]}
touches: {value: [touchDetails]},
targetTouches: {value: [touchDetails]}
});
return event;
+1
View File
@@ -62,6 +62,7 @@ export class DemoApp {
{name: 'Datepicker', route: '/datepicker'},
{name: 'Dialog', route: '/dialog'},
{name: 'Drawer', route: '/drawer'},
{name: 'Drag and Drop', route: '/drag-drop'},
{name: 'Expansion Panel', route: '/expansion'},
{name: 'Focus Origin', route: '/focus-origin'},
{name: 'Gestures', route: '/gestures'},
+2
View File
@@ -63,6 +63,7 @@ import {TypographyDemo} from '../typography/typography-demo';
import {VirtualScrollDemo} from '../virtual-scroll/virtual-scroll-demo';
import {DemoApp, Home} from './demo-app';
import {DEMO_APP_ROUTES} from './routes';
import {DragAndDropDemo} from '../drag-drop/drag-drop-demo';
@NgModule({
imports: [
@@ -96,6 +97,7 @@ import {DEMO_APP_ROUTES} from './routes';
DialogDemo,
DrawerDemo,
ExampleBottomSheet,
DragAndDropDemo,
ExpansionDemo,
FocusOriginDemo,
GesturesDemo,
+2
View File
@@ -55,6 +55,7 @@ import {PaginatorDemo} from '../paginator/paginator-demo';
import {ExamplesPage} from '../examples-page/examples-page';
import {TableDemo} from '../table/table-demo';
import {DragAndDropDemo} from '../drag-drop/drag-drop-demo';
export const DEMO_APP_ROUTES: Routes = [
{path: '', component: DemoApp, children: [
@@ -71,6 +72,7 @@ export const DEMO_APP_ROUTES: Routes = [
{path: 'datepicker', component: DatepickerDemo},
{path: 'dialog', component: DialogDemo},
{path: 'drawer', component: DrawerDemo},
{path: 'drag-drop', component: DragAndDropDemo},
{path: 'expansion', component: ExpansionDemo},
{path: 'focus-origin', component: FocusOriginDemo},
{path: 'gestures', component: GesturesDemo},
+2
View File
@@ -8,6 +8,7 @@
import {ScrollingModule} from '@angular/cdk-experimental/scrolling';
import {DialogModule} from '@angular/cdk-experimental/dialog';
import {DragDropModule} from '@angular/cdk-experimental/drag-drop';
import {A11yModule} from '@angular/cdk/a11y';
import {CdkAccordionModule} from '@angular/cdk/accordion';
import {BidiModule} from '@angular/cdk/bidi';
@@ -112,6 +113,7 @@ import {
PortalModule,
ScrollingModule,
DialogModule,
DragDropModule,
]
})
export class DemoMaterialModule {}
@@ -0,0 +1,38 @@
<div class="list">
<h2>To do</h2>
<cdk-drop
#one
(cdkDragDropped)="drop($event)"
[data]="todo"
[connectedTo]="[two]">
<div *ngFor="let item of todo" cdkDrag>
{{item}}
<mat-icon cdkDragHandle svgIcon="dnd-move"></mat-icon>
</div>
</cdk-drop>
</div>
<div class="list">
<h2>Done</h2>
<cdk-drop
#two
(cdkDragDropped)="drop($event)"
[data]="done"
[connectedTo]="[one]">
<div *ngFor="let item of done" cdkDrag>
{{item}}
<mat-icon cdkDragHandle svgIcon="dnd-move"></mat-icon>
</div>
</cdk-drop>
</div>
<div class="list">
<h2>Data</h2>
<pre>{{todo.join(', ')}}</pre>
<pre>{{done.join(', ')}}</pre>
</div>
<div class="list">
<h2>Free dragging</h2>
<div cdkDrag class="free-draggable">Drag me around</div>
</div>
@@ -0,0 +1,71 @@
.list {
width: 500px;
max-width: 100%;
margin-bottom: 25px;
display: inline-block;
margin-right: 25px;
vertical-align: top;
}
.cdk-drop {
border: solid 1px #ccc;
min-height: 60px;
display: block;
}
.cdk-drag {
padding: 20px 10px;
border-bottom: solid 1px #ccc;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
box-sizing: border-box;
.cdk-drop &:last-child {
border: none;
}
.cdk-drop-dragging & {
transition: transform 500ms ease;
}
}
.cdk-drag-preview {
box-sizing: border-box;
opacity: 0.5;
}
.cdk-drag-animating {
transition: transform 500ms ease;
}
.cdk-drag-placeholder {
opacity: 0.5;
}
.wrapper {
border: solid 1px red;
}
.cdk-drag-handle {
cursor: move;
svg {
fill: rgba(0, 0, 0, 0.5);
}
}
pre {
white-space: normal;
}
.free-draggable {
width: 200px;
height: 200px;
border: solid 1px #ccc;
cursor: move;
display: flex;
justify-content: center;
align-items: center;
}
+56
View File
@@ -0,0 +1,56 @@
/**
* @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 {Component, ViewEncapsulation} from '@angular/core';
import {MatIconRegistry} from '@angular/material/icon';
import {DomSanitizer} from '@angular/platform-browser';
import {CdkDragDrop, moveItemInArray, transferArrayItem} from '@angular/cdk-experimental/drag-drop';
@Component({
moduleId: module.id,
selector: 'drag-drop-demo',
templateUrl: 'drag-drop-demo.html',
styleUrls: ['drag-drop-demo.css'],
encapsulation: ViewEncapsulation.None,
})
export class DragAndDropDemo {
todo = [
'Come up with catchy start-up name',
'Add "blockchain" to name',
'Sell out',
'Profit',
'Go to sleep'
];
done = [
'Get up',
'Have breakfast',
'Brush teeth',
'Check reddit'
];
constructor(iconRegistry: MatIconRegistry, sanitizer: DomSanitizer) {
iconRegistry.addSvgIconLiteral('dnd-move', sanitizer.bypassSecurityTrustHtml(`
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="M10 9h4V6h3l-5-5-5 5h3v3zm-1 1H6V7l-5 5 5 5v-3h3v-4zm14 2l-5-5v3h-3v4h3v3l5` +
`-5zm-9 3h-4v3H7l5 5 5-5h-3v-3z"/>
<path d="M0 0h24v24H0z" fill="none"/>
</svg>
`));
}
drop(event: CdkDragDrop<string[]>) {
if (event.previousContainer === event.container) {
moveItemInArray(event.container.data, event.previousIndex, event.currentIndex);
} else {
transferArrayItem(event.previousContainer.data,
event.container.data,
event.previousIndex,
event.currentIndex);
}
}
}
+2
View File
@@ -64,6 +64,8 @@ System.config({
'@angular/cdk-experimental/scrolling': 'dist/packages/cdk-experimental/scrolling/index.js',
'@angular/cdk-experimental/dialog': 'dist/packages/cdk-experimental/dialog/index.js',
'@angular/cdk-experimental/drag-drop':
'dist/packages/cdk-experimental/drag-drop/index.js',
'@angular/material/autocomplete': 'dist/packages/material/autocomplete/index.js',
'@angular/material/bottom-sheet': 'dist/packages/material/bottom-sheet/index.js',
+2
View File
@@ -1,5 +1,6 @@
import {ScrollingModule} from '@angular/cdk-experimental/scrolling';
import {DialogModule} from '@angular/cdk-experimental/dialog';
import {DragDropModule} from '@angular/cdk-experimental/drag-drop';
import {FullscreenOverlayContainer, OverlayContainer} from '@angular/cdk/overlay';
import {NgModule} from '@angular/core';
import {ReactiveFormsModule} from '@angular/forms';
@@ -71,6 +72,7 @@ import {VirtualScrollE2E} from './virtual-scroll/virtual-scroll-e2e';
MatNativeDateModule,
ScrollingModule,
DialogModule,
DragDropModule,
]
})
export class E2eMaterialModule {}
+1
View File
@@ -54,6 +54,7 @@ System.config({
'@angular/cdk-experimental/scrolling': 'dist/bundles/cdk-experimental-scrolling.umd.js',
'@angular/cdk-experimental/dialog': 'dist/bundles/cdk-experimental-dialog.umd.js',
'@angular/cdk-experimental/drag-drop': 'dist/bundles/cdk-experimental-drag-drop.umd.js',
'@angular/material/autocomplete': 'dist/bundles/material-autocomplete.umd.js',
'@angular/material/bottom-sheet': 'dist/bundles/material-bottom-sheet.umd.js',
+2
View File
@@ -74,6 +74,8 @@ System.config({
'@angular/cdk-experimental/scrolling': 'dist/packages/cdk-experimental/scrolling/index.js',
'@angular/cdk-experimental/dialog': 'dist/packages/cdk-experimental/dialog/index.js',
'@angular/cdk-experimental/drag-drop':
'dist/packages/cdk-experimental/drag-drop/index.js',
'@angular/material/autocomplete': 'dist/packages/material/autocomplete/index.js',
'@angular/material/badge': 'dist/packages/material/badge/index.js',