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

IgniteUI / igniteui-angular / 13012537466

28 Jan 2025 02:23PM CUT coverage: 91.593% (-0.01%) from 91.607%
13012537466

Pull #15309

github

web-flow
Merge 2828fd786 into 24fff3ffc
Pull Request #15309: fix(mem-leak): Fix potential memory leaks.

12984 of 15222 branches covered (85.3%)

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

4 existing lines in 2 files now uncovered.

26323 of 28739 relevant lines covered (91.59%)

33980.02 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
/* 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,994✔
47
        public igxForOf: U,
210,994✔
48
        public index: number,
210,994✔
49
        public count: number
210,994✔
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,565✔
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,565✔
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,565✔
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,565✔
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,565✔
222

223
    @Output()
224
    public beforeViewDestroyed = new EventEmitter<EmbeddedViewRef<any>>();
48,565✔
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,565✔
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,565✔
256
        startIndex: 0,
257
        chunkSize: 0
258
    };
259

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

281
    private _totalItemCount: number = null;
48,565✔
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,565✔
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;
909,647✔
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,914✔
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;
907,234✔
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;
231,055✔
358
    }
359
    public set scrollPosition(val: number) {
360
        if (val === this.scrollComponent.scrollAmount) {
44,498✔
361
            return;
43,745✔
362
        }
363
        if (this.igxForScrollOrientation === 'horizontal' && this.scrollComponent) {
753✔
364
            this.scrollComponent.nativeElement.scrollLeft = this.isRTL ? -val : val;
235!
365
        } else if (this.scrollComponent) {
518✔
366
            this.scrollComponent.nativeElement.scrollTop = val;
518✔
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,787,363✔
380
    }
381
    protected set sizesCache(value: number[]) {
382
        this._sizesCache = value;
2,664✔
383
    }
384

385
    private get _isScrolledToBottom() {
386
        if (!this.getScroll()) {
396!
387
            return true;
×
388
        }
389
        const scrollHeight = this.getScroll().scrollHeight;
396✔
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;
396✔
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,565✔
401
        protected _template: TemplateRef<NgForOfContext<T>>,
48,565✔
402
        protected _differs: IterableDiffers,
48,565✔
403
        public cdr: ChangeDetectorRef,
48,565✔
404
        protected _zone: NgZone,
48,565✔
405
        protected syncScrollService: IgxForOfScrollSyncService,
48,565✔
406
        protected platformUtil: PlatformUtil,
48,565✔
407
        @Inject(DOCUMENT)
408
        protected document: any,
48,565✔
409
    ) {
410
        super();
48,565✔
411
    }
412

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

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

421
    /**
422
     * @hidden
423
     */
424
    public ngOnInit(): void {
425
        const vc = this.igxForScrollContainer ? this.igxForScrollContainer._viewContainer : this._viewContainer;
48,565✔
426
        this.igxForSizePropName = this.igxForSizePropName || 'width';
48,565✔
427
        this.dc = this._viewContainer.createComponent(DisplayContainerComponent, { index: 0 });
48,565✔
428
        this.dc.instance.scrollDirection = this.igxForScrollOrientation;
48,565✔
429
        if (this.igxForOf && this.igxForOf.length) {
48,565✔
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,541✔
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,693✔
440
                const embeddedView = this.dc.instance._vcr.createEmbeddedView(
165,693✔
441
                    this._template,
442
                    new IgxForOfContext<T, U>(input, this.igxForOf, this.getContextIndex(input), this.igxForOf.length)
443
                );
444
                this._embeddedViews.push(embeddedView);
165,693✔
445
            }
446
        }
447
        this._maxSize = this._calcMaxBrowserSize();
48,565✔
448
        if (this.igxForScrollOrientation === 'vertical') {
48,565✔
449
            this.dc.instance._viewContainer.element.nativeElement.style.top = '0px';
11,598✔
450
            this.scrollComponent = this.syncScrollService.getScrollMaster(this.igxForScrollOrientation);
11,598✔
451
            if (!this.scrollComponent || this.scrollComponent.destroyed) {
11,598✔
452
                this.scrollComponent = vc.createComponent(VirtualHelperComponent).instance;
4,272✔
453
            }
454

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

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

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

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

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

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

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

583

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

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

612
        this._bScrollInternal = true;
56✔
613
        this._virtScrollPosition += add;
56✔
614
        this._virtScrollPosition = this._virtScrollPosition > 0 ?
56✔
615
            (this._virtScrollPosition < maxVirtScrollTop ? this._virtScrollPosition : maxVirtScrollTop) :
54✔
616
            0;
617

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

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

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

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

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

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

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

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

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

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

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

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

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

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

844
            // update scrBar heights/widths
845
            const reducer = (acc, val) => acc + val;
14,417✔
846

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

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

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

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

918
        this.dc.instance._viewContainer.element.nativeElement.style.top = -(scrollOffset) + 'px';
67✔
919

920
        this._zone.onStable.pipe(first()).subscribe(this.recalcUpdateSizes.bind(this));
67✔
921

922
        this.dc.changeDetectorRef.detectChanges();
67✔
923
        if (prevStartIndex !== this.state.startIndex) {
67✔
924
            this.chunkLoad.emit(this.state);
54✔
925
        }
926
    }
