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

IgniteUI / igniteui-angular / 12298124880

12 Dec 2024 02:10PM CUT coverage: 91.589% (-0.007%) from 91.596%
12298124880

Pull #15164

github

web-flow
Merge 2aa1c6e11 into 217ea33e2
Pull Request #15164: refactor(themes): update theme preset mixins

12978 of 15214 branches covered (85.3%)

26299 of 28714 relevant lines covered (91.59%)

33965.19 hits per line

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

82.74
/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

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

11

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

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

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

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

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

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

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

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

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

109
            this.grid.navigateTo(0, 0, (obj) => {
×
110
                this.grid.clearCellSelection();
×
111
                obj.target.activate(event);
×
112
            });
113

114
        } else {
115
            super.focusTbody(event);
95✔
116
        }
117
    }
118

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

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

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

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

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

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

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

283
        return { inView: isInView, offset: calcOffset };
31✔
284
    }
285

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

303
    private clearActivation() {
304
        // clear if previous activation exists.
305
        if (this.activeNode && Object.keys(this.activeNode).length) {
521✔
306
            this.activeNode = Object.assign({} as IActiveNode);
46✔
307
        }
308
    }
309

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

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

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

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

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