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

IgniteUI / igniteui-angular / 11704035360

06 Nov 2024 12:56PM UTC coverage: 91.77% (-0.04%) from 91.814%
11704035360

push

github

igdmdimitrov
fix(query-builder): delete empty groups

12534 of 14634 branches covered (85.65%)

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

4 existing lines in 2 files now uncovered.

25637 of 27936 relevant lines covered (91.77%)

32901.26 hits per line

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

87.24
/projects/igniteui-angular/src/lib/query-builder/query-builder.component.ts
1
import { AfterViewInit, ContentChild, EventEmitter, LOCALE_ID, Optional, 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 { DisplayDensityBase, DisplayDensityToken, IDisplayDensityOptions } from '../core/density';
13
import { IQueryBuilderResourceStrings, QueryBuilderResourceStringsEN } from '../core/i18n/query-builder-resources';
14
import { PlatformUtil } from '../core/utils';
15
import { DataType, DataUtil } from '../data-operations/data-util';
16
import { IgxBooleanFilteringOperand, IgxDateFilteringOperand, IgxDateTimeFilteringOperand, IgxNumberFilteringOperand, IgxStringFilteringOperand, IgxTimeFilteringOperand } from '../data-operations/filtering-condition';
17
import { FilteringLogic, IFilteringExpression } from '../data-operations/filtering-expression.interface';
18
import { FilteringExpressionsTree, IExpressionTree } from '../data-operations/filtering-expressions-tree';
19
import { IgxDatePickerComponent } from '../date-picker/date-picker.component';
20

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

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

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

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

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

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

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

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

105
/**
106
 * A component used for operating with complex filters by creating or editing conditions
107
 * and grouping them using AND/OR logic.
108
 * It is used internally in the Advanced Filtering of the Grid.
109
 *
110
 * @example
111
 * ```html
112
 * <igx-query-builder [fields]="this.fields">
113
 * </igx-query-builder>
114
 * ```
115
 */
116
@Component({
117
    selector: 'igx-query-builder',
118
    templateUrl: './query-builder.component.html',
119
    standalone: true,
120
    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]
121
})
122
export class IgxQueryBuilderComponent extends DisplayDensityBase implements AfterViewInit, OnDestroy {
2✔
123
    /**
124
     * @hidden @internal
125
     */
126
    @HostBinding('class.igx-query-builder')
127
    public cssClass = 'igx-query-builder';
91✔
128

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

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

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

149
        if (this._fields) {
534✔
150
            this.registerSVGIcons();
534✔
151

152
            this._fields.forEach(field => {
534✔
153
                this.setFilters(field);
3,180✔
154
                this.setFormat(field);
3,180✔
155
            });
156
        }
157
    }
158

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

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

173
        this.init();
113✔
174
    }
175

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

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

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

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

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

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

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

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

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

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

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

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

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

258
        this._editingInputsContainer = value;
267✔
259
    }
260

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

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

275
        this._addModeContainer = value;
267✔
276
    }
277

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

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

292
        this._currentGroupButtonsContainer = value;
267✔
293
    }
294

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

398
    constructor(public cdr: ChangeDetectorRef,
91✔
399
        protected iconService: IgxIconService,
91✔
400
        protected platform: PlatformUtil,
91✔
401
        protected el: ElementRef,
91✔
402
        @Inject(LOCALE_ID) protected _localeId: string,
91✔
403
        @Optional() @Inject(DisplayDensityToken) protected _displayDensityOptions?: IDisplayDensityOptions) {
91✔
404
        super(_displayDensityOptions, el);
91✔
405
        this.locale = this.locale || this._localeId;
91✔
406
    }
407

408
    /**
409
     * @hidden @internal
410
     */
411
    public ngAfterViewInit(): void {
412
        this._overlaySettings.outlet = this.overlayOutlet;
91✔
413
        this.fieldSelectOverlaySettings.outlet = this.overlayOutlet;
91✔
414
        this.conditionSelectOverlaySettings.outlet = this.overlayOutlet;
91✔
415
    }
416

417
    /**
418
     * @hidden @internal
419
     */
420
    public ngOnDestroy(): void {
421
        this.destroy$.next(true);
91✔
422
        this.destroy$.complete();
91✔
423
    }
424

425
    /**
426
     * @hidden @internal
427
     */