927

928

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

941
    protected updateSizes() {
942
        if (!this.scrollComponent.nativeElement.isConnected) return;
815✔
943
        const scrollable = this.isScrollable();
762✔
944
        this.recalcUpdateSizes();
762✔
945
        this._applyChanges();
762✔
946
        this._updateScrollOffset();
762✔
947
        if (scrollable !== this.isScrollable()) {
762✔
948
            this.scrollbarVisibilityChanged.emit();
9✔
949
        } else {
950
            this.contentSizeChange.emit();
753✔
951
        }
952
    }
953

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

961
        if (newStart + this.state.chunkSize > count) {
3,096✔
962
            newStart = count - this.state.chunkSize;
840✔
963
        }
964

965
        const prevStart = this.state.startIndex;
3,096✔
966
        const diff = newStart - this.state.startIndex;
3,096✔
967
        this.state.startIndex = newStart;
3,096✔
968

969
        if (diff) {
3,096✔
970
            this.chunkPreload.emit(this.state);
973✔
971
            if (!this.isRemote) {
973✔
972

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

986
        return inScrollTop - this.sizesCache[this.state.startIndex];
3,096✔
987
    }
988

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

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

1005
                this.updateTemplateContext(embView.context, i);
819✔
1006
                container.insert(view);
819✔
1007
                this._embeddedViews.push(embView);
819✔
1008
            }
1009
        }
1010
    }
1011

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

1025
                this.updateTemplateContext(embView.context, i);
277✔
1026
                container.insert(view, 0);
277✔
1027
                this._embeddedViews.unshift(embView);
277✔
1028
            }
1029
        }
1030
    }
1031

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1246
    protected _updateSizeCache() {
1247
        if (this.igxForScrollOrientation === 'horizontal') {
1,040✔
1248
            this.initSizesCache(this.igxForOf);
332✔
1249
            return;
332✔
1250
        }
1251
        const oldHeight = this.individualSizeCache.length > 0 ? this.individualSizeCache.reduce((acc, val) => acc + val) : 0;
2,048,022✔
1252
        const newHeight = this.initSizesCache(this.igxForOf);
708✔
1253

1254
        const diff = oldHeight - newHeight;
708✔
1255
        this._adjustScrollPositionAfterSizeChange(diff);
708✔
1256
    }
1257

1258
    /**
1259
     * @hidden
1260
     */
1261
    protected _calcMaxChunkSize(): number {
1262
        let i = 0;
53,695✔
1263
        let length = 0;
53,695✔
1264
        let maxLength = 0;
53,695✔
1265
        const arr = [];
53,695✔
1266
        let sum = 0;
53,695✔
1267
        const availableSize = parseInt(this.igxForContainerSize, 10);
53,695✔
1268
        if (!availableSize) {
53,695✔
1269
            return 0;
10,561✔
1270
        }
1271
        const dimension = this.igxForScrollOrientation === 'horizontal' ?
43,134✔
1272
            this.igxForSizePropName : 'height';
1273
        const reducer = (accumulator, currentItem) => accumulator + this._getItemSize(currentItem, dimension);
37,314,588✔
1274
        for (i; i < this.igxForOf.length; i++) {
43,134✔
1275
            let item: T | { value: T, height: number } = this.igxForOf[i];
5,372,496✔
1276
            if (dimension === 'height') {
5,372,496✔
1277
                item = { value: this.igxForOf[i], height: this.individualSizeCache[i] };
5,038,293✔
1278
            }
1279
            const size = dimension === 'height' ?
5,372,496✔
1280
                this.individualSizeCache[i] :
1281
                this._getItemSize(item, dimension);
1282
            sum = arr.reduce(reducer, size);
5,372,496✔
1283
            if (sum < availableSize) {
5,372,496✔
1284
                arr.push(item);
172,023✔
1285
                length = arr.length;
172,023✔
1286
                if (i === this.igxForOf.length - 1) {
172,023✔
1287
                    // reached end without exceeding
1288
                    // include prev items until size is filled or first item is reached.
1289
                    let curItem = dimension === 'height' ? arr[0].value : arr[0];
9,946✔
1290
                    let prevIndex = this.igxForOf.indexOf(curItem) - 1;
9,946✔
1291
                    while (prevIndex >= 0 && sum <= availableSize) {
9,946✔
1292
                        curItem = dimension === 'height' ? arr[0].value : arr[0];
214✔
1293
                        prevIndex = this.igxForOf.indexOf(curItem) - 1;
214✔
1294
                        const prevItem = this.igxForOf[prevIndex];
214✔
1295
                        const prevSize = dimension === 'height' ?
214✔
1296
                            this.individualSizeCache[prevIndex] :
1297
                            parseInt(prevItem[dimension], 10);
1298
                        sum = arr.reduce(reducer, prevSize);
214✔
1299
                        arr.unshift(prevItem);
214✔
1300
                        length = arr.length;
214✔
1301
                    }
1302
                }
1303
            } else {
1304
                arr.push(item);
5,200,473✔
1305
                length = arr.length + 1;
5,200,473✔
1306
                arr.shift();
5,200,473✔
1307
            }
1308
            if (length > maxLength) {
5,372,496✔
1309
                maxLength = length;
204,476✔
1310
            }
1311
        }
1312
        return maxLength;
43,134✔
1313
    }
