Skip to content

refactor(drag-drop): clean up DragRef public API #14654

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 15, 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
13 changes: 11 additions & 2 deletions src/cdk/drag-drop/directives/drag-handle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
* found in the LICENSE file at https://angular.io/license
*/

import {Directive, ElementRef, Inject, Optional, Input} from '@angular/core';
import {Directive, ElementRef, Inject, Optional, Input, OnDestroy} from '@angular/core';
import {coerceBooleanProperty} from '@angular/cdk/coercion';
import {Subject} from 'rxjs';
import {CDK_DRAG_PARENT} from '../drag-parent';
import {toggleNativeDragInteractions} from '../drag-styling';

Expand All @@ -18,15 +19,19 @@ import {toggleNativeDragInteractions} from '../drag-styling';
'class': 'cdk-drag-handle'
}
})
export class CdkDragHandle {
export class CdkDragHandle implements OnDestroy {
/** Closest parent draggable instance. */
_parentDrag: {} | undefined;

/** Emits when the state of the handle has changed. */
_stateChanges = new Subject<CdkDragHandle>();

/** Whether starting to drag through this handle is disabled. */
@Input('cdkDragHandleDisabled')
get disabled(): boolean { return this._disabled; }
set disabled(value: boolean) {
this._disabled = coerceBooleanProperty(value);
this._stateChanges.next(this);
}
private _disabled = false;

Expand All @@ -37,4 +42,8 @@ export class CdkDragHandle {
this._parentDrag = parentDrag;
toggleNativeDragInteractions(element.nativeElement, false);
}

ngOnDestroy() {
this._stateChanges.complete();
}
}
69 changes: 47 additions & 22 deletions src/cdk/drag-drop/directives/drag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ import {
SimpleChanges,
} from '@angular/core';
import {coerceBooleanProperty} from '@angular/cdk/coercion';
import {Observable, Subscription, Observer} from 'rxjs';
import {startWith, take, map} from 'rxjs/operators';
import {Observable, Observer, Subject, merge} from 'rxjs';
import {startWith, take, map, takeUntil, switchMap, tap} from 'rxjs/operators';
import {DragDropRegistry} from '../drag-drop-registry';
import {
CdkDragDrop,
Expand Down Expand Up @@ -73,8 +73,7 @@ export function CDK_DRAG_CONFIG_FACTORY(): DragRefConfig {
providers: [{provide: CDK_DRAG_PARENT, useExisting: CdkDrag}]
})
export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
/** Subscription to the stream that initializes the root element. */
private _rootElementInitSubscription = Subscription.EMPTY;
private _destroyed = new Subject<void>();

/** Reference to the underlying drag instance. */
_dragRef: DragRef<CdkDrag<T>>;
Expand Down Expand Up @@ -178,16 +177,7 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
this._config, this.dropContainer ? this.dropContainer._dropListRef : undefined,
this._dir);
ref.data = this;
ref.beforeStarted.subscribe(() => {
if (!ref.isDragging()) {
ref.disabled = this.disabled;
ref.lockAxis = this.lockAxis;
ref
.withBoundaryElement(this._getBoundaryElement())
.withPlaceholderTemplate(this._placeholderTemplate)
.withPreviewTemplate(this._previewTemplate);
}
});
this._syncInputs(ref);
this._proxyEvents(ref);
}

