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

IgniteUI / igniteui-angular / 13416627295

19 Feb 2025 03:46PM CUT coverage: 91.615% (+0.02%) from 91.595%
13416627295

Pull #15246

github

web-flow
Merge 2a114cdda into 10ddb05cf
Pull Request #15246: fix(excel-export): Get correct grid column collection from row island…

12987 of 15218 branches covered (85.34%)

3 of 3 new or added lines in 1 file covered. (100.0%)

380 existing lines in 31 files now uncovered.

26385 of 28800 relevant lines covered (91.61%)

34358.69 hits per line

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

92.12
/projects/igniteui-angular/src/lib/grids/grid-navigation.service.ts
1
import { Injectable } from '@angular/core';
2
import { first } from 'rxjs/operators';
3
import { IgxForOfDirective } from '../directives/for-of/for_of.directive';
4
import { GridType } from './common/grid.interface';
5
import {
6
    NAVIGATION_KEYS,
7
    ROW_COLLAPSE_KEYS,
8
    ROW_EXPAND_KEYS,
9
    SUPPORTED_KEYS,
10
    HORIZONTAL_NAV_KEYS,
11
    HEADER_KEYS,
12
    ROW_ADD_KEYS,
13
    PlatformUtil
14
} from '../core/utils';
15
import { GridKeydownTargetType, GridSelectionMode, FilterMode } from './common/enums';
16
import { IActiveNodeChangeEventArgs } from './common/events';
17
import { IgxGridGroupByRowComponent } from './grid/groupby-row.component';
18
import { IMultiRowLayoutNode } from './common/types';
19
import { SortingDirection } from '../data-operations/sorting-strategy';
20
export interface ColumnGroupsCache {
21
    level: number;
22
    visibleIndex: number;
23
}
24
export interface IActiveNode {
25
    gridID?: string;
26
    row: number;
27
    column?: number;
28
    level?: number;
29
    mchCache?: ColumnGroupsCache;
30
    layout?: IMultiRowLayoutNode;
31
}
32

