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

IgniteUI / igniteui-angular / 17322895485

29 Aug 2025 11:43AM UTC coverage: 91.568% (-0.05%) from 91.618%
17322895485

push

github

web-flow
Grid Cell merging  (#16024)

13819 of 16209 branches covered (85.26%)

234 of 254 new or added lines in 11 files covered. (92.13%)

16 existing lines in 2 files now uncovered.

27757 of 30313 relevant lines covered (91.57%)

34712.72 hits per line

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

82.66
/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
    DOCUMENT
24
} from '@angular/core';
25
import { animationFrameScheduler, fromEvent, interval, Subject } from 'rxjs';
26
import { takeUntil, throttle } from 'rxjs/operators';
27
import { IBaseEventArgs, PlatformUtil } from '../../core/utils';
28
import { IDropStrategy, IgxDefaultDropStrategy } from './drag-drop.strategy';
29

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

37
export enum DragDirection {
3✔
38
    VERTICAL,
3✔
39
    HORIZONTAL,
3✔
40
    BOTH
3✔
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 {
3✔
162

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

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

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

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

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

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

186
@Directive({
187
    exportAs: 'drag',
188
    selector: '[igxDrag]',
189
    standalone: true
190
})
191
export class IgxDragDirective implements AfterContentInit, OnDestroy {
3✔
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;
7,493✔
203
    }
204

205
    public get data(): any {
206
        return this._data;
1,213✔
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;
30,193✔
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;
30,193✔
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;
30,193✔
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 = '';
30,193✔
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 = {};
30,193✔
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
30,193✔
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>();
30,193✔
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>();
30,193✔
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>();
30,193✔
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>();
30,193✔
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>();
30,193✔
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>();
30,193✔
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>();
30,193✔
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;
30,193✔
481

482
    /**
483
     * @hidden
484
     */
485
    @HostBinding('class.igx-drag--select-disabled')
486
    public selectDisabled = false;
30,193✔
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';
92,919✔
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) {
3,397✔
522
            return this.ghostLeft;
3,352✔
523
        }
524
        return this.baseLeft + this.windowScrollLeft;
45✔
525
    }
526

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

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

541
    protected get baseTop(): number {
542
        return this.element.nativeElement.getBoundingClientRect().top;
355✔
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) {
1,754✔
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);
1,754✔
557
            // If ghost host is defined it needs to be taken into account.
558
            this.ghostElement.style.left = (pageX - ghostMarginLeft - this._ghostHostX) + 'px';
1,754✔
559
        }
560
    }
561

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

568
    protected set ghostTop(pageY: number) {
569
        if (this.ghostElement) {
1,754✔
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);
1,754✔
572
            // If ghost host is defined it needs to be taken into account.
573
            this.ghostElement.style.top = (pageY - ghostMarginTop - this._ghostHostY) + 'px';
1,754✔
574
        }
575
    }
576

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

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

587
    protected get windowScrollLeft() {
588
        return this.document.documentElement.scrollLeft || window.scrollX;
5,523✔
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';
30,193✔
603

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

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

614
    protected ghostContext: any = null;
30,193✔
615
    protected _startX = 0;
30,193✔
616
    protected _startY = 0;
30,193✔
617
    protected _lastX = 0;
30,193✔
618
    protected _lastY = 0;
30,193✔
619
    protected _dragStarted = false;
30,193✔
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;
30,193✔
629
    protected _ghostHostY = 0;
30,193✔
630
    protected _dynamicGhostRef: EmbeddedViewRef<any>;
631

632
    protected _pointerDownId = null;
30,193✔
633
    protected _clicked = false;
30,193✔
634
    protected _lastDropArea = null;
30,193✔
635

636
    protected _destroy = new Subject<boolean>();
30,193✔
637
    protected _removeOnDestroy = true;
30,193✔
638
    protected _data: any;
639
    protected _scrollContainer = null;
30,193✔
640
    protected _originalScrollContainerWidth = 0;
30,193✔
641
    protected _originalScrollContainerHeight = 0;
30,193✔
642
    protected _scrollContainerStep = 5;
30,193✔
643
    protected _scrollContainerStepMs = 10;
30,193✔
644
    protected _scrollContainerThreshold = 25;
30,193✔
645
    protected _containerScrollIntervalId = null;
30,193✔
646
    private document = inject(DOCUMENT);
30,193✔
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;
167✔
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;
167✔
688
    }