Expand All @@ -214,15 +204,32 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
// element to be in the proper place in the DOM. This is mostly relevant
// for draggable elements inside portals since they get stamped out in
// their original DOM position and then they get transferred to the portal.
this._rootElementInitSubscription = this._ngZone.onStable.asObservable()
.pipe(take(1))
this._ngZone.onStable.asObservable()
.pipe(take(1), takeUntil(this._destroyed))
.subscribe(() => {
this._updateRootElement();
this._handles.changes
.pipe(startWith(this._handles))
.subscribe((handleList: QueryList<CdkDragHandle>) => {
this._dragRef.withHandles(handleList.filter(handle => handle._parentDrag === this));
});

// Listen for any newly-added handles.
this._handles.changes.pipe(
startWith(this._handles),
// Sync the new handles with the DragRef.
tap((handles: QueryList<CdkDragHandle>) => {
const childHandleElements = handles
.filter(handle => handle._parentDrag === this)
.map(handle => handle.element);
this._dragRef.withHandles(childHandleElements);
}),
// Listen if the state of any of the handles changes.
switchMap((handles: QueryList<CdkDragHandle>) => {
return merge(...handles.map(item => item._stateChanges));
}),
takeUntil(this._destroyed)
).subscribe(handleInstance => {
// Enabled/disable the handle that changed in the DragRef.
const dragRef = this._dragRef;
const handle = handleInstance.element.nativeElement;
handleInstance.disabled ? dragRef.disableHandle(handle) : dragRef.enableHandle(handle);
});
});
}

Expand All @@ -237,7 +244,8 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
}

ngOnDestroy() {
this._rootElementInitSubscription.unsubscribe();
this._destroyed.next();
this._destroyed.complete();
this._dragRef.dispose();
}

Expand All @@ -261,6 +269,23 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
return selector ? getClosestMatchingAncestor(this.element.nativeElement, selector) : null;
}

/** Syncs the inputs of the CdkDrag with the options of the underlying DragRef. */
private _syncInputs(ref: DragRef<CdkDrag<T>>) {
ref.beforeStarted.subscribe(() => {
if (!ref.isDragging()) {
const {_placeholderTemplate: placeholder, _previewTemplate: preview} = this;

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);
}
});
}

/**
* Proxies the events from a DragRef to events that
* match the interfaces of the CdkDrag outputs.
Expand Down
105 changes: 58 additions & 47 deletions src/cdk/drag-drop/drag-ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {EmbeddedViewRef, ElementRef, NgZone, ViewContainerRef, TemplateRef} from
import {ViewportRuler} from '@angular/cdk/scrolling';
import {Directionality} from '@angular/cdk/bidi';
import {normalizePassiveListenerOptions} from '@angular/cdk/platform';
import {coerceBooleanProperty} from '@angular/cdk/coercion';
import {coerceBooleanProperty, coerceElement} from '@angular/cdk/coercion';
import {Subscription, Subject, Observable, Observer} from 'rxjs';
import {DropListRefInternal as DropListRef} from './drop-list-ref';
import {DragDropRegistry} from './drag-drop-registry';
Expand Down Expand Up @@ -46,24 +46,10 @@ const activeEventListenerOptions = normalizePassiveListenerOptions({passive: fal
*/
const MOUSE_EVENT_IGNORE_TIME = 800;

/**
* Template that can be used to create a drag helper element (e.g. a preview or a placeholder).
*/
interface DragHelperTemplate<T = any> {
templateRef: TemplateRef<T>;
data: T;
}

interface DragHandle {
element: ElementRef<HTMLElement>;
disabled: boolean;
}

// TODO(crisbeto): add auto-scrolling functionality.
// TODO(crisbeto): add an API for moving a draggable up/down the
// list programmatically. Useful for keyboard controls.


/**
* Internal compile-time-only representation of a `DragRef`.
* Used to avoid circular import issues between the `DragRef` and the `DropListRef`.
Expand Down Expand Up @@ -187,13 +173,16 @@ export class DragRef<T = any> {
private _boundaryRect?: ClientRect;

/** Element that will be used as a template to create the draggable item's preview. */
private _previewTemplate: DragHelperTemplate | null;
private _previewTemplate?: {template: TemplateRef<any> | null, context?: any};

/** Template for placeholder element rendered to show where a draggable would be dropped. */
private _placeholderTemplate: DragHelperTemplate | null;
private _placeholderTemplate?: {template: TemplateRef<any> | null, context?: any};

