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

IgniteUI / igniteui-angular / 11793675311

12 Nov 2024 08:44AM CUT coverage: 91.769% (-0.02%) from 91.787%
11793675311

push

github

web-flow
Merge pull request #15038 from IgniteUI/igniteui-angular-15004-17-2-x

fix(dropdown): fix typography for boostrap and fluent themes

12538 of 14639 branches covered (85.65%)

25643 of 27943 relevant lines covered (91.77%)

32901.22 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,
206,210✔
47
        public igxForOf: U,
206,210✔
48
        public index: number,
206,210✔
49
        public count: number
206,210✔
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';
47,398✔
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>();
47,398✔
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>();
47,398✔
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>();
47,398✔
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>();
47,398✔
222

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

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

281
    private _totalItemCount: number = null;
47,398✔
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;
47,398✔
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;
895,873✔
316
    }
317

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

333
    public get displayContainer(): HTMLElement | undefined {
334
        return this.dc?.instance?._viewContainer?.element?.nativeElement;
6,811✔
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;
893,443✔
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;
228,830✔
358
    }
359
    public set scrollPosition(val: number) {
360
        if (val === this.scrollComponent.scrollAmount) {
39,280✔
361
            return;
38,521✔
362
        }
363
        if (this.igxForScrollOrientation === 'horizontal' && this.scrollComponent) {
759✔
364
            this.scrollComponent.nativeElement.scrollLeft = this.isRTL ? -val : val;
241!
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');
241✔
375
        return dir === 'rtl';
241✔
376
    }
377

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

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

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

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

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

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

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

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

500
    /**
501
     * @hidden
502
     */
503
    public ngOnDestroy() {
504
        this.removeScrollEventListeners();
46,984✔
505
        this.destroy$.next(true);
46,984✔
506
        this.destroy$.complete();
46,984✔
507
        if (this.contentObserver) {
46,984✔
508
            this.contentObserver.disconnect();
11,270✔
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,027✔
529
        if (forOf in changes) {
2,027✔
530
            const value = changes[forOf].currentValue;
1,650✔
531
            if (!this._differ && value) {
1,650✔
532
                try {
947✔
533
                    this._differ = this._differs.find(value).create(this.igxForTrackBy);
947✔
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,027✔
542
        if (defaultItemSize in changes && !changes[defaultItemSize].firstChange && this.igxForOf) {
2,027✔
543
            // handle default item size changed.
544
            this.initSizesCache(this.igxForOf);
147✔
545
            this._applyChanges();
147✔
546
        }
547
        const containerSize = 'igxForContainerSize';
2,027✔
548
        if (containerSize in changes && !changes[containerSize].firstChange && this.igxForOf) {
2,027✔
549
            const prevSize = parseInt(changes[containerSize].previousValue, 10);
300✔
550
            const newSize = parseInt(changes[containerSize].currentValue, 10);
300✔
551
            this._recalcOnContainerChange({prevSize, newSize});
300✔
552
        }
553
    }
554

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

577

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

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

606
        this._bScrollInternal = true;
56✔
607
        this._virtScrollPosition += add;
56✔
608
        this._virtScrollPosition = this._virtScrollPosition > 0 ?
56✔
609
            (this._virtScrollPosition < maxVirtScrollTop ? this._virtScrollPosition : maxVirtScrollTop) :
54✔
610
            0;
611

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

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

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

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

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

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

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

711
    /**
712
     * @hidden
713
     */
714
    public getColumnScrollLeft(colIndex) {
715
        return this.sizesCache[colIndex];
4,615✔
716
    }
717

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

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

754
    /**
755
     * @hidden
756
     * Function that is called to get the native scrollbar size that the browsers renders.
757
     */
758
    public getScrollNativeSize() {
759
        return this.scrollComponent ? this.scrollComponent.scrollNativeSize : 0;
178,275!
760
    }
761

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

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

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

838
            // update scrBar heights/widths
839
            const reducer = (acc, val) => acc + val;
14,344✔
840

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

872
    /**
873
     * @hidden
874
     * Reset scroll position.
875
     * Needed in case scrollbar is hidden/detached but we still need to reset it.
876
     */
877
    public resetScrollPosition() {
878
        this.scrollPosition = 0;
38,407✔
879
        this.scrollComponent.scrollAmount = 0;
38,407✔
880
    }
881

882
    /**
883
     * @hidden
884
     */
885
    protected removeScrollEventListeners() {
886
        if (this.igxForScrollOrientation === 'horizontal') {
93,435✔
887
            this._zone.runOutsideAngular(() => this.scrollComponent?.nativeElement?.removeEventListener('scroll', this.func));
71,487✔
888
        } else {
889
            this._zone.runOutsideAngular(() =>
21,948✔
890
                this.scrollComponent?.nativeElement?.removeEventListener('scroll', this.verticalScrollHandler)
21,948✔
891
            );
892
        }
893
    }
894

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

912
        this.dc.instance._viewContainer.element.nativeElement.style.top = -(scrollOffset) + 'px';
70✔
913

914
        this._zone.onStable.pipe(first()).subscribe(this.recalcUpdateSizes.bind(this));
70✔
915

916
        this.dc.changeDetectorRef.detectChanges();
70✔
917
        if (prevStartIndex !== this.state.startIndex) {
70✔
918
            this.chunkLoad.emit(this.state);
55✔
919
        }
920
    }
921

922

923
    /**
924
     * @hidden
925
     * @internal
926
     */
927
    public updateScroll(): void {
928
        if (this.igxForScrollOrientation === "horizontal") {
807✔
929
            const scrollAmount = this.scrollComponent.nativeElement["scrollLeft"];
807✔
930
            this.scrollComponent.scrollAmount = scrollAmount;
807✔
931
            this._updateScrollOffset();
807✔
932
        }
933
    }
934

935
    protected updateSizes() {
936
        if (!this.scrollComponent.nativeElement.isConnected) return;
837✔
937
        const scrollable = this.isScrollable();
764✔
938
        this.recalcUpdateSizes();
764✔
939
        this._applyChanges();
764✔
940
        this._updateScrollOffset();
764✔
941
        if (scrollable !== this.isScrollable()) {
764✔
942
            this.scrollbarVisibilityChanged.emit();
8✔
943
        } else {
944
            this.contentSizeChange.emit();
756✔
945
        }
946
    }
947

948
    /**
949
     * @hidden
950
     */
951
    protected fixedUpdateAllElements(inScrollTop: number): number {
952
        const count = this.isRemote ? this.totalItemCount : this.igxForOf.length;
3,141✔
953
        let newStart = this.getIndexAt(inScrollTop, this.sizesCache);
3,141✔
954

955
        if (newStart + this.state.chunkSize > count) {
3,141✔
956
            newStart = count - this.state.chunkSize;
841✔
957
        }
958

959
        const prevStart = this.state.startIndex;
3,141✔
960
        const diff = newStart - this.state.startIndex;
3,141✔
961
        this.state.startIndex = newStart;
3,141✔
962

963
        if (diff) {
3,141✔
964
            this.chunkPreload.emit(this.state);
984✔
965
            if (!this.isRemote) {
984✔
966

967
                // recalculate and apply page size.
968
                if (diff && Math.abs(diff) <= MAX_PERF_SCROLL_DIFF) {
975✔
969
                    if (diff > 0) {
728✔
970
                        this.moveApplyScrollNext(prevStart);
512✔
971
                    } else {
972
                        this.moveApplyScrollPrev(prevStart);
216✔
973
                    }
974
                } else {
975
                    this.fixedApplyScroll();
247✔
976
                }
977
            }
978
        }
979

980
        return inScrollTop - this.sizesCache[this.state.startIndex];
3,141✔
981
    }
982

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

992
        for (let i = start; i < end && this.igxForOf[i] !== undefined; i++) {
512✔
993
            const embView = this._embeddedViews.shift();
825✔
994
            if (!embView.destroyed) {
825✔
995
                this.scrollFocus(embView.rootNodes.find(node => node.nodeType === Node.ELEMENT_NODE)
1,409!
996
                    || embView.rootNodes[0].nextElementSibling);
997
                const view = container.detach(0);
825✔
998

999
                this.updateTemplateContext(embView.context, i);
825✔
1000
                container.insert(view);
825✔
1001
                this._embeddedViews.push(embView);
825✔
1002
            }
1003
        }
1004
    }
1005

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

1019
                this.updateTemplateContext(embView.context, i);
284✔
1020
                container.insert(view, 0);
284✔
1021
                this._embeddedViews.unshift(embView);
284✔
1022
            }
1023
        }
1024
    }
