tin-react-components
Version:
All components used for Omadi apps
290 lines (284 loc) • 8.16 kB
JavaScript
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