33
/** @hidden */
34
@Injectable()
35
export class IgxGridNavigationService {
2✔
36
    public grid: GridType;
37
    public _activeNode: IActiveNode = {} as IActiveNode;
3,705✔
38
    public lastActiveNode: IActiveNode = {} as IActiveNode;
3,705✔
39
    protected pendingNavigation = false;
3,705✔
40

41
    public get activeNode() {
42
        return this._activeNode;
1,761,375✔
43
    }
44

45
    public set activeNode(value: IActiveNode) {
46
        this._activeNode = value;
271✔
47
    }
48

49
    constructor(protected platform: PlatformUtil) { }
3,705✔
50

51
    public handleNavigation(event: KeyboardEvent) {
52
        const key = event.key.toLowerCase();
723✔
53
        if (NAVIGATION_KEYS.has(key)) {
723✔
54
            event.stopPropagation();
542✔
55
        }
56
        if (this.grid.crudService.cell && NAVIGATION_KEYS.has(key)) {
723✔
57
            return;
6✔
58
        }
59
        if (event.repeat && SUPPORTED_KEYS.has(key) || (key === 'tab' && this.grid.crudService.cell)) {
717!
60
            event.preventDefault();
82✔
61
        }
62
        if (event.repeat) {
717!
63
            setTimeout(() => this.dispatchEvent(event), 1);
×
64
        } else {
65
            this.dispatchEvent(event);
717✔
66
        }
67
    }
68

69
    public dispatchEvent(event: KeyboardEvent) {
70
        const key = event.key.toLowerCase();
723✔
71
        const cellOrRowInEdit = this.grid.crudService.cell || this.grid.crudService.row;
723✔
72
        if (!this.activeNode || !(SUPPORTED_KEYS.has(key) || (key === 'tab' && cellOrRowInEdit))) {
723✔
73
            return;
2✔
74
        }
75
        const shift = event.shiftKey;
721✔
76
        const ctrl = event.ctrlKey;
721✔
77
        if (NAVIGATION_KEYS.has(key) && this.pendingNavigation) {
721!
78
            event.preventDefault();
×
79
            return;
×
80
        }
81

82
        const type = this.isDataRow(this.activeNode.row) ? 'dataCell' :
721✔
83
            this.isDataRow(this.activeNode.row, true) ? 'summaryCell' : 'groupRow';
100✔
84
        if (this.emitKeyDown(type, this.activeNode.row, event)) {
721!
85
            return;
×
86
        }
87
        if (event.altKey) {
721✔
88
            this.handleAlt(key, event);
41✔
89
            return;
41✔
90
        }
91
        if ([' ', 'spacebar', 'space'].indexOf(key) === -1) {
680✔
92
            this.grid.selectionService.keyboardStateOnKeydown(this.activeNode, shift, shift && key === 'tab');
670✔
93
        }
94
        const position = this.getNextPosition(this.activeNode.row, this.activeNode.column, key, shift, ctrl, event);
680✔
95
        if (NAVIGATION_KEYS.has(key)) {
680✔
96
            event.preventDefault();
503✔
97
            this.navigateInBody(position.rowIndex, position.colIndex, (obj) => {
503✔
98
                obj.target.activate(event);
460✔
99
                this.grid.cdr.detectChanges();
460✔
100
            });
101
        }
102
        this.grid.cdr.detectChanges();
680✔
103
    }
104

105
    public summaryNav(event: KeyboardEvent) {
106
        if (this.grid.hasSummarizedColumns) {
30✔
107
            this.horizontalNav(event, event.key.toLowerCase(), this.grid.dataView.length, 'summaryCell');
30✔
108
        }
109
    }
110

111
    public headerNavigation(event: KeyboardEvent) {
112
        const key = event.key.toLowerCase();
133✔
113
        if (!HEADER_KEYS.has(key) || this.activeNode?.row !== -1) {
133✔
114
            return;
21✔
115
        }
116
        event.preventDefault();
112✔
117

118
        const ctrl = event.ctrlKey;
112✔
119
        const shift = event.shiftKey;
112✔
120
        const alt = event.altKey;
112✔
121

122
        this.performHeaderKeyCombination(this.currentActiveColumn, key, shift, ctrl, alt, event);
112✔
123
        if (shift || alt || (ctrl && (key.includes('down') || key.includes('down')))) {
112✔
124
            return;
28✔
125
        }
126
        if (this.grid.hasColumnGroups) {
84✔
127
            this.handleMCHeaderNav(key, ctrl);
36✔
128
        } else {
129
            this.horizontalNav(event, key, -1, 'headerCell');
48✔
130
        }
131
    }
132

133
    public focusTbody(event) {
134
        const gridRows = this.grid.verticalScrollContainer.totalItemCount ?? this.grid.dataView.length;
246✔
135
        if (gridRows < 1) {
246✔
136
            this.activeNode = null;
1✔
137
            return;
1✔
138
        }
139
        if (!this.activeNode || !Object.keys(this.activeNode).length || this.activeNode.row < 0 || this.activeNode.row > gridRows - 1) {
245✔
140
            const hasLastActiveNode = Object.keys(this.lastActiveNode).length;
76✔
141
            const shouldClearSelection = hasLastActiveNode && (this.lastActiveNode.row < 0 || this.lastActiveNode.row > gridRows - 1);
76✔
142
            this.setActiveNode(this.lastActiveNode.row >= 0 && this.lastActiveNode.row < gridRows ?
76✔
143
                this.firstVisibleNode(this.lastActiveNode.row) : this.firstVisibleNode());
144
            if (shouldClearSelection || (this.grid.cellSelection !== GridSelectionMode.multiple)) {
76!
145
                this.grid.clearCellSelection();
×
146
                this.grid.navigateTo(this.activeNode.row, this.activeNode.column, (obj) => {
×
147
                    obj.target?.activate(event);
×
148
                    this.grid.cdr.detectChanges();
×
149
                });
150
            } else {
151
                if (hasLastActiveNode && !this.grid.selectionService.selected(this.lastActiveNode)) {
76✔
152
                    return;
1✔
153
                }
154
                const range = {
75✔
155
                    rowStart: this.activeNode.row, rowEnd: this.activeNode.row,
156
                    columnStart: this.activeNode.column, columnEnd: this.activeNode.column
157
                };
158
                this.grid.selectRange(range);
75✔
159
                this.grid.notifyChanges();
75✔
160
            }
161
        }
162
    }
163

164
    public focusFirstCell(header = true) {
252✔
165
        if ((header || this.grid.dataView.length) && this.activeNode &&
253✔
166
            (this.activeNode.row === -1 || this.activeNode.row === this.grid.dataView.length ||
167
                (!header && !this.grid.hasSummarizedColumns))) {
168
            return;
94✔
169
        }
170
        const shouldScrollIntoView = this.lastActiveNode && (header && this.lastActiveNode.row !== -1) ||
159✔
171
            (!header && this.lastActiveNode.row !== this.grid.dataView.length);
172
        this.setActiveNode(this.firstVisibleNode(header ? -1 : this.grid.dataView.length));
159✔
173
        if (shouldScrollIntoView) {
159✔
174
            this.performHorizontalScrollToCell(this.activeNode.column);
158✔
175
        }
176
    }
177

178
    public isColumnFullyVisible(columnIndex: number) {
179
        if (columnIndex < 0 || this.isColumnPinned(columnIndex, this.forOfDir())) {
1,444✔
180
            return true;
495✔
181
        }
182
        const index = this.getColumnUnpinnedIndex(columnIndex);
949✔
183
        const width = this.forOfDir().getColumnScrollLeft(index + 1) - this.forOfDir().getColumnScrollLeft(index);
949✔
184
        if (this.displayContainerWidth < width && this.displayContainerScrollLeft === this.forOfDir().getColumnScrollLeft(index)) {
949!
185
            return true;
×
186
        }
187
        return this.displayContainerWidth >= this.forOfDir().getColumnScrollLeft(index + 1) - this.displayContainerScrollLeft &&
949✔
188
            this.displayContainerScrollLeft <= this.forOfDir().getColumnScrollLeft(index);
189
    }
190

191
    public shouldPerformHorizontalScroll(visibleColIndex: number, rowIndex = -1) {
485✔
192
        if (visibleColIndex < 0 || visibleColIndex > this.grid.visibleColumns.length - 1) {
1,320✔
193
            return false;
218✔
194
        }
195
        if (rowIndex < 0 || rowIndex > this.grid.dataView.length - 1) {
1,102✔
196
            return !this.isColumnFullyVisible(visibleColIndex);
485✔
197
        }
198
        const row = this.grid.dataView[rowIndex];
617✔
199
        return row.expression || row.detailsData ? false : !this.isColumnFullyVisible(visibleColIndex);
617✔
200
    }
201

202
    public shouldPerformVerticalScroll(targetRowIndex: number, _visibleColIndex: number): boolean {
203
        if (this.grid.isRecordPinnedByViewIndex(targetRowIndex)) {
1,100✔
204
            return false;
14✔
205
        }
206
        const scrollRowIndex = this.grid.hasPinnedRecords && this.grid.isRowPinningToTop ?
1,086✔
207
            targetRowIndex - this.grid.pinnedDataView.length : targetRowIndex;
208
        const targetRow = this.getRowElementByIndex(targetRowIndex);
1,086✔
209
        const rowHeight = this.grid.verticalScrollContainer.getSizeAt(scrollRowIndex);
1,086✔
210
        const containerHeight = this.grid.calcHeight ? Math.ceil(this.grid.calcHeight) : 0;
1,086✔
211
        const endTopOffset = targetRow ? targetRow.offsetTop + rowHeight + this.containerTopOffset : containerHeight + rowHeight;
1,086✔
212
        // this is workaround: endTopOffset - containerHeight > 5 and should be replaced with: containerHeight < endTopOffset
213
        // when the page is zoomed the grid does not scroll the row completely in the view
214
        return !targetRow || targetRow.offsetTop < Math.abs(this.containerTopOffset)
1,086✔
215
            || containerHeight && endTopOffset - containerHeight > 5;
216
    }
217

218
    public performVerticalScrollToCell(rowIndex: number, visibleColIndex = -1, cb?: () => void) {
×
219
        if (!this.shouldPerformVerticalScroll(rowIndex, visibleColIndex)) {
137!
220
            if (cb) {
×
UNCOV
221
                cb();
×
222
            }
UNCOV
223
            return;
×
224
        }
225
        this.pendingNavigation = true;
137✔
226
        // Only for top pinning we need to subtract pinned count because virtualization indexing doesn't count pinned rows.
227
        const scrollRowIndex = this.grid.hasPinnedRecords && this.grid.isRowPinningToTop ?
137✔
228
            rowIndex - this.grid.pinnedDataView.length : rowIndex;
229
        this.grid.verticalScrollContainer.scrollTo(scrollRowIndex);
137✔
230
        this.grid.verticalScrollContainer.chunkLoad
137✔
231
            .pipe(first()).subscribe(() => {
232
                this.pendingNavigation = false;
134✔
233
                if (cb) {
134✔
234
                    cb();
134✔
235
                }
236
            });
237
    }
238

239
    public performHorizontalScrollToCell(visibleColumnIndex: number, cb?: () => void) {
240
        if (this.grid.rowList < 1 && this.grid.summariesRowList.length < 1 && this.grid.hasColumnGroups) {
440✔
241
            let column = this.grid.getColumnByVisibleIndex(visibleColumnIndex);
4✔
242
            while (column.parent) {
4✔
243
                column = column.parent;
5✔
244
            }
245
            visibleColumnIndex = this.forOfDir().igxForOf.indexOf(column);
4✔
246
        }
247
        if (!this.shouldPerformHorizontalScroll(visibleColumnIndex)) {
440✔
248
            return;
326✔
249
        }
250
        this.pendingNavigation = true;
114✔
251
        this.grid.parentVirtDir.chunkLoad
114✔
252
            .pipe(first())
253
            .subscribe(() => {
254
                this.pendingNavigation = false;
114✔
255
                if (cb) {
114✔
256
                    cb();
80✔
257
                }
258
            });
259
        this.forOfDir().scrollTo(this.getColumnUnpinnedIndex(visibleColumnIndex));
114✔
260
    }
261

262
    public isDataRow(rowIndex: number, includeSummary = false) {
2,855✔
263
        let curRow: any;
264

265
        if (rowIndex < 0 || rowIndex > this.grid.dataView.length - 1) {
3,831✔
266
            curRow = this.grid.dataView[rowIndex - this.grid.virtualizationState.startIndex];
35✔
267
            if (!curRow) {
35✔
268
                // if data is remote, record might not be in the view yet.
269
                return this.grid.verticalScrollContainer.isRemote && rowIndex >= 0 && rowIndex <= (this.grid as any).totalItemCount - 1;
35!
270
            }
271
        } else {
272
            curRow = this.grid.dataView[rowIndex];
3,796✔
273
        }
274
        return curRow && !this.grid.isGroupByRecord(curRow) && !this.grid.isDetailRecord(curRow)
3,796✔
275
            && !curRow.childGridsData && (includeSummary || !curRow.summaries);
276
    }
277

278
    public isGroupRow(rowIndex: number): boolean {
279
        if (rowIndex < 0 || rowIndex > this.grid.dataView.length - 1) {
10!
UNCOV
280
            return false;
×
281
        }
282
        const curRow = this.grid.dataView[rowIndex];
10✔
283
        return curRow && this.grid.isGroupByRecord(curRow);
10✔
284
    }
285

286
    public setActiveNode(activeNode: IActiveNode) {
287
        if (!this.isActiveNodeChanged(activeNode)) {
1,573✔
288
            return;
71✔
289
        }
290

291
        if (!this.activeNode) {
1,502✔
292
            this.activeNode = activeNode;
2✔
293
        }
294

295
        Object.assign(this.activeNode, activeNode);
1,502✔
296

297
        const currRow = this.grid.dataView[activeNode.row];
1,502✔
298
        const type: GridKeydownTargetType = activeNode.row < 0 ? 'headerCell' :
1,502✔
299
            this.isDataRow(activeNode.row) ? 'dataCell' :
1,177✔
300
                currRow && this.grid.isGroupByRecord(currRow) ? 'groupRow' :
411✔
301
                    currRow && this.grid.isDetailRecord(currRow) ? 'masterDetailRow' : 'summaryCell';
282✔
302

303
        const args: IActiveNodeChangeEventArgs = {
1,502✔
304
            row: this.activeNode.row,
305
            column: this.activeNode.column,
306
            level: this.activeNode.level,
307
            tag: type
308
        };
309

310
        this.grid.activeNodeChange.emit(args);
1,502✔
311
    }
312

313
    public isActiveNodeChanged(activeNode: IActiveNode) {
314
        let isChanged = false;
1,573✔
315
        const checkInnerProp = (aciveNode: ColumnGroupsCache | IMultiRowLayoutNode, prop) => {
1,573✔
316
            if (!aciveNode) {
212✔
317
                isChanged = true;
2✔
318
                return;
2✔
319
            }
320

321
            props = Object.getOwnPropertyNames(aciveNode);
210✔
322
            for (const propName of props) {
210✔
323
                if (this.activeNode[prop][propName] !== aciveNode[propName]) {
507✔
324
                    isChanged = true;
172✔
325
                }
326
            }
327
        };
328

329
        if (!this.activeNode) {
1,573✔
330
            return isChanged = true;
2✔
331
        }
332

333
        let props = Object.getOwnPropertyNames(activeNode);
1,571✔
334
        for (const propName of props) {
1,571✔
335
            if (!!this.activeNode[propName] && typeof this.activeNode[propName] === 'object') {
4,350✔
336
                checkInnerProp(activeNode[propName], propName);
212✔
337
            } else if (this.activeNode[propName] !== activeNode[propName]) {
4,138✔
338
                isChanged = true;
3,095✔
339
            }
340
        }
341

342
        return isChanged;
1,571✔
343
    }
344

345
    /** Focus the Grid section (header, body, footer) depending on the current activeNode */
346
    public restoreActiveNodeFocus() {
347
        if (!this.activeNode || !Object.keys(this.activeNode).length) {
14✔
348
            return;
7✔
349
        }
350

351
        if (this.activeNode.row >= 0 && this.activeNode.row < this.grid.dataView.length) {
7✔
352
            this.grid.tbody.nativeElement.focus();
7✔
353
        }
354
        if (this.activeNode.row === -1) {
7!
355
            this.grid.theadRow.nativeElement.focus();
×
356
        }
357
        if (this.activeNode.row === this.grid.dataView.length) {
7!
UNCOV
358
            this.grid.tfoot.nativeElement.focus();
×
359
        }
360
    }
361

362
    protected getNextPosition(rowIndex: number, colIndex: number, key: string, shift: boolean, ctrl: boolean, event: KeyboardEvent) {
363
        if (!this.isDataRow(rowIndex, true) && (key.indexOf('down') < 0 || key.indexOf('up') < 0) && ctrl) {
570✔
364
            return { rowIndex, colIndex };
2✔
365
        }
366
        switch (key) {
568!
367
            case 'pagedown':
368
            case 'pageup':
369
                event.preventDefault();
4✔
370
                if (key === 'pagedown') {
4✔
371
                    this.grid.verticalScrollContainer.scrollNextPage();
2✔
372
                } else {
373
                    this.grid.verticalScrollContainer.scrollPrevPage();
2✔
374
                }
375
                const editCell = this.grid.crudService.cell;
4✔
376
                this.grid.verticalScrollContainer.chunkLoad
4✔
377
                    .pipe(first()).subscribe(() => {
378
                        if (editCell && this.grid.rowList.map(r => r.index).indexOf(editCell.rowIndex) < 0) {
4!
UNCOV
379
                            this.grid.tbody.nativeElement.focus({ preventScroll: true });
×
380
                        }
381
                    });
382
                break;
4✔
383
            case 'tab':
384
                this.handleEditing(shift, event);
82✔
385
                break;
82✔
386
            case 'end':
387
                rowIndex = ctrl ? this.findLastDataRowIndex() : this.activeNode.row;
11✔
388
                colIndex = this.lastColumnIndex;
11✔
389
                break;
11✔
390
            case 'home':
391
                rowIndex = ctrl ? this.findFirstDataRowIndex() : this.activeNode.row;
9✔
392
                colIndex = 0;
9✔
393
                break;
9✔
394
            case 'arrowleft':
395
            case 'left':
396
                colIndex = ctrl ? 0 : this.activeNode.column - 1;
55✔
397
                break;
55✔
398
            case 'arrowright':
399
            case 'right':
400
                colIndex = ctrl ? this.lastColumnIndex : this.activeNode.column + 1;
74✔
401
                break;
74✔
402
            case 'arrowup':
403
            case 'up':
404
                if (ctrl && !this.isDataRow(rowIndex) || (this.grid.rowEditable && this.grid.crudService.rowEditingBlocked)) {
102!
405
                    break;
1✔
406
                }
407
                colIndex = this.activeNode.column !== undefined ? this.activeNode.column : 0;
101!
408
                rowIndex = ctrl ? this.findFirstDataRowIndex() : this.activeNode.row - 1;
101✔
409
                break;
101✔
410
            case 'arrowdown':
411
            case 'down':
412
                if ((ctrl && !this.isDataRow(rowIndex)) || (this.grid.rowEditable && this.grid.crudService.rowEditingBlocked)) {
130✔
413
                    break;
1✔
414
                }
415
                colIndex = this.activeNode.column !== undefined ? this.activeNode.column : 0;
129✔
416
                rowIndex = ctrl ? this.findLastDataRowIndex() : this.activeNode.row + 1;
129✔
417
                break;
129✔
418
            case 'enter':
419
            case 'f2':
420
                const cell = this.grid.gridAPI.get_cell_by_visible_index(this.activeNode.row, this.activeNode.column);
63✔
421
                if (!this.isDataRow(rowIndex) || !cell.editable) {
63✔
422
                    break;
1✔
423
                }
424
                this.grid.crudService.enterEditMode(cell, event);
62✔
425
                break;
62✔
426
            case 'escape':
427
            case 'esc':
428
                if (!this.isDataRow(rowIndex)) {
28✔
429
                    break;
1✔
430
                }
431

432
                if (this.grid.crudService.isInCompositionMode) {
27!
UNCOV
433
                    return;
×
434
                }
435

436
                if (this.grid.crudService.cellInEditMode || this.grid.crudService.rowInEditMode) {
27!
437
                    this.grid.crudService.endEdit(false, event);
27✔
438
                    if (this.platform.isEdge) {
27!
UNCOV
439
                        this.grid.cdr.detectChanges();
×
440
                    }
441
                    this.grid.tbody.nativeElement.focus();
27✔
442
                }
443
                break;
27✔
444
            case ' ':
445
            case 'spacebar':
446
            case 'space':
447
                const rowObj = this.grid.gridAPI.get_row_by_index(this.activeNode.row);
10✔
448
                if (this.grid.isRowSelectable && rowObj) {
10✔
449
                    if (this.isDataRow(rowIndex)) {
10✔
450
                        if (rowObj.selected) {
6✔
451
                            this.grid.selectionService.deselectRow(rowObj.key, event);
3✔
452
                        } else {
453
                            this.grid.selectionService.selectRowById(rowObj.key, false, event);
3✔
454
                        }
455
                    }
456
                    if (this.isGroupRow(rowIndex)) {
10✔
457
                        ((rowObj as any) as IgxGridGroupByRowComponent).onGroupSelectorClick(event);
4✔
458
                    }
459
                }
460
                break;
10✔
461
            default:
UNCOV
462
                return;
×
463
        }
464
        return { rowIndex, colIndex };
568✔
465
    }
466

467
    protected horizontalNav(event: KeyboardEvent, key: string, rowIndex: number, tag: GridKeydownTargetType) {
468
        const ctrl = event.ctrlKey;
78✔
469
        if (!HORIZONTAL_NAV_KEYS.has(event.key.toLowerCase())) {
78✔
470
            return;
16✔
471
        }
472
        event.preventDefault();
62✔
473
        this.activeNode.row = rowIndex;
62✔
474
        if (rowIndex > 0) {
62✔
475
            if (this.emitKeyDown('summaryCell', this.activeNode.row, event)) {
28!
UNCOV
476
                return;
×
477
            }
478
        }
479

480
        const newActiveNode = {
62✔
481
            column: this.activeNode.column,
482
            mchCache: {
483
                level: this.activeNode.level,
484
                visibleIndex: this.activeNode.column
485
            }
486
        };
487

488
        if ((key.includes('left') || key === 'home') && this.activeNode.column > 0) {
62✔
489
            newActiveNode.column = ctrl || key === 'home' ? 0 : this.activeNode.column - 1;
22✔
490
        }
491
        if ((key.includes('right') || key === 'end') && this.activeNode.column < this.lastColumnIndex) {
62✔
492
            newActiveNode.column = ctrl || key === 'end' ? this.lastColumnIndex : this.activeNode.column + 1;
37✔
493
        }
494

495
        if (tag === 'headerCell') {
62✔
496
            const column = this.grid.getColumnByVisibleIndex(newActiveNode.column);
34✔
497
            newActiveNode.mchCache.level = column.level;
34✔
498
            newActiveNode.mchCache.visibleIndex = column.visibleIndex;
34✔
499
        }
500

501
        this.setActiveNode({ row: this.activeNode.row, column: newActiveNode.column, mchCache: newActiveNode.mchCache });
62✔
502
        this.performHorizontalScrollToCell(this.activeNode.column);
62✔
503
    }
504

505
    public get lastColumnIndex() {
506
        return Math.max(...this.grid.visibleColumns.map(col => col.visibleIndex));
7,442✔
507
    }
508
    public get displayContainerWidth() {
509
        return Math.round(this.grid.parentVirtDir.dc.instance._viewContainer.element.nativeElement.offsetWidth);
2,282✔
510
    }
511
    public get displayContainerScrollLeft() {
512
        return Math.ceil(this.grid.headerContainer.scrollPosition);
2,050✔
513
    }
514
    public get containerTopOffset() {
515
        return parseInt(this.grid.verticalScrollContainer.dc.instance._viewContainer.element.nativeElement.style.top, 10);
1,910✔
516
    }
517

518
    protected getColumnUnpinnedIndex(visibleColumnIndex: number) {
519
        const column = this.grid.unpinnedColumns.find((col) => !col.columnGroup && col.visibleIndex === visibleColumnIndex);
5,538✔
520
        return this.grid.pinnedColumns.length ? this.grid.unpinnedColumns.filter((c) => !c.columnGroup).indexOf(column) :
1,671✔
521
            visibleColumnIndex;
522
    }
523

524
    protected forOfDir(): IgxForOfDirective<any> {
525
        const forOfDir = this.grid.dataRowList.length > 0 ? this.grid.dataRowList.first.virtDirRow : this.grid.summariesRowList.length ?
6,201✔
526
            this.grid.summariesRowList.first.virtDirRow : this.grid.headerContainer;
527
        return forOfDir as IgxForOfDirective<any>;
6,201✔
528
    }
529

530
    protected handleAlt(key: string, event: KeyboardEvent) {
531
        event.preventDefault();
41✔
532
        // todo TODO ROW
533
        const row = this.grid.gridAPI.get_row_by_index(this.activeNode.row);
41✔
534

535
        if (!(this.isToggleKey(key) || this.isAddKey(key)) || !row) {
41!
UNCOV
536
            return;
×
537
        }
538
        if (this.isAddKey(key)) {
41✔
539
            if (!this.grid.rowEditable) {
2!
UNCOV
540
                console.warn('The grid must be in row edit mode to perform row adding!');
×
UNCOV
541
                return;
×
542
            }
543

544
            if (event.shiftKey && row.treeRow !== undefined) {
2!
UNCOV
545
                this.grid.crudService.enterAddRowMode(row, true, event);
×
546
            } else if (!event.shiftKey) {
2✔
547
                this.grid.crudService.enterAddRowMode(row, false, event);
1✔
548
            }
549
        } else if (!row.expanded && ROW_EXPAND_KEYS.has(key)) {
39✔
550
            if (row.key === undefined) {
17✔
551
                // TODO use expanded row.expanded = !row.expanded;
552
                (row as any).toggle();
4✔
553
            } else {
554
                this.grid.gridAPI.set_row_expansion_state(row.key, true, event);
13✔
555
            }
556
        } else if (row.expanded && ROW_COLLAPSE_KEYS.has(key)) {
22✔
557
            if (row.key === undefined) {
17✔
558
                // TODO use expanded row.expanded = !row.expanded;
559
                (row as any).toggle();
5✔
560
            } else {
561
                this.grid.gridAPI.set_row_expansion_state(row.key, false, event);
12✔
562
            }
563
        }
564
        this.grid.notifyChanges();
41✔
565
    }
566

567
    protected handleEditing(shift: boolean, event: KeyboardEvent) {
568
        const next = shift ? this.grid.getPreviousCell(this.activeNode.row, this.activeNode.column, col => col.editable) :
215✔
569
            this.grid.getNextCell(this.activeNode.row, this.activeNode.column, col => col.editable);
400✔
570
        if (!this.grid.crudService.rowInEditMode && this.isActiveNode(next.rowIndex, next.visibleColumnIndex)) {
82!
UNCOV
571
            this.grid.crudService.endEdit(true, event);
×
UNCOV
572
            this.grid.tbody.nativeElement.focus();
×
UNCOV
573
            return;
×
574
        }
575
        event.preventDefault();
82✔
576
        if ((this.grid.crudService.rowInEditMode && this.grid.rowEditTabs.length) &&
82✔
577
            (this.activeNode.row !== next.rowIndex || this.isActiveNode(next.rowIndex, next.visibleColumnIndex))) {
578
            const args = this.grid.crudService.updateCell(true, event);
11✔
579
            if (args.cancel) {
11!
UNCOV
580
                return;
×
581
            } else if (shift) {
11✔
582
                this.grid.rowEditTabs.last.element.nativeElement.focus();
5✔
583
            } else {
584
                this.grid.rowEditTabs.first.element.nativeElement.focus();
6✔
585
            }
586
            return;
11✔
587
        }
588

589
        if (this.grid.crudService.rowInEditMode && !this.grid.rowEditTabs.length) {
71✔
590
            if (shift && next.rowIndex === this.activeNode.row && next.visibleColumnIndex === this.activeNode.column) {
2✔
591
                next.visibleColumnIndex = this.grid.lastEditableColumnIndex;
1✔
592
            } else if (!shift && next.rowIndex === this.activeNode.row && next.visibleColumnIndex === this.activeNode.column) {
1!
UNCOV
593
                next.visibleColumnIndex = this.grid.firstEditableColumnIndex;
×
594
            } else {
595
                next.rowIndex = this.activeNode.row;
1✔
596
            }
597
        }
598

599
        this.navigateInBody(next.rowIndex, next.visibleColumnIndex, (obj) => {
71✔
600
            obj.target.activate(event);
71✔
601
            this.grid.cdr.detectChanges();
71✔
602
        });
603
    }
604

605
    protected navigateInBody(rowIndex, visibleColIndex, cb: (arg: any) => void = null): void {
×
606
        if (!this.isValidPosition(rowIndex, visibleColIndex) || this.isActiveNode(rowIndex, visibleColIndex)) {
559✔
607
            return;
35✔
608
        }
609
        this.grid.navigateTo(rowIndex, visibleColIndex, cb);
524✔
610
    }
611

612

613
    protected emitKeyDown(type: GridKeydownTargetType, rowIndex, event) {
614
        const row = this.grid.summariesRowList.toArray().concat(this.grid.rowList.toArray()).find(r => r.index === rowIndex);
2,075✔
615
        if (!row) {
749✔
616
            return;
8✔
617
        }
618

619
        const target = type === 'groupRow' ? row :
741✔
620
            type === 'dataCell' ? row.cells?.find(c => c.visibleColumnIndex === this.activeNode.column) :
1,593✔
621
                row.summaryCells?.find(c => c.visibleColumnIndex === this.activeNode.column);
227✔
622
        const keydownArgs = { targetType: type, event, cancel: false, target };
741✔
623
        this.grid.gridKeydown.emit(keydownArgs);
741✔
624
        if (keydownArgs.cancel && type === 'dataCell') {
741!
UNCOV
625
            this.grid.selectionService.clear();
×
UNCOV
626
            this.grid.selectionService.keyboardState.active = true;
×
UNCOV
627
            return keydownArgs.cancel;
×
628
        }
629
    }
630

631
    protected isColumnPinned(columnIndex: number, forOfDir: IgxForOfDirective<any>): boolean {
632
        const horizontalScroll = forOfDir.getScroll();
1,545✔
633
        return (!horizontalScroll.clientWidth || this.grid.getColumnByVisibleIndex(columnIndex)?.pinned);
1,545✔
634
    }
635

636
    protected findFirstDataRowIndex(): number {
637
        return this.grid.dataView.findIndex(rec => !this.grid.isGroupByRecord(rec) && !this.grid.isDetailRecord(rec) && !rec.summaries);
26✔
638
    }
639

640
    protected findLastDataRowIndex(): number {
641
        if ((this.grid as any).totalItemCount) {
23!
UNCOV
642
            return (this.grid as any).totalItemCount - 1;
×
643
        }
644
        let i = this.grid.dataView.length;
23✔
645
        while (i--) {
23✔
646
            if (this.isDataRow(i)) {
27✔
647
                return i;
23✔
648
            }
649
        }
650
    }
651

652
    protected getRowElementByIndex(index) {
653
        if (this.grid.hasDetails) {
1,124✔
654
            const detail = this.grid.nativeElement.querySelector(`[detail="true"][data-rowindex="${index}"]`);
44✔
655
            if (detail) {
44✔
656
                return detail;
10✔
657
            }
658
        }
659
        return this.grid.rowList.toArray().concat(this.grid.summariesRowList.toArray()).find(r => r.index === index)?.nativeElement;
3,866✔
660
    }
661

662
    protected isValidPosition(rowIndex: number, colIndex: number): boolean {
663
        const length = (this.grid as any).totalItemCount ?? this.grid.dataView.length;
443✔
664
        if (rowIndex < 0 || colIndex < 0 || length - 1 < rowIndex || this.lastColumnIndex < colIndex) {
443✔
665
            return false;
10✔
666
        }
667
        return this.activeNode.column !== colIndex && !this.isDataRow(rowIndex, true) ? false : true;
433!
668
    }
669
    protected performHeaderKeyCombination(column, key, shift, ctrl, alt, event) {
670
        let direction = this.grid.sortingExpressions.find(expr => expr.fieldName === column.field)?.dir;
132✔
671
        if (ctrl && key.includes('up') && column.sortable && !column.columnGroup) {
132✔
672
            direction = direction === SortingDirection.Asc ? SortingDirection.None : SortingDirection.Asc;
3✔
673
            this.grid.sort({ fieldName: column.field, dir: direction, ignoreCase: false });
3✔
674
            return;
3✔
675
        }
676
        if (ctrl && key.includes('down') && column.sortable && !column.columnGroup) {
129✔
677
            direction = direction === SortingDirection.Desc ? SortingDirection.None : SortingDirection.Desc;
2✔
678
            this.grid.sort({ fieldName: column.field, dir: direction, ignoreCase: false });
2✔
679
            return;
2✔
680
        }
681
        if (shift && alt && this.isToggleKey(key) && !column.columnGroup && column.groupable) {
127✔
682
            direction = direction || SortingDirection.Asc;
7✔
683
            if (key.includes('right')) {
7✔
684
                (this.grid as any).groupBy({
5✔
685
                    fieldName: column.field,
686
                    dir: direction,
687
                    ignoreCase: column.sortingIgnoreCase,
688
                    strategy: column.sortStrategy,
689
                    groupingComparer: column.groupingComparer,
690
                });
691
            } else {
692
                (this.grid as any).clearGrouping(column.field);
2✔
693
            }
694
            this.activeNode.column = key.includes('right') && (this.grid as any).hideGroupedColumns &&
7✔
695
                column.visibleIndex === this.lastColumnIndex ? this.lastColumnIndex - 1 : this.activeNode.column;
696
            return;
7✔
697
        }
698
        if (alt && (ROW_EXPAND_KEYS.has(key) || ROW_COLLAPSE_KEYS.has(key))) {
120✔
699
            this.handleMCHExpandCollapse(key, column);
6✔
700
            return;
6✔
701
        }
702
        if ([' ', 'spacebar', 'space'].indexOf(key) !== -1) {
114✔
703
            this.handleColumnSelection(column, event);
10✔
704
        }
705
        if (alt && (key === 'l' || key === '¬') && this.grid.allowAdvancedFiltering) {
114!
706
            this.grid.openAdvancedFilteringDialog();
2✔
707
        }
708
        if (ctrl && shift && key === 'l' && this.grid.allowFiltering && !column.columnGroup && column.filterable) {
114✔
709
            if (this.grid.filterMode === FilterMode.excelStyleFilter) {
4✔
710
                const headerEl = this.grid.headerGroups.find(g => g.active).nativeElement;
5✔
711
                this.grid.filteringService.toggleFilterDropdown(headerEl, column);
2✔
712
            } else {
713
                this.performHorizontalScrollToCell(column.visibleIndex);
2✔
714
                this.grid.filteringService.filteredColumn = column;
2✔
715
                this.grid.filteringService.isFilterRowVisible = true;
2✔
716
            }
717
        }
718
    }
719

720
    private firstVisibleNode(rowIndex?) {
721
        const colIndex = this.lastActiveNode.column !== undefined ? this.lastActiveNode.column :
235✔
722
            this.grid.visibleColumns.sort((c1, c2) => c1.visibleIndex - c2.visibleIndex)
1,635✔
723
                .find(c => this.isColumnFullyVisible(c.visibleIndex))?.visibleIndex;
244✔
724
        const column = this.grid.visibleColumns.find((col) => !col.columnLayout && col.visibleIndex === colIndex);
248✔
725
        const rowInd = rowIndex ? rowIndex : this.grid.rowList.find(r => !this.shouldPerformVerticalScroll(r.index, colIndex))?.index;
235✔
726
        const node = {
235✔
727
            row: rowInd ?? 0,
235!
728
            column: column?.visibleIndex ?? 0, level: column?.level ?? 0,
470!
729
            mchCache: column ? { level: column.level, visibleIndex: column.visibleIndex } : {} as ColumnGroupsCache,
235!
730
            layout: column && column.columnLayoutChild ? {
705✔
731
                rowStart: column.rowStart, colStart: column.colStart,
732
                rowEnd: column.rowEnd, colEnd: column.colEnd, columnVisibleIndex: column.visibleIndex
733
            } : null
734
        };
735
        return node;
235✔
736
    }
737

738
    private handleMCHeaderNav(key: string, ctrl: boolean) {
739
        const newHeaderNode: ColumnGroupsCache = {
36✔
740
            visibleIndex: this.activeNode.mchCache.visibleIndex,
741
            level: this.activeNode.mchCache.level
742
        };
743
        const activeCol = this.currentActiveColumn;
36✔
744
        const lastGroupIndex = Math.max(... this.grid.visibleColumns.
36✔
745
            filter(c => c.level <= this.activeNode.level).map(col => col.visibleIndex));
492✔
746
        let nextCol = activeCol;
36✔
747
        if ((key.includes('left') || key === 'home') && this.activeNode.column > 0) {
36✔
748
            const index = ctrl || key === 'home' ? 0 : this.activeNode.column - 1;
9✔
749
            nextCol = this.getNextColumnMCH(index);
9✔
750
            newHeaderNode.visibleIndex = nextCol.visibleIndex;
9✔
751
        }
752
        if ((key.includes('right') || key === 'end') && activeCol.visibleIndex < lastGroupIndex) {
36✔
753
            const nextVIndex = activeCol.children ? Math.max(...activeCol.allChildren.map(c => c.visibleIndex)) + 1 :
13✔
754
                activeCol.visibleIndex + 1;
755
            nextCol = ctrl || key === 'end' ? this.getNextColumnMCH(this.lastColumnIndex) : this.getNextColumnMCH(nextVIndex);
13✔
756
            newHeaderNode.visibleIndex = nextCol.visibleIndex;
13✔
757
        }
758
        if (!ctrl && key.includes('up') && this.activeNode.level > 0) {
36✔
759
            nextCol = activeCol.parent;
3✔
760
            newHeaderNode.level = nextCol.level;
3✔
761
        }
762
        if (!ctrl && key.includes('down') && activeCol.children) {
36✔
763
            nextCol = activeCol.children.find(c => c.visibleIndex === newHeaderNode.visibleIndex) ||
7✔
764
                activeCol.children.toArray().sort((a, b) => b.visibleIndex - a.visibleIndex)
1✔
765
                    .filter(col => col.visibleIndex < newHeaderNode.visibleIndex)[0];
2✔
766
            newHeaderNode.level = nextCol.level;
5✔
767
        }
768

769
        this.setActiveNode({
36✔
770
            row: this.activeNode.row,
771
            column: nextCol.visibleIndex,
772
            level: nextCol.level,
773
            mchCache: newHeaderNode
774
        });
775
        this.performHorizontalScrollToCell(nextCol.visibleIndex);
36✔
776
    }
777

778
    private handleMCHExpandCollapse(key, column) {
779
        if (!column.children || !column.collapsible) {
6✔
780
            return;
4✔
781
        }
782
        if (!column.expanded && ROW_EXPAND_KEYS.has(key)) {
2✔
783
            column.expanded = true;
1✔
784
        } else if (column.expanded && ROW_COLLAPSE_KEYS.has(key)) {
1✔
785
            column.expanded = false;
1✔
786
        }
787
    }
788

789
    private handleColumnSelection(column, event) {
790
        if (!column.selectable || this.grid.columnSelection === GridSelectionMode.none) {
10✔
791
            return;
3✔
792
        }
793
        const clearSelection = this.grid.columnSelection === GridSelectionMode.single;
7✔
794
        const columnsToSelect = !column.children ? [column.field] :
7✔
795
            column.allChildren.filter(c => !c.hidden && c.selectable && !c.columnGroup).map(c => c.field);
8✔
796
        if (column.selected) {
7✔
797
            this.grid.selectionService.deselectColumns(columnsToSelect, event);
3✔
798
        } else {
799
            this.grid.selectionService.selectColumns(columnsToSelect, clearSelection, false, event);
4✔
800
        }
801
    }
802

803
    private getNextColumnMCH(visibleIndex) {
804
        let col = this.grid.getColumnByVisibleIndex(visibleIndex);
22✔
805
        let parent = col.parent;
22✔
806
        while (parent && col.level > this.activeNode.mchCache.level) {
22✔
807
            col = col.parent;
12✔
808
            parent = col.parent;
12✔
809
        }
810
        return col;
22✔
811
    }
812

813
    private get currentActiveColumn() {
814
        return this.grid.visibleColumns.find(c => c.visibleIndex === this.activeNode.column && c.level === this.activeNode.level);
635✔
815
    }
816

817
    private isActiveNode(rIndex: number, cIndex: number): boolean {
818
        return this.activeNode ? this.activeNode.row === rIndex && this.activeNode.column === cIndex : false;
622!
819
    }
820

821
    private isToggleKey(key: string): boolean {
822
        return ROW_COLLAPSE_KEYS.has(key) || ROW_EXPAND_KEYS.has(key);
50✔
823
    }
824

825
    private isAddKey(key: string): boolean {
826
        return ROW_ADD_KEYS.has(key);
43✔
827
    }
828
}
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