import { Iterable } from 'ix'; import * as React from 'react'; import { MouseEvent, MouseEventHandler } from 'react'; import { Button } from 'react-bootstrap'; import { Icon } from 'react-fa'; import { IterableLike } from '../../../WebRx'; import { PanelFragment, PanelItemContext, PanelItemProps, PanelRenderProps, } from '../Panel/Panel'; import { ItemsPresenter, ItemsPresenterTemplateProps } from './ItemsPresenter'; export type RecursiveItemsSource = (item: T) => IterableLike | undefined; export interface TreeItemSourceProps { /** * function to produce the recursive items source for the provided item */ itemsSource: RecursiveItemsSource; } export interface TreeItemTemplateProps { /** * Override the expander icon template * default behaviour is to render an icon using the expandedIconName and collapsedIconName */ expanderIconTemplate?: ( isExpanded: boolean, expandedIconName?: string, collapsedIconName?: string, ) => PanelFragment; /** * Override the header template * compose a custom header out of the subcomponents */ headerTemplate?: ( item: T, index: number, indent: PanelFragment[], expander: PanelFragment, headerContent: PanelFragment, view: TreeItem, ) => PanelFragment; /** * template to render each item belonging to the bound item * use this template to define your own items presenter template */ itemsTemplate?: ( items: IterableLike | undefined, view: TreeItem, ) => PanelFragment; } export interface TreeItemRenderProps { /** * tree node depth (defaults to 0) * this should not be assigned unless defining your own itemsTemplate */ depth?: number; /** * set the isExpanded intial state (modified existing state) */ startExpanded?: boolean; /** * override the current expansion state (does not modify state) * true will override isExpanded to true, false will not override the current state * use a function to handle async changes in state overrides where override logic is computerd * outside the treeitem container component (i.e., outside the treeview). */ overrideExpanded?: boolean | (() => boolean); /** * override the default expanded icon name * default is a '-' in a rounded square */ expandedIconName?: string; /** * override the default collapsed icon name * default is a '+' in a rounded square */ collapsedIconName?: string; /** * flag to enable clicking anywhere on the node to toggle expansion state */ clickToExpand?: boolean; } export interface TreeItemFacadeProps< T = {}, TContext extends PanelItemContext = PanelItemContext > extends TreeItemSourceProps, TreeItemTemplateProps, TreeItemRenderProps, ItemsPresenterTemplateProps, PanelItemProps, PanelRenderProps {} export interface TreeItemProps< T = {}, TContext extends PanelItemContext = PanelItemContext > extends TreeItemFacadeProps { /** * the item that this tree node represents */ item: {}; /** * the index this item is found within its parent container. * this is necessary for key construction if no key is specified */ index: number; } export interface TreeItemComponentProps extends React.HTMLProps, TreeItemProps {} export interface TreeItemState { /** * true if the tree item is expanded */ isExpanded: boolean; } export class TreeItem extends React.Component< TreeItemComponentProps, TreeItemState > { static defaultProps: Partial = { depth: 0, }; public static defaultExpandedIconName = 'minus-square-o'; public static defaultCollapsedIconName = 'plus-square-o'; public static generateKey(fragment: PanelFragment, index: number) { if (React.isValidElement(fragment) && fragment.key != null) { return fragment.key; } return index; } public static renderTreeItem( item: {}, index: number, props: TreeItemFacadeProps, ) { return ( ); } public static defaultHeaderTemplate( item: {}, index: number, indent: PanelFragment[], expander: PanelFragment, headerContent: PanelFragment, view: TreeItem, ) { return (
>(view) : undefined } > {indent}
{expander}
{headerContent}
); } public static defaultItemsTemplate( items: IterableLike<{}> | undefined, view: TreeItem, ) { const isExpanded = view.getIsExpanded(); const template = (item: {}, index: number) => { return TreeItem.renderTreeItem(item, index, view.props); }; return isExpanded === false || items == null ? ( false ) : ( ); } public static defaultExpanderIconTemplate( isExpanded: boolean, expandedIconName = TreeItem.defaultExpandedIconName, collapsedIconName = TreeItem.defaultCollapsedIconName, ) { const iconName = isExpanded === true ? expandedIconName : collapsedIconName; return ; } constructor(props: any) { super(props); this.toggleExpansion = this.toggleExpansion.bind(this); this.state = { isExpanded: this.props.startExpanded || false, }; } componentWillReceiveProps( nextProps: Readonly, nextContext: any, ) { if ( nextProps.startExpanded != null && nextProps.startExpanded !== this.state.isExpanded ) { this.setState((prevState, props) => { // props.startExpanded should not be null (as per above) but we do a check just in case if (props.startExpanded == null) { return prevState; } return { isExpanded: props.startExpanded, }; }); } } render() { const { className, props, rest, children } = this.restProps(x => { const { item, index, itemsSource, expanderIconTemplate, headerTemplate, itemsTemplate, depth, startExpanded, overrideExpanded, expandedIconName, collapsedIconName, clickToExpand, viewTemplate, itemsPanelTemplate, itemTemplate, itemClassName, itemStyle, itemProps, compact, emptyContent, } = x; return { item, index, itemsSource, expanderIconTemplate, headerTemplate, itemsTemplate, depth, startExpanded, overrideExpanded, expandedIconName, collapsedIconName, clickToExpand, viewTemplate, itemsPanelTemplate, itemTemplate, itemClassName, itemStyle, itemProps, compact, emptyContent, }; }); const headerContent = this.renderHeaderContent(); const key = TreeItem.generateKey(headerContent, this.props.index); const items = this.props.itemsSource(this.props.item); const indent = this.renderIndent(); const expander = this.renderExpander(items); const header = this.renderHeader(indent, expander, headerContent); const treeItems = this.renderItems(items); return (
{header} {treeItems && (
{treeItems}
)}
); } protected getIsExpanded() { if (this.props.overrideExpanded != null) { // if an override prop is provided, then check to see if it is set to true const override = this.props.overrideExpanded instanceof Function ? this.props.overrideExpanded() : this.props.overrideExpanded; if (override) { // override is set to true so return true for isExpanded state (overridden) return true; } } // either no override prop is provided or the override result was false // so fallback onto standard component state return this.state.isExpanded || false; } protected toggleExpansion(e: MouseEvent) { e.stopPropagation(); this.setState((prevState, props) => { return { isExpanded: !this.state.isExpanded, }; }); } protected renderIndent() { return Iterable.range(0, this.props.depth || 0) .map((x, i) => { return
as | PanelFragment | false; }) .defaultIfEmpty(false) .toArray(); } protected renderExpanderIcon() { const template = this.props.expanderIconTemplate || TreeItem.defaultExpanderIconTemplate; const isExpanded = this.getIsExpanded(); return template( isExpanded, this.props.expandedIconName, this.props.collapsedIconName, ); } protected renderExpander(items: IterableLike<{}> | undefined) { const isEmpty = items == null || Iterable.from(items).isEmpty(); return isEmpty ? ( false ) : ( ); } protected renderHeaderContent() { const itemTemplate = this.props.itemTemplate || ItemsPresenter.defaultItemTemplate; return itemTemplate(this.props.item, this.props.index, this.state); } protected renderHeader( indent: PanelFragment[], expander: PanelFragment, headerContent: PanelFragment, ) { const template = this.props.headerTemplate || TreeItem.defaultHeaderTemplate; return template( this.props.item, this.props.index, indent, expander, headerContent, this, ); } protected renderItems(items: IterableLike<{}> | undefined): PanelFragment { const template = this.props.itemsTemplate || TreeItem.defaultItemsTemplate; return template(items, this); } }