UNPKG

@angular/cdk

Version:

Angular Material Component Development Kit

938 lines (932 loc) 189 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@angular/core'), require('@angular/common'), require('@angular/cdk/scrolling'), require('@angular/cdk/platform'), require('@angular/cdk/coercion'), require('rxjs'), require('rxjs/operators'), require('@angular/cdk/bidi')) : typeof define === 'function' && define.amd ? define('@angular/cdk/drag-drop', ['exports', '@angular/core', '@angular/common', '@angular/cdk/scrolling', '@angular/cdk/platform', '@angular/cdk/coercion', 'rxjs', 'rxjs/operators', '@angular/cdk/bidi'], factory) : (global = global || self, factory((global.ng = global.ng || {}, global.ng.cdk = global.ng.cdk || {}, global.ng.cdk.dragDrop = {}), global.ng.core, global.ng.common, global.ng.cdk.scrolling, global.ng.cdk.platform, global.ng.cdk.coercion, global.rxjs, global.rxjs.operators, global.ng.cdk.bidi)); }(this, (function (exports, i0, i1, i2, platform, coercion, rxjs, operators, bidi) { 'use strict'; /** * @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 */ /** * Shallow-extends a stylesheet object with another stylesheet object. * @docs-private */ function extendStyles(dest, source) { for (var key in source) { if (source.hasOwnProperty(key)) { dest[key] = source[key]; } } return dest; } /** * Toggles whether the native drag interactions should be enabled for an element. * @param element Element on which to toggle the drag interactions. * @param enable Whether the drag interactions should be enabled. * @docs-private */ function toggleNativeDragInteractions(element, enable) { var userSelect = enable ? '' : 'none'; extendStyles(element.style, { touchAction: enable ? '' : 'none', webkitUserDrag: enable ? '' : 'none', webkitTapHighlightColor: enable ? '' : 'transparent', userSelect: userSelect, msUserSelect: userSelect, webkitUserSelect: userSelect, MozUserSelect: userSelect }); } /** * Toggles whether an element is visible while preserving its dimensions. * @param element Element whose visibility to toggle * @param enable Whether the element should be visible. * @docs-private */ function toggleVisibility(element, enable) { var styles = element.style; styles.position = enable ? '' : 'fixed'; styles.top = styles.opacity = enable ? '' : '0'; styles.left = enable ? '' : '-999em'; } /** * @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 */ /** Parses a CSS time value to milliseconds. */ function parseCssTimeUnitsToMs(value) { // Some browsers will return it in seconds, whereas others will return milliseconds. var 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. */ function getTransformTransitionDurationInMs(element) { var computedStyle = getComputedStyle(element); var transitionedProperties = parseCssPropertyValue(computedStyle, 'transition-property'); var property = transitionedProperties.find(function (prop) { return 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`. var propertyIndex = transitionedProperties.indexOf(property); var rawDurations = parseCssPropertyValue(computedStyle, 'transition-duration'); var rawDelays = parseCssPropertyValue(computedStyle, 'transition-delay'); return parseCssTimeUnitsToMs(rawDurations[propertyIndex]) + parseCssTimeUnitsToMs(rawDelays[propertyIndex]); } /** Parses out multiple values from a computed style into an array. */ function parseCssPropertyValue(computedStyle, name) { var value = computedStyle.getPropertyValue(name); return value.split(',').map(function (part) { return part.trim(); }); } /** * @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 */ /** Gets a mutable version of an element's bounding `ClientRect`. */ function getMutableClientRect(element) { var clientRect = element.getBoundingClientRect(); // We need to clone the `clientRect` here, because all the values on it are readonly // and we need to be able to update them. Also we can't use a spread here, because // the values on a `ClientRect` aren't own properties. See: // https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect#Notes return { top: clientRect.top, right: clientRect.right, bottom: clientRect.bottom, left: clientRect.left, width: clientRect.width, height: clientRect.height }; } /** * Checks whether some coordinates are within a `ClientRect`. * @param clientRect ClientRect that is being checked. * @param x Coordinates along the X axis. * @param y Coordinates along the Y axis. */ function isInsideClientRect(clientRect, x, y) { var top = clientRect.top, bottom = clientRect.bottom, left = clientRect.left, right = clientRect.right; return y >= top && y <= bottom && x >= left && x <= right; } /** * Updates the top/left positions of a `ClientRect`, as well as their bottom/right counterparts. * @param clientRect `ClientRect` that should be updated. * @param top Amount to add to the `top` position. * @param left Amount to add to the `left` position. */ function adjustClientRect(clientRect, top, left) { clientRect.top += top; clientRect.bottom = clientRect.top + clientRect.height; clientRect.left += left; clientRect.right = clientRect.left + clientRect.width; } /** * Checks whether the pointer coordinates are close to a ClientRect. * @param rect ClientRect to check against. * @param threshold Threshold around the ClientRect. * @param pointerX Coordinates along the X axis. * @param pointerY Coordinates along the Y axis. */ function isPointerNearClientRect(rect, threshold, pointerX, pointerY) { var top = rect.top, right = rect.right, bottom = rect.bottom, left = rect.left, width = rect.width, height = rect.height; var xThreshold = width * threshold; var yThreshold = height * threshold; return pointerY > top - yThreshold && pointerY < bottom + yThreshold && pointerX > left - xThreshold && pointerX < right + xThreshold; } /** * @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 */ /** Keeps track of the scroll position and dimensions of the parents of an element. */ var ParentPositionTracker = /** @class */ (function () { function ParentPositionTracker(_document, _viewportRuler) { this._document = _document; this._viewportRuler = _viewportRuler; /** Cached positions of the scrollable parent elements. */ this.positions = new Map(); } /** Clears the cached positions. */ ParentPositionTracker.prototype.clear = function () { this.positions.clear(); }; /** Caches the positions. Should be called at the beginning of a drag sequence. */ ParentPositionTracker.prototype.cache = function (elements) { var _this = this; this.clear(); this.positions.set(this._document, { scrollPosition: this._viewportRuler.getViewportScrollPosition(), }); elements.forEach(function (element) { _this.positions.set(element, { scrollPosition: { top: element.scrollTop, left: element.scrollLeft }, clientRect: getMutableClientRect(element) }); }); }; /** Handles scrolling while a drag is taking place. */ ParentPositionTracker.prototype.handleScroll = function (event) { var target = event.target; var cachedPosition = this.positions.get(target); if (!cachedPosition) { return null; } // Used when figuring out whether an element is inside the scroll parent. If the scrolled // parent is the `document`, we use the `documentElement`, because IE doesn't support // `contains` on the `document`. var scrolledParentNode = target === this._document ? target.documentElement : target; var scrollPosition = cachedPosition.scrollPosition; var newTop; var newLeft; if (target === this._document) { var viewportScrollPosition = this._viewportRuler.getViewportScrollPosition(); newTop = viewportScrollPosition.top; newLeft = viewportScrollPosition.left; } else { newTop = target.scrollTop; newLeft = target.scrollLeft; } var topDifference = scrollPosition.top - newTop; var leftDifference = scrollPosition.left - newLeft; // Go through and update the cached positions of the scroll // parents that are inside the element that was scrolled. this.positions.forEach(function (position, node) { if (position.clientRect && target !== node && scrolledParentNode.contains(node)) { adjustClientRect(position.clientRect, topDifference, leftDifference); } }); scrollPosition.top = newTop; scrollPosition.left = newLeft; return { top: topDifference, left: leftDifference }; }; return ParentPositionTracker; }()); /** * @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 */ /** Creates a deep clone of an element. */ function deepCloneNode(node) { var clone = node.cloneNode(true); var descendantsWithId = clone.querySelectorAll('[id]'); var nodeName = node.nodeName.toLowerCase(); // 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'); } if (nodeName === 'canvas') { transferCanvasData(node, clone); } else if (nodeName === 'input' || nodeName === 'select' || nodeName === 'textarea') { transferInputData(node, clone); } transferData('canvas', node, clone, transferCanvasData); transferData('input, textarea, select', node, clone, transferInputData); return clone; } /** Matches elements between an element and its clone and allows for their data to be cloned. */ function transferData(selector, node, clone, callback) { var descendantElements = node.querySelectorAll(selector); if (descendantElements.length) { var cloneElements = clone.querySelectorAll(selector); for (var i = 0; i < descendantElements.length; i++) { callback(descendantElements[i], cloneElements[i]); } } } // Counter for unique cloned radio button names. var cloneUniqueId = 0; /** Transfers the data of one input element to another. */ function transferInputData(source, clone) { // Browsers throw an error when assigning the value of a file input programmatically. if (clone.type !== 'file') { clone.value = source.value; } // Radio button `name` attributes must be unique for radio button groups // otherwise original radio buttons can lose their checked state // once the clone is inserted in the DOM. if (clone.type === 'radio' && clone.name) { clone.name = "mat-clone-" + clone.name + "-" + cloneUniqueId++; } } /** Transfers the data of one canvas element to another. */ function transferCanvasData(source, clone) { var context = clone.getContext('2d'); if (context) { // In some cases `drawImage` can throw (e.g. if the canvas size is 0x0). // We can't do much about it so just ignore the error. try { context.drawImage(source, 0, 0); } catch (_a) { } } } /** * @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 */ /** Options that can be used to bind a passive event listener. */ var passiveEventListenerOptions = platform.normalizePassiveListenerOptions({ passive: true }); /** Options that can be used to bind an active event listener. */ var activeEventListenerOptions = platform.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 rxjs.Subject(); /** Subscription to pointer movement events. */ this._pointerMoveSubscription = rxjs.Subscription.EMPTY; /** Subscription to the event that is dispatched when the user lifts their pointer. */ this._pointerUpSubscription = rxjs.Subscription.EMPTY; /** Subscription to the viewport being scrolled. */ this._scrollSubscription = rxjs.Subscription.EMPTY; /** Subscription to the viewport being resized. */ this._resizeSubscription = rxjs.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 rxjs.Subject(); /** Emits when the user starts dragging the item. */ this.started = new rxjs.Subject(); /** Emits when the user has released a drag item, before any animations have started. */ this.released = new rxjs.Subject(); /** Emits when the user stops dragging an item in the container. */ this.ended = new rxjs.Subject(); /** Emits when the user has moved the item into a new container. */ this.entered = new rxjs.Subject(); /** Emits when the user removes the item its container by dragging it into another container. */ this.exited = new rxjs.Subject(); /** Emits when the user drops the item inside a container. */ this.dropped = new rxjs.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; /** 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(); var pointerPosition = _this._getPointerPositionOnPage(event); if (!_this._hasStartedDragging) { 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); var container = _this._dropContainer; 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 (!container || (!container.isDragging() && !container.isReceiving())) { _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(pointerPosition); _this._hasMoved = true; _this._lastKnownPointerPosition = pointerPosition; _this._updatePointerDirectionDelta(constrainedPointerPosition); if (_this._dropContainer) { _this._updateActiveDropContainer(constrainedPointerPosition, pointerPosition); } 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); this._parentPositions = new ParentPositionTracker(_document, _viewportRuler); _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 = coercion.coerceBooleanProperty(value); if (newValue !== this._disabled) { this._disabled = newValue; this._toggleNativeDragInteractions(); this._handles.forEach(function (handle) { return toggleNativeDragInteractions(handle, newValue); }); } }, enumerable: false, 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; }; /** * Gets the currently-visible element that represents the drag item. * While dragging this is the placeholder, otherwise it's the root element. */ DragRef.prototype.getVisibleElement = function () { return this.isDragging() ? this.getPlaceholderElement() : this.getRootElement(); }; /** Registers the handles that can be used to drag the element. */ DragRef.prototype.withHandles = function (handles) { var _this = this; this._handles = handles.map(function (handle) { return coercion.coerceElement(handle); }); this._handles.forEach(function (handle) { return toggleNativeDragInteractions(handle, _this.disabled); }); this._toggleNativeDragInteractions(); // Delete any lingering disabled handles that may have been destroyed. Note that we re-create // the set, rather than iterate over it and filter out the destroyed handles, because while // the ES spec allows for sets to be modified while they're being iterated over, some polyfills // use an array internally which may throw an error. var disabledHandles = new Set(); this._disabledHandles.forEach(function (handle) { if (_this._handles.indexOf(handle) > -1) { disabledHandles.add(handle); } }); this._disabledHandles = disabledHandles; 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 _this = this; var element = coercion.coerceElement(rootElement); if (element !== this._rootElement) { if (this._rootElement) { this._removeRootElementListeners(this._rootElement); } this._ngZone.runOutsideAngular(function () { element.addEventListener('mousedown', _this._pointerDown, activeEventListenerOptions); element.addEventListener('touchstart', _this._pointerDown, passiveEventListenerOptions); }); this._initialTransform = undefined; this._rootElement = element; } if (typeof SVGElement !== 'undefined' && this._rootElement instanceof SVGElement) { this._ownerSVGElement = this._rootElement.ownerSVGElement; } return this; }; /** * Element to which the draggable's position will be constrained. */ DragRef.prototype.withBoundaryElement = function (boundaryElement) { var _this = this; this._boundaryElement = boundaryElement ? coercion.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._parentPositions.clear(); this._boundaryElement = this._rootElement = this._ownerSVGElement = 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._disabledHandles.has(handle) && this._handles.indexOf(handle) > -1) { this._disabledHandles.add(handle); toggleNativeDragInteractions(handle, true); } }; /** * Enables a handle, if it has been disabled. * @param handle Handle element to be enabled. */ DragRef.prototype.enableHandle = function (handle) { if (this._disabledHandles.has(handle)) { this._disabledHandles.delete(handle); toggleNativeDragInteractions(handle, this.disabled); } }; /** 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._lastKnownPointerPosition; if (position && this._dropContainer) { this._updateActiveDropContainer(this._getConstrainedPointerPosition(position), 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) { if (isTouchEvent(event)) { this._lastTouchEventTime = Date.now(); } this._toggleNativeDragInteractions(); var dropContainer = this._dropContainer; if (dropContainer) { var element = this._rootElement; var parent = element.parentNode; var preview = this._preview = this._createPreviewElement(); var placeholder = this._placeholder = this._createPlaceholderElement(); var anchor = this._anchor = this._anchor || this._document.createComment(''); // Needs to happen before the root element is moved. var shadowRoot = this._getShadowRoot(); // Insert an anchor node so that we can restore the element's position in the DOM. parent.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. toggleVisibility(element, false); this._document.body.appendChild(parent.replaceChild(placeholder, element)); getPreviewInsertionPoint(this._document, shadowRoot).appendChild(preview); this.started.next({ source: this }); // Emit before notifying the container. dropContainer.start(); this._initialContainer = dropContainer; this._initialIndex = dropContainer.getItemIndex(this); } else { this.started.next({ source: this }); this._initialContainer = this._initialIndex = undefined; } // Important to run after we've called `start` on the parent container // so that it has had time to resolve its scrollable parents. this._parentPositions.cache(dropContainer ? dropContainer.getScrollableParents() : []); }; /** * 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; // 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.subscribe(function (scrollEvent) { _this._updateOnScroll(scrollEvent); }); if (this._boundaryElement) { this._boundaryRect = getMutableClientRect(this._boundaryElement); } // If we have a custom preview we can't know ahead of time how large it'll be so we position // it next to the cursor. The exception is when the consumer has opted into making the preview // the same size as the root element, in which case we do know the size. var previewTemplate = this._previewTemplate; this._pickupPositionInElement = previewTemplate && previewTemplate.template && !previewTemplate.matchSize ? { x: 0, y: 0 } : this._getPointerPositionInElement(referenceElement, event); var pointerPosition = this._pickupPositionOnPage = this._lastKnownPointerPosition = 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. toggleVisibility(this._rootElement, true); 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._initialIndex, container: container, previousContainer: _this._initialContainer, isPointerOverContainer: isPointerOverContainer, distance: distance }); container.drop(_this, currentIndex, _this._initialIndex, _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, _b) { var _this = this; var x = _a.x, y = _a.y; var rawX = _b.x, rawY = _b.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, newContainer === _this._initialContainer && // If we're re-entering the initial container and sorting is disabled, // put item the into its starting index to begin with. newContainer.sortingDisabled ? _this._initialIndex : undefined); _this.entered.next({ item: _this, container: newContainer, currentIndex: newContainer.getItemIndex(_this) }); }); } this._dropContainer._startScrollingIfNecessary(rawX, rawY); 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 && previewConfig) { // Measure the element before we've inserted the preview // since the insertion could throw off the measurement. var rootRect = previewConfig.matchSize ? this._rootElement.getBoundingClientRect() : null; var viewRef = previewConfig.viewContainer.createEmbeddedView(previewTemplate, previewConfig.context); viewRef.detectCh