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

IgniteUI / igniteui-angular / 13764229053

10 Mar 2025 12:07PM CUT coverage: 91.647% (-0.003%) from 91.65%
13764229053

Pull #15376

github

web-flow
Merge 7373ea4c3 into 09e3ffb12
Pull Request #15376: feat(pivotGrid): Improve PivotGrid visually when there's no data or has empty area at the bottom.

13336 of 15606 branches covered (85.45%)

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

1 existing line in 1 file now uncovered.

26892 of 29343 relevant lines covered (91.65%)

33704.78 hits per line

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

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

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

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

38
const MAX_PERF_SCROLL_DIFF = 4;
2✔
39

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

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

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

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

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

79
}
80

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

230
    @Output()
231
    public beforeViewDestroyed = new EventEmitter<EmbeddedViewRef<any>>();
46,773✔
232

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

420
    public verticalScrollHandler(event) {
421
        this.onScroll(event);
49✔
422
    }
423

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

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

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

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

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

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

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

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

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

590

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

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

619
        this._bScrollInternal = true;
58✔
620
        this._virtScrollPosition += add;
58✔
621
        this._virtScrollPosition = this._virtScrollPosition > 0 ?
58✔
622
            (this._virtScrollPosition < maxVirtScrollTop ? this._virtScrollPosition : maxVirtScrollTop) :
56✔
623
            0;
624

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

895
    /**
896
     * @hidden
897
     */
898
    protected removeScrollEventListeners() {
899
        if (this.igxForScrollOrientation === 'horizontal') {
91,967✔
900
            this._zone.runOutsideAngular(() => this.scrollComponent?.nativeElement?.removeEventListener('scroll', this.func));
69,505✔
901
        } else {
902
            this._zone.runOutsideAngular(() =>
22,462✔
903
                this.scrollComponent?.nativeElement?.removeEventListener('scroll', this.verticalScrollHandler)
22,462✔
904
            );
905
        }
906
    }
907

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

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

927
        this._zone.onStable.pipe(first()).subscribe(this.recalcUpdateSizes.bind(this));
70✔
928

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

935

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

948
    protected updateSizes() {
949
        if (!this.scrollComponent.nativeElement.isConnected) return;
823✔
950
        const scrollable = this.isScrollable();
759✔
951
        this.recalcUpdateSizes();
759✔
952
        this._applyChanges();
759✔
953
        this._updateScrollOffset();
759✔
954
        if (scrollable !== this.isScrollable()) {
759✔
955
            this.scrollbarVisibilityChanged.emit();
9✔
956
        } else {
957
            this.contentSizeChange.emit();
750✔
958
        }
959
    }
960

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

968
        if (newStart + this.state.chunkSize > count) {
3,116✔
969
            newStart = count - this.state.chunkSize;
837✔
970
        }
971

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

976
        if (diff) {
3,116✔
977
            this.chunkPreload.emit(this.state);
989✔
978
            if (!this.isRemote) {
989✔
979

980
                // recalculate and apply page size.
981
                if (diff && Math.abs(diff) <= MAX_PERF_SCROLL_DIFF) {
977✔
982
                    if (diff > 0) {
724✔
983
                        this.moveApplyScrollNext(prevStart);
508✔
984
                    } else {
985
                        this.moveApplyScrollPrev(prevStart);
216✔
986
                    }
987
                } else {
988
                    this.fixedApplyScroll();
253✔
989
                }
990
            }
991
        }
992

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1261
        const diff = oldHeight - newHeight;
851✔
1262
        this._adjustScrollPositionAfterSizeChange(diff);
851✔
1263
    }
1264

1265
    /**
1266
     * @hidden
1267
     */
