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

IgniteUI / igniteui-angular / 4387408864

pending completion
4387408864

push

github

GitHub
fix(adv-filter): commit edits via the context menu (#12722)

14979 of 17621 branches covered (85.01%)

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

26702 of 28895 relevant lines covered (92.41%)

29815.9 hits per line

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

84.98
/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 { CommonModule, getLocaleFirstDayOfWeek } from '@angular/common';
3
import { Inject } from '@angular/core';
4
import {
5
    Component, Input, ViewChild, ChangeDetectorRef, ViewChildren, QueryList, ElementRef, OnDestroy, HostBinding, NgModule
6
} from '@angular/core';
7
import { FormsModule } from '@angular/forms';
8
import { editor } from '@igniteui/material-icons-extended';
9
import { Subject } from 'rxjs';
10
import { IButtonGroupEventArgs, IgxButtonGroupModule } from '../buttonGroup/buttonGroup.component';
11
import { IgxChipComponent } from '../chips/chip.component';
12
import { IgxChipsModule } from '../chips/chips.module';
13
import { DisplayDensityBase, DisplayDensityToken, IDisplayDensityOptions } from '../core/displayDensity';
14
import { IQueryBuilderResourceStrings } from '../core/i18n/query-builder-resources';
15
import { CurrentResourceStrings } from '../core/i18n/resources';
16
import { PlatformUtil } from '../core/utils';
17
import { DataType, DataUtil } from '../data-operations/data-util';
18
import { IgxBooleanFilteringOperand, IgxDateFilteringOperand, IgxDateTimeFilteringOperand, IgxNumberFilteringOperand, IgxStringFilteringOperand, IgxTimeFilteringOperand } from '../data-operations/filtering-condition';
19
import { FilteringLogic, IFilteringExpression } from '../data-operations/filtering-expression.interface';
20
import { FilteringExpressionsTree, IExpressionTree } from '../data-operations/filtering-expressions-tree';
21
import { IgxDatePickerComponent } from '../date-picker/date-picker.component';
22
import { IgxDatePickerModule } from '../date-picker/date-picker.module';
23
import { IgxButtonModule } from '../directives/button/button.directive';
24
import { IgxDateTimeEditorModule } from '../directives/date-time-editor/date-time-editor.directive';
25
import { IgxDragDropModule } from '../directives/drag-drop/drag-drop.directive';
26
import { IgxOverlayOutletDirective, IgxToggleDirective, IgxToggleModule } from '../directives/toggle/toggle.directive';
27
import { FieldType } from '../grids/common/grid.interface';
28
import { IActiveNode } from '../grids/grid-navigation.service';
29
import { IgxIconModule, IgxIconService } from '../icon/public_api';
30
import { IgxInputGroupModule } from '../input-group/public_api';
31
import { IgxSelectComponent } from '../select/select.component';
32
import { IgxSelectModule } from '../select/select.module';
33
import { HorizontalAlignment, OverlaySettings, Point, VerticalAlignment } from '../services/overlay/utilities';
2✔
34
import { AbsoluteScrollStrategy, AutoPositionStrategy, CloseScrollStrategy, ConnectedPositioningStrategy } from '../services/public_api';
2✔
35
import { IgxTimePickerComponent, IgxTimePickerModule } from '../time-picker/time-picker.component';
2✔
36
import { IgxQueryBuilderHeaderComponent } from './query-builder-header.component';
2✔
37

2✔
38
const DEFAULT_PIPE_DATE_FORMAT = 'mediumDate';
2✔
39
const DEFAULT_PIPE_TIME_FORMAT = 'mediumTime';
2✔
40
const DEFAULT_PIPE_DATE_TIME_FORMAT = 'medium';
41
const DEFAULT_PIPE_DIGITS_INFO = '1.0-3';
×
42
const DEFAULT_DATE_TIME_FORMAT = 'dd/MM/yyyy HH:mm:ss tt';
43
const DEFAULT_TIME_FORMAT = 'hh:mm:ss tt';
44

2✔
45
@Pipe({ name: 'fieldFormatter' })
46
export class IgxFieldFormatterPipe implements PipeTransform {
47

48
    public transform(value: any, formatter: (v: any, data: any, fieldData?: any) => any, rowData: any, fieldData?: any) {
49
        return formatter(value, rowData, fieldData);
50
    }
347✔
51
}
52

53
/**
54
 * @hidden @internal
55
 */
103✔
56
class ExpressionItem {
103✔
57
    public parent: ExpressionGroupItem;
103✔
58
    public selected: boolean;
59
    constructor(parent?: ExpressionGroupItem) {
60
        this.parent = parent;
61
    }
62
}
244✔
63

244✔
64
/**
65
 * @hidden @internal
66
 */
2✔
67
class ExpressionGroupItem extends ExpressionItem {
68
    public operator: FilteringLogic;
80✔
69
    public children: ExpressionItem[];
80✔
70
    constructor(operator: FilteringLogic, parent?: ExpressionGroupItem) {
80✔
71
        super(parent);
80✔
72
        this.operator = operator;
80✔
73
        this.children = [];
80✔
74
    }
80✔
75
}
80✔
76

77
/**
78
 * @hidden @internal
79
 */
80✔
80
class ExpressionOperandItem extends ExpressionItem {
81
    public expression: IFilteringExpression;
82
    public inEditMode: boolean;
83
    public inAddMode: boolean;
80✔
84
    public hovered: boolean;
85
    public fieldLabel: string;
86
    constructor(expression: IFilteringExpression, parent: ExpressionGroupItem) {
87
        super(parent);
80✔
88
        this.expression = expression;
89
    }
90
}
91

80✔
92
/**
93
 * A component used for operating with complex filters by creating or editing conditions
94
 * and grouping them using AND/OR logic.
95
 * It is used internally in the Advanced Filtering of the Grid.
96
 *
97
 * @example
98
 * ```html
99
 * <igx-query-builder [fields]="this.fields">
80✔
100
 * </igx-query-builder>
101
 * ```
102
 */
103
@Component({
104
    selector: 'igx-query-builder',
80✔
105
    templateUrl: './query-builder.component.html',
80✔
106
})
80✔
107
export class IgxQueryBuilderComponent extends DisplayDensityBase implements AfterViewInit, OnDestroy {
80✔
108
    /**
80✔
109
     * @hidden @internal
110
     */
111
    @HostBinding('class.igx-query-builder')
112
    public cssClass = 'igx-query-builder';
80✔
113

114
    /**
115
     * @hidden @internal
116
     */
80✔
117
    @ViewChild('fieldSelect', { read: IgxSelectComponent })
118
    public fieldSelect: IgxSelectComponent;
119

120
    /**
121
     * @hidden @internal
122
     */
80✔
123
    @ViewChild('conditionSelect', { read: IgxSelectComponent })
124
    public conditionSelect: IgxSelectComponent;
125

231✔
126
    /**
127
     * @hidden @internal
46✔
128
     */
42✔
129
    @ViewChild('searchValueInput', { read: ElementRef })
130
    public searchValueInput: ElementRef;
131

231✔
132
    /**
133
     * @hidden @internal
134
     */
135
    @ViewChild('picker')
136
    public picker: IgxDatePickerComponent | IgxTimePickerComponent;
137

×
138
    /**
139
     * @hidden @internal
140
     */
231!
141
    @ViewChild('addRootAndGroupButton', { read: ElementRef })
142
    public addRootAndGroupButton: ElementRef;
5✔
143

2✔
144
    /**
145
     * @hidden @internal
146
     */
231✔
147
    @ViewChild('addConditionButton', { read: ElementRef })
148
    public addConditionButton: ElementRef;
149

150
    /**
151
     * @hidden @internal
152
     */
×
153
    @ContentChild(IgxQueryBuilderHeaderComponent)
154
    public headerContent: IgxQueryBuilderHeaderComponent;
155

231✔
156
    /**
157
     * @hidden @internal
74✔
158
     */
62✔
159
    @ViewChild('editingInputsContainer', { read: ElementRef })
160
    public set editingInputsContainer(value: ElementRef) {
161
        if ((value && !this._editingInputsContainer) ||
231✔
162
            (value && this._editingInputsContainer && this._editingInputsContainer.nativeElement !== value.nativeElement)) {
163
            requestAnimationFrame(() => {
164
                this.scrollElementIntoView(value.nativeElement);
165
            });
166
        }
167

×
168
        this._editingInputsContainer = value;
169
    }
170

171
    /**
172
     * @hidden @internal
173
     */
80✔
174
    public get editingInputsContainer(): ElementRef {
80✔
175
        return this._editingInputsContainer;
80✔
176
    }
177

178
    /**
179
     * @hidden @internal
180
     */
181
    @ViewChild('addModeContainer', { read: ElementRef })
80✔
182
    public set addModeContainer(value: ElementRef) {
80✔
183
        if ((value && !this._addModeContainer) ||
184
            (value && this._addModeContainer && this._addModeContainer.nativeElement !== value.nativeElement)) {
185
            requestAnimationFrame(() => {
186
                this.scrollElementIntoView(value.nativeElement);
187
            });
188
        }
79✔
189

79✔
190
        this._addModeContainer = value;
78✔
191
    }
78✔
192

2✔
193
    /**
2✔
194
     * @hidden @internal
2✔
195
     */
196
    public get addModeContainer(): ElementRef {
197
        return this._addModeContainer;
198
    }
199

200
    /**
201
     * @hidden @internal
202
     */
23,011✔
203
    @ViewChild('currentGroupButtonsContainer', { read: ElementRef })
204
    public set currentGroupButtonsContainer(value: ElementRef) {
205
        if ((value && !this._currentGroupButtonsContainer) ||
206
            (value && this._currentGroupButtonsContainer && this._currentGroupButtonsContainer.nativeElement !== value.nativeElement)) {
207
            requestAnimationFrame(() => {
208
                this.scrollElementIntoView(value.nativeElement);
1,207✔
209
            });
210
        }
211

827✔
212
        this._currentGroupButtonsContainer = value;
827!
213
    }
827✔
214

827✔
215
    /**
4,929✔
216
     * @hidden @internal
4,929✔
217
     */
218
    public get currentGroupButtonsContainer(): ElementRef {
219
        return this._currentGroupButtonsContainer;
220
    }
221

222
    /**
223
     * @hidden @internal
224
     */
119✔
225
    @ViewChild(IgxToggleDirective)
226
    public contextMenuToggle: IgxToggleDirective;
227

92✔
228
    /**
92✔
229
     * @hidden @internal
230
     */
231
    @ViewChildren(IgxChipComponent)
138✔
232
    public chips: QueryList<IgxChipComponent>;
233

234
    /**
235
     * @hidden @internal
236
     */
237
    @HostBinding('style.display')
238
    public display = 'block';
160✔
239

240
    /**
160✔
241
     * @hidden @internal
160✔
242
     */
243
    @ViewChild('expressionsContainer')
244
    protected expressionsContainer: ElementRef;
×
245

246
    /**
247
     * @hidden @internal
248
     */
×
249
     @ViewChild('overlayOutlet', { read: IgxOverlayOutletDirective, static: true })
250
     protected overlayOutlet: IgxOverlayOutletDirective;
251

252
    /**
253
     * @hidden @internal
254
     */
28,177✔
255
    public rootGroup: ExpressionGroupItem;
256

257
    /**
258
     * @hidden @internal
259
     */
260
    public selectedExpressions: ExpressionOperandItem[] = [];
80✔
261

262
    /**
263
     * @hidden @internal
264
     */
265
    public selectedGroups: ExpressionGroupItem[] = [];
266

3✔
267
    /**
268
     * @hidden @internal
269
     */
270
    public currentGroup: ExpressionGroupItem;
271

272
    /**
4,511✔
273
     * @hidden @internal
274
     */
275
    public editedExpression: ExpressionOperandItem;
276

277
    /**
278
     * @hidden @internal
41✔
279
     */
41✔
280
    public addModeExpression: ExpressionOperandItem;
281

282
    /**
283
     * @hidden @internal
284
     */
285
    public contextualGroup: ExpressionGroupItem;
41✔
286

2✔
287
    /**
2✔
288
     * @hidden @internal
289
     */
290
    public filteringLogics;
39✔
291

292
    /**
41✔
293
     * @hidden @internal
294
     */
295
    public selectedCondition: string;
296

297
    /**
298
     * @hidden @internal
30✔
299
     */
300
    public searchValue: any;
301

302
    /**
303
     * @hidden @internal
304
     */
4✔
305
    public lastActiveNode = {} as IActiveNode;
306

307
    /**
308
     * @hidden @internal
309
     */
310
     public pickerOutlet: IgxOverlayOutletDirective | ElementRef;
1✔
311

312
    /**
313
     * @hidden @internal
314
     */
315
    public fieldSelectOverlaySettings: OverlaySettings = {
316
        scrollStrategy: new AbsoluteScrollStrategy(),
45✔
317
        modal: false,
28✔
318
        closeOnOutsideClick: false
28✔
319
    };
28✔
320

28!
321
    /**
322
     * @hidden @internal
28✔
323
     */
324
    public conditionSelectOverlaySettings: OverlaySettings = {
325
        scrollStrategy: new AbsoluteScrollStrategy(),
28✔
326
        modal: false,
28✔
327
        closeOnOutsideClick: false
328
    };
45✔
329

45✔
330
    private destroy$ = new Subject<any>();
331
    private _selectedField: FieldType;
332
    private _clickTimer;
333
    private _dblClickDelay = 200;
334
    private _preventChipClick = false;
335
    private _editingInputsContainer: ElementRef;
214✔
336
    private _addModeContainer: ElementRef;
4✔
337
    private _currentGroupButtonsContainer: ElementRef;
4✔
338
    private _fields: FieldType[];
339
    private _expressionTree: IExpressionTree;
340
    private _locale;
341
    private _resourceStrings = CurrentResourceStrings.QueryBuilderResStrings;
342

343
    private _positionSettings = {
344
        horizontalStartPoint: HorizontalAlignment.Right,
99✔
345
        verticalStartPoint: VerticalAlignment.Top
7✔
346
    };
7✔
347

6✔
348
    constructor(public cdr: ChangeDetectorRef,
349
        protected iconService: IgxIconService,
7✔
350
        protected platform: PlatformUtil,
351
        @Inject(LOCALE_ID) protected _localeId: string,
352
        @Optional() @Inject(DisplayDensityToken) protected _displayDensityOptions?: IDisplayDensityOptions) {
353
        super(_displayDensityOptions);
354
        this.locale = this.locale || this._localeId;
355
    }
356

992✔
357
    /**
358
     * @hidden @internal
359
     */
360
    public ngAfterViewInit(): void {
361
        this.overlaySettings.outlet = this.overlayOutlet;
362
        this.fieldSelectOverlaySettings.outlet = this.overlayOutlet;
363
        this.conditionSelectOverlaySettings.outlet = this.overlayOutlet;
117✔
364
    }
112✔
365

366
    /**
5✔
367
     * @hidden @internal
1✔
368
     */
369
    public ngOnDestroy(): void {
370
        this.destroy$.next(true);
4✔
371
        this.destroy$.complete();
372
    }
373

374
    /**
375
     * @hidden @internal
376
     */
377
    public set selectedField(value: FieldType) {
4,659✔
378
        const oldValue = this._selectedField;
379

380
        if (this._selectedField !== value) {
381
            this._selectedField = value;
382
            if (oldValue && this._selectedField && this._selectedField.dataType !== oldValue.dataType) {
383
                this.selectedCondition = null;
3✔
384
                this.searchValue = null;
385
                this.cdr.detectChanges();
386
            }
387
        }
388
    }
389

22✔
390
    /**
22!
391
     * @hidden @internal
22✔
392
     */
393
    public get selectedField(): FieldType {
22✔
394
        return this._selectedField;
395
    }
396

397
    /**
398
    * Returns the fields.
399
    */
400
    public get fields(): FieldType[] {
4✔
401
        return this._fields;
4✔
402
    }
4✔
403

404
    /**
405
     * An @Input property that sets the fields.
406
     */
407
    @Input()
408
    public set fields(fields: FieldType[]) {
46✔
409
        this._fields = fields;
46✔
410

46✔
411
        if (this._fields) {
46!
412
            this.registerSVGIcons();
×
413

414
            this._fields.forEach(field => {
46✔
415
                this.setFilters(field);
46✔
416
                this.setFormat(field);
12✔
417
            });
46✔
418
        }
419
    }
46✔
420

46✔
421
    /**
46✔
422
    * Returns the expression tree.
46✔
423
    */
46✔
424
     public get expressionTree(): IExpressionTree {
46✔
425
        return this._expressionTree;
46✔
426
    }
46✔
427

46✔
428
    /**
46✔
429
     * An @Input property that sets the expression tree.
46✔
430
     */
41✔
431
    @Input()
432
    public set expressionTree(expressionTree: IExpressionTree) {
5!
433
        this._expressionTree = expressionTree;
×
434

435
        this.init();
436
    }
5!
437

5✔
438
    /**
439
     * Gets the `locale` of the query builder.
440
     * If not set, defaults to application's locale.
441
     */
442
    @Input()
443
    public get locale(): string {
444
        return this._locale;
152✔
445
    }
5✔
446

447
    /**
152✔
448
     * Sets the `locale` of the query builder.
152✔
449
     * Expects a valid BCP 47 language tag.
20✔
450
     */
451
    public set locale(value: string) {
152✔
452
        this._locale = value;
152✔
453
        // if value is invalid, set it back to _localeId
454
        try {
455
            getLocaleFirstDayOfWeek(this._locale);
456
        } catch (e) {
457
            this._locale = this._localeId;
458
        }
5✔
459
    }
5✔
460

5!
461
    /**
×
462
     * Sets the resource strings.
463
     * By default it uses EN resources.
5✔
464
     */
5✔
465
    @Input()
5!
466
    public set resourceStrings(value: IQueryBuilderResourceStrings) {
×
467
        this._resourceStrings = Object.assign({}, this._resourceStrings, value);
468
    }
469

470
    /**
471
     * Returns the resource strings.
472
     */
473
    public get resourceStrings(): IQueryBuilderResourceStrings {
6✔
474
        return this._resourceStrings;
475
    }
476

477
    /**
478
     * Event fired as the expression tree is changed.
479
     *
1✔
480
     * ```html
1✔
481
     *  <igx-query-builder (expressionTreeChange)='onExpressionTreeChange()'></igx-query-builder>
1!
482
     * ```
1✔
483
     */
484
    @Output()
485
    public expressionTreeChange = new EventEmitter();
486

487
    /**
488
     * @hidden @internal
489
     */
1✔
490
    public overlaySettings: OverlaySettings = {
491
        closeOnOutsideClick: false,
492
        modal: false,
493
        positionStrategy: new ConnectedPositioningStrategy(this._positionSettings),
494
        scrollStrategy: new CloseScrollStrategy()
495
    };
1✔
496

497
    /**
498
     * @hidden @internal
499
     */
500
    public setPickerOutlet(outlet?: IgxOverlayOutletDirective | ElementRef) {
501
        this.pickerOutlet = outlet;
1✔
502
    }
2✔
503

504
    /**
1✔
505
     * @hidden @internal
506
     */
507
    public get isContextMenuVisible(): boolean {
508
        return !this.contextMenuToggle.collapsed;
509
    }
510

17✔
511
    /**
512
     * @hidden @internal
513
     */
514
    public get hasEditedExpression(): boolean {
515
        return this.editedExpression !== undefined && this.editedExpression !== null;
516
    }
1✔
517

1✔
518
    /**
1!
519
     * @hidden @internal
1✔
520
     */
1✔
521
    public addCondition(parent: ExpressionGroupItem, afterExpression?: ExpressionItem) {
1✔
522
        this.cancelOperandAdd();
2✔
523

524
        const operandItem = new ExpressionOperandItem({
525
            fieldName: null,
1✔
526
            condition: null,
1✔
527
            ignoreCase: true,
528
            searchVal: null
529
        }, parent);
530

531
        if (afterExpression) {
532
            const index = parent.children.indexOf(afterExpression);
1✔
533
            parent.children.splice(index + 1, 0, operandItem);
1✔
534
        } else {
1!
535
            parent.children.push(operandItem);
1✔
536
        }
1✔
537

538
        this.enterExpressionEdit(operandItem);
539
    }
×
540

541
    /**
1✔
542
     * @hidden @internal
1✔
543
     */
544
    public addAndGroup(parent?: ExpressionGroupItem, afterExpression?: ExpressionItem) {
545
        this.addGroup(FilteringLogic.And, parent, afterExpression);
546
    }
547

548
    /**
15✔
549
     * @hidden @internal
15✔
550
     */
551
    public addOrGroup(parent?: ExpressionGroupItem, afterExpression?: ExpressionItem) {
552
        this.addGroup(FilteringLogic.Or, parent, afterExpression);
553
    }
554

555
    /**
15,224!
556
     * @hidden @internal
557
     */
558
    public endGroup(groupItem: ExpressionGroupItem) {
559
        this.currentGroup = groupItem.parent;
560
    }
561

3,184✔
562
    /**
563
     * @hidden @internal
564
     */
565
    public commitOperandEdit() {
566
        if (this.editedExpression) {
567
            this.editedExpression.expression.fieldName = this.selectedField.field;
×
568
            this.editedExpression.expression.condition = this.selectedField.filters.condition(this.selectedCondition);
×
569
            this.editedExpression.expression.searchVal = DataUtil.parseValue(this.selectedField.dataType, this.searchValue);
×
570
            this.editedExpression.fieldLabel = this.selectedField.label
571
                ? this.selectedField.label
572
                : this.selectedField.header
573
                    ? this.selectedField.header
574
                    : this.selectedField.field;
575
            this.editedExpression.inEditMode = false;
576
            this.editedExpression = null;
4!
577
        }
4✔
578

4✔
579
        this._expressionTree = this.createExpressionTreeFromGroupItem(this.rootGroup);
580
        this.expressionTreeChange.emit();
581
    }
582

583
    /**
584
     * @hidden @internal
585
     */
×
586
    public cancelOperandAdd() {
×
587
        if (this.addModeExpression) {
×
588
            this.addModeExpression.inAddMode = false;
589
            this.addModeExpression = null;
590
        }
591
    }
592

593
    /**
594
     * @hidden @internal
595
     */
×
596
    public cancelOperandEdit() {
597
        if (this.editedExpression) {
598
            this.editedExpression.inEditMode = false;
599

600
            if (!this.editedExpression.expression.fieldName) {
601
                this.deleteItem(this.editedExpression);
987✔
602
            }
603

604
            this.editedExpression = null;
605
        }
606
    }
607

30✔
608
    /**
609
     * @hidden @internal
610
     */
611
    public operandCanBeCommitted(): boolean {
612
        return this.selectedField && this.selectedCondition &&
613
            (!!this.searchValue || this.selectedField.filters.condition(this.selectedCondition).isUnary);
30✔
614
    }
615

616
    /**
617
     * @hidden @internal
618
     */
619
    public exitOperandEdit() {
73✔
620
        if (!this.editedExpression) {
8✔
621
            return;
622
        }
65✔
623

58✔
624
        if (this.operandCanBeCommitted()) {
625
            this.commitOperandEdit();
626
        } else {
627
            this.cancelOperandEdit();
628
        }
629
    }
630

7,641✔
631
    /**
632
     * @hidden @internal
633
     */
634
    public isExpressionGroup(expression: ExpressionItem): boolean {
635
        return expression instanceof ExpressionGroupItem;
636
    }
637

638
    /**
639
     * @hidden @internal
13✔
640
     */
13✔
641
    public onChipRemove(expressionItem: ExpressionItem) {
11✔
642
        this.deleteItem(expressionItem);
11✔
643
    }
11✔
644

7✔
645
    /**
646
     * @hidden @internal
647
     */
4✔
648
    public onChipClick(expressionItem: ExpressionOperandItem) {
649
        this._clickTimer = setTimeout(() => {
650
            if (!this._preventChipClick) {
651
                this.onToggleExpression(expressionItem);
652
            }
4,929!
653
            this._preventChipClick = false;
×
654
        }, this._dblClickDelay);
655
    }
4,929!
656

×
657
    /**
×
658
     * @hidden @internal
659
     */
660
    public onChipDblClick(expressionItem: ExpressionOperandItem) {
4,929!
661
        clearTimeout(this._clickTimer);
×
662
        this._preventChipClick = true;
663
        this.enterExpressionEdit(expressionItem);
4,929!
664
    }
×
665

666
    /**
667
     * @hidden @internal
668
     */
4,929!
669
    public enterExpressionEdit(expressionItem: ExpressionOperandItem) {
×
670
        this.clearSelection();
671
        this.exitOperandEdit();
×
672
        this.cancelOperandAdd();
×
673

674
        if (this.editedExpression) {
675
            this.editedExpression.inEditMode = false;
676
        }
×
677

×
678
        expressionItem.hovered = false;
679

×
680
        this.selectedField = expressionItem.expression.fieldName ?
×
681
            this.fields.find(field => field.field === expressionItem.expression.fieldName) : null;
682
        this.selectedCondition = expressionItem.expression.condition ?
×
683
            expressionItem.expression.condition.name : null;
×
684
        this.searchValue = expressionItem.expression.searchVal;
685

×
686
        expressionItem.inEditMode = true;
×
687
        this.editedExpression = expressionItem;
688

689
        this.cdr.detectChanges();
×
690

×
691
        this.fieldSelectOverlaySettings.target = this.fieldSelect.element;
692
        this.fieldSelectOverlaySettings.excludeFromOutsideClick = [this.fieldSelect.element as HTMLElement];
693
        this.fieldSelectOverlaySettings.positionStrategy = new AutoPositionStrategy();
694
        this.conditionSelectOverlaySettings.target = this.conditionSelect.element;
695
        this.conditionSelectOverlaySettings.excludeFromOutsideClick = [this.conditionSelect.element as HTMLElement];
22✔
696
        this.conditionSelectOverlaySettings.positionStrategy = new AutoPositionStrategy();
22✔
697

22✔
698
        if (!this.selectedField) {
699
            this.fieldSelect.input.nativeElement.focus();
700
            } else if (this.selectedField.filters.condition(this.selectedCondition).isUnary) {
64✔
701
                this.conditionSelect.input.nativeElement.focus();
64✔
702
        } else {
47✔
703
            const input = this.searchValueInput?.nativeElement || this.picker?.getEditElement();
704
            input.focus();
705
        }
17✔
706
    }
17✔
707

17✔
708
    /**
709
     * @hidden @internal
710
     */
711
    public clearSelection() {
34✔
712
        for (const group of this.selectedGroups) {
34✔
713
            group.selected = false;
34✔
714
        }
3✔
715
        this.selectedGroups = [];
1✔
716

1✔
717
        for (const expr of this.selectedExpressions) {
718
            expr.selected = false;
719
        }
2✔
720
        this.selectedExpressions = [];
721

722
        this.toggleContextMenu();
723
    }
31✔
724

725
    /**
34✔
726
     * @hidden @internal
34✔
727
     */
728
    public enterExpressionAdd(expressionItem: ExpressionOperandItem) {
729
        this.clearSelection();
730
        this.exitOperandEdit();
116✔
731

67✔
732
        if (this.addModeExpression) {
67✔
733
            this.addModeExpression.inAddMode = false;
227✔
734
        }
24✔
735

736
        expressionItem.inAddMode = true;
737
        this.addModeExpression = expressionItem;
203✔
738
        if (expressionItem.selected) {
203✔
739
            this.toggleExpression(expressionItem);
740
        }
741
    }
742

743
    /**
744
     * @hidden @internal
203✔
745
     */
539✔
746
    public contextMenuClosed() {
203✔
747
        this.contextualGroup = null;
203✔
748
    }
749

750
    /**
751
     * @hidden @internal
116✔
752
     */
753
    public onKeyDown(eventArgs: KeyboardEvent) {
754
        eventArgs.stopPropagation();
755
        const key = eventArgs.key;
756
        if (!this.contextMenuToggle.collapsed && (key === this.platform.KEYMAP.ESCAPE)) {
757
            this.clearSelection();
94!
758
        }
×
759
    }
760

94✔
761
    /**
94✔
762
     * @hidden @internal
140✔
763
     */
34✔
764
    public createAndGroup() {
34✔
765
        this.createGroup(FilteringLogic.And);
766
    }
767

106✔
768
    /**
769
     * @hidden @internal
770
     */
94✔
771
    public createOrGroup() {
772
        this.createGroup(FilteringLogic.Or);
773
    }
190✔
774

190✔
775
    /**
19✔
776
     * @hidden @internal
19✔
777
     */
12✔
778
    public deleteFilters() {
779
        for (const expr of this.selectedExpressions) {
780
            this.deleteItem(expr);
781
        }
782

783
        this.clearSelection();
784
    }
785

786
    /**
787
     * @hidden @internal
788
     */
789
    public onGroupClick(groupItem: ExpressionGroupItem) {
171✔
790
        this.toggleGroup(groupItem);
91✔
791
    }
792

793
    /**
794
     * @hidden @internal
203✔
795
     */
40✔
796
    public ungroup() {
17!
797
        const selectedGroup = this.contextualGroup;
17✔
798
        const parent = selectedGroup.parent;
799
        if (parent) {
800
            const index = parent.children.indexOf(selectedGroup);
186✔
801
            parent.children.splice(index, 1, ...selectedGroup.children);
802

803
            for (const expr of selectedGroup.children) {
54!
804
                expr.parent = parent;
×
805
            }
806
        }
54✔
807

40✔
808
        this.clearSelection();
809
        this.commitOperandEdit();
14✔
810
    }
811

812
    /**
21✔
813
     * @hidden @internal
6✔
814
     */
6✔
815
    public deleteGroup() {
6✔
816
        const selectedGroup = this.contextualGroup;
6✔
817
        const parent = selectedGroup.parent;
818
        if (parent) {
15!
819
            const index = parent.children.indexOf(selectedGroup);
×
820
            parent.children.splice(index, 1);
821
        } else {
15✔
822
            this.rootGroup = null;
15✔
823
        }
15✔
824

15✔
825
        this.clearSelection();
15✔
826
        this.commitOperandEdit();
6✔
827
    }
828

15✔
829
    /**
830
     * @hidden @internal
831
     */
2✔
832
    public selectFilteringLogic(event: IButtonGroupEventArgs) {
8✔
833
        this.contextualGroup.operator = event.index as FilteringLogic;
2✔
834
        this.commitOperandEdit();
2✔
835
    }
2✔
836

2✔
837
    /**
2✔
838
     * @hidden @internal
2✔
839
     */
4✔
840
    public getConditionFriendlyName(name: string): string {
4✔
841
        return this.resourceStrings[`igx_query_builder_filter_${name}`] || name;
4✔
842
    }
843

2✔
844
    /**
845
     * @hidden @internal
846
     */
17✔
847
    public isDate(value: any) {
17✔
848
        return value instanceof Date;
16✔
849
    }
16✔
850

4✔
851
    /**
852
     * @hidden @internal
16✔
853
     */
854
    public onExpressionsScrolled() {
855
        if (!this.contextMenuToggle.collapsed) {
856
            this.calculateContextMenuTarget();
24!
857
            this.contextMenuToggle.reposition();
24✔
858
        }
24✔
859
    }
16✔
860

861
    /**
862
     * @hidden @internal
8✔
863
     */
8✔
864
    public invokeClick(eventArgs: KeyboardEvent) {
865
        if (this.platform.isActivationKey(eventArgs)) {
866
            eventArgs.preventDefault();
24✔
867
            (eventArgs.currentTarget as HTMLElement).click();
50✔
868
        }
8✔
869
    }
870

871
    /**
42✔
872
     * @hidden @internal
42!
873
     */
42✔
874
    public openPicker(args: KeyboardEvent) {
875
        if (this.platform.isActivationKey(args)) {
876
            args.preventDefault();
877
            this.picker.open();
878
        }
879
    }
56✔
880

56✔
881
    /**
35!
882
     * @hidden @internal
×
883
     */
×
884
    public onOutletPointerDown(event) {
×
885
        // This prevents closing the select's dropdown when clicking the scroll
886
        event.preventDefault();
35✔
887
    }
888

889
    /**
890
     * @hidden @internal
11✔
891
     */
44✔
892
    public getConditionList(): string[] {
24✔
893
        return this.selectedField ? this.selectedField.filters.conditionList() : [];
11✔
894
    }
11✔
895

24✔
896
    /**
11✔
897
     * @hidden @internal
11✔
898
     */
11✔
899
    public getFormatter(field: string) {
900
        return this.fields.find(el => el.field === field).formatter;
901
    }
106✔
902

106✔
903
    /**
106✔
904
     * @hidden @internal
106✔
905
     */
3✔
906
    public getFormat(field: string) {
907
        return this.fields.find(el => el.field === field).pipeArgs.format;
103✔
908
    }
3✔
909

910
    /**
911
     * @hidden @internal
912
     */
92✔
913
    public setAddButtonFocus() {
92✔
914
        if (this.addRootAndGroupButton) {
92✔
915
            this.addRootAndGroupButton.nativeElement.focus();
92✔
916
        } else if (this.addConditionButton) {
92✔
917
            this.addConditionButton.nativeElement.focus();
918
        }
919
    }
827✔
920

49,620✔
921
    /**
922
     * @hidden @internal
2✔
923
     */
924
    public context(expression: ExpressionItem, afterExpression?: ExpressionItem) {
925
        return {
926
            $implicit: expression,
927
            afterExpression
928
        };
929
    }
2✔
930

931
    /**
932
     * @hidden @internal
933
     */
934
    public onChipSelectionEnd() {
935
        const contextualGroup = this.findSingleSelectedGroup();
936
        if (contextualGroup || this.selectedExpressions.length > 1) {
937
            this.contextualGroup = contextualGroup;
938
            this.calculateContextMenuTarget();
939
            if (this.contextMenuToggle.collapsed) {
940
                this.contextMenuToggle.open(this.overlaySettings);
941
            } else {
942
                this.contextMenuToggle.reposition();
943
            }
944
        }
945
    }
946

947
    private setFormat(field: FieldType) {
948
        if (!field.pipeArgs) {
949
            field.pipeArgs = { digitsInfo: DEFAULT_PIPE_DIGITS_INFO };
950
        }
951

952
        if (!field.pipeArgs.format) {
953
            field.pipeArgs.format = field.dataType === DataType.Time ?
2✔
954
                DEFAULT_PIPE_TIME_FORMAT : field.dataType === DataType.DateTime ?
955
                    DEFAULT_PIPE_DATE_TIME_FORMAT : DEFAULT_PIPE_DATE_FORMAT;
956
        }
957

958
        if (!field.defaultDateTimeFormat) {
959
            field.defaultDateTimeFormat = DEFAULT_DATE_TIME_FORMAT;
960
        }
2✔
961

962
        if (!field.defaultTimeFormat) {
2✔
963
            field.defaultTimeFormat = DEFAULT_TIME_FORMAT;
964
        }
965
    }
966

967
    private setFilters(field: FieldType) {
968
        if (!field.filters) {
969
            switch (field.dataType) {
970
                case DataType.Boolean:
971
                    field.filters = IgxBooleanFilteringOperand.instance();
972
                    break;
973
                case DataType.Number:
974
                case DataType.Currency:
975
                case DataType.Percent:
976
                    field.filters = IgxNumberFilteringOperand.instance();
977
                    break;
978
                case DataType.Date:
979
                    field.filters = IgxDateFilteringOperand.instance();
980
                    break;
981
                case DataType.Time:
982
                    field.filters = IgxTimeFilteringOperand.instance();
983
                    break;
984
                case DataType.DateTime:
985
                    field.filters = IgxDateTimeFilteringOperand.instance();
986
                    break;
987
                case DataType.String:
988
                default:
989
                    field.filters = IgxStringFilteringOperand.instance();
990
                    break;
991
            }
992

993
        }
994
    }
995

996
    private onToggleExpression(expressionItem: ExpressionOperandItem) {
997
        this.exitOperandEdit();
998
        this.toggleExpression(expressionItem);
999

1000
        this.toggleContextMenu();
1001
    }
1002

1003
    private toggleExpression(expressionItem: ExpressionOperandItem) {
1004
        expressionItem.selected = !expressionItem.selected;
1005

1006
        if (expressionItem.selected) {
1007
            this.selectedExpressions.push(expressionItem);
1008
        } else {
1009
            const index = this.selectedExpressions.indexOf(expressionItem);
1010
            this.selectedExpressions.splice(index, 1);
1011
            this.deselectParentRecursive(expressionItem);
1012
        }
1013
    }
1014

1015
    private addGroup(operator: FilteringLogic, parent?: ExpressionGroupItem, afterExpression?: ExpressionItem) {
1016
        this.cancelOperandAdd();
1017

1018
        const groupItem = new ExpressionGroupItem(operator, parent);
1019

1020
        if (parent) {
1021
            if (afterExpression) {
1022
                const index = parent.children.indexOf(afterExpression);
1023
                parent.children.splice(index + 1, 0, groupItem);
1024
            } else {
1025
                parent.children.push(groupItem);
1026
            }
1027
        } else {
1028
            this.rootGroup = groupItem;
1029
        }
1030

1031
        this.addCondition(groupItem);
1032
        this.currentGroup = groupItem;
1033
    }
1034

1035
    private createExpressionGroupItem(expressionTree: IExpressionTree, parent?: ExpressionGroupItem): ExpressionGroupItem {
1036
        let groupItem: ExpressionGroupItem;
1037
        if (expressionTree) {
1038
            groupItem = new ExpressionGroupItem(expressionTree.operator, parent);
1039

1040
            for (const expr of expressionTree.filteringOperands) {
1041
                if (expr instanceof FilteringExpressionsTree) {
1042
                    groupItem.children.push(this.createExpressionGroupItem(expr, groupItem));
1043
                } else {
1044
                    const filteringExpr = expr as IFilteringExpression;
1045
                    const exprCopy: IFilteringExpression = {
1046
                        fieldName: filteringExpr.fieldName,
1047
                        condition: filteringExpr.condition,
1048
                        searchVal: filteringExpr.searchVal,
1049
                        ignoreCase: filteringExpr.ignoreCase
1050
                    };
1051
                    const operandItem = new ExpressionOperandItem(exprCopy, groupItem);
1052
                    const field = this.fields.find(el => el.field === filteringExpr.fieldName);
1053
                    operandItem.fieldLabel = field.label || field.header || field.field;
1054
                    groupItem.children.push(operandItem);
1055
                }
1056
            }
1057
        }
1058

1059
        return groupItem;
1060
    }
1061

1062
    /**
1063
     * @hidden @internal
1064
     */
1065
    public createExpressionTreeFromGroupItem(groupItem: ExpressionGroupItem): FilteringExpressionsTree {
1066
        if (!groupItem) {
1067
            return null;
1068
        }
1069

1070
        const expressionTree = new FilteringExpressionsTree(groupItem.operator);
1071

1072
        for (const item of groupItem.children) {
1073
            if (item instanceof ExpressionGroupItem) {
1074
                const subTree = this.createExpressionTreeFromGroupItem((item as ExpressionGroupItem));
1075
                expressionTree.filteringOperands.push(subTree);
1076
            } else {
1077
                expressionTree.filteringOperands.push((item as ExpressionOperandItem).expression);
1078
            }
1079
        }
1080

1081
        return expressionTree;
1082
    }
1083

1084
    private toggleContextMenu() {
1085
        const contextualGroup = this.findSingleSelectedGroup();
1086

1087
        if (contextualGroup || this.selectedExpressions.length > 1) {
1088
            this.contextualGroup = contextualGroup;
1089

1090
            if (contextualGroup) {
1091
                this.filteringLogics = [
1092
                    {
1093
                        label: this.resourceStrings.igx_query_builder_filter_operator_and,
1094
                        selected: contextualGroup.operator === FilteringLogic.And
1095
                    },
1096
                    {
1097
                        label: this.resourceStrings.igx_query_builder_filter_operator_or,
1098
                        selected: contextualGroup.operator === FilteringLogic.Or
1099
                    }
1100
                ];
1101
            }
1102
        } else if (this.contextMenuToggle) {
1103
            this.contextMenuToggle.close();
1104
        }
1105
    }
1106

1107
    private findSingleSelectedGroup(): ExpressionGroupItem {
1108
        for (const group of this.selectedGroups) {
1109
            const containsAllSelectedExpressions = this.selectedExpressions.every(op => this.isInsideGroup(op, group));
1110

1111
            if (containsAllSelectedExpressions) {
1112
                return group;
1113
            }
1114
        }
1115

1116
        return null;
1117
    }
1118

1119
    private isInsideGroup(item: ExpressionItem, group: ExpressionGroupItem): boolean {
1120
        if (!item) {
1121
            return false;
1122
        }
1123

1124
        if (item.parent === group) {
1125
            return true;
1126
        }
1127

1128
        return this.isInsideGroup(item.parent, group);
1129
    }
1130

1131
    private deleteItem(expressionItem: ExpressionItem) {
1132
        if (!expressionItem.parent) {
1133
            this.rootGroup = null;
1134
            this.currentGroup = null;
1135
            this._expressionTree = null;
1136
            return;
1137
        }
1138

1139
        if (expressionItem === this.currentGroup) {
1140
            this.currentGroup = this.currentGroup.parent;
1141
        }
1142

1143
        const children = expressionItem.parent.children;
1144
        const index = children.indexOf(expressionItem);
1145
        children.splice(index, 1);
1146
        this._expressionTree = this.createExpressionTreeFromGroupItem(this.rootGroup);
1147

1148
        if (!children.length) {
1149
            this.deleteItem(expressionItem.parent);
1150
        }
1151

1152
        this.expressionTreeChange.emit();
1153
    }
1154

1155
    private createGroup(operator: FilteringLogic) {
1156
        const chips = this.chips.toArray();
1157
        const minIndex = this.selectedExpressions.reduce((i, e) => Math.min(i, chips.findIndex(c => c.data === e)), Number.MAX_VALUE);
1158
        const firstExpression = chips[minIndex].data;
1159

1160
        const parent = firstExpression.parent;
1161
        const groupItem = new ExpressionGroupItem(operator, parent);
1162

1163
        const index = parent.children.indexOf(firstExpression);
1164
        parent.children.splice(index, 0, groupItem);
1165

1166
        for (const expr of this.selectedExpressions) {
1167
            this.deleteItem(expr);
1168
            groupItem.children.push(expr);
1169
            expr.parent = groupItem;
1170
        }
1171

1172
        this.clearSelection();
1173
    }
1174

1175
    private toggleGroup(groupItem: ExpressionGroupItem) {
1176
        this.exitOperandEdit();
1177
        if (groupItem.children && groupItem.children.length) {
1178
            this.toggleGroupRecursive(groupItem, !groupItem.selected);
1179
            if (!groupItem.selected) {
1180
                this.deselectParentRecursive(groupItem);
1181
            }
1182
            this.toggleContextMenu();
1183
        }
1184
    }
1185

1186
    private toggleGroupRecursive(groupItem: ExpressionGroupItem, selected: boolean) {
1187
        if (groupItem.selected !== selected) {
1188
            groupItem.selected = selected;
1189

1190
            if (groupItem.selected) {
1191
                this.selectedGroups.push(groupItem);
1192
            } else {
1193
                const index = this.selectedGroups.indexOf(groupItem);
1194
                this.selectedGroups.splice(index, 1);
1195
            }
1196
        }
1197

1198
        for (const expr of groupItem.children) {
1199
            if (expr instanceof ExpressionGroupItem) {
1200
                this.toggleGroupRecursive(expr, selected);
1201
            } else {
1202
                const operandExpression = expr as ExpressionOperandItem;
1203
                if (operandExpression.selected !== selected) {
1204
                    this.toggleExpression(operandExpression);
1205
                }
1206
            }
1207
        }
1208
    }
1209

1210
    private deselectParentRecursive(expressionItem: ExpressionItem) {
1211
        const parent = expressionItem.parent;
1212
        if (parent) {
1213
            if (parent.selected) {
1214
                parent.selected = false;
1215
                const index = this.selectedGroups.indexOf(parent);
1216
                this.selectedGroups.splice(index, 1);
1217
            }
1218
            this.deselectParentRecursive(parent);
1219
        }
1220
    }
1221

1222
    private calculateContextMenuTarget() {
1223
        const containerRect = this.expressionsContainer.nativeElement.getBoundingClientRect();
1224
        const chips = this.chips.filter(c => this.selectedExpressions.indexOf(c.data) !== -1);
1225
        let minTop = chips.reduce((t, c) =>
1226
            Math.min(t, c.nativeElement.getBoundingClientRect().top), Number.MAX_VALUE);
1227
        minTop = Math.max(containerRect.top, minTop);
1228
        minTop = Math.min(containerRect.bottom, minTop);
1229
        let maxRight = chips.reduce((r, c) =>
1230
            Math.max(r, c.nativeElement.getBoundingClientRect().right), 0);
1231
        maxRight = Math.max(maxRight, containerRect.left);
1232
        maxRight = Math.min(maxRight, containerRect.right);
1233
        this.overlaySettings.target = new Point(maxRight, minTop);
1234
    }
1235

1236
    private scrollElementIntoView(target: HTMLElement) {
1237
        const container = this.expressionsContainer.nativeElement;
1238
        const targetOffset = target.offsetTop - container.offsetTop;
1239
        const delta = 10;
1240

1241
        if (container.scrollTop + delta > targetOffset) {
1242
            container.scrollTop = targetOffset - delta;
1243
        } else if (container.scrollTop + container.clientHeight < targetOffset + target.offsetHeight + delta) {
1244
            container.scrollTop = targetOffset + target.offsetHeight + delta - container.clientHeight;
1245
        }
1246
    }
1247

1248
    private init() {
1249
        this.clearSelection();
1250
        this.cancelOperandAdd();
1251
        this.cancelOperandEdit();
1252
        this.rootGroup = this.createExpressionGroupItem(this.expressionTree);
1253
        this.currentGroup = this.rootGroup;
1254
    }
1255

1256
    private registerSVGIcons(): void {
1257
        const editorIcons = editor as any[];
1258
        editorIcons.forEach(icon => this.iconService.addSvgIconFromText(icon.name, icon.value, 'imx-icons'));
1259
    }
1260
}
1261

1262
/**
1263
 * @hidden
1264
 */
1265
@NgModule({
1266
    declarations: [IgxQueryBuilderComponent, IgxQueryBuilderHeaderComponent, IgxFieldFormatterPipe],
1267
    exports: [IgxQueryBuilderComponent, IgxQueryBuilderHeaderComponent],
1268
    imports: [
1269
        CommonModule,
1270
        FormsModule,
1271
        IgxButtonModule,
1272
        IgxButtonGroupModule,
1273
        IgxDatePickerModule,
1274
        IgxDateTimeEditorModule,
1275
        IgxInputGroupModule,
1276
        IgxTimePickerModule,
1277
        IgxChipsModule,
1278
        IgxDragDropModule,
1279
        IgxIconModule,
1280
        IgxSelectModule,
1281
        IgxToggleModule
1282
    ]
1283
})
1284
export class IgxQueryBuilderModule { }
1285

STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc