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

IgniteUI / igniteui-angular / 11349003465

15 Oct 2024 03:10PM UTC coverage: 91.609% (+0.03%) from 91.576%
11349003465

push

github

web-flow
fix(grid): nav service cdr & event emit while row edit (#14735)

* test(grid): add test for row edit nav service handling/events

* fix(grid): nav service cdr & event emit while row edit

* fix(grid): nav service cdr & event emit while row edit

---------

Co-authored-by: Radoslav Karaivanov <rkaraivanov@infragistics.com>
Co-authored-by: Desislava Dincheva <34240583+ddincheva@users.noreply.github.com>

12911 of 15123 branches covered (85.37%)

4 of 4 new or added lines in 2 files covered. (100.0%)

143 existing lines in 9 files now uncovered.

26212 of 28613 relevant lines covered (91.61%)

33835.34 hits per line

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

89.66
/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;
388✔
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);
124✔
82
        this.operator = operator;
124✔
83
        this.children = [];
124✔
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);
264✔
100
        this.expression = expression;
264✔
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';
93✔
127

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

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

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

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

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

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

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

172
        this.init();
115✔
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;
153✔
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;
186✔
190
        // if value is invalid, set it back to _localeId
191
        try {
186✔
192
            getLocaleFirstDayOfWeek(this._locale);
186✔
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,531✔
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();
93✔
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) ||
270✔
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;
270✔
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) ||
270!
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;
270✔
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) ||
270✔
285
            (value && this._currentGroupButtonsContainer && this._currentGroupButtonsContainer.nativeElement !== value.nativeElement)) {
286
            requestAnimationFrame(() => {
85✔
287
                this.scrollElementIntoView(value.nativeElement);
50✔
288
            });
289
        }
290

291
        this._currentGroupButtonsContainer = value;
270✔
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[] = [];
93✔
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 = {
93✔
355
        scrollStrategy: new AbsoluteScrollStrategy(),
356
        modal: false,
357
        closeOnOutsideClick: false
358
    };
359

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

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

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

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

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

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

414
    /**
415
     * @hidden @internal
416
     */
417
    public ngOnDestroy(): void {
418
        this.destroy$.next(true);
93✔
419
        this.destroy$.complete();
93✔
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;
93✔
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,761✔
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) {
36✔
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);
36✔
532
        this.expressionTreeChange.emit();
36✔
533
    }
534

535
    /**
536
     * @hidden @internal
537
     */
538
    public cancelOperandAdd() {
539
        if (this._addModeExpression) {
259✔
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) {
122✔
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) {
131✔
575
            return;
126✔
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,683✔
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) {
184✔
667
            group.selected = false;
5✔
668
        }
669
        this._selectedGroups = [];
184✔
670

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

676
        this.toggleContextMenu();
184✔
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;
11✔
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);
17✔
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
        const selectedGroup = this.contextualGroup;
1✔
771
        const parent = selectedGroup.parent;
1✔
772
        if (parent) {
1!
773
            const index = parent.children.indexOf(selectedGroup);
1✔
774
            parent.children.splice(index, 1);
1✔
775
        } else {
776
            this.rootGroup = null;
×
777
        }
778

779
        this.clearSelection();
1✔
780
        this.commitOperandEdit();
1✔
781
    }
782

783
    /**
784
     * @hidden @internal
785
     */
786
    public selectFilteringLogic(event: IButtonGroupEventArgs) {
787
        this.contextualGroup.operator = event.index as FilteringLogic;
3✔
788
        this.commitOperandEdit();
3✔
789
    }
790

791
    /**
792
     * @hidden @internal
793
     */
794
    public getConditionFriendlyName(name: string): string {
795
        return this.resourceStrings[`igx_query_builder_filter_${name}`] || name;
8,904!
796
    }
797

798
    /**
799
     * @hidden @internal
800
     */
801
    public isDate(value: any) {
802
        return value instanceof Date;
1,760✔
803
    }
804

805
    /**
806
     * @hidden @internal
807
     */
808
    public onExpressionsScrolled() {
809
        if (!this.contextMenuToggle.collapsed) {
×
810
            this.calculateContextMenuTarget();
×
811
            this.contextMenuToggle.reposition();
×
812
        }
813
    }
814

815
    /**
816
     * @hidden @internal
817
     */
818
    public invokeClick(eventArgs: KeyboardEvent) {
819
        if (this.platform.isActivationKey(eventArgs)) {
4✔
820
            eventArgs.preventDefault();
4✔
821
            (eventArgs.currentTarget as HTMLElement).click();
4✔
822
        }
823
    }
824

825
    /**
826
     * @hidden @internal
827
     */
828
    public openPicker(args: KeyboardEvent) {
829
        if (this.platform.isActivationKey(args)) {
×
830
            args.preventDefault();
×
831
            this.picker.open();
×
832
        }
833
    }
834

835
    /**
836
     * @hidden @internal
837
     */
838
    public onOutletPointerDown(event) {
839
        // This prevents closing the select's dropdown when clicking the scroll
840
        event.preventDefault();
×
841
    }
842

843
    /**
844
     * @hidden @internal
845
     */
846
    public getConditionList(): string[] {
847
        return this.selectedField ? this.selectedField.filters.conditionList() : [];
616✔
848
    }
849

850
    /**
851
     * @hidden @internal
852
     */
853
    public getFormatter(field: string) {
854
        return this.fields.find(el => el.field === field).formatter;
20✔
855
    }
856

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

864
    /**
865
     * @hidden @internal
866
     *
867
     * used by the grid
868
     */
869
    public setAddButtonFocus() {
870
        if (this.addRootAndGroupButton) {
86✔
871
            this.addRootAndGroupButton.nativeElement.focus();
10✔
872
        } else if (this.addConditionButton) {
76✔
873
            this.addConditionButton.nativeElement.focus();
43✔
874
        }
875
    }
876

877
    /**
878
     * @hidden @internal
879
     */
880
    public context(expression: ExpressionItem, afterExpression?: ExpressionItem) {
881
        return {
4,549✔
882
            $implicit: expression,
883
            afterExpression
884
        };
885
    }
886

887
    /**
888
     * @hidden @internal
889
     */
890
    public onChipSelectionEnd() {
891
        const contextualGroup = this.findSingleSelectedGroup();
66✔
892
        if (contextualGroup || this.selectedExpressions.length > 1) {
66✔
893
            this.contextualGroup = contextualGroup;
34✔
894
            this.calculateContextMenuTarget();
34✔
895
            if (this.contextMenuToggle.collapsed) {
34✔
896
                this.contextMenuToggle.open(this._overlaySettings);
15✔
897
            } else {
898
                this.contextMenuToggle.reposition();
19✔
899
            }
900
        }
901
    }
902

903
    private setFormat(field: FieldType) {
904
        if (!field.pipeArgs) {
3,795!
905
            field.pipeArgs = { digitsInfo: DEFAULT_PIPE_DIGITS_INFO };
×
906
        }
907

908
        if (!field.pipeArgs.format) {
3,795!
909
            field.pipeArgs.format = field.dataType === DataType.Time ?
×
910
                DEFAULT_PIPE_TIME_FORMAT : field.dataType === DataType.DateTime ?
×
911
                    DEFAULT_PIPE_DATE_TIME_FORMAT : DEFAULT_PIPE_DATE_FORMAT;
912
        }
913
    }
914

915
    private setFilters(field: FieldType) {
916
        if (!field.filters) {
3,795!
UNCOV
917
            switch (field.dataType) {
×
918
                case DataType.Boolean:
919
                    field.filters = IgxBooleanFilteringOperand.instance();
×
UNCOV
920
                    break;
×
921
                case DataType.Number:
922
                case DataType.Currency:
923
                case DataType.Percent:
UNCOV
924
                    field.filters = IgxNumberFilteringOperand.instance();
×
925
                    break;
×
926
                case DataType.Date:
927
                    field.filters = IgxDateFilteringOperand.instance();
×
928
                    break;
×
929
                case DataType.Time:
UNCOV
930
                    field.filters = IgxTimeFilteringOperand.instance();
×
UNCOV
931
                    break;
×
932
                case DataType.DateTime:
933
                    field.filters = IgxDateTimeFilteringOperand.instance();
×
UNCOV
934
                    break;
×
935
                case DataType.String:
936
                default:
UNCOV
937
                    field.filters = IgxStringFilteringOperand.instance();
×
938
                    break;
×
939
            }
940

941
        }
942
    }
