Skip to content

Instantly share code, notes, and snippets.

@LouisSung
Last active May 26, 2020 06:32
Show Gist options
  • Save LouisSung/03dd2b0bf1d89cfc0bd8e8f17a607fec to your computer and use it in GitHub Desktop.
Save LouisSung/03dd2b0bf1d89cfc0bd8e8f17a607fec to your computer and use it in GitHub Desktop.
Example for adding keyboard control for NzTree (NG-ZORRO) https://github.com/NG-ZORRO/ng-zorro-antd/issues/5288
<div [style.width.px]="300" [style.margin.px]="16">
<nz-input-group>
<label><input type="text" nz-input placeholder="Search" [(ngModel)]="searchValue"></label>
</nz-input-group>
<nz-tree nzShowLine #aTree [nzData]="treeNodeOptions" [nzSearchValue]="searchValue"
(keydown)="onKeyDown($event.key)" (nzExpandChange)="treeFocus()"
(nzClick)="onTreeNodeClick($event)" (nzDblClick)="onTreeNodeDblClick($event)">
</nz-tree>
</div>
import {AfterViewInit, Component, ElementRef, OnDestroy, Renderer2, ViewChild} from '@angular/core';
import {NzFormatEmitEvent, NzTreeComponent, NzTreeNode, NzTreeNodeBaseComponent} from 'ng-zorro-antd';
import {staticNodeOptions} from './tree-node-options';
@Component({
selector: 'app-root',
templateUrl: './app.component.html'
})
export class AppComponent implements AfterViewInit, OnDestroy {
@ViewChild('aTree') public treeComponent?: NzTreeComponent;
@ViewChild('aTree', {read: ElementRef}) public treeRef?: ElementRef<HTMLElement>;
public treeNodeOptions = staticNodeOptions;
public searchValue = '';
private treeInputElement?: HTMLInputElement;
private focusedNode?: NzTreeNode;
private readonly boundEvents: Array<() => void> = [];
constructor(private readonly renderer: Renderer2) {}
// prettier-ignore
public ngAfterViewInit(): void {
this.focusedNode = this.treeComponent?.getTreeNodes()[0];
this.treeInputElement = this.treeRef?.nativeElement.querySelector('input') as HTMLInputElement;
this.boundEvents.push(this.renderer.listen(
this.treeInputElement, 'focus', (event: FocusEvent) => { if (event.relatedTarget !== null) { this.treeFocus(); } }
));
this.boundEvents.push(this.renderer.listen(this.treeInputElement, 'blur', () => { this.treeFocus(false); }));
}
public ngOnDestroy(): void {
// prettier-ignore
for (const unbind of this.boundEvents) { unbind(); }
}
// eslint-disable-next-line max-lines-per-function, complexity
public onKeyDown(key: string): void {
if (!['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Enter', 'Escape', '*', '+', '-'].includes(key)) {
return;
}
const currentFocusedNode = this.focusedNode as NzTreeNode;
const parentNode = currentFocusedNode.parentNode;
const sibling = (parentNode === null ? this.treeComponent?.getTreeNodes() : parentNode.children) as Array<NzTreeNode>;
const nodeIndex = sibling.findIndex((x: NzTreeNode) => x.key === currentFocusedNode.key);
let focusOrBlur = true;
currentFocusedNode.isSelected = false;
// prettier-ignore
switch (key) {
case 'ArrowUp': keyArrowUp.call(this); break;
case 'ArrowDown': keyArrowDown.call(this); break;
case 'ArrowLeft': keyArrowLeft.call(this); break;
case 'ArrowRight': keyArrowRight.call(this); break;
case 'Enter': keyEnter.call(this); break;
case 'Escape': focusOrBlur = false; break;
case '*': this.recursivelyNodeCollapseExpand(currentFocusedNode, true, false); break;
case '+': this.recursivelyNodeCollapseExpand(currentFocusedNode, false, false); break;
case '-': this.recursivelyNodeCollapseExpand(currentFocusedNode); break;
default: break;
}
this.treeFocus(focusOrBlur, true);
function keyArrowUp(this: AppComponent): void {
// eslint-disable-next-line arrow-body-style
const recursivelyGetPreviousNode = (targetNode: NzTreeNode): NzTreeNode => {
return targetNode.isExpanded && targetNode.children.length > 0
? recursivelyGetPreviousNode(targetNode.children[targetNode.children.length - 1])
: targetNode;
};
this.focusedNode = nodeIndex > 0 ? recursivelyGetPreviousNode(sibling[nodeIndex - 1]) : parentNode ?? this.focusedNode;
}
function keyArrowDown(this: AppComponent): void {
// eslint-disable-next-line complexity
const recursivelyGetNextNode = (
targetNode: NzTreeNode,
rollbackToParent: boolean = false,
skipChildren: boolean = false
): NzTreeNode | null => {
const tmpParent = targetNode.parentNode;
const tmpSibling = tmpParent ? tmpParent.children : (this.treeComponent?.getTreeNodes() as Array<NzTreeNode>);
const tmpIndex = tmpSibling.findIndex((x: NzTreeNode) => x.key === targetNode.key);
if (rollbackToParent) {
return tmpIndex < tmpSibling.length - 1 ? targetNode : tmpParent ? recursivelyGetNextNode(tmpParent, true) : null;
} else if (targetNode.isExpanded && targetNode.children.length > 0 && !skipChildren) {
return targetNode.children[0];
} else if (tmpIndex < tmpSibling.length - 1) {
return tmpSibling[tmpIndex + 1];
} else if (tmpParent) {
const tmpRollback = recursivelyGetNextNode(tmpParent, true);
return tmpRollback ? recursivelyGetNextNode(tmpRollback, false, true) : null;
} else {
return null;
}
};
this.focusedNode = recursivelyGetNextNode(currentFocusedNode) ?? this.focusedNode;
}
function keyArrowRight(this: AppComponent): void {
if (!currentFocusedNode.isExpanded && currentFocusedNode.children.length > 0) {
this.recursivelyNodeCollapseExpand(currentFocusedNode, false, false);
} else {
keyArrowDown.call(this);
}
}
function keyArrowLeft(this: AppComponent): void {
if (currentFocusedNode.isExpanded) {
this.recursivelyNodeCollapseExpand(currentFocusedNode);
} else {
this.focusedNode = parentNode ? parentNode : this.treeComponent?.getTreeNodes()[0];
}
}
function keyEnter(this: AppComponent): void {
if (currentFocusedNode.isLeaf) {
return;
}
if (currentFocusedNode.isExpanded) {
this.recursivelyNodeCollapseExpand(currentFocusedNode);
} else {
this.recursivelyNodeCollapseExpand(currentFocusedNode, false, false);
}
}
}
public onTreeNodeClick(event: NzFormatEmitEvent): void {
const clickedNode = event.node as NzTreeNode;
if (this.focusedNode !== clickedNode) {
(this.focusedNode as NzTreeNode).isSelected = false;
this.focusedNode = clickedNode;
}
this.treeFocus();
}
public onTreeNodeDblClick(event: NzFormatEmitEvent): void {
if (!event.node?.isLeaf && event.node) {
this.recursivelyNodeCollapseExpand(event.node, event.node?.isExpanded, event.node?.isExpanded);
}
}
public treeFocus(focusOrBlur: boolean = true, scrollIntoView: boolean = false): void {
if (focusOrBlur) {
this.treeInputElement?.focus();
if (scrollIntoView) {
// scroll after expand or collapse is finished
setTimeout(() => {
(this.focusedNode?.component as TreeNodeComponent)?.elementRef.nativeElement
.querySelector('nz-tree-node-switcher')
?.scrollIntoView({block: 'nearest', inline: 'nearest'});
});
}
} else {
this.treeInputElement?.blur();
}
(this.focusedNode as NzTreeNode).isSelected = focusOrBlur;
}
private recursivelyNodeCollapseExpand(treeNode: NzTreeNode, recursive: boolean = true, collapseOrExpand: boolean = true): void {
if (treeNode.isExpanded === collapseOrExpand || (!collapseOrExpand && recursive)) {
treeNode.isExpanded = !collapseOrExpand;
if (recursive) {
for (const childNode of treeNode.children) {
this.recursivelyNodeCollapseExpand(childNode, recursive, collapseOrExpand);
}
}
}
}
}
interface TreeNodeComponent extends NzTreeNodeBaseComponent {
elementRef: ElementRef<HTMLElement>;
}
export const staticNodeOptions = [
{
title: 'AAA',
key: '/AAA',
children: [
{
title: '111',
key: '/AAA/111',
children: [
{
title: 'aaa',
key: '/AAA/111/aaa',
children: [
{key: '/AAA/111/aaa§i', title: 'i', isLeaf: true},
{key: '/AAA/111/aaa§ii', title: 'ii', isLeaf: true},
{key: '/AAA/111/aaa§iii', title: 'iii', isLeaf: true}
],
isLeaf: false
},
{
title: 'bbb',
key: '/AAA/111/bbb',
children: [
{
title: 'i',
key: '/AAA/111/bbb/i',
children: [
{key: '/AAA/111/bbb/i§!!!', title: '!!!', isLeaf: true},
{key: '/AAA/111/bbb/i§@@@', title: '@@@', isLeaf: true},
{key: '/AAA/111/bbb/i§###', title: '###', isLeaf: true}
],
isLeaf: false
},
{key: '/AAA/111/bbb§iv', title: 'iv', preview: '70x70', isLeaf: true},
{key: '/AAA/111/bbb§v', title: 'v', preview: '70x70', isLeaf: true}
],
isLeaf: false
}
],
isLeaf: false
},
{title: '222', key: '/AAA/222', children: [], isLeaf: true},
{title: '333', key: '/AAA/333', children: [], isLeaf: true},
{title: '444', key: '/AAA/444', children: [], isLeaf: true},
{
title: '555',
key: '/AAA/555',
children: [
{
title: 'ccc',
key: '/AAA/555/ccc',
children: [{key: '/AAA/555/ccc§vi', title: 'vi', preview: '15x32', isLeaf: true}],
isLeaf: false
}
],
isLeaf: false
},
{
title: '666',
key: '/AAA/666',
children: [
{
title: 'ddd',
key: '/AAA/666/ddd',
children: [{key: '/AAA/666/ddd§vii', title: 'vii', preview: '20x30', isLeaf: true}],
isLeaf: false
}
],
isLeaf: false
},
{title: '777', key: '/AAA/777', children: [], isLeaf: true},
{title: '888', key: '/AAA/888', children: [], isLeaf: true},
{title: '999', key: '/AAA/999', children: [], isLeaf: true},
{title: '1111', key: '/AAA/1111', children: [], isLeaf: true},
{title: '2222', key: '/AAA/2222', children: [], isLeaf: true},
{title: '3333', key: '/AAA/3333', children: [], isLeaf: true},
{title: '000', key: '/AAA/000', children: [], isLeaf: true}
],
isLeaf: false
},
{
title: 'BBB',
key: '/BBB',
children: [
{
title: '4444',
key: '/BBB/4444',
children: [
{
title: 'eee',
key: '/BBB/4444/eee',
children: [
{
key: '/BBB/4444/eee§viii',
title: 'viii',
preview: '30x30',
spec: {'Dummy Spec 1': 'Hello', 'Dummy Spec 2': 'Sysmaker'},
isLeaf: true
}
],
isLeaf: false
}
],
isLeaf: false
},
{title: '5555', key: '/BBB/5555', children: [], isLeaf: true},
{title: '6666', key: '/BBB/6666', children: [], isLeaf: true},
{title: '7777', key: '/BBB/7777', children: [], isLeaf: true},
{title: '8888', key: '/BBB/8888', children: [], isLeaf: true},
{title: '9999', key: '/BBB/9999', children: [], isLeaf: true},
{
title: '11111',
key: '/BBB/11111',
children: [
{
title: 'fff',
key: '/BBB/11111/fff',
children: [{key: '/BBB/11111/fff§ix', title: 'ix', preview: '16x20', isLeaf: true}],
isLeaf: false
}
],
isLeaf: false
},
{title: '22222', key: '/BBB/22222', children: [], isLeaf: true},
{title: '33333', key: '/BBB/33333', children: [], isLeaf: true},
{title: '44444', key: '/BBB/44444', children: [], isLeaf: true},
{title: '55555', key: '/BBB/55555', children: [], isLeaf: true},
{title: '0000', key: '/BBB/0000', children: [], isLeaf: true}
],
isLeaf: false
},
{
title: 'CCC',
key: '/CCC',
children: [
{key: '/CCC§66666', title: '66666', isLeaf: true},
{key: '/CCC§77777', title: '77777', isLeaf: true}
],
isLeaf: false
}
];
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment