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

IgniteUI / igniteui-angular / 11800403270

12 Nov 2024 03:26PM UTC coverage: 91.577% (+0.005%) from 91.572%
11800403270

push

github

web-flow
fix(query-builder): delete empty groups - master (#15012)

* fix(query-builder): delete empty groups

12957 of 15185 branches covered (85.33%)

8 of 9 new or added lines in 1 file covered. (88.89%)

1 existing line in 1 file now uncovered.

26266 of 28682 relevant lines covered (91.58%)

33912.88 hits per line

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

89.53
/projects/igniteui-angular/src/lib/query-builder/query-builder.component.ts
1
import { AfterViewInit, ContentChild, EventEmitter, LOCALE_ID, Output, Pipe, PipeTransform } from '@angular/core';
2
import { getLocaleFirstDayOfWeek, NgIf, NgFor, NgTemplateOutlet, NgClass, DatePipe } from '@angular/common';
3
import { Inject } from '@angular/core';
4
import {
5
    Component, Input, ViewChild, ChangeDetectorRef, ViewChildren, QueryList, ElementRef, OnDestroy, HostBinding
6
} from '@angular/core';
7
import { FormsModule } from '@angular/forms';
8
import { Subject } from 'rxjs';
9
import { editor } from '@igniteui/material-icons-extended';
10
import { IButtonGroupEventArgs, IgxButtonGroupComponent } from '../buttonGroup/buttonGroup.component';
11
import { IgxChipComponent } from '../chips/chip.component';
12
import { IQueryBuilderResourceStrings, QueryBuilderResourceStringsEN } from '../core/i18n/query-builder-resources';
13
import { PlatformUtil } from '../core/utils';
14
import { DataType, DataUtil } from '../data-operations/data-util';
15
import { IgxBooleanFilteringOperand, IgxDateFilteringOperand, IgxDateTimeFilteringOperand, IgxNumberFilteringOperand, IgxStringFilteringOperand, IgxTimeFilteringOperand } from '../data-operations/filtering-condition';
16
import { FilteringLogic, IFilteringExpression } from '../data-operations/filtering-expression.interface';
17
import { FilteringExpressionsTree, IExpressionTree } from '../data-operations/filtering-expressions-tree';
18
import { IgxDatePickerComponent } from '../date-picker/date-picker.component';
19

20
import { IgxButtonDirective } from '../directives/button/button.directive';
21
import { IgxDateTimeEditorDirective } from '../directives/date-time-editor/date-time-editor.directive';
22

23
import { IgxOverlayOutletDirective, IgxToggleDirective } from '../directives/toggle/toggle.directive';
24
import { FieldType } from '../grids/common/grid.interface';
25
import { IgxIconService } from '../icon/icon.service';
26
import { IgxSelectComponent } from '../select/select.component';
27
import { HorizontalAlignment, OverlaySettings, Point, VerticalAlignment } from '../services/overlay/utilities';
28
import { AbsoluteScrollStrategy, AutoPositionStrategy, CloseScrollStrategy, ConnectedPositioningStrategy } from '../services/public_api';
29
import { IgxTimePickerComponent } from '../time-picker/time-picker.component';
30
import { IgxQueryBuilderHeaderComponent } from './query-builder-header.component';
31
import { IgxPickerToggleComponent, IgxPickerClearComponent } from '../date-common/picker-icons.common';
32
import { IgxInputDirective } from '../directives/input/input.directive';
33
import { IgxInputGroupComponent } from '../input-group/input-group.component';
34
import { IgxSelectItemComponent } from '../select/select-item.component';
35
import { IgxSuffixDirective } from '../directives/suffix/suffix.directive';
36
import { IgxPrefixDirective } from '../directives/prefix/prefix.directive';
37
import { IgxIconComponent } from '../icon/icon.component';
38
import { getCurrentResourceStrings } from '../core/i18n/resources';
39
import { IgxIconButtonDirective } from '../directives/button/icon-button.directive';
40

41
const DEFAULT_PIPE_DATE_FORMAT = 'mediumDate';
2✔
42
const DEFAULT_PIPE_TIME_FORMAT = 'mediumTime';
2✔
43
const DEFAULT_PIPE_DATE_TIME_FORMAT = 'medium';
2✔
44
const DEFAULT_PIPE_DIGITS_INFO = '1.0-3';
2✔
45
const DEFAULT_DATE_TIME_FORMAT = 'dd/MM/yyyy HH:mm:ss a';
2✔
46
const DEFAULT_TIME_FORMAT = 'hh:mm:ss a';
2✔
47

48
@Pipe({
49
    name: 'fieldFormatter',
50
    standalone: true
51
})
52
export class IgxFieldFormatterPipe implements PipeTransform {
2✔
53

54
    public transform(value: any, formatter: (v: any, data: any, fieldData?: any) => any, rowData: any, fieldData?: any) {
55
        return formatter(value, rowData, fieldData);
×
56
    }
57
}
58

59
/**
60
 * @hidden @internal
61
 *
62
 * Internal class usage
63
 */
64
class ExpressionItem {
65
    public parent: ExpressionGroupItem;
66
    public selected: boolean;
67
    constructor(parent?: ExpressionGroupItem) {
68
        this.parent = parent;
395✔
69
    }
70
}
71

72
/**
73
 * @hidden @internal
74
 *
75
 * Internal class usage
76
 */
77
class ExpressionGroupItem extends ExpressionItem {
78
    public operator: FilteringLogic;
79
    public children: ExpressionItem[];
80
    constructor(operator: FilteringLogic, parent?: ExpressionGroupItem) {
81
        super(parent);
128✔
82
        this.operator = operator;
128✔
83
        this.children = [];
128✔
84
    }
85
}
86

87
/**
88
 * @hidden @internal
89
 *
90
 * Internal class usage
91
 */
92
class ExpressionOperandItem extends ExpressionItem {
93
    public expression: IFilteringExpression;
94
    public inEditMode: boolean;
95
    public inAddMode: boolean;
96
    public hovered: boolean;
97
    public fieldLabel: string;
98
    constructor(expression: IFilteringExpression, parent: ExpressionGroupItem) {
99
        super(parent);
267✔
100
        this.expression = expression;
267✔
101
    }
102
}
103

104
/**
105
 * A component used for operating with complex filters by creating or editing conditions
106
 * and grouping them using AND/OR logic.
107
 * It is used internally in the Advanced Filtering of the Grid.
108
 *
109
 * @example
110
 * ```html
111
 * <igx-query-builder [fields]="this.fields">
112
 * </igx-query-builder>
113
 * ```
114
 */
115
@Component({
116
    selector: 'igx-query-builder',
117
    templateUrl: './query-builder.component.html',
118
    standalone: true,
119
    imports: [NgIf, IgxQueryBuilderHeaderComponent, IgxButtonDirective, IgxIconComponent, IgxChipComponent, IgxPrefixDirective, IgxSuffixDirective, IgxSelectComponent, FormsModule, NgFor, IgxSelectItemComponent, IgxInputGroupComponent, IgxInputDirective, IgxDatePickerComponent, IgxPickerToggleComponent, IgxPickerClearComponent, IgxTimePickerComponent, IgxDateTimeEditorDirective, NgTemplateOutlet, NgClass, IgxToggleDirective, IgxButtonGroupComponent, IgxOverlayOutletDirective, DatePipe, IgxFieldFormatterPipe, IgxIconButtonDirective]
120
})
121
export class IgxQueryBuilderComponent implements AfterViewInit, OnDestroy {
2✔
122
    /**
123
     * @hidden @internal
124
     */
125
    @HostBinding('class.igx-query-builder')
126
    public cssClass = 'igx-query-builder';
94✔
127

128
    /**
129
     * @hidden @internal
130
     */
131
    @HostBinding('style.display')
132
    public display = 'block';
94✔
133

134
    /**
135
    * Returns the fields.
136
    */
137
    public get fields(): FieldType[] {
138
        return this._fields;
850✔
139
    }
140

141
    /**
142
     * Sets the fields.
143
     */
144
    @Input()
145
    public set fields(fields: FieldType[]) {
146
        this._fields = fields;
559✔
147

148
        if (this._fields) {
559✔
149
            this.registerSVGIcons();
559✔
150

151
            this._fields.forEach(field => {
559✔
152
                this.setFilters(field);
3,823✔
153
                this.setFormat(field);
3,823✔
154
            });
155
        }
156
    }
157

158
    /**
159
    * Returns the expression tree.
160
    */
161
     public get expressionTree(): IExpressionTree {
162
        return this._expressionTree;
147✔
163
    }
164

165
    /**
166
     * Sets the expression tree.
167
     */
168
    @Input()
169
    public set expressionTree(expressionTree: IExpressionTree) {
170
        this._expressionTree = expressionTree;
116✔
171

172
        this.init();
116✔
173
    }
174

175
    /**
176
     * Gets the `locale` of the query builder.
177
     * If not set, defaults to application's locale.
178
     */
179
    @Input()
180
    public get locale(): string {
181
        return this._locale;
154✔
182
    }
183

184
    /**
185
     * Sets the `locale` of the query builder.
186
     * Expects a valid BCP 47 language tag.
187
     */
188
    public set locale(value: string) {
189
        this._locale = value;
188✔
190
        // if value is invalid, set it back to _localeId
191
        try {
188✔
192
            getLocaleFirstDayOfWeek(this._locale);
188✔
193
        } catch (e) {
194
            this._locale = this._localeId;
×
195
        }
196
    }
197

198
    /**
199
     * Sets the resource strings.
200
     * By default it uses EN resources.
201
     */
202
    @Input()
203
    public set resourceStrings(value: IQueryBuilderResourceStrings) {
204
        this._resourceStrings = Object.assign({}, this._resourceStrings, value);
×
205
    }
206

207
    /**
208
     * Returns the resource strings.
209
     */
210
    public get resourceStrings(): IQueryBuilderResourceStrings {
211
        return this._resourceStrings;
17,604✔
212
    }
213

214
    /**
215
     * Event fired as the expression tree is changed.
216
     *
217
     * ```html
218
     *  <igx-query-builder (expressionTreeChange)='onExpressionTreeChange()'></igx-query-builder>
219
     * ```
220
     */
221
    @Output()
222
    public expressionTreeChange = new EventEmitter();
94✔
223

224
    @ViewChild('fieldSelect', { read: IgxSelectComponent })
225
    private fieldSelect: IgxSelectComponent;
226

227
    @ViewChild('conditionSelect', { read: IgxSelectComponent })
228
    private conditionSelect: IgxSelectComponent;
229

230
    @ViewChild('searchValueInput', { read: ElementRef })
231
    private searchValueInput: ElementRef;
232

233
    @ViewChild('picker')
234
    private picker: IgxDatePickerComponent | IgxTimePickerComponent;
235

236
    @ViewChild('addRootAndGroupButton', { read: ElementRef })
237
    private addRootAndGroupButton: ElementRef;
238

239
    @ViewChild('addConditionButton', { read: ElementRef })
240
    private addConditionButton: ElementRef;
241

242
    /**
243
     * @hidden @internal
244
     */
245
    @ContentChild(IgxQueryBuilderHeaderComponent)
246
    public headerContent: IgxQueryBuilderHeaderComponent;
247

248
    @ViewChild('editingInputsContainer', { read: ElementRef })
249
    protected set editingInputsContainer(value: ElementRef) {
250
        if ((value && !this._editingInputsContainer) ||
272✔
251
            (value && this._editingInputsContainer && this._editingInputsContainer.nativeElement !== value.nativeElement)) {
252
            requestAnimationFrame(() => {
54✔
253
                this.scrollElementIntoView(value.nativeElement);
49✔
254
            });
255
        }
256

257
        this._editingInputsContainer = value;
272✔
258
    }
259

260
    /** @hidden */
261
    protected get editingInputsContainer(): ElementRef {
262
        return this._editingInputsContainer;
×
263
    }
264

265
    @ViewChild('addModeContainer', { read: ElementRef })
266
    protected set addModeContainer(value: ElementRef) {
267
        if ((value && !this._addModeContainer) ||
272!
268
            (value && this._addModeContainer && this._addModeContainer.nativeElement !== value.nativeElement)) {
269
            requestAnimationFrame(() => {
4✔
270
                this.scrollElementIntoView(value.nativeElement);
1✔
271
            });
272
        }
273

274
        this._addModeContainer = value;
272✔
275
    }
276

277
    /** @hidden */
278
    protected get addModeContainer(): ElementRef {
279
        return this._addModeContainer;
×
280
    }
281

282
    @ViewChild('currentGroupButtonsContainer', { read: ElementRef })
283
    protected set currentGroupButtonsContainer(value: ElementRef) {
284
        if ((value && !this._currentGroupButtonsContainer) ||
272✔
285
            (value && this._currentGroupButtonsContainer && this._currentGroupButtonsContainer.nativeElement !== value.nativeElement)) {
286
            requestAnimationFrame(() => {
86✔
287
                this.scrollElementIntoView(value.nativeElement);
51✔
288
            });
289
        }
290

291
        this._currentGroupButtonsContainer = value;
272✔
292
    }
293

294
    /** @hidden */
295
    protected get currentGroupButtonsContainer(): ElementRef {
296
        return this._currentGroupButtonsContainer;
×
297
    }
298

299
    @ViewChild(IgxToggleDirective)
300
    private contextMenuToggle: IgxToggleDirective;
301

302
    @ViewChildren(IgxChipComponent)
303
    private chips: QueryList<IgxChipComponent>;
304

305
    @ViewChild('expressionsContainer')
306
    private expressionsContainer: ElementRef;
307

308
    @ViewChild('overlayOutlet', { read: IgxOverlayOutletDirective, static: true })
309
    private overlayOutlet: IgxOverlayOutletDirective;
310

311
    /**
312
     * @hidden @internal
313
     */
314
    public rootGroup: ExpressionGroupItem;
315

316
    /**
317
     * @hidden @internal
318
     */
319
    public selectedExpressions: ExpressionOperandItem[] = [];
94✔
320

321
    /**
322
     * @hidden @internal
323
     */
324
    public currentGroup: ExpressionGroupItem;
325

326
    /**
327
     * @hidden @internal
328
     */
329
    public contextualGroup: ExpressionGroupItem;
330

331
    /**
332
     * @hidden @internal
333
     */
334
    public filteringLogics;
335

336
    /**
337
     * @hidden @internal
338
     */
339
    public selectedCondition: string;
340

341
    /**
342
     * @hidden @internal
343
     */
344
    public searchValue: any;
345

346
    /**
347
     * @hidden @internal
348
     */
349
    public pickerOutlet: IgxOverlayOutletDirective | ElementRef;
350

351
    /**
352
     * @hidden @internal
353
     */
354
    public fieldSelectOverlaySettings: OverlaySettings = {
94✔
355
        scrollStrategy: new AbsoluteScrollStrategy(),
356
        modal: false,
357
        closeOnOutsideClick: false
358
    };
359

360
    /**
361
     * @hidden @internal
362
     */
363
    public conditionSelectOverlaySettings: OverlaySettings = {
94✔
364
        scrollStrategy: new AbsoluteScrollStrategy(),
365
        modal: false,
366
        closeOnOutsideClick: false
367
    };
368

369
    private destroy$ = new Subject<any>();
94✔
370
    private _selectedField: FieldType;
371
    private _clickTimer;
372
    private _dblClickDelay = 200;
94✔
373
    private _preventChipClick = false;
94✔
374
    private _editingInputsContainer: ElementRef;
375
    private _addModeContainer: ElementRef;
376
    private _currentGroupButtonsContainer: ElementRef;
377
    private _addModeExpression: ExpressionOperandItem;
378
    private _editedExpression: ExpressionOperandItem;
379
    private _selectedGroups: ExpressionGroupItem[] = [];
94✔
380
    private _fields: FieldType[];
381
    private _expressionTree: IExpressionTree;
382
    private _locale;
383
    private _resourceStrings = getCurrentResourceStrings(QueryBuilderResourceStringsEN);
94✔
384

385
    private _positionSettings = {
94✔
386
        horizontalStartPoint: HorizontalAlignment.Right,
387
        verticalStartPoint: VerticalAlignment.Top
388
    };
389

390
    private _overlaySettings: OverlaySettings = {
94✔
391
        closeOnOutsideClick: false,
392
        modal: false,
393
        positionStrategy: new ConnectedPositioningStrategy(this._positionSettings),
394
        scrollStrategy: new CloseScrollStrategy()
395
    };
396

397
    constructor(public cdr: ChangeDetectorRef,
94✔
398
        protected iconService: IgxIconService,
94✔
399
        protected platform: PlatformUtil,
94✔
400
        protected el: ElementRef,
94✔
401
        @Inject(LOCALE_ID) protected _localeId: string) {
94✔
402
        this.locale = this.locale || this._localeId;
94✔
403
    }
404

405
    /**
406
     * @hidden @internal
407
     */
408
    public ngAfterViewInit(): void {
409
        this._overlaySettings.outlet = this.overlayOutlet;
94✔
410
        this.fieldSelectOverlaySettings.outlet = this.overlayOutlet;
94✔
411
        this.conditionSelectOverlaySettings.outlet = this.overlayOutlet;
94✔
412
    }
413

414
    /**
415
     * @hidden @internal
416
     */
417
    public ngOnDestroy(): void {
418
        this.destroy$.next(true);
94✔
419
        this.destroy$.complete();
94✔
420
    }
421

422
    /**
423
     * @hidden @internal
424
     */
425
    public set selectedField(value: FieldType) {
426
        const oldValue = this._selectedField;
94✔
427

428
        if (this._selectedField !== value) {
94✔
429
            this._selectedField = value;
93✔
430
            if (oldValue && this._selectedField && this._selectedField.dataType !== oldValue.dataType) {
93✔
431
                this.selectedCondition = null;
2✔
432
                this.searchValue = null;
2✔
433
                this.cdr.detectChanges();
2✔
434
            }
435
        }
436
    }
437

438
    /**
439
     * @hidden @internal
440
     */
441
    public get selectedField(): FieldType {
442
        return this._selectedField;
14,053✔
443
    }
444

445
    /**
446
     * @hidden @internal
447
     *
448
     * used by the grid
449
     */
450
    public setPickerOutlet(outlet?: IgxOverlayOutletDirective | ElementRef) {
451
        this.pickerOutlet = outlet;
94✔
452
    }
453

454
    /**
455
     * @hidden @internal
456
     *
457
     * used by the grid
458
     */
459
    public get isContextMenuVisible(): boolean {
460
        return !this.contextMenuToggle.collapsed;
3✔
461
    }
462

463
    /**
464
     * @hidden @internal
465
     */
466
    public get hasEditedExpression(): boolean {
467
        return this._editedExpression !== undefined && this._editedExpression !== null;
2,788✔
468
    }
469

470
    /**
471
     * @hidden @internal
472
     */
473
    public addCondition(parent: ExpressionGroupItem, afterExpression?: ExpressionItem) {
474
        this.cancelOperandAdd();
48✔
475

476
        const operandItem = new ExpressionOperandItem({
48✔
477
            fieldName: null,
478
            condition: null,
479
            ignoreCase: true,
480
            searchVal: null
481
        }, parent);
482

483
        if (afterExpression) {
48✔
484
            const index = parent.children.indexOf(afterExpression);
1✔
485
            parent.children.splice(index + 1, 0, operandItem);
1✔
486
        } else {
487
            parent.children.push(operandItem);
47✔
488
        }
489

490
        this.enterExpressionEdit(operandItem);
48✔
491
    }
492

493
    /**
494
     * @hidden @internal
495
     */
496
    public addAndGroup(parent?: ExpressionGroupItem, afterExpression?: ExpressionItem) {
497
        this.addGroup(FilteringLogic.And, parent, afterExpression);
37✔
498
    }
499

500
    /**
501
     * @hidden @internal
502
     */
503
    public addOrGroup(parent?: ExpressionGroupItem, afterExpression?: ExpressionItem) {
504
        this.addGroup(FilteringLogic.Or, parent, afterExpression);
4✔
505
    }
506

507
    /**
508
     * @hidden @internal
509
     */
510
    public endGroup(groupItem: ExpressionGroupItem) {
511
        this.currentGroup = groupItem.parent;
1✔
512
    }
513

514
    /**
515
     * @hidden @internal
516
     */
517
    public commitOperandEdit() {
518
        if (this._editedExpression) {
37✔
519
            this._editedExpression.expression.fieldName = this.selectedField.field;
31✔
520
            this._editedExpression.expression.condition = this.selectedField.filters.condition(this.selectedCondition);
31✔
521
            this._editedExpression.expression.searchVal = DataUtil.parseValue(this.selectedField.dataType, this.searchValue);
31✔
522
            this._editedExpression.fieldLabel = this.selectedField.label
31!
523
                ? this.selectedField.label
524
                : this.selectedField.header
31✔
525
                    ? this.selectedField.header
526
                    : this.selectedField.field;
527
            this._editedExpression.inEditMode = false;
31✔
528
            this._editedExpression = null;
31✔
529
        }
530

531
        this._expressionTree = this.createExpressionTreeFromGroupItem(this.rootGroup);
37✔
532
        this.expressionTreeChange.emit();
37✔
533
    }
534

535
    /**
536
     * @hidden @internal
537
     */
538
    public cancelOperandAdd() {
539
        if (this._addModeExpression) {
260✔
540
            this._addModeExpression.inAddMode = false;
4✔
541
            this._addModeExpression = null;
4✔
542
        }
543
    }
544

545
    /**
546
     * @hidden @internal
547
     */
548
    public cancelOperandEdit() {
549
        if (this._editedExpression) {
123✔
550
            this._editedExpression.inEditMode = false;
7✔
551

552
            if (!this._editedExpression.expression.fieldName) {
7✔
553
                this.deleteItem(this._editedExpression);
6✔
554
            }
555

556
            this._editedExpression = null;
7✔
557
        }
558
    }
559

560
    /**
561
     * @hidden @internal
562
     */
563
    public operandCanBeCommitted(): boolean {
564
        return this.selectedField && this.selectedCondition &&
621✔
565
            (!!this.searchValue || this.selectedField.filters.condition(this.selectedCondition).isUnary);
566
    }
567

568
    /**
569
     * @hidden @internal
570
     *
571
     * used by the grid
572
     */
573
    public exitOperandEdit() {
574
        if (!this._editedExpression) {
132✔
575
            return;
127✔
576
        }
577

578
        if (this.operandCanBeCommitted()) {
5✔
579
            this.commitOperandEdit();
1✔
580
        } else {
581
            this.cancelOperandEdit();
4✔
582
        }
583
    }
584

585
    /**
586
     * @hidden @internal
587
     */
588
    public isExpressionGroup(expression: ExpressionItem): boolean {
589
        return expression instanceof ExpressionGroupItem;
2,722✔
590
    }
591

592
    /**
593
     * @hidden @internal
594
     */
595
    public onChipRemove(expressionItem: ExpressionItem) {
596
        this.deleteItem(expressionItem);
3✔
597
    }
598

599
    /**
600
     * @hidden @internal
601
     */
602
    public onChipClick(expressionItem: ExpressionOperandItem) {
603
        this._clickTimer = setTimeout(() => {
24✔
604
            if (!this._preventChipClick) {
24✔
605
                this.onToggleExpression(expressionItem);
24✔
606
            }
607
            this._preventChipClick = false;
24✔
608
        }, this._dblClickDelay);
609
    }
610

611
    /**
612
     * @hidden @internal
613
     */
614
    public onChipDblClick(expressionItem: ExpressionOperandItem) {
615
        clearTimeout(this._clickTimer);
4✔
616
        this._preventChipClick = true;
4✔
617
        this.enterExpressionEdit(expressionItem);
4✔
618
    }
619

620
    /**
621
     * @hidden @internal
622
     */
623
    public enterExpressionEdit(expressionItem: ExpressionOperandItem) {
624
        this.clearSelection();
54✔
625
        this.exitOperandEdit();
54✔
626
        this.cancelOperandAdd();
54✔
627

628
        if (this._editedExpression) {
54!
629
            this._editedExpression.inEditMode = false;
×
630
        }
631

632
        expressionItem.hovered = false;
54✔
633

634
        this.selectedField = expressionItem.expression.fieldName ?
54✔
635
            this.fields.find(field => field.field === expressionItem.expression.fieldName) : null;
19✔
636
        this.selectedCondition = expressionItem.expression.condition ?
54✔
637
            expressionItem.expression.condition.name : null;
638
        this.searchValue = expressionItem.expression.searchVal;
54✔
639

640
        expressionItem.inEditMode = true;
54✔
641
        this._editedExpression = expressionItem;
54✔
642

643
        this.cdr.detectChanges();
54✔
644

645
        this.fieldSelectOverlaySettings.target = this.fieldSelect.element;
54✔
646
        this.fieldSelectOverlaySettings.excludeFromOutsideClick = [this.fieldSelect.element as HTMLElement];
54✔
647
        this.fieldSelectOverlaySettings.positionStrategy = new AutoPositionStrategy();
54✔
648
        this.conditionSelectOverlaySettings.target = this.conditionSelect.element;
54✔
649
        this.conditionSelectOverlaySettings.excludeFromOutsideClick = [this.conditionSelect.element as HTMLElement];
54✔
650
        this.conditionSelectOverlaySettings.positionStrategy = new AutoPositionStrategy();
54✔
651

652
        if (!this.selectedField) {
54✔
653
            this.fieldSelect.input.nativeElement.focus();
48✔
654
            } else if (this.selectedField.filters.condition(this.selectedCondition).isUnary) {
6!
655
                this.conditionSelect.input.nativeElement.focus();
×
656
        } else {
657
            const input = this.searchValueInput?.nativeElement || this.picker?.getEditElement();
6!
658
            input.focus();
6✔
659
        }
660
    }
661

662
    /**
663
     * @hidden @internal
664
     */
665
    public clearSelection() {
666
        for (const group of this._selectedGroups) {
186✔
667
            group.selected = false;
8✔
668
        }
669
        this._selectedGroups = [];
186✔
670

671
        for (const expr of this.selectedExpressions) {
186✔
672
            expr.selected = false;
24✔
673
        }
674
        this.selectedExpressions = [];
186✔
675

676
        this.toggleContextMenu();
186✔
677
    }
678

679
    /**
680
     * @hidden @internal
681
     */
682
    public enterExpressionAdd(expressionItem: ExpressionOperandItem) {
683
        this.clearSelection();
5✔
684
        this.exitOperandEdit();
5✔
685

686
        if (this._addModeExpression) {
5!
687
            this._addModeExpression.inAddMode = false;
×
688
        }
689

690
        expressionItem.inAddMode = true;
5✔
691
        this._addModeExpression = expressionItem;
5✔
692
        if (expressionItem.selected) {
5!
693
            this.toggleExpression(expressionItem);
×
694
        }
695
    }
696

697
    /**
698
     * @hidden @internal
699
     */
700
    public contextMenuClosed() {
701
        this.contextualGroup = null;
12✔
702
    }
703

704
    /**
705
     * @hidden @internal
706
     */
707
    public onKeyDown(eventArgs: KeyboardEvent) {
708
        eventArgs.stopPropagation();
1✔
709
        const key = eventArgs.key;
1✔
710
        if (!this.contextMenuToggle.collapsed && (key === this.platform.KEYMAP.ESCAPE)) {
1✔
711
            this.clearSelection();
1✔
712
        }
713
    }
714

715
    /**
716
     * @hidden @internal
717
     */
718
    public createAndGroup() {
719
        this.createGroup(FilteringLogic.And);
1✔
720
    }
721

722
    /**
723
     * @hidden @internal
724
     */
725
    public createOrGroup() {
726
        this.createGroup(FilteringLogic.Or);
2✔
727
    }
728

729
    /**
730
     * @hidden @internal
731
     */
732
    public deleteFilters() {
733
        for (const expr of this.selectedExpressions) {
1✔
734
            this.deleteItem(expr);
2✔
735
        }
736

737
        this.clearSelection();
1✔
738
    }
739

740
    /**
741
     * @hidden @internal
742
     */
743
    public onGroupClick(groupItem: ExpressionGroupItem) {
744
        this.toggleGroup(groupItem);
18✔
745
    }
746

747
    /**
748
     * @hidden @internal
749
     */
750
    public ungroup() {
751
        const selectedGroup = this.contextualGroup;
1✔
752
        const parent = selectedGroup.parent;
1✔
753
        if (parent) {
1✔
754
            const index = parent.children.indexOf(selectedGroup);
1✔
755
            parent.children.splice(index, 1, ...selectedGroup.children);
1✔
756

757
            for (const expr of selectedGroup.children) {
1✔
758
                expr.parent = parent;
2✔
759
            }
760
        }
761

762
        this.clearSelection();
1✔
763
        this.commitOperandEdit();
1✔
764
    }
765

766
    /**
767
     * @hidden @internal
768
     */
769
    public deleteGroup() {
770
        let selectedGroup = this.contextualGroup;
2✔
771
        let parent = selectedGroup.parent;
2✔
772
        if (!parent) {
2!
NEW
773
            this.rootGroup = null;
×
774
        }
775

776
        while (parent) {
2✔
777
            let index = parent.children.indexOf(selectedGroup);
2✔
778
            parent.children.splice(index, 1);
2✔
779
            selectedGroup = parent;
2✔
780
            parent = parent.children.length === 0 ? parent.parent : null;
2!
781
        }
782

783
        if (this.rootGroup?.children.length === 0) {
2!
UNCOV
784
            this.rootGroup = null;
×
785
        }
786

787
        this.clearSelection();
2✔
788
        this.commitOperandEdit();
2✔
789
    }
790

791
    /**
792
     * @hidden @internal
793
     */
794
    public selectFilteringLogic(event: IButtonGroupEventArgs) {
795
        this.contextualGroup.operator = event.index as FilteringLogic;
3✔
796
        this.commitOperandEdit();
3✔
797
    }
798

799
    /**
800
     * @hidden @internal
801
     */
802
    public getConditionFriendlyName(name: string): string {
803
        return this.resourceStrings[`igx_query_builder_filter_${name}`] || name;
8,925!
804
    }
805

806
    /**
807
     * @hidden @internal
808
     */
809
    public isDate(value: any) {
810
        return value instanceof Date;
1,781✔
811
    }
812

813
    /**
814
     * @hidden @internal
815
     */
816
    public onExpressionsScrolled() {
817
        if (!this.contextMenuToggle.collapsed) {
×
818
            this.calculateContextMenuTarget();
×
819
            this.contextMenuToggle.reposition();
×
820
        }
821
    }
822

823
    /**
824
     * @hidden @internal
825
     */
826
    public invokeClick(eventArgs: KeyboardEvent) {
827
        if (this.platform.isActivationKey(eventArgs)) {
4✔
828
            eventArgs.preventDefault();
4✔
829
            (eventArgs.currentTarget as HTMLElement).click();
4✔
830
        }
831
    }
832

833
    /**
834
     * @hidden @internal
835
     */
836
    public openPicker(args: KeyboardEvent) {
837
        if (this.platform.isActivationKey(args)) {
×
838
            args.preventDefault();
×
839
            this.picker.open();
×
840
        }
841
    }
842

843
    /**
844
     * @hidden @internal
845
     */
846
    public onOutletPointerDown(event) {
847
        // This prevents closing the select's dropdown when clicking the scroll
848
        event.preventDefault();
×
849
    }
850

851
    /**
852
     * @hidden @internal
853
     */
854
    public getConditionList(): string[] {
855
        return this.selectedField ? this.selectedField.filters.conditionList() : [];
616✔
856
    }
857

858
    /**
859
     * @hidden @internal
860
     */
861
    public getFormatter(field: string) {
862
        return this.fields.find(el => el.field === field).formatter;
20✔
863
    }
864

865
    /**
866
     * @hidden @internal
867
     */
868
    public getFormat(field: string) {
869
        return this.fields.find(el => el.field === field).pipeArgs.format;
20✔
870
    }
871

872
    /**
873
     * @hidden @internal
874
     *
875
     * used by the grid
876
     */
877
    public setAddButtonFocus() {
878
        if (this.addRootAndGroupButton) {
87✔
879
            this.addRootAndGroupButton.nativeElement.focus();
10✔
880
        } else if (this.addConditionButton) {
77✔
881
            this.addConditionButton.nativeElement.focus();
44✔
882
        }
883
    }
884

885
    /**
886
     * @hidden @internal
887
     */
888
    public context(expression: ExpressionItem, afterExpression?: ExpressionItem) {
889
        return {
4,606✔
890
            $implicit: expression,
891
            afterExpression
892
        };
893
    }
894

895
    /**
896
     * @hidden @internal
897
     */
898
    public onChipSelectionEnd() {
899
        const contextualGroup = this.findSingleSelectedGroup();
68✔
900
        if (contextualGroup || this.selectedExpressions.length > 1) {
68✔
901
            this.contextualGroup = contextualGroup;
36✔
902
            this.calculateContextMenuTarget();
36✔
903
            if (this.contextMenuToggle.collapsed) {
36✔
904
                this.contextMenuToggle.open(this._overlaySettings);
16✔
905
            } else {
906
                this.contextMenuToggle.reposition();
20✔
907
            }
908
        }
909
    }
910

911
    private setFormat(field: FieldType) {
912
        if (!field.pipeArgs) {
3,823!
913
            field.pipeArgs = { digitsInfo: DEFAULT_PIPE_DIGITS_INFO };
×
914
        }
915

916
        if (!field.pipeArgs.format) {
3,823!
917
            field.pipeArgs.format = field.dataType === DataType.Time ?
×
918
                DEFAULT_PIPE_TIME_FORMAT : field.dataType === DataType.DateTime ?
×
919
                    DEFAULT_PIPE_DATE_TIME_FORMAT : DEFAULT_PIPE_DATE_FORMAT;
920
        }
921
    }
922

923
    private setFilters(field: FieldType) {
924
        if (!field.filters) {
3,823!
925
            switch (field.dataType) {
×
926
                case DataType.Boolean:
927
                    field.filters = IgxBooleanFilteringOperand.instance();
×
928
                    break;
×
929
                case DataType.Number:
930
                case DataType.Currency:
931
                case DataType.Percent:
932
                    field.filters = IgxNumberFilteringOperand.instance();
×
933
                    break;
×
934
                case DataType.Date:
935
                    field.filters = IgxDateFilteringOperand.instance();
×
936
                    break;
×
937
                case DataType.Time:
938
                    field.filters = IgxTimeFilteringOperand.instance();
×
939
                    break;
×
940
                case DataType.DateTime:
941
                    field.filters = IgxDateTimeFilteringOperand.instance();
×
942
                    break;
×
943
                case DataType.String:
944
                default:
945
                    field.filters = IgxStringFilteringOperand.instance();
×
946
                    break;
×
947
            }
948

949
        }
950
    }
951

952
    private onToggleExpression(expressionItem: ExpressionOperandItem) {
953
        this.exitOperandEdit();
24✔
954
        this.toggleExpression(expressionItem);
24✔
955

956
        this.toggleContextMenu();
24✔
957
    }
958

959
    private toggleExpression(expressionItem: ExpressionOperandItem) {
960
        expressionItem.selected = !expressionItem.selected;
68✔
961

962
        if (expressionItem.selected) {
68✔
963
            this.selectedExpressions.push(expressionItem);
51✔
964
        } else {
965
            const index = this.selectedExpressions.indexOf(expressionItem);
17✔
966
            this.selectedExpressions.splice(index, 1);
17✔
967
            this.deselectParentRecursive(expressionItem);
17✔
968
        }
969
    }
970

971
    private addGroup(operator: FilteringLogic, parent?: ExpressionGroupItem, afterExpression?: ExpressionItem) {
972
        this.cancelOperandAdd();
41✔
973

974
        const groupItem = new ExpressionGroupItem(operator, parent);
41✔
975

976
        if (parent) {
41✔
977
            if (afterExpression) {
3✔
978
                const index = parent.children.indexOf(afterExpression);
1✔
979
                parent.children.splice(index + 1, 0, groupItem);
1✔
980
            } else {
981
                parent.children.push(groupItem);
2✔
982
            }
983
        } else {
984
            this.rootGroup = groupItem;
38✔
985
        }
986

987
        this.addCondition(groupItem);
41✔
988
        this.currentGroup = groupItem;
41✔
989
    }
990

991
    private createExpressionGroupItem(expressionTree: IExpressionTree, parent?: ExpressionGroupItem): ExpressionGroupItem | null {
992
        if (!expressionTree) {
144✔
993
            return null;
60✔
994
        }
995

996
        const groupItem = new ExpressionGroupItem(expressionTree.operator, parent);
84✔
997

998
        for (const expr of expressionTree.filteringOperands) {
84✔
999
            if (expr instanceof FilteringExpressionsTree) {
248✔
1000
                const childGroup = this.createExpressionGroupItem(expr, groupItem);
28✔
1001
                if (childGroup) {
28✔
1002
                    groupItem.children.push(childGroup);
28✔
1003
                }
1004
            } else {
1005
                const filteringExpr = expr as IFilteringExpression;
220✔
1006
                const field = this.fields.find(el => el.field === filteringExpr.fieldName);
594✔
1007

1008
                if (field) {
220✔
1009
                    const exprCopy: IFilteringExpression = { ...filteringExpr };
219✔
1010
                    const operandItem = new ExpressionOperandItem(exprCopy, groupItem);
219✔
1011
                    operandItem.fieldLabel = field.label || field.header || field.field;
219✔
1012
                    groupItem.children.push(operandItem);
219✔
1013
                }
1014
            }
1015
        }
1016

1017
        return groupItem.children.length > 0 ? groupItem : null;
84✔
1018
    }
1019

1020
    private createExpressionTreeFromGroupItem(groupItem: ExpressionGroupItem): FilteringExpressionsTree {
1021
        if (!groupItem) {
80!
1022
            return null;
×
1023
        }
1024

1025
        const expressionTree = new FilteringExpressionsTree(groupItem.operator);
80✔
1026

1027
        for (const item of groupItem.children) {
80✔
1028
            if (item instanceof ExpressionGroupItem) {
110✔
1029
                const subTree = this.createExpressionTreeFromGroupItem((item as ExpressionGroupItem));
26✔
1030
                expressionTree.filteringOperands.push(subTree);
26✔
1031
            } else {
1032
                expressionTree.filteringOperands.push((item as ExpressionOperandItem).expression);
84✔
1033
            }
1034
        }
1035

1036
        return expressionTree;
80✔
1037
    }
1038

1039
    private toggleContextMenu() {
1040
        const contextualGroup = this.findSingleSelectedGroup();
227✔
1041

1042
        if (contextualGroup || this.selectedExpressions.length > 1) {
227✔
1043
            this.contextualGroup = contextualGroup;
21✔
1044

1045
            if (contextualGroup) {
21✔
1046
                this.filteringLogics = [
13✔
1047
                    {
1048
                        label: this.resourceStrings.igx_query_builder_filter_operator_and,
1049
                        selected: contextualGroup.operator === FilteringLogic.And
1050
                    },
1051
                    {
1052
                        label: this.resourceStrings.igx_query_builder_filter_operator_or,
1053
                        selected: contextualGroup.operator === FilteringLogic.Or
1054
                    }
1055
                ];
1056
            }
1057
        } else if (this.contextMenuToggle) {
206✔
1058
            this.contextMenuToggle.close();
112✔
1059
        }
1060
    }
1061

1062
    private findSingleSelectedGroup(): ExpressionGroupItem {
1063
        for (const group of this._selectedGroups) {
295✔
1064
            const containsAllSelectedExpressions = this.selectedExpressions.every(op => this.isInsideGroup(op, group));
114✔
1065

1066
            if (containsAllSelectedExpressions) {
43✔
1067
                return group;
43✔
1068
            }
1069
        }
1070

1071
        return null;
252✔
1072
    }
1073

1074
    private isInsideGroup(item: ExpressionItem, group: ExpressionGroupItem): boolean {
1075
        if (!item) {
180!
1076
            return false;
×
1077
        }
1078

1079
        if (item.parent === group) {
180✔
1080
            return true;
114✔
1081
        }
1082

1083
        return this.isInsideGroup(item.parent, group);
66✔
1084
    }
1085

1086
    private deleteItem(expressionItem: ExpressionItem) {
1087
        if (!expressionItem.parent) {
23✔
1088
            this.rootGroup = null;
6✔
1089
            this.currentGroup = null;
6✔
1090
            this._expressionTree = null;
6✔
1091
            return;
6✔
1092
        }
1093

1094
        if (expressionItem === this.currentGroup) {
17!
1095
            this.currentGroup = this.currentGroup.parent;
×
1096
        }
1097

1098
        const children = expressionItem.parent.children;
17✔
1099
        const index = children.indexOf(expressionItem);
17✔
1100
        children.splice(index, 1);
17✔
1101
        this._expressionTree = this.createExpressionTreeFromGroupItem(this.rootGroup);
17✔
1102

1103
        if (!children.length) {
17✔
1104
            this.deleteItem(expressionItem.parent);
6✔
1105
        }
1106

1107
        this.expressionTreeChange.emit();
17✔
1108
    }
1109

1110
    private createGroup(operator: FilteringLogic) {
1111
        const chips = this.chips.toArray();
3✔
1112
        const minIndex = this.selectedExpressions.reduce((i, e) => Math.min(i, chips.findIndex(c => c.data === e)), Number.MAX_VALUE);
11✔
1113
        const firstExpression = chips[minIndex].data;
3✔
1114

1115
        const parent = firstExpression.parent;
3✔
1116
        const groupItem = new ExpressionGroupItem(operator, parent);
3✔
1117

1118
        const index = parent.children.indexOf(firstExpression);
3✔
1119
        parent.children.splice(index, 0, groupItem);
3✔
1120

1121
        for (const expr of this.selectedExpressions) {
3✔
1122
            groupItem.children.push(expr);
6✔
1123
            this.deleteItem(expr);
6✔
1124
            expr.parent = groupItem;
6✔
1125
        }
1126

1127
        this.clearSelection();
3✔
1128
    }
1129

1130
    private toggleGroup(groupItem: ExpressionGroupItem) {
1131
        this.exitOperandEdit();
18✔
1132
        if (groupItem.children && groupItem.children.length) {
18✔
1133
            this.toggleGroupRecursive(groupItem, !groupItem.selected);
17✔
1134
            if (!groupItem.selected) {
17✔
1135
                this.deselectParentRecursive(groupItem);
4✔
1136
            }
1137
            this.toggleContextMenu();
17✔
1138
        }
1139
    }
1140

1141
    private toggleGroupRecursive(groupItem: ExpressionGroupItem, selected: boolean) {
1142
        if (groupItem.selected !== selected) {
27✔
1143
            groupItem.selected = selected;
27✔
1144

1145
            if (groupItem.selected) {
27✔
1146
                this._selectedGroups.push(groupItem);
19✔
1147
            } else {
1148
                const index = this._selectedGroups.indexOf(groupItem);
8✔
1149
                this._selectedGroups.splice(index, 1);
8✔
1150
            }
1151
        }
1152

1153
        for (const expr of groupItem.children) {
27✔
1154
            if (expr instanceof ExpressionGroupItem) {
54✔
1155
                this.toggleGroupRecursive(expr, selected);
10✔
1156
            } else {
1157
                const operandExpression = expr as ExpressionOperandItem;
44✔
1158
                if (operandExpression.selected !== selected) {
44✔
1159
                    this.toggleExpression(operandExpression);
44✔
1160
                }
1161
            }
1162
        }
1163
    }
1164

1165
    private deselectParentRecursive(expressionItem: ExpressionItem) {
1166
        const parent = expressionItem.parent;
56✔
1167
        if (parent) {
56✔
1168
            if (parent.selected) {
35!
1169
                parent.selected = false;
×
1170
                const index = this._selectedGroups.indexOf(parent);
×
1171
                this._selectedGroups.splice(index, 1);
×
1172
            }
1173
            this.deselectParentRecursive(parent);
35✔
1174
        }
1175
    }
1176

1177
    private calculateContextMenuTarget() {
1178
        const containerRect = this.expressionsContainer.nativeElement.getBoundingClientRect();
36✔
1179
        const chips = this.chips.filter(c => this.selectedExpressions.indexOf(c.data) !== -1);
150✔
1180
        let minTop = chips.reduce((t, c) =>
36✔
1181
            Math.min(t, c.nativeElement.getBoundingClientRect().top), Number.MAX_VALUE);
96✔
1182
        minTop = Math.max(containerRect.top, minTop);
36✔
1183
        minTop = Math.min(containerRect.bottom, minTop);
36✔
1184
        let maxRight = chips.reduce((r, c) =>
36✔
1185
            Math.max(r, c.nativeElement.getBoundingClientRect().right), 0);
96✔
1186
        maxRight = Math.max(maxRight, containerRect.left);
36✔
1187
        maxRight = Math.min(maxRight, containerRect.right);
36✔
1188
        this._overlaySettings.target = new Point(maxRight, minTop);
36✔
1189
    }
1190

1191
    private scrollElementIntoView(target: HTMLElement) {
1192
        const container = this.expressionsContainer.nativeElement;
101✔
1193
        const targetOffset = target.offsetTop - container.offsetTop;
101✔
1194
        const delta = 10;
101✔
1195

1196
        if (container.scrollTop + delta > targetOffset) {
101✔
1197
            container.scrollTop = targetOffset - delta;
2✔
1198
        } else if (container.scrollTop + container.clientHeight < targetOffset + target.offsetHeight + delta) {
99✔
1199
            container.scrollTop = targetOffset + target.offsetHeight + delta - container.clientHeight;
3✔
1200
        }
1201
    }
1202

1203
    private init() {
1204
        this.clearSelection();
116✔
1205
        this.cancelOperandAdd();
116✔
1206
        this.cancelOperandEdit();
116✔
1207
        this.rootGroup = this.createExpressionGroupItem(this.expressionTree);
116✔
1208
        this.currentGroup = this.rootGroup;
116✔
1209
    }
1210

1211
    private registerSVGIcons(): void {
1212
        const editorIcons = editor as any[];
559✔
1213

1214
        editorIcons.forEach((icon) => {
559✔
1215
            this.iconService.addSvgIconFromText(icon.name, icon.value, 'imx-icons');
33,540✔
1216
        });
1217
    }
1218
}
1219

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

© 2026 Coveralls, Inc