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
JavaScript
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