@angular/cdk
Version:
Angular Material Component Development Kit
956 lines • 154 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 } from '@angular/cdk/coercion';
import { Subscription, Subject } from 'rxjs';
import { startWith } from 'rxjs/operators';
import { extendStyles, toggleNativeDragInteractions } from './drag-styling';
import { getTransformTransitionDurationInMs } from './transition-duration';
/** Options that can be used to bind a passive event listener. */
var passiveEventListenerOptions = normalizePassiveListenerOptions({ passive: true });
/** Options that can be used to bind an active event listener. */
var 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.
*/
var MOUSE_EVENT_IGNORE_TIME = 800;
/**
* Reference to a draggable item. Used to manipulate or dispose of the item.
*/
var DragRef = /** @class */ (function () {
function DragRef(element, _config, _document, _ngZone, _viewportRuler, _dragDropRegistry) {
var _this = this;
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();
/** 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;
/** Subscription to the viewport being scrolled. */
this._scrollSubscription = Subscription.EMPTY;
/** Subscription to the viewport being resized. */
this._resizeSubscription = 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';
/**
* Amount of milliseconds to wait after the user has put their
* pointer down before starting to drag the element.
*/
this.dragStartDelay = 0;
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 = this._moveEvents.asObservable();
/** Handler for the `mousedown`/`touchstart` events. */
this._pointerDown = function (event) {
_this.beforeStarted.next();
// Delegate the event based on whether it started from a handle or the element itself.
if (_this._handles.length) {
var targetHandle = _this._handles.find(function (handle) {
var target = event.target;
return !!target && (target === handle || handle.contains(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 = function (event) {
// Prevent the default action as early as possible in order to block
// native actions like dragging the selected text or images with the mouse.
event.preventDefault();
if (!_this._hasStartedDragging) {
var pointerPosition = _this._getPointerPositionOnPage(event);
var distanceX = Math.abs(pointerPosition.x - _this._pickupPositionOnPage.x);
var distanceY = Math.abs(pointerPosition.y - _this._pickupPositionOnPage.y);
var isOverThreshold = distanceX + distanceY >= _this._config.dragStartThreshold;
// 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 (isOverThreshold) {
var isDelayElapsed = Date.now() >= _this._dragStartTime + _this._getDragStartDelay(event);
if (!isDelayElapsed) {
_this._endDragSequence(event);
return;
}
// Prevent other drag sequences from starting while something in the container is still
// being dragged. This can happen while we're waiting for the drop animation to finish
// and can cause errors, because some elements might still be moving around.
if (!_this._dropContainer || !_this._dropContainer.isDragging()) {
_this._hasStartedDragging = true;
_this._ngZone.run(function () { return _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();
}
}
var constrainedPointerPosition = _this._getConstrainedPointerPosition(event);
_this._hasMoved = true;
_this._updatePointerDirectionDelta(constrainedPointerPosition);
if (_this._dropContainer) {
_this._updateActiveDropContainer(constrainedPointerPosition);
}
else {
var activeTransform = _this._activeTransform;
activeTransform.x =
constrainedPointerPosition.x - _this._pickupPositionOnPage.x + _this._passiveTransform.x;
activeTransform.y =
constrainedPointerPosition.y - _this._pickupPositionOnPage.y + _this._passiveTransform.y;
_this._applyRootElementTransform(activeTransform.x, activeTransform.y);
// Apply transform as attribute if dragging and svg element to work for IE
if (typeof SVGElement !== 'undefined' && _this._rootElement instanceof SVGElement) {
var 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._moveEvents.observers.length) {
_this._ngZone.run(function () {
_this._moveEvents.next({
source: _this,
pointerPosition: constrainedPointerPosition,
event: event,
distance: _this._getDragDistance(constrainedPointerPosition),
delta: _this._pointerDirectionDelta
});
});
}
};
/** Handler that is invoked when the user lifts their pointer up, after initiating a drag. */
this._pointerUp = function (event) {
_this._endDragSequence(event);
};
this.withRootElement(element);
_dragDropRegistry.registerDragItem(this);
}
Object.defineProperty(DragRef.prototype, "disabled", {
/** Whether starting to drag this element is disabled. */
get: function () {
return this._disabled || !!(this._dropContainer && this._dropContainer.disabled);
},
set: function (value) {
var newValue = coerceBooleanProperty(value);
if (newValue !== this._disabled) {
this._disabled = newValue;
this._toggleNativeDragInteractions();
}
},
enumerable: true,
configurable: true
});
/**
* Returns the element that is being used as a placeholder
* while the current element is being dragged.
*/
DragRef.prototype.getPlaceholderElement = function () {
return this._placeholder;
};
/** Returns the root draggable element. */
DragRef.prototype.getRootElement = function () {
return this._rootElement;
};
/** Registers the handles that can be used to drag the element. */
DragRef.prototype.withHandles = function (handles) {
this._handles = handles.map(function (handle) { return coerceElement(handle); });
this._handles.forEach(function (handle) { return toggleNativeDragInteractions(handle, false); });
this._toggleNativeDragInteractions();
return this;
};
/**
* Registers the template that should be used for the drag preview.
* @param template Template that from which to stamp out the preview.
*/
DragRef.prototype.withPreviewTemplate = function (template) {
this._previewTemplate = template;
return this;
};
/**
* Registers the template that should be used for the drag placeholder.
* @param template Template that from which to stamp out the placeholder.
*/
DragRef.prototype.withPlaceholderTemplate = function (template) {
this._placeholderTemplate = template;
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.
*/
DragRef.prototype.withRootElement = function (rootElement) {
var element = coerceElement(rootElement);
if (element !== this._rootElement) {
if (this._rootElement) {
this._removeRootElementListeners(this._rootElement);
}
element.addEventListener('mousedown', this._pointerDown, activeEventListenerOptions);
element.addEventListener('touchstart', this._pointerDown, passiveEventListenerOptions);
this._initialTransform = undefined;
this._rootElement = element;
}
return this;
};
/**
* Element to which the draggable's position will be constrained.
*/
DragRef.prototype.withBoundaryElement = function (boundaryElement) {
var _this = this;
this._boundaryElement = boundaryElement ? coerceElement(boundaryElement) : null;
this._resizeSubscription.unsubscribe();
if (boundaryElement) {
this._resizeSubscription = this._viewportRuler
.change(10)
.subscribe(function () { return _this._containInsideBoundaryOnResize(); });
}
return this;
};
/** Removes the dragging functionality from the DOM element. */
DragRef.prototype.dispose = function () {
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.
removeNode(this._rootElement);
}
removeNode(this._anchor);
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._resizeSubscription.unsubscribe();
this._boundaryElement = this._rootElement = this._placeholderTemplate =
this._previewTemplate = this._anchor = null;
};
/** Checks whether the element is currently being dragged. */
DragRef.prototype.isDragging = function () {
return this._hasStartedDragging && this._dragDropRegistry.isDragging(this);
};
/** Resets a standalone drag item to its initial position. */
DragRef.prototype.reset = function () {
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.
*/
DragRef.prototype.disableHandle = function (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.
*/
DragRef.prototype.enableHandle = function (handle) {
this._disabledHandles.delete(handle);
};
/** Sets the layout direction of the draggable item. */
DragRef.prototype.withDirection = function (direction) {
this._direction = direction;
return this;
};
/** Sets the container that the item is part of. */
DragRef.prototype._withDropContainer = function (container) {
this._dropContainer = container;
};
/**
* Gets the current position in pixels the draggable outside of a drop container.
*/
DragRef.prototype.getFreeDragPosition = function () {
var position = this.isDragging() ? this._activeTransform : this._passiveTransform;
return { x: position.x, y: position.y };
};
/**
* Sets the current position in pixels the draggable outside of a drop container.
* @param value New position to be set.
*/
DragRef.prototype.setFreeDragPosition = function (value) {
this._activeTransform = { x: 0, y: 0 };
this._passiveTransform.x = value.x;
this._passiveTransform.y = value.y;
if (!this._dropContainer) {
this._applyRootElementTransform(value.x, value.y);
}
return this;
};
/** Updates the item's sort order based on the last-known pointer position. */
DragRef.prototype._sortFromLastPointerPosition = function () {
var position = this._pointerPositionAtLastDirectionChange;
if (position && this._dropContainer) {
this._updateActiveDropContainer(position);
}
};
/** Unsubscribes from the global subscriptions. */
DragRef.prototype._removeSubscriptions = function () {
this._pointerMoveSubscription.unsubscribe();
this._pointerUpSubscription.unsubscribe();
this._scrollSubscription.unsubscribe();
};
/** Destroys the preview element and its ViewRef. */
DragRef.prototype._destroyPreview = function () {
if (this._preview) {
removeNode(this._preview);
}
if (this._previewRef) {
this._previewRef.destroy();
}
this._preview = this._previewRef = null;
};
/** Destroys the placeholder element and its ViewRef. */
DragRef.prototype._destroyPlaceholder = function () {
if (this._placeholder) {
removeNode(this._placeholder);
}
if (this._placeholderRef) {
this._placeholderRef.destroy();
}
this._placeholder = this._placeholderRef = null;
};
/**
* Clears subscriptions and stops the dragging sequence.
* @param event Browser event object that ended the sequence.
*/
DragRef.prototype._endDragSequence = function (event) {
var _this = this;
// 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);
this._toggleNativeDragInteractions();
if (this._handles) {
this._rootElement.style.webkitTapHighlightColor = this._rootElementTapHighlight;
}
if (!this._hasStartedDragging) {
return;
}
this.released.next({ source: this });
if (this._dropContainer) {
// Stop scrolling immediately, instead of waiting for the animation to finish.
this._dropContainer._stopScrolling();
this._animatePreviewToPlaceholder().then(function () {
_this._cleanupDragArtifacts(event);
_this._cleanupCachedDimensions();
_this._dragDropRegistry.stopDragging(_this);
});
}
else {
// 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(function () {
_this.ended.next({
source: _this,
distance: _this._getDragDistance(_this._getPointerPositionOnPage(event))
});
});
this._cleanupCachedDimensions();
this._dragDropRegistry.stopDragging(this);
}
};
/** Starts the dragging sequence. */
DragRef.prototype._startDragSequence = function (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();
}
this._toggleNativeDragInteractions();
if (this._dropContainer) {
var element = this._rootElement;
var parent_1 = element.parentNode;
var preview = this._preview = this._createPreviewElement();
var placeholder = this._placeholder = this._createPlaceholderElement();
var anchor = this._anchor = this._anchor || this._document.createComment('');
// Insert an anchor node so that we can restore the element's position in the DOM.
parent_1.insertBefore(anchor, element);
// 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(parent_1.replaceChild(placeholder, element));
getPreviewInsertionPoint(this._document).appendChild(preview);
this._dropContainer.start();
}
};
/**
* Sets up the different variables and subscriptions
* that will be necessary for the dragging sequence.
* @param referenceElement Element that started the drag sequence.
* @param event Browser event object that started the sequence.
*/
DragRef.prototype._initializeDragSequence = function (referenceElement, event) {
var _this = this;
// 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();
var isDragging = this.isDragging();
var isTouchSequence = isTouchEvent(event);
var isAuxiliaryMouseButton = !isTouchSequence && event.button !== 0;
var rootElement = this._rootElement;
var 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 && 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;
}
// 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._hasStartedDragging = this._hasMoved = false;
this._initialContainer = this._dropContainer;
// Avoid multiple subscriptions and memory leaks when multi touch
// (isDragging check above isn't enough because of possible temporal and/or dimensional delays)
this._removeSubscriptions();
this._pointerMoveSubscription = this._dragDropRegistry.pointerMove.subscribe(this._pointerMove);
this._pointerUpSubscription = this._dragDropRegistry.pointerUp.subscribe(this._pointerUp);
this._scrollSubscription = this._dragDropRegistry.scroll.pipe(startWith(null)).subscribe(function () {
_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);
var pointerPosition = this._pickupPositionOnPage = this._getPointerPositionOnPage(event);
this._pointerDirectionDelta = { x: 0, y: 0 };
this._pointerPositionAtLastDirectionChange = { x: pointerPosition.x, y: pointerPosition.y };
this._dragStartTime = Date.now();
this._dragDropRegistry.startDragging(this, event);
};
/** Cleans up the DOM artifacts that were added to facilitate the element being dragged. */
DragRef.prototype._cleanupDragArtifacts = function (event) {
var _this = this;
// 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 = '';
this._anchor.parentNode.replaceChild(this._rootElement, this._anchor);
this._destroyPreview();
this._destroyPlaceholder();
this._boundaryRect = this._previewRect = undefined;
// Re-enter the NgZone since we bound `document` events on the outside.
this._ngZone.run(function () {
var container = _this._dropContainer;
var currentIndex = container.getItemIndex(_this);
var pointerPosition = _this._getPointerPositionOnPage(event);
var distance = _this._getDragDistance(_this._getPointerPositionOnPage(event));
var isPointerOverContainer = container._isOverContainer(pointerPosition.x, pointerPosition.y);
_this.ended.next({ source: _this, distance: distance });
_this.dropped.next({
item: _this,
currentIndex: currentIndex,
previousIndex: _this._initialContainer.getItemIndex(_this),
container: container,
previousContainer: _this._initialContainer,
isPointerOverContainer: isPointerOverContainer,
distance: distance
});
container.drop(_this, currentIndex, _this._initialContainer, isPointerOverContainer, distance);
_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.
*/
DragRef.prototype._updateActiveDropContainer = function (_a) {
var _this = this;
var x = _a.x, y = _a.y;
// Drop container that draggable has been moved into.
var newContainer = this._initialContainer._getSiblingContainerFromPosition(this, x, y);
// If we couldn't find a new container to move the item into, and the item has left its
// 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(function () {
// Notify the old container that the item has left.
_this.exited.next({ item: _this, container: _this._dropContainer });
_this._dropContainer.exit(_this);
// Notify the new container that the item has entered.
_this._dropContainer = newContainer;
_this._dropContainer.enter(_this, x, y);
_this.entered.next({
item: _this,
container: newContainer,
currentIndex: newContainer.getItemIndex(_this)
});
});
}
this._dropContainer._startScrollingIfNecessary(x, y);
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.
*/
DragRef.prototype._createPreviewElement = function () {
var previewConfig = this._previewTemplate;
var previewClass = this.previewClass;
var previewTemplate = previewConfig ? previewConfig.template : null;
var preview;
if (previewTemplate) {
var viewRef = previewConfig.viewContainer.createEmbeddedView(previewTemplate, previewConfig.context);
preview = getRootNode(viewRef, this._document);
this._previewRef = viewRef;
preview.style.transform =
getTransform(this._pickupPositionOnPage.x, this._pickupPositionOnPage.y);
}
else {
var element = this._rootElement;
var 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',
// We have to reset the margin, because it can throw off positioning relative to the viewport.
margin: '0',
position: 'fixed',
top: '0',
left: '0',
zIndex: '1000'
});
toggleNativeDragInteractions(preview, false);
preview.classList.add('cdk-drag-preview');
preview.setAttribute('dir', this._direction);
if (previewClass) {
if (Array.isArray(previewClass)) {
previewClass.forEach(function (className) { return preview.classList.add(className); });
}
else {
preview.classList.add(previewClass);
}
}
return preview;
};
/**
* Animates the preview element from its current position to the location of the drop placeholder.
* @returns Promise that resolves when the animation completes.
*/
DragRef.prototype._animatePreviewToPlaceholder = function () {
var _this = this;
// If the user hasn't moved yet, the transitionend event won't fire.
if (!this._hasMoved) {
return Promise.resolve();
}
var 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.
var duration = getTransformTransitionDurationInMs(this._preview);
if (duration === 0) {
return Promise.resolve();
}
return this._ngZone.runOutsideAngular(function () {
return new Promise(function (resolve) {
var handler = (function (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.
var timeout = setTimeout(handler, duration * 1.5);
_this._preview.addEventListener('transitionend', handler);
});
});
};
/** Creates an element that will be shown instead of the current element while dragging. */
DragRef.prototype._createPlaceholderElement = function () {
var placeholderConfig = this._placeholderTemplate;
var placeholderTemplate = placeholderConfig ? placeholderConfig.template : null;
var placeholder;
if (placeholderTemplate) {
this._placeholderRef = placeholderConfig.viewContainer.createEmbeddedView(placeholderTemplate, placeholderConfig.context);
placeholder = getRootNode(this._placeholderRef, this._document);
}
else {
placeholder = deepCloneNode(this._rootElement);
}
placeholder.classList.add('cdk-drag-placeholder');
return placeholder;
};
/**
* Figures out the coordinates at which an element was picked up.
* @param referenceElement Element that initiated the dragging.
* @param event Event that initiated the dragging.
*/
DragRef.prototype._getPointerPositionInElement = function (referenceElement, event) {
var elementRect = this._rootElement.getBoundingClientRect();
var handleElement = referenceElement === this._rootElement ? null : referenceElement;
var referenceRect = handleElement ? handleElement.getBoundingClientRect() : elementRect;
var point = isTouchEvent(event) ? event.targetTouches[0] : event;
var x = point.pageX - referenceRect.left - this._scrollPosition.left;
var 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. */
DragRef.prototype._getPointerPositionOnPage = function (event) {
// `touches` will be empty for start/end events so we have to fall back to `changedTouches`.
var 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. */
DragRef.prototype._getConstrainedPointerPosition = function (event) {
var point = this._getPointerPositionOnPage(event);
var constrainedPoint = this.constrainPosition ? this.constrainPosition(point, this) : point;
var dropContainerLock = this._dropContainer ? this._dropContainer.lockAxis : null;
if (this.lockAxis === 'x' || dropContainerLock === 'x') {
constrainedPoint.y = this._pickupPositionOnPage.y;
}
else if (this.lockAxis === 'y' || dropContainerLock === 'y') {
constrainedPoint.x = this._pickupPositionOnPage.x;
}
if (this._boundaryRect) {
var _a = this._pickupPositionInElement, pickupX = _a.x, pickupY = _a.y;
var boundaryRect = this._boundaryRect;
var previewRect = this._previewRect;
var minY = boundaryRect.top + pickupY;
var maxY = boundaryRect.bottom - (previewRect.height - pickupY);
var minX = boundaryRect.left + pickupX;
var maxX = boundaryRect.right - (previewRect.width - pickupX);
constrainedPoint.x = clamp(constrainedPoint.x, minX, maxX);
constrainedPoint.y = clamp(constrainedPoint.y, minY, maxY);
}
return constrainedPoint;
};
/** Updates the current drag delta, based on the user's current pointer position on the page. */
DragRef.prototype._updatePointerDirectionDelta = function (pointerPositionOnPage) {
var x = pointerPositionOnPage.x, y = pointerPositionOnPage.y;
var delta = this._pointerDirectionDelta;
var positionSinceLastChange = this._pointerPositionAtLastDirectionChange;
// Amount of pixels the user has dragged since the last time the direction changed.
var changeX = Math.abs(x - positionSinceLastChange.x);
var 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. */
DragRef.prototype._toggleNativeDragInteractions = function () {
if (!this._rootElement || !this._handles) {
return;
}
var shouldEnable = this._handles.length > 0 || !this.isDragging();
if (shouldEnable !== this._nativeInteractionsEnabled) {
this._nativeInteractionsEnabled = shouldEnable;
toggleNativeDragInteractions(this._rootElement, shouldEnable);
}
};
/** Removes the manually-added event listeners from the root element. */
DragRef.prototype._removeRootElementListeners = function (element) {
element.removeEventListener('mousedown', this._pointerDown, activeEventListenerOptions);
element.removeEventListener('touchstart', this._pointerDown, passiveEventListenerOptions);
};
/**
* Applies a `transform` to the root element, taking into account any existing transforms on it.
* @param x New transform value along the X axis.
* @param y New transform value along the Y axis.
*/
DragRef.prototype._applyRootElementTransform = function (x, y) {
var transform = getTransform(x, y);
// 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 || '';
}
// 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;
};
/**
* Gets the distance that the user has dragged during the current drag sequence.
* @param currentPosition Current position of the user's pointer.
*/
DragRef.prototype._getDragDistance = function (currentPosition) {
var pickupPosition = this._pickupPositionOnPage;
if (pickupPosition) {
return { x: currentPosition.x - pickupPosition.x, y: currentPosition.y - pickupPosition.y };
}
return { x: 0, y: 0 };
};
/** Cleans up any cached element dimensions that we don't need after dragging has stopped. */
DragRef.prototype._cleanupCachedDimensions = function () {
this._boundaryRect = this._previewRect = undefined;
};
/**
* Checks whether the element is still inside its boundary after the viewport has been resized.
* If not, the position is adjusted so that the element fits again.
*/
DragRef.prototype._containInsideBoundaryOnResize = function () {
var _a = this._passiveTransform, x = _a.x, y = _a.y;
if ((x === 0 && y === 0) || this.isDragging() || !this._boundaryElement) {
return;
}
var boundaryRect = this._boundaryElement.getBoundingClientRect();
var elementRect = this._rootElement.getBoundingClientRect();
// It's possible that the element got hidden away after dragging (e.g. by switching to a
// different tab). Don't do anything in this case so we don't clear the user's position.
if ((boundaryRect.width === 0 && boundaryRect.height === 0) ||
(elementRect.width === 0 && elementRect.height === 0)) {
return;
}
var leftOverflow = boundaryRect.left - elementRect.left;
var rightOverflow = elementRect.right - boundaryRect.right;
var topOverflow = boundaryRect.top - elementRect.top;
var bottomOverflow = elementRect.bottom - boundaryRect.bottom;
// If the element has become wider than the boundary, we can't
// do much to make it fit so we just anchor it to the left.
if (boundaryRect.width > elementRect.width) {
if (leftOverflow > 0) {
x += leftOverflow;
}
if (rightOverflow > 0) {
x -= rightOverflow;
}
}
else {
x = 0;
}
// If the element has become taller than the boundary, we can't
// do much to make it fit so we just anchor it to the top.
if (boundaryRect.height > elementRect.height) {
if (topOverflow > 0) {
y += topOverflow;
}
if (bottomOverflow > 0) {
y -= bottomOverflow;
}
}
else {
y = 0;
}
if (x !== this._passiveTransform.x || y !== this._passiveTransform.y) {
this.setFreeDragPosition({ y: y, x: x });
}
};
/** Gets the drag start delay, based on the event type. */
DragRef.prototype._getDragStartDelay = function (event) {
var value = this.dragStartDelay;
if (typeof value === 'number') {
return value;
}
else if (isTouchEvent(event)) {
return value.touch;
}
return value ? value.mouse : 0;
};
return DragRef;
}());
export { DragRef };
/**
* 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.
*/
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. */
function deepCloneNode(node) {
var clone = node.cloneNode(true);
var descendantsWithId = clone.querySelectorAll('[id]');
var descendantCanvases = node.querySelectorAll('canvas');
// Remove the `id` to avoid having multiple elements with the same id on the page.
clone.removeAttribute('id');
for (var i = 0; i < descendantsWithId.length; i++) {
descendantsWithId[i].removeAttribute('id');
}
// `cloneNode` won't transfer the content of `canvas` elements so we have to do it ourselves.
// We match up the cloned canvas to their sources using their index in the DOM.
if (descendantCanvases.length) {
var cloneCanvases = clone.querySelectorAll('canvas');
for (var i = 0; i < descendantCanvases.length; i++) {
var correspondingCloneContext = cloneCanvases[i].getContext('2d');
if (correspondingCloneContext) {
correspondingCloneContext.drawImage(descendantCanvases[i], 0, 0);
}
}
}
return clone;
}
/** Clamps a value between a minimum and a maximum. */
function clamp(value, min, max) {
return Math.max(min, Math.min(max, value));
}
/**
* Helper to remove a node from the DOM and to do all the necessary null checks.
* @param node Node to be removed.
*/
function removeNode(node) {
if (node && node.parentNode) {
node.parentNode.removeChild(node);
}
}
/** Determines whether an event is a touch event. */
function isTouchEvent(event) {
// This function is called for every pixel that the user has dragged so we need it to be
// as fast as possible. Since we only bind mouse events and touch events, we can assume
// that if the event's name starts with `t`, it's a touch event.
return event.type[0] === 't';
}
/** Gets the element into which the drag preview should be inserted. */
function getPreviewInsertionPoint(documentRef) {
// We can't use the body if the user is in fullscreen mode,
// because the preview will render under the fullscreen element.
// TODO(crisbeto): dedupe this with the `FullscreenOverlayContainer` eventually.
return documentRef.fullscreenElement ||
documentRef.webkitFullscreenElement ||
documentRef.mozFullScreenElement ||
documentRef.msFullscreenElement ||
documentRef.body;
}
/**
* Gets the root HTML element of an embedded view.
* If the root is not an HTML element it gets wrapped in one.
*/
function getRootNode(viewRef, _document) {
var rootNode = viewRef.rootNodes[0];
if (rootNode.nodeType !== _document.ELEMENT_NODE) {
var wrapper = _document.createElement('div');
wrapper.appendChild(rootNode);
return wrapper;
}
return rootNode;
}
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZHJhZy1yZWYuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi8uLi8uLi8uLi8uLi8uLi8uLi9zcmMvY2RrL2RyYWctZHJvcC9kcmFnLXJlZi50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7Ozs7O0dBTUc7QUFLSCxPQUFPLEVBQUMsK0JBQStCLEVBQUMsTUFBTSx1QkFBdUIsQ0FBQztBQUN0RSxPQUFPLEVBQUMscUJBQXFCLEVBQUUsYUFBYSxFQUFDLE1BQU0sdUJBQXVCLENBQUM7QUFDM0UsT0FBTyxFQUFDLFlBQVksRUFBRSxPQUFPLEVBQWEsTUFBTSxNQUFNLENBQUM7QUFDdkQsT0FBTyxFQUFDLFNBQVMsRUFBQyxNQUFNLGdCQUFnQixDQUFDO0FBR3pDLE9BQU8sRUFBQyxZQUFZLEVBQUUsNEJBQTRCLEVBQUMsTUFBTSxnQkFBZ0IsQ0FBQztBQUMxRSxPQUFPLEVBQUMsa0NBQWtDLEVBQUMsTUFBTSx1QkFBdUIsQ0FBQztBQWlCekUsaUVBQWlFO0FBQ2pFLElBQU0sMkJBQTJCLEdBQUcsK0JBQStCLENBQUMsRUFBQyxPQUFPLEVBQUUsSUFBSSxFQUFDLENBQUMsQ0FBQztBQUVyRixpRUFBaUU7QUFDakUsSUFBTSwwQkFBMEIsR0FBRywrQkFBK0IsQ0FBQyxFQUFDLE9BQU8sRUFBRSxLQUFLLEVBQUMsQ0FBQyxDQUFDO0FBRXJGOzs7OztHQUtHO0FBQ0gsSUFBTSx1QkFBdUIsR0FBRyxHQUFHLENBQUM7QUFtQnBDOztHQUVHO0FBQ0g7SUFtTkUsaUJBQ0UsT0FBOEMsRUFDdEMsT0FBc0IsRUFDdEIsU0FBbUIsRUFDbkIsT0FBZSxFQUNmLGNBQTZCLEVBQzdCLGlCQUF5RDtRQU5uRSxpQkFVQztRQVJTLFlBQU8sR0FBUCxPQUFPLENBQWU7UUFDdEIsY0FBUyxHQUFULFNBQVMsQ0FBVTtRQUNuQixZQUFPLEdBQVAsT0FBTyxDQUFRO1FBQ2YsbUJBQWMsR0FBZCxjQUFjLENBQWU7UUFDN0Isc0JBQWlCLEdBQWpCLGlCQUFpQixDQUF3QztRQWhNbkU7Ozs7O1dBS0c7UUFDSyxzQkFBaUIsR0FBVSxFQUFDLENBQUMsRUFBRSxDQUFDLEVBQUUsQ0FBQyxFQUFFLENBQUMsRUFBQyxDQUFDO1FBRWhELCtFQUErRTtRQUN2RSxxQkFBZ0IsR0FBVSxFQUFDLENBQUMsRUFBRSxDQUFDLEVBQUUsQ0FBQyxFQUFFLENBQUMsRUFBQyxDQUFDO1FBb0IvQywwQ0FBMEM7UUFDbEMsZ0JBQVcsR0FBRyxJQUFJLE9BQU8sRUFNN0IsQ0FBQztRQW9CTCwrQ0FBK0M7UUFDdkMsNkJBQXdCLEdBQUcsWUFBWSxDQUFDLEtBQUssQ0FBQztRQUV0RCxzRkFBc0Y7UUFDOUUsMkJBQXNCLEdBQUcsWUFBWSxDQUFDLEtBQUssQ0FBQztRQUVwRCxtREFBbUQ7UUFDM0Msd0JBQW1CLEdBQUcsWUFBWSxDQUFDLEtBQUssQ0FBQztRQUVqRCxrREFBa0Q7UUFDMUMsd0JBQW1CLEdBQUcsWUFBWSxDQUFDLEtBQUssQ0FBQztRQVlqRCxnREFBZ0Q7UUFDeEMscUJBQWdCLEdBQXVCLElBQUksQ0FBQztRQUVwRCxzRkFBc0Y7UUFDOUUsK0JBQTBCLEdBQUcsSUFBSSxDQUFDO1FBYzFDLDREQUE0RDtRQUNwRCxhQUFRLEdBQWtCLEVBQUUsQ0FBQztRQUVyQyxzREFBc0Q7UUFDOUMscUJBQWdCLEdBQUcsSUFBSSxHQUFHLEVBQWUsQ0FBQztRQUtsRCxvQ0FBb0M7UUFDNUIsZUFBVSxHQUFjLEtBQUssQ0FBQztRQUt0Qzs7O1dBR0c7UUFDSCxtQkFBYyxHQUE0QyxDQUFDLEN