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

IgniteUI / igniteui-angular / 15877151967

25 Jun 2025 01:05PM UTC coverage: 91.418% (-0.003%) from 91.421%
15877151967

Pull #15995

github

web-flow
Merge b90c4280b into cac01386a
Pull Request #15995: fix(grid): Cancel endEdit if there is no active editing

13390 of 15719 branches covered (85.18%)

2 of 2 new or added lines in 1 file covered. (100.0%)

1 existing line in 1 file now uncovered.

27069 of 29610 relevant lines covered (91.42%)

36683.33 hits per line

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

95.38
/projects/igniteui-angular/src/lib/directives/for-of/for_of.directive.ts
1
import { NgForOfContext } from '@angular/common';
2
import {
3
    ChangeDetectorRef,
4
    ComponentRef,
5
    Directive,
6
    DoCheck,
7
    EmbeddedViewRef,
8
    EventEmitter,
9
    Input,
10
    IterableChanges,
11
    IterableDiffer,
12
    IterableDiffers,
13
    NgZone,
14
    OnChanges,
15
    OnDestroy,
16
    OnInit,
17
    Output,
18
    SimpleChanges,
19
    TemplateRef,
20
    TrackByFunction,
21
    ViewContainerRef,
22
    AfterViewInit,
23
    Inject,
24
    booleanAttribute,
25
    DOCUMENT
26
} from '@angular/core';
27

28
import { DisplayContainerComponent } from './display.container';
29
import { HVirtualHelperComponent } from './horizontal.virtual.helper.component';
30
import { VirtualHelperComponent } from './virtual.helper.component';
31

32
import { IgxForOfSyncService, IgxForOfScrollSyncService } from './for_of.sync.service';
33
import { Subject } from 'rxjs';
34
import { takeUntil, filter, throttleTime, first } from 'rxjs/operators';
35
import { getResizeObserver } from '../../core/utils';
36
import { IBaseEventArgs, PlatformUtil } from '../../core/utils';
37
import { VirtualHelperBaseDirective } from './base.helper.component';
38

39
const MAX_PERF_SCROLL_DIFF = 4;
3✔
40

41
/**
42
 *  @publicApi
43
 */
44
export class IgxForOfContext<T, U extends T[] = T[]> {
45
    constructor(
46
        public $implicit: T,
205,996✔
47
        public igxForOf: U,
205,996✔
48
        public index: number,
205,996✔
49
        public count: number
205,996✔
50
    ) { }
51

52
    /**
53
     * A function that returns whether the element is the first or not
54
     */
55
    public get first(): boolean {
56
        return this.index === 0;
200✔
57
    }
58

59
    /**
60
     * A function that returns whether the element is the last or not
61
     */
62
    public get last(): boolean {
63
        return this.index === this.count - 1;
200✔
64
    }
65

66
    /**
67
     * A function that returns whether the element is even or not
68
     */
69
    public get even(): boolean {
70
        return this.index % 2 === 0;
400✔
71
    }
72

73
    /**
74
     * A function that returns whether the element is odd or not
75
     */
76
    public get odd(): boolean {
77
        return !this.even;
200✔
78
    }
79

80
}
81

82
/** @hidden @internal */
83
export abstract class IgxForOfToken<T, U extends T[] = T[]> {
84
    public abstract igxForOf: U & T[] | null;
85
    public abstract state: IForOfState;
86
    public abstract totalItemCount: number;
87
    public abstract scrollPosition: number;
88

89
    public abstract chunkLoad: EventEmitter<IForOfState>;
90
    public abstract chunkPreload: EventEmitter<IForOfState>;
91

92
    public abstract scrollTo(index: number): void;
93
    public abstract getScrollForIndex(index: number, bottom?: boolean): number;
94
    public abstract getScroll(): HTMLElement | undefined;
95

96
    // TODO: Re-evaluate use for this internally, better expose through separate API
97
    public abstract igxForItemSize: any;
98
    public abstract igxForContainerSize: any;
99
    /** @hidden */
100
    public abstract dc: ComponentRef<any>
101
}
102

