UNPKG

igniteui-angular-sovn

Version:

Ignite UI for Angular is a dependency-free Angular toolkit for building modern web apps

1,445 lines (1,314 loc) 78 kB
import { Directive, ElementRef, EventEmitter, HostBinding, HostListener, Input, NgZone, OnDestroy, OnInit, Output, Renderer2, ChangeDetectorRef, ViewContainerRef, AfterContentInit, TemplateRef, ContentChildren, QueryList, } from "@angular/core"; import { animationFrameScheduler, fromEvent, interval, Subject } from "rxjs"; import { takeUntil, throttle } from "rxjs/operators"; import { IBaseEventArgs, PlatformUtil } from "../../core/utils"; import { IDropStrategy, IgxDefaultDropStrategy } from "./drag-drop.strategy"; enum DragScrollDirection { UP, DOWN, LEFT, RIGHT, } export enum DragDirection { VERTICAL, HORIZONTAL, BOTH, } export interface IgxDragCustomEventDetails { startX: number; startY: number; pageX: number; pageY: number; owner: IgxDragDirective; originalEvent: any; } export interface IDropBaseEventArgs extends IBaseEventArgs { /** * Reference to the original event that caused the draggable element to enter the igxDrop element. * Can be PointerEvent, TouchEvent or MouseEvent. */ originalEvent: any; /** The owner igxDrop directive that triggered this event. */ owner: IgxDropDirective; /** The igxDrag directive instanced on an element that entered the area of the igxDrop element */ drag: IgxDragDirective; /** The data contained for the draggable element in igxDrag directive. */ dragData: any; /** The initial position of the pointer on X axis when the dragged element began moving */ startX: number; /** The initial position of the pointer on Y axis when the dragged element began moving */ startY: number; /** * The current position of the pointer on X axis when the event was triggered. * Note: The browser might trigger the event with some delay and pointer would be already inside the igxDrop. */ pageX: number; /** * The current position of the pointer on Y axis when the event was triggered. * Note: The browser might trigger the event with some delay and pointer would be already inside the igxDrop. */ pageY: number; /** * The current position of the pointer on X axis relative to the container that initializes the igxDrop. * Note: The browser might trigger the event with some delay and pointer would be already inside the igxDrop. */ offsetX: number; /** * The current position of the pointer on Y axis relative to the container that initializes the igxDrop. * Note: The browser might trigger the event with some delay and pointer would be already inside the igxDrop. */ offsetY: number; } export interface IDropDroppedEventArgs extends IDropBaseEventArgs { /** Specifies if the default drop logic related to the event should be canceled. */ cancel: boolean; } export interface IDragBaseEventArgs extends IBaseEventArgs { /** * Reference to the original event that caused the interaction with the element. * Can be PointerEvent, TouchEvent or MouseEvent. */ originalEvent: PointerEvent | MouseEvent | TouchEvent; /** The owner igxDrag directive that triggered this event. */ owner: IgxDragDirective; /** The initial position of the pointer on X axis when the dragged element began moving */ startX: number; /** The initial position of the pointer on Y axis when the dragged element began moving */ startY: number; /** * The current position of the pointer on X axis when the event was triggered. * Note: The browser might trigger the event with some delay and pointer would be already inside the igxDrop. */ pageX: number; /** * The current position of the pointer on Y axis when the event was triggered. * Note: The browser might trigger the event with some delay and pointer would be already inside the igxDrop. */ pageY: number; } export interface IDragStartEventArgs extends IDragBaseEventArgs { /** Set if the the dragging should be canceled. */ cancel: boolean; } export interface IDragMoveEventArgs extends IDragStartEventArgs { /** The new pageX position of the pointer that the igxDrag will use. It can be overridden to limit dragged element X movement. */ nextPageX: number; /** The new pageX position of the pointer that the igxDrag will use. It can be overridden to limit dragged element Y movement. */ nextPageY: number; } export interface IDragGhostBaseEventArgs extends IBaseEventArgs { /** The owner igxDrag directive that triggered this event. */ owner: IgxDragDirective; /** Instance to the ghost element that is created when dragging starts. */ ghostElement: any; /** Set if the ghost creation/destruction should be canceled. */ cancel: boolean; } export interface IDragCustomTransitionArgs { duration?: number; timingFunction?: string; delay?: number; } export class IgxDragLocation { public pageX: number; public pageY: number; constructor(private _pageX, private _pageY) { this.pageX = parseFloat(_pageX); this.pageY = parseFloat(_pageY); } } @Directive({ selector: "[igxDragHandle]", standalone: true, }) export class IgxDragHandleDirective { @HostBinding("class.igx-drag__handle") public baseClass = true; constructor(public element: ElementRef<any>) {} } @Directive({ selector: "[igxDragIgnore]", standalone: true, }) export class IgxDragIgnoreDirective { @HostBinding("class.igx-drag__ignore") public baseClass = true; constructor(public element: ElementRef<any>) {} } @Directive({ exportAs: "drag", selector: "[igxDrag]", standalone: true, }) export class IgxDragDirective implements AfterContentInit, OnDestroy { /** * - Save data inside the `igxDrag` directive. This can be set when instancing `igxDrag` on an element. * ```html * <div [igxDrag]="{ source: myElement }"></div> * ``` * * @memberof IgxDragDirective */ @Input("igxDrag") public set data(value: any) { this._data = value; } public get data(): any { return this._data; } /** * An @Input property that indicates when the drag should start. * By default the drag starts after the draggable element is moved by 5px. * ```html * <div igxDrag [dragTolerance]="100"> * <span>Drag Me!</span> * </div> * ``` * * @memberof IgxDragDirective */ @Input() public dragTolerance = 5; /** * An @Input property that indicates the directions that the element can be dragged. * By default it is set to both horizontal and vertical directions. * ```html * <div igxDrag [dragDirection]="dragDir"> * <span>Drag Me!</span> * </div> * ``` * ```typescript * public dragDir = DragDirection.HORIZONTAL; * ``` * * @memberof IgxDragDirective */ @Input() public dragDirection = DragDirection.BOTH; /** * An @Input property that provide a way for igxDrag and igxDrop to be linked through channels. * It accepts single value or an array of values and evaluates then using strict equality. * ```html * <div igxDrag [dragChannel]="'odd'"> * <span>95</span> * </div> * <div igxDrop [dropChannel]="['odd', 'irrational']"> * <span>Numbers drop area!</span> * </div> * ``` * * @memberof IgxDragDirective */ @Input() public dragChannel: number | string | number[] | string[]; /** * An @Input property that specifies if the base element should not be moved and a ghost element should be rendered that represents it. * By default it is set to `true`. * If it is set to `false` when dragging the base element is moved instead and no ghost elements are rendered. * ```html * <div igxDrag [ghost]="false"> * <span>Drag Me!</span> * </div> * ``` * * @memberof IgxDragDirective */ @Input() public ghost = true; /** * Sets a custom class that will be added to the `ghostElement` element. * ```html * <div igxDrag [ghostClass]="'ghostElement'"> * <span>Drag Me!</span> * </div> * ``` * * @memberof IgxDragDirective */ @Input() public ghostClass = ""; /** * An @Input property that specifies a template for the ghost element created when dragging starts and `ghost` is true. * By default a clone of the base element the igxDrag is instanced is created. * ```html * <div igxDrag [ghostTemplate]="customGhost"> * <span>Drag Me!</span> * </div> * <ng-template #customGhost> * <div class="customGhostStyle"> * <span>I am being dragged!</span> * </div> * </ng-template> * ``` * * @memberof IgxDragDirective */ @Input() public ghostTemplate: TemplateRef<any>; /** * An @Input property that sets the element to which the dragged element will be appended. * By default it's set to null and the dragged element is appended to the body. * ```html * <div #hostDiv></div> * <div igxDrag [ghostHost]="hostDiv"> * <span>Drag Me!</span> * </div> * ``` * * @memberof IgxDragDirective */ @Input() public ghostHost; /** * An @Input assigning a scroll container to the dragged element. By default its the window. */ @Input() public scrollContainer: HTMLElement = null; /** * Event triggered when the draggable element drag starts. * ```html * <div igxDrag (dragStart)="onDragStart()"> * <span>Drag Me!</span> * </div> * ``` * ```typescript * public onDragStart(){ * alert("The drag has stared!"); * } * ``` * * @memberof IgxDragDirective */ @Output() public dragStart = new EventEmitter<IDragStartEventArgs>(); /** * Event triggered when the draggable element has been moved. * ```html * <div igxDrag (dragMove)="onDragMove()"> * <span>Drag Me!</span> * </div> * ``` * ```typescript * public onDragMove(){ * alert("The element has moved!"); * } * ``` * * @memberof IgxDragDirective */ @Output() public dragMove = new EventEmitter<IDragMoveEventArgs>(); /** * Event triggered when the draggable element is released. * ```html * <div igxDrag (dragEnd)="onDragEnd()"> * <span>Drag Me!</span> * </div> * ``` * ```typescript * public onDragEnd(){ * alert("The drag has ended!"); * } * ``` * * @memberof IgxDragDirective */ @Output() public dragEnd = new EventEmitter<IDragBaseEventArgs>(); /** * Event triggered when the draggable element is clicked. * ```html * <div igxDrag (dragClick)="onDragClick()"> * <span>Drag Me!</span> * </div> * ``` * ```typescript * public onDragClick(){ * alert("The element has been clicked!"); * } * ``` * * @memberof IgxDragDirective */ @Output() public dragClick = new EventEmitter<IDragBaseEventArgs>(); /** * Event triggered when the drag ghost element is created. * ```html * <div igxDrag (ghostCreate)="ghostCreated()"> * <span>Drag Me!</span> * </div> * ``` * ```typescript * public ghostCreated(){ * alert("The ghost has been created!"); * } * ``` * * @memberof IgxDragDirective */ @Output() public ghostCreate = new EventEmitter<IDragGhostBaseEventArgs>(); /** * Event triggered when the drag ghost element is created. * ```html * <div igxDrag (ghostDestroy)="ghostDestroyed()"> * <span>Drag Me!</span> * </div> * ``` * ```typescript * public ghostDestroyed(){ * alert("The ghost has been destroyed!"); * } * ``` * * @memberof IgxDragDirective */ @Output() public ghostDestroy = new EventEmitter<IDragGhostBaseEventArgs>(); /** * Event triggered after the draggable element is released and after its animation has finished. * ```html * <div igxDrag (transitioned)="onMoveEnd()"> * <span>Drag Me!</span> * </div> * ``` * ```typescript * public onMoveEnd(){ * alert("The move has ended!"); * } * ``` * * @memberof IgxDragDirective */ @Output() public transitioned = new EventEmitter<IDragBaseEventArgs>(); /** * @hidden */ @ContentChildren(IgxDragHandleDirective, { descendants: true }) public dragHandles: QueryList<IgxDragHandleDirective>; /** * @hidden */ @ContentChildren(IgxDragIgnoreDirective, { descendants: true }) public dragIgnoredElems: QueryList<IgxDragIgnoreDirective>; /** * @hidden */ @HostBinding("class.igx-drag") public baseClass = true; /** * @hidden */ @HostBinding("class.igx-drag--select-disabled") public selectDisabled = false; /** * Gets the current location of the element relative to the page. */ public get location(): IgxDragLocation { return new IgxDragLocation(this.pageX, this.pageY); } /** * Gets the original location of the element before dragging started. */ public get originLocation(): IgxDragLocation { return new IgxDragLocation(this.baseOriginLeft, this.baseOriginTop); } /** * @hidden */ public get pointerEventsEnabled() { return typeof PointerEvent !== "undefined"; } /** * @hidden */ public get touchEventsEnabled() { return "ontouchstart" in window; } /** * @hidden */ public get pageX() { if (this.ghost && this.ghostElement) { return this.ghostLeft; } return this.baseLeft + this.windowScrollLeft; } /** * @hidden */ public get pageY() { if (this.ghost && this.ghostElement) { return this.ghostTop; } return this.baseTop + this.windowScrollTop; } protected get baseLeft(): number { return this.element.nativeElement.getBoundingClientRect().left; } protected get baseTop(): number { return this.element.nativeElement.getBoundingClientRect().top; } protected get baseOriginLeft(): number { return this.baseLeft - this.getTransformX(this.element.nativeElement); } protected get baseOriginTop(): number { return this.baseTop - this.getTransformY(this.element.nativeElement); } protected set ghostLeft(pageX: number) { if (this.ghostElement) { // We need to take into account marginLeft, since top style does not include margin, but pageX includes the margin. const ghostMarginLeft = parseInt( document.defaultView.getComputedStyle(this.ghostElement)[ "margin-left" ], 10 ); // If ghost host is defined it needs to be taken into account. this.ghostElement.style.left = pageX - ghostMarginLeft - this._ghostHostX + "px"; } } protected get ghostLeft() { if (this.ghostElement) { return ( parseInt(this.ghostElement.style.left, 10) + this._ghostHostX ); } } protected set ghostTop(pageY: number) { if (this.ghostElement) { // We need to take into account marginTop, since top style does not include margin, but pageY includes the margin. const ghostMarginTop = parseInt( document.defaultView.getComputedStyle(this.ghostElement)[ "margin-top" ], 10 ); // If ghost host is defined it needs to be taken into account. this.ghostElement.style.top = pageY - ghostMarginTop - this._ghostHostY + "px"; } } protected get ghostTop() { if (this.ghostElement) { return parseInt(this.ghostElement.style.top, 10) + this._ghostHostY; } } protected get windowScrollTop() { return document.documentElement.scrollTop || window.scrollY; } protected get windowScrollLeft() { return document.documentElement.scrollLeft || window.scrollX; } protected get windowScrollHeight() { return document.documentElement.scrollHeight; } protected get windowScrollWidth() { return document.documentElement.scrollWidth; } /** * @hidden */ public defaultReturnDuration = "0.5s"; /** * @hidden */ public ghostElement; /** * @hidden */ public animInProgress = false; protected ghostContext: any = null; protected _startX = 0; protected _startY = 0; protected _lastX = 0; protected _lastY = 0; protected _dragStarted = false; /** Drag ghost related properties */ protected _defaultOffsetX; protected _defaultOffsetY; protected _offsetX; protected _offsetY; protected _ghostStartX; protected _ghostStartY; protected _ghostHostX = 0; protected _ghostHostY = 0; protected _dynamicGhostRef; protected _pointerDownId = null; protected _clicked = false; protected _lastDropArea = null; protected _destroy = new Subject<boolean>(); protected _removeOnDestroy = true; protected _data: any; protected _scrollContainer = null; protected _originalScrollContainerWidth = 0; protected _originalScrollContainerHeight = 0; protected _scrollContainerStep = 5; protected _scrollContainerStepMs = 10; protected _scrollContainerThreshold = 25; protected _containerScrollIntervalId = null; /** * An @Input property that specifies the offset of the dragged element relative to the mouse in pixels. * By default it's taking the relative position to the mouse when the drag started and keeps it the same. * ```html * <div #hostDiv></div> * <div igxDrag [ghostOffsetX]="0"> * <span>Drag Me!</span> * </div> * ``` * * @memberof IgxDragDirective */ @Input() public set ghostOffsetX(value) { this._offsetX = parseInt(value, 10); } public get ghostOffsetX() { return this._offsetX !== undefined ? this._offsetX : this._defaultOffsetX; } /** * An @Input property that specifies the offset of the dragged element relative to the mouse in pixels. * By default it's taking the relative position to the mouse when the drag started and keeps it the same. * ```html * <div #hostDiv></div> * <div igxDrag [ghostOffsetY]="0"> * <span>Drag Me!</span> * </div> * ``` * * @memberof IgxDragDirective */ @Input() public set ghostOffsetY(value) { this._offsetY = parseInt(value, 10); } public get ghostOffsetY() { return this._offsetY !== undefined ? this._offsetY : this._defaultOffsetY; } constructor( public cdr: ChangeDetectorRef, public element: ElementRef, public viewContainer: ViewContainerRef, public zone: NgZone, public renderer: Renderer2, protected platformUtil: PlatformUtil ) {} /** * @hidden */ public ngAfterContentInit() { if (!this.dragHandles || !this.dragHandles.length) { // Set user select none to the whole draggable element if no drag handles are defined. this.selectDisabled = true; } // Bind events this.zone.runOutsideAngular(() => { if (!this.platformUtil.isBrowser) { return; } const targetElements = this.dragHandles && this.dragHandles.length ? this.dragHandles.map((item) => item.element.nativeElement) : [this.element.nativeElement]; targetElements.forEach((element) => { if (this.pointerEventsEnabled) { fromEvent(element, "pointerdown") .pipe(takeUntil(this._destroy)) .subscribe((res) => this.onPointerDown(res)); fromEvent(element, "pointermove") .pipe( throttle(() => interval(0, animationFrameScheduler) ), takeUntil(this._destroy) ) .subscribe((res) => this.onPointerMove(res)); fromEvent(element, "pointerup") .pipe(takeUntil(this._destroy)) .subscribe((res) => this.onPointerUp(res)); if (!this.ghost) { // Do not bind `lostpointercapture` to the target, because we will bind it on the ghost later. fromEvent(element, "lostpointercapture") .pipe(takeUntil(this._destroy)) .subscribe((res) => this.onPointerLost(res)); } } else if (this.touchEventsEnabled) { fromEvent(element, "touchstart") .pipe(takeUntil(this._destroy)) .subscribe((res) => this.onPointerDown(res)); } else { // We don't have pointer events and touch events. Use then mouse events. fromEvent(element, "mousedown") .pipe(takeUntil(this._destroy)) .subscribe((res) => this.onPointerDown(res)); } }); // We should bind to document events only once when there are no pointer events. if (!this.pointerEventsEnabled && this.touchEventsEnabled) { fromEvent(document.defaultView, "touchmove") .pipe( throttle(() => interval(0, animationFrameScheduler)), takeUntil(this._destroy) ) .subscribe((res) => this.onPointerMove(res)); fromEvent(document.defaultView, "touchend") .pipe(takeUntil(this._destroy)) .subscribe((res) => this.onPointerUp(res)); } else if (!this.pointerEventsEnabled) { fromEvent(document.defaultView, "mousemove") .pipe( throttle(() => interval(0, animationFrameScheduler)), takeUntil(this._destroy) ) .subscribe((res) => this.onPointerMove(res)); fromEvent(document.defaultView, "mouseup") .pipe(takeUntil(this._destroy)) .subscribe((res) => this.onPointerUp(res)); } this.element.nativeElement.addEventListener( "transitionend", (args) => { this.onTransitionEnd(args); } ); }); // Set transition duration to 0s. This also helps with setting `visibility: hidden` to the base to not lag. this.element.nativeElement.style.transitionDuration = "0.0s"; } /** * @hidden */ public ngOnDestroy() { this._destroy.next(true); this._destroy.complete(); if (this.ghost && this.ghostElement && this._removeOnDestroy) { this.ghostElement.parentNode.removeChild(this.ghostElement); this.ghostElement = null; if (this._dynamicGhostRef) { this._dynamicGhostRef.destroy(); this._dynamicGhostRef = null; } } if (this._containerScrollIntervalId) { clearInterval(this._containerScrollIntervalId); this._containerScrollIntervalId = null; } } /** * Sets desired location of the base element or ghost element if rended relative to the document. * * @param newLocation New location that should be applied. It is advised to get new location using getBoundingClientRects() + scroll. */ public setLocation(newLocation: IgxDragLocation) { // We do not subtract marginLeft and marginTop here because here we calculate deltas. if (this.ghost && this.ghostElement) { const offsetHostX = this.ghostHost ? this.ghostHostOffsetLeft(this.ghostHost) : 0; const offsetHostY = this.ghostHost ? this.ghostHostOffsetTop(this.ghostHost) : 0; this.ghostLeft = newLocation.pageX - offsetHostX + this.windowScrollLeft; this.ghostTop = newLocation.pageY - offsetHostY + this.windowScrollTop; } else if (!this.ghost) { const deltaX = newLocation.pageX - this.pageX; const deltaY = newLocation.pageY - this.pageY; const transformX = this.getTransformX(this.element.nativeElement); const transformY = this.getTransformY(this.element.nativeElement); this.setTransformXY(transformX + deltaX, transformY + deltaY); } this._startX = this.baseLeft; this._startY = this.baseTop; } /** * Animates the base or ghost element depending on the `ghost` input to its initial location. * If `ghost` is true but there is not ghost rendered, it will be created and animated. * If the base element has changed its DOM position its initial location will be changed accordingly. * * @param customAnimArgs Custom transition properties that will be applied when performing the transition. * @param startLocation Start location from where the transition should start. */ public transitionToOrigin( customAnimArgs?: IDragCustomTransitionArgs, startLocation?: IgxDragLocation ) { if ( (!!startLocation && startLocation.pageX === this.baseOriginLeft && startLocation.pageY === this.baseOriginLeft) || (!startLocation && this.ghost && !this.ghostElement) ) { return; } if ( !!startLocation && startLocation.pageX !== this.pageX && startLocation.pageY !== this.pageY ) { if (this.ghost && !this.ghostElement) { this._startX = startLocation.pageX; this._startY = startLocation.pageY; this._ghostStartX = this._startX; this._ghostStartY = this._startY; this.createGhost(this._startX, this._startY); } this.setLocation(startLocation); } this.animInProgress = true; // Use setTimeout because we need to be sure that the element is positioned first correctly if there is start location. setTimeout(() => { if (this.ghost) { this.ghostElement.style.transitionProperty = "top, left"; this.ghostElement.style.transitionDuration = customAnimArgs && customAnimArgs.duration ? customAnimArgs.duration + "s" : this.defaultReturnDuration; this.ghostElement.style.transitionTimingFunction = customAnimArgs && customAnimArgs.timingFunction ? customAnimArgs.timingFunction : ""; this.ghostElement.style.transitionDelay = customAnimArgs && customAnimArgs.delay ? customAnimArgs.delay + "s" : ""; this.setLocation( new IgxDragLocation(this.baseLeft, this.baseTop) ); } else if (!this.ghost) { this.element.nativeElement.style.transitionProperty = "transform"; this.element.nativeElement.style.transitionDuration = customAnimArgs && customAnimArgs.duration ? customAnimArgs.duration + "s" : this.defaultReturnDuration; this.element.nativeElement.style.transitionTimingFunction = customAnimArgs && customAnimArgs.timingFunction ? customAnimArgs.timingFunction : ""; this.element.nativeElement.style.transitionDelay = customAnimArgs && customAnimArgs.delay ? customAnimArgs.delay + "s" : ""; this._startX = this.baseLeft; this._startY = this.baseTop; this.setTransformXY(0, 0); } }, 0); } /** * Animates the base or ghost element to a specific target location or other element using transition. * If `ghost` is true but there is not ghost rendered, it will be created and animated. * It is recommended to use 'getBoundingClientRects() + pageScroll' when determining desired location. * * @param target Target that the base or ghost will transition to. It can be either location in the page or another HTML element. * @param customAnimArgs Custom transition properties that will be applied when performing the transition. * @param startLocation Start location from where the transition should start. */ public transitionTo( target: IgxDragLocation | ElementRef, customAnimArgs?: IDragCustomTransitionArgs, startLocation?: IgxDragLocation ) { if (!!startLocation && this.ghost && !this.ghostElement) { this._startX = startLocation.pageX; this._startY = startLocation.pageY; this._ghostStartX = this._startX; this._ghostStartY = this._startY; } else if (!!startLocation && (!this.ghost || this.ghostElement)) { this.setLocation(startLocation); } else if (this.ghost && !this.ghostElement) { this._startX = this.baseLeft; this._startY = this.baseTop; this._ghostStartX = this._startX + this.windowScrollLeft; this._ghostStartY = this._startY + this.windowScrollTop; } if (this.ghost && !this.ghostElement) { this.createGhost(this._startX, this._startY); } this.animInProgress = true; // Use setTimeout because we need to be sure that the element is positioned first correctly if there is start location. setTimeout(() => { const movedElem = this.ghost ? this.ghostElement : this.element.nativeElement; movedElem.style.transitionProperty = this.ghost && this.ghostElement ? "left, top" : "transform"; movedElem.style.transitionDuration = customAnimArgs && customAnimArgs.duration ? customAnimArgs.duration + "s" : this.defaultReturnDuration; movedElem.style.transitionTimingFunction = customAnimArgs && customAnimArgs.timingFunction ? customAnimArgs.timingFunction : ""; movedElem.style.transitionDelay = customAnimArgs && customAnimArgs.delay ? customAnimArgs.delay + "s" : ""; if (target instanceof IgxDragLocation) { this.setLocation( new IgxDragLocation(target.pageX, target.pageY) ); } else { const targetRects = target.nativeElement.getBoundingClientRect(); this.setLocation( new IgxDragLocation( targetRects.left - this.windowScrollLeft, targetRects.top - this.windowScrollTop ) ); } }, 0); } /** * @hidden * Method bound to the PointerDown event of the base element igxDrag is initialized. * @param event PointerDown event captured */ public onPointerDown(event) { const ignoredElement = this.dragIgnoredElems.find( (elem) => elem.element.nativeElement === event.target ); if (ignoredElement) { return; } this._clicked = true; this._pointerDownId = event.pointerId; // Set pointer capture so we detect pointermove even if mouse is out of bounds until ghostElement is created. const handleFound = this.dragHandles.find( (handle) => handle.element.nativeElement === event.currentTarget ); const targetElement = handleFound ? handleFound.element.nativeElement : this.element.nativeElement; if (this.pointerEventsEnabled) { targetElement.setPointerCapture(this._pointerDownId); } else { targetElement.focus(); event.preventDefault(); } if (this.pointerEventsEnabled || !this.touchEventsEnabled) { // Check first for pointer events or non touch, because we can have pointer events and touch events at once. this._startX = event.pageX; this._startY = event.pageY; } else if (this.touchEventsEnabled) { this._startX = event.touches[0].pageX; this._startY = event.touches[0].pageY; } this._defaultOffsetX = this.baseLeft - this._startX + this.windowScrollLeft; this._defaultOffsetY = this.baseTop - this._startY + this.windowScrollTop; this._ghostStartX = this._startX + this.ghostOffsetX; this._ghostStartY = this._startY + this.ghostOffsetY; this._lastX = this._startX; this._lastY = this._startY; } /** * @hidden * Perform drag move logic when dragging and dispatching events if there is igxDrop under the pointer. * This method is bound at first at the base element. * If dragging starts and after the ghostElement is rendered the pointerId is reassigned it. Then this method is bound to it. * @param event PointerMove event captured */ public onPointerMove(event) { if (this._clicked) { let pageX; let pageY; if (this.pointerEventsEnabled || !this.touchEventsEnabled) { // Check first for pointer events or non touch, because we can have pointer events and touch events at once. pageX = event.pageX; pageY = event.pageY; } else if (this.touchEventsEnabled) { pageX = event.touches[0].pageX; pageY = event.touches[0].pageY; // Prevent scrolling on touch while dragging event.preventDefault(); } const totalMovedX = pageX - this._startX; const totalMovedY = pageY - this._startY; if ( !this._dragStarted && (Math.abs(totalMovedX) > this.dragTolerance || Math.abs(totalMovedY) > this.dragTolerance) ) { const dragStartArgs: IDragStartEventArgs = { originalEvent: event, owner: this, startX: pageX - totalMovedX, startY: pageY - totalMovedY, pageX, pageY, cancel: false, }; this.zone.run(() => { this.dragStart.emit(dragStartArgs); }); if (!dragStartArgs.cancel) { this._dragStarted = true; if (this.ghost) { // We moved enough so ghostElement can be rendered and actual dragging to start. // When creating it will take into account any offset set by the user by default. this.createGhost(pageX, pageY); } else if ( this._offsetX !== undefined || this._offsetY !== undefined ) { // There is no need for ghost, but we will need to position initially the base element to reflect any offset. const transformX = (this._offsetX !== undefined ? this._offsetX - this._defaultOffsetX : 0) + this.getTransformX(this.element.nativeElement); const transformY = (this._offsetY !== undefined ? this._offsetY - this._defaultOffsetY : 0) + this.getTransformY(this.element.nativeElement); this.setTransformXY(transformX, transformY); } } else { return; } } else if (!this._dragStarted) { return; } const moveArgs: IDragMoveEventArgs = { originalEvent: event, owner: this, startX: this._startX, startY: this._startY, pageX: this._lastX, pageY: this._lastY, nextPageX: pageX, nextPageY: pageY, cancel: false, }; this.dragMove.emit(moveArgs); const setPageX = moveArgs.nextPageX; const setPageY = moveArgs.nextPageY; if (!moveArgs.cancel) { // Scroll root container if the user reaches its boundaries. this.onScrollContainer(); // Move the actual element around if (this.ghost) { const updatedTotalMovedX = this.dragDirection === DragDirection.VERTICAL ? 0 : setPageX - this._startX; const updatedTotalMovedY = this.dragDirection === DragDirection.HORIZONTAL ? 0 : setPageY - this._startY; this.ghostLeft = this._ghostStartX + updatedTotalMovedX; this.ghostTop = this._ghostStartY + updatedTotalMovedY; } else { const lastMovedX = this.dragDirection === DragDirection.VERTICAL ? 0 : setPageX - this._lastX; const lastMovedY = this.dragDirection === DragDirection.HORIZONTAL ? 0 : setPageY - this._lastY; const translateX = this.getTransformX(this.element.nativeElement) + lastMovedX; const translateY = this.getTransformY(this.element.nativeElement) + lastMovedY; this.setTransformXY(translateX, translateY); } this.dispatchDragEvents(pageX, pageY, event); } this._lastX = setPageX; this._lastY = setPageY; } } /** * @hidden * Perform drag end logic when releasing the ghostElement and dispatching drop event if igxDrop is under the pointer. * This method is bound at first at the base element. * If dragging starts and after the ghostElement is rendered the pointerId is reassigned to it. Then this method is bound to it. * @param event PointerUp event captured */ public onPointerUp(event) { if (!this._clicked) { return; } let pageX; let pageY; if (this.pointerEventsEnabled || !this.touchEventsEnabled) { // Check first for pointer events or non touch, because we can have pointer events and touch events at once. pageX = event.pageX; pageY = event.pageY; } else if (this.touchEventsEnabled) { pageX = event.touches[0].pageX; pageY = event.touches[0].pageY; // Prevent scrolling on touch while dragging event.preventDefault(); } const eventArgs: IDragBaseEventArgs = { originalEvent: event, owner: this, startX: this._startX, startY: this._startY, pageX, pageY, }; this._pointerDownId = null; this._clicked = false; if (this._dragStarted) { if ( this._lastDropArea && this._lastDropArea !== this.element.nativeElement ) { this.dispatchDropEvent(event.pageX, event.pageY, event); } this.zone.run(() => { this.dragEnd.emit(eventArgs); }); if (!this.animInProgress) { this.onTransitionEnd(null); } } else { // Trigger our own click event because when there is no ghost, native click cannot be prevented when dragging. this.zone.run(() => { this.dragClick.emit(eventArgs); }); } if (this._containerScrollIntervalId) { clearInterval(this._containerScrollIntervalId); this._containerScrollIntervalId = null; } } /** * @hidden * Execute this method whe the pointer capture has been lost. * This means that during dragging the user has performed other action like right clicking and then clicking somewhere else. * This method will ensure that the drag state is being reset in this case as if the user released the dragged element. * @param event Event captured */ public onPointerLost(event) { if (!this._clicked) { return; } const eventArgs = { originalEvent: event, owner: this, startX: this._startX, startY: this._startY, pageX: event.pageX, pageY: event.pageY, }; this._pointerDownId = null; this._clicked = false; if (this._dragStarted) { this.zone.run(() => { this.dragEnd.emit(eventArgs); }); if (!this.animInProgress) { this.onTransitionEnd(null); } } } /** * @hidden */ public onTransitionEnd(event) { if ((!this._dragStarted && !this.animInProgress) || this._clicked) { // Return if no dragging started and there is no animation in progress. return; } if (this.ghost && this.ghostElement) { this._ghostStartX = this.baseLeft + this.windowScrollLeft; this._ghostStartY = this.baseTop + this.windowScrollTop; const ghostDestroyArgs: IDragGhostBaseEventArgs = { owner: this, ghostElement: this.ghostElement, cancel: false, }; this.ghostDestroy.emit(ghostDestroyArgs); if (ghostDestroyArgs.cancel) { return; } this.ghostElement.remove(); this.ghostElement = null; if (this._dynamicGhostRef) { this._dynamicGhostRef.destroy(); this._dynamicGhostRef = null; } } else if (!this.ghost) { this.element.nativeElement.style.transitionProperty = ""; this.element.nativeElement.style.transitionDuration = "0.0s"; this.element.nativeElement.style.transitionTimingFunction = ""; this.element.nativeElement.style.transitionDelay = ""; } this.animInProgress = false; this._dragStarted = false; // Execute transitioned after everything is reset so if the user sets new location on the base now it would work as expected. this.zone.run(() => { this.transitioned.emit({ originalEvent: event, owner: this, startX: this._startX, startY: this._startY, pageX: this._startX, pageY: this._startY, }); }); } /** * @hidden * Create ghost element - if a Node object is provided it creates a clone of that node, * otherwise it clones the host element. * Bind all needed events. * @param pageX Latest pointer position on the X axis relative to the page. * @param pageY Latest pointer position on the Y axis relative to the page. * @param node The Node object to be cloned. */ protected createGhost(pageX, pageY, node: any = null) { if (!this.ghost) { return; } if (this.ghostTemplate) { this._dynamicGhostRef = this.viewContainer.createEmbeddedView( this.ghostTemplate, this.ghostContext ); if ( this._dynamicGhostRef.rootNodes[0].style.display === "contents" ) { // Change the display to default since display contents does not position the element absolutely. this._dynamicGhostRef.rootNodes[0].style.display = "block"; } this.ghostElement = this._dynamicGhostRef.rootNodes[0]; } else { this.ghostElement = node ? node.cloneNode(true) : this.element.nativeElement.cloneNode(true); } const totalMovedX = pageX - this._startX; const totalMovedY = pageY - this._startY; this._ghostHostX = this.ghostHost ? this.ghostHostOffsetLeft(this.ghostHost) : 0; this._ghostHostY = this.ghostHost ? this.ghostHostOffsetTop(this.ghostHost) : 0; this.ghostElement.style.transitionDuration = "0.0s"; this.ghostElement.style.position = "absolute"; if (this.ghostClass) { this.renderer.addClass(this.ghostElement, this.ghostClass); } const createEventArgs = { owner: this, ghostElement: this.ghostElement, cancel: false, }; this.ghostCreate.emit(createEventArgs); if (createEventArgs.cancel) { this.ghostElement = null; if (this.ghostTemplate && this._dynamicGhostRef) { this._dynamicGhostRef.destroy(); } return; } if (this.ghostHost) { this.ghostHost.appendChild(this.ghostElement); } else { document.body.appendChild(this.ghostElement); } const ghostMarginLeft = parseInt( document.defaultView.getComputedStyle(this.ghostElement)[ "margin-left" ], 10 ); const ghostMarginTop = parseInt( document.defaultView.getComputedStyle(this.ghostElement)[ "margin-top" ], 10 ); this.ghostElement.style.left = this._ghostStartX - ghostMarginLeft + totalMovedX - this._ghostHostX + "px"; this.ghostElement.style.top = this._ghostStartY - ghostMarginTop + totalMovedY - this._ghostHostY + "px"; if (this.pointerEventsEnabled) { // The ghostElement takes control for moving and dragging after it has been rendered. if (this._pointerDownId !== null) { this.ghostElement.setPointerCapture(this._pointerDownId); } this.ghostElement.addEventListener("pointermove", (args) => { this.onPointerMove(args); }); this.ghostElement.addEventListener("pointerup", (args) => { this.onPointerUp(args); }); this.ghostElement.addEventListener("lostpointercapture", (args) => { this.onPointerLost(args); }); } // Transition animation when the ghostElement is released and it returns to it's original position. this.ghostElement.addEventListener("transitionend", (args) => { this.onTransitionEnd(args); }); this.cdr.detectChanges(); } /** * @hidden * Dispatch custom igxDragEnter/igxDragLeave events based on current pointer position and if drop area is under. */ protected dispatchDragEvents(pageX: number, pageY: number, originalEvent) { let topDropArea; const customEventArgs: IgxDragCustomEventDetails = { startX: this._startX, startY: this._startY, pageX, pageY, owner: this, originalEvent, }; const elementsFromPoint = this.getElementsAtPoint(pageX, pageY); let targetElements = []; // Check for shadowRoot instance and use it if present for (const elFromPoint of elementsFromPoint) { if (elFromPoint?.shadowRoot) { targetElements = targetElements.concat(