1268
    protected _calcMaxChunkSize(): number {
1269
        let i = 0;
50,168✔
1270
        let length = 0;
50,168✔
1271
        let maxLength = 0;
50,168✔
1272
        const arr = [];
50,168✔
1273
        let sum = 0;
50,168✔
1274
        const availableSize = parseInt(this.igxForContainerSize, 10);
50,168✔
1275
        if (!availableSize) {
50,168✔
1276
            return 0;
10,421✔
1277
        }
1278
        const dimension = this.igxForScrollOrientation === 'horizontal' ?
39,747✔
1279
            this.igxForSizePropName : 'height';
1280
        const reducer = (accumulator, currentItem) => accumulator + this._getItemSize(currentItem, dimension);
38,817,723✔
1281
        for (i; i < this.igxForOf.length; i++) {
39,747✔
1282
            let item: T | { value: T, height: number } = this.igxForOf[i];
5,472,853✔
1283
            if (dimension === 'height') {
5,472,853✔
1284
                item = { value: this.igxForOf[i], height: this.individualSizeCache[i] };
5,156,871✔
1285
            }
1286
            const size = dimension === 'height' ?
5,472,853✔
1287
                this.individualSizeCache[i] :
1288
                this._getItemSize(item, dimension);
1289
            sum = arr.reduce(reducer, size);
5,472,853✔
1290
            if (sum < availableSize) {
5,472,853✔
1291
                arr.push(item);
161,954✔
1292
                length = arr.length;
161,954✔
1293
                if (i === this.igxForOf.length - 1) {
161,954✔
1294
                    // reached end without exceeding
1295
                    // include prev items until size is filled or first item is reached.
1296
                    let curItem = dimension === 'height' ? arr[0].value : arr[0];
9,585✔
1297
                    let prevIndex = this.igxForOf.indexOf(curItem) - 1;
9,585✔
1298
                    while (prevIndex >= 0 && sum <= availableSize) {
9,585✔
1299
                        curItem = dimension === 'height' ? arr[0].value : arr[0];
159✔
1300
                        prevIndex = this.igxForOf.indexOf(curItem) - 1;
159✔
1301
                        const prevItem = this.igxForOf[prevIndex];
159✔
1302
                        const prevSize = dimension === 'height' ?
159✔
1303
                            this.individualSizeCache[prevIndex] :
1304
                            parseInt(prevItem[dimension], 10);
1305
                        sum = arr.reduce(reducer, prevSize);
159✔
1306
                        arr.unshift(prevItem);
159✔
1307
                        length = arr.length;
159✔
1308
                    }
1309
                }
1310
            } else {
1311
                arr.push(item);
5,310,899✔
1312
                length = arr.length + 1;
5,310,899✔
1313
                arr.shift();
5,310,899✔
1314
            }
1315
            if (length > maxLength) {
5,472,853✔
1316
                maxLength = length;
191,399✔
1317
            }
1318
        }
1319
        return maxLength;
39,747✔
1320
    }
1321

1322
    /**
1323
     * @hidden
1324
     */
1325
    protected getIndexAt(left, set) {
1326
        let start = 0;
90,472✔
1327
        let end = set.length - 1;
90,472✔
1328
        if (left === 0) {
90,472✔
1329
            return 0;
87,829✔
1330
        }
1331
        while (start <= end) {
2,643✔
1332
            const midIdx = Math.floor((start + end) / 2);
10,068✔
1333
            const midLeft = set[midIdx];
10,068✔
1334
            const cmp = left - midLeft;
10,068✔
1335
            if (cmp > 0) {
10,068✔
1336
                start = midIdx + 1;
4,545✔
1337
            } else if (cmp < 0) {
5,523✔
1338
                end = midIdx - 1;
5,218✔
1339
            } else {
1340
                return midIdx;
305✔
1341
            }
1342
        }
1343
        return end;
2,338✔
1344
    }
1345

1346
    protected _recalcScrollBarSize(containerSizeInfo = null) {
57,300✔
1347
        const count = this.isRemote ? this.totalItemCount : (this.igxForOf ? this.igxForOf.length : 0);
89,845!
1348
        this.dc.instance.notVirtual = !(this.igxForContainerSize && this.dc && this.state.chunkSize < count);
89,845✔
1349
        const scrollable = containerSizeInfo ? this.scrollComponent.size > containerSizeInfo.prevSize : this.isScrollable();
89,845✔
1350
        if (this.igxForScrollOrientation === 'horizontal') {
89,845✔
1351
            const totalWidth = parseInt(this.igxForContainerSize, 10) > 0 ? this._calcSize() : 0;
73,626✔
1352
            if (totalWidth <= parseInt(this.igxForContainerSize, 10)) {
73,626✔
1353
                this.resetScrollPosition();
36,779✔
1354
            }
1355
            this.scrollComponent.nativeElement.style.width = this.igxForContainerSize + 'px';
73,626✔
1356
            this.scrollComponent.size = totalWidth;
73,626✔
1357
        }
1358
        if (this.igxForScrollOrientation === 'vertical') {
89,845✔
1359
            const totalHeight = this._calcSize();
16,219✔
1360
            if (totalHeight <= parseInt(this.igxForContainerSize, 10)) {
16,219✔
1361
                this.resetScrollPosition();
5,521✔
1362
            }
1363
            this.scrollComponent.nativeElement.style.height = parseInt(this.igxForContainerSize, 10) + 'px';
16,219✔
1364
            this.scrollComponent.size = totalHeight;
16,219✔
1365
        }
1366
        if (scrollable !== this.isScrollable()) {
89,845✔
1367
            // scrollbar visibility has changed
1368
            this.scrollbarVisibilityChanged.emit();
18,330✔
1369
        }
1370
    }
1371

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

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

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

1407
        this.state.chunkSize--;
10,603✔
1408
    }
1409

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

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

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

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

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

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

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

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

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

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

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

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

1515
export interface IForOfDataChangingEventArgs extends IBaseEventArgs {
1516
    containerSize: number;
1517
    state: IForOfState;
1518
}
1519

1520
export class IgxGridForOfContext<T, U extends T[] = T[]> extends IgxForOfContext<T, U> {
1521
    constructor(
1522
        $implicit: T,
1523
        public igxGridForOf: U,
43,298✔
1524
        index: number,
1525
        count: number
1526
    ) {
1527
        super($implicit, igxGridForOf, index, count);
43,298✔
1528
    }
1529
}
1530

