Skip to content

feat(drag-drop): add service for attaching drag&drop to arbitrary DOM nodes #14437

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 25, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/cdk/drag-drop/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ ng_test_library(
name = "drag-drop_test_sources",
srcs = glob(["**/*.spec.ts"]),
deps = [
"@rxjs",
"//src/cdk/testing",
"//src/cdk/bidi",
":drag-drop",
Expand Down
5 changes: 3 additions & 2 deletions src/cdk/drag-drop/directives/drag.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
createTouchEvent,
} from '@angular/cdk/testing';
import {Directionality} from '@angular/cdk/bidi';
import {of as observableOf} from 'rxjs';
import {CdkDrag, CDK_DRAG_CONFIG} from './drag';
import {CdkDragDrop} from '../drag-events';
import {moveItemInArray} from '../drag-utils';
Expand Down Expand Up @@ -1140,7 +1141,7 @@ describe('CdkDrag', () => {
it('should dispatch the correct `dropped` event in RTL horizontal drop zone', fakeAsync(() => {
const fixture = createComponent(DraggableInHorizontalDropZone, [{
provide: Directionality,
useValue: ({value: 'rtl'})
useValue: ({value: 'rtl', change: observableOf()})
}]);

fixture.nativeElement.setAttribute('dir', 'rtl');
Expand Down Expand Up @@ -1296,7 +1297,7 @@ describe('CdkDrag', () => {
it('should pass the proper direction to the preview in rtl', fakeAsync(() => {
const fixture = createComponent(DraggableInDropZone, [{
provide: Directionality,
useValue: ({value: 'rtl'})
useValue: ({value: 'rtl', change: observableOf()})
}]);

fixture.detectChanges();
Expand Down
62 changes: 44 additions & 18 deletions src/cdk/drag-drop/directives/drag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import {CDK_DRAG_PARENT} from '../drag-parent';
import {DragRef, DragRefConfig} from '../drag-ref';
import {DropListRef} from '../drop-list-ref';
import {CdkDropListInternal as CdkDropList} from './drop-list';
import {DragDrop} from '../drag-drop';

/** Injection token that can be used to configure the behavior of `CdkDrag`. */
export const CDK_DRAG_CONFIG = new InjectionToken<DragRefConfig>('CDK_DRAG_CONFIG', {
Expand Down Expand Up @@ -167,18 +168,30 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
@Inject(DOCUMENT) private _document: any,
private _ngZone: NgZone,
private _viewContainerRef: ViewContainerRef,
private _viewportRuler: ViewportRuler,
private _dragDropRegistry: DragDropRegistry<DragRef, DropListRef>,
@Inject(CDK_DRAG_CONFIG) private _config: DragRefConfig,
@Optional() private _dir: Directionality) {

const ref = this._dragRef = new DragRef(element, this._document, this._ngZone,
this._viewContainerRef, this._viewportRuler, this._dragDropRegistry,
this._config, this.dropContainer ? this.dropContainer._dropListRef : undefined,
this._dir);
ref.data = this;
this._syncInputs(ref);
this._proxyEvents(ref);
viewportRuler: ViewportRuler,
dragDropRegistry: DragDropRegistry<DragRef, DropListRef>,
@Inject(CDK_DRAG_CONFIG) config: DragRefConfig,
@Optional() private _dir: Directionality,

/**
* @deprecated `viewportRuler` and `dragDropRegistry` parameters
* to be removed. Also `dragDrop` parameter to be made required.
* @breaking-change 8.0.0.
*/
dragDrop?: DragDrop) {


// @breaking-change 8.0.0 Remove null check once the paramter is made required.
if (dragDrop) {
this._dragRef = dragDrop.createDrag(element, config);
} else {
this._dragRef = new DragRef(element, config, _document, _ngZone, viewportRuler,
dragDropRegistry);
}

this._dragRef.data = this;
this._syncInputs(this._dragRef);
this._proxyEvents(this._dragRef);
}

/**
Expand Down Expand Up @@ -273,15 +286,28 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
private _syncInputs(ref: DragRef<CdkDrag<T>>) {
ref.beforeStarted.subscribe(() => {
if (!ref.isDragging()) {
const {_placeholderTemplate: placeholder, _previewTemplate: preview} = this;
const dir = this._dir;
const placeholder = this._placeholderTemplate ? {
template: this._placeholderTemplate.templateRef,
context: this._placeholderTemplate.data,
viewContainer: this._viewContainerRef
} : null;
const preview = this._previewTemplate ? {
template: this._previewTemplate.templateRef,
context: this._previewTemplate.data,
viewContainer: this._viewContainerRef
} : null;

ref.disabled = this.disabled;
ref.lockAxis = this.lockAxis;
ref.withBoundaryElement(this._getBoundaryElement());
placeholder ? ref.withPlaceholderTemplate(placeholder.templateRef, placeholder.data) :
ref.withPlaceholderTemplate(null);
preview ? ref.withPreviewTemplate(preview.templateRef, preview.data) :
ref.withPreviewTemplate(null);
ref
.withBoundaryElement(this._getBoundaryElement())
.withPlaceholderTemplate(placeholder)
.withPreviewTemplate(preview);

if (dir) {
ref.withDirection(dir.value);
}
}
});
}
Expand Down
70 changes: 54 additions & 16 deletions src/cdk/drag-drop/directives/drop-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
ChangeDetectorRef,
SkipSelf,
Inject,
AfterContentInit,
} from '@angular/core';
import {DOCUMENT} from '@angular/common';
import {Directionality} from '@angular/cdk/bidi';
Expand All @@ -31,6 +32,9 @@ import {CDK_DROP_LIST_CONTAINER, CdkDropListContainer} from '../drop-list-contai
import {CdkDropListGroup} from './drop-list-group';
import {DropListRef} from '../drop-list-ref';
import {DragRef} from '../drag-ref';
import {DragDrop} from '../drag-drop';
import {Subject} from 'rxjs';
import {startWith, takeUntil} from 'rxjs/operators';

/** Counter used to generate unique ids for drop zones. */
let _uniqueIdCounter = 0;
Expand Down Expand Up @@ -62,15 +66,22 @@ export interface CdkDropListInternal extends CdkDropList {}
'[class.cdk-drop-list-receiving]': '_dropListRef.isReceiving()',
}
})
export class CdkDropList<T = any> implements CdkDropListContainer, OnDestroy {
export class CdkDropList<T = any> implements CdkDropListContainer, AfterContentInit, OnDestroy {
/** Emits when the list has been destroyed. */
private _destroyed = new Subject<void>();

/** Keeps track of the drop lists that are currently on the page. */
private static _dropLists: CdkDropList[] = [];

/** Reference to the underlying drop list instance. */
_dropListRef: DropListRef<CdkDropList<T>>;

/** Draggable items in the container. */
@ContentChildren(forwardRef(() => CdkDrag)) _draggables: QueryList<CdkDrag>;
@ContentChildren(forwardRef(() => CdkDrag), {
// Explicitly set to false since some of the logic below makes assumptions about it.
// The `.withItems` call below should be updated if we ever need to switch this to `true`.
descendants: false
}) _draggables: QueryList<CdkDrag>;

/**
* Other draggable containers that this container is connected to and into which the
Expand Down Expand Up @@ -134,34 +145,52 @@ export class CdkDropList<T = any> implements CdkDropListContainer, OnDestroy {
sorted: EventEmitter<CdkDragSortEvent<T>> = new EventEmitter<CdkDragSortEvent<T>>();

constructor(
/** Element that the drop list is attached to. */
public element: ElementRef<HTMLElement>,
dragDropRegistry: DragDropRegistry<DragRef, DropListRef>,
private _changeDetectorRef: ChangeDetectorRef,
@Optional() dir?: Directionality,
@Optional() private _dir?: Directionality,
@Optional() @SkipSelf() private _group?: CdkDropListGroup<CdkDropList>,
// @breaking-change 8.0.0 `_document` parameter to be made required.
@Optional() @Inject(DOCUMENT) _document?: any) {

@Optional() @Inject(DOCUMENT) _document?: any,

/**
* @deprecated `dragDropRegistry` and `_document` parameters to be removed.
* Also `dragDrop` parameter to be made required.
* @breaking-change 8.0.0.
*/
dragDrop?: DragDrop) {

// @breaking-change 8.0.0 Remove null check once `dragDrop` parameter is made required.
if (dragDrop) {
this._dropListRef = dragDrop.createDropList(element);
} else {
this._dropListRef = new DropListRef(element, dragDropRegistry, _document || document);
}

// @breaking-change 8.0.0 Remove || once `_document` parameter is required.
const ref = this._dropListRef = new DropListRef(element, dragDropRegistry,
_document || document, dir);
ref.data = this;
ref.enterPredicate = (drag: DragRef<CdkDrag>, drop: DropListRef<CdkDropList>) => {
this._dropListRef.data = this;
this._dropListRef.enterPredicate = (drag: DragRef<CdkDrag>, drop: DropListRef<CdkDropList>) => {
return this.enterPredicate(drag.data, drop.data);
};
this._syncInputs(ref);
this._handleEvents(ref);

this._syncInputs(this._dropListRef);
this._handleEvents(this._dropListRef);
CdkDropList._dropLists.push(this);

if (_group) {
_group._items.add(this);
}
}

ngAfterContentInit() {
this._draggables.changes
.pipe(startWith(this._draggables), takeUntil(this._destroyed))
.subscribe((items: QueryList<CdkDrag>) => {
this._dropListRef.withItems(items.map(drag => drag._dragRef));
});
}

ngOnDestroy() {
const index = CdkDropList._dropLists.indexOf(this);
this._dropListRef.dispose();

if (index > -1) {
CdkDropList._dropLists.splice(index, 1);
Expand All @@ -170,6 +199,10 @@ export class CdkDropList<T = any> implements CdkDropListContainer, OnDestroy {
if (this._group) {
this._group._items.delete(this);
}

this._dropListRef.dispose();
this._destroyed.next();
this._destroyed.complete();
}

/** Starts dragging an item. */
Expand Down Expand Up @@ -253,6 +286,12 @@ export class CdkDropList<T = any> implements CdkDropListContainer, OnDestroy {

/** Syncs the inputs of the CdkDropList with the options of the underlying DropListRef. */
private _syncInputs(ref: DropListRef<CdkDropList>) {
if (this._dir) {
this._dir.change
.pipe(startWith(this._dir.value), takeUntil(this._destroyed))
.subscribe(value => ref.withDirection(value));
}

ref.beforeStarted.subscribe(() => {
const siblings = coerceArray(this.connectedTo).map(drop => {
return typeof drop === 'string' ?
Expand All @@ -270,8 +309,7 @@ export class CdkDropList<T = any> implements CdkDropListContainer, OnDestroy {
ref.lockAxis = this.lockAxis;
ref
.connectedTo(siblings.filter(drop => drop && drop !== this).map(list => list._dropListRef))
.withOrientation(this.orientation)
.withItems(this._draggables.map(drag => drag._dragRef));
.withOrientation(this.orientation);
});
}

Expand Down
4 changes: 4 additions & 0 deletions src/cdk/drag-drop/drag-drop-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {CdkDrag} from './directives/drag';
import {CdkDragHandle} from './directives/drag-handle';
import {CdkDragPreview} from './directives/drag-preview';
import {CdkDragPlaceholder} from './directives/drag-placeholder';
import {DragDrop} from './drag-drop';

@NgModule({
declarations: [
Expand All @@ -31,5 +32,8 @@ import {CdkDragPlaceholder} from './directives/drag-placeholder';
CdkDragPreview,
CdkDragPlaceholder,
],
providers: [
DragDrop,
]
})
export class DragDropModule {}
47 changes: 47 additions & 0 deletions src/cdk/drag-drop/drag-drop.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import {Component, ElementRef} from '@angular/core';
import {fakeAsync, TestBed, inject} from '@angular/core/testing';
import {DragDropModule} from './drag-drop-module';
import {DragDrop} from './drag-drop';
import {DragRef} from './drag-ref';
import {DropListRef} from './drop-list-ref';

describe('DragDrop', () => {
let service: DragDrop;

beforeEach(fakeAsync(() => {
TestBed.configureTestingModule({
declarations: [TestComponent],
imports: [DragDropModule],
});

TestBed.compileComponents();
}));

beforeEach(inject([DragDrop], (d: DragDrop) => {
service = d;
}));

it('should be able to attach a DragRef to a DOM node', () => {
const fixture = TestBed.createComponent(TestComponent);
fixture.detectChanges();
const ref = service.createDrag(fixture.componentInstance.elementRef);

expect(ref instanceof DragRef).toBe(true);
});

it('should be able to attach a DropListRef to a DOM node', () => {
const fixture = TestBed.createComponent(TestComponent);
fixture.detectChanges();
const ref = service.createDropList(fixture.componentInstance.elementRef);

expect(ref instanceof DropListRef).toBe(true);
});
});


@Component({
template: '<div></div>'
})
class TestComponent {
constructor(public elementRef: ElementRef<HTMLElement>) {}
}
52 changes: 52 additions & 0 deletions src/cdk/drag-drop/drag-drop.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* @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 {Injectable, Inject, NgZone, ElementRef} from '@angular/core';
import {DOCUMENT} from '@angular/common';
import {ViewportRuler} from '@angular/cdk/scrolling';
import {DragRef, DragRefConfig} from './drag-ref';
import {DropListRef} from './drop-list-ref';
import {DragDropRegistry} from './drag-drop-registry';

/** Default configuration to be used when creating a `DragRef`. */
const DEFAULT_CONFIG = {
dragStartThreshold: 5,
pointerDirectionChangeThreshold: 5
};

/**
* Service that allows for drag-and-drop functionality to be attached to DOM elements.
*/
@Injectable({providedIn: 'root'})
export class DragDrop {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add unit tests for consuming this service?

constructor(
@Inject(DOCUMENT) private _document: any,
private _ngZone: NgZone,
private _viewportRuler: ViewportRuler,
private _dragDropRegistry: DragDropRegistry<DragRef, DropListRef>) {}

/**
* Turns an element into a draggable item.
* @param element Element to which to attach the dragging functionality.
* @param config Object used to configure the dragging behavior.
*/
createDrag<T = any>(element: ElementRef<HTMLElement> | HTMLElement,
config: DragRefConfig = DEFAULT_CONFIG): DragRef<T> {

return new DragRef<T>(element, config, this._document, this._ngZone, this._viewportRuler,
this._dragDropRegistry);
}

/**
* Turns an element into a drop list.
* @param element Element to which to attach the drop list functionality.
*/
createDropList<T = any>(element: ElementRef<HTMLElement> | HTMLElement): DropListRef<T> {
return new DropListRef<T>(element, this._dragDropRegistry, this._document);
}
}
Loading