689

690
    constructor(
691
        public cdr: ChangeDetectorRef,
30,193✔
692
        public element: ElementRef,
30,193✔
693
        public viewContainer: ViewContainerRef,
30,193✔
694
        public zone: NgZone,
30,193✔
695
        public renderer: Renderer2,
30,193✔
696
        protected platformUtil: PlatformUtil
30,193✔
697
    ) {
698
        this.onTransitionEnd = this.onTransitionEnd.bind(this);
30,193✔
699
        this.onPointerMove = this.onPointerMove.bind(this);
30,193✔
700
        this.onPointerUp = this.onPointerUp.bind(this);
30,193✔
701
        this.onPointerLost = this.onPointerLost.bind(this);
30,193✔
702
    }
703

704
    /**
705
     * @hidden
706
     */
707
    public ngAfterContentInit() {
708
        if (!this.dragHandles || !this.dragHandles.length) {
30,193✔
709
            // Set user select none to the whole draggable element if no drag handles are defined.
710
            this.selectDisabled = true;
29,757✔
711
        }
712

713
        // Bind events
714
        this.zone.runOutsideAngular(() => {
30,193✔
715
            if (!this.platformUtil.isBrowser) {
30,193!
716
                return;
×
717
            }
718
            const targetElements = this.dragHandles && this.dragHandles.length
30,193✔
719
                ? this.dragHandles
720
                    .filter(item => item.parentDragElement === null)
450✔
721
                    .map(item => {
722
                        item.parentDragElement = this.element.nativeElement;
436✔
723
                        return item.element.nativeElement;
436✔
724
                    })
725
                : [this.element.nativeElement];
726
            targetElements.forEach((element) => {
30,193✔
727
                if (this.pointerEventsEnabled) {
30,193!
728
                    fromEvent(element, 'pointerdown').pipe(takeUntil(this._destroy))
30,193✔
729
                        .subscribe((res) => this.onPointerDown(res));
159✔
730

731
                    fromEvent(element, 'pointermove').pipe(
30,193✔
732
                        throttle(() => interval(0, animationFrameScheduler)),
168✔
733
                        takeUntil(this._destroy)
734
                    ).subscribe((res) => this.onPointerMove(res));
168✔
735

736
                    fromEvent(element, 'pointerup').pipe(takeUntil(this._destroy))
30,193✔
737
                        .subscribe((res) => this.onPointerUp(res));
236✔
738

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

754
            // We should bind to document events only once when there are no pointer events.
755
            if (!this.pointerEventsEnabled && this.touchEventsEnabled) {
30,193!
756
                fromEvent(this.document.defaultView, 'touchmove').pipe(
×
757
                    throttle(() => interval(0, animationFrameScheduler)),
×
758
                    takeUntil(this._destroy)
759
                ).subscribe((res) => this.onPointerMove(res));
×
760

761
                fromEvent(this.document.defaultView, 'touchend').pipe(takeUntil(this._destroy))
×
762
                    .subscribe((res) => this.onPointerUp(res));
×
763
            } else if (!this.pointerEventsEnabled) {
30,193!
764
                fromEvent(this.document.defaultView, 'mousemove').pipe(
×
765
                    throttle(() => interval(0, animationFrameScheduler)),
×
766
                    takeUntil(this._destroy)
767
                ).subscribe((res) => this.onPointerMove(res));
×
768

769
                fromEvent(this.document.defaultView, 'mouseup').pipe(takeUntil(this._destroy))
×
770
                    .subscribe((res) => this.onPointerUp(res));
×
771
            }
772
            this.element.nativeElement.addEventListener('transitionend', this.onTransitionEnd);
30,193✔
773
        });
774

775
        // Set transition duration to 0s. This also helps with setting `visibility: hidden` to the base to not lag.
776
        this.element.nativeElement.style.transitionDuration = '0.0s';
30,193✔
777
    }
778

779
    /**
780
     * @hidden
781
     */
782
    public ngOnDestroy() {
783
        this._destroy.next(true);
30,058✔
784
        this._destroy.complete();
30,058✔
785

786
        if (this.ghostElement) {
30,058✔
787
            if (this._removeOnDestroy) {
32✔
788
                this.clearGhost();
26✔
789
            } else {
790
                this.detachGhost();
6✔
791
            }
792
        }
793

794
        this.element.nativeElement.removeEventListener('transitionend', this.onTransitionEnd);
30,058✔
795

796
        if (this._containerScrollIntervalId) {
30,058✔
797
            clearInterval(this._containerScrollIntervalId);
1✔
798
            this._containerScrollIntervalId = null;
1✔
799
        }
800
    }
801

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

820
        this._startX = this.baseLeft;
12✔
821
        this._startY = this.baseTop;
12✔
822
    }
823

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

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

847
            this.setLocation(startLocation);
1✔
848
        }
849

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1041
            const setPageX = moveArgs.nextPageX;
1,707✔
1042
            const setPageY = moveArgs.nextPageY;
1,707✔
1043
            if (!moveArgs.cancel) {
1,707✔
1044
                // Scroll root container if the user reaches its boundaries.
1045
                this.onScrollContainer();
1,707✔
1046

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

1063
            this._lastX = setPageX;
1,707✔
1064
            this._lastY = setPageY;
1,707✔
1065
        }
1066
    }
