• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

IgniteUI / igniteui-angular / 12300330513

12 Dec 2024 04:10PM CUT coverage: 91.592% (-0.03%) from 91.621%
12300330513

push

github

web-flow
Merge pull request #15120 from IgniteUI/19.0.x

Mass Merge 19.0.x to master

12984 of 15225 branches covered (85.28%)

86 of 106 new or added lines in 16 files covered. (81.13%)

12 existing lines in 3 files now uncovered.

26320 of 28736 relevant lines covered (91.59%)

34007.75 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

84.52
/projects/igniteui-angular/src/lib/directives/drag-drop/drag-drop.directive.ts
1
import {
2
    Directive,
3
    ElementRef,
4
    EventEmitter,
5
    HostBinding,
6
    HostListener,
7
    Input,
8
    NgZone,
9
    OnDestroy,
10
    OnInit,
11
    Output,
12
    Renderer2,
13
    ChangeDetectorRef,
14
    ViewContainerRef,
15
    AfterContentInit,
16
    TemplateRef,
17
    ContentChildren,
18
    QueryList,
19
    RendererStyleFlags2,
20
    booleanAttribute,
21
    EmbeddedViewRef,
22
    inject
23
} from '@angular/core';
24
import { animationFrameScheduler, fromEvent, interval, Subject } from 'rxjs';
25
import { takeUntil, throttle } from 'rxjs/operators';
26
import { IBaseEventArgs, PlatformUtil } from '../../core/utils';
27
import { IDropStrategy, IgxDefaultDropStrategy } from './drag-drop.strategy';
28
import { DOCUMENT } from '@angular/common';
29

30
enum DragScrollDirection {
2✔
31
    UP,
2✔
32
    DOWN,
2✔
33
    LEFT,
2✔
34
    RIGHT
2✔
35
}
36

37
export enum DragDirection {
2✔
38
    VERTICAL,
2✔
39
    HORIZONTAL,
2✔
40
    BOTH
2✔
41
}
42

43
export interface IgxDragCustomEventDetails {
44
    startX: number;
45
    startY: number;
46
    pageX: number;
47
    pageY: number;
48
    owner: IgxDragDirective;
49
    originalEvent: any;
50
}
51

52
export interface IDropBaseEventArgs extends IBaseEventArgs {
53
    /**
54
     * Reference to the original event that caused the draggable element to enter the igxDrop element.
55
     * Can be PointerEvent, TouchEvent or MouseEvent.
56
     */
57
    originalEvent: any;
58
    /** The owner igxDrop directive that triggered this event. */
59
    owner: IgxDropDirective;
60
    /** The igxDrag directive instanced on an element that entered the area of the igxDrop element */
61
    drag: IgxDragDirective;
62
    /** The data contained for the draggable element in igxDrag directive. */
63
    dragData: any;
64
    /** The initial position of the pointer on X axis when the dragged element began moving */
65
    startX: number;
66
    /** The initial position of the pointer on Y axis when the dragged element began moving */
67
    startY: number;
68
    /**
69
     * The current position of the pointer on X axis when the event was triggered.
70
     * Note: The browser might trigger the event with some delay and pointer would be already inside the igxDrop.
71
     */
72
    pageX: number;
73
    /**
74
     * The current position of the pointer on Y axis when the event was triggered.
75
     * Note: The browser might trigger the event with some delay and pointer would be already inside the igxDrop.
76
     */
77
    pageY: number;
78
    /**
79
     * The current position of the pointer on X axis relative to the container that initializes the igxDrop.
80
     * Note: The browser might trigger the event with some delay and pointer would be already inside the igxDrop.
81
     */
82
    offsetX: number;
83
    /**
84
     * The current position of the pointer on Y axis relative to the container that initializes the igxDrop.
85
     * Note: The browser might trigger the event with some delay and pointer would be already inside the igxDrop.
86
     */
87
    offsetY: number;
88
}
89

90
export interface IDropDroppedEventArgs extends IDropBaseEventArgs {
91
    /** Specifies if the default drop logic related to the event should be canceled. */
92
    cancel: boolean;
93
}
94

95
export interface IDragBaseEventArgs extends IBaseEventArgs {
96
    /**
97
     * Reference to the original event that caused the interaction with the element.
98
     * Can be PointerEvent, TouchEvent or MouseEvent.
99
     */
100
    originalEvent: PointerEvent | MouseEvent | TouchEvent;
101
    /** The owner igxDrag directive that triggered this event. */
102
    owner: IgxDragDirective;
103
    /** The initial position of the pointer on X axis when the dragged element began moving */
104
    startX: number;
105
    /** The initial position of the pointer on Y axis when the dragged element began moving */
106
    startY: number;
107
    /**
108
     * The current position of the pointer on X axis when the event was triggered.
109
     * Note: The browser might trigger the event with some delay and pointer would be already inside the igxDrop.
110
     */
111
    pageX: number;
112
    /**
113
     * The current position of the pointer on Y axis when the event was triggered.
114
     * Note: The browser might trigger the event with some delay and pointer would be already inside the igxDrop.
115
     */
116
    pageY: number;
117
}
118

119
export interface IDragStartEventArgs extends IDragBaseEventArgs {
120
    /** Set if the the dragging should be canceled. */
121
    cancel: boolean;
122
}
123

124
export interface IDragMoveEventArgs extends IDragStartEventArgs {
125
    /** The new pageX position of the pointer that the igxDrag will use. It can be overridden to limit dragged element X movement. */
126
    nextPageX: number;
127
    /** The new pageX position of the pointer that the igxDrag will use. It can be overridden to limit dragged element Y movement. */
128
    nextPageY: number;
129
}
130

131

132
export interface IDragGhostBaseEventArgs extends IBaseEventArgs {
133
    /** The owner igxDrag directive that triggered this event. */
134
    owner: IgxDragDirective;
135
    /** Instance to the ghost element that is created when dragging starts. */
136
    ghostElement: any;
137
    /** Set if the ghost creation/destruction should be canceled. */
138
    cancel: boolean;
139
}
140

141
export interface IDragCustomTransitionArgs {
142
    duration?: number;
143
    timingFunction?: string;
144
    delay?: number;
145
}
146

147
export class IgxDragLocation {
148
    public pageX: number;
149
    public pageY: number;
150

151
    constructor(private _pageX, private _pageY) {
24✔
152
        this.pageX = parseFloat(_pageX);
24✔
153
        this.pageY = parseFloat(_pageY);
24✔
154
    }
155
}
156

157
@Directive({
158
    selector: '[igxDragHandle]',
159
    standalone: true
160
})
161
export class IgxDragHandleDirective {
2✔
162

163
    @HostBinding('class.igx-drag__handle')
164
    public baseClass = true;
470✔
165

166
    /**
167
     * @hidden
168
     */
169
    public parentDragElement: HTMLElement = null;
470✔
170

171
    constructor(public element: ElementRef<any>) { }
470✔
172
}
173

174
@Directive({
175
    selector: '[igxDragIgnore]',
176
    standalone: true
177
})
178
export class IgxDragIgnoreDirective {
2✔
179

180
    @HostBinding('class.igx-drag__ignore')
181
    public baseClass = true;
87✔
182

183
    constructor(public element: ElementRef<any>) { }
87✔
184
}
185

