@angular/cdk
Version:
Angular Material Component Development Kit
1,223 lines (1,216 loc) • 118 kB
JavaScript
/**
* @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 { normalizePassiveListenerOptions } from '@angular/cdk/platform';
import { coerceBooleanProperty, coerceElement, coerceArray } from '@angular/cdk/coercion';
import { Subscription, Subject, Observable, merge } from 'rxjs';
import { ElementRef, Injectable, NgZone, Inject, InjectionToken, NgModule, ContentChildren, EventEmitter, forwardRef, Input, Output, Optional, Directive, ChangeDetectorRef, SkipSelf, ContentChild, ViewContainerRef, TemplateRef, defineInjectable, inject } from '@angular/core';
import { DOCUMENT } from '@angular/common';
import { ViewportRuler } from '@angular/cdk/scrolling';
import { Directionality } from '@angular/cdk/bidi';
import { startWith, take, map, takeUntil, switchMap, tap } from 'rxjs/operators';
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
/**
* Shallow-extends a stylesheet object with another stylesheet object.
* \@docs-private
* @param {?} dest
* @param {?} source
* @return {?}
*/
function extendStyles(dest, source) {
for (let key in source) {
if (source.hasOwnProperty(key)) {
dest[(/** @type {?} */ (key))] = source[(/** @type {?} */ (key))];
}
}
return dest;
}
/**
* Toggles whether the native drag interactions should be enabled for an element.
* \@docs-private
* @param {?} element Element on which to toggle the drag interactions.
* @param {?} enable Whether the drag interactions should be enabled.
* @return {?}
*/
function toggleNativeDragInteractions(element, enable) {
/** @type {?} */
const userSelect = enable ? '' : 'none';
extendStyles(element.style, {
touchAction: enable ? '' : 'none',
webkitUserDrag: enable ? '' : 'none',
webkitTapHighlightColor: enable ? '' : 'transparent',
userSelect: userSelect,
msUserSelect: userSelect,
webkitUserSelect: userSelect,
MozUserSelect: userSelect
});
}
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
/**
* Parses a CSS time value to milliseconds.
* @param {?} value
* @return {?}
*/
function parseCssTimeUnitsToMs(value) {
// Some browsers will return it in seconds, whereas others will return milliseconds.
/** @type {?} */
const multiplier = value.toLowerCase().indexOf('ms') > -1 ? 1 : 1000;
return parseFloat(value) * multiplier;
}
/**
* Gets the transform transition duration, including the delay, of an element in milliseconds.
* @param {?} element
* @return {?}
*/
function getTransformTransitionDurationInMs(element) {
/** @type {?} */
const computedStyle = getComputedStyle(element);
/** @type {?} */
const transitionedProperties = parseCssPropertyValue(computedStyle, 'transition-property');
/** @type {?} */
const property = transitionedProperties.find(prop => prop === 'transform' || prop === 'all');
// If there's no transition for `all` or `transform`, we shouldn't do anything.
if (!property) {
return 0;
}
// Get the index of the property that we're interested in and match
// it up to the same index in `transition-delay` and `transition-duration`.
/** @type {?} */
const propertyIndex = transitionedProperties.indexOf(property);
/** @type {?} */
const rawDurations = parseCssPropertyValue(computedStyle, 'transition-duration');
/** @type {?} */
const rawDelays = parseCssPropertyValue(computedStyle, 'transition-delay');
return parseCssTimeUnitsToMs(rawDurations[propertyIndex]) +
parseCssTimeUnitsToMs(rawDelays[propertyIndex]);
}
/**
* Parses out multiple values from a computed style into an array.
* @param {?} computedStyle
* @param {?} name
* @return {?}
*/
function parseCssPropertyValue(computedStyle, name) {
/** @type {?} */
const value = computedStyle.getPropertyValue(name);
return value.split(',').map(part => part.trim());
}
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
/**
* Options that can be used to bind a passive event listener.
* @type {?}
*/
const passiveEventListenerOptions = normalizePassiveListenerOptions({ passive: true });
/**
* Options that can be used to bind an active event listener.
* @type {?}
*/
const activeEventListenerOptions = normalizePassiveListenerOptions({ passive: false });
/**
* Time in milliseconds for which to ignore mouse events, after
* receiving a touch event. Used to avoid doing double work for
* touch devices where the browser fires fake mouse events, in
* addition to touch events.
* @type {?}
*/
const MOUSE_EVENT_IGNORE_TIME = 800;
/**
* Reference to a draggable item. Used to manipulate or dispose of the item.
* \@docs-private
* @template T
*/
class DragRef {
/**
* @param {?} element
* @param {?} _config
* @param {?} _document
* @param {?} _ngZone
* @param {?} _viewportRuler
* @param {?} _dragDropRegistry
*/
constructor(element, _config, _document, _ngZone, _viewportRuler, _dragDropRegistry) {
this._config = _config;
this._document = _document;
this._ngZone = _ngZone;
this._viewportRuler = _viewportRuler;
this._dragDropRegistry = _dragDropRegistry;
/**
* 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`.
*/
this._passiveTransform = { x: 0, y: 0 };
/**
* CSS `transform` that is applied to the element while it's being dragged.
*/
this._activeTransform = { x: 0, y: 0 };
/**
* Emits when the item is being moved.
*/
this._moveEvents = new Subject();
/**
* Amount of subscriptions to the move event. Used to avoid
* hitting the zone if the consumer didn't subscribe to it.
*/
this._moveEventSubscriptions = 0;
/**
* Subscription to pointer movement events.
*/
this._pointerMoveSubscription = Subscription.EMPTY;
/**
* Subscription to the event that is dispatched when the user lifts their pointer.
*/
this._pointerUpSubscription = Subscription.EMPTY;
/**
* Cached reference to the boundary element.
*/
this._boundaryElement = null;
/**
* Whether the native dragging interactions have been enabled on the root element.
*/
this._nativeInteractionsEnabled = true;
/**
* Elements that can be used to drag the draggable item.
*/
this._handles = [];
/**
* Registered handles that are currently disabled.
*/
this._disabledHandles = new Set();
/**
* Layout direction of the item.
*/
this._direction = 'ltr';
this._disabled = false;
/**
* Emits as the drag sequence is being prepared.
*/
this.beforeStarted = new Subject();
/**
* Emits when the user starts dragging the item.
*/
this.started = new Subject();
/**
* Emits when the user has released a drag item, before any animations have started.
*/
this.released = new Subject();
/**
* Emits when the user stops dragging an item in the container.
*/
this.ended = new Subject();
/**
* Emits when the user has moved the item into a new container.
*/
this.entered = new Subject();
/**
* Emits when the user removes the item its container by dragging it into another container.
*/
this.exited = new Subject();
/**
* Emits when the user drops the item inside a container.
*/
this.dropped = new Subject();
/**
* Emits as the user is dragging the item. Use with caution,
* because this event will fire for every pixel that the user has dragged.
*/
this.moved = new Observable((observer) => {
/** @type {?} */
const subscription = this._moveEvents.subscribe(observer);
this._moveEventSubscriptions++;
return () => {
subscription.unsubscribe();
this._moveEventSubscriptions--;
};
});
/**
* Handler for the `mousedown`/`touchstart` events.
*/
this._pointerDown = (event) => {
this.beforeStarted.next();
// Delegate the event based on whether it started from a handle or the element itself.
if (this._handles.length) {
/** @type {?} */
const targetHandle = this._handles.find(handle => {
/** @type {?} */
const target = event.target;
return !!target && (target === handle || handle.contains((/** @type {?} */ (target))));
});
if (targetHandle && !this._disabledHandles.has(targetHandle) && !this.disabled) {
this._initializeDragSequence(targetHandle, event);
}
}
else if (!this.disabled) {
this._initializeDragSequence(this._rootElement, event);
}
};
/**
* Handler that is invoked when the user moves their pointer after they've initiated a drag.
*/
this._pointerMove = (event) => {
if (!this._hasStartedDragging) {
/** @type {?} */
const pointerPosition = this._getPointerPositionOnPage(event);
/** @type {?} */
const distanceX = Math.abs(pointerPosition.x - this._pickupPositionOnPage.x);
/** @type {?} */
const distanceY = Math.abs(pointerPosition.y - this._pickupPositionOnPage.y);
// Only start dragging after the user has moved more than the minimum distance in either
// direction. Note that this is preferrable over doing something like `skip(minimumDistance)`
// in the `pointerMove` subscription, because we're not guaranteed to have one move event
// per pixel of movement (e.g. if the user moves their pointer quickly).
if (distanceX + distanceY >= this._config.dragStartThreshold) {
this._hasStartedDragging = true;
this._ngZone.run(() => this._startDragSequence(event));
}
return;
}
// We only need the preview dimensions if we have a boundary element.
if (this._boundaryElement) {
// Cache the preview element rect if we haven't cached it already or if
// we cached it too early before the element dimensions were computed.
if (!this._previewRect || (!this._previewRect.width && !this._previewRect.height)) {
this._previewRect = (this._preview || this._rootElement).getBoundingClientRect();
}
}
/** @type {?} */
const constrainedPointerPosition = this._getConstrainedPointerPosition(event);
this._hasMoved = true;
event.preventDefault();
this._updatePointerDirectionDelta(constrainedPointerPosition);
if (this._dropContainer) {
this._updateActiveDropContainer(constrainedPointerPosition);
}
else {
/** @type {?} */
const activeTransform = this._activeTransform;
activeTransform.x =
constrainedPointerPosition.x - this._pickupPositionOnPage.x + this._passiveTransform.x;
activeTransform.y =
constrainedPointerPosition.y - this._pickupPositionOnPage.y + this._passiveTransform.y;
/** @type {?} */
const transform = getTransform(activeTransform.x, activeTransform.y);
// Preserve the previous `transform` value, if there was one. Note that we apply our own
// transform before the user's, because things like rotation can affect which direction
// the element will be translated towards.
this._rootElement.style.transform = this._initialTransform ?
transform + ' ' + this._initialTransform : transform;
// Apply transform as attribute if dragging and svg element to work for IE
if (typeof SVGElement !== 'undefined' && this._rootElement instanceof SVGElement) {
/** @type {?} */
const appliedTransform = `translate(${activeTransform.x} ${activeTransform.y})`;
this._rootElement.setAttribute('transform', appliedTransform);
}
}
// Since this event gets fired for every pixel while dragging, we only
// want to fire it if the consumer opted into it. Also we have to
// re-enter the zone because we run all of the events on the outside.
if (this._moveEventSubscriptions > 0) {
this._ngZone.run(() => {
this._moveEvents.next({
source: this,
pointerPosition: constrainedPointerPosition,
event,
delta: this._pointerDirectionDelta
});
});
}
};
/**
* Handler that is invoked when the user lifts their pointer up, after initiating a drag.
*/
this._pointerUp = (event) => {
// Note that here we use `isDragging` from the service, rather than from `this`.
// The difference is that the one from the service reflects whether a dragging sequence
// has been initiated, whereas the one on `this` includes whether the user has passed
// the minimum dragging threshold.
if (!this._dragDropRegistry.isDragging(this)) {
return;
}
this._removeSubscriptions();
this._dragDropRegistry.stopDragging(this);
if (this._handles) {
this._rootElement.style.webkitTapHighlightColor = this._rootElementTapHighlight;
}
if (!this._hasStartedDragging) {
return;
}
this.released.next({ source: this });
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.next({ source: this }));
this._dragDropRegistry.stopDragging(this);
return;
}
this._animatePreviewToPlaceholder().then(() => {
this._cleanupDragArtifacts(event);
this._dragDropRegistry.stopDragging(this);
});
};
this.withRootElement(element);
_dragDropRegistry.registerDragItem(this);
}
/**
* Whether starting to drag this element is disabled.
* @return {?}
*/
get disabled() {
return this._disabled || !!(this._dropContainer && this._dropContainer.disabled);
}
/**
* @param {?} value
* @return {?}
*/
set disabled(value) {
/** @type {?} */
const newValue = coerceBooleanProperty(value);
if (newValue !== this._disabled) {
this._disabled = newValue;
this._toggleNativeDragInteractions();
}
}
/**
* Returns the element that is being used as a placeholder
* while the current element is being dragged.
* @return {?}
*/
getPlaceholderElement() {
return this._placeholder;
}
/**
* Returns the root draggable element.
* @return {?}
*/
getRootElement() {
return this._rootElement;
}
/**
* Registers the handles that can be used to drag the element.
* @template THIS
* @this {THIS}
* @param {?} handles
* @return {THIS}
*/
withHandles(handles) {
(/** @type {?} */ (this))._handles = handles.map(handle => coerceElement(handle));
(/** @type {?} */ (this))._handles.forEach(handle => toggleNativeDragInteractions(handle, false));
(/** @type {?} */ (this))._toggleNativeDragInteractions();
return (/** @type {?} */ (this));
}
/**
* Registers the template that should be used for the drag preview.
* @template THIS
* @this {THIS}
* @param {?} template Template that from which to stamp out the preview.
* @return {THIS}
*/
withPreviewTemplate(template) {
(/** @type {?} */ (this))._previewTemplate = template;
return (/** @type {?} */ (this));
}
/**
* Registers the template that should be used for the drag placeholder.
* @template THIS
* @this {THIS}
* @param {?} template Template that from which to stamp out the placeholder.
* @return {THIS}
*/
withPlaceholderTemplate(template) {
(/** @type {?} */ (this))._placeholderTemplate = template;
return (/** @type {?} */ (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.
* @template THIS
* @this {THIS}
* @param {?} rootElement
* @return {THIS}
*/
withRootElement(rootElement) {
/** @type {?} */
const element = coerceElement(rootElement);
if (element !== (/** @type {?} */ (this))._rootElement) {
if ((/** @type {?} */ (this))._rootElement) {
(/** @type {?} */ (this))._removeRootElementListeners((/** @type {?} */ (this))._rootElement);
}
element.addEventListener('mousedown', (/** @type {?} */ (this))._pointerDown, activeEventListenerOptions);
element.addEventListener('touchstart', (/** @type {?} */ (this))._pointerDown, passiveEventListenerOptions);
(/** @type {?} */ (this))._initialTransform = undefined;
(/** @type {?} */ (this))._rootElement = element;
}
return (/** @type {?} */ (this));
}
/**
* Element to which the draggable's position will be constrained.
* @template THIS
* @this {THIS}
* @param {?} boundaryElement
* @return {THIS}
*/
withBoundaryElement(boundaryElement) {
(/** @type {?} */ (this))._boundaryElement = boundaryElement ? coerceElement(boundaryElement) : null;
return (/** @type {?} */ (this));
}
/**
* Removes the dragging functionality from the DOM element.
* @return {?}
*/
dispose() {
this._removeRootElementListeners(this._rootElement);
// Do this check before removing from the registry since it'll
// stop being considered as dragged once it is removed.
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.
removeElement(this._rootElement);
}
this._destroyPreview();
this._destroyPlaceholder();
this._dragDropRegistry.removeDragItem(this);
this._removeSubscriptions();
this.beforeStarted.complete();
this.started.complete();
this.released.complete();
this.ended.complete();
this.entered.complete();
this.exited.complete();
this.dropped.complete();
this._moveEvents.complete();
this._handles = [];
this._disabledHandles.clear();
this._dropContainer = undefined;
this._boundaryElement = this._rootElement = this._placeholderTemplate =
this._previewTemplate = this._nextSibling = (/** @type {?} */ (null));
}
/**
* Checks whether the element is currently being dragged.
* @return {?}
*/
isDragging() {
return this._hasStartedDragging && this._dragDropRegistry.isDragging(this);
}
/**
* Resets a standalone drag item to its initial position.
* @return {?}
*/
reset() {
this._rootElement.style.transform = this._initialTransform || '';
this._activeTransform = { x: 0, y: 0 };
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.
* @return {?}
*/
disableHandle(handle) {
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.
* @return {?}
*/
enableHandle(handle) {
this._disabledHandles.delete(handle);
}
/**
* Sets the layout direction of the draggable item.
* @template THIS
* @this {THIS}
* @param {?} direction
* @return {THIS}
*/
withDirection(direction) {
(/** @type {?} */ (this))._direction = direction;
return (/** @type {?} */ (this));
}
/**
* Sets the container that the item is part of.
* @param {?} container
* @return {?}
*/
_withDropContainer(container) {
this._dropContainer = container;
}
/**
* Unsubscribes from the global subscriptions.
* @private
* @return {?}
*/
_removeSubscriptions() {
this._pointerMoveSubscription.unsubscribe();
this._pointerUpSubscription.unsubscribe();
}
/**
* Destroys the preview element and its ViewRef.
* @private
* @return {?}
*/
_destroyPreview() {
if (this._preview) {
removeElement(this._preview);
}
if (this._previewRef) {
this._previewRef.destroy();
}
this._preview = this._previewRef = (/** @type {?} */ (null));
}
/**
* Destroys the placeholder element and its ViewRef.
* @private
* @return {?}
*/
_destroyPlaceholder() {
if (this._placeholder) {
removeElement(this._placeholder);
}
if (this._placeholderRef) {
this._placeholderRef.destroy();
}
this._placeholder = this._placeholderRef = (/** @type {?} */ (null));
}
/**
* Starts the dragging sequence.
* @private
* @param {?} event
* @return {?}
*/
_startDragSequence(event) {
// Emit the event on the item before the one on the container.
this.started.next({ source: this });
if (isTouchEvent(event)) {
this._lastTouchEventTime = Date.now();
}
if (this._dropContainer) {
/** @type {?} */
const element = this._rootElement;
// Grab the `nextSibling` before the preview and placeholder
// have been created so we don't get the preview by accident.
this._nextSibling = element.nextSibling;
/** @type {?} */
const preview = this._preview = this._createPreviewElement();
/** @type {?} */
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((/** @type {?} */ (element.parentNode)).replaceChild(placeholder, element));
this._document.body.appendChild(preview);
this._dropContainer.start();
}
}
/**
* Sets up the different variables and subscriptions
* that will be necessary for the dragging sequence.
* @private
* @param {?} referenceElement Element that started the drag sequence.
* @param {?} event Browser event object that started the sequence.
* @return {?}
*/
_initializeDragSequence(referenceElement, event) {
// Always stop propagation for the event that initializes
// the dragging sequence, in order to prevent it from potentially
// starting another sequence for a draggable parent somewhere up the DOM tree.
event.stopPropagation();
/** @type {?} */
const isDragging = this.isDragging();
/** @type {?} */
const isTouchSequence = isTouchEvent(event);
/** @type {?} */
const isAuxiliaryMouseButton = !isTouchSequence && ((/** @type {?} */ (event))).button !== 0;
/** @type {?} */
const rootElement = this._rootElement;
/** @type {?} */
const isSyntheticEvent = !isTouchSequence && this._lastTouchEventTime &&
this._lastTouchEventTime + MOUSE_EVENT_IGNORE_TIME > Date.now();
// If the event started from an element with the native HTML drag&drop, it'll interfere
// with our own dragging (e.g. `img` tags do it by default). Prevent the default action
// to stop it from happening. Note that preventing on `dragstart` also seems to work, but
// it's flaky and it fails if the user drags it away quickly. Also note that we only want
// to do this for `mousedown` since doing the same for `touchstart` will stop any `click`
// events from firing on touch devices.
if (event.target && ((/** @type {?} */ (event.target))).draggable && event.type === 'mousedown') {
event.preventDefault();
}
// Abort if the user is already dragging or is using a mouse button other than the primary one.
if (isDragging || isAuxiliaryMouseButton || isSyntheticEvent) {
return;
}
// Cache the previous transform amount only after the first drag sequence, because
// we don't want our own transforms to stack on top of each other.
if (this._initialTransform == null) {
this._initialTransform = this._rootElement.style.transform || '';
}
// If we've got handles, we need to disable the tap highlight on the entire root element,
// otherwise iOS will still add it, even though all the drag interactions on the handle
// are disabled.
if (this._handles.length) {
this._rootElementTapHighlight = rootElement.style.webkitTapHighlightColor;
rootElement.style.webkitTapHighlightColor = 'transparent';
}
this._toggleNativeDragInteractions();
this._hasStartedDragging = this._hasMoved = false;
this._initialContainer = (/** @type {?} */ (this._dropContainer));
this._pointerMoveSubscription = this._dragDropRegistry.pointerMove.subscribe(this._pointerMove);
this._pointerUpSubscription = this._dragDropRegistry.pointerUp.subscribe(this._pointerUp);
this._scrollPosition = this._viewportRuler.getViewportScrollPosition();
if (this._boundaryElement) {
this._boundaryRect = this._boundaryElement.getBoundingClientRect();
}
// 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 && this._previewTemplate.template ?
{ x: 0, y: 0 } :
this._getPointerPositionInElement(referenceElement, event);
/** @type {?} */
const pointerPosition = this._pickupPositionOnPage = this._getPointerPositionOnPage(event);
this._pointerDirectionDelta = { x: 0, y: 0 };
this._pointerPositionAtLastDirectionChange = { x: pointerPosition.x, y: pointerPosition.y };
this._dragDropRegistry.startDragging(this, event);
}
/**
* Cleans up the DOM artifacts that were added to facilitate the element being dragged.
* @private
* @param {?} event
* @return {?}
*/
_cleanupDragArtifacts(event) {
// Restore the element's visibility and insert it at its old position in the DOM.
// It's important that we maintain the position, because moving the element around in the DOM
// can throw off `NgFor` which does smart diffing and re-creates elements only when necessary,
// while moving the existing elements in all other cases.
this._rootElement.style.display = '';
if (this._nextSibling) {
(/** @type {?} */ (this._nextSibling.parentNode)).insertBefore(this._rootElement, this._nextSibling);
}
else {
this._initialContainer.element.appendChild(this._rootElement);
}
this._destroyPreview();
this._destroyPlaceholder();
this._boundaryRect = this._previewRect = undefined;
// Re-enter the NgZone since we bound `document` events on the outside.
this._ngZone.run(() => {
/** @type {?} */
const container = (/** @type {?} */ (this._dropContainer));
/** @type {?} */
const currentIndex = container.getItemIndex(this);
const { x, y } = this._getPointerPositionOnPage(event);
/** @type {?} */
const isPointerOverContainer = container._isOverContainer(x, y);
this.ended.next({ source: this });
this.dropped.next({
item: this,
currentIndex,
previousIndex: this._initialContainer.getItemIndex(this),
container: container,
previousContainer: this._initialContainer,
isPointerOverContainer
});
container.drop(this, currentIndex, this._initialContainer, isPointerOverContainer);
this._dropContainer = 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
* @param {?} __0
* @return {?}
*/
_updateActiveDropContainer({ x, y }) {
// Drop container that draggable has been moved into.
/** @type {?} */
let newContainer = (/** @type {?} */ (this._dropContainer))._getSiblingContainerFromPosition(this, x, y) ||
this._initialContainer._getSiblingContainerFromPosition(this, x, y);
// If we couldn't find a new container to move the item into, and the item has left it's
// initial container, check whether the it's over the initial container. This handles the
// case where two containers are connected one way and the user tries to undo dragging an
// item into a new container.
if (!newContainer && this._dropContainer !== this._initialContainer &&
this._initialContainer._isOverContainer(x, y)) {
newContainer = this._initialContainer;
}
if (newContainer && newContainer !== this._dropContainer) {
this._ngZone.run(() => {
// Notify the old container that the item has left.
this.exited.next({ item: this, container: (/** @type {?} */ (this._dropContainer)) });
(/** @type {?} */ (this._dropContainer)).exit(this);
// Notify the new container that the item has entered.
this.entered.next({ item: this, container: (/** @type {?} */ (newContainer)) });
this._dropContainer = (/** @type {?} */ (newContainer));
this._dropContainer.enter(this, x, y);
});
}
(/** @type {?} */ (this._dropContainer))._sortItem(this, x, y, this._pointerDirectionDelta);
this._preview.style.transform =
getTransform(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
* @return {?}
*/
_createPreviewElement() {
/** @type {?} */
const previewConfig = this._previewTemplate;
/** @type {?} */
const previewTemplate = previewConfig ? previewConfig.template : null;
/** @type {?} */
let preview;
if (previewTemplate) {
/** @type {?} */
const viewRef = (/** @type {?} */ (previewConfig)).viewContainer.createEmbeddedView(previewTemplate, (/** @type {?} */ (previewConfig)).context);
preview = viewRef.rootNodes[0];
this._previewRef = viewRef;
preview.style.transform =
getTransform(this._pickupPositionOnPage.x, this._pickupPositionOnPage.y);
}
else {
/** @type {?} */
const element = this._rootElement;
/** @type {?} */
const elementRect = element.getBoundingClientRect();
preview = deepCloneNode(element);
preview.style.width = `${elementRect.width}px`;
preview.style.height = `${elementRect.height}px`;
preview.style.transform = getTransform(elementRect.left, elementRect.top);
}
extendStyles(preview.style, {
// It's important that we disable the pointer events on the preview, because
// it can throw off the `document.elementFromPoint` calls in the `CdkDropList`.
pointerEvents: 'none',
position: 'fixed',
top: '0',
left: '0',
zIndex: '1000'
});
toggleNativeDragInteractions(preview, false);
preview.classList.add('cdk-drag-preview');
preview.setAttribute('dir', this._direction);
return preview;
}
/**
* Animates the preview element from its current position to the location of the drop placeholder.
* @private
* @return {?} Promise that resolves when the animation completes.
*/
_animatePreviewToPlaceholder() {
// If the user hasn't moved yet, the transitionend event won't fire.
if (!this._hasMoved) {
return Promise.resolve();
}
/** @type {?} */
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._preview.style.transform = getTransform(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.
/** @type {?} */
const duration = getTransformTransitionDurationInMs(this._preview);
if (duration === 0) {
return Promise.resolve();
}
return this._ngZone.runOutsideAngular(() => {
return new Promise(resolve => {
/** @type {?} */
const handler = (/** @type {?} */ (((event) => {
if (!event || (event.target === this._preview && event.propertyName === 'transform')) {
this._preview.removeEventListener('transitionend', handler);
resolve();
clearTimeout(timeout);
}
})));
// If a transition is short enough, the browser might not fire the `transitionend` event.
// Since we know how long it's supposed to take, add a timeout with a 50% buffer that'll
// fire if the transition hasn't completed when it was supposed to.
/** @type {?} */
const timeout = setTimeout((/** @type {?} */ (handler)), duration * 1.5);
this._preview.addEventListener('transitionend', handler);
});
});
}
/**
* Creates an element that will be shown instead of the current element while dragging.
* @private
* @return {?}
*/
_createPlaceholderElement() {
/** @type {?} */
const placeholderConfig = this._placeholderTemplate;
/** @type {?} */
const placeholderTemplate = placeholderConfig ? placeholderConfig.template : null;
/** @type {?} */
let placeholder;
if (placeholderTemplate) {
this._placeholderRef = (/** @type {?} */ (placeholderConfig)).viewContainer.createEmbeddedView(placeholderTemplate, (/** @type {?} */ (placeholderConfig)).context);
placeholder = this._placeholderRef.rootNodes[0];
}
else {
placeholder = deepCloneNode(this._rootElement);
}
placeholder.classList.add('cdk-drag-placeholder');
return placeholder;
}
/**
* Figures out the coordinates at which an element was picked up.
* @private
* @param {?} referenceElement Element that initiated the dragging.
* @param {?} event Event that initiated the dragging.
* @return {?}
*/
_getPointerPositionInElement(referenceElement, event) {
/** @type {?} */
const elementRect = this._rootElement.getBoundingClientRect();
/** @type {?} */
const handleElement = referenceElement === this._rootElement ? null : referenceElement;
/** @type {?} */
const referenceRect = handleElement ? handleElement.getBoundingClientRect() : elementRect;
/** @type {?} */
const point = isTouchEvent(event) ? event.targetTouches[0] : event;
/** @type {?} */
const x = point.pageX - referenceRect.left - this._scrollPosition.left;
/** @type {?} */
const y = point.pageY - referenceRect.top - this._scrollPosition.top;
return {
x: referenceRect.left - elementRect.left + x,
y: referenceRect.top - elementRect.top + y
};
}
/**
* Determines the point of the page that was touched by the user.
* @private
* @param {?} event
* @return {?}
*/
_getPointerPositionOnPage(event) {
// `touches` will be empty for start/end events so we have to fall back to `changedTouches`.
/** @type {?} */
const point = isTouchEvent(event) ? (event.touches[0] || event.changedTouches[0]) : event;
return {
x: point.pageX - this._scrollPosition.left,
y: point.pageY - this._scrollPosition.top
};
}
/**
* Gets the pointer position on the page, accounting for any position constraints.
* @private
* @param {?} event
* @return {?}
*/
_getConstrainedPointerPosition(event) {
/** @type {?} */
const point = this._getPointerPositionOnPage(event);
/** @type {?} */
const dropContainerLock = this._dropContainer ? this._dropContainer.lockAxis : null;
if (this.lockAxis === 'x' || dropContainerLock === 'x') {
point.y = this._pickupPositionOnPage.y;
}
else if (this.lockAxis === 'y' || dropContainerLock === 'y') {
point.x = this._pickupPositionOnPage.x;
}
if (this._boundaryRect) {
const { x: pickupX, y: pickupY } = this._pickupPositionInElement;
/** @type {?} */
const boundaryRect = this._boundaryRect;
/** @type {?} */
const previewRect = (/** @type {?} */ (this._previewRect));
/** @type {?} */
const minY = boundaryRect.top + pickupY;
/** @type {?} */
const maxY = boundaryRect.bottom - (previewRect.height - pickupY);
/** @type {?} */
const minX = boundaryRect.left + pickupX;
/** @type {?} */
const maxX = boundaryRect.right - (previewRect.width - pickupX);
point.x = clamp(point.x, minX, maxX);
point.y = clamp(point.y, minY, maxY);
}
return point;
}
/**
* Updates the current drag delta, based on the user's current pointer position on the page.
* @private
* @param {?} pointerPositionOnPage
* @return {?}
*/
_updatePointerDirectionDelta(pointerPositionOnPage) {
const { x, y } = pointerPositionOnPage;
/** @type {?} */
const delta = this._pointerDirectionDelta;
/** @type {?} */
const positionSinceLastChange = this._pointerPositionAtLastDirectionChange;
// Amount of pixels the user has dragged since the last time the direction changed.
/** @type {?} */
const changeX = Math.abs(x - positionSinceLastChange.x);
/** @type {?} */
const changeY = Math.abs(y - positionSinceLastChange.y);
// Because we handle pointer events on a per-pixel basis, we don't want the delta
// to change for every pixel, otherwise anything that depends on it can look erratic.
// To make the delta more consistent, we track how much the user has moved since the last
// delta change and we only update it after it has reached a certain threshold.
if (changeX > this._config.pointerDirectionChangeThreshold) {
delta.x = x > positionSinceLastChange.x ? 1 : -1;
positionSinceLastChange.x = x;
}
if (changeY > this._config.pointerDirectionChangeThreshold) {
delta.y = y > positionSinceLastChange.y ? 1 : -1;
positionSinceLastChange.y = y;
}
return delta;
}
/**
* Toggles the native drag interactions, based on how many handles are registered.
* @private
* @return {?}
*/
_toggleNativeDragInteractions() {
if (!this._rootElement || !this._handles) {
return;
}
/** @type {?} */
const shouldEnable = this.disabled || this._handles.length > 0;
if (shouldEnable !== this._nativeInteractionsEnabled) {
this._nativeInteractionsEnabled = shouldEnable;
toggleNativeDragInteractions(this._rootElement, shouldEnable);
}
}
/**
* Removes the manually-added event listeners from the root element.
* @private
* @param {?} element
* @return {?}
*/
_removeRootElementListeners(element) {
element.removeEventListener('mousedown', this._pointerDown, activeEventListenerOptions);
element.removeEventListener('touchstart', this._pointerDown, passiveEventListenerOptions);
}
}
/**
* Gets a 3d `transform` that can be applied to an element.
* @param {?} x Desired position of the element along the X axis.
* @param {?} y Desired position of the element along the Y axis.
* @return {?}
*/
function getTransform(x, y) {
// Round the transforms since some browsers will
// blur the elements for sub-pixel transforms.
return `translate3d(${Math.round(x)}px, ${Math.round(y)}px, 0)`;
}
/**
* Creates a deep clone of an element.
* @param {?} node
* @return {?}
*/
function deepCloneNode(node) {
/** @type {?} */
const clone = (/** @type {?} */ (node.cloneNode(true)));
// Remove the `id` to avoid having multiple elements with the same id on the page.
clone.removeAttribute('id');
return clone;
}
/**
* Clamps a value between a minimum and a maximum.
* @param {?} value
* @param {?} min
* @param {?} max
* @return {?}
*/
function clamp(value, min, max) {
return Math.max(min, Math.min(max, value));
}
/**
* Helper to remove an element from the DOM and to do all the necessary null checks.
* @param {?} element Element to be removed.
* @return {?}
*/
function removeElement(element) {
if (element && element.parentNode) {
element.parentNode.removeChild(element);
}
}
/**
* Determines whether an event is a touch event.
* @param {?} event
* @return {?}
*/
function isTouchEvent(event) {
return event.type.startsWith('touch');
}
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
/**
* Moves an item one index in an array to another.
* @template T
* @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.
* @return {?}
*/
function moveItemInArray(array, fromIndex, toIndex) {
/** @type {?} */
const from = clamp$1(fromIndex, array.length - 1);
/** @type {?} */
const to = clamp$1(toIndex, array.length - 1);
if (from === to) {
return;
}
/** @type {?} */
const target = array[from];
/** @type {?} */
const delta = to < from ? -1 : 1;
for (let i = from; i !== to; i += delta) {
array[i] = array[i + delta];
}
array[to] = target;
}
/**
* Moves an item from one array to another.
* @template T
* @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.
* @return {?}
*/
function transferArrayItem(currentArray, targetArray, currentIndex, targetIndex) {
/** @type {?} */
const from = clamp$1(currentIndex, currentArray.length - 1);
/** @type {?} */
const to = clamp$1(targetIndex, targetArray.length);
if (currentArray.length) {
targetArray.splice(to, 0, currentArray.splice(from, 1)[0]);
}
}
/**
* Copies an item from one array to another, leaving it in its
* original position in current array.
* @template T
* @param {?} currentArray Array from which to copy the item.
* @param {?} targetArray Array into which is copy the item.
* @param {?} currentIndex Index of the item in its current array.
* @param {?} targetIndex Index at which to insert the item.
*
* @return {?}
*/
function copyArrayItem(currentArray, targetArray, currentIndex, targetIndex) {
/** @type {?} */
const to = clamp$1(targetIndex, targetArray.length);
if (currentArray.length) {
targetArray.splice(to, 0, currentArray[currentIndex]);
}
}
/**
* Clamps a number between zero and a maximum.
* @param {?} value
* @param {?} max
* @return {?}
*/
function clamp$1(value, max) {
return Math.max(0, Math.min(max, value));
}
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
/**
* Counter used to generate unique ids for drop refs.
* @type {?}
*/
let _uniqueIdCounter = 0;
/**
* Proximity, as a ratio to width/height, at which a
* dragged item will affect the drop container.
* @type {?}
*/
const DROP_PROXIMITY_THRESHOLD = 0.05;
/**
* Reference to a drop list. Used to manipulate or dispose of the container.
* \@docs-private
* @template T
*/
class DropListRef {
/**
* @param {?} element
* @param {?} _dragDropRegistry
* @param {?} _document
*/
constructor(element, _dragDropRegistry, _document) {
this._dragDropRegistry = _dragDropRegistry;
/**
* Unique ID for the drop list.
* @deprecated No longer being used. To be removed.
* \@breaking-change 8.0.0
*/
this.id = `cdk-drop-list-ref-${_uniqueIdCounter++}`;
/**
* Whether starting a dragging sequence from this container is disabled.
*/
this.disabled = false;
/**
* Function that is used to determine whether an item
* is allowed to be moved into a drop container.
*/
this.enterPredicate = () => true;
/**
* Emits right before dragging has started.
*/
this.beforeStarted = new Subject();
/**
* Emits when the user has moved a new drag item into this container.
*/
this.entered = new Subject();
/**
* Emits when the user removes an item from the container
* by dragging it into another container.
*/
this.exited = new Subject();
/**
* Emits when the user drops an item inside the container.
*/
this.dropped = new Subject();
/**
* Emits as the user is swapping items while actively dragging.
*/
this.sorted = new Subject();
/**
* Whether an item in the list is being dragged.
*/
this._isDragging = false;
/**
* Cache of the dimensions of all the items inside the container.
*/
this._itemPositions = [];
/**
* Keeps track of the item that was last swapped with the dragge