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

IgniteUI / igniteui-angular / 11819965427

13 Nov 2024 03:03PM CUT coverage: 91.575% (-0.002%) from 91.577%
11819965427

Pull #15060

github

web-flow
Merge e4a9cb562 into c8366c27c
Pull Request #15060: fix(grid): fix column resize line positioning for scaled grid - master

12960 of 15190 branches covered (85.32%)

10 of 10 new or added lines in 2 files covered. (100.0%)

1 existing line in 1 file now uncovered.

26271 of 28688 relevant lines covered (91.57%)

33912.64 hits per line

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

95.37
/projects/igniteui-angular/src/lib/directives/for-of/for_of.directive.ts
1
/* eslint-disable @angular-eslint/no-conflicting-lifecycle */
2
import { DOCUMENT, NgForOfContext } from '@angular/common';
3
import {
4
    ChangeDetectorRef,
5
    ComponentRef,
6
    Directive,
7
    DoCheck,
8
    EmbeddedViewRef,
9
    EventEmitter,
10
    Input,
11
    IterableChanges,
12
    IterableDiffer,
13
    IterableDiffers,
14
    NgZone,
15
    OnChanges,
16
    OnDestroy,
17
    OnInit,
18
    Output,
19
    SimpleChanges,
20
    TemplateRef,
21
    TrackByFunction,
22
    ViewContainerRef,
23
    AfterViewInit,
24
    Inject,
25
    booleanAttribute
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;
2✔
40

41
/**
42
 *  @publicApi
43
 */
44
export class IgxForOfContext<T, U extends T[] = T[]> {
45
    constructor(
46
        public $implicit: T,
210,937✔
47
        public igxForOf: U,
210,937✔
48
        public index: number,
210,937✔
49
        public count: number
210,937✔
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 {
2✔
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';
48,557✔
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
     * Sets the px-affixed size of the item along the axis of scrolling.
174
     * For "horizontal" orientation this value is the width of the column and for "vertical" is the height or the row.
175
     * ```html
176
     * <ng-template igxFor let-item [igxForOf]="data" [igxForScrollOrientation]="'horizontal'" [igxForItemSize]="'50px'"></ng-template>
177
     * ```
178
     */
179
    @Input()
180
    public igxForItemSize: any;
181

182
    /**
183
     * An event that is emitted after a new chunk has been loaded.
184
     * ```html
185
     * <ng-template igxFor [igxForOf]="data" [igxForScrollOrientation]="'horizontal'" (chunkLoad)="loadChunk($event)"></ng-template>
186
     * ```
187
     * ```typescript
188
     * loadChunk(e){
189
     * alert("chunk loaded!");
190
     * }
191
     * ```
192
     */
193
    @Output()
194
    public chunkLoad = new EventEmitter<IForOfState>();
48,557✔
195

196
    /**
197
     * @hidden @internal
198
     * An event that is emitted when scrollbar visibility has changed.
199
     */
200
    @Output()
201
    public scrollbarVisibilityChanged = new EventEmitter<any>();
48,557✔
202

203
    /**
204
     * An event that is emitted after the rendered content size of the igxForOf has been changed.
205
     */
206
    @Output()
207
    public contentSizeChange = new EventEmitter<any>();
48,557✔
208

209
    /**
210
     * An event that is emitted after data has been changed.
211
     * ```html
212
     * <ng-template igxFor [igxForOf]="data" [igxForScrollOrientation]="'horizontal'" (dataChanged)="dataChanged($event)"></ng-template>
213
     * ```
214
     * ```typescript
215
     * dataChanged(e){
216
     * alert("data changed!");
217
     * }
218
     * ```
219
     */
220
    @Output()
221
    public dataChanged = new EventEmitter<any>();
48,557✔
222

223
    @Output()
224
    public beforeViewDestroyed = new EventEmitter<EmbeddedViewRef<any>>();
48,557✔
225

226
    /**
227
     * An event that is emitted on chunk loading to emit the current state information - startIndex, endIndex, totalCount.
228
     * Can be used for implementing remote load on demand for the igxFor data.
229
     * ```html
230
     * <ng-template igxFor [igxForOf]="data" [igxForScrollOrientation]="'horizontal'" (chunkPreload)="chunkPreload($event)"></ng-template>
231
     * ```
232
     * ```typescript
233
     * chunkPreload(e){
234
     * alert("chunk is loading!");
235
     * }
236
     * ```
237
     */
238
    @Output()
239
    public chunkPreload = new EventEmitter<IForOfState>();
48,557✔
240

241
    /**
242
     * @hidden
243
     */
244
    public dc: ComponentRef<DisplayContainerComponent>;
245

246
    /**
247
     * The current state of the directive. It contains `startIndex` and `chunkSize`.
248
     * state.startIndex - The index of the item at which the current visible chunk begins.
249
     * state.chunkSize - The number of items the current visible chunk holds.
250
     * These options can be used when implementing remote virtualization as they provide the necessary state information.
251
     * ```typescript
252
     * const gridState = this.parentVirtDir.state;
253
     * ```
254
     */
255
    public state: IForOfState = {
48,557✔
256
        startIndex: 0,
257
        chunkSize: 0
258
    };
259

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

281
    private _totalItemCount: number = null;
48,557✔
282
    private _adjustToIndex;
283
    // Start properties related to virtual size handling due to browser limitation
284
    /** Maximum size for an element of the browser. */
285
    private _maxSize;
286
    /**
287
     * Ratio for height that's being virtualizaed and the one visible
288
     * If _virtHeightRatio = 1, the visible height and the virtualized are the same, also _maxSize > _virtHeight.
289
     */
290
    private _virtRatio = 1;
48,557✔
291

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

308
    /**
309
     * The total count of the virtual data items, when using remote service.
310
     * ```typescript
311
     * this.parentVirtDir.totalItemCount = data.Count;
312
     * ```
313
     */
314
    public get totalItemCount() {
315
        return this._totalItemCount;
908,299✔
316
    }
317

318
    public set totalItemCount(val) {
319
        if (this._totalItemCount !== val) {
20✔
320
            this._totalItemCount = val;
19✔
321
            // update sizes in case total count changes.
322
            const newSize = this.initSizesCache(this.igxForOf);
19✔
323
            const sizeDiff = this.scrollComponent.size - newSize;
19✔
324
            this.scrollComponent.size = newSize;
19✔
325
            const lastChunkExceeded = this.state.startIndex + this.state.chunkSize > val;
19✔
326
            if (lastChunkExceeded) {
19!
327
                this.state.startIndex = val - this.state.chunkSize;
×
328
            }
329
            this._adjustScrollPositionAfterSizeChange(sizeDiff);
19✔
330
        }
331
    }
332

333
    public get displayContainer(): HTMLElement | undefined {
334
        return this.dc?.instance?._viewContainer?.element?.nativeElement;
6,913✔
335
    }
336

337
    public get virtualHelper() {
338
        return this.scrollComponent.nativeElement;
×
339
    }
340

341
    /**
342
     * @hidden
343
     */
344
    public get isRemote(): boolean {
345
        return this.totalItemCount !== null;
905,883✔
346
    }
347

348
    /**
349
     *
350
     * Gets/Sets the scroll position.
351
     * ```typescript
352
     * const position = directive.scrollPosition;
353
     * directive.scrollPosition = value;
354
     * ```
355
     */
356
    public get scrollPosition(): number {
357
        return this.scrollComponent.scrollAmount;
230,751✔
358
    }
359
    public set scrollPosition(val: number) {
360
        if (val === this.scrollComponent.scrollAmount) {
44,297✔
361
            return;
43,547✔
362
        }
363
        if (this.igxForScrollOrientation === 'horizontal' && this.scrollComponent) {
750✔
364
            this.scrollComponent.nativeElement.scrollLeft = this.isRTL ? -val : val;
235!
365
        } else if (this.scrollComponent) {
515✔
366
            this.scrollComponent.nativeElement.scrollTop = val;
515✔
367
        }
368
    }
369

370
    /**
371
     * @hidden
372
     */
373
    protected get isRTL() {
374
        const dir = window.getComputedStyle(this.dc.instance._viewContainer.element.nativeElement).getPropertyValue('direction');
235✔
375
        return dir === 'rtl';
235✔
376
    }
377

378
    protected get sizesCache(): number[] {
379
        return this._sizesCache;
3,788,225✔
380
    }
381
    protected set sizesCache(value: number[]) {
382
        this._sizesCache = value;
2,644✔
383
    }
384

385
    private get _isScrolledToBottom() {
386
        if (!this.getScroll()) {
388!
387
            return true;
×
388
        }
389
        const scrollHeight = this.getScroll().scrollHeight;
388✔
390
        // Use === and not >= because `scrollTop + container size` can't be bigger than `scrollHeight`, unless something isn't updated.
391
        // Also use Math.round because Chrome has some inconsistencies and `scrollTop + container` can be float when zooming the page.
392
        return Math.round(this.getScroll().scrollTop + this.igxForContainerSize) === scrollHeight;
388✔
393
    }
394

395
    private get _isAtBottomIndex() {
396
        return this.igxForOf && this.state.startIndex + this.state.chunkSize > this.igxForOf.length;
9✔
397
    }
398

399
    constructor(
400
        private _viewContainer: ViewContainerRef,
48,557✔
401
        protected _template: TemplateRef<NgForOfContext<T>>,
48,557✔
402
        protected _differs: IterableDiffers,
48,557✔
403
        public cdr: ChangeDetectorRef,
48,557✔
404
        protected _zone: NgZone,
48,557✔
405
        protected syncScrollService: IgxForOfScrollSyncService,
48,557✔
406
        protected platformUtil: PlatformUtil,
48,557✔
407
        @Inject(DOCUMENT)
408
        protected document: any,
48,557✔
409
    ) {
410
        super();
48,557✔
411
    }
412

413
    public verticalScrollHandler(event) {
414
        this.onScroll(event);
47✔
415
    }
416

417
    public isScrollable() {
418
        return this.scrollComponent.size > parseInt(this.igxForContainerSize, 10);
255,356✔
419
    }
420

421
    /**
422
     * @hidden
423
     */
424
    public ngOnInit(): void {
425
        const vc = this.igxForScrollContainer ? this.igxForScrollContainer._viewContainer : this._viewContainer;
48,557✔
426
        this.igxForSizePropName = this.igxForSizePropName || 'width';
48,557✔
427
        this.dc = this._viewContainer.createComponent(DisplayContainerComponent, { index: 0 });
48,557✔
428
        this.dc.instance.scrollDirection = this.igxForScrollOrientation;
48,557✔
429
        if (this.igxForOf && this.igxForOf.length) {
48,557✔
430
            this.scrollComponent = this.syncScrollService.getScrollMaster(this.igxForScrollOrientation);
40,755✔
431
            this.state.chunkSize = this._calculateChunkSize();
40,755✔
432
            this.dc.instance.notVirtual = !(this.igxForContainerSize && this.state.chunkSize < this.igxForOf.length);
40,755✔
433
            if (this.scrollComponent && !this.scrollComponent.destroyed) {
40,755✔
434
                this.state.startIndex = Math.min(this.getIndexAt(this.scrollPosition, this.sizesCache),
33,543✔
435
                    this.igxForOf.length - this.state.chunkSize);
436
            }
437
            for (let i = this.state.startIndex; i < this.state.startIndex + this.state.chunkSize &&
40,755✔
438
                this.igxForOf[i] !== undefined; i++) {
439
                const input = this.igxForOf[i];
165,646✔
440
                const embeddedView = this.dc.instance._vcr.createEmbeddedView(
165,646✔
441
                    this._template,
442
                    new IgxForOfContext<T, U>(input, this.igxForOf, this.getContextIndex(input), this.igxForOf.length)
443
                );
444
                this._embeddedViews.push(embeddedView);
165,646✔
445
            }
446
        }
447
        this._maxSize = this._calcMaxBrowserSize();
48,557✔
448
        if (this.igxForScrollOrientation === 'vertical') {
48,557✔
449
            this.dc.instance._viewContainer.element.nativeElement.style.top = '0px';
11,587✔
450
            this.scrollComponent = this.syncScrollService.getScrollMaster(this.igxForScrollOrientation);
11,587✔
451
            if (!this.scrollComponent || this.scrollComponent.destroyed) {
11,587✔
452
                this.scrollComponent = vc.createComponent(VirtualHelperComponent).instance;
4,265✔
453
            }
454

455
            this.scrollComponent.size = this.igxForOf ? this._calcSize() : 0;
11,587✔
456
            this.syncScrollService.setScrollMaster(this.igxForScrollOrientation, this.scrollComponent);
11,587✔
457
            this._zone.runOutsideAngular(() => {
11,587✔
458
                this.verticalScrollHandler = this.verticalScrollHandler.bind(this);
11,587✔
459
                this.scrollComponent.nativeElement.addEventListener('scroll', this.verticalScrollHandler);
11,587✔
460
                this.dc.instance.scrollContainer = this.scrollComponent.nativeElement;
11,587✔
461
            });
462
            const destructor = takeUntil<any>(this.destroy$);
11,587✔
463
            this.contentResizeNotify.pipe(
11,587✔
464
                filter(() => this.igxForContainerSize && this.igxForOf && this.igxForOf.length > 0),
5,230✔
465
                throttleTime(40, undefined, { leading: false, trailing: true }),
466
                destructor
467
            ).subscribe(() => this._zone.runTask(() => this.updateSizes()));
811✔
468
        }
469

470
        if (this.igxForScrollOrientation === 'horizontal') {
48,557✔
471
            this.func = (evt) => this.onHScroll(evt);
36,970✔
472
            this.scrollComponent = this.syncScrollService.getScrollMaster(this.igxForScrollOrientation);
36,970✔
473
            if (!this.scrollComponent) {
36,970✔
474
                this.scrollComponent = vc.createComponent(HVirtualHelperComponent).instance;
3,574✔
475
                this.scrollComponent.size = this.igxForOf ? this._calcSize() : 0;
3,574!
476
                this.syncScrollService.setScrollMaster(this.igxForScrollOrientation, this.scrollComponent);
3,574✔
477
                this._zone.runOutsideAngular(() => {
3,574✔
478
                    this.scrollComponent.nativeElement.addEventListener('scroll', this.func);
3,574✔
479
                    this.dc.instance.scrollContainer = this.scrollComponent.nativeElement;
3,574✔
480
                });
481
            } else {
482
                this._zone.runOutsideAngular(() => {
33,396✔
483
                    this.scrollComponent.nativeElement.addEventListener('scroll', this.func);
33,396✔
484
                    this.dc.instance.scrollContainer = this.scrollComponent.nativeElement;
33,396✔
485
                });
486
            }
487
            this._updateScrollOffset();
36,970✔
488
        }
489
    }
490

491
    public ngAfterViewInit(): void {
492
        if (this.igxForScrollOrientation === 'vertical') {
48,557✔
493
            this._zone.runOutsideAngular(() => {
11,587✔
494
                this.contentObserver = new (getResizeObserver())(() => this.contentResizeNotify.next());
11,587✔
495
                this.contentObserver.observe(this.dc.instance._viewContainer.element.nativeElement);
11,587✔
496
            });
497
        }
498
    }
499

500
    /**
501
     * @hidden
502
     */
503
    public ngOnDestroy() {
504
        this.removeScrollEventListeners();
48,145✔
505
        this.destroy$.next(true);
48,145✔
506
        this.destroy$.complete();
48,145✔
507
        if (this.contentObserver) {
48,145✔
508
            this.contentObserver.disconnect();
11,506✔
509
        }
510
    }
511

512
    /**
513
     * @hidden @internal
514
     * Asserts the correct type of the context for the template that `igxForOf` will render.
515
     *
516
     * The presence of this method is a signal to the Ivy template type-check compiler that the
517
     * `IgxForOf` structural directive renders its template with a specific context type.
518
     */
519
    public static ngTemplateContextGuard<T, U extends T[]>(dir: IgxForOfDirective<T, U>, ctx: any):
520
        ctx is IgxForOfContext<T, U> {
521
        return true;
×
522
    }
523

524
    /**
525
     * @hidden
526
     */
527
    public ngOnChanges(changes: SimpleChanges): void {
528
        const forOf = 'igxForOf';
2,100✔
529
        if (forOf in changes) {
2,100✔
530
            const value = changes[forOf].currentValue;
1,714✔
531
            if (!this._differ && value) {
1,714✔
532
                try {
992✔
533
                    this._differ = this._differs.find(value).create(this.igxForTrackBy);
992✔
534
                } catch (e) {
535
                    throw new Error(
×
536
                        `Cannot find a differ supporting object "${value}" of type "${getTypeNameForDebugging(value)}".
537
                     NgFor only supports binding to Iterables such as Arrays.`);
538
                }
539
            }
540
        }
541
        const defaultItemSize = 'igxForItemSize';
2,100✔
542
        if (defaultItemSize in changes && !changes[defaultItemSize].firstChange && this.igxForOf) {
2,100✔
543
            // handle default item size changed.
544
            this.initSizesCache(this.igxForOf);
173✔
545
            this._applyChanges();
173✔
546
        }
547
        const containerSize = 'igxForContainerSize';
2,100✔
548
        if (containerSize in changes && !changes[containerSize].firstChange && this.igxForOf) {
2,100✔
549
            const prevSize = parseInt(changes[containerSize].previousValue, 10);
275✔
550
            const newSize = parseInt(changes[containerSize].currentValue, 10);
275✔
551
            this._recalcOnContainerChange({prevSize, newSize});
275✔
552
        }
553
    }
554

555
    /**
556
     * @hidden
557
     */
558
    public ngDoCheck(): void {
559
        if (this._differ) {
9,617✔
560
            const changes = this._differ.diff(this.igxForOf);
9,617✔
561
            if (changes) {
9,617✔
562
                //  re-init cache.
563
                if (!this.igxForOf) {
1,033✔
564
                    this.igxForOf = [] as U;
1✔
565
                }
566
                this._updateSizeCache();
1,033✔
567
                this._zone.run(() => {
1,033✔
568
                    this._applyChanges();
1,033✔
569
                    this.cdr.markForCheck();
1,033✔
570
                    this._updateScrollOffset();
1,033✔
571
                    const args: IForOfDataChangingEventArgs = {
1,033✔
572
                        containerSize: this.igxForContainerSize,
573
                        state: this.state
574
                    };
575
                    this.dataChanged.emit(args);
1,033✔
576
                });
577
            }
578
        }
579
    }
580

581

582
    /**
583
     * Shifts the scroll thumb position.
584
     * ```typescript
585
     * this.parentVirtDir.addScroll(5);
586
     * ```
587
     *
588
     * @param addTop negative value to scroll up and positive to scroll down;
589
     */
590
    public addScrollTop(add: number): boolean {
591
        return this.addScroll(add);
24✔
592
    }
593

594
    /**
595
     * Shifts the scroll thumb position.
596
     * ```typescript
597
     * this.parentVirtDir.addScroll(5);
598
     * ```
599
     *
600
     * @param add negative value to scroll previous and positive to scroll next;
601
     */
602
    public addScroll(add: number): boolean {
603
        if (add === 0) {
53!
604
            return false;
×
605
        }
606
        const originalVirtScrollTop = this._virtScrollPosition;
53✔
607
        const containerSize = parseInt(this.igxForContainerSize, 10);
53✔
608
        const maxVirtScrollTop = this._virtSize - containerSize;
53✔
609

610
        this._bScrollInternal = true;
53✔
611
        this._virtScrollPosition += add;
53✔
612
        this._virtScrollPosition = this._virtScrollPosition > 0 ?
53✔
613
            (this._virtScrollPosition < maxVirtScrollTop ? this._virtScrollPosition : maxVirtScrollTop) :
51✔
614
            0;
615

616
        this.scrollPosition += add / this._virtRatio;
53✔
617
        if (Math.abs(add / this._virtRatio) < 1) {
53!
618
            // Actual scroll delta that was added is smaller than 1 and onScroll handler doesn't trigger when scrolling < 1px
619
            const scrollOffset = this.fixedUpdateAllElements(this._virtScrollPosition);
×
620
            // scrollOffset = scrollOffset !== parseInt(this.igxForItemSize, 10) ? scrollOffset : 0;
621
            this.dc.instance._viewContainer.element.nativeElement.style.top = -(scrollOffset) + 'px';
×
622
        }
623

624
        const maxRealScrollTop = this.scrollComponent.nativeElement.scrollHeight - containerSize;
53✔
625
        if ((this._virtScrollPosition > 0 && this.scrollPosition === 0) ||
53✔
626
            (this._virtScrollPosition < maxVirtScrollTop && this.scrollPosition === maxRealScrollTop)) {
627
            // Actual scroll position is at the top or bottom, but virtual one is not at the top or bottom (there's more to scroll)
628
            // Recalculate actual scroll position based on the virtual scroll.
629
            this.scrollPosition = this._virtScrollPosition / this._virtRatio;
21✔
630
        } else if (this._virtScrollPosition === 0 && this.scrollPosition > 0) {
32!
631
            // Actual scroll position is not at the top, but virtual scroll is. Just update the actual scroll
UNCOV
632
            this.scrollPosition = 0;
×
633
        } else if (this._virtScrollPosition === maxVirtScrollTop && this.scrollPosition < maxRealScrollTop) {
32✔
634
            // Actual scroll position is not at the bottom, but virtual scroll is. Just update the acual scroll
635
            this.scrollPosition = maxRealScrollTop;
12✔
636
        }
637
        return this._virtScrollPosition !== originalVirtScrollTop;
53✔
638
    }
639

640
    /**
641
     * Scrolls to the specified index.
642
     * ```typescript
643
     * this.parentVirtDir.scrollTo(5);
644
     * ```
645
     *
646
     * @param index
647
     */
648
    public scrollTo(index: number) {
649
        if (index < 0 || index > (this.isRemote ? this.totalItemCount : this.igxForOf.length) - 1) {
754✔
650
            return;
16✔
651
        }
652
        const containerSize = parseInt(this.igxForContainerSize, 10);
738✔
653
        const isPrevItem = index < this.state.startIndex || this.scrollPosition > this.sizesCache[index];
738✔
654
        let nextScroll = isPrevItem ? this.sizesCache[index] : this.sizesCache[index + 1] - containerSize;
738✔
655
        if (nextScroll < 0) {
738✔
656
            return;
335✔
657
        }
658
        const maxVirtScrollTop = this._virtSize - containerSize;
403✔
659
        if (nextScroll > maxVirtScrollTop) {
403!
660
            nextScroll = maxVirtScrollTop;
×
661
        }
662
        this._bScrollInternal = true;
403✔
663
        this._virtScrollPosition = nextScroll;
403✔
664
        this.scrollPosition = this._virtScrollPosition / this._virtRatio;
403✔
665
        this._adjustToIndex = !isPrevItem ? index : null;
403✔
666
    }
667

668
    /**
669
     * Scrolls by one item into the appropriate next direction.
670
     * For "horizontal" orientation that will be the right column and for "vertical" that is the lower row.
671
     * ```typescript
672
     * this.parentVirtDir.scrollNext();
673
     * ```
674
     */
675
    public scrollNext() {
676
        const scr = Math.abs(Math.ceil(this.scrollPosition));
5✔
677
        const endIndex = this.getIndexAt(scr + parseInt(this.igxForContainerSize, 10), this.sizesCache);
5✔
678
        this.scrollTo(endIndex);
5✔
679
    }
680

681
    /**
682
     * Scrolls by one item into the appropriate previous direction.
683
     * For "horizontal" orientation that will be the left column and for "vertical" that is the upper row.
684
     * ```typescript
685
     * this.parentVirtDir.scrollPrev();
686
     * ```
687
     */
688
    public scrollPrev() {
689
        this.scrollTo(this.state.startIndex - 1);
2✔
690
    }
691

692
    /**
693
     * Scrolls by one page into the appropriate next direction.
694
     * For "horizontal" orientation that will be one view to the right and for "vertical" that is one view to the bottom.
695
     * ```typescript
696
     * this.parentVirtDir.scrollNextPage();
697
     * ```
698
     */
699
    public scrollNextPage() {
700
        this.addScroll(parseInt(this.igxForContainerSize, 10));
2✔
701
    }
702

703
    /**
704
     * Scrolls by one page into the appropriate previous direction.
705
     * For "horizontal" orientation that will be one view to the left and for "vertical" that is one view to the top.
706
     * ```typescript
707
     * this.parentVirtDir.scrollPrevPage();
708
     * ```
709
     */
710
    public scrollPrevPage() {
711
        const containerSize = (parseInt(this.igxForContainerSize, 10));
2✔
712
        this.addScroll(-containerSize);
2✔
713
    }
714

715
    /**
716
     * @hidden
717
     */
718
    public getColumnScrollLeft(colIndex) {
719
        return this.sizesCache[colIndex];
3,985✔
720
    }
721

722
    /**
723
     * Returns the total number of items that are fully visible.
724
     * ```typescript
725
     * this.parentVirtDir.getItemCountInView();
726
     * ```
727
     */
728
    public getItemCountInView() {
729
        let startIndex = this.getIndexAt(this.scrollPosition, this.sizesCache);
2✔
730
        if (this.scrollPosition - this.sizesCache[startIndex] > 0) {
2✔
731
            // fisrt item is not fully in view
732
            startIndex++;
2✔
733
        }
734
        const endIndex = this.getIndexAt(this.scrollPosition + parseInt(this.igxForContainerSize, 10), this.sizesCache);
2✔
735
        return endIndex - startIndex;
2✔
736
    }
737

738
    /**
739
     * Returns a reference to the scrollbar DOM element.
740
     * This is either a vertical or horizontal scrollbar depending on the specified igxForScrollOrientation.
741
     * ```typescript
742
     * dir.getScroll();
743
     * ```
744
     */
745
    public getScroll() {
746
        return this.scrollComponent?.nativeElement;
18,083✔
747
    }
748
    /**
749
     * Returns the size of the element at the specified index.
750
     * ```typescript
751
     * this.parentVirtDir.getSizeAt(1);
752
     * ```
753
     */
754
    public getSizeAt(index: number) {
755
        return this.sizesCache[index + 1] - this.sizesCache[index];
1,132✔
756
    }
757

758
    /**
759
     * @hidden
760
     * Function that is called to get the native scrollbar size that the browsers renders.
761
     */
762
    public getScrollNativeSize() {
763
        return this.scrollComponent ? this.scrollComponent.scrollNativeSize : 0;
186,549!
764
    }
765

766
    /**
767
     * Returns the scroll offset of the element at the specified index.
768
     * ```typescript
769
     * this.parentVirtDir.getScrollForIndex(1);
770
     * ```
771
     */
772
    public getScrollForIndex(index: number, bottom?: boolean) {
773
        const containerSize = parseInt(this.igxForContainerSize, 10);
123✔
774
        const scroll = bottom ? Math.max(0, this.sizesCache[index + 1] - containerSize) : this.sizesCache[index];
123✔
775
        return scroll;
123✔
776
    }
777

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

805
    /**
806
     * @hidden
807
     * Function that recalculates and updates cache sizes.
808
     */
809
    public recalcUpdateSizes() {
810
        const dimension = this.igxForScrollOrientation === 'horizontal' ?
2,206✔
811
            this.igxForSizePropName : 'height';
812
        const diffs = [];
2,206✔
813
        let totalDiff = 0;
2,206✔
814
        const l = this._embeddedViews.length;
2,206✔
815
        const rNodes = this._embeddedViews.map(view =>
2,206✔
816
            view.rootNodes.find(node => node.nodeType === Node.ELEMENT_NODE) || view.rootNodes[0].nextElementSibling);
20,065✔
817
        for (let i = 0; i < l; i++) {
2,206✔
818
            const rNode = rNodes[i];
12,872✔
819
            if (rNode) {
12,872✔
820
                const height = window.getComputedStyle(rNode).getPropertyValue('height');
12,866✔
821
                const h = parseFloat(height) || parseInt(this.igxForItemSize, 10);
12,866✔
822
                const index = this.state.startIndex + i;
12,866✔
823
                if (!this.isRemote && !this.igxForOf[index]) {
12,866✔
824
                    continue;
15✔
825
                }
826
                const margin = this.getMargin(rNode, dimension);
12,851✔
827
                const oldVal = this.individualSizeCache[index];
12,851✔
828
                const newVal = (dimension === 'height' ? h : rNode.clientWidth) + margin;
12,851✔
829
                this.individualSizeCache[index] = newVal;
12,851✔
830
                const currDiff = newVal - oldVal;
12,851✔
831
                diffs.push(currDiff);
12,851✔
832
                totalDiff += currDiff;
12,851✔
833
                this.sizesCache[index + 1] = (this.sizesCache[index] || 0) + newVal;
12,851✔
834
            }
835
        }
836
        // update cache
837
        if (Math.abs(totalDiff) > 0) {
2,206✔
838
            for (let j = this.state.startIndex + this.state.chunkSize + 1; j < this.sizesCache.length; j++) {
388✔
839
                this.sizesCache[j] = (this.sizesCache[j] || 0) + totalDiff;
8,370!
840
            }
841

842
            // update scrBar heights/widths
843
            const reducer = (acc, val) => acc + val;
14,314✔
844

845
            const hSum = this.individualSizeCache.reduce(reducer);
388✔
846
            if (hSum > this._maxSize) {
388!
847
                this._virtRatio = hSum / this._maxSize;
×
848
            }
849
            this.scrollComponent.size = Math.min(this.scrollComponent.size + totalDiff, this._maxSize);
388✔
850
            this._virtSize = hSum;
388✔
851
            if (!this.scrollComponent.destroyed) {
388✔
852
                this.scrollComponent.cdr.detectChanges();
384✔
853
            }
854
            const scrToBottom = this._isScrolledToBottom && !this.dc.instance.notVirtual;
388✔
855
            if (scrToBottom && !this._isAtBottomIndex) {
388✔
856
                const containerSize = parseInt(this.igxForContainerSize, 10);
8✔
857
                const maxVirtScrollTop = this._virtSize - containerSize;
8✔
858
                this._bScrollInternal = true;
8✔
859
                this._virtScrollPosition = maxVirtScrollTop;
8✔
860
                this.scrollPosition = maxVirtScrollTop;
8✔
861
                return;
8✔
862
            }
863
            if (this._adjustToIndex) {
380✔
864
                // in case scrolled to specific index where after scroll heights are changed
865
                // need to adjust the offsets so that item is last in view.
866
                const updatesToIndex = this._adjustToIndex - this.state.startIndex + 1;
38✔
867
                const sumDiffs = diffs.slice(0, updatesToIndex).reduce(reducer);
38✔
868
                if (sumDiffs !== 0) {
38✔
869
                    this.addScroll(sumDiffs);
25✔
870
                }
871
                this._adjustToIndex = null;
38✔
872
            }
873
        }
874
    }
875

876
    /**
877
     * @hidden
878
     * Reset scroll position.
879
     * Needed in case scrollbar is hidden/detached but we still need to reset it.
880
     */
881
    public resetScrollPosition() {
882
        this.scrollPosition = 0;
43,452✔
883
        this.scrollComponent.scrollAmount = 0;
43,452✔
884
    }
885

886
    /**
887
     * @hidden
888
     */
889
    protected removeScrollEventListeners() {
890
        if (this.igxForScrollOrientation === 'horizontal') {
95,710✔
891
            this._zone.runOutsideAngular(() => this.scrollComponent?.nativeElement?.removeEventListener('scroll', this.func));
73,335✔
892
        } else {
893
            this._zone.runOutsideAngular(() =>
22,375✔
894
                this.scrollComponent?.nativeElement?.removeEventListener('scroll', this.verticalScrollHandler)
22,375✔
895
            );
896
        }
897
    }
898

899
    /**
900
     * @hidden
901
     * Function that is called when scrolling vertically
902
     */
903
    protected onScroll(event) {
904
        /* in certain situations this may be called when no scrollbar is visible */
905
        if (!parseInt(this.scrollComponent.nativeElement.style.height, 10)) {
68!
906
            return;
×
907
        }
908
        if (!this._bScrollInternal) {
68✔
909
            this._calcVirtualScrollPosition(event.target.scrollTop);
35✔
910
        } else {
911
            this._bScrollInternal = false;
33✔
912
        }
913
        const prevStartIndex = this.state.startIndex;
68✔
914
        const scrollOffset = this.fixedUpdateAllElements(this._virtScrollPosition);
68✔
915

916
        this.dc.instance._viewContainer.element.nativeElement.style.top = -(scrollOffset) + 'px';
68✔
917

918
        this._zone.onStable.pipe(first()).subscribe(this.recalcUpdateSizes.bind(this));
68✔
919

920
        this.dc.changeDetectorRef.detectChanges();
68✔
921
        if (prevStartIndex !== this.state.startIndex) {
68✔
922
            this.chunkLoad.emit(this.state);
55✔
923
        }
924
    }
925

926

927
    /**
928
     * @hidden
929
     * @internal
930
     */
931
    public updateScroll(): void {
932
        if (this.igxForScrollOrientation === "horizontal") {
886✔
933
            const scrollAmount = this.scrollComponent.nativeElement["scrollLeft"];
886✔
934
            this.scrollComponent.scrollAmount = scrollAmount;
886✔
935
            this._updateScrollOffset();
886✔
936
        }
937
    }
938

939
    protected updateSizes() {
940
        if (!this.scrollComponent.nativeElement.isConnected) return;
811✔
941
        const scrollable = this.isScrollable();
760✔
942
        this.recalcUpdateSizes();
760✔
943
        this._applyChanges();
760✔
944
        this._updateScrollOffset();
760✔
945
        if (scrollable !== this.isScrollable()) {
760✔
946
            this.scrollbarVisibilityChanged.emit();
9✔
947
        } else {
948
            this.contentSizeChange.emit();
751✔
949
        }
950
    }
951

952
    /**
953
     * @hidden
954
     */
955
    protected fixedUpdateAllElements(inScrollTop: number): number {
956
        const count = this.isRemote ? this.totalItemCount : this.igxForOf.length;
3,088✔
957
        let newStart = this.getIndexAt(inScrollTop, this.sizesCache);
3,088✔
958

959
        if (newStart + this.state.chunkSize > count) {
3,088✔
960
            newStart = count - this.state.chunkSize;
839✔
961
        }
962

963
        const prevStart = this.state.startIndex;
3,088✔
964
        const diff = newStart - this.state.startIndex;
3,088✔
965
        this.state.startIndex = newStart;
3,088✔
966

967
        if (diff) {
3,088✔
968
            this.chunkPreload.emit(this.state);
963✔
969
            if (!this.isRemote) {
963✔
970

971
                // recalculate and apply page size.
972
                if (diff && Math.abs(diff) <= MAX_PERF_SCROLL_DIFF) {
952✔
973
                    if (diff > 0) {
702✔
974
                        this.moveApplyScrollNext(prevStart);
504✔
975
                    } else {
976
                        this.moveApplyScrollPrev(prevStart);
198✔
977
                    }
978
                } else {
979
                    this.fixedApplyScroll();
250✔
980
                }
981
            }
982
        }
983

984
        return inScrollTop - this.sizesCache[this.state.startIndex];
3,088✔
985
    }
986

987
    /**
988
     * @hidden
989
     * The function applies an optimized state change for scrolling down/right employing context change with view rearrangement
990
     */
991
    protected moveApplyScrollNext(prevIndex: number): void {
992
        const start = prevIndex + this.state.chunkSize;
504✔
993
        const end = start + this.state.startIndex - prevIndex;
504✔
994
        const container = this.dc.instance._vcr as ViewContainerRef;
504✔
995

996
        for (let i = start; i < end && this.igxForOf[i] !== undefined; i++) {
504✔
997
            const embView = this._embeddedViews.shift();
818✔
998
            if (!embView.destroyed) {
818✔
999
                this.scrollFocus(embView.rootNodes.find(node => node.nodeType === Node.ELEMENT_NODE)
1,405!
1000
                    || embView.rootNodes[0].nextElementSibling);
1001
                const view = container.detach(0);
818✔
1002

1003
                this.updateTemplateContext(embView.context, i);
818✔
1004
                container.insert(view);
818✔
1005
                this._embeddedViews.push(embView);
818✔
1006
            }
1007
        }
1008
    }
1009

1010
    /**
1011
     * @hidden
1012
     * The function applies an optimized state change for scrolling up/left employing context change with view rearrangement
1013
     */
1014
    protected moveApplyScrollPrev(prevIndex: number): void {
1015
        const container = this.dc.instance._vcr as ViewContainerRef;
198✔
1016
        for (let i = prevIndex - 1; i >= this.state.startIndex && this.igxForOf[i] !== undefined; i--) {
198✔
1017
            const embView = this._embeddedViews.pop();
244✔
1018
            if (!embView.destroyed) {
244✔
1019
                this.scrollFocus(embView.rootNodes.find(node => node.nodeType === Node.ELEMENT_NODE)
523!
1020
                    || embView.rootNodes[0].nextElementSibling);
1021
                const view = container.detach(container.length - 1);
244✔
1022

1023
                this.updateTemplateContext(embView.context, i);
244✔
1024
                container.insert(view, 0);
244✔
1025
                this._embeddedViews.unshift(embView);
244✔
1026
            }
1027
        }
1028
    }
1029

1030
    /**
1031
     * @hidden
1032
     */
1033
    protected getContextIndex(input) {
1034
        return this.isRemote ? this.state.startIndex + this.igxForOf.indexOf(input) : this.igxForOf.indexOf(input);
464,068✔
1035
    }
1036

1037
    /**
1038
     * @hidden
1039
     * Function which updates the passed context of an embedded view with the provided index
1040
     * from the view container.
1041
     * Often, called while handling a scroll event.
1042
     */
1043
    protected updateTemplateContext(context: any, index = 0): void {
×
1044
        context.$implicit = this.igxForOf[index];
253,131✔
1045
        context.index = this.getContextIndex(this.igxForOf[index]);
253,131✔
1046
        context.count = this.igxForOf.length;
253,131✔
1047
    }
1048

1049
    /**
1050
     * @hidden
1051
     * The function applies an optimized state change through context change for each view
1052
     */
1053
    protected fixedApplyScroll(): void {
1054
        let j = 0;
250✔
1055
        const endIndex = this.state.startIndex + this.state.chunkSize;
250✔
1056
        for (let i = this.state.startIndex; i < endIndex && this.igxForOf[i] !== undefined; i++) {
250✔
1057
            const embView = this._embeddedViews[j++];
1,653✔
1058
            this.updateTemplateContext(embView.context, i);
1,653✔
1059
        }
1060
    }
1061

1062
    /**
1063
     * @hidden
1064
     * @internal
1065
     *
1066
     * Clears focus inside the virtualized container on small scroll swaps.
1067
     */
1068
    protected scrollFocus(node?: HTMLElement): void {
1069
        if (!node) {
1,062!
1070
            return;
×
1071
        }
1072
        const document = node.getRootNode() as Document | ShadowRoot;
1,062✔
1073
        const activeElement = document.activeElement as HTMLElement;
1,062✔
1074

1075
        // Remove focus in case the the active element is inside the view container.
1076
        // Otherwise we hit an exception while doing the 'small' scrolls swapping.
1077
        // For more information:
1078
        //
1079
        // https://developer.mozilla.org/en-US/docs/Web/API/Node/removeChild
1080
        // https://bugs.chromium.org/p/chromium/issues/detail?id=432392
1081
        if (node && node.contains(activeElement)) {
1,062!
1082
            activeElement.blur();
×
1083
        }
1084
    }
1085

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

1113
        this.dc.changeDetectorRef.detectChanges();
107✔
1114
        if (prevStartIndex !== this.state.startIndex) {
107✔
1115
            this.chunkLoad.emit(this.state);
70✔
1116
        }
1117
    }
1118

1119
    /**
1120
     * Gets the function used to track changes in the items collection.
1121
     * By default the object references are compared. However this can be optimized if you have unique identifier
1122
     * value that can be used for the comparison instead of the object ref or if you have some other property values
1123
     * in the item object that should be tracked for changes.
1124
     * This option is similar to ngForTrackBy.
1125
     * ```typescript
1126
     * const trackFunc = this.parentVirtDir.igxForTrackBy;
1127
     * ```
1128
     */
1129
    @Input()
1130
    public get igxForTrackBy(): TrackByFunction<T> {
1131
        return this._trackByFn;
48,555✔
1132
    }
1133

1134
    /**
1135
     * Sets the function used to track changes in the items collection.
1136
     * This function can be set in scenarios where you want to optimize or
1137
     * customize the tracking of changes for the items in the collection.
1138
     * The igxForTrackBy function takes the index and the current item as arguments and needs to return the unique identifier for this item.
1139
     * ```typescript
1140
     * this.parentVirtDir.igxForTrackBy = (index, item) => {
1141
     *      return item.id + item.width;
1142
     * };
1143
     * ```
1144
     */
1145
    public set igxForTrackBy(fn: TrackByFunction<T>) {
1146
        this._trackByFn = fn;
39,592✔
1147
    }
1148

1149
    /**
1150
     * @hidden
1151
     */
1152
    protected _applyChanges() {
1153
        const prevChunkSize = this.state.chunkSize;
1,247✔
1154
        this.applyChunkSizeChange();
1,247✔
1155
        this._recalcScrollBarSize();
1,247✔
1156
        if (this.igxForOf && this.igxForOf.length && this.dc) {
1,247✔
1157
            const embeddedViewCopy = Object.assign([], this._embeddedViews);
998✔
1158
            let startIndex = this.state.startIndex;
998✔
1159
            let endIndex = this.state.chunkSize + this.state.startIndex;
998✔
1160
            if (this.isRemote) {
998✔
1161
                startIndex = 0;
28✔
1162
                endIndex = this.igxForOf.length;
28✔
1163
            }
1164
            for (let i = startIndex; i < endIndex && this.igxForOf[i] !== undefined; i++) {
998✔
1165
                const embView = embeddedViewCopy.shift();
7,325✔
1166
                this.updateTemplateContext(embView.context, i);
7,325✔
1167
            }
1168
            if (prevChunkSize !== this.state.chunkSize) {
998✔
1169
                this.chunkLoad.emit(this.state);
358✔
1170
            }
1171
        }
1172
    }
1173

1174
    /**
1175
     * @hidden
1176
     */
1177
    protected _calcMaxBrowserSize(): number {
1178
        if (!this.platformUtil.isBrowser) {
48,557!
1179
            return 0;
×
1180
        }
1181
        const div = this.document.createElement('div');
48,557✔
1182
        const style = div.style;
48,557✔
1183
        style.position = 'absolute';
48,557✔
1184
        const dir = this.igxForScrollOrientation === 'horizontal' ? 'left' : 'top';
48,557✔
1185
        style[dir] = '9999999999999999px';
48,557✔
1186
        this.document.body.appendChild(div);
48,557✔
1187
        const size = Math.abs(div.getBoundingClientRect()[dir]);
48,557✔
1188
        this.document.body.removeChild(div);
48,557✔
1189
        return size;
48,557✔
1190
    }
1191

1192
    /**
1193
     * @hidden
1194
     * Recalculates the chunkSize based on current startIndex and returns the new size.
1195
     * This should be called after this.state.startIndex is updated, not before.
1196
     */
1197
    protected _calculateChunkSize(): number {
1198
        let chunkSize = 0;
131,771✔
1199
        if (this.igxForContainerSize !== null && this.igxForContainerSize !== undefined) {
131,771✔
1200
            if (!this.sizesCache || this.sizesCache.length === 0) {
130,936✔
1201
                this.initSizesCache(this.igxForOf);
14,524✔
1202
            }
1203
            chunkSize = this._calcMaxChunkSize();
130,936✔
1204
            if (this.igxForOf && chunkSize > this.igxForOf.length) {
130,936✔
1205
                chunkSize = this.igxForOf.length;
11,986✔
1206
            }
1207
        } else {
1208
            if (this.igxForOf) {
835✔
1209
                chunkSize = this.igxForOf.length;
835✔
1210
            }
1211
        }
1212
        return chunkSize;
131,771✔
1213
    }
1214

1215
    /**
1216
     * @hidden
1217
     */
1218
    protected getElement(viewref, nodeName) {
1219
        const elem = viewref.element.nativeElement.parentNode.getElementsByTagName(nodeName);
×
1220
        return elem.length > 0 ? elem[0] : null;
×
1221
    }
1222

1223
    /**
1224
     * @hidden
1225
     */
1226
    protected initSizesCache(items: U): number {
1227
        let totalSize = 0;
2,644✔
1228
        let size = 0;
2,644✔
1229
        const dimension = this.igxForSizePropName || 'height';
2,644!
1230
        let i = 0;
2,644✔
1231
        this.sizesCache = [];
2,644✔
1232
        this.individualSizeCache = [];
2,644✔
1233
        this.sizesCache.push(0);
2,644✔
1234
        const count = this.isRemote ? this.totalItemCount : items.length;
2,644✔
1235
        for (i; i < count; i++) {
2,644✔
1236
            size = this._getItemSize(items[i], dimension);
3,752,137✔
1237
            this.individualSizeCache.push(size);
3,752,137✔
1238
            totalSize += size;
3,752,137✔
1239
            this.sizesCache.push(totalSize);
3,752,137✔
1240
        }
1241
        return totalSize;
2,644✔
1242
    }
1243

1244
    protected _updateSizeCache() {
1245
        if (this.igxForScrollOrientation === 'horizontal') {
1,033✔
1246
            this.initSizesCache(this.igxForOf);
332✔
1247
            return;
332✔
1248
        }
1249
        const oldHeight = this.individualSizeCache.length > 0 ? this.individualSizeCache.reduce((acc, val) => acc + val) : 0;
2,049,005✔
1250
        const newHeight = this.initSizesCache(this.igxForOf);
701✔
1251

1252
        const diff = oldHeight - newHeight;
701✔
1253
        this._adjustScrollPositionAfterSizeChange(diff);
701✔
1254
    }
1255

1256
    /**
1257
     * @hidden
1258
     */
1259
    protected _calcMaxChunkSize(): number {
1260
        let i = 0;
53,577✔
1261
        let length = 0;
53,577✔
1262
        let maxLength = 0;
53,577✔
1263
        const arr = [];
53,577✔
1264
        let sum = 0;
53,577✔
1265
        const availableSize = parseInt(this.igxForContainerSize, 10);
53,577✔
1266
        if (!availableSize) {
53,577✔
1267
            return 0;
10,565✔
1268
        }
1269
        const dimension = this.igxForScrollOrientation === 'horizontal' ?
43,012✔
1270
            this.igxForSizePropName : 'height';
1271
        const reducer = (accumulator, currentItem) => accumulator + this._getItemSize(currentItem, dimension);
37,312,152✔
1272
        for (i; i < this.igxForOf.length; i++) {
43,012✔
1273
            let item: T | { value: T, height: number } = this.igxForOf[i];
5,371,391✔
1274
            if (dimension === 'height') {
5,371,391✔
1275
                item = { value: this.igxForOf[i], height: this.individualSizeCache[i] };
5,037,623✔
1276
            }
1277
            const size = dimension === 'height' ?
5,371,391✔
1278
                this.individualSizeCache[i] :
1279
                this._getItemSize(item, dimension);
1280
            sum = arr.reduce(reducer, size);
5,371,391✔
1281
            if (sum < availableSize) {
5,371,391✔
1282
                arr.push(item);
177,669✔
1283
                length = arr.length;
177,669✔
1284
                if (i === this.igxForOf.length - 1) {
177,669✔
1285
                    // reached end without exceeding
1286
                    // include prev items until size is filled or first item is reached.
1287
                    let curItem = dimension === 'height' ? arr[0].value : arr[0];
15,715✔
1288
                    let prevIndex = this.igxForOf.indexOf(curItem) - 1;
15,715✔
1289
                    while (prevIndex >= 0 && sum <= availableSize) {
15,715✔
1290
                        curItem = dimension === 'height' ? arr[0].value : arr[0];
209✔
1291
                        prevIndex = this.igxForOf.indexOf(curItem) - 1;
209✔
1292
                        const prevItem = this.igxForOf[prevIndex];
209✔
1293
                        const prevSize = dimension === 'height' ?
209✔
1294
                            this.individualSizeCache[prevIndex] :
1295
                            parseInt(prevItem[dimension], 10);
1296
                        sum = arr.reduce(reducer, prevSize);
209✔
1297
                        arr.unshift(prevItem);
209✔
1298
                        length = arr.length;
209✔
1299
                    }
1300
                }
1301
            } else {
1302
                arr.push(item);
5,193,722✔
1303
                length = arr.length + 1;
5,193,722✔
1304
                arr.shift();
5,193,722✔
1305
            }
1306
            if (length > maxLength) {
5,371,391✔
1307
                maxLength = length;
204,234✔
1308
            }
1309
        }
1310
        return maxLength;
43,012✔
1311
    }
1312

1313
    /**
1314
     * @hidden
1315
     */
1316
    protected getIndexAt(left, set) {
1317
        let start = 0;
93,728✔
1318
        let end = set.length - 1;
93,728✔
1319
        if (left === 0) {
93,728✔
1320
            return 0;
91,085✔
1321
        }
1322
        while (start <= end) {
2,643✔
1323
            const midIdx = Math.floor((start + end) / 2);
10,067✔
1324
            const midLeft = set[midIdx];
10,067✔
1325
            const cmp = left - midLeft;
10,067✔
1326
            if (cmp > 0) {
10,067✔
1327
                start = midIdx + 1;
4,556✔
1328
            } else if (cmp < 0) {
5,511✔
1329
                end = midIdx - 1;
5,207✔
1330
            } else {
1331
                return midIdx;
304✔
1332
            }
1333
        }
1334
        return end;
2,339✔
1335
    }
1336

1337
    protected _recalcScrollBarSize(containerSizeInfo = null) {
58,493✔
1338
        const count = this.isRemote ? this.totalItemCount : (this.igxForOf ? this.igxForOf.length : 0);
91,052!
1339
        this.dc.instance.notVirtual = !(this.igxForContainerSize && this.dc && this.state.chunkSize < count);
91,052✔
1340
        const scrollable = containerSizeInfo ? this.scrollComponent.size > containerSizeInfo.prevSize : this.isScrollable();
91,052✔
1341
        if (this.igxForScrollOrientation === 'horizontal') {
91,052✔
1342
            const totalWidth = parseInt(this.igxForContainerSize, 10) > 0 ? this._calcSize() : 0;
74,989✔
1343
            if (totalWidth <= parseInt(this.igxForContainerSize, 10)) {
74,989✔
1344
                this.resetScrollPosition();
37,919✔
1345
            }
1346
            this.scrollComponent.nativeElement.style.width = this.igxForContainerSize + 'px';
74,989✔
1347
            this.scrollComponent.size = totalWidth;
74,989✔
1348
        }
1349
        if (this.igxForScrollOrientation === 'vertical') {
91,052✔
1350
            const totalHeight = this._calcSize();
16,063✔
1351
            if (totalHeight <= parseInt(this.igxForContainerSize, 10)) {
16,063✔
1352
                this.resetScrollPosition();
5,508✔
1353
            }
1354
            this.scrollComponent.nativeElement.style.height = parseInt(this.igxForContainerSize, 10) + 'px';
16,063✔
1355
            this.scrollComponent.size = totalHeight;
16,063✔
1356
        }
1357
        if (scrollable !== this.isScrollable()) {
91,052✔
1358
            // scrollbar visibility has changed
1359
            this.scrollbarVisibilityChanged.emit();
18,216✔
1360
        }
1361
    }
1362

1363
    protected _calcSize(): number {
1364
        let size;
1365
        if (this.individualSizeCache && this.individualSizeCache.length > 0) {
104,203✔
1366
            size = this.individualSizeCache.reduce((acc, val) => acc + val, 0);
5,841,306✔
1367
        } else {
1368
            size = this.initSizesCache(this.igxForOf);
8,490✔
1369
        }
1370
        this._virtSize = size;
104,203✔
1371
        if (size > this._maxSize) {
104,203!
1372
            this._virtRatio = size / this._maxSize;
×
1373
            size = this._maxSize;
×
1374
        }
1375
        return size;
104,203✔
1376
    }
1377

1378
    protected _recalcOnContainerChange(containerSizeInfo = null) {
×
1379
        const prevChunkSize = this.state.chunkSize;
32,559✔
1380
        this.applyChunkSizeChange();
32,559✔
1381
        this._recalcScrollBarSize(containerSizeInfo);
32,559✔
1382
        if (prevChunkSize !== this.state.chunkSize) {
32,559✔
1383
            this.chunkLoad.emit(this.state);
8,487✔
1384
        }
1385
    }
1386

1387
    /**
1388
     * @hidden
1389
     * Removes an element from the embedded views and updates chunkSize.
1390
     */
1391
    protected removeLastElem() {
1392
        const oldElem = this._embeddedViews.pop();
10,570✔
1393
        this.beforeViewDestroyed.emit(oldElem);
10,570✔
1394
        // also detach from ViewContainerRef to make absolutely sure this is removed from the view container.
1395
        this.dc.instance._vcr.detach(this.dc.instance._vcr.length - 1);
10,570✔
1396
        oldElem.destroy();
10,570✔
1397

1398
        this.state.chunkSize--;
10,570✔
1399
    }
1400

1401
    /**
1402
     * @hidden
1403
     * If there exists an element that we can create embedded view for creates it, appends it and updates chunkSize
1404
     */
1405
    protected addLastElem() {
1406
        let elemIndex = this.state.startIndex + this.state.chunkSize;
1,623✔
1407
        if (!this.isRemote && !this.igxForOf) {
1,623!
1408
            return;
×
1409
        }
1410

1411
        if (elemIndex >= this.igxForOf.length) {
1,623✔
1412
            elemIndex = this.igxForOf.length - this.state.chunkSize;
5✔
1413
        }
1414
        const input = this.igxForOf[elemIndex];
1,623✔
1415
        const embeddedView = this.dc.instance._vcr.createEmbeddedView(
1,623✔
1416
            this._template,
1417
            new IgxForOfContext<T, U>(input, this.igxForOf, this.getContextIndex(input), this.igxForOf.length)
1418
        );
1419

1420
        this._embeddedViews.push(embeddedView);
1,623✔
1421
        this.state.chunkSize++;
1,623✔
1422

1423
        this._zone.run(() => this.cdr.markForCheck());
1,623✔
1424
    }
1425

1426
    /**
1427
     * Recalculates chunkSize and adds/removes elements if need due to the change.
1428
     * this.state.chunkSize is updated in @addLastElem() or @removeLastElem()
1429
     */
1430
    protected applyChunkSizeChange() {
1431
        const chunkSize = this.isRemote ? (this.igxForOf ? this.igxForOf.length : 0) : this._calculateChunkSize();
91,052!
1432
        if (chunkSize > this.state.chunkSize) {
91,052✔
1433
            const diff = chunkSize - this.state.chunkSize;
8,692✔
1434
            for (let i = 0; i < diff; i++) {
8,692✔
1435
                this.addLastElem();
45,291✔
1436
            }
1437
        } else if (chunkSize < this.state.chunkSize) {
82,360✔
1438
            const diff = this.state.chunkSize - chunkSize;
4,222✔
1439
            for (let i = 0; i < diff; i++) {
4,222✔
1440
                this.removeLastElem();
10,570✔
1441
            }
1442
        }
1443
    }
1444

1445
    protected _calcVirtualScrollPosition(scrollPosition: number) {
1446
        const containerSize = parseInt(this.igxForContainerSize, 10);
207✔
1447
        const maxRealScrollPosition = this.scrollComponent.size - containerSize;
207✔
1448
        const realPercentScrolled = maxRealScrollPosition !== 0 ? scrollPosition / maxRealScrollPosition : 0;
207!
1449
        const maxVirtScroll = this._virtSize - containerSize;
207✔
1450
        this._virtScrollPosition = realPercentScrolled * maxVirtScroll;
207✔
1451
    }
1452

1453
    protected _getItemSize(item, dimension: string): number {
1454
        const dim = item ? item[dimension] : null;
42,116,309✔
1455
        return typeof dim === 'number' ? dim : parseInt(this.igxForItemSize, 10) || 0;
42,116,309✔
1456
    }
1457

1458
    protected _updateScrollOffset() {
1459
        let scrollOffset = 0;
96,176✔
1460
        let currentScroll = this.scrollPosition;
96,176✔
1461
        if (this._virtRatio !== 1) {
96,176!
1462
            this._calcVirtualScrollPosition(this.scrollPosition);
×
1463
            currentScroll = this._virtScrollPosition;
×
1464
        }
1465
        const scroll = this.scrollComponent.nativeElement;
96,176✔
1466
        scrollOffset = scroll && this.scrollComponent.size ?
96,176✔
1467
        currentScroll - this.sizesCache[this.state.startIndex] : 0;
1468
        const dir = this.igxForScrollOrientation === 'horizontal' ? 'left' : 'top';
96,176✔
1469
        this.dc.instance._viewContainer.element.nativeElement.style[dir] = -(scrollOffset) + 'px';
96,176✔
1470
    }
1471

1472
    protected _adjustScrollPositionAfterSizeChange(sizeDiff) {
1473
        // if data has been changed while container is scrolled
1474
        // should update scroll top/left according to change so that same startIndex is in view
1475
        if (Math.abs(sizeDiff) > 0 && this.scrollPosition > 0) {
40,474✔
1476
            this.recalcUpdateSizes();
94✔
1477
            const offset = this.igxForScrollOrientation === 'horizontal' ?
94✔
1478
                parseInt(this.dc.instance._viewContainer.element.nativeElement.style.left, 10) :
1479
                parseInt(this.dc.instance._viewContainer.element.nativeElement.style.top, 10);
1480
            const newSize = this.sizesCache[this.state.startIndex] - offset;
94✔
1481
            this.scrollPosition = newSize;
94✔
1482
            if (this.scrollPosition !== newSize) {
94✔
1483
                this.scrollComponent.scrollAmount = newSize;
32✔
1484
            }
1485
        }
1486
    }
1487

1488
    private getMargin(node, dimension: string): number {
1489
        const styles = window.getComputedStyle(node);
12,851✔
1490
        if (dimension === 'height') {
12,851✔
1491
            return parseFloat(styles['marginTop']) +
12,177✔
1492
                parseFloat(styles['marginBottom']) || 0;
1493
        }
1494
        return parseFloat(styles['marginLeft']) +
674✔
1495
            parseFloat(styles['marginRight']) || 0;
1496
    }
1497
}
1498

1499
export const getTypeNameForDebugging = (type: any): string => type.name || typeof type;
2!
1500

1501
export interface IForOfState extends IBaseEventArgs {
1502
    startIndex?: number;
1503
    chunkSize?: number;
1504
}
1505

1506
export interface IForOfDataChangingEventArgs extends IBaseEventArgs {
1507
    containerSize: number;
1508
    state: IForOfState;
1509
}
1510

1511
export class IgxGridForOfContext<T, U extends T[] = T[]> extends IgxForOfContext<T, U> {
1512
    constructor(
1513
        $implicit: T,
1514
        public igxGridForOf: U,
43,668✔
1515
        index: number,
1516
        count: number
1517
    ) {
1518
        super($implicit, igxGridForOf, index, count);
43,668✔
1519
    }
1520
}
1521

1522
@Directive({
1523
    selector: '[igxGridFor][igxGridForOf]',
1524
    standalone: true
1525
})
1526
export class IgxGridForOfDirective<T, U extends T[] = T[]> extends IgxForOfDirective<T, U> implements OnInit, OnChanges, DoCheck {
2✔
1527
    @Input()
1528
    public set igxGridForOf(value: U & T[] | null) {
1529
        this.igxForOf = value;
147,875✔
1530
    }
1531

1532
    public get igxGridForOf() {
1533
        return this.igxForOf;
33✔
1534
    }
1535

1536
    @Input({ transform: booleanAttribute })
1537
    public igxGridForOfUniqueSizeCache = false;
47,565✔
1538

1539
    @Input({ transform: booleanAttribute })
1540
    public igxGridForOfVariableSizes = true;
47,565✔
1541

1542
    /**
1543
     * @hidden
1544
     * @internal
1545
     */
1546
    public override get sizesCache(): number[] {
1547
        if (this.igxForScrollOrientation === 'horizontal') {
950,858✔
1548
            if (this.igxGridForOfUniqueSizeCache || this.syncService.isMaster(this)) {
762,259✔
1549
                return this._sizesCache;
481,593✔
1550
            }
1551
            return this.syncService.sizesCache(this.igxForScrollOrientation);
280,666✔
1552
        } else {
1553
            return this._sizesCache;
188,599✔
1554
        }
1555
    }
1556
    /**
1557
     * @hidden
1558
     * @internal
1559
     */
1560
    public override set sizesCache(value: number[]) {
1561
        this._sizesCache = value;
78,190✔
1562
    }
1563

1564
    protected get itemsDimension() {
1565
        return this.igxForSizePropName || 'height';
×
1566
    }
1567

1568
    public override recalcUpdateSizes() {
1569
        if (this.igxGridForOfVariableSizes && this.igxForScrollOrientation === 'vertical') {
1,138✔
1570
            super.recalcUpdateSizes();
1,062✔
1571
        }
1572
    }
1573

1574
    /**
1575
     * @hidden @internal
1576
     * An event that is emitted after data has been changed but before the view is refreshed
1577
     */
1578
    @Output()
1579
    public dataChanging = new EventEmitter<IForOfDataChangingEventArgs>();
47,565✔
1580

1581
    constructor(
1582
        _viewContainer: ViewContainerRef,
1583
        _template: TemplateRef<NgForOfContext<T>>,
1584
        _differs: IterableDiffers,
1585
        cdr: ChangeDetectorRef,
1586
        _zone: NgZone,
1587
        _platformUtil: PlatformUtil,
1588
        @Inject(DOCUMENT) _document: any,
1589
        syncScrollService: IgxForOfScrollSyncService,
1590
        protected syncService: IgxForOfSyncService) {
47,565✔
1591
        super(_viewContainer, _template, _differs, cdr, _zone, syncScrollService, _platformUtil, _document);
47,565✔
1592
    }
1593

1594
    /**
1595
     * @hidden @internal
1596
     * Asserts the correct type of the context for the template that `IgxGridForOfDirective` will render.
1597
     *
1598
     * The presence of this method is a signal to the Ivy template type-check compiler that the
1599
     * `IgxGridForOfDirective` structural directive renders its template with a specific context type.
1600
     */
1601
    public static override ngTemplateContextGuard<T, U extends T[]>(dir: IgxGridForOfDirective<T, U>, ctx: any):
1602
        ctx is IgxGridForOfContext<T, U> {
1603
        return true;
×
1604
    }
1605

1606
    public override ngOnInit() {
1607
        this.syncService.setMaster(this);
47,565✔
1608
        super.ngOnInit();
47,565✔
1609
        this.removeScrollEventListeners();
47,565✔
1610
    }
1611

1612
    public override ngOnChanges(changes: SimpleChanges) {
1613
        const forOf = 'igxGridForOf';
157,503✔
1614
        this.syncService.setMaster(this);
157,503✔
1615
        if (forOf in changes) {
157,503✔
1616
            const value = changes[forOf].currentValue;
147,875✔
1617
            if (!this._differ && value) {
147,875✔
1618
                try {
47,563✔
1619
                    this._differ = this._differs.find(value).create(this.igxForTrackBy);
47,563✔
1620
                } catch (e) {
1621
                    throw new Error(
×
1622
                        `Cannot find a differ supporting object "${value}" of type "${getTypeNameForDebugging(value)}".
1623
                     NgFor only supports binding to Iterables such as Arrays.`);
1624
                }
1625
            }
1626
            if (this.igxForScrollOrientation === 'horizontal') {
147,875✔
1627
                // in case collection has changes, reset sync service
1628
                this.syncService.setMaster(this, this.igxGridForOfUniqueSizeCache);
132,751✔
1629
            }
1630
        }
1631
        const defaultItemSize = 'igxForItemSize';
157,503✔
1632
        if (defaultItemSize in changes && !changes[defaultItemSize].firstChange &&
157,503✔
1633
            this.igxForScrollOrientation === 'vertical' && this.igxForOf) {
1634
            // handle default item size changed.
1635
            this.initSizesCache(this.igxForOf);
99✔
1636
        }
1637
        const containerSize = 'igxForContainerSize';
157,503✔
1638
        if (containerSize in changes && !changes[containerSize].firstChange && this.igxForOf) {
157,503✔
1639
            const prevSize = parseInt(changes[containerSize].previousValue, 10);
32,284✔
1640
            const newSize = parseInt(changes[containerSize].currentValue, 10);
32,284✔
1641
            this._recalcOnContainerChange({prevSize, newSize});
32,284✔
1642
        }
1643
    }
1644

1645
    /**
1646
     * @hidden
1647
     * @internal
1648
     */
1649
    public assumeMaster(): void {
1650
        this._sizesCache = this.syncService.sizesCache(this.igxForScrollOrientation);
16,899✔
1651
        this.syncService.setMaster(this, true);
16,899✔
1652
    }
1653

1654
    public override ngDoCheck() {
1655
        if (this._differ) {
446,803✔
1656
            const changes = this._differ.diff(this.igxForOf);
446,699✔
1657
            if (changes) {
446,699✔
1658
                const args: IForOfDataChangingEventArgs = {
56,527✔
1659
                    containerSize: this.igxForContainerSize,
1660
                    state: this.state
1661
                };
1662
                this.dataChanging.emit(args);
56,527✔
1663
                //  re-init cache.
1664
                if (!this.igxForOf) {
56,527!
1665
                    this.igxForOf = [] as U;
×
1666
                }
1667
                /* we need to reset the master dir if all rows are removed
1668
                (e.g. because of filtering); if all columns are hidden, rows are
1669
                still rendered empty, so we should not reset master */
1670
                if (!this.igxForOf.length &&
56,527✔
1671
                    this.igxForScrollOrientation === 'vertical') {
1672
                    this.syncService.resetMaster();
110✔
1673
                }
1674
                this.syncService.setMaster(this);
56,527✔
1675
                this.igxForContainerSize = args.containerSize;
56,527✔
1676
                const sizeDiff = this._updateSizeCache(changes);
56,527✔
1677
                this._applyChanges();
56,527✔
1678
                if (sizeDiff) {
56,527✔
1679
                    this._adjustScrollPositionAfterSizeChange(sizeDiff);
39,754✔
1680
                }
1681
                this._updateScrollOffset();
56,527✔
1682
                this.dataChanged.emit(args);
56,527✔
1683
            }
1684
        }
1685
    }
1686

1687
    public override onScroll(event) {
1688
        if (!parseInt(this.scrollComponent.nativeElement.style.height, 10)) {
319!
1689
            return;
×
1690
        }
1691
        if (!this._bScrollInternal) {
319✔
1692
            this._calcVirtualScrollPosition(event.target.scrollTop);
65✔
1693
        } else {
1694
            this._bScrollInternal = false;
254✔
1695
        }
1696
        const scrollOffset = this.fixedUpdateAllElements(this._virtScrollPosition);
319✔
1697

1698
        this.dc.instance._viewContainer.element.nativeElement.style.top = -(scrollOffset) + 'px';
319✔
1699

1700
        this._zone.onStable.pipe(first()).subscribe(this.recalcUpdateSizes.bind(this));
319✔
1701
        this.cdr.markForCheck();
319✔
1702
    }
1703

1704
    public override onHScroll(scrollAmount) {
1705
        /* in certain situations this may be called when no scrollbar is visible */
1706
        const firstScrollChild = this.scrollComponent.nativeElement.children.item(0) as HTMLElement;
2,594✔
1707
        if (!this.scrollComponent || !parseInt(firstScrollChild.style.width, 10)) {
2,594!
1708
            return;
×
1709
        }
1710
        // Updating horizontal chunks
1711
        const scrollOffset = this.fixedUpdateAllElements(Math.abs(scrollAmount));
2,594✔
1712
        if (scrollAmount < 0) {
2,594!
1713
            // RTL
1714
            this.dc.instance._viewContainer.element.nativeElement.style.left = scrollOffset + 'px';
×
1715
        } else {
1716
            // LTR
1717
            this.dc.instance._viewContainer.element.nativeElement.style.left = -scrollOffset + 'px';
2,594✔
1718
        }
1719
    }
1720

1721
    protected getItemSize(item) {
1722
        let size = 0;
1,039,277✔
1723
        const dimension = this.igxForSizePropName || 'height';
1,039,277!
1724
        if (this.igxForScrollOrientation === 'vertical') {
1,039,277✔
1725
            size = this._getItemSize(item, dimension);
718,252✔
1726
            if (item && item.summaries) {
718,252✔
1727
                size = item.max;
1,574✔
1728
            } else if (item && item.groups && item.height) {
716,678✔
1729
                size = item.height;
3,019✔
1730
            }
1731
        } else {
1732
            size = parseInt(item[dimension], 10) || 0;
321,025✔
1733
        }
1734
        return size;
1,039,277✔
1735
    }
1736

1737
    protected override initSizesCache(items: U): number {
1738
        if (!this.syncService.isMaster(this) && this.igxForScrollOrientation === 'horizontal') {
21,694✔
1739
            const masterSizesCache = this.syncService.sizesCache(this.igxForScrollOrientation);
25✔
1740
            return masterSizesCache[masterSizesCache.length - 1];
25✔
1741
        }
1742
        let totalSize = 0;
21,669✔
1743
        let size = 0;
21,669✔
1744
        let i = 0;
21,669✔
1745
        this.sizesCache = [];
21,669✔
1746
        this.individualSizeCache = [];
21,669✔
1747
        this.sizesCache.push(0);
21,669✔
1748
        const count = this.isRemote ? this.totalItemCount : items.length;
21,669✔
1749
        for (i; i < count; i++) {
21,669✔
1750
            size = this.getItemSize(items[i]);
154,377✔
1751
            this.individualSizeCache.push(size);
154,377✔
1752
            totalSize += size;
154,377✔
1753
            this.sizesCache.push(totalSize);
154,377✔
1754
        }
1755
        return totalSize;
21,669✔
1756
    }
1757

1758
    protected override _updateSizeCache(changes: IterableChanges<T> = null) {
×
1759
        const oldSize = this.individualSizeCache.length > 0 ? this.individualSizeCache.reduce((acc, val) => acc + val) : 0;
242,728✔
1760
        let newSize = oldSize;
56,527✔
1761
        if (changes && !this.isRemote) {
56,527✔
1762
            newSize = this.handleCacheChanges(changes);
56,521✔
1763
        } else {
1764
            return;
6✔
1765
        }
1766

1767
        const diff = oldSize - newSize;
56,521✔
1768
        return diff;
56,521✔
1769
    }
1770

1771
    protected handleCacheChanges(changes: IterableChanges<T>) {
1772
        const identityChanges = [];
56,521✔
1773
        const newHeightCache = [];
56,521✔
1774
        const newSizesCache = [];
56,521✔
1775
        newSizesCache.push(0);
56,521✔
1776
        let newHeight = 0;
56,521✔
1777

1778
        // When there are more than one removed items the changes are not reliable so those with identity change should be default size.
1779
        let numRemovedItems = 0;
56,521✔
1780
        changes.forEachRemovedItem(() => numRemovedItems++);
66,490✔
1781

1782
        // Get the identity changes to determine later if those that have changed their indexes should be assigned default item size.
1783
        changes.forEachIdentityChange((item) => {
56,521✔
1784
            if (item.currentIndex !== item.previousIndex) {
2,259✔
1785
                // Filter out ones that have not changed their index.
1786
                identityChanges[item.currentIndex] = item;
917✔
1787
            }
1788
        });
1789

1790
        // Processing each item that is passed to the igxForOf so far seem to be most reliable. We parse the updated list of items.
1791
        changes.forEachItem((item) => {
56,521✔
1792
            if (item.previousIndex !== null &&
908,244✔
1793
                (numRemovedItems < 2 || !identityChanges.length || identityChanges[item.currentIndex])
1794
                && this.igxForScrollOrientation !== "horizontal") {
1795
                // Reuse cache on those who have previousIndex.
1796
                // When there are more than one removed items currently the changes are not readable so ones with identity change
1797
                // should be racalculated.
1798
                newHeightCache[item.currentIndex] = this.individualSizeCache[item.previousIndex];
23,344✔
1799
            } else {
1800
                // Assign default item size.
1801
                newHeightCache[item.currentIndex] = this.getItemSize(item.item);
884,900✔
1802
            }
1803
            newSizesCache[item.currentIndex + 1] = newSizesCache[item.currentIndex] + newHeightCache[item.currentIndex];
908,244✔
1804
            newHeight += newHeightCache[item.currentIndex];
908,244✔
1805
        });
1806
        this.individualSizeCache = newHeightCache;
56,521✔
1807
        this.sizesCache = newSizesCache;
56,521✔
1808
        return newHeight;
56,521✔
1809
    }
1810

1811
    protected override addLastElem() {
1812
        let elemIndex = this.state.startIndex + this.state.chunkSize;
43,668✔
1813
        if (!this.isRemote && !this.igxForOf) {
43,668!
1814
            return;
×
1815
        }
1816

1817
        if (elemIndex >= this.igxForOf.length) {
43,668✔
1818
            elemIndex = this.igxForOf.length - this.state.chunkSize;
15✔
1819
        }
1820
        const input = this.igxForOf[elemIndex];
43,668✔
1821
        const embeddedView = this.dc.instance._vcr.createEmbeddedView(
43,668✔
1822
            this._template,
1823
            new IgxGridForOfContext<T, U>(input, this.igxForOf, this.getContextIndex(input), this.igxForOf.length)
1824
        );
1825

1826
        this._embeddedViews.push(embeddedView);
43,668✔
1827
        this.state.chunkSize++;
43,668✔
1828
    }
1829

1830
    protected _updateViews(prevChunkSize) {
1831
        if (this.igxForOf && this.igxForOf.length && this.dc) {
57,246✔
1832
            const embeddedViewCopy = Object.assign([], this._embeddedViews);
57,090✔
1833
            let startIndex;
1834
            let endIndex;
1835
            if (this.isRemote) {
57,090✔
1836
                startIndex = 0;
5✔
1837
                endIndex = this.igxForOf.length;
5✔
1838
            } else {
1839
                startIndex = this.getIndexAt(this.scrollPosition, this.sizesCache);
57,085✔
1840
                if (startIndex + this.state.chunkSize > this.igxForOf.length) {
57,085✔
1841
                    startIndex = this.igxForOf.length - this.state.chunkSize;
145✔
1842
                }
1843
                this.state.startIndex = startIndex;
57,085✔
1844
                endIndex = this.state.chunkSize + this.state.startIndex;
57,085✔
1845
            }
1846

1847
            for (let i = startIndex; i < endIndex && this.igxForOf[i] !== undefined; i++) {
57,090✔
1848
                const embView = embeddedViewCopy.shift();
243,091✔
1849
                this.updateTemplateContext(embView.context, i);
243,091✔
1850
            }
1851
            if (prevChunkSize !== this.state.chunkSize) {
57,090✔
1852
                this.chunkLoad.emit(this.state);
3,878✔
1853
            }
1854
        }
1855
    }
1856
    protected override _applyChanges() {
1857
        const prevChunkSize = this.state.chunkSize;
57,246✔
1858
        this.applyChunkSizeChange();
57,246✔
1859
        this._recalcScrollBarSize();
57,246✔
1860
        this._updateViews(prevChunkSize);
57,246✔
1861
    }
1862

1863
    /**
1864
     * @hidden
1865
     */
1866
    protected override _calcMaxChunkSize(): number {
1867
        if (this.igxForScrollOrientation === 'horizontal') {
128,924✔
1868
            if (this.syncService.isMaster(this)) {
111,191✔
1869
                return super._calcMaxChunkSize();
33,830✔
1870
            }
1871
            return this.syncService.chunkSize(this.igxForScrollOrientation);
77,361✔
1872
        } else {
1873
            return super._calcMaxChunkSize();
17,733✔
1874
        }
1875

1876
    }
1877
}
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