1314

1315
    /**
1316
     * @hidden
1317
     */
1318
    protected getIndexAt(left, set) {
1319
        let start = 0;
93,906✔
1320
        let end = set.length - 1;
93,906✔
1321
        if (left === 0) {
93,906✔
1322
            return 0;
91,268✔
1323
        }
1324
        while (start <= end) {
2,638✔
1325
            const midIdx = Math.floor((start + end) / 2);
10,048✔
1326
            const midLeft = set[midIdx];
10,048✔
1327
            const cmp = left - midLeft;
10,048✔
1328
            if (cmp > 0) {
10,048✔
1329
                start = midIdx + 1;
4,548✔
1330
            } else if (cmp < 0) {
5,500✔
1331
                end = midIdx - 1;
5,198✔
1332
            } else {
1333
                return midIdx;
302✔
1334
            }
1335
        }
1336
        return end;
2,336✔
1337
    }
1338

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

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

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

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

1400
        this.state.chunkSize--;
10,588✔
1401
    }
1402

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

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

1422
        this._embeddedViews.push(embeddedView);
1,647✔
1423
        this.state.chunkSize++;
1,647✔
1424

1425
        this._zone.run(() => this.cdr.markForCheck());
1,647✔
1426
    }
1427

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

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

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

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

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

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

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

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

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

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

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

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

1538
    @Input({ transform: booleanAttribute })
1539
    public igxGridForOfUniqueSizeCache = false;
47,567✔
1540

1541
    @Input({ transform: booleanAttribute })
1542
    public igxGridForOfVariableSizes = true;
47,567✔
1543

1544
    /**
1545
     * @hidden
1546
     * @internal
1547
     */
1548
    public override get sizesCache(): number[] {
1549
        if (this.igxForScrollOrientation === 'horizontal') {
952,069✔
1550
            if (this.igxGridForOfUniqueSizeCache || this.syncService.isMaster(this)) {
763,136✔
1551
                return this._sizesCache;
482,285✔
1552
            }
1553
            return this.syncService.sizesCache(this.igxForScrollOrientation);
280,851✔
1554
        } else {
1555
            return this._sizesCache;
188,933✔
1556
        }
1557
    }
1558
    /**
1559
     * @hidden
1560
     * @internal
1561
     */
1562
    public override set sizesCache(value: number[]) {
1563
        this._sizesCache = value;
78,382✔
1564
    }
1565

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

1570
    public override recalcUpdateSizes() {
1571
        if (this.igxGridForOfVariableSizes && this.igxForScrollOrientation === 'vertical') {
1,144✔
1572
            super.recalcUpdateSizes();
1,068✔
1573
        }
1574
    }
1575

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

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

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

1608
    public override ngOnInit() {
1609
        this.syncService.setMaster(this);
47,567✔
1610
        super.ngOnInit();
47,567✔
1611
        this.removeScrollEventListeners();
47,567✔
1612
    }
1613

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

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

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

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

1700
        this.dc.instance._viewContainer.element.nativeElement.style.top = -(scrollOffset) + 'px';
322✔
1701

1702
        this._zone.onStable.pipe(first()).subscribe(this.recalcUpdateSizes.bind(this));
322✔
1703
        this.cdr.markForCheck();
322✔
1704
    }
1705

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

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

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

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

1769
        const diff = oldSize - newSize;
56,687✔
1770
        return diff;
56,687✔
1771
    }
1772

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

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

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

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

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

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

1828
        this._embeddedViews.push(embeddedView);
43,654✔
1829
        this.state.chunkSize++;
43,654✔
1830
    }
1831

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

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

1865
    /**
1866
     * @hidden
1867
     */
1868
    protected override _calcMaxChunkSize(): number {
1869
        if (this.igxForScrollOrientation === 'horizontal') {
129,055✔
1870
            if (this.syncService.isMaster(this)) {
111,318✔
1871
                return super._calcMaxChunkSize();
33,932✔
1872
            }
1873
            return this.syncService.chunkSize(this.igxForScrollOrientation);
77,386✔
1874
        } else {
1875
            return super._calcMaxChunkSize();
17,737✔
1876
        }
1877

1878
    }
1879
}
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