943

944
    private onToggleExpression(expressionItem: ExpressionOperandItem) {
945
        this.exitOperandEdit();
24✔
946
        this.toggleExpression(expressionItem);
24✔
947

948
        this.toggleContextMenu();
24✔
949
    }
950

951
    private toggleExpression(expressionItem: ExpressionOperandItem) {
952
        expressionItem.selected = !expressionItem.selected;
66✔
953

954
        if (expressionItem.selected) {
66✔
955
            this.selectedExpressions.push(expressionItem);
49✔
956
        } else {
957
            const index = this.selectedExpressions.indexOf(expressionItem);
17✔
958
            this.selectedExpressions.splice(index, 1);
17✔
959
            this.deselectParentRecursive(expressionItem);
17✔
960
        }
961
    }
962

963
    private addGroup(operator: FilteringLogic, parent?: ExpressionGroupItem, afterExpression?: ExpressionItem) {
964
        this.cancelOperandAdd();
41✔
965

966
        const groupItem = new ExpressionGroupItem(operator, parent);
41✔
967

968
        if (parent) {
41✔
969
            if (afterExpression) {
3✔
970
                const index = parent.children.indexOf(afterExpression);
1✔
971
                parent.children.splice(index + 1, 0, groupItem);
1✔
972
            } else {
973
                parent.children.push(groupItem);
2✔
974
            }
975
        } else {
976
            this.rootGroup = groupItem;
38✔
977
        }
978

979
        this.addCondition(groupItem);
41✔
980
        this.currentGroup = groupItem;
41✔
981
    }
982

983
    private createExpressionGroupItem(expressionTree: IExpressionTree, parent?: ExpressionGroupItem): ExpressionGroupItem | null {
984
        if (!expressionTree) {
140✔
985
            return null;
60✔
986
        }
987

988
        const groupItem = new ExpressionGroupItem(expressionTree.operator, parent);
80✔
989

990
        for (const expr of expressionTree.filteringOperands) {
80✔
991
            if (expr instanceof FilteringExpressionsTree) {
242✔
992
                const childGroup = this.createExpressionGroupItem(expr, groupItem);
25✔
993
                if (childGroup) {
25✔
994
                    groupItem.children.push(childGroup);
25✔
995
                }
996
            } else {
997
                const filteringExpr = expr as IFilteringExpression;
217✔
998
                const field = this.fields.find(el => el.field === filteringExpr.fieldName);
587✔
999

1000
                if (field) {
217✔
1001
                    const exprCopy: IFilteringExpression = { ...filteringExpr };
216✔
1002
                    const operandItem = new ExpressionOperandItem(exprCopy, groupItem);
216✔
1003
                    operandItem.fieldLabel = field.label || field.header || field.field;
216✔
1004
                    groupItem.children.push(operandItem);
216✔
1005
                }
1006
            }
1007
        }
1008

1009
        return groupItem.children.length > 0 ? groupItem : null;
80✔
1010
    }
1011

1012
    private createExpressionTreeFromGroupItem(groupItem: ExpressionGroupItem): FilteringExpressionsTree {
1013
        if (!groupItem) {
79!
UNCOV
1014
            return null;
×
1015
        }
1016

1017
        const expressionTree = new FilteringExpressionsTree(groupItem.operator);
79✔
1018

1019
        for (const item of groupItem.children) {
79✔
1020
            if (item instanceof ExpressionGroupItem) {
109✔
1021
                const subTree = this.createExpressionTreeFromGroupItem((item as ExpressionGroupItem));
26✔
1022
                expressionTree.filteringOperands.push(subTree);
26✔
1023
            } else {
1024
                expressionTree.filteringOperands.push((item as ExpressionOperandItem).expression);
83✔
1025
            }
1026
        }
1027

1028
        return expressionTree;
79✔
1029
    }
1030

1031
    private toggleContextMenu() {
1032
        const contextualGroup = this.findSingleSelectedGroup();
224✔
1033

1034
        if (contextualGroup || this.selectedExpressions.length > 1) {
224✔
1035
            this.contextualGroup = contextualGroup;
20✔
1036

1037
            if (contextualGroup) {
20✔
1038
                this.filteringLogics = [
12✔
1039
                    {
1040
                        label: this.resourceStrings.igx_query_builder_filter_operator_and,
1041
                        selected: contextualGroup.operator === FilteringLogic.And
1042
                    },
1043
                    {
1044
                        label: this.resourceStrings.igx_query_builder_filter_operator_or,
1045
                        selected: contextualGroup.operator === FilteringLogic.Or
1046
                    }
1047
                ];
1048
            }
1049
        } else if (this.contextMenuToggle) {
204✔
1050
            this.contextMenuToggle.close();
111✔
1051
        }
1052
    }
1053

1054
    private findSingleSelectedGroup(): ExpressionGroupItem {
1055
        for (const group of this._selectedGroups) {
290✔
1056
            const containsAllSelectedExpressions = this.selectedExpressions.every(op => this.isInsideGroup(op, group));
108✔
1057

1058
            if (containsAllSelectedExpressions) {
40✔
1059
                return group;
40✔
1060
            }
1061
        }
1062

1063
        return null;
250✔
1064
    }
1065

1066
    private isInsideGroup(item: ExpressionItem, group: ExpressionGroupItem): boolean {
1067
        if (!item) {
162!
UNCOV
1068
            return false;
×
1069
        }
1070

1071
        if (item.parent === group) {
162✔
1072
            return true;
108✔
1073
        }
1074

1075
        return this.isInsideGroup(item.parent, group);
54✔
1076
    }
1077

1078
    private deleteItem(expressionItem: ExpressionItem) {
1079
        if (!expressionItem.parent) {
23✔
1080
            this.rootGroup = null;
6✔
1081
            this.currentGroup = null;
6✔
1082
            this._expressionTree = null;
6✔
1083
            return;
6✔
1084
        }
1085

1086
        if (expressionItem === this.currentGroup) {
17!
UNCOV
1087
            this.currentGroup = this.currentGroup.parent;
×
1088
        }
1089

1090
        const children = expressionItem.parent.children;
17✔
1091
        const index = children.indexOf(expressionItem);
17✔
1092
        children.splice(index, 1);
17✔
1093
        this._expressionTree = this.createExpressionTreeFromGroupItem(this.rootGroup);
17✔
1094

1095
        if (!children.length) {
17✔
1096
            this.deleteItem(expressionItem.parent);
6✔
1097
        }
1098

1099
        this.expressionTreeChange.emit();
17✔
1100
    }
1101

1102
    private createGroup(operator: FilteringLogic) {
1103
        const chips = this.chips.toArray();
3✔
1104
        const minIndex = this.selectedExpressions.reduce((i, e) => Math.min(i, chips.findIndex(c => c.data === e)), Number.MAX_VALUE);
11✔
1105
        const firstExpression = chips[minIndex].data;
3✔
1106

1107
        const parent = firstExpression.parent;
3✔
1108
        const groupItem = new ExpressionGroupItem(operator, parent);
3✔
1109

1110
        const index = parent.children.indexOf(firstExpression);
3✔
1111
        parent.children.splice(index, 0, groupItem);
3✔
1112

1113
        for (const expr of this.selectedExpressions) {
3✔
1114
            groupItem.children.push(expr);
6✔
1115
            this.deleteItem(expr);
6✔
1116
            expr.parent = groupItem;
6✔
1117
        }
1118

1119
        this.clearSelection();
3✔
1120
    }
1121

1122
    private toggleGroup(groupItem: ExpressionGroupItem) {
1123
        this.exitOperandEdit();
17✔
1124
        if (groupItem.children && groupItem.children.length) {
17✔
1125
            this.toggleGroupRecursive(groupItem, !groupItem.selected);
16✔
1126
            if (!groupItem.selected) {
16✔
1127
                this.deselectParentRecursive(groupItem);
4✔
1128
            }
1129
            this.toggleContextMenu();
16✔
1130
        }
1131
    }
