UNPKG

@angular/cdk

Version:

Angular Material Component Development Kit

1,223 lines (1,216 loc) 118 kB
/** * @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