import { lazy } from '@fuselab/ui-shared/lib'; import { BaseComponent } from '@uifabric/utilities/lib'; import * as React from 'react'; import classNames from './tree.classNames'; import { ITreeNodeView, TreeNode, TreeNodeViewProps } from './tree.types'; export interface TreeNodeState { selected: boolean; expanded: boolean; editing: boolean; } /** * tree node view */ export class TreeNodeView extends BaseComponent, TreeNodeState> implements ITreeNodeView { private _nameSpan: HTMLSpanElement; private _children: ITreeNodeView[]; constructor(props: TreeNodeViewProps) { super(props); this.state = { selected: false, expanded: false, editing: false }; } public select = (selected: boolean) => { this.setState({ selected }); if (!selected && this.state.editing) { this.setState({ editing: false }); } } public expand = () => { this.setState({ expanded: true }); } public collapse = () => { this.setState({ expanded: false }); } public edit = () => { this.setState({ editing: true }); } public componentDidMount() { if (this.node.type === 'container') { if (this.props.data.level === 0) { this.expand(); this.props.loadContainer(this.node).then( () => { this.forceUpdate(); }, err => { // ignore }); } } this.props.mounted(this); } public componentWillUnmount() { if (this.state.selected) { this.props.select(null); } } public componentDidUpdate(prevProps: TreeNodeViewProps, prevState: TreeNodeState) { if (this.state.editing && !prevState.editing && this._nameSpan) { // select text this.selectEditingText(); this._nameSpan.focus(); } } public render(): JSX.Element { const node = this.props.data; const level = node.level; return (
{this.renderIcon()} {this.props.data.name}
{this.renderChildren()}
); } public get elementRef(): HTMLElement { return this._nameSpan; } public get node(): TreeNode { return this.props.data; } public get parent(): ITreeNodeView { return this.props.parent; } public get children(): Iterator { return this.iterateChildren(); } public get expanded(): boolean { return this.state.expanded; } public get editing(): boolean { return this.state.editing; } public get theme(): 'dark' | 'light' { return this.props.theme; } private * iterateChildren() { if (this._children && this._children.length) { for (let c of this._children) { yield c; } } } private get nodeSelectClassName(): string { const styles = this.props.getStyle(this); const names = classNames(styles, this.theme); const classes = [names.root]; if (this.state.selected) { classes.push(names.rootSelected); } if (!this.parent && this.props.hideRoot) { classes.push(names.hiddenRoot); } return classes.join(' '); } private get nodeIconClassName(): string { const iconClass = this.node.type === 'container' ? `container${this.state.expanded ? 'Expanded' : 'Collapsed'}` : 'leaf'; return classNames(this.props.getStyle(this), this.theme)[iconClass]; } private renderIcon(): JSX.Element { if (this.node.type === 'container') { return ; } return ; } private renderChildren(): JSX.Element[] { if (!this.state.expanded || this.node.type === 'leaf') { return null; } this._children = []; return this.node.children.map((x, i) => { return ( this._children[i] = c} />); }); } private selectEditingText(): void { const range = document.createRange(); range.selectNodeContents(this._nameSpan); const selection = window.getSelection(); selection.removeAllRanges(); selection.addRange(range); } private unselectAll(): void { const selection = window.getSelection(); selection.removeAllRanges(); } @lazy() private get toggleNode(): React.MouseEventHandler { return x => { x.preventDefault(); x.stopPropagation(); this.setState({ expanded: !this.state.expanded }); }; } @lazy() private get toggleContextMenu(): React.MouseEventHandler { return x => { x.preventDefault(); x.stopPropagation(); this.props.toggleContextMenu(this, x); }; } @lazy() private get selectNode(): React.MouseEventHandler { return x => { x.preventDefault(); x.stopPropagation(); this.props.select(this); }; } @lazy() private get setName(): (x: HTMLSpanElement) => void { return x => { this._nameSpan = x; }; } @lazy() private get nameKeyPress(): React.KeyboardEventHandler { return x => { if (x.key === 'Enter') { x.preventDefault(); const val = x.currentTarget.textContent.trim(); this.node.name = val; this.setState({ editing: false }); this.props.blur(); } }; } @lazy() private get keyDown(): React.KeyboardEventHandler { return async x => { // if ESC if (x.keyCode === 27) { this.unselectAll(); if (!this.node.name) { await this.props.remove(this.node); } else { this._nameSpan.textContent = this.node.name; this.setState({ editing: false }); } this.props.blur(); } }; } }