@angular/cdk
Version:
Angular Material Component Development Kit
1,115 lines (1,106 loc) • 204 kB
JavaScript
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;