428
    public set selectedField(value: FieldType) {
429
        const oldValue = this._selectedField;
88✔
430

431
        if (this._selectedField !== value) {
88✔
432
            this._selectedField = value;
87✔
433
            if (oldValue && this._selectedField && this._selectedField.dataType !== oldValue.dataType) {
87✔
434
                this.selectedCondition = null;
3✔
435
                this.searchValue = null;
3✔
436
                this.cdr.detectChanges();
3✔
437
            }
438
        }
439
    }
440

441
    /**
442
     * @hidden @internal
443
     */
444
    public get selectedField(): FieldType {
445
        return this._selectedField;
13,025✔
446
    }
447

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

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

466
    /**
467
     * @hidden @internal
468
     */
469
    public get hasEditedExpression(): boolean {
470
        return this._editedExpression !== undefined && this._editedExpression !== null;
2,692✔
471
    }
472

473
    /**
474
     * @hidden @internal
475
     */
476
    public addCondition(parent: ExpressionGroupItem, afterExpression?: ExpressionItem) {
477
        this.cancelOperandAdd();
44✔
478

479
        const operandItem = new ExpressionOperandItem({
44✔
480
            fieldName: null,
481
            condition: null,
482
            ignoreCase: true,
483
            searchVal: null
484
        }, parent);
485

486
        if (afterExpression) {
44✔
487
            const index = parent.children.indexOf(afterExpression);
1✔
488
            parent.children.splice(index + 1, 0, operandItem);
1✔
489
        } else {
490
            parent.children.push(operandItem);
43✔
491
        }
492

493
        this.enterExpressionEdit(operandItem);
44✔
494
    }
495

496
    /**
497
     * @hidden @internal
498
     */
499
    public addAndGroup(parent?: ExpressionGroupItem, afterExpression?: ExpressionItem) {
500
        this.addGroup(FilteringLogic.And, parent, afterExpression);
33✔
501
    }
502

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

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

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

534
        this._expressionTree = this.createExpressionTreeFromGroupItem(this.rootGroup);
38✔
535
        this.expressionTreeChange.emit();
38✔
536
    }
537

538
    /**
539
     * @hidden @internal
540
     */
541
    public cancelOperandAdd() {
542
        if (this._addModeExpression) {
247✔
543
            this._addModeExpression.inAddMode = false;
4✔
544
            this._addModeExpression = null;
4✔
545
        }
546
    }
547

548
    /**
549
     * @hidden @internal
550
     */
551
    public cancelOperandEdit() {
552
        if (this._editedExpression) {
120✔
553
            this._editedExpression.inEditMode = false;
7✔
554

555
            if (!this._editedExpression.expression.fieldName) {
7✔
556
                this.deleteItem(this._editedExpression);
6✔
557
            }
558

559
            this._editedExpression = null;
7✔
560
        }
561
    }
562

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

571
    /**
572
     * @hidden @internal
573
     *
574
     * used by the grid
575
     */
576
    public exitOperandEdit() {
577
        if (!this._editedExpression) {
131✔
578
            return;
126✔
579
        }
580

581
        if (this.operandCanBeCommitted()) {
5✔
582
            this.commitOperandEdit();
1✔
583
        } else {
584
            this.cancelOperandEdit();
4✔
585
        }
586
    }
587

588
    /**
589
     * @hidden @internal
590
     */
591
    public isExpressionGroup(expression: ExpressionItem): boolean {
592
        return expression instanceof ExpressionGroupItem;
2,708✔
593
    }
594

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

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

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

623
    /**
624
     * @hidden @internal
625
     */
