UNPKG

tin-react-components

Version:
290 lines (284 loc) 8.16 kB
import React, { Component } from 'react' import PropTypes from 'prop-types' import classnames from 'classnames' import './style.css' /** * Component to allow for basic click actions * @author: Jason Baddley */ class Tree extends Component { constructor (props) { super(props) this.state = { nodes: {} } this.handleClick = this.handleClick.bind(this) this.handleSearch = this.handleSearch.bind(this) } componentWillMount () { const selected = this.markSelected(this.props.data, this.props.selected) this.setState({ data: this.props.data, selected }) } componentWillReceiveProps (nextProps) { const selected = this.markSelected(nextProps.data, nextProps.selected) this.setState({ data: this.state.search ? this.state.data : this.props.data, selected }) } handleClick (e) { const { onClick, log, allowLogging } = this.props if (onClick) { onClick(e) } if (allowLogging && log) { log('Tree', 'handleClick', e, 'info') } } checkName (name, search) { return name.toLowerCase().indexOf(search.toLowerCase()) > -1 } markSelected (children = [], selected, col = []) { return children.reduce((collection, child) => { if (selected === child.id) { collection.push(child.id) return collection } if (child.children && child.children.length) { const childCol = this.markSelected(child.children, selected, []) if (childCol.length) { collection.push(child.id, ...childCol) return collection } } return col }, col) } expandSearch (children = [], search = '', col = []) { return children.reduce((collection, child) => { if (child.name.toLowerCase().includes(search.toLowerCase())) { collection.push(child.id) return collection } if ('children' in child && child.children.length) { const childCol = this.expandSearch(child.children, search, []) if (childCol.length) { collection.push(child.id, ...childCol) return collection } } return col }, col) } filterData (search, data = [], nodes) { if (!search || !data) return data data = [...data] return data.filter(d => { if (this.checkName(d.name, search)) { nodes[d.id] = true return true } if ('children' in d) { d.children = this.filterData(search, d.children, nodes) } if (d.children && d.children.length) { nodes[d.id] = true return true } nodes[d.id] = false return false }) } mapData (data) { if (!data) return data return data.map(d => { const datum = {...d} datum.children = this.mapData(datum.children) return datum }) } handleSearch (e) { const { log, allowLogging, data } = this.props const nodes = {} const newData = this.filterData(e.target.value, this.mapData(data), nodes) this.setState({ search: e.target.value || '', data: newData, nodes, expandedSearchNodes: this.expandSearch(data, e.target.value) }) if (allowLogging && log) { log('Tree', 'handleSearch', e, 'info') } } renderNodeText (text) { const { search } = this.state if (!search) return <span className='node-text'>{text}</span> const parts = text.toLowerCase().split(search.toLowerCase()) let index = 0 return parts.map((part, i) => { const p = text.substring(index, index + part.length) const s = text.substring(index + part.length, index + part.length + search.length) index += part.length + search.length return ( <span className='node-text'> <span className='no-match'>{p}</span> <span className='match'>{s}</span> </span> ) }) } renderChildren (children = [], level = 0) { const { selected, search, expandedSearchNodes } = this.state const { onOpenNode, nodes = {}, onToggleAll, onMouseOut, onMouseOver } = this.props const { all } = nodes return children.map(node => { const hasChildren = node.children && node.children.length const handleOpen = (e) => { e.stopPropagation() if (level === 0) { if (onToggleAll) { onToggleAll() } } else if (onOpenNode) { onOpenNode(node) } } let open = level === 0 || all || nodes[node.id] if (search && expandedSearchNodes) { open = expandedSearchNodes.includes(node.id) || open } const isSelected = selected.includes(node.id) const selectedLeaf = this.props.selected === node.id const classes = classnames(`level-${level} node`, { open }, { selected: isSelected }, { 'selected-leaf': selectedLeaf }, { barren: !hasChildren }) const iconDir = open ? 'up' : 'down' const icon = hasChildren ? <i onClick={handleOpen} className={`icon-caret-${iconDir}`} /> : null const click = () => this.handleClick(node) const mouseOver = () => onMouseOver(node) const mouseOut = () => onMouseOut(node) return ( <div className={classes} data-id={node.id}> <a onClick={click} onMouseOut={mouseOut} onMouseOver={mouseOver}>{this.renderNodeText(node.name)} {icon}</a> {this.renderChildren(node.children, level + 1)} </div> ) }) } render () { const { search = '', data = [] } = this.state const classes = classnames('component-wrapper', 'tree') const iconClasses = classnames('icon', { x: search }) const tree = this.renderChildren(data) return ( <div className={classes}> <div className='search-container'> <div className='clear' onClick={this.handleSearch}> <div className={iconClasses} /> </div> <input placeholder='search' className='search' value={search} onChange={this.handleSearch} /> </div> {tree} </div> ) } } Tree.propTypes = { /** * Text to filter the data by */ search: PropTypes.string, /** * Function whose only argument the event * as an object with the following argument structure: * @structure: * { * target: 'ELEMENT * } */ onClick: PropTypes.func, /** * Function whose only argument the event * as an object with the following argument structure: * @structure: * { * target: 'ELEMENT * } */ onMouseOver: PropTypes.func, /** * Function whose only argument the event * as an object with the following argument structure: * @structure: * { * target: 'ELEMENT * } */ onMouseOut: PropTypes.func, /** * Function whose only argument the event * as an object with the following argument structure: * @structure: * { * target: 'ELEMENT * } */ onOpenNode: PropTypes.func, /** * Function whose only argument the event * as an object with the following argument structure: * @structure: * { * target: 'ELEMENT * } */ onToggleAll: PropTypes.func, /** * Object of open nodes by id * @structure: * { * [id]: true * } */ nodes: PropTypes.object, /** * Data array that represents the tree */ data: PropTypes.array, /** * Function that allows for advanced logging. * @arguments: * [ * { * type: 'string', * name: 'componentName', * description: 'Name of component', * * }, * { * type: 'string', * name: 'methodName', * description: 'Name of method fired', * * }, * { * type: 'object', * name: 'event', * description: 'The data to be logged', * * }, * { * type: 'string', * name: 'type', * description: 'Type of log', * options: ['error', 'warning', 'info', 'debug'] * } * ] */ log: PropTypes.func, /** * Flag to indicate whether to log events or not */ allowLogging: PropTypes.bool } export default Tree