UNPKG

176 kBJavaScriptView Raw
1import * as i0 from '@angular/core';
2import { Injectable, Inject, InjectionToken, Directive, Optional, SkipSelf, Input, EventEmitter, Self, ContentChildren, ContentChild, Output, NgModule } from '@angular/core';
3import { DOCUMENT } from '@angular/common';
4import * as i1 from '@angular/cdk/scrolling';
5import { CdkScrollableModule } from '@angular/cdk/scrolling';
6import { _getEventTarget, normalizePassiveListenerOptions, _getShadowRoot } from '@angular/cdk/platform';
7import { coerceBooleanProperty, coerceElement, coerceNumberProperty, coerceArray } from '@angular/cdk/coercion';
8import { isFakeTouchstartFromScreenReader, isFakeMousedownFromScreenReader } from '@angular/cdk/a11y';
9import { Subject, Subscription, interval, animationFrameScheduler, Observable, merge } from 'rxjs';
10import { takeUntil, map, take, startWith, tap, switchMap } from 'rxjs/operators';
11import * as i1$1 from '@angular/cdk/bidi';
12
13/**
14 * Shallow-extends a stylesheet object with another stylesheet-like object.
15 * Note that the keys in `source` have to be dash-cased.
16 * @docs-private
17 */
18function extendStyles(dest, source, importantProperties) {
19 for (let key in source) {
20 if (source.hasOwnProperty(key)) {
21 const value = source[key];
22 if (value) {
23 dest.setProperty(key, value, importantProperties?.has(key) ? 'important' : '');
24 }
25 else {
26 dest.removeProperty(key);
27 }
28 }
29 }
30 return dest;
31}
32/**
33 * Toggles whether the native drag interactions should be enabled for an element.
34 * @param element Element on which to toggle the drag interactions.
35 * @param enable Whether the drag interactions should be enabled.
36 * @docs-private
37 */
38function toggleNativeDragInteractions(element, enable) {
39 const userSelect = enable ? '' : 'none';
40 extendStyles(element.style, {
41 'touch-action': enable ? '' : 'none',
42 '-webkit-user-drag': enable ? '' : 'none',
43 '-webkit-tap-highlight-color': enable ? '' : 'transparent',
44 'user-select': userSelect,
45 '-ms-user-select': userSelect,
46 '-webkit-user-select': userSelect,
47 '-moz-user-select': userSelect,
48 });
49}
50/**
51 * Toggles whether an element is visible while preserving its dimensions.
52 * @param element Element whose visibility to toggle
53 * @param enable Whether the element should be visible.
54 * @param importantProperties Properties to be set as `!important`.
55 * @docs-private
56 */
57function toggleVisibility(element, enable, importantProperties) {
58 extendStyles(element.style, {
59 position: enable ? '' : 'fixed',
60 top: enable ? '' : '0',
61 opacity: enable ? '' : '0',
62 left: enable ? '' : '-999em',
63 }, importantProperties);
64}
65/**
66 * Combines a transform string with an optional other transform
67 * that exited before the base transform was applied.
68 */
69function combineTransforms(transform, initialTransform) {
70 return initialTransform && initialTransform != 'none'
71 ? transform + ' ' + initialTransform
72 : transform;
73}
74
75/** Parses a CSS time value to milliseconds. */
76function parseCssTimeUnitsToMs(value) {
77 // Some browsers will return it in seconds, whereas others will return milliseconds.
78 const multiplier = value.toLowerCase().indexOf('ms') > -1 ? 1 : 1000;
79 return parseFloat(value) * multiplier;
80}
81/** Gets the transform transition duration, including the delay, of an element in milliseconds. */
82function getTransformTransitionDurationInMs(element) {
83 const computedStyle = getComputedStyle(element);
84 const transitionedProperties = parseCssPropertyValue(computedStyle, 'transition-property');
85 const property = transitionedProperties.find(prop => prop === 'transform' || prop === 'all');
86 // If there's no transition for `all` or `transform`, we shouldn't do anything.
87 if (!property) {
88 return 0;
89 }
90 // Get the index of the property that we're interested in and match
91 // it up to the same index in `transition-delay` and `transition-duration`.
92 const propertyIndex = transitionedProperties.indexOf(property);
93 const rawDurations = parseCssPropertyValue(computedStyle, 'transition-duration');
94 const rawDelays = parseCssPropertyValue(computedStyle, 'transition-delay');
95 return (parseCssTimeUnitsToMs(rawDurations[propertyIndex]) +
96 parseCssTimeUnitsToMs(rawDelays[propertyIndex]));
97}
98/** Parses out multiple values from a computed style into an array. */
99function parseCssPropertyValue(computedStyle, name) {
100 const value = computedStyle.getPropertyValue(name);
101 return value.split(',').map(part => part.trim());
102}
103
104/** Gets a mutable version of an element's bounding `ClientRect`. */
105function getMutableClientRect(element) {
106 const clientRect = element.getBoundingClientRect();
107 // We need to clone the `clientRect` here, because all the values on it are readonly
108 // and we need to be able to update them. Also we can't use a spread here, because
109 // the values on a `ClientRect` aren't own properties. See:
110 // https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect#Notes
111 return {
112 top: clientRect.top,
113 right: clientRect.right,
114 bottom: clientRect.bottom,
115 left: clientRect.left,
116 width: clientRect.width,
117 height: clientRect.height,
118 x: clientRect.x,
119 y: clientRect.y,
120 };
121}
122/**
123 * Checks whether some coordinates are within a `ClientRect`.
124 * @param clientRect ClientRect that is being checked.
125 * @param x Coordinates along the X axis.
126 * @param y Coordinates along the Y axis.
127 */
128function isInsideClientRect(clientRect, x, y) {
129 const { top, bottom, left, right } = clientRect;
130 return y >= top && y <= bottom && x >= left && x <= right;
131}
132/**
133 * Updates the top/left positions of a `ClientRect`, as well as their bottom/right counterparts.
134 * @param clientRect `ClientRect` that should be updated.
135 * @param top Amount to add to the `top` position.
136 * @param left Amount to add to the `left` position.
137 */
138function adjustClientRect(clientRect, top, left) {
139 clientRect.top += top;
140 clientRect.bottom = clientRect.top + clientRect.height;
141 clientRect.left += left;
142 clientRect.right = clientRect.left + clientRect.width;
143}
144/**
145 * Checks whether the pointer coordinates are close to a ClientRect.
146 * @param rect ClientRect to check against.
147 * @param threshold Threshold around the ClientRect.
148 * @param pointerX Coordinates along the X axis.
149 * @param pointerY Coordinates along the Y axis.
150 */
151function isPointerNearClientRect(rect, threshold, pointerX, pointerY) {
152 const { top, right, bottom, left, width, height } = rect;
153 const xThreshold = width * threshold;
154 const yThreshold = height * threshold;
155 return (pointerY > top - yThreshold &&
156 pointerY < bottom + yThreshold &&
157 pointerX > left - xThreshold &&
158 pointerX < right + xThreshold);
159}
160
161/** Keeps track of the scroll position and dimensions of the parents of an element. */
162class ParentPositionTracker {
163 constructor(_document) {
164 this._document = _document;
165 /** Cached positions of the scrollable parent elements. */
166 this.positions = new Map();
167 }
168 /** Clears the cached positions. */
169 clear() {
170 this.positions.clear();
171 }
172 /** Caches the positions. Should be called at the beginning of a drag sequence. */
173 cache(elements) {
174 this.clear();
175 this.positions.set(this._document, {
176 scrollPosition: this.getViewportScrollPosition(),
177 });
178 elements.forEach(element => {
179 this.positions.set(element, {
180 scrollPosition: { top: element.scrollTop, left: element.scrollLeft },
181 clientRect: getMutableClientRect(element),
182 });
183 });
184 }
185 /** Handles scrolling while a drag is taking place. */
186 handleScroll(event) {
187 const target = _getEventTarget(event);
188 const cachedPosition = this.positions.get(target);
189 if (!cachedPosition) {
190 return null;
191 }
192 const scrollPosition = cachedPosition.scrollPosition;
193 let newTop;
194 let newLeft;
195 if (target === this._document) {
196 const viewportScrollPosition = this.getViewportScrollPosition();
197 newTop = viewportScrollPosition.top;
198 newLeft = viewportScrollPosition.left;
199 }
200 else {
201 newTop = target.scrollTop;
202 newLeft = target.scrollLeft;
203 }
204 const topDifference = scrollPosition.top - newTop;
205 const leftDifference = scrollPosition.left - newLeft;
206 // Go through and update the cached positions of the scroll
207 // parents that are inside the element that was scrolled.
208 this.positions.forEach((position, node) => {
209 if (position.clientRect && target !== node && target.contains(node)) {
210 adjustClientRect(position.clientRect, topDifference, leftDifference);
211 }
212 });
213 scrollPosition.top = newTop;
214 scrollPosition.left = newLeft;
215 return { top: topDifference, left: leftDifference };
216 }
217 /**
218 * Gets the scroll position of the viewport. Note that we use the scrollX and scrollY directly,
219 * instead of going through the `ViewportRuler`, because the first value the ruler looks at is
220 * the top/left offset of the `document.documentElement` which works for most cases, but breaks
221 * if the element is offset by something like the `BlockScrollStrategy`.
222 */
223 getViewportScrollPosition() {
224 return { top: window.scrollY, left: window.scrollX };
225 }
226}
227
228/** Creates a deep clone of an element. */
229function deepCloneNode(node) {
230 const clone = node.cloneNode(true);
231 const descendantsWithId = clone.querySelectorAll('[id]');
232 const nodeName = node.nodeName.toLowerCase();
233 // Remove the `id` to avoid having multiple elements with the same id on the page.
234 clone.removeAttribute('id');
235 for (let i = 0; i < descendantsWithId.length; i++) {
236 descendantsWithId[i].removeAttribute('id');
237 }
238 if (nodeName === 'canvas') {
239 transferCanvasData(node, clone);
240 }
241 else if (nodeName === 'input' || nodeName === 'select' || nodeName === 'textarea') {
242 transferInputData(node, clone);
243 }
244 transferData('canvas', node, clone, transferCanvasData);
245 transferData('input, textarea, select', node, clone, transferInputData);
246 return clone;
247}
248/** Matches elements between an element and its clone and allows for their data to be cloned. */
249function transferData(selector, node, clone, callback) {
250 const descendantElements = node.querySelectorAll(selector);
251 if (descendantElements.length) {
252 const cloneElements = clone.querySelectorAll(selector);
253 for (let i = 0; i < descendantElements.length; i++) {
254 callback(descendantElements[i], cloneElements[i]);
255 }
256 }
257}
258// Counter for unique cloned radio button names.
259let cloneUniqueId = 0;
260/** Transfers the data of one input element to another. */
261function transferInputData(source, clone) {
262 // Browsers throw an error when assigning the value of a file input programmatically.
263 if (clone.type !== 'file') {
264 clone.value = source.value;
265 }
266 // Radio button `name` attributes must be unique for radio button groups
267 // otherwise original radio buttons can lose their checked state
268 // once the clone is inserted in the DOM.
269 if (clone.type === 'radio' && clone.name) {
270 clone.name = `mat-clone-${clone.name}-${cloneUniqueId++}`;
271 }
272}
273/** Transfers the data of one canvas element to another. */
274function transferCanvasData(source, clone) {
275 const context = clone.getContext('2d');
276 if (context) {
277 // In some cases `drawImage` can throw (e.g. if the canvas size is 0x0).
278 // We can't do much about it so just ignore the error.
279 try {
280 context.drawImage(source, 0, 0);
281 }
282 catch { }
283 }
284}
285
286/** Options that can be used to bind a passive event listener. */
287const passiveEventListenerOptions = normalizePassiveListenerOptions({ passive: true });
288/** Options that can be used to bind an active event listener. */
289const activeEventListenerOptions = normalizePassiveListenerOptions({ passive: false });
290/**
291 * Time in milliseconds for which to ignore mouse events, after
292 * receiving a touch event. Used to avoid doing double work for
293 * touch devices where the browser fires fake mouse events, in
294 * addition to touch events.
295 */
296const MOUSE_EVENT_IGNORE_TIME = 800;
297/** Inline styles to be set as `!important` while dragging. */
298const dragImportantProperties = new Set([
299 // Needs to be important, because some `mat-table` sets `position: sticky !important`. See #22781.
300 'position',
301]);
302/**
303 * Reference to a draggable item. Used to manipulate or dispose of the item.
304 */
305class DragRef {
306 /** Whether starting to drag this element is disabled. */
307 get disabled() {
308 return this._disabled || !!(this._dropContainer && this._dropContainer.disabled);
309 }
310 set disabled(value) {
311 const newValue = coerceBooleanProperty(value);
312 if (newValue !== this._disabled) {
313 this._disabled = newValue;
314 this._toggleNativeDragInteractions();
315 this._handles.forEach(handle => toggleNativeDragInteractions(handle, newValue));
316 }
317 }
318 constructor(element, _config, _document, _ngZone, _viewportRuler, _dragDropRegistry) {
319 this._config = _config;
320 this._document = _document;
321 this._ngZone = _ngZone;
322 this._viewportRuler = _viewportRuler;
323 this._dragDropRegistry = _dragDropRegistry;
324 /**
325 * CSS `transform` applied to the element when it isn't being dragged. We need a
326 * passive transform in order for the dragged element to retain its new position
327 * after the user has stopped dragging and because we need to know the relative
328 * position in case they start dragging again. This corresponds to `element.style.transform`.
329 */
330 this._passiveTransform = { x: 0, y: 0 };
331 /** CSS `transform` that is applied to the element while it's being dragged. */
332 this._activeTransform = { x: 0, y: 0 };
333 /**
334 * Whether the dragging sequence has been started. Doesn't
335 * necessarily mean that the element has been moved.
336 */
337 this._hasStartedDragging = false;
338 /** Emits when the item is being moved. */
339 this._moveEvents = new Subject();
340 /** Subscription to pointer movement events. */
341 this._pointerMoveSubscription = Subscription.EMPTY;
342 /** Subscription to the event that is dispatched when the user lifts their pointer. */
343 this._pointerUpSubscription = Subscription.EMPTY;
344 /** Subscription to the viewport being scrolled. */
345 this._scrollSubscription = Subscription.EMPTY;
346 /** Subscription to the viewport being resized. */
347 this._resizeSubscription = Subscription.EMPTY;
348 /** Cached reference to the boundary element. */
349 this._boundaryElement = null;
350 /** Whether the native dragging interactions have been enabled on the root element. */
351 this._nativeInteractionsEnabled = true;
352 /** Elements that can be used to drag the draggable item. */
353 this._handles = [];
354 /** Registered handles that are currently disabled. */
355 this._disabledHandles = new Set();
356 /** Layout direction of the item. */
357 this._direction = 'ltr';
358 /**
359 * Amount of milliseconds to wait after the user has put their
360 * pointer down before starting to drag the element.
361 */
362 this.dragStartDelay = 0;
363 this._disabled = false;
364 /** Emits as the drag sequence is being prepared. */
365 this.beforeStarted = new Subject();
366 /** Emits when the user starts dragging the item. */
367 this.started = new Subject();
368 /** Emits when the user has released a drag item, before any animations have started. */
369 this.released = new Subject();
370 /** Emits when the user stops dragging an item in the container. */
371 this.ended = new Subject();
372 /** Emits when the user has moved the item into a new container. */
373 this.entered = new Subject();
374 /** Emits when the user removes the item its container by dragging it into another container. */
375 this.exited = new Subject();
376 /** Emits when the user drops the item inside a container. */
377 this.dropped = new Subject();
378 /**
379 * Emits as the user is dragging the item. Use with caution,
380 * because this event will fire for every pixel that the user has dragged.
381 */
382 this.moved = this._moveEvents;
383 /** Handler for the `mousedown`/`touchstart` events. */
384 this._pointerDown = (event) => {
385 this.beforeStarted.next();
386 // Delegate the event based on whether it started from a handle or the element itself.
387 if (this._handles.length) {
388 const targetHandle = this._getTargetHandle(event);
389 if (targetHandle && !this._disabledHandles.has(targetHandle) && !this.disabled) {
390 this._initializeDragSequence(targetHandle, event);
391 }
392 }
393 else if (!this.disabled) {
394 this._initializeDragSequence(this._rootElement, event);
395 }
396 };
397 /** Handler that is invoked when the user moves their pointer after they've initiated a drag. */
398 this._pointerMove = (event) => {
399 const pointerPosition = this._getPointerPositionOnPage(event);
400 if (!this._hasStartedDragging) {
401 const distanceX = Math.abs(pointerPosition.x - this._pickupPositionOnPage.x);
402 const distanceY = Math.abs(pointerPosition.y - this._pickupPositionOnPage.y);
403 const isOverThreshold = distanceX + distanceY >= this._config.dragStartThreshold;
404 // Only start dragging after the user has moved more than the minimum distance in either
405 // direction. Note that this is preferable over doing something like `skip(minimumDistance)`
406 // in the `pointerMove` subscription, because we're not guaranteed to have one move event
407 // per pixel of movement (e.g. if the user moves their pointer quickly).
408 if (isOverThreshold) {
409 const isDelayElapsed = Date.now() >= this._dragStartTime + this._getDragStartDelay(event);
410 const container = this._dropContainer;
411 if (!isDelayElapsed) {
412 this._endDragSequence(event);
413 return;
414 }
415 // Prevent other drag sequences from starting while something in the container is still
416 // being dragged. This can happen while we're waiting for the drop animation to finish
417 // and can cause errors, because some elements might still be moving around.
418 if (!container || (!container.isDragging() && !container.isReceiving())) {
419 // Prevent the default action as soon as the dragging sequence is considered as
420 // "started" since waiting for the next event can allow the device to begin scrolling.
421 event.preventDefault();
422 this._hasStartedDragging = true;
423 this._ngZone.run(() => this._startDragSequence(event));
424 }
425 }
426 return;
427 }
428 // We prevent the default action down here so that we know that dragging has started. This is
429 // important for touch devices where doing this too early can unnecessarily block scrolling,
430 // if there's a dragging delay.
431 event.preventDefault();
432 const constrainedPointerPosition = this._getConstrainedPointerPosition(pointerPosition);
433 this._hasMoved = true;
434 this._lastKnownPointerPosition = pointerPosition;
435 this._updatePointerDirectionDelta(constrainedPointerPosition);
436 if (this._dropContainer) {
437 this._updateActiveDropContainer(constrainedPointerPosition, pointerPosition);
438 }
439 else {
440 // If there's a position constraint function, we want the element's top/left to be at the
441 // specific position on the page. Use the initial position as a reference if that's the case.
442 const offset = this.constrainPosition ? this._initialClientRect : this._pickupPositionOnPage;
443 const activeTransform = this._activeTransform;
444 activeTransform.x = constrainedPointerPosition.x - offset.x + this._passiveTransform.x;
445 activeTransform.y = constrainedPointerPosition.y - offset.y + this._passiveTransform.y;
446 this._applyRootElementTransform(activeTransform.x, activeTransform.y);
447 }
448 // Since this event gets fired for every pixel while dragging, we only
449 // want to fire it if the consumer opted into it. Also we have to
450 // re-enter the zone because we run all of the events on the outside.
451 if (this._moveEvents.observers.length) {
452 this._ngZone.run(() => {
453 this._moveEvents.next({
454 source: this,
455 pointerPosition: constrainedPointerPosition,
456 event,
457 distance: this._getDragDistance(constrainedPointerPosition),
458 delta: this._pointerDirectionDelta,
459 });
460 });
461 }
462 };
463 /** Handler that is invoked when the user lifts their pointer up, after initiating a drag. */
464 this._pointerUp = (event) => {
465 this._endDragSequence(event);
466 };
467 /** Handles a native `dragstart` event. */
468 this._nativeDragStart = (event) => {
469 if (this._handles.length) {
470 const targetHandle = this._getTargetHandle(event);
471 if (targetHandle && !this._disabledHandles.has(targetHandle) && !this.disabled) {
472 event.preventDefault();
473 }
474 }
475 else if (!this.disabled) {
476 // Usually this isn't necessary since the we prevent the default action in `pointerDown`,
477 // but some cases like dragging of links can slip through (see #24403).
478 event.preventDefault();
479 }
480 };
481 this.withRootElement(element).withParent(_config.parentDragRef || null);
482 this._parentPositions = new ParentPositionTracker(_document);
483 _dragDropRegistry.registerDragItem(this);
484 }
485 /**
486 * Returns the element that is being used as a placeholder
487 * while the current element is being dragged.
488 */
489 getPlaceholderElement() {
490 return this._placeholder;
491 }
492 /** Returns the root draggable element. */
493 getRootElement() {
494 return this._rootElement;
495 }
496 /**
497 * Gets the currently-visible element that represents the drag item.
498 * While dragging this is the placeholder, otherwise it's the root element.
499 */
500 getVisibleElement() {
501 return this.isDragging() ? this.getPlaceholderElement() : this.getRootElement();
502 }
503 /** Registers the handles that can be used to drag the element. */
504 withHandles(handles) {
505 this._handles = handles.map(handle => coerceElement(handle));
506 this._handles.forEach(handle => toggleNativeDragInteractions(handle, this.disabled));
507 this._toggleNativeDragInteractions();
508 // Delete any lingering disabled handles that may have been destroyed. Note that we re-create
509 // the set, rather than iterate over it and filter out the destroyed handles, because while
510 // the ES spec allows for sets to be modified while they're being iterated over, some polyfills
511 // use an array internally which may throw an error.
512 const disabledHandles = new Set();
513 this._disabledHandles.forEach(handle => {
514 if (this._handles.indexOf(handle) > -1) {
515 disabledHandles.add(handle);
516 }
517 });
518 this._disabledHandles = disabledHandles;
519 return this;
520 }
521 /**
522 * Registers the template that should be used for the drag preview.
523 * @param template Template that from which to stamp out the preview.
524 */
525 withPreviewTemplate(template) {
526 this._previewTemplate = template;
527 return this;
528 }
529 /**
530 * Registers the template that should be used for the drag placeholder.
531 * @param template Template that from which to stamp out the placeholder.
532 */
533 withPlaceholderTemplate(template) {
534 this._placeholderTemplate = template;
535 return this;
536 }
537 /**
538 * Sets an alternate drag root element. The root element is the element that will be moved as
539 * the user is dragging. Passing an alternate root element is useful when trying to enable
540 * dragging on an element that you might not have access to.
541 */
542 withRootElement(rootElement) {
543 const element = coerceElement(rootElement);
544 if (element !== this._rootElement) {
545 if (this._rootElement) {
546 this._removeRootElementListeners(this._rootElement);
547 }
548 this._ngZone.runOutsideAngular(() => {
549 element.addEventListener('mousedown', this._pointerDown, activeEventListenerOptions);
550 element.addEventListener('touchstart', this._pointerDown, passiveEventListenerOptions);
551 element.addEventListener('dragstart', this._nativeDragStart, activeEventListenerOptions);
552 });
553 this._initialTransform = undefined;
554 this._rootElement = element;
555 }
556 if (typeof SVGElement !== 'undefined' && this._rootElement instanceof SVGElement) {
557 this._ownerSVGElement = this._rootElement.ownerSVGElement;
558 }
559 return this;
560 }
561 /**
562 * Element to which the draggable's position will be constrained.
563 */
564 withBoundaryElement(boundaryElement) {
565 this._boundaryElement = boundaryElement ? coerceElement(boundaryElement) : null;
566 this._resizeSubscription.unsubscribe();
567 if (boundaryElement) {
568 this._resizeSubscription = this._viewportRuler
569 .change(10)
570 .subscribe(() => this._containInsideBoundaryOnResize());
571 }
572 return this;
573 }
574 /** Sets the parent ref that the ref is nested in. */
575 withParent(parent) {
576 this._parentDragRef = parent;
577 return this;
578 }
579 /** Removes the dragging functionality from the DOM element. */
580 dispose() {
581 this._removeRootElementListeners(this._rootElement);
582 // Do this check before removing from the registry since it'll
583 // stop being considered as dragged once it is removed.
584 if (this.isDragging()) {
585 // Since we move out the element to the end of the body while it's being
586 // dragged, we have to make sure that it's removed if it gets destroyed.
587 this._rootElement?.remove();
588 }
589 this._anchor?.remove();
590 this._destroyPreview();
591 this._destroyPlaceholder();
592 this._dragDropRegistry.removeDragItem(this);
593 this._removeSubscriptions();
594 this.beforeStarted.complete();
595 this.started.complete();
596 this.released.complete();
597 this.ended.complete();
598 this.entered.complete();
599 this.exited.complete();
600 this.dropped.complete();
601 this._moveEvents.complete();
602 this._handles = [];
603 this._disabledHandles.clear();
604 this._dropContainer = undefined;
605 this._resizeSubscription.unsubscribe();
606 this._parentPositions.clear();
607 this._boundaryElement =
608 this._rootElement =
609 this._ownerSVGElement =
610 this._placeholderTemplate =
611 this._previewTemplate =
612 this._anchor =
613 this._parentDragRef =
614 null;
615 }
616 /** Checks whether the element is currently being dragged. */
617 isDragging() {
618 return this._hasStartedDragging && this._dragDropRegistry.isDragging(this);
619 }
620 /** Resets a standalone drag item to its initial position. */
621 reset() {
622 this._rootElement.style.transform = this._initialTransform || '';
623 this._activeTransform = { x: 0, y: 0 };
624 this._passiveTransform = { x: 0, y: 0 };
625 }
626 /**
627 * Sets a handle as disabled. While a handle is disabled, it'll capture and interrupt dragging.
628 * @param handle Handle element that should be disabled.
629 */
630 disableHandle(handle) {
631 if (!this._disabledHandles.has(handle) && this._handles.indexOf(handle) > -1) {
632 this._disabledHandles.add(handle);
633 toggleNativeDragInteractions(handle, true);
634 }
635 }
636 /**
637 * Enables a handle, if it has been disabled.
638 * @param handle Handle element to be enabled.
639 */
640 enableHandle(handle) {
641 if (this._disabledHandles.has(handle)) {
642 this._disabledHandles.delete(handle);
643 toggleNativeDragInteractions(handle, this.disabled);
644 }
645 }
646 /** Sets the layout direction of the draggable item. */
647 withDirection(direction) {
648 this._direction = direction;
649 return this;
650 }
651 /** Sets the container that the item is part of. */
652 _withDropContainer(container) {
653 this._dropContainer = container;
654 }
655 /**
656 * Gets the current position in pixels the draggable outside of a drop container.
657 */
658 getFreeDragPosition() {
659 const position = this.isDragging() ? this._activeTransform : this._passiveTransform;
660 return { x: position.x, y: position.y };
661 }
662 /**
663 * Sets the current position in pixels the draggable outside of a drop container.
664 * @param value New position to be set.
665 */
666 setFreeDragPosition(value) {
667 this._activeTransform = { x: 0, y: 0 };
668 this._passiveTransform.x = value.x;
669 this._passiveTransform.y = value.y;
670 if (!this._dropContainer) {
671 this._applyRootElementTransform(value.x, value.y);
672 }
673 return this;
674 }
675 /**
676 * Sets the container into which to insert the preview element.
677 * @param value Container into which to insert the preview.
678 */
679 withPreviewContainer(value) {
680 this._previewContainer = value;
681 return this;
682 }
683 /** Updates the item's sort order based on the last-known pointer position. */
684 _sortFromLastPointerPosition() {
685 const position = this._lastKnownPointerPosition;
686 if (position && this._dropContainer) {
687 this._updateActiveDropContainer(this._getConstrainedPointerPosition(position), position);
688 }
689 }
690 /** Unsubscribes from the global subscriptions. */
691 _removeSubscriptions() {
692 this._pointerMoveSubscription.unsubscribe();
693 this._pointerUpSubscription.unsubscribe();
694 this._scrollSubscription.unsubscribe();
695 }
696 /** Destroys the preview element and its ViewRef. */
697 _destroyPreview() {
698 this._preview?.remove();
699 this._previewRef?.destroy();
700 this._preview = this._previewRef = null;
701 }
702 /** Destroys the placeholder element and its ViewRef. */
703 _destroyPlaceholder() {
704 this._placeholder?.remove();
705 this._placeholderRef?.destroy();
706 this._placeholder = this._placeholderRef = null;
707 }
708 /**
709 * Clears subscriptions and stops the dragging sequence.
710 * @param event Browser event object that ended the sequence.
711 */
712 _endDragSequence(event) {
713 // Note that here we use `isDragging` from the service, rather than from `this`.
714 // The difference is that the one from the service reflects whether a dragging sequence
715 // has been initiated, whereas the one on `this` includes whether the user has passed
716 // the minimum dragging threshold.
717 if (!this._dragDropRegistry.isDragging(this)) {
718 return;
719 }
720 this._removeSubscriptions();
721 this._dragDropRegistry.stopDragging(this);
722 this._toggleNativeDragInteractions();
723 if (this._handles) {
724 this._rootElement.style.webkitTapHighlightColor =
725 this._rootElementTapHighlight;
726 }
727 if (!this._hasStartedDragging) {
728 return;
729 }
730 this.released.next({ source: this, event });
731 if (this._dropContainer) {
732 // Stop scrolling immediately, instead of waiting for the animation to finish.
733 this._dropContainer._stopScrolling();
734 this._animatePreviewToPlaceholder().then(() => {
735 this._cleanupDragArtifacts(event);
736 this._cleanupCachedDimensions();
737 this._dragDropRegistry.stopDragging(this);
738 });
739 }
740 else {
741 // Convert the active transform into a passive one. This means that next time
742 // the user starts dragging the item, its position will be calculated relatively
743 // to the new passive transform.
744 this._passiveTransform.x = this._activeTransform.x;
745 const pointerPosition = this._getPointerPositionOnPage(event);
746 this._passiveTransform.y = this._activeTransform.y;
747 this._ngZone.run(() => {
748 this.ended.next({
749 source: this,
750 distance: this._getDragDistance(pointerPosition),
751 dropPoint: pointerPosition,
752 event,
753 });
754 });
755 this._cleanupCachedDimensions();
756 this._dragDropRegistry.stopDragging(this);
757 }
758 }
759 /** Starts the dragging sequence. */
760 _startDragSequence(event) {
761 if (isTouchEvent(event)) {
762 this._lastTouchEventTime = Date.now();
763 }
764 this._toggleNativeDragInteractions();
765 const dropContainer = this._dropContainer;
766 if (dropContainer) {
767 const element = this._rootElement;
768 const parent = element.parentNode;
769 const placeholder = (this._placeholder = this._createPlaceholderElement());
770 const anchor = (this._anchor = this._anchor || this._document.createComment(''));
771 // Needs to happen before the root element is moved.
772 const shadowRoot = this._getShadowRoot();
773 // Insert an anchor node so that we can restore the element's position in the DOM.
774 parent.insertBefore(anchor, element);
775 // There's no risk of transforms stacking when inside a drop container so
776 // we can keep the initial transform up to date any time dragging starts.
777 this._initialTransform = element.style.transform || '';
778 // Create the preview after the initial transform has
779 // been cached, because it can be affected by the transform.
780 this._preview = this._createPreviewElement();
781 // We move the element out at the end of the body and we make it hidden, because keeping it in
782 // place will throw off the consumer's `:last-child` selectors. We can't remove the element
783 // from the DOM completely, because iOS will stop firing all subsequent events in the chain.
784 toggleVisibility(element, false, dragImportantProperties);
785 this._document.body.appendChild(parent.replaceChild(placeholder, element));
786 this._getPreviewInsertionPoint(parent, shadowRoot).appendChild(this._preview);
787 this.started.next({ source: this, event }); // Emit before notifying the container.
788 dropContainer.start();
789 this._initialContainer = dropContainer;
790 this._initialIndex = dropContainer.getItemIndex(this);
791 }
792 else {
793 this.started.next({ source: this, event });
794 this._initialContainer = this._initialIndex = undefined;
795 }
796 // Important to run after we've called `start` on the parent container
797 // so that it has had time to resolve its scrollable parents.
798 this._parentPositions.cache(dropContainer ? dropContainer.getScrollableParents() : []);
799 }
800 /**
801 * Sets up the different variables and subscriptions
802 * that will be necessary for the dragging sequence.
803 * @param referenceElement Element that started the drag sequence.
804 * @param event Browser event object that started the sequence.
805 */
806 _initializeDragSequence(referenceElement, event) {
807 // Stop propagation if the item is inside another
808 // draggable so we don't start multiple drag sequences.
809 if (this._parentDragRef) {
810 event.stopPropagation();
811 }
812 const isDragging = this.isDragging();
813 const isTouchSequence = isTouchEvent(event);
814 const isAuxiliaryMouseButton = !isTouchSequence && event.button !== 0;
815 const rootElement = this._rootElement;
816 const target = _getEventTarget(event);
817 const isSyntheticEvent = !isTouchSequence &&
818 this._lastTouchEventTime &&
819 this._lastTouchEventTime + MOUSE_EVENT_IGNORE_TIME > Date.now();
820 const isFakeEvent = isTouchSequence
821 ? isFakeTouchstartFromScreenReader(event)
822 : isFakeMousedownFromScreenReader(event);
823 // If the event started from an element with the native HTML drag&drop, it'll interfere
824 // with our own dragging (e.g. `img` tags do it by default). Prevent the default action
825 // to stop it from happening. Note that preventing on `dragstart` also seems to work, but
826 // it's flaky and it fails if the user drags it away quickly. Also note that we only want
827 // to do this for `mousedown` since doing the same for `touchstart` will stop any `click`
828 // events from firing on touch devices.
829 if (target && target.draggable && event.type === 'mousedown') {
830 event.preventDefault();
831 }
832 // Abort if the user is already dragging or is using a mouse button other than the primary one.
833 if (isDragging || isAuxiliaryMouseButton || isSyntheticEvent || isFakeEvent) {
834 return;
835 }
836 // If we've got handles, we need to disable the tap highlight on the entire root element,
837 // otherwise iOS will still add it, even though all the drag interactions on the handle
838 // are disabled.
839 if (this._handles.length) {
840 const rootStyles = rootElement.style;
841 this._rootElementTapHighlight = rootStyles.webkitTapHighlightColor || '';
842 rootStyles.webkitTapHighlightColor = 'transparent';
843 }
844 this._hasStartedDragging = this._hasMoved = false;
845 // Avoid multiple subscriptions and memory leaks when multi touch
846 // (isDragging check above isn't enough because of possible temporal and/or dimensional delays)
847 this._removeSubscriptions();
848 this._initialClientRect = this._rootElement.getBoundingClientRect();
849 this._pointerMoveSubscription = this._dragDropRegistry.pointerMove.subscribe(this._pointerMove);
850 this._pointerUpSubscription = this._dragDropRegistry.pointerUp.subscribe(this._pointerUp);
851 this._scrollSubscription = this._dragDropRegistry
852 .scrolled(this._getShadowRoot())
853 .subscribe(scrollEvent => this._updateOnScroll(scrollEvent));
854 if (this._boundaryElement) {
855 this._boundaryRect = getMutableClientRect(this._boundaryElement);
856 }
857 // If we have a custom preview we can't know ahead of time how large it'll be so we position
858 // it next to the cursor. The exception is when the consumer has opted into making the preview
859 // the same size as the root element, in which case we do know the size.
860 const previewTemplate = this._previewTemplate;
861 this._pickupPositionInElement =
862 previewTemplate && previewTemplate.template && !previewTemplate.matchSize
863 ? { x: 0, y: 0 }
864 : this._getPointerPositionInElement(this._initialClientRect, referenceElement, event);
865 const pointerPosition = (this._pickupPositionOnPage =
866 this._lastKnownPointerPosition =
867 this._getPointerPositionOnPage(event));
868 this._pointerDirectionDelta = { x: 0, y: 0 };
869 this._pointerPositionAtLastDirectionChange = { x: pointerPosition.x, y: pointerPosition.y };
870 this._dragStartTime = Date.now();
871 this._dragDropRegistry.startDragging(this, event);
872 }
873 /** Cleans up the DOM artifacts that were added to facilitate the element being dragged. */
874 _cleanupDragArtifacts(event) {
875 // Restore the element's visibility and insert it at its old position in the DOM.
876 // It's important that we maintain the position, because moving the element around in the DOM
877 // can throw off `NgFor` which does smart diffing and re-creates elements only when necessary,
878 // while moving the existing elements in all other cases.
879 toggleVisibility(this._rootElement, true, dragImportantProperties);
880 this._anchor.parentNode.replaceChild(this._rootElement, this._anchor);
881 this._destroyPreview();
882 this._destroyPlaceholder();
883 this._initialClientRect =
884 this._boundaryRect =
885 this._previewRect =
886 this._initialTransform =
887 undefined;
888 // Re-enter the NgZone since we bound `document` events on the outside.
889 this._ngZone.run(() => {
890 const container = this._dropContainer;
891 const currentIndex = container.getItemIndex(this);
892 const pointerPosition = this._getPointerPositionOnPage(event);
893 const distance = this._getDragDistance(pointerPosition);
894 const isPointerOverContainer = container._isOverContainer(pointerPosition.x, pointerPosition.y);
895 this.ended.next({ source: this, distance, dropPoint: pointerPosition, event });
896 this.dropped.next({
897 item: this,
898 currentIndex,
899 previousIndex: this._initialIndex,
900 container: container,
901 previousContainer: this._initialContainer,
902 isPointerOverContainer,
903 distance,
904 dropPoint: pointerPosition,
905 event,
906 });
907 container.drop(this, currentIndex, this._initialIndex, this._initialContainer, isPointerOverContainer, distance, pointerPosition, event);
908 this._dropContainer = this._initialContainer;
909 });
910 }
911 /**
912 * Updates the item's position in its drop container, or moves it
913 * into a new one, depending on its current drag position.
914 */
915 _updateActiveDropContainer({ x, y }, { x: rawX, y: rawY }) {
916 // Drop container that draggable has been moved into.
917 let newContainer = this._initialContainer._getSiblingContainerFromPosition(this, x, y);
918 // If we couldn't find a new container to move the item into, and the item has left its
919 // initial container, check whether the it's over the initial container. This handles the
920 // case where two containers are connected one way and the user tries to undo dragging an
921 // item into a new container.
922 if (!newContainer &&
923 this._dropContainer !== this._initialContainer &&
924 this._initialContainer._isOverContainer(x, y)) {
925 newContainer = this._initialContainer;
926 }
927 if (newContainer && newContainer !== this._dropContainer) {
928 this._ngZone.run(() => {
929 // Notify the old container that the item has left.
930 this.exited.next({ item: this, container: this._dropContainer });
931 this._dropContainer.exit(this);
932 // Notify the new container that the item has entered.
933 this._dropContainer = newContainer;
934 this._dropContainer.enter(this, x, y, newContainer === this._initialContainer &&
935 // If we're re-entering the initial container and sorting is disabled,
936 // put item the into its starting index to begin with.
937 newContainer.sortingDisabled
938 ? this._initialIndex
939 : undefined);
940 this.entered.next({
941 item: this,
942 container: newContainer,
943 currentIndex: newContainer.getItemIndex(this),
944 });
945 });
946 }
947 // Dragging may have been interrupted as a result of the events above.
948 if (this.isDragging()) {
949 this._dropContainer._startScrollingIfNecessary(rawX, rawY);
950 this._dropContainer._sortItem(this, x, y, this._pointerDirectionDelta);
951 if (this.constrainPosition) {
952 this._applyPreviewTransform(x, y);
953 }
954 else {
955 this._applyPreviewTransform(x - this._pickupPositionInElement.x, y - this._pickupPositionInElement.y);
956 }
957 }
958 }
959 /**
960 * Creates the element that will be rendered next to the user's pointer
961 * and will be used as a preview of the element that is being dragged.
962 */
963 _createPreviewElement() {
964 const previewConfig = this._previewTemplate;
965 const previewClass = this.previewClass;
966 const previewTemplate = previewConfig ? previewConfig.template : null;
967 let preview;
968 if (previewTemplate && previewConfig) {
969 // Measure the element before we've inserted the preview
970 // since the insertion could throw off the measurement.
971 const rootRect = previewConfig.matchSize ? this._initialClientRect : null;
972 const viewRef = previewConfig.viewContainer.createEmbeddedView(previewTemplate, previewConfig.context);
973 viewRef.detectChanges();
974 preview = getRootNode(viewRef, this._document);
975 this._previewRef = viewRef;
976 if (previewConfig.matchSize) {
977 matchElementSize(preview, rootRect);
978 }
979 else {
980 preview.style.transform = getTransform(this._pickupPositionOnPage.x, this._pickupPositionOnPage.y);
981 }
982 }
983 else {
984 preview = deepCloneNode(this._rootElement);
985 matchElementSize(preview, this._initialClientRect);
986 if (this._initialTransform) {
987 preview.style.transform = this._initialTransform;
988 }
989 }
990 extendStyles(preview.style, {
991 // It's important that we disable the pointer events on the preview, because
992 // it can throw off the `document.elementFromPoint` calls in the `CdkDropList`.
993 'pointer-events': 'none',
994 // We have to reset the margin, because it can throw off positioning relative to the viewport.
995 'margin': '0',
996 'position': 'fixed',
997 'top': '0',
998 'left': '0',
999 'z-index': `${this._config.zIndex || 1000}`,
1000 }, dragImportantProperties);
1001 toggleNativeDragInteractions(preview, false);
1002 preview.classList.add('cdk-drag-preview');
1003 preview.setAttribute('dir', this._direction);
1004 if (previewClass) {
1005 if (Array.isArray(previewClass)) {
1006 previewClass.forEach(className => preview.classList.add(className));
1007 }
1008 else {
1009 preview.classList.add(previewClass);
1010 }
1011 }
1012 return preview;
1013 }
1014 /**
1015 * Animates the preview element from its current position to the location of the drop placeholder.
1016 * @returns Promise that resolves when the animation completes.
1017 */
1018 _animatePreviewToPlaceholder() {
1019 // If the user hasn't moved yet, the transitionend event won't fire.
1020 if (!this._hasMoved) {
1021 return Promise.resolve();
1022 }
1023 const placeholderRect = this._placeholder.getBoundingClientRect();
1024 // Apply the class that adds a transition to the preview.
1025 this._preview.classList.add('cdk-drag-animating');
1026 // Move the preview to the placeholder position.
1027 this._applyPreviewTransform(placeholderRect.left, placeholderRect.top);
1028 // If the element doesn't have a `transition`, the `transitionend` event won't fire. Since
1029 // we need to trigger a style recalculation in order for the `cdk-drag-animating` class to
1030 // apply its style, we take advantage of the available info to figure out whether we need to
1031 // bind the event in the first place.
1032 const duration = getTransformTransitionDurationInMs(this._preview);
1033 if (duration === 0) {
1034 return Promise.resolve();
1035 }
1036 return this._ngZone.runOutsideAngular(() => {
1037 return new Promise(resolve => {
1038 const handler = ((event) => {
1039 if (!event ||
1040 (_getEventTarget(event) === this._preview && event.propertyName === 'transform')) {
1041 this._preview?.removeEventListener('transitionend', handler);
1042 resolve();
1043 clearTimeout(timeout);
1044 }
1045 });
1046 // If a transition is short enough, the browser might not fire the `transitionend` event.
1047 // Since we know how long it's supposed to take, add a timeout with a 50% buffer that'll
1048 // fire if the transition hasn't completed when it was supposed to.
1049 const timeout = setTimeout(handler, duration * 1.5);
1050 this._preview.addEventListener('transitionend', handler);
1051 });
1052 });
1053 }
1054 /** Creates an element that will be shown instead of the current element while dragging. */
1055 _createPlaceholderElement() {
1056 const placeholderConfig = this._placeholderTemplate;
1057 const placeholderTemplate = placeholderConfig ? placeholderConfig.template : null;
1058 let placeholder;
1059 if (placeholderTemplate) {
1060 this._placeholderRef = placeholderConfig.viewContainer.createEmbeddedView(placeholderTemplate, placeholderConfig.context);
1061 this._placeholderRef.detectChanges();
1062 placeholder = getRootNode(this._placeholderRef, this._document);
1063 }
1064 else {
1065 placeholder = deepCloneNode(this._rootElement);
1066 }
1067 // Stop pointer events on the preview so the user can't
1068 // interact with it while the preview is animating.
1069 placeholder.style.pointerEvents = 'none';
1070 placeholder.classList.add('cdk-drag-placeholder');
1071 return placeholder;
1072 }
1073 /**
1074 * Figures out the coordinates at which an element was picked up.
1075 * @param referenceElement Element that initiated the dragging.
1076 * @param event Event that initiated the dragging.
1077 */
1078 _getPointerPositionInElement(elementRect, referenceElement, event) {
1079 const handleElement = referenceElement === this._rootElement ? null : referenceElement;
1080 const referenceRect = handleElement ? handleElement.getBoundingClientRect() : elementRect;
1081 const point = isTouchEvent(event) ? event.targetTouches[0] : event;
1082 const scrollPosition = this._getViewportScrollPosition();
1083 const x = point.pageX - referenceRect.left - scrollPosition.left;
1084 const y = point.pageY - referenceRect.top - scrollPosition.top;
1085 return {
1086 x: referenceRect.left - elementRect.left + x,
1087 y: referenceRect.top - elementRect.top + y,
1088 };
1089 }
1090 /** Determines the point of the page that was touched by the user. */
1091 _getPointerPositionOnPage(event) {
1092 const scrollPosition = this._getViewportScrollPosition();
1093 const point = isTouchEvent(event)
1094 ? // `touches` will be empty for start/end events so we have to fall back to `changedTouches`.
1095 // Also note that on real devices we're guaranteed for either `touches` or `changedTouches`
1096 // to have a value, but Firefox in device emulation mode has a bug where both can be empty
1097 // for `touchstart` and `touchend` so we fall back to a dummy object in order to avoid
1098 // throwing an error. The value returned here will be incorrect, but since this only
1099 // breaks inside a developer tool and the value is only used for secondary information,
1100 // we can get away with it. See https://bugzilla.mozilla.org/show_bug.cgi?id=1615824.
1101 event.touches[0] || event.changedTouches[0] || { pageX: 0, pageY: 0 }
1102 : event;
1103 const x = point.pageX - scrollPosition.left;
1104 const y = point.pageY - scrollPosition.top;
1105 // if dragging SVG element, try to convert from the screen coordinate system to the SVG
1106 // coordinate system
1107 if (this._ownerSVGElement) {
1108 const svgMatrix = this._ownerSVGElement.getScreenCTM();
1109 if (svgMatrix) {
1110 const svgPoint = this._ownerSVGElement.createSVGPoint();
1111 svgPoint.x = x;
1112 svgPoint.y = y;
1113 return svgPoint.matrixTransform(svgMatrix.inverse());
1114 }
1115 }
1116 return { x, y };
1117 }
1118 /** Gets the pointer position on the page, accounting for any position constraints. */
1119 _getConstrainedPointerPosition(point) {
1120 const dropContainerLock = this._dropContainer ? this._dropContainer.lockAxis : null;
1121 let { x, y } = this.constrainPosition
1122 ? this.constrainPosition(point, this, this._initialClientRect, this._pickupPositionInElement)
1123 : point;
1124 if (this.lockAxis === 'x' || dropContainerLock === 'x') {
1125 y = this._pickupPositionOnPage.y;
1126 }
1127 else if (this.lockAxis === 'y' || dropContainerLock === 'y') {
1128 x = this._pickupPositionOnPage.x;
1129 }
1130 if (this._boundaryRect) {
1131 const { x: pickupX, y: pickupY } = this._pickupPositionInElement;
1132 const boundaryRect = this._boundaryRect;
1133 const { width: previewWidth, height: previewHeight } = this._getPreviewRect();
1134 const minY = boundaryRect.top + pickupY;
1135 const maxY = boundaryRect.bottom - (previewHeight - pickupY);
1136 const minX = boundaryRect.left + pickupX;
1137 const maxX = boundaryRect.right - (previewWidth - pickupX);
1138 x = clamp$1(x, minX, maxX);
1139 y = clamp$1(y, minY, maxY);
1140 }
1141 return { x, y };
1142 }
1143 /** Updates the current drag delta, based on the user's current pointer position on the page. */
1144 _updatePointerDirectionDelta(pointerPositionOnPage) {
1145 const { x, y } = pointerPositionOnPage;
1146 const delta = this._pointerDirectionDelta;
1147 const positionSinceLastChange = this._pointerPositionAtLastDirectionChange;
1148 // Amount of pixels the user has dragged since the last time the direction changed.
1149 const changeX = Math.abs(x - positionSinceLastChange.x);
1150 const changeY = Math.abs(y - positionSinceLastChange.y);
1151 // Because we handle pointer events on a per-pixel basis, we don't want the delta
1152 // to change for every pixel, otherwise anything that depends on it can look erratic.
1153 // To make the delta more consistent, we track how much the user has moved since the last
1154 // delta change and we only update it after it has reached a certain threshold.
1155 if (changeX > this._config.pointerDirectionChangeThreshold) {
1156 delta.x = x > positionSinceLastChange.x ? 1 : -1;
1157 positionSinceLastChange.x = x;
1158 }
1159 if (changeY > this._config.pointerDirectionChangeThreshold) {
1160 delta.y = y > positionSinceLastChange.y ? 1 : -1;
1161 positionSinceLastChange.y = y;
1162 }
1163 return delta;
1164 }
1165 /** Toggles the native drag interactions, based on how many handles are registered. */
1166 _toggleNativeDragInteractions() {
1167 if (!this._rootElement || !this._handles) {
1168 return;
1169 }
1170 const shouldEnable = this._handles.length > 0 || !this.isDragging();
1171 if (shouldEnable !== this._nativeInteractionsEnabled) {
1172 this._nativeInteractionsEnabled = shouldEnable;
1173 toggleNativeDragInteractions(this._rootElement, shouldEnable);
1174 }
1175 }
1176 /** Removes the manually-added event listeners from the root element. */
1177 _removeRootElementListeners(element) {
1178 element.removeEventListener('mousedown', this._pointerDown, activeEventListenerOptions);
1179 element.removeEventListener('touchstart', this._pointerDown, passiveEventListenerOptions);
1180 element.removeEventListener('dragstart', this._nativeDragStart, activeEventListenerOptions);
1181 }
1182 /**
1183 * Applies a `transform` to the root element, taking into account any existing transforms on it.
1184 * @param x New transform value along the X axis.
1185 * @param y New transform value along the Y axis.
1186 */
1187 _applyRootElementTransform(x, y) {
1188 const transform = getTransform(x, y);
1189 const styles = this._rootElement.style;
1190 // Cache the previous transform amount only after the first drag sequence, because
1191 // we don't want our own transforms to stack on top of each other.
1192 // Should be excluded none because none + translate3d(x, y, x) is invalid css
1193 if (this._initialTransform == null) {
1194 this._initialTransform =
1195 styles.transform && styles.transform != 'none' ? styles.transform : '';
1196 }
1197 // Preserve the previous `transform` value, if there was one. Note that we apply our own
1198 // transform before the user's, because things like rotation can affect which direction
1199 // the element will be translated towards.
1200 styles.transform = combineTransforms(transform, this._initialTransform);
1201 }
1202 /**
1203 * Applies a `transform` to the preview, taking into account any existing transforms on it.
1204 * @param x New transform value along the X axis.
1205 * @param y New transform value along the Y axis.
1206 */
1207 _applyPreviewTransform(x, y) {
1208 // Only apply the initial transform if the preview is a clone of the original element, otherwise
1209 // it could be completely different and the transform might not make sense anymore.
1210 const initialTransform = this._previewTemplate?.template ? undefined : this._initialTransform;
1211 const transform = getTransform(x, y);
1212 this._preview.style.transform = combineTransforms(transform, initialTransform);
1213 }
1214 /**
1215 * Gets the distance that the user has dragged during the current drag sequence.
1216 * @param currentPosition Current position of the user's pointer.
1217 */
1218 _getDragDistance(currentPosition) {
1219 const pickupPosition = this._pickupPositionOnPage;
1220 if (pickupPosition) {
1221 return { x: currentPosition.x - pickupPosition.x, y: currentPosition.y - pickupPosition.y };
1222 }
1223 return { x: 0, y: 0 };
1224 }
1225 /** Cleans up any cached element dimensions that we don't need after dragging has stopped. */
1226 _cleanupCachedDimensions() {
1227 this._boundaryRect = this._previewRect = undefined;
1228 this._parentPositions.clear();
1229 }
1230 /**
1231 * Checks whether the element is still inside its boundary after the viewport has been resized.
1232 * If not, the position is adjusted so that the element fits again.
1233 */
1234 _containInsideBoundaryOnResize() {
1235 let { x, y } = this._passiveTransform;
1236 if ((x === 0 && y === 0) || this.isDragging() || !this._boundaryElement) {
1237 return;
1238 }
1239 // Note: don't use `_clientRectAtStart` here, because we want the latest position.
1240 const elementRect = this._rootElement.getBoundingClientRect();
1241 const boundaryRect = this._boundaryElement.getBoundingClientRect();
1242 // It's possible that the element got hidden away after dragging (e.g. by switching to a
1243 // different tab). Don't do anything in this case so we don't clear the user's position.
1244 if ((boundaryRect.width === 0 && boundaryRect.height === 0) ||
1245 (elementRect.width === 0 && elementRect.height === 0)) {
1246 return;
1247 }
1248 const leftOverflow = boundaryRect.left - elementRect.left;
1249 const rightOverflow = elementRect.right - boundaryRect.right;
1250 const topOverflow = boundaryRect.top - elementRect.top;
1251 const bottomOverflow = elementRect.bottom - boundaryRect.bottom;
1252 // If the element has become wider than the boundary, we can't
1253 // do much to make it fit so we just anchor it to the left.
1254 if (boundaryRect.width > elementRect.width) {
1255 if (leftOverflow > 0) {
1256 x += leftOverflow;
1257 }
1258 if (rightOverflow > 0) {
1259 x -= rightOverflow;
1260 }
1261 }
1262 else {
1263 x = 0;
1264 }
1265 // If the element has become taller than the boundary, we can't
1266 // do much to make it fit so we just anchor it to the top.
1267 if (boundaryRect.height > elementRect.height) {
1268 if (topOverflow > 0) {
1269 y += topOverflow;
1270 }
1271 if (bottomOverflow > 0) {
1272 y -= bottomOverflow;
1273 }
1274 }
1275 else {
1276 y = 0;
1277 }
1278 if (x !== this._passiveTransform.x || y !== this._passiveTransform.y) {
1279 this.setFreeDragPosition({ y, x });
1280 }
1281 }
1282 /** Gets the drag start delay, based on the event type. */
1283 _getDragStartDelay(event) {
1284 const value = this.dragStartDelay;
1285 if (typeof value === 'number') {
1286 return value;
1287 }
1288 else if (isTouchEvent(event)) {
1289 return value.touch;
1290 }
1291 return value ? value.mouse : 0;
1292 }
1293 /** Updates the internal state of the draggable element when scrolling has occurred. */
1294 _updateOnScroll(event) {
1295 const scrollDifference = this._parentPositions.handleScroll(event);
1296 if (scrollDifference) {
1297 const target = _getEventTarget(event);
1298 // ClientRect dimensions are based on the scroll position of the page and its parent
1299 // node so we have to update the cached boundary ClientRect if the user has scrolled.
1300 if (this._boundaryRect &&
1301 target !== this._boundaryElement &&
1302 target.contains(this._boundaryElement)) {
1303 adjustClientRect(this._boundaryRect, scrollDifference.top, scrollDifference.left);
1304 }
1305 this._pickupPositionOnPage.x += scrollDifference.left;
1306 this._pickupPositionOnPage.y += scrollDifference.top;
1307 // If we're in free drag mode, we have to update the active transform, because
1308 // it isn't relative to the viewport like the preview inside a drop list.
1309 if (!this._dropContainer) {
1310 this._activeTransform.x -= scrollDifference.left;
1311 this._activeTransform.y -= scrollDifference.top;
1312 this._applyRootElementTransform(this._activeTransform.x, this._activeTransform.y);
1313 }
1314 }
1315 }
1316 /** Gets the scroll position of the viewport. */
1317 _getViewportScrollPosition() {
1318 return (this._parentPositions.positions.get(this._document)?.scrollPosition ||
1319 this._parentPositions.getViewportScrollPosition());
1320 }
1321 /**
1322 * Lazily resolves and returns the shadow root of the element. We do this in a function, rather
1323 * than saving it in property directly on init, because we want to resolve it as late as possible
1324 * in order to ensure that the element has been moved into the shadow DOM. Doing it inside the
1325 * constructor might be too early if the element is inside of something like `ngFor` or `ngIf`.
1326 */
1327 _getShadowRoot() {
1328 if (this._cachedShadowRoot === undefined) {
1329 this._cachedShadowRoot = _getShadowRoot(this._rootElement);
1330 }
1331 return this._cachedShadowRoot;
1332 }
1333 /** Gets the element into which the drag preview should be inserted. */
1334 _getPreviewInsertionPoint(initialParent, shadowRoot) {
1335 const previewContainer = this._previewContainer || 'global';
1336 if (previewContainer === 'parent') {
1337 return initialParent;
1338 }
1339 if (previewContainer === 'global') {
1340 const documentRef = this._document;
1341 // We can't use the body if the user is in fullscreen mode,
1342 // because the preview will render under the fullscreen element.
1343 // TODO(crisbeto): dedupe this with the `FullscreenOverlayContainer` eventually.
1344 return (shadowRoot ||
1345 documentRef.fullscreenElement ||
1346 documentRef.webkitFullscreenElement ||
1347 documentRef.mozFullScreenElement ||
1348 documentRef.msFullscreenElement ||
1349 documentRef.body);
1350 }
1351 return coerceElement(previewContainer);
1352 }
1353 /** Lazily resolves and returns the dimensions of the preview. */
1354 _getPreviewRect() {
1355 // Cache the preview element rect if we haven't cached it already or if
1356 // we cached it too early before the element dimensions were computed.
1357 if (!this._previewRect || (!this._previewRect.width && !this._previewRect.height)) {
1358 this._previewRect = this._preview
1359 ? this._preview.getBoundingClientRect()
1360 : this._initialClientRect;
1361 }
1362 return this._previewRect;
1363 }
1364 /** Gets a handle that is the target of an event. */
1365 _getTargetHandle(event) {
1366 return this._handles.find(handle => {
1367 return event.target && (event.target === handle || handle.contains(event.target));
1368 });
1369 }
1370}
1371/**
1372 * Gets a 3d `transform` that can be applied to an element.
1373 * @param x Desired position of the element along the X axis.
1374 * @param y Desired position of the element along the Y axis.
1375 */
1376function getTransform(x, y) {
1377 // Round the transforms since some browsers will
1378 // blur the elements for sub-pixel transforms.
1379 return `translate3d(${Math.round(x)}px, ${Math.round(y)}px, 0)`;
1380}
1381/** Clamps a value between a minimum and a maximum. */
1382function clamp$1(value, min, max) {
1383 return Math.max(min, Math.min(max, value));
1384}
1385/** Determines whether an event is a touch event. */
1386function isTouchEvent(event) {
1387 // This function is called for every pixel that the user has dragged so we need it to be
1388 // as fast as possible. Since we only bind mouse events and touch events, we can assume
1389 // that if the event's name starts with `t`, it's a touch event.
1390 return event.type[0] === 't';
1391}
1392/**
1393 * Gets the root HTML element of an embedded view.
1394 * If the root is not an HTML element it gets wrapped in one.
1395 */
1396function getRootNode(viewRef, _document) {
1397 const rootNodes = viewRef.rootNodes;
1398 if (rootNodes.length === 1 && rootNodes[0].nodeType === _document.ELEMENT_NODE) {
1399 return rootNodes[0];
1400 }
1401 const wrapper = _document.createElement('div');
1402 rootNodes.forEach(node => wrapper.appendChild(node));
1403 return wrapper;
1404}
1405/**
1406 * Matches the target element's size to the source's size.
1407 * @param target Element that needs to be resized.
1408 * @param sourceRect Dimensions of the source element.
1409 */
1410function matchElementSize(target, sourceRect) {
1411 target.style.width = `${sourceRect.width}px`;
1412 target.style.height = `${sourceRect.height}px`;
1413 target.style.transform = getTransform(sourceRect.left, sourceRect.top);
1414}
1415
1416/**
1417 * Moves an item one index in an array to another.
1418 * @param array Array in which to move the item.
1419 * @param fromIndex Starting index of the item.
1420 * @param toIndex Index to which the item should be moved.
1421 */
1422function moveItemInArray(array, fromIndex, toIndex) {
1423 const from = clamp(fromIndex, array.length - 1);
1424 const to = clamp(toIndex, array.length - 1);
1425 if (from === to) {
1426 return;
1427 }
1428 const target = array[from];
1429 const delta = to < from ? -1 : 1;
1430 for (let i = from; i !== to; i += delta) {
1431 array[i] = array[i + delta];
1432 }
1433 array[to] = target;
1434}
1435/**
1436 * Moves an item from one array to another.
1437 * @param currentArray Array from which to transfer the item.
1438 * @param targetArray Array into which to put the item.
1439 * @param currentIndex Index of the item in its current array.
1440 * @param targetIndex Index at which to insert the item.
1441 */
1442function transferArrayItem(currentArray, targetArray, currentIndex, targetIndex) {
1443 const from = clamp(currentIndex, currentArray.length - 1);
1444 const to = clamp(targetIndex, targetArray.length);
1445 if (currentArray.length) {
1446 targetArray.splice(to, 0, currentArray.splice(from, 1)[0]);
1447 }
1448}
1449/**
1450 * Copies an item from one array to another, leaving it in its
1451 * original position in current array.
1452 * @param currentArray Array from which to copy the item.
1453 * @param targetArray Array into which is copy the item.
1454 * @param currentIndex Index of the item in its current array.
1455 * @param targetIndex Index at which to insert the item.
1456 *
1457 */
1458function copyArrayItem(currentArray, targetArray, currentIndex, targetIndex) {
1459 const to = clamp(targetIndex, targetArray.length);
1460 if (currentArray.length) {
1461 targetArray.splice(to, 0, currentArray[currentIndex]);
1462 }
1463}
1464/** Clamps a number between zero and a maximum. */
1465function clamp(value, max) {
1466 return Math.max(0, Math.min(max, value));
1467}
1468
1469/**
1470 * Strategy that only supports sorting along a single axis.
1471 * Items are reordered using CSS transforms which allows for sorting to be animated.
1472 * @docs-private
1473 */
1474class SingleAxisSortStrategy {
1475 constructor(_element, _dragDropRegistry) {
1476 this._element = _element;
1477 this._dragDropRegistry = _dragDropRegistry;
1478 /** Cache of the dimensions of all the items inside the container. */
1479 this._itemPositions = [];
1480 /** Direction in which the list is oriented. */
1481 this.orientation = 'vertical';
1482 /**
1483 * Keeps track of the item that was last swapped with the dragged item, as well as what direction
1484 * the pointer was moving in when the swap occurred and whether the user's pointer continued to
1485 * overlap with the swapped item after the swapping occurred.
1486 */
1487 this._previousSwap = {
1488 drag: null,
1489 delta: 0,
1490 overlaps: false,
1491 };
1492 }
1493 /**
1494 * To be called when the drag sequence starts.
1495 * @param items Items that are currently in the list.
1496 */
1497 start(items) {
1498 this.withItems(items);
1499 }
1500 /**
1501 * To be called when an item is being sorted.
1502 * @param item Item to be sorted.
1503 * @param pointerX Position of the item along the X axis.
1504 * @param pointerY Position of the item along the Y axis.
1505 * @param pointerDelta Direction in which the pointer is moving along each axis.
1506 */
1507 sort(item, pointerX, pointerY, pointerDelta) {
1508 const siblings = this._itemPositions;
1509 const newIndex = this._getItemIndexFromPointerPosition(item, pointerX, pointerY, pointerDelta);
1510 if (newIndex === -1 && siblings.length > 0) {
1511 return null;
1512 }
1513 const isHorizontal = this.orientation === 'horizontal';
1514 const currentIndex = siblings.findIndex(currentItem => currentItem.drag === item);
1515 const siblingAtNewPosition = siblings[newIndex];
1516 const currentPosition = siblings[currentIndex].clientRect;
1517 const newPosition = siblingAtNewPosition.clientRect;
1518 const delta = currentIndex > newIndex ? 1 : -1;
1519 // How many pixels the item's placeholder should be offset.
1520 const itemOffset = this._getItemOffsetPx(currentPosition, newPosition, delta);
1521 // How many pixels all the other items should be offset.
1522 const siblingOffset = this._getSiblingOffsetPx(currentIndex, siblings, delta);
1523 // Save the previous order of the items before moving the item to its new index.
1524 // We use this to check whether an item has been moved as a result of the sorting.
1525 const oldOrder = siblings.slice();
1526 // Shuffle the array in place.
1527 moveItemInArray(siblings, currentIndex, newIndex);
1528 siblings.forEach((sibling, index) => {
1529 // Don't do anything if the position hasn't changed.
1530 if (oldOrder[index] === sibling) {
1531 return;
1532 }
1533 const isDraggedItem = sibling.drag === item;
1534 const offset = isDraggedItem ? itemOffset : siblingOffset;
1535 const elementToOffset = isDraggedItem
1536 ? item.getPlaceholderElement()
1537 : sibling.drag.getRootElement();
1538 // Update the offset to reflect the new position.
1539 sibling.offset += offset;
1540 // Since we're moving the items with a `transform`, we need to adjust their cached
1541 // client rects to reflect their new position, as well as swap their positions in the cache.
1542 // Note that we shouldn't use `getBoundingClientRect` here to update the cache, because the
1543 // elements may be mid-animation which will give us a wrong result.
1544 if (isHorizontal) {
1545 // Round the transforms since some browsers will
1546 // blur the elements, for sub-pixel transforms.
1547 elementToOffset.style.transform = combineTransforms(`translate3d(${Math.round(sibling.offset)}px, 0, 0)`, sibling.initialTransform);
1548 adjustClientRect(sibling.clientRect, 0, offset);
1549 }
1550 else {
1551 elementToOffset.style.transform = combineTransforms(`translate3d(0, ${Math.round(sibling.offset)}px, 0)`, sibling.initialTransform);
1552 adjustClientRect(sibling.clientRect, offset, 0);
1553 }
1554 });
1555 // Note that it's important that we do this after the client rects have been adjusted.
1556 this._previousSwap.overlaps = isInsideClientRect(newPosition, pointerX, pointerY);
1557 this._previousSwap.drag = siblingAtNewPosition.drag;
1558 this._previousSwap.delta = isHorizontal ? pointerDelta.x : pointerDelta.y;
1559 return { previousIndex: currentIndex, currentIndex: newIndex };
1560 }
1561 /**
1562 * Called when an item is being moved into the container.
1563 * @param item Item that was moved into the container.
1564 * @param pointerX Position of the item along the X axis.
1565 * @param pointerY Position of the item along the Y axis.
1566 * @param index Index at which the item entered. If omitted, the container will try to figure it
1567 * out automatically.
1568 */
1569 enter(item, pointerX, pointerY, index) {
1570 const newIndex = index == null || index < 0
1571 ? // We use the coordinates of where the item entered the drop
1572 // zone to figure out at which index it should be inserted.
1573 this._getItemIndexFromPointerPosition(item, pointerX, pointerY)
1574 : index;
1575 const activeDraggables = this._activeDraggables;
1576 const currentIndex = activeDraggables.indexOf(item);
1577 const placeholder = item.getPlaceholderElement();
1578 let newPositionReference = activeDraggables[newIndex];
1579 // If the item at the new position is the same as the item that is being dragged,
1580 // it means that we're trying to restore the item to its initial position. In this
1581 // case we should use the next item from the list as the reference.
1582 if (newPositionReference === item) {
1583 newPositionReference = activeDraggables[newIndex + 1];
1584 }
1585 // If we didn't find a new position reference, it means that either the item didn't start off
1586 // in this container, or that the item requested to be inserted at the end of the list.
1587 if (!newPositionReference &&
1588 (newIndex == null || newIndex === -1 || newIndex < activeDraggables.length - 1) &&
1589 this._shouldEnterAsFirstChild(pointerX, pointerY)) {
1590 newPositionReference = activeDraggables[0];
1591 }
1592 // Since the item may be in the `activeDraggables` already (e.g. if the user dragged it
1593 // into another container and back again), we have to ensure that it isn't duplicated.
1594 if (currentIndex > -1) {
1595 activeDraggables.splice(currentIndex, 1);
1596 }
1597 // Don't use items that are being dragged as a reference, because
1598 // their element has been moved down to the bottom of the body.
1599 if (newPositionReference && !this._dragDropRegistry.isDragging(newPositionReference)) {
1600 const element = newPositionReference.getRootElement();
1601 element.parentElement.insertBefore(placeholder, element);
1602 activeDraggables.splice(newIndex, 0, item);
1603 }
1604 else {
1605 coerceElement(this._element).appendChild(placeholder);
1606 activeDraggables.push(item);
1607 }
1608 // The transform needs to be cleared so it doesn't throw off the measurements.
1609 placeholder.style.transform = '';
1610 // Note that usually `start` is called together with `enter` when an item goes into a new
1611 // container. This will cache item positions, but we need to refresh them since the amount
1612 // of items has changed.
1613 this._cacheItemPositions();
1614 }
1615 /** Sets the items that are currently part of the list. */
1616 withItems(items) {
1617 this._activeDraggables = items.slice();
1618 this._cacheItemPositions();
1619 }
1620 /** Assigns a sort predicate to the strategy. */
1621 withSortPredicate(predicate) {
1622 this._sortPredicate = predicate;
1623 }
1624 /** Resets the strategy to its initial state before dragging was started. */
1625 reset() {
1626 // TODO(crisbeto): may have to wait for the animations to finish.
1627 this._activeDraggables.forEach(item => {
1628 const rootElement = item.getRootElement();
1629 if (rootElement) {
1630 const initialTransform = this._itemPositions.find(p => p.drag === item)?.initialTransform;
1631 rootElement.style.transform = initialTransform || '';
1632 }
1633 });
1634 this._itemPositions = [];
1635 this._activeDraggables = [];
1636 this._previousSwap.drag = null;
1637 this._previousSwap.delta = 0;
1638 this._previousSwap.overlaps = false;
1639 }
1640 /**
1641 * Gets a snapshot of items currently in the list.
1642 * Can include items that we dragged in from another list.
1643 */
1644 getActiveItemsSnapshot() {
1645 return this._activeDraggables;
1646 }
1647 /** Gets the index of a specific item. */
1648 getItemIndex(item) {
1649 // Items are sorted always by top/left in the cache, however they flow differently in RTL.
1650 // The rest of the logic still stands no matter what orientation we're in, however
1651 // we need to invert the array when determining the index.
1652 const items = this.orientation === 'horizontal' && this.direction === 'rtl'
1653 ? this._itemPositions.slice().reverse()
1654 : this._itemPositions;
1655 return items.findIndex(currentItem => currentItem.drag === item);
1656 }
1657 /** Used to notify the strategy that the scroll position has changed. */
1658 updateOnScroll(topDifference, leftDifference) {
1659 // Since we know the amount that the user has scrolled we can shift all of the
1660 // client rectangles ourselves. This is cheaper than re-measuring everything and
1661 // we can avoid inconsistent behavior where we might be measuring the element before
1662 // its position has changed.
1663 this._itemPositions.forEach(({ clientRect }) => {
1664 adjustClientRect(clientRect, topDifference, leftDifference);
1665 });
1666 // We need two loops for this, because we want all of the cached
1667 // positions to be up-to-date before we re-sort the item.
1668 this._itemPositions.forEach(({ drag }) => {
1669 if (this._dragDropRegistry.isDragging(drag)) {
1670 // We need to re-sort the item manually, because the pointer move
1671 // events won't be dispatched while the user is scrolling.
1672 drag._sortFromLastPointerPosition();
1673 }
1674 });
1675 }
1676 /** Refreshes the position cache of the items and sibling containers. */
1677 _cacheItemPositions() {
1678 const isHorizontal = this.orientation === 'horizontal';
1679 this._itemPositions = this._activeDraggables
1680 .map(drag => {
1681 const elementToMeasure = drag.getVisibleElement();
1682 return {
1683 drag,
1684 offset: 0,
1685 initialTransform: elementToMeasure.style.transform || '',
1686 clientRect: getMutableClientRect(elementToMeasure),
1687 };
1688 })
1689 .sort((a, b) => {
1690 return isHorizontal
1691 ? a.clientRect.left - b.clientRect.left
1692 : a.clientRect.top - b.clientRect.top;
1693 });
1694 }
1695 /**
1696 * Gets the offset in pixels by which the item that is being dragged should be moved.
1697 * @param currentPosition Current position of the item.
1698 * @param newPosition Position of the item where the current item should be moved.
1699 * @param delta Direction in which the user is moving.
1700 */
1701 _getItemOffsetPx(currentPosition, newPosition, delta) {
1702 const isHorizontal = this.orientation === 'horizontal';
1703 let itemOffset = isHorizontal
1704 ? newPosition.left - currentPosition.left
1705 : newPosition.top - currentPosition.top;
1706 // Account for differences in the item width/height.
1707 if (delta === -1) {
1708 itemOffset += isHorizontal
1709 ? newPosition.width - currentPosition.width
1710 : newPosition.height - currentPosition.height;
1711 }
1712 return itemOffset;
1713 }
1714 /**
1715 * Gets the offset in pixels by which the items that aren't being dragged should be moved.
1716 * @param currentIndex Index of the item currently being dragged.
1717 * @param siblings All of the items in the list.
1718 * @param delta Direction in which the user is moving.
1719 */
1720 _getSiblingOffsetPx(currentIndex, siblings, delta) {
1721 const isHorizontal = this.orientation === 'horizontal';
1722 const currentPosition = siblings[currentIndex].clientRect;
1723 const immediateSibling = siblings[currentIndex + delta * -1];
1724 let siblingOffset = currentPosition[isHorizontal ? 'width' : 'height'] * delta;
1725 if (immediateSibling) {
1726 const start = isHorizontal ? 'left' : 'top';
1727 const end = isHorizontal ? 'right' : 'bottom';
1728 // Get the spacing between the start of the current item and the end of the one immediately
1729 // after it in the direction in which the user is dragging, or vice versa. We add it to the
1730 // offset in order to push the element to where it will be when it's inline and is influenced
1731 // by the `margin` of its siblings.
1732 if (delta === -1) {
1733 siblingOffset -= immediateSibling.clientRect[start] - currentPosition[end];
1734 }
1735 else {
1736 siblingOffset += currentPosition[start] - immediateSibling.clientRect[end];
1737 }
1738 }
1739 return siblingOffset;
1740 }
1741 /**
1742 * Checks if pointer is entering in the first position
1743 * @param pointerX Position of the user's pointer along the X axis.
1744 * @param pointerY Position of the user's pointer along the Y axis.
1745 */
1746 _shouldEnterAsFirstChild(pointerX, pointerY) {
1747 if (!this._activeDraggables.length) {
1748 return false;
1749 }
1750 const itemPositions = this._itemPositions;
1751 const isHorizontal = this.orientation === 'horizontal';
1752 // `itemPositions` are sorted by position while `activeDraggables` are sorted by child index
1753 // check if container is using some sort of "reverse" ordering (eg: flex-direction: row-reverse)
1754 const reversed = itemPositions[0].drag !== this._activeDraggables[0];
1755 if (reversed) {
1756 const lastItemRect = itemPositions[itemPositions.length - 1].clientRect;
1757 return isHorizontal ? pointerX >= lastItemRect.right : pointerY >= lastItemRect.bottom;
1758 }
1759 else {
1760 const firstItemRect = itemPositions[0].clientRect;
1761 return isHorizontal ? pointerX <= firstItemRect.left : pointerY <= firstItemRect.top;
1762 }
1763 }
1764 /**
1765 * Gets the index of an item in the drop container, based on the position of the user's pointer.
1766 * @param item Item that is being sorted.
1767 * @param pointerX Position of the user's pointer along the X axis.
1768 * @param pointerY Position of the user's pointer along the Y axis.
1769 * @param delta Direction in which the user is moving their pointer.
1770 */
1771 _getItemIndexFromPointerPosition(item, pointerX, pointerY, delta) {
1772 const isHorizontal = this.orientation === 'horizontal';
1773 const index = this._itemPositions.findIndex(({ drag, clientRect }) => {
1774 // Skip the item itself.
1775 if (drag === item) {
1776 return false;
1777 }
1778 if (delta) {
1779 const direction = isHorizontal ? delta.x : delta.y;
1780 // If the user is still hovering over the same item as last time, their cursor hasn't left
1781 // the item after we made the swap, and they didn't change the direction in which they're
1782 // dragging, we don't consider it a direction swap.
1783 if (drag === this._previousSwap.drag &&
1784 this._previousSwap.overlaps &&
1785 direction === this._previousSwap.delta) {
1786 return false;
1787 }
1788 }
1789 return isHorizontal
1790 ? // Round these down since most browsers report client rects with
1791 // sub-pixel precision, whereas the pointer coordinates are rounded to pixels.
1792 pointerX >= Math.floor(clientRect.left) && pointerX < Math.floor(clientRect.right)
1793 : pointerY >= Math.floor(clientRect.top) && pointerY < Math.floor(clientRect.bottom);
1794 });
1795 return index === -1 || !this._sortPredicate(index, item) ? -1 : index;
1796 }
1797}
1798
1799/**
1800 * Proximity, as a ratio to width/height, at which a
1801 * dragged item will affect the drop container.
1802 */
1803const DROP_PROXIMITY_THRESHOLD = 0.05;
1804/**
1805 * Proximity, as a ratio to width/height at which to start auto-scrolling the drop list or the
1806 * viewport. The value comes from trying it out manually until it feels right.
1807 */
1808const SCROLL_PROXIMITY_THRESHOLD = 0.05;
1809/**
1810 * Reference to a drop list. Used to manipulate or dispose of the container.
1811 */
1812class DropListRef {
1813 constructor(element, _dragDropRegistry, _document, _ngZone, _viewportRuler) {
1814 this._dragDropRegistry = _dragDropRegistry;
1815 this._ngZone = _ngZone;
1816 this._viewportRuler = _viewportRuler;
1817 /** Whether starting a dragging sequence from this container is disabled. */
1818 this.disabled = false;
1819 /** Whether sorting items within the list is disabled. */
1820 this.sortingDisabled = false;
1821 /**
1822 * Whether auto-scrolling the view when the user
1823 * moves their pointer close to the edges is disabled.
1824 */
1825 this.autoScrollDisabled = false;
1826 /** Number of pixels to scroll for each frame when auto-scrolling an element. */
1827 this.autoScrollStep = 2;
1828 /**
1829 * Function that is used to determine whether an item
1830 * is allowed to be moved into a drop container.
1831 */
1832 this.enterPredicate = () => true;
1833 /** Function that is used to determine whether an item can be sorted into a particular index. */
1834 this.sortPredicate = () => true;
1835 /** Emits right before dragging has started. */
1836 this.beforeStarted = new Subject();
1837 /**
1838 * Emits when the user has moved a new drag item into this container.
1839 */
1840 this.entered = new Subject();
1841 /**
1842 * Emits when the user removes an item from the container
1843 * by dragging it into another container.
1844 */
1845 this.exited = new Subject();
1846 /** Emits when the user drops an item inside the container. */
1847 this.dropped = new Subject();
1848 /** Emits as the user is swapping items while actively dragging. */
1849 this.sorted = new Subject();
1850 /** Emits when a dragging sequence is started in a list connected to the current one. */
1851 this.receivingStarted = new Subject();
1852 /** Emits when a dragging sequence is stopped from a list connected to the current one. */
1853 this.receivingStopped = new Subject();
1854 /** Whether an item in the list is being dragged. */
1855 this._isDragging = false;
1856 /** Draggable items in the container. */
1857 this._draggables = [];
1858 /** Drop lists that are connected to the current one. */
1859 this._siblings = [];
1860 /** Connected siblings that currently have a dragged item. */
1861 this._activeSiblings = new Set();
1862 /** Subscription to the window being scrolled. */
1863 this._viewportScrollSubscription = Subscription.EMPTY;
1864 /** Vertical direction in which the list is currently scrolling. */
1865 this._verticalScrollDirection = 0 /* AutoScrollVerticalDirection.NONE */;
1866 /** Horizontal direction in which the list is currently scrolling. */
1867 this._horizontalScrollDirection = 0 /* AutoScrollHorizontalDirection.NONE */;
1868 /** Used to signal to the current auto-scroll sequence when to stop. */
1869 this._stopScrollTimers = new Subject();
1870 /** Shadow root of the current element. Necessary for `elementFromPoint` to resolve correctly. */
1871 this._cachedShadowRoot = null;
1872 /** Starts the interval that'll auto-scroll the element. */
1873 this._startScrollInterval = () => {
1874 this._stopScrolling();
1875 interval(0, animationFrameScheduler)
1876 .pipe(takeUntil(this._stopScrollTimers))
1877 .subscribe(() => {
1878 const node = this._scrollNode;
1879 const scrollStep = this.autoScrollStep;
1880 if (this._verticalScrollDirection === 1 /* AutoScrollVerticalDirection.UP */) {
1881 node.scrollBy(0, -scrollStep);
1882 }
1883 else if (this._verticalScrollDirection === 2 /* AutoScrollVerticalDirection.DOWN */) {
1884 node.scrollBy(0, scrollStep);
1885 }
1886 if (this._horizontalScrollDirection === 1 /* AutoScrollHorizontalDirection.LEFT */) {
1887 node.scrollBy(-scrollStep, 0);
1888 }
1889 else if (this._horizontalScrollDirection === 2 /* AutoScrollHorizontalDirection.RIGHT */) {
1890 node.scrollBy(scrollStep, 0);
1891 }
1892 });
1893 };
1894 this.element = coerceElement(element);
1895 this._document = _document;
1896 this.withScrollableParents([this.element]);
1897 _dragDropRegistry.registerDropContainer(this);
1898 this._parentPositions = new ParentPositionTracker(_document);
1899 this._sortStrategy = new SingleAxisSortStrategy(this.element, _dragDropRegistry);
1900 this._sortStrategy.withSortPredicate((index, item) => this.sortPredicate(index, item, this));
1901 }
1902 /** Removes the drop list functionality from the DOM element. */
1903 dispose() {
1904 this._stopScrolling();
1905 this._stopScrollTimers.complete();
1906 this._viewportScrollSubscription.unsubscribe();
1907 this.beforeStarted.complete();
1908 this.entered.complete();
1909 this.exited.complete();
1910 this.dropped.complete();
1911 this.sorted.complete();
1912 this.receivingStarted.complete();
1913 this.receivingStopped.complete();
1914 this._activeSiblings.clear();
1915 this._scrollNode = null;
1916 this._parentPositions.clear();
1917 this._dragDropRegistry.removeDropContainer(this);
1918 }
1919 /** Whether an item from this list is currently being dragged. */
1920 isDragging() {
1921 return this._isDragging;
1922 }
1923 /** Starts dragging an item. */
1924 start() {
1925 this._draggingStarted();
1926 this._notifyReceivingSiblings();
1927 }
1928 /**
1929 * Attempts to move an item into the container.
1930 * @param item Item that was moved into the container.
1931 * @param pointerX Position of the item along the X axis.
1932 * @param pointerY Position of the item along the Y axis.
1933 * @param index Index at which the item entered. If omitted, the container will try to figure it
1934 * out automatically.
1935 */
1936 enter(item, pointerX, pointerY, index) {
1937 this._draggingStarted();
1938 // If sorting is disabled, we want the item to return to its starting
1939 // position if the user is returning it to its initial container.
1940 if (index == null && this.sortingDisabled) {
1941 index = this._draggables.indexOf(item);
1942 }
1943 this._sortStrategy.enter(item, pointerX, pointerY, index);
1944 // Note that this usually happens inside `_draggingStarted` as well, but the dimensions
1945 // can change when the sort strategy moves the item around inside `enter`.
1946 this._cacheParentPositions();
1947 // Notify siblings at the end so that the item has been inserted into the `activeDraggables`.
1948 this._notifyReceivingSiblings();
1949 this.entered.next({ item, container: this, currentIndex: this.getItemIndex(item) });
1950 }
1951 /**
1952 * Removes an item from the container after it was dragged into another container by the user.
1953 * @param item Item that was dragged out.
1954 */
1955 exit(item) {
1956 this._reset();
1957 this.exited.next({ item, container: this });
1958 }
1959 /**
1960 * Drops an item into this container.
1961 * @param item Item being dropped into the container.
1962 * @param currentIndex Index at which the item should be inserted.
1963 * @param previousIndex Index of the item when dragging started.
1964 * @param previousContainer Container from which the item got dragged in.
1965 * @param isPointerOverContainer Whether the user's pointer was over the
1966 * container when the item was dropped.
1967 * @param distance Distance the user has dragged since the start of the dragging sequence.
1968 * @param event Event that triggered the dropping sequence.
1969 *
1970 * @breaking-change 15.0.0 `previousIndex` and `event` parameters to become required.
1971 */
1972 drop(item, currentIndex, previousIndex, previousContainer, isPointerOverContainer, distance, dropPoint, event = {}) {
1973 this._reset();
1974 this.dropped.next({
1975 item,
1976 currentIndex,
1977 previousIndex,
1978 container: this,
1979 previousContainer,
1980 isPointerOverContainer,
1981 distance,
1982 dropPoint,
1983 event,
1984 });
1985 }
1986 /**
1987 * Sets the draggable items that are a part of this list.
1988 * @param items Items that are a part of this list.
1989 */
1990 withItems(items) {
1991 const previousItems = this._draggables;
1992 this._draggables = items;
1993 items.forEach(item => item._withDropContainer(this));
1994 if (this.isDragging()) {
1995 const draggedItems = previousItems.filter(item => item.isDragging());
1996 // If all of the items being dragged were removed
1997 // from the list, abort the current drag sequence.
1998 if (draggedItems.every(item => items.indexOf(item) === -1)) {
1999 this._reset();
2000 }
2001 else {
2002 this._sortStrategy.withItems(this._draggables);
2003 }
2004 }
2005 return this;
2006 }
2007 /** Sets the layout direction of the drop list. */
2008 withDirection(direction) {
2009 this._sortStrategy.direction = direction;
2010 return this;
2011 }
2012 /**
2013 * Sets the containers that are connected to this one. When two or more containers are
2014 * connected, the user will be allowed to transfer items between them.
2015 * @param connectedTo Other containers that the current containers should be connected to.
2016 */
2017 connectedTo(connectedTo) {
2018 this._siblings = connectedTo.slice();
2019 return this;
2020 }
2021 /**
2022 * Sets the orientation of the container.
2023 * @param orientation New orientation for the container.
2024 */
2025 withOrientation(orientation) {
2026 // TODO(crisbeto): eventually we should be constructing the new sort strategy here based on
2027 // the new orientation. For now we can assume that it'll always be `SingleAxisSortStrategy`.
2028 this._sortStrategy.orientation = orientation;
2029 return this;
2030 }
2031 /**
2032 * Sets which parent elements are can be scrolled while the user is dragging.
2033 * @param elements Elements that can be scrolled.
2034 */
2035 withScrollableParents(elements) {
2036 const element = coerceElement(this.element);
2037 // We always allow the current element to be scrollable
2038 // so we need to ensure that it's in the array.
2039 this._scrollableElements =
2040 elements.indexOf(element) === -1 ? [element, ...elements] : elements.slice();
2041 return this;
2042 }
2043 /** Gets the scrollable parents that are registered with this drop container. */
2044 getScrollableParents() {
2045 return this._scrollableElements;
2046 }
2047 /**
2048 * Figures out the index of an item in the container.
2049 * @param item Item whose index should be determined.
2050 */
2051 getItemIndex(item) {
2052 return this._isDragging
2053 ? this._sortStrategy.getItemIndex(item)
2054 : this._draggables.indexOf(item);
2055 }
2056 /**
2057 * Whether the list is able to receive the item that
2058 * is currently being dragged inside a connected drop list.
2059 */
2060 isReceiving() {
2061 return this._activeSiblings.size > 0;
2062 }
2063 /**
2064 * Sorts an item inside the container based on its position.
2065 * @param item Item to be sorted.
2066 * @param pointerX Position of the item along the X axis.
2067 * @param pointerY Position of the item along the Y axis.
2068 * @param pointerDelta Direction in which the pointer is moving along each axis.
2069 */
2070 _sortItem(item, pointerX, pointerY, pointerDelta) {
2071 // Don't sort the item if sorting is disabled or it's out of range.
2072 if (this.sortingDisabled ||
2073 !this._clientRect ||
2074 !isPointerNearClientRect(this._clientRect, DROP_PROXIMITY_THRESHOLD, pointerX, pointerY)) {
2075 return;
2076 }
2077 const result = this._sortStrategy.sort(item, pointerX, pointerY, pointerDelta);
2078 if (result) {
2079 this.sorted.next({
2080 previousIndex: result.previousIndex,
2081 currentIndex: result.currentIndex,
2082 container: this,
2083 item,
2084 });
2085 }
2086 }
2087 /**
2088 * Checks whether the user's pointer is close to the edges of either the
2089 * viewport or the drop list and starts the auto-scroll sequence.
2090 * @param pointerX User's pointer position along the x axis.
2091 * @param pointerY User's pointer position along the y axis.
2092 */
2093 _startScrollingIfNecessary(pointerX, pointerY) {
2094 if (this.autoScrollDisabled) {
2095 return;
2096 }
2097 let scrollNode;
2098 let verticalScrollDirection = 0 /* AutoScrollVerticalDirection.NONE */;
2099 let horizontalScrollDirection = 0 /* AutoScrollHorizontalDirection.NONE */;
2100 // Check whether we should start scrolling any of the parent containers.
2101 this._parentPositions.positions.forEach((position, element) => {
2102 // We have special handling for the `document` below. Also this would be
2103 // nicer with a for...of loop, but it requires changing a compiler flag.
2104 if (element === this._document || !position.clientRect || scrollNode) {
2105 return;
2106 }
2107 if (isPointerNearClientRect(position.clientRect, DROP_PROXIMITY_THRESHOLD, pointerX, pointerY)) {
2108 [verticalScrollDirection, horizontalScrollDirection] = getElementScrollDirections(element, position.clientRect, pointerX, pointerY);
2109 if (verticalScrollDirection || horizontalScrollDirection) {
2110 scrollNode = element;
2111 }
2112 }
2113 });
2114 // Otherwise check if we can start scrolling the viewport.
2115 if (!verticalScrollDirection && !horizontalScrollDirection) {
2116 const { width, height } = this._viewportRuler.getViewportSize();
2117 const clientRect = {
2118 width,
2119 height,
2120 top: 0,
2121 right: width,
2122 bottom: height,
2123 left: 0,
2124 };
2125 verticalScrollDirection = getVerticalScrollDirection(clientRect, pointerY);
2126 horizontalScrollDirection = getHorizontalScrollDirection(clientRect, pointerX);
2127 scrollNode = window;
2128 }
2129 if (scrollNode &&
2130 (verticalScrollDirection !== this._verticalScrollDirection ||
2131 horizontalScrollDirection !== this._horizontalScrollDirection ||
2132 scrollNode !== this._scrollNode)) {
2133 this._verticalScrollDirection = verticalScrollDirection;
2134 this._horizontalScrollDirection = horizontalScrollDirection;
2135 this._scrollNode = scrollNode;
2136 if ((verticalScrollDirection || horizontalScrollDirection) && scrollNode) {
2137 this._ngZone.runOutsideAngular(this._startScrollInterval);
2138 }
2139 else {
2140 this._stopScrolling();
2141 }
2142 }
2143 }
2144 /** Stops any currently-running auto-scroll sequences. */
2145 _stopScrolling() {
2146 this._stopScrollTimers.next();
2147 }
2148 /** Starts the dragging sequence within the list. */
2149 _draggingStarted() {
2150 const styles = coerceElement(this.element).style;
2151 this.beforeStarted.next();
2152 this._isDragging = true;
2153 // We need to disable scroll snapping while the user is dragging, because it breaks automatic
2154 // scrolling. The browser seems to round the value based on the snapping points which means
2155 // that we can't increment/decrement the scroll position.
2156 this._initialScrollSnap = styles.msScrollSnapType || styles.scrollSnapType || '';
2157 styles.scrollSnapType = styles.msScrollSnapType = 'none';
2158 this._sortStrategy.start(this._draggables);
2159 this._cacheParentPositions();
2160 this._viewportScrollSubscription.unsubscribe();
2161 this._listenToScrollEvents();
2162 }
2163 /** Caches the positions of the configured scrollable parents. */
2164 _cacheParentPositions() {
2165 const element = coerceElement(this.element);
2166 this._parentPositions.cache(this._scrollableElements);
2167 // The list element is always in the `scrollableElements`
2168 // so we can take advantage of the cached `ClientRect`.
2169 this._clientRect = this._parentPositions.positions.get(element).clientRect;
2170 }
2171 /** Resets the container to its initial state. */
2172 _reset() {
2173 this._isDragging = false;
2174 const styles = coerceElement(this.element).style;
2175 styles.scrollSnapType = styles.msScrollSnapType = this._initialScrollSnap;
2176 this._siblings.forEach(sibling => sibling._stopReceiving(this));
2177 this._sortStrategy.reset();
2178 this._stopScrolling();
2179 this._viewportScrollSubscription.unsubscribe();
2180 this._parentPositions.clear();
2181 }
2182 /**
2183 * Checks whether the user's pointer is positioned over the container.
2184 * @param x Pointer position along the X axis.
2185 * @param y Pointer position along the Y axis.
2186 */
2187 _isOverContainer(x, y) {
2188 return this._clientRect != null && isInsideClientRect(this._clientRect, x, y);
2189 }
2190 /**
2191 * Figures out whether an item should be moved into a sibling
2192 * drop container, based on its current position.
2193 * @param item Drag item that is being moved.
2194 * @param x Position of the item along the X axis.
2195 * @param y Position of the item along the Y axis.
2196 */
2197 _getSiblingContainerFromPosition(item, x, y) {
2198 return this._siblings.find(sibling => sibling._canReceive(item, x, y));
2199 }
2200 /**
2201 * Checks whether the drop list can receive the passed-in item.
2202 * @param item Item that is being dragged into the list.
2203 * @param x Position of the item along the X axis.
2204 * @param y Position of the item along the Y axis.
2205 */
2206 _canReceive(item, x, y) {
2207 if (!this._clientRect ||
2208 !isInsideClientRect(this._clientRect, x, y) ||
2209 !this.enterPredicate(item, this)) {
2210 return false;
2211 }
2212 const elementFromPoint = this._getShadowRoot().elementFromPoint(x, y);
2213 // If there's no element at the pointer position, then
2214 // the client rect is probably scrolled out of the view.
2215 if (!elementFromPoint) {
2216 return false;
2217 }
2218 const nativeElement = coerceElement(this.element);
2219 // The `ClientRect`, that we're using to find the container over which the user is
2220 // hovering, doesn't give us any information on whether the element has been scrolled
2221 // out of the view or whether it's overlapping with other containers. This means that
2222 // we could end up transferring the item into a container that's invisible or is positioned
2223 // below another one. We use the result from `elementFromPoint` to get the top-most element
2224 // at the pointer position and to find whether it's one of the intersecting drop containers.
2225 return elementFromPoint === nativeElement || nativeElement.contains(elementFromPoint);
2226 }
2227 /**
2228 * Called by one of the connected drop lists when a dragging sequence has started.
2229 * @param sibling Sibling in which dragging has started.
2230 */
2231 _startReceiving(sibling, items) {
2232 const activeSiblings = this._activeSiblings;
2233 if (!activeSiblings.has(sibling) &&
2234 items.every(item => {
2235 // Note that we have to add an exception to the `enterPredicate` for items that started off
2236 // in this drop list. The drag ref has logic that allows an item to return to its initial
2237 // container, if it has left the initial container and none of the connected containers
2238 // allow it to enter. See `DragRef._updateActiveDropContainer` for more context.
2239 return this.enterPredicate(item, this) || this._draggables.indexOf(item) > -1;
2240 })) {
2241 activeSiblings.add(sibling);
2242 this._cacheParentPositions();
2243 this._listenToScrollEvents();
2244 this.receivingStarted.next({
2245 initiator: sibling,
2246 receiver: this,
2247 items,
2248 });
2249 }
2250 }
2251 /**
2252 * Called by a connected drop list when dragging has stopped.
2253 * @param sibling Sibling whose dragging has stopped.
2254 */
2255 _stopReceiving(sibling) {
2256 this._activeSiblings.delete(sibling);
2257 this._viewportScrollSubscription.unsubscribe();
2258 this.receivingStopped.next({ initiator: sibling, receiver: this });
2259 }
2260 /**
2261 * Starts listening to scroll events on the viewport.
2262 * Used for updating the internal state of the list.
2263 */
2264 _listenToScrollEvents() {
2265 this._viewportScrollSubscription = this._dragDropRegistry
2266 .scrolled(this._getShadowRoot())
2267 .subscribe(event => {
2268 if (this.isDragging()) {
2269 const scrollDifference = this._parentPositions.handleScroll(event);
2270 if (scrollDifference) {
2271 this._sortStrategy.updateOnScroll(scrollDifference.top, scrollDifference.left);
2272 }
2273 }
2274 else if (this.isReceiving()) {
2275 this._cacheParentPositions();
2276 }
2277 });
2278 }
2279 /**
2280 * Lazily resolves and returns the shadow root of the element. We do this in a function, rather
2281 * than saving it in property directly on init, because we want to resolve it as late as possible
2282 * in order to ensure that the element has been moved into the shadow DOM. Doing it inside the
2283 * constructor might be too early if the element is inside of something like `ngFor` or `ngIf`.
2284 */
2285 _getShadowRoot() {
2286 if (!this._cachedShadowRoot) {
2287 const shadowRoot = _getShadowRoot(coerceElement(this.element));
2288 this._cachedShadowRoot = (shadowRoot || this._document);
2289 }
2290 return this._cachedShadowRoot;
2291 }
2292 /** Notifies any siblings that may potentially receive the item. */
2293 _notifyReceivingSiblings() {
2294 const draggedItems = this._sortStrategy
2295 .getActiveItemsSnapshot()
2296 .filter(item => item.isDragging());
2297 this._siblings.forEach(sibling => sibling._startReceiving(this, draggedItems));
2298 }
2299}
2300/**
2301 * Gets whether the vertical auto-scroll direction of a node.
2302 * @param clientRect Dimensions of the node.
2303 * @param pointerY Position of the user's pointer along the y axis.
2304 */
2305function getVerticalScrollDirection(clientRect, pointerY) {
2306 const { top, bottom, height } = clientRect;
2307 const yThreshold = height * SCROLL_PROXIMITY_THRESHOLD;
2308 if (pointerY >= top - yThreshold && pointerY <= top + yThreshold) {
2309 return 1 /* AutoScrollVerticalDirection.UP */;
2310 }
2311 else if (pointerY >= bottom - yThreshold && pointerY <= bottom + yThreshold) {
2312 return 2 /* AutoScrollVerticalDirection.DOWN */;
2313 }
2314 return 0 /* AutoScrollVerticalDirection.NONE */;
2315}
2316/**
2317 * Gets whether the horizontal auto-scroll direction of a node.
2318 * @param clientRect Dimensions of the node.
2319 * @param pointerX Position of the user's pointer along the x axis.
2320 */
2321function getHorizontalScrollDirection(clientRect, pointerX) {
2322 const { left, right, width } = clientRect;
2323 const xThreshold = width * SCROLL_PROXIMITY_THRESHOLD;
2324 if (pointerX >= left - xThreshold && pointerX <= left + xThreshold) {
2325 return 1 /* AutoScrollHorizontalDirection.LEFT */;
2326 }
2327 else if (pointerX >= right - xThreshold && pointerX <= right + xThreshold) {
2328 return 2 /* AutoScrollHorizontalDirection.RIGHT */;
2329 }
2330 return 0 /* AutoScrollHorizontalDirection.NONE */;
2331}
2332/**
2333 * Gets the directions in which an element node should be scrolled,
2334 * assuming that the user's pointer is already within it scrollable region.
2335 * @param element Element for which we should calculate the scroll direction.
2336 * @param clientRect Bounding client rectangle of the element.
2337 * @param pointerX Position of the user's pointer along the x axis.
2338 * @param pointerY Position of the user's pointer along the y axis.
2339 */
2340function getElementScrollDirections(element, clientRect, pointerX, pointerY) {
2341 const computedVertical = getVerticalScrollDirection(clientRect, pointerY);
2342 const computedHorizontal = getHorizontalScrollDirection(clientRect, pointerX);
2343 let verticalScrollDirection = 0 /* AutoScrollVerticalDirection.NONE */;
2344 let horizontalScrollDirection = 0 /* AutoScrollHorizontalDirection.NONE */;
2345 // Note that we here we do some extra checks for whether the element is actually scrollable in
2346 // a certain direction and we only assign the scroll direction if it is. We do this so that we
2347 // can allow other elements to be scrolled, if the current element can't be scrolled anymore.
2348 // This allows us to handle cases where the scroll regions of two scrollable elements overlap.
2349 if (computedVertical) {
2350 const scrollTop = element.scrollTop;
2351 if (computedVertical === 1 /* AutoScrollVerticalDirection.UP */) {
2352 if (scrollTop > 0) {
2353 verticalScrollDirection = 1 /* AutoScrollVerticalDirection.UP */;
2354 }
2355 }
2356 else if (element.scrollHeight - scrollTop > element.clientHeight) {
2357 verticalScrollDirection = 2 /* AutoScrollVerticalDirection.DOWN */;
2358 }
2359 }
2360 if (computedHorizontal) {
2361 const scrollLeft = element.scrollLeft;
2362 if (computedHorizontal === 1 /* AutoScrollHorizontalDirection.LEFT */) {
2363 if (scrollLeft > 0) {
2364 horizontalScrollDirection = 1 /* AutoScrollHorizontalDirection.LEFT */;
2365 }
2366 }
2367 else if (element.scrollWidth - scrollLeft > element.clientWidth) {
2368 horizontalScrollDirection = 2 /* AutoScrollHorizontalDirection.RIGHT */;
2369 }
2370 }
2371 return [verticalScrollDirection, horizontalScrollDirection];
2372}
2373
2374/** Event options that can be used to bind an active, capturing event. */
2375const activeCapturingEventOptions = normalizePassiveListenerOptions({
2376 passive: false,
2377 capture: true,
2378});
2379/**
2380 * Service that keeps track of all the drag item and drop container
2381 * instances, and manages global event listeners on the `document`.
2382 * @docs-private
2383 */
2384// Note: this class is generic, rather than referencing CdkDrag and CdkDropList directly, in order
2385// to avoid circular imports. If we were to reference them here, importing the registry into the
2386// classes that are registering themselves will introduce a circular import.
2387class DragDropRegistry {
2388 constructor(_ngZone, _document) {
2389 this._ngZone = _ngZone;
2390 /** Registered drop container instances. */
2391 this._dropInstances = new Set();
2392 /** Registered drag item instances. */
2393 this._dragInstances = new Set();
2394 /** Drag item instances that are currently being dragged. */
2395 this._activeDragInstances = [];
2396 /** Keeps track of the event listeners that we've bound to the `document`. */
2397 this._globalListeners = new Map();
2398 /**
2399 * Predicate function to check if an item is being dragged. Moved out into a property,
2400 * because it'll be called a lot and we don't want to create a new function every time.
2401 */
2402 this._draggingPredicate = (item) => item.isDragging();
2403 /**
2404 * Emits the `touchmove` or `mousemove` events that are dispatched
2405 * while the user is dragging a drag item instance.
2406 */
2407 this.pointerMove = new Subject();
2408 /**
2409 * Emits the `touchend` or `mouseup` events that are dispatched
2410 * while the user is dragging a drag item instance.
2411 */
2412 this.pointerUp = new Subject();
2413 /**
2414 * Emits when the viewport has been scrolled while the user is dragging an item.
2415 * @deprecated To be turned into a private member. Use the `scrolled` method instead.
2416 * @breaking-change 13.0.0
2417 */
2418 this.scroll = new Subject();
2419 /**
2420 * Event listener that will prevent the default browser action while the user is dragging.
2421 * @param event Event whose default action should be prevented.
2422 */
2423 this._preventDefaultWhileDragging = (event) => {
2424 if (this._activeDragInstances.length > 0) {
2425 event.preventDefault();
2426 }
2427 };
2428 /** Event listener for `touchmove` that is bound even if no dragging is happening. */
2429 this._persistentTouchmoveListener = (event) => {
2430 if (this._activeDragInstances.length > 0) {
2431 // Note that we only want to prevent the default action after dragging has actually started.
2432 // Usually this is the same time at which the item is added to the `_activeDragInstances`,
2433 // but it could be pushed back if the user has set up a drag delay or threshold.
2434 if (this._activeDragInstances.some(this._draggingPredicate)) {
2435 event.preventDefault();
2436 }
2437 this.pointerMove.next(event);
2438 }
2439 };
2440 this._document = _document;
2441 }
2442 /** Adds a drop container to the registry. */
2443 registerDropContainer(drop) {
2444 if (!this._dropInstances.has(drop)) {
2445 this._dropInstances.add(drop);
2446 }
2447 }
2448 /** Adds a drag item instance to the registry. */
2449 registerDragItem(drag) {
2450 this._dragInstances.add(drag);
2451 // The `touchmove` event gets bound once, ahead of time, because WebKit
2452 // won't preventDefault on a dynamically-added `touchmove` listener.
2453 // See https://bugs.webkit.org/show_bug.cgi?id=184250.
2454 if (this._dragInstances.size === 1) {
2455 this._ngZone.runOutsideAngular(() => {
2456 // The event handler has to be explicitly active,
2457 // because newer browsers make it passive by default.
2458 this._document.addEventListener('touchmove', this._persistentTouchmoveListener, activeCapturingEventOptions);
2459 });
2460 }
2461 }
2462 /** Removes a drop container from the registry. */
2463 removeDropContainer(drop) {
2464 this._dropInstances.delete(drop);
2465 }
2466 /** Removes a drag item instance from the registry. */
2467 removeDragItem(drag) {
2468 this._dragInstances.delete(drag);
2469 this.stopDragging(drag);
2470 if (this._dragInstances.size === 0) {
2471 this._document.removeEventListener('touchmove', this._persistentTouchmoveListener, activeCapturingEventOptions);
2472 }
2473 }
2474 /**
2475 * Starts the dragging sequence for a drag instance.
2476 * @param drag Drag instance which is being dragged.
2477 * @param event Event that initiated the dragging.
2478 */
2479 startDragging(drag, event) {
2480 // Do not process the same drag twice to avoid memory leaks and redundant listeners
2481 if (this._activeDragInstances.indexOf(drag) > -1) {
2482 return;
2483 }
2484 this._activeDragInstances.push(drag);
2485 if (this._activeDragInstances.length === 1) {
2486 const isTouchEvent = event.type.startsWith('touch');
2487 // We explicitly bind __active__ listeners here, because newer browsers will default to
2488 // passive ones for `mousemove` and `touchmove`. The events need to be active, because we
2489 // use `preventDefault` to prevent the page from scrolling while the user is dragging.
2490 this._globalListeners
2491 .set(isTouchEvent ? 'touchend' : 'mouseup', {
2492 handler: (e) => this.pointerUp.next(e),
2493 options: true,
2494 })
2495 .set('scroll', {
2496 handler: (e) => this.scroll.next(e),
2497 // Use capturing so that we pick up scroll changes in any scrollable nodes that aren't
2498 // the document. See https://github.com/angular/components/issues/17144.
2499 options: true,
2500 })
2501 // Preventing the default action on `mousemove` isn't enough to disable text selection
2502 // on Safari so we need to prevent the selection event as well. Alternatively this can
2503 // be done by setting `user-select: none` on the `body`, however it has causes a style
2504 // recalculation which can be expensive on pages with a lot of elements.
2505 .set('selectstart', {
2506 handler: this._preventDefaultWhileDragging,
2507 options: activeCapturingEventOptions,
2508 });
2509 // We don't have to bind a move event for touch drag sequences, because
2510 // we already have a persistent global one bound from `registerDragItem`.
2511 if (!isTouchEvent) {
2512 this._globalListeners.set('mousemove', {
2513 handler: (e) => this.pointerMove.next(e),
2514 options: activeCapturingEventOptions,
2515 });
2516 }
2517 this._ngZone.runOutsideAngular(() => {
2518 this._globalListeners.forEach((config, name) => {
2519 this._document.addEventListener(name, config.handler, config.options);
2520 });
2521 });
2522 }
2523 }
2524 /** Stops dragging a drag item instance. */
2525 stopDragging(drag) {
2526 const index = this._activeDragInstances.indexOf(drag);
2527 if (index > -1) {
2528 this._activeDragInstances.splice(index, 1);
2529 if (this._activeDragInstances.length === 0) {
2530 this._clearGlobalListeners();
2531 }
2532 }
2533 }
2534 /** Gets whether a drag item instance is currently being dragged. */
2535 isDragging(drag) {
2536 return this._activeDragInstances.indexOf(drag) > -1;
2537 }
2538 /**
2539 * Gets a stream that will emit when any element on the page is scrolled while an item is being
2540 * dragged.
2541 * @param shadowRoot Optional shadow root that the current dragging sequence started from.
2542 * Top-level listeners won't pick up events coming from the shadow DOM so this parameter can
2543 * be used to include an additional top-level listener at the shadow root level.
2544 */
2545 scrolled(shadowRoot) {
2546 const streams = [this.scroll];
2547 if (shadowRoot && shadowRoot !== this._document) {
2548 // Note that this is basically the same as `fromEvent` from rxjs, but we do it ourselves,
2549 // because we want to guarantee that the event is bound outside of the `NgZone`. With
2550 // `fromEvent` it'll only happen if the subscription is outside the `NgZone`.
2551 streams.push(new Observable((observer) => {
2552 return this._ngZone.runOutsideAngular(() => {
2553 const eventOptions = true;
2554 const callback = (event) => {
2555 if (this._activeDragInstances.length) {
2556 observer.next(event);
2557 }
2558 };
2559 shadowRoot.addEventListener('scroll', callback, eventOptions);
2560 return () => {
2561 shadowRoot.removeEventListener('scroll', callback, eventOptions);
2562 };
2563 });
2564 }));
2565 }
2566 return merge(...streams);
2567 }
2568 ngOnDestroy() {
2569 this._dragInstances.forEach(instance => this.removeDragItem(instance));
2570 this._dropInstances.forEach(instance => this.removeDropContainer(instance));
2571 this._clearGlobalListeners();
2572 this.pointerMove.complete();
2573 this.pointerUp.complete();
2574 }
2575 /** Clears out the global event listeners from the `document`. */
2576 _clearGlobalListeners() {
2577 this._globalListeners.forEach((config, name) => {
2578 this._document.removeEventListener(name, config.handler, config.options);
2579 });
2580 this._globalListeners.clear();
2581 }
2582 static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: DragDropRegistry, deps: [{ token: i0.NgZone }, { token: DOCUMENT }], target: i0.ɵɵFactoryTarget.Injectable }); }
2583 static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: DragDropRegistry, providedIn: 'root' }); }
2584}
2585i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: DragDropRegistry, decorators: [{
2586 type: Injectable,
2587 args: [{ providedIn: 'root' }]
2588 }], ctorParameters: function () { return [{ type: i0.NgZone }, { type: undefined, decorators: [{
2589 type: Inject,
2590 args: [DOCUMENT]
2591 }] }]; } });
2592
2593/** Default configuration to be used when creating a `DragRef`. */
2594const DEFAULT_CONFIG = {
2595 dragStartThreshold: 5,
2596 pointerDirectionChangeThreshold: 5,
2597};
2598/**
2599 * Service that allows for drag-and-drop functionality to be attached to DOM elements.
2600 */
2601class DragDrop {
2602 constructor(_document, _ngZone, _viewportRuler, _dragDropRegistry) {
2603 this._document = _document;
2604 this._ngZone = _ngZone;
2605 this._viewportRuler = _viewportRuler;
2606 this._dragDropRegistry = _dragDropRegistry;
2607 }
2608 /**
2609 * Turns an element into a draggable item.
2610 * @param element Element to which to attach the dragging functionality.
2611 * @param config Object used to configure the dragging behavior.
2612 */
2613 createDrag(element, config = DEFAULT_CONFIG) {
2614 return new DragRef(element, config, this._document, this._ngZone, this._viewportRuler, this._dragDropRegistry);
2615 }
2616 /**
2617 * Turns an element into a drop list.
2618 * @param element Element to which to attach the drop list functionality.
2619 */
2620 createDropList(element) {
2621 return new DropListRef(element, this._dragDropRegistry, this._document, this._ngZone, this._viewportRuler);
2622 }
2623 static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: DragDrop, deps: [{ token: DOCUMENT }, { token: i0.NgZone }, { token: i1.ViewportRuler }, { token: DragDropRegistry }], target: i0.ɵɵFactoryTarget.Injectable }); }
2624 static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: DragDrop, providedIn: 'root' }); }
2625}
2626i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: DragDrop, decorators: [{
2627 type: Injectable,
2628 args: [{ providedIn: 'root' }]
2629 }], ctorParameters: function () { return [{ type: undefined, decorators: [{
2630 type: Inject,
2631 args: [DOCUMENT]
2632 }] }, { type: i0.NgZone }, { type: i1.ViewportRuler }, { type: DragDropRegistry }]; } });
2633
2634/**
2635 * Injection token that can be used for a `CdkDrag` to provide itself as a parent to the
2636 * drag-specific child directive (`CdkDragHandle`, `CdkDragPreview` etc.). Used primarily
2637 * to avoid circular imports.
2638 * @docs-private
2639 */
2640const CDK_DRAG_PARENT = new InjectionToken('CDK_DRAG_PARENT');
2641
2642/**
2643 * Asserts that a particular node is an element.
2644 * @param node Node to be checked.
2645 * @param name Name to attach to the error message.
2646 */
2647function assertElementNode(node, name) {
2648 if (node.nodeType !== 1) {
2649 throw Error(`${name} must be attached to an element node. ` + `Currently attached to "${node.nodeName}".`);
2650 }
2651}
2652
2653/**
2654 * Injection token that can be used to reference instances of `CdkDragHandle`. It serves as
2655 * alternative token to the actual `CdkDragHandle` class which could cause unnecessary
2656 * retention of the class and its directive metadata.
2657 */
2658const CDK_DRAG_HANDLE = new InjectionToken('CdkDragHandle');
2659/** Handle that can be used to drag a CdkDrag instance. */
2660class CdkDragHandle {
2661 /** Whether starting to drag through this handle is disabled. */
2662 get disabled() {
2663 return this._disabled;
2664 }
2665 set disabled(value) {
2666 this._disabled = coerceBooleanProperty(value);
2667 this._stateChanges.next(this);
2668 }
2669 constructor(element, parentDrag) {
2670 this.element = element;
2671 /** Emits when the state of the handle has changed. */
2672 this._stateChanges = new Subject();
2673 this._disabled = false;
2674 if (typeof ngDevMode === 'undefined' || ngDevMode) {
2675 assertElementNode(element.nativeElement, 'cdkDragHandle');
2676 }
2677 this._parentDrag = parentDrag;
2678 }
2679 ngOnDestroy() {
2680 this._stateChanges.complete();
2681 }
2682 static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkDragHandle, deps: [{ token: i0.ElementRef }, { token: CDK_DRAG_PARENT, optional: true, skipSelf: true }], target: i0.ɵɵFactoryTarget.Directive }); }
2683 static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.0.0", type: CdkDragHandle, isStandalone: true, selector: "[cdkDragHandle]", inputs: { disabled: ["cdkDragHandleDisabled", "disabled"] }, host: { classAttribute: "cdk-drag-handle" }, providers: [{ provide: CDK_DRAG_HANDLE, useExisting: CdkDragHandle }], ngImport: i0 }); }
2684}
2685i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkDragHandle, decorators: [{
2686 type: Directive,
2687 args: [{
2688 selector: '[cdkDragHandle]',
2689 standalone: true,
2690 host: {
2691 'class': 'cdk-drag-handle',
2692 },
2693 providers: [{ provide: CDK_DRAG_HANDLE, useExisting: CdkDragHandle }],
2694 }]
2695 }], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: undefined, decorators: [{
2696 type: Inject,
2697 args: [CDK_DRAG_PARENT]
2698 }, {
2699 type: Optional
2700 }, {
2701 type: SkipSelf
2702 }] }]; }, propDecorators: { disabled: [{
2703 type: Input,
2704 args: ['cdkDragHandleDisabled']
2705 }] } });
2706
2707/**
2708 * Injection token that can be used to reference instances of `CdkDragPlaceholder`. It serves as
2709 * alternative token to the actual `CdkDragPlaceholder` class which could cause unnecessary
2710 * retention of the class and its directive metadata.
2711 */
2712const CDK_DRAG_PLACEHOLDER = new InjectionToken('CdkDragPlaceholder');
2713/**
2714 * Element that will be used as a template for the placeholder of a CdkDrag when
2715 * it is being dragged. The placeholder is displayed in place of the element being dragged.
2716 */
2717class CdkDragPlaceholder {
2718 constructor(templateRef) {
2719 this.templateRef = templateRef;
2720 }
2721 static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkDragPlaceholder, deps: [{ token: i0.TemplateRef }], target: i0.ɵɵFactoryTarget.Directive }); }
2722 static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.0.0", type: CdkDragPlaceholder, isStandalone: true, selector: "ng-template[cdkDragPlaceholder]", inputs: { data: "data" }, providers: [{ provide: CDK_DRAG_PLACEHOLDER, useExisting: CdkDragPlaceholder }], ngImport: i0 }); }
2723}
2724i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkDragPlaceholder, decorators: [{
2725 type: Directive,
2726 args: [{
2727 selector: 'ng-template[cdkDragPlaceholder]',
2728 standalone: true,
2729 providers: [{ provide: CDK_DRAG_PLACEHOLDER, useExisting: CdkDragPlaceholder }],
2730 }]
2731 }], ctorParameters: function () { return [{ type: i0.TemplateRef }]; }, propDecorators: { data: [{
2732 type: Input
2733 }] } });
2734
2735/**
2736 * Injection token that can be used to reference instances of `CdkDragPreview`. It serves as
2737 * alternative token to the actual `CdkDragPreview` class which could cause unnecessary
2738 * retention of the class and its directive metadata.
2739 */
2740const CDK_DRAG_PREVIEW = new InjectionToken('CdkDragPreview');
2741/**
2742 * Element that will be used as a template for the preview
2743 * of a CdkDrag when it is being dragged.
2744 */
2745class CdkDragPreview {
2746 /** Whether the preview should preserve the same size as the item that is being dragged. */
2747 get matchSize() {
2748 return this._matchSize;
2749 }
2750 set matchSize(value) {
2751 this._matchSize = coerceBooleanProperty(value);
2752 }
2753 constructor(templateRef) {
2754 this.templateRef = templateRef;
2755 this._matchSize = false;
2756 }
2757 static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkDragPreview, deps: [{ token: i0.TemplateRef }], target: i0.ɵɵFactoryTarget.Directive }); }
2758 static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.0.0", type: CdkDragPreview, isStandalone: true, selector: "ng-template[cdkDragPreview]", inputs: { data: "data", matchSize: "matchSize" }, providers: [{ provide: CDK_DRAG_PREVIEW, useExisting: CdkDragPreview }], ngImport: i0 }); }
2759}
2760i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkDragPreview, decorators: [{
2761 type: Directive,
2762 args: [{
2763 selector: 'ng-template[cdkDragPreview]',
2764 standalone: true,
2765 providers: [{ provide: CDK_DRAG_PREVIEW, useExisting: CdkDragPreview }],
2766 }]
2767 }], ctorParameters: function () { return [{ type: i0.TemplateRef }]; }, propDecorators: { data: [{
2768 type: Input
2769 }], matchSize: [{
2770 type: Input
2771 }] } });
2772
2773/**
2774 * Injection token that can be used to configure the
2775 * behavior of the drag&drop-related components.
2776 */
2777const CDK_DRAG_CONFIG = new InjectionToken('CDK_DRAG_CONFIG');
2778
2779const DRAG_HOST_CLASS = 'cdk-drag';
2780/**
2781 * Injection token that can be used to reference instances of `CdkDropList`. It serves as
2782 * alternative token to the actual `CdkDropList` class which could cause unnecessary
2783 * retention of the class and its directive metadata.
2784 */
2785const CDK_DROP_LIST = new InjectionToken('CdkDropList');
2786/** Element that can be moved inside a CdkDropList container. */
2787class CdkDrag {
2788 static { this._dragInstances = []; }
2789 /** Whether starting to drag this element is disabled. */
2790 get disabled() {
2791 return this._disabled || (this.dropContainer && this.dropContainer.disabled);
2792 }
2793 set disabled(value) {
2794 this._disabled = coerceBooleanProperty(value);
2795 this._dragRef.disabled = this._disabled;
2796 }
2797 constructor(
2798 /** Element that the draggable is attached to. */
2799 element,
2800 /** Droppable container that the draggable is a part of. */
2801 dropContainer,
2802 /**
2803 * @deprecated `_document` parameter no longer being used and will be removed.
2804 * @breaking-change 12.0.0
2805 */
2806 _document, _ngZone, _viewContainerRef, config, _dir, dragDrop, _changeDetectorRef, _selfHandle, _parentDrag) {
2807 this.element = element;
2808 this.dropContainer = dropContainer;
2809 this._ngZone = _ngZone;
2810 this._viewContainerRef = _viewContainerRef;
2811 this._dir = _dir;
2812 this._changeDetectorRef = _changeDetectorRef;
2813 this._selfHandle = _selfHandle;
2814 this._parentDrag = _parentDrag;
2815 this._destroyed = new Subject();
2816 /** Emits when the user starts dragging the item. */
2817 this.started = new EventEmitter();
2818 /** Emits when the user has released a drag item, before any animations have started. */
2819 this.released = new EventEmitter();
2820 /** Emits when the user stops dragging an item in the container. */
2821 this.ended = new EventEmitter();
2822 /** Emits when the user has moved the item into a new container. */
2823 this.entered = new EventEmitter();
2824 /** Emits when the user removes the item its container by dragging it into another container. */
2825 this.exited = new EventEmitter();
2826 /** Emits when the user drops the item inside a container. */
2827 this.dropped = new EventEmitter();
2828 /**
2829 * Emits as the user is dragging the item. Use with caution,
2830 * because this event will fire for every pixel that the user has dragged.
2831 */
2832 this.moved = new Observable((observer) => {
2833 const subscription = this._dragRef.moved
2834 .pipe(map(movedEvent => ({
2835 source: this,
2836 pointerPosition: movedEvent.pointerPosition,
2837 event: movedEvent.event,
2838 delta: movedEvent.delta,
2839 distance: movedEvent.distance,
2840 })))
2841 .subscribe(observer);
2842 return () => {
2843 subscription.unsubscribe();
2844 };
2845 });
2846 this._dragRef = dragDrop.createDrag(element, {
2847 dragStartThreshold: config && config.dragStartThreshold != null ? config.dragStartThreshold : 5,
2848 pointerDirectionChangeThreshold: config && config.pointerDirectionChangeThreshold != null
2849 ? config.pointerDirectionChangeThreshold
2850 : 5,
2851 zIndex: config?.zIndex,
2852 });
2853 this._dragRef.data = this;
2854 // We have to keep track of the drag instances in order to be able to match an element to
2855 // a drag instance. We can't go through the global registry of `DragRef`, because the root
2856 // element could be different.
2857 CdkDrag._dragInstances.push(this);
2858 if (config) {
2859 this._assignDefaults(config);
2860 }
2861 // Note that usually the container is assigned when the drop list is picks up the item, but in
2862 // some cases (mainly transplanted views with OnPush, see #18341) we may end up in a situation
2863 // where there are no items on the first change detection pass, but the items get picked up as
2864 // soon as the user triggers another pass by dragging. This is a problem, because the item would
2865 // have to switch from standalone mode to drag mode in the middle of the dragging sequence which
2866 // is too late since the two modes save different kinds of information. We work around it by
2867 // assigning the drop container both from here and the list.
2868 if (dropContainer) {
2869 this._dragRef._withDropContainer(dropContainer._dropListRef);
2870 dropContainer.addItem(this);
2871 }
2872 this._syncInputs(this._dragRef);
2873 this._handleEvents(this._dragRef);
2874 }
2875 /**
2876 * Returns the element that is being used as a placeholder
2877 * while the current element is being dragged.
2878 */
2879 getPlaceholderElement() {
2880 return this._dragRef.getPlaceholderElement();
2881 }
2882 /** Returns the root draggable element. */
2883 getRootElement() {
2884 return this._dragRef.getRootElement();
2885 }
2886 /** Resets a standalone drag item to its initial position. */
2887 reset() {
2888 this._dragRef.reset();
2889 }
2890 /**
2891 * Gets the pixel coordinates of the draggable outside of a drop container.
2892 */
2893 getFreeDragPosition() {
2894 return this._dragRef.getFreeDragPosition();
2895 }
2896 /**
2897 * Sets the current position in pixels the draggable outside of a drop container.
2898 * @param value New position to be set.
2899 */
2900 setFreeDragPosition(value) {
2901 this._dragRef.setFreeDragPosition(value);
2902 }
2903 ngAfterViewInit() {
2904 // Normally this isn't in the zone, but it can cause major performance regressions for apps
2905 // using `zone-patch-rxjs` because it'll trigger a change detection when it unsubscribes.
2906 this._ngZone.runOutsideAngular(() => {
2907 // We need to wait for the zone to stabilize, in order for the reference
2908 // element to be in the proper place in the DOM. This is mostly relevant
2909 // for draggable elements inside portals since they get stamped out in
2910 // their original DOM position and then they get transferred to the portal.
2911 this._ngZone.onStable.pipe(take(1), takeUntil(this._destroyed)).subscribe(() => {
2912 this._updateRootElement();
2913 this._setupHandlesListener();
2914 if (this.freeDragPosition) {
2915 this._dragRef.setFreeDragPosition(this.freeDragPosition);
2916 }
2917 });
2918 });
2919 }
2920 ngOnChanges(changes) {
2921 const rootSelectorChange = changes['rootElementSelector'];
2922 const positionChange = changes['freeDragPosition'];
2923 // We don't have to react to the first change since it's being
2924 // handled in `ngAfterViewInit` where it needs to be deferred.
2925 if (rootSelectorChange && !rootSelectorChange.firstChange) {
2926 this._updateRootElement();
2927 }
2928 // Skip the first change since it's being handled in `ngAfterViewInit`.
2929 if (positionChange && !positionChange.firstChange && this.freeDragPosition) {
2930 this._dragRef.setFreeDragPosition(this.freeDragPosition);
2931 }
2932 }
2933 ngOnDestroy() {
2934 if (this.dropContainer) {
2935 this.dropContainer.removeItem(this);
2936 }
2937 const index = CdkDrag._dragInstances.indexOf(this);
2938 if (index > -1) {
2939 CdkDrag._dragInstances.splice(index, 1);
2940 }
2941 // Unnecessary in most cases, but used to avoid extra change detections with `zone-paths-rxjs`.
2942 this._ngZone.runOutsideAngular(() => {
2943 this._destroyed.next();
2944 this._destroyed.complete();
2945 this._dragRef.dispose();
2946 });
2947 }
2948 /** Syncs the root element with the `DragRef`. */
2949 _updateRootElement() {
2950 const element = this.element.nativeElement;
2951 let rootElement = element;
2952 if (this.rootElementSelector) {
2953 rootElement =
2954 element.closest !== undefined
2955 ? element.closest(this.rootElementSelector)
2956 : // Comment tag doesn't have closest method, so use parent's one.
2957 element.parentElement?.closest(this.rootElementSelector);
2958 }
2959 if (rootElement && (typeof ngDevMode === 'undefined' || ngDevMode)) {
2960 assertElementNode(rootElement, 'cdkDrag');
2961 }
2962 this._dragRef.withRootElement(rootElement || element);
2963 }
2964 /** Gets the boundary element, based on the `boundaryElement` value. */
2965 _getBoundaryElement() {
2966 const boundary = this.boundaryElement;
2967 if (!boundary) {
2968 return null;
2969 }
2970 if (typeof boundary === 'string') {
2971 return this.element.nativeElement.closest(boundary);
2972 }
2973 return coerceElement(boundary);
2974 }
2975 /** Syncs the inputs of the CdkDrag with the options of the underlying DragRef. */
2976 _syncInputs(ref) {
2977 ref.beforeStarted.subscribe(() => {
2978 if (!ref.isDragging()) {
2979 const dir = this._dir;
2980 const dragStartDelay = this.dragStartDelay;
2981 const placeholder = this._placeholderTemplate
2982 ? {
2983 template: this._placeholderTemplate.templateRef,
2984 context: this._placeholderTemplate.data,
2985 viewContainer: this._viewContainerRef,
2986 }
2987 : null;
2988 const preview = this._previewTemplate
2989 ? {
2990 template: this._previewTemplate.templateRef,
2991 context: this._previewTemplate.data,
2992 matchSize: this._previewTemplate.matchSize,
2993 viewContainer: this._viewContainerRef,
2994 }
2995 : null;
2996 ref.disabled = this.disabled;
2997 ref.lockAxis = this.lockAxis;
2998 ref.dragStartDelay =
2999 typeof dragStartDelay === 'object' && dragStartDelay
3000 ? dragStartDelay
3001 : coerceNumberProperty(dragStartDelay);
3002 ref.constrainPosition = this.constrainPosition;
3003 ref.previewClass = this.previewClass;
3004 ref
3005 .withBoundaryElement(this._getBoundaryElement())
3006 .withPlaceholderTemplate(placeholder)
3007 .withPreviewTemplate(preview)
3008 .withPreviewContainer(this.previewContainer || 'global');
3009 if (dir) {
3010 ref.withDirection(dir.value);
3011 }
3012 }
3013 });
3014 // This only needs to be resolved once.
3015 ref.beforeStarted.pipe(take(1)).subscribe(() => {
3016 // If we managed to resolve a parent through DI, use it.
3017 if (this._parentDrag) {
3018 ref.withParent(this._parentDrag._dragRef);
3019 return;
3020 }
3021 // Otherwise fall back to resolving the parent by looking up the DOM. This can happen if
3022 // the item was projected into another item by something like `ngTemplateOutlet`.
3023 let parent = this.element.nativeElement.parentElement;
3024 while (parent) {
3025 if (parent.classList.contains(DRAG_HOST_CLASS)) {
3026 ref.withParent(CdkDrag._dragInstances.find(drag => {
3027 return drag.element.nativeElement === parent;
3028 })?._dragRef || null);
3029 break;
3030 }
3031 parent = parent.parentElement;
3032 }
3033 });
3034 }
3035 /** Handles the events from the underlying `DragRef`. */
3036 _handleEvents(ref) {
3037 ref.started.subscribe(startEvent => {
3038 this.started.emit({ source: this, event: startEvent.event });
3039 // Since all of these events run outside of change detection,
3040 // we need to ensure that everything is marked correctly.
3041 this._changeDetectorRef.markForCheck();
3042 });
3043 ref.released.subscribe(releaseEvent => {
3044 this.released.emit({ source: this, event: releaseEvent.event });
3045 });
3046 ref.ended.subscribe(endEvent => {
3047 this.ended.emit({
3048 source: this,
3049 distance: endEvent.distance,
3050 dropPoint: endEvent.dropPoint,
3051 event: endEvent.event,
3052 });
3053 // Since all of these events run outside of change detection,
3054 // we need to ensure that everything is marked correctly.
3055 this._changeDetectorRef.markForCheck();
3056 });
3057 ref.entered.subscribe(enterEvent => {
3058 this.entered.emit({
3059 container: enterEvent.container.data,
3060 item: this,
3061 currentIndex: enterEvent.currentIndex,
3062 });
3063 });
3064 ref.exited.subscribe(exitEvent => {
3065 this.exited.emit({
3066 container: exitEvent.container.data,
3067 item: this,
3068 });
3069 });
3070 ref.dropped.subscribe(dropEvent => {
3071 this.dropped.emit({
3072 previousIndex: dropEvent.previousIndex,
3073 currentIndex: dropEvent.currentIndex,
3074 previousContainer: dropEvent.previousContainer.data,
3075 container: dropEvent.container.data,
3076 isPointerOverContainer: dropEvent.isPointerOverContainer,
3077 item: this,
3078 distance: dropEvent.distance,
3079 dropPoint: dropEvent.dropPoint,
3080 event: dropEvent.event,
3081 });
3082 });
3083 }
3084 /** Assigns the default input values based on a provided config object. */
3085 _assignDefaults(config) {
3086 const { lockAxis, dragStartDelay, constrainPosition, previewClass, boundaryElement, draggingDisabled, rootElementSelector, previewContainer, } = config;
3087 this.disabled = draggingDisabled == null ? false : draggingDisabled;
3088 this.dragStartDelay = dragStartDelay || 0;
3089 if (lockAxis) {
3090 this.lockAxis = lockAxis;
3091 }
3092 if (constrainPosition) {
3093 this.constrainPosition = constrainPosition;
3094 }
3095 if (previewClass) {
3096 this.previewClass = previewClass;
3097 }
3098 if (boundaryElement) {
3099 this.boundaryElement = boundaryElement;
3100 }
3101 if (rootElementSelector) {
3102 this.rootElementSelector = rootElementSelector;
3103 }
3104 if (previewContainer) {
3105 this.previewContainer = previewContainer;
3106 }
3107 }
3108 /** Sets up the listener that syncs the handles with the drag ref. */
3109 _setupHandlesListener() {
3110 // Listen for any newly-added handles.
3111 this._handles.changes
3112 .pipe(startWith(this._handles),
3113 // Sync the new handles with the DragRef.
3114 tap((handles) => {
3115 const childHandleElements = handles
3116 .filter(handle => handle._parentDrag === this)
3117 .map(handle => handle.element);
3118 // Usually handles are only allowed to be a descendant of the drag element, but if
3119 // the consumer defined a different drag root, we should allow the drag element
3120 // itself to be a handle too.
3121 if (this._selfHandle && this.rootElementSelector) {
3122 childHandleElements.push(this.element);
3123 }
3124 this._dragRef.withHandles(childHandleElements);
3125 }),
3126 // Listen if the state of any of the handles changes.
3127 switchMap((handles) => {
3128 return merge(...handles.map(item => {
3129 return item._stateChanges.pipe(startWith(item));
3130 }));
3131 }), takeUntil(this._destroyed))
3132 .subscribe(handleInstance => {
3133 // Enabled/disable the handle that changed in the DragRef.
3134 const dragRef = this._dragRef;
3135 const handle = handleInstance.element.nativeElement;
3136 handleInstance.disabled ? dragRef.disableHandle(handle) : dragRef.enableHandle(handle);
3137 });
3138 }
3139 static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkDrag, deps: [{ token: i0.ElementRef }, { token: CDK_DROP_LIST, optional: true, skipSelf: true }, { token: DOCUMENT }, { token: i0.NgZone }, { token: i0.ViewContainerRef }, { token: CDK_DRAG_CONFIG, optional: true }, { token: i1$1.Directionality, optional: true }, { token: DragDrop }, { token: i0.ChangeDetectorRef }, { token: CDK_DRAG_HANDLE, optional: true, self: true }, { token: CDK_DRAG_PARENT, optional: true, skipSelf: true }], target: i0.ɵɵFactoryTarget.Directive }); }
3140 static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.0.0", type: CdkDrag, isStandalone: true, selector: "[cdkDrag]", inputs: { data: ["cdkDragData", "data"], lockAxis: ["cdkDragLockAxis", "lockAxis"], rootElementSelector: ["cdkDragRootElement", "rootElementSelector"], boundaryElement: ["cdkDragBoundary", "boundaryElement"], dragStartDelay: ["cdkDragStartDelay", "dragStartDelay"], freeDragPosition: ["cdkDragFreeDragPosition", "freeDragPosition"], disabled: ["cdkDragDisabled", "disabled"], constrainPosition: ["cdkDragConstrainPosition", "constrainPosition"], previewClass: ["cdkDragPreviewClass", "previewClass"], previewContainer: ["cdkDragPreviewContainer", "previewContainer"] }, outputs: { started: "cdkDragStarted", released: "cdkDragReleased", ended: "cdkDragEnded", entered: "cdkDragEntered", exited: "cdkDragExited", dropped: "cdkDragDropped", moved: "cdkDragMoved" }, host: { properties: { "class.cdk-drag-disabled": "disabled", "class.cdk-drag-dragging": "_dragRef.isDragging()" }, classAttribute: "cdk-drag" }, providers: [{ provide: CDK_DRAG_PARENT, useExisting: CdkDrag }], queries: [{ propertyName: "_previewTemplate", first: true, predicate: CDK_DRAG_PREVIEW, descendants: true }, { propertyName: "_placeholderTemplate", first: true, predicate: CDK_DRAG_PLACEHOLDER, descendants: true }, { propertyName: "_handles", predicate: CDK_DRAG_HANDLE, descendants: true }], exportAs: ["cdkDrag"], usesOnChanges: true, ngImport: i0 }); }
3141}
3142i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkDrag, decorators: [{
3143 type: Directive,
3144 args: [{
3145 selector: '[cdkDrag]',
3146 exportAs: 'cdkDrag',
3147 standalone: true,
3148 host: {
3149 'class': DRAG_HOST_CLASS,
3150 '[class.cdk-drag-disabled]': 'disabled',
3151 '[class.cdk-drag-dragging]': '_dragRef.isDragging()',
3152 },
3153 providers: [{ provide: CDK_DRAG_PARENT, useExisting: CdkDrag }],
3154 }]
3155 }], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: undefined, decorators: [{
3156 type: Inject,
3157 args: [CDK_DROP_LIST]
3158 }, {
3159 type: Optional
3160 }, {
3161 type: SkipSelf
3162 }] }, { type: undefined, decorators: [{
3163 type: Inject,
3164 args: [DOCUMENT]
3165 }] }, { type: i0.NgZone }, { type: i0.ViewContainerRef }, { type: undefined, decorators: [{
3166 type: Optional
3167 }, {
3168 type: Inject,
3169 args: [CDK_DRAG_CONFIG]
3170 }] }, { type: i1$1.Directionality, decorators: [{
3171 type: Optional
3172 }] }, { type: DragDrop }, { type: i0.ChangeDetectorRef }, { type: CdkDragHandle, decorators: [{
3173 type: Optional
3174 }, {
3175 type: Self
3176 }, {
3177 type: Inject,
3178 args: [CDK_DRAG_HANDLE]
3179 }] }, { type: CdkDrag, decorators: [{
3180 type: Optional
3181 }, {
3182 type: SkipSelf
3183 }, {
3184 type: Inject,
3185 args: [CDK_DRAG_PARENT]
3186 }] }]; }, propDecorators: { _handles: [{
3187 type: ContentChildren,
3188 args: [CDK_DRAG_HANDLE, { descendants: true }]
3189 }], _previewTemplate: [{
3190 type: ContentChild,
3191 args: [CDK_DRAG_PREVIEW]
3192 }], _placeholderTemplate: [{
3193 type: ContentChild,
3194 args: [CDK_DRAG_PLACEHOLDER]
3195 }], data: [{
3196 type: Input,
3197 args: ['cdkDragData']
3198