1025

1026
    /**
1027
     * @hidden
1028
     */
1029
    protected getContextIndex(input) {
1030
        return this.isRemote ? this.state.startIndex + this.igxForOf.indexOf(input) : this.igxForOf.indexOf(input);
457,471✔
1031
    }
1032

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

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

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

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

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

1109
        this.dc.changeDetectorRef.detectChanges();
107✔
1110
        if (prevStartIndex !== this.state.startIndex) {
107✔
1111
            this.chunkLoad.emit(this.state);
70✔
1112
        }
1113
    }
1114

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

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

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

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

1188
    /**
1189
     * @hidden
1190
     * Recalculates the chunkSize based on current startIndex and returns the new size.
1191
     * This should be called after this.state.startIndex is updated, not before.
1192
     */
1193
    protected _calculateChunkSize(): number {
1194
        let chunkSize = 0;
129,215✔
1195
        if (this.igxForContainerSize !== null && this.igxForContainerSize !== undefined) {
129,215✔
1196
            if (!this.sizesCache || this.sizesCache.length === 0) {
128,388✔
1197
                this.initSizesCache(this.igxForOf);
13,910✔
1198
            }
1199
            chunkSize = this._calcMaxChunkSize();
128,388✔
1200
            if (this.igxForOf && chunkSize > this.igxForOf.length) {
128,388✔
1201
                chunkSize = this.igxForOf.length;
13,722✔
1202
            }
1203
        } else {
1204
            if (this.igxForOf) {
827✔
1205
                chunkSize = this.igxForOf.length;
827✔
1206
            }
1207
        }
1208
        return chunkSize;
129,215✔
1209
    }
