• 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

2.67
/projects/igniteui-angular/src/lib/tree/tree-selection.service.ts
1
import { Injectable } from '@angular/core';
2
import { IgxTree, IgxTreeNode, IgxTreeSelectionType, ITreeNodeSelectionEvent } from './common';
3

4
/** A collection containing the nodes affected in the selection as well as their direct parents */
5
interface CascadeSelectionNodeCollection {
6
    nodes: Set<IgxTreeNode<any>>;
7
    parents: Set<IgxTreeNode<any>>;
8
}
9

10
/** @hidden @internal */
11
@Injectable()
12
export class IgxTreeSelectionService {
2✔
13
    private tree: IgxTree;
14
    private nodeSelection: Set<IgxTreeNode<any>> = new Set<IgxTreeNode<any>>();
4✔
15
    private indeterminateNodes: Set<IgxTreeNode<any>> = new Set<IgxTreeNode<any>>();
4✔
16

17
    private nodesToBeSelected: Set<IgxTreeNode<any>>;
18
    private nodesToBeIndeterminate: Set<IgxTreeNode<any>>;
19

20
    public register(tree: IgxTree) {
21
        this.tree = tree;
4✔
22
    }
23

24
    /** Select range from last selected node to the current specified node. */
25
    public selectMultipleNodes(node: IgxTreeNode<any>, event?: Event): void {
UNCOV
26
        if (!this.nodeSelection.size) {
×
27
            this.selectNode(node);
×
28
            return;
×
29
        }
UNCOV
30
        const lastSelectedNodeIndex = this.tree.nodes.toArray().indexOf(this.getSelectedNodes()[this.nodeSelection.size - 1]);
×
UNCOV
31
        const currentNodeIndex = this.tree.nodes.toArray().indexOf(node);
×
UNCOV
32
        const nodes = this.tree.nodes.toArray().slice(Math.min(currentNodeIndex, lastSelectedNodeIndex),
×
33
            Math.max(currentNodeIndex, lastSelectedNodeIndex) + 1);
34

UNCOV
35
        const added = nodes.filter(_node => !this.isNodeSelected(_node));
×
UNCOV
36
        const newSelection = this.getSelectedNodes().concat(added);
×
UNCOV
37
        this.emitNodeSelectionEvent(newSelection, added, [], event);
×
38
    }
39

40
    /** Select the specified node and emit event. */
41
    public selectNode(node: IgxTreeNode<any>, event?: Event): void {
UNCOV
42
        if (this.tree.selection === IgxTreeSelectionType.None) {
×
UNCOV
43
            return;
×
44
        }
UNCOV
45
        this.emitNodeSelectionEvent([...this.getSelectedNodes(), node], [node], [], event);
×
46
    }
47

48
    /** Deselect the specified node and emit event. */
49
    public deselectNode(node: IgxTreeNode<any>, event?: Event): void {
UNCOV
50
        const newSelection = this.getSelectedNodes().filter(r => r !== node);
×
UNCOV
51
        this.emitNodeSelectionEvent(newSelection, [], [node], event);
×
52
    }
53

54
    /** Clears node selection */
55
    public clearNodesSelection(): void {
UNCOV
56
        this.nodeSelection.clear();
×
UNCOV
57
        this.indeterminateNodes.clear();
×
58
    }
59

60
    public isNodeSelected(node: IgxTreeNode<any>): boolean {
UNCOV
61
        return this.nodeSelection.has(node);
×
62
    }
63

64
    public isNodeIndeterminate(node: IgxTreeNode<any>): boolean {
UNCOV
65
        return this.indeterminateNodes.has(node);
×
66
    }
67

68
    /** Select specified nodes. No event is emitted. */
69
    public selectNodesWithNoEvent(nodes: IgxTreeNode<any>[], clearPrevSelection = false, shouldEmit = true): void {
×
UNCOV
70
        if (this.tree && this.tree.selection === IgxTreeSelectionType.Cascading) {
×
UNCOV
71
            this.cascadeSelectNodesWithNoEvent(nodes, clearPrevSelection);
×
UNCOV
72
            return;
×
73
        }
74

UNCOV
75
        const oldSelection = this.getSelectedNodes();
×
76

UNCOV
77
        if (clearPrevSelection) {
×
UNCOV
78
            this.nodeSelection.clear();
×
79
        }
UNCOV
80
        nodes.forEach(node => this.nodeSelection.add(node));
×
81

UNCOV
82
        if (shouldEmit) {
×
UNCOV
83
            this.emitSelectedChangeEvent(oldSelection);
×
84
        }
85
    }
86

87
    /** Deselect specified nodes. No event is emitted. */
88
    public deselectNodesWithNoEvent(nodes?: IgxTreeNode<any>[], shouldEmit = true): void {
×
UNCOV
89
        const oldSelection = this.getSelectedNodes();
×
90

UNCOV
91
        if (!nodes) {
×
UNCOV
92
            this.nodeSelection.clear();
×
UNCOV
93
        } else if (this.tree && this.tree.selection === IgxTreeSelectionType.Cascading) {
×
UNCOV
94
            this.cascadeDeselectNodesWithNoEvent(nodes);
×
95
        } else {
UNCOV
96
            nodes.forEach(node => this.nodeSelection.delete(node));
×
97
        }
98

UNCOV
99
        if (shouldEmit) {
×
UNCOV
100
            this.emitSelectedChangeEvent(oldSelection);
×
101
        }
102
    }
103

104
    /** Called on `node.ngOnDestroy` to ensure state is correct after node is removed */
105
    public ensureStateOnNodeDelete(node: IgxTreeNode<any>): void {
UNCOV
106
        if (this.tree?.selection !== IgxTreeSelectionType.Cascading) {
×
UNCOV
107
            return;
×
108
        }
UNCOV
109
        requestAnimationFrame(() => {
×
UNCOV
110
            if (this.isNodeSelected(node)) {
×
111
                // node is destroyed, do not emit event
UNCOV
112
                this.deselectNodesWithNoEvent([node], false);
×
113
            } else {
UNCOV
114
                if (!node.parentNode) {
×
UNCOV
115
                    return;
×
116
                }
UNCOV
117
                const assitantLeafNode = node.parentNode?.allChildren.find(e => !e._children?.length);
×
UNCOV
118
                if (!assitantLeafNode) {
×
UNCOV
119
                    return;
×
120
                }
UNCOV
121
                this.retriggerNodeState(assitantLeafNode);
×
122
            }
123
        });
124
    }
125

126
    /** Retriggers a node's selection state */
127
    private retriggerNodeState(node: IgxTreeNode<any>): void {
UNCOV
128
        if (node.selected) {
×
UNCOV
129
            this.nodeSelection.delete(node);
×
UNCOV
130
            this.selectNodesWithNoEvent([node], false, false);
×
131
        } else {
UNCOV
132
            this.nodeSelection.add(node);
×
UNCOV
133
            this.deselectNodesWithNoEvent([node], false);
×
134
        }
135
    }
136

137
    /** Returns array of the selected nodes. */
138
    private getSelectedNodes(): IgxTreeNode<any>[] {
UNCOV
139
        return this.nodeSelection.size ? Array.from(this.nodeSelection) : [];
×
140
    }
141

142
    /** Returns array of the nodes in indeterminate state. */
143
    private getIndeterminateNodes(): IgxTreeNode<any>[] {
UNCOV
144
        return this.indeterminateNodes.size ? Array.from(this.indeterminateNodes) : [];
×
145
    }
146

147
    private emitNodeSelectionEvent(
148
        newSelection: IgxTreeNode<any>[], added: IgxTreeNode<any>[], removed: IgxTreeNode<any>[], event: Event
149
    ): boolean {
UNCOV
150
        if (this.tree.selection === IgxTreeSelectionType.Cascading) {
×
UNCOV
151
            this.emitCascadeNodeSelectionEvent(newSelection, added, removed, event);
×
UNCOV
152
            return;
×
153
        }
UNCOV
154
        const currSelection = this.getSelectedNodes();
×
UNCOV
155
        if (this.areEqualCollections(currSelection, newSelection)) {
×
UNCOV
156
            return;
×
157
        }
158

UNCOV
159
        const args: ITreeNodeSelectionEvent = {
×
160
            oldSelection: currSelection, newSelection,
161
            added, removed, event, cancel: false, owner: this.tree
162
        };
UNCOV
163
        this.tree.nodeSelection.emit(args);
×
UNCOV
164
        if (args.cancel) {
×
UNCOV
165
            return;
×
166
        }
UNCOV
167
        this.selectNodesWithNoEvent(args.newSelection, true);
×
168
    }
169

170
    private areEqualCollections(first: IgxTreeNode<any>[], second: IgxTreeNode<any>[]): boolean {
UNCOV
171
        return first.length === second.length && new Set(first.concat(second)).size === first.length;
×
172
    }
173

174
    private cascadeSelectNodesWithNoEvent(nodes?: IgxTreeNode<any>[], clearPrevSelection = false): void {
×
UNCOV
175
        const oldSelection = this.getSelectedNodes();
×
176

UNCOV
177
        if (clearPrevSelection) {
×
UNCOV
178
            this.indeterminateNodes.clear();
×
UNCOV
179
            this.nodeSelection.clear();
×
UNCOV
180
            this.calculateNodesNewSelectionState({ added: nodes, removed: [] });
×
181
        } else {
UNCOV
182
            const newSelection = [...oldSelection, ...nodes];
×
UNCOV
183
            const args: Partial<ITreeNodeSelectionEvent> = { oldSelection, newSelection };
×
184

185
            // retrieve only the rows without their parents/children which has to be added to the selection
UNCOV
186
            this.populateAddRemoveArgs(args);
×
187

UNCOV
188
            this.calculateNodesNewSelectionState(args);
×
189
        }
UNCOV
190
        this.nodeSelection = new Set(this.nodesToBeSelected);
×
UNCOV
191
        this.indeterminateNodes = new Set(this.nodesToBeIndeterminate);
×
192

UNCOV
193
        this.emitSelectedChangeEvent(oldSelection);
×
194
    }
195

196
    private cascadeDeselectNodesWithNoEvent(nodes: IgxTreeNode<any>[]): void {
UNCOV
197
        const args = { added: [], removed: nodes };
×
UNCOV
198
        this.calculateNodesNewSelectionState(args);
×
199

UNCOV
200
        this.nodeSelection = new Set<IgxTreeNode<any>>(this.nodesToBeSelected);
×
UNCOV
201
        this.indeterminateNodes = new Set<IgxTreeNode<any>>(this.nodesToBeIndeterminate);
×
202
    }
203

204
    /**
205
     * populates the nodesToBeSelected and nodesToBeIndeterminate sets
206
     * with the nodes which will be eventually in selected/indeterminate state
207
     */
208
    private calculateNodesNewSelectionState(args: Partial<ITreeNodeSelectionEvent>): void {
UNCOV
209
        this.nodesToBeSelected = new Set<IgxTreeNode<any>>(args.oldSelection ? args.oldSelection : this.getSelectedNodes());
×
UNCOV
210
        this.nodesToBeIndeterminate = new Set<IgxTreeNode<any>>(this.getIndeterminateNodes());
×
211

UNCOV
212
        this.cascadeSelectionState(args.removed, false);
×
UNCOV
213
        this.cascadeSelectionState(args.added, true);
×
214
    }
215

216
    /** Ensures proper selection state for all predescessors and descendants during a selection event */
217
    private cascadeSelectionState(nodes: IgxTreeNode<any>[], selected: boolean): void {
UNCOV
218
        if (!nodes || nodes.length === 0) {
×
UNCOV
219
            return;
×
220
        }
221

UNCOV
222
        if (nodes && nodes.length > 0) {
×
UNCOV
223
            const nodeCollection: CascadeSelectionNodeCollection = this.getCascadingNodeCollection(nodes);
×
224

UNCOV
225
            nodeCollection.nodes.forEach(node => {
×
UNCOV
226
                if (selected) {
×
UNCOV
227
                    this.nodesToBeSelected.add(node);
×
228
                } else {
UNCOV
229
                    this.nodesToBeSelected.delete(node);
×
230
                }
UNCOV
231
                this.nodesToBeIndeterminate.delete(node);
×
232
            });
233

UNCOV
234
            Array.from(nodeCollection.parents).forEach((parent) => {
×
UNCOV
235
                this.handleParentSelectionState(parent);
×
236
            });
237
        }
238
    }
239

240
    private emitCascadeNodeSelectionEvent(newSelection, added, removed, event?): boolean {
UNCOV
241
        const currSelection = this.getSelectedNodes();
×
UNCOV
242
        if (this.areEqualCollections(currSelection, newSelection)) {
×
UNCOV
243
            return;
×
244
        }
245

UNCOV
246
        const args: ITreeNodeSelectionEvent = {
×
247
            oldSelection: currSelection, newSelection,
248
            added, removed, event, cancel: false, owner: this.tree
249
        };
250

UNCOV
251
        this.calculateNodesNewSelectionState(args);
×
252

UNCOV
253
        args.newSelection = Array.from(this.nodesToBeSelected);
×
254

255
        // retrieve nodes/parents/children which has been added/removed from the selection
UNCOV
256
        this.populateAddRemoveArgs(args);
×
257

UNCOV
258
        this.tree.nodeSelection.emit(args);
×
259

UNCOV
260
        if (args.cancel) {
×
UNCOV
261
            return;
×
262
        }
263

264
        // if args.newSelection hasn't been modified
UNCOV
265
        if (this.areEqualCollections(Array.from(this.nodesToBeSelected), args.newSelection)) {
×
UNCOV
266
            this.nodeSelection = new Set<IgxTreeNode<any>>(this.nodesToBeSelected);
×
UNCOV
267
            this.indeterminateNodes = new Set(this.nodesToBeIndeterminate);
×
UNCOV
268
            this.emitSelectedChangeEvent(currSelection);
×
269
        } else {
270
            // select the nodes within the modified args.newSelection with no event
UNCOV
271
            this.cascadeSelectNodesWithNoEvent(args.newSelection, true);
×
272
        }
273
    }
274

275
    /**
276
     * recursively handle the selection state of the direct and indirect parents
277
     */
278
    private handleParentSelectionState(node: IgxTreeNode<any>) {
UNCOV
279
        if (!node) {
×
280
            return;
×
281
        }
UNCOV
282
        this.handleNodeSelectionState(node);
×
UNCOV
283
        if (node.parentNode) {
×
UNCOV
284
            this.handleParentSelectionState(node.parentNode);
×
285
        }
286
    }
287

288
    /**
289
     * Handle the selection state of a given node based the selection states of its direct children
290
     */
291
    private handleNodeSelectionState(node: IgxTreeNode<any>) {
UNCOV
292
        const nodesArray = (node && node._children) ? node._children.toArray() : [];
×
UNCOV
293
        if (nodesArray.length) {
×
UNCOV
294
            if (nodesArray.every(n => this.nodesToBeSelected.has(n))) {
×
UNCOV
295
                this.nodesToBeSelected.add(node);
×
UNCOV
296
                this.nodesToBeIndeterminate.delete(node);
×
UNCOV
297
            } else if (nodesArray.some(n => this.nodesToBeSelected.has(n) || this.nodesToBeIndeterminate.has(n))) {
×
UNCOV
298
                this.nodesToBeIndeterminate.add(node);
×
UNCOV
299
                this.nodesToBeSelected.delete(node);
×
300
            } else {
UNCOV
301
                this.nodesToBeIndeterminate.delete(node);
×
UNCOV
302
                this.nodesToBeSelected.delete(node);
×
303
            }
304
        } else {
305
            // if the children of the node has been deleted and the node was selected do not change its state
306
            if (this.isNodeSelected(node)) {
×
307
                this.nodesToBeSelected.add(node);
×
308
            } else {
309
                this.nodesToBeSelected.delete(node);
×
310
            }
311
            this.nodesToBeIndeterminate.delete(node);
×
312
        }
313
    }
314

315
    /**
316
     * Get a collection of all nodes affected by the change event
317
     *
318
     * @param nodesToBeProcessed set of the nodes to be selected/deselected
319
     * @returns a collection of all affected nodes and all their parents
320
     */
321
    private getCascadingNodeCollection(nodes: IgxTreeNode<any>[]): CascadeSelectionNodeCollection {
UNCOV
322
        const collection: CascadeSelectionNodeCollection = {
×
323
            parents: new Set<IgxTreeNode<any>>(),
324
            nodes: new Set<IgxTreeNode<any>>(nodes)
325
        };
326

UNCOV
327
        Array.from(collection.nodes).forEach((node) => {
×
UNCOV
328
            const nodeAndAllChildren = node.allChildren?.toArray() || [];
×
UNCOV
329
            nodeAndAllChildren.forEach(n => {
×
UNCOV
330
                collection.nodes.add(n);
×
331
            });
332

UNCOV
333
            if (node && node.parentNode) {
×
UNCOV
334
                collection.parents.add(node.parentNode);
×
335
            }
336
        });
UNCOV
337
        return collection;
×
338
    }
339

340
    /**
341
     * retrieve the nodes which should be added/removed to/from the old selection
342
     */
343
    private populateAddRemoveArgs(args: Partial<ITreeNodeSelectionEvent>): void {
UNCOV
344
        args.removed = args.oldSelection.filter(x => args.newSelection.indexOf(x) < 0);
×
UNCOV
345
        args.added = args.newSelection.filter(x => args.oldSelection.indexOf(x) < 0);
×
346
    }
347

348
    /** Emits the `selectedChange` event for each node affected by the selection */
349
    private emitSelectedChangeEvent(oldSelection: IgxTreeNode<any>[]): void {
UNCOV
350
        this.getSelectedNodes().forEach(n => {
×
UNCOV
351
            if (oldSelection.indexOf(n) < 0) {
×
UNCOV
352
                n.selectedChange.emit(true);
×
353
            }
354
        });
355

UNCOV
356
        oldSelection.forEach(n => {
×
UNCOV
357
            if (!this.nodeSelection.has(n)) {
×
UNCOV
358
                n.selectedChange.emit(false);
×
359
            }
360
        });
361
    }
362
}
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