| import { ɵɵdefineInjectable, ɵɵinject, NgZone, Injectable, Inject, InjectionToken, Directive, Input, EventEmitter, ElementRef, ChangeDetectorRef, Optional, SkipSelf, Output, TemplateRef, ViewContainerRef, Self, ContentChildren, ContentChild, NgModule } from '@angular/core'; |
| import { DOCUMENT } from '@angular/common'; |
| import { ViewportRuler, ScrollDispatcher, CdkScrollableModule } from '@angular/cdk/scrolling'; |
| import { normalizePassiveListenerOptions, _getShadowRoot } from '@angular/cdk/platform'; |
| import { coerceBooleanProperty, coerceElement, coerceArray, coerceNumberProperty } from '@angular/cdk/coercion'; |
| import { Subject, Subscription, interval, animationFrameScheduler, Observable, merge } from 'rxjs'; |
| import { takeUntil, startWith, map, take, tap, switchMap } from 'rxjs/operators'; |
| import { Directionality } from '@angular/cdk/bidi'; |
| |
| /** |
| * @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 (let 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) { |
| const 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) { |
| const 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. |
| 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. */ |
| function getTransformTransitionDurationInMs(element) { |
| const computedStyle = getComputedStyle(element); |
| const transitionedProperties = parseCssPropertyValue(computedStyle, 'transition-property'); |
| 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`. |
| const propertyIndex = transitionedProperties.indexOf(property); |
| const rawDurations = parseCssPropertyValue(computedStyle, 'transition-duration'); |
| const 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) { |
| const value = computedStyle.getPropertyValue(name); |
| return value.split(',').map(part => 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) { |
| const 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) { |
| const { top, bottom, left, right } = clientRect; |
| 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) { |
| const { top, right, bottom, left, width, height } = rect; |
| const xThreshold = width * threshold; |
| const 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. */ |
| class ParentPositionTracker { |
| constructor(_document, _viewportRuler) { |
| this._document = _document; |
| this._viewportRuler = _viewportRuler; |
| /** Cached positions of the scrollable parent elements. */ |
| this.positions = new Map(); |
| } |
| /** Clears the cached positions. */ |
| clear() { |
| this.positions.clear(); |
| } |
| /** Caches the positions. Should be called at the beginning of a drag sequence. */ |
| cache(elements) { |
| this.clear(); |
| this.positions.set(this._document, { |
| scrollPosition: this._viewportRuler.getViewportScrollPosition(), |
| }); |
| elements.forEach(element => { |
| this.positions.set(element, { |
| scrollPosition: { top: element.scrollTop, left: element.scrollLeft }, |
| clientRect: getMutableClientRect(element) |
| }); |
| }); |
| } |
| /** Handles scrolling while a drag is taking place. */ |
| handleScroll(event) { |
| const target = event.target; |
| const 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`. |
| const scrolledParentNode = target === this._document ? target.documentElement : target; |
| const scrollPosition = cachedPosition.scrollPosition; |
| let newTop; |
| let newLeft; |
| if (target === this._document) { |
| const viewportScrollPosition = this._viewportRuler.getViewportScrollPosition(); |
| newTop = viewportScrollPosition.top; |
| newLeft = viewportScrollPosition.left; |
| } |
| else { |
| newTop = target.scrollTop; |
| newLeft = target.scrollLeft; |
| } |
| const topDifference = scrollPosition.top - newTop; |
| const 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((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 }; |
| } |
| } |
| |
| /** |
| * @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) { |
| const clone = node.cloneNode(true); |
| const descendantsWithId = clone.querySelectorAll('[id]'); |
| const nodeName = node.nodeName.toLowerCase(); |
| // Remove the `id` to avoid having multiple elements with the same id on the page. |
| clone.removeAttribute('id'); |
| for (let 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) { |
| const descendantElements = node.querySelectorAll(selector); |
| if (descendantElements.length) { |
| const cloneElements = clone.querySelectorAll(selector); |
| for (let i = 0; i < descendantElements.length; i++) { |
| callback(descendantElements[i], cloneElements[i]); |
| } |
| } |
| } |
| // Counter for unique cloned radio button names. |
| let 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) { |
| const 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. */ |
| const passiveEventListenerOptions = normalizePassiveListenerOptions({ passive: true }); |
| /** Options that can be used to bind an active event listener. */ |
| 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. |
| */ |
| const MOUSE_EVENT_IGNORE_TIME = 800; |
| /** |
| * Reference to a draggable item. Used to manipulate or dispose of the item. |
| */ |
| class DragRef { |
| 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(); |
| /** 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; |
| /** 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) { |
| const targetHandle = this._handles.find(handle => { |
| const 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 = (event) => { |
| const pointerPosition = this._getPointerPositionOnPage(event); |
| if (!this._hasStartedDragging) { |
| const distanceX = Math.abs(pointerPosition.x - this._pickupPositionOnPage.x); |
| const distanceY = Math.abs(pointerPosition.y - this._pickupPositionOnPage.y); |
| const 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) { |
| const isDelayElapsed = Date.now() >= this._dragStartTime + this._getDragStartDelay(event); |
| const 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())) { |
| // Prevent the default action as soon as the dragging sequence is considered as |
| // "started" since waiting for the next event can allow the device to begin scrolling. |
| event.preventDefault(); |
| 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(); |
| } |
| } |
| // We prevent the default action down here so that we know that dragging has started. This is |
| // important for touch devices where doing this too early can unnecessarily block scrolling, |
| // if there's a dragging delay. |
| event.preventDefault(); |
| const constrainedPointerPosition = this._getConstrainedPointerPosition(pointerPosition); |
| this._hasMoved = true; |
| this._lastKnownPointerPosition = pointerPosition; |
| this._updatePointerDirectionDelta(constrainedPointerPosition); |
| if (this._dropContainer) { |
| this._updateActiveDropContainer(constrainedPointerPosition, pointerPosition); |
| } |
| else { |
| const 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) { |
| 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._moveEvents.observers.length) { |
| this._ngZone.run(() => { |
| this._moveEvents.next({ |
| source: this, |
| pointerPosition: constrainedPointerPosition, |
| 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 = (event) => { |
| this._endDragSequence(event); |
| }; |
| this.withRootElement(element).withParent(_config.parentDragRef || null); |
| this._parentPositions = new ParentPositionTracker(_document, _viewportRuler); |
| _dragDropRegistry.registerDragItem(this); |
| } |
| /** Whether starting to drag this element is disabled. */ |
| get disabled() { |
| return this._disabled || !!(this._dropContainer && this._dropContainer.disabled); |
| } |
| set disabled(value) { |
| const newValue = coerceBooleanProperty(value); |
| if (newValue !== this._disabled) { |
| this._disabled = newValue; |
| this._toggleNativeDragInteractions(); |
| this._handles.forEach(handle => toggleNativeDragInteractions(handle, newValue)); |
| } |
| } |
| /** |
| * Returns the element that is being used as a placeholder |
| * while the current element is being dragged. |
| */ |
| getPlaceholderElement() { |
| return this._placeholder; |
| } |
| /** Returns the root draggable element. */ |
| getRootElement() { |
| 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. |
| */ |
| getVisibleElement() { |
| return this.isDragging() ? this.getPlaceholderElement() : this.getRootElement(); |
| } |
| /** Registers the handles that can be used to drag the element. */ |
| withHandles(handles) { |
| this._handles = handles.map(handle => coerceElement(handle)); |
| this._handles.forEach(handle => 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. |
| const disabledHandles = new Set(); |
| this._disabledHandles.forEach(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. |
| */ |
| withPreviewTemplate(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. |
| */ |
| withPlaceholderTemplate(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. |
| */ |
| withRootElement(rootElement) { |
| const element = coerceElement(rootElement); |
| if (element !== this._rootElement) { |
| if (this._rootElement) { |
| this._removeRootElementListeners(this._rootElement); |
| } |
| this._ngZone.runOutsideAngular(() => { |
| 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. |
| */ |
| withBoundaryElement(boundaryElement) { |
| this._boundaryElement = boundaryElement ? coerceElement(boundaryElement) : null; |
| this._resizeSubscription.unsubscribe(); |
| if (boundaryElement) { |
| this._resizeSubscription = this._viewportRuler |
| .change(10) |
| .subscribe(() => this._containInsideBoundaryOnResize()); |
| } |
| return this; |
| } |
| /** Sets the parent ref that the ref is nested in. */ |
| withParent(parent) { |
| this._parentDragRef = parent; |
| return this; |
| } |
| /** Removes the dragging functionality from the DOM element. */ |
| 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. |
| 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 = this._parentDragRef = null; |
| } |
| /** Checks whether the element is currently being dragged. */ |
| isDragging() { |
| return this._hasStartedDragging && this._dragDropRegistry.isDragging(this); |
| } |
| /** Resets a standalone drag item to its initial position. */ |
| 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. |
| */ |
| disableHandle(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. |
| */ |
| enableHandle(handle) { |
| if (this._disabledHandles.has(handle)) { |
| this._disabledHandles.delete(handle); |
| toggleNativeDragInteractions(handle, this.disabled); |
| } |
| } |
| /** Sets the layout direction of the draggable item. */ |
| withDirection(direction) { |
| this._direction = direction; |
| return this; |
| } |
| /** Sets the container that the item is part of. */ |
| _withDropContainer(container) { |
| this._dropContainer = container; |
| } |
| /** |
| * Gets the current position in pixels the draggable outside of a drop container. |
| */ |
| getFreeDragPosition() { |
| const 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. |
| */ |
| setFreeDragPosition(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. */ |
| _sortFromLastPointerPosition() { |
| const position = this._lastKnownPointerPosition; |
| if (position && this._dropContainer) { |
| this._updateActiveDropContainer(this._getConstrainedPointerPosition(position), position); |
| } |
| } |
| /** Unsubscribes from the global subscriptions. */ |
| _removeSubscriptions() { |
| this._pointerMoveSubscription.unsubscribe(); |
| this._pointerUpSubscription.unsubscribe(); |
| this._scrollSubscription.unsubscribe(); |
| } |
| /** Destroys the preview element and its ViewRef. */ |
| _destroyPreview() { |
| if (this._preview) { |
| removeNode(this._preview); |
| } |
| if (this._previewRef) { |
| this._previewRef.destroy(); |
| } |
| this._preview = this._previewRef = null; |
| } |
| /** Destroys the placeholder element and its ViewRef. */ |
| _destroyPlaceholder() { |
| 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. |
| */ |
| _endDragSequence(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); |
| 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(() => { |
| 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(() => { |
| this.ended.next({ |
| source: this, |
| distance: this._getDragDistance(this._getPointerPositionOnPage(event)) |
| }); |
| }); |
| this._cleanupCachedDimensions(); |
| this._dragDropRegistry.stopDragging(this); |
| } |
| } |
| /** Starts the dragging sequence. */ |
| _startDragSequence(event) { |
| if (isTouchEvent(event)) { |
| this._lastTouchEventTime = Date.now(); |
| } |
| this._toggleNativeDragInteractions(); |
| const dropContainer = this._dropContainer; |
| if (dropContainer) { |
| const element = this._rootElement; |
| const parent = element.parentNode; |
| const preview = this._preview = this._createPreviewElement(); |
| const placeholder = this._placeholder = this._createPlaceholderElement(); |
| const anchor = this._anchor = this._anchor || this._document.createComment(''); |
| // Needs to happen before the root element is moved. |
| const 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. |
| */ |
| _initializeDragSequence(referenceElement, event) { |
| // Stop propagation if the item is inside another |
| // draggable so we don't start multiple drag sequences. |
| if (this._parentDragRef) { |
| event.stopPropagation(); |
| } |
| const isDragging = this.isDragging(); |
| const isTouchSequence = isTouchEvent(event); |
| const isAuxiliaryMouseButton = !isTouchSequence && event.button !== 0; |
| const rootElement = this._rootElement; |
| 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 && 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(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. |
| const previewTemplate = this._previewTemplate; |
| this._pickupPositionInElement = previewTemplate && previewTemplate.template && |
| !previewTemplate.matchSize ? { x: 0, y: 0 } : |
| this._getPointerPositionInElement(referenceElement, event); |
| const 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. */ |
| _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. |
| 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(() => { |
| const container = this._dropContainer; |
| const currentIndex = container.getItemIndex(this); |
| const pointerPosition = this._getPointerPositionOnPage(event); |
| const distance = this._getDragDistance(this._getPointerPositionOnPage(event)); |
| const isPointerOverContainer = container._isOverContainer(pointerPosition.x, pointerPosition.y); |
| this.ended.next({ source: this, distance }); |
| this.dropped.next({ |
| item: this, |
| currentIndex, |
| previousIndex: this._initialIndex, |
| container: container, |
| previousContainer: this._initialContainer, |
| isPointerOverContainer, |
| 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. |
| */ |
| _updateActiveDropContainer({ x, y }, { x: rawX, y: rawY }) { |
| // Drop container that draggable has been moved into. |
| let 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(() => { |
| // 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. |
| */ |
| _createPreviewElement() { |
| const previewConfig = this._previewTemplate; |
| const previewClass = this.previewClass; |
| const previewTemplate = previewConfig ? previewConfig.template : null; |
| let preview; |
| if (previewTemplate && previewConfig) { |
| // Measure the element before we've inserted the preview |
| // since the insertion could throw off the measurement. |
| const rootRect = previewConfig.matchSize ? this._rootElement.getBoundingClientRect() : null; |
| const viewRef = previewConfig.viewContainer.createEmbeddedView(previewTemplate, previewConfig.context); |
| viewRef.detectChanges(); |
| preview = getRootNode(viewRef, this._document); |
| this._previewRef = viewRef; |
| if (previewConfig.matchSize) { |
| matchElementSize(preview, rootRect); |
| } |
| else { |
| preview.style.transform = |
| getTransform(this._pickupPositionOnPage.x, this._pickupPositionOnPage.y); |
| } |
| } |
| else { |
| const element = this._rootElement; |
| preview = deepCloneNode(element); |
| matchElementSize(preview, element.getBoundingClientRect()); |
| } |
| 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: `${this._config.zIndex || 1000}` |
| }); |
| toggleNativeDragInteractions(preview, false); |
| preview.classList.add('cdk-drag-preview'); |
| preview.setAttribute('dir', this._direction); |
| if (previewClass) { |
| if (Array.isArray(previewClass)) { |
| previewClass.forEach(className => 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. |
| */ |
| _animatePreviewToPlaceholder() { |
| // If the user hasn't moved yet, the transitionend event won't fire. |
| if (!this._hasMoved) { |
| return Promise.resolve(); |
| } |
| 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. |
| const duration = getTransformTransitionDurationInMs(this._preview); |
| if (duration === 0) { |
| return Promise.resolve(); |
| } |
| return this._ngZone.runOutsideAngular(() => { |
| return new Promise(resolve => { |
| const handler = ((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. |
| const 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. */ |
| _createPlaceholderElement() { |
| const placeholderConfig = this._placeholderTemplate; |
| const placeholderTemplate = placeholderConfig ? placeholderConfig.template : null; |
| let placeholder; |
| if (placeholderTemplate) { |
| this._placeholderRef = placeholderConfig.viewContainer.createEmbeddedView(placeholderTemplate, placeholderConfig.context); |
| this._placeholderRef.detectChanges(); |
| 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. |
| */ |
| _getPointerPositionInElement(referenceElement, event) { |
| const elementRect = this._rootElement.getBoundingClientRect(); |
| const handleElement = referenceElement === this._rootElement ? null : referenceElement; |
| const referenceRect = handleElement ? handleElement.getBoundingClientRect() : elementRect; |
| const point = isTouchEvent(event) ? event.targetTouches[0] : event; |
| const scrollPosition = this._getViewportScrollPosition(); |
| const x = point.pageX - referenceRect.left - scrollPosition.left; |
| const y = point.pageY - referenceRect.top - 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. */ |
| _getPointerPositionOnPage(event) { |
| const scrollPosition = this._getViewportScrollPosition(); |
| const point = isTouchEvent(event) ? |
| // `touches` will be empty for start/end events so we have to fall back to `changedTouches`. |
| // Also note that on real devices we're guaranteed for either `touches` or `changedTouches` |
| // to have a value, but Firefox in device emulation mode has a bug where both can be empty |
| // for `touchstart` and `touchend` so we fall back to a dummy object in order to avoid |
| // throwing an error. The value returned here will be incorrect, but since this only |
| // breaks inside a developer tool and the value is only used for secondary information, |
| // we can get away with it. See https://bugzilla.mozilla.org/show_bug.cgi?id=1615824. |
| (event.touches[0] || event.changedTouches[0] || { pageX: 0, pageY: 0 }) : event; |
| const x = point.pageX - scrollPosition.left; |
| const y = point.pageY - scrollPosition.top; |
| // if dragging SVG element, try to convert from the screen coordinate system to the SVG |
| // coordinate system |
| if (this._ownerSVGElement) { |
| const svgMatrix = this._ownerSVGElement.getScreenCTM(); |
| if (svgMatrix) { |
| const svgPoint = this._ownerSVGElement.createSVGPoint(); |
| svgPoint.x = x; |
| svgPoint.y = y; |
| return svgPoint.matrixTransform(svgMatrix.inverse()); |
| } |
| } |
| return { x, y }; |
| } |
| /** Gets the pointer position on the page, accounting for any position constraints. */ |
| _getConstrainedPointerPosition(point) { |
| const dropContainerLock = this._dropContainer ? this._dropContainer.lockAxis : null; |
| let { x, y } = this.constrainPosition ? this.constrainPosition(point, this) : point; |
| if (this.lockAxis === 'x' || dropContainerLock === 'x') { |
| y = this._pickupPositionOnPage.y; |
| } |
| else if (this.lockAxis === 'y' || dropContainerLock === 'y') { |
| x = this._pickupPositionOnPage.x; |
| } |
| if (this._boundaryRect) { |
| const { x: pickupX, y: pickupY } = this._pickupPositionInElement; |
| const boundaryRect = this._boundaryRect; |
| const previewRect = this._previewRect; |
| const minY = boundaryRect.top + pickupY; |
| const maxY = boundaryRect.bottom - (previewRect.height - pickupY); |
| const minX = boundaryRect.left + pickupX; |
| const maxX = boundaryRect.right - (previewRect.width - pickupX); |
| x = clamp(x, minX, maxX); |
| y = clamp(y, minY, maxY); |
| } |
| return { x, y }; |
| } |
| /** Updates the current drag delta, based on the user's current pointer position on the page. */ |
| _updatePointerDirectionDelta(pointerPositionOnPage) { |
| const { x, y } = pointerPositionOnPage; |
| const delta = this._pointerDirectionDelta; |
| const positionSinceLastChange = this._pointerPositionAtLastDirectionChange; |
| // Amount of pixels the user has dragged since the last time the direction changed. |
| const changeX = Math.abs(x - positionSinceLastChange.x); |
| 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. */ |
| _toggleNativeDragInteractions() { |
| if (!this._rootElement || !this._handles) { |
| return; |
| } |
| const 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. */ |
| _removeRootElementListeners(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. |
| */ |
| _applyRootElementTransform(x, y) { |
| const 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. |
| */ |
| _getDragDistance(currentPosition) { |
| const 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. */ |
| _cleanupCachedDimensions() { |
| this._boundaryRect = this._previewRect = undefined; |
| this._parentPositions.clear(); |
| } |
| /** |
| * 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. |
| */ |
| _containInsideBoundaryOnResize() { |
| let { x, y } = this._passiveTransform; |
| if ((x === 0 && y === 0) || this.isDragging() || !this._boundaryElement) { |
| return; |
| } |
| const boundaryRect = this._boundaryElement.getBoundingClientRect(); |
| const 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; |
| } |
| const leftOverflow = boundaryRect.left - elementRect.left; |
| const rightOverflow = elementRect.right - boundaryRect.right; |
| const topOverflow = boundaryRect.top - elementRect.top; |
| const 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, x }); |
| } |
| } |
| /** Gets the drag start delay, based on the event type. */ |
| _getDragStartDelay(event) { |
| const value = this.dragStartDelay; |
| if (typeof value === 'number') { |
| return value; |
| } |
| else if (isTouchEvent(event)) { |
| return value.touch; |
| } |
| return value ? value.mouse : 0; |
| } |
| /** Updates the internal state of the draggable element when scrolling has occurred. */ |
| _updateOnScroll(event) { |
| const scrollDifference = this._parentPositions.handleScroll(event); |
| if (scrollDifference) { |
| const target = event.target; |
| // ClientRect dimensions are based on the scroll position of the page and its parent node so |
| // we have to update the cached boundary ClientRect if the user has scrolled. Check for |
| // the `document` specifically since IE doesn't support `contains` on it. |
| if (this._boundaryRect && (target === this._document || |
| (target !== this._boundaryElement && target.contains(this._boundaryElement)))) { |
| adjustClientRect(this._boundaryRect, scrollDifference.top, scrollDifference.left); |
| } |
| this._pickupPositionOnPage.x += scrollDifference.left; |
| this._pickupPositionOnPage.y += scrollDifference.top; |
| // If we're in free drag mode, we have to update the active transform, because |
| // it isn't relative to the viewport like the preview inside a drop list. |
| if (!this._dropContainer) { |
| this._activeTransform.x -= scrollDifference.left; |
| this._activeTransform.y -= scrollDifference.top; |
| this._applyRootElementTransform(this._activeTransform.x, this._activeTransform.y); |
| } |
| } |
| } |
| /** Gets the scroll position of the viewport. */ |
| _getViewportScrollPosition() { |
| const cachedPosition = this._parentPositions.positions.get(this._document); |
| return cachedPosition ? cachedPosition.scrollPosition : |
| this._viewportRuler.getViewportScrollPosition(); |
| } |
| /** |
| * Lazily resolves and returns the shadow root of the element. We do this in a function, rather |
| * than saving it in property directly on init, because we want to resolve it as late as possible |
| * in order to ensure that the element has been moved into the shadow DOM. Doing it inside the |
| * constructor might be too early if the element is inside of something like `ngFor` or `ngIf`. |
| */ |
| _getShadowRoot() { |
| if (this._cachedShadowRoot === undefined) { |
| this._cachedShadowRoot = _getShadowRoot(this._rootElement); |
| } |
| return this._cachedShadowRoot; |
| } |
| } |
| /** |
| * 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)`; |
| } |
| /** 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, shadowRoot) { |
| // 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 shadowRoot || |
| 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) { |
| const rootNodes = viewRef.rootNodes; |
| if (rootNodes.length === 1 && rootNodes[0].nodeType === _document.ELEMENT_NODE) { |
| return rootNodes[0]; |
| } |
| const wrapper = _document.createElement('div'); |
| rootNodes.forEach(node => wrapper.appendChild(node)); |
| return wrapper; |
| } |
| /** |
| * Matches the target element's size to the source's size. |
| * @param target Element that needs to be resized. |
| * @param sourceRect Dimensions of the source element. |
| */ |
| function matchElementSize(target, sourceRect) { |
| target.style.width = `${sourceRect.width}px`; |
| target.style.height = `${sourceRect.height}px`; |
| target.style.transform = getTransform(sourceRect.left, sourceRect.top); |
| } |
| |
| /** |
| * @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 |
| */ |
| /** |
| * Moves an item one index in an array to another. |
| * @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. |
| */ |
| function moveItemInArray(array, fromIndex, toIndex) { |
| const from = clamp$1(fromIndex, array.length - 1); |
| const to = clamp$1(toIndex, array.length - 1); |
| if (from === to) { |
| return; |
| } |
| const target = array[from]; |
| 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. |
| * @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. |
| */ |
| function transferArrayItem(currentArray, targetArray, currentIndex, targetIndex) { |
| const from = clamp$1(currentIndex, currentArray.length - 1); |
| 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. |
| * @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. |
| * |
| */ |
| function copyArrayItem(currentArray, targetArray, currentIndex, targetIndex) { |
| const to = clamp$1(targetIndex, targetArray.length); |
| if (currentArray.length) { |
| targetArray.splice(to, 0, currentArray[currentIndex]); |
| } |
| } |
| /** Clamps a number between zero and a maximum. */ |
| function clamp$1(value, max) { |
| return Math.max(0, Math.min(max, value)); |
| } |
| |
| /** |
| * @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 |
| */ |
| /** |
| * Proximity, as a ratio to width/height, at which a |
| * dragged item will affect the drop container. |
| */ |
| const DROP_PROXIMITY_THRESHOLD = 0.05; |
| /** |
| * Proximity, as a ratio to width/height at which to start auto-scrolling the drop list or the |
| * viewport. The value comes from trying it out manually until it feels right. |
| */ |
| const SCROLL_PROXIMITY_THRESHOLD = 0.05; |
| /** |
| * Reference to a drop list. Used to manipulate or dispose of the container. |
| */ |
| class DropListRef { |
| constructor(element, _dragDropRegistry, _document, _ngZone, _viewportRuler) { |
| this._dragDropRegistry = _dragDropRegistry; |
| this._ngZone = _ngZone; |
| this._viewportRuler = _viewportRuler; |
| /** Whether starting a dragging sequence from this container is disabled. */ |
| this.disabled = false; |
| /** Whether sorting items within the list is disabled. */ |
| this.sortingDisabled = false; |
| /** |
| * Whether auto-scrolling the view when the user |
| * moves their pointer close to the edges is disabled. |
| */ |
| this.autoScrollDisabled = false; |
| /** Number of pixels to scroll for each frame when auto-scrolling an element. */ |
| this.autoScrollStep = 2; |
| /** |
| * Function that is used to determine whether an item |
| * is allowed to be moved into a drop container. |
| */ |
| this.enterPredicate = () => true; |
| /** Functions that is used to determine whether an item can be sorted into a particular index. */ |
| this.sortPredicate = () => 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 dragged item, as well as what direction |
| * the pointer was moving in when the swap occured and whether the user's pointer continued to |
| * overlap with the swapped item after the swapping occurred. |
| */ |
| this._previousSwap = { drag: null, delta: 0, overlaps: false }; |
| /** Draggable items in the container. */ |
| this._draggables = []; |
| /** Drop lists that are connected to the current one. */ |
| this._siblings = []; |
| /** Direction in which the list is oriented. */ |
| this._orientation = 'vertical'; |
| /** Connected siblings that currently have a dragged item. */ |
| this._activeSiblings = new Set(); |
| /** Layout direction of the drop list. */ |
| this._direction = 'ltr'; |
| /** Subscription to the window being scrolled. */ |
| this._viewportScrollSubscription = Subscription.EMPTY; |
| /** Vertical direction in which the list is currently scrolling. */ |
| this._verticalScrollDirection = 0 /* NONE */; |
| /** Horizontal direction in which the list is currently scrolling. */ |
| this._horizontalScrollDirection = 0 /* NONE */; |
| /** Used to signal to the current auto-scroll sequence when to stop. */ |
| this._stopScrollTimers = new Subject(); |
| /** Shadow root of the current element. Necessary for `elementFromPoint` to resolve correctly. */ |
| this._cachedShadowRoot = null; |
| /** Starts the interval that'll auto-scroll the element. */ |
| this._startScrollInterval = () => { |
| this._stopScrolling(); |
| interval(0, animationFrameScheduler) |
| .pipe(takeUntil(this._stopScrollTimers)) |
| .subscribe(() => { |
| const node = this._scrollNode; |
| const scrollStep = this.autoScrollStep; |
| if (this._verticalScrollDirection === 1 /* UP */) { |
| incrementVerticalScroll(node, -scrollStep); |
| } |
| else if (this._verticalScrollDirection === 2 /* DOWN */) { |
| incrementVerticalScroll(node, scrollStep); |
| } |
| if (this._horizontalScrollDirection === 1 /* LEFT */) { |
| incrementHorizontalScroll(node, -scrollStep); |
| } |
| else if (this._horizontalScrollDirection === 2 /* RIGHT */) { |
| incrementHorizontalScroll(node, scrollStep); |
| } |
| }); |
| }; |
| this.element = coerceElement(element); |
| this._document = _document; |
| this.withScrollableParents([this.element]); |
| _dragDropRegistry.registerDropContainer(this); |
| this._parentPositions = new ParentPositionTracker(_document, _viewportRuler); |
| } |
| /** Removes the drop list functionality from the DOM element. */ |
| dispose() { |
| this._stopScrolling(); |
| this._stopScrollTimers.complete(); |
| this._viewportScrollSubscription.unsubscribe(); |
| this.beforeStarted.complete(); |
| this.entered.complete(); |
| this.exited.complete(); |
| this.dropped.complete(); |
| this.sorted.complete(); |
| this._activeSiblings.clear(); |
| this._scrollNode = null; |
| this._parentPositions.clear(); |
| this._dragDropRegistry.removeDropContainer(this); |
| } |
| /** Whether an item from this list is currently being dragged. */ |
| isDragging() { |
| return this._isDragging; |
| } |
| /** Starts dragging an item. */ |
| start() { |
| this._draggingStarted(); |
| this._notifyReceivingSiblings(); |
| } |
| /** |
| * Emits an event to indicate that the user moved an item into the container. |
| * @param item Item that was moved into the container. |
| * @param pointerX Position of the item along the X axis. |
| * @param pointerY Position of the item along the Y axis. |
| * @param index Index at which the item entered. If omitted, the container will try to figure it |
| * out automatically. |
| */ |
| enter(item, pointerX, pointerY, index) { |
| this._draggingStarted(); |
| // If sorting is disabled, we want the item to return to its starting |
| // position if the user is returning it to its initial container. |
| let newIndex; |
| if (index == null) { |
| newIndex = this.sortingDisabled ? this._draggables.indexOf(item) : -1; |
| if (newIndex === -1) { |
| // We use the coordinates of where the item entered the drop |
| // zone to figure out at which index it should be inserted. |
| newIndex = this._getItemIndexFromPointerPosition(item, pointerX, pointerY); |
| } |
| } |
| else { |
| newIndex = index; |
| } |
| const activeDraggables = this._activeDraggables; |
| const currentIndex = activeDraggables.indexOf(item); |
| const placeholder = item.getPlaceholderElement(); |
| let newPositionReference = activeDraggables[newIndex]; |
| // If the item at the new position is the same as the item that is being dragged, |
| // it means that we're trying to restore the item to its initial position. In this |
| // case we should use the next item from the list as the reference. |
| if (newPositionReference === item) { |
| newPositionReference = activeDraggables[newIndex + 1]; |
| } |
| // Since the item may be in the `activeDraggables` already (e.g. if the user dragged it |
| // into another container and back again), we have to ensure that it isn't duplicated. |
| if (currentIndex > -1) { |
| activeDraggables.splice(currentIndex, 1); |
| } |
| // Don't use items that are being dragged as a reference, because |
| // their element has been moved down to the bottom of the body. |
| if (newPositionReference && !this._dragDropRegistry.isDragging(newPositionReference)) { |
| const element = newPositionReference.getRootElement(); |
| element.parentElement.insertBefore(placeholder, element); |
| activeDraggables.splice(newIndex, 0, item); |
| } |
| else if (this._shouldEnterAsFirstChild(pointerX, pointerY)) { |
| const reference = activeDraggables[0].getRootElement(); |
| reference.parentNode.insertBefore(placeholder, reference); |
| activeDraggables.unshift(item); |
| } |
| else { |
| coerceElement(this.element).appendChild(placeholder); |
| activeDraggables.push(item); |
| } |
| // The transform needs to be cleared so it doesn't throw off the measurements. |
| placeholder.style.transform = ''; |
| // Note that the positions were already cached when we called `start` above, |
| // but we need to refresh them since the amount of items has changed and also parent rects. |
| this._cacheItemPositions(); |
| this._cacheParentPositions(); |
| // Notify siblings at the end so that the item has been inserted into the `activeDraggables`. |
| this._notifyReceivingSiblings(); |
| this.entered.next({ item, container: this, currentIndex: this.getItemIndex(item) }); |
| } |
| /** |
| * Removes an item from the container after it was dragged into another container by the user. |
| * @param item Item that was dragged out. |
| */ |
| exit(item) { |
| this._reset(); |
| this.exited.next({ item, container: this }); |
| } |
| /** |
| * Drops an item into this container. |
| * @param item Item being dropped into the container. |
| * @param currentIndex Index at which the item should be inserted. |
| * @param previousIndex Index of the item when dragging started. |
| * @param previousContainer Container from which the item got dragged in. |
| * @param isPointerOverContainer Whether the user's pointer was over the |
| * container when the item was dropped. |
| * @param distance Distance the user has dragged since the start of the dragging sequence. |
| */ |
| drop(item, currentIndex, previousIndex, previousContainer, isPointerOverContainer, distance) { |
| this._reset(); |
| this.dropped.next({ |
| item, |
| currentIndex, |
| previousIndex, |
| container: this, |
| previousContainer, |
| isPointerOverContainer, |
| distance |
| }); |
| } |
| /** |
| * Sets the draggable items that are a part of this list. |
| * @param items Items that are a part of this list. |
| */ |
| withItems(items) { |
| const previousItems = this._draggables; |
| this._draggables = items; |
| items.forEach(item => item._withDropContainer(this)); |
| if (this.isDragging()) { |
| const draggedItems = previousItems.filter(item => item.isDragging()); |
| // If all of the items being dragged were removed |
| // from the list, abort the current drag sequence. |
| if (draggedItems.every(item => items.indexOf(item) === -1)) { |
| this._reset(); |
| } |
| else { |
| this._cacheItems(); |
| } |
| } |
| return this; |
| } |
| /** Sets the layout direction of the drop list. */ |
| withDirection(direction) { |
| this._direction = direction; |
| return this; |
| } |
| /** |
| * Sets the containers that are connected to this one. When two or more containers are |
| * connected, the user will be allowed to transfer items between them. |
| * @param connectedTo Other containers that the current containers should be connected to. |
| */ |
| connectedTo(connectedTo) { |
| this._siblings = connectedTo.slice(); |
| return this; |
| } |
| /** |
| * Sets the orientation of the container. |
| * @param orientation New orientation for the container. |
| */ |
| withOrientation(orientation) { |
| this._orientation = orientation; |
| return this; |
| } |
| /** |
| * Sets which parent elements are can be scrolled while the user is dragging. |
| * @param elements Elements that can be scrolled. |
| */ |
| withScrollableParents(elements) { |
| const element = coerceElement(this.element); |
| // We always allow the current element to be scrollable |
| // so we need to ensure that it's in the array. |
| this._scrollableElements = |
| elements.indexOf(element) === -1 ? [element, ...elements] : elements.slice(); |
| return this; |
| } |
| /** Gets the scrollable parents that are registered with this drop container. */ |
| getScrollableParents() { |
| return this._scrollableElements; |
| } |
| /** |
| * Figures out the index of an item in the container. |
| * @param item Item whose index should be determined. |
| */ |
| getItemIndex(item) { |
| if (!this._isDragging) { |
| return this._draggables.indexOf(item); |
| } |
| // Items are sorted always by top/left in the cache, however they flow differently in RTL. |
| // The rest of the logic still stands no matter what orientation we're in, however |
| // we need to invert the array when determining the index. |
| const items = this._orientation === 'horizontal' && this._direction === 'rtl' ? |
| this._itemPositions.slice().reverse() : this._itemPositions; |
| return findIndex(items, currentItem => currentItem.drag === item); |
| } |
| /** |
| * Whether the list is able to receive the item that |
| * is currently being dragged inside a connected drop list. |
| */ |
| isReceiving() { |
| return this._activeSiblings.size > 0; |
| } |
| /** |
| * Sorts an item inside the container based on its position. |
| * @param item Item to be sorted. |
| * @param pointerX Position of the item along the X axis. |
| * @param pointerY Position of the item along the Y axis. |
| * @param pointerDelta Direction in which the pointer is moving along each axis. |
| */ |
| _sortItem(item, pointerX, pointerY, pointerDelta) { |
| // Don't sort the item if sorting is disabled or it's out of range. |
| if (this.sortingDisabled || !this._clientRect || |
| !isPointerNearClientRect(this._clientRect, DROP_PROXIMITY_THRESHOLD, pointerX, pointerY)) { |
| return; |
| } |
| const siblings = this._itemPositions; |
| const newIndex = this._getItemIndexFromPointerPosition(item, pointerX, pointerY, pointerDelta); |
| if (newIndex === -1 && siblings.length > 0) { |
| return; |
| } |
| const isHorizontal = this._orientation === 'horizontal'; |
| const currentIndex = findIndex(siblings, currentItem => currentItem.drag === item); |
| const siblingAtNewPosition = siblings[newIndex]; |
| const currentPosition = siblings[currentIndex].clientRect; |
| const newPosition = siblingAtNewPosition.clientRect; |
| const delta = currentIndex > newIndex ? 1 : -1; |
| // How many pixels the item's placeholder should be offset. |
| const itemOffset = this._getItemOffsetPx(currentPosition, newPosition, delta); |
| // How many pixels all the other items should be offset. |
| const siblingOffset = this._getSiblingOffsetPx(currentIndex, siblings, delta); |
| // Save the previous order of the items before moving the item to its new index. |
| // We use this to check whether an item has been moved as a result of the sorting. |
| const oldOrder = siblings.slice(); |
| // Shuffle the array in place. |
| moveItemInArray(siblings, currentIndex, newIndex); |
| this.sorted.next({ |
| previousIndex: currentIndex, |
| currentIndex: newIndex, |
| container: this, |
| item |
| }); |
| siblings.forEach((sibling, index) => { |
| // Don't do anything if the position hasn't changed. |
| if (oldOrder[index] === sibling) { |
| return; |
| } |
| const isDraggedItem = sibling.drag === item; |
| const offset = isDraggedItem ? itemOffset : siblingOffset; |
| const elementToOffset = isDraggedItem ? item.getPlaceholderElement() : |
| sibling.drag.getRootElement(); |
| // Update the offset to reflect the new position. |
| sibling.offset += offset; |
| // Since we're moving the items with a `transform`, we need to adjust their cached |
| // client rects to reflect their new position, as well as swap their positions in the cache. |
| // Note that we shouldn't use `getBoundingClientRect` here to update the cache, because the |
| // elements may be mid-animation which will give us a wrong result. |
| if (isHorizontal) { |
| // Round the transforms since some browsers will |
| // blur the elements, for sub-pixel transforms. |
| elementToOffset.style.transform = `translate3d(${Math.round(sibling.offset)}px, 0, 0)`; |
| adjustClientRect(sibling.clientRect, 0, offset); |
| } |
| else { |
| elementToOffset.style.transform = `translate3d(0, ${Math.round(sibling.offset)}px, 0)`; |
| adjustClientRect(sibling.clientRect, offset, 0); |
| } |
| }); |
| // Note that it's important that we do this after the client rects have been adjusted. |
| this._previousSwap.overlaps = isInsideClientRect(newPosition, pointerX, pointerY); |
| this._previousSwap.drag = siblingAtNewPosition.drag; |
| this._previousSwap.delta = isHorizontal ? pointerDelta.x : pointerDelta.y; |
| } |
| /** |
| * Checks whether the user's pointer is close to the edges of either the |
| * viewport or the drop list and starts the auto-scroll sequence. |
| * @param pointerX User's pointer position along the x axis. |
| * @param pointerY User's pointer position along the y axis. |
| */ |
| _startScrollingIfNecessary(pointerX, pointerY) { |
| if (this.autoScrollDisabled) { |
| return; |
| } |
| let scrollNode; |
| let verticalScrollDirection = 0 /* NONE */; |
| let horizontalScrollDirection = 0 /* NONE */; |
| // Check whether we should start scrolling any of the parent containers. |
| this._parentPositions.positions.forEach((position, element) => { |
| // We have special handling for the `document` below. Also this would be |
| // nicer with a for...of loop, but it requires changing a compiler flag. |
| if (element === this._document || !position.clientRect || scrollNode) { |
| return; |
| } |
| if (isPointerNearClientRect(position.clientRect, DROP_PROXIMITY_THRESHOLD, pointerX, pointerY)) { |
| [verticalScrollDirection, horizontalScrollDirection] = getElementScrollDirections(element, position.clientRect, pointerX, pointerY); |
| if (verticalScrollDirection || horizontalScrollDirection) { |
| scrollNode = element; |
| } |
| } |
| }); |
| // Otherwise check if we can start scrolling the viewport. |
| if (!verticalScrollDirection && !horizontalScrollDirection) { |
| const { width, height } = this._viewportRuler.getViewportSize(); |
| const clientRect = { width, height, top: 0, right: width, bottom: height, left: 0 }; |
| verticalScrollDirection = getVerticalScrollDirection(clientRect, pointerY); |
| horizontalScrollDirection = getHorizontalScrollDirection(clientRect, pointerX); |
| scrollNode = window; |
| } |
| if (scrollNode && (verticalScrollDirection !== this._verticalScrollDirection || |
| horizontalScrollDirection !== this._horizontalScrollDirection || |
| scrollNode !== this._scrollNode)) { |
| this._verticalScrollDirection = verticalScrollDirection; |
| this._horizontalScrollDirection = horizontalScrollDirection; |
| this._scrollNode = scrollNode; |
| if ((verticalScrollDirection || horizontalScrollDirection) && scrollNode) { |
| this._ngZone.runOutsideAngular(this._startScrollInterval); |
| } |
| else { |
| this._stopScrolling(); |
| } |
| } |
| } |
| /** Stops any currently-running auto-scroll sequences. */ |
| _stopScrolling() { |
| this._stopScrollTimers.next(); |
| } |
| /** Starts the dragging sequence within the list. */ |
| _draggingStarted() { |
| const styles = coerceElement(this.element).style; |
| this.beforeStarted.next(); |
| this._isDragging = true; |
| // We need to disable scroll snapping while the user is dragging, because it breaks automatic |
| // scrolling. The browser seems to round the value based on the snapping points which means |
| // that we can't increment/decrement the scroll position. |
| this._initialScrollSnap = styles.msScrollSnapType || styles.scrollSnapType || ''; |
| styles.scrollSnapType = styles.msScrollSnapType = 'none'; |
| this._cacheItems(); |
| this._viewportScrollSubscription.unsubscribe(); |
| this._listenToScrollEvents(); |
| } |
| /** Caches the positions of the configured scrollable parents. */ |
| _cacheParentPositions() { |
| const element = coerceElement(this.element); |
| this._parentPositions.cache(this._scrollableElements); |
| // The list element is always in the `scrollableElements` |
| // so we can take advantage of the cached `ClientRect`. |
| this._clientRect = this._parentPositions.positions.get(element).clientRect; |
| } |
| /** Refreshes the position cache of the items and sibling containers. */ |
| _cacheItemPositions() { |
| const isHorizontal = this._orientation === 'horizontal'; |
| this._itemPositions = this._activeDraggables.map(drag => { |
| const elementToMeasure = drag.getVisibleElement(); |
| return { drag, offset: 0, clientRect: getMutableClientRect(elementToMeasure) }; |
| }).sort((a, b) => { |
| return isHorizontal ? a.clientRect.left - b.clientRect.left : |
| a.clientRect.top - b.clientRect.top; |
| }); |
| } |
| /** Resets the container to its initial state. */ |
| _reset() { |
| this._isDragging = false; |
| const styles = coerceElement(this.element).style; |
| styles.scrollSnapType = styles.msScrollSnapType = this._initialScrollSnap; |
| // TODO(crisbeto): may have to wait for the animations to finish. |
| this._activeDraggables.forEach(item => { |
| const rootElement = item.getRootElement(); |
| if (rootElement) { |
| rootElement.style.transform = ''; |
| } |
| }); |
| this._siblings.forEach(sibling => sibling._stopReceiving(this)); |
| this._activeDraggables = []; |
| this._itemPositions = []; |
| this._previousSwap.drag = null; |
| this._previousSwap.delta = 0; |
| this._previousSwap.overlaps = false; |
| this._stopScrolling(); |
| this._viewportScrollSubscription.unsubscribe(); |
| this._parentPositions.clear(); |
| } |
| /** |
| * Gets the offset in pixels by which the items that aren't being dragged should be moved. |
| * @param currentIndex Index of the item currently being dragged. |
| * @param siblings All of the items in the list. |
| * @param delta Direction in which the user is moving. |
| */ |
| _getSiblingOffsetPx(currentIndex, siblings, delta) { |
| const isHorizontal = this._orientation === 'horizontal'; |
| const currentPosition = siblings[currentIndex].clientRect; |
| const immediateSibling = siblings[currentIndex + delta * -1]; |
| let siblingOffset = currentPosition[isHorizontal ? 'width' : 'height'] * delta; |
| if (immediateSibling) { |
| const start = isHorizontal ? 'left' : 'top'; |
| const end = isHorizontal ? 'right' : 'bottom'; |
| // Get the spacing between the start of the current item and the end of the one immediately |
| // after it in the direction in which the user is dragging, or vice versa. We add it to the |
| // offset in order to push the element to where it will be when it's inline and is influenced |
| // by the `margin` of its siblings. |
| if (delta === -1) { |
| siblingOffset -= immediateSibling.clientRect[start] - currentPosition[end]; |
| } |
| else { |
| siblingOffset += currentPosition[start] - immediateSibling.clientRect[end]; |
| } |
| } |
| return siblingOffset; |
| } |
| /** |
| * Gets the offset in pixels by which the item that is being dragged should be moved. |
| * @param currentPosition Current position of the item. |
| * @param newPosition Position of the item where the current item should be moved. |
| * @param delta Direction in which the user is moving. |
| */ |
| _getItemOffsetPx(currentPosition, newPosition, delta) { |
| const isHorizontal = this._orientation === 'horizontal'; |
| let itemOffset = isHorizontal ? newPosition.left - currentPosition.left : |
| newPosition.top - currentPosition.top; |
| // Account for differences in the item width/height. |
| if (delta === -1) { |
| itemOffset += isHorizontal ? newPosition.width - currentPosition.width : |
| newPosition.height - currentPosition.height; |
| } |
| return itemOffset; |
| } |
| /** |
| * Checks if pointer is entering in the first position |
| * @param pointerX Position of the user's pointer along the X axis. |
| * @param pointerY Position of the user's pointer along the Y axis. |
| */ |
| _shouldEnterAsFirstChild(pointerX, pointerY) { |
| if (!this._activeDraggables.length) { |
| return false; |
| } |
| const itemPositions = this._itemPositions; |
| const isHorizontal = this._orientation === 'horizontal'; |
| // `itemPositions` are sorted by position while `activeDraggables` are sorted by child index |
| // check if container is using some sort of "reverse" ordering (eg: flex-direction: row-reverse) |
| const reversed = itemPositions[0].drag !== this._activeDraggables[0]; |
| if (reversed) { |
| const lastItemRect = itemPositions[itemPositions.length - 1].clientRect; |
| return isHorizontal ? pointerX >= lastItemRect.right : pointerY >= lastItemRect.bottom; |
| } |
| else { |
| const firstItemRect = itemPositions[0].clientRect; |
| return isHorizontal ? pointerX <= firstItemRect.left : pointerY <= firstItemRect.top; |
| } |
| } |
| /** |
| * Gets the index of an item in the drop container, based on the position of the user's pointer. |
| * @param item Item that is being sorted. |
| * @param pointerX Position of the user's pointer along the X axis. |
| * @param pointerY Position of the user's pointer along the Y axis. |
| * @param delta Direction in which the user is moving their pointer. |
| */ |
| _getItemIndexFromPointerPosition(item, pointerX, pointerY, delta) { |
| const isHorizontal = this._orientation === 'horizontal'; |
| const index = findIndex(this._itemPositions, ({ drag, clientRect }, _, array) => { |
| if (drag === item) { |
| // If there's only one item left in the container, it must be |
| // the dragged item itself so we use it as a reference. |
| return array.length < 2; |
| } |
| if (delta) { |
| const direction = isHorizontal ? delta.x : delta.y; |
| // If the user is still hovering over the same item as last time, their cursor hasn't left |
| // the item after we made the swap, and they didn't change the direction in which they're |
| // dragging, we don't consider it a direction swap. |
| if (drag === this._previousSwap.drag && this._previousSwap.overlaps && |
| direction === this._previousSwap.delta) { |
| return false; |
| } |
| } |
| return isHorizontal ? |
| // Round these down since most browsers report client rects with |
| // sub-pixel precision, whereas the pointer coordinates are rounded to pixels. |
| pointerX >= Math.floor(clientRect.left) && pointerX < Math.floor(clientRect.right) : |
| pointerY >= Math.floor(clientRect.top) && pointerY < Math.floor(clientRect.bottom); |
| }); |
| return (index === -1 || !this.sortPredicate(index, item, this)) ? -1 : index; |
| } |
| /** Caches the current items in the list and their positions. */ |
| _cacheItems() { |
| this._activeDraggables = this._draggables.slice(); |
| this._cacheItemPositions(); |
| this._cacheParentPositions(); |
| } |
| /** |
| * Checks whether the user's pointer is positioned over the container. |
| * @param x Pointer position along the X axis. |
| * @param y Pointer position along the Y axis. |
| */ |
| _isOverContainer(x, y) { |
| return this._clientRect != null && isInsideClientRect(this._clientRect, x, y); |
| } |
| /** |
| * Figures out whether an item should be moved into a sibling |
| * drop container, based on its current position. |
| * @param item Drag item that is being moved. |
| * @param x Position of the item along the X axis. |
| * @param y Position of the item along the Y axis. |
| */ |
| _getSiblingContainerFromPosition(item, x, y) { |
| return this._siblings.find(sibling => sibling._canReceive(item, x, y)); |
| } |
| /** |
| * Checks whether the drop list can receive the passed-in item. |
| * @param item Item that is being dragged into the list. |
| * @param x Position of the item along the X axis. |
| * @param y Position of the item along the Y axis. |
| */ |
| _canReceive(item, x, y) { |
| if (!this._clientRect || !isInsideClientRect(this._clientRect, x, y) || |
| !this.enterPredicate(item, this)) { |
| return false; |
| } |
| const elementFromPoint = this._getShadowRoot().elementFromPoint(x, y); |
| // If there's no element at the pointer position, then |
| // the client rect is probably scrolled out of the view. |
| if (!elementFromPoint) { |
| return false; |
| } |
| const nativeElement = coerceElement(this.element); |
| // The `ClientRect`, that we're using to find the container over which the user is |
| // hovering, doesn't give us any information on whether the element has been scrolled |
| // out of the view or whether it's overlapping with other containers. This means that |
| // we could end up transferring the item into a container that's invisible or is positioned |
| // below another one. We use the result from `elementFromPoint` to get the top-most element |
| // at the pointer position and to find whether it's one of the intersecting drop containers. |
| return elementFromPoint === nativeElement || nativeElement.contains(elementFromPoint); |
| } |
| /** |
| * Called by one of the connected drop lists when a dragging sequence has started. |
| * @param sibling Sibling in which dragging has started. |
| */ |
| _startReceiving(sibling, items) { |
| const activeSiblings = this._activeSiblings; |
| if (!activeSiblings.has(sibling) && items.every(item => { |
| // Note that we have to add an exception to the `enterPredicate` for items that started off |
| // in this drop list. The drag ref has logic that allows an item to return to its initial |
| // container, if it has left the initial container and none of the connected containers |
| // allow it to enter. See `DragRef._updateActiveDropContainer` for more context. |
| return this.enterPredicate(item, this) || this._draggables.indexOf(item) > -1; |
| })) { |
| activeSiblings.add(sibling); |
| this._cacheParentPositions(); |
| this._listenToScrollEvents(); |
| } |
| } |
| /** |
| * Called by a connected drop list when dragging has stopped. |
| * @param sibling Sibling whose dragging has stopped. |
| */ |
| _stopReceiving(sibling) { |
| this._activeSiblings.delete(sibling); |
| this._viewportScrollSubscription.unsubscribe(); |
| } |
| /** |
| * Starts listening to scroll events on the viewport. |
| * Used for updating the internal state of the list. |
| */ |
| _listenToScrollEvents() { |
| this._viewportScrollSubscription = this._dragDropRegistry.scroll.subscribe(event => { |
| if (this.isDragging()) { |
| const scrollDifference = this._parentPositions.handleScroll(event); |
| if (scrollDifference) { |
| // Since we know the amount that the user has scrolled we can shift all of the |
| // client rectangles ourselves. This is cheaper than re-measuring everything and |
| // we can avoid inconsistent behavior where we might be measuring the element before |
| // its position has changed. |
| this._itemPositions.forEach(({ clientRect }) => { |
| adjustClientRect(clientRect, scrollDifference.top, scrollDifference.left); |
| }); |
| // We need two loops for this, because we want all of the cached |
| // positions to be up-to-date before we re-sort the item. |
| this._itemPositions.forEach(({ drag }) => { |
| if (this._dragDropRegistry.isDragging(drag)) { |
| // We need to re-sort the item manually, because the pointer move |
| // events won't be dispatched while the user is scrolling. |
| drag._sortFromLastPointerPosition(); |
| } |
| }); |
| } |
| } |
| else if (this.isReceiving()) { |
| this._cacheParentPositions(); |
| } |
| }); |
| } |
| /** |
| * Lazily resolves and returns the shadow root of the element. We do this in a function, rather |
| * than saving it in property directly on init, because we want to resolve it as late as possible |
| * in order to ensure that the element has been moved into the shadow DOM. Doing it inside the |
| * constructor might be too early if the element is inside of something like `ngFor` or `ngIf`. |
| */ |
| _getShadowRoot() { |
| if (!this._cachedShadowRoot) { |
| const shadowRoot = _getShadowRoot(coerceElement(this.element)); |
| this._cachedShadowRoot = shadowRoot || this._document; |
| } |
| return this._cachedShadowRoot; |
| } |
| /** Notifies any siblings that may potentially receive the item. */ |
| _notifyReceivingSiblings() { |
| const draggedItems = this._activeDraggables.filter(item => item.isDragging()); |
| this._siblings.forEach(sibling => sibling._startReceiving(this, draggedItems)); |
| } |
| } |
| /** |
| * Finds the index of an item that matches a predicate function. Used as an equivalent |
| * of `Array.prototype.findIndex` which isn't part of the standard Google typings. |
| * @param array Array in which to look for matches. |
| * @param predicate Function used to determine whether an item is a match. |
| */ |
| function findIndex(array, predicate) { |
| for (let i = 0; i < array.length; i++) { |
| if (predicate(array[i], i, array)) { |
| return i; |
| } |
| } |
| return -1; |
| } |
| /** |
| * Increments the vertical scroll position of a node. |
| * @param node Node whose scroll position should change. |
| * @param amount Amount of pixels that the `node` should be scrolled. |
| */ |
| function incrementVerticalScroll(node, amount) { |
| if (node === window) { |
| node.scrollBy(0, amount); |
| } |
| else { |
| // Ideally we could use `Element.scrollBy` here as well, but IE and Edge don't support it. |
| node.scrollTop += amount; |
| } |
| } |
| /** |
| * Increments the horizontal scroll position of a node. |
| * @param node Node whose scroll position should change. |
| * @param amount Amount of pixels that the `node` should be scrolled. |
| */ |
| function incrementHorizontalScroll(node, amount) { |
| if (node === window) { |
| node.scrollBy(amount, 0); |
| } |
| else { |
| // Ideally we could use `Element.scrollBy` here as well, but IE and Edge don't support it. |
| node.scrollLeft += amount; |
| } |
| } |
| /** |
| * Gets whether the vertical auto-scroll direction of a node. |
| * @param clientRect Dimensions of the node. |
| * @param pointerY Position of the user's pointer along the y axis. |
| */ |
| function getVerticalScrollDirection(clientRect, pointerY) { |
| const { top, bottom, height } = clientRect; |
| const yThreshold = height * SCROLL_PROXIMITY_THRESHOLD; |
| if (pointerY >= top - yThreshold && pointerY <= top + yThreshold) { |
| return 1 /* UP */; |
| } |
| else if (pointerY >= bottom - yThreshold && pointerY <= bottom + yThreshold) { |
| return 2 /* DOWN */; |
| } |
| return 0 /* NONE */; |
| } |
| /** |
| * Gets whether the horizontal auto-scroll direction of a node. |
| * @param clientRect Dimensions of the node. |
| * @param pointerX Position of the user's pointer along the x axis. |
| */ |
| function getHorizontalScrollDirection(clientRect, pointerX) { |
| const { left, right, width } = clientRect; |
| const xThreshold = width * SCROLL_PROXIMITY_THRESHOLD; |
| if (pointerX >= left - xThreshold && pointerX <= left + xThreshold) { |
| return 1 /* LEFT */; |
| } |
| else if (pointerX >= right - xThreshold && pointerX <= right + xThreshold) { |
| return 2 /* RIGHT */; |
| } |
| return 0 /* NONE */; |
| } |
| /** |
| * Gets the directions in which an element node should be scrolled, |
| * assuming that the user's pointer is already within it scrollable region. |
| * @param element Element for which we should calculate the scroll direction. |
| * @param clientRect Bounding client rectangle of the element. |
| * @param pointerX Position of the user's pointer along the x axis. |
| * @param pointerY Position of the user's pointer along the y axis. |
| */ |
| function getElementScrollDirections(element, clientRect, pointerX, pointerY) { |
| const computedVertical = getVerticalScrollDirection(clientRect, pointerY); |
| const computedHorizontal = getHorizontalScrollDirection(clientRect, pointerX); |
| let verticalScrollDirection = 0 /* NONE */; |
| let horizontalScrollDirection = 0 /* NONE */; |
| // Note that we here we do some extra checks for whether the element is actually scrollable in |
| // a certain direction and we only assign the scroll direction if it is. We do this so that we |
| // can allow other elements to be scrolled, if the current element can't be scrolled anymore. |
| // This allows us to handle cases where the scroll regions of two scrollable elements overlap. |
| if (computedVertical) { |
| const scrollTop = element.scrollTop; |
| if (computedVertical === 1 /* UP */) { |
| if (scrollTop > 0) { |
| verticalScrollDirection = 1 /* UP */; |
| } |
| } |
| else if (element.scrollHeight - scrollTop > element.clientHeight) { |
| verticalScrollDirection = 2 /* DOWN */; |
| } |
| } |
| if (computedHorizontal) { |
| const scrollLeft = element.scrollLeft; |
| if (computedHorizontal === 1 /* LEFT */) { |
| if (scrollLeft > 0) { |
| horizontalScrollDirection = 1 /* LEFT */; |
| } |
| } |
| else if (element.scrollWidth - scrollLeft > element.clientWidth) { |
| horizontalScrollDirection = 2 /* RIGHT */; |
| } |
| } |
| return [verticalScrollDirection, horizontalScrollDirection]; |
| } |
| |
| /** |
| * @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 |
| */ |
| /** Event options that can be used to bind an active, capturing event. */ |
| const activeCapturingEventOptions = normalizePassiveListenerOptions({ |
| passive: false, |
| capture: true |
| }); |
| /** |
| * Service that keeps track of all the drag item and drop container |
| * instances, and manages global event listeners on the `document`. |
| * @docs-private |
| */ |
| // Note: this class is generic, rather than referencing CdkDrag and CdkDropList directly, in order |
| // to avoid circular imports. If we were to reference them here, importing the registry into the |
| // classes that are registering themselves will introduce a circular import. |
| class DragDropRegistry { |
| constructor(_ngZone, _document) { |
| this._ngZone = _ngZone; |
| /** Registered drop container instances. */ |
| this._dropInstances = new Set(); |
| /** Registered drag item instances. */ |
| this._dragInstances = new Set(); |
| /** Drag item instances that are currently being dragged. */ |
| this._activeDragInstances = []; |
| /** Keeps track of the event listeners that we've bound to the `document`. */ |
| this._globalListeners = new Map(); |
| /** |
| * Predicate function to check if an item is being dragged. Moved out into a property, |
| * because it'll be called a lot and we don't want to create a new function every time. |
| */ |
| this._draggingPredicate = (item) => item.isDragging(); |
| /** |
| * Emits the `touchmove` or `mousemove` events that are dispatched |
| * while the user is dragging a drag item instance. |
| */ |
| this.pointerMove = new Subject(); |
| /** |
| * Emits the `touchend` or `mouseup` events that are dispatched |
| * while the user is dragging a drag item instance. |
| */ |
| this.pointerUp = new Subject(); |
| /** Emits when the viewport has been scrolled while the user is dragging an item. */ |
| this.scroll = new Subject(); |
| /** |
| * Event listener that will prevent the default browser action while the user is dragging. |
| * @param event Event whose default action should be prevented. |
| */ |
| this._preventDefaultWhileDragging = (event) => { |
| if (this._activeDragInstances.length > 0) { |
| event.preventDefault(); |
| } |
| }; |
| /** Event listener for `touchmove` that is bound even if no dragging is happening. */ |
| this._persistentTouchmoveListener = (event) => { |
| if (this._activeDragInstances.length > 0) { |
| // Note that we only want to prevent the default action after dragging has actually started. |
| // Usually this is the same time at which the item is added to the `_activeDragInstances`, |
| // but it could be pushed back if the user has set up a drag delay or threshold. |
| if (this._activeDragInstances.some(this._draggingPredicate)) { |
| event.preventDefault(); |
| } |
| this.pointerMove.next(event); |
| } |
| }; |
| this._document = _document; |
| } |
| /** Adds a drop container to the registry. */ |
| registerDropContainer(drop) { |
| if (!this._dropInstances.has(drop)) { |
| this._dropInstances.add(drop); |
| } |
| } |
| /** Adds a drag item instance to the registry. */ |
| registerDragItem(drag) { |
| this._dragInstances.add(drag); |
| // The `touchmove` event gets bound once, ahead of time, because WebKit |
| // won't preventDefault on a dynamically-added `touchmove` listener. |
| // See https://bugs.webkit.org/show_bug.cgi?id=184250. |
| if (this._dragInstances.size === 1) { |
| this._ngZone.runOutsideAngular(() => { |
| // The event handler has to be explicitly active, |
| // because newer browsers make it passive by default. |
| this._document.addEventListener('touchmove', this._persistentTouchmoveListener, activeCapturingEventOptions); |
| }); |
| } |
| } |
| /** Removes a drop container from the registry. */ |
| removeDropContainer(drop) { |
| this._dropInstances.delete(drop); |
| } |
| /** Removes a drag item instance from the registry. */ |
| removeDragItem(drag) { |
| this._dragInstances.delete(drag); |
| this.stopDragging(drag); |
| if (this._dragInstances.size === 0) { |
| this._document.removeEventListener('touchmove', this._persistentTouchmoveListener, activeCapturingEventOptions); |
| } |
| } |
| /** |
| * Starts the dragging sequence for a drag instance. |
| * @param drag Drag instance which is being dragged. |
| * @param event Event that initiated the dragging. |
| */ |
| startDragging(drag, event) { |
| // Do not process the same drag twice to avoid memory leaks and redundant listeners |
| if (this._activeDragInstances.indexOf(drag) > -1) { |
| return; |
| } |
| this._activeDragInstances.push(drag); |
| if (this._activeDragInstances.length === 1) { |
| const isTouchEvent = event.type.startsWith('touch'); |
| // We explicitly bind __active__ listeners here, because newer browsers will default to |
| // passive ones for `mousemove` and `touchmove`. The events need to be active, because we |
| // use `preventDefault` to prevent the page from scrolling while the user is dragging. |
| this._globalListeners |
| .set(isTouchEvent ? 'touchend' : 'mouseup', { |
| handler: (e) => this.pointerUp.next(e), |
| options: true |
| }) |
| .set('scroll', { |
| handler: (e) => this.scroll.next(e), |
| // Use capturing so that we pick up scroll changes in any scrollable nodes that aren't |
| // the document. See https://github.com/angular/components/issues/17144. |
| options: true |
| }) |
| // Preventing the default action on `mousemove` isn't enough to disable text selection |
| // on Safari so we need to prevent the selection event as well. Alternatively this can |
| // be done by setting `user-select: none` on the `body`, however it has causes a style |
| // recalculation which can be expensive on pages with a lot of elements. |
| .set('selectstart', { |
| handler: this._preventDefaultWhileDragging, |
| options: activeCapturingEventOptions |
| }); |
| // We don't have to bind a move event for touch drag sequences, because |
| // we already have a persistent global one bound from `registerDragItem`. |
| if (!isTouchEvent) { |
| this._globalListeners.set('mousemove', { |
| handler: (e) => this.pointerMove.next(e), |
| options: activeCapturingEventOptions |
| }); |
| } |
| this._ngZone.runOutsideAngular(() => { |
| this._globalListeners.forEach((config, name) => { |
| this._document.addEventListener(name, config.handler, config.options); |
| }); |
| }); |
| } |
| } |
| /** Stops dragging a drag item instance. */ |
| stopDragging(drag) { |
| const index = this._activeDragInstances.indexOf(drag); |
| if (index > -1) { |
| this._activeDragInstances.splice(index, 1); |
| if (this._activeDragInstances.length === 0) { |
| this._clearGlobalListeners(); |
| } |
| } |
| } |
| /** Gets whether a drag item instance is currently being dragged. */ |
| isDragging(drag) { |
| return this._activeDragInstances.indexOf(drag) > -1; |
| } |
| ngOnDestroy() { |
| this._dragInstances.forEach(instance => this.removeDragItem(instance)); |
| this._dropInstances.forEach(instance => this.removeDropContainer(instance)); |
| this._clearGlobalListeners(); |
| this.pointerMove.complete(); |
| this.pointerUp.complete(); |
| } |
| /** Clears out the global event listeners from the `document`. */ |
| _clearGlobalListeners() { |
| this._globalListeners.forEach((config, name) => { |
| this._document.removeEventListener(name, config.handler, config.options); |
| }); |
| this._globalListeners.clear(); |
| } |
| } |
| DragDropRegistry.ɵprov = ɵɵdefineInjectable({ factory: function DragDropRegistry_Factory() { return new DragDropRegistry(ɵɵinject(NgZone), ɵɵinject(DOCUMENT)); }, token: DragDropRegistry, providedIn: "root" }); |
| DragDropRegistry.decorators = [ |
| { type: Injectable, args: [{ providedIn: 'root' },] } |
| ]; |
| DragDropRegistry.ctorParameters = () => [ |
| { type: NgZone }, |
| { type: undefined, decorators: [{ type: Inject, args: [DOCUMENT,] }] } |
| ]; |
| |
| /** |
| * @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 |
| */ |
| /** Default configuration to be used when creating a `DragRef`. */ |
| const DEFAULT_CONFIG = { |
| dragStartThreshold: 5, |
| pointerDirectionChangeThreshold: 5 |
| }; |
| /** |
| * Service that allows for drag-and-drop functionality to be attached to DOM elements. |
| */ |
| class DragDrop { |
| constructor(_document, _ngZone, _viewportRuler, _dragDropRegistry) { |
| this._document = _document; |
| this._ngZone = _ngZone; |
| this._viewportRuler = _viewportRuler; |
| this._dragDropRegistry = _dragDropRegistry; |
| } |
| /** |
| * Turns an element into a draggable item. |
| * @param element Element to which to attach the dragging functionality. |
| * @param config Object used to configure the dragging behavior. |
| */ |
| createDrag(element, config = DEFAULT_CONFIG) { |
| return new DragRef(element, config, this._document, this._ngZone, this._viewportRuler, this._dragDropRegistry); |
| } |
| /** |
| * Turns an element into a drop list. |
| * @param element Element to which to attach the drop list functionality. |
| */ |
| createDropList(element) { |
| return new DropListRef(element, this._dragDropRegistry, this._document, this._ngZone, this._viewportRuler); |
| } |
| } |
| DragDrop.ɵprov = ɵɵdefineInjectable({ factory: function DragDrop_Factory() { return new DragDrop(ɵɵinject(DOCUMENT), ɵɵinject(NgZone), ɵɵinject(ViewportRuler), ɵɵinject(DragDropRegistry)); }, token: DragDrop, providedIn: "root" }); |
| DragDrop.decorators = [ |
| { type: Injectable, args: [{ providedIn: 'root' },] } |
| ]; |
| DragDrop.ctorParameters = () => [ |
| { type: undefined, decorators: [{ type: Inject, args: [DOCUMENT,] }] }, |
| { type: NgZone }, |
| { type: ViewportRuler }, |
| { type: DragDropRegistry } |
| ]; |
| |
| /** |
| * @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 |
| */ |
| /** |
| * Injection token that can be used for a `CdkDrag` to provide itself as a parent to the |
| * drag-specific child directive (`CdkDragHandle`, `CdkDragPreview` etc.). Used primarily |
| * to avoid circular imports. |
| * @docs-private |
| */ |
| const CDK_DRAG_PARENT = new InjectionToken('CDK_DRAG_PARENT'); |
| |
| /** |
| * @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 |
| */ |
| |
| /** |
| * @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 |
| */ |
| /** |
| * Injection token that can be used to reference instances of `CdkDropListGroup`. It serves as |
| * alternative token to the actual `CdkDropListGroup` class which could cause unnecessary |
| * retention of the class and its directive metadata. |
| */ |
| const CDK_DROP_LIST_GROUP = new InjectionToken('CdkDropListGroup'); |
| /** |
| * Declaratively connects sibling `cdkDropList` instances together. All of the `cdkDropList` |
| * elements that are placed inside a `cdkDropListGroup` will be connected to each other |
| * automatically. Can be used as an alternative to the `cdkDropListConnectedTo` input |
| * from `cdkDropList`. |
| */ |
| class CdkDropListGroup { |
| constructor() { |
| /** Drop lists registered inside the group. */ |
| this._items = new Set(); |
| this._disabled = false; |
| } |
| /** Whether starting a dragging sequence from inside this group is disabled. */ |
| get disabled() { return this._disabled; } |
| set disabled(value) { |
| this._disabled = coerceBooleanProperty(value); |
| } |
| ngOnDestroy() { |
| this._items.clear(); |
| } |
| } |
| CdkDropListGroup.decorators = [ |
| { type: Directive, args: [{ |
| selector: '[cdkDropListGroup]', |
| exportAs: 'cdkDropListGroup', |
| providers: [{ provide: CDK_DROP_LIST_GROUP, useExisting: CdkDropListGroup }], |
| },] } |
| ]; |
| CdkDropListGroup.propDecorators = { |
| disabled: [{ type: Input, args: ['cdkDropListGroupDisabled',] }] |
| }; |
| |
| /** |
| * @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 |
| */ |
| /** |
| * Injection token that can be used to configure the |
| * behavior of the drag&drop-related components. |
| */ |
| const CDK_DRAG_CONFIG = new InjectionToken('CDK_DRAG_CONFIG'); |
| |
| /** |
| * @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 |
| */ |
| /** |
| * Asserts that a particular node is an element. |
| * @param node Node to be checked. |
| * @param name Name to attach to the error message. |
| */ |
| function assertElementNode(node, name) { |
| if (node.nodeType !== 1) { |
| throw Error(`${name} must be attached to an element node. ` + |
| `Currently attached to "${node.nodeName}".`); |
| } |
| } |
| |
| /** |
| * @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 |
| */ |
| /** Counter used to generate unique ids for drop zones. */ |
| let _uniqueIdCounter = 0; |
| /** |
| * Injection token that can be used to reference instances of `CdkDropList`. It serves as |
| * alternative token to the actual `CdkDropList` class which could cause unnecessary |
| * retention of the class and its directive metadata. |
| */ |
| const CDK_DROP_LIST = new InjectionToken('CdkDropList'); |
| const ɵ0 = undefined; |
| /** Container that wraps a set of draggable items. */ |
| class CdkDropList { |
| constructor( |
| /** Element that the drop list is attached to. */ |
| element, dragDrop, _changeDetectorRef, _scrollDispatcher, _dir, _group, config) { |
| this.element = element; |
| this._changeDetectorRef = _changeDetectorRef; |
| this._scrollDispatcher = _scrollDispatcher; |
| this._dir = _dir; |
| this._group = _group; |
| /** Emits when the list has been destroyed. */ |
| this._destroyed = new Subject(); |
| /** |
| * Other draggable containers that this container is connected to and into which the |
| * container's items can be transferred. Can either be references to other drop containers, |
| * or their unique IDs. |
| */ |
| this.connectedTo = []; |
| /** |
| * Unique ID for the drop zone. Can be used as a reference |
| * in the `connectedTo` of another `CdkDropList`. |
| */ |
| this.id = `cdk-drop-list-${_uniqueIdCounter++}`; |
| /** |
| * Function that is used to determine whether an item |
| * is allowed to be moved into a drop container. |
| */ |
| this.enterPredicate = () => true; |
| /** Functions that is used to determine whether an item can be sorted into a particular index. */ |
| this.sortPredicate = () => true; |
| /** Emits when the user drops an item inside the container. */ |
| this.dropped = new EventEmitter(); |
| /** |
| * Emits when the user has moved a new drag item into this container. |
| */ |
| this.entered = new EventEmitter(); |
| /** |
| * Emits when the user removes an item from the container |
| * by dragging it into another container. |
| */ |
| this.exited = new EventEmitter(); |
| /** Emits as the user is swapping items while actively dragging. */ |
| this.sorted = new EventEmitter(); |
| /** |
| * Keeps track of the items that are registered with this container. Historically we used to |
| * do this with a `ContentChildren` query, however queries don't handle transplanted views very |
| * well which means that we can't handle cases like dragging the headers of a `mat-table` |
| * correctly. What we do instead is to have the items register themselves with the container |
| * and then we sort them based on their position in the DOM. |
| */ |
| this._unsortedItems = new Set(); |
| if (typeof ngDevMode === 'undefined' || ngDevMode) { |
| assertElementNode(element.nativeElement, 'cdkDropList'); |
| } |
| this._dropListRef = dragDrop.createDropList(element); |
| this._dropListRef.data = this; |
| if (config) { |
| this._assignDefaults(config); |
| } |
| this._dropListRef.enterPredicate = (drag, drop) => { |
| return this.enterPredicate(drag.data, drop.data); |
| }; |
| this._dropListRef.sortPredicate = |
| (index, drag, drop) => { |
| return this.sortPredicate(index, drag.data, drop.data); |
| }; |
| this._setupInputSyncSubscription(this._dropListRef); |
| this._handleEvents(this._dropListRef); |
| CdkDropList._dropLists.push(this); |
| if (_group) { |
| _group._items.add(this); |
| } |
| } |
| /** Whether starting a dragging sequence from this container is disabled. */ |
| get disabled() { |
| return this._disabled || (!!this._group && this._group.disabled); |
| } |
| set disabled(value) { |
| // Usually we sync the directive and ref state right before dragging starts, in order to have |
| // a single point of failure and to avoid having to use setters for everything. `disabled` is |
| // a special case, because it can prevent the `beforeStarted` event from firing, which can lock |
| // the user in a disabled state, so we also need to sync it as it's being set. |
| this._dropListRef.disabled = this._disabled = coerceBooleanProperty(value); |
| } |
| /** Registers an items with the drop list. */ |
| addItem(item) { |
| this._unsortedItems.add(item); |
| if (this._dropListRef.isDragging()) { |
| this._syncItemsWithRef(); |
| } |
| } |
| /** Removes an item from the drop list. */ |
| removeItem(item) { |
| this._unsortedItems.delete(item); |
| if (this._dropListRef.isDragging()) { |
| this._syncItemsWithRef(); |
| } |
| } |
| /** Gets the registered items in the list, sorted by their position in the DOM. */ |
| getSortedItems() { |
| return Array.from(this._unsortedItems).sort((a, b) => { |
| const documentPosition = a._dragRef.getVisibleElement().compareDocumentPosition(b._dragRef.getVisibleElement()); |
| // `compareDocumentPosition` returns a bitmask so we have to use a bitwise operator. |
| // https://developer.mozilla.org/en-US/docs/Web/API/Node/compareDocumentPosition |
| // tslint:disable-next-line:no-bitwise |
| return documentPosition & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1; |
| }); |
| } |
| ngOnDestroy() { |
| const index = CdkDropList._dropLists.indexOf(this); |
| if (index > -1) { |
| CdkDropList._dropLists.splice(index, 1); |
| } |
| if (this._group) { |
| this._group._items.delete(this); |
| } |
| this._unsortedItems.clear(); |
| this._dropListRef.dispose(); |
| this._destroyed.next(); |
| this._destroyed.complete(); |
| } |
| /** Syncs the inputs of the CdkDropList with the options of the underlying DropListRef. */ |
| _setupInputSyncSubscription(ref) { |
| if (this._dir) { |
| this._dir.change |
| .pipe(startWith(this._dir.value), takeUntil(this._destroyed)) |
| .subscribe(value => ref.withDirection(value)); |
| } |
| ref.beforeStarted.subscribe(() => { |
| const siblings = coerceArray(this.connectedTo).map(drop => { |
| if (typeof drop === 'string') { |
| const correspondingDropList = CdkDropList._dropLists.find(list => list.id === drop); |
| if (!correspondingDropList && (typeof ngDevMode === 'undefined' || ngDevMode)) { |
| console.warn(`CdkDropList could not find connected drop list with id "${drop}"`); |
| } |
| return correspondingDropList; |
| } |
| return drop; |
| }); |
| if (this._group) { |
| this._group._items.forEach(drop => { |
| if (siblings.indexOf(drop) === -1) { |
| siblings.push(drop); |
| } |
| }); |
| } |
| // Note that we resolve the scrollable parents here so that we delay the resolution |
| // as long as possible, ensuring that the element is in its final place in the DOM. |
| if (!this._scrollableParentsResolved) { |
| const scrollableParents = this._scrollDispatcher |
| .getAncestorScrollContainers(this.element) |
| .map(scrollable => scrollable.getElementRef().nativeElement); |
| this._dropListRef.withScrollableParents(scrollableParents); |
| // Only do this once since it involves traversing the DOM and the parents |
| // shouldn't be able to change without the drop list being destroyed. |
| this._scrollableParentsResolved = true; |
| } |
| ref.disabled = this.disabled; |
| ref.lockAxis = this.lockAxis; |
| ref.sortingDisabled = coerceBooleanProperty(this.sortingDisabled); |
| ref.autoScrollDisabled = coerceBooleanProperty(this.autoScrollDisabled); |
| ref.autoScrollStep = coerceNumberProperty(this.autoScrollStep, 2); |
| ref |
| .connectedTo(siblings.filter(drop => drop && drop !== this).map(list => list._dropListRef)) |
| .withOrientation(this.orientation); |
| }); |
| } |
| /** Handles events from the underlying DropListRef. */ |
| _handleEvents(ref) { |
| ref.beforeStarted.subscribe(() => { |
| this._syncItemsWithRef(); |
| this._changeDetectorRef.markForCheck(); |
| }); |
| ref.entered.subscribe(event => { |
| this.entered.emit({ |
| container: this, |
| item: event.item.data, |
| currentIndex: event.currentIndex |
| }); |
| }); |
| ref.exited.subscribe(event => { |
| this.exited.emit({ |
| container: this, |
| item: event.item.data |
| }); |
| this._changeDetectorRef.markForCheck(); |
| }); |
| ref.sorted.subscribe(event => { |
| this.sorted.emit({ |
| previousIndex: event.previousIndex, |
| currentIndex: event.currentIndex, |
| container: this, |
| item: event.item.data |
| }); |
| }); |
| ref.dropped.subscribe(event => { |
| this.dropped.emit({ |
| previousIndex: event.previousIndex, |
| currentIndex: event.currentIndex, |
| previousContainer: event.previousContainer.data, |
| container: event.container.data, |
| item: event.item.data, |
| isPointerOverContainer: event.isPointerOverContainer, |
| distance: event.distance |
| }); |
| // Mark for check since all of these events run outside of change |
| // detection and we're not guaranteed for something else to have triggered it. |
| this._changeDetectorRef.markForCheck(); |
| }); |
| } |
| /** Assigns the default input values based on a provided config object. */ |
| _assignDefaults(config) { |
| const { lockAxis, draggingDisabled, sortingDisabled, listAutoScrollDisabled, listOrientation } = config; |
| this.disabled = draggingDisabled == null ? false : draggingDisabled; |
| this.sortingDisabled = sortingDisabled == null ? false : sortingDisabled; |
| this.autoScrollDisabled = listAutoScrollDisabled == null ? false : listAutoScrollDisabled; |
| this.orientation = listOrientation || 'vertical'; |
| if (lockAxis) { |
| this.lockAxis = lockAxis; |
| } |
| } |
| /** Syncs up the registered drag items with underlying drop list ref. */ |
| _syncItemsWithRef() { |
| this._dropListRef.withItems(this.getSortedItems().map(item => item._dragRef)); |
| } |
| } |
| /** Keeps track of the drop lists that are currently on the page. */ |
| CdkDropList._dropLists = []; |
| CdkDropList.decorators = [ |
| { type: Directive, args: [{ |
| selector: '[cdkDropList], cdk-drop-list', |
| exportAs: 'cdkDropList', |
| providers: [ |
| // Prevent child drop lists from picking up the same group as their parent. |
| { provide: CDK_DROP_LIST_GROUP, useValue: ɵ0 }, |
| { provide: CDK_DROP_LIST, useExisting: CdkDropList }, |
| ], |
| host: { |
| 'class': 'cdk-drop-list', |
| '[attr.id]': 'id', |
| '[class.cdk-drop-list-disabled]': 'disabled', |
| '[class.cdk-drop-list-dragging]': '_dropListRef.isDragging()', |
| '[class.cdk-drop-list-receiving]': '_dropListRef.isReceiving()', |
| } |
| },] } |
| ]; |
| CdkDropList.ctorParameters = () => [ |
| { type: ElementRef }, |
| { type: DragDrop }, |
| { type: ChangeDetectorRef }, |
| { type: ScrollDispatcher }, |
| { type: Directionality, decorators: [{ type: Optional }] }, |
| { type: CdkDropListGroup, decorators: [{ type: Optional }, { type: Inject, args: [CDK_DROP_LIST_GROUP,] }, { type: SkipSelf }] }, |
| { type: undefined, decorators: [{ type: Optional }, { type: Inject, args: [CDK_DRAG_CONFIG,] }] } |
| ]; |
| CdkDropList.propDecorators = { |
| connectedTo: [{ type: Input, args: ['cdkDropListConnectedTo',] }], |
| data: [{ type: Input, args: ['cdkDropListData',] }], |
| orientation: [{ type: Input, args: ['cdkDropListOrientation',] }], |
| id: [{ type: Input }], |
| lockAxis: [{ type: Input, args: ['cdkDropListLockAxis',] }], |
| disabled: [{ type: Input, args: ['cdkDropListDisabled',] }], |
| sortingDisabled: [{ type: Input, args: ['cdkDropListSortingDisabled',] }], |
| enterPredicate: [{ type: Input, args: ['cdkDropListEnterPredicate',] }], |
| sortPredicate: [{ type: Input, args: ['cdkDropListSortPredicate',] }], |
| autoScrollDisabled: [{ type: Input, args: ['cdkDropListAutoScrollDisabled',] }], |
| autoScrollStep: [{ type: Input, args: ['cdkDropListAutoScrollStep',] }], |
| dropped: [{ type: Output, args: ['cdkDropListDropped',] }], |
| entered: [{ type: Output, args: ['cdkDropListEntered',] }], |
| exited: [{ type: Output, args: ['cdkDropListExited',] }], |
| sorted: [{ type: Output, args: ['cdkDropListSorted',] }] |
| }; |
| |
| /** |
| * @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 |
| */ |
| /** |
| * Injection token that can be used to reference instances of `CdkDragHandle`. It serves as |
| * alternative token to the actual `CdkDragHandle` class which could cause unnecessary |
| * retention of the class and its directive metadata. |
| */ |
| const CDK_DRAG_HANDLE = new InjectionToken('CdkDragHandle'); |
| /** Handle that can be used to drag a CdkDrag instance. */ |
| class CdkDragHandle { |
| constructor(element, parentDrag) { |
| this.element = element; |
| /** Emits when the state of the handle has changed. */ |
| this._stateChanges = new Subject(); |
| this._disabled = false; |
| if (typeof ngDevMode === 'undefined' || ngDevMode) { |
| assertElementNode(element.nativeElement, 'cdkDragHandle'); |
| } |
| this._parentDrag = parentDrag; |
| } |
| /** Whether starting to drag through this handle is disabled. */ |
| get disabled() { return this._disabled; } |
| set disabled(value) { |
| this._disabled = coerceBooleanProperty(value); |
| this._stateChanges.next(this); |
| } |
| ngOnDestroy() { |
| this._stateChanges.complete(); |
| } |
| } |
| CdkDragHandle.decorators = [ |
| { type: Directive, args: [{ |
| selector: '[cdkDragHandle]', |
| host: { |
| 'class': 'cdk-drag-handle' |
| }, |
| providers: [{ provide: CDK_DRAG_HANDLE, useExisting: CdkDragHandle }], |
| },] } |
| ]; |
| CdkDragHandle.ctorParameters = () => [ |
| { type: ElementRef }, |
| { type: undefined, decorators: [{ type: Inject, args: [CDK_DRAG_PARENT,] }, { type: Optional }, { type: SkipSelf }] } |
| ]; |
| CdkDragHandle.propDecorators = { |
| disabled: [{ type: Input, args: ['cdkDragHandleDisabled',] }] |
| }; |
| |
| /** |
| * @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 |
| */ |
| /** |
| * Injection token that can be used to reference instances of `CdkDragPlaceholder`. It serves as |
| * alternative token to the actual `CdkDragPlaceholder` class which could cause unnecessary |
| * retention of the class and its directive metadata. |
| */ |
| const CDK_DRAG_PLACEHOLDER = new InjectionToken('CdkDragPlaceholder'); |
| /** |
| * Element that will be used as a template for the placeholder of a CdkDrag when |
| * it is being dragged. The placeholder is displayed in place of the element being dragged. |
| */ |
| class CdkDragPlaceholder { |
| constructor(templateRef) { |
| this.templateRef = templateRef; |
| } |
| } |
| CdkDragPlaceholder.decorators = [ |
| { type: Directive, args: [{ |
| selector: 'ng-template[cdkDragPlaceholder]', |
| providers: [{ provide: CDK_DRAG_PLACEHOLDER, useExisting: CdkDragPlaceholder }], |
| },] } |
| ]; |
| CdkDragPlaceholder.ctorParameters = () => [ |
| { type: TemplateRef } |
| ]; |
| CdkDragPlaceholder.propDecorators = { |
| data: [{ type: Input }] |
| }; |
| |
| /** |
| * @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 |
| */ |
| /** |
| * Injection token that can be used to reference instances of `CdkDragPreview`. It serves as |
| * alternative token to the actual `CdkDragPreview` class which could cause unnecessary |
| * retention of the class and its directive metadata. |
| */ |
| const CDK_DRAG_PREVIEW = new InjectionToken('CdkDragPreview'); |
| /** |
| * Element that will be used as a template for the preview |
| * of a CdkDrag when it is being dragged. |
| */ |
| class CdkDragPreview { |
| constructor(templateRef) { |
| this.templateRef = templateRef; |
| this._matchSize = false; |
| } |
| /** Whether the preview should preserve the same size as the item that is being dragged. */ |
| get matchSize() { return this._matchSize; } |
| set matchSize(value) { this._matchSize = coerceBooleanProperty(value); } |
| } |
| CdkDragPreview.decorators = [ |
| { type: Directive, args: [{ |
| selector: 'ng-template[cdkDragPreview]', |
| providers: [{ provide: CDK_DRAG_PREVIEW, useExisting: CdkDragPreview }], |
| },] } |
| ]; |
| CdkDragPreview.ctorParameters = () => [ |
| { type: TemplateRef } |
| ]; |
| CdkDragPreview.propDecorators = { |
| data: [{ type: Input }], |
| matchSize: [{ type: Input }] |
| }; |
| |
| /** |
| * @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 |
| */ |
| const DRAG_HOST_CLASS = 'cdk-drag'; |
| /** Element that can be moved inside a CdkDropList container. */ |
| class CdkDrag { |
| constructor( |
| /** Element that the draggable is attached to. */ |
| element, |
| /** Droppable container that the draggable is a part of. */ |
| dropContainer, |
| /** |
| * @deprecated `_document` parameter no longer being used and will be removed. |
| * @breaking-change 12.0.0 |
| */ |
| _document, _ngZone, _viewContainerRef, config, _dir, dragDrop, _changeDetectorRef, _selfHandle, _parentDrag) { |
| this.element = element; |
| this.dropContainer = dropContainer; |
| this._ngZone = _ngZone; |
| this._viewContainerRef = _viewContainerRef; |
| this._dir = _dir; |
| this._changeDetectorRef = _changeDetectorRef; |
| this._selfHandle = _selfHandle; |
| this._parentDrag = _parentDrag; |
| this._destroyed = new Subject(); |
| /** Emits when the user starts dragging the item. */ |
| this.started = new EventEmitter(); |
| /** Emits when the user has released a drag item, before any animations have started. */ |
| this.released = new EventEmitter(); |
| /** Emits when the user stops dragging an item in the container. */ |
| this.ended = new EventEmitter(); |
| /** Emits when the user has moved the item into a new container. */ |
| this.entered = new EventEmitter(); |
| /** Emits when the user removes the item its container by dragging it into another container. */ |
| this.exited = new EventEmitter(); |
| /** Emits when the user drops the item inside a container. */ |
| this.dropped = new EventEmitter(); |
| /** |
| * 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) => { |
| const subscription = this._dragRef.moved.pipe(map(movedEvent => ({ |
| source: this, |
| pointerPosition: movedEvent.pointerPosition, |
| event: movedEvent.event, |
| delta: movedEvent.delta, |
| distance: movedEvent.distance |
| }))).subscribe(observer); |
| return () => { |
| subscription.unsubscribe(); |
| }; |
| }); |
| this._dragRef = dragDrop.createDrag(element, { |
| dragStartThreshold: config && config.dragStartThreshold != null ? |
| config.dragStartThreshold : 5, |
| pointerDirectionChangeThreshold: config && config.pointerDirectionChangeThreshold != null ? |
| config.pointerDirectionChangeThreshold : 5, |
| zIndex: config === null || config === void 0 ? void 0 : config.zIndex, |
| }); |
| this._dragRef.data = this; |
| // We have to keep track of the drag instances in order to be able to match an element to |
| // a drag instance. We can't go through the global registry of `DragRef`, because the root |
| // element could be different. |
| CdkDrag._dragInstances.push(this); |
| if (config) { |
| this._assignDefaults(config); |
| } |
| // Note that usually the container is assigned when the drop list is picks up the item, but in |
| // some cases (mainly transplanted views with OnPush, see #18341) we may end up in a situation |
| // where there are no items on the first change detection pass, but the items get picked up as |
| // soon as the user triggers another pass by dragging. This is a problem, because the item would |
| // have to switch from standalone mode to drag mode in the middle of the dragging sequence which |
| // is too late since the two modes save different kinds of information. We work around it by |
| // assigning the drop container both from here and the list. |
| if (dropContainer) { |
| this._dragRef._withDropContainer(dropContainer._dropListRef); |
| dropContainer.addItem(this); |
| } |
| this._syncInputs(this._dragRef); |
| this._handleEvents(this._dragRef); |
| } |
| /** Whether starting to drag this element is disabled. */ |
| get disabled() { |
| return this._disabled || (this.dropContainer && this.dropContainer.disabled); |
| } |
| set disabled(value) { |
| this._disabled = coerceBooleanProperty(value); |
| this._dragRef.disabled = this._disabled; |
| } |
| /** |
| * Returns the element that is being used as a placeholder |
| * while the current element is being dragged. |
| */ |
| getPlaceholderElement() { |
| return this._dragRef.getPlaceholderElement(); |
| } |
| /** Returns the root draggable element. */ |
| getRootElement() { |
| return this._dragRef.getRootElement(); |
| } |
| /** Resets a standalone drag item to its initial position. */ |
| reset() { |
| this._dragRef.reset(); |
| } |
| /** |
| * Gets the pixel coordinates of the draggable outside of a drop container. |
| */ |
| getFreeDragPosition() { |
| return this._dragRef.getFreeDragPosition(); |
| } |
| ngAfterViewInit() { |
| // We need to wait for the zone to stabilize, in order for the reference |
| // element to be in the proper place in the DOM. This is mostly relevant |
| // for draggable elements inside portals since they get stamped out in |
| // their original DOM position and then they get transferred to the portal. |
| this._ngZone.onStable |
| .pipe(take(1), takeUntil(this._destroyed)) |
| .subscribe(() => { |
| this._updateRootElement(); |
| // Listen for any newly-added handles. |
| this._handles.changes.pipe(startWith(this._handles), |
| // Sync the new handles with the DragRef. |
| tap((handles) => { |
| const childHandleElements = handles |
| .filter(handle => handle._parentDrag === this) |
| .map(handle => handle.element); |
| // Usually handles are only allowed to be a descendant of the drag element, but if |
| // the consumer defined a different drag root, we should allow the drag element |
| // itself to be a handle too. |
| if (this._selfHandle && this.rootElementSelector) { |
| childHandleElements.push(this.element); |
| } |
| this._dragRef.withHandles(childHandleElements); |
| }), |
| // Listen if the state of any of the handles changes. |
| switchMap((handles) => { |
| return merge(...handles.map(item => { |
| return item._stateChanges.pipe(startWith(item)); |
| })); |
| }), takeUntil(this._destroyed)).subscribe(handleInstance => { |
| // Enabled/disable the handle that changed in the DragRef. |
| const dragRef = this._dragRef; |
| const handle = handleInstance.element.nativeElement; |
| handleInstance.disabled ? dragRef.disableHandle(handle) : dragRef.enableHandle(handle); |
| }); |
| if (this.freeDragPosition) { |
| this._dragRef.setFreeDragPosition(this.freeDragPosition); |
| } |
| }); |
| } |
| ngOnChanges(changes) { |
| const rootSelectorChange = changes['rootElementSelector']; |
| const positionChange = changes['freeDragPosition']; |
| // We don't have to react to the first change since it's being |
| // handled in `ngAfterViewInit` where it needs to be deferred. |
| if (rootSelectorChange && !rootSelectorChange.firstChange) { |
| this._updateRootElement(); |
| } |
| // Skip the first change since it's being handled in `ngAfterViewInit`. |
| if (positionChange && !positionChange.firstChange && this.freeDragPosition) { |
| this._dragRef.setFreeDragPosition(this.freeDragPosition); |
| } |
| } |
| ngOnDestroy() { |
| if (this.dropContainer) { |
| this.dropContainer.removeItem(this); |
| } |
| const index = CdkDrag._dragInstances.indexOf(this); |
| if (index > -1) { |
| CdkDrag._dragInstances.splice(index, 1); |
| } |
| this._destroyed.next(); |
| this._destroyed.complete(); |
| this._dragRef.dispose(); |
| } |
| /** Syncs the root element with the `DragRef`. */ |
| _updateRootElement() { |
| const element = this.element.nativeElement; |
| const rootElement = this.rootElementSelector ? |
| getClosestMatchingAncestor(element, this.rootElementSelector) : element; |
| if (rootElement && (typeof ngDevMode === 'undefined' || ngDevMode)) { |
| assertElementNode(rootElement, 'cdkDrag'); |
| } |
| this._dragRef.withRootElement(rootElement || element); |
| } |
| /** Gets the boundary element, based on the `boundaryElement` value. */ |
| _getBoundaryElement() { |
| const boundary = this.boundaryElement; |
| if (!boundary) { |
| return null; |
| } |
| if (typeof boundary === 'string') { |
| return getClosestMatchingAncestor(this.element.nativeElement, boundary); |
| } |
| const element = coerceElement(boundary); |
| if ((typeof ngDevMode === 'undefined' || ngDevMode) && |
| !element.contains(this.element.nativeElement)) { |
| throw Error('Draggable element is not inside of the node passed into cdkDragBoundary.'); |
| } |
| return element; |
| } |
| /** Syncs the inputs of the CdkDrag with the options of the underlying DragRef. */ |
| _syncInputs(ref) { |
| ref.beforeStarted.subscribe(() => { |
| if (!ref.isDragging()) { |
| const dir = this._dir; |
| const dragStartDelay = this.dragStartDelay; |
| const placeholder = this._placeholderTemplate ? { |
| template: this._placeholderTemplate.templateRef, |
| context: this._placeholderTemplate.data, |
| viewContainer: this._viewContainerRef |
| } : null; |
| const preview = this._previewTemplate ? { |
| template: this._previewTemplate.templateRef, |
| context: this._previewTemplate.data, |
| matchSize: this._previewTemplate.matchSize, |
| viewContainer: this._viewContainerRef |
| } : null; |
| ref.disabled = this.disabled; |
| ref.lockAxis = this.lockAxis; |
| ref.dragStartDelay = (typeof dragStartDelay === 'object' && dragStartDelay) ? |
| dragStartDelay : coerceNumberProperty(dragStartDelay); |
| ref.constrainPosition = this.constrainPosition; |
| ref.previewClass = this.previewClass; |
| ref |
| .withBoundaryElement(this._getBoundaryElement()) |
| .withPlaceholderTemplate(placeholder) |
| .withPreviewTemplate(preview); |
| if (dir) { |
| ref.withDirection(dir.value); |
| } |
| } |
| }); |
| // This only needs to be resolved once. |
| ref.beforeStarted.pipe(take(1)).subscribe(() => { |
| var _a, _b; |
| // If we managed to resolve a parent through DI, use it. |
| if (this._parentDrag) { |
| ref.withParent(this._parentDrag._dragRef); |
| return; |
| } |
| // Otherwise fall back to resolving the parent by looking up the DOM. This can happen if |
| // the item was projected into another item by something like `ngTemplateOutlet`. |
| let parent = this.element.nativeElement.parentElement; |
| while (parent) { |
| // `classList` needs to be null checked, because IE doesn't have it on some elements. |
| if ((_a = parent.classList) === null || _a === void 0 ? void 0 : _a.contains(DRAG_HOST_CLASS)) { |
| ref.withParent(((_b = CdkDrag._dragInstances.find(drag => { |
| return drag.element.nativeElement === parent; |
| })) === null || _b === void 0 ? void 0 : _b._dragRef) || null); |
| break; |
| } |
| parent = parent.parentElement; |
| } |
| }); |
| } |
| /** Handles the events from the underlying `DragRef`. */ |
| _handleEvents(ref) { |
| ref.started.subscribe(() => { |
| this.started.emit({ source: this }); |
| // Since all of these events run outside of change detection, |
| // we need to ensure that everything is marked correctly. |
| this._changeDetectorRef.markForCheck(); |
| }); |
| ref.released.subscribe(() => { |
| this.released.emit({ source: this }); |
| }); |
| ref.ended.subscribe(event => { |
| this.ended.emit({ source: this, distance: event.distance }); |
| // Since all of these events run outside of change detection, |
| // we need to ensure that everything is marked correctly. |
| this._changeDetectorRef.markForCheck(); |
| }); |
| ref.entered.subscribe(event => { |
| this.entered.emit({ |
| container: event.container.data, |
| item: this, |
| currentIndex: event.currentIndex |
| }); |
| }); |
| ref.exited.subscribe(event => { |
| this.exited.emit({ |
| container: event.container.data, |
| item: this |
| }); |
| }); |
| ref.dropped.subscribe(event => { |
| this.dropped.emit({ |
| previousIndex: event.previousIndex, |
| currentIndex: event.currentIndex, |
| previousContainer: event.previousContainer.data, |
| container: event.container.data, |
| isPointerOverContainer: event.isPointerOverContainer, |
| item: this, |
| distance: event.distance |
| }); |
| }); |
| } |
| /** Assigns the default input values based on a provided config object. */ |
| _assignDefaults(config) { |
| const { lockAxis, dragStartDelay, constrainPosition, previewClass, boundaryElement, draggingDisabled, rootElementSelector } = config; |
| this.disabled = draggingDisabled == null ? false : draggingDisabled; |
| this.dragStartDelay = dragStartDelay || 0; |
| if (lockAxis) { |
| this.lockAxis = lockAxis; |
| } |
| if (constrainPosition) { |
| this.constrainPosition = constrainPosition; |
| } |
| if (previewClass) { |
| this.previewClass = previewClass; |
| } |
| if (boundaryElement) { |
| this.boundaryElement = boundaryElement; |
| } |
| if (rootElementSelector) { |
| this.rootElementSelector = rootElementSelector; |
| } |
| } |
| } |
| CdkDrag._dragInstances = []; |
| CdkDrag.decorators = [ |
| { type: Directive, args: [{ |
| selector: '[cdkDrag]', |
| exportAs: 'cdkDrag', |
| host: { |
| 'class': DRAG_HOST_CLASS, |
| '[class.cdk-drag-disabled]': 'disabled', |
| '[class.cdk-drag-dragging]': '_dragRef.isDragging()', |
| }, |
| providers: [{ provide: CDK_DRAG_PARENT, useExisting: CdkDrag }] |
| },] } |
| ]; |
| CdkDrag.ctorParameters = () => [ |
| { type: ElementRef }, |
| { type: undefined, decorators: [{ type: Inject, args: [CDK_DROP_LIST,] }, { type: Optional }, { type: SkipSelf }] }, |
| { type: undefined, decorators: [{ type: Inject, args: [DOCUMENT,] }] }, |
| { type: NgZone }, |
| { type: ViewContainerRef }, |
| { type: undefined, decorators: [{ type: Optional }, { type: Inject, args: [CDK_DRAG_CONFIG,] }] }, |
| { type: Directionality, decorators: [{ type: Optional }] }, |
| { type: DragDrop }, |
| { type: ChangeDetectorRef }, |
| { type: CdkDragHandle, decorators: [{ type: Optional }, { type: Self }, { type: Inject, args: [CDK_DRAG_HANDLE,] }] }, |
| { type: CdkDrag, decorators: [{ type: Optional }, { type: SkipSelf }, { type: Inject, args: [CDK_DRAG_PARENT,] }] } |
| ]; |
| CdkDrag.propDecorators = { |
| _handles: [{ type: ContentChildren, args: [CDK_DRAG_HANDLE, { descendants: true },] }], |
| _previewTemplate: [{ type: ContentChild, args: [CDK_DRAG_PREVIEW,] }], |
| _placeholderTemplate: [{ type: ContentChild, args: [CDK_DRAG_PLACEHOLDER,] }], |
| data: [{ type: Input, args: ['cdkDragData',] }], |
| lockAxis: [{ type: Input, args: ['cdkDragLockAxis',] }], |
| rootElementSelector: [{ type: Input, args: ['cdkDragRootElement',] }], |
| boundaryElement: [{ type: Input, args: ['cdkDragBoundary',] }], |
| dragStartDelay: [{ type: Input, args: ['cdkDragStartDelay',] }], |
| freeDragPosition: [{ type: Input, args: ['cdkDragFreeDragPosition',] }], |
| disabled: [{ type: Input, args: ['cdkDragDisabled',] }], |
| constrainPosition: [{ type: Input, args: ['cdkDragConstrainPosition',] }], |
| previewClass: [{ type: Input, args: ['cdkDragPreviewClass',] }], |
| started: [{ type: Output, args: ['cdkDragStarted',] }], |
| released: [{ type: Output, args: ['cdkDragReleased',] }], |
| ended: [{ type: Output, args: ['cdkDragEnded',] }], |
| entered: [{ type: Output, args: ['cdkDragEntered',] }], |
| exited: [{ type: Output, args: ['cdkDragExited',] }], |
| dropped: [{ type: Output, args: ['cdkDragDropped',] }], |
| moved: [{ type: Output, args: ['cdkDragMoved',] }] |
| }; |
| /** Gets the closest ancestor of an element that matches a selector. */ |
| function getClosestMatchingAncestor(element, selector) { |
| let currentElement = element.parentElement; |
| while (currentElement) { |
| // IE doesn't support `matches` so we have to fall back to `msMatchesSelector`. |
| if (currentElement.matches ? currentElement.matches(selector) : |
| currentElement.msMatchesSelector(selector)) { |
| return currentElement; |
| } |
| currentElement = currentElement.parentElement; |
| } |
| return null; |
| } |
| |
| /** |
| * @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 |
| */ |
| class DragDropModule { |
| } |
| DragDropModule.decorators = [ |
| { type: NgModule, args: [{ |
| declarations: [ |
| CdkDropList, |
| CdkDropListGroup, |
| CdkDrag, |
| CdkDragHandle, |
| CdkDragPreview, |
| CdkDragPlaceholder, |
| ], |
| exports: [ |
| CdkScrollableModule, |
| CdkDropList, |
| CdkDropListGroup, |
| CdkDrag, |
| CdkDragHandle, |
| CdkDragPreview, |
| CdkDragPlaceholder, |
| ], |
| providers: [ |
| DragDrop, |
| ] |
| },] } |
| ]; |
| |
| /** |
| * @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 |
| */ |
| |
| /** |
| * Generated bundle index. Do not edit. |
| */ |
| |
| export { CDK_DRAG_CONFIG, CDK_DRAG_HANDLE, CDK_DRAG_PARENT, CDK_DRAG_PLACEHOLDER, CDK_DRAG_PREVIEW, CDK_DROP_LIST, CDK_DROP_LIST_GROUP, CdkDrag, CdkDragHandle, CdkDragPlaceholder, CdkDragPreview, CdkDropList, CdkDropListGroup, DragDrop, DragDropModule, DragDropRegistry, DragRef, DropListRef, copyArrayItem, moveItemInArray, transferArrayItem }; |
| //# sourceMappingURL=drag-drop.js.map |