1067

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

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

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

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

1108
            this.zone.run(() => {
135✔
1109
                this.dragEnd.emit(eventArgs);
135✔
1110
            });
1111

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

1122
        if (this._containerScrollIntervalId) {
143✔
1123
            clearInterval(this._containerScrollIntervalId);
30✔
1124
            this._containerScrollIntervalId = null;
30✔
1125
        }
1126
    }
1127

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

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

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

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

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

1188
        if (this.ghost && this.ghostElement) {
132✔
1189
            this._ghostStartX = this.baseLeft + this.windowScrollLeft;
124✔
1190
            this._ghostStartY = this.baseTop + this.windowScrollTop;
124✔
1191

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

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

1224
    protected detachGhost() {
1225
        this.ghostElement.removeEventListener('pointermove', this.onPointerMove);
156✔
1226
        this.ghostElement.removeEventListener('pointerup', this.onPointerUp);
156✔
1227
        this.ghostElement.removeEventListener('lostpointercapture', this.onPointerLost);
156✔
1228
        this.ghostElement.removeEventListener('transitionend', this.onTransitionEnd);
156✔
1229
    }
1230

1231
    protected clearGhost() {
1232
        this.detachGhost();
150✔
1233
        this.ghostElement.remove();
150✔
1234
        this.ghostElement = null;
150✔
1235

1236
        if (this._dynamicGhostRef) {
150✔
1237
            this._dynamicGhostRef.destroy();
9✔
1238
            this._dynamicGhostRef = null;
9✔
1239
        }
1240
    }
1241

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

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

1270
        const totalMovedX = pageX - this._startX;
151✔
1271
        const totalMovedY = pageY - this._startY;
151✔
1272
        this._ghostHostX = this.getGhostHostBaseOffsetX();
151✔
1273
        this._ghostHostY = this.getGhostHostBaseOffsetY();
151✔
1274

1275
        this.ghostElement.style.transitionDuration = '0.0s';
151✔
1276
        this.ghostElement.style.position = 'absolute';
151✔
1277

1278
        if (this.ghostClass) {
151✔
1279
            this.ghostElement.classList.add(this.ghostClass);
90✔
1280
        }
1281

1282
        if (this.ghostStyle) {
151✔
1283
            Object.entries(this.ghostStyle).map(([name, value]) => {
151✔
1284
                this.renderer.setStyle(this.ghostElement, name, value, RendererStyleFlags2.DashCase);
33✔
1285
            });
1286
        }
1287

1288
        const createEventArgs = {
151✔
1289
            owner: this,
1290
            ghostElement: this.ghostElement,
1291
            cancel: false
1292
        };
1293
        this.ghostCreate.emit(createEventArgs);
151✔
1294
        if (createEventArgs.cancel) {
151!
1295
            this.ghostElement = null;
×
1296
            if (this.ghostTemplate && this._dynamicGhostRef) {
×
1297
                this._dynamicGhostRef.destroy();
×
1298
            }
1299
            return;
×
1300
        }
1301

1302
        if (this.ghostHost) {
151✔
1303
            this.ghostHost.appendChild(this.ghostElement);
59✔
1304
        } else {
1305
            this.document.body.appendChild(this.ghostElement);
92✔
1306
        }
1307

1308
        const ghostMarginLeft = parseInt(this.document.defaultView.getComputedStyle(this.ghostElement)['margin-left'], 10);
151✔
1309
        const ghostMarginTop = parseInt(this.document.defaultView.getComputedStyle(this.ghostElement)['margin-top'], 10);
151✔
1310
        this.ghostElement.style.left = (this._ghostStartX - ghostMarginLeft + totalMovedX - this._ghostHostX) + 'px';
151✔
1311
        this.ghostElement.style.top = (this._ghostStartY - ghostMarginTop + totalMovedY - this._ghostHostY) + 'px';
151✔
1312

1313
        if (this.pointerEventsEnabled) {
151✔
1314
            // The ghostElement takes control for moving and dragging after it has been rendered.
1315
            if (this._pointerDownId !== null) {
151✔
1316
                this.ghostElement.setPointerCapture(this._pointerDownId);
149✔
1317
            }
1318
            this.ghostElement.addEventListener('pointermove', this.onPointerMove);
151✔
1319
            this.ghostElement.addEventListener('pointerup', this.onPointerUp);
151✔
1320
            this.ghostElement.addEventListener('lostpointercapture', this.onPointerLost);
151✔
1321
        }
1322

1323
        // Transition animation when the ghostElement is released and it returns to it's original position.
1324
        this.ghostElement.addEventListener('transitionend', this.onTransitionEnd);
151✔
1325

1326
        this.cdr.detectChanges();
151✔
1327
    }