626
    public enterExpressionEdit(expressionItem: ExpressionOperandItem) {
627
        this.clearSelection();
52✔
628
        this.exitOperandEdit();
52✔
629
        this.cancelOperandAdd();
52✔
630

631
        if (this._editedExpression) {
52!
632
            this._editedExpression.inEditMode = false;
×
633
        }
634

635
        expressionItem.hovered = false;
52✔
636

637
        this.selectedField = expressionItem.expression.fieldName ?
52✔
638
            this.fields.find(field => field.field === expressionItem.expression.fieldName) : null;
23✔
639
        this.selectedCondition = expressionItem.expression.condition ?
52✔
640
            expressionItem.expression.condition.name : null;
641
        this.searchValue = expressionItem.expression.searchVal;
52✔
642

643
        expressionItem.inEditMode = true;
52✔
644
        this._editedExpression = expressionItem;
52✔
645

646
        this.cdr.detectChanges();
52✔
647

648
        this.fieldSelectOverlaySettings.target = this.fieldSelect.element;
52✔
649
        this.fieldSelectOverlaySettings.excludeFromOutsideClick = [this.fieldSelect.element as HTMLElement];
52✔
650
        this.fieldSelectOverlaySettings.positionStrategy = new AutoPositionStrategy();
52✔
651
        this.conditionSelectOverlaySettings.target = this.conditionSelect.element;
52✔
652
        this.conditionSelectOverlaySettings.excludeFromOutsideClick = [this.conditionSelect.element as HTMLElement];
52✔
653
        this.conditionSelectOverlaySettings.positionStrategy = new AutoPositionStrategy();
52✔
654

655
        if (!this.selectedField) {
52✔
656
            this.fieldSelect.input.nativeElement.focus();
44✔
657
            } else if (this.selectedField.filters.condition(this.selectedCondition).isUnary) {
8!
658
                this.conditionSelect.input.nativeElement.focus();
×
659
        } else {
660
            const input = this.searchValueInput?.nativeElement || this.picker?.getEditElement();
8!
661
            requestAnimationFrame(() => input.focus());
8✔
662
        }
663
    }
664

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

674
        for (const expr of this.selectedExpressions) {
181✔
675
            expr.selected = false;
25✔
676
        }
677
        this.selectedExpressions = [];
181✔
678

679
        this.toggleContextMenu();
181✔
680
    }
681

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

689
        if (this._addModeExpression) {
5!
690
            this._addModeExpression.inAddMode = false;
×
691
        }
692

693
        expressionItem.inAddMode = true;
5✔
694
        this._addModeExpression = expressionItem;
5✔
695
        if (expressionItem.selected) {
5!
696
            this.toggleExpression(expressionItem);
×
697
        }
698
    }
699

700
    /**
701
     * @hidden @internal
702
     */
703
    public contextMenuClosed() {
704
        this.contextualGroup = null;
12✔
705
    }
706

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

718
    /**
719
     * @hidden @internal
720
     */
721
    public createAndGroup() {
722
        this.createGroup(FilteringLogic.And);
1✔
723
    }
724

725
    /**
726
     * @hidden @internal
727
     */
728
    public createOrGroup() {
729
        this.createGroup(FilteringLogic.Or);
2✔
730
    }
731

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

740
        this.clearSelection();
1✔
741
    }
742

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

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

760
            for (const expr of selectedGroup.children) {
1✔
761
                expr.parent = parent;
2✔
762
            }
763
        }
764

765
        this.clearSelection();
1✔
766
        this.commitOperandEdit();
1✔
767
    }
768

769
    /**
770
     * @hidden @internal
771
     */
772
    public deleteGroup() {
773
        const selectedGroup = this.contextualGroup;
2✔
774
        let parent = selectedGroup.parent;
2✔
775
        if (parent) {
2!
776
            let index = parent.children.indexOf(selectedGroup);
2✔
777
            parent.children.splice(index, 1);
2✔
778

779
            if (parent.children.length === 0) {
2!
NEW
780
                let childGroup = parent;
×
NEW
781
                parent = parent.parent;
×
NEW
782
                while (parent && parent.children.length === 1) {
×
NEW
783
                    childGroup = parent;
×
NEW
784
                    parent = parent.parent;
×
785
                }
786

NEW
787
                if (parent) {
×
NEW
788
                    index = parent.children.indexOf(childGroup);
×
NEW
789
                    parent.children.splice(index, 1);
×
790
                } else {
NEW
791
                    this.rootGroup = null;
×
792
                }
793
            }
794
        } else {
795
            this.rootGroup = null;
×
796
        }
797
        this.clearSelection();
2✔
798
        this.commitOperandEdit();
2✔
799
    }
800

801
    /**
802
     * @hidden @internal
803
     */
804
    public selectFilteringLogic(event: IButtonGroupEventArgs) {
805
        this.contextualGroup.operator = event.index as FilteringLogic;
3✔
806
        this.commitOperandEdit();
3✔
807
    }
808

809
    /**
810
     * @hidden @internal
811
     */
