mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
feat: add experimental drag-drop package (#11864)
This commit is contained in:
committed by
Joey Perrott
parent
0ca08b78aa
commit
27811a3329
@@ -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
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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');
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 = [];
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -0,0 +1 @@
|
||||
declare var module: {id: string};
|
||||
@@ -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;
|
||||
|
||||
@@ -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'},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user