1210

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

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

1240
    protected _updateSizeCache() {
1241
        if (this.igxForScrollOrientation === 'horizontal') {
1,021✔
1242
            this.initSizesCache(this.igxForOf);
332✔
1243
            return;
332✔
1244
        }
1245
        const oldHeight = this.individualSizeCache.length > 0 ? this.individualSizeCache.reduce((acc, val) => acc + val) : 0;
2,043,005✔
1246
        const newHeight = this.initSizesCache(this.igxForOf);
689✔
1247

1248
        const diff = oldHeight - newHeight;
689✔
1249
        this._adjustScrollPositionAfterSizeChange(diff);
689✔
1250
    }
1251

1252
    /**
1253
     * @hidden
1254
     */
1255
    protected _calcMaxChunkSize(): number {
1256
        let i = 0;
51,457✔
1257
        let length = 0;
51,457✔
1258
        let maxLength = 0;
51,457✔
1259
        const arr = [];
51,457✔
1260
        let sum = 0;
51,457✔
1261
        const availableSize = parseInt(this.igxForContainerSize, 10);
51,457✔
1262
        if (!availableSize) {
51,457✔
1263
            return 0;
10,452✔
1264
        }
1265
        const dimension = this.igxForScrollOrientation === 'horizontal' ?
41,005✔
1266
            this.igxForSizePropName : 'height';
1267
        const reducer = (accumulator, currentItem) => accumulator + this._getItemSize(currentItem, dimension);
37,295,083✔
1268
        for (i; i < this.igxForOf.length; i++) {
41,005✔
1269
            let item: T | { value: T, height: number } = this.igxForOf[i];
5,359,632✔
1270
            if (dimension === 'height') {
5,359,632✔
1271
                item = { value: this.igxForOf[i], height: this.individualSizeCache[i] };
5,033,898✔
1272
            }
1273
            const size = dimension === 'height' ?
5,359,632✔
1274
                this.individualSizeCache[i] :
1275
                this._getItemSize(item, dimension);
1276
            sum = arr.reduce(reducer, size);
5,359,632✔
1277
            if (sum < availableSize) {
5,359,632✔
1278
                arr.push(item);
167,577✔
1279
                length = arr.length;
167,577✔
1280
                if (i === this.igxForOf.length - 1) {
167,577✔
1281
                    // reached end without exceeding
1282
                    // include prev items until size is filled or first item is reached.
1283
                    let curItem = dimension === 'height' ? arr[0].value : arr[0];
12,839✔
1284
                    let prevIndex = this.igxForOf.indexOf(curItem) - 1;
12,839✔
1285
                    while (prevIndex >= 0 && sum <= availableSize) {
12,839✔
1286
                        curItem = dimension === 'height' ? arr[0].value : arr[0];
222✔
1287
                        prevIndex = this.igxForOf.indexOf(curItem) - 1;
222✔
1288
                        const prevItem = this.igxForOf[prevIndex];
222✔
1289
                        const prevSize = dimension === 'height' ?
222✔
1290
                            this.individualSizeCache[prevIndex] :
1291
                            parseInt(prevItem[dimension], 10);
1292
                        sum = arr.reduce(reducer, prevSize);
222✔
1293
                        arr.unshift(prevItem);
222✔
1294
                        length = arr.length;
222✔
1295
                    }
1296
                }
1297
            } else {
1298
                arr.push(item);
5,192,055✔
1299
                length = arr.length + 1;
5,192,055✔
1300
                arr.shift();
5,192,055✔
1301
            }
1302
            if (length > maxLength) {
5,359,632✔
1303
                maxLength = length;
195,072✔
1304
            }
1305
        }
1306
        return maxLength;
41,005✔
1307
    }