812
    public getConditionFriendlyName(name: string): string {
813
        return this.resourceStrings[`igx_query_builder_filter_${name}`] || name;
8,433!
814
    }
815

816
    /**
817
     * @hidden @internal
818
     */
819
    public isDate(value: any) {
820
        return value instanceof Date;
1,817✔
821
    }
822

823
    /**
824
     * @hidden @internal
825
     */
826
    public onExpressionsScrolled() {
827
        if (!this.contextMenuToggle.collapsed) {
×
828
            this.calculateContextMenuTarget();
×
829
            this.contextMenuToggle.reposition();
×
830
        }
831
    }
832

833
    /**
834
     * @hidden @internal
835
     */
836
    public invokeClick(eventArgs: KeyboardEvent) {
837
        if (this.platform.isActivationKey(eventArgs)) {
6✔
838
            eventArgs.preventDefault();
6✔
839
            (eventArgs.currentTarget as HTMLElement).click();
6✔
840
        }
841
    }
842

843
    /**
844
     * @hidden @internal
845
     */
846
    public openPicker(args: KeyboardEvent) {
847
        if (this.platform.isActivationKey(args)) {
×
848
            args.preventDefault();
×
849
            this.picker.open();
×
850
        }
851
    }
852

853
    /**
854
     * @hidden @internal
855
     */
856
    public onOutletPointerDown(event) {
857
        // This prevents closing the select's dropdown when clicking the scroll
858
        event.preventDefault();
×
859
    }
860

861
    /**
862
     * @hidden @internal
863
     */
864
    public getConditionList(): string[] {
865
        return this.selectedField ? this.selectedField.filters.conditionList() : [];
566✔
866
    }
867

868
    /**
869
     * @hidden @internal
870
     */
871
    public getFormatter(field: string) {
872
        return this.fields.find(el => el.field === field).formatter;
20✔
873
    }
874

875
    /**
876
     * @hidden @internal
877
     */
878
    public getFormat(field: string) {
879
        return this.fields.find(el => el.field === field).pipeArgs.format;
20✔
880
    }
881

882
    /**
883
     * @hidden @internal
884
     *
885
     * used by the grid
886
     */
887
    public setAddButtonFocus() {
888
        if (this.addRootAndGroupButton) {
84✔
889
            this.addRootAndGroupButton.nativeElement.focus();
10✔
890
        } else if (this.addConditionButton) {
74✔
891
            this.addConditionButton.nativeElement.focus();
45✔
892
        }
893
    }
894

895
    /**
896
     * @hidden @internal
897
     */
898
    public context(expression: ExpressionItem, afterExpression?: ExpressionItem) {
899
        return {
4,520✔
900
            $implicit: expression,
901
            afterExpression
902
        };
903
    }
904

905
    /**
906
     * @hidden @internal
907
     */
908
    public onChipSelectionEnd() {
909
        const contextualGroup = this.findSingleSelectedGroup();
69✔
910
        if (contextualGroup || this.selectedExpressions.length > 1) {
69✔
911
            this.contextualGroup = contextualGroup;
36✔
912
            this.calculateContextMenuTarget();
36✔
913
            if (this.contextMenuToggle.collapsed) {
36✔
914
                this.contextMenuToggle.open(this._overlaySettings);
16✔
915
            } else {
916
                this.contextMenuToggle.reposition();
20✔
917
            }
918
        }
919
    }
920

921
    private setFormat(field: FieldType) {
922
        if (!field.pipeArgs) {
3,180!
923
            field.pipeArgs = { digitsInfo: DEFAULT_PIPE_DIGITS_INFO };
×
924
        }
925

926
        if (!field.pipeArgs.format) {
3,180!
927
            field.pipeArgs.format = field.dataType === DataType.Time ?
×
928
                DEFAULT_PIPE_TIME_FORMAT : field.dataType === DataType.DateTime ?
×
929
                    DEFAULT_PIPE_DATE_TIME_FORMAT : DEFAULT_PIPE_DATE_FORMAT;
930
        }
931

932
        if (!field.defaultDateTimeFormat) {
3,180!
933
            field.defaultDateTimeFormat = DEFAULT_DATE_TIME_FORMAT;
×
934
        }
935

936
        if (!field.defaultTimeFormat) {
3,180!
937
            field.defaultTimeFormat = DEFAULT_TIME_FORMAT;
×
938
        }
939
    }
940