1328

1329
    /**
1330
     * @hidden
1331
     * Dispatch custom igxDragEnter/igxDragLeave events based on current pointer position and if drop area is under.
1332
     */
1333
    protected dispatchDragEvents(pageX: number, pageY: number, originalEvent) {
1334
        let topDropArea;
1335
        const customEventArgs: IgxDragCustomEventDetails = {
1,707✔
1336
            startX: this._startX,
1337
            startY: this._startY,
1338
            pageX,
1339
            pageY,
1340
            owner: this,
1341
            originalEvent
1342
        };
1343

1344
        const elementsFromPoint = this.getElementsAtPoint(pageX, pageY);
1,707✔
1345
        let targetElements = [];
1,707✔
1346
        // Check for shadowRoot instance and use it if present
1347
        for (const elFromPoint of elementsFromPoint) {
1,707✔
1348
            if (elFromPoint?.shadowRoot) {
23,905!
1349
                targetElements = targetElements.concat(this.getFromShadowRoot(elFromPoint, pageX, pageY, elementsFromPoint));
×
1350
            } else if (targetElements.indexOf(elFromPoint) === -1) {
23,905✔
1351
                targetElements.push(elFromPoint);
23,901✔
1352
            }
1353
        }
1354

1355
        for (const element of targetElements) {
1,707✔
1356
            if (element.getAttribute('droppable') === 'true' &&
16,007✔
1357
                element !== this.ghostElement && element !== this.element.nativeElement) {
1358
                topDropArea = element;
692✔
1359
                break;
692✔
1360
            }
1361
        }
1362

1363
        if (topDropArea &&
1,707✔
1364
            (!this._lastDropArea || (this._lastDropArea && this._lastDropArea !== topDropArea))) {
1365
            if (this._lastDropArea) {
166✔
1366
                this.dispatchEvent(this._lastDropArea, 'igxDragLeave', customEventArgs);
24✔
1367
            }
1368

1369
            this._lastDropArea = topDropArea;
166✔
1370
            this.dispatchEvent(this._lastDropArea, 'igxDragEnter', customEventArgs);
166✔
1371
        } else if (!topDropArea && this._lastDropArea) {
1,541✔
1372
            this.dispatchEvent(this._lastDropArea, 'igxDragLeave', customEventArgs);
34✔
1373
            this._lastDropArea = null;
34✔
1374
            return;
34✔
1375
        }
1376

1377
        if (topDropArea) {
1,673✔
1378
            this.dispatchEvent(topDropArea, 'igxDragOver', customEventArgs);
692✔
1379
        }
1380
    }
