UNPKG

igniteui-angular-sovn

Version:

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

253 lines (224 loc) 8.16 kB
import { Injectable, OnDestroy } from '@angular/core'; import { IgxTree, IgxTreeNode, IgxTreeSelectionType } from './common'; import { NAVIGATION_KEYS } from '../core/utils'; import { IgxTreeService } from './tree.service'; import { IgxTreeSelectionService } from './tree-selection.service'; import { Subject } from 'rxjs'; /** @hidden @internal */ @Injectable() export class IgxTreeNavigationService implements OnDestroy { private tree: IgxTree; private _focusedNode: IgxTreeNode<any> = null; private _lastFocusedNode: IgxTreeNode<any> = null; private _activeNode: IgxTreeNode<any> = null; private _visibleChildren: IgxTreeNode<any>[] = []; private _invisibleChildren: Set<IgxTreeNode<any>> = new Set(); private _disabledChildren: Set<IgxTreeNode<any>> = new Set(); private _cacheChange = new Subject<void>(); constructor(private treeService: IgxTreeService, private selectionService: IgxTreeSelectionService) { this._cacheChange.subscribe(() => { this._visibleChildren = this.tree?.nodes ? this.tree.nodes.filter(e => !(this._invisibleChildren.has(e) || this._disabledChildren.has(e))) : []; }); } public register(tree: IgxTree) { this.tree = tree; } public get focusedNode() { return this._focusedNode; } public set focusedNode(value: IgxTreeNode<any>) { 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(); } } public get activeNode() { return this._activeNode; } public set activeNode(value: IgxTreeNode<any>) { if (this._activeNode === value) { return; } this._activeNode = value; this.tree.activeNodeChanged.emit(this._activeNode); } public get visibleChildren(): IgxTreeNode<any>[] { return this._visibleChildren; } public update_disabled_cache(node: IgxTreeNode<any>): void { if (node.disabled) { this._disabledChildren.add(node); } else { this._disabledChildren.delete(node); } this._cacheChange.next(); } public init_invisible_cache() { this.tree.nodes.filter(e => e.level === 0).forEach(node => { this.update_visible_cache(node, node.expanded, false); }); this._cacheChange.next(); } public update_visible_cache(node: IgxTreeNode<any>, expanded: boolean, shouldEmit = true): void { 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 */ public setFocusedAndActiveNode(node: IgxTreeNode<any>, isActive = true): void { if (isActive) { this.activeNode = node; } this.focusedNode = node; } /** Handler for keydown events. Used in tree.component.ts */ public handleKeydown(event: KeyboardEvent) { 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); } } public ngOnDestroy() { this._cacheChange.next(); this._cacheChange.complete(); } private handleNavigation(event: KeyboardEvent) { 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; } } private handleArrowLeft(): void { 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); } } } private handleArrowRight(): void { 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); } } } } private handleUpDownArrow(isUp: boolean, event: KeyboardEvent): void { 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); } } private handleAsterisk(): void { 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(); } }); } private handleSpace(shiftKey = false): void { 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 */ private getVisibleNode(node: IgxTreeNode<any>, dir: 1 | -1 = 1): IgxTreeNode<any> { const nodeIndex = this.visibleChildren.indexOf(node); return this.visibleChildren[nodeIndex + dir] || node; } }