186
@Directive({
187
    exportAs: 'drag',
188
    selector: '[igxDrag]',
189
    standalone: true
190
})
191
export class IgxDragDirective implements AfterContentInit, OnDestroy {
2✔
192
    /**
193
     * - Save data inside the `igxDrag` directive. This can be set when instancing `igxDrag` on an element.
194
     * ```html
195
     * <div [igxDrag]="{ source: myElement }"></div>
196
     * ```
197
     *
198
     * @memberof IgxDragDirective
199
     */
200
    @Input('igxDrag')
201
    public set data(value: any) {
202
        this._data = value;
6,867✔
203
    }
204

205
    public get data(): any {
206
        return this._data;
140✔
207
    }
208

209
    /**
210
     * Sets the tolerance in pixels before drag starts.
211
     * By default the drag starts after the draggable element is moved by 5px.
212
     * ```html
213
     * <div igxDrag [dragTolerance]="100">
214
     *         <span>Drag Me!</span>
215
     * </div>
216
     * ```
217
     *
218
     * @memberof IgxDragDirective
219
     */
220
    @Input()
221
    public dragTolerance = 5;
35,319✔
222

223
    /**
224
     * Sets the directions that the element can be dragged.
225
     * By default it is set to both horizontal and vertical directions.
226
     * ```html
227
     * <div igxDrag [dragDirection]="dragDir">
228
     *         <span>Drag Me!</span>
229
     * </div>
230
     * ```
231
     * ```typescript
232
     * public dragDir = DragDirection.HORIZONTAL;
233
     * ```
234
     *
235
     * @memberof IgxDragDirective
236
     */
237
    @Input()
238
    public dragDirection = DragDirection.BOTH;
35,319✔
239

240
    /**
241
     * A property that provides a way for igxDrag and igxDrop to be linked through channels.
242
     * It accepts single value or an array of values and evaluates then using strict equality.
243
     * ```html
244
     * <div igxDrag [dragChannel]="'odd'">
245
     *         <span>95</span>
246
     * </div>
247
     * <div igxDrop [dropChannel]="['odd', 'irrational']">
248
     *         <span>Numbers drop area!</span>
249
     * </div>
250
     * ```
251
     *
252
     * @memberof IgxDragDirective
253
     */
254
    @Input()
255
    public dragChannel: number | string | number[] | string[];
256

257
    /**
258
     * Sets whether the base element should be moved, or a ghost element should be rendered that represents it instead.
259
     * By default it is set to `true`.
260
     * If it is set to `false` when dragging the base element is moved instead and no ghost elements are rendered.
261
     * ```html
262
     * <div igxDrag [ghost]="false">
263
     *      <span>Drag Me!</span>
264
     * </div>
265
     * ```
266
     *
267
     * @memberof IgxDragDirective
268
     */
269
    @Input({ transform: booleanAttribute })
270
    public ghost = true;
35,319✔
271

272
    /**
273
     * Sets a custom class that will be added to the `ghostElement` element.
274
     * ```html
275
     * <div igxDrag [ghostClass]="'ghostElement'">
276
     *         <span>Drag Me!</span>
277
     * </div>
278
     * ```
279
     *
280
     * @memberof IgxDragDirective
281
     */
282
    @Input()
283
    public ghostClass = '';
35,319✔
284

285
    /**
286
     * Set styles that will be added to the `ghostElement` element.
287
     * ```html
288
     * <div igxDrag [ghostStyle]="{'--ig-size': 'var(--ig-size-small)'}">
289
     *         <span>Drag Me!</span>
290
     * </div>
291
     * ```
292
     *
293
     * @memberof IgxDragDirective
294
     */
295
    @Input()
296
    public ghostStyle = {};
35,319✔
297

298
    /**
299
     * Specifies a template for the ghost element created when dragging starts and `ghost` is true.
300
     * By default a clone of the base element the igxDrag is instanced is created.
301
     * ```html
302
     * <div igxDrag [ghostTemplate]="customGhost">
303
     *         <span>Drag Me!</span>
304
     * </div>
305
     * <ng-template #customGhost>
306
     *      <div class="customGhostStyle">
307
     *          <span>I am being dragged!</span>
308
     *      </div>
309
     * </ng-template>
310
     * ```
311
     *
312
     * @memberof IgxDragDirective
313
     */
314
    @Input()
315
    public ghostTemplate: TemplateRef<any>;
316

317
    /**
318
     * Sets the element to which the dragged element will be appended.
319
     * By default it's set to null and the dragged element is appended to the body.
320
     * ```html
321
     * <div #hostDiv></div>
322
     * <div igxDrag [ghostHost]="hostDiv">
323
     *         <span>Drag Me!</span>
324
     * </div>
325
     * ```
326
     *
327
     * @memberof IgxDragDirective
328
     */
329
    @Input()
330
    public ghostHost;
331

332
    /**
333
     * Overrides the scroll container of the dragged element. By default its the window.
334
     */
335
    @Input()
336
    public scrollContainer: HTMLElement = null
35,319✔
337

338
    /**
339
     * Event triggered when the draggable element drag starts.
340
     * ```html
341
     * <div igxDrag (dragStart)="onDragStart()">
342
     *         <span>Drag Me!</span>
343
     * </div>
344
     * ```
345
     * ```typescript
346
     * public onDragStart(){
347
     *      alert("The drag has stared!");
348
     * }
349
     * ```
350
     *
351
     * @memberof IgxDragDirective
352
     */
353
    @Output()
354
    public dragStart = new EventEmitter<IDragStartEventArgs>();
35,319✔
355

356
    /**
357
     * Event triggered when the draggable element has been moved.
358
     * ```html
359
     * <div igxDrag  (dragMove)="onDragMove()">
360
     *         <span>Drag Me!</span>
361
     * </div>
362
     * ```
363
     * ```typescript
364
     * public onDragMove(){
365
     *      alert("The element has moved!");
366
     * }
367
     * ```
368
     *
369
     * @memberof IgxDragDirective
370
     */
371
    @Output()
372
    public dragMove = new EventEmitter<IDragMoveEventArgs>();
35,319✔
373

374
    /**
375
     * Event triggered when the draggable element is released.
376
     * ```html
377
     * <div igxDrag (dragEnd)="onDragEnd()">
378
     *         <span>Drag Me!</span>
379
     * </div>
380
     * ```
381
     * ```typescript
382
     * public onDragEnd(){
383
     *      alert("The drag has ended!");
384
     * }
385
     * ```
386
     *
387
     * @memberof IgxDragDirective
388
     */
389
    @Output()
390
    public dragEnd = new EventEmitter<IDragBaseEventArgs>();
35,319✔
391

392
    /**
393
     * Event triggered when the draggable element is clicked.
394
     * ```html
395
     * <div igxDrag (dragClick)="onDragClick()">
396
     *         <span>Drag Me!</span>
397
     * </div>
398
     * ```
399
     * ```typescript
400
     * public onDragClick(){
401
     *      alert("The element has been clicked!");
402
     * }
403
     * ```
404
     *
405
     * @memberof IgxDragDirective
406
     */
407
    @Output()
408
    public dragClick = new EventEmitter<IDragBaseEventArgs>();
35,319✔
409

410
    /**
411
     * Event triggered when the drag ghost element is created.
412
     * ```html
413
     * <div igxDrag (ghostCreate)="ghostCreated()">
414
     *         <span>Drag Me!</span>
415
     * </div>
416
     * ```
417
     * ```typescript
418
     * public ghostCreated(){
419
     *      alert("The ghost has been created!");
420
     * }
421
     * ```
422
     *
423
     * @memberof IgxDragDirective
424
     */
425
    @Output()
426
    public ghostCreate = new EventEmitter<IDragGhostBaseEventArgs>();
35,319✔
427

428
    /**
429
     * Event triggered when the drag ghost element is created.
430
     * ```html
431
     * <div igxDrag (ghostDestroy)="ghostDestroyed()">
432
     *         <span>Drag Me!</span>
433
     * </div>
434
     * ```
435
     * ```typescript
436
     * public ghostDestroyed(){
437
     *      alert("The ghost has been destroyed!");
438
     * }
439
     * ```
440
     *
441
     * @memberof IgxDragDirective
442
     */
443
    @Output()
444
    public ghostDestroy = new EventEmitter<IDragGhostBaseEventArgs>();
35,319✔
445

446
    /**
447
     * Event triggered after the draggable element is released and after its animation has finished.
448
     * ```html
449
     * <div igxDrag (transitioned)="onMoveEnd()">
450
     *         <span>Drag Me!</span>
451
     * </div>
452
     * ```
453
     * ```typescript
454
     * public onMoveEnd(){
455
     *      alert("The move has ended!");
456
     * }
457
     * ```
458
     *
459
     * @memberof IgxDragDirective
460
     */
461
    @Output()
462
    public transitioned = new EventEmitter<IDragBaseEventArgs>();
35,319✔
463

464
    /**
465
     * @hidden
466
     */
467
    @ContentChildren(IgxDragHandleDirective, { descendants: true })
468
    public dragHandles: QueryList<IgxDragHandleDirective>;
469

470
    /**
471
     * @hidden
472
     */
473
    @ContentChildren(IgxDragIgnoreDirective, { descendants: true })
474
    public dragIgnoredElems: QueryList<IgxDragIgnoreDirective>;
475

476
    /**
477
     * @hidden
478
     */
479
    @HostBinding('class.igx-drag')
480
    public baseClass = true;
35,319✔
481

482
    /**
483
     * @hidden
484
     */
485
    @HostBinding('class.igx-drag--select-disabled')
486
    public selectDisabled = false;
35,319✔
487

488

489
    /**
490
     * Gets the current location of the element relative to the page.
491
     */
492
    public get location(): IgxDragLocation {
493
        return new IgxDragLocation(this.pageX, this.pageY);
2✔
494
    }
495

496
    /**
497
     * Gets the original location of the element before dragging started.
498
     */
499
    public get originLocation(): IgxDragLocation {
500
        return new IgxDragLocation(this.baseOriginLeft, this.baseOriginTop);
4✔
501
    }
502

503
    /**
504
     * @hidden
505
     */
506
    public get pointerEventsEnabled() {
507
        return typeof PointerEvent !== 'undefined';
106,768✔
508
    }
509

510
    /**
511
     * @hidden
512
     */
513
    public get touchEventsEnabled() {
514
        return 'ontouchstart' in window;
×
515
    }
516

517
    /**
518
     * @hidden
519
     */
520
    public get pageX() {
521
        if (this.ghost && this.ghostElement) {
509✔
522
            return this.ghostLeft;
464✔
523
        }
524
        return this.baseLeft + this.windowScrollLeft;
45✔
525
    }
526

527
    /**
528
     * @hidden
529
     */
530
    public get pageY() {
531
        if (this.ghost && this.ghostElement) {
535✔
532
            return this.ghostTop;
490✔
533
        }
534
        return this.baseTop + this.windowScrollTop;
45✔
535
    }
536

537
    protected get baseLeft(): number {
538
        return this.element.nativeElement.getBoundingClientRect().left;
321✔
539
    }
540

541
    protected get baseTop(): number {
542
        return this.element.nativeElement.getBoundingClientRect().top;
320✔
543
    }
544

545
    protected get baseOriginLeft(): number {
546
        return this.baseLeft - this.getTransformX(this.element.nativeElement);
5✔
547
    }
548

549
    protected get baseOriginTop(): number {
550
        return this.baseTop - this.getTransformY(this.element.nativeElement);
4✔
551
    }
552

553
    protected set ghostLeft(pageX: number) {
554
        if (this.ghostElement) {
316✔
555
            // We need to take into account marginLeft, since top style does not include margin, but pageX includes the margin.
556
            const ghostMarginLeft = parseInt(this.document.defaultView.getComputedStyle(this.ghostElement)['margin-left'], 10);
315✔
557
            // If ghost host is defined it needs to be taken into account.
558
            this.ghostElement.style.left = (pageX - ghostMarginLeft - this._ghostHostX) + 'px';
315✔
559
        }
560
    }
561

562
    protected get ghostLeft() {
563
        if (this.ghostElement) {
470✔
564
            return parseInt(this.ghostElement.style.left, 10) + this._ghostHostX;
469✔
565
        }
566
    }
567

568
    protected set ghostTop(pageY: number) {
569
        if (this.ghostElement) {
316✔
570
            // We need to take into account marginTop, since top style does not include margin, but pageY includes the margin.
571
            const ghostMarginTop = parseInt(this.document.defaultView.getComputedStyle(this.ghostElement)['margin-top'], 10);
315✔
572
            // If ghost host is defined it needs to be taken into account.
573
            this.ghostElement.style.top = (pageY - ghostMarginTop - this._ghostHostY) + 'px';
315✔
574
        }
575
    }
576

577
    protected get ghostTop() {
578
        if (this.ghostElement) {
496✔
579
            return parseInt(this.ghostElement.style.top, 10) + this._ghostHostY;
495✔
580
        }
581
    }
582

583
    protected get windowScrollTop() {
584
        return this.document.documentElement.scrollTop || window.scrollY;
1,168✔
585
    }
586

587
    protected get windowScrollLeft() {
588
        return this.document.documentElement.scrollLeft || window.scrollX;
1,168✔
589
    }
590

591
    protected get windowScrollHeight() {
592
        return this.document.documentElement.scrollHeight;
31✔
593
    }
594

595
    protected get windowScrollWidth() {
596
        return this.document.documentElement.scrollWidth;
31✔
597
    }
598

599
    /**
600
     * @hidden
601
     */
602
    public defaultReturnDuration = '0.5s';
35,319✔
603

604
    /**
605
     * @hidden
606
     */
607
    public ghostElement;
608

609
    /**
610
     * @hidden
611
     */
612
    public animInProgress = false;
35,319✔
613

614
    protected ghostContext: any = null;
35,319✔
615
    protected _startX = 0;
35,319✔
616
    protected _startY = 0;
35,319✔
617
    protected _lastX = 0;
35,319✔
618
    protected _lastY = 0;
35,319✔
619
    protected _dragStarted = false;
35,319✔
620

621
    /** Drag ghost related properties */
622
    protected _defaultOffsetX;
623
    protected _defaultOffsetY;
624
    protected _offsetX;
625
    protected _offsetY;
626
    protected _ghostStartX;
627
    protected _ghostStartY;
628
    protected _ghostHostX = 0;
35,319✔
629
    protected _ghostHostY = 0;
35,319✔
630
    protected _dynamicGhostRef: EmbeddedViewRef<any>;
631

632
    protected _pointerDownId = null;
35,319✔
633
    protected _clicked = false;
35,319✔
634
    protected _lastDropArea = null;
35,319✔
635

636
    protected _destroy = new Subject<boolean>();
35,319✔
637
    protected _removeOnDestroy = true;
35,319✔
638
    protected _data: any;
639
    protected _scrollContainer = null;
35,319✔
640
    protected _originalScrollContainerWidth = 0;
35,319✔
641
    protected _originalScrollContainerHeight = 0;
35,319✔
642
    protected _scrollContainerStep = 5;
35,319✔
643
    protected _scrollContainerStepMs = 10;
35,319✔
644
    protected _scrollContainerThreshold = 25;
35,319✔
645
    protected _containerScrollIntervalId = null;
35,319✔
646
    private document = inject(DOCUMENT);
35,319✔
647

648
    /**
649
     * Sets the offset of the dragged element relative to the mouse in pixels.
650
     * By default it's taking the relative position to the mouse when the drag started and keeps it the same.
651
     * ```html
652
     * <div #hostDiv></div>
653
     * <div igxDrag [ghostOffsetX]="0">
654
     *         <span>Drag Me!</span>
655
     * </div>
656
     * ```
657
     *
658
     * @memberof IgxDragDirective
659
     */
660
    @Input()
661
    public set ghostOffsetX(value) {
662
        this._offsetX = parseInt(value, 10);
4✔
663
    }
664

665
    public get ghostOffsetX() {
666
        return this._offsetX !== undefined ? this._offsetX : this._defaultOffsetX;
142✔
667
    }
668

669
    /**
670
     * Sets the offset of the dragged element relative to the mouse in pixels.
671
     * By default it's taking the relative position to the mouse when the drag started and keeps it the same.
672
     * ```html
673
     * <div #hostDiv></div>
674
     * <div igxDrag [ghostOffsetY]="0">
675
     *         <span>Drag Me!</span>
676
     * </div>
677
     * ```
678
     *
679
     * @memberof IgxDragDirective
680
     */
681
    @Input()
682
    public set ghostOffsetY(value) {
683
        this._offsetY = parseInt(value, 10);
4✔
684
    }
685

686
    public get ghostOffsetY() {
687
        return this._offsetY !== undefined ? this._offsetY : this._defaultOffsetY;
142✔
688
    }
689

690
    constructor(
691
        public cdr: ChangeDetectorRef,
35,319✔
692
        public element: ElementRef,
35,319✔
693
        public viewContainer: ViewContainerRef,
35,319✔
694
        public zone: NgZone,
35,319✔
695
        public renderer: Renderer2,
35,319✔
696
        protected platformUtil: PlatformUtil
35,319✔
697
    ) {
698
    }
699

700
    /**
701
     * @hidden
702
     */
703
    public ngAfterContentInit() {
704
        if (!this.dragHandles || !this.dragHandles.length) {
35,319✔
705
            // Set user select none to the whole draggable element if no drag handles are defined.
706
            this.selectDisabled = true;
34,849✔
707
        }
708

709
        // Bind events
710
        this.zone.runOutsideAngular(() => {
35,319✔
711
            if (!this.platformUtil.isBrowser) {
35,319!
712
                return;
×
713
            }
714
            const targetElements = this.dragHandles && this.dragHandles.length
35,319✔
715
                ? this.dragHandles
716
                    .filter(item => item.parentDragElement === null)
484✔
717
                    .map(item => {
718
                        item.parentDragElement = this.element.nativeElement;
470✔
719
                        return item.element.nativeElement;
470✔
720
                    })
721
                : [this.element.nativeElement];
722
            targetElements.forEach((element) => {
35,319✔
723
                if (this.pointerEventsEnabled) {
35,319!
724
                    fromEvent(element, 'pointerdown').pipe(takeUntil(this._destroy))
35,319✔
725
                        .subscribe((res) => this.onPointerDown(res));
157✔
726

727
                    fromEvent(element, 'pointermove').pipe(
35,319✔
728
                        throttle(() => interval(0, animationFrameScheduler)),
171✔
729
                        takeUntil(this._destroy)
730
                    ).subscribe((res) => this.onPointerMove(res));
171✔
731

732
                    fromEvent(element, 'pointerup').pipe(takeUntil(this._destroy))
35,319✔
733
                        .subscribe((res) => this.onPointerUp(res));
230✔
734

735
                    if (!this.ghost) {
35,319✔
736
                        // Do not bind `lostpointercapture` to the target, because we will bind it on the ghost later.
737
                        fromEvent(element, 'lostpointercapture').pipe(takeUntil(this._destroy))
130✔
738
                            .subscribe((res) => this.onPointerLost(res));
×
739
                    }
740
                } else if (this.touchEventsEnabled) {
×
741
                    fromEvent(element, 'touchstart').pipe(takeUntil(this._destroy))
×
742
                        .subscribe((res) => this.onPointerDown(res));
×
743
                } else {
744
                    // We don't have pointer events and touch events. Use then mouse events.
745
                    fromEvent(element, 'mousedown').pipe(takeUntil(this._destroy))
×
746
                        .subscribe((res) => this.onPointerDown(res));
×
747
                }
748
            });
749

750
            // We should bind to document events only once when there are no pointer events.
751
            if (!this.pointerEventsEnabled && this.touchEventsEnabled) {
35,319!
NEW
752
                fromEvent(this.document.defaultView, 'touchmove').pipe(
×
753
                    throttle(() => interval(0, animationFrameScheduler)),
×
754
                    takeUntil(this._destroy)
755
                ).subscribe((res) => this.onPointerMove(res));
×
756

NEW
757
                fromEvent(this.document.defaultView, 'touchend').pipe(takeUntil(this._destroy))
×
758
                    .subscribe((res) => this.onPointerUp(res));
×
759
            } else if (!this.pointerEventsEnabled) {
35,319!
NEW
760
                fromEvent(this.document.defaultView, 'mousemove').pipe(
×
761
                    throttle(() => interval(0, animationFrameScheduler)),
×
762
                    takeUntil(this._destroy)
763
                ).subscribe((res) => this.onPointerMove(res));
×
764

NEW
765
                fromEvent(this.document.defaultView, 'mouseup').pipe(takeUntil(this._destroy))
×
766
                    .subscribe((res) => this.onPointerUp(res));
×
767
            }
768
            this.element.nativeElement.addEventListener('transitionend', this.onTransitionEnd.bind(this));
35,319✔
769
        });
770

771
        // Set transition duration to 0s. This also helps with setting `visibility: hidden` to the base to not lag.
772
        this.element.nativeElement.style.transitionDuration = '0.0s';
35,319✔
773
    }
774

775
    /**
776
     * @hidden
777
     */
778
    public ngOnDestroy() {
779
        this._destroy.next(true);
35,200✔
780
        this._destroy.complete();
35,200✔
781

782
        if (this.ghost && this.ghostElement && this._removeOnDestroy) {
35,200✔
783
            this.ghostElement.parentNode.removeChild(this.ghostElement);
11✔
784
            this.ghostElement = null;
11✔
785

786
            if (this._dynamicGhostRef) {
11!
787
                this._dynamicGhostRef.destroy();
×
788
                this._dynamicGhostRef = null;
×
789
            }
790
        }
791

792
        this.element.nativeElement.removeEventListener('transitionend', this.onTransitionEnd);
35,200✔
793

794
        if (this._containerScrollIntervalId) {
35,200✔
795
            clearInterval(this._containerScrollIntervalId);
1✔
796
            this._containerScrollIntervalId = null;
1✔
797
        }
798
    }
799

800
    /**
801
     * Sets desired location of the base element or ghost element if rended relative to the document.
802
     *
803
     * @param newLocation New location that should be applied. It is advised to get new location using getBoundingClientRects() + scroll.
804
     */
805
    public setLocation(newLocation: IgxDragLocation) {
806
        // We do not subtract marginLeft and marginTop here because here we calculate deltas.
807
        if (this.ghost && this.ghostElement) {
12✔
808
            this.ghostLeft = newLocation.pageX + this.windowScrollLeft;
8✔
809
            this.ghostTop = newLocation.pageY + this.windowScrollTop;
8✔
810
        } else if (!this.ghost) {
4✔
811
            const deltaX = newLocation.pageX - this.pageX;
4✔
812
            const deltaY = newLocation.pageY - this.pageY;
4✔
813
            const transformX = this.getTransformX(this.element.nativeElement);
4✔
814
            const transformY = this.getTransformY(this.element.nativeElement);
4✔
815
            this.setTransformXY(transformX + deltaX, transformY + deltaY);
4✔
816
        }
817

818
        this._startX = this.baseLeft;
12✔
819
        this._startY = this.baseTop;
12✔
820
    }
821

822
    /**
823
     * Animates the base or ghost element depending on the `ghost` input to its initial location.
824
     * If `ghost` is true but there is not ghost rendered, it will be created and animated.
825
     * If the base element has changed its DOM position its initial location will be changed accordingly.
826
     *
827
     * @param customAnimArgs Custom transition properties that will be applied when performing the transition.
828
     * @param startLocation Start location from where the transition should start.
829
     */
830
    public transitionToOrigin(customAnimArgs?: IDragCustomTransitionArgs, startLocation?: IgxDragLocation) {
831
        if ((!!startLocation && startLocation.pageX === this.baseOriginLeft && startLocation.pageY === this.baseOriginLeft) ||
4!
832
            (!startLocation && this.ghost && !this.ghostElement)) {
833
            return;
1✔
834
        }
835

836
        if (!!startLocation && startLocation.pageX !== this.pageX && startLocation.pageY !== this.pageY) {
3✔
837
            if (this.ghost && !this.ghostElement) {
1✔
838
                this._startX = startLocation.pageX;
1✔
839
                this._startY = startLocation.pageY;
1✔
840
                this._ghostStartX = this._startX;
1✔
841
                this._ghostStartY = this._startY;
1✔
842
                this.createGhost(this._startX, this._startY);
1✔
843
            }
844

845
            this.setLocation(startLocation);
1✔
846
        }
847

848
        this.animInProgress = true;
3✔
849
        // Use setTimeout because we need to be sure that the element is positioned first correctly if there is start location.
850
        setTimeout(() => {
3✔
851
            if (this.ghost) {
3✔
852
                this.ghostElement.style.transitionProperty = 'top, left';
2✔
853
                this.ghostElement.style.transitionDuration =
2✔
854
                    customAnimArgs && customAnimArgs.duration ? customAnimArgs.duration + 's' : this.defaultReturnDuration;
5!
855
                this.ghostElement.style.transitionTimingFunction =
2✔
856
                    customAnimArgs && customAnimArgs.timingFunction ? customAnimArgs.timingFunction : '';
5!
857
                this.ghostElement.style.transitionDelay = customAnimArgs && customAnimArgs.delay ? customAnimArgs.delay + 's' : '';
2!
858
                this.setLocation(new IgxDragLocation(this.baseLeft, this.baseTop));
2✔
859
            } else if (!this.ghost) {
1✔
860
                this.element.nativeElement.style.transitionProperty = 'transform';
1✔
861
                this.element.nativeElement.style.transitionDuration =
1✔
862
                    customAnimArgs && customAnimArgs.duration ? customAnimArgs.duration + 's' : this.defaultReturnDuration;
2!
863
                this.element.nativeElement.style.transitionTimingFunction =
1✔
864
                    customAnimArgs && customAnimArgs.timingFunction ? customAnimArgs.timingFunction : '';
2!
865
                this.element.nativeElement.style.transitionDelay = customAnimArgs && customAnimArgs.delay ? customAnimArgs.delay + 's' : '';
1!
866
                this._startX = this.baseLeft;
1✔
867
                this._startY = this.baseTop;
1✔
868
                this.setTransformXY(0, 0);
1✔
869
            }
870
        }, 0);
871
    }
872

873
    /**
874
     * Animates the base or ghost element to a specific target location or other element using transition.
875
     * If `ghost` is true but there is not ghost rendered, it will be created and animated.
876
     * It is recommended to use 'getBoundingClientRects() + pageScroll' when determining desired location.
877
     *
878
     * @param target Target that the base or ghost will transition to. It can be either location in the page or another HTML element.
879
     * @param customAnimArgs Custom transition properties that will be applied when performing the transition.
880
     * @param startLocation Start location from where the transition should start.
881
     */
882
    public transitionTo(target: IgxDragLocation | ElementRef, customAnimArgs?: IDragCustomTransitionArgs, startLocation?: IgxDragLocation) {
883
        if (!!startLocation && this.ghost && !this.ghostElement) {
5✔
884
            this._startX = startLocation.pageX;
1✔
885
            this._startY = startLocation.pageY;
1✔
886
            this._ghostStartX = this._startX;
1✔
887
            this._ghostStartY = this._startY;
1✔
888
        } else if (!!startLocation && (!this.ghost || this.ghostElement)) {
4✔
889
            this.setLocation(startLocation);
2✔
890
        } else if (this.ghost && !this.ghostElement) {
2!
891
            this._startX = this.baseLeft;
×
892
            this._startY = this.baseTop;
×
893
            this._ghostStartX = this._startX + this.windowScrollLeft;
×
894
            this._ghostStartY = this._startY + this.windowScrollTop;
×
895
        }
896

897
        if (this.ghost && !this.ghostElement) {
5✔
898
            this.createGhost(this._startX, this._startY);
1✔
899
        }
900

901
        this.animInProgress = true;
5✔
902
        // Use setTimeout because we need to be sure that the element is positioned first correctly if there is start location.
903
        setTimeout(() => {
5✔
904
            const movedElem = this.ghost ? this.ghostElement : this.element.nativeElement;
5✔
905
            movedElem.style.transitionProperty = this.ghost && this.ghostElement ? 'left, top' : 'transform';
5✔
906
            movedElem.style.transitionDuration =
5✔
907
                customAnimArgs && customAnimArgs.duration ? customAnimArgs.duration + 's' : this.defaultReturnDuration;
13!
908
            movedElem.style.transitionTimingFunction =
5✔
909
                customAnimArgs && customAnimArgs.timingFunction ? customAnimArgs.timingFunction : '';
13!
910
            movedElem.style.transitionDelay = customAnimArgs && customAnimArgs.delay ? customAnimArgs.delay + 's' : '';
5!
911

912
            if (target instanceof IgxDragLocation) {
5!
913
                this.setLocation(new IgxDragLocation(target.pageX, target.pageY));
5✔
914
            } else {
915
                const targetRects = target.nativeElement.getBoundingClientRect();
×
916
                this.setLocation(new IgxDragLocation(
×
917
                    targetRects.left - this.windowScrollLeft,
918
                    targetRects.top - this.windowScrollTop
919
                ));
920
            }
921
        }, 0);
922
    }
923

924
    /**
925
     * @hidden
926
     * Method bound to the PointerDown event of the base element igxDrag is initialized.
927
     * @param event PointerDown event captured
928
     */
929
    public onPointerDown(event) {
930
        const ignoredElement = this.dragIgnoredElems.find(elem => elem.element.nativeElement === event.target);
142✔
931
        if (ignoredElement) {
142!
932
            return;
×
933
        }
934

935
        // Set pointer capture so we detect pointermove even if mouse is out of bounds until ghostElement is created.
936
        const handleFound = this.dragHandles.find(handle => handle.element.nativeElement === event.target);
142✔
937
        const targetElement = handleFound ? handleFound.element.nativeElement : event.target || this.element.nativeElement;
142✔
938
        if (this.pointerEventsEnabled && targetElement.isConnected) {
142!
939
            this._pointerDownId = event.pointerId;
142✔
940
            targetElement.setPointerCapture(this._pointerDownId);
142✔
941
        } else if (targetElement.isConnected) {
×
942
            targetElement.focus();
×
943
            event.preventDefault();
×
944
        } else {
945
            return;
×
946
        }
947

948
        this._clicked = true;
142✔
949
        if (this.pointerEventsEnabled || !this.touchEventsEnabled) {
142!
950
            // Check first for pointer events or non touch, because we can have pointer events and touch events at once.
951
            this._startX = event.pageX;
142✔
952
            this._startY = event.pageY;
142✔
953
        } else if (this.touchEventsEnabled) {
×
954
            this._startX = event.touches[0].pageX;
×
955
            this._startY = event.touches[0].pageY;
×
956
        }
957

958
        this._defaultOffsetX = this.baseLeft - this._startX + this.windowScrollLeft;
142✔
959
        this._defaultOffsetY = this.baseTop - this._startY + this.windowScrollTop;
142✔
960
        this._ghostStartX = this._startX + this.ghostOffsetX;
142✔
961
        this._ghostStartY = this._startY + this.ghostOffsetY;
142✔
962
        this._lastX = this._startX;
142✔
963
        this._lastY = this._startY;
142✔
964
    }
965

966
    /**
967
     * @hidden
968
     * Perform drag move logic when dragging and dispatching events if there is igxDrop under the pointer.
969
     * This method is bound at first at the base element.
970
     * If dragging starts and after the ghostElement is rendered the pointerId is reassigned it. Then this method is bound to it.
971
     * @param event PointerMove event captured
972
     */
973
    public onPointerMove(event) {
974
        if (this._clicked) {
269✔
975
            let pageX; let pageY;
976
            if (this.pointerEventsEnabled || !this.touchEventsEnabled) {
268!
977
                // Check first for pointer events or non touch, because we can have pointer events and touch events at once.
978
                pageX = event.pageX;
268✔
979
                pageY = event.pageY;
268✔
980
            } else if (this.touchEventsEnabled) {
×
981
                pageX = event.touches[0].pageX;
×
982
                pageY = event.touches[0].pageY;
×
983

984
                // Prevent scrolling on touch while dragging
985
                event.preventDefault();
×
986
            }
987

988
            const totalMovedX = pageX - this._startX;
268✔
989
            const totalMovedY = pageY - this._startY;
268✔
990
            if (!this._dragStarted &&
268✔
991
                (Math.abs(totalMovedX) > this.dragTolerance || Math.abs(totalMovedY) > this.dragTolerance)) {
992
                const dragStartArgs: IDragStartEventArgs = {
134✔
993
                    originalEvent: event,
994
                    owner: this,
995
                    startX: pageX - totalMovedX,
996
                    startY: pageY - totalMovedY,
997
                    pageX,
998
                    pageY,
999
                    cancel: false
1000
                };
1001
                this.zone.run(() => {
134✔
1002
                    this.dragStart.emit(dragStartArgs);
134✔
1003
                });
1004

1005
                if (!dragStartArgs.cancel) {
134✔
1006
                    this._dragStarted = true;
132✔
1007
                    if (this.ghost) {
132✔
1008
                        // We moved enough so ghostElement can be rendered and actual dragging to start.
1009
                        // When creating it will take into account any offset set by the user by default.
1010
                        this.createGhost(pageX, pageY);
123✔
1011
                    } else if (this._offsetX !== undefined || this._offsetY !== undefined) {
9✔
1012
                        // There is no need for ghost, but we will need to position initially the base element to reflect any offset.
1013
                        const transformX = (this._offsetX !== undefined ? this._offsetX - this._defaultOffsetX : 0) +
1!
1014
                            this.getTransformX(this.element.nativeElement);
1015
                        const transformY = (this._offsetY !== undefined ? this._offsetY - this._defaultOffsetY : 0) +
1!
1016
                            this.getTransformY(this.element.nativeElement);
1017
                        this.setTransformXY(transformX, transformY);
1✔
1018
                    }
1019
                } else {
1020
                    return;
2✔
1021
                }
1022
            } else if (!this._dragStarted) {
134✔
1023
                return;
3✔
1024
            }
1025

1026
            const moveArgs: IDragMoveEventArgs = {
263✔
1027
                originalEvent: event,
1028
                owner: this,
1029
                startX: this._startX,
1030
                startY: this._startY,
1031
                pageX: this._lastX,
1032
                pageY: this._lastY,
1033
                nextPageX: pageX,
1034
                nextPageY: pageY,
1035
                cancel: false
1036
            };
1037
            this.dragMove.emit(moveArgs);
263✔
1038

1039
            const setPageX = moveArgs.nextPageX;
263✔
1040
            const setPageY = moveArgs.nextPageY;
263✔
1041
            if (!moveArgs.cancel) {
263✔
1042
                // Scroll root container if the user reaches its boundaries.
1043
                this.onScrollContainer();
263✔
1044

1045
                // Move the actual element around
1046
                if (this.ghost) {
263✔
1047
                    const updatedTotalMovedX = this.dragDirection === DragDirection.VERTICAL ? 0 : setPageX - this._startX;
245✔
1048
                    const updatedTotalMovedY = this.dragDirection === DragDirection.HORIZONTAL ? 0 : setPageY - this._startY;
245✔
1049
                    this.ghostLeft = this._ghostStartX + updatedTotalMovedX;
245✔
1050
                    this.ghostTop = this._ghostStartY + updatedTotalMovedY;
245✔
1051
                } else {
1052
                    const lastMovedX = this.dragDirection === DragDirection.VERTICAL ? 0 : setPageX - this._lastX;
18✔
1053
                    const lastMovedY = this.dragDirection === DragDirection.HORIZONTAL ? 0 : setPageY - this._lastY;
18✔
1054
                    const translateX = this.getTransformX(this.element.nativeElement) + lastMovedX;
18✔
1055
                    const translateY = this.getTransformY(this.element.nativeElement) + lastMovedY;
18✔
1056
                    this.setTransformXY(translateX, translateY);
18✔
1057
                }
1058
                this.dispatchDragEvents(pageX, pageY, event);
263✔
1059
            }
1060

1061
            this._lastX = setPageX;
263✔
1062
            this._lastY = setPageY;
263✔
1063
        }
1064
    }
1065

1066
    /**
1067
     * @hidden
1068
     * Perform drag end logic when releasing the ghostElement and dispatching drop event if igxDrop is under the pointer.
1069
     * This method is bound at first at the base element.
1070
     * If dragging starts and after the ghostElement is rendered the pointerId is reassigned to it. Then this method is bound to it.
1071
     * @param event PointerUp event captured
1072
     */
1073
    public onPointerUp(event) {
1074
        if (!this._clicked) {
292✔
1075
            return;
159✔
1076
        }
1077

1078
        let pageX; let pageY;
1079
        if (this.pointerEventsEnabled || !this.touchEventsEnabled) {
133!
1080
            // Check first for pointer events or non touch, because we can have pointer events and touch events at once.
1081
            pageX = event.pageX;
133✔
1082
            pageY = event.pageY;
133✔
1083
        } else if (this.touchEventsEnabled) {
×
1084
            pageX = event.touches[0].pageX;
×
1085
            pageY = event.touches[0].pageY;
×
1086

1087
            // Prevent scrolling on touch while dragging
1088
            event.preventDefault();
×
1089
        }
1090

1091
        const eventArgs: IDragBaseEventArgs = {
133✔
1092
            originalEvent: event,
1093
            owner: this,
1094
            startX: this._startX,
1095
            startY: this._startY,
1096
            pageX,
1097
            pageY
1098
        };
1099
        this._pointerDownId = null;
133✔
1100
        this._clicked = false;
133✔
1101
        if (this._dragStarted) {
133✔
1102
            if (this._lastDropArea && this._lastDropArea !== this.element.nativeElement) {
125✔
1103
                this.dispatchDropEvent(event.pageX, event.pageY, event);
87✔
1104
            }
1105

1106
            this.zone.run(() => {
125✔
1107
                this.dragEnd.emit(eventArgs);
125✔
1108
            });
1109

1110
            if (!this.animInProgress) {
125✔
1111
                this.onTransitionEnd(null);
121✔
1112
            }
1113
        } else {
1114
            // Trigger our own click event because when there is no ghost, native click cannot be prevented when dragging.
1115
            this.zone.run(() => {
8✔
1116
                this.dragClick.emit(eventArgs);
8✔
1117
            });
1118
        }
1119

1120
        if (this._containerScrollIntervalId) {
133✔
1121
            clearInterval(this._containerScrollIntervalId);
30✔
1122
            this._containerScrollIntervalId = null;
30✔
1123
        }
1124
    }
1125

1126
    /**
1127
     * @hidden
1128
     * Execute this method whe the pointer capture has been lost.
1129
     * This means that during dragging the user has performed other action like right clicking and then clicking somewhere else.
1130
     * This method will ensure that the drag state is being reset in this case as if the user released the dragged element.
1131
     * @param event Event captured
1132
     */
1133
    public onPointerLost(event) {
1134
        if (!this._clicked) {
1!
1135
            return;
×
1136
        }
1137

1138
        // When the base element is moved to previous index, angular reattaches the ghost template as a sibling by default.
1139
        // This is the defaut place for the EmbededViewRef when recreated.
1140
        // That's why we need to move it to the proper place and set pointer capture again.
1141
        if (this._pointerDownId && this.ghostElement && this._dynamicGhostRef && !this._dynamicGhostRef.destroyed) {
1!
1142
            let ghostReattached = false;
×
1143
            if (this.ghostHost && !Array.from(this.ghostHost.children).includes(this.ghostElement)) {
×
1144
                ghostReattached = true;
×
1145
                this.ghostHost.appendChild(this.ghostElement);
×
NEW
1146
            } else if (!this.ghostHost && !Array.from(this.document.body.children).includes(this.ghostElement)) {
×
1147
                ghostReattached = true;
×
NEW
1148
                this.document.body.appendChild(this.ghostElement);
×
1149
            }
1150

1151
            if (ghostReattached) {
×
1152
                this.ghostElement.setPointerCapture(this._pointerDownId);
×
1153
                return;
×
1154
            }
1155
        }
1156

1157
        const eventArgs = {
1✔
1158
            originalEvent: event,
1159
            owner: this,
1160
            startX: this._startX,
1161
            startY: this._startY,
1162
            pageX: event.pageX,
1163
            pageY: event.pageY
1164
        };
1165
        this._pointerDownId = null;
1✔
1166
        this._clicked = false;
1✔
1167
        if (this._dragStarted) {
1✔
1168
            this.zone.run(() => {
1✔
1169
                this.dragEnd.emit(eventArgs);
1✔
1170
            });
1171
            if (!this.animInProgress) {
1✔
1172
                this.onTransitionEnd(null);
1✔
1173
            }
1174
        }
1175
    }
1176

1177
    /**
1178
     * @hidden
1179
     */
1180
    public onTransitionEnd(event) {
1181
        if ((!this._dragStarted && !this.animInProgress) || this._clicked) {
172✔
1182
            // Return if no dragging started and there is no animation in progress.
1183
            return;
50✔
1184
        }
1185

1186
        if (this.ghost && this.ghostElement) {
122✔
1187
            this._ghostStartX = this.baseLeft + this.windowScrollLeft;
114✔
1188
            this._ghostStartY = this.baseTop + this.windowScrollTop;
114✔
1189

1190
            const ghostDestroyArgs: IDragGhostBaseEventArgs = {
114✔
1191
                owner: this,
1192
                ghostElement: this.ghostElement,
1193
                cancel: false
1194
            };
1195
            this.ghostDestroy.emit(ghostDestroyArgs);
114✔
1196
            if (ghostDestroyArgs.cancel) {
114!
1197
                return;
×
1198
            }
1199
            this.clearGhost();
114✔
1200
        } else if (!this.ghost) {
8✔
1201
            this.element.nativeElement.style.transitionProperty = '';
8✔
1202
            this.element.nativeElement.style.transitionDuration = '0.0s';
8✔
1203
            this.element.nativeElement.style.transitionTimingFunction = '';
8✔
1204
            this.element.nativeElement.style.transitionDelay = '';
8✔
1205
        }
1206
        this.animInProgress = false;
122✔
1207
        this._dragStarted = false;
122✔
1208

1209
        // Execute transitioned after everything is reset so if the user sets new location on the base now it would work as expected.
1210
        this.zone.run(() => {
122✔
1211
            this.transitioned.emit({
122✔
1212
                originalEvent: event,
1213
                owner: this,
1214
                startX: this._startX,
1215
                startY: this._startY,
1216
                pageX: this._startX,
1217
                pageY: this._startY
1218
            });
1219
        });
1220
    }
1221

1222
    protected clearGhost() {
1223
        this.ghostElement.removeEventListener('pointermove', this.onPointerMove);
114✔
1224
        this.ghostElement.removeEventListener('pointerup', this.onPointerUp);
114✔
1225
        this.ghostElement.removeEventListener('lostpointercapture', this.onPointerLost);
114✔
1226
        this.ghostElement.removeEventListener('transitionend', this.onTransitionEnd);
114✔
1227
        this.ghostElement.remove();
114✔
1228
        this.ghostElement = null;
114✔
1229

1230
        if (this._dynamicGhostRef) {
114✔
1231
            this._dynamicGhostRef.destroy();
9✔
1232
            this._dynamicGhostRef = null;
9✔
1233
        }
1234
    }
1235

1236
    /**
1237
     * @hidden
1238
     * Create ghost element - if a Node object is provided it creates a clone of that node,
1239
     * otherwise it clones the host element.
1240
     * Bind all needed events.
1241
     * @param pageX Latest pointer position on the X axis relative to the page.
1242
     * @param pageY Latest pointer position on the Y axis relative to the page.
1243
     * @param node The Node object to be cloned.
1244
     */
1245
    protected createGhost(pageX, pageY, node: any = null) {
96✔
1246
        if (!this.ghost) {
126!
1247
            return;
×
1248
        }
1249

1250
        if (this.ghostTemplate) {
126✔
1251
            this.zone.run(() => {
9✔
1252
                // Create template in zone, so it gets updated by it automatically.
1253
                this._dynamicGhostRef = this.viewContainer.createEmbeddedView(this.ghostTemplate, this.ghostContext);
9✔
1254
            });
1255
            if (this._dynamicGhostRef.rootNodes[0].style.display === 'contents') {
9✔
1256
                // Change the display to default since display contents does not position the element absolutely.
1257
                this._dynamicGhostRef.rootNodes[0].style.display = 'block';
1✔
1258
            }
1259
            this.ghostElement = this._dynamicGhostRef.rootNodes[0];
9✔
1260
        } else {
1261
            this.ghostElement = node ? node.cloneNode(true) : this.element.nativeElement.cloneNode(true);
117✔
1262
        }
1263

1264
        const totalMovedX = pageX - this._startX;
126✔
1265
        const totalMovedY = pageY - this._startY;
126✔
1266
        this._ghostHostX = this.getGhostHostBaseOffsetX();
126✔
1267
        this._ghostHostY = this.getGhostHostBaseOffsetY();
126✔
1268

1269
        this.ghostElement.style.transitionDuration = '0.0s';
126✔
1270
        this.ghostElement.style.position = 'absolute';
126✔
1271

1272
        if (this.ghostClass) {
126✔
1273
            this.ghostElement.classList.add(this.ghostClass);
65✔
1274
        }
1275

1276
        if (this.ghostStyle) {
126✔
1277
            Object.entries(this.ghostStyle).map(([name, value]) => {
126✔
1278
                this.renderer.setStyle(this.ghostElement, name, value, RendererStyleFlags2.DashCase);
8✔
1279
            });
1280
        }
1281

1282
        const createEventArgs = {
126✔
1283
            owner: this,
1284
            ghostElement: this.ghostElement,
1285
            cancel: false
1286
        };
1287
        this.ghostCreate.emit(createEventArgs);
126✔
1288
        if (createEventArgs.cancel) {
126!
1289
            this.ghostElement = null;
×
1290
            if (this.ghostTemplate && this._dynamicGhostRef) {
×
1291
                this._dynamicGhostRef.destroy();
×
1292
            }
1293
            return;
×
1294
        }
1295

1296
        if (this.ghostHost) {
126✔
1297
            this.ghostHost.appendChild(this.ghostElement);
59✔
1298
        } else {
1299
            this.document.body.appendChild(this.ghostElement);
67✔
1300
        }
1301

1302
        const ghostMarginLeft = parseInt(this.document.defaultView.getComputedStyle(this.ghostElement)['margin-left'], 10);
126✔
1303
        const ghostMarginTop = parseInt(this.document.defaultView.getComputedStyle(this.ghostElement)['margin-top'], 10);
126✔
1304
        this.ghostElement.style.left = (this._ghostStartX - ghostMarginLeft + totalMovedX - this._ghostHostX) + 'px';
126✔
1305
        this.ghostElement.style.top = (this._ghostStartY - ghostMarginTop + totalMovedY - this._ghostHostY) + 'px';
126✔
1306

1307
        if (this.pointerEventsEnabled) {
126✔
1308
            // The ghostElement takes control for moving and dragging after it has been rendered.
1309
            if (this._pointerDownId !== null) {
126✔
1310
                this.ghostElement.setPointerCapture(this._pointerDownId);
124✔
1311
            }
1312
            this.ghostElement.addEventListener('pointermove', this.onPointerMove.bind(this));
126✔
1313
            this.ghostElement.addEventListener('pointerup', this.onPointerUp.bind(this));
126✔
1314
            this.ghostElement.addEventListener('lostpointercapture', this.onPointerLost.bind(this));
126✔
1315
        }
1316

1317
        // Transition animation when the ghostElement is released and it returns to it's original position.
1318
        this.ghostElement.addEventListener('transitionend', this.onTransitionEnd.bind(this));
126✔
1319

1320
        this.cdr.detectChanges();
126✔
1321
    }
1322

1323
    /**
1324
     * @hidden
1325
     * Dispatch custom igxDragEnter/igxDragLeave events based on current pointer position and if drop area is under.
1326
     */
1327
    protected dispatchDragEvents(pageX: number, pageY: number, originalEvent) {
1328
        let topDropArea;
1329
        const customEventArgs: IgxDragCustomEventDetails = {
263✔
1330
            startX: this._startX,
1331
            startY: this._startY,
1332
            pageX,
1333
            pageY,
1334
            owner: this,
1335
            originalEvent
1336
        };
1337

1338
        const elementsFromPoint = this.getElementsAtPoint(pageX, pageY);
263✔
1339
        let targetElements = [];
263✔
1340
        // Check for shadowRoot instance and use it if present
1341
        for (const elFromPoint of elementsFromPoint) {
263✔
1342
            if (elFromPoint?.shadowRoot) {
2,645!
1343
                targetElements = targetElements.concat(this.getFromShadowRoot(elFromPoint, pageX, pageY, elementsFromPoint));
×
1344
            } else if (targetElements.indexOf(elFromPoint) === -1) {
2,645✔
1345
                targetElements.push(elFromPoint);
2,645✔
1346
            }
1347
        }
1348

1349
        for (const element of targetElements) {
263✔
1350
            if (element.getAttribute('droppable') === 'true' &&
1,894✔
1351
                element !== this.ghostElement && element !== this.element.nativeElement) {
1352
                topDropArea = element;
108✔
1353
                break;
108✔
1354
            }
1355
        }
1356

1357
        if (topDropArea &&
263✔
1358
            (!this._lastDropArea || (this._lastDropArea && this._lastDropArea !== topDropArea))) {
1359
            if (this._lastDropArea) {
103✔
1360
                this.dispatchEvent(this._lastDropArea, 'igxDragLeave', customEventArgs);
9✔
1361
            }
1362

1363
            this._lastDropArea = topDropArea;
103✔
1364
            this.dispatchEvent(this._lastDropArea, 'igxDragEnter', customEventArgs);
103✔
1365
        } else if (!topDropArea && this._lastDropArea) {
160✔
1366
            this.dispatchEvent(this._lastDropArea, 'igxDragLeave', customEventArgs);
4✔
1367
            this._lastDropArea = null;
4✔
1368
            return;
4✔
1369
        }
1370

1371
        if (topDropArea) {
259✔
1372
            this.dispatchEvent(topDropArea, 'igxDragOver', customEventArgs);
108✔
1373
        }
1374
    }
1375

1376
    /**
1377
     * @hidden
1378
     * Traverse shadow dom in depth.
1379
     */
1380
    protected getFromShadowRoot(elem, pageX, pageY, parentDomElems) {
1381
        const elementsFromPoint = elem.shadowRoot.elementsFromPoint(pageX, pageY);
×
1382
        const shadowElements = elementsFromPoint.filter(cur => parentDomElems.indexOf(cur) === -1);
×
1383
        let res = [];
×
1384
        for (const elFromPoint of shadowElements) {
×
1385
            if (!!elFromPoint?.shadowRoot && elFromPoint.shadowRoot !== elem.shadowRoot) {
×
1386
                res = res.concat(this.getFromShadowRoot(elFromPoint, pageX, pageY, elementsFromPoint));
×
1387
            }
1388
            res.push(elFromPoint);
×
1389
        }
1390
        return res;
×
1391
    }
1392

1393
    /**
1394
     * @hidden
1395
     * Dispatch custom igxDrop event based on current pointer position if there is last recorder drop area under the pointer.
1396
     * Last recorder drop area is updated in @dispatchDragEvents method.
1397
     */
1398
    protected dispatchDropEvent(pageX: number, pageY: number, originalEvent) {
1399
        const eventArgs: IgxDragCustomEventDetails = {
87✔
1400
            startX: this._startX,
1401
            startY: this._startY,
1402
            pageX,
1403
            pageY,
1404
            owner: this,
1405
            originalEvent
1406
        };
1407

1408
        this.dispatchEvent(this._lastDropArea, 'igxDrop', eventArgs);
87✔
1409
        this.dispatchEvent(this._lastDropArea, 'igxDragLeave', eventArgs);
87✔
1410
        this._lastDropArea = null;
87✔
1411
    }
1412

1413
    /**
1414
     * @hidden
1415
     */
1416
    protected getElementsAtPoint(pageX: number, pageY: number) {
1417
        // correct the coordinates with the current scroll position, because
1418
        // document.elementsFromPoint consider position within the current viewport
1419
        // window.pageXOffset == window.scrollX; // always true
1420
        // using window.pageXOffset for IE9 compatibility
1421
        const viewPortX = pageX - window.pageXOffset;
283✔
1422
        const viewPortY = pageY - window.pageYOffset;
283✔
1423
        if (this.document['msElementsFromPoint']) {
283!
1424
            // Edge and IE special snowflakes
NEW
1425
            const elements = this.document['msElementsFromPoint'](viewPortX, viewPortY);
×
1426
            return elements === null ? [] : elements;
×
1427
        } else {
1428
            // Other browsers like Chrome, Firefox, Opera
1429
            return this.document.elementsFromPoint(viewPortX, viewPortY);
283✔
1430
        }
1431
    }
1432

1433
    /**
1434
     * @hidden
1435
     */
1436
    protected dispatchEvent(target, eventName: string, eventArgs: IgxDragCustomEventDetails) {
1437
        // This way is IE11 compatible.
1438
        // const dragLeaveEvent = document.createEvent('CustomEvent');
1439
        // dragLeaveEvent.initCustomEvent(eventName, false, false, eventArgs);
1440
        // target.dispatchEvent(dragLeaveEvent);
1441
        // Otherwise can be used `target.dispatchEvent(new CustomEvent(eventName, eventArgs));`
1442
        target.dispatchEvent(new CustomEvent(eventName, { detail: eventArgs }));
398✔
1443
    }
1444

1445
    protected getTransformX(elem) {
1446
        let posX = 0;
28✔
1447
        if (elem.style.transform) {
28✔
1448
            const matrix = elem.style.transform;
14✔
1449
            const values = matrix ? matrix.match(/-?[\d\.]+/g) : undefined;
14!
1450
            posX = values ? Number(values[1]) : 0;
14!
1451
        }
1452

1453
        return posX;
28✔
1454
    }
1455

1456
    protected getTransformY(elem) {
1457
        let posY = 0;
27✔
1458
        if (elem.style.transform) {
27✔
1459
            const matrix = elem.style.transform;
14✔
1460
            const values = matrix ? matrix.match(/-?[\d\.]+/g) : undefined;
14!
1461
            posY = values ? Number(values[2]) : 0;
14!
1462
        }
1463

1464
        return posY;
27✔
1465
    }
1466

1467
    /** Method setting transformation to the base draggable element. */
1468
    protected setTransformXY(x: number, y: number) {
1469
        if(x === 0 && y === 0) {
24✔
1470
            this.element.nativeElement.style.transform = '';
2✔
1471
            return;
2✔
1472
        }
1473
        this.element.nativeElement.style.transform = 'translate3d(' + x + 'px, ' + y + 'px, 0px)';
22✔
1474
    }
1475

1476
    /**
1477
     * Since we are using absolute position to move the ghost, the ghost host might not have position: relative.
1478
     * Combined with position static, this means that the absolute position in the browser is relative to the offsetParent.
1479
     * The offsetParent is pretty much the closes parent that has position: relative, or if no such until it reaches the body.
1480
     * That's why if this is the case, we need to know how much we should compensate for the ghostHost being offset from
1481
     * its offsetParent.
1482
     *
1483
     * OffsetParent can be null in the case of position: fixed applied to the ghost host or display: none. In that case
1484
     * just get the clientRects of the ghost host.
1485
     */
1486
    protected getGhostHostBaseOffsetX() {
1487
        if (!this.ghostHost) return 0;
126✔
1488

1489
        const ghostPosition = this.document.defaultView.getComputedStyle(this.ghostHost).getPropertyValue('position');
59✔
1490
        if (ghostPosition === 'static' && this.ghostHost.offsetParent && this.ghostHost.offsetParent === this.document.body) {
59✔
1491
            return 0;
1✔
1492
        } else if (ghostPosition === 'static' && this.ghostHost.offsetParent) {
58!
1493
            return this.ghostHost.offsetParent.getBoundingClientRect().left + this.windowScrollLeft;
×
1494
        }
1495
        return this.ghostHost.getBoundingClientRect().left + this.windowScrollLeft;
58✔
1496
    }
1497

1498
    protected getGhostHostBaseOffsetY() {
1499
        if (!this.ghostHost) return 0;
126✔
1500

1501
        const ghostPosition = this.document.defaultView.getComputedStyle(this.ghostHost).getPropertyValue('position');
59✔
1502
        if (ghostPosition === 'static' && this.ghostHost.offsetParent && this.ghostHost.offsetParent === this.document.body) {
59✔
1503
            return 0;
1✔
1504
        } else if (ghostPosition === 'static' && this.ghostHost.offsetParent) {
58!
1505
            return this.ghostHost.offsetParent.getBoundingClientRect().top + this.windowScrollTop;
×
1506
        }
1507
        return this.ghostHost.getBoundingClientRect().top + this.windowScrollTop;
58✔
1508
    }
1509

1510
    protected getContainerScrollDirection() {
1511
        const containerBounds = this.scrollContainer ? this.scrollContainer.getBoundingClientRect() : null;
263!
1512
        const scrolledX = !this.scrollContainer ? this.windowScrollLeft > 0 : this.scrollContainer.scrollLeft > 0;
263!
1513
        const scrolledY = !this.scrollContainer ? this.windowScrollTop > 0 : this.scrollContainer.scrollTop > 0;
263!
1514
        // Take into account window scroll top because we do not use fixed positioning to the window.
1515
        const topBorder = (!this.scrollContainer ? 0 : containerBounds.top) + this.windowScrollTop + this._scrollContainerThreshold;
263!
1516
        // Subtract the element height because we position it from top left corner.
1517
        const elementHeight = this.ghost && this.ghostElement ? this.ghostElement.offsetHeight : this.element.nativeElement.offsetHeight;
263✔
1518
        const bottomBorder = (!this.scrollContainer ? window.innerHeight : containerBounds.bottom) +
263!
1519
            this.windowScrollTop - this._scrollContainerThreshold - elementHeight;
1520
        // Same for window scroll left
1521
        const leftBorder = (!this.scrollContainer ? 0 : containerBounds.left) + this.windowScrollLeft + this._scrollContainerThreshold;
263!
1522
        // Subtract the element width again because we position it from top left corner.
1523
        const elementWidth = this.ghost && this.ghostElement ? this.ghostElement.offsetWidth : this.element.nativeElement.offsetWidth;
263✔
1524
        const rightBorder = (!this.scrollContainer ? window.innerWidth : containerBounds.right) +
263!
1525
            this.windowScrollLeft - this._scrollContainerThreshold - elementWidth
1526

1527
        if (this.pageY <= topBorder && scrolledY) {
263!
1528
            return DragScrollDirection.UP;
×
1529
        } else if (this.pageY > bottomBorder) {
263✔
1530
            return DragScrollDirection.DOWN;
13✔
1531
        } else if (this.pageX < leftBorder && scrolledX) {
250!
1532
            return DragScrollDirection.LEFT;
×
1533
        } else if (this.pageX > rightBorder) {
250✔
1534
            return DragScrollDirection.RIGHT;
44✔
1535
        }
1536
        return null;
206✔
1537
    }
1538

1539
    protected onScrollContainerStep(scrollDir: DragScrollDirection) {
1540
        animationFrameScheduler.schedule(() => {
6✔
1541

1542
            let xDir = scrollDir == DragScrollDirection.LEFT ? -1 : (scrollDir == DragScrollDirection.RIGHT ? 1 : 0);
6!
1543
            let yDir = scrollDir == DragScrollDirection.UP ? -1 : (scrollDir == DragScrollDirection.DOWN ? 1 : 0);
6!
1544
            if (!this.scrollContainer) {
6!
1545
                // Cap scrolling so we don't scroll past the window max scroll position.
1546
                const maxScrollX = this._originalScrollContainerWidth - this.document.documentElement.clientWidth;
6✔
1547
                const maxScrollY = this._originalScrollContainerHeight - this.document.documentElement.clientHeight;
6✔
1548
                xDir = (this.windowScrollLeft <= 0 && xDir < 0) || (this.windowScrollLeft >= maxScrollX && xDir > 0) ? 0 : xDir;
6!
1549
                yDir = (this.windowScrollTop <= 0 && yDir < 0) || (this.windowScrollTop >= maxScrollY && yDir > 0) ? 0 : yDir;
6!
1550
            } else {
1551
                // Cap scrolling so we don't scroll past the container max scroll position.
1552
                const maxScrollX = this._originalScrollContainerWidth - this.scrollContainer.clientWidth;
×
1553
                const maxScrollY = this._originalScrollContainerHeight - this.scrollContainer.clientHeight;
×
1554
                xDir = (this.scrollContainer.scrollLeft <= 0 && xDir < 0) || (this.scrollContainer.scrollLeft >= maxScrollX && xDir > 0) ? 0 : xDir;
×
1555
                yDir = (this.scrollContainer.scrollTop <= 0 && yDir < 0) || (this.scrollContainer.scrollTop >= maxScrollY && yDir > 0) ? 0 : yDir;
×
1556
            }
1557

1558
            const scrollByX = xDir * this._scrollContainerStep;
6✔
1559
            const scrollByY = yDir * this._scrollContainerStep;
6✔
1560

1561
            // Scroll the corresponding window or container.
1562
            if (!this.scrollContainer) {
6!
1563
                window.scrollBy(scrollByX, scrollByY);
6✔
1564
            } else {
1565
                this.scrollContainer.scrollLeft += scrollByX;
×
1566
                this.scrollContainer.scrollTop += scrollByY;
×
1567
            }
1568

1569
            if (this.ghost && !this.scrollContainer) {
6!
1570
                // Scroll the ghost only when there is no container specifies.
1571
                // If it has container the ghost pretty much stays in the same position while the container is scrolled since e use top/left position.
1572
                // Otherwise increase the position the same amount we have scrolled the window
1573
                this.ghostLeft += scrollByX;
6✔
1574
                this.ghostTop += scrollByY;
6✔
1575
            } else if (!this.ghost) {
×
1576
                // Move the base element the same amount we moved the window/container because we use transformations.
1577
                const translateX = this.getTransformX(this.element.nativeElement) + scrollByX;
×
1578
                const translateY = this.getTransformY(this.element.nativeElement) + scrollByY;
×
1579
                this.setTransformXY(translateX, translateY);
×
1580
                if (!this.scrollContainer) {
×
1581
                    this._lastX += scrollByX;
×
1582
                    this._lastY += scrollByY;
×
1583
                }
1584
            }
1585
        })
1586
    }
1587

1588
    protected onScrollContainer() {
1589
        const scrollDir = this.getContainerScrollDirection();
263✔
1590
        if (scrollDir !== null && scrollDir !== undefined && !this._containerScrollIntervalId) {
263✔
1591
            // Save original container sizes to ensure that we don't increase scroll sizes infinitely when out of bounds.
1592
            this._originalScrollContainerWidth = this.scrollContainer ? this.scrollContainer.scrollWidth : this.windowScrollWidth;
31!
1593
            this._originalScrollContainerHeight = this.scrollContainer ? this.scrollContainer.scrollHeight : this.windowScrollHeight;
31!
1594

1595
            this._containerScrollIntervalId = setInterval(() => this.onScrollContainerStep(scrollDir), this._scrollContainerStepMs);
31✔
1596
        } else if ((scrollDir === null || scrollDir === undefined) && this._containerScrollIntervalId) {
232!
1597
            // We moved out of end bounds and there is interval started
1598
            clearInterval(this._containerScrollIntervalId);
×
1599
            this._containerScrollIntervalId = null;
×
1600
        }
1601
    }
1602
}
1603

