UNPKG

@angular/cdk

Version:

Angular Material Component Development Kit

1,115 lines (1,106 loc) 204 kB
import * as i0 from '@angular/core'; import { signal, Component, ViewEncapsulation, ChangeDetectionStrategy, inject, NgZone, RendererFactory2, Injectable, InjectionToken, ElementRef, booleanAttribute, Directive, Input, ViewContainerRef, ChangeDetectorRef, EventEmitter, Injector, afterNextRender, numberAttribute, Output, TemplateRef, NgModule } from '@angular/core'; import { DOCUMENT } from '@angular/common'; import { Subject, Subscription, interval, animationFrameScheduler, Observable, merge, BehaviorSubject } from 'rxjs'; import { _ as _getEventTarget, a as _getShadowRoot } from './shadow-dom-B0oHn41l.mjs'; import { a as isFakeTouchstartFromScreenReader, i as isFakeMousedownFromScreenReader } from './fake-event-detection-DWOdFTFz.mjs'; import { a as coerceElement, c as coerceNumberProperty } from './element-x4z00URv.mjs'; import { _ as _bindEventWithOptions } from './backwards-compatibility-DHR38MsD.mjs'; import { takeUntil, map, take, tap, switchMap, startWith } from 'rxjs/operators'; import { _ as _CdkPrivateStyleLoader } from './style-loader-Cu9AvjH9.mjs'; import { ViewportRuler, ScrollDispatcher, CdkScrollableModule } from './scrolling.mjs'; export { CdkScrollable as ɵɵCdkScrollable } from './scrolling.mjs'; import { D as Directionality } from './directionality-CBXD4hga.mjs'; import { _ as _IdGenerator } from './id-generator-Dw_9dSDu.mjs'; import { c as coerceArray } from './array-I1yfCXUO.mjs'; import './platform-DmdVEw_C.mjs'; import './scrolling-BkvA05C8.mjs'; import './bidi.mjs'; import './recycle-view-repeater-strategy-DoWdPqVw.mjs'; import './data-source-D34wiQZj.mjs'; /** 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 { } } } /** Gets a mutable version of an element's bounding `DOMRect`. */ function getMutableClientRect(element) { const rect = 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 `DOMRect` aren't own properties. See: // https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect#Notes return { top: rect.top, right: rect.right, bottom: rect.bottom, left: rect.left, width: rect.width, height: rect.height, x: rect.x, y: rect.y, }; } /** * Checks whether some coordinates are within a `DOMRect`. * @param clientRect DOMRect 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 `DOMRect`, as well as their bottom/right counterparts. * @param domRect `DOMRect` that should be updated. * @param top Amount to add to the `top` position. * @param left Amount to add to the `left` position. */ function adjustDomRect(domRect, top, left) { domRect.top += top; domRect.bottom = domRect.top + domRect.height; domRect.left += left; domRect.right = domRect.left + domRect.width; } /** * Checks whether the pointer coordinates are close to a DOMRect. * @param rect DOMRect to check against. * @param threshold Threshold around the DOMRect. * @param pointerX Coordinates along the X axis. * @param pointerY Coordinates along the Y axis. */ function isPointerNearDomRect(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); } /** Keeps track of the scroll position and dimensions of the parents of an element. */ class ParentPositionTracker { _document; /** Cached positions of the scrollable parent elements. */ positions = new Map(); constructor(_document) { this._document = _document; } /** 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.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 = _getEventTarget(event); const cachedPosition = this.positions.get(target); if (!cachedPosition) { return null; } const scrollPosition = cachedPosition.scrollPosition; let newTop; let newLeft; if (target === this._document) { const viewportScrollPosition = this.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 && target.contains(node)) { adjustDomRect(position.clientRect, topDifference, leftDifference); } }); scrollPosition.top = newTop; scrollPosition.left = newLeft; return { top: topDifference, left: leftDifference }; } /** * Gets the scroll position of the viewport. Note that we use the scrollX and scrollY directly, * instead of going through the `ViewportRuler`, because the first value the ruler looks at is * the top/left offset of the `document.documentElement` which works for most cases, but breaks * if the element is offset by something like the `BlockScrollStrategy`. */ getViewportScrollPosition() { return { top: window.scrollY, left: window.scrollX }; } } /** * 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; } /** * Shallow-extends a stylesheet object with another stylesheet-like object. * Note that the keys in `source` have to be dash-cased. * @docs-private */ function extendStyles(dest, source, importantProperties) { for (let key in source) { if (source.hasOwnProperty(key)) { const value = source[key]; if (value) { dest.setProperty(key, value, importantProperties?.has(key) ? 'important' : ''); } else { dest.removeProperty(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, { 'touch-action': enable ? '' : 'none', '-webkit-user-drag': enable ? '' : 'none', '-webkit-tap-highlight-color': enable ? '' : 'transparent', 'user-select': userSelect, '-ms-user-select': userSelect, '-webkit-user-select': userSelect, '-moz-user-select': 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. * @param importantProperties Properties to be set as `!important`. * @docs-private */ function toggleVisibility(element, enable, importantProperties) { extendStyles(element.style, { position: enable ? '' : 'fixed', top: enable ? '' : '0', opacity: enable ? '' : '0', left: enable ? '' : '-999em', }, importantProperties); } /** * Combines a transform string with an optional other transform * that exited before the base transform was applied. */ function combineTransforms(transform, initialTransform) { return initialTransform && initialTransform != 'none' ? transform + ' ' + initialTransform : transform; } /** * 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); } /** * 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)`; } /** 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()); } /** Inline styles to be set as `!important` while dragging. */ const importantProperties = new Set([ // Needs to be important, because some `mat-table` sets `position: sticky !important`. See #22781. 'position', ]); class PreviewRef { _document; _rootElement; _direction; _initialDomRect; _previewTemplate; _previewClass; _pickupPositionOnPage; _initialTransform; _zIndex; _renderer; /** Reference to the view of the preview element. */ _previewEmbeddedView; /** Reference to the preview element. */ _preview; get element() { return this._preview; } constructor(_document, _rootElement, _direction, _initialDomRect, _previewTemplate, _previewClass, _pickupPositionOnPage, _initialTransform, _zIndex, _renderer) { this._document = _document; this._rootElement = _rootElement; this._direction = _direction; this._initialDomRect = _initialDomRect; this._previewTemplate = _previewTemplate; this._previewClass = _previewClass; this._pickupPositionOnPage = _pickupPositionOnPage; this._initialTransform = _initialTransform; this._zIndex = _zIndex; this._renderer = _renderer; } attach(parent) { this._preview = this._createPreview(); parent.appendChild(this._preview); // The null check is necessary for browsers that don't support the popover API. // Note that we use a string access for compatibility with Closure. if (supportsPopover(this._preview)) { this._preview['showPopover'](); } } destroy() { this._preview.remove(); this._previewEmbeddedView?.destroy(); this._preview = this._previewEmbeddedView = null; } setTransform(value) { this._preview.style.transform = value; } getBoundingClientRect() { return this._preview.getBoundingClientRect(); } addClass(className) { this._preview.classList.add(className); } getTransitionDuration() { return getTransformTransitionDurationInMs(this._preview); } addEventListener(name, handler) { return this._renderer.listen(this._preview, name, handler); } _createPreview() { 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._initialDomRect : null; const viewRef = previewConfig.viewContainer.createEmbeddedView(previewTemplate, previewConfig.context); viewRef.detectChanges(); preview = getRootNode(viewRef, this._document); this._previewEmbeddedView = viewRef; if (previewConfig.matchSize) { matchElementSize(preview, rootRect); } else { preview.style.transform = getTransform(this._pickupPositionOnPage.x, this._pickupPositionOnPage.y); } } else { preview = deepCloneNode(this._rootElement); matchElementSize(preview, this._initialDomRect); if (this._initialTransform) { preview.style.transform = this._initialTransform; } } 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`. 'pointer-events': 'none', // If the preview has a margin, it can throw off our positioning so we reset it. The reset // value for `margin-right` needs to be `auto` when opened as a popover, because our // positioning is always top/left based, but native popover seems to position itself // to the top/right if `<html>` or `<body>` have `dir="rtl"` (see #29604). Setting it // to `auto` pushed it to the top/left corner in RTL and is a noop in LTR. 'margin': supportsPopover(preview) ? '0 auto 0 0' : '0', 'position': 'fixed', 'top': '0', 'left': '0', 'z-index': this._zIndex + '', }, importantProperties); toggleNativeDragInteractions(preview, false); preview.classList.add('cdk-drag-preview'); preview.setAttribute('popover', 'manual'); 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; } } /** Checks whether a specific element supports the popover API. */ function supportsPopover(element) { return 'showPopover' in element; } /** Options that can be used to bind a passive event listener. */ const passiveEventListenerOptions = { passive: true }; /** Options that can be used to bind an active event listener. */ const activeEventListenerOptions = { passive: false }; /** Event options that can be used to bind an active, capturing event. */ const activeCapturingEventOptions$1 = { passive: false, capture: true, }; /** * 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; /** Inline styles to be set as `!important` while dragging. */ const dragImportantProperties = new Set([ // Needs to be important, because some `mat-table` sets `position: sticky !important`. See #22781. 'position', ]); /** * Reference to a draggable item. Used to manipulate or dispose of the item. */ class DragRef { _config; _document; _ngZone; _viewportRuler; _dragDropRegistry; _renderer; _rootElementCleanups; _cleanupShadowRootSelectStart; /** Element displayed next to the user's pointer while the element is dragged. */ _preview; /** Container into which to insert the preview. */ _previewContainer; /** Reference to the view of the placeholder element. */ _placeholderRef; /** Element that is rendered instead of the draggable item while it is being sorted. */ _placeholder; /** Coordinates within the element at which the user picked up the element. */ _pickupPositionInElement; /** Coordinates on the page at which the user picked up the element. */ _pickupPositionOnPage; /** * Anchor node used to save the place in the DOM where the element was * picked up so that it can be restored at the end of the drag sequence. */ _anchor; /** * 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`. */ _passiveTransform = { x: 0, y: 0 }; /** CSS `transform` that is applied to the element while it's being dragged. */ _activeTransform = { x: 0, y: 0 }; /** Inline `transform` value that the element had before the first dragging sequence. */ _initialTransform; /** * Whether the dragging sequence has been started. Doesn't * necessarily mean that the element has been moved. */ _hasStartedDragging = signal(false); /** Whether the element has moved since the user started dragging it. */ _hasMoved; /** Drop container in which the DragRef resided when dragging began. */ _initialContainer; /** Index at which the item started in its initial container. */ _initialIndex; /** Cached positions of scrollable parent elements. */ _parentPositions; /** Emits when the item is being moved. */ _moveEvents = new Subject(); /** Keeps track of the direction in which the user is dragging along each axis. */ _pointerDirectionDelta; /** Pointer position at which the last change in the delta occurred. */ _pointerPositionAtLastDirectionChange; /** Position of the pointer at the last pointer event. */ _lastKnownPointerPosition; /** * Root DOM node of the drag instance. This is the element that will * be moved around as the user is dragging. */ _rootElement; /** * Nearest ancestor SVG, relative to which coordinates are calculated if dragging SVGElement */ _ownerSVGElement; /** * Inline style value of `-webkit-tap-highlight-color` at the time the * dragging was started. Used to restore the value once we're done dragging. */ _rootElementTapHighlight; /** Subscription to pointer movement events. */ _pointerMoveSubscription = Subscription.EMPTY; /** Subscription to the event that is dispatched when the user lifts their pointer. */ _pointerUpSubscription = Subscription.EMPTY; /** Subscription to the viewport being scrolled. */ _scrollSubscription = Subscription.EMPTY; /** Subscription to the viewport being resized. */ _resizeSubscription = Subscription.EMPTY; /** * Time at which the last touch event occurred. Used to avoid firing the same * events multiple times on touch devices where the browser will fire a fake * mouse event for each touch event, after a certain time. */ _lastTouchEventTime; /** Time at which the last dragging sequence was started. */ _dragStartTime; /** Cached reference to the boundary element. */ _boundaryElement = null; /** Whether the native dragging interactions have been enabled on the root element. */ _nativeInteractionsEnabled = true; /** Client rect of the root element when the dragging sequence has started. */ _initialDomRect; /** Cached dimensions of the preview element. Should be read via `_getPreviewRect`. */ _previewRect; /** Cached dimensions of the boundary element. */ _boundaryRect; /** Element that will be used as a template to create the draggable item's preview. */ _previewTemplate; /** Template for placeholder element rendered to show where a draggable would be dropped. */ _placeholderTemplate; /** Elements that can be used to drag the draggable item. */ _handles = []; /** Registered handles that are currently disabled. */ _disabledHandles = new Set(); /** Droppable container that the draggable is a part of. */ _dropContainer; /** Layout direction of the item. */ _direction = 'ltr'; /** Ref that the current drag item is nested in. */ _parentDragRef; /** * Cached shadow root that the element is placed in. `null` means that the element isn't in * the shadow DOM and `undefined` means that it hasn't been resolved yet. Should be read via * `_getShadowRoot`, not directly. */ _cachedShadowRoot; /** Axis along which dragging is locked. */ lockAxis; /** * Amount of milliseconds to wait after the user has put their * pointer down before starting to drag the element. */ dragStartDelay = 0; /** Class to be added to the preview element. */ previewClass; /** * If the parent of the dragged element has a `scale` transform, it can throw off the * positioning when the user starts dragging. Use this input to notify the CDK of the scale. */ scale = 1; /** Whether starting to drag this element is disabled. */ get disabled() { return this._disabled || !!(this._dropContainer && this._dropContainer.disabled); } set disabled(value) { if (value !== this._disabled) { this._disabled = value; this._toggleNativeDragInteractions(); this._handles.forEach(handle => toggleNativeDragInteractions(handle, value)); } } _disabled = false; /** Emits as the drag sequence is being prepared. */ beforeStarted = new Subject(); /** Emits when the user starts dragging the item. */ started = new Subject(); /** Emits when the user has released a drag item, before any animations have started. */ released = new Subject(); /** Emits when the user stops dragging an item in the container. */ ended = new Subject(); /** Emits when the user has moved the item into a new container. */ entered = new Subject(); /** Emits when the user removes the item its container by dragging it into another container. */ exited = new Subject(); /** Emits when the user drops the item inside a container. */ 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. */ moved = this._moveEvents; /** Arbitrary data that can be attached to the drag item. */ data; /** * Function that can be used to customize the logic of how the position of the drag item * is limited while it's being dragged. Gets called with a point containing the current position * of the user's pointer on the page, a reference to the item being dragged and its dimensions. * Should return a point describing where the item should be rendered. */ constrainPosition; constructor(element, _config, _document, _ngZone, _viewportRuler, _dragDropRegistry, _renderer) { this._config = _config; this._document = _document; this._ngZone = _ngZone; this._viewportRuler = _viewportRuler; this._dragDropRegistry = _dragDropRegistry; this._renderer = _renderer; this.withRootElement(element).withParent(_config.parentDragRef || null); this._parentPositions = new ParentPositionTracker(_document); _dragDropRegistry.registerDragItem(this); } /** * 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) { this._removeRootElementListeners(); this._rootElementCleanups = this._ngZone.runOutsideAngular(() => [ _bindEventWithOptions(this._renderer, element, 'mousedown', this._pointerDown, activeEventListenerOptions), _bindEventWithOptions(this._renderer, element, 'touchstart', this._pointerDown, passiveEventListenerOptions), _bindEventWithOptions(this._renderer, element, 'dragstart', this._nativeDragStart, activeEventListenerOptions), ]); 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(); // 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. this._rootElement?.remove(); } this._anchor?.remove(); this._destroyPreview(); this._destroyPlaceholder(); this._dragDropRegistry.removeDragItem(this); this._removeListeners(); 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; } /** * Sets the container into which to insert the preview element. * @param value Container into which to insert the preview. */ withPreviewContainer(value) { this._previewContainer = value; 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. */ _removeListeners() { this._pointerMoveSubscription.unsubscribe(); this._pointerUpSubscription.unsubscribe(); this._scrollSubscription.unsubscribe(); this._cleanupShadowRootSelectStart?.(); this._cleanupShadowRootSelectStart = undefined; } /** Destroys the preview element and its ViewRef. */ _destroyPreview() { this._preview?.destroy(); this._preview = null; } /** Destroys the placeholder element and its ViewRef. */ _destroyPlaceholder() { this._placeholder?.remove(); this._placeholderRef?.destroy(); this._placeholder = this._placeholderRef = null; } /** Handler for the `mousedown`/`touchstart` events. */ _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._getTargetHandle(event); 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. */ _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 preferable 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. if (event.cancelable) { event.preventDefault(); } this._hasStartedDragging.set(true); this._ngZone.run(() => this._startDragSequence(event)); } } return; } // 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. if (event.cancelable) { event.preventDefault(); } const constrainedPointerPosition = this._getConstrainedPointerPosition(pointerPosition); this._hasMoved = true; this._lastKnownPointerPosition = pointerPosition; this._updatePointerDirectionDelta(constrainedPointerPosition); if (this._dropContainer) { this._updateActiveDropContainer(constrainedPointerPosition, pointerPosition); } else { // If there's a position constraint function, we want the element's top/left to be at the // specific position on the page. Use the initial position as a reference if that's the case. const offset = this.constrainPosition ? this._initialDomRect : this._pickupPositionOnPage; const activeTransform = this._activeTransform; activeTransform.x = constrainedPointerPosition.x - offset.x + this._passiveTransform.x; activeTransform.y = constrainedPointerPosition.y - offset.y + this._passiveTransform.y; this._applyRootElementTransform(activeTransform.x, activeTransform.y); } // 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. */ _pointerUp = (event) => { this._endDragSequence(event); }; /** * 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._removeListeners(); this._dragDropRegistry.stopDragging(this); this._toggleNativeDragInteractions(); if (this._handles) { this._rootElement.style.webkitTapHighlightColor = this._rootElementTapHighlight; } if (!this._hasStartedDragging()) { return; } this.released.next({ source: this, event }); 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; const pointerPosition = this._getPointerPositionOnPage(event); this._passiveTransform.y = this._activeTransform.y; this._ngZone.run(() => { this.ended.next({ source: this, distance: this._getDragDistance(pointerPosition), dropPoint: pointerPosition, event, }); }); this._cleanupCachedDimensions(); this._dragDropRegistry.stopDragging(this); } } /** Starts the dragging sequence. */ _startDragSequence(event) { if (isTouchEvent(event)) { this._lastTouchEventTime = Date.now(); } this._toggleNativeDragInteractions(); // Needs to happen before the root element is moved. const shadowRoot = this._getShadowRoot(); const dropContainer = this._dropContainer; if (shadowRoot) { // In some browsers the global `selectstart` that we maintain in the `DragDropRegistry` // doesn't cross the shadow boundary so we have to prevent it at the shadow root (see #28792). this._ngZone.runOutsideAngular(() => { this._cleanupShadowRootSelectStart = _bindEventWithOptions(this._renderer, shadowRoot, 'selectstart', shadowDomSelectStart, activeCapturingEventOptions$1); }); } if (dropContainer) { const element = this._rootElement; const parent = element.parentNode; const placeholder = (this._placeholder = this._createPlaceholderElement()); const anchor = (this._anchor = this._anchor || this._document.createComment(typeof ngDevMode === 'undefined' || ngDevMode ? 'cdk-drag-anchor' : '')); // Insert an anchor node so that we can restore the element's position in the DOM. parent.insertBefore(anchor, element); // There's no risk of transforms stacking when inside a drop container so // we can keep the initial transform up to date any time dragging starts. this._initialTransform = element.style.transform || ''; // Create the preview after the initial transform has // been cached, because it can be affected by the transform. this._preview = new PreviewRef(this._document, this._rootElement, this._direction, this._initialDomRect, this._previewTemplate || null, this.previewClass || null, this._pickupPositionOnPage, this._initialTransform, this._config.zIndex || 1000, this._renderer); this._preview.attach(this._getPreviewInsertionPoint(parent, shadowRoot)); // 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, dragImportantProperties); this._document.body.appendChild(parent.replaceChild(placeholder, element)); this.started.next({ source: this, event }); // Emit before notifying the container. dropContainer.start(); this._initialContainer = dropContainer; this._initialIndex = dropContainer.getItemIndex(this); } else { this.started.next({ source: this, event }); 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 target = _getEventTarget(event); const isSyntheticEvent = !isTouchSequence && this._lastTouchEventTime && this._lastTouchEventTime + MOUSE_EVENT_IGNORE_TIME > Date.now(); const isFakeEvent = isTouchSequence ? isFakeTouchstartFromScreenReader(event) : isFakeMousedownFromScreenReader(event); // 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 (target && 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 || isFakeEvent) { 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) { const rootStyles = rootElement.style;