1381

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

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

1414
        this.dispatchEvent(this._lastDropArea, 'igxDrop', eventArgs);
96✔
1415
        this.dispatchEvent(this._lastDropArea, 'igxDragLeave', eventArgs);
96✔
1416
        this._lastDropArea = null;
96✔
1417
    }
1418

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

1439
    /**
1440
     * @hidden
1441
     */
1442
    protected dispatchEvent(target, eventName: string, eventArgs: IgxDragCustomEventDetails) {
1443
        // This way is IE11 compatible.
1444
        // const dragLeaveEvent = document.createEvent('CustomEvent');
1445
        // dragLeaveEvent.initCustomEvent(eventName, false, false, eventArgs);
1446
        // target.dispatchEvent(dragLeaveEvent);
1447
        // Otherwise can be used `target.dispatchEvent(new CustomEvent(eventName, eventArgs));`
1448
        target.dispatchEvent(new CustomEvent(eventName, { detail: eventArgs }));
1,108✔
1449
    }
1450

1451
    protected getTransformX(elem) {
1452
        let posX = 0;
28✔
1453
        if (elem.style.transform) {
28✔
1454
            const matrix = elem.style.transform;
14✔
1455
            const values = matrix ? matrix.match(/-?[\d\.]+/g) : undefined;
14!
1456
            posX = values ? Number(values[1]) : 0;
14!
1457
        }
1458

1459
        return posX;
28✔
1460
    }
1461

1462
    protected getTransformY(elem) {
1463
        let posY = 0;
27✔
1464
        if (elem.style.transform) {
27✔
1465
            const matrix = elem.style.transform;
14✔
1466
            const values = matrix ? matrix.match(/-?[\d\.]+/g) : undefined;
14!
1467
            posY = values ? Number(values[2]) : 0;
14!
1468
        }
1469

1470
        return posY;
27✔
1471
    }
1472

1473
    /** Method setting transformation to the base draggable element. */
1474
    protected setTransformXY(x: number, y: number) {
1475
        if(x === 0 && y === 0) {
24✔
1476
            this.element.nativeElement.style.transform = '';
2✔
1477
            return;
2✔
1478
        }
1479
        this.element.nativeElement.style.transform = 'translate3d(' + x + 'px, ' + y + 'px, 0px)';
22✔
1480
    }
1481

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

1495
        const ghostPosition = this.document.defaultView.getComputedStyle(this.ghostHost).getPropertyValue('position');
59✔
1496
        if (ghostPosition === 'static' && this.ghostHost.offsetParent && this.ghostHost.offsetParent === this.document.body) {
59✔
1497
            return 0;
1✔
1498
        } else if (ghostPosition === 'static' && this.ghostHost.offsetParent) {
58!
1499
            return this.ghostHost.offsetParent.getBoundingClientRect().left + this.windowScrollLeft;
×
1500
        }
