UNPKG

igniteui-angular

Version:

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

1,208 lines (1,203 loc) 76.4 kB
import * as i0 from '@angular/core'; import { InjectionToken, Injectable, inject, ElementRef, HostListener, HostBinding, Input, Directive, ChangeDetectorRef, EventEmitter, TemplateRef, booleanAttribute, ViewChild, ContentChildren, Output, Component, ContentChild, NgModule } from '@angular/core'; import { takeUntil, throttleTime } from 'rxjs/operators'; import { NAVIGATION_KEYS, getCurrentResourceStrings, TreeResourceStringsEN, PlatformUtil, resizeObservable } from 'igniteui-angular/core'; import { Subject } from 'rxjs'; import { NgTemplateOutlet, NgClass } from '@angular/common'; import { IgxIconComponent } from 'igniteui-angular/icon'; import { IgxCheckboxComponent } from 'igniteui-angular/checkbox'; import { IgxCircularProgressBarComponent } from 'igniteui-angular/progressbar'; import { ToggleAnimationPlayer } from 'igniteui-angular/expansion-panel'; import { growVerOut, growVerIn } from 'igniteui-angular/animations'; // Enums const IgxTreeSelectionType = { None: 'None', BiState: 'BiState', Cascading: 'Cascading' }; // Token const IGX_TREE_COMPONENT = /*@__PURE__*/ new InjectionToken('IgxTreeToken'); const IGX_TREE_NODE_COMPONENT = /*@__PURE__*/ new InjectionToken('IgxTreeNodeToken'); /** @hidden @internal */ class IgxTreeService { constructor() { this.expandedNodes = new Set(); this.collapsingNodes = new Set(); this.siblingComparer = (data, node) => node !== data && node.level === data.level; } /** * Adds the node to the `expandedNodes` set and fires the nodes change event * * @param node target node * @param uiTrigger is the event triggered by a ui interraction (so we know if we should animate) * @returns void */ expand(node, uiTrigger) { this.collapsingNodes.delete(node); if (!this.expandedNodes.has(node)) { node.expandedChange.emit(true); } else { return; } this.expandedNodes.add(node); if (this.tree.singleBranchExpand) { this.tree.findNodes(node, this.siblingComparer)?.forEach(e => { if (uiTrigger) { e.collapse(); } else { e.expanded = false; } }); } } /** * Adds a node to the `collapsing` collection * * @param node target node */ collapsing(node) { this.collapsingNodes.add(node); } /** * Removes the node from the 'expandedNodes' set and emits the node's change event * * @param node target node * @returns void */ collapse(node) { if (this.expandedNodes.has(node)) { node.expandedChange.emit(false); } this.collapsingNodes.delete(node); this.expandedNodes.delete(node); } isExpanded(node) { return this.expandedNodes.has(node); } register(tree) { this.tree = tree; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: IgxTreeService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: IgxTreeService }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: IgxTreeService, decorators: [{ type: Injectable }] }); /** @hidden @internal */ class IgxTreeSelectionService { constructor() { this.nodeSelection = new Set(); this.indeterminateNodes = new Set(); } register(tree) { this.tree = tree; } /** Select range from last selected node to the current specified node. */ selectMultipleNodes(node, event) { if (!this.nodeSelection.size) { this.selectNode(node); return; } const lastSelectedNodeIndex = this.tree.nodes.toArray().indexOf(this.getSelectedNodes()[this.nodeSelection.size - 1]); const currentNodeIndex = this.tree.nodes.toArray().indexOf(node); const nodes = this.tree.nodes.toArray().slice(Math.min(currentNodeIndex, lastSelectedNodeIndex), Math.max(currentNodeIndex, lastSelectedNodeIndex) + 1); const added = nodes.filter(_node => !this.isNodeSelected(_node)); const newSelection = this.getSelectedNodes().concat(added); this.emitNodeSelectionEvent(newSelection, added, [], event); } /** Select the specified node and emit event. */ selectNode(node, event) { if (this.tree.selection === IgxTreeSelectionType.None) { return; } this.emitNodeSelectionEvent([...this.getSelectedNodes(), node], [node], [], event); } /** Deselect the specified node and emit event. */ deselectNode(node, event) { const newSelection = this.getSelectedNodes().filter(r => r !== node); this.emitNodeSelectionEvent(newSelection, [], [node], event); } /** Clears node selection */ clearNodesSelection() { this.nodeSelection.clear(); this.indeterminateNodes.clear(); } isNodeSelected(node) { return this.nodeSelection.has(node); } isNodeIndeterminate(node) { return this.indeterminateNodes.has(node); } /** Select specified nodes. No event is emitted. */ selectNodesWithNoEvent(nodes, clearPrevSelection = false, shouldEmit = true) { if (this.tree && this.tree.selection === IgxTreeSelectionType.Cascading) { this.cascadeSelectNodesWithNoEvent(nodes, clearPrevSelection); return; } const oldSelection = this.getSelectedNodes(); if (clearPrevSelection) { this.nodeSelection.clear(); } nodes.forEach(node => this.nodeSelection.add(node)); if (shouldEmit) { this.emitSelectedChangeEvent(oldSelection); } } /** Deselect specified nodes. No event is emitted. */ deselectNodesWithNoEvent(nodes, shouldEmit = true) { const oldSelection = this.getSelectedNodes(); if (!nodes) { this.nodeSelection.clear(); } else if (this.tree && this.tree.selection === IgxTreeSelectionType.Cascading) { this.cascadeDeselectNodesWithNoEvent(nodes); } else { nodes.forEach(node => this.nodeSelection.delete(node)); } if (shouldEmit) { this.emitSelectedChangeEvent(oldSelection); } } /** Called on `node.ngOnDestroy` to ensure state is correct after node is removed */ ensureStateOnNodeDelete(node) { if (this.tree?.selection !== IgxTreeSelectionType.Cascading) { return; } requestAnimationFrame(() => { if (this.isNodeSelected(node)) { // node is destroyed, do not emit event this.deselectNodesWithNoEvent([node], false); } else { if (!node.parentNode) { return; } const assitantLeafNode = node.parentNode?.allChildren.find(e => !e._children?.length); if (!assitantLeafNode) { return; } this.retriggerNodeState(assitantLeafNode); } }); } /** Retriggers a node's selection state */ retriggerNodeState(node) { if (node.selected) { this.nodeSelection.delete(node); this.selectNodesWithNoEvent([node], false, false); } else { this.nodeSelection.add(node); this.deselectNodesWithNoEvent([node], false); } } /** Returns array of the selected nodes. */ getSelectedNodes() { return this.nodeSelection.size ? Array.from(this.nodeSelection) : []; } /** Returns array of the nodes in indeterminate state. */ getIndeterminateNodes() { return this.indeterminateNodes.size ? Array.from(this.indeterminateNodes) : []; } emitNodeSelectionEvent(newSelection, added, removed, event) { if (this.tree.selection === IgxTreeSelectionType.Cascading) { this.emitCascadeNodeSelectionEvent(newSelection, added, removed, event); return; } const currSelection = this.getSelectedNodes(); if (this.areEqualCollections(currSelection, newSelection)) { return; } const args = { oldSelection: currSelection, newSelection, added, removed, event, cancel: false, owner: this.tree }; this.tree.nodeSelection.emit(args); if (args.cancel) { return; } this.selectNodesWithNoEvent(args.newSelection, true); } areEqualCollections(first, second) { return first.length === second.length && new Set(first.concat(second)).size === first.length; } cascadeSelectNodesWithNoEvent(nodes, clearPrevSelection = false) { const oldSelection = this.getSelectedNodes(); if (clearPrevSelection) { this.indeterminateNodes.clear(); this.nodeSelection.clear(); this.calculateNodesNewSelectionState({ added: nodes, removed: [] }); } else { const newSelection = [...oldSelection, ...nodes]; const args = { oldSelection, newSelection }; // retrieve only the rows without their parents/children which has to be added to the selection this.populateAddRemoveArgs(args); this.calculateNodesNewSelectionState(args); } this.nodeSelection = new Set(this.nodesToBeSelected); this.indeterminateNodes = new Set(this.nodesToBeIndeterminate); this.emitSelectedChangeEvent(oldSelection); } cascadeDeselectNodesWithNoEvent(nodes) { const args = { added: [], removed: nodes }; this.calculateNodesNewSelectionState(args); this.nodeSelection = new Set(this.nodesToBeSelected); this.indeterminateNodes = new Set(this.nodesToBeIndeterminate); } /** * populates the nodesToBeSelected and nodesToBeIndeterminate sets * with the nodes which will be eventually in selected/indeterminate state */ calculateNodesNewSelectionState(args) { this.nodesToBeSelected = new Set(args.oldSelection ? args.oldSelection : this.getSelectedNodes()); this.nodesToBeIndeterminate = new Set(this.getIndeterminateNodes()); this.cascadeSelectionState(args.removed, false); this.cascadeSelectionState(args.added, true); } /** Ensures proper selection state for all predescessors and descendants during a selection event */ cascadeSelectionState(nodes, selected) { if (!nodes || nodes.length === 0) { return; } if (nodes && nodes.length > 0) { const nodeCollection = this.getCascadingNodeCollection(nodes); nodeCollection.nodes.forEach(node => { if (selected) { this.nodesToBeSelected.add(node); } else { this.nodesToBeSelected.delete(node); } this.nodesToBeIndeterminate.delete(node); }); Array.from(nodeCollection.parents).forEach((parent) => { this.handleParentSelectionState(parent); }); } } emitCascadeNodeSelectionEvent(newSelection, added, removed, event) { const currSelection = this.getSelectedNodes(); if (this.areEqualCollections(currSelection, newSelection)) { return; } const args = { oldSelection: currSelection, newSelection, added, removed, event, cancel: false, owner: this.tree }; this.calculateNodesNewSelectionState(args); args.newSelection = Array.from(this.nodesToBeSelected); // retrieve nodes/parents/children which has been added/removed from the selection this.populateAddRemoveArgs(args); this.tree.nodeSelection.emit(args); if (args.cancel) { return; } // if args.newSelection hasn't been modified if (this.areEqualCollections(Array.from(this.nodesToBeSelected), args.newSelection)) { this.nodeSelection = new Set(this.nodesToBeSelected); this.indeterminateNodes = new Set(this.nodesToBeIndeterminate); this.emitSelectedChangeEvent(currSelection); } else { // select the nodes within the modified args.newSelection with no event this.cascadeSelectNodesWithNoEvent(args.newSelection, true); } } /** * recursively handle the selection state of the direct and indirect parents */ handleParentSelectionState(node) { if (!node) { return; } this.handleNodeSelectionState(node); if (node.parentNode) { this.handleParentSelectionState(node.parentNode); } } /** * Handle the selection state of a given node based the selection states of its direct children */ handleNodeSelectionState(node) { const nodesArray = (node && node._children) ? node._children.toArray() : []; if (nodesArray.length) { if (nodesArray.every(n => this.nodesToBeSelected.has(n))) { this.nodesToBeSelected.add(node); this.nodesToBeIndeterminate.delete(node); } else if (nodesArray.some(n => this.nodesToBeSelected.has(n) || this.nodesToBeIndeterminate.has(n))) { this.nodesToBeIndeterminate.add(node); this.nodesToBeSelected.delete(node); } else { this.nodesToBeIndeterminate.delete(node); this.nodesToBeSelected.delete(node); } } else { // if the children of the node has been deleted and the node was selected do not change its state if (this.isNodeSelected(node)) { this.nodesToBeSelected.add(node); } else { this.nodesToBeSelected.delete(node); } this.nodesToBeIndeterminate.delete(node); } } /** * Get a collection of all nodes affected by the change event * * @param nodesToBeProcessed set of the nodes to be selected/deselected * @returns a collection of all affected nodes and all their parents */ getCascadingNodeCollection(nodes) { const collection = { parents: new Set(), nodes: new Set(nodes) }; Array.from(collection.nodes).forEach((node) => { const nodeAndAllChildren = node.allChildren?.toArray() || []; nodeAndAllChildren.forEach(n => { collection.nodes.add(n); }); if (node && node.parentNode) { collection.parents.add(node.parentNode); } }); return collection; } /** * retrieve the nodes which should be added/removed to/from the old selection */ populateAddRemoveArgs(args) { args.removed = args.oldSelection.filter(x => args.newSelection.indexOf(x) < 0); args.added = args.newSelection.filter(x => args.oldSelection.indexOf(x) < 0); } /** Emits the `selectedChange` event for each node affected by the selection */ emitSelectedChangeEvent(oldSelection) { this.getSelectedNodes().forEach(n => { if (oldSelection.indexOf(n) < 0) { n.selectedChange.emit(true); } }); oldSelection.forEach(n => { if (!this.nodeSelection.has(n)) { n.selectedChange.emit(false); } }); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: IgxTreeSelectionService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: IgxTreeSelectionService }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: IgxTreeSelectionService, decorators: [{ type: Injectable }] }); /** @hidden @internal */ class IgxTreeNavigationService { constructor() { this.treeService = inject(IgxTreeService); this.selectionService = inject(IgxTreeSelectionService); this._focusedNode = null; this._lastFocusedNode = null; this._activeNode = null; this._visibleChildren = []; this._invisibleChildren = new Set(); this._disabledChildren = new Set(); this._cacheChange = new Subject(); this._cacheChange.subscribe(() => { this._visibleChildren = this.tree?.nodes ? this.tree.nodes.filter(e => !(this._invisibleChildren.has(e) || this._disabledChildren.has(e))) : []; }); } register(tree) { this.tree = tree; } get focusedNode() { return this._focusedNode; } set focusedNode(value) { if (this._focusedNode === value) { return; } this._lastFocusedNode = this._focusedNode; if (this._lastFocusedNode) { this._lastFocusedNode.tabIndex = -1; } this._focusedNode = value; if (this._focusedNode !== null) { this._focusedNode.tabIndex = 0; this._focusedNode.header.nativeElement.focus(); } } get activeNode() { return this._activeNode; } set activeNode(value) { if (this._activeNode === value) { return; } this._activeNode = value; this.tree.activeNodeChanged.emit(this._activeNode); } get visibleChildren() { return this._visibleChildren; } update_disabled_cache(node) { if (node.disabled) { this._disabledChildren.add(node); } else { this._disabledChildren.delete(node); } this._cacheChange.next(); } init_invisible_cache() { this.tree.nodes.filter(e => e.level === 0).forEach(node => { this.update_visible_cache(node, node.expanded, false); }); this._cacheChange.next(); } update_visible_cache(node, expanded, shouldEmit = true) { if (expanded) { node._children.forEach(child => { this._invisibleChildren.delete(child); this.update_visible_cache(child, child.expanded, false); }); } else { node.allChildren.forEach(c => this._invisibleChildren.add(c)); } if (shouldEmit) { this._cacheChange.next(); } } /** * Sets the node as focused (and active) * * @param node target node * @param isActive if true, sets the node as active */ setFocusedAndActiveNode(node, isActive = true) { if (isActive) { this.activeNode = node; } this.focusedNode = node; } /** Handler for keydown events. Used in tree.component.ts */ handleKeydown(event) { const key = event.key.toLowerCase(); if (!this.focusedNode) { return; } if (!(NAVIGATION_KEYS.has(key) || key === '*')) { if (key === 'enter') { this.activeNode = this.focusedNode; } return; } event.preventDefault(); if (event.repeat) { setTimeout(() => this.handleNavigation(event), 1); } else { this.handleNavigation(event); } } ngOnDestroy() { this._cacheChange.next(); this._cacheChange.complete(); } handleNavigation(event) { switch (event.key.toLowerCase()) { case 'home': this.setFocusedAndActiveNode(this.visibleChildren[0]); break; case 'end': this.setFocusedAndActiveNode(this.visibleChildren[this.visibleChildren.length - 1]); break; case 'arrowleft': case 'left': this.handleArrowLeft(); break; case 'arrowright': case 'right': this.handleArrowRight(); break; case 'arrowup': case 'up': this.handleUpDownArrow(true, event); break; case 'arrowdown': case 'down': this.handleUpDownArrow(false, event); break; case '*': this.handleAsterisk(); break; case ' ': case 'spacebar': case 'space': this.handleSpace(event.shiftKey); break; default: return; } } handleArrowLeft() { if (this.focusedNode.expanded && !this.treeService.collapsingNodes.has(this.focusedNode) && this.focusedNode._children?.length) { this.activeNode = this.focusedNode; this.focusedNode.collapse(); } else { const parentNode = this.focusedNode.parentNode; if (parentNode && !parentNode.disabled) { this.setFocusedAndActiveNode(parentNode); } } } handleArrowRight() { if (this.focusedNode._children.length > 0) { if (!this.focusedNode.expanded) { this.activeNode = this.focusedNode; this.focusedNode.expand(); } else { if (this.treeService.collapsingNodes.has(this.focusedNode)) { this.focusedNode.expand(); return; } const firstChild = this.focusedNode._children.find(node => !node.disabled); if (firstChild) { this.setFocusedAndActiveNode(firstChild); } } } } handleUpDownArrow(isUp, event) { const next = this.getVisibleNode(this.focusedNode, isUp ? -1 : 1); if (next === this.focusedNode) { return; } if (event.ctrlKey) { this.setFocusedAndActiveNode(next, false); } else { this.setFocusedAndActiveNode(next); } } handleAsterisk() { const nodes = this.focusedNode.parentNode ? this.focusedNode.parentNode._children : this.tree.rootNodes; nodes?.forEach(node => { if (!node.disabled && (!node.expanded || this.treeService.collapsingNodes.has(node))) { node.expand(); } }); } handleSpace(shiftKey = false) { if (this.tree.selection === IgxTreeSelectionType.None) { return; } this.activeNode = this.focusedNode; if (shiftKey) { this.selectionService.selectMultipleNodes(this.focusedNode); return; } if (this.focusedNode.selected) { this.selectionService.deselectNode(this.focusedNode); } else { this.selectionService.selectNode(this.focusedNode); } } /** Gets the next visible node in the given direction - 1 -> next, -1 -> previous */ getVisibleNode(node, dir = 1) { const nodeIndex = this.visibleChildren.indexOf(node); return this.visibleChildren[nodeIndex + dir] || node; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: IgxTreeNavigationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: IgxTreeNavigationService }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: IgxTreeNavigationService, decorators: [{ type: Injectable }], ctorParameters: () => [] }); // TODO: Implement aria functionality /** * @hidden @internal * Used for links (`a` tags) in the body of an `igx-tree-node`. Handles aria and event dispatch. */ class IgxTreeNodeLinkDirective { constructor() { this.node = inject(IGX_TREE_NODE_COMPONENT, { optional: true }); this.navService = inject(IgxTreeNavigationService); this.elementRef = inject(ElementRef); this.role = 'treeitem'; this._parentNode = null; } /** * The node's parent. Should be used only when the link is defined * in `<ng-template>` tag outside of its parent, as Angular DI will not properly provide a reference * * ```html * <igx-tree> * <igx-tree-node #myNode *ngFor="let node of data" [data]="node"> * <ng-template *ngTemplateOutlet="nodeTemplate; context: { $implicit: data, parentNode: myNode }"> * </ng-template> * </igx-tree-node> * ... * <!-- node template is defined under tree to access related services --> * <ng-template #nodeTemplate let-data let-node="parentNode"> * <a [igxTreeNodeLink]="node">{{ data.label }}</a> * </ng-template> * </igx-tree> * ``` */ set parentNode(val) { if (val) { this._parentNode = val; this._parentNode.addLinkChild(this); } } get parentNode() { return this._parentNode; } /** A pointer to the parent node */ get target() { return this.node || this.parentNode; } /** @hidden @internal */ get tabIndex() { return this.navService.focusedNode === this.target ? (this.target?.disabled ? -1 : 0) : -1; } /** * @hidden @internal * Clear the node's focused state */ handleBlur() { this.target.isFocused = false; } /** * @hidden @internal * Set the node as focused */ handleFocus() { if (this.target && !this.target.disabled) { if (this.navService.focusedNode !== this.target) { this.navService.focusedNode = this.target; } this.target.isFocused = true; } } ngOnDestroy() { this.target.removeLinkChild(this); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: IgxTreeNodeLinkDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.2", type: IgxTreeNodeLinkDirective, isStandalone: true, selector: "[igxTreeNodeLink]", inputs: { parentNode: ["igxTreeNodeLink", "parentNode"] }, host: { listeners: { "blur": "handleBlur()", "focus": "handleFocus()" }, properties: { "attr.role": "this.role", "attr.tabindex": "this.tabIndex" } }, ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: IgxTreeNodeLinkDirective, decorators: [{ type: Directive, args: [{ selector: `[igxTreeNodeLink]`, standalone: true }] }], propDecorators: { role: [{ type: HostBinding, args: ['attr.role'] }], parentNode: [{ type: Input, args: ['igxTreeNodeLink'] }], tabIndex: [{ type: HostBinding, args: ['attr.tabindex'] }], handleBlur: [{ type: HostListener, args: ['blur'] }], handleFocus: [{ type: HostListener, args: ['focus'] }] } }); /** * * The tree node component represents a child node of the tree component or another tree node. * Usage: * * ```html * <igx-tree> * ... * <igx-tree-node [data]="data" [selected]="service.isNodeSelected(data.Key)" [expanded]="service.isNodeExpanded(data.Key)"> * {{ data.FirstName }} {{ data.LastName }} * </igx-tree-node> * ... * </igx-tree> * ``` */ class IgxTreeNodeComponent extends ToggleAnimationPlayer { constructor() { super(...arguments); this.tree = inject(IGX_TREE_COMPONENT); this.selectionService = inject(IgxTreeSelectionService); this.treeService = inject(IgxTreeService); this.navService = inject(IgxTreeNavigationService); this.cdr = inject(ChangeDetectorRef); this.element = inject(ElementRef); this.parentNode = inject(IGX_TREE_NODE_COMPONENT, { optional: true, skipSelf: true }); /** * To be used for load-on-demand scenarios in order to specify whether the node is loading data. * * @remarks * Loading nodes do not render children. */ this.loading = false; /** * Emitted when the node's `selected` property changes. * * ```html * <igx-tree> * <igx-tree-node *ngFor="let node of data" [data]="node" [(selected)]="node.selected"> * </igx-tree-node> * </igx-tree> * ``` * * ```typescript * const node: IgxTreeNode<any> = this.tree.findNodes(data[0])[0]; * node.selectedChange.pipe(takeUntil(this.destroy$)).subscribe((e: boolean) => console.log("Node selection changed to ", e)) * ``` */ this.selectedChange = new EventEmitter(); /** * Emitted when the node's `expanded` property changes. * * ```html * <igx-tree> * <igx-tree-node *ngFor="let node of data" [data]="node" [(expanded)]="node.expanded"> * </igx-tree-node> * </igx-tree> * ``` * * ```typescript * const node: IgxTreeNode<any> = this.tree.findNodes(data[0])[0]; * node.expandedChange.pipe(takeUntil(this.destroy$)).subscribe((e: boolean) => console.log("Node expansion state changed to ", e)) * ``` */ this.expandedChange = new EventEmitter(); /** @hidden @internal */ this.cssClass = 'igx-tree-node'; /** @hidden @internal */ this.registeredChildren = []; /** @hidden @internal */ this._resourceStrings = getCurrentResourceStrings(TreeResourceStringsEN); this._tabIndex = null; this._disabled = false; } // TO DO: return different tab index depending on anchor child /** @hidden @internal */ set tabIndex(val) { this._tabIndex = val; } /** @hidden @internal */ get tabIndex() { if (this.disabled) { return -1; } if (this._tabIndex === null) { if (this.navService.focusedNode === null) { return this.hasLinkChildren ? -1 : 0; } return -1; } return this.hasLinkChildren ? -1 : this._tabIndex; } /** @hidden @internal */ get animationSettings() { return this.tree.animationSettings; } /** * Gets/Sets the resource strings. * * @remarks * Uses EN resources by default. */ set resourceStrings(value) { this._resourceStrings = Object.assign({}, this._resourceStrings, value); } /** * An accessor that returns the resource strings. */ get resourceStrings() { return this._resourceStrings; } /** * Gets/Sets the active state of the node * * @param value: boolean */ set active(value) { if (value) { this.navService.activeNode = this; this.tree.activeNodeBindingChange.emit(this); } } get active() { return this.navService.activeNode === this; } /** @hidden @internal */ get focused() { return this.isFocused && this.navService.focusedNode === this; } /** * Retrieves the full path to the node incuding itself * * ```typescript * const node: IgxTreeNode<any> = this.tree.findNodes(data[0])[0]; * const path: IgxTreeNode<any>[] = node.path; * ``` */ get path() { return this.parentNode?.path ? [...this.parentNode.path, this] : [this]; } // TODO: bind to disabled state when node is dragged /** * Gets/Sets the disabled state of the node * * @param value: boolean */ get disabled() { return this._disabled; } set disabled(value) { if (value !== this._disabled) { this._disabled = value; this.tree.disabledChange.emit(this); } } /** @hidden @internal */ get role() { return this.hasLinkChildren ? 'none' : 'treeitem'; } /** * Return the child nodes of the node (if any) * * @remarks * Returns `null` if node does not have children * * @example * ```typescript * const node: IgxTreeNode<any> = this.tree.findNodes(data[0])[0]; * const children: IgxTreeNode<any>[] = node.children; * ``` */ get children() { return this._children?.length ? this._children.toArray() : null; } get hasLinkChildren() { return this.linkChildren?.length > 0 || this.registeredChildren?.length > 0; } /** * @hidden @internal */ get showSelectors() { return this.tree.selection !== IgxTreeSelectionType.None; } /** * @hidden @internal */ get indeterminate() { return this.selectionService.isNodeIndeterminate(this); } /** The depth of the node, relative to the root * * ```html * <igx-tree> * ... * <igx-tree-node #node> * My level is {{ node.level }} * </igx-tree-node> * </igx-tree> * ``` * * ```typescript * const node: IgxTreeNode<any> = this.tree.findNodes(data[12])[0]; * const level: number = node.level; * ``` */ get level() { return this.parentNode ? this.parentNode.level + 1 : 0; } /** Get/set whether the node is selected. Supporst two-way binding. * * ```html * <igx-tree> * ... * <igx-tree-node *ngFor="let node of data" [(selected)]="node.selected"> * {{ node.label }} * </igx-tree-node> * </igx-tree> * ``` * * ```typescript * const node: IgxTreeNode<any> = this.tree.findNodes(data[0])[0]; * const selected = node.selected; * node.selected = true; * ``` */ get selected() { return this.selectionService.isNodeSelected(this); } set selected(val) { if (!(this.tree?.nodes && this.tree.nodes.find((e) => e === this)) && val) { this.tree.forceSelect.push(this); return; } if (val && !this.selectionService.isNodeSelected(this)) { this.selectionService.selectNodesWithNoEvent([this]); } if (!val && this.selectionService.isNodeSelected(this)) { this.selectionService.deselectNodesWithNoEvent([this]); } } /** Get/set whether the node is expanded * * ```html * <igx-tree> * ... * <igx-tree-node *ngFor="let node of data" [expanded]="node.name === this.expandedNode"> * {{ node.label }} * </igx-tree-node> * </igx-tree> * ``` * * ```typescript * const node: IgxTreeNode<any> = this.tree.findNodes(data[0])[0]; * const expanded = node.expanded; * node.expanded = true; * ``` */ get expanded() { return this.treeService.isExpanded(this); } set expanded(val) { if (val) { this.treeService.expand(this, false); } else { this.treeService.collapse(this); } } /** @hidden @internal */ get expandIndicatorTemplate() { return this.tree?.expandIndicator || this._defaultExpandIndicatorTemplate; } /** * The native DOM element representing the node. Could be null in certain environments. * * ```typescript * // get the nativeElement of the second node * const node: IgxTreeNode = this.tree.nodes.first(); * const nodeElement: HTMLElement = node.nativeElement; * ``` */ /** @hidden @internal */ get nativeElement() { return this.element.nativeElement; } /** @hidden @internal */ ngOnInit() { this.openAnimationDone.pipe(takeUntil(this.destroy$)).subscribe(() => { this.tree.nodeExpanded.emit({ owner: this.tree, node: this }); }); this.closeAnimationDone.pipe(takeUntil(this.destroy$)).subscribe(() => { this.tree.nodeCollapsed.emit({ owner: this.tree, node: this }); this.treeService.collapse(this); this.cdr.markForCheck(); }); } /** * @hidden @internal * Sets the focus to the node's <a> child, if present * Sets the node as the tree service's focusedNode * Marks the node as the current active element */ handleFocus() { if (this.disabled) { return; } if (this.navService.focusedNode !== this) { this.navService.focusedNode = this; } this.isFocused = true; if (this.linkChildren?.length) { this.linkChildren.first.nativeElement.focus(); return; } if (this.registeredChildren.length) { this.registeredChildren[0].elementRef.nativeElement.focus(); return; } } /** * @hidden @internal * Clear the node's focused status */ clearFocus() { this.isFocused = false; } /** * @hidden @internal */ onSelectorPointerDown(event) { event.preventDefault(); event.stopPropagation(); } /** * @hidden @internal */ onSelectorClick(event) { // event.stopPropagation(); event.preventDefault(); // this.navService.handleFocusedAndActiveNode(this); if (event.shiftKey) { this.selectionService.selectMultipleNodes(this, event); return; } if (this.selected) { this.selectionService.deselectNode(this, event); } else { this.selectionService.selectNode(this, event); } } /** * Toggles the node expansion state, triggering animation * * ```html * <igx-tree> * <igx-tree-node #node>My Node</igx-tree-node> * </igx-tree> * <button type="button" igxButton (click)="node.toggle()">Toggle Node</button> * ``` * * ```typescript * const myNode: IgxTreeNode<any> = this.tree.findNodes(data[0])[0]; * myNode.toggle(); * ``` */ toggle() { if (this.expanded) { this.collapse(); } else { this.expand(); } } /** @hidden @internal */ indicatorClick() { if (!this.tree.toggleNodeOnClick) { this.toggle(); this.navService.setFocusedAndActiveNode(this); } } /** * @hidden @internal */ onPointerDown(event) { event.stopPropagation(); //Toggle the node only on left mouse click - https://w3c.github.io/pointerevents/#button-states if (this.tree.toggleNodeOnClick && event.button === 0) { this.toggle(); } this.navService.setFocusedAndActiveNode(this); } ngOnDestroy() { super.ngOnDestroy(); this.selectionService.ensureStateOnNodeDelete(this); } /** * Expands the node, triggering animation * * ```html * <igx-tree> * <igx-tree-node #node>My Node</igx-tree-node> * </igx-tree> * <button type="button" igxButton (click)="node.expand()">Expand Node</button> * ``` * * ```typescript * const myNode: IgxTreeNode<any> = this.tree.findNodes(data[0])[0]; * myNode.expand(); * ``` */ expand() { if (this.expanded && !this.treeService.collapsingNodes.has(this)) { return; } const args = { owner: this.tree, node: this, cancel: false }; this.tree.nodeExpanding.emit(args); if (!args.cancel) { this.treeService.expand(this, true); this.cdr.detectChanges(); this.playOpenAnimation(this.childrenContainer); } } /** * Collapses the node, triggering animation * * ```html * <igx-tree> * <igx-tree-node #node>My Node</igx-tree-node> * </igx-tree> * <button type="button" igxButton (click)="node.collapse()">Collapse Node</button> * ``` * * ```typescript * const myNode: IgxTreeNode<any> = this.tree.findNodes(data[0])[0]; * myNode.collapse(); * ``` */ collapse() { if (!this.expanded || this.treeService.collapsingNodes.has(this)) { return; } const args = { owner: this.tree, node: this, cancel: false }; this.tree.nodeCollapsing.emit(args); if (!args.cancel) { this.treeService.collapsing(this); this.playCloseAnimation(this.childrenContainer); } } /** @hidden @internal */ addLinkChild(link) { this._tabIndex = -1; this.registeredChildren.push(link); } /** @hidden @internal */ removeLinkChild(link) { const index = this.registeredChildren.indexOf(link); if (index !== -1) { this.registeredChildren.splice(index, 1); } } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: IgxTreeNodeComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.2", type: IgxTreeNodeComponent, isStandalone: true, selector: "igx-tree-node", inputs: { data: "data", loading: ["loading", "loading", booleanAttribute], resourceStrings: "resourceStrings", active: ["active", "active", booleanAttribute], disabled: ["disabled", "disabled", booleanAttribute], selected: ["selected", "selected", booleanAttribute], expanded: ["expanded", "expanded", booleanAttribute] }, outputs: { selectedChange: "selectedChange", expandedChange: "expandedChange" }, host: { properties: { "class.igx-tree-node--disabled": "this.disabled", "class.igx-tree-node": "this.cssClass", "attr.role": "this.role" } }, providers: [ { provide: IGX_TREE_NODE_COMPONENT, useExisting: IgxTreeNodeComponent } ], queries: [{ propertyName: "linkChildren", predicate: IgxTreeNodeLinkDirective, read: ElementRef }, { propertyName: "_children", predicate: IGX_TREE_NODE_COMPONENT, read: IGX_TREE_NODE_COMPONENT }, { propertyName: "allChildren", predicate: IGX_TREE_NODE_COMPONENT, descendants: true, read: IGX_TREE_NODE_COMPONENT }], viewQueries: [{ propertyName: "header", first: true, predicate: ["ghostTemplate"], descendants: true, read: ElementRef }, { propertyName: "_defaultExpandIndicatorTemplate", first: true, predicate: ["defaultIndicator"], descendants: true, read: TemplateRef, static: true }, { propertyName: "childrenContainer", first: true, predicate: ["childrenContainer"], descendants: true, read: ElementRef }], usesInheritance: true, ngImport: i0, template: "<ng-template #noDragTemplate>\n <ng-template *ngTemplateOutlet=\"headerTemplate\"></ng-template>\n</ng-template>\n\n<!-- Will switch templates depending on dragDrop -->\n<ng-template *ngTemplateOutlet=\"noDragTemplate\">\n</ng-template>\n\n@if (expanded && !loading) {\n <div #childrenContainer\n class=\"igx-tree-node__group\"\n role=\"group\"\n >\n <ng-content select=\"igx-tree-node\"></ng-content>\n </div>\n}\n\n\n<ng-template #defaultIndicator>\n <igx-icon\n [attr.aria-label]=\"expanded ? resourceStrings.igx_collapse : resourceStrings.igx_expand\"\n [name]=\"!expanded ? 'tree_expand' : 'tree_collapse'\"\n family=\"default\"\n >\n </igx-icon>\n</ng-template>\n\n<!-- separated in a template in case this ever needs to be templatable -->\n<ng-template #selectMarkerTemplate>\n <igx-checkbox [checked]=\"selected\" [disabled]=\"disabled\" [readonly]=\"true\" [indeterminate]=\"indeterminate\" [tabindex]=\"-1\">\n </igx-checkbox>\n</ng-template>\n\n<ng-template #headerTemplate>\n <div #ghostTemplate class=\"igx-tree-node__wrapper\"\n [attr.role]=\"role\"\n [tabIndex]=\"tabIndex\"\n [ngClass]=\"{\n 'igx-tree-node__wrapper--selected': selected,\n 'igx-tree-node__wrapper--active' : this.active,\n 'igx-tree-node__wrapper--focused' : this.focused,\n 'igx-tree-node__wrapper--disabled' : this.disabled\n }\"\n (pointerdown)=\"onPointerDown($event)\"\n (focus)=\"handleFocus()\"\n (blur)=\"clearFocus()\"\n >\n <div aria-hidden=\"true\">\n @for (item of [].constructor(level); track $index) {\n <span\n aria-hidden=\"true\"\n class=\"igx-tree-node__spacer\"\n ></span>\n }\n </div>\n\n <!-- Expand/Collapse indicator -->\n @if (!loading) {\n <span\n class=\"igx-tree-node__toggle-button\"\n [ngClass]=\"{ 'igx-tree-node__toggle-button--hidden': !_children?.length }\"\n (click)=\"indicatorClick()\"\n >\n <ng-container *ngTemplateOutlet=\"expandIndicatorTemplate, context: { $implicit: expanded }\">\n </ng-container>\n </span>\n }\n @if (loading) {\n <span class=\"igx-tree-node__toggle-button\">\n <igx-circular-bar\n [animate]=\"false\"\n [indeterminate]=\"true\"\n [textVisibility]=\"false\"\n >\n </igx-circular-bar>\n </span>\n }\n\n <!-- Item selection -->\n @if (showSelectors) {\n <div\n class=\"igx-tree-node__select\"\n (pointerdown)=\"onSelectorPointerDown($event)\"\n (click)=\"onSelectorClick($event)\">\n <ng-container *ngTemplateOutlet=\"selectMarkerTemplate\">\n </ng-container>\n </div>\n }\n\n <div class=\"igx-tree-node__content\">\n <!-- Ghost content -->\n <ng-content></ng-content>\n </div>\n </div>\n\n <!-- Buffer element for 'move after' when D&D is implemented-->\n <div class=\"igx-tree-node__drop-indicator\">\n @for (item of [].constructor(level); track $index) {\n <span aria-hidden=\"true\" class=\"igx-tree-node__spacer\"></span>\n }\n <!-- style rules target this div, do not delete it -->\n <div></div>\n </div>\n</ng-template>\n\n<ng-template #dragTemplate>\n <!-- Drag drop goes here\n igxDrop\n #dropRef=\"drop\"\n [igxNodeDrag]=\"this\"\n (dragStart)=\"logDrop(dropRef)\"\n (leave)=\"emitLeave()\"\n (enter)=\"emitEnter()\" -->\n <div class=\"igx-tree-node__drag-wrapper\">\n <ng-template *ngTemplateOutlet=\"headerTemplate\"></ng-template>\n </div>\n</ng-template>\n", dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: IgxIconComponent, selector: "igx-icon", inputs: ["ariaHidden", "family", "name", "active"] }, { kind: "component", type: IgxCheckboxComponent, selector: "igx-checkbox", inputs: ["indeterminate", "checked", "disabled", "invalid", "readonly", "disableTransitions"] }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: IgxCircularProgressBarComponent, selector: "igx-circular-bar", inputs: ["id", "textVisibility", "type"] }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: IgxTreeNodeComponent, decorators: [{ type: Component, args: [{ selector: 'igx-tree-node', providers: [ { provide: IGX_TREE_NODE_COMPONENT, useExisting: IgxTreeNodeComponent } ], imports: [NgTemplateOutlet, IgxIconComponent, IgxCheckboxComponent, NgClass, IgxCircularProgressBarComponent], template: "<ng-template #noDragTemplate>\n <ng-template *ngTemplateOutlet=\"headerTemplate\"></ng-template>\n</ng-template>\n\n<!-- Will switch templates depending on dragDrop -->\n<ng-template *ngTemplateOutlet=\"noDragTemplate\">\n</ng-template>\n\n@if (expanded && !loading) {\n <div #childrenContainer\n class=\"igx-tree-node__group\"\n role=\"group\"\n >\n <ng-content select=\"igx-tree-node\"></ng-content>\n </div>\n}\n\n\n<ng-template #defaultIndicator>\n <igx-icon\n [attr.aria-label]=\"expanded ? resourceStrings.igx_collapse : resourceStrings.igx_expand\"\n [name]=\"!expanded ? 'tree_expand' : 'tree_collapse'\"\n family=\"default\"\n >\n </igx-icon>\n</ng-template>\n\n<!-- separated in a template in case this ever needs to be templatable -->\n<ng-template #selectMarkerTemplate>\n <igx-checkbox [checked]=\"selected\" [disabled]=\"disabled\" [readonly]=\"true\" [indeterminate]=\"indeterminate\" [tabindex]=\"-1\">\n </igx-checkbox>\n</ng-template>\n\n<ng-template #headerTemplate>\n <div #ghostTemplate class=\"igx-tree-node__wrapper\"\n [attr.role]=\"role\"\n [tabIndex]=\"tabIndex\"\n [ngClass]=\"{\n 'ig