1308

1309
    /**
1310
     * @hidden
1311
     */
1312
    protected getIndexAt(left, set) {
1313
        let start = 0;
92,438✔
1314
        let end = set.length - 1;
92,438✔
1315
        if (left === 0) {
92,438✔
1316
            return 0;
89,768✔
1317
        }
1318
        while (start <= end) {
2,670✔
1319
            const midIdx = Math.floor((start + end) / 2);
10,139✔
1320
            const midLeft = set[midIdx];
10,139✔
1321
            const cmp = left - midLeft;
10,139✔
1322
            if (cmp > 0) {
10,139✔
1323
                start = midIdx + 1;
4,574✔
1324
            } else if (cmp < 0) {
5,565✔
1325
                end = midIdx - 1;
5,266✔
1326
            } else {
1327
                return midIdx;
299✔
1328
            }
1329
        }
1330
        return end;
2,371✔
1331
    }
1332

1333
    protected _recalcScrollBarSize(containerSizeInfo = null) {
57,985✔
1334
        const count = this.isRemote ? this.totalItemCount : (this.igxForOf ? this.igxForOf.length : 0);
89,458!
1335
        this.dc.instance.notVirtual = !(this.igxForContainerSize && this.dc && this.state.chunkSize < count);
89,458✔
1336
        const scrollable = containerSizeInfo ? this.scrollComponent.size > containerSizeInfo.prevSize : this.isScrollable();
89,458✔
1337
        if (this.igxForScrollOrientation === 'horizontal') {
89,458✔
1338
            const totalWidth = parseInt(this.igxForContainerSize, 10) > 0 ? this._calcSize() : 0;
74,325✔
1339
            if (totalWidth <= parseInt(this.igxForContainerSize, 10)) {
74,325✔
1340
                this.resetScrollPosition();
33,434✔
1341
            }
1342
            this.scrollComponent.nativeElement.style.width = this.igxForContainerSize + 'px';
74,325✔
1343
            this.scrollComponent.size = totalWidth;
74,325✔
1344
        }
1345
        if (this.igxForScrollOrientation === 'vertical') {
89,458✔
1346
            const totalHeight = this._calcSize();
15,133✔
1347
            if (totalHeight <= parseInt(this.igxForContainerSize, 10)) {
15,133✔
1348
                this.resetScrollPosition();
4,948✔
1349
            }
1350
            this.scrollComponent.nativeElement.style.height = parseInt(this.igxForContainerSize, 10) + 'px';
15,133✔
1351
            this.scrollComponent.size = totalHeight;
15,133✔
1352
        }
1353
        if (scrollable !== this.isScrollable()) {
89,458✔
1354
            // scrollbar visibility has changed
1355
            this.scrollbarVisibilityChanged.emit();
15,822✔
1356
        }
1357
    }
1358

1359
    protected _calcSize(): number {
1360
        let size;
1361
        if (this.individualSizeCache && this.individualSizeCache.length > 0) {
102,400✔
1362
            size = this.individualSizeCache.reduce((acc, val) => acc + val, 0);
5,824,036✔
1363
        } else {
1364
            size = this.initSizesCache(this.igxForOf);
8,251✔
1365
        }
1366
        this._virtSize = size;
102,400✔
1367
        if (size > this._maxSize) {
102,400!
1368
            this._virtRatio = size / this._maxSize;
×
1369
            size = this._maxSize;
×
1370
        }
1371
        return size;
102,400✔
1372
    }
1373

1374
    protected _recalcOnContainerChange(containerSizeInfo = null) {
×
1375
        const prevChunkSize = this.state.chunkSize;
31,473✔
1376
        this.applyChunkSizeChange();
31,473✔
1377
        this._recalcScrollBarSize(containerSizeInfo);
31,473✔
1378
        if (prevChunkSize !== this.state.chunkSize) {
31,473✔
1379
            this.chunkLoad.emit(this.state);
8,534✔
1380
        }
1381
    }
1382

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