1501
        return this.ghostHost.getBoundingClientRect().left + this.windowScrollLeft;
58✔
1502
    }
1503

1504
    protected getGhostHostBaseOffsetY() {
1505
        if (!this.ghostHost) return 0;
151✔
1506

1507
        const ghostPosition = this.document.defaultView.getComputedStyle(this.ghostHost).getPropertyValue('position');
59✔
1508
        if (ghostPosition === 'static' && this.ghostHost.offsetParent && this.ghostHost.offsetParent === this.document.body) {
59✔
1509
            return 0;
1✔
1510
        } else if (ghostPosition === 'static' && this.ghostHost.offsetParent) {
58!
1511
            return this.ghostHost.offsetParent.getBoundingClientRect().top + this.windowScrollTop;
×
1512
        }
1513
        return this.ghostHost.getBoundingClientRect().top + this.windowScrollTop;
58✔
1514
    }
1515

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

1533
        if (this.pageY <= topBorder && scrolledY) {
1,707!
1534
            return DragScrollDirection.UP;
×
1535
        } else if (this.pageY > bottomBorder) {
1,707✔
1536
            return DragScrollDirection.DOWN;
13✔
1537
        } else if (this.pageX < leftBorder && scrolledX) {
1,694!
1538
            return DragScrollDirection.LEFT;
×
1539
        } else if (this.pageX > rightBorder) {
1,694✔
1540
            return DragScrollDirection.RIGHT;
44✔
1541
        }
1542
        return null;
1,650✔
1543
    }
1544