1531
@Directive({
1532
    selector: '[igxGridFor][igxGridForOf]',
1533
    standalone: true
1534
})
1535
export class IgxGridForOfDirective<T, U extends T[] = T[]> extends IgxForOfDirective<T, U> implements OnInit, OnChanges, DoCheck {
2✔
1536
    @Input()
1537
    public set igxGridForOf(value: U & T[] | null) {
1538
        this.igxForOf = value;
147,158✔
1539
    }
1540

1541
    public get igxGridForOf() {
1542
        return this.igxForOf;
33✔
1543
    }
1544

1545
    @Input({ transform: booleanAttribute })
1546
    public igxGridForOfUniqueSizeCache = false;
45,634✔
1547

1548
    @Input({ transform: booleanAttribute })
1549
    public igxGridForOfVariableSizes = true;
45,634✔
1550

1551
    /**
1552
     * @hidden
1553
     * @internal
1554
     */
1555
    public override get sizesCache(): number[] {
1556
        if (this.igxForScrollOrientation === 'horizontal') {
955,736✔
1557
            if (this.igxGridForOfUniqueSizeCache || this.syncService.isMaster(this)) {
737,191✔
1558
                return this._sizesCache;
456,558✔
1559
            }
1560
            return this.syncService.sizesCache(this.igxForScrollOrientation);
280,633✔
1561
        } else {
1562
            return this._sizesCache;
218,545✔
1563
        }
1564
    }
1565
    /**
1566
     * @hidden
1567
     * @internal
1568
     */
1569
    public override set sizesCache(value: number[]) {
1570
        this._sizesCache = value;
74,938✔
1571
    }
1572

1573
    protected get itemsDimension() {
1574
        return this.igxForSizePropName || 'height';
×
1575
    }
1576

1577
    public override recalcUpdateSizes() {
1578
        if (this.igxGridForOfVariableSizes && this.igxForScrollOrientation === 'vertical') {
1,150✔
1579
            super.recalcUpdateSizes();
1,074✔
1580
        }
1581
    }
1582

1583
    /**
1584
     * @hidden @internal
1585
     * An event that is emitted after data has been changed but before the view is refreshed
1586
     */
1587
    @Output()
1588
    public dataChanging = new EventEmitter<IForOfDataChangingEventArgs>();
45,634✔
1589

1590
    constructor(
1591
        _viewContainer: ViewContainerRef,
1592
        _template: TemplateRef<NgForOfContext<T>>,
1593
        _differs: IterableDiffers,
1594
        cdr: ChangeDetectorRef,
1595
        _zone: NgZone,
1596
        _platformUtil: PlatformUtil,
1597
        @Inject(DOCUMENT) _document: any,
1598
        syncScrollService: IgxForOfScrollSyncService,
1599
        protected syncService: IgxForOfSyncService) {
45,634✔
1600
        super(_viewContainer, _template, _differs, cdr, _zone, syncScrollService, _platformUtil, _document);
45,634✔
1601
    }
1602

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

1615
    public override ngOnInit() {
1616
        this.syncService.setMaster(this);
45,634✔
1617
        super.ngOnInit();
45,634✔
1618
        this.removeScrollEventListeners();
45,634✔
1619
    }
1620

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

1654
    /**
1655
     * @hidden
1656
     * @internal
1657
     */
1658
    public assumeMaster(): void {
1659
        this._sizesCache = this.syncService.sizesCache(this.igxForScrollOrientation);
16,793✔
1660
        this.syncService.setMaster(this, true);
16,793✔
1661
    }
1662

1663
    public override ngDoCheck() {
1664
        if (this._differ) {
444,739✔
1665
            const changes = this._differ.diff(this.igxForOf);
444,633✔
1666
            if (changes) {
444,633✔
1667
                const args: IForOfDataChangingEventArgs = {
55,081✔
1668
                    containerSize: this.igxForContainerSize,
1669
                    state: this.state
1670
                };
1671
                this.dataChanging.emit(args);
55,081✔
1672
                //  re-init cache.
1673
                if (!this.igxForOf) {
55,081!
1674
                    this.igxForOf = [] as U;
×
1675
                }
1676
                /* we need to reset the master dir if all rows are removed
1677
                (e.g. because of filtering); if all columns are hidden, rows are
1678
                still rendered empty, so we should not reset master */
1679
                if (!this.igxForOf.length &&
55,081✔
1680
                    this.igxForScrollOrientation === 'vertical') {
1681
                    this.syncService.resetMaster();
104✔
1682
                }
1683
                this.syncService.setMaster(this);
55,081✔
1684
                this.igxForContainerSize = args.containerSize;
55,081✔
1685
                const sizeDiff = this._updateSizeCache(changes);
55,081✔
1686
                this._applyChanges();
55,081✔
1687
                if (sizeDiff) {
55,081✔
1688
                    this._adjustScrollPositionAfterSizeChange(sizeDiff);
39,811✔
1689
                }
1690
                this._updateScrollOffset();
55,081✔
1691
                this.dataChanged.emit(args);
55,081✔
1692
            }
1693
        }
1694
    }
1695

1696
    public override onScroll(event) {
1697
        if (!parseInt(this.scrollComponent.nativeElement.style.height, 10)) {
325!
1698
            return;
×
1699
        }
1700
        if (!this._bScrollInternal) {
325✔
1701
            this._calcVirtualScrollPosition(event.target.scrollTop);
64✔
1702
        } else {
1703
            this._bScrollInternal = false;
261✔
1704
        }
1705
        const scrollOffset = this.fixedUpdateAllElements(this._virtScrollPosition);
325✔
1706

1707
        this.dc.instance._viewContainer.element.nativeElement.style.top = -(scrollOffset) + 'px';
325✔
1708

1709
        this._zone.onStable.pipe(first()).subscribe(this.recalcUpdateSizes.bind(this));
325✔
1710
        this.cdr.markForCheck();
325✔
1711
    }
1712

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

1730
    protected getItemSize(item) {
1731
        let size = 0;
1,081,073✔
1732
        const dimension = this.igxForSizePropName || 'height';
1,081,073!
1733
        if (this.igxForScrollOrientation === 'vertical') {
1,081,073✔
1734
            size = this._getItemSize(item, dimension);
778,468✔
1735
            if (item && item.summaries) {
778,468✔
1736
                size = item.max;
1,574✔
1737
            } else if (item && item.groups && item.height) {
776,894✔
1738
                size = item.height;
23,019✔
1739
            }
1740
        } else {
1741
            size = parseInt(item[dimension], 10) || 0;
302,605✔
1742
        }
1743
        return size;
1,081,073✔
1744
    }
1745

1746
    protected override initSizesCache(items: U): number {
1747
        if (!this.syncService.isMaster(this) && this.igxForScrollOrientation === 'horizontal') {
19,888✔
1748
            const masterSizesCache = this.syncService.sizesCache(this.igxForScrollOrientation);
25✔
1749
            return masterSizesCache[masterSizesCache.length - 1];
25✔
1750
        }
1751
        let totalSize = 0;
19,863✔
1752
        let size = 0;
19,863✔
1753
        let i = 0;
19,863✔
1754
        this.sizesCache = [];
19,863✔
1755
        this.individualSizeCache = [];
19,863✔
1756
        this.sizesCache.push(0);
19,863✔
1757
        const count = this.isRemote ? this.totalItemCount : items.length;
19,863✔
1758
        for (i; i < count; i++) {
19,863✔
1759
            size = this.getItemSize(items[i]);
173,940✔
1760
            this.individualSizeCache.push(size);
173,940✔
1761
            totalSize += size;
173,940✔
1762
            this.sizesCache.push(totalSize);
173,940✔
1763
        }
1764
        return totalSize;
19,863✔
1765
    }
1766

1767
    protected override _updateSizeCache(changes: IterableChanges<T> = null) {
×
1768
        const oldSize = this.individualSizeCache.length > 0 ? this.individualSizeCache.reduce((acc, val) => acc + val) : 0;
265,604✔
1769
        let newSize = oldSize;
55,081✔
1770
        if (changes && !this.isRemote) {
55,081✔
1771
            newSize = this.handleCacheChanges(changes);
55,075✔
1772
        } else {
1773
            return;
6✔
1774
        }
1775

1776
        const diff = oldSize - newSize;
55,075✔
1777
        return diff;
55,075✔
1778
    }
1779

1780
    protected handleCacheChanges(changes: IterableChanges<T>) {
1781
        const identityChanges = [];
55,075✔
1782
        const newHeightCache = [];
55,075✔
1783
        const newSizesCache = [];
55,075✔
1784
        newSizesCache.push(0);
55,075✔
1785
        let newHeight = 0;
55,075✔
1786

1787
        // When there are more than one removed items the changes are not reliable so those with identity change should be default size.
1788
        let numRemovedItems = 0;
55,075✔
1789
        changes.forEachRemovedItem(() => numRemovedItems++);
67,981✔
1790

1791
        // Get the identity changes to determine later if those that have changed their indexes should be assigned default item size.
1792
        changes.forEachIdentityChange((item) => {
55,075✔
1793
            if (item.currentIndex !== item.previousIndex) {
2,671✔
1794
                // Filter out ones that have not changed their index.
1795
                identityChanges[item.currentIndex] = item;
1,055✔
1796
            }
1797
        });
1798

1799
        // Processing each item that is passed to the igxForOf so far seem to be most reliable. We parse the updated list of items.
1800
        changes.forEachItem((item) => {
55,075✔
1801
            if (item.previousIndex !== null &&
930,510✔
1802
                (numRemovedItems < 2 || !identityChanges.length || identityChanges[item.currentIndex])
1803
                && this.igxForScrollOrientation !== "horizontal") {
1804
                // Reuse cache on those who have previousIndex.
1805
                // When there are more than one removed items currently the changes are not readable so ones with identity change
1806
                // should be racalculated.
1807
                newHeightCache[item.currentIndex] = this.individualSizeCache[item.previousIndex];
23,377✔
1808
            } else {
1809
                // Assign default item size.
1810
                newHeightCache[item.currentIndex] = this.getItemSize(item.item);
907,133✔
1811
            }
1812
            newSizesCache[item.currentIndex + 1] = newSizesCache[item.currentIndex] + newHeightCache[item.currentIndex];
930,510✔
1813
            newHeight += newHeightCache[item.currentIndex];
930,510✔
1814
        });
1815
        this.individualSizeCache = newHeightCache;
55,075✔
1816
        this.sizesCache = newSizesCache;
55,075✔
1817
        return newHeight;
55,075✔
1818
    }
1819

1820
    protected override addLastElem() {
1821
        let elemIndex = this.state.startIndex + this.state.chunkSize;
43,298✔
1822
        if (!this.isRemote && !this.igxForOf) {
43,298!
1823
            return;
×
1824
        }
1825

1826
        if (elemIndex >= this.igxForOf.length) {
43,298✔
1827
            elemIndex = this.igxForOf.length - this.state.chunkSize;
15✔
1828
        }
1829
        const input = this.igxForOf[elemIndex];
43,298✔
1830
        const embeddedView = this.dc.instance._vcr.createEmbeddedView(
43,298✔
1831
            this._template,
1832
            new IgxGridForOfContext<T, U>(input, this.igxForOf, this.getContextIndex(input), this.igxForOf.length)
1833
        );
1834

1835
        this._embeddedViews.push(embeddedView);
43,298✔
1836
        this.state.chunkSize++;
43,298✔
1837
    }
1838

1839
    protected _updateViews(prevChunkSize) {
1840
        if (this.igxForOf && this.igxForOf.length && this.dc) {
55,800✔
1841
            const embeddedViewCopy = Object.assign([], this._embeddedViews);
55,645✔
1842
            let startIndex;
1843
            let endIndex;
1844
            if (this.isRemote) {
55,645✔
1845
                startIndex = 0;
5✔
1846
                endIndex = this.igxForOf.length;
5✔
1847
            } else {
1848
                startIndex = this.getIndexAt(this.scrollPosition, this.sizesCache);
55,640✔
1849
                if (startIndex + this.state.chunkSize > this.igxForOf.length) {
55,640✔
1850
                    startIndex = this.igxForOf.length - this.state.chunkSize;
142✔
1851
                }
1852
                this.state.startIndex = startIndex;
55,640✔
1853
                endIndex = this.state.chunkSize + this.state.startIndex;
55,640✔
1854
            }
1855

1856
            for (let i = startIndex; i < endIndex && this.igxForOf[i] !== undefined; i++) {
55,645✔
1857
                const embView = embeddedViewCopy.shift();
238,262✔
1858
                this.updateTemplateContext(embView.context, i);
238,262✔
1859
            }
1860
            if (prevChunkSize !== this.state.chunkSize) {
55,645✔
1861
                this.chunkLoad.emit(this.state);
3,921✔
1862
            }
1863
        }
1864
    }
1865
    protected override _applyChanges() {
1866
        const prevChunkSize = this.state.chunkSize;
55,800✔
1867
        this.applyChunkSizeChange();
55,800✔
1868
        this._recalcScrollBarSize();
55,800✔
1869
        this._updateViews(prevChunkSize);
55,800✔
1870
    }
1871

1872
    /**
1873
     * @hidden
1874
     */
1875
    protected override _calcMaxChunkSize(): number {
1876
        if (this.igxForScrollOrientation === 'horizontal') {
125,500✔
1877
            if (this.syncService.isMaster(this)) {
107,992✔
1878
                return super._calcMaxChunkSize();
30,682✔
1879
            }
1880
            return this.syncService.chunkSize(this.igxForScrollOrientation);
77,310✔
1881
        } else {
1882
            return super._calcMaxChunkSize();
17,508✔
1883
        }
1884

1885
    }
1886
}
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