1394
        this.state.chunkSize--;
10,972✔
1395
    }
1396

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

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

1416
        this._embeddedViews.push(embeddedView);
1,577✔
1417
        this.state.chunkSize++;
1,577✔
1418

1419
        this._zone.run(() => this.cdr.markForCheck());
1,577✔
1420
    }
1421

1422
    /**
1423
     * Recalculates chunkSize and adds/removes elements if need due to the change.
1424
     * this.state.chunkSize is updated in @addLastElem() or @removeLastElem()
1425
     */
1426
    protected applyChunkSizeChange() {
1427
        const chunkSize = this.isRemote ? (this.igxForOf ? this.igxForOf.length : 0) : this._calculateChunkSize();
89,458!
1428
        if (chunkSize > this.state.chunkSize) {
89,458✔
1429
            const diff = chunkSize - this.state.chunkSize;
8,500✔
1430
            for (let i = 0; i < diff; i++) {
8,500✔
1431
                this.addLastElem();
44,447✔
1432
            }
1433
        } else if (chunkSize < this.state.chunkSize) {
80,958✔
1434
            const diff = this.state.chunkSize - chunkSize;
4,288✔
1435
            for (let i = 0; i < diff; i++) {
4,288✔
1436
                this.removeLastElem();
10,972✔
1437
            }
1438
        }
1439
    }
1440

1441
    protected _calcVirtualScrollPosition(scrollPosition: number) {
1442
        const containerSize = parseInt(this.igxForContainerSize, 10);
206✔
1443
        const maxRealScrollPosition = this.scrollComponent.size - containerSize;
206✔
1444
        const realPercentScrolled = maxRealScrollPosition !== 0 ? scrollPosition / maxRealScrollPosition : 0;
206!
1445
        const maxVirtScroll = this._virtSize - containerSize;
206✔
1446
        this._virtScrollPosition = realPercentScrolled * maxVirtScroll;
206✔
1447
    }
1448

1449
    protected _getItemSize(item, dimension: string): number {
1450
        const dim = item ? item[dimension] : null;
42,082,050✔
1451
        return typeof dim === 'number' ? dim : parseInt(this.igxForItemSize, 10) || 0;
42,082,050✔
1452
    }
1453

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

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

1484
    private getMargin(node, dimension: string): number {
1485
        const styles = window.getComputedStyle(node);
12,877✔
1486
        if (dimension === 'height') {
12,877✔
1487
            return parseFloat(styles['marginTop']) +
12,203✔
1488
                parseFloat(styles['marginBottom']) || 0;
1489
        }
1490
        return parseFloat(styles['marginLeft']) +
674✔
1491
            parseFloat(styles['marginRight']) || 0;
1492
    }
1493
}
1494

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

1497
export interface IForOfState extends IBaseEventArgs {
1498
    startIndex?: number;
1499
    chunkSize?: number;
1500
}
1501

1502
export interface IForOfDataChangingEventArgs extends IBaseEventArgs {
1503
    containerSize: number;
1504
}
1505

1506
export class IgxGridForOfContext<T, U extends T[] = T[]> extends IgxForOfContext<T, U> {
1507
    constructor(
1508
        $implicit: T,
1509
        public igxGridForOf: U,
42,870✔
1510
        index: number,
1511
        count: number
1512
    ) {
1513
        super($implicit, igxGridForOf, index, count);
42,870✔
1514
    }
1515
}
1516

