igniteui-angular
Version:
Ignite UI for Angular is a dependency-free Angular toolkit for building modern web apps
1 lines • 205 kB
Source Map (JSON)
{"version":3,"file":"igniteui-angular-query-builder.mjs","sources":["../../../projects/igniteui-angular/query-builder/src/query-builder/query-builder-header.component.ts","../../../projects/igniteui-angular/query-builder/src/query-builder/query-builder-header.component.html","../../../projects/igniteui-angular/query-builder/src/query-builder/query-builder.common.ts","../../../projects/igniteui-angular/query-builder/src/query-builder/query-builder-drag.service.ts","../../../projects/igniteui-angular/query-builder/src/query-builder/query-builder-tree.component.ts","../../../projects/igniteui-angular/query-builder/src/query-builder/query-builder-tree.component.html","../../../projects/igniteui-angular/query-builder/src/query-builder/query-builder.directives.ts","../../../projects/igniteui-angular/query-builder/src/query-builder/query-builder.component.ts","../../../projects/igniteui-angular/query-builder/src/query-builder/query-builder.component.html","../../../projects/igniteui-angular/query-builder/src/query-builder/public_api.ts","../../../projects/igniteui-angular/query-builder/src/query-builder/query-builder.module.ts","../../../projects/igniteui-angular/query-builder/src/igniteui-angular-query-builder.ts"],"sourcesContent":["import { Component, HostBinding, Input } from '@angular/core';\nimport { IQueryBuilderResourceStrings, QueryBuilderResourceStringsEN } from 'igniteui-angular/core';\nimport { getCurrentResourceStrings } from 'igniteui-angular/core';\n\n@Component({\n selector: 'igx-query-builder-header',\n templateUrl: 'query-builder-header.component.html'\n})\nexport class IgxQueryBuilderHeaderComponent {\n\n private _resourceStrings = getCurrentResourceStrings(QueryBuilderResourceStringsEN);\n\n /**\n * @hidden @internal\n */\n @HostBinding('class') public get getClass() {\n return 'igx-query-builder__header';\n }\n\n /**\n * Sets the title of the `IgxQueryBuilderHeaderComponent`.\n *\n * @example\n * ```html\n * <igx-query-builder-header title=\"Sample Query Builder\"></igx-query-builder-header>\n * ```\n */\n @Input()\n public title: string;\n\n /**\n * Show/hide the legend.\n *\n * @example\n * ```html\n * <igx-query-builder-header [showLegend]=\"false\"></igx-query-builder-header>\n * ```\n * @deprecated in version 19.1.0.\n */\n @Input()\n public showLegend = true;\n\n /**\n * Sets the resource strings.\n * By default it uses EN resources.\n *\n * @deprecated in version 19.1.0.\n */\n @Input()\n public set resourceStrings(value: IQueryBuilderResourceStrings) {\n this._resourceStrings = Object.assign({}, this._resourceStrings, value);\n }\n\n /**\n * Returns the resource strings.\n */\n public get resourceStrings(): IQueryBuilderResourceStrings {\n return this._resourceStrings;\n }\n}\n","<div class=\"igx-query-builder__title\">{{ title }}</div>\n<ng-content></ng-content>\n","import { Pipe, PipeTransform } from '@angular/core';\nimport { FilteringLogic, IFilteringExpression } from 'igniteui-angular/core';\n\n@Pipe({\n name: 'fieldFormatter',\n standalone: true\n})\nexport class IgxFieldFormatterPipe implements PipeTransform {\n\n public transform(value: any, formatter: (v: any, data: any, fieldData?: any) => any, rowData: any, fieldData?: any) {\n return formatter(value, rowData, fieldData);\n }\n}\n\n/**\n * @hidden @internal\n */\nexport class ExpressionItem {\n public parent: ExpressionGroupItem;\n public expanded: boolean;\n constructor(parent?: ExpressionGroupItem) {\n this.parent = parent;\n }\n}\n\n/**\n * @hidden @internal\n */\nexport class ExpressionGroupItem extends ExpressionItem {\n public operator: FilteringLogic;\n public children: ExpressionItem[];\n constructor(operator: FilteringLogic, parent?: ExpressionGroupItem) {\n super(parent);\n this.operator = operator;\n this.children = [];\n }\n}\n\n/**\n * @hidden @internal\n */\nexport class ExpressionOperandItem extends ExpressionItem {\n public expression: IFilteringExpression;\n public inEditMode: boolean;\n public inAddMode: boolean;\n public hovered: boolean;\n public focused: boolean;\n public fieldLabel: string;\n constructor(expression: IFilteringExpression, parent: ExpressionGroupItem) {\n super(parent);\n this.expression = expression;\n }\n}\n\nconst IGX_QUERY_BUILDER = 'igx-query-builder';\nconst IGX_FILTER_TREE = 'igx-filter-tree';\n\n/**\n * @hidden @internal\n */\nexport const QueryBuilderSelectors = {\n DRAG_INDICATOR: 'igx-drag-indicator',\n CHIP_GHOST: 'igx-chip__ghost',\n\n DROP_DOWN_LIST_SCROLL: 'igx-drop-down__list-scroll',\n DROP_DOWN_ITEM_DISABLED: 'igx-drop-down__item--disabled',\n\n FILTER_TREE: IGX_FILTER_TREE,\n FILTER_TREE_EXPRESSION_CONTEXT_MENU: IGX_FILTER_TREE + '__expression-context-menu',\n FILTER_TREE_EXPRESSION_ITEM: IGX_FILTER_TREE + '__expression-item',\n FILTER_TREE_EXPRESSION_ITEM_DROP_GHOST: IGX_FILTER_TREE + '__expression-item-drop-ghost',\n FILTER_TREE_EXPRESSION_ITEM_KEYBOARD_GHOST: IGX_FILTER_TREE + '__expression-item-keyboard-ghost',\n FILTER_TREE_EXPRESSION_ITEM_GHOST: IGX_FILTER_TREE + '__expression-item-ghost',\n FILTER_TREE_EXPRESSION_SECTION: IGX_FILTER_TREE + '__expression-section',\n\n FILTER_TREE_LINE_AND: IGX_FILTER_TREE + '__line--and',\n FILTER_TREE_LINE_OR: IGX_FILTER_TREE + '__line--or',\n FILTER_TREE_SUBQUERY: IGX_FILTER_TREE + '__subquery',\n\n QUERY_BUILDER: IGX_QUERY_BUILDER,\n QUERY_BUILDER_BODY: IGX_QUERY_BUILDER + '__main',\n QUERY_BUILDER_HEADER: IGX_QUERY_BUILDER + '__header',\n QUERY_BUILDER_TREE: IGX_QUERY_BUILDER + '-tree',\n}\n","import { filter, fromEvent, sampleTime, Subscription } from 'rxjs';\nimport { IgxQueryBuilderTreeComponent } from './query-builder-tree.component';\nimport { ElementRef, Injectable } from '@angular/core';\nimport { ExpressionGroupItem, ExpressionItem, ExpressionOperandItem, QueryBuilderSelectors } from './query-builder.common';\nimport { IgxChipComponent } from 'igniteui-angular/chips';\n\nconst DEFAULT_SET_Z_INDEX_DELAY = 10;\nconst Z_INDEX_TO_SET = 10010; //overlay z-index is 10005\n\n/** @hidden @internal */\n@Injectable()\nexport class IgxQueryBuilderDragService {\n\n /** The ExpressionItem that's actually the drop ghost's content */\n public dropGhostExpression: ExpressionItem;\n public isKeyboardDrag: boolean;\n private _queryBuilderTreeComponent: IgxQueryBuilderTreeComponent;\n private _queryBuilderTreeComponentElRef: ElementRef;\n private _sourceExpressionItem: ExpressionItem;\n private _sourceElement: HTMLElement;\n private _targetExpressionItem: ExpressionItem;\n private _dropUnder: boolean;\n private _ghostChipMousemoveSubscription$: Subscription;\n private _keyboardSubscription$: Subscription;\n private _keyDragCurrentIndex: number = 0;\n private _keyDragInitialIndex: number = 0;\n private _isKeyDragsFirstMove: boolean = true;\n /** Stores a flat ordered list of possible drop locations as Tuple <[targetExpression, dropUnder]>, while performing the keyboard drag&drop */\n private _possibleDropLocations: Array<[ExpressionItem, boolean]>;\n private _timeoutId: any;\n\n\n /** Get the dragged ghost as a HTMLElement*/\n private get getDragGhostElement(): HTMLElement {\n return (document.querySelector(`.${QueryBuilderSelectors.CHIP_GHOST}[ghostclass=\"${QueryBuilderSelectors.CHIP_GHOST}\"]`) as HTMLElement);\n }\n\n /** Get the drop ghost chip component */\n private get getDropGhostElement(): IgxChipComponent {\n return this._queryBuilderTreeComponent.expressionsChips.find(x => x.data === this.dropGhostExpression);\n }\n\n private get getMainExpressionTree(): HTMLElement {\n return this._queryBuilderTreeComponentElRef.nativeElement.querySelector(`.${QueryBuilderSelectors.FILTER_TREE}`);\n }\n\n\n public register(tree: IgxQueryBuilderTreeComponent, el: ElementRef) {\n this._queryBuilderTreeComponent = tree;\n this._queryBuilderTreeComponentElRef = el;\n }\n\n /** When chip is picked up for dragging\n *\n * @param sourceDragElement The HTML element of the chip that's been dragged\n * @param sourceExpressionItem The expressionItem of the chip that's been dragged\n * @param isKeyboardDrag If it's a mouse drag or keyboard reorder\n *\n */\n public onMoveStart(sourceDragElement: HTMLElement, sourceExpressionItem: ExpressionItem, isKeyboardDrag: boolean): void {\n this.resetDragAndDrop(true);\n this._queryBuilderTreeComponent._expressionTreeCopy = this._queryBuilderTreeComponent._expressionTree;\n this.isKeyboardDrag = isKeyboardDrag;\n this._sourceExpressionItem = sourceExpressionItem;\n this._sourceElement = sourceDragElement;\n\n this.listenToKeyboard();\n\n if (!this.isKeyboardDrag) {\n this._sourceElement.style.display = 'none';\n this.setDragGhostZIndex();\n }\n }\n\n /** When dragged chip is let go outside a proper drop zone */\n public onMoveEnd(): void {\n if (!this._sourceElement || !this._sourceExpressionItem) {\n return;\n }\n\n if (this.dropGhostExpression) {\n //If there is a ghost chip presented to the user, execute drop\n this.onChipDropped();\n } else {\n this.resetDragAndDrop(true);\n }\n\n this._ghostChipMousemoveSubscription$?.unsubscribe();\n this._keyboardSubscription$?.unsubscribe();\n }\n\n /** When mouse drag enters a chip's area\n * @param targetDragElement The HTML element of the drop area chip that's been dragged to\n * @param targetExpressionItem The expressionItem of the drop area chip that's been dragged to\n */\n public onChipEnter(targetDragElement: HTMLElement, targetExpressionItem: ExpressionItem) {\n if (!this._sourceElement || !this._sourceExpressionItem) {\n return;\n }\n\n //If entering the one that's been picked up don't do any thing\n if (targetExpressionItem === this.dropGhostExpression) {\n return;\n }\n\n //Simulate leaving the last entered chip in case of no Leave event triggered due to the artificial drop zone of a north positioned ghost chip\n if (this._targetExpressionItem) {\n this.resetDragAndDrop(false);\n }\n\n this._targetExpressionItem = targetExpressionItem;\n\n //Determine the middle point of the chip.\n const appendUnder = this.ghostInLowerPart(targetDragElement);\n\n this.renderDropGhostChip(appendUnder);\n }\n\n /** When mouse drag moves in a div's drop area\n * @param targetDragElement The HTML element of the drop area chip that's been dragged to\n * @param targetExpressionItem The expressionItem of the drop area chip that's been dragged to\n */\n public onDivOver(targetDragElement: HTMLElement, targetExpressionItem: ExpressionItem) {\n if (this._targetExpressionItem === targetExpressionItem) {\n this.onChipOver(targetDragElement)\n } else {\n this.onChipEnter(targetDragElement, targetExpressionItem);\n }\n }\n\n /** When mouse drag moves in a chip's drop area\n * @param targetDragElement The HTML element of the drop area chip that's been dragged to\n */\n public onChipOver(targetDragElement: HTMLElement): void {\n if (!this._sourceElement || !this._sourceExpressionItem) {\n return;\n }\n\n //Determine the middle point of the chip.\n const appendUnder = this.ghostInLowerPart(targetDragElement);\n\n this.renderDropGhostChip(appendUnder);\n }\n\n /** When mouse drag leaves a chip's drop area */\n public onChipLeave() {\n if (!this._sourceElement || !this._sourceExpressionItem) {\n return;\n }\n\n //if the drag ghost is on the drop ghost row don't trigger leave\n if (this.dragGhostIsOnDropGhostRow()) {\n return;\n }\n\n if (this._targetExpressionItem) {\n this.resetDragAndDrop(false)\n }\n }\n\n /** When dragged chip is let go in div's drop area\n * @param targetExpressionItem The expressionItem of the drop area chip that's been dragged to\n */\n public onDivDropped(targetExpressionItem: ExpressionItem) {\n if (targetExpressionItem !== this._sourceExpressionItem) {\n this.onChipDropped();\n }\n }\n\n /** When dragged chip is let go in chip's drop area */\n public onChipDropped() {\n if (!this._sourceElement || !this._sourceExpressionItem) {\n return;\n }\n\n //Determine which chip to be focused after drop completes\n const [dropLocationIndex, _] = this.countChipsBeforeDropLocation(this._queryBuilderTreeComponent.rootGroup);\n\n //Delete from old place\n this._queryBuilderTreeComponent.deleteItem(this._sourceExpressionItem);\n this.dropGhostExpression = null;\n\n this._queryBuilderTreeComponent.focusChipAfterDrag(dropLocationIndex);\n\n this.resetDragAndDrop(true);\n\n this._queryBuilderTreeComponent.exitEditAddMode();\n }\n\n /** When mouse drag moves in a AND/OR drop area\n * @param targetDragElement The HTML element of the drop area chip that's been dragged to\n * @param targetExpressionItem The expressionItem of the drop area chip that's been dragged to\n */\n public onGroupRootOver(targetDragElement: HTMLElement, targetExpressionItem: ExpressionGroupItem) {\n if (!this._sourceElement || !this._sourceExpressionItem) {\n return;\n }\n\n let newTargetExpressionItem;\n\n if (this.ghostInLowerPart(targetDragElement) || !targetExpressionItem.parent) {\n //if ghost is in lower part of the AND/OR (or it's the main group) => drop as first child of that group\n //accounting for the fact that the drop ghost might already be there as first child\n if (targetExpressionItem.children[0] !== this.dropGhostExpression) {\n newTargetExpressionItem = targetExpressionItem.children[0];\n } else {\n newTargetExpressionItem = targetExpressionItem.children[1];\n }\n } else {\n //if ghost is in upper part => drop before the group starts\n newTargetExpressionItem = targetExpressionItem;\n }\n\n if (this._targetExpressionItem !== newTargetExpressionItem) {\n this.resetDragAndDrop(false);\n this._targetExpressionItem = newTargetExpressionItem;\n this.renderDropGhostChip(false);\n }\n }\n\n /** When mouse drag moves in 'Add condition' button's drop area\n * @param addConditionElement The Add condition button HTML Element\n * @param rootGroup The root group of the query tree\n */\n public onAddConditionEnter(addConditionElement: HTMLElement, rootGroup: ExpressionGroupItem) {\n if (!this._sourceElement || !this._sourceExpressionItem) {\n return;\n }\n const lastElement = addConditionElement.parentElement.previousElementSibling.lastElementChild;\n\n //simulate entering in the lower part of the last chip/group\n this.onChipEnter(lastElement as HTMLElement, rootGroup.children[rootGroup.children.length - 1]);\n }\n\n /** When chip's drag indicator is focused\n *\n * @param sourceDragElement The HTML element of the chip that's been dragged\n * @param sourceExpressionItem The expressionItem of the chip that's been dragged\n *\n */\n public onChipDragIndicatorFocus(sourceDragElement: HTMLElement, sourceExpressionItem: ExpressionItem) {\n //if drag is not underway, already\n if (!this.getDropGhostElement) {\n this.onMoveStart(sourceDragElement, sourceExpressionItem, true);\n }\n }\n\n /** When chip's drag indicator looses focus*/\n public onChipDragIndicatorFocusOut() {\n if (this._sourceElement?.style?.display !== 'none') {\n this.resetDragAndDrop(true);\n this._keyboardSubscription$?.unsubscribe();\n }\n }\n\n /** Upon blurring the tree, if Keyboard drag is underway and the next active item is not the drop ghost's drag indicator icon, cancel the drag&drop procedure*/\n public onDragFocusOut() {\n if (this.isKeyboardDrag && this.getDropGhostElement) {\n //have to wait a tick because upon blur, the next activeElement is always body, right before the next element gains focus\n setTimeout(() => {\n if (document.activeElement.className.indexOf(QueryBuilderSelectors.DRAG_INDICATOR) === -1) {\n this.resetDragAndDrop(true);\n this._keyboardSubscription$?.unsubscribe();\n }\n }, 0);\n }\n }\n\n /** Checks if the dragged ghost is horizontally on the same line with the drop ghost*/\n private dragGhostIsOnDropGhostRow() {\n const dragGhostBounds = this.getDragGhostElement.getBoundingClientRect();\n\n const dropGhostBounds = this.getDropGhostElement?.nativeElement?.parentElement.getBoundingClientRect();\n\n if (!dragGhostBounds || !dropGhostBounds) {\n return false;\n }\n\n const tolerance = dragGhostBounds.bottom - dragGhostBounds.top;\n\n return !(dragGhostBounds.bottom < dropGhostBounds.top - tolerance || dragGhostBounds.top > dropGhostBounds.bottom + tolerance);\n }\n\n /** Checks if the dragged ghost is north or south of a target element's center*/\n private ghostInLowerPart(ofElement: HTMLElement) {\n const ghostBounds = this.getDragGhostElement.getBoundingClientRect();\n const targetBounds = ofElement.getBoundingClientRect();\n\n return ((ghostBounds.top + ghostBounds.bottom) / 2) >= ((targetBounds.top + targetBounds.bottom) / 2);\n }\n\n /** Make a copy of the _sourceExpressionItem's chip and paste it in the tree north or south of the _targetExpressionItem's chip */\n private renderDropGhostChip(appendUnder: boolean): void {\n if (appendUnder !== this._dropUnder || this.isKeyboardDrag) {\n this.clearDropGhost();\n\n //Copy dragged chip\n const dragCopy = { ...this._sourceExpressionItem };\n dragCopy.parent = this._targetExpressionItem.parent;\n this.dropGhostExpression = dragCopy;\n\n //Paste chip\n this._dropUnder = appendUnder;\n const pasteIndex = this._targetExpressionItem.parent.children.indexOf(this._targetExpressionItem);\n this._targetExpressionItem.parent.children.splice(pasteIndex + (this._dropUnder ? 1 : 0), 0, dragCopy);\n }\n\n //Put focus on the drag icon of the ghost while performing keyboard drag\n if (this.isKeyboardDrag) {\n setTimeout(() => { // this will make the execution after the drop ghost is rendered\n const dropGhostDragIndicator = this.getDropGhostElement?.nativeElement?.querySelector(`.${QueryBuilderSelectors.DRAG_INDICATOR}`) as HTMLElement;\n if (dropGhostDragIndicator) {\n dropGhostDragIndicator.focus();\n }\n }, 0);\n }\n\n //Attach a mousemove event listener (if not already in place) to the dragged ghost (if present)\n if (!this.isKeyboardDrag && this.getDragGhostElement && (!this._ghostChipMousemoveSubscription$ || this._ghostChipMousemoveSubscription$?.closed === true)) {\n const mouseMoves = fromEvent<MouseEvent>(this.getDragGhostElement, 'mousemove');\n\n //When mouse moves and there is a drop ghost => trigger onChipLeave to check if the drop ghost has to be removed\n //effectively solving the case when mouse leaves the QB and a drop ghost is still in place\n this._ghostChipMousemoveSubscription$ = mouseMoves.pipe(sampleTime(100)).subscribe(() => {\n if (this.getDropGhostElement) {\n this.onChipLeave();\n }\n });\n }\n\n this.setDragCursor('grab');\n }\n\n /** Set the cursor when dragging a ghost*/\n private setDragCursor(cursor: string) {\n if (this.getDragGhostElement) {\n this.getDragGhostElement.style.cursor = cursor;\n }\n }\n\n /** Removes the drop ghost expression from the tree and it's chip effectively */\n private clearDropGhost() {\n if (this.dropGhostExpression) {\n const children = this.dropGhostExpression.parent.children;\n const delIndex = children.indexOf(this.dropGhostExpression);\n children.splice(delIndex, 1);\n this.dropGhostExpression = null;\n }\n }\n\n /** Reset Drag&Drop vars. Optionally the drag source vars too*/\n private resetDragAndDrop(clearDragged: boolean) {\n this._targetExpressionItem = null;\n this._dropUnder = null;\n this.clearDropGhost();\n this._keyDragInitialIndex = 0;\n this._keyDragCurrentIndex = 0;\n this._possibleDropLocations = null;\n this._isKeyDragsFirstMove = true;\n this.setDragCursor('no-drop');\n\n if (this._queryBuilderTreeComponent._expressionTreeCopy) {\n this._queryBuilderTreeComponent._expressionTree = this._queryBuilderTreeComponent._expressionTreeCopy;\n }\n\n if ((clearDragged || this.isKeyboardDrag) && this._sourceElement) {\n this._sourceElement.style.display = '';\n }\n\n if (clearDragged) {\n this._queryBuilderTreeComponent._expressionTreeCopy = null;\n this._sourceExpressionItem = null;\n this._sourceElement = null;\n }\n }\n\n /** Start listening for drag and drop specific keys */\n private listenToKeyboard() {\n this._keyboardSubscription$?.unsubscribe();\n this._keyboardSubscription$ = fromEvent<KeyboardEvent>(this.getMainExpressionTree, 'keydown')\n .pipe(filter(e => ['ArrowUp', 'ArrowDown', 'Enter', 'Space', 'Escape', 'Tab'].includes(e.key)))\n // .pipe(tap(e => {\n // //Inhibit Tabs if keyboard drag is underway (don't allow to loose focus of the drop ghost's drag indicator)\n // if (e.key === 'Tab' && this.getDropGhostElement) {\n // e.preventDefault();\n // }\n // }))\n .pipe(filter(event => !event.repeat))\n .subscribe(e => {\n if (e.key === 'Escape') {\n //TODO cancel mouse drag once it's implemented in igx-chip draggable\n this.resetDragAndDrop(false);\n //Regain focus on the drag icon after keyboard drag cancel\n if (this.isKeyboardDrag) {\n (this._sourceElement.firstElementChild.firstElementChild.firstElementChild.firstElementChild as HTMLElement).focus();\n }\n } else if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {\n this.arrowDrag(e.key);\n } else if (e.key === 'Enter' || e.key === 'Space') {\n //this.platform.isActivationKey(eventArgs) Maybe use this rather that Enter/Space?\n this.onChipDropped();\n this._keyboardSubscription$.unsubscribe();\n }\n });\n }\n\n /** Perform up/down movement of drop ghost along the expression tree*/\n private arrowDrag(key: string) {\n if (!this._sourceElement || !this._sourceExpressionItem) {\n return;\n }\n\n const rootGroup = this._queryBuilderTreeComponent.rootGroup;\n\n if (this._isKeyDragsFirstMove) {\n this._possibleDropLocations = this.getPossibleDropLocations(rootGroup, true);\n this._keyDragInitialIndex = this._possibleDropLocations.findIndex(e => e[0] === this._sourceExpressionItem && e[1] === true);\n this._keyDragCurrentIndex = this._keyDragInitialIndex;\n if (this._keyDragInitialIndex === -1) {\n console.error(\"Dragged expression not found\");\n }\n this._sourceElement.style.display = 'none';\n }\n\n let newKeyIndexOffset = this._keyDragCurrentIndex;\n if (key === 'ArrowUp') {\n //decrease index capped at top of tree\n newKeyIndexOffset && newKeyIndexOffset--;\n } else if (key === 'ArrowDown') {\n //increase index capped at bottom of tree\n newKeyIndexOffset < this._possibleDropLocations.length - 1 && newKeyIndexOffset++;\n } else {\n console.error('wrong key');\n return;\n }\n\n //if drop location has no change\n if (newKeyIndexOffset !== this._keyDragCurrentIndex || this._isKeyDragsFirstMove) {\n this._keyDragCurrentIndex = newKeyIndexOffset;\n\n const newDropTarget = this._possibleDropLocations[this._keyDragCurrentIndex];\n this._targetExpressionItem = newDropTarget[0]\n\n this.renderDropGhostChip(newDropTarget[1]);\n\n //Situations when drop ghost hasn't really moved, run one more time\n if (this._keyDragCurrentIndex === this._keyDragInitialIndex ||\n (this._isKeyDragsFirstMove && this._keyDragCurrentIndex === this._keyDragInitialIndex - 1)) {\n this._isKeyDragsFirstMove = false;\n this.arrowDrag(key);\n }\n\n this._isKeyDragsFirstMove = false;\n }\n\n return;\n }\n\n /** Produces a flat ordered list of possible drop locations as Tuple <[targetExpression, dropUnder]>, while performing the keyboard drag&drop */\n private getPossibleDropLocations(group: ExpressionGroupItem, isRoot: boolean): Array<[ExpressionItem, boolean]> {\n const result = new Array() as Array<[ExpressionItem, boolean]>;\n\n //Add dropZone under AND/OR (as first child of group)\n result.push([(group as ExpressionGroupItem).children[0], false]);\n\n for (let i = 0; i < group.children.length; i++) {\n if (group.children[i] instanceof ExpressionGroupItem) {\n result.push(...this.getPossibleDropLocations(group.children[i] as ExpressionGroupItem, false));\n } else {\n result.push([group.children[i], true]);\n }\n }\n\n //Add dropZone under the whole group\n if (!isRoot) {\n result.push([group, true]);\n }\n\n return result;\n }\n\n /** Counts how many chips will be in the tree (from top to bottom) before the dropped one */\n private countChipsBeforeDropLocation(group: ExpressionGroupItem): [number, boolean] {\n let count = 0, totalCount = 0, targetReached = false;\n\n for (let i = 0; i < group.children.length; i++) {\n const child = group.children[i];\n\n if (targetReached) {\n break;\n }\n\n if (child instanceof ExpressionGroupItem) {\n if (child === this._targetExpressionItem) {\n if (this._dropUnder) {\n [count] = this.countChipsBeforeDropLocation(child as ExpressionGroupItem);\n totalCount += count;\n }\n targetReached = true;\n } else {\n [count, targetReached] = this.countChipsBeforeDropLocation(child as ExpressionGroupItem);\n totalCount += count;\n }\n } else {\n if (child !== this._sourceExpressionItem && //not the hidden source chip\n child !== this.dropGhostExpression && //not the drop ghost\n !((child as ExpressionOperandItem).inEditMode && this._queryBuilderTreeComponent.operandCanBeCommitted() !== true) //not a chip in edit mode that will be discarded\n ) {\n totalCount++;\n }\n\n if (child === this._targetExpressionItem) {\n targetReached = true;\n if (!this._dropUnder &&\n !((child as ExpressionOperandItem).inEditMode && this._queryBuilderTreeComponent.operandCanBeCommitted() !== true)) {\n totalCount--;\n }\n }\n }\n }\n\n totalCount === -1 && totalCount++;\n\n return [totalCount, targetReached];\n }\n\n /** Sets the z-index of the drag ghost with a little delay, since we don't have access to ghostCreated() but we know it's executed right after moveStart() */\n private setDragGhostZIndex() {\n if (this._timeoutId) {\n clearTimeout(this._timeoutId);\n }\n\n this._timeoutId = setTimeout(() => {\n if (this.getDragGhostElement?.style) {\n this.getDragGhostElement.style.zIndex = `${Z_INDEX_TO_SET}`;\n }\n }, DEFAULT_SET_Z_INDEX_DELAY);\n }\n}\n","import { AfterViewInit, EventEmitter, LOCALE_ID, Output, TemplateRef, inject } from '@angular/core';\nimport { getLocaleFirstDayOfWeek, NgTemplateOutlet, NgClass, DatePipe } from '@angular/common';\n\nimport {\n Component, Input, ViewChild, ChangeDetectorRef, ViewChildren, QueryList, ElementRef, OnDestroy, HostBinding\n} from '@angular/core';\nimport { FormsModule } from '@angular/forms';\nimport { Subject } from 'rxjs';\nimport { IgxChipComponent } from 'igniteui-angular/chips';\nimport {\n IQueryBuilderResourceStrings,\n QueryBuilderResourceStringsEN,\n PlatformUtil,\n trackByIdentity,\n GridColumnDataType,\n DataUtil,\n IgxBooleanFilteringOperand,\n IgxDateFilteringOperand,\n IgxDateTimeFilteringOperand,\n IgxNumberFilteringOperand,\n IgxStringFilteringOperand,\n IgxTimeFilteringOperand,\n FilteringLogic,\n IFilteringExpression,\n FilteringExpressionsTree,\n IExpressionTree,\n IFilteringExpressionsTree,\n FieldType,\n EntityType,\n HorizontalAlignment,\n OverlaySettings,\n VerticalAlignment,\n AbsoluteScrollStrategy,\n AutoPositionStrategy,\n CloseScrollStrategy,\n ConnectedPositioningStrategy,\n IgxPickerToggleComponent,\n IgxPickerClearComponent,\n getCurrentResourceStrings,\n isTree,\n IgxOverlayOutletDirective\n} from 'igniteui-angular/core';\nimport { IgxDatePickerComponent } from 'igniteui-angular/date-picker';\n\nimport {\n IgxButtonDirective,\n IgxDateTimeEditorDirective,\n IgxIconButtonDirective,\n IgxTooltipDirective,\n IgxTooltipTargetDirective,\n IgxDragIgnoreDirective,\n IgxDropDirective\n} from 'igniteui-angular/directives';\nimport { IgxSelectComponent } from 'igniteui-angular/select';\nimport { IgxTimePickerComponent } from 'igniteui-angular/time-picker';\nimport { IgxInputGroupComponent, IgxInputDirective, IgxPrefixDirective } from 'igniteui-angular/input-group';\nimport { IgxSelectItemComponent } from 'igniteui-angular/select';\nimport { IgxIconComponent } from 'igniteui-angular/icon';\nimport { IComboSelectionChangingEventArgs, IgxComboComponent, IgxComboHeaderDirective } from 'igniteui-angular/combo';\nimport { IgxCheckboxComponent, IChangeCheckboxEventArgs } from 'igniteui-angular/checkbox';\nimport { IgxDialogComponent } from 'igniteui-angular/dialog';\nimport {\n ISelectionEventArgs,\n IgxDropDownComponent,\n IgxDropDownItemComponent,\n IgxDropDownItemNavigationDirective\n} from 'igniteui-angular/drop-down';\nimport { IgxQueryBuilderSearchValueTemplateDirective } from './query-builder.directives';\nimport { IgxQueryBuilderComponent } from './query-builder.component';\nimport { IgxQueryBuilderDragService } from './query-builder-drag.service';\nimport { ExpressionGroupItem, ExpressionItem, ExpressionOperandItem, IgxFieldFormatterPipe } from './query-builder.common';\n\nconst DEFAULT_PIPE_DATE_FORMAT = 'mediumDate';\nconst DEFAULT_PIPE_TIME_FORMAT = 'mediumTime';\nconst DEFAULT_PIPE_DATE_TIME_FORMAT = 'medium';\nconst DEFAULT_PIPE_DIGITS_INFO = '1.0-3';\nconst DEFAULT_CHIP_FOCUS_DELAY = 50;\n\n/** @hidden */\n@Component({\n selector: 'igx-query-builder-tree',\n templateUrl: './query-builder-tree.component.html',\n host: { 'class': 'igx-query-builder-tree' },\n imports: [\n DatePipe,\n FormsModule,\n IgxButtonDirective,\n IgxCheckboxComponent,\n IgxChipComponent,\n IgxComboComponent,\n IgxComboHeaderDirective,\n IgxDatePickerComponent,\n IgxDateTimeEditorDirective,\n IgxDialogComponent,\n IgxDragIgnoreDirective,\n IgxDropDirective,\n IgxDropDownComponent,\n IgxDropDownItemComponent,\n IgxDropDownItemNavigationDirective,\n IgxFieldFormatterPipe,\n IgxIconButtonDirective,\n IgxIconComponent,\n IgxInputDirective,\n IgxInputGroupComponent,\n IgxOverlayOutletDirective,\n IgxPickerClearComponent,\n IgxPickerToggleComponent,\n IgxPrefixDirective,\n IgxSelectComponent,\n IgxSelectItemComponent,\n IgxTimePickerComponent,\n IgxTooltipDirective,\n IgxTooltipTargetDirective,\n NgClass,\n NgTemplateOutlet\n ],\n providers: [\n IgxQueryBuilderDragService\n ],\n})\nexport class IgxQueryBuilderTreeComponent implements AfterViewInit, OnDestroy {\n public cdr = inject(ChangeDetectorRef);\n public dragService = inject(IgxQueryBuilderDragService);\n protected platform = inject(PlatformUtil);\n private elRef = inject(ElementRef);\n protected _localeId = inject(LOCALE_ID);\n\n /**\n * @hidden @internal\n */\n public _expressionTree: IExpressionTree;\n\n /**\n * @hidden @internal\n */\n public _expressionTreeCopy: IExpressionTree;\n\n /**\n * @hidden @internal\n */\n @HostBinding('class') public get getClass() {\n return `igx-query-builder-tree--level-${this.level}`;\n }\n\n /**\n * Sets/gets the entities.\n */\n @Input()\n public entities: EntityType[];\n\n /**\n * Sets/gets the parent query builder component.\n */\n @Input()\n public queryBuilder: IgxQueryBuilderComponent;\n\n /**\n * Sets/gets the search value template.\n */\n @Input()\n public searchValueTemplate: TemplateRef<IgxQueryBuilderSearchValueTemplateDirective> = null;\n\n /**\n * Returns the parent expression operand.\n */\n @Input()\n public get parentExpression(): ExpressionOperandItem {\n return this._parentExpression;\n }\n\n /**\n * Sets the parent expression operand.\n */\n public set parentExpression(value: ExpressionOperandItem) {\n this._parentExpression = value;\n }\n\n /**\n * Returns the fields.\n */\n public get fields(): FieldType[] {\n if (!this._fields && this.isAdvancedFiltering()) {\n this._fields = this.entities[0].fields;\n }\n\n return this._fields;\n }\n\n /**\n * Sets the fields.\n */\n @Input()\n public set fields(fields: FieldType[]) {\n this._fields = fields;\n\n this._fields = this._fields?.map(f => ({...f, filters: this.getFilters(f), pipeArgs: this.getPipeArgs(f) }));\n\n if (!this._fields && this.isAdvancedFiltering()) {\n this._fields = this.entities[0].fields;\n }\n }\n\n /**\n * Returns the expression tree.\n */\n public get expressionTree(): IExpressionTree {\n return this._expressionTree;\n }\n\n /**\n * Sets the expression tree.\n */\n @Input()\n public set expressionTree(expressionTree: IExpressionTree) {\n this._expressionTree = expressionTree;\n if (!expressionTree) {\n this._selectedEntity = this.isAdvancedFiltering() && this.entities.length === 1 ? this.entities[0] : null;\n this._selectedReturnFields = this._selectedEntity ? this._selectedEntity.fields?.map(f => f.field) : [];\n }\n\n if (!this._preventInit) {\n this.init();\n }\n }\n\n /**\n * Gets the `locale` of the query builder.\n * If not set, defaults to application's locale.\n */\n @Input()\n public get locale(): string {\n return this._locale;\n }\n\n /**\n * Sets the `locale` of the query builder.\n * Expects a valid BCP 47 language tag.\n */\n public set locale(value: string) {\n this._locale = value;\n // if value is invalid, set it back to _localeId\n try {\n getLocaleFirstDayOfWeek(this._locale);\n } catch {\n this._locale = this._localeId;\n }\n }\n\n /**\n * Sets the resource strings.\n * By default it uses EN resources.\n */\n @Input()\n public set resourceStrings(value: IQueryBuilderResourceStrings) {\n this._resourceStrings = Object.assign({}, this._resourceStrings, value);\n }\n\n /**\n * Returns the resource strings.\n */\n public get resourceStrings(): IQueryBuilderResourceStrings {\n return this._resourceStrings;\n }\n\n /**\n * Gets/sets the expected return field.\n */\n @Input() public expectedReturnField: string = null;\n\n /**\n * Event fired as the expression tree is changed.\n */\n @Output()\n public expressionTreeChange = new EventEmitter<IExpressionTree>();\n\n /**\n * Event fired if a nested query builder tree is being edited.\n */\n @Output()\n public inEditModeChange = new EventEmitter<ExpressionOperandItem>();\n\n @ViewChild('entitySelect', { read: IgxSelectComponent })\n protected entitySelect: IgxSelectComponent;\n\n @ViewChild('editingInputs', { read: ElementRef })\n private editingInputs: ElementRef;\n\n @ViewChild('returnFieldsCombo', { read: IgxComboComponent })\n private returnFieldsCombo: IgxComboComponent;\n\n @ViewChild('returnFieldSelect', { read: IgxSelectComponent })\n protected returnFieldSelect: IgxSelectComponent;\n\n @ViewChild('fieldSelect', { read: IgxSelectComponent })\n private fieldSelect: IgxSelectComponent;\n\n @ViewChild('conditionSelect', { read: IgxSelectComponent })\n private conditionSelect: IgxSelectComponent;\n\n @ViewChild('searchValueInput', { read: ElementRef })\n private searchValueInput: ElementRef;\n\n @ViewChild('picker')\n private picker: IgxDatePickerComponent | IgxTimePickerComponent;\n\n @ViewChild('addRootAndGroupButton', { read: ElementRef })\n private addRootAndGroupButton: ElementRef;\n\n @ViewChild('addConditionButton', { read: ElementRef })\n private addConditionButton: ElementRef;\n\n @ViewChild('entityChangeDialog', { read: IgxDialogComponent })\n private entityChangeDialog: IgxDialogComponent;\n\n @ViewChild('addOptionsDropDown', { read: IgxDropDownComponent })\n private addExpressionItemDropDown: IgxDropDownComponent;\n\n @ViewChild('groupContextMenuDropDown', { read: IgxDropDownComponent })\n private groupContextMenuDropDown: IgxDropDownComponent;\n\n /**\n * @hidden @internal\n */\n @ViewChildren(IgxChipComponent, { read: IgxChipComponent })\n public expressionsChips: QueryList<IgxChipComponent>;\n\n @ViewChild('editingInputsContainer', { read: ElementRef })\n protected set editingInputsContainer(value: ElementRef) {\n if ((value && !this._editingInputsContainer) ||\n (value && this._editingInputsContainer && this._editingInputsContainer.nativeElement !== value.nativeElement)) {\n requestAnimationFrame(() => {\n this.scrollElementIntoView(value.nativeElement);\n });\n }\n\n this._editingInputsContainer = value;\n }\n\n /** @hidden */\n protected get editingInputsContainer(): ElementRef {\n return this._editingInputsContainer;\n }\n\n @ViewChild('currentGroupButtonsContainer', { read: ElementRef })\n protected set currentGroupButtonsContainer(value: ElementRef) {\n if ((value && !this._currentGroupButtonsContainer) ||\n (value && this._currentGroupButtonsContainer && this._currentGroupButtonsContainer.nativeElement !== value.nativeElement)) {\n requestAnimationFrame(() => {\n this.scrollElementIntoView(value.nativeElement);\n });\n }\n\n this._currentGroupButtonsContainer = value;\n }\n\n /** @hidden */\n protected get currentGroupButtonsContainer(): ElementRef {\n return this._currentGroupButtonsContainer;\n }\n\n @ViewChild('expressionsContainer')\n private expressionsContainer: ElementRef;\n\n @ViewChild('overlayOutlet', { read: IgxOverlayOutletDirective, static: true })\n private overlayOutlet: IgxOverlayOutletDirective;\n\n @ViewChildren(IgxQueryBuilderTreeComponent)\n private innerQueries: QueryList<IgxQueryBuilderTreeComponent>;\n\n /**\n * @hidden @internal\n */\n public innerQueryNewExpressionTree: IExpressionTree;\n\n /**\n * @hidden @internal\n */\n public rootGroup: ExpressionGroupItem;\n\n /**\n * @hidden @internal\n */\n public selectedExpressions: ExpressionOperandItem[] = [];\n\n /**\n * @hidden @internal\n */\n public currentGroup: ExpressionGroupItem;\n\n /**\n * @hidden @internal\n */\n public contextualGroup: ExpressionGroupItem;\n\n /**\n * @hidden @internal\n */\n public filteringLogics;\n\n /**\n * @hidden @internal\n */\n public selectedCondition: string;\n\n /**\n * @hidden @internal\n */\n public searchValue: { value: any } = { value: null };\n\n /**\n * @hidden @internal\n */\n public pickerOutlet: IgxOverlayOutletDirective | ElementRef;\n\n /**\n * @hidden @internal\n */\n public prevFocusedExpression: ExpressionOperandItem;\n\n /**\n * @hidden @internal\n */\n public initialOperator = 0;\n\n /**\n * @hidden @internal\n */\n public returnFieldSelectOverlaySettings: OverlaySettings = {\n scrollStrategy: new AbsoluteScrollStrategy(),\n modal: false,\n closeOnOutsideClick: true\n };\n\n /**\n * @hidden @internal\n */\n public entitySelectOverlaySettings: OverlaySettings = {\n scrollStrategy: new AbsoluteScrollStrategy(),\n modal: false,\n closeOnOutsideClick: true\n };\n\n /**\n * @hidden @internal\n */\n public fieldSelectOverlaySettings: OverlaySettings = {\n scrollStrategy: new AbsoluteScrollStrategy(),\n modal: false,\n closeOnOutsideClick: true\n };\n\n /**\n * @hidden @internal\n */\n public conditionSelectOverlaySettings: OverlaySettings = {\n scrollStrategy: new AbsoluteScrollStrategy(),\n modal: false,\n closeOnOutsideClick: true\n };\n\n /**\n * @hidden @internal\n */\n public addExpressionDropDownOverlaySettings: OverlaySettings = {\n scrollStrategy: new AbsoluteScrollStrategy(),\n modal: false,\n closeOnOutsideClick: true\n };\n\n /**\n * @hidden @internal\n */\n public groupContextMenuDropDownOverlaySettings: OverlaySettings = {\n scrollStrategy: new AbsoluteScrollStrategy(),\n modal: false,\n closeOnOutsideClick: true\n };\n\n private destroy$ = new Subject<any>();\n private _timeoutId: any;\n private _lastFocusedChipIndex: number;\n private _focusDelay = DEFAULT_CHIP_FOCUS_DELAY;\n private _parentExpression: ExpressionOperandItem;\n private _selectedEntity: EntityType;\n private _selectedReturnFields: string | string[];\n private _selectedField: FieldType;\n private _editingInputsContainer: ElementRef;\n private _currentGroupButtonsContainer: ElementRef;\n private _addModeExpression: ExpressionOperandItem;\n private _editedExpression: ExpressionOperandItem;\n private _preventInit = false;\n private _prevFocusedContainer: ElementRef;\n private _expandedExpressions: IFilteringExpression[] = [];\n private _fields: FieldType[];\n private _locale;\n private _entityNewValue: EntityType;\n private _resourceStrings = getCurrentResourceStrings(QueryBuilderResourceStringsEN);\n\n /**\n * Returns if the select entity dropdown at the root level is disabled after the initial selection.\n */\n public get disableEntityChange(): boolean {\n\n return !this.parentExpression && this.selectedEntity ? this.queryBuilder.disableEntityChange : false;\n }\n\n /**\n * Returns if the fields combo at the root level is disabled.\n */\n public get disableReturnFieldsChange(): boolean {\n\n return !this.selectedEntity || this.queryBuilder.disableReturnFieldsChange;\n }\n\n /**\n * Returns the current level.\n */\n public get level(): number {\n let parent = this.elRef.nativeElement.parentElement;\n let _level = 0;\n while (parent) {\n if (parent.localName === 'igx-query-builder-tree') {\n _level++;\n }\n parent = parent.parentElement;\n }\n return _level;\n }\n\n private _positionSettings = {\n horizontalStartPoint: HorizontalAlignment.Right,\n verticalStartPoint: VerticalAlignment.Top\n };\n\n private _overlaySettings: OverlaySettings = {\n closeOnOutsideClick: false,\n modal: false,\n positionStrategy: new ConnectedPositioningStrategy(this._positionSettings),\n scrollStrategy: new CloseScrollStrategy()\n };\n\n /** @hidden */\n protected isAdvancedFiltering(): boolean {\n return (this.entities?.length === 1 && !this.entities[0]?.name) ||\n this.entities?.find(e => e.childEntities?.length > 0) !== undefined ||\n (this.entities?.length > 0 && this.queryBuilder?.entities?.length > 0 && this.entities !== this.queryBuilder?.entities);\n }\n\n /** @hidden */\n protected isHierarchicalNestedQuery(): boolean {\n return this.queryBuilder.entities !== this.entities\n }\n\n /** @hidden */\n protected isSearchValueInputDisabled(): boolean {\n return !this.selectedField ||\n !this.selectedCondition ||\n (this.selectedField &&\n (this.selectedField.filters.condition(this.selectedCondition).isUnary ||\n this.selectedField.filters.condition(this.selectedCondition).isNestedQuery));\n }\n\n constructor() {\n const elRef = this.elRef;\n\n this.locale = this.locale || this._localeId;\n this.dragService.register(this, elRef);\n }\n\n /**\n * @hidden @internal\n */\n public ngAfterViewInit(): void {\n this._overlaySettings.outlet = this.overlayOutlet;\n this.entitySelectOverlaySettings.outlet = this.overlayOutlet;\n this.fieldSelectOverlaySettings.outlet = this.overlayOutlet;\n this.conditionSelectOverlaySettings.outlet = this.overlayOutlet;\n this.returnFieldSelectOverlaySettings.outlet = this.overlayOutlet;\n this.addExpressionDropDownOverlaySettings.outlet = this.overlayOutlet;\n this.groupContextMenuDropDownOverlaySettings.outlet = this.overlayOutlet;\n\n if (this.isAdvancedFiltering() && this.entities?.length === 1) {\n this.selectedEntity = this.entities[0].name;\n if (this._selectedEntity.fields.find(f => f.field === this.expectedReturnField)) {\n this._selectedReturnFields = [this.expectedReturnField];\n }\n }\n\n // Trigger additional change detection cycle\n this.cdr.detectChanges();\n }\n\n /**\n * @hidden @internal\n */\n public ngOnDestroy(): void {\n this.destroy$.next(true);\n this.destroy$.complete();\n }\n\n /**\n * @hidden @internal\n */\n public set selectedEntity(value: string) {\n this._selectedEntity = this.entities?.find(el => el.name === value);\n }\n\n /**\n * @hidden @internal\n */\n public get selectedEntity(): EntityType {\n return this._selectedEntity;\n }\n\n /**\n * @hidden @internal\n */\n public onEntitySelectChanging(event: ISelectionEventArgs) {\n event.cancel = true;\n this._entityNewValue = event.newSelection.value;\n if (event.oldSelection.value && this.queryBuilder.showEntityChangeDialog) {\n this.entityChangeDialog.open();\n } else {\n this.onEntityChangeConfirm();\n }\n }\n\n /**\n * @hidden\n */\n public onShowEntityChangeDialogChange(eventArgs: IChangeCheckboxEventArgs) {\n this.queryBuilder.showEntityChangeDialog = !eventArgs.checked;\n }\n\n /**\n * @hidden\n */\n public onEntityChangeCancel() {\n this.entityChangeDialog.close();\n this.entitySelect.close();\n this._entityNewValue = null;\n }\n\n /**\n * @hidden\n */\n public onEntityChangeConfirm() {\n if (this._parentExpression) {\n this._expressionTree = this.createExpressionTreeFromGroupItem(this.createExpressionGroupItem(this._expressionTree));\n }\n\n this._selectedEntity = this._entityNewValue;\n if (!this._selectedEntity.fields) {\n this._selectedEntity.fields = [];\n }\n this.fields = this._entityNewValue ? this._entityNewValue.fields : [];\n\n if (this._selectedEntity.fields