941
    private setFilters(field: FieldType) {
942
        if (!field.filters) {
3,180!
943
            switch (field.dataType) {
×
944
                case DataType.Boolean:
945
                    field.filters = IgxBooleanFilteringOperand.instance();
×
946
                    break;
×
947
                case DataType.Number:
948
                case DataType.Currency:
949
                case DataType.Percent:
950
                    field.filters = IgxNumberFilteringOperand.instance();
×
951
                    break;
×
952
                case DataType.Date:
953
                    field.filters = IgxDateFilteringOperand.instance();
×
954
                    break;
×
955
                case DataType.Time:
956
                    field.filters = IgxTimeFilteringOperand.instance();
×
957
                    break;
×
958
                case DataType.DateTime:
959
                    field.filters = IgxDateTimeFilteringOperand.instance();
×
960
                    break;
×
961
                case DataType.String:
962
                default:
963
                    field.filters = IgxStringFilteringOperand.instance();
×
964
                    break;
×
965
            }
966

967
        }
968
    }
969

970
    private onToggleExpression(expressionItem: ExpressionOperandItem) {
971
        this.exitOperandEdit();
25✔
972
        this.toggleExpression(expressionItem);
25✔
973

974
        this.toggleContextMenu();
25✔
975
    }
976

977
    private toggleExpression(expressionItem: ExpressionOperandItem) {
978
        expressionItem.selected = !expressionItem.selected;
69✔
979

980
        if (expressionItem.selected) {
69✔
981
            this.selectedExpressions.push(expressionItem);
52✔
982
        } else {
983
            const index = this.selectedExpressions.indexOf(expressionItem);
17✔
984
            this.selectedExpressions.splice(index, 1);
17✔
985
            this.deselectParentRecursive(expressionItem);
17✔
986
        }
987
    }
988

989
    private addGroup(operator: FilteringLogic, parent?: ExpressionGroupItem, afterExpression?: ExpressionItem) {
990
        this.cancelOperandAdd();
37✔
991

992
        const groupItem = new ExpressionGroupItem(operator, parent);
37✔
993

994
        if (parent) {
37✔
995
            if (afterExpression) {
3✔
996
                const index = parent.children.indexOf(afterExpression);
1✔
997
                parent.children.splice(index + 1, 0, groupItem);
1✔
998
            } else {
999
                parent.children.push(groupItem);
2✔
1000
            }
1001
        } else {
1002
            this.rootGroup = groupItem;
34✔
1003
        }
1004

1005
        this.addCondition(groupItem);
37✔
1006
        this.currentGroup = groupItem;
37✔
1007
    }
1008

1009
    private createExpressionGroupItem(expressionTree: IExpressionTree, parent?: ExpressionGroupItem): ExpressionGroupItem | null {
1010
        if (!expressionTree) {
141✔
1011
            return null;
56✔
1012
        }
1013

1014
        const groupItem = new ExpressionGroupItem(expressionTree.operator, parent);
85✔
1015

1016
        for (const expr of expressionTree.filteringOperands) {
85✔
1017
            if (expr instanceof FilteringExpressionsTree) {
251✔
1018
                const childGroup = this.createExpressionGroupItem(expr, groupItem);
28✔
1019
                if (childGroup) {
28✔
1020
                    groupItem.children.push(childGroup);
28✔
1021
                }
1022
            } else {
1023
                const filteringExpr = expr as IFilteringExpression;
223✔
1024
                const field = this.fields.find(el => el.field === filteringExpr.fieldName);
600✔
1025

1026
                if (field) {
223✔
1027
                    const exprCopy: IFilteringExpression = { ...filteringExpr };
222✔
1028
                    const operandItem = new ExpressionOperandItem(exprCopy, groupItem);
222✔
1029
                    operandItem.fieldLabel = field.label || field.header || field.field;
222✔
1030
                    groupItem.children.push(operandItem);
222✔
1031
                }
1032
            }
1033
        }
1034

1035
        return groupItem.children.length > 0 ? groupItem : null;
85✔
1036
    }
1037