1545
    protected onScrollContainerStep(scrollDir: DragScrollDirection) {
1546
        animationFrameScheduler.schedule(() => {
6✔
1547

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

UNCOV
1564
            const scrollByX = xDir * this._scrollContainerStep;
×
UNCOV
1565
            const scrollByY = yDir * this._scrollContainerStep;
×
1566

1567
            // Scroll the corresponding window or container.
UNCOV
1568
            if (!this.scrollContainer) {
×
UNCOV
1569
                window.scrollBy(scrollByX, scrollByY);
×
1570
            } else {
1571
                this.scrollContainer.scrollLeft += scrollByX;
×
1572
                this.scrollContainer.scrollTop += scrollByY;
×
1573
            }
1574

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

1594
    protected onScrollContainer() {
1595
        const scrollDir = this.getContainerScrollDirection();
1,707✔
1596
        if (scrollDir !== null && scrollDir !== undefined && !this._containerScrollIntervalId) {
1,707✔
1597
            // Save original container sizes to ensure that we don't increase scroll sizes infinitely when out of bounds.
1598
            this._originalScrollContainerWidth = this.scrollContainer ? this.scrollContainer.scrollWidth : this.windowScrollWidth;
31!
1599
            this._originalScrollContainerHeight = this.scrollContainer ? this.scrollContainer.scrollHeight : this.windowScrollHeight;
31!
1600

1601
            this._containerScrollIntervalId = setInterval(() => this.onScrollContainerStep(scrollDir), this._scrollContainerStepMs);
31✔
1602
        } else if ((scrollDir === null || scrollDir === undefined) && this._containerScrollIntervalId) {
1,676!
1603
            // We moved out of end bounds and there is interval started
1604
            clearInterval(this._containerScrollIntervalId);
×
1605
            this._containerScrollIntervalId = null;
×
1606
        }
1607
    }
1608
}
1609

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

1629
    public get data(): any {
1630
        return this._data;
1✔
1631
    }
1632

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

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

1681
    public get dropStrategy() {
1682
        return this._dropStrategy;
×
1683
    }
1684

1685
    /**
1686
     * Event triggered when dragged element enters the area of the element.
1687
     * ```html
1688
     * <div class="cageArea" igxDrop (enter)="dragEnter()" (igxDragEnter)="onDragCageEnter()" (igxDragLeave)="onDragCageLeave()">
1689
     * </div>
1690
     * ```
1691
     * ```typescript
1692
     * public dragEnter(){
1693
     *     alert("A draggable element has entered the chip area!");
1694
     * }
1695
     * ```
1696
     *
1697
     * @memberof IgxDropDirective
1698
     */
1699
    @Output()
1700
    public enter = new EventEmitter<IDropBaseEventArgs>();
34,384✔
1701

1702
    /**
1703
     * Event triggered when dragged element enters the area of the element.
1704
     * ```html
1705
     * <div class="cageArea" igxDrop (enter)="dragEnter()" (igxDragEnter)="onDragCageEnter()" (igxDragLeave)="onDragCageLeave()">
1706
     * </div>
1707
     * ```
1708
     * ```typescript
1709
     * public dragEnter(){
1710
     *     alert("A draggable element has entered the chip area!");
1711
     * }
1712
     * ```
1713
     *
1714
     * @memberof IgxDropDirective
1715
     */
1716
    @Output()
1717
    public over = new EventEmitter<IDropBaseEventArgs>();
34,384✔
1718

1719
    /**
1720
     * Event triggered when dragged element leaves the area of the element.
1721
     * ```html
1722
     * <div class="cageArea" igxDrop (leave)="dragLeave()" (igxDragEnter)="onDragCageEnter()" (igxDragLeave)="onDragCageLeave()">
1723
     * </div>
1724
     * ```
1725
     * ```typescript
1726
     * public dragLeave(){
1727
     *     alert("A draggable element has left the chip area!");
1728
     * }
1729
     * ```
1730
     *
1731
     * @memberof IgxDropDirective
1732
     */
1733
    @Output()
1734
    public leave = new EventEmitter<IDropBaseEventArgs>();
34,384✔
1735

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

1755
    /**
1756
     * @hidden
1757
     */
1758
    @HostBinding('attr.droppable')
1759
    public droppable = true;
34,384✔
1760

1761
    /**
1762
     * @hidden
1763
     */
1764
    @HostBinding('class.dragOver')
1765
    public dragover = false;
34,384✔
1766

1767
    /**
1768
     * @hidden
1769
     */
1770
    protected _destroy = new Subject<boolean>();
34,384✔
1771
    protected _dropStrategy: IDropStrategy;
1772

1773
    private _data: any;
1774

1775
    constructor(public element: ElementRef, private _renderer: Renderer2, private _zone: NgZone) {
34,384✔
1776
        this._dropStrategy = new IgxDefaultDropStrategy();
34,384✔
1777
    }
1778

1779
    /**
1780
     * @hidden
1781
     */
1782
    @HostListener('igxDrop', ['$event'])
1783
    public onDragDrop(event) {
1784
        if (!this.isDragLinked(event.detail.owner)) {
53✔
1785
            return;
2✔
1786
        }
1787

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

1809
        if (this._dropStrategy && !args.cancel) {
51✔
1810
            const elementsAtPoint = event.detail.owner.getElementsAtPoint(event.detail.pageX, event.detail.pageY);
24✔
1811
            const insertIndex = this.getInsertIndexAt(event.detail.owner, elementsAtPoint);
24✔
1812
            this._dropStrategy.dropAction(event.detail.owner, this, insertIndex);
24✔
1813
        }
1814
    }
1815

1816
    /**
1817
     * @hidden
1818
     */
1819
    public ngOnInit() {
1820
        this._zone.runOutsideAngular(() => {
34,384✔
1821
            fromEvent(this.element.nativeElement, 'igxDragEnter').pipe(takeUntil(this._destroy))
34,384✔
1822
                .subscribe((res) => this.onDragEnter(res as CustomEvent<IgxDragCustomEventDetails>));
171✔
1823

1824
            fromEvent(this.element.nativeElement, 'igxDragLeave').pipe(takeUntil(this._destroy)).subscribe((res) => this.onDragLeave(res));
34,384✔
1825
            fromEvent(this.element.nativeElement, 'igxDragOver').pipe(takeUntil(this._destroy)).subscribe((res) => this.onDragOver(res));
34,384✔
1826
        });
1827
    }
1828

1829
    /**
1830
     * @hidden
1831
     */
1832
    public ngOnDestroy() {
1833
        this._destroy.next(true);
34,251✔
1834
        this._destroy.complete();
34,251✔
1835
    }
1836

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

1858
        this.over.emit(eventArgs);
643✔
1859
    }
1860

1861
    /**
1862
     * @hidden
1863
     */
1864
    public onDragEnter(event: CustomEvent<IgxDragCustomEventDetails>) {
1865
        if (!this.isDragLinked(event.detail.owner)) {
111✔
1866
            return;
2✔
1867
        }
1868

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

1891
    /**
1892
     * @hidden
1893
     */
1894
    public onDragLeave(event) {
1895
        if (!this.isDragLinked(event.detail.owner)) {
99✔
1896
            return;
2✔
1897
        }
1898

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

1921
    protected getWindowScrollTop() {
1922
        return window.scrollY ? window.scrollY : (window.pageYOffset ? window.pageYOffset : 0);
900!
1923
    }
1924

1925
    protected getWindowScrollLeft() {
1926
        return window.scrollX ? window.scrollX : (window.pageXOffset ? window.pageXOffset : 0);
900!
1927
    }
1928

1929
    protected isDragLinked(drag: IgxDragDirective): boolean {
1930
        const dragLinkArray = drag.dragChannel instanceof Array;
263✔
1931
        const dropLinkArray = this.dropChannel instanceof Array;
263✔
1932

1933
        if (!dragLinkArray && !dropLinkArray) {
263✔
1934
            return this.dropChannel === drag.dragChannel;
257✔
1935
        } else if (!dragLinkArray && dropLinkArray) {
6!
1936
            const dropLinks = this.dropChannel as any[];
×
1937
            for (const link of dropLinks) {
×
1938
                if (link === drag.dragChannel) {
×
1939
                    return true;
×
1940
                }
1941
            }
1942
        } else if (dragLinkArray && !dropLinkArray) {
6✔
1943
            const dragLinks = drag.dragChannel as any[];
3✔
1944
            for (const link of dragLinks) {
3✔
1945
                if (link === this.dropChannel) {
3✔
1946
                    return true;
3✔
1947
                }
1948
            }
1949
        } else {
1950
            const dragLinks = drag.dragChannel as any[];
3✔
1951
            const dropLinks = this.dropChannel as any[];
3✔
1952
            for (const draglink of dragLinks) {
3✔
1953
                for (const droplink of dropLinks) {
9✔
1954
                    if (draglink === droplink) {
18!
1955
                        return true;
×
1956
                    }
1957
                }
1958
            }
1959
        }
1960

1961
        return false;
3✔
1962
    }
1963

1964
    protected getInsertIndexAt(draggedDir: IgxDragDirective, elementsAtPoint: any[]): number {
1965
        let insertIndex = -1;
24✔
1966
        const dropChildren = Array.prototype.slice.call(this.element.nativeElement.children);
24✔
1967
        if (!dropChildren.length) {
24✔
1968
            return insertIndex;
2✔
1969
        }
1970

1971
        let i = 0;
22✔
1972
        let childUnder = null;
22✔
1973
        while (!childUnder && i < elementsAtPoint.length) {
22✔
1974
            if (elementsAtPoint[i].parentElement === this.element.nativeElement) {
127✔
1975
                childUnder = elementsAtPoint[i];
16✔
1976
            }
1977
            i++;
127✔
1978
        }
1979

1980
        const draggedElemIndex = dropChildren.indexOf(draggedDir.element.nativeElement);
22✔
1981
        insertIndex = dropChildren.indexOf(childUnder);
22✔
1982
        if (draggedElemIndex !== -1 && draggedElemIndex < insertIndex) {
22!
1983
            insertIndex++;
×
1984
        }
1985

1986
        return insertIndex;
22✔
1987
    }
1988
}
1989

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