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

IgniteUI / igniteui-angular / 13331632524

14 Feb 2025 02:51PM CUT coverage: 22.015% (-69.6%) from 91.622%
13331632524

Pull #15372

github

web-flow
Merge d52d57714 into bcb78ae0a
Pull Request #15372: chore(*): test ci passing

1990 of 15592 branches covered (12.76%)

431 of 964 new or added lines in 18 files covered. (44.71%)

19956 existing lines in 307 files now uncovered.

6452 of 29307 relevant lines covered (22.02%)

249.17 hits per line

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

9.27
/projects/igniteui-angular/src/lib/grids/selection/selection.service.ts
1
import { EventEmitter, Injectable, NgZone } from '@angular/core';
2
import { Subject } from 'rxjs';
3
import { PlatformUtil } from '../../core/utils';
4
import { FilteringExpressionsTree } from '../../data-operations/filtering-expressions-tree';
5
import { IRowSelectionEventArgs } from '../common/events';
6
import { GridType } from '../common/grid.interface';
7
import {
8
    GridSelectionRange,
9
    IColumnSelectionState,
10
    IMultiRowLayoutNode,
11
    ISelectionKeyboardState,
12
    ISelectionNode,
13
    ISelectionPointerState,
14
    SelectionState
15
} from '../common/types';
16
import { PivotUtil } from '../pivot-grid/pivot-util';
17

18