1132

1133
    private toggleGroupRecursive(groupItem: ExpressionGroupItem, selected: boolean) {
1134
        if (groupItem.selected !== selected) {
24✔
1135
            groupItem.selected = selected;
24✔
1136

1137
            if (groupItem.selected) {
24✔
1138
                this._selectedGroups.push(groupItem);
16✔
1139
            } else {
1140
                const index = this._selectedGroups.indexOf(groupItem);
8✔
1141
                this._selectedGroups.splice(index, 1);
8✔
1142
            }
1143
        }
1144

1145
        for (const expr of groupItem.children) {
24✔
1146
            if (expr instanceof ExpressionGroupItem) {
50✔
1147
                this.toggleGroupRecursive(expr, selected);
8✔
1148
            } else {
1149
                const operandExpression = expr as ExpressionOperandItem;
42✔
1150
                if (operandExpression.selected !== selected) {
42✔
1151
                    this.toggleExpression(operandExpression);
42✔
1152
                }
1153
            }
1154
        }
1155
    }
1156

1157
    private deselectParentRecursive(expressionItem: ExpressionItem) {
1158
        const parent = expressionItem.parent;
56✔
1159
        if (parent) {
56✔
1160
            if (parent.selected) {
35!
UNCOV
1161
                parent.selected = false;
×
UNCOV
1162
                const index = this._selectedGroups.indexOf(parent);
×
UNCOV
1163
                this._selectedGroups.splice(index, 1);
×
1164
            }
1165
            this.deselectParentRecursive(parent);
35✔
1166
        }
1167
    }
1168

1169
    private calculateContextMenuTarget() {
1170
        const containerRect = this.expressionsContainer.nativeElement.getBoundingClientRect();
34✔
1171
        const chips = this.chips.filter(c => this.selectedExpressions.indexOf(c.data) !== -1);
144✔
1172
        let minTop = chips.reduce((t, c) =>
34✔
1173
            Math.min(t, c.nativeElement.getBoundingClientRect().top), Number.MAX_VALUE);
92✔
1174
        minTop = Math.max(containerRect.top, minTop);
34✔
1175
        minTop = Math.min(containerRect.bottom, minTop);
34✔
1176
        let maxRight = chips.reduce((r, c) =>
34✔
1177
            Math.max(r, c.nativeElement.getBoundingClientRect().right), 0);
92✔
1178
        maxRight = Math.max(maxRight, containerRect.left);
34✔
1179
        maxRight = Math.min(maxRight, containerRect.right);
34✔
1180
        this._overlaySettings.target = new Point(maxRight, minTop);
34✔
1181
    }
1182

1183
    private scrollElementIntoView(target: HTMLElement) {
1184
        const container = this.expressionsContainer.nativeElement;
100✔
1185
        const targetOffset = target.offsetTop - container.offsetTop;
100✔
1186
        const delta = 10;
100✔
1187

1188
        if (container.scrollTop + delta > targetOffset) {
100✔
1189
            container.scrollTop = targetOffset - delta;
2✔
1190
        } else if (container.scrollTop + container.clientHeight < targetOffset + target.offsetHeight + delta) {
98✔
1191
            container.scrollTop = targetOffset + target.offsetHeight + delta - container.clientHeight;
3✔
1192
        }
1193
    }
1194

1195
    private init() {
1196
        this.clearSelection();
115✔
1197
        this.cancelOperandAdd();
115✔
1198
        this.cancelOperandEdit();
115✔
1199
        this.rootGroup = this.createExpressionGroupItem(this.expressionTree);
115✔
1200
        this.currentGroup = this.rootGroup;
115✔
1201
    }
1202

1203
    private registerSVGIcons(): void {
1204
        const editorIcons = editor as any[];
555✔
1205

1206
        editorIcons.forEach((icon) => {
555✔
1207
            this.iconService.addSvgIconFromText(icon.name, icon.value, 'imx-icons');
33,300✔
1208
        });
1209
    }
1210
}
1211

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