import { AfterViewInit, ContentChild, EventEmitter, LOCALE_ID, Optional, Output, Pipe, PipeTransform } from '@angular/core'; import { getLocaleFirstDayOfWeek, NgIf, NgFor, NgTemplateOutlet, NgClass, DatePipe } from '@angular/common'; import { Inject } from '@angular/core'; import { Component, Input, ViewChild, ChangeDetectorRef, ViewChildren, QueryList, ElementRef, OnDestroy, HostBinding } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { Subject } from 'rxjs'; import { editor } from '@igniteui/material-icons-extended'; import { IButtonGroupEventArgs, IgxButtonGroupComponent } from '../buttonGroup/buttonGroup.component'; import { IgxChipComponent } from '../chips/chip.component'; import { DisplayDensityBase, DisplayDensityToken, IDisplayDensityOptions } from '../core/density'; import { IQueryBuilderResourceStrings } from '../core/i18n/query-builder-resources'; import { CurrentResourceStrings } from '../core/i18n/resources'; import { PlatformUtil } from '../core/utils'; import { DataType, DataUtil } from '../data-operations/data-util'; import { IgxBooleanFilteringOperand, IgxDateFilteringOperand, IgxDateTimeFilteringOperand, IgxNumberFilteringOperand, IgxStringFilteringOperand, IgxTimeFilteringOperand } from '../data-operations/filtering-condition'; import { FilteringLogic, IFilteringExpression } from '../data-operations/filtering-expression.interface'; import { FilteringExpressionsTree, IExpressionTree } from '../data-operations/filtering-expressions-tree'; import { IgxDatePickerComponent } from '../date-picker/date-picker.component'; import { IgxButtonDirective } from '../directives/button/button.directive'; import { IgxDateTimeEditorDirective } from '../directives/date-time-editor/date-time-editor.directive'; import { IgxOverlayOutletDirective, IgxToggleDirective } from '../directives/toggle/toggle.directive'; import { FieldType } from '../grids/common/grid.interface'; import { IgxIconService } from '../icon/icon.service'; import { IgxSelectComponent } from '../select/select.component'; import { HorizontalAlignment, OverlaySettings, Point, VerticalAlignment } from '../services/overlay/utilities'; import { AbsoluteScrollStrategy, AutoPositionStrategy, CloseScrollStrategy, ConnectedPositioningStrategy } from '../services/public_api'; import { IgxTimePickerComponent } from '../time-picker/time-picker.component'; import { IgxQueryBuilderHeaderComponent } from './query-builder-header.component'; import { IgxPickerToggleComponent, IgxPickerClearComponent } from '../date-common/picker-icons.common'; import { IgxInputDirective } from '../directives/input/input.directive'; import { IgxInputGroupComponent } from '../input-group/input-group.component'; import { IgxSelectItemComponent } from '../select/select-item.component'; import { IgxSuffixDirective } from '../directives/suffix/suffix.directive'; import { IgxPrefixDirective } from '../directives/prefix/prefix.directive'; import { IgxIconComponent } from '../icon/icon.component'; const DEFAULT_PIPE_DATE_FORMAT = 'mediumDate'; const DEFAULT_PIPE_TIME_FORMAT = 'mediumTime'; const DEFAULT_PIPE_DATE_TIME_FORMAT = 'medium'; const DEFAULT_PIPE_DIGITS_INFO = '1.0-3'; const DEFAULT_DATE_TIME_FORMAT = 'dd/MM/yyyy HH:mm:ss tt'; const DEFAULT_TIME_FORMAT = 'hh:mm:ss tt'; @Pipe({ name: 'fieldFormatter', standalone: true }) export class IgxFieldFormatterPipe implements PipeTransform { public transform(value: any, formatter: (v: any, data: any, fieldData?: any) => any, rowData: any, fieldData?: any) { return formatter(value, rowData, fieldData); } } /** * @hidden @internal * * Internal class usage */ class ExpressionItem { public parent: ExpressionGroupItem; public selected: boolean; constructor(parent?: ExpressionGroupItem) { this.parent = parent; } } /** * @hidden @internal * * Internal class usage */ class ExpressionGroupItem extends ExpressionItem { public operator: FilteringLogic; public children: ExpressionItem[]; constructor(operator: FilteringLogic, parent?: ExpressionGroupItem) { super(parent); this.operator = operator; this.children = []; } } /** * @hidden @internal * * Internal class usage */ class ExpressionOperandItem extends ExpressionItem { public expression: IFilteringExpression; public inEditMode: boolean; public inAddMode: boolean; public hovered: boolean; public fieldLabel: string; constructor(expression: IFilteringExpression, parent: ExpressionGroupItem) { super(parent); this.expression = expression; } } /** * A component used for operating with complex filters by creating or editing conditions * and grouping them using AND/OR logic. * It is used internally in the Advanced Filtering of the Grid. * * @example * ```html * * * ``` */ @Component({ selector: 'igx-query-builder', templateUrl: './query-builder.component.html', standalone: true, 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] }) export class IgxQueryBuilderComponent extends DisplayDensityBase implements AfterViewInit, OnDestroy { /** * @hidden @internal */ @HostBinding('class.igx-query-builder') public cssClass = 'igx-query-builder'; /** * @hidden @internal */ @HostBinding('style.display') public display = 'block'; /** * Returns the fields. */ public get fields(): FieldType[] { return this._fields; } /** * An @Input property that sets the fields. */ @Input() public set fields(fields: FieldType[]) { this._fields = fields; if (this._fields) { this.registerSVGIcons(); this._fields.forEach(field => { this.setFilters(field); this.setFormat(field); }); } } /** * Returns the expression tree. */ public get expressionTree(): IExpressionTree { return this._expressionTree; } /** * An @Input property that sets the expression tree. */ @Input() public set expressionTree(expressionTree: IExpressionTree) { this._expressionTree = expressionTree; this.init(); } /** * Gets the `locale` of the query builder. * If not set, defaults to application's locale. */ @Input() public get locale(): string { return this._locale; } /** * Sets the `locale` of the query builder. * Expects a valid BCP 47 language tag. */ public set locale(value: string) { this._locale = value; // if value is invalid, set it back to _localeId try { getLocaleFirstDayOfWeek(this._locale); } catch (e) { this._locale = this._localeId; } } /** * Sets the resource strings. * By default it uses EN resources. */ @Input() public set resourceStrings(value: IQueryBuilderResourceStrings) { this._resourceStrings = Object.assign({}, this._resourceStrings, value); } /** * Returns the resource strings. */ public get resourceStrings(): IQueryBuilderResourceStrings { return this._resourceStrings; } /** * Event fired as the expression tree is changed. * * ```html * * ``` */ @Output() public expressionTreeChange = new EventEmitter(); @ViewChild('fieldSelect', { read: IgxSelectComponent }) private fieldSelect: IgxSelectComponent; @ViewChild('conditionSelect', { read: IgxSelectComponent }) private conditionSelect: IgxSelectComponent; @ViewChild('searchValueInput', { read: ElementRef }) private searchValueInput: ElementRef; @ViewChild('picker') private picker: IgxDatePickerComponent | IgxTimePickerComponent; @ViewChild('addRootAndGroupButton', { read: ElementRef }) private addRootAndGroupButton: ElementRef; @ViewChild('addConditionButton', { read: ElementRef }) private addConditionButton: ElementRef; /** * @hidden @internal */ @ContentChild(IgxQueryBuilderHeaderComponent) public headerContent: IgxQueryBuilderHeaderComponent; @ViewChild('editingInputsContainer', { read: ElementRef }) protected set editingInputsContainer(value: ElementRef) { if ((value && !this._editingInputsContainer) || (value && this._editingInputsContainer && this._editingInputsContainer.nativeElement !== value.nativeElement)) { requestAnimationFrame(() => { this.scrollElementIntoView(value.nativeElement); }); } this._editingInputsContainer = value; } /** @hidden */ protected get editingInputsContainer(): ElementRef { return this._editingInputsContainer; } @ViewChild('addModeContainer', { read: ElementRef }) protected set addModeContainer(value: ElementRef) { if ((value && !this._addModeContainer) || (value && this._addModeContainer && this._addModeContainer.nativeElement !== value.nativeElement)) { requestAnimationFrame(() => { this.scrollElementIntoView(value.nativeElement); }); } this._addModeContainer = value; } /** @hidden */ protected get addModeContainer(): ElementRef { return this._addModeContainer; } @ViewChild('currentGroupButtonsContainer', { read: ElementRef }) protected set currentGroupButtonsContainer(value: ElementRef) { if ((value && !this._currentGroupButtonsContainer) || (value && this._currentGroupButtonsContainer && this._currentGroupButtonsContainer.nativeElement !== value.nativeElement)) { requestAnimationFrame(() => { this.scrollElementIntoView(value.nativeElement); }); } this._currentGroupButtonsContainer = value; } /** @hidden */ protected get currentGroupButtonsContainer(): ElementRef { return this._currentGroupButtonsContainer; } @ViewChild(IgxToggleDirective) private contextMenuToggle: IgxToggleDirective; @ViewChildren(IgxChipComponent) private chips: QueryList; @ViewChild('expressionsContainer') private expressionsContainer: ElementRef; @ViewChild('overlayOutlet', { read: IgxOverlayOutletDirective, static: true }) private overlayOutlet: IgxOverlayOutletDirective; /** * @hidden @internal */ public rootGroup: ExpressionGroupItem; /** * @hidden @internal */ public selectedExpressions: ExpressionOperandItem[] = []; /** * @hidden @internal */ public currentGroup: ExpressionGroupItem; /** * @hidden @internal */ public contextualGroup: ExpressionGroupItem; /** * @hidden @internal */ public filteringLogics; /** * @hidden @internal */ public selectedCondition: string; /** * @hidden @internal */ public searchValue: any; /** * @hidden @internal */ public pickerOutlet: IgxOverlayOutletDirective | ElementRef; /** * @hidden @internal */ public fieldSelectOverlaySettings: OverlaySettings = { scrollStrategy: new AbsoluteScrollStrategy(), modal: false, closeOnOutsideClick: false }; /** * @hidden @internal */ public conditionSelectOverlaySettings: OverlaySettings = { scrollStrategy: new AbsoluteScrollStrategy(), modal: false, closeOnOutsideClick: false }; private destroy$ = new Subject(); private _selectedField: FieldType; private _clickTimer; private _dblClickDelay = 200; private _preventChipClick = false; private _editingInputsContainer: ElementRef; private _addModeContainer: ElementRef; private _currentGroupButtonsContainer: ElementRef; private _addModeExpression: ExpressionOperandItem; private _editedExpression: ExpressionOperandItem; private _selectedGroups: ExpressionGroupItem[] = []; private _fields: FieldType[]; private _expressionTree: IExpressionTree; private _locale; private _resourceStrings = CurrentResourceStrings.QueryBuilderResStrings; private _positionSettings = { horizontalStartPoint: HorizontalAlignment.Right, verticalStartPoint: VerticalAlignment.Top }; private _overlaySettings: OverlaySettings = { closeOnOutsideClick: false, modal: false, positionStrategy: new ConnectedPositioningStrategy(this._positionSettings), scrollStrategy: new CloseScrollStrategy() }; constructor(public cdr: ChangeDetectorRef, protected iconService: IgxIconService, protected platform: PlatformUtil, @Inject(LOCALE_ID) protected _localeId: string, @Optional() @Inject(DisplayDensityToken) protected _displayDensityOptions?: IDisplayDensityOptions) { super(_displayDensityOptions); this.locale = this.locale || this._localeId; } /** * @hidden @internal */ public ngAfterViewInit(): void { this._overlaySettings.outlet = this.overlayOutlet; this.fieldSelectOverlaySettings.outlet = this.overlayOutlet; this.conditionSelectOverlaySettings.outlet = this.overlayOutlet; } /** * @hidden @internal */ public ngOnDestroy(): void { this.destroy$.next(true); this.destroy$.complete(); } /** * @hidden @internal */ public set selectedField(value: FieldType) { const oldValue = this._selectedField; if (this._selectedField !== value) { this._selectedField = value; if (oldValue && this._selectedField && this._selectedField.dataType !== oldValue.dataType) { this.selectedCondition = null; this.searchValue = null; this.cdr.detectChanges(); } } } /** * @hidden @internal */ public get selectedField(): FieldType { return this._selectedField; } /** * @hidden @internal * * used by the grid */ public setPickerOutlet(outlet?: IgxOverlayOutletDirective | ElementRef) { this.pickerOutlet = outlet; } /** * @hidden @internal * * used by the grid */ public get isContextMenuVisible(): boolean { return !this.contextMenuToggle.collapsed; } /** * @hidden @internal */ public get hasEditedExpression(): boolean { return this._editedExpression !== undefined && this._editedExpression !== null; } /** * @hidden @internal */ public addCondition(parent: ExpressionGroupItem, afterExpression?: ExpressionItem) { this.cancelOperandAdd(); const operandItem = new ExpressionOperandItem({ fieldName: null, condition: null, ignoreCase: true, searchVal: null }, parent); if (afterExpression) { const index = parent.children.indexOf(afterExpression); parent.children.splice(index + 1, 0, operandItem); } else { parent.children.push(operandItem); } this.enterExpressionEdit(operandItem); } /** * @hidden @internal */ public addAndGroup(parent?: ExpressionGroupItem, afterExpression?: ExpressionItem) { this.addGroup(FilteringLogic.And, parent, afterExpression); } /** * @hidden @internal */ public addOrGroup(parent?: ExpressionGroupItem, afterExpression?: ExpressionItem) { this.addGroup(FilteringLogic.Or, parent, afterExpression); } /** * @hidden @internal */ public endGroup(groupItem: ExpressionGroupItem) { this.currentGroup = groupItem.parent; } /** * @hidden @internal */ public commitOperandEdit() { if (this._editedExpression) { this._editedExpression.expression.fieldName = this.selectedField.field; this._editedExpression.expression.condition = this.selectedField.filters.condition(this.selectedCondition); this._editedExpression.expression.searchVal = DataUtil.parseValue(this.selectedField.dataType, this.searchValue); this._editedExpression.fieldLabel = this.selectedField.label ? this.selectedField.label : this.selectedField.header ? this.selectedField.header : this.selectedField.field; this._editedExpression.inEditMode = false; this._editedExpression = null; } this._expressionTree = this.createExpressionTreeFromGroupItem(this.rootGroup); this.expressionTreeChange.emit(); } /** * @hidden @internal */ public cancelOperandAdd() { if (this._addModeExpression) { this._addModeExpression.inAddMode = false; this._addModeExpression = null; } } /** * @hidden @internal */ public cancelOperandEdit() { if (this._editedExpression) { this._editedExpression.inEditMode = false; if (!this._editedExpression.expression.fieldName) { this.deleteItem(this._editedExpression); } this._editedExpression = null; } } /** * @hidden @internal */ public operandCanBeCommitted(): boolean { return this.selectedField && this.selectedCondition && (!!this.searchValue || this.selectedField.filters.condition(this.selectedCondition).isUnary); } /** * @hidden @internal * * used by the grid */ public exitOperandEdit() { if (!this._editedExpression) { return; } if (this.operandCanBeCommitted()) { this.commitOperandEdit(); } else { this.cancelOperandEdit(); } } /** * @hidden @internal */ public isExpressionGroup(expression: ExpressionItem): boolean { return expression instanceof ExpressionGroupItem; } /** * @hidden @internal */ public onChipRemove(expressionItem: ExpressionItem) { this.deleteItem(expressionItem); } /** * @hidden @internal */ public onChipClick(expressionItem: ExpressionOperandItem) { this._clickTimer = setTimeout(() => { if (!this._preventChipClick) { this.onToggleExpression(expressionItem); } this._preventChipClick = false; }, this._dblClickDelay); } /** * @hidden @internal */ public onChipDblClick(expressionItem: ExpressionOperandItem) { clearTimeout(this._clickTimer); this._preventChipClick = true; this.enterExpressionEdit(expressionItem); } /** * @hidden @internal */ public enterExpressionEdit(expressionItem: ExpressionOperandItem) { this.clearSelection(); this.exitOperandEdit(); this.cancelOperandAdd(); if (this._editedExpression) { this._editedExpression.inEditMode = false; } expressionItem.hovered = false; this.selectedField = expressionItem.expression.fieldName ? this.fields.find(field => field.field === expressionItem.expression.fieldName) : null; this.selectedCondition = expressionItem.expression.condition ? expressionItem.expression.condition.name : null; this.searchValue = expressionItem.expression.searchVal; expressionItem.inEditMode = true; this._editedExpression = expressionItem; this.cdr.detectChanges(); this.fieldSelectOverlaySettings.target = this.fieldSelect.element; this.fieldSelectOverlaySettings.excludeFromOutsideClick = [this.fieldSelect.element as HTMLElement]; this.fieldSelectOverlaySettings.positionStrategy = new AutoPositionStrategy(); this.conditionSelectOverlaySettings.target = this.conditionSelect.element; this.conditionSelectOverlaySettings.excludeFromOutsideClick = [this.conditionSelect.element as HTMLElement]; this.conditionSelectOverlaySettings.positionStrategy = new AutoPositionStrategy(); if (!this.selectedField) { this.fieldSelect.input.nativeElement.focus(); } else if (this.selectedField.filters.condition(this.selectedCondition).isUnary) { this.conditionSelect.input.nativeElement.focus(); } else { const input = this.searchValueInput?.nativeElement || this.picker?.getEditElement(); input.focus(); } } /** * @hidden @internal */ public clearSelection() { for (const group of this._selectedGroups) { group.selected = false; } this._selectedGroups = []; for (const expr of this.selectedExpressions) { expr.selected = false; } this.selectedExpressions = []; this.toggleContextMenu(); } /** * @hidden @internal */ public enterExpressionAdd(expressionItem: ExpressionOperandItem) { this.clearSelection(); this.exitOperandEdit(); if (this._addModeExpression) { this._addModeExpression.inAddMode = false; } expressionItem.inAddMode = true; this._addModeExpression = expressionItem; if (expressionItem.selected) { this.toggleExpression(expressionItem); } } /** * @hidden @internal */ public contextMenuClosed() { this.contextualGroup = null; } /** * @hidden @internal */ public onKeyDown(eventArgs: KeyboardEvent) { eventArgs.stopPropagation(); const key = eventArgs.key; if (!this.contextMenuToggle.collapsed && (key === this.platform.KEYMAP.ESCAPE)) { this.clearSelection(); } } /** * @hidden @internal */ public createAndGroup() { this.createGroup(FilteringLogic.And); } /** * @hidden @internal */ public createOrGroup() { this.createGroup(FilteringLogic.Or); } /** * @hidden @internal */ public deleteFilters() { for (const expr of this.selectedExpressions) { this.deleteItem(expr); } this.clearSelection(); } /** * @hidden @internal */ public onGroupClick(groupItem: ExpressionGroupItem) { this.toggleGroup(groupItem); } /** * @hidden @internal */ public ungroup() { const selectedGroup = this.contextualGroup; const parent = selectedGroup.parent; if (parent) { const index = parent.children.indexOf(selectedGroup); parent.children.splice(index, 1, ...selectedGroup.children); for (const expr of selectedGroup.children) { expr.parent = parent; } } this.clearSelection(); this.commitOperandEdit(); } /** * @hidden @internal */ public deleteGroup() { const selectedGroup = this.contextualGroup; const parent = selectedGroup.parent; if (parent) { const index = parent.children.indexOf(selectedGroup); parent.children.splice(index, 1); } else { this.rootGroup = null; } this.clearSelection(); this.commitOperandEdit(); } /** * @hidden @internal */ public selectFilteringLogic(event: IButtonGroupEventArgs) { this.contextualGroup.operator = event.index as FilteringLogic; this.commitOperandEdit(); } /** * @hidden @internal */ public getConditionFriendlyName(name: string): string { return this.resourceStrings[`igx_query_builder_filter_${name}`] || name; } /** * @hidden @internal */ public isDate(value: any) { return value instanceof Date; } /** * @hidden @internal */ public onExpressionsScrolled() { if (!this.contextMenuToggle.collapsed) { this.calculateContextMenuTarget(); this.contextMenuToggle.reposition(); } } /** * @hidden @internal */ public invokeClick(eventArgs: KeyboardEvent) { if (this.platform.isActivationKey(eventArgs)) { eventArgs.preventDefault(); (eventArgs.currentTarget as HTMLElement).click(); } } /** * @hidden @internal */ public openPicker(args: KeyboardEvent) { if (this.platform.isActivationKey(args)) { args.preventDefault(); this.picker.open(); } } /** * @hidden @internal */ public onOutletPointerDown(event) { // This prevents closing the select's dropdown when clicking the scroll event.preventDefault(); } /** * @hidden @internal */ public getConditionList(): string[] { return this.selectedField ? this.selectedField.filters.conditionList() : []; } /** * @hidden @internal */ public getFormatter(field: string) { return this.fields.find(el => el.field === field).formatter; } /** * @hidden @internal */ public getFormat(field: string) { return this.fields.find(el => el.field === field).pipeArgs.format; } /** * @hidden @internal * * used by the grid */ public setAddButtonFocus() { if (this.addRootAndGroupButton) { this.addRootAndGroupButton.nativeElement.focus(); } else if (this.addConditionButton) { this.addConditionButton.nativeElement.focus(); } } /** * @hidden @internal */ public context(expression: ExpressionItem, afterExpression?: ExpressionItem) { return { $implicit: expression, afterExpression }; } /** * @hidden @internal */ public onChipSelectionEnd() { const contextualGroup = this.findSingleSelectedGroup(); if (contextualGroup || this.selectedExpressions.length > 1) { this.contextualGroup = contextualGroup; this.calculateContextMenuTarget(); if (this.contextMenuToggle.collapsed) { this.contextMenuToggle.open(this._overlaySettings); } else { this.contextMenuToggle.reposition(); } } } private setFormat(field: FieldType) { if (!field.pipeArgs) { field.pipeArgs = { digitsInfo: DEFAULT_PIPE_DIGITS_INFO }; } if (!field.pipeArgs.format) { field.pipeArgs.format = field.dataType === DataType.Time ? DEFAULT_PIPE_TIME_FORMAT : field.dataType === DataType.DateTime ? DEFAULT_PIPE_DATE_TIME_FORMAT : DEFAULT_PIPE_DATE_FORMAT; } if (!field.defaultDateTimeFormat) { field.defaultDateTimeFormat = DEFAULT_DATE_TIME_FORMAT; } if (!field.defaultTimeFormat) { field.defaultTimeFormat = DEFAULT_TIME_FORMAT; } } private setFilters(field: FieldType) { if (!field.filters) { switch (field.dataType) { case DataType.Boolean: field.filters = IgxBooleanFilteringOperand.instance(); break; case DataType.Number: case DataType.Currency: case DataType.Percent: field.filters = IgxNumberFilteringOperand.instance(); break; case DataType.Date: field.filters = IgxDateFilteringOperand.instance(); break; case DataType.Time: field.filters = IgxTimeFilteringOperand.instance(); break; case DataType.DateTime: field.filters = IgxDateTimeFilteringOperand.instance(); break; case DataType.String: default: field.filters = IgxStringFilteringOperand.instance(); break; } } } private onToggleExpression(expressionItem: ExpressionOperandItem) { this.exitOperandEdit(); this.toggleExpression(expressionItem); this.toggleContextMenu(); } private toggleExpression(expressionItem: ExpressionOperandItem) { expressionItem.selected = !expressionItem.selected; if (expressionItem.selected) { this.selectedExpressions.push(expressionItem); } else { const index = this.selectedExpressions.indexOf(expressionItem); this.selectedExpressions.splice(index, 1); this.deselectParentRecursive(expressionItem); } } private addGroup(operator: FilteringLogic, parent?: ExpressionGroupItem, afterExpression?: ExpressionItem) { this.cancelOperandAdd(); const groupItem = new ExpressionGroupItem(operator, parent); if (parent) { if (afterExpression) { const index = parent.children.indexOf(afterExpression); parent.children.splice(index + 1, 0, groupItem); } else { parent.children.push(groupItem); } } else { this.rootGroup = groupItem; } this.addCondition(groupItem); this.currentGroup = groupItem; } private createExpressionGroupItem(expressionTree: IExpressionTree, parent?: ExpressionGroupItem): ExpressionGroupItem { let groupItem: ExpressionGroupItem; if (expressionTree) { groupItem = new ExpressionGroupItem(expressionTree.operator, parent); for (const expr of expressionTree.filteringOperands) { if (expr instanceof FilteringExpressionsTree) { groupItem.children.push(this.createExpressionGroupItem(expr, groupItem)); } else { const filteringExpr = expr as IFilteringExpression; const exprCopy: IFilteringExpression = { fieldName: filteringExpr.fieldName, condition: filteringExpr.condition, searchVal: filteringExpr.searchVal, ignoreCase: filteringExpr.ignoreCase }; const operandItem = new ExpressionOperandItem(exprCopy, groupItem); const field = this.fields.find(el => el.field === filteringExpr.fieldName); operandItem.fieldLabel = field.label || field.header || field.field; groupItem.children.push(operandItem); } } } return groupItem; } private createExpressionTreeFromGroupItem(groupItem: ExpressionGroupItem): FilteringExpressionsTree { if (!groupItem) { return null; } const expressionTree = new FilteringExpressionsTree(groupItem.operator); for (const item of groupItem.children) { if (item instanceof ExpressionGroupItem) { const subTree = this.createExpressionTreeFromGroupItem((item as ExpressionGroupItem)); expressionTree.filteringOperands.push(subTree); } else { expressionTree.filteringOperands.push((item as ExpressionOperandItem).expression); } } return expressionTree; } private toggleContextMenu() { const contextualGroup = this.findSingleSelectedGroup(); if (contextualGroup || this.selectedExpressions.length > 1) { this.contextualGroup = contextualGroup; if (contextualGroup) { this.filteringLogics = [ { label: this.resourceStrings.igx_query_builder_filter_operator_and, selected: contextualGroup.operator === FilteringLogic.And }, { label: this.resourceStrings.igx_query_builder_filter_operator_or, selected: contextualGroup.operator === FilteringLogic.Or } ]; } } else if (this.contextMenuToggle) { this.contextMenuToggle.close(); } } private findSingleSelectedGroup(): ExpressionGroupItem { for (const group of this._selectedGroups) { const containsAllSelectedExpressions = this.selectedExpressions.every(op => this.isInsideGroup(op, group)); if (containsAllSelectedExpressions) { return group; } } return null; } private isInsideGroup(item: ExpressionItem, group: ExpressionGroupItem): boolean { if (!item) { return false; } if (item.parent === group) { return true; } return this.isInsideGroup(item.parent, group); } private deleteItem(expressionItem: ExpressionItem) { if (!expressionItem.parent) { this.rootGroup = null; this.currentGroup = null; this._expressionTree = null; return; } if (expressionItem === this.currentGroup) { this.currentGroup = this.currentGroup.parent; } const children = expressionItem.parent.children; const index = children.indexOf(expressionItem); children.splice(index, 1); this._expressionTree = this.createExpressionTreeFromGroupItem(this.rootGroup); if (!children.length) { this.deleteItem(expressionItem.parent); } this.expressionTreeChange.emit(); } private createGroup(operator: FilteringLogic) { const chips = this.chips.toArray(); const minIndex = this.selectedExpressions.reduce((i, e) => Math.min(i, chips.findIndex(c => c.data === e)), Number.MAX_VALUE); const firstExpression = chips[minIndex].data; const parent = firstExpression.parent; const groupItem = new ExpressionGroupItem(operator, parent); const index = parent.children.indexOf(firstExpression); parent.children.splice(index, 0, groupItem); for (const expr of this.selectedExpressions) { groupItem.children.push(expr); this.deleteItem(expr); expr.parent = groupItem; } this.clearSelection(); } private toggleGroup(groupItem: ExpressionGroupItem) { this.exitOperandEdit(); if (groupItem.children && groupItem.children.length) { this.toggleGroupRecursive(groupItem, !groupItem.selected); if (!groupItem.selected) { this.deselectParentRecursive(groupItem); } this.toggleContextMenu(); } } private toggleGroupRecursive(groupItem: ExpressionGroupItem, selected: boolean) { if (groupItem.selected !== selected) { groupItem.selected = selected; if (groupItem.selected) { this._selectedGroups.push(groupItem); } else { const index = this._selectedGroups.indexOf(groupItem); this._selectedGroups.splice(index, 1); } } for (const expr of groupItem.children) { if (expr instanceof ExpressionGroupItem) { this.toggleGroupRecursive(expr, selected); } else { const operandExpression = expr as ExpressionOperandItem; if (operandExpression.selected !== selected) { this.toggleExpression(operandExpression); } } } } private deselectParentRecursive(expressionItem: ExpressionItem) { const parent = expressionItem.parent; if (parent) { if (parent.selected) { parent.selected = false; const index = this._selectedGroups.indexOf(parent); this._selectedGroups.splice(index, 1); } this.deselectParentRecursive(parent); } } private calculateContextMenuTarget() { const containerRect = this.expressionsContainer.nativeElement.getBoundingClientRect(); const chips = this.chips.filter(c => this.selectedExpressions.indexOf(c.data) !== -1); let minTop = chips.reduce((t, c) => Math.min(t, c.nativeElement.getBoundingClientRect().top), Number.MAX_VALUE); minTop = Math.max(containerRect.top, minTop); minTop = Math.min(containerRect.bottom, minTop); let maxRight = chips.reduce((r, c) => Math.max(r, c.nativeElement.getBoundingClientRect().right), 0); maxRight = Math.max(maxRight, containerRect.left); maxRight = Math.min(maxRight, containerRect.right); this._overlaySettings.target = new Point(maxRight, minTop); } private scrollElementIntoView(target: HTMLElement) { const container = this.expressionsContainer.nativeElement; const targetOffset = target.offsetTop - container.offsetTop; const delta = 10; if (container.scrollTop + delta > targetOffset) { container.scrollTop = targetOffset - delta; } else if (container.scrollTop + container.clientHeight < targetOffset + target.offsetHeight + delta) { container.scrollTop = targetOffset + target.offsetHeight + delta - container.clientHeight; } } private init() { this.clearSelection(); this.cancelOperandAdd(); this.cancelOperandEdit(); this.rootGroup = this.createExpressionGroupItem(this.expressionTree); this.currentGroup = this.rootGroup; } private registerSVGIcons(): void { const editorIcons = editor as any[]; editorIcons.forEach(icon => this.iconService.addSvgIconFromText(icon.name, icon.value, 'imx-icons')); } }