UNPKG

igniteui-angular

Version:

Ignite UI for Angular is a dependency-free Angular toolkit for building modern web apps

1 lines • 116 kB
{"version":3,"file":"igniteui-angular-tree.mjs","sources":["../../../projects/igniteui-angular/tree/src/tree/common.ts","../../../projects/igniteui-angular/tree/src/tree/tree.service.ts","../../../projects/igniteui-angular/tree/src/tree/tree-selection.service.ts","../../../projects/igniteui-angular/tree/src/tree/tree-navigation.service.ts","../../../projects/igniteui-angular/tree/src/tree/tree-node/tree-node.component.ts","../../../projects/igniteui-angular/tree/src/tree/tree-node/tree-node.component.html","../../../projects/igniteui-angular/tree/src/tree/tree.component.ts","../../../projects/igniteui-angular/tree/src/tree/tree.component.html","../../../projects/igniteui-angular/tree/src/tree/public_api.ts","../../../projects/igniteui-angular/tree/src/tree/tree.module.ts","../../../projects/igniteui-angular/tree/src/igniteui-angular-tree.ts"],"sourcesContent":["import { ElementRef, EventEmitter, InjectionToken, QueryList, TemplateRef } from '@angular/core';\nimport { IBaseCancelableBrowserEventArgs, IBaseEventArgs } from 'igniteui-angular/core';\nimport { ToggleAnimationSettings } from 'igniteui-angular/expansion-panel';\n\n// Component interfaces\n\n/** Comparer function that can be used when searching through IgxTreeNode<any>[] */\nexport type IgxTreeSearchResolver = (data: any, node: IgxTreeNode<any>) => boolean;\n\nexport interface IgxTree {\n /** @hidden @internal */\n nodes: QueryList<IgxTreeNode<any>>;\n /** @hidden @internal */\n rootNodes: IgxTreeNode<any>[];\n singleBranchExpand: boolean;\n toggleNodeOnClick: boolean;\n selection: IgxTreeSelectionType;\n expandIndicator: TemplateRef<any>;\n animationSettings: ToggleAnimationSettings;\n /** @hidden @internal */\n forceSelect: IgxTreeNode<any>[];\n /** @hidden @internal */\n disabledChange: EventEmitter<IgxTreeNode<any>>;\n /** @hidden @internal */\n activeNodeBindingChange: EventEmitter<IgxTreeNode<any>>;\n nodeSelection: EventEmitter<ITreeNodeSelectionEvent>;\n nodeExpanding: EventEmitter<ITreeNodeTogglingEventArgs>;\n nodeExpanded: EventEmitter<ITreeNodeToggledEventArgs>;\n nodeCollapsing: EventEmitter<ITreeNodeTogglingEventArgs>;\n nodeCollapsed: EventEmitter<ITreeNodeToggledEventArgs>;\n activeNodeChanged: EventEmitter<IgxTreeNode<any>>;\n expandAll(nodes: IgxTreeNode<any>[]): void;\n collapseAll(nodes: IgxTreeNode<any>[]): void;\n deselectAll(node?: IgxTreeNode<any>[]): void;\n findNodes(searchTerm: any, comparer?: IgxTreeSearchResolver): IgxTreeNode<any>[] | null;\n}\n\n// Item interfaces\nexport interface IgxTreeNode<T> {\n parentNode?: IgxTreeNode<any> | null;\n loading: boolean;\n path: IgxTreeNode<any>[];\n expanded: boolean | null;\n /** @hidden @internal */\n indeterminate: boolean;\n selected: boolean | null;\n disabled: boolean;\n /** @hidden @internal */\n isFocused: boolean;\n active: boolean;\n level: number;\n data: T;\n /** @hidden @internal */\n nativeElement: HTMLElement;\n /** @hidden @internal */\n header: ElementRef;\n /** @hidden @internal */\n tabIndex: number;\n /** @hidden @internal */\n allChildren: QueryList<IgxTreeNode<any>>;\n /** @hidden @internal */\n _children: QueryList<IgxTreeNode<any>> | null;\n selectedChange: EventEmitter<boolean>;\n expandedChange: EventEmitter<boolean>;\n expand(): void;\n collapse(): void;\n toggle(): void;\n /** @hidden @internal */\n addLinkChild(node: any): void;\n /** @hidden @internal */\n removeLinkChild(node: any): void;\n}\n\n// Events\nexport interface ITreeNodeSelectionEvent extends IBaseCancelableBrowserEventArgs {\n oldSelection: IgxTreeNode<any>[];\n newSelection: IgxTreeNode<any>[];\n added: IgxTreeNode<any>[];\n removed: IgxTreeNode<any>[];\n event?: Event;\n}\n\nexport interface ITreeNodeEditingEvent extends IBaseCancelableBrowserEventArgs {\n node: IgxTreeNode<any>;\n value: string;\n}\n\nexport interface ITreeNodeEditedEvent extends IBaseEventArgs {\n node: IgxTreeNode<any>;\n value: any;\n}\n\nexport interface ITreeNodeTogglingEventArgs extends IBaseEventArgs, IBaseCancelableBrowserEventArgs {\n node: IgxTreeNode<any>;\n}\n\nexport interface ITreeNodeToggledEventArgs extends IBaseEventArgs {\n node: IgxTreeNode<any>;\n}\n\n// Enums\nexport const IgxTreeSelectionType = {\n None: 'None',\n BiState: 'BiState',\n Cascading: 'Cascading'\n} as const;\nexport type IgxTreeSelectionType = (typeof IgxTreeSelectionType)[keyof typeof IgxTreeSelectionType];\n\n// Token\nexport const IGX_TREE_COMPONENT = /*@__PURE__*/new InjectionToken<IgxTree>('IgxTreeToken');\nexport const IGX_TREE_NODE_COMPONENT = /*@__PURE__*/new InjectionToken<IgxTreeNode<any>>('IgxTreeNodeToken');\n","import { Injectable } from '@angular/core';\nimport { IgxTree, IgxTreeNode } from './common';\n\n/** @hidden @internal */\n@Injectable()\nexport class IgxTreeService {\n public expandedNodes: Set<IgxTreeNode<any>> = new Set<IgxTreeNode<any>>();\n public collapsingNodes: Set<IgxTreeNode<any>> = new Set<IgxTreeNode<any>>();\n private tree: IgxTree;\n\n /**\n * Adds the node to the `expandedNodes` set and fires the nodes change event\n *\n * @param node target node\n * @param uiTrigger is the event triggered by a ui interraction (so we know if we should animate)\n * @returns void\n */\n public expand(node: IgxTreeNode<any>, uiTrigger?: boolean): void {\n this.collapsingNodes.delete(node);\n if (!this.expandedNodes.has(node)) {\n node.expandedChange.emit(true);\n } else {\n return;\n }\n this.expandedNodes.add(node);\n if (this.tree.singleBranchExpand) {\n this.tree.findNodes(node, this.siblingComparer)?.forEach(e => {\n if (uiTrigger) {\n e.collapse();\n } else {\n e.expanded = false;\n }\n });\n }\n }\n\n /**\n * Adds a node to the `collapsing` collection\n *\n * @param node target node\n */\n public collapsing(node: IgxTreeNode<any>): void {\n this.collapsingNodes.add(node);\n }\n\n /**\n * Removes the node from the 'expandedNodes' set and emits the node's change event\n *\n * @param node target node\n * @returns void\n */\n public collapse(node: IgxTreeNode<any>): void {\n if (this.expandedNodes.has(node)) {\n node.expandedChange.emit(false);\n }\n this.collapsingNodes.delete(node);\n this.expandedNodes.delete(node);\n }\n\n public isExpanded(node: IgxTreeNode<any>): boolean {\n return this.expandedNodes.has(node);\n }\n\n public register(tree: IgxTree) {\n this.tree = tree;\n }\n\n private siblingComparer:\n (data: IgxTreeNode<any>, node: IgxTreeNode<any>) => boolean =\n (data: IgxTreeNode<any>, node: IgxTreeNode<any>) => node !== data && node.level === data.level;\n}\n","import { Injectable } from '@angular/core';\nimport { IgxTree, IgxTreeNode, IgxTreeSelectionType, ITreeNodeSelectionEvent } from './common';\n\n/** A collection containing the nodes affected in the selection as well as their direct parents */\ninterface CascadeSelectionNodeCollection {\n nodes: Set<IgxTreeNode<any>>;\n parents: Set<IgxTreeNode<any>>;\n}\n\n/** @hidden @internal */\n@Injectable()\nexport class IgxTreeSelectionService {\n private tree: IgxTree;\n private nodeSelection: Set<IgxTreeNode<any>> = new Set<IgxTreeNode<any>>();\n private indeterminateNodes: Set<IgxTreeNode<any>> = new Set<IgxTreeNode<any>>();\n\n private nodesToBeSelected: Set<IgxTreeNode<any>>;\n private nodesToBeIndeterminate: Set<IgxTreeNode<any>>;\n\n public register(tree: IgxTree) {\n this.tree = tree;\n }\n\n /** Select range from last selected node to the current specified node. */\n public selectMultipleNodes(node: IgxTreeNode<any>, event?: Event): void {\n if (!this.nodeSelection.size) {\n this.selectNode(node);\n return;\n }\n const lastSelectedNodeIndex = this.tree.nodes.toArray().indexOf(this.getSelectedNodes()[this.nodeSelection.size - 1]);\n const currentNodeIndex = this.tree.nodes.toArray().indexOf(node);\n const nodes = this.tree.nodes.toArray().slice(Math.min(currentNodeIndex, lastSelectedNodeIndex),\n Math.max(currentNodeIndex, lastSelectedNodeIndex) + 1);\n\n const added = nodes.filter(_node => !this.isNodeSelected(_node));\n const newSelection = this.getSelectedNodes().concat(added);\n this.emitNodeSelectionEvent(newSelection, added, [], event);\n }\n\n /** Select the specified node and emit event. */\n public selectNode(node: IgxTreeNode<any>, event?: Event): void {\n if (this.tree.selection === IgxTreeSelectionType.None) {\n return;\n }\n this.emitNodeSelectionEvent([...this.getSelectedNodes(), node], [node], [], event);\n }\n\n /** Deselect the specified node and emit event. */\n public deselectNode(node: IgxTreeNode<any>, event?: Event): void {\n const newSelection = this.getSelectedNodes().filter(r => r !== node);\n this.emitNodeSelectionEvent(newSelection, [], [node], event);\n }\n\n /** Clears node selection */\n public clearNodesSelection(): void {\n this.nodeSelection.clear();\n this.indeterminateNodes.clear();\n }\n\n public isNodeSelected(node: IgxTreeNode<any>): boolean {\n return this.nodeSelection.has(node);\n }\n\n public isNodeIndeterminate(node: IgxTreeNode<any>): boolean {\n return this.indeterminateNodes.has(node);\n }\n\n /** Select specified nodes. No event is emitted. */\n public selectNodesWithNoEvent(nodes: IgxTreeNode<any>[], clearPrevSelection = false, shouldEmit = true): void {\n if (this.tree && this.tree.selection === IgxTreeSelectionType.Cascading) {\n this.cascadeSelectNodesWithNoEvent(nodes, clearPrevSelection);\n return;\n }\n\n const oldSelection = this.getSelectedNodes();\n\n if (clearPrevSelection) {\n this.nodeSelection.clear();\n }\n nodes.forEach(node => this.nodeSelection.add(node));\n\n if (shouldEmit) {\n this.emitSelectedChangeEvent(oldSelection);\n }\n }\n\n /** Deselect specified nodes. No event is emitted. */\n public deselectNodesWithNoEvent(nodes?: IgxTreeNode<any>[], shouldEmit = true): void {\n const oldSelection = this.getSelectedNodes();\n\n if (!nodes) {\n this.nodeSelection.clear();\n } else if (this.tree && this.tree.selection === IgxTreeSelectionType.Cascading) {\n this.cascadeDeselectNodesWithNoEvent(nodes);\n } else {\n nodes.forEach(node => this.nodeSelection.delete(node));\n }\n\n if (shouldEmit) {\n this.emitSelectedChangeEvent(oldSelection);\n }\n }\n\n /** Called on `node.ngOnDestroy` to ensure state is correct after node is removed */\n public ensureStateOnNodeDelete(node: IgxTreeNode<any>): void {\n if (this.tree?.selection !== IgxTreeSelectionType.Cascading) {\n return;\n }\n requestAnimationFrame(() => {\n if (this.isNodeSelected(node)) {\n // node is destroyed, do not emit event\n this.deselectNodesWithNoEvent([node], false);\n } else {\n if (!node.parentNode) {\n return;\n }\n const assitantLeafNode = node.parentNode?.allChildren.find(e => !e._children?.length);\n if (!assitantLeafNode) {\n return;\n }\n this.retriggerNodeState(assitantLeafNode);\n }\n });\n }\n\n /** Retriggers a node's selection state */\n private retriggerNodeState(node: IgxTreeNode<any>): void {\n if (node.selected) {\n this.nodeSelection.delete(node);\n this.selectNodesWithNoEvent([node], false, false);\n } else {\n this.nodeSelection.add(node);\n this.deselectNodesWithNoEvent([node], false);\n }\n }\n\n /** Returns array of the selected nodes. */\n private getSelectedNodes(): IgxTreeNode<any>[] {\n return this.nodeSelection.size ? Array.from(this.nodeSelection) : [];\n }\n\n /** Returns array of the nodes in indeterminate state. */\n private getIndeterminateNodes(): IgxTreeNode<any>[] {\n return this.indeterminateNodes.size ? Array.from(this.indeterminateNodes) : [];\n }\n\n private emitNodeSelectionEvent(\n newSelection: IgxTreeNode<any>[], added: IgxTreeNode<any>[], removed: IgxTreeNode<any>[], event: Event\n ): boolean {\n if (this.tree.selection === IgxTreeSelectionType.Cascading) {\n this.emitCascadeNodeSelectionEvent(newSelection, added, removed, event);\n return;\n }\n const currSelection = this.getSelectedNodes();\n if (this.areEqualCollections(currSelection, newSelection)) {\n return;\n }\n\n const args: ITreeNodeSelectionEvent = {\n oldSelection: currSelection, newSelection,\n added, removed, event, cancel: false, owner: this.tree\n };\n this.tree.nodeSelection.emit(args);\n if (args.cancel) {\n return;\n }\n this.selectNodesWithNoEvent(args.newSelection, true);\n }\n\n private areEqualCollections(first: IgxTreeNode<any>[], second: IgxTreeNode<any>[]): boolean {\n return first.length === second.length && new Set(first.concat(second)).size === first.length;\n }\n\n private cascadeSelectNodesWithNoEvent(nodes?: IgxTreeNode<any>[], clearPrevSelection = false): void {\n const oldSelection = this.getSelectedNodes();\n\n if (clearPrevSelection) {\n this.indeterminateNodes.clear();\n this.nodeSelection.clear();\n this.calculateNodesNewSelectionState({ added: nodes, removed: [] });\n } else {\n const newSelection = [...oldSelection, ...nodes];\n const args: Partial<ITreeNodeSelectionEvent> = { oldSelection, newSelection };\n\n // retrieve only the rows without their parents/children which has to be added to the selection\n this.populateAddRemoveArgs(args);\n\n this.calculateNodesNewSelectionState(args);\n }\n this.nodeSelection = new Set(this.nodesToBeSelected);\n this.indeterminateNodes = new Set(this.nodesToBeIndeterminate);\n\n this.emitSelectedChangeEvent(oldSelection);\n }\n\n private cascadeDeselectNodesWithNoEvent(nodes: IgxTreeNode<any>[]): void {\n const args = { added: [], removed: nodes };\n this.calculateNodesNewSelectionState(args);\n\n this.nodeSelection = new Set<IgxTreeNode<any>>(this.nodesToBeSelected);\n this.indeterminateNodes = new Set<IgxTreeNode<any>>(this.nodesToBeIndeterminate);\n }\n\n /**\n * populates the nodesToBeSelected and nodesToBeIndeterminate sets\n * with the nodes which will be eventually in selected/indeterminate state\n */\n private calculateNodesNewSelectionState(args: Partial<ITreeNodeSelectionEvent>): void {\n this.nodesToBeSelected = new Set<IgxTreeNode<any>>(args.oldSelection ? args.oldSelection : this.getSelectedNodes());\n this.nodesToBeIndeterminate = new Set<IgxTreeNode<any>>(this.getIndeterminateNodes());\n\n this.cascadeSelectionState(args.removed, false);\n this.cascadeSelectionState(args.added, true);\n }\n\n /** Ensures proper selection state for all predescessors and descendants during a selection event */\n private cascadeSelectionState(nodes: IgxTreeNode<any>[], selected: boolean): void {\n if (!nodes || nodes.length === 0) {\n return;\n }\n\n if (nodes && nodes.length > 0) {\n const nodeCollection: CascadeSelectionNodeCollection = this.getCascadingNodeCollection(nodes);\n\n nodeCollection.nodes.forEach(node => {\n if (selected) {\n this.nodesToBeSelected.add(node);\n } else {\n this.nodesToBeSelected.delete(node);\n }\n this.nodesToBeIndeterminate.delete(node);\n });\n\n Array.from(nodeCollection.parents).forEach((parent) => {\n this.handleParentSelectionState(parent);\n });\n }\n }\n\n private emitCascadeNodeSelectionEvent(newSelection, added, removed, event?): boolean {\n const currSelection = this.getSelectedNodes();\n if (this.areEqualCollections(currSelection, newSelection)) {\n return;\n }\n\n const args: ITreeNodeSelectionEvent = {\n oldSelection: currSelection, newSelection,\n added, removed, event, cancel: false, owner: this.tree\n };\n\n this.calculateNodesNewSelectionState(args);\n\n args.newSelection = Array.from(this.nodesToBeSelected);\n\n // retrieve nodes/parents/children which has been added/removed from the selection\n this.populateAddRemoveArgs(args);\n\n this.tree.nodeSelection.emit(args);\n\n if (args.cancel) {\n return;\n }\n\n // if args.newSelection hasn't been modified\n if (this.areEqualCollections(Array.from(this.nodesToBeSelected), args.newSelection)) {\n this.nodeSelection = new Set<IgxTreeNode<any>>(this.nodesToBeSelected);\n this.indeterminateNodes = new Set(this.nodesToBeIndeterminate);\n this.emitSelectedChangeEvent(currSelection);\n } else {\n // select the nodes within the modified args.newSelection with no event\n this.cascadeSelectNodesWithNoEvent(args.newSelection, true);\n }\n }\n\n /**\n * recursively handle the selection state of the direct and indirect parents\n */\n private handleParentSelectionState(node: IgxTreeNode<any>) {\n if (!node) {\n return;\n }\n this.handleNodeSelectionState(node);\n if (node.parentNode) {\n this.handleParentSelectionState(node.parentNode);\n }\n }\n\n /**\n * Handle the selection state of a given node based the selection states of its direct children\n */\n private handleNodeSelectionState(node: IgxTreeNode<any>) {\n const nodesArray = (node && node._children) ? node._children.toArray() : [];\n if (nodesArray.length) {\n if (nodesArray.every(n => this.nodesToBeSelected.has(n))) {\n this.nodesToBeSelected.add(node);\n this.nodesToBeIndeterminate.delete(node);\n } else if (nodesArray.some(n => this.nodesToBeSelected.has(n) || this.nodesToBeIndeterminate.has(n))) {\n this.nodesToBeIndeterminate.add(node);\n this.nodesToBeSelected.delete(node);\n } else {\n this.nodesToBeIndeterminate.delete(node);\n this.nodesToBeSelected.delete(node);\n }\n } else {\n // if the children of the node has been deleted and the node was selected do not change its state\n if (this.isNodeSelected(node)) {\n this.nodesToBeSelected.add(node);\n } else {\n this.nodesToBeSelected.delete(node);\n }\n this.nodesToBeIndeterminate.delete(node);\n }\n }\n\n /**\n * Get a collection of all nodes affected by the change event\n *\n * @param nodesToBeProcessed set of the nodes to be selected/deselected\n * @returns a collection of all affected nodes and all their parents\n */\n private getCascadingNodeCollection(nodes: IgxTreeNode<any>[]): CascadeSelectionNodeCollection {\n const collection: CascadeSelectionNodeCollection = {\n parents: new Set<IgxTreeNode<any>>(),\n nodes: new Set<IgxTreeNode<any>>(nodes)\n };\n\n Array.from(collection.nodes).forEach((node) => {\n const nodeAndAllChildren = node.allChildren?.toArray() || [];\n nodeAndAllChildren.forEach(n => {\n collection.nodes.add(n);\n });\n\n if (node && node.parentNode) {\n collection.parents.add(node.parentNode);\n }\n });\n return collection;\n }\n\n /**\n * retrieve the nodes which should be added/removed to/from the old selection\n */\n private populateAddRemoveArgs(args: Partial<ITreeNodeSelectionEvent>): void {\n args.removed = args.oldSelection.filter(x => args.newSelection.indexOf(x) < 0);\n args.added = args.newSelection.filter(x => args.oldSelection.indexOf(x) < 0);\n }\n\n /** Emits the `selectedChange` event for each node affected by the selection */\n private emitSelectedChangeEvent(oldSelection: IgxTreeNode<any>[]): void {\n this.getSelectedNodes().forEach(n => {\n if (oldSelection.indexOf(n) < 0) {\n n.selectedChange.emit(true);\n }\n });\n\n oldSelection.forEach(n => {\n if (!this.nodeSelection.has(n)) {\n n.selectedChange.emit(false);\n }\n });\n }\n}\n","import { Injectable, OnDestroy, inject } from '@angular/core';\nimport { IgxTree, IgxTreeNode, IgxTreeSelectionType } from './common';\nimport { NAVIGATION_KEYS } from 'igniteui-angular/core';\nimport { IgxTreeService } from './tree.service';\nimport { IgxTreeSelectionService } from './tree-selection.service';\nimport { Subject } from 'rxjs';\n\n/** @hidden @internal */\n@Injectable()\nexport class IgxTreeNavigationService implements OnDestroy {\n private treeService = inject(IgxTreeService);\n private selectionService = inject(IgxTreeSelectionService);\n\n private tree: IgxTree;\n\n private _focusedNode: IgxTreeNode<any> = null;\n private _lastFocusedNode: IgxTreeNode<any> = null;\n private _activeNode: IgxTreeNode<any> = null;\n\n private _visibleChildren: IgxTreeNode<any>[] = [];\n private _invisibleChildren: Set<IgxTreeNode<any>> = new Set();\n private _disabledChildren: Set<IgxTreeNode<any>> = new Set();\n\n private _cacheChange = new Subject<void>();\n\n constructor() {\n this._cacheChange.subscribe(() => {\n this._visibleChildren =\n this.tree?.nodes ?\n this.tree.nodes.filter(e => !(this._invisibleChildren.has(e) || this._disabledChildren.has(e))) :\n [];\n });\n }\n\n public register(tree: IgxTree) {\n this.tree = tree;\n }\n\n public get focusedNode() {\n return this._focusedNode;\n }\n\n public set focusedNode(value: IgxTreeNode<any>) {\n if (this._focusedNode === value) {\n return;\n }\n this._lastFocusedNode = this._focusedNode;\n if (this._lastFocusedNode) {\n this._lastFocusedNode.tabIndex = -1;\n }\n this._focusedNode = value;\n if (this._focusedNode !== null) {\n this._focusedNode.tabIndex = 0;\n this._focusedNode.header.nativeElement.focus();\n }\n }\n\n public get activeNode() {\n return this._activeNode;\n }\n\n public set activeNode(value: IgxTreeNode<any>) {\n if (this._activeNode === value) {\n return;\n }\n this._activeNode = value;\n this.tree.activeNodeChanged.emit(this._activeNode);\n }\n\n public get visibleChildren(): IgxTreeNode<any>[] {\n return this._visibleChildren;\n }\n\n public update_disabled_cache(node: IgxTreeNode<any>): void {\n if (node.disabled) {\n this._disabledChildren.add(node);\n } else {\n this._disabledChildren.delete(node);\n }\n this._cacheChange.next();\n }\n\n public init_invisible_cache() {\n this.tree.nodes.filter(e => e.level === 0).forEach(node => {\n this.update_visible_cache(node, node.expanded, false);\n });\n this._cacheChange.next();\n }\n\n public update_visible_cache(node: IgxTreeNode<any>, expanded: boolean, shouldEmit = true): void {\n if (expanded) {\n node._children.forEach(child => {\n this._invisibleChildren.delete(child);\n this.update_visible_cache(child, child.expanded, false);\n });\n } else {\n node.allChildren.forEach(c => this._invisibleChildren.add(c));\n }\n\n if (shouldEmit) {\n this._cacheChange.next();\n }\n }\n\n /**\n * Sets the node as focused (and active)\n *\n * @param node target node\n * @param isActive if true, sets the node as active\n */\n public setFocusedAndActiveNode(node: IgxTreeNode<any>, isActive = true): void {\n if (isActive) {\n this.activeNode = node;\n }\n this.focusedNode = node;\n }\n\n /** Handler for keydown events. Used in tree.component.ts */\n public handleKeydown(event: KeyboardEvent) {\n const key = event.key.toLowerCase();\n if (!this.focusedNode) {\n return;\n }\n if (!(NAVIGATION_KEYS.has(key) || key === '*')) {\n if (key === 'enter') {\n this.activeNode = this.focusedNode;\n }\n return;\n }\n event.preventDefault();\n if (event.repeat) {\n setTimeout(() => this.handleNavigation(event), 1);\n } else {\n this.handleNavigation(event);\n }\n }\n\n public ngOnDestroy() {\n this._cacheChange.next();\n this._cacheChange.complete();\n }\n\n private handleNavigation(event: KeyboardEvent) {\n switch (event.key.toLowerCase()) {\n case 'home':\n this.setFocusedAndActiveNode(this.visibleChildren[0]);\n break;\n case 'end':\n this.setFocusedAndActiveNode(this.visibleChildren[this.visibleChildren.length - 1]);\n break;\n case 'arrowleft':\n case 'left':\n this.handleArrowLeft();\n break;\n case 'arrowright':\n case 'right':\n this.handleArrowRight();\n break;\n case 'arrowup':\n case 'up':\n this.handleUpDownArrow(true, event);\n break;\n case 'arrowdown':\n case 'down':\n this.handleUpDownArrow(false, event);\n break;\n case '*':\n this.handleAsterisk();\n break;\n case ' ':\n case 'spacebar':\n case 'space':\n this.handleSpace(event.shiftKey);\n break;\n default:\n return;\n }\n }\n\n private handleArrowLeft(): void {\n if (this.focusedNode.expanded && !this.treeService.collapsingNodes.has(this.focusedNode) && this.focusedNode._children?.length) {\n this.activeNode = this.focusedNode;\n this.focusedNode.collapse();\n } else {\n const parentNode = this.focusedNode.parentNode;\n if (parentNode && !parentNode.disabled) {\n this.setFocusedAndActiveNode(parentNode);\n }\n }\n }\n\n private handleArrowRight(): void {\n if (this.focusedNode._children.length > 0) {\n if (!this.focusedNode.expanded) {\n this.activeNode = this.focusedNode;\n this.focusedNode.expand();\n } else {\n if (this.treeService.collapsingNodes.has(this.focusedNode)) {\n this.focusedNode.expand();\n return;\n }\n const firstChild = this.focusedNode._children.find(node => !node.disabled);\n if (firstChild) {\n this.setFocusedAndActiveNode(firstChild);\n }\n }\n }\n }\n\n private handleUpDownArrow(isUp: boolean, event: KeyboardEvent): void {\n const next = this.getVisibleNode(this.focusedNode, isUp ? -1 : 1);\n if (next === this.focusedNode) {\n return;\n }\n\n if (event.ctrlKey) {\n this.setFocusedAndActiveNode(next, false);\n } else {\n this.setFocusedAndActiveNode(next);\n }\n }\n\n private handleAsterisk(): void {\n const nodes = this.focusedNode.parentNode ? this.focusedNode.parentNode._children : this.tree.rootNodes;\n nodes?.forEach(node => {\n if (!node.disabled && (!node.expanded || this.treeService.collapsingNodes.has(node))) {\n node.expand();\n }\n });\n }\n\n private handleSpace(shiftKey = false): void {\n if (this.tree.selection === IgxTreeSelectionType.None) {\n return;\n }\n\n this.activeNode = this.focusedNode;\n if (shiftKey) {\n this.selectionService.selectMultipleNodes(this.focusedNode);\n return;\n }\n\n if (this.focusedNode.selected) {\n this.selectionService.deselectNode(this.focusedNode);\n } else {\n this.selectionService.selectNode(this.focusedNode);\n }\n }\n\n /** Gets the next visible node in the given direction - 1 -> next, -1 -> previous */\n private getVisibleNode(node: IgxTreeNode<any>, dir: 1 | -1 = 1): IgxTreeNode<any> {\n const nodeIndex = this.visibleChildren.indexOf(node);\n return this.visibleChildren[nodeIndex + dir] || node;\n }\n}\n","import { ChangeDetectorRef, Component, ContentChildren, Directive, ElementRef, EventEmitter, HostBinding, HostListener, Input, OnDestroy, OnInit, Output, QueryList, TemplateRef, ViewChild, booleanAttribute, inject } from '@angular/core';\nimport { takeUntil } from 'rxjs/operators';\nimport {\n IgxTree,\n IgxTreeNode,\n IgxTreeSelectionType,\n IGX_TREE_COMPONENT,\n IGX_TREE_NODE_COMPONENT,\n ITreeNodeTogglingEventArgs\n} from '../common';\nimport { IgxTreeNavigationService } from '../tree-navigation.service';\nimport { IgxTreeSelectionService } from '../tree-selection.service';\nimport { IgxTreeService } from '../tree.service';\nimport { NgTemplateOutlet, NgClass } from '@angular/common';\nimport { IgxIconComponent } from 'igniteui-angular/icon';\nimport { IgxCheckboxComponent } from 'igniteui-angular/checkbox';\nimport { IgxCircularProgressBarComponent } from 'igniteui-angular/progressbar';\nimport { ToggleAnimationPlayer, ToggleAnimationSettings } from 'igniteui-angular/expansion-panel';\nimport { getCurrentResourceStrings, ITreeResourceStrings, TreeResourceStringsEN } from 'igniteui-angular/core';\n\n// TODO: Implement aria functionality\n/**\n * @hidden @internal\n * Used for links (`a` tags) in the body of an `igx-tree-node`. Handles aria and event dispatch.\n */\n@Directive({\n selector: `[igxTreeNodeLink]`,\n standalone: true\n})\nexport class IgxTreeNodeLinkDirective implements OnDestroy {\n private node = inject<IgxTreeNode<any>>(IGX_TREE_NODE_COMPONENT, { optional: true });\n private navService = inject(IgxTreeNavigationService);\n public elementRef = inject(ElementRef);\n\n\n @HostBinding('attr.role')\n public role = 'treeitem';\n\n /**\n * The node's parent. Should be used only when the link is defined\n * in `<ng-template>` tag outside of its parent, as Angular DI will not properly provide a reference\n *\n * ```html\n * <igx-tree>\n * <igx-tree-node #myNode *ngFor=\"let node of data\" [data]=\"node\">\n * <ng-template *ngTemplateOutlet=\"nodeTemplate; context: { $implicit: data, parentNode: myNode }\">\n * </ng-template>\n * </igx-tree-node>\n * ...\n * <!-- node template is defined under tree to access related services -->\n * <ng-template #nodeTemplate let-data let-node=\"parentNode\">\n * <a [igxTreeNodeLink]=\"node\">{{ data.label }}</a>\n * </ng-template>\n * </igx-tree>\n * ```\n */\n @Input('igxTreeNodeLink')\n public set parentNode(val: any) {\n if (val) {\n this._parentNode = val;\n (this._parentNode as any).addLinkChild(this);\n }\n }\n\n public get parentNode(): any {\n return this._parentNode;\n }\n\n /** A pointer to the parent node */\n private get target(): IgxTreeNode<any> {\n return this.node || this.parentNode;\n }\n\n private _parentNode: IgxTreeNode<any> = null;\n\n /** @hidden @internal */\n @HostBinding('attr.tabindex')\n public get tabIndex(): number {\n return this.navService.focusedNode === this.target ? (this.target?.disabled ? -1 : 0) : -1;\n }\n\n /**\n * @hidden @internal\n * Clear the node's focused state\n */\n @HostListener('blur')\n public handleBlur() {\n this.target.isFocused = false;\n }\n\n /**\n * @hidden @internal\n * Set the node as focused\n */\n @HostListener('focus')\n public handleFocus() {\n if (this.target && !this.target.disabled) {\n if (this.navService.focusedNode !== this.target) {\n this.navService.focusedNode = this.target;\n }\n this.target.isFocused = true;\n }\n }\n\n public ngOnDestroy() {\n this.target.removeLinkChild(this);\n }\n}\n\n/**\n *\n * The tree node component represents a child node of the tree component or another tree node.\n * Usage:\n *\n * ```html\n * <igx-tree>\n * ...\n * <igx-tree-node [data]=\"data\" [selected]=\"service.isNodeSelected(data.Key)\" [expanded]=\"service.isNodeExpanded(data.Key)\">\n * {{ data.FirstName }} {{ data.LastName }}\n * </igx-tree-node>\n * ...\n * </igx-tree>\n * ```\n */\n@Component({\n selector: 'igx-tree-node',\n templateUrl: 'tree-node.component.html',\n providers: [\n { provide: IGX_TREE_NODE_COMPONENT, useExisting: IgxTreeNodeComponent }\n ],\n imports: [NgTemplateOutlet, IgxIconComponent, IgxCheckboxComponent, NgClass, IgxCircularProgressBarComponent]\n})\nexport class IgxTreeNodeComponent<T> extends ToggleAnimationPlayer implements IgxTreeNode<T>, OnInit, OnDestroy {\n public tree = inject<IgxTree>(IGX_TREE_COMPONENT);\n protected selectionService = inject(IgxTreeSelectionService);\n protected treeService = inject(IgxTreeService);\n protected navService = inject(IgxTreeNavigationService);\n protected cdr = inject(ChangeDetectorRef);\n private element = inject<ElementRef<HTMLElement>>(ElementRef);\n public parentNode = inject<IgxTreeNode<any>>(IGX_TREE_NODE_COMPONENT, { optional: true, skipSelf: true });\n\n /**\n * The data entry that the node is visualizing.\n *\n * @remarks\n * Required for searching through nodes.\n *\n * @example\n * ```html\n * <igx-tree>\n * ...\n * <igx-tree-node [data]=\"data\">\n * {{ data.FirstName }} {{ data.LastName }}\n * </igx-tree-node>\n * ...\n * </igx-tree>\n * ```\n */\n @Input()\n public data: T;\n\n /**\n * To be used for load-on-demand scenarios in order to specify whether the node is loading data.\n *\n * @remarks\n * Loading nodes do not render children.\n */\n @Input({ transform: booleanAttribute })\n public loading = false;\n\n // TO DO: return different tab index depending on anchor child\n /** @hidden @internal */\n public set tabIndex(val: number) {\n this._tabIndex = val;\n }\n\n /** @hidden @internal */\n public get tabIndex(): number {\n if (this.disabled) {\n return -1;\n }\n if (this._tabIndex === null) {\n if (this.navService.focusedNode === null) {\n return this.hasLinkChildren ? -1 : 0;\n }\n return -1;\n }\n return this.hasLinkChildren ? -1 : this._tabIndex;\n }\n\n /** @hidden @internal */\n public override get animationSettings(): ToggleAnimationSettings {\n return this.tree.animationSettings;\n }\n\n /**\n * Gets/Sets the resource strings.\n *\n * @remarks\n * Uses EN resources by default.\n */\n @Input()\n public set resourceStrings(value: ITreeResourceStrings) {\n this._resourceStrings = Object.assign({}, this._resourceStrings, value);\n }\n\n /**\n * An accessor that returns the resource strings.\n */\n public get resourceStrings(): ITreeResourceStrings {\n return this._resourceStrings;\n }\n\n /**\n * Gets/Sets the active state of the node\n *\n * @param value: boolean\n */\n @Input({ transform: booleanAttribute })\n public set active(value: boolean) {\n if (value) {\n this.navService.activeNode = this;\n this.tree.activeNodeBindingChange.emit(this);\n }\n }\n\n public get active(): boolean {\n return this.navService.activeNode === this;\n }\n\n /**\n * Emitted when the node's `selected` property changes.\n *\n * ```html\n * <igx-tree>\n * <igx-tree-node *ngFor=\"let node of data\" [data]=\"node\" [(selected)]=\"node.selected\">\n * </igx-tree-node>\n * </igx-tree>\n * ```\n *\n * ```typescript\n * const node: IgxTreeNode<any> = this.tree.findNodes(data[0])[0];\n * node.selectedChange.pipe(takeUntil(this.destroy$)).subscribe((e: boolean) => console.log(\"Node selection changed to \", e))\n * ```\n */\n @Output()\n public selectedChange = new EventEmitter<boolean>();\n\n /**\n * Emitted when the node's `expanded` property changes.\n *\n * ```html\n * <igx-tree>\n * <igx-tree-node *ngFor=\"let node of data\" [data]=\"node\" [(expanded)]=\"node.expanded\">\n * </igx-tree-node>\n * </igx-tree>\n * ```\n *\n * ```typescript\n * const node: IgxTreeNode<any> = this.tree.findNodes(data[0])[0];\n * node.expandedChange.pipe(takeUntil(this.destroy$)).subscribe((e: boolean) => console.log(\"Node expansion state changed to \", e))\n * ```\n */\n @Output()\n public expandedChange = new EventEmitter<boolean>();\n\n /** @hidden @internal */\n public get focused() {\n return this.isFocused &&\n this.navService.focusedNode === this;\n }\n\n /**\n * Retrieves the full path to the node incuding itself\n *\n * ```typescript\n * const node: IgxTreeNode<any> = this.tree.findNodes(data[0])[0];\n * const path: IgxTreeNode<any>[] = node.path;\n * ```\n */\n public get path(): IgxTreeNode<any>[] {\n return this.parentNode?.path ? [...this.parentNode.path, this] : [this];\n }\n\n // TODO: bind to disabled state when node is dragged\n /**\n * Gets/Sets the disabled state of the node\n *\n * @param value: boolean\n */\n @Input({ transform: booleanAttribute })\n @HostBinding('class.igx-tree-node--disabled')\n public get disabled(): boolean {\n return this._disabled;\n }\n\n public set disabled(value: boolean) {\n if (value !== this._disabled) {\n this._disabled = value;\n this.tree.disabledChange.emit(this);\n }\n }\n\n /** @hidden @internal */\n @HostBinding('class.igx-tree-node')\n public cssClass = 'igx-tree-node';\n\n /** @hidden @internal */\n @HostBinding('attr.role')\n public get role() {\n return this.hasLinkChildren ? 'none' : 'treeitem';\n }\n\n /** @hidden @internal */\n @ContentChildren(IgxTreeNodeLinkDirective, { read: ElementRef })\n public linkChildren: QueryList<ElementRef>;\n\n /** @hidden @internal */\n @ContentChildren(IGX_TREE_NODE_COMPONENT, { read: IGX_TREE_NODE_COMPONENT })\n public _children: QueryList<IgxTreeNode<any>>;\n\n /** @hidden @internal */\n @ContentChildren(IGX_TREE_NODE_COMPONENT, { read: IGX_TREE_NODE_COMPONENT, descendants: true })\n public allChildren: QueryList<IgxTreeNode<any>>;\n\n /**\n * Return the child nodes of the node (if any)\n *\n * @remarks\n * Returns `null` if node does not have children\n *\n * @example\n * ```typescript\n * const node: IgxTreeNode<any> = this.tree.findNodes(data[0])[0];\n * const children: IgxTreeNode<any>[] = node.children;\n * ```\n */\n public get children(): IgxTreeNode<any>[] {\n return this._children?.length ? this._children.toArray() : null;\n }\n\n // TODO: will be used in Drag and Drop implementation\n /** @hidden @internal */\n @ViewChild('ghostTemplate', { read: ElementRef })\n public header: ElementRef;\n\n @ViewChild('defaultIndicator', { read: TemplateRef, static: true })\n private _defaultExpandIndicatorTemplate: TemplateRef<any>;\n\n @ViewChild('childrenContainer', { read: ElementRef })\n private childrenContainer: ElementRef;\n\n private get hasLinkChildren(): boolean {\n return this.linkChildren?.length > 0 || this.registeredChildren?.length > 0;\n }\n\n /** @hidden @internal */\n public isFocused: boolean;\n\n /** @hidden @internal */\n public registeredChildren: IgxTreeNodeLinkDirective[] = [];\n\n /** @hidden @internal */\n private _resourceStrings = getCurrentResourceStrings(TreeResourceStringsEN);\n\n private _tabIndex = null;\n private _disabled = false;\n\n /**\n * @hidden @internal\n */\n public get showSelectors() {\n return this.tree.selection !== IgxTreeSelectionType.None;\n }\n\n /**\n * @hidden @internal\n */\n public get indeterminate(): boolean {\n return this.selectionService.isNodeIndeterminate(this);\n }\n\n /** The depth of the node, relative to the root\n *\n * ```html\n * <igx-tree>\n * ...\n * <igx-tree-node #node>\n * My level is {{ node.level }}\n * </igx-tree-node>\n * </igx-tree>\n * ```\n *\n * ```typescript\n * const node: IgxTreeNode<any> = this.tree.findNodes(data[12])[0];\n * const level: number = node.level;\n * ```\n */\n public get level(): number {\n return this.parentNode ? this.parentNode.level + 1 : 0;\n }\n\n /** Get/set whether the node is selected. Supporst two-way binding.\n *\n * ```html\n * <igx-tree>\n * ...\n * <igx-tree-node *ngFor=\"let node of data\" [(selected)]=\"node.selected\">\n * {{ node.label }}\n * </igx-tree-node>\n * </igx-tree>\n * ```\n *\n * ```typescript\n * const node: IgxTreeNode<any> = this.tree.findNodes(data[0])[0];\n * const selected = node.selected;\n * node.selected = true;\n * ```\n */\n @Input({ transform: booleanAttribute })\n public get selected(): boolean {\n return this.selectionService.isNodeSelected(this);\n }\n\n public set selected(val: boolean) {\n if (!(this.tree?.nodes && this.tree.nodes.find((e) => e === this)) && val) {\n this.tree.forceSelect.push(this);\n return;\n }\n if (val && !this.selectionService.isNodeSelected(this)) {\n this.selectionService.selectNodesWithNoEvent([this]);\n }\n if (!val && this.selectionService.isNodeSelected(this)) {\n this.selectionService.deselectNodesWithNoEvent([this]);\n }\n }\n\n /** Get/set whether the node is expanded\n *\n * ```html\n * <igx-tree>\n * ...\n * <igx-tree-node *ngFor=\"let node of data\" [expanded]=\"node.name === this.expandedNode\">\n * {{ node.label }}\n * </igx-tree-node>\n * </igx-tree>\n * ```\n *\n * ```typescript\n * const node: IgxTreeNode<any> = this.tree.findNodes(data[0])[0];\n * const expanded = node.expanded;\n * node.expanded = true;\n * ```\n */\n @Input({ transform: booleanAttribute })\n public get expanded() {\n return this.treeService.isExpanded(this);\n }\n\n public set expanded(val: boolean) {\n if (val) {\n this.treeService.expand(this, false);\n } else {\n this.treeService.collapse(this);\n }\n }\n\n /** @hidden @internal */\n public get expandIndicatorTemplate(): TemplateRef<any> {\n return this.tree?.expandIndicator || this._defaultExpandIndicatorTemplate;\n }\n\n /**\n * The native DOM element representing the node. Could be null in certain environments.\n *\n * ```typescript\n * // get the nativeElement of the second node\n * const node: IgxTreeNode = this.tree.nodes.first();\n * const nodeElement: HTMLElement = node.nativeElement;\n * ```\n */\n /** @hidden @internal */\n public get nativeElement() {\n return this.element.nativeElement;\n }\n\n /** @hidden @internal */\n public ngOnInit() {\n this.openAnimationDone.pipe(takeUntil(this.destroy$)).subscribe(\n () => {\n this.tree.nodeExpanded.emit({ owner: this.tree, node: this });\n }\n );\n this.closeAnimationDone.pipe(takeUntil(this.destroy$)).subscribe(() => {\n this.tree.nodeCollapsed.emit({ owner: this.tree, node: this });\n this.treeService.collapse(this);\n this.cdr.markForCheck();\n });\n }\n\n /**\n * @hidden @internal\n * Sets the focus to the node's <a> child, if present\n * Sets the node as the tree service's focusedNode\n * Marks the node as the current active element\n */\n public handleFocus(): void {\n if (this.disabled) {\n return;\n }\n if (this.navService.focusedNode !== this) {\n this.navService.focusedNode = this;\n }\n this.isFocused = true;\n if (this.linkChildren?.length) {\n this.linkChildren.first.nativeElement.focus();\n return;\n }\n if (this.registeredChildren.length) {\n this.registeredChildren[0].elementRef.nativeElement.focus();\n return;\n }\n }\n\n /**\n * @hidden @internal\n * Clear the node's focused status\n */\n public clearFocus(): void {\n this.isFocused = false;\n }\n\n /**\n * @hidden @internal\n */\n public onSelectorPointerDown(event) {\n event.preventDefault();\n event.stopPropagation()\n }\n\n /**\n * @hidden @internal\n */\n public onSelectorClick(event) {\n // event.stopPropagation();\n event.preventDefault();\n // this.navService.handleFocusedAndActiveNode(this);\n if (event.shiftKey) {\n this.selectionService.selectMultipleNodes(this, event);\n return;\n }\n if (this.selected) {\n this.selectionService.deselectNode(this, event);\n } else {\n this.selectionService.selectNode(this, event);\n }\n }\n\n /**\n * Toggles the node expansion state, triggering animation\n *\n * ```html\n * <igx-tree>\n * <igx-tree-node #node>My Node</igx-tree-node>\n * </igx-tree>\n * <button type=\"button\" igxButton (click)=\"node.toggle()\">Toggle Node</button>\n * ```\n *\n * ```typescript\n * const myNode: IgxTreeNode<any> = this.tree.findNodes(data[0])[0];\n * myNode.toggle();\n * ```\n */\n public toggle() {\n if (this.expanded) {\n this.collapse();\n } else {\n this.expand();\n }\n }\n\n /** @hidden @internal */\n public indicatorClick() {\n if(!this.tree.toggleNodeOnClick) {\n this.toggle();\n this.navService.setFocusedAndActiveNode(this);\n }\n }\n\n /**\n * @hidden @internal\n */\n public onPointerDown(event) {\n event.stopPropagation();\n\n //Toggle the node only on left mouse click - https://w3c.github.io/pointerevents/#button-states\n if(this.tree.toggleNodeOnClick && event.button === 0) {\n this.toggle();\n }\n\n this.navService.setFocusedAndActiveNode(this);\n }\n\n public override ngOnDestroy() {\n super.ngOnDestroy();\n this.selectionService.ensureStateOnNodeDelete(this);\n }\n\n /**\n * Expands the node, triggering animation\n *\n * ```html\n * <igx-tree>\n * <igx-tree-node #node>My Node</igx-tree-node>\n * </igx-tree>\n * <button type=\"button\" igxButton (click)=\"node.expand()\">Expand Node</button>\n * ```\n *\n * ```typescript\n * const myNode: IgxTreeNode<any> = this.tree.findNodes(data[0])[0];\n * myNode.expand();\n * ```\n */\n public expand() {\n if (this.expanded && !this.treeService.collapsingNodes.has(this)) {\n return;\n }\n const args: ITreeNodeTogglingEventArgs = {\n owner: this.tree,\n node: this,\n cancel: false\n\n };\n this.tree.nodeExpanding.emit(args);\n if (!args.cancel) {\n this.treeService.expand(this, true);\n this.cdr.detectChanges();\n this.playOpenAnimation(\n this.childrenContainer\n );\n }\n }\n\n /**\n * Collapses the node, triggering animation\n *\n * ```html\n * <igx-tree>\n * <igx-tree-node #node>My Node</igx-tree-node>\n * </igx-tree>\n * <button type=\"button\" igxButton (cli