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

IgniteUI / igniteui-angular / 12312016438

13 Dec 2024 08:16AM CUT coverage: 91.598% (-0.004%) from 91.602%
12312016438

Pull #15166

github

web-flow
Merge 0e0678db7 into 88995e04f
Pull Request #15166: fix(i18n): Change advanced filtering cancel and apply buttons to start with capital letter

12983 of 15222 branches covered (85.29%)

26318 of 28732 relevant lines covered (91.6%)

34018.35 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,
211,155✔
46
        public igxForOf: U,
211,155✔
47
        public index: number,
211,155✔
48
        public count: number
211,155✔
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';
48,610✔
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
     * Sets the px-affixed size of the item along the axis of scrolling.
173
     * For "horizontal" orientation this value is the width of the column and for "vertical" is the height or the row.
174
     * ```html
175
     * <ng-template igxFor let-item [igxForOf]="data" [igxForScrollOrientation]="'horizontal'" [igxForItemSize]="'50px'"></ng-template>
176
     * ```
177
     */
178
    @Input()
179
    public igxForItemSize: any;
180

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

412
    public verticalScrollHandler(event) {
413
        this.onScroll(event);
48✔
414
    }
415

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

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

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

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

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

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

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

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

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

582

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

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

611
        this._bScrollInternal = true;
52✔
612
        this._virtScrollPosition += add;
52✔
613
        this._virtScrollPosition = this._virtScrollPosition > 0 ?
52✔
614
            (this._virtScrollPosition < maxVirtScrollTop ? this._virtScrollPosition : maxVirtScrollTop) :
50✔
615
            0;
616

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

919
        this._zone.onStable.pipe(first()).subscribe(this.recalcUpdateSizes.bind(this));
69✔
920

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

927

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

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

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

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

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

968
        if (diff) {
3,100✔
969
            this.chunkPreload.emit(this.state);
974✔
970
            if (!this.isRemote) {
974✔
971

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1314
    /**
1315
     * @hidden
1316
     */
1317
    protected getIndexAt(left, set) {
1318
        let start = 0;
93,816✔
1319
        let end = set.length - 1;
93,816✔
1320
        if (left === 0) {
93,816✔
1321
            return 0;
91,181✔
1322
        }
1323
        while (start <= end) {
2,635✔
1324
            const midIdx = Math.floor((start + end) / 2);
10,036✔
1325
            const midLeft = set[midIdx];
10,036✔
1326
            const cmp = left - midLeft;
10,036✔
1327
            if (cmp > 0) {
10,036✔
1328
                start = midIdx + 1;
4,540✔
1329
            } else if (cmp < 0) {
5,496✔
1330
                end = midIdx - 1;
5,190✔
1331
            } else {
1332
                return midIdx;
306✔
1333
            }
1334
        }
1335
        return end;
2,329✔
1336
    }
1337

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

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

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

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

1399
        this.state.chunkSize--;
10,568✔
1400
    }
1401

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1543
    /**
1544
     * @hidden
1545
     * @internal
1546
     */
1547
    public override get sizesCache(): number[] {
1548
        if (this.igxForScrollOrientation === 'horizontal') {
951,354✔
1549
            if (this.igxGridForOfUniqueSizeCache || this.syncService.isMaster(this)) {
762,869✔
1550
                return this._sizesCache;
482,066✔
1551
            }
1552
            return this.syncService.sizesCache(this.igxForScrollOrientation);
280,803✔
1553
        } else {
1554
            return this._sizesCache;
188,485✔
1555
        }
1556
    }
1557
    /**
1558
     * @hidden
1559
     * @internal
1560
     */
1561
    public override set sizesCache(value: number[]) {
1562
        this._sizesCache = value;
78,257✔
1563
    }
1564

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1768
        const diff = oldSize - newSize;
56,558✔
1769
        return diff;
56,558✔
1770
    }
1771

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

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

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

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

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

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

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

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

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

1864
    /**
1865
     * @hidden
1866
     */
1867
    protected override _calcMaxChunkSize(): number {
1868
        if (this.igxForScrollOrientation === 'horizontal') {
129,021✔
1869
            if (this.syncService.isMaster(this)) {
111,267✔
1870
                return super._calcMaxChunkSize();
33,874✔
1871
            }
1872
            return this.syncService.chunkSize(this.igxForScrollOrientation);
77,393✔
1873
        } else {
1874
            return super._calcMaxChunkSize();
17,754✔
1875
        }
1876

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