103
@Directive({
104
    selector: '[igxFor][igxForOf]',
105
    providers: [
106
        IgxForOfScrollSyncService,
107
        { provide: IgxForOfToken, useExisting: IgxForOfDirective }
108
    ],
109
    standalone: true
110
})
111
export class IgxForOfDirective<T, U extends T[] = T[]> extends IgxForOfToken<T,U> implements OnInit, OnChanges, DoCheck, OnDestroy, AfterViewInit {
3✔
112

113
    /**
114
     * Sets the data to be rendered.
115
     * ```html
116
     * <ng-template igxFor let-item [igxForOf]="data" [igxForScrollOrientation]="'horizontal'"></ng-template>
117
     * ```
118
     */
119
    @Input()
120
    public igxForOf: U & T[] | null;
121

122
    /**
123
     * Sets the property name from which to read the size in the data object.
124
     */
125
    @Input()
126
    public igxForSizePropName;
127

128
    /**
129
     * Specifies the scroll orientation.
130
     * Scroll orientation can be "vertical" or "horizontal".
131
     * ```html
132
     * <ng-template igxFor let-item [igxForOf]="data" [igxForScrollOrientation]="'horizontal'"></ng-template>
133
     * ```
134
     */
135
    @Input()
136
    public igxForScrollOrientation = 'vertical';
47,199✔
137

138
    /**
139
     * Optionally pass the parent `igxFor` instance to create a virtual template scrolling both horizontally and vertically.
140
     * ```html
141
     * <ng-template #scrollContainer igxFor let-rowData [igxForOf]="data"
142
     *       [igxForScrollOrientation]="'vertical'"
143
     *       [igxForContainerSize]="'500px'"
144
     *       [igxForItemSize]="'50px'"
145
     *       let-rowIndex="index">
146
     *       <div [style.display]="'flex'" [style.height]="'50px'">
147
     *           <ng-template #childContainer igxFor let-item [igxForOf]="data"
148
     *               [igxForScrollOrientation]="'horizontal'"
149
     *               [igxForScrollContainer]="parentVirtDir"
150
     *               [igxForContainerSize]="'500px'">
151
     *                   <div [style.min-width]="'50px'">{{rowIndex}} : {{item.text}}</div>
152
     *           </ng-template>
153
     *       </div>
154
     * </ng-template>
155
     * ```
156
     */
157
    @Input()
158
    public igxForScrollContainer: any;
159

160
    /**
161
     * Sets the px-affixed size of the container along the axis of scrolling.
162
     * For "horizontal" orientation this value is the width of the container and for "vertical" is the height.
163
     * ```html
164
     * <ng-template igxFor let-item [igxForOf]="data" [igxForContainerSize]="'500px'"
165
     *      [igxForScrollOrientation]="'horizontal'">
166
     * </ng-template>
167
     * ```
168
     */
169
    @Input()
170
    public igxForContainerSize: any;
171

172
    /**
173
     * @hidden
174
     * @internal
175
     * Initial chunk size if no container size is passed. If container size is passed then the igxForOf calculates its chunk size
176
     */
177
    @Input()
178
    public igxForInitialChunkSize: any;
179

180
    /**
181
     * Sets the px-affixed size of the item along the axis of scrolling.
182
     * For "horizontal" orientation this value is the width of the column and for "vertical" is the height or the row.
183
     * ```html
184
     * <ng-template igxFor let-item [igxForOf]="data" [igxForScrollOrientation]="'horizontal'" [igxForItemSize]="'50px'"></ng-template>
185
     * ```
186
     */
187
    @Input()
188
    public igxForItemSize: any;
189

190
    /**
191
     * An event that is emitted after a new chunk has been loaded.
192
     * ```html
193
     * <ng-template igxFor [igxForOf]="data" [igxForScrollOrientation]="'horizontal'" (chunkLoad)="loadChunk($event)"></ng-template>
194
     * ```
195
     * ```typescript
196
     * loadChunk(e){
197
     * alert("chunk loaded!");
198
     * }
199
     * ```
200
     */
201
    @Output()
202
    public chunkLoad = new EventEmitter<IForOfState>();
47,199✔
203

204
    /**
205
     * @hidden @internal
206
     * An event that is emitted when scrollbar visibility has changed.
207
     */
208
    @Output()
209
    public scrollbarVisibilityChanged = new EventEmitter<any>();
47,199✔
210

211
    /**
212
     * An event that is emitted after the rendered content size of the igxForOf has been changed.
213
     */
214
    @Output()
215
    public contentSizeChange = new EventEmitter<any>();
47,199✔
216

217
    /**
218
     * An event that is emitted after data has been changed.
219
     * ```html
220
     * <ng-template igxFor [igxForOf]="data" [igxForScrollOrientation]="'horizontal'" (dataChanged)="dataChanged($event)"></ng-template>
221
     * ```
222
     * ```typescript
223
     * dataChanged(e){
224
     * alert("data changed!");
225
     * }
226
     * ```
227
     */
228
    @Output()
229
    public dataChanged = new EventEmitter<IForOfDataChangeEventArgs>();
47,199✔
230

231
    @Output()
232
    public beforeViewDestroyed = new EventEmitter<EmbeddedViewRef<any>>();
47,199✔
233

234
    /**
235
     * An event that is emitted on chunk loading to emit the current state information - startIndex, endIndex, totalCount.
236
     * Can be used for implementing remote load on demand for the igxFor data.
237
     * ```html
238
     * <ng-template igxFor [igxForOf]="data" [igxForScrollOrientation]="'horizontal'" (chunkPreload)="chunkPreload($event)"></ng-template>
239
     * ```
240
     * ```typescript
241
     * chunkPreload(e){
242
     * alert("chunk is loading!");
243
     * }
244
     * ```
245
     */
246
    @Output()
247
    public chunkPreload = new EventEmitter<IForOfState>();
47,199✔
248

249
    /**
250
     * @hidden
251
     */
252
    public dc: ComponentRef<DisplayContainerComponent>;
253

254
    /**
255
     * The current state of the directive. It contains `startIndex` and `chunkSize`.
256
     * state.startIndex - The index of the item at which the current visible chunk begins.
257
     * state.chunkSize - The number of items the current visible chunk holds.
258
     * These options can be used when implementing remote virtualization as they provide the necessary state information.
259
     * ```typescript
260
     * const gridState = this.parentVirtDir.state;
261
     * ```
262
     */
263
    public state: IForOfState = {
47,199✔
264
        startIndex: 0,
265
        chunkSize: 0
266
    };
267

268
    protected func;
269
    protected _sizesCache: number[] = [];
47,199✔
270
    protected scrollComponent: VirtualHelperBaseDirective;
271
    protected _differ: IterableDiffer<T> | null = null;
47,199✔
272
    protected _trackByFn: TrackByFunction<T>;
273
    protected individualSizeCache: number[] = [];
47,199✔
274
    /** Internal track for scroll top that is being virtualized */
275
    protected _virtScrollPosition = 0;
47,199✔
276
    /** If the next onScroll event is triggered due to internal setting of scrollTop */
277
    protected _bScrollInternal = false;
47,199✔
278
    // End properties related to virtual height handling
279
    protected _embeddedViews: Array<EmbeddedViewRef<any>> = [];
47,199✔
280
    protected contentResizeNotify = new Subject<void>();
47,199✔
281
    protected contentObserver: ResizeObserver;
282
    /** Size that is being virtualized. */
283
    protected _virtSize = 0;
47,199✔
284
    /**
285
     * @hidden
286
     */
287
    protected destroy$ = new Subject<any>();
47,199✔
288

289
    private _totalItemCount: number = null;
47,199✔
290
    private _adjustToIndex;
291
    // Start properties related to virtual size handling due to browser limitation
292
    /** Maximum size for an element of the browser. */
293
    private _maxSize;
294
    /**
295
     * Ratio for height that's being virtualizaed and the one visible
296
     * If _virtHeightRatio = 1, the visible height and the virtualized are the same, also _maxSize > _virtHeight.
297
     */
298
    private _virtRatio = 1;
47,199✔
299

300
    /**
301
     * The total count of the virtual data items, when using remote service.
302
     * Similar to the property totalItemCount, but this will allow setting the data count into the template.
303
     * ```html
304
     * <ng-template igxFor let-item [igxForOf]="data | async" [igxForTotalItemCount]="count | async"
305
     *  [igxForContainerSize]="'500px'" [igxForItemSize]="'50px'"></ng-template>
306
     * ```
307
     */
308
    @Input()
309
    public get igxForTotalItemCount(): number {
310
        return this.totalItemCount;
×
311
    }
312
    public set igxForTotalItemCount(value: number) {
313
        this.totalItemCount = value;
2✔
314
    }
315

316
    /**
317
     * The total count of the virtual data items, when using remote service.
318
     * ```typescript
319
     * this.parentVirtDir.totalItemCount = data.Count;
320
     * ```
321
     */
322
    public get totalItemCount() {
323
        return this._totalItemCount;
922,602✔
324
    }
325

326
    public set totalItemCount(val) {
327
        if (this._totalItemCount !== val) {
20✔
328
            this._totalItemCount = val;
19✔
329
            // update sizes in case total count changes.
330
            const newSize = this.initSizesCache(this.igxForOf);
19✔
331
            const sizeDiff = this.scrollComponent.size - newSize;
19✔
332
            this.scrollComponent.size = newSize;
19✔
333
            const lastChunkExceeded = this.state.startIndex + this.state.chunkSize > val;
19✔
334
            if (lastChunkExceeded) {
19!
335
                this.state.startIndex = val - this.state.chunkSize;
×
336
            }
337
            this._adjustScrollPositionAfterSizeChange(sizeDiff);
19✔
338
        }
339
    }
340

341
    public get displayContainer(): HTMLElement | undefined {
342
        return this.dc?.instance?._viewContainer?.element?.nativeElement;
6,994✔
343
    }
344

345
    public get virtualHelper() {
346
        return this.scrollComponent.nativeElement;
×
347
    }
348

349
    /**
350
     * @hidden
351
     */
352
    public get isRemote(): boolean {
353
        return this.totalItemCount !== null;
918,584✔
354
    }
355

356
    /**
357
     *
358
     * Gets/Sets the scroll position.
359
     * ```typescript
360
     * const position = directive.scrollPosition;
361
     * directive.scrollPosition = value;
362
     * ```
363
     */
364
    public get scrollPosition(): number {
365
        return this.scrollComponent.scrollAmount;
231,367✔
366
    }
367
    public set scrollPosition(val: number) {
368
        if (val === this.scrollComponent.scrollAmount) {
44,945✔
369
            return;
44,178✔
370
        }
371
        if (this.igxForScrollOrientation === 'horizontal' && this.scrollComponent) {
767✔
372
            this.scrollComponent.nativeElement.scrollLeft = this.isRTL ? -val : val;
234!
373
        } else if (this.scrollComponent) {
533✔
374
            this.scrollComponent.nativeElement.scrollTop = val;
533✔
375
        }
376
    }
377

378
    /**
379
     * @hidden
380
     */
381
    protected get isRTL() {
382
        const dir = window.getComputedStyle(this.dc.instance._viewContainer.element.nativeElement).getPropertyValue('direction');
234✔
383
        return dir === 'rtl';
234✔
384
    }
385

386
    protected get sizesCache(): number[] {
387
        return this._sizesCache;
3,895,702✔
388
    }
389
    protected set sizesCache(value: number[]) {
390
        this._sizesCache = value;
3,286✔
391
    }
392

393
    private get _isScrolledToBottom() {
394
        if (!this.getScroll()) {
404!
395
            return true;
×
396
        }
397
        const scrollHeight = this.getScroll().scrollHeight;
404✔
398
        // Use === and not >= because `scrollTop + container size` can't be bigger than `scrollHeight`, unless something isn't updated.
399
        // Also use Math.round because Chrome has some inconsistencies and `scrollTop + container` can be float when zooming the page.
400
        return Math.round(this.getScroll().scrollTop + this.igxForContainerSize) === scrollHeight;
404✔
401
    }
402

403
    private get _isAtBottomIndex() {
404
        return this.igxForOf && this.state.startIndex + this.state.chunkSize > this.igxForOf.length;
9✔
405
    }
406

407
    constructor(
408
        private _viewContainer: ViewContainerRef,
47,199✔
409
        protected _template: TemplateRef<NgForOfContext<T>>,
47,199✔
410
        protected _differs: IterableDiffers,
47,199✔
411
        public cdr: ChangeDetectorRef,
47,199✔
412
        protected _zone: NgZone,
47,199✔
413
        protected syncScrollService: IgxForOfScrollSyncService,
47,199✔
414
        protected platformUtil: PlatformUtil,
47,199✔
415
        @Inject(DOCUMENT)
416
        protected document: any,
47,199✔
417
    ) {
418
        super();
47,199✔
419
    }
420

421
    public verticalScrollHandler(event) {
422
        this.onScroll(event);
48✔
423
    }
424

425
    public isScrollable() {
426
        return this.scrollComponent.size > parseInt(this.igxForContainerSize, 10);
260,721✔
427
    }
428

429
    /**
430
     * @hidden
431
     */
432
    public ngOnInit(): void {
433
        const vc = this.igxForScrollContainer ? this.igxForScrollContainer._viewContainer : this._viewContainer;
47,199✔
434
        this.igxForSizePropName = this.igxForSizePropName || 'width';
47,199✔
435
        this.dc = this._viewContainer.createComponent(DisplayContainerComponent, { index: 0 });
47,199✔
436
        this.dc.instance.scrollDirection = this.igxForScrollOrientation;
47,199✔
437
        if (this.igxForOf && this.igxForOf.length) {
47,199✔
438
            this.scrollComponent = this.syncScrollService.getScrollMaster(this.igxForScrollOrientation);
39,305✔
439
            this.state.chunkSize = this._calculateChunkSize();
39,305✔
440
            this.dc.instance.notVirtual = !(this.igxForContainerSize && this.state.chunkSize < this.igxForOf.length);
39,305✔
441
            if (this.scrollComponent && !this.scrollComponent.destroyed) {
39,305✔
442
                this.state.startIndex = Math.min(this.getIndexAt(this.scrollPosition, this.sizesCache),
31,981✔
443
                    this.igxForOf.length - this.state.chunkSize);
444
            }
445
            for (let i = this.state.startIndex; i < this.state.startIndex + this.state.chunkSize &&
39,305✔
446
                this.igxForOf[i] !== undefined; i++) {
447
                const input = this.igxForOf[i];
160,213✔
448
                const embeddedView = this.dc.instance._vcr.createEmbeddedView(
160,213✔
449
                    this._template,
450
                    new IgxForOfContext<T, U>(input, this.igxForOf, this.getContextIndex(input), this.igxForOf.length)
451
                );
452
                this._embeddedViews.push(embeddedView);
160,213✔
453
            }
454
        }
455
        this._maxSize = this._calcMaxBrowserSize();
47,199✔
456
        if (this.igxForScrollOrientation === 'vertical') {
47,199✔
457
            this.dc.instance._viewContainer.element.nativeElement.style.top = '0px';
11,833✔
458
            this.scrollComponent = this.syncScrollService.getScrollMaster(this.igxForScrollOrientation);
11,833✔
459
            if (!this.scrollComponent || this.scrollComponent.destroyed) {
11,833✔
460
                this.scrollComponent = vc.createComponent(VirtualHelperComponent).instance;
4,463✔
461
            }
462

463
            this.scrollComponent.size = this.igxForOf ? this._calcSize() : 0;
11,833✔
464
            this.syncScrollService.setScrollMaster(this.igxForScrollOrientation, this.scrollComponent);
11,833✔
465
            this._zone.runOutsideAngular(() => {
11,833✔
466
                this.verticalScrollHandler = this.verticalScrollHandler.bind(this);
11,833✔
467
                this.scrollComponent.nativeElement.addEventListener('scroll', this.verticalScrollHandler);
11,833✔
468
                this.dc.instance.scrollContainer = this.scrollComponent.nativeElement;
11,833✔
469
            });
470
            const destructor = takeUntil<any>(this.destroy$);
11,833✔
471
            this.contentResizeNotify.pipe(
11,833✔
472
                filter(() => this.igxForContainerSize && this.igxForOf && this.igxForOf.length > 0),
5,451✔
473
                throttleTime(40, undefined, { leading: false, trailing: true }),
474
                destructor
475
            ).subscribe(() => this._zone.runTask(() => this.updateSizes()));
877✔
476
        }
477

478
        if (this.igxForScrollOrientation === 'horizontal') {
47,199✔
479
            this.func = (evt) => this.onHScroll(evt);
35,366✔
480
            this.scrollComponent = this.syncScrollService.getScrollMaster(this.igxForScrollOrientation);
35,366✔
481
            if (!this.scrollComponent) {
35,366✔
482
                this.scrollComponent = vc.createComponent(HVirtualHelperComponent).instance;
3,600✔
483
                this.scrollComponent.size = this.igxForOf ? this._calcSize() : 0;
3,600!
484
                this.syncScrollService.setScrollMaster(this.igxForScrollOrientation, this.scrollComponent);
3,600✔
485
                this._zone.runOutsideAngular(() => {
3,600✔
486
                    this.scrollComponent.nativeElement.addEventListener('scroll', this.func);
3,600✔
487
                    this.dc.instance.scrollContainer = this.scrollComponent.nativeElement;
3,600✔
488
                });
489
            } else {
490
                this._zone.runOutsideAngular(() => {
31,766✔
491
                    this.scrollComponent.nativeElement.addEventListener('scroll', this.func);
31,766✔
492
                    this.dc.instance.scrollContainer = this.scrollComponent.nativeElement;
31,766✔
493
                });
494
            }
495
            this._updateScrollOffset();
35,366✔
496
        }
497
    }
498

499
    public ngAfterViewInit(): void {
500
        if (this.igxForScrollOrientation === 'vertical') {
47,199✔
501
            this._zone.runOutsideAngular(() => {
11,833✔
502
                if (this.platformUtil.isBrowser) {
11,833✔
503
                    this.contentObserver = new (getResizeObserver())(() => this.contentResizeNotify.next());
11,833✔
504
                    this.contentObserver.observe(this.dc.instance._viewContainer.element.nativeElement);
11,833✔
505
                }
506
            });
507
        }
508
    }
509

510
    /**
511
     * @hidden
512
     */
513
    public ngOnDestroy() {
514
        this.removeScrollEventListeners();
46,759✔
515
        this.destroy$.next(true);
46,759✔
516
        this.destroy$.complete();
46,759✔
517
        if (this.contentObserver) {
46,759✔
518
            this.contentObserver.disconnect();
11,740✔
519
        }
520
    }
521

522
    /**
523
     * @hidden @internal
524
     * Asserts the correct type of the context for the template that `igxForOf` will render.
525
     *
526
     * The presence of this method is a signal to the Ivy template type-check compiler that the
527
     * `IgxForOf` structural directive renders its template with a specific context type.
528
     */
529
    public static ngTemplateContextGuard<T, U extends T[]>(dir: IgxForOfDirective<T, U>, ctx: any):
530
        ctx is IgxForOfContext<T, U> {
531
        return true;
×
532
    }
533

534
    /**
535
     * @hidden
536
     */
537
    public ngOnChanges(changes: SimpleChanges): void {
538
        const forOf = 'igxForOf';
2,661✔
539
        if (forOf in changes) {
2,661✔
540
            const value = changes[forOf].currentValue;
2,170✔
541
            if (!this._differ && value) {
2,170✔
542
                try {
1,164✔
543
                    this._differ = this._differs.find(value).create(this.igxForTrackBy);
1,164✔
544
                } catch (e) {
545
                    throw new Error(
×
546
                        `Cannot find a differ supporting object "${value}" of type "${getTypeNameForDebugging(value)}".
547
                     NgFor only supports binding to Iterables such as Arrays.`);
548
                }
549
            }
550
        }
551
        const defaultItemSize = 'igxForItemSize';
2,661✔
552
        if (defaultItemSize in changes && !changes[defaultItemSize].firstChange && this.igxForOf) {
2,661✔
553
            // handle default item size changed.
554
            this.initSizesCache(this.igxForOf);
283✔
555
            this._applyChanges();
283✔
556
        }
557
        const containerSize = 'igxForContainerSize';
2,661✔
558
        if (containerSize in changes && !changes[containerSize].firstChange && this.igxForOf) {
2,661✔
559
            const prevSize = parseInt(changes[containerSize].previousValue, 10);
388✔
560
            const newSize = parseInt(changes[containerSize].currentValue, 10);
388✔
561
            this._recalcOnContainerChange({prevSize, newSize});
388✔
562
        }
563
    }
564

565
    /**
566
     * @hidden
567
     */
568
    public ngDoCheck(): void {
569
        if (this._differ) {
13,116✔
570
            const changes = this._differ.diff(this.igxForOf);
13,116✔
571
            if (changes) {
13,116✔
572
                //  re-init cache.
573
                if (!this.igxForOf) {
1,377✔
574
                    this.igxForOf = [] as U;
1✔
575
                }
576
                this._updateSizeCache();
1,377✔
577
                this._zone.run(() => {
1,377✔
578
                    this._applyChanges();
1,377✔
579
                    this.cdr.markForCheck();
1,377✔
580
                    this._updateScrollOffset();
1,377✔
581
                    const args: IForOfDataChangingEventArgs = {
1,377✔
582
                        containerSize: this.igxForContainerSize,
583
                        state: this.state
584
                    };
585
                    this.dataChanged.emit(args);
1,377✔
586
                });
587
            }
588
        }
589
    }
590

591

592
    /**
593
     * Shifts the scroll thumb position.
594
     * ```typescript
595
     * this.parentVirtDir.addScroll(5);
596
     * ```
597
     *
598
     * @param addTop negative value to scroll up and positive to scroll down;
599
     */
600
    public addScrollTop(add: number): boolean {
601
        return this.addScroll(add);
27✔
602
    }
603

604
    /**
605
     * Shifts the scroll thumb position.
606
     * ```typescript
607
     * this.parentVirtDir.addScroll(5);
608
     * ```
609
     *
610
     * @param add negative value to scroll previous and positive to scroll next;
611
     */
612
    public addScroll(add: number): boolean {
613
        if (add === 0) {
59!
614
            return false;
×
615
        }
616
        const originalVirtScrollTop = this._virtScrollPosition;
59✔
617
        const containerSize = parseInt(this.igxForContainerSize, 10);
59✔
618
        const maxVirtScrollTop = this._virtSize - containerSize;
59✔
619

620
        this._bScrollInternal = true;
59✔
621
        this._virtScrollPosition += add;
59✔
622
        this._virtScrollPosition = this._virtScrollPosition > 0 ?
59✔
623
            (this._virtScrollPosition < maxVirtScrollTop ? this._virtScrollPosition : maxVirtScrollTop) :
57✔
624
            0;
625

626
        this.scrollPosition += add / this._virtRatio;
59✔
627
        if (Math.abs(add / this._virtRatio) < 1) {
59!
628
            // Actual scroll delta that was added is smaller than 1 and onScroll handler doesn't trigger when scrolling < 1px
629
            const scrollOffset = this.fixedUpdateAllElements(this._virtScrollPosition);
×
630
            // scrollOffset = scrollOffset !== parseInt(this.igxForItemSize, 10) ? scrollOffset : 0;
631
            this.dc.instance._viewContainer.element.nativeElement.style.top = -(scrollOffset) + 'px';
×
632
        }
633

634
        const maxRealScrollTop = this.scrollComponent.nativeElement.scrollHeight - containerSize;
59✔
635
        if ((this._virtScrollPosition > 0 && this.scrollPosition === 0) ||
59✔
636
            (this._virtScrollPosition < maxVirtScrollTop && this.scrollPosition === maxRealScrollTop)) {
637
            // Actual scroll position is at the top or bottom, but virtual one is not at the top or bottom (there's more to scroll)
638
            // Recalculate actual scroll position based on the virtual scroll.
639
            this.scrollPosition = this._virtScrollPosition / this._virtRatio;
21✔
640
        } else if (this._virtScrollPosition === 0 && this.scrollPosition > 0) {
38!
641
            // Actual scroll position is not at the top, but virtual scroll is. Just update the actual scroll
UNCOV
642
            this.scrollPosition = 0;
×
643
        } else if (this._virtScrollPosition === maxVirtScrollTop && this.scrollPosition < maxRealScrollTop) {
38✔
644
            // Actual scroll position is not at the bottom, but virtual scroll is. Just update the acual scroll
645
            this.scrollPosition = maxRealScrollTop;
12✔
646
        }
647
        return this._virtScrollPosition !== originalVirtScrollTop;
59✔
648
    }
649

650
    /**
651
     * Scrolls to the specified index.
652
     * ```typescript
653
     * this.parentVirtDir.scrollTo(5);
654
     * ```
655
     *
656
     * @param index
657
     */
658
    public scrollTo(index: number) {
659
        if (index < 0 || index > (this.isRemote ? this.totalItemCount : this.igxForOf.length) - 1) {
758✔
660
            return;
16✔
661
        }
662
        const containerSize = parseInt(this.igxForContainerSize, 10);
742✔
663
        const isPrevItem = index < this.state.startIndex || this.scrollPosition > this.sizesCache[index];
742✔
664
        let nextScroll = isPrevItem ? this.sizesCache[index] : this.sizesCache[index + 1] - containerSize;
742✔
665
        if (nextScroll < 0) {
742✔
666
            return;
330✔
667
        }
668
        const maxVirtScrollTop = this._virtSize - containerSize;
412✔
669
        if (nextScroll > maxVirtScrollTop) {
412!
670
            nextScroll = maxVirtScrollTop;
×
671
        }
672
        this._bScrollInternal = true;
412✔
673
        this._virtScrollPosition = nextScroll;
412✔
674
        this.scrollPosition = this._virtScrollPosition / this._virtRatio;
412✔
675
        this._adjustToIndex = !isPrevItem ? index : null;
412✔
676
    }
677

678
    /**
679
     * Scrolls by one item into the appropriate next direction.
680
     * For "horizontal" orientation that will be the right column and for "vertical" that is the lower row.
681
     * ```typescript
682
     * this.parentVirtDir.scrollNext();
683
     * ```
684
     */
685
    public scrollNext() {
686
        const scr = Math.abs(Math.ceil(this.scrollPosition));
5✔
687
        const endIndex = this.getIndexAt(scr + parseInt(this.igxForContainerSize, 10), this.sizesCache);
5✔
688
        this.scrollTo(endIndex);
5✔
689
    }
690

691
    /**
692
     * Scrolls by one item into the appropriate previous direction.
693
     * For "horizontal" orientation that will be the left column and for "vertical" that is the upper row.
694
     * ```typescript
695
     * this.parentVirtDir.scrollPrev();
696
     * ```
697
     */
698
    public scrollPrev() {
699
        this.scrollTo(this.state.startIndex - 1);
2✔
700
    }
701

702
    /**
703
     * Scrolls by one page into the appropriate next direction.
704
     * For "horizontal" orientation that will be one view to the right and for "vertical" that is one view to the bottom.
705
     * ```typescript
706
     * this.parentVirtDir.scrollNextPage();
707
     * ```
708
     */
709
    public scrollNextPage() {
710
        this.addScroll(parseInt(this.igxForContainerSize, 10));
2✔
711
    }
712

713
    /**
714
     * Scrolls by one page into the appropriate previous direction.
715
     * For "horizontal" orientation that will be one view to the left and for "vertical" that is one view to the top.
716
     * ```typescript
717
     * this.parentVirtDir.scrollPrevPage();
718
     * ```
719
     */
720
    public scrollPrevPage() {
721
        const containerSize = (parseInt(this.igxForContainerSize, 10));
2✔
722
        this.addScroll(-containerSize);
2✔
723
    }
724

725
    /**
726
     * @hidden
727
     */
728
    public getColumnScrollLeft(colIndex) {
729
        return this.sizesCache[colIndex];
3,993✔
730
    }
731

732
    /**
733
     * Returns the total number of items that are fully visible.
734
     * ```typescript
735
     * this.parentVirtDir.getItemCountInView();
736
     * ```
737
     */
738
    public getItemCountInView() {
739
        let startIndex = this.getIndexAt(this.scrollPosition, this.sizesCache);
2✔
740
        if (this.scrollPosition - this.sizesCache[startIndex] > 0) {
2✔
741
            // fisrt item is not fully in view
742
            startIndex++;
2✔
743
        }
744
        const endIndex = this.getIndexAt(this.scrollPosition + parseInt(this.igxForContainerSize, 10), this.sizesCache);
2✔
745
        return endIndex - startIndex;
2✔
746
    }
747

748
    /**
749
     * Returns a reference to the scrollbar DOM element.
750
     * This is either a vertical or horizontal scrollbar depending on the specified igxForScrollOrientation.
751
     * ```typescript
752
     * dir.getScroll();
753
     * ```
754
     */
755
    public getScroll() {
756
        return this.scrollComponent?.nativeElement;
18,618✔
757
    }
758
    /**
759
     * Returns the size of the element at the specified index.
760
     * ```typescript
761
     * this.parentVirtDir.getSizeAt(1);
762
     * ```
763
     */
764
    public getSizeAt(index: number) {
765
        return this.sizesCache[index + 1] - this.sizesCache[index];
1,137✔
766
    }
767

768
    /**
769
     * @hidden
770
     * Function that is called to get the native scrollbar size that the browsers renders.
771
     */
772
    public getScrollNativeSize() {
773
        return this.scrollComponent ? this.scrollComponent.scrollNativeSize : 0;
187,941!
774
    }
775

776
    /**
777
     * Returns the scroll offset of the element at the specified index.
778
     * ```typescript
779
     * this.parentVirtDir.getScrollForIndex(1);
780
     * ```
781
     */
782
    public getScrollForIndex(index: number, bottom?: boolean) {
783
        const containerSize = parseInt(this.igxForContainerSize, 10);
127✔
784
        const scroll = bottom ? Math.max(0, this.sizesCache[index + 1] - containerSize) : this.sizesCache[index];
127✔
785
        return scroll;
127✔
786
    }
787

788
    /**
789
     * Returns the index of the element at the specified offset.
790
     * ```typescript
791
     * this.parentVirtDir.getIndexAtScroll(100);
792
     * ```
793
     */
794
    public getIndexAtScroll(scrollOffset: number) {
795
        return this.getIndexAt(scrollOffset, this.sizesCache);
3✔
796
    }
797
    /**
798
     * Returns whether the target index is outside the view.
799
     * ```typescript
800
     * this.parentVirtDir.isIndexOutsideView(10);
801
     * ```
802
     */
803
    public isIndexOutsideView(index: number) {
804
        const targetNode = index >= this.state.startIndex && index <= this.state.startIndex + this.state.chunkSize ?
17!
805
            this._embeddedViews.map(view =>
806
                view.rootNodes.find(node => node.nodeType === Node.ELEMENT_NODE) || view.rootNodes[0].nextElementSibling)[index - this.state.startIndex] : null;
169!
807
        const rowHeight = this.getSizeAt(index);
17✔
808
        const containerSize = parseInt(this.igxForContainerSize, 10);
17✔
809
        const containerOffset = -(this.scrollPosition - this.sizesCache[this.state.startIndex]);
17✔
810
        const endTopOffset = targetNode ? targetNode.offsetTop + rowHeight + containerOffset : containerSize + rowHeight;
17!
811
        return !targetNode || targetNode.offsetTop < Math.abs(containerOffset)
17✔
812
            || containerSize && endTopOffset - containerSize > 5;
813
    }
814

815
    /**
816
     * @hidden
817
     * Function that recalculates and updates cache sizes.
818
     */
819
    public recalcUpdateSizes() {
820
        const dimension = this.igxForScrollOrientation === 'horizontal' ?
2,367✔
821
            this.igxForSizePropName : 'height';
822
        const diffs = [];
2,367✔
823
        let totalDiff = 0;
2,367✔
824
        const l = this._embeddedViews.length;
2,367✔
825
        const rNodes = this._embeddedViews.map(view =>
2,367✔
826
            view.rootNodes.find(node => node.nodeType === Node.ELEMENT_NODE) || view.rootNodes[0].nextElementSibling);
20,784✔
827
        for (let i = 0; i < l; i++) {
2,367✔
828
            const rNode = rNodes[i];
13,349✔
829
            if (rNode) {
13,349✔
830
                const height = window.getComputedStyle(rNode).getPropertyValue('height');
13,344✔
831
                const h = parseFloat(height) || parseInt(this.igxForItemSize, 10);
13,344✔
832
                const index = this.state.startIndex + i;
13,344✔
833
                if (!this.isRemote && !this.igxForOf[index]) {
13,344✔
834
                    continue;
15✔
835
                }
836
                const margin = this.getMargin(rNode, dimension);
13,329✔
837
                const oldVal = this.individualSizeCache[index];
13,329✔
838
                const newVal = (dimension === 'height' ? h : rNode.clientWidth) + margin;
13,329✔
839
                this.individualSizeCache[index] = newVal;
13,329✔
840
                const currDiff = newVal - oldVal;
13,329✔
841
                diffs.push(currDiff);
13,329✔
842
                totalDiff += currDiff;
13,329✔
843
                this.sizesCache[index + 1] = (this.sizesCache[index] || 0) + newVal;
13,329✔
844
            }
845
        }
846
        // update cache
847
        if (Math.abs(totalDiff) > 0) {
2,367✔
848
            for (let j = this.state.startIndex + this.state.chunkSize + 1; j < this.sizesCache.length; j++) {
404✔
849
                this.sizesCache[j] = (this.sizesCache[j] || 0) + totalDiff;
8,588!
850
            }
851

852
            // update scrBar heights/widths
853
            const reducer = (acc, val) => acc + val;
14,647✔
854

855
            const hSum = this.individualSizeCache.reduce(reducer);
404✔
856
            if (hSum > this._maxSize) {
404!
857
                this._virtRatio = hSum / this._maxSize;
×
858
            }
859
            this.scrollComponent.size = Math.min(this.scrollComponent.size + totalDiff, this._maxSize);
404✔
860
            this._virtSize = hSum;
404✔
861
            if (!this.scrollComponent.destroyed) {
404✔
862
                this.scrollComponent.cdr.detectChanges();
400✔
863
            }
864
            const scrToBottom = this._isScrolledToBottom && !this.dc.instance.notVirtual;
404✔
865
            if (scrToBottom && !this._isAtBottomIndex) {
404✔
866
                const containerSize = parseInt(this.igxForContainerSize, 10);
8✔
867
                const maxVirtScrollTop = this._virtSize - containerSize;
8✔
868
                this._bScrollInternal = true;
8✔
869
                this._virtScrollPosition = maxVirtScrollTop;
8✔
870
                this.scrollPosition = maxVirtScrollTop;
8✔
871
                return;
8✔
872
            }
873
            if (this._adjustToIndex) {
396✔
874
                // in case scrolled to specific index where after scroll heights are changed
875
                // need to adjust the offsets so that item is last in view.
876
                const updatesToIndex = this._adjustToIndex - this.state.startIndex + 1;
40✔
877
                const sumDiffs = diffs.slice(0, updatesToIndex).reduce(reducer);
40✔
878
                if (sumDiffs !== 0) {
40✔
879
                    this.addScroll(sumDiffs);
28✔
880
                }
881
                this._adjustToIndex = null;
40✔
882
            }
883
        }
884
    }
885

886
    /**
887
     * @hidden
888
     * Reset scroll position.
889
     * Needed in case scrollbar is hidden/detached but we still need to reset it.
890
     */
891
    public resetScrollPosition() {
892
        this.scrollPosition = 0;
44,074✔
893
        this.scrollComponent.scrollAmount = 0;
44,074✔
894
    }
895

896
    /**
897
     * @hidden
898
     */
899
    protected removeScrollEventListeners() {
900
        if (this.igxForScrollOrientation === 'horizontal') {
92,794✔
901
            this._zone.runOutsideAngular(() => this.scrollComponent?.nativeElement?.removeEventListener('scroll', this.func));
70,111✔
902
        } else {
903
            this._zone.runOutsideAngular(() =>
22,683✔
904
                this.scrollComponent?.nativeElement?.removeEventListener('scroll', this.verticalScrollHandler)
22,683✔
905
            );
906
        }
907
    }
908

909
    /**
910
     * @hidden
911
     * Function that is called when scrolling vertically
912
     */
913
    protected onScroll(event) {
914
        /* in certain situations this may be called when no scrollbar is visible */
915
        if (!parseInt(this.scrollComponent.nativeElement.style.height, 10)) {
69!
916
            return;
×
917
        }
918
        if (!this._bScrollInternal) {
69✔
919
            this._calcVirtualScrollPosition(event.target.scrollTop);
35✔
920
        } else {
921
            this._bScrollInternal = false;
34✔
922
        }
923
        const prevStartIndex = this.state.startIndex;
69✔
924
        const scrollOffset = this.fixedUpdateAllElements(this._virtScrollPosition);
69✔
925

926
        this.dc.instance._viewContainer.element.nativeElement.style.top = -(scrollOffset) + 'px';
69✔
927

928
        this._zone.onStable.pipe(first()).subscribe(this.recalcUpdateSizes.bind(this));
69✔
929

930
        this.dc.changeDetectorRef.detectChanges();
69✔
931
        if (prevStartIndex !== this.state.startIndex) {
69✔
932
            this.chunkLoad.emit(this.state);
55✔
933
        }
934
    }
935

936

937
    /**
938
     * @hidden
939
     * @internal
940
     */
941
    public updateScroll(): void {
942
        if (this.igxForScrollOrientation === "horizontal") {
869✔
943
            const scrollAmount = this.scrollComponent.nativeElement["scrollLeft"];
869✔
944
            this.scrollComponent.scrollAmount = scrollAmount;
869✔
945
            this._updateScrollOffset();
869✔
946
        }
947
    }
948

949
    protected updateSizes() {
950
        if (!this.scrollComponent.nativeElement.isConnected) return;
877✔
951
        const scrollable = this.isScrollable();
800✔
952
        this.recalcUpdateSizes();
800✔
953
        this._applyChanges();
800✔
954
        this._updateScrollOffset();
800✔
955
        if (scrollable !== this.isScrollable()) {
800✔
956
            this.scrollbarVisibilityChanged.emit();
11✔
957
        } else {
958
            this.contentSizeChange.emit();
789✔
959
        }
960
    }
961

962
    /**
963
     * @hidden
964
     */
965
    protected fixedUpdateAllElements(inScrollTop: number): number {
966
        const count = this.isRemote ? this.totalItemCount : this.igxForOf.length;
3,027✔
967
        let newStart = this.getIndexAt(inScrollTop, this.sizesCache);
3,027✔
968

969
        if (newStart + this.state.chunkSize > count) {
3,027✔
970
            newStart = count - this.state.chunkSize;
822✔
971
        }
972

973
        const prevStart = this.state.startIndex;
3,027✔
974
        const diff = newStart - this.state.startIndex;
3,027✔
975
        this.state.startIndex = newStart;
3,027✔
976

977
        if (diff) {
3,027✔
978
            this.chunkPreload.emit(this.state);
958✔
979
            if (!this.isRemote) {
958✔
980

981
                // recalculate and apply page size.
982
                if (diff && Math.abs(diff) <= MAX_PERF_SCROLL_DIFF) {
947✔
983
                    if (diff > 0) {
706✔
984
                        this.moveApplyScrollNext(prevStart);
501✔
985
                    } else {
986
                        this.moveApplyScrollPrev(prevStart);
205✔
987
                    }
988
                } else {
989
                    this.fixedApplyScroll();
241✔
990
                }
991
            }
992
        }
993

994
        return inScrollTop - this.sizesCache[this.state.startIndex];
3,027✔
995
    }
996

997
    /**
998
     * @hidden
999
     * The function applies an optimized state change for scrolling down/right employing context change with view rearrangement
1000
     */
1001
    protected moveApplyScrollNext(prevIndex: number): void {
1002
        const start = prevIndex + this.state.chunkSize;
501✔
1003
        const end = start + this.state.startIndex - prevIndex;
501✔
1004
        const container = this.dc.instance._vcr as ViewContainerRef;
501✔
1005

1006
        for (let i = start; i < end && this.igxForOf[i] !== undefined; i++) {
501✔
1007
            const embView = this._embeddedViews.shift();
819✔
1008
            if (!embView.destroyed) {
819✔
1009
                this.scrollFocus(embView.rootNodes.find(node => node.nodeType === Node.ELEMENT_NODE)
1,408!
1010
                    || embView.rootNodes[0].nextElementSibling);
1011
                const view = container.detach(0);
819✔
1012

1013
                this.updateTemplateContext(embView.context, i);
819✔
1014
                container.insert(view);
819✔
1015
                this._embeddedViews.push(embView);
819✔
1016
            }
1017
        }
1018
    }
1019

1020
    /**
1021
     * @hidden
1022
     * The function applies an optimized state change for scrolling up/left employing context change with view rearrangement
1023
     */
1024
    protected moveApplyScrollPrev(prevIndex: number): void {
1025
        const container = this.dc.instance._vcr as ViewContainerRef;
205✔
1026
        for (let i = prevIndex - 1; i >= this.state.startIndex && this.igxForOf[i] !== undefined; i--) {
205✔
1027
            const embView = this._embeddedViews.pop();
251✔
1028
            if (!embView.destroyed) {
251✔
1029
                this.scrollFocus(embView.rootNodes.find(node => node.nodeType === Node.ELEMENT_NODE)
536!
1030
                    || embView.rootNodes[0].nextElementSibling);
1031
                const view = container.detach(container.length - 1);
251✔
1032

1033
                this.updateTemplateContext(embView.context, i);
251✔
1034
                container.insert(view, 0);
251✔
1035
                this._embeddedViews.unshift(embView);
251✔
1036
            }
1037
        }
1038
    }
1039

1040
    /**
1041
     * @hidden
1042
     */
1043
    protected getContextIndex(input) {
1044
        return this.isRemote ? this.state.startIndex + this.igxForOf.indexOf(input) : this.igxForOf.indexOf(input);
468,618✔
1045
    }
1046

1047
    /**
1048
     * @hidden
1049
     * Function which updates the passed context of an embedded view with the provided index
1050
     * from the view container.
1051
     * Often, called while handling a scroll event.
1052
     */
1053
    protected updateTemplateContext(context: any, index = 0): void {
×
1054
        context.$implicit = this.igxForOf[index];
262,622✔
1055
        context.index = this.getContextIndex(this.igxForOf[index]);
262,622✔
1056
        context.count = this.igxForOf.length;
262,622✔
1057
    }
1058

1059
    /**
1060
     * @hidden
1061
     * The function applies an optimized state change through context change for each view
1062
     */
1063
    protected fixedApplyScroll(): void {
1064
        let j = 0;
241✔
1065
        const endIndex = this.state.startIndex + this.state.chunkSize;
241✔
1066
        for (let i = this.state.startIndex; i < endIndex && this.igxForOf[i] !== undefined; i++) {
241✔
1067
            const embView = this._embeddedViews[j++];
1,590✔
1068
            this.updateTemplateContext(embView.context, i);
1,590✔
1069
        }
1070
    }
1071

1072
    /**
1073
     * @hidden
1074
     * @internal
1075
     *
1076
     * Clears focus inside the virtualized container on small scroll swaps.
1077
     */
1078
    protected scrollFocus(node?: HTMLElement): void {
1079
        if (!node) {
1,070!
1080
            return;
×
1081
        }
1082
        const document = node.getRootNode() as Document | ShadowRoot;
1,070✔
1083
        const activeElement = document.activeElement as HTMLElement;
1,070✔
1084

1085
        // Remove focus in case the the active element is inside the view container.
1086
        // Otherwise we hit an exception while doing the 'small' scrolls swapping.
1087
        // For more information:
1088
        //
1089
        // https://developer.mozilla.org/en-US/docs/Web/API/Node/removeChild
1090
        // https://bugs.chromium.org/p/chromium/issues/detail?id=432392
1091
        if (node && node.contains(activeElement)) {
1,070!
1092
            activeElement.blur();
×
1093
        }
1094
    }
1095

1096
    /**
1097
     * @hidden
1098
     * Function that is called when scrolling horizontally
1099
     */
1100
    protected onHScroll(event) {
1101
        /* in certain situations this may be called when no scrollbar is visible */
1102
        const firstScrollChild = this.scrollComponent.nativeElement.children.item(0) as HTMLElement;
107✔
1103
        if (!parseInt(firstScrollChild.style.width, 10)) {
107!
1104
            return;
×
1105
        }
1106
        if (!this._bScrollInternal) {
107!
1107
            this._calcVirtualScrollPosition(event.target.scrollLeft);
107✔
1108
        } else {
1109
            this._bScrollInternal = false;
×
1110
        }
1111
        const prevStartIndex = this.state.startIndex;
107✔
1112
        const scrLeft = event.target.scrollLeft;
107✔
1113
        // Updating horizontal chunks
1114
        const scrollOffset = this.fixedUpdateAllElements(Math.abs(this._virtScrollPosition));
107✔
1115
        if (scrLeft < 0) {
107✔
1116
            // RTL
1117
            this.dc.instance._viewContainer.element.nativeElement.style.left = scrollOffset + 'px';
10✔
1118
        } else {
1119
            this.dc.instance._viewContainer.element.nativeElement.style.left = -scrollOffset + 'px';
97✔
1120
        }
1121
        this._zone.onStable.pipe(first()).subscribe(this.recalcUpdateSizes.bind(this));
107✔
1122

1123
        this.dc.changeDetectorRef.detectChanges();
107✔
1124
        if (prevStartIndex !== this.state.startIndex) {
107✔
1125
            this.chunkLoad.emit(this.state);
70✔
1126
        }
1127
    }
1128

1129
    /**
1130
     * Gets the function used to track changes in the items collection.
1131
     * By default the object references are compared. However this can be optimized if you have unique identifier
1132
     * value that can be used for the comparison instead of the object ref or if you have some other property values
1133
     * in the item object that should be tracked for changes.
1134
     * This option is similar to ngForTrackBy.
1135
     * ```typescript
1136
     * const trackFunc = this.parentVirtDir.igxForTrackBy;
1137
     * ```
1138
     */
1139
    @Input()
1140
    public get igxForTrackBy(): TrackByFunction<T> {
1141
        return this._trackByFn;
47,196✔
1142
    }
1143

1144
    /**
1145
     * Sets the function used to track changes in the items collection.
1146
     * This function can be set in scenarios where you want to optimize or
1147
     * customize the tracking of changes for the items in the collection.
1148
     * The igxForTrackBy function takes the index and the current item as arguments and needs to return the unique identifier for this item.
1149
     * ```typescript
1150
     * this.parentVirtDir.igxForTrackBy = (index, item) => {
1151
     *      return item.id + item.width;
1152
     * };
1153
     * ```
1154
     */
1155
    public set igxForTrackBy(fn: TrackByFunction<T>) {
1156
        this._trackByFn = fn;
38,005✔
1157
    }
1158

1159
    /**
1160
     * @hidden
1161
     */
1162
    protected _applyChanges() {
1163
        const prevChunkSize = this.state.chunkSize;
1,700✔
1164
        this.applyChunkSizeChange();
1,700✔
1165
        this._recalcScrollBarSize();
1,700✔
1166
        if (this.igxForOf && this.igxForOf.length && this.dc) {
1,700✔
1167
            const embeddedViewCopy = Object.assign([], this._embeddedViews);
1,439✔
1168
            let startIndex = this.state.startIndex;
1,439✔
1169
            let endIndex = this.state.chunkSize + this.state.startIndex;
1,439✔
1170
            if (this.isRemote) {
1,439✔
1171
                startIndex = 0;
28✔
1172
                endIndex = this.igxForOf.length;
28✔
1173
            }
1174
            for (let i = startIndex; i < endIndex && this.igxForOf[i] !== undefined; i++) {
1,439✔
1175
                const embView = embeddedViewCopy.shift();
9,674✔
1176
                this.updateTemplateContext(embView.context, i);
9,674✔
1177
            }
1178
            if (prevChunkSize !== this.state.chunkSize) {
1,439✔
1179
                this.chunkLoad.emit(this.state);
534✔
1180
            }
1181
        }
1182
    }
1183

1184
    /**
1185
     * @hidden
1186
     */
1187
    protected _calcMaxBrowserSize(): number {
1188
        if (!this.platformUtil.isBrowser) {
47,199!
1189
            return 0;
×
1190
        }
1191
        const div = this.document.createElement('div');
47,199✔
1192
        const style = div.style;
47,199✔
1193
        style.position = 'absolute';
47,199✔
1194
        const dir = this.igxForScrollOrientation === 'horizontal' ? 'left' : 'top';
47,199✔
1195
        style[dir] = '9999999999999999px';
47,199✔
1196
        this.document.body.appendChild(div);
47,199✔
1197
        const size = Math.abs(div.getBoundingClientRect()[dir]);
47,199✔
1198
        this.document.body.removeChild(div);
47,199✔
1199
        return size;
47,199✔
1200
    }
1201

1202
    /**
1203
     * @hidden
1204
     * Recalculates the chunkSize based on current startIndex and returns the new size.
1205
     * This should be called after this.state.startIndex is updated, not before.
1206
     */
1207
    protected _calculateChunkSize(): number {
1208
        let chunkSize = 0;
132,734✔
1209
        if (this.igxForContainerSize !== null && this.igxForContainerSize !== undefined) {
132,734✔
1210
            if (!this.sizesCache || this.sizesCache.length === 0) {
131,440✔
1211
                this.initSizesCache(this.igxForOf);
12,689✔
1212
            }
1213
            chunkSize = this._calcMaxChunkSize();
131,440✔
1214
            if (this.igxForOf && chunkSize > this.igxForOf.length) {
131,440✔
1215
                chunkSize = this.igxForOf.length;
17,583✔
1216
            }
1217
        } else {
1218
            if (this.igxForOf) {
1,294✔
1219
                chunkSize = Math.min(this.igxForInitialChunkSize || this.igxForOf.length, this.igxForOf.length);
1,294✔
1220
            }
1221
        }
1222
        return chunkSize;
132,734✔
1223
    }
1224

1225
    /**
1226
     * @hidden
1227
     */
1228
    protected getElement(viewref, nodeName) {
1229
        const elem = viewref.element.nativeElement.parentNode.getElementsByTagName(nodeName);
×
1230
        return elem.length > 0 ? elem[0] : null;
×
1231
    }
1232

1233
    /**
1234
     * @hidden
1235
     */
1236
    protected initSizesCache(items: U): number {
1237
        let totalSize = 0;
3,286✔
1238
        let size = 0;
3,286✔
1239
        const dimension = this.igxForSizePropName || 'height';
3,286!
1240
        let i = 0;
3,286✔
1241
        this.sizesCache = [];
3,286✔
1242
        this.individualSizeCache = [];
3,286✔
1243
        this.sizesCache.push(0);
3,286✔
1244
        const count = this.isRemote ? this.totalItemCount : items.length;
3,286✔
1245
        for (i; i < count; i++) {
3,286✔
1246
            size = this._getItemSize(items[i], dimension);
3,858,424✔
1247
            this.individualSizeCache.push(size);
3,858,424✔
1248
            totalSize += size;
3,858,424✔
1249
            this.sizesCache.push(totalSize);
3,858,424✔
1250
        }
1251
        return totalSize;
3,286✔
1252
    }
1253

1254
    protected _updateSizeCache() {
1255
        if (this.igxForScrollOrientation === 'horizontal') {
1,377✔
1256
            this.initSizesCache(this.igxForOf);
332✔
1257
            return;
332✔
1258
        }
1259
        const oldHeight = this.individualSizeCache.length > 0 ? this.individualSizeCache.reduce((acc, val) => acc + val) : 0;
2,098,943✔
1260
        const newHeight = this.initSizesCache(this.igxForOf);
1,045✔
1261

1262
        const diff = oldHeight - newHeight;
1,045✔
1263
        this._adjustScrollPositionAfterSizeChange(diff);
1,045✔
1264
    }
1265

1266
    /**
1267
     * @hidden
1268
     */
1269
    protected _calcMaxChunkSize(): number {
1270
        let i = 0;
52,632✔
1271
        let length = 0;
52,632✔
1272
        let maxLength = 0;
52,632✔
1273
        const arr = [];
52,632✔
1274
        let sum = 0;
52,632✔
1275
        const availableSize = parseInt(this.igxForContainerSize, 10);
52,632✔
1276
        if (!availableSize) {
52,632✔
1277
            return 0;
10,531✔
1278
        }
1279
        const dimension = this.igxForScrollOrientation === 'horizontal' ?
42,101✔
1280
            this.igxForSizePropName : 'height';
1281
        const reducer = (accumulator, currentItem) => accumulator + this._getItemSize(currentItem, dimension);
38,788,135✔
1282
        for (i; i < this.igxForOf.length; i++) {
42,101✔
1283
            let item: T | { value: T, height: number } = this.igxForOf[i];
5,470,061✔
1284
            if (dimension === 'height') {
5,470,061✔
1285
                item = { value: this.igxForOf[i], height: this.individualSizeCache[i] };
5,143,226✔
1286
            }
1287
            const size = dimension === 'height' ?
5,470,061✔
1288
                this.individualSizeCache[i] :
1289
                this._getItemSize(item, dimension);
1290
            sum = arr.reduce(reducer, size);
5,470,061✔
1291
            if (sum < availableSize) {
5,470,061✔
1292
                arr.push(item);
169,350✔
1293
                length = arr.length;
169,350✔
1294
                if (i === this.igxForOf.length - 1) {
169,350✔
1295
                    // reached end without exceeding
1296
                    // include prev items until size is filled or first item is reached.
1297
                    let curItem = dimension === 'height' ? arr[0].value : arr[0];
9,853✔
1298
                    let prevIndex = this.igxForOf.indexOf(curItem) - 1;
9,853✔
1299
                    while (prevIndex >= 0 && sum <= availableSize) {
9,853✔
1300
                        curItem = dimension === 'height' ? arr[0].value : arr[0];
176✔
1301
                        prevIndex = this.igxForOf.indexOf(curItem) - 1;
176✔
1302
                        const prevItem = this.igxForOf[prevIndex];
176✔
1303
                        const prevSize = dimension === 'height' ?
176✔
1304
                            this.individualSizeCache[prevIndex] :
1305
                            parseInt(prevItem[dimension], 10);
1306
                        sum = arr.reduce(reducer, prevSize);
176✔
1307
                        arr.unshift(prevItem);
176✔
1308
                        length = arr.length;
176✔
1309
                    }
1310
                }
1311
            } else {
1312
                arr.push(item);
5,300,711✔
1313
                length = arr.length + 1;
5,300,711✔
1314
                arr.shift();
5,300,711✔
1315
            }
1316
            if (length > maxLength) {
5,470,061✔
1317
                maxLength = length;
200,868✔
1318
            }
1319
        }
1320
        return maxLength;
42,101✔
1321
    }
1322

1323
    /**
1324
     * @hidden
1325
     */
1326
    protected getIndexAt(left, set) {
1327
        let start = 0;
93,576✔
1328
        let end = set.length - 1;
93,576✔
1329
        if (left === 0) {
93,576✔
1330
            return 0;
90,961✔
1331
        }
1332
        while (start <= end) {
2,615✔
1333
            const midIdx = Math.floor((start + end) / 2);
9,952✔
1334
            const midLeft = set[midIdx];
9,952✔
1335
            const cmp = left - midLeft;
9,952✔
1336
            if (cmp > 0) {
9,952✔
1337
                start = midIdx + 1;
4,478✔
1338
            } else if (cmp < 0) {
5,474✔
1339
                end = midIdx - 1;
5,170✔
1340
            } else {
1341
                return midIdx;
304✔
1342
            }
1343
        }
1344
        return end;
2,311✔
1345
    }
1346

1347
    protected _recalcScrollBarSize(containerSizeInfo = null) {
60,408✔
1348
        const count = this.isRemote ? this.totalItemCount : (this.igxForOf ? this.igxForOf.length : 0);
93,465!
1349
        this.dc.instance.notVirtual = !(this.igxForContainerSize && this.dc && this.state.chunkSize < count);
93,465✔
1350
        const scrollable = containerSizeInfo ? this.scrollComponent.size > containerSizeInfo.prevSize : this.isScrollable();
93,465✔
1351
        if (this.igxForScrollOrientation === 'horizontal') {
93,465✔
1352
            const totalWidth = parseInt(this.igxForContainerSize, 10) > 0 ? this._calcSize() : 0;
76,892✔
1353
            if (totalWidth <= parseInt(this.igxForContainerSize, 10)) {
76,892✔
1354
                this.resetScrollPosition();
38,267✔
1355
            }
1356
            this.scrollComponent.nativeElement.style.width = this.igxForContainerSize + 'px';
76,892✔
1357
            this.scrollComponent.size = totalWidth;
76,892✔
1358
        }
1359
        if (this.igxForScrollOrientation === 'vertical') {
93,465✔
1360
            const totalHeight = this._calcSize();
16,573✔
1361
            if (totalHeight <= parseInt(this.igxForContainerSize, 10)) {
16,573✔
1362
                this.resetScrollPosition();
5,781✔
1363
            }
1364
            this.scrollComponent.nativeElement.style.height = parseInt(this.igxForContainerSize, 10) + 'px';
16,573✔
1365
            this.scrollComponent.size = totalHeight;
16,573✔
1366
        }
1367
        if (scrollable !== this.isScrollable()) {
93,465✔
1368
            // scrollbar visibility has changed
1369
            this.scrollbarVisibilityChanged.emit();
18,641✔
1370
        }
1371
    }
1372

1373
    protected _calcSize(): number {
1374
        let size;
1375
        if (this.individualSizeCache && this.individualSizeCache.length > 0) {
106,976✔
1376
            size = this.individualSizeCache.reduce((acc, val) => acc + val, 0);
6,064,589✔
1377
        } else {
1378
            size = this.initSizesCache(this.igxForOf);
8,872✔
1379
        }
1380
        this._virtSize = size;
106,976✔
1381
        if (size > this._maxSize) {
106,976!
1382
            this._virtRatio = size / this._maxSize;
×
1383
            size = this._maxSize;
×
1384
        }
1385
        return size;
106,976✔
1386
    }
1387

1388
    protected _recalcOnContainerChange(containerSizeInfo = null) {
×
1389
        const prevChunkSize = this.state.chunkSize;
33,057✔
1390
        this.applyChunkSizeChange();
33,057✔
1391
        this._recalcScrollBarSize(containerSizeInfo);
33,057✔
1392
        if (prevChunkSize !== this.state.chunkSize) {
33,057✔
1393
            this.chunkLoad.emit(this.state);
8,625✔
1394
        }
1395
    }
1396

1397
    /**
1398
     * @hidden
1399
     * Removes an element from the embedded views and updates chunkSize.
1400
     */
1401
    protected removeLastElem() {
1402
        const oldElem = this._embeddedViews.pop();
10,650✔
1403
        this.beforeViewDestroyed.emit(oldElem);
10,650✔
1404
        // also detach from ViewContainerRef to make absolutely sure this is removed from the view container.
1405
        this.dc.instance._vcr.detach(this.dc.instance._vcr.length - 1);
10,650✔
1406
        oldElem.destroy();
10,650✔
1407

1408
        this.state.chunkSize--;
10,650✔
1409
    }
1410

1411
    /**
1412
     * @hidden
1413
     * If there exists an element that we can create embedded view for creates it, appends it and updates chunkSize
1414
     */
1415
    protected addLastElem() {
1416
        let elemIndex = this.state.startIndex + this.state.chunkSize;
2,113✔
1417
        if (!this.isRemote && !this.igxForOf) {
2,113!
1418
            return;
×
1419
        }
1420

1421
        if (elemIndex >= this.igxForOf.length) {
2,113✔
1422
            elemIndex = this.igxForOf.length - this.state.chunkSize;
5✔
1423
        }
1424
        const input = this.igxForOf[elemIndex];
2,113✔
1425
        const embeddedView = this.dc.instance._vcr.createEmbeddedView(
2,113✔
1426
            this._template,
1427
            new IgxForOfContext<T, U>(input, this.igxForOf, this.getContextIndex(input), this.igxForOf.length)
1428
        );
1429

1430
        this._embeddedViews.push(embeddedView);
2,113✔
1431
        this.state.chunkSize++;
2,113✔
1432

1433
        this._zone.run(() => this.cdr.markForCheck());
2,113✔
1434
    }
1435

1436
    /**
1437
     * Recalculates chunkSize and adds/removes elements if need due to the change.
1438
     * this.state.chunkSize is updated in @addLastElem() or @removeLastElem()
1439
     */
1440
    protected applyChunkSizeChange() {
1441
        const chunkSize = this.isRemote ? (this.igxForOf ? this.igxForOf.length : 0) : this._calculateChunkSize();
93,465!
1442
        if (chunkSize > this.state.chunkSize) {
93,465✔
1443
            const diff = chunkSize - this.state.chunkSize;
8,958✔
1444
            for (let i = 0; i < diff; i++) {
8,958✔
1445
                this.addLastElem();
45,783✔
1446
            }
1447
        } else if (chunkSize < this.state.chunkSize) {
84,507✔
1448
            const diff = this.state.chunkSize - chunkSize;
4,309✔
1449
            for (let i = 0; i < diff; i++) {
4,309✔
1450
                this.removeLastElem();
10,650✔
1451
            }
1452
        }
1453
    }
1454

1455
    protected _calcVirtualScrollPosition(scrollPosition: number) {
1456
        const containerSize = parseInt(this.igxForContainerSize, 10);
200✔
1457
        const maxRealScrollPosition = this.scrollComponent.size - containerSize;
200✔
1458
        const realPercentScrolled = maxRealScrollPosition !== 0 ? scrollPosition / maxRealScrollPosition : 0;
200!
1459
        const maxVirtScroll = this._virtSize - containerSize;
200✔
1460
        this._virtScrollPosition = realPercentScrolled * maxVirtScroll;
200✔
1461
    }
1462

1463
    protected _getItemSize(item, dimension: string): number {
1464
        const dim = item ? item[dimension] : null;
43,738,866✔
1465
        return typeof dim === 'number' ? dim : parseInt(this.igxForItemSize, 10) || 0;
43,738,866✔
1466
    }
1467

1468
    protected _updateScrollOffset() {
1469
        let scrollOffset = 0;
96,360✔
1470
        let currentScroll = this.scrollPosition;
96,360✔
1471
        if (this._virtRatio !== 1) {
96,360!
1472
            this._calcVirtualScrollPosition(this.scrollPosition);
×
1473
            currentScroll = this._virtScrollPosition;
×
1474
        }
1475
        const scroll = this.scrollComponent.nativeElement;
96,360✔
1476
        scrollOffset = scroll && this.scrollComponent.size ?
96,360✔
1477
        currentScroll - this.sizesCache[this.state.startIndex] : 0;
1478
        const dir = this.igxForScrollOrientation === 'horizontal' ? 'left' : 'top';
96,360✔
1479
        this.dc.instance._viewContainer.element.nativeElement.style[dir] = -(scrollOffset) + 'px';
96,360✔
1480
    }
1481

1482
    protected _adjustScrollPositionAfterSizeChange(sizeDiff) {
1483
        // if data has been changed while container is scrolled
1484
        // should update scroll top/left according to change so that same startIndex is in view
1485
        if (Math.abs(sizeDiff) > 0 && this.scrollPosition > 0) {
41,357✔
1486
            this.recalcUpdateSizes();
96✔
1487
            const offset = this.igxForScrollOrientation === 'horizontal' ?
96✔
1488
                parseInt(this.dc.instance._viewContainer.element.nativeElement.style.left, 10) :
1489
                parseInt(this.dc.instance._viewContainer.element.nativeElement.style.top, 10);
1490
            const newSize = this.sizesCache[this.state.startIndex] - offset;
96✔
1491
            this.scrollPosition = newSize;
96✔
1492
            if (this.scrollPosition !== newSize) {
96✔
1493
                this.scrollComponent.scrollAmount = newSize;
29✔
1494
            }
1495
        }
1496
    }
1497

1498
    private getMargin(node, dimension: string): number {
1499
        const styles = window.getComputedStyle(node);
13,329✔
1500
        if (dimension === 'height') {
13,329✔
1501
            return parseFloat(styles['marginTop']) +
12,655✔
1502
                parseFloat(styles['marginBottom']) || 0;
1503
        }
1504
        return parseFloat(styles['marginLeft']) +
674✔
1505
            parseFloat(styles['marginRight']) || 0;
1506
    }
1507
}
1508

1509
export const getTypeNameForDebugging = (type: any): string => type.name || typeof type;
3!
1510

1511
export interface IForOfState extends IBaseEventArgs {
1512
    startIndex?: number;
1513
    chunkSize?: number;
1514
}
1515

1516
/**
1517
 * @deprecated in 19.2.7. Use `IForOfDataChangeEventArgs` instead.
1518
 */
1519
export interface IForOfDataChangingEventArgs extends IBaseEventArgs {
1520
    containerSize: number;
1521
    state: IForOfState;
1522
}
1523

1524
export interface IForOfDataChangeEventArgs extends IForOfDataChangingEventArgs {}
1525

1526
export class IgxGridForOfContext<T, U extends T[] = T[]> extends IgxForOfContext<T, U> {
1527
    constructor(
1528
        $implicit: T,
1529
        public igxGridForOf: U,
43,670✔
1530
        index: number,
1531
        count: number
1532
    ) {
1533
        super($implicit, igxGridForOf, index, count);
43,670✔
1534
    }
1535
}
1536

1537
@Directive({
1538
    selector: '[igxGridFor][igxGridForOf]',
1539
    standalone: true
1540
})
1541
export class IgxGridForOfDirective<T, U extends T[] = T[]> extends IgxForOfDirective<T, U> implements OnInit, OnChanges, DoCheck {
3✔
1542
    @Input()
1543
    public set igxGridForOf(value: U & T[] | null) {
1544
        this.igxForOf = value;
149,114✔
1545
    }
1546

1547
    public get igxGridForOf() {
1548
        return this.igxForOf;
36✔
1549
    }
1550

1551
    @Input({ transform: booleanAttribute })
1552
    public igxGridForOfUniqueSizeCache = false;
46,035✔
1553

1554
    @Input({ transform: booleanAttribute })
1555
    public igxGridForOfVariableSizes = true;
46,035✔
1556

1557
    /**
1558
     * @hidden
1559
     * @internal
1560
     */
1561
    public override get sizesCache(): number[] {
1562
        if (this.igxForScrollOrientation === 'horizontal') {
970,230✔
1563
            if (this.igxGridForOfUniqueSizeCache || this.syncService.isMaster(this)) {
756,310✔
1564
                return this._sizesCache;
470,347✔
1565
            }
1566
            return this.syncService.sizesCache(this.igxForScrollOrientation);
285,963✔
1567
        } else {
1568
            return this._sizesCache;
213,920✔
1569
        }
1570
    }
1571
    /**
1572
     * @hidden
1573
     * @internal
1574
     */
1575
    public override set sizesCache(value: number[]) {
1576
        this._sizesCache = value;
77,982✔
1577
    }
1578

1579
    protected get itemsDimension() {
1580
        return this.igxForSizePropName || 'height';
×
1581
    }
1582

1583
    public override recalcUpdateSizes() {
1584
        if (this.igxGridForOfVariableSizes && this.igxForScrollOrientation === 'vertical') {
1,180✔
1585
            super.recalcUpdateSizes();
1,104✔
1586
        }
1587
    }
1588

1589
    /**
1590
     * @hidden @internal
1591
     * An event that is emitted after data has been changed but before the view is refreshed
1592
     */
1593
    @Output()
1594
    public dataChanging = new EventEmitter<IForOfDataChangeEventArgs>();
46,035✔
1595

1596
    constructor(
1597
        _viewContainer: ViewContainerRef,
1598
        _template: TemplateRef<NgForOfContext<T>>,
1599
        _differs: IterableDiffers,
1600
        cdr: ChangeDetectorRef,
1601
        _zone: NgZone,
1602
        _platformUtil: PlatformUtil,
1603
        @Inject(DOCUMENT) _document: any,
1604
        syncScrollService: IgxForOfScrollSyncService,
1605
        protected syncService: IgxForOfSyncService) {
46,035✔
1606
        super(_viewContainer, _template, _differs, cdr, _zone, syncScrollService, _platformUtil, _document);
46,035✔
1607
    }
1608

1609
    /**
1610
     * @hidden @internal
1611
     * Asserts the correct type of the context for the template that `IgxGridForOfDirective` will render.
1612
     *
1613
     * The presence of this method is a signal to the Ivy template type-check compiler that the
1614
     * `IgxGridForOfDirective` structural directive renders its template with a specific context type.
1615
     */
1616
    public static override ngTemplateContextGuard<T, U extends T[]>(dir: IgxGridForOfDirective<T, U>, ctx: any):
1617
        ctx is IgxGridForOfContext<T, U> {
1618
        return true;
×
1619
    }
1620

1621
    public override ngOnInit() {
1622
        this.syncService.setMaster(this);
46,035✔
1623
        super.ngOnInit();
46,035✔
1624
        this.removeScrollEventListeners();
46,035✔
1625
    }
1626

1627
    public override ngOnChanges(changes: SimpleChanges) {
1628
        const forOf = 'igxGridForOf';
158,593✔
1629
        this.syncService.setMaster(this);
158,593✔
1630
        if (forOf in changes) {
158,593✔
1631
            const value = changes[forOf].currentValue;
149,114✔
1632
            if (!this._differ && value) {
149,114✔
1633
                try {
46,032✔
1634
                    this._differ = this._differs.find(value).create(this.igxForTrackBy);
46,032✔
1635
                } catch (e) {
1636
                    throw new Error(
×
1637
                        `Cannot find a differ supporting object "${value}" of type "${getTypeNameForDebugging(value)}".
1638
                     NgFor only supports binding to Iterables such as Arrays.`);
1639
                }
1640
            }
1641
            if (this.igxForScrollOrientation === 'horizontal') {
149,114✔
1642
                // in case collection has changes, reset sync service
1643
                this.syncService.setMaster(this, this.igxGridForOfUniqueSizeCache);
133,900✔
1644
            }
1645
        }
1646
        const defaultItemSize = 'igxForItemSize';
158,593✔
1647
        if (defaultItemSize in changes && !changes[defaultItemSize].firstChange &&
158,593✔
1648
            this.igxForScrollOrientation === 'vertical' && this.igxForOf) {
1649
            // handle default item size changed.
1650
            this.initSizesCache(this.igxForOf);
104✔
1651
        }
1652
        const containerSize = 'igxForContainerSize';
158,593✔
1653
        if (containerSize in changes && !changes[containerSize].firstChange && this.igxForOf) {
158,593✔
1654
            const prevSize = parseInt(changes[containerSize].previousValue, 10);
32,669✔
1655
            const newSize = parseInt(changes[containerSize].currentValue, 10);
32,669✔
1656
            this._recalcOnContainerChange({prevSize, newSize});
32,669✔
1657
        }
1658
    }
1659

1660
    /**
1661
     * @hidden
1662
     * @internal
1663
     */
1664
    public assumeMaster(): void {
1665
        this._sizesCache = this.syncService.sizesCache(this.igxForScrollOrientation);
17,097✔
1666
        this.syncService.setMaster(this, true);
17,097✔
1667
    }
1668

1669
    public override ngDoCheck() {
1670
        if (this._differ) {
450,198✔
1671
            const changes = this._differ.diff(this.igxForOf);
450,086✔
1672
            if (changes) {
450,086✔
1673
                const args: IForOfDataChangingEventArgs = {
57,948✔
1674
                    containerSize: this.igxForContainerSize,
1675
                    state: this.state
1676
                };
1677
                this.dataChanging.emit(args);
57,948✔
1678
                //  re-init cache.
1679
                if (!this.igxForOf) {
57,948!
1680
                    this.igxForOf = [] as U;
×
1681
                }
1682
                /* we need to reset the master dir if all rows are removed
1683
                (e.g. because of filtering); if all columns are hidden, rows are
1684
                still rendered empty, so we should not reset master */
1685
                if (!this.igxForOf.length &&
57,948✔
1686
                    this.igxForScrollOrientation === 'vertical') {
1687
                    this.syncService.resetMaster();
105✔
1688
                }
1689
                this.syncService.setMaster(this);
57,948✔
1690
                this.igxForContainerSize = args.containerSize;
57,948✔
1691
                const sizeDiff = this._updateSizeCache(changes);
57,948✔
1692
                this._applyChanges();
57,948✔
1693
                if (sizeDiff) {
57,948✔
1694
                    this._adjustScrollPositionAfterSizeChange(sizeDiff);
40,293✔
1695
                }
1696
                this._updateScrollOffset();
57,948✔
1697
                this.dataChanged.emit(args);
57,948✔
1698
            }
1699
        }
1700
    }
1701

1702
    public override onScroll(event) {
1703
        if (!parseInt(this.scrollComponent.nativeElement.style.height, 10)) {
315!
1704
            return;
×
1705
        }
1706
        if (!this._bScrollInternal) {
315✔
1707
            this._calcVirtualScrollPosition(event.target.scrollTop);
58✔
1708
        } else {
1709
            this._bScrollInternal = false;
257✔
1710
        }
1711
        const scrollOffset = this.fixedUpdateAllElements(this._virtScrollPosition);
315✔
1712

1713
        this.dc.instance._viewContainer.element.nativeElement.style.top = -(scrollOffset) + 'px';
315✔
1714

1715
        this._zone.onStable.pipe(first()).subscribe(this.recalcUpdateSizes.bind(this));
315✔
1716
        this.cdr.markForCheck();
315✔
1717
    }
1718

1719
    public override onHScroll(scrollAmount) {
1720
        /* in certain situations this may be called when no scrollbar is visible */
1721
        const firstScrollChild = this.scrollComponent.nativeElement.children.item(0) as HTMLElement;
2,536✔
1722
        if (!this.scrollComponent || !parseInt(firstScrollChild.style.width, 10)) {
2,536!
1723
            return;
×
1724
        }
1725
        // Updating horizontal chunks
1726
        const scrollOffset = this.fixedUpdateAllElements(Math.abs(scrollAmount));
2,536✔
1727
        if (scrollAmount < 0) {
2,536!
1728
            // RTL
1729
            this.dc.instance._viewContainer.element.nativeElement.style.left = scrollOffset + 'px';
×
1730
        } else {
1731
            // LTR
1732
            this.dc.instance._viewContainer.element.nativeElement.style.left = -scrollOffset + 'px';
2,536✔
1733
        }
1734
    }
1735

1736
    protected getItemSize(item) {
1737
        let size = 0;
1,083,236✔
1738
        const dimension = this.igxForSizePropName || 'height';
1,083,236!
1739
        if (this.igxForScrollOrientation === 'vertical') {
1,083,236✔
1740
            size = this._getItemSize(item, dimension);
765,472✔
1741
            if (item && item.summaries) {
765,472✔
1742
                size = item.max;
1,574✔
1743
            } else if (item && item.groups && item.height) {
763,898✔
1744
                size = item.height;
23,020✔
1745
            }
1746
        } else {
1747
            size = parseInt(item[dimension], 10) || 0;
317,764✔
1748
        }
1749
        return size;
1,083,236✔
1750
    }
1751

1752
    protected override initSizesCache(items: U): number {
1753
        if (!this.syncService.isMaster(this) && this.igxForScrollOrientation === 'horizontal') {
20,058✔
1754
            const masterSizesCache = this.syncService.sizesCache(this.igxForScrollOrientation);
18✔
1755
            return masterSizesCache[masterSizesCache.length - 1];
18✔
1756
        }
1757
        let totalSize = 0;
20,040✔
1758
        let size = 0;
20,040✔
1759
        let i = 0;
20,040✔
1760
        this.sizesCache = [];
20,040✔
1761
        this.individualSizeCache = [];
20,040✔
1762
        this.sizesCache.push(0);
20,040✔
1763
        const count = this.isRemote ? this.totalItemCount : items.length;
20,040✔
1764
        for (i; i < count; i++) {
20,040✔
1765
            size = this.getItemSize(items[i]);
168,114✔
1766
            this.individualSizeCache.push(size);
168,114✔
1767
            totalSize += size;
168,114✔
1768
            this.sizesCache.push(totalSize);
168,114✔
1769
        }
1770
        return totalSize;
20,040✔
1771
    }
1772

1773
    protected override _updateSizeCache(changes: IterableChanges<T> = null) {
×
1774
        const oldSize = this.individualSizeCache.length > 0 ? this.individualSizeCache.reduce((acc, val) => acc + val) : 0;
268,876✔
1775
        let newSize = oldSize;
57,948✔
1776
        if (changes && !this.isRemote) {
57,948✔
1777
            newSize = this.handleCacheChanges(changes);
57,942✔
1778
        } else {
1779
            return;
6✔
1780
        }
1781

1782
        const diff = oldSize - newSize;
57,942✔
1783
        return diff;
57,942✔
1784
    }
1785

1786
    protected handleCacheChanges(changes: IterableChanges<T>) {
1787
        const identityChanges = [];
57,942✔
1788
        const newHeightCache = [];
57,942✔
1789
        const newSizesCache = [];
57,942✔
1790
        newSizesCache.push(0);
57,942✔
1791
        let newHeight = 0;
57,942✔
1792

1793
        // When there are more than one removed items the changes are not reliable so those with identity change should be default size.
1794
        let numRemovedItems = 0;
57,942✔
1795
        changes.forEachRemovedItem(() => numRemovedItems++);
80,745✔
1796

1797
        // Get the identity changes to determine later if those that have changed their indexes should be assigned default item size.
1798
        changes.forEachIdentityChange((item) => {
57,942✔
1799
            if (item.currentIndex !== item.previousIndex) {
808✔
1800
                // Filter out ones that have not changed their index.
1801
                identityChanges[item.currentIndex] = item;
316✔
1802
            }
1803
        });
1804

1805
        // Processing each item that is passed to the igxForOf so far seem to be most reliable. We parse the updated list of items.
1806
        changes.forEachItem((item) => {
57,942✔
1807
            if (item.previousIndex !== null &&
938,421✔
1808
                (numRemovedItems < 2 || !identityChanges.length || identityChanges[item.currentIndex])
1809
                && this.igxForScrollOrientation !== "horizontal" && this.individualSizeCache.length > 0) {
1810
                // Reuse cache on those who have previousIndex.
1811
                // When there are more than one removed items currently the changes are not readable so ones with identity change
1812
                // should be racalculated.
1813
                newHeightCache[item.currentIndex] = this.individualSizeCache[item.previousIndex];
23,299✔
1814
            } else {
1815
                // Assign default item size.
1816
                newHeightCache[item.currentIndex] = this.getItemSize(item.item);
915,122✔
1817
            }
1818
            newSizesCache[item.currentIndex + 1] = newSizesCache[item.currentIndex] + newHeightCache[item.currentIndex];
938,421✔
1819
            newHeight += newHeightCache[item.currentIndex];
938,421✔
1820
        });
1821
        this.individualSizeCache = newHeightCache;
57,942✔
1822
        this.sizesCache = newSizesCache;
57,942✔
1823
        return newHeight;
57,942✔
1824
    }
1825

1826
    protected override addLastElem() {
1827
        let elemIndex = this.state.startIndex + this.state.chunkSize;
43,670✔
1828
        if (!this.isRemote && !this.igxForOf) {
43,670!
1829
            return;
×
1830
        }
1831

1832
        if (elemIndex >= this.igxForOf.length) {
43,670✔
1833
            elemIndex = this.igxForOf.length - this.state.chunkSize;
15✔
1834
        }
1835
        const input = this.igxForOf[elemIndex];
43,670✔
1836
        const embeddedView = this.dc.instance._vcr.createEmbeddedView(
43,670✔
1837
            this._template,
1838
            new IgxGridForOfContext<T, U>(input, this.igxForOf, this.getContextIndex(input), this.igxForOf.length)
1839
        );
1840

1841
        this._embeddedViews.push(embeddedView);
43,670✔
1842
        this.state.chunkSize++;
43,670✔
1843
    }
1844

1845
    protected _updateViews(prevChunkSize) {
1846
        if (this.igxForOf && this.igxForOf.length && this.dc) {
58,708✔
1847
            const embeddedViewCopy = Object.assign([], this._embeddedViews);
58,561✔
1848
            let startIndex;
1849
            let endIndex;
1850
            if (this.isRemote) {
58,561✔
1851
                startIndex = 0;
5✔
1852
                endIndex = this.igxForOf.length;
5✔
1853
            } else {
1854
                startIndex = this.getIndexAt(this.scrollPosition, this.sizesCache);
58,556✔
1855
                if (startIndex + this.state.chunkSize > this.igxForOf.length) {
58,556✔
1856
                    startIndex = this.igxForOf.length - this.state.chunkSize;
141✔
1857
                }
1858
                this.state.startIndex = startIndex;
58,556✔
1859
                endIndex = this.state.chunkSize + this.state.startIndex;
58,556✔
1860
            }
1861

1862
            for (let i = startIndex; i < endIndex && this.igxForOf[i] !== undefined; i++) {
58,561✔
1863
                const embView = embeddedViewCopy.shift();
250,288✔
1864
                this.updateTemplateContext(embView.context, i);
250,288✔
1865
            }
1866
            if (prevChunkSize !== this.state.chunkSize) {
58,561✔
1867
                this.chunkLoad.emit(this.state);
3,915✔
1868
            }
1869
        }
1870
    }
1871
    protected override _applyChanges() {
1872
        const prevChunkSize = this.state.chunkSize;
58,708✔
1873
        this.applyChunkSizeChange();
58,708✔
1874
        this._recalcScrollBarSize();
58,708✔
1875
        this._updateViews(prevChunkSize);
58,708✔
1876
    }
1877

1878
    /**
1879
     * @hidden
1880
     */
1881
    protected override _calcMaxChunkSize(): number {
1882
        if (this.igxForScrollOrientation === 'horizontal') {
129,251✔
1883
            if (this.syncService.isMaster(this)) {
111,560✔
1884
                return super._calcMaxChunkSize();
32,750✔
1885
            }
1886
            return this.syncService.chunkSize(this.igxForScrollOrientation);
78,810✔
1887
        } else {
1888
            return super._calcMaxChunkSize();
17,691✔
1889
        }
1890

1891
    }
1892
}
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