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

IgniteUI / igniteui-angular / 15971931624

30 Jun 2025 11:41AM UTC coverage: 91.634%. Remained the same
15971931624

Pull #15858

github

web-flow
Merge 0d82a6f54 into 077b5861c
Pull Request #15858: fix(grid-validation): add open/close animations for error tooltip - 19.2.x

13452 of 15747 branches covered (85.43%)

27055 of 29525 relevant lines covered (91.63%)

37025.72 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;
3✔
39

40
/**
41
 *  @publicApi
42
 */
43
export class IgxForOfContext<T, U extends T[] = T[]> {
44
    constructor(
45
        public $implicit: T,
206,151✔
46
        public igxForOf: U,
206,151✔
47
        public index: number,
206,151✔
48
        public count: number
206,151✔
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 {
3✔
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';
47,264✔
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>();
47,264✔
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>();
47,264✔
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>();
47,264✔
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<IForOfDataChangeEventArgs>();
47,264✔
229

230
    @Output()
231
    public beforeViewDestroyed = new EventEmitter<EmbeddedViewRef<any>>();
47,264✔
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>();
47,264✔
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 = {
47,264✔
263
        startIndex: 0,
264
        chunkSize: 0
265
    };
266

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

288
    private _totalItemCount: number = null;
47,264✔
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;
47,264✔
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;
924,894✔
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;
7,009✔
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;
920,868✔
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;
232,460✔
365
    }
366
    public set scrollPosition(val: number) {
367
        if (val === this.scrollComponent.scrollAmount) {
45,226✔
368
            return;
44,459✔
369
        }
370
        if (this.igxForScrollOrientation === 'horizontal' && this.scrollComponent) {
767✔
371
            this.scrollComponent.nativeElement.scrollLeft = this.isRTL ? -val : val;
235!
372
        } else if (this.scrollComponent) {
532✔
373
            this.scrollComponent.nativeElement.scrollTop = val;
532✔
374
        }
375
    }
376

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

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

392
    private get _isScrolledToBottom() {
393
        if (!this.getScroll()) {
403!
394
            return true;
×
395
        }
396
        const scrollHeight = this.getScroll().scrollHeight;
403✔
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;
403✔
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,
47,264✔
408
        protected _template: TemplateRef<NgForOfContext<T>>,
47,264✔
409
        protected _differs: IterableDiffers,
47,264✔
410
        public cdr: ChangeDetectorRef,
47,264✔
411
        protected _zone: NgZone,
47,264✔
412
        protected syncScrollService: IgxForOfScrollSyncService,
47,264✔
413
        protected platformUtil: PlatformUtil,
47,264✔
414
        @Inject(DOCUMENT)
415
        protected document: any,
47,264✔
416
    ) {
417
        super();
47,264✔
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);
261,395✔
426
    }
427

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

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

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

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

509
    /**
510
     * @hidden
511
     */
512
    public ngOnDestroy() {
513
        this.removeScrollEventListeners();
46,824✔
514
        this.destroy$.next(true);
46,824✔
515
        this.destroy$.complete();
46,824✔
516
        if (this.contentObserver) {
46,824✔
517
            this.contentObserver.disconnect();
11,765✔
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,664✔
538
        if (forOf in changes) {
2,664✔
539
            const value = changes[forOf].currentValue;
2,172✔
540
            if (!this._differ && value) {
2,172✔
541
                try {
1,165✔
542
                    this._differ = this._differs.find(value).create(this.igxForTrackBy);
1,165✔
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,664✔
551
        if (defaultItemSize in changes && !changes[defaultItemSize].firstChange && this.igxForOf) {
2,664✔
552
            // handle default item size changed.
553
            this.initSizesCache(this.igxForOf);
283✔
554
            this._applyChanges();
283✔
555
        }
556
        const containerSize = 'igxForContainerSize';
2,664✔
557
        if (containerSize in changes && !changes[containerSize].firstChange && this.igxForOf) {
2,664✔
558
            const prevSize = parseInt(changes[containerSize].previousValue, 10);
389✔
559
            const newSize = parseInt(changes[containerSize].currentValue, 10);
389✔
560
            this._recalcOnContainerChange({prevSize, newSize});
389✔
561
        }
562
    }
563

564
    /**
565
     * @hidden
566
     */
567
    public ngDoCheck(): void {
568
        if (this._differ) {
13,121✔
569
            const changes = this._differ.diff(this.igxForOf);
13,121✔
570
            if (changes) {
13,121✔
571
                //  re-init cache.
572
                if (!this.igxForOf) {
1,379✔
573
                    this.igxForOf = [] as U;
1✔
574
                }
575
                this._updateSizeCache();
1,379✔
576
                this._zone.run(() => {
1,379✔
577
                    this._applyChanges();
1,379✔
578
                    this.cdr.markForCheck();
1,379✔
579
                    this._updateScrollOffset();
1,379✔
580
                    const args: IForOfDataChangingEventArgs = {
1,379✔
581
                        containerSize: this.igxForContainerSize,
582
                        state: this.state
583
                    };
584
                    this.dataChanged.emit(args);
1,379✔
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
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) {
758✔
659
            return;
16✔
660
        }
661
        const containerSize = parseInt(this.igxForContainerSize, 10);
742✔
662
        const isPrevItem = index < this.state.startIndex || this.scrollPosition > this.sizesCache[index];
742✔
663
        let nextScroll = isPrevItem ? this.sizesCache[index] : this.sizesCache[index + 1] - containerSize;
742✔
664
        if (nextScroll < 0) {
742✔
665
            return;
330✔
666
        }
667
        const maxVirtScrollTop = this._virtSize - containerSize;
412✔
668
        if (nextScroll > maxVirtScrollTop) {
412!
669
            nextScroll = maxVirtScrollTop;
×
670
        }
671
        this._bScrollInternal = true;
412✔
672
        this._virtScrollPosition = nextScroll;
412✔
673
        this.scrollPosition = this._virtScrollPosition / this._virtRatio;
412✔
674
        this._adjustToIndex = !isPrevItem ? index : null;
412✔
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,006✔
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,653✔
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;
188,263!
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);
127✔
783
        const scroll = bottom ? Math.max(0, this.sizesCache[index + 1] - containerSize) : this.sizesCache[index];
127✔
784
        return scroll;
127✔
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,358✔
820
            this.igxForSizePropName : 'height';
821
        const diffs = [];
2,358✔
822
        let totalDiff = 0;
2,358✔
823
        const l = this._embeddedViews.length;
2,358✔
824
        const rNodes = this._embeddedViews.map(view =>
2,358✔
825
            view.rootNodes.find(node => node.nodeType === Node.ELEMENT_NODE) || view.rootNodes[0].nextElementSibling);
20,643✔
826
        for (let i = 0; i < l; i++) {
2,358✔
827
            const rNode = rNodes[i];
13,294✔
828
            if (rNode) {
13,294✔
829
                const height = window.getComputedStyle(rNode).getPropertyValue('height');
13,289✔
830
                const h = parseFloat(height) || parseInt(this.igxForItemSize, 10);
13,289✔
831
                const index = this.state.startIndex + i;
13,289✔
832
                if (!this.isRemote && !this.igxForOf[index]) {
13,289✔
833
                    continue;
15✔
834
                }
835
                const margin = this.getMargin(rNode, dimension);
13,274✔
836
                const oldVal = this.individualSizeCache[index];
13,274✔
837
                const newVal = (dimension === 'height' ? h : rNode.clientWidth) + margin;
13,274✔
838
                this.individualSizeCache[index] = newVal;
13,274✔
839
                const currDiff = newVal - oldVal;
13,274✔
840
                diffs.push(currDiff);
13,274✔
841
                totalDiff += currDiff;
13,274✔
842
                this.sizesCache[index + 1] = (this.sizesCache[index] || 0) + newVal;
13,274✔
843
            }
844
        }
845
        // update cache
846
        if (Math.abs(totalDiff) > 0) {
2,358✔
847
            for (let j = this.state.startIndex + this.state.chunkSize + 1; j < this.sizesCache.length; j++) {
403✔
848
                this.sizesCache[j] = (this.sizesCache[j] || 0) + totalDiff;
8,519!
849
            }
850

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

854
            const hSum = this.individualSizeCache.reduce(reducer);
403✔
855
            if (hSum > this._maxSize) {
403!
856
                this._virtRatio = hSum / this._maxSize;
×
857
            }
858
            this.scrollComponent.size = Math.min(this.scrollComponent.size + totalDiff, this._maxSize);
403✔
859
            this._virtSize = hSum;
403✔
860
            if (!this.scrollComponent.destroyed) {
403✔
861
                this.scrollComponent.cdr.detectChanges();
399✔
862
            }
863
            const scrToBottom = this._isScrolledToBottom && !this.dc.instance.notVirtual;
403✔
864
            if (scrToBottom && !this._isAtBottomIndex) {
403✔
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) {
395✔
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;
39✔
876
                const sumDiffs = diffs.slice(0, updatesToIndex).reduce(reducer);
39✔
877
                if (sumDiffs !== 0) {
39✔
878
                    this.addScroll(sumDiffs);
27✔
879
                }
880
                this._adjustToIndex = null;
39✔
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;
44,355✔
892
        this.scrollComponent.scrollAmount = 0;
44,355✔
893
    }
894

895
    /**
896
     * @hidden
897
     */
898
    protected removeScrollEventListeners() {
899
        if (this.igxForScrollOrientation === 'horizontal') {
92,923✔
900
            this._zone.runOutsideAngular(() => this.scrollComponent?.nativeElement?.removeEventListener('scroll', this.func));
70,191✔
901
        } else {
902
            this._zone.runOutsideAngular(() =>
22,732✔
903
                this.scrollComponent?.nativeElement?.removeEventListener('scroll', this.verticalScrollHandler)
22,732✔
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") {
869✔
942
            const scrollAmount = this.scrollComponent.nativeElement["scrollLeft"];
869✔
943
            this.scrollComponent.scrollAmount = scrollAmount;
869✔
944
            this._updateScrollOffset();
869✔
945
        }
946
    }
947

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

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

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

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

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

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

993
        return inScrollTop - this.sizesCache[this.state.startIndex];
3,045✔
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;
205✔
1025
        for (let i = prevIndex - 1; i >= this.state.startIndex && this.igxForOf[i] !== undefined; i--) {
205✔
1026
            const embView = this._embeddedViews.pop();
251✔
1027
            if (!embView.destroyed) {
251✔
1028
                this.scrollFocus(embView.rootNodes.find(node => node.nodeType === Node.ELEMENT_NODE)
536!
1029
                    || embView.rootNodes[0].nextElementSibling);
1030
                const view = container.detach(container.length - 1);
251✔
1031

1032
                this.updateTemplateContext(embView.context, i);
251✔
1033
                container.insert(view, 0);
251✔
1034
                this._embeddedViews.unshift(embView);
251✔
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);
469,439✔
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];
263,288✔
1054
        context.index = this.getContextIndex(this.igxForOf[index]);
263,288✔
1055
        context.count = this.igxForOf.length;
263,288✔
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;
250✔
1064
        const endIndex = this.state.startIndex + this.state.chunkSize;
250✔
1065
        for (let i = this.state.startIndex; i < endIndex && this.igxForOf[i] !== undefined; i++) {
250✔
1066
            const embView = this._embeddedViews[j++];
1,652✔
1067
            this.updateTemplateContext(embView.context, i);
1,652✔
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,077!
1079
            return;
×
1080
        }
1081
        const document = node.getRootNode() as Document | ShadowRoot;
1,077✔
1082
        const activeElement = document.activeElement as HTMLElement;
1,077✔
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,077!
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;
47,261✔
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;
38,053✔
1156
    }
1157

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

1183
    /**
1184
     * @hidden
1185
     */
1186
    protected _calcMaxBrowserSize(): number {
1187
        if (!this.platformUtil.isBrowser) {
47,264!
1188
            return 0;
×
1189
        }
1190
        const div = this.document.createElement('div');
47,264✔
1191
        const style = div.style;
47,264✔
1192
        style.position = 'absolute';
47,264✔
1193
        const dir = this.igxForScrollOrientation === 'horizontal' ? 'left' : 'top';
47,264✔
1194
        style[dir] = '9999999999999999px';
47,264✔
1195
        this.document.body.appendChild(div);
47,264✔
1196
        const size = Math.abs(div.getBoundingClientRect()[dir]);
47,264✔
1197
        this.document.body.removeChild(div);
47,264✔
1198
        return size;
47,264✔
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;
133,124✔
1208
        if (this.igxForContainerSize !== null && this.igxForContainerSize !== undefined) {
133,124✔
1209
            if (!this.sizesCache || this.sizesCache.length === 0) {
131,830✔
1210
                this.initSizesCache(this.igxForOf);
12,714✔
1211
            }
1212
            chunkSize = this._calcMaxChunkSize();
131,830✔
1213
            if (this.igxForOf && chunkSize > this.igxForOf.length) {
131,830✔
1214
                chunkSize = this.igxForOf.length;
17,740✔
1215
            }
1216
        } else {
1217
            if (this.igxForOf) {
1,294✔
1218
                chunkSize = Math.min(this.igxForInitialChunkSize || this.igxForOf.length, this.igxForOf.length);
1,294✔
1219
            }
1220
        }
1221
        return chunkSize;
133,124✔
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,289✔
1237
        let size = 0;
3,289✔
1238
        const dimension = this.igxForSizePropName || 'height';
3,289!
1239
        let i = 0;
3,289✔
1240
        this.sizesCache = [];
3,289✔
1241
        this.individualSizeCache = [];
3,289✔
1242
        this.sizesCache.push(0);
3,289✔
1243
        const count = this.isRemote ? this.totalItemCount : items.length;
3,289✔
1244
        for (i; i < count; i++) {
3,289✔
1245
            size = this._getItemSize(items[i], dimension);
3,859,422✔
1246
            this.individualSizeCache.push(size);
3,859,422✔
1247
            totalSize += size;
3,859,422✔
1248
            this.sizesCache.push(totalSize);
3,859,422✔
1249
        }
1250
        return totalSize;
3,289✔
1251
    }
1252

1253
    protected _updateSizeCache() {
1254
        if (this.igxForScrollOrientation === 'horizontal') {
1,379✔
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,937✔
1259
        const newHeight = this.initSizesCache(this.igxForOf);
1,047✔
1260

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

1265
    /**
1266
     * @hidden
1267
     */
1268
    protected _calcMaxChunkSize(): number {
1269
        let i = 0;
52,872✔
1270
        let length = 0;
52,872✔
1271
        let maxLength = 0;
52,872✔
1272
        const arr = [];
52,872✔
1273
        let sum = 0;
52,872✔
1274
        const availableSize = parseInt(this.igxForContainerSize, 10);
52,872✔
1275
        if (!availableSize) {
52,872✔
1276
            return 0;
10,547✔
1277
        }
1278
        const dimension = this.igxForScrollOrientation === 'horizontal' ?
42,325✔
1279
            this.igxForSizePropName : 'height';
1280
        const reducer = (accumulator, currentItem) => accumulator + this._getItemSize(currentItem, dimension);
40,308,128✔
1281
        for (i; i < this.igxForOf.length; i++) {
42,325✔
1282
            let item: T | { value: T, height: number } = this.igxForOf[i];
5,538,244✔
1283
            if (dimension === 'height') {
5,538,244✔
1284
                item = { value: this.igxForOf[i], height: this.individualSizeCache[i] };
5,210,868✔
1285
            }
1286
            const size = dimension === 'height' ?
5,538,244✔
1287
                this.individualSizeCache[i] :
1288
                this._getItemSize(item, dimension);
1289
            sum = arr.reduce(reducer, size);
5,538,244✔
1290
            if (sum < availableSize) {
5,538,244✔
1291
                arr.push(item);
169,736✔
1292
                length = arr.length;
169,736✔
1293
                if (i === this.igxForOf.length - 1) {
169,736✔
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,938✔
1297
                    let prevIndex = this.igxForOf.indexOf(curItem) - 1;
9,938✔
1298
                    while (prevIndex >= 0 && sum <= availableSize) {
9,938✔
1299
                        curItem = dimension === 'height' ? arr[0].value : arr[0];
177✔
1300
                        prevIndex = this.igxForOf.indexOf(curItem) - 1;
177✔
1301
                        const prevItem = this.igxForOf[prevIndex];
177✔
1302
                        const prevSize = dimension === 'height' ?
177✔
1303
                            this.individualSizeCache[prevIndex] :
1304
                            parseInt(prevItem[dimension], 10);
1305
                        sum = arr.reduce(reducer, prevSize);
177✔
1306
                        arr.unshift(prevItem);
177✔
1307
                        length = arr.length;
177✔
1308
                    }
1309
                }
1310
            } else {
1311
                arr.push(item);
5,368,508✔
1312
                length = arr.length + 1;
5,368,508✔
1313
                arr.shift();
5,368,508✔
1314
            }
1315
            if (length > maxLength) {
5,538,244✔
1316
                maxLength = length;
201,400✔
1317
            }
1318
        }
1319
        return maxLength;
42,325✔
1320
    }
1321

1322
    /**
1323
     * @hidden
1324
     */
1325
    protected getIndexAt(left, set) {
1326
        let start = 0;
93,966✔
1327
        let end = set.length - 1;
93,966✔
1328
        if (left === 0) {
93,966✔
1329
            return 0;
91,332✔
1330
        }
1331
        while (start <= end) {
2,634✔
1332
            const midIdx = Math.floor((start + end) / 2);
10,040✔
1333
            const midLeft = set[midIdx];
10,040✔
1334
            const cmp = left - midLeft;
10,040✔
1335
            if (cmp > 0) {
10,040✔
1336
                start = midIdx + 1;
4,540✔
1337
            } else if (cmp < 0) {
5,500✔
1338
                end = midIdx - 1;
5,200✔
1339
            } else {
1340
                return midIdx;
300✔
1341
            }
1342
        }
1343
        return end;
2,334✔
1344
    }
1345

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

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

1387
    protected _recalcOnContainerChange(containerSizeInfo = null) {
×
1388
        const prevChunkSize = this.state.chunkSize;
33,056✔
1389
        this.applyChunkSizeChange();
33,056✔
1390
        this._recalcScrollBarSize(containerSizeInfo);
33,056✔
1391
        if (prevChunkSize !== this.state.chunkSize) {
33,056✔
1392
            this.chunkLoad.emit(this.state);
8,639✔
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,654✔
1402
        this.beforeViewDestroyed.emit(oldElem);
10,654✔
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,654✔
1405
        oldElem.destroy();
10,654✔
1406

1407
        this.state.chunkSize--;
10,654✔
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,113✔
1416
        if (!this.isRemote && !this.igxForOf) {
2,113!
1417
            return;
×
1418
        }
1419

1420
        if (elemIndex >= this.igxForOf.length) {
2,113✔
1421
            elemIndex = this.igxForOf.length - this.state.chunkSize;
5✔
1422
        }
1423
        const input = this.igxForOf[elemIndex];
2,113✔
1424
        const embeddedView = this.dc.instance._vcr.createEmbeddedView(
2,113✔
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,113✔
1430
        this.state.chunkSize++;
2,113✔
1431

1432
        this._zone.run(() => this.cdr.markForCheck());
2,113✔
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();
93,808!
1441
        if (chunkSize > this.state.chunkSize) {
93,808✔
1442
            const diff = chunkSize - this.state.chunkSize;
8,973✔
1443
            for (let i = 0; i < diff; i++) {
8,973✔
1444
                this.addLastElem();
45,836✔
1445
            }
1446
        } else if (chunkSize < this.state.chunkSize) {
84,835✔
1447
            const diff = this.state.chunkSize - chunkSize;
4,326✔
1448
            for (let i = 0; i < diff; i++) {
4,326✔
1449
                this.removeLastElem();
10,654✔
1450
            }
1451
        }
1452
    }
1453

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

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

1467
    protected _updateScrollOffset() {
1468
        let scrollOffset = 0;
96,744✔
1469
        let currentScroll = this.scrollPosition;
96,744✔
1470
        if (this._virtRatio !== 1) {
96,744!
1471
            this._calcVirtualScrollPosition(this.scrollPosition);
×
1472
            currentScroll = this._virtScrollPosition;
×
1473
        }
1474
        const scroll = this.scrollComponent.nativeElement;
96,744✔
1475
        scrollOffset = scroll && this.scrollComponent.size ?
96,744✔
1476
        currentScroll - this.sizesCache[this.state.startIndex] : 0;
1477
        const dir = this.igxForScrollOrientation === 'horizontal' ? 'left' : 'top';
96,744✔
1478
        this.dc.instance._viewContainer.element.nativeElement.style[dir] = -(scrollOffset) + 'px';
96,744✔
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) {
41,687✔
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,274✔
1499
        if (dimension === 'height') {
13,274✔
1500
            return parseFloat(styles['marginTop']) +
12,600✔
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;
3!
1509

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

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

1523
export interface IForOfDataChangeEventArgs extends IForOfDataChangingEventArgs {}
1524

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

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

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

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

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

1556
    /**
1557
     * @hidden
1558
     * @internal
1559
     */
1560
    public override get sizesCache(): number[] {
1561
        if (this.igxForScrollOrientation === 'horizontal') {
979,144✔
1562
            if (this.igxGridForOfUniqueSizeCache || this.syncService.isMaster(this)) {
758,528✔
1563
                return this._sizesCache;
471,943✔
1564
            }
1565
            return this.syncService.sizesCache(this.igxForScrollOrientation);
286,585✔
1566
        } else {
1567
            return this._sizesCache;
220,616✔
1568
        }
1569
    }
1570
    /**
1571
     * @hidden
1572
     * @internal
1573
     */
1574
    public override set sizesCache(value: number[]) {
1575
        this._sizesCache = value;
78,376✔
1576
    }
1577

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

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

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

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

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

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

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

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

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

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

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

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

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

1735
    protected getItemSize(item) {
1736
        let size = 0;
1,099,605✔
1737
        const dimension = this.igxForSizePropName || 'height';
1,099,605!
1738
        if (this.igxForScrollOrientation === 'vertical') {
1,099,605✔
1739
            size = this._getItemSize(item, dimension);
780,486✔
1740
            if (item && item.summaries) {
780,486✔
1741
                size = item.max;
1,574✔
1742
            } else if (item && item.groups && item.height) {
778,912✔
1743
                size = item.height;
23,020✔
1744
            }
1745
        } else {
1746
            size = parseInt(item[dimension], 10) || 0;
319,119✔
1747
        }
1748
        return size;
1,099,605✔
1749
    }
1750

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

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

1781
        const diff = oldSize - newSize;
58,296✔
1782
        return diff;
58,296✔
1783
    }
1784

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

1792
        // When there are more than one removed items the changes are not reliable so those with identity change should be default size.
1793
        let numRemovedItems = 0;
58,296✔
1794
        changes.forEachRemovedItem(() => numRemovedItems++);
84,511✔
1795

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

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

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

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

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

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

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

1877
    /**
1878
     * @hidden
1879
     */
1880
    protected override _calcMaxChunkSize(): number {
1881
        if (this.igxForScrollOrientation === 'horizontal') {
129,637✔
1882
            if (this.syncService.isMaster(this)) {
111,922✔
1883
                return super._calcMaxChunkSize();
32,962✔
1884
            }
1885
            return this.syncService.chunkSize(this.igxForScrollOrientation);
78,960✔
1886
        } else {
1887
            return super._calcMaxChunkSize();
17,715✔
1888
        }
1889

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