1604
@Directive({
1605
    exportAs: 'drop',
1606
    selector: '[igxDrop]',
1607
    standalone: true
1608
})
1609
export class IgxDropDirective implements OnInit, OnDestroy {
2✔
1610
    /**
1611
     * - Save data inside the `igxDrop` directive. This can be set when instancing `igxDrop` on an element.
1612
     * ```html
1613
     * <div [igxDrop]="{ source: myElement }"></div>
1614
     * ```
1615
     *
1616
     * @memberof IgxDropDirective
1617
     */
1618
    @Input('igxDrop')
1619
    public set data(v: any) {
1620
        this._data = v;
8,422✔
1621
    }
1622

1623
    public get data(): any {
1624
        return this._data;
1✔
1625
    }
1626

1627
    /**
1628
     * A property that provides a way for igxDrag and igxDrop to be linked through channels.
1629
     * It accepts single value or an array of values and evaluates then using strict equality.
1630
     * ```html
1631
     * <div igxDrag [dragChannel]="'odd'">
1632
     *         <span>95</span>
1633
     * </div>
1634
     * <div igxDrop [dropChannel]="['odd', 'irrational']">
1635
     *         <span>Numbers drop area!</span>
1636
     * </div>
1637
     * ```
1638
     *
1639
     * @memberof IgxDropDirective
1640
     */
1641
    @Input()
1642
    public dropChannel: number | string | number[] | string[];
1643

1644
    /**
1645
     * Sets a drop strategy type that will be executed when an `IgxDrag` element is released inside
1646
     *  the current drop area. The provided strategies are:
1647
     *  - IgxDefaultDropStrategy - This is the default base strategy and it doesn't perform any actions.
1648
     *  - IgxAppendDropStrategy - Appends the dropped element to last position as a direct child to the `igxDrop`.
1649
     *  - IgxPrependDropStrategy - Prepends the dropped element to first position as a direct child to the `igxDrop`.
1650
     *  - IgxInsertDropStrategy - If the dropped element is released above a child element of the `igxDrop`, it will be inserted
1651
     *      at that position. Otherwise the dropped element will be appended if released outside any child of the `igxDrop`.
1652
     * ```html
1653
     * <div igxDrag>
1654
     *      <span>DragMe</span>
1655
     * </div>
1656
     * <div igxDrop [dropStrategy]="myDropStrategy">
1657
     *         <span>Numbers drop area!</span>
1658
     * </div>
1659
     * ```
1660
     * ```typescript
1661
     * import { IgxAppendDropStrategy } from 'igniteui-angular';
1662
     *
1663
     * export class App {
1664
     *      public myDropStrategy = IgxAppendDropStrategy;
1665
     * }
1666
     * ```
1667
     *
1668
     * @memberof IgxDropDirective
1669
     */
1670
    @Input()
1671
    public set dropStrategy(classRef: any) {
1672
        this._dropStrategy = new classRef(this._renderer);
8✔
1673
    }
1674

1675
    public get dropStrategy() {
1676
        return this._dropStrategy;
×
1677
    }
1678

1679
    /**
1680
     * Event triggered when dragged element enters the area of the element.
1681
     * ```html
1682
     * <div class="cageArea" igxDrop (enter)="dragEnter()" (igxDragEnter)="onDragCageEnter()" (igxDragLeave)="onDragCageLeave()">
1683
     * </div>
1684
     * ```
1685
     * ```typescript
1686
     * public dragEnter(){
1687
     *     alert("A draggable element has entered the chip area!");
1688
     * }
1689
     * ```
1690
     *
1691
     * @memberof IgxDropDirective
1692
     */
1693
    @Output()
1694
    public enter = new EventEmitter<IDropBaseEventArgs>();
37,444✔
1695

1696
    /**
1697
     * Event triggered when dragged element enters the area of the element.
1698
     * ```html
1699
     * <div class="cageArea" igxDrop (enter)="dragEnter()" (igxDragEnter)="onDragCageEnter()" (igxDragLeave)="onDragCageLeave()">
1700
     * </div>
1701
     * ```
1702
     * ```typescript
1703
     * public dragEnter(){
1704
     *     alert("A draggable element has entered the chip area!");
1705
     * }
1706
     * ```
1707
     *
1708
     * @memberof IgxDropDirective
1709
     */
1710
    @Output()
1711
    public over = new EventEmitter<IDropBaseEventArgs>();
37,444✔
1712

1713
    /**
1714
     * Event triggered when dragged element leaves the area of the element.
1715
     * ```html
1716
     * <div class="cageArea" igxDrop (leave)="dragLeave()" (igxDragEnter)="onDragCageEnter()" (igxDragLeave)="onDragCageLeave()">
1717
     * </div>
1718
     * ```
1719
     * ```typescript
1720
     * public dragLeave(){
1721
     *     alert("A draggable element has left the chip area!");
1722
     * }
1723
     * ```
1724
     *
1725
     * @memberof IgxDropDirective
1726
     */
1727
    @Output()
1728
    public leave = new EventEmitter<IDropBaseEventArgs>();
37,444✔
1729

1730
    /**
1731
     * Event triggered when dragged element is dropped in the area of the element.
1732
     * Since the `igxDrop` has default logic that appends the dropped element as a child, it can be canceled here.
1733
     * To cancel the default logic the `cancel` property of the event needs to be set to true.
1734
     * ```html
1735
     * <div class="cageArea" igxDrop (dropped)="dragDrop()" (igxDragEnter)="onDragCageEnter()" (igxDragLeave)="onDragCageLeave()">
1736
     * </div>
1737
     * ```
1738
     * ```typescript
1739
     * public dragDrop(){
1740
     *     alert("A draggable element has been dropped in the chip area!");
1741
     * }
1742
     * ```
1743
     *
1744
     * @memberof IgxDropDirective
1745
     */
1746
    @Output()
1747
    public dropped = new EventEmitter<IDropDroppedEventArgs>();
37,444✔
1748

1749
    /**
1750
     * @hidden
1751
     */
1752
    @HostBinding('attr.droppable')
1753
    public droppable = true;
37,444✔
1754

1755
    /**
1756
     * @hidden
1757
     */
1758
    @HostBinding('class.dragOver')
1759
    public dragover = false;
37,444✔
1760

1761
    /**
1762
     * @hidden
1763
     */
1764
    protected _destroy = new Subject<boolean>();
37,444✔
1765
    protected _dropStrategy: IDropStrategy;
1766

1767
    private _data: any;
1768

1769
    constructor(public element: ElementRef, private _renderer: Renderer2, private _zone: NgZone) {
37,444✔
1770
        this._dropStrategy = new IgxDefaultDropStrategy();
37,444✔
1771
    }
1772

1773
    /**
1774
     * @hidden
1775
     */
1776
    @HostListener('igxDrop', ['$event'])
1777
    public onDragDrop(event) {
1778
        if (!this.isDragLinked(event.detail.owner)) {
44✔
1779
            return;
2✔
1780
        }
1781

1782
        const elementPosX = this.element.nativeElement.getBoundingClientRect().left + this.getWindowScrollLeft();
42✔
1783
        const elementPosY = this.element.nativeElement.getBoundingClientRect().top + this.getWindowScrollTop();
42✔
1784
        const offsetX = event.detail.pageX - elementPosX;
42✔
1785
        const offsetY = event.detail.pageY - elementPosY;
42✔
1786
        const args: IDropDroppedEventArgs = {
42✔
1787
            owner: this,
1788
            originalEvent: event.detail.originalEvent,
1789
            drag: event.detail.owner,
1790
            dragData: event.detail.owner.data,
1791
            startX: event.detail.startX,
1792
            startY: event.detail.startY,
1793
            pageX: event.detail.pageX,
1794
            pageY: event.detail.pageY,
1795
            offsetX,
1796
            offsetY,
1797
            cancel: false
1798
        };
1799
        this._zone.run(() => {
42✔
1800
            this.dropped.emit(args);
42✔
1801
        });
1802

1803
        if (this._dropStrategy && !args.cancel) {
42✔
1804
            const elementsAtPoint = event.detail.owner.getElementsAtPoint(event.detail.pageX, event.detail.pageY);
20✔
1805
            const insertIndex = this.getInsertIndexAt(event.detail.owner, elementsAtPoint);
20✔
1806
            this._dropStrategy.dropAction(event.detail.owner, this, insertIndex);
20✔
1807
        }
1808
    }
1809

1810
    /**
1811
     * @hidden
1812
     */
1813
    public ngOnInit() {
1814
        this._zone.runOutsideAngular(() => {
37,444✔
1815
            fromEvent(this.element.nativeElement, 'igxDragEnter').pipe(takeUntil(this._destroy))
37,444✔
1816
                .subscribe((res) => this.onDragEnter(res as CustomEvent<IgxDragCustomEventDetails>));
108✔
1817

1818
            fromEvent(this.element.nativeElement, 'igxDragLeave').pipe(takeUntil(this._destroy)).subscribe((res) => this.onDragLeave(res));
37,444✔
1819
            fromEvent(this.element.nativeElement, 'igxDragOver').pipe(takeUntil(this._destroy)).subscribe((res) => this.onDragOver(res));
37,444✔
1820
        });
1821
    }
1822

1823
    /**
1824
     * @hidden
1825
     */
1826
    public ngOnDestroy() {
1827
        this._destroy.next(true);
37,327✔
1828
        this._destroy.complete();
37,327✔
1829
    }
1830

1831
    /**
1832
     * @hidden
1833
     */
1834
    public onDragOver(event) {
1835
        const elementPosX = this.element.nativeElement.getBoundingClientRect().left + this.getWindowScrollLeft();
62✔
1836
        const elementPosY = this.element.nativeElement.getBoundingClientRect().top + this.getWindowScrollTop();
62✔
1837
        const offsetX = event.detail.pageX - elementPosX;
62✔
1838
        const offsetY = event.detail.pageY - elementPosY;
62✔
1839
        const eventArgs: IDropBaseEventArgs = {
62✔
1840
            originalEvent: event.detail.originalEvent,
1841
            owner: this,
1842
            drag: event.detail.owner,
1843
            dragData: event.detail.owner.data,
1844
            startX: event.detail.startX,
1845
            startY: event.detail.startY,
1846
            pageX: event.detail.pageX,
1847
            pageY: event.detail.pageY,
1848
            offsetX,
1849
            offsetY
1850
        };
1851

1852
        this.over.emit(eventArgs);
62✔
1853
    }
1854

1855
    /**
1856
     * @hidden
1857
     */
1858
    public onDragEnter(event: CustomEvent<IgxDragCustomEventDetails>) {
1859
        if (!this.isDragLinked(event.detail.owner)) {
48✔
1860
            return;
2✔
1861
        }
1862

1863
        this.dragover = true;
46✔
1864
        const elementPosX = this.element.nativeElement.getBoundingClientRect().left + this.getWindowScrollLeft();
46✔
1865
        const elementPosY = this.element.nativeElement.getBoundingClientRect().top + this.getWindowScrollTop();
46✔
1866
        const offsetX = event.detail.pageX - elementPosX;
46✔
1867
        const offsetY = event.detail.pageY - elementPosY;
46✔
1868
        const eventArgs: IDropBaseEventArgs = {
46✔
1869
            originalEvent: event.detail.originalEvent,
1870
            owner: this,
1871
            drag: event.detail.owner,
1872
            dragData: event.detail.owner.data,
1873
            startX: event.detail.startX,
1874
            startY: event.detail.startY,
1875
            pageX: event.detail.pageX,
1876
            pageY: event.detail.pageY,
1877
            offsetX,
1878
            offsetY
1879
        };
1880
        this._zone.run(() => {
46✔
1881
            this.enter.emit(eventArgs);
46✔
1882
        });
1883
    }
1884

1885
    /**
1886
     * @hidden
1887
     */
1888
    public onDragLeave(event) {
1889
        if (!this.isDragLinked(event.detail.owner)) {
45✔
1890
            return;
2✔
1891
        }
1892

1893
        this.dragover = false;
43✔
1894
        const elementPosX = this.element.nativeElement.getBoundingClientRect().left + this.getWindowScrollLeft();
43✔
1895
        const elementPosY = this.element.nativeElement.getBoundingClientRect().top + this.getWindowScrollTop();
43✔
1896
        const offsetX = event.detail.pageX - elementPosX;
43✔
1897
        const offsetY = event.detail.pageY - elementPosY;
43✔
1898
        const eventArgs: IDropBaseEventArgs = {
43✔
1899
            originalEvent: event.detail.originalEvent,
1900
            owner: this,
1901
            drag: event.detail.owner,
1902
            dragData: event.detail.owner.data,
1903
            startX: event.detail.startX,
1904
            startY: event.detail.startY,
1905
            pageX: event.detail.pageX,
1906
            pageY: event.detail.pageY,
1907
            offsetX,
1908
            offsetY
1909
        };
1910
        this._zone.run(() => {
43✔
1911
            this.leave.emit(eventArgs);
43✔
1912
        });
1913
    }
1914

1915
    protected getWindowScrollTop() {
1916
        return window.scrollY ? window.scrollY : (window.pageYOffset ? window.pageYOffset : 0);
193!
1917
    }
1918

1919
    protected getWindowScrollLeft() {
1920
        return window.scrollX ? window.scrollX : (window.pageXOffset ? window.pageXOffset : 0);
193!
1921
    }
1922

1923
    protected isDragLinked(drag: IgxDragDirective): boolean {
1924
        const dragLinkArray = drag.dragChannel instanceof Array;
137✔
1925
        const dropLinkArray = this.dropChannel instanceof Array;
137✔
1926

1927
        if (!dragLinkArray && !dropLinkArray) {
137✔
1928
            return this.dropChannel === drag.dragChannel;
131✔
1929
        } else if (!dragLinkArray && dropLinkArray) {
6!
1930
            const dropLinks = this.dropChannel as any[];
×
1931
            for (const link of dropLinks) {
×
1932
                if (link === drag.dragChannel) {
×
1933
                    return true;
×
1934
                }
1935
            }
1936
        } else if (dragLinkArray && !dropLinkArray) {
6✔
1937
            const dragLinks = drag.dragChannel as any[];
3✔
1938
            for (const link of dragLinks) {
3✔
1939
                if (link === this.dropChannel) {
3✔
1940
                    return true;
3✔
1941
                }
1942
            }
1943
        } else {
1944
            const dragLinks = drag.dragChannel as any[];
3✔
1945
            const dropLinks = this.dropChannel as any[];
3✔
1946
            for (const draglink of dragLinks) {
3✔
1947
                for (const droplink of dropLinks) {
9✔
1948
                    if (draglink === droplink) {
18!
1949
                        return true;
×
1950
                    }
1951
                }
1952
            }
1953
        }
1954

1955
        return false;
3✔
1956
    }
1957

1958
    protected getInsertIndexAt(draggedDir: IgxDragDirective, elementsAtPoint: any[]): number {
1959
        let insertIndex = -1;
20✔
1960
        const dropChildren = Array.prototype.slice.call(this.element.nativeElement.children);
20✔
1961
        if (!dropChildren.length) {
20✔
1962
            return insertIndex;
2✔
1963
        }
1964

1965
        let i = 0;
18✔
1966
        let childUnder = null;
18✔
1967
        while (!childUnder && i < elementsAtPoint.length) {
18✔
1968
            if (elementsAtPoint[i].parentElement === this.element.nativeElement) {
51✔
1969
                childUnder = elementsAtPoint[i];
16✔
1970
            }
1971
            i++;
51✔
1972
        }
1973

1974
        const draggedElemIndex = dropChildren.indexOf(draggedDir.element.nativeElement);
18✔
1975
        insertIndex = dropChildren.indexOf(childUnder);
18✔
1976
        if (draggedElemIndex !== -1 && draggedElemIndex < insertIndex) {
18!
1977
            insertIndex++;
×
1978
        }
1979

1980
        return insertIndex;
18✔
1981
    }
1982
}
1983

STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc