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

IgniteUI / igniteui-angular / 6795060062

08 Nov 2023 07:27AM CUT coverage: 92.256% (-0.02%) from 92.277%
6795060062

push

github

web-flow
Merge pull request #13615 from IgniteUI/mkirova/fix-empty-pivot-15.1.x

fix(igxPivotGrid): Add check in case data is empty due to removing al…

15309 of 17987 branches covered (0.0%)

26853 of 29107 relevant lines covered (92.26%)

29749.76 hits per line

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

82.83
/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-grid-navigation.service.ts
1
import { Injectable } from '@angular/core';
2
import { first } from 'rxjs/operators';
3
import { NAVIGATION_KEYS, SUPPORTED_KEYS } from '../../core/utils';
4
import { GridType, IPathSegment, RowType } from '../common/grid.interface';
5
import { IActiveNode, IgxGridNavigationService } from '../grid-navigation.service';
6

2✔
7
@Injectable()
8
export class IgxHierarchicalGridNavigationService extends IgxGridNavigationService {
821✔
9
    protected _pendingNavigation = false;
821✔
10

11

12
    public override dispatchEvent(event: KeyboardEvent) {
47✔
13
        const key = event.key.toLowerCase();
47!
14
        if (!this.activeNode || !(SUPPORTED_KEYS.has(key) || (key === 'tab' && this.grid.crudService.cell)) &&
15
            !this.grid.crudService.rowEditingBlocked && !this.grid.crudService.rowInEditMode) {
×
16
            return;
17
        }
47✔
18

47!
19
        const targetGrid = this.getClosestElemByTag(event.target, 'igx-hierarchical-grid');
×
20
        if (targetGrid !== this.grid.nativeElement) {
21
            return;
47!
22
        }
23

24
        if (this._pendingNavigation && NAVIGATION_KEYS.has(key)) {
25
            // In case focus needs to be moved from one grid to another, however there is a pending scroll operation
×
26
            // which is an async operation, any additional navigation keys should be ignored
×
27
            // untill operation complete.
28
            event.preventDefault();
47✔
29
            return;
30
        }
×
31
        super.dispatchEvent(event);
49✔
32
    }
49✔
33

34
    public override navigateInBody(rowIndex, visibleColIndex, cb: (arg: any) => void = null): void {
15✔
35
        const rec = this.grid.dataView[rowIndex];
15✔
36
        if (rec && this.grid.isChildGridRecord(rec)) {
15✔
37
             // target is child grid
15✔
38
            const virtState = this.grid.verticalScrollContainer.state;
15!
39
             const inView = rowIndex >= virtState.startIndex && rowIndex <= virtState.startIndex + virtState.chunkSize;
15✔
40
             const isNext =  this.activeNode.row < rowIndex;
41
             const targetLayoutIndex = isNext ? null : this.grid.childLayoutKeys.length - 1;
42
             if (inView) {
×
43
                this._moveToChild(rowIndex, visibleColIndex, isNext, targetLayoutIndex, cb);
×
44
            } else {
×
45
                let scrollAmount = this.grid.verticalScrollContainer.getScrollForIndex(rowIndex, !isNext);
×
46
                scrollAmount += isNext ? 1 : -1;
×
47
                this.grid.verticalScrollContainer.getScroll().scrollTop = scrollAmount;
×
48
                this._pendingNavigation = true;
×
49
                this.grid.verticalScrollContainer.chunkLoad.pipe(first()).subscribe(() => {
50
                    this._moveToChild(rowIndex, visibleColIndex, isNext, targetLayoutIndex, cb);
51
                    this._pendingNavigation = false;
15✔
52
                });
53
            }
34✔
54
            return;
34✔
55
        }
56

57
        const isLast = rowIndex === this.grid.dataView.length;
10✔
58
        if ((rowIndex === -1 || isLast) &&
10✔
59
            this.grid.parent !== null) {
4✔
60
            // reached end of child grid
61
            const nextSiblingIndex = this.nextSiblingIndex(isLast);
62
            if (nextSiblingIndex !== null) {
6✔
63
                this.grid.parent.navigation._moveToChild(this.grid.childRow.index, visibleColIndex, isLast, nextSiblingIndex, cb);
64
            } else {
10✔
65
                this._moveToParent(isLast, visibleColIndex, cb);
66
            }
24✔
67
            return;
16✔
68
        }
16✔
69

15✔
70
        if (this.grid.parent) {
15✔
71
            const isNext = this.activeNode && typeof this.activeNode.row === 'number' ? rowIndex > this.activeNode.row : false;
72
            const cbHandler = (args) => {
16!
73
                this._handleScrollInChild(rowIndex, isNext);
×
74
                cb(args);
75
            };
16✔
76
            if (!this.activeNode) {
16✔
77
                this.activeNode = { row: null, column: null };
78
            }
8!
79
            super.navigateInBody(rowIndex, visibleColIndex, cbHandler);
×
80
            return;
81
        }
8✔
82

83
        if (!this.activeNode) {
×
84
            this.activeNode = { row: null, column: null };
139✔
85
        }
139✔
86
        super.navigateInBody(rowIndex, visibleColIndex, cb);
2✔
87
    }
2✔
88

2!
89
    public override shouldPerformVerticalScroll(index, visibleColumnIndex = -1, isNext?) {
2✔
90
        const targetRec = this.grid.dataView[index];
91
        if (this.grid.isChildGridRecord(targetRec)) {
92
            const scrollAmount = this.grid.verticalScrollContainer.getScrollForIndex(index, !isNext);
137✔
93
            const currScroll = this.grid.verticalScrollContainer.getScroll().scrollTop;
94
            const shouldScroll = !isNext ? scrollAmount > currScroll : currScroll < scrollAmount;
95
            return shouldScroll;
96
        } else {
93!
97
            return super.shouldPerformVerticalScroll(index, visibleColumnIndex);
×
98
        }
99
    }
100

101
    public override focusTbody(event) {
×
102
        if (!this.activeNode || this.activeNode.row === null) {
×
103
            this.activeNode = {
×
104
                row: 0,
105
                column: 0
106
            };
107

93✔
108
            this.grid.navigateTo(0, 0, (obj) => {
109
                this.grid.clearCellSelection();
110
                obj.target.activate(event);
111
            });
10✔
112

10✔
113
        } else {
10✔
114
            super.focusTbody(event);
10✔
115
        }
4✔
116
    }
117

118
    protected nextSiblingIndex(isNext) {
6✔
119
        const layoutKey = this.grid.childRow.layout.key;
120
        const layoutIndex = this.grid.parent.childLayoutKeys.indexOf(layoutKey);
121
        const nextIndex = isNext ? layoutIndex + 1 : layoutIndex - 1;
122
        if (nextIndex <= this.grid.parent.childLayoutKeys.length - 1 && nextIndex > -1) {
123
            return nextIndex;
124
        } else {
125
            return null;
126
        }
127
    }
128

129
    /**
31✔
130
     * Handles scrolling in child grid and ensures target child row is in main grid view port.
31✔
131
     *
8✔
132
     * @param rowIndex The row index which should be in view.
8✔
133
     * @param isNext  Optional. Whether we are navigating to next. Used to determine scroll direction.
134
     * @param cb  Optional.Callback function called when operation is complete.
135
     */
136
    protected _handleScrollInChild(rowIndex: number, isNext?: boolean, cb?: () => void) {
23✔
137
        const shouldScroll = this.shouldPerformVerticalScroll(rowIndex, -1, isNext);
138
        if (shouldScroll) {
139
            this.grid.navigation.performVerticalScrollToCell(rowIndex, -1, () => {
140
                this.positionInParent(rowIndex, isNext, cb);
141
            });
142
        } else {
143
            this.positionInParent(rowIndex, isNext, cb);
144
        }
145
    }
146

31✔
147
    /**
31!
148
     *
×
149
     * @param rowIndex Row index that should come in view.
×
150
     * @param isNext  Whether we are navigating to next. Used to determine scroll direction.
151
     * @param cb  Optional.Callback function called when operation is complete.
×
152
     */
153
    protected positionInParent(rowIndex, isNext, cb?: () => void) {
31✔
154
        const row = this.grid.gridAPI.get_row_by_index(rowIndex);
31✔
155
        if (!row) {
156
            if (cb) {
11✔
157
                cb();
11✔
158
            }
11✔
159
            return;
11✔
160
        }
11✔
161
        const positionInfo = this.getPositionInfo(row, isNext);
11✔
162
        if (!positionInfo.inView) {
11✔
163
            // stop event from triggering multiple times before scrolling is complete.
1✔
164
            this._pendingNavigation = true;
165
            const scrollableGrid = isNext ? this.getNextScrollableDown(this.grid) : this.getNextScrollableUp(this.grid);
166
            scrollableGrid.grid.verticalScrollContainer.recalcUpdateSizes();
167
            scrollableGrid.grid.verticalScrollContainer.addScrollTop(positionInfo.offset);
168
            scrollableGrid.grid.verticalScrollContainer.chunkLoad.pipe(first()).subscribe(() => {
20✔
169
                this._pendingNavigation = false;
15✔
170
                if (cb) {
171
                    cb();
172
                }
173
            });
174
        } else {
175
            if (cb) {
176
                cb();
177
            }
178
        }
179
    }
180

20✔
181
    /**
182
     * Moves navigation to child grid.
20✔
183
     *
20✔
184
     * @param parentRowIndex The parent row index, at which the child grid is rendered.
185
     * @param childLayoutIndex Optional. The index of the child row island to which the child grid belongs to. Uses first if not set.
186
     */
187
    protected _moveToChild(parentRowIndex: number, visibleColIndex: number, isNext: boolean, childLayoutIndex?: number,
20✔
188
                            cb?: (arg: any) => void) {
20✔
189
        const ri = typeof childLayoutIndex !== 'number' ?
20✔
190
         this.grid.childLayoutList.first : this.grid.childLayoutList.toArray()[childLayoutIndex];
20✔
191
        const rowId = this.grid.dataView[parentRowIndex].rowID;
192
        const pathSegment: IPathSegment = {
4✔
193
            rowID: rowId,
4✔
194
            rowIslandKey: ri.key
195
        };
16✔
196
        const childGrid =  this.grid.gridAPI.getChildGrid([pathSegment]);
197
        const targetIndex = isNext ? 0 : childGrid.dataView.length - 1;
1✔
198
        const targetRec =  childGrid.dataView[targetIndex];
1✔
199
        if (!targetRec) {
1✔
200
            // if no target rec, then move on in next sibling or parent
1!
201
            childGrid.navigation.navigateInBody(targetIndex, visibleColIndex, cb);
1✔
202
            return;
203
        }
1✔
204
        if (childGrid.isChildGridRecord(targetRec)) {
205
            // if target is a child grid record should move into it.
15✔
206
            this.grid.navigation.activeNode.row = null;
15✔
207
            childGrid.navigation.activeNode = { row: targetIndex, column: this.activeNode.column};
15✔
208
            childGrid.navigation._handleScrollInChild(targetIndex, isNext, () => {
15✔
209
                const targetLayoutIndex = isNext ? 0 : childGrid.childLayoutList.toArray().length - 1;
15✔
210
                childGrid.navigation._moveToChild(targetIndex, visibleColIndex, isNext, targetLayoutIndex, cb);
15✔
211
            });
15✔
212
            return;
15✔
213
        }
15✔
214

215
        const childGridNav =  childGrid.navigation;
216
        this.clearActivation();
217
        const lastVisibleIndex = childGridNav.lastColumnIndex;
218
        const columnIndex = visibleColIndex <= lastVisibleIndex ? visibleColIndex : lastVisibleIndex;
219
        childGridNav.activeNode = { row: targetIndex, column: columnIndex};
220
        childGrid.tbody.nativeElement.focus({preventScroll: true});
221
        this._pendingNavigation = false;
222
        childGrid.navigation._handleScrollInChild(targetIndex, isNext, () => {
6✔
223
            childGrid.navigateTo(targetIndex, columnIndex, cb);
6✔
224
        });
6!
225
    }
×
226

227
    /**
6✔
228
     * Moves navigation back to parent grid.
6✔
229
     *
6✔
230
     * @param rowIndex
6!
231
     */
6✔
232
    protected _moveToParent(isNext: boolean, columnIndex, cb?) {
6✔
233
        const indexInParent = this.grid.childRow.index;
6✔
234
        const hasNextTarget = this.hasNextTarget(this.grid.parent, indexInParent, isNext);
6✔
235
        if (!hasNextTarget) {
6✔
236
            return;
237
        }
6✔
238
        this.clearActivation();
239
        const targetRowIndex =  isNext ? indexInParent + 1 : indexInParent - 1;
240
        const lastVisibleIndex = this.grid.parent.navigation.lastColumnIndex;
241
        const nextColumnIndex = columnIndex <= lastVisibleIndex ? columnIndex : lastVisibleIndex;
242
        this._pendingNavigation = true;
243
        const cbFunc = (args) => {
244
            this._pendingNavigation = false;
245
            cb(args);
246
            args.target.grid.tbody.nativeElement.focus();
247
        };
248
        this.grid.parent.navigation.navigateInBody(targetRowIndex, nextColumnIndex, cbFunc);
31✔
249
    }
31✔
250

1✔
251
    /**
1!
252
     * Gets information on the row position relative to the root grid view port.
1✔
253
     * Returns whether the row is in view and its offset.
254
     *
255
     * @param rowObj
256
     * @param isNext
1✔
257
     */
1✔
258
    protected getPositionInfo(row: RowType, isNext: boolean) {
259
        // XXX: Fix type
31✔
260
        let rowElem = row.nativeElement;
31✔
261
        if ((row as any).layout) {
31✔
262
            const childLayoutKeys = this.grid.childLayoutKeys;
31✔
263
            const riKey = isNext ? childLayoutKeys[0] : childLayoutKeys[childLayoutKeys.length - 1];
264
            const pathSegment: IPathSegment = {
265
                rowID: row.data.rowID,
31✔
266
                rowIslandKey: riKey
31✔
267
            };
31✔
268
            const childGrid =  this.grid.gridAPI.getChildGrid([pathSegment]);
269
            rowElem = childGrid.tfoot.nativeElement;
270
        }
271
        const gridBottom = this._getMinBottom(this.grid);
272
        const diffBottom =
273
        rowElem.getBoundingClientRect().bottom - gridBottom;
274
        const gridTop = this._getMaxTop(this.grid);
275
        const diffTop = rowElem.getBoundingClientRect().bottom -
276
        rowElem.offsetHeight - gridTop;
47✔
277
        // Adding Math.Round because Chrome has some inconsistencies when the page is zoomed
47✔
278
        const isInView = isNext ? Math.round(diffBottom) <= 0 : Math.round(diffTop) >= 0;
181✔
279
        const calcOffset =  isNext ? diffBottom : diffTop;
47✔
280

281
        return { inView: isInView, offset: calcOffset };
134✔
282
    }
283

×
284
    /**
285
     * Gets closest element by its tag name.
286
     *
287
     * @param sourceElem The element from which to start the search.
521✔
288
     * @param targetTag The target element tag name, for which to search.
46✔
289
     */
290
    protected getClosestElemByTag(sourceElem, targetTag) {
291
        let result = sourceElem;
292
        while (result !== null && result.nodeType === 1) {
6✔
293
            if (result.tagName.toLowerCase() === targetTag.toLowerCase()) {
6✔
294
                return result;
6!
295
            }
6✔
296
            result = result.parentNode;
297
        }
298
        return null;
×
299
    }
×
300

×
301
    private clearActivation() {
×
302
        // clear if previous activation exists.
303
        if (this.activeNode && Object.keys(this.activeNode).length) {
×
304
            this.activeNode = Object.assign({} as IActiveNode);
305
        }
306
    }
307

308
    private hasNextTarget(grid: GridType, index: number, isNext: boolean) {
309
        const targetRowIndex =  isNext ? index + 1 : index - 1;
310
        const hasTargetRecord = !!grid.dataView[targetRowIndex];
311
        if (hasTargetRecord) {
312
            return true;
31✔
313
        } else {
31✔
314
            let hasTargetRecordInParent = false;
31✔
315
            if (grid.parent) {
34✔
316
                const indexInParent = grid.childRow.index;
34!
317
                hasTargetRecordInParent = this.hasNextTarget(grid.parent, indexInParent, isNext);
34✔
318
            }
319
            return hasTargetRecordInParent;
31✔
320
        }
321
    }
322

323
    /**
324
     * Gets the max top view in the current grid hierarchy.
325
     *
326
     * @param grid
327
     */
31✔
328
    private _getMaxTop(grid) {
31✔
329
        let currGrid = grid;
31✔
330
        let top = currGrid.tbody.nativeElement.getBoundingClientRect().top;
34✔
331
        while (currGrid.parent) {
34!
332
            currGrid = currGrid.parent;
34✔
333
            const pinnedRowsHeight = currGrid.hasPinnedRecords && currGrid.isRowPinningToTop ? currGrid.pinnedRowHeight : 0;
334
            top = Math.max(top, currGrid.tbody.nativeElement.getBoundingClientRect().top + pinnedRowsHeight);
31✔
335
        }
336
        return top;
337
    }
338

339
    /**
340
     * Gets the min bottom view in the current grid hierarchy.
341
     *
342
     * @param grid
5✔
343
     */
5!
344
    private _getMinBottom(grid) {
×
345
        let currGrid = grid;
346
        let bottom = currGrid.tbody.nativeElement.getBoundingClientRect().bottom;
5✔
347
        while (currGrid.parent) {
5✔
348
            currGrid = currGrid.parent;
5✔
349
            const pinnedRowsHeight = currGrid.hasPinnedRecords && !currGrid.isRowPinningToTop ? currGrid.pinnedRowHeight : 0;
350
            bottom = Math.min(bottom, currGrid.tbody.nativeElement.getBoundingClientRect().bottom - pinnedRowsHeight);
5✔
351
        }
5!
352
        return bottom;
×
353
    }
×
354

×
355
    /**
×
356
     * Finds the next grid that allows scrolling down.
×
357
     *
358
     * @param grid The grid from which to begin the search.
359
     */
5✔
360
    private getNextScrollableDown(grid) {
361
        let currGrid = grid.parent;
362
        if (!currGrid) {
363
            return { grid, prev: null };
364
        }
365
        let scrollTop = currGrid.verticalScrollContainer.scrollPosition;
366
        let scrollHeight = currGrid.verticalScrollContainer.getScroll().scrollHeight;
367
        let nonScrollable = scrollHeight === 0 ||
6✔
368
            Math.round(scrollTop + currGrid.verticalScrollContainer.igxForContainerSize) === scrollHeight;
6!
369
        let prev = grid;
×
370
        while (nonScrollable && currGrid.parent !== null) {
371
            prev = currGrid;
6✔
372
            currGrid = currGrid.parent;
6✔
373
            scrollTop = currGrid.verticalScrollContainer.scrollPosition;
6✔
374
            scrollHeight = currGrid.verticalScrollContainer.getScroll().scrollHeight;
1✔
375
            nonScrollable = scrollHeight === 0 ||
1✔
376
                Math.round(scrollTop + currGrid.verticalScrollContainer.igxForContainerSize) === scrollHeight;
1✔
377
        }
378
        return { grid: currGrid, prev };
6✔
379
    }
380

381
    /**
2✔
382
     * Finds the next grid that allows scrolling up.
383
     *
384
     * @param grid The grid from which to begin the search.
385
     */
386
    private getNextScrollableUp(grid) {
387
        let currGrid = grid.parent;
388
        if (!currGrid) {
389
            return { grid, prev: null };
390
        }
391
        let nonScrollable = currGrid.verticalScrollContainer.scrollPosition === 0;
392
        let prev = grid;
393
        while (nonScrollable && currGrid.parent !== null) {
394
            prev = currGrid;
395
            currGrid = currGrid.parent;
396
            nonScrollable = currGrid.verticalScrollContainer.scrollPosition === 0;
397
        }
398
        return { grid: currGrid, prev };
399
    }
400
}
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