1038
    private createExpressionTreeFromGroupItem(groupItem: ExpressionGroupItem): FilteringExpressionsTree {
1039
        if (!groupItem) {
81!
1040
            return null;
×
1041
        }
1042

1043
        const expressionTree = new FilteringExpressionsTree(groupItem.operator);
81✔
1044

1045
        for (const item of groupItem.children) {
81✔
1046
            if (item instanceof ExpressionGroupItem) {
112✔
1047
                const subTree = this.createExpressionTreeFromGroupItem((item as ExpressionGroupItem));
26✔
1048
                expressionTree.filteringOperands.push(subTree);
26✔
1049
            } else {
1050
                expressionTree.filteringOperands.push((item as ExpressionOperandItem).expression);
86✔
1051
            }
1052
        }
1053

1054
        return expressionTree;
81✔
1055
    }
1056

1057
    private toggleContextMenu() {
1058
        const contextualGroup = this.findSingleSelectedGroup();
223✔
1059

1060
        if (contextualGroup || this.selectedExpressions.length > 1) {
223✔
1061
            this.contextualGroup = contextualGroup;
21✔
1062

1063
            if (contextualGroup) {
21✔
1064
                this.filteringLogics = [
13✔
1065
                    {
1066
                        label: this.resourceStrings.igx_query_builder_filter_operator_and,
1067
                        selected: contextualGroup.operator === FilteringLogic.And
1068
                    },
1069
                    {
1070
                        label: this.resourceStrings.igx_query_builder_filter_operator_or,
1071
                        selected: contextualGroup.operator === FilteringLogic.Or
1072
                    }
1073
                ];
1074
            }
1075
        } else if (this.contextMenuToggle) {
202✔
1076
            this.contextMenuToggle.close();
111✔
1077
        }
1078
    }
1079

1080
    private findSingleSelectedGroup(): ExpressionGroupItem {
1081
        for (const group of this._selectedGroups) {
292✔
1082
            const containsAllSelectedExpressions = this.selectedExpressions.every(op => this.isInsideGroup(op, group));
114✔
1083

1084
            if (containsAllSelectedExpressions) {
43✔
1085
                return group;
43✔
1086
            }
1087
        }
1088

1089
        return null;
249✔
1090
    }
1091

1092
    private isInsideGroup(item: ExpressionItem, group: ExpressionGroupItem): boolean {
1093
        if (!item) {
180!
1094
            return false;
×
1095
        }
1096

1097
        if (item.parent === group) {
180✔
1098
            return true;
114✔
1099
        }
1100

1101
        return this.isInsideGroup(item.parent, group);
66✔
1102
    }
1103

1104
    private deleteItem(expressionItem: ExpressionItem) {
1105
        if (!expressionItem.parent) {
23✔
1106
            this.rootGroup = null;
6✔
1107
            this.currentGroup = null;
6✔
1108
            this._expressionTree = null;
6✔
1109
            return;
6✔
1110
        }
1111

1112
        if (expressionItem === this.currentGroup) {
17!
1113
            this.currentGroup = this.currentGroup.parent;
×
1114
        }
1115

1116
        const children = expressionItem.parent.children;
17✔
1117
        const index = children.indexOf(expressionItem);
17✔
1118
        children.splice(index, 1);
17✔
1119
        this._expressionTree = this.createExpressionTreeFromGroupItem(this.rootGroup);
17✔
1120

1121
        if (!children.length) {
17✔
1122
            this.deleteItem(expressionItem.parent);
6✔
1123
        }
1124

1125
        this.expressionTreeChange.emit();
17✔
1126
    }
1127

1128
    private createGroup(operator: FilteringLogic) {
1129
        const chips = this.chips.toArray();
3✔
1130
        const minIndex = this.selectedExpressions.reduce((i, e) => Math.min(i, chips.findIndex(c => c.data === e)), Number.MAX_VALUE);
11✔
1131
        const firstExpression = chips[minIndex].data;
3✔
1132

1133
        const parent = firstExpression.parent;
3✔
1134
        const groupItem = new ExpressionGroupItem(operator, parent);
3✔
1135

1136
        const index = parent.children.indexOf(firstExpression);
3✔
1137
        parent.children.splice(index, 0, groupItem);
3✔
1138

1139
        for (const expr of this.selectedExpressions) {
3✔
1140
            groupItem.children.push(expr);
6✔
1141
            this.deleteItem(expr);
6✔
1142
            expr.parent = groupItem;
6✔
1143
        }
1144

1145
        this.clearSelection();
3✔
1146
    }
1147