19
@Injectable()
20
export class IgxGridSelectionService {
2✔
21
    public grid: GridType;
22
    public dragMode = false;
39✔
23
    public activeElement: ISelectionNode | null;
24
    public keyboardState = {} as ISelectionKeyboardState;
39✔
25
    public pointerState = {} as ISelectionPointerState;
39✔
26
    public columnsState = {} as IColumnSelectionState;
39✔
27

28
    public selection = new Map<number, Set<number>>();
39✔
29
    public temp = new Map<number, Set<number>>();
39✔
30
    public rowSelection: Set<any> = new Set<any>();
39✔
31
    public indeterminateRows: Set<any> = new Set<any>();
39✔
32
    public columnSelection: Set<string> = new Set<string>();
39✔
33
    /**
34
     * @hidden @internal
35
     */
36
    public selectedRowsChange = new Subject<any[]>();
39✔
37

38
    /**
39
     * Toggled when a pointerdown event is triggered inside the grid body (cells).
40
     * When `false` the drag select behavior is disabled.
41
     */
42
    private pointerEventInGridBody = false;
39✔
43

44
    private allRowsSelected: boolean;
45
    private _lastSelectedNode: ISelectionNode;
46
    private _ranges: Set<string> = new Set<string>();
39✔
47
    private _selectionRange: Range;
48

49
    /**
50
     * Returns the current selected ranges in the grid from both
51
     * keyboard and pointer interactions
52
     */
53
    public get ranges(): GridSelectionRange[] {
54

55
        // The last action was keyboard + shift selection -> add it
UNCOV
56
        this.addKeyboardRange();
×
57

UNCOV
58
        const ranges = Array.from(this._ranges).map(range => JSON.parse(range));
×
59

60
        // No ranges but we have a focused cell -> add it
UNCOV
61
        if (!ranges.length && this.activeElement && this.grid.isCellSelectable) {
×
UNCOV
62
            ranges.push(this.generateRange(this.activeElement));
×
63
        }
64

UNCOV
65
        return ranges;
×
66
    }
67

68
    public get primaryButton(): boolean {
UNCOV
69
        return this.pointerState.primaryButton;
×
70
    }
71

72
    public set primaryButton(value: boolean) {
UNCOV
73
        this.pointerState.primaryButton = value;
×
74
    }
75

76
    constructor(private zone: NgZone, protected platform: PlatformUtil) {
39✔
77
        this.initPointerState();
39✔
78
        this.initKeyboardState();
39✔
79
        this.initColumnsState();
39✔
80
    }
81

82
    /**
83
     * Resets the keyboard state
84
     */
85
    public initKeyboardState(): void {
86
        this.keyboardState.node = null;
39✔
87
        this.keyboardState.shift = false;
39✔
88
        this.keyboardState.range = null;
39✔
89
        this.keyboardState.active = false;
39✔
90
    }
91

92
    /**
93
     * Resets the pointer state
94
     */
95
    public initPointerState(): void {
96
        this.pointerState.node = null;
39✔
97
        this.pointerState.ctrl = false;
39✔
98
        this.pointerState.shift = false;
39✔
99
        this.pointerState.range = null;
39✔
100
        this.pointerState.primaryButton = true;
39✔
101
    }
102

103
    /**
104
     * Resets the columns state
105
     */
106
    public initColumnsState(): void {
107
        this.columnsState.field = null;
39✔
108
        this.columnsState.range = [];
39✔
109
    }
110

111
    /**
112
     * Adds a single node.
113
     * Single clicks | Ctrl + single clicks on cells is the usual case.
114
     */
115
    public add(node: ISelectionNode, addToRange = true): void {
×
UNCOV
116
        if (this.selection.has(node.row)) {
×
UNCOV
117
            this.selection.get(node.row).add(node.column);
×
118
        } else {
UNCOV
119
            this.selection.set(node.row, new Set<number>()).get(node.row).add(node.column);
×
120
        }
121

UNCOV
122
        if (addToRange) {
×
UNCOV
123
            this._ranges.add(JSON.stringify(this.generateRange(node)));
×
124
        }
125
    }
126

127
    /**
128
     * Adds the active keyboard range selection (if any) to the `ranges` meta.
129
     */
130
    public addKeyboardRange(): void {
UNCOV
131
        if (this.keyboardState.range) {
×
UNCOV
132
            this._ranges.add(JSON.stringify(this.keyboardState.range));
×
133
        }
134
    }
135

136
    public remove(node: ISelectionNode): void {
UNCOV
137
        if (this.selection.has(node.row)) {
×
UNCOV
138
            this.selection.get(node.row).delete(node.column);
×
139
        }
UNCOV
140
        if (this.isActiveNode(node)) {
×
UNCOV
141
            this.activeElement = null;
×
142
        }
UNCOV
143
        this._ranges.delete(JSON.stringify(this.generateRange(node)));
×
144
    }
145

146
    public isInMap(node: ISelectionNode): boolean {
147
        return (this.selection.has(node.row) && this.selection.get(node.row).has(node.column)) ||
19,876!
148
            (this.temp.has(node.row) && this.temp.get(node.row).has(node.column));
149
    }
150

151
    public selected(node: ISelectionNode): boolean {
152
        return (this.isActiveNode(node) && this.grid.isCellSelectable) || this.isInMap(node);
19,876!
153
    }
154

155
    public isActiveNode(node: ISelectionNode): boolean {
156
        if (this.activeElement) {
19,876!
UNCOV
157
            const isActive = this.activeElement.column === node.column && this.activeElement.row === node.row;
×
UNCOV
158
            if (this.grid.hasColumnLayouts) {
×
UNCOV
159
                const layout = this.activeElement.layout;
×
UNCOV
160
                return isActive && this.isActiveLayout(layout, node.layout);
×
161
            }
UNCOV
162
            return isActive;
×
163
        }
164
        return false;
19,876✔
165
    }
166

167
    public isActiveLayout(current: IMultiRowLayoutNode, target: IMultiRowLayoutNode): boolean {
UNCOV
168
        return current.columnVisibleIndex === target.columnVisibleIndex;
×
169
    }
170

171
    public addRangeMeta(node: ISelectionNode, state?: SelectionState): void {
UNCOV
172
        this._ranges.add(JSON.stringify(this.generateRange(node, state)));
×
173
    }
174

175
    public removeRangeMeta(node: ISelectionNode, state?: SelectionState): void {
UNCOV
176
        this._ranges.delete(JSON.stringify(this.generateRange(node, state)));
×
177
    }
178

179
    /**
180
     * Generates a new selection range from the given `node`.
181
     * If `state` is passed instead it will generate the range based on the passed `node`
182
     * and the start node of the `state`.
183
     */
184
    public generateRange(node: ISelectionNode, state?: SelectionState): GridSelectionRange {
UNCOV
185
        this._lastSelectedNode = node;
×
186

UNCOV
187
        if (!state) {
×
UNCOV
188
            return {
×
189
                rowStart: node.row,
190
                rowEnd: node.row,
191
                columnStart: node.column,
192
                columnEnd: node.column
193
            };
194
        }
195

UNCOV
196
        const { row, column } = state.node;
×
UNCOV
197
        const rowStart = Math.min(node.row, row);
×
UNCOV
198
        const rowEnd = Math.max(node.row, row);
×
UNCOV
199
        const columnStart = Math.min(node.column, column);
×
UNCOV
200
        const columnEnd = Math.max(node.column, column);
×
201

UNCOV
202
        return { rowStart, rowEnd, columnStart, columnEnd };
×
203
    }
204

205
    /**
206
     *
207
     */
208
    public keyboardStateOnKeydown(node: ISelectionNode, shift: boolean, shiftTab: boolean): void {
UNCOV
209
        this.keyboardState.active = true;
×
UNCOV
210
        this.initPointerState();
×
UNCOV
211
        this.keyboardState.shift = shift && !shiftTab;
×
UNCOV
212
        if (!this.grid.navigation.isDataRow(node.row)) {
×
UNCOV
213
            return;
×
214
        }
215
        // Kb navigation with shift and no previous node.
216
        // Clear the current selection init the start node.
UNCOV
217
        if (this.keyboardState.shift && !this.keyboardState.node) {
×
UNCOV
218
            this.clear();
×
UNCOV
219
            this.keyboardState.node = Object.assign({}, node);
×
220
        }
221
    }
222

223
    public keyboardStateOnFocus(node: ISelectionNode, emitter: EventEmitter<GridSelectionRange>, dom): void {
UNCOV
224
        const kbState = this.keyboardState;
×
225

226
        // Focus triggered by keyboard navigation
UNCOV
227
        if (kbState.active) {
×
UNCOV
228
            if (this.platform.isChromium) {
×
UNCOV
229
                this._moveSelectionChrome(dom);
×
230
            }
231
            // Start generating a range if shift is hold
UNCOV
232
            if (kbState.shift) {
×
UNCOV
233
                this.dragSelect(node, kbState);
×
UNCOV
234
                kbState.range = this.generateRange(node, kbState);
×
UNCOV
235
                emitter.emit(this.generateRange(node, kbState));
×
UNCOV
236
                return;
×
237
            }
238

UNCOV
239
            this.initKeyboardState();
×
UNCOV
240
            this.clear();
×
UNCOV
241
            this.add(node);
×
242
        }
243
    }
244

245
    public pointerDown(node: ISelectionNode, shift: boolean, ctrl: boolean): void {
UNCOV
246
        this.addKeyboardRange();
×
UNCOV
247
        this.initKeyboardState();
×
UNCOV
248
        this.pointerState.ctrl = ctrl;
×
UNCOV
249
        this.pointerState.shift = shift;
×
UNCOV
250
        this.pointerEventInGridBody = true;
×
UNCOV
251
        this.grid.document.body.addEventListener('pointerup', this.pointerOriginHandler);
×
252

253
        // No ctrl key pressed - no multiple selection
UNCOV
254
        if (!ctrl) {
×
UNCOV
255
            this.clear();
×
256
        }
257

UNCOV
258
        if (shift) {
×
259
            // No previously 'clicked' node. Use the last active node.
UNCOV
260
            if (!this.pointerState.node) {
×
UNCOV
261
                this.pointerState.node = this.activeElement || node;
×
262
            }
UNCOV
263
            this.pointerDownShiftKey(node);
×
UNCOV
264
            this.clearTextSelection();
×
UNCOV
265
            return;
×
266
        }
267

UNCOV
268
        this.removeRangeMeta(node);
×
UNCOV
269
        this.pointerState.node = node;
×
270
    }
271

272
    public pointerDownShiftKey(node: ISelectionNode): void {
UNCOV
273
        this.clear();
×
UNCOV
274
        this.selectRange(node, this.pointerState);
×
275
    }
276

277
    public mergeMap(target: Map<number, Set<number>>, source: Map<number, Set<number>>): void {
UNCOV
278
        const iterator = source.entries();
×
UNCOV
279
        let pair = iterator.next();
×
280
        let key: number;
281
        let value: Set<number>;
282

UNCOV
283
        while (!pair.done) {
×
UNCOV
284
            [key, value] = pair.value;
×
UNCOV
285
            if (target.has(key)) {
×
UNCOV
286
                const newValue = target.get(key);
×
UNCOV
287
                value.forEach(record => newValue.add(record));
×
UNCOV
288
                target.set(key, newValue);
×
289
            } else {
UNCOV
290
                target.set(key, value);
×
291
            }
UNCOV
292
            pair = iterator.next();
×
293
        }
294
    }
295

296
    public pointerEnter(node: ISelectionNode, event: PointerEvent): boolean {
297
        // https://www.w3.org/TR/pointerevents/#the-button-property
UNCOV
298
        this.dragMode = (event.buttons === 1 && (event.button === -1 || event.button === 0)) && this.pointerEventInGridBody;
×
UNCOV
299
        if (!this.dragMode) {
×
UNCOV
300
            return false;
×
301
        }
UNCOV
302
        this.clearTextSelection();
×
303

304
        // If the users triggers a drag-like event by first clicking outside the grid cells
305
        // and then enters in the grid body we may not have a initial pointer starting node.
306
        // Assume the first pointerenter node is where we start.
UNCOV
307
        if (!this.pointerState.node) {
×
308
            this.pointerState.node = node;
×
309
        }
310

UNCOV
311
        if (this.pointerState.ctrl) {
×
UNCOV
312
            this.selectRange(node, this.pointerState, this.temp);
×
313
        } else {
UNCOV
314
            this.dragSelect(node, this.pointerState);
×
315
        }
UNCOV
316
        return true;
×
317
    }
318

319
    public pointerUp(node: ISelectionNode, emitter: EventEmitter<GridSelectionRange>, firedOutsideGrid?: boolean): boolean {
UNCOV
320
        if (this.dragMode || firedOutsideGrid) {
×
UNCOV
321
            this.restoreTextSelection();
×
UNCOV
322
            this.addRangeMeta(node, this.pointerState);
×
UNCOV
323
            this.mergeMap(this.selection, this.temp);
×
UNCOV
324
            this.zone.runTask(() => emitter.emit(this.generateRange(node, this.pointerState)));
×
UNCOV
325
            this.temp.clear();
×
UNCOV
326
            this.dragMode = false;
×
UNCOV
327
            return true;
×
328
        }
329

UNCOV
330
        if (this.pointerState.shift) {
×
UNCOV
331
            this.clearTextSelection();
×
UNCOV
332
            this.restoreTextSelection();
×
UNCOV
333
            this.addRangeMeta(node, this.pointerState);
×
UNCOV
334
            emitter.emit(this.generateRange(node, this.pointerState));
×
UNCOV
335
            return true;
×
336
        }
337

UNCOV
338
        if (this.pointerEventInGridBody && this.isActiveNode(node)) {
×
UNCOV
339
            this.add(node);
×
340
        }
UNCOV
341
        return false;
×
342
    }
343

344
    public selectRange(node: ISelectionNode, state: SelectionState, collection: Map<number, Set<number>> = this.selection): void {
×
UNCOV
345
        if (collection === this.temp) {
×
UNCOV
346
            collection.clear();
×
347
        }
UNCOV
348
        const { rowStart, rowEnd, columnStart, columnEnd } = this.generateRange(node, state);
×
UNCOV
349
        for (let i = rowStart; i <= rowEnd; i++) {
×
UNCOV
350
            for (let j = columnStart as number; j <= (columnEnd as number); j++) {
×
UNCOV
351
                if (collection.has(i)) {
×
UNCOV
352
                    collection.get(i).add(j);
×
353
                } else {
UNCOV
354
                    collection.set(i, new Set<number>()).get(i).add(j);
×
355
                }
356
            }
357
        }
358
    }
359

360
    public dragSelect(node: ISelectionNode, state: SelectionState): void {
UNCOV
361
        if (!this.pointerState.ctrl) {
×
UNCOV
362
            this.selection.clear();
×
363
        }
UNCOV
364
        this.selectRange(node, state);
×
365
    }
366

367
    public clear(clearAcriveEl = false): void {
×
UNCOV
368
        if (clearAcriveEl) {
×
UNCOV
369
            this.activeElement = null;
×
370
        }
UNCOV
371
        this.selection.clear();
×
UNCOV
372
        this.temp.clear();
×
UNCOV
373
        this._ranges.clear();
×
374
    }
375

376
    public clearTextSelection(): void {
UNCOV
377
        const selection = window.getSelection();
×
UNCOV
378
        if (selection.rangeCount) {
×
UNCOV
379
            this._selectionRange = selection.getRangeAt(0);
×
UNCOV
380
            this._selectionRange.collapse(true);
×
UNCOV
381
            selection.removeAllRanges();
×
382
        }
383
    }
384

385
    public restoreTextSelection(): void {
UNCOV
386
        const selection = window.getSelection();
×
UNCOV
387
        if (!selection.rangeCount) {
×
UNCOV
388
            selection.addRange(this._selectionRange || this.grid.document.createRange());
×
389
        }
390
    }
391

392
    public getSelectedRowsData() {
UNCOV
393
        if (this.grid.type === 'pivot') {
×
UNCOV
394
            return this.grid.dataView.filter(r => {
×
UNCOV
395
                const keys = r.dimensions.map(d => PivotUtil.getRecordKey(r, d));
×
UNCOV
396
                return keys.some(k => this.isPivotRowSelected(k));
×
397
            });
398
        }
UNCOV
399
        if (!this.grid.primaryKey) {
×
UNCOV
400
            return Array.from(this.rowSelection);
×
401
        }
UNCOV
402
        const selection = [];
×
UNCOV
403
        const gridDataMap = {};
×
UNCOV
404
        this.grid.gridAPI.get_all_data(true).forEach(row => gridDataMap[this.getRecordKey(row)] = row);
×
UNCOV
405
        this.rowSelection.forEach(rID => {
×
UNCOV
406
            const rData = gridDataMap[rID];
×
UNCOV
407
            const partialRowData = {};
×
UNCOV
408
            partialRowData[this.grid.primaryKey] = rID;
×
UNCOV
409
            selection.push(rData ? rData : partialRowData);
×
410
        });
UNCOV
411
        return selection;
×
412
    }
413

414
    /** Returns array of the selected row id's. */
415
    public getSelectedRows(): Array<any> {
UNCOV
416
        return this.rowSelection.size ? Array.from(this.rowSelection.keys()) : [];
×
417
    }
418

419
    /** Returns array of the rows in indeterminate state. */
420
    public getIndeterminateRows(): Array<any> {
UNCOV
421
        return this.indeterminateRows.size ? Array.from(this.indeterminateRows.keys()) : [];
×
422
    }
423

424
    /** Clears row selection, if filtering is applied clears only selected rows from filtered data. */
425
    public clearRowSelection(event?): void {
UNCOV
426
        const selectedRows = this.getSelectedRowsData();
×
UNCOV
427
        const removedRec = this.isFilteringApplied() ?
×
UNCOV
428
            this.allData.filter(row => this.isRowSelected(this.getRecordKey(row))) : selectedRows;
×
429
        let newSelection;
UNCOV
430
        if (this.grid.primaryKey) {
×
UNCOV
431
            newSelection = this.isFilteringApplied() ? selectedRows.filter(x => {
×
UNCOV
432
                return !removedRec.some(item => item[this.grid.primaryKey] === x[this.grid.primaryKey]);
×
433
            }) : [];
434
        } else {
435
            newSelection = this.isFilteringApplied() ? selectedRows.filter(x => !removedRec.includes(x)) : [];
×
436
        }
UNCOV
437
        this.emitRowSelectionEvent(newSelection, [], removedRec, event, selectedRows);
×
438
    }
439

440
    /** Select all rows, if filtering is applied select only from filtered data. */
441
    public selectAllRows(event?) {
UNCOV
442
        const addedRows = this.allData.filter((row) => !this.rowSelection.has(this.getRecordKey(row)));
×
UNCOV
443
        const selectedRows = this.getSelectedRowsData();
×
UNCOV
444
        const newSelection = this.rowSelection.size ? selectedRows.concat(addedRows) : addedRows;
×
UNCOV
445
        this.indeterminateRows.clear();
×
UNCOV
446
        this.emitRowSelectionEvent(newSelection, addedRows, [], event, selectedRows);
×
447
    }
448

449
    /** Select the specified row and emit event. */
450
    public selectRowById(rowID, clearPrevSelection?, event?): void {
UNCOV
451
        if (!(this.grid.isRowSelectable || this.grid.type === 'pivot') || this.isRowDeleted(rowID)) {
×
UNCOV
452
            return;
×
453
        }
UNCOV
454
        clearPrevSelection = !this.grid.isMultiRowSelectionEnabled || clearPrevSelection;
×
UNCOV
455
        if (this.grid.type === 'pivot') {
×
UNCOV
456
            this.selectPivotRowById(rowID, clearPrevSelection, event);
×
UNCOV
457
            return;
×
458
        }
UNCOV
459
        const selectedRows = this.getSelectedRowsData();
×
UNCOV
460
        const newSelection = clearPrevSelection ? [this.getRowDataById(rowID)] : this.rowSelection.has(rowID) ?
×
461
            selectedRows : [...selectedRows, this.getRowDataById(rowID)];
UNCOV
462
        const removed = clearPrevSelection ? selectedRows : [];
×
UNCOV
463
        this.emitRowSelectionEvent(newSelection, [this.getRowDataById(rowID)], removed, event, selectedRows);
×
464
    }
465

466
    public selectPivotRowById(rowID, clearPrevSelection: boolean, event?): void {
UNCOV
467
        const selectedRows = this.getSelectedRows();
×
UNCOV
468
        const newSelection = clearPrevSelection ? [rowID] : this.rowSelection.has(rowID) ? selectedRows : [...selectedRows, rowID];
×
UNCOV
469
        const added = this.getPivotRowsByIds([rowID]);
×
UNCOV
470
        const removed = this.getPivotRowsByIds(clearPrevSelection ? selectedRows : []);
×
UNCOV
471
        this.emitRowSelectionEventPivotGrid(selectedRows, newSelection, added, removed, event);
×
472
    }
473

474
    /** Deselect the specified row and emit event. */
475
    public deselectRow(rowID, event?): void {
UNCOV
476
        if (!this.isRowSelected(rowID)) {
×
477
            return;
×
478
        }
UNCOV
479
        if(this.grid.type === 'pivot') {
×
UNCOV
480
            this.deselectPivotRowByID(rowID, event);
×
UNCOV
481
            return;
×
482
        }
UNCOV
483
        const selectedRows = this.getSelectedRowsData();
×
UNCOV
484
        const newSelection = selectedRows.filter(r =>  this.getRecordKey(r) !== rowID);
×
UNCOV
485
        if (this.rowSelection.size && this.rowSelection.has(rowID)) {
×
UNCOV
486
            this.emitRowSelectionEvent(newSelection, [], [this.getRowDataById(rowID)], event, selectedRows);
×
487
        }
488
    }
489

490
    public deselectPivotRowByID(rowID, event?) {
UNCOV
491
        if (this.rowSelection.size && this.rowSelection.has(rowID)) {
×
UNCOV
492
            const currSelection = this.getSelectedRows();
×
UNCOV
493
            const newSelection = currSelection.filter(r => r !== rowID);
×
UNCOV
494
            const removed  = this.getPivotRowsByIds([rowID]);
×
UNCOV
495
            this.emitRowSelectionEventPivotGrid(currSelection, newSelection, [], removed, event);
×
496
        }
497
    }
498

499
    private emitRowSelectionEventPivotGrid(currSelection, newSelection, added, removed, event) {
UNCOV
500
        if (this.areEqualCollections(currSelection, newSelection)) {
×
501
            return;
×
502
        }
UNCOV
503
        const currSelectedRows = this.getSelectedRowsData();
×
UNCOV
504
        const args: IRowSelectionEventArgs = {
×
505
            owner: this.grid,
506
            oldSelection: currSelectedRows,
507
            newSelection: this.getPivotRowsByIds(newSelection),
508
            added,
509
            removed,
510
            event,
511
            cancel: false,
512
            allRowsSelected: this.areAllRowSelected(newSelection)
513
        };
UNCOV
514
        this.grid.rowSelectionChanging.emit(args);
×
UNCOV
515
        if (args.cancel) {
×
516
            this.clearHeaderCBState();
×
517
            return;
×
518
        }
UNCOV
519
        this.selectRowsWithNoEvent(newSelection, true);
×
520
    }
521

522
    /** Select the specified rows and emit event. */
523
    public selectRows(keys: any[], clearPrevSelection?: boolean, event?): void {
UNCOV
524
        if (!this.grid.isMultiRowSelectionEnabled) {
×
525
            return;
×
526
        }
527

UNCOV
528
        let rowsToSelect = keys.filter(x => !this.isRowDeleted(x) && !this.rowSelection.has(x));
×
UNCOV
529
        if (!rowsToSelect.length && !clearPrevSelection) {
×
530
            // no valid/additional rows to select and no clear
531
            return;
×
532
        }
533

UNCOV
534
        const selectedRows = this.getSelectedRowsData();
×
UNCOV
535
        rowsToSelect = this.grid.primaryKey ? rowsToSelect.map(r => this.getRowDataById(r)) : rowsToSelect;
×
UNCOV
536
        const newSelection = clearPrevSelection ? rowsToSelect : [...selectedRows, ...rowsToSelect];
×
UNCOV
537
        const keysAsSet = new Set(rowsToSelect);
×
UNCOV
538
        const removed = clearPrevSelection ? selectedRows.filter(x => !keysAsSet.has(x)) : [];
×
UNCOV
539
        this.emitRowSelectionEvent(newSelection, rowsToSelect, removed, event, selectedRows);
×
540
    }
541

542
    public deselectRows(keys: any[], event?): void {
UNCOV
543
        if (!this.rowSelection.size) {
×
544
            return;
×
545
        }
UNCOV
546
        let rowsToDeselect = keys.filter(x => this.rowSelection.has(x));
×
UNCOV
547
        if (!rowsToDeselect.length) {
×
548
            return;
×
549
        }
UNCOV
550
        const selectedRows = this.getSelectedRowsData();
×
UNCOV
551
        rowsToDeselect = this.grid.primaryKey ? rowsToDeselect.map(r => this.getRowDataById(r)) : rowsToDeselect;
×
UNCOV
552
        const keysAsSet = new Set(rowsToDeselect);
×
UNCOV
553
        const newSelection = selectedRows.filter(r => !keysAsSet.has(r));
×
UNCOV
554
        this.emitRowSelectionEvent(newSelection, [], rowsToDeselect, event, selectedRows);
×
555
    }
556

557
    /** Select specified rows. No event is emitted. */
558
    public selectRowsWithNoEvent(rowIDs: any[], clearPrevSelection?): void {
UNCOV
559
        if (clearPrevSelection) {
×
UNCOV
560
            this.rowSelection.clear();
×
561
        }
UNCOV
562
        rowIDs.forEach(rowID => this.rowSelection.add(rowID));
×
UNCOV
563
        this.clearHeaderCBState();
×
UNCOV
564
        this.selectedRowsChange.next(rowIDs);
×
565
    }
566

567
    /** Deselect specified rows. No event is emitted. */
568
    public deselectRowsWithNoEvent(rowIDs: any[]): void {
UNCOV
569
        this.clearHeaderCBState();
×
UNCOV
570
        rowIDs.forEach(rowID => this.rowSelection.delete(rowID));
×
UNCOV
571
        this.selectedRowsChange.next(this.getSelectedRows());
×
572
    }
573

574
    public isRowSelected(rowID): boolean {
575
        return this.rowSelection.size > 0 && this.rowSelection.has(rowID);
12,822!
576
    }
577

578
    public isPivotRowSelected(rowID): boolean {
UNCOV
579
        let contains = false;
×
UNCOV
580
        this.rowSelection.forEach(x => {
×
UNCOV
581
            const correctRowId = rowID.replace(x,'');
×
UNCOV
582
            if (rowID.includes(x) && (correctRowId === '' || correctRowId.startsWith('_')) ) {
×
UNCOV
583
                contains = true;
×
UNCOV
584
                return;
×
585
            }
586
        });
UNCOV
587
        return this.rowSelection.size > 0 && contains;
×
588
    }
589

590
    public isRowInIndeterminateState(rowID): boolean {
UNCOV
591
        return this.indeterminateRows.size > 0 && this.indeterminateRows.has(rowID);
×
592
    }
593

594
    /** Select range from last selected row to the current specified row. */
595
    public selectMultipleRows(rowID, rowData, event?): void {
UNCOV
596
        this.clearHeaderCBState();
×
UNCOV
597
        if (!this.rowSelection.size || this.isRowDeleted(rowID)) {
×
598
            this.selectRowById(rowID);
×
599
            return;
×
600
        }
UNCOV
601
        const gridData = this.allData;
×
UNCOV
602
        const lastRowID = this.getSelectedRows()[this.rowSelection.size - 1];
×
UNCOV
603
        const currIndex = gridData.indexOf(this.getRowDataById(lastRowID));
×
UNCOV
604
        const newIndex = gridData.indexOf(rowData);
×
UNCOV
605
        const rows = gridData.slice(Math.min(currIndex, newIndex), Math.max(currIndex, newIndex) + 1);
×
UNCOV
606
        const currSelection = this.getSelectedRowsData();
×
UNCOV
607
        const added = rows.filter(r => !this.isRowSelected(this.getRecordKey(r)));
×
UNCOV
608
        const newSelection = currSelection.concat(added);
×
UNCOV
609
        this.emitRowSelectionEvent(newSelection, added, [], event, currSelection);
×
610
    }
611

612
    public areAllRowSelected(newSelection?): boolean {
UNCOV
613
        if (!this.grid.data && !newSelection) {
×
614
            return false;
×
615
        }
UNCOV
616
        if (this.allRowsSelected !== undefined && !newSelection) {
×
UNCOV
617
            return this.allRowsSelected;
×
618
        }
UNCOV
619
        const selectedData = new Set(this.getRowIDs(newSelection || this.rowSelection));
×
UNCOV
620
        return this.allRowsSelected = this.allData.length > 0 && this.allData.every(row => selectedData.has(this.getRecordKey(row)));
×
621
    }
622

623
    public hasSomeRowSelected(): boolean {
UNCOV
624
        const filteredData = this.isFilteringApplied() ?
×
UNCOV
625
            this.getRowIDs(this.grid.filteredData).some(rID => this.isRowSelected(rID)) : true;
×
UNCOV
626
        return this.rowSelection.size > 0 && filteredData && !this.areAllRowSelected();
×
627
    }
628

629
    public get filteredSelectedRowIds(): any[] {
UNCOV
630
        return this.isFilteringApplied() ?
×
UNCOV
631
            this.getRowIDs(this.allData).filter(rowID => this.isRowSelected(rowID)) :
×
UNCOV
632
            this.getSelectedRows().filter(rowID => !this.isRowDeleted(rowID));
×
633
    }
634

635
    public emitRowSelectionEvent(newSelection, added, removed, event?, currSelection?): boolean {
UNCOV
636
        currSelection = currSelection ?? this.getSelectedRowsData();
×
UNCOV
637
        if (this.areEqualCollections(currSelection, newSelection)) {
×
UNCOV
638
            return;
×
639
        }
640
        
UNCOV
641
        const args: IRowSelectionEventArgs = {
×
642
            owner: this.grid,
643
            oldSelection: currSelection,
644
            newSelection,
645
            added,
646
            removed,
647
            event,
648
            cancel: false,
649
            allRowsSelected: this.areAllRowSelected(newSelection)
650
        };
651

UNCOV
652
        this.grid.rowSelectionChanging.emit(args);
×
UNCOV
653
        if (args.cancel) {
×
UNCOV
654
            this.clearHeaderCBState();
×
UNCOV
655
            return;
×
656
        }
UNCOV
657
        this.selectRowsWithNoEvent(args.newSelection.map(r => this.getRecordKey(r)), true);
×
658
    }
659

660
    public getPivotRowsByIds(ids: any[]) {
UNCOV
661
        return this.grid.dataView.filter(r => {
×
UNCOV
662
            const keys = r.dimensions.map(d => PivotUtil.getRecordKey(r, d));
×
UNCOV
663
            return new Set(ids.concat(keys)).size < ids.length + keys.length;
×
664
        });
665
    }
666

667
    public getRowDataById(rowID): any {
UNCOV
668
        if (!this.grid.primaryKey) {
×
UNCOV
669
            return rowID;
×
670
        }
UNCOV
671
        const rowIndex = this.getRowIDs(this.grid.gridAPI.get_all_data(true)).indexOf(rowID);
×
UNCOV
672
        return rowIndex < 0 ? rowID : this.grid.gridAPI.get_all_data(true)[rowIndex];
×
673
    }
674

675
    public clearHeaderCBState(): void {
676
        this.allRowsSelected = undefined;
104✔
677
    }
678

679
    public getRowIDs(data): Array<any> {
UNCOV
680
        return this.grid.primaryKey && data.length ? data.map(rec => rec[this.grid.primaryKey]) : data;
×
681
    }
682

683
    public getRecordKey(record) {
UNCOV
684
        return this.grid.primaryKey ? record[this.grid.primaryKey] : record;
×
685
    }
686

687
    /** Clear rowSelection and update checkbox state */
688
    public clearAllSelectedRows(): void {
UNCOV
689
        this.rowSelection.clear();
×
UNCOV
690
        this.indeterminateRows.clear();
×
UNCOV
691
        this.clearHeaderCBState();
×
UNCOV
692
        this.selectedRowsChange.next([]);
×
693
    }
694

695
    /** Returns all data in the grid, with applied filtering and sorting and without deleted rows. */
696
    public get allData(): Array<any> {
697
        let allData;
698
        // V.T. Jan 17th, 2024 #13757 Adding an additional conditional check to take account WITHIN range of groups
UNCOV
699
        if (this.isFilteringApplied() || this.grid.sortingExpressions.length || this.grid.groupingExpressions?.length) {
×
UNCOV
700
            allData = this.grid.pinnedRecordsCount ? this.grid._filteredSortedUnpinnedData : this.grid.filteredSortedData;
×
701
        } else {
UNCOV
702
            allData = this.grid.gridAPI.get_all_data(true);
×
703
        }
UNCOV
704
        return allData.filter(rData => !this.isRowDeleted(this.grid.gridAPI.get_row_id(rData)));
×
705
    }
706

707
    /** Returns array of the selected columns fields. */
708
    public getSelectedColumns(): Array<any> {
UNCOV
709
        return this.columnSelection.size ? Array.from(this.columnSelection.keys()) : [];
×
710
    }
711

712
    public isColumnSelected(field: string): boolean {
713
        return this.columnSelection.size > 0 && this.columnSelection.has(field);
24,548!
714
    }
715

716
    /** Select the specified column and emit event. */
717
    public selectColumn(field: string, clearPrevSelection?, selectColumnsRange?, event?): void {
UNCOV
718
        const stateColumn = this.columnsState.field ? this.grid.getColumnByName(this.columnsState.field) : null;
×
UNCOV
719
        if (!event || !stateColumn || stateColumn.visibleIndex < 0 || !selectColumnsRange) {
×
UNCOV
720
            this.columnsState.field = field;
×
UNCOV
721
            this.columnsState.range = [];
×
722

UNCOV
723
            const newSelection = clearPrevSelection ? [field] : this.getSelectedColumns().indexOf(field) !== -1 ?
×
724
                this.getSelectedColumns() : [...this.getSelectedColumns(), field];
UNCOV
725
            const removed = clearPrevSelection ? this.getSelectedColumns().filter(colField => colField !== field) : [];
×
UNCOV
726
            const added = this.isColumnSelected(field) ? [] : [field];
×
UNCOV
727
            this.emitColumnSelectionEvent(newSelection, added, removed, event);
×
UNCOV
728
        } else if (selectColumnsRange) {
×
UNCOV
729
            this.selectColumnsRange(field, event);
×
730
        }
731
    }
732

733
    /** Select specified columns. And emit event. */
734
    public selectColumns(fields: string[], clearPrevSelection?, selectColumnsRange?, event?): void {
UNCOV
735
        const columns = fields.map(f => this.grid.getColumnByName(f)).sort((a, b) => a.visibleIndex - b.visibleIndex);
×
UNCOV
736
        const stateColumn = this.columnsState.field ? this.grid.getColumnByName(this.columnsState.field) : null;
×
UNCOV
737
        if (!stateColumn || stateColumn.visibleIndex < 0 || !selectColumnsRange) {
×
UNCOV
738
            this.columnsState.field = columns[0] ? columns[0].field : null;
×
UNCOV
739
            this.columnsState.range = [];
×
740

UNCOV
741
            const added = fields.filter(colField => !this.isColumnSelected(colField));
×
UNCOV
742
            const removed = clearPrevSelection ? this.getSelectedColumns().filter(colField => fields.indexOf(colField) === -1) : [];
×
UNCOV
743
            const newSelection = clearPrevSelection ? fields : this.getSelectedColumns().concat(added);
×
744

UNCOV
745
            this.emitColumnSelectionEvent(newSelection, added, removed, event);
×
746
        } else {
747
            const filedStart = stateColumn.visibleIndex >
×
748
                columns[columns.length - 1].visibleIndex ? columns[0].field : columns[columns.length - 1].field;
749
            this.selectColumnsRange(filedStart, event);
×
750
        }
751
    }
752

753
    /** Select range from last clicked column to the current specified column. */
754
    public selectColumnsRange(field: string, event): void {
UNCOV
755
        const currIndex = this.grid.getColumnByName(this.columnsState.field).visibleIndex;
×
UNCOV
756
        const newIndex = this.grid.columnToVisibleIndex(field);
×
UNCOV
757
        const columnsFields = this.grid.visibleColumns
×
UNCOV
758
            .filter(c => !c.columnGroup)
×
UNCOV
759
            .sort((a, b) => a.visibleIndex - b.visibleIndex)
×
760
            .slice(Math.min(currIndex, newIndex), Math.max(currIndex, newIndex) + 1)
UNCOV
761
            .filter(col => col.selectable).map(col => col.field);
×
UNCOV
762
        const removed = [];
×
UNCOV
763
        const oldAdded = [];
×
UNCOV
764
        const added = columnsFields.filter(colField => !this.isColumnSelected(colField));
×
UNCOV
765
        this.columnsState.range.forEach(f => {
×
UNCOV
766
            if (columnsFields.indexOf(f) === -1) {
×
UNCOV
767
                removed.push(f);
×
768
            } else {
769
                oldAdded.push(f);
×
770
            }
771
        });
UNCOV
772
        this.columnsState.range = columnsFields.filter(colField => !this.isColumnSelected(colField) || oldAdded.indexOf(colField) > -1);
×
UNCOV
773
        const newSelection = this.getSelectedColumns().concat(added).filter(c => removed.indexOf(c) === -1);
×
UNCOV
774
        this.emitColumnSelectionEvent(newSelection, added, removed, event);
×
775
    }
776

777
    /** Select specified columns. No event is emitted. */
778
    public selectColumnsWithNoEvent(fields: string[], clearPrevSelection?): void {
UNCOV
779
        if (clearPrevSelection) {
×
UNCOV
780
            this.columnSelection.clear();
×
781
        }
UNCOV
782
        fields.forEach(field => {
×
UNCOV
783
            this.columnSelection.add(field);
×
784
        });
785
    }
786

787
    /** Deselect the specified column and emit event. */
788
    public deselectColumn(field: string, event?): void {
UNCOV
789
        this.initColumnsState();
×
UNCOV
790
        const newSelection = this.getSelectedColumns().filter(c => c !== field);
×
UNCOV
791
        this.emitColumnSelectionEvent(newSelection, [], [field], event);
×
792
    }
793

794
    /** Deselect specified columns. No event is emitted. */
795
    public deselectColumnsWithNoEvent(fields: string[]): void {
796
        fields.forEach(field => this.columnSelection.delete(field));
6✔
797
    }
798

799
    /** Deselect specified columns. And emit event. */
800
    public deselectColumns(fields: string[], event?): void {
UNCOV
801
        const removed = this.getSelectedColumns().filter(colField => fields.indexOf(colField) > -1);
×
UNCOV
802
        const newSelection = this.getSelectedColumns().filter(colField => fields.indexOf(colField) === -1);
×
803

UNCOV
804
        this.emitColumnSelectionEvent(newSelection, [], removed, event);
×
805
    }
806

807
    public emitColumnSelectionEvent(newSelection, added, removed, event?): boolean {
UNCOV
808
        const currSelection = this.getSelectedColumns();
×
UNCOV
809
        if (this.areEqualCollections(currSelection, newSelection)) {
×
810
            return;
×
811
        }
812

UNCOV
813
        const args = {
×
814
            oldSelection: currSelection, newSelection,
815
            added, removed, event, cancel: false
816
        };
UNCOV
817
        this.grid.columnSelectionChanging.emit(args);
×
UNCOV
818
        if (args.cancel) {
×
UNCOV
819
            return;
×
820
        }
UNCOV
821
        this.selectColumnsWithNoEvent(args.newSelection, true);
×
822
    }
823

824
    /** Clear columnSelection */
825
    public clearAllSelectedColumns(): void {
UNCOV
826
        this.columnSelection.clear();
×
827
    }
828

829
    protected areEqualCollections(first, second): boolean {
UNCOV
830
        return first.length === second.length && new Set(first.concat(second)).size === first.length;
×
831
    }
832

833
    /**
834
     * (╯°□°)╯︵ ┻━┻
835
     * Chrome and Chromium don't care about the active
836
     * range after keyboard navigation, thus this.
837
     */
838
    private _moveSelectionChrome(node: Node) {
UNCOV
839
        const selection = window.getSelection();
×
UNCOV
840
        selection.removeAllRanges();
×
UNCOV
841
        const range = new Range();
×
UNCOV
842
        range.selectNode(node);
×
UNCOV
843
        range.collapse(true);
×
UNCOV
844
        selection.addRange(range);
×
845
    }
846

847
    private isFilteringApplied(): boolean {
UNCOV
848
        return !FilteringExpressionsTree.empty(this.grid.filteringExpressionsTree) ||
×
849
            !FilteringExpressionsTree.empty(this.grid.advancedFilteringExpressionsTree);
850
    }
851

852
    private isRowDeleted(rowID): boolean {
UNCOV
853
        return this.grid.gridAPI.row_deleted_transaction(rowID);
×
854
    }
855

856
    private pointerOriginHandler = (event) => {
39✔
UNCOV
857
        this.pointerEventInGridBody = false;
×
UNCOV
858
        this.grid.document.body.removeEventListener('pointerup', this.pointerOriginHandler);
×
859

UNCOV
860
        const targetTagName = event.target.tagName.toLowerCase();
×
UNCOV
861
        if (targetTagName !== 'igx-grid-cell' && targetTagName !== 'igx-tree-grid-cell') {
×
UNCOV
862
            this.pointerUp(this._lastSelectedNode, this.grid.rangeSelected, true);
×
863
        }
864
    };
865
}
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