/** Elements that can be used to drag the draggable item. */
private _handles: DragHandle[] = [];
private _handles: HTMLElement[] = [];

/** Registered handles that are currently disabled. */
private _disabledHandles = new Set<HTMLElement>();

/** Axis along which dragging is locked. */
lockAxis: 'x' | 'y';
Expand Down Expand Up @@ -292,36 +281,40 @@ export class DragRef<T = any> {
}

/** Registers the handles that can be used to drag the element. */
withHandles(handles: DragHandle[]): this {
// TODO(crisbeto): have this accept HTMLElement[] | ElementRef<HTMLElement>[]
this._handles = handles;
handles.forEach(handle => toggleNativeDragInteractions(handle.element.nativeElement, false));
withHandles(handles: (HTMLElement | ElementRef<HTMLElement>)[]): this {
this._handles = handles.map(handle => coerceElement(handle));
this._handles.forEach(handle => toggleNativeDragInteractions(handle, false));
this._toggleNativeDragInteractions();
return this;
}

/** Registers the template that should be used for the drag preview. */
withPreviewTemplate(template: DragHelperTemplate | null): this {
// TODO(crisbeto): have this accept a TemplateRef
this._previewTemplate = template;
/**
* Registers the template that should be used for the drag preview.
* @param template Template that from which to stamp out the preview.
* @param context Variables to add to the template's context.
*/
withPreviewTemplate(template: TemplateRef<any> | null, context?: any): this {
this._previewTemplate = {template, context};
return this;
}

/** Registers the template that should be used for the drag placeholder. */
withPlaceholderTemplate(template: DragHelperTemplate | null): this {
// TODO(crisbeto): have this accept a TemplateRef
this._placeholderTemplate = template;
/**
* Registers the template that should be used for the drag placeholder.
* @param template Template that from which to stamp out the placeholder.
* @param context Variables to add to the template's context.
*/
withPlaceholderTemplate(template: TemplateRef<any> | null, context?: any): this {
this._placeholderTemplate = {template, context};
return this;
}


/**
* Sets an alternate drag root element. The root element is the element that will be moved as
* the user is dragging. Passing an alternate root element is useful when trying to enable
* dragging on an element that you might not have access to.
*/
withRootElement(rootElement: ElementRef<HTMLElement> | HTMLElement): this {
const element = rootElement instanceof ElementRef ? rootElement.nativeElement : rootElement;
const element = coerceElement(rootElement);

if (element !== this._rootElement) {
if (this._rootElement) {
Expand All @@ -340,8 +333,7 @@ export class DragRef<T = any> {
* Element to which the draggable's position will be constrained.
*/
withBoundaryElement(boundaryElement: ElementRef<HTMLElement> | HTMLElement | null): this {
this._boundaryElement = boundaryElement instanceof ElementRef ?
boundaryElement.nativeElement : boundaryElement;
this._boundaryElement = boundaryElement ? coerceElement(boundaryElement) : null;
return this;
}

Expand Down Expand Up @@ -370,6 +362,7 @@ export class DragRef<T = any> {
this.dropped.complete();
this._moveEvents.complete();
this._handles = [];
this._disabledHandles.clear();
this._boundaryElement = this._rootElement = this._placeholderTemplate =
this._previewTemplate = this._nextSibling = null!;
}
Expand All @@ -386,6 +379,24 @@ export class DragRef<T = any> {
this._passiveTransform = {x: 0, y: 0};
}

/**
* Sets a handle as disabled. While a handle is disabled, it'll capture and interrupt dragging.
* @param handle Handle element that should be disabled.
*/
disableHandle(handle: HTMLElement) {
Copy link
Member Author

Choose a reason for hiding this comment

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

Note: I'm not too happy with having to add this method and enableHandle below, since it means having to do this above, however we need them in order to keep backwards compatibility. They allow us to handle the case where the user starts to drag from a disabled handle, in which case the dragging sequence should be interrupted. In the future we can reconsider whether supporting that case is worth it.

if (this._handles.indexOf(handle) > -1) {
this._disabledHandles.add(handle);
}
}

/**
* Enables a handle, if it has been disabled.
* @param handle Handle element to be enabled.
*/
enableHandle(handle: HTMLElement) {
this._disabledHandles.delete(handle);
}

/** Unsubscribes from the global subscriptions. */
private _removeSubscriptions() {
this._pointerMoveSubscription.unsubscribe();
Expand Down Expand Up @@ -418,21 +429,19 @@ export class DragRef<T = any> {
this._placeholder = this._placeholderRef = null!;
}


/** Handler for the `mousedown`/`touchstart` events. */
private _pointerDown = (event: MouseEvent | TouchEvent) => {
this.beforeStarted.next();

// Delegate the event based on whether it started from a handle or the element itself.
if (this._handles.length) {
const targetHandle = this._handles.find(handle => {
const element = handle.element.nativeElement;
const target = event.target;
return !!target && (target === element || element.contains(target as HTMLElement));
return !!target && (target === handle || handle.contains(target as HTMLElement));
});

if (targetHandle && !targetHandle.disabled && !this.disabled) {
this._initializeDragSequence(targetHandle.element.nativeElement, event);
if (targetHandle && !this._disabledHandles.has(targetHandle) && !this.disabled) {
this._initializeDragSequence(targetHandle, event);
}
} else if (!this.disabled) {
this._initializeDragSequence(this._rootElement, event);
Expand Down Expand Up @@ -638,7 +647,8 @@ export class DragRef<T = any> {

// 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._pickupPositionInElement = this._previewTemplate && this._previewTemplate.template ?
{x: 0, y: 0} :
this._getPointerPositionInElement(referenceElement, event);
const pointerPosition = this._pickupPositionOnPage = this._getPointerPositionOnPage(event);
this._pointerDirectionDelta = {x: 0, y: 0};
Expand Down Expand Up @@ -724,12 +734,12 @@ export class DragRef<T = any> {
* and will be used as a preview of the element that is being dragged.
*/
private _createPreviewElement(): HTMLElement {
const previewTemplate = this._previewTemplate;
let preview: HTMLElement;

if (this._previewTemplate) {
const viewRef = this._viewContainerRef.createEmbeddedView(this._previewTemplate.templateRef,
this._previewTemplate.data);

if (previewTemplate && previewTemplate.template) {
const viewRef = this._viewContainerRef.createEmbeddedView(previewTemplate.template,
previewTemplate.context);
preview = viewRef.rootNodes[0];
this._previewRef = viewRef;
preview.style.transform =
Expand Down Expand Up @@ -811,12 +821,13 @@ export class DragRef<T = any> {

/** Creates an element that will be shown instead of the current element while dragging. */
private _createPlaceholderElement(): HTMLElement {
const placeholderTemplate = this._placeholderTemplate;
let placeholder: HTMLElement;

if (this._placeholderTemplate) {
if (placeholderTemplate && placeholderTemplate.template) {
this._placeholderRef = this._viewContainerRef.createEmbeddedView(
this._placeholderTemplate.templateRef,
this._placeholderTemplate.data
placeholderTemplate.template,
placeholderTemplate.context
);
placeholder = this._placeholderRef.rootNodes[0];
} else {
Expand Down
5 changes: 1 addition & 4 deletions src/cdk/drag-drop/drop-list-ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,10 +147,7 @@ export class DropListRef<T = any> {
*/
private _previousSwap = {drag: null as DragRef | null, delta: 0};

/**
* Draggable items in the container.
* TODO(crisbeto): support arrays.
*/
/** Draggable items in the container. */
private _draggables: DragRef[];

private _siblings: DropListRef[] = [];
Expand Down
Loading