1517
@Directive({
1518
    selector: '[igxGridFor][igxGridForOf]',
1519
    standalone: true
1520
})
1521
export class IgxGridForOfDirective<T, U extends T[] = T[]> extends IgxForOfDirective<T, U> implements OnInit, OnChanges, DoCheck {
2✔
1522
    @Input()
1523
    public set igxGridForOf(value: U & T[] | null) {
1524
        this.igxForOf = value;
143,124✔
1525
    }
1526

1527
    public get igxGridForOf() {
1528
        return this.igxForOf;
29✔
1529
    }
1530

1531
    @Input({ transform: booleanAttribute })
1532
    public igxGridForOfUniqueSizeCache = false;
46,451✔
1533

1534
    @Input({ transform: booleanAttribute })
1535
    public igxGridForOfVariableSizes = true;
46,451✔
1536

1537
    /**
1538
     * @hidden
1539
     * @internal
1540
     */
1541
    public override get sizesCache(): number[] {
1542
        if (this.igxForScrollOrientation === 'horizontal') {
937,415✔
1543
            if (this.igxGridForOfUniqueSizeCache || this.syncService.isMaster(this)) {
751,837✔
1544
                return this._sizesCache;
472,278✔
1545
            }
1546
            return this.syncService.sizesCache(this.igxForScrollOrientation);
279,559✔
1547
        } else {
1548
            return this._sizesCache;
185,578✔
1549
        }
1550
    }
1551
    /**
1552
     * @hidden
1553
     * @internal
1554
     */
1555
    public override set sizesCache(value: number[]) {
1556
        this._sizesCache = value;
76,918✔
1557
    }
1558

1559
    protected get itemsDimension() {
1560
        return this.igxForSizePropName || 'height';
×
1561
    }
1562

1563
    public override recalcUpdateSizes() {
1564
        if (this.igxGridForOfVariableSizes && this.igxForScrollOrientation === 'vertical') {
1,147✔
1565
            super.recalcUpdateSizes();
1,062✔
1566
        }
1567
    }
1568

1569
    /**
1570
     * @hidden @internal
1571
     * An event that is emitted after data has been changed but before the view is refreshed
1572
     */
1573
    @Output()
1574
    public dataChanging = new EventEmitter<IForOfDataChangingEventArgs>();
46,451✔
1575

1576
    constructor(
1577
        _viewContainer: ViewContainerRef,
1578
        _template: TemplateRef<NgForOfContext<T>>,
1579
        _differs: IterableDiffers,
1580
        cdr: ChangeDetectorRef,
1581
        _zone: NgZone,
1582
        _platformUtil: PlatformUtil,
1583
        @Inject(DOCUMENT) _document: any,
1584
        syncScrollService: IgxForOfScrollSyncService,
1585
        protected syncService: IgxForOfSyncService) {
46,451✔
1586
        super(_viewContainer, _template, _differs, cdr, _zone, syncScrollService, _platformUtil, _document);
46,451✔
1587
    }
1588

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

1601
    public override ngOnInit() {
1602
        this.syncService.setMaster(this);
46,451✔
1603
        super.ngOnInit();
46,451✔
1604
        this.removeScrollEventListeners();
46,451✔
1605
    }
1606

1607
    public override ngOnChanges(changes: SimpleChanges) {
1608
        const forOf = 'igxGridForOf';
150,191✔
1609
        this.syncService.setMaster(this);
150,191✔
1610
        if (forOf in changes) {
150,191✔
1611
            const value = changes[forOf].currentValue;
143,124✔
1612
            if (!this._differ && value) {
143,124✔
1613
                try {
46,449✔
1614
                    this._differ = this._differs.find(value).create(this.igxForTrackBy);
46,449✔
1615
                } catch (e) {
1616
                    throw new Error(
×
1617
                        `Cannot find a differ supporting object "${value}" of type "${getTypeNameForDebugging(value)}".
1618
                     NgFor only supports binding to Iterables such as Arrays.`);
1619
                }
1620
            }
1621
            if (this.igxForScrollOrientation === 'horizontal') {
143,124✔
1622
                // in case collection has changes, reset sync service
1623
                this.syncService.setMaster(this, this.igxGridForOfUniqueSizeCache);
128,413✔
1624
            }
1625
        }
1626
        const defaultItemSize = 'igxForItemSize';
150,191✔
1627
        if (defaultItemSize in changes && !changes[defaultItemSize].firstChange &&
150,191✔
1628
            this.igxForScrollOrientation === 'vertical' && this.igxForOf) {
1629
            // handle default item size changed.
1630
            this.initSizesCache(this.igxForOf);
57✔
1631
        }
1632
        const containerSize = 'igxForContainerSize';
150,191✔
1633
        if (containerSize in changes && !changes[containerSize].firstChange && this.igxForOf) {
150,191✔
1634
            const prevSize = parseInt(changes[containerSize].previousValue, 10);
31,173✔
1635
            const newSize = parseInt(changes[containerSize].currentValue, 10);
31,173✔
1636
            this._recalcOnContainerChange({prevSize, newSize});
31,173✔
1637
        }
1638
    }
1639

1640
    /**
1641
     * @hidden
1642
     * @internal
1643
     */
1644
    public assumeMaster(): void {
1645
        this._sizesCache = this.syncService.sizesCache(this.igxForScrollOrientation);
16,210✔
1646
        this.syncService.setMaster(this, true);
16,210✔
1647
    }
1648

1649
    public override ngDoCheck() {
1650
        if (this._differ) {
421,036✔
1651
            const changes = this._differ.diff(this.igxForOf);
420,932✔
1652
            if (changes) {
420,932✔
1653
                const args: IForOfDataChangingEventArgs = {
56,053✔
1654
                    containerSize: this.igxForContainerSize
1655
                };
1656
                this.dataChanging.emit(args);
56,053✔
1657
                //  re-init cache.
1658
                if (!this.igxForOf) {
56,053!
1659
                    this.igxForOf = [] as U;
×
1660
                }
1661
                /* we need to reset the master dir if all rows are removed
1662
                (e.g. because of filtering); if all columns are hidden, rows are
1663
                still rendered empty, so we should not reset master */
1664
                if (!this.igxForOf.length &&
56,053✔
1665
                    this.igxForScrollOrientation === 'vertical') {
1666
                    this.syncService.resetMaster();
108✔
1667
                }
1668
                this.syncService.setMaster(this);
56,053✔
1669
                this.igxForContainerSize = args.containerSize;
56,053✔
1670
                const sizeDiff = this._updateSizeCache(changes);
56,053✔
1671
                this._applyChanges();
56,053✔
1672
                if (sizeDiff) {
56,053✔
1673
                    this._adjustScrollPositionAfterSizeChange(sizeDiff);
40,330✔
1674
                }
1675
                this._updateScrollOffset();
56,053✔
1676
                this.dataChanged.emit();
56,053✔
1677
            }
1678
        }
1679
    }
1680

1681
    public override onScroll(event) {
1682
        if (!parseInt(this.scrollComponent.nativeElement.style.height, 10)) {
320!
1683
            return;
×
1684
        }
1685
        if (!this._bScrollInternal) {
320✔
1686
            this._calcVirtualScrollPosition(event.target.scrollTop);
63✔
1687
        } else {
1688
            this._bScrollInternal = false;
257✔
1689
        }
1690
        const scrollOffset = this.fixedUpdateAllElements(this._virtScrollPosition);
320✔
1691

1692
        this.dc.instance._viewContainer.element.nativeElement.style.top = -(scrollOffset) + 'px';
320✔
1693

1694
        this._zone.onStable.pipe(first()).subscribe(this.recalcUpdateSizes.bind(this));
320✔
1695
        this.cdr.markForCheck();
320✔
1696
    }
1697

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

1715
    protected getItemSize(item) {
1716
        let size = 0;
1,030,572✔
1717
        const dimension = this.igxForSizePropName || 'height';
1,030,572!
1718
        if (this.igxForScrollOrientation === 'vertical') {
1,030,572✔
1719
            size = this._getItemSize(item, dimension);
716,318✔
1720
            if (item && item.summaries) {
716,318✔
1721
                size = item.max;
1,574✔
1722
            } else if (item && item.groups && item.height) {
714,744✔
1723
                size = item.height;
3,019✔
1724
            }
1725
        } else {
1726
            size = parseInt(item[dimension], 10) || 0;
314,254✔
1727
        }
1728
        return size;
1,030,572✔
1729
    }
1730

1731
    protected override initSizesCache(items: U): number {
1732
        if (!this.syncService.isMaster(this) && this.igxForScrollOrientation === 'horizontal') {
20,896✔
1733
            const masterSizesCache = this.syncService.sizesCache(this.igxForScrollOrientation);
25✔
1734
            return masterSizesCache[masterSizesCache.length - 1];
25✔
1735
        }
1736
        let totalSize = 0;
20,871✔
1737
        let size = 0;
20,871✔
1738
        let i = 0;
20,871✔
1739
        this.sizesCache = [];
20,871✔
1740
        this.individualSizeCache = [];
20,871✔
1741
        this.sizesCache.push(0);
20,871✔
1742
        const count = this.isRemote ? this.totalItemCount : items.length;
20,871✔
1743
        for (i; i < count; i++) {
20,871✔
1744
            size = this.getItemSize(items[i]);
150,644✔
1745
            this.individualSizeCache.push(size);
150,644✔
1746
            totalSize += size;
150,644✔
1747
            this.sizesCache.push(totalSize);
150,644✔
1748
        }
1749
        return totalSize;
20,871✔
1750
    }
1751

1752
    protected override _updateSizeCache(changes: IterableChanges<T> = null) {
×
1753
        const oldSize = this.individualSizeCache.length > 0 ? this.individualSizeCache.reduce((acc, val) => acc + val) : 0;
240,909✔
1754
        let newSize = oldSize;
56,053✔
1755
        if (changes && !this.isRemote) {
56,053✔
1756
            newSize = this.handleCacheChanges(changes);
56,047✔
1757
        } else {
1758
            return;
6✔
1759
        }
1760

1761
        const diff = oldSize - newSize;
56,047✔
1762
        return diff;
56,047✔
1763
    }
1764

1765
    protected handleCacheChanges(changes: IterableChanges<T>) {
1766
        const identityChanges = [];
56,047✔
1767
        const newHeightCache = [];
56,047✔
1768
        const newSizesCache = [];
56,047✔
1769
        newSizesCache.push(0);
56,047✔
1770
        let newHeight = 0;
56,047✔
1771

1772
        // When there are more than one removed items the changes are not reliable so those with identity change should be default size.
1773
        let numRemovedItems = 0;
56,047✔
1774
        changes.forEachRemovedItem(() => numRemovedItems++);
67,990✔
1775

1776
        // Get the identity changes to determine later if those that have changed their indexes should be assigned default item size.
1777
        changes.forEachIdentityChange((item) => {
56,047✔
1778
            if (item.currentIndex !== item.previousIndex) {
2,352✔
1779
                // Filter out ones that have not changed their index.
1780
                identityChanges[item.currentIndex] = item;
993✔
1781
            }
1782
        });
1783

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

1805
    protected override addLastElem() {
1806
        let elemIndex = this.state.startIndex + this.state.chunkSize;
42,870✔
1807
        if (!this.isRemote && !this.igxForOf) {
42,870!
1808
            return;
×
1809
        }
1810

1811
        if (elemIndex >= this.igxForOf.length) {
42,870✔
1812
            elemIndex = this.igxForOf.length - this.state.chunkSize;
15✔
1813
        }
1814
        const input = this.igxForOf[elemIndex];
42,870✔
1815
        const embeddedView = this.dc.instance._vcr.createEmbeddedView(
42,870✔
1816
            this._template,
1817
            new IgxGridForOfContext<T, U>(input, this.igxForOf, this.getContextIndex(input), this.igxForOf.length)
1818
        );
1819

1820
        this._embeddedViews.push(embeddedView);
42,870✔
1821
        this.state.chunkSize++;
42,870✔
1822
    }
1823

1824
    protected _updateViews(prevChunkSize) {
1825
        if (this.igxForOf && this.igxForOf.length && this.dc) {
56,766✔
1826
            const embeddedViewCopy = Object.assign([], this._embeddedViews);
56,614✔
1827
            let startIndex;
1828
            let endIndex;
1829
            if (this.isRemote) {
56,614✔
1830
                startIndex = 0;
5✔
1831
                endIndex = this.igxForOf.length;
5✔
1832
            } else {
1833
                startIndex = this.getIndexAt(this.scrollPosition, this.sizesCache);
56,609✔
1834
                if (startIndex + this.state.chunkSize > this.igxForOf.length) {
56,609✔
1835
                    startIndex = this.igxForOf.length - this.state.chunkSize;
144✔
1836
                }
1837
                this.state.startIndex = startIndex;
56,609✔
1838
                endIndex = this.state.chunkSize + this.state.startIndex;
56,609✔
1839
            }
1840

1841
            for (let i = startIndex; i < endIndex && this.igxForOf[i] !== undefined; i++) {
56,614✔
1842
                const embView = embeddedViewCopy.shift();
240,999✔
1843
                this.updateTemplateContext(embView.context, i);
240,999✔
1844
            }
1845
            if (prevChunkSize !== this.state.chunkSize) {
56,614✔
1846
                this.chunkLoad.emit(this.state);
3,718✔
1847
            }
1848
        }
1849
    }
1850
    protected override _applyChanges() {
1851
        const prevChunkSize = this.state.chunkSize;
56,766✔
1852
        this.applyChunkSizeChange();
56,766✔
1853
        this._recalcScrollBarSize();
56,766✔
1854
        this._updateViews(prevChunkSize);
56,766✔
1855
    }
1856

1857
    /**
1858
     * @hidden
1859
     */
1860
    protected override _calcMaxChunkSize(): number {
1861
        if (this.igxForScrollOrientation === 'horizontal') {
126,372✔
1862
            if (this.syncService.isMaster(this)) {
109,635✔
1863
                return super._calcMaxChunkSize();
32,702✔
1864
            }
1865
            return this.syncService.chunkSize(this.igxForScrollOrientation);
76,933✔
1866
        } else {
1867
            return super._calcMaxChunkSize();
16,737✔
1868
        }
1869

1870
    }
1871
}
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