1148
    private toggleGroup(groupItem: ExpressionGroupItem) {
1149
        this.exitOperandEdit();
18✔
1150
        if (groupItem.children && groupItem.children.length) {
18✔
1151
            this.toggleGroupRecursive(groupItem, !groupItem.selected);
17✔
1152
            if (!groupItem.selected) {
17✔
1153
                this.deselectParentRecursive(groupItem);
4✔
1154
            }
1155
            this.toggleContextMenu();
17✔
1156
        }
1157
    }
1158

1159
    private toggleGroupRecursive(groupItem: ExpressionGroupItem, selected: boolean) {
1160
        if (groupItem.selected !== selected) {
27✔
1161
            groupItem.selected = selected;
27✔
1162

1163
            if (groupItem.selected) {
27✔
1164
                this._selectedGroups.push(groupItem);
19✔
1165
            } else {
1166
                const index = this._selectedGroups.indexOf(groupItem);
8✔
1167
                this._selectedGroups.splice(index, 1);
8✔
1168
            }
1169
        }
1170

1171
        for (const expr of groupItem.children) {
27✔
1172
            if (expr instanceof ExpressionGroupItem) {
54✔
1173
                this.toggleGroupRecursive(expr, selected);
10✔
1174
            } else {
1175
                const operandExpression = expr as ExpressionOperandItem;
44✔
1176
                if (operandExpression.selected !== selected) {
44✔
1177
                    this.toggleExpression(operandExpression);
44✔
1178
                }
1179
            }
1180
        }
1181
    }
1182

1183
    private deselectParentRecursive(expressionItem: ExpressionItem) {
1184
        const parent = expressionItem.parent;
56✔
1185
        if (parent) {
56✔
1186
            if (parent.selected) {
35!
1187
                parent.selected = false;
×
1188
                const index = this._selectedGroups.indexOf(parent);
×
1189
                this._selectedGroups.splice(index, 1);
×
1190
            }
1191
            this.deselectParentRecursive(parent);
35✔
1192
        }
1193
    }
1194

1195
    private calculateContextMenuTarget() {
1196
        const containerRect = this.expressionsContainer.nativeElement.getBoundingClientRect();
36✔
1197
        const chips = this.chips.filter(c => this.selectedExpressions.indexOf(c.data) !== -1);
150✔
1198
        let minTop = chips.reduce((t, c) =>
36✔
1199
            Math.min(t, c.nativeElement.getBoundingClientRect().top), Number.MAX_VALUE);
96✔
1200
        minTop = Math.max(containerRect.top, minTop);
36✔
1201
        minTop = Math.min(containerRect.bottom, minTop);
36✔
1202
        let maxRight = chips.reduce((r, c) =>
36✔
1203
            Math.max(r, c.nativeElement.getBoundingClientRect().right), 0);
96✔
1204
        maxRight = Math.max(maxRight, containerRect.left);
36✔
1205
        maxRight = Math.min(maxRight, containerRect.right);
36✔
1206
        this._overlaySettings.target = new Point(maxRight, minTop);
36✔
1207
    }
1208

1209
    private scrollElementIntoView(target: HTMLElement) {
1210
        const container = this.expressionsContainer.nativeElement;
98✔
1211
        const targetOffset = target.offsetTop - container.offsetTop;
98✔
1212
        const delta = 10;
98✔
1213

1214
        if (container.scrollTop + delta > targetOffset) {
98✔
1215
            container.scrollTop = targetOffset - delta;
2✔
1216
        } else if (container.scrollTop + container.clientHeight < targetOffset + target.offsetHeight + delta) {
96✔
1217
            container.scrollTop = targetOffset + target.offsetHeight + delta - container.clientHeight;
3✔
1218
        }
1219
    }
1220

1221
    private init() {
1222
        this.clearSelection();
113✔
1223
        this.cancelOperandAdd();
113✔
1224
        this.cancelOperandEdit();
113✔
1225
        this.rootGroup = this.createExpressionGroupItem(this.expressionTree);
113✔
1226
        this.currentGroup = this.rootGroup;
113✔
1227
    }
1228

1229
    private registerSVGIcons(): void {
1230
        const editorIcons = editor as any[];
534✔
1231
        editorIcons.forEach(icon => this.iconService.addSvgIconFromText(icon.name, icon.value, 'imx-icons'));
32,040✔
1232
    }
1233
}
1234

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