最近在react项目中需要一个树状组件,但是又不想因为这个去引入一套UI组件,故自己封装了一个基于react的树状组件,
个人认为比较难得部分在于数据的处理,话不多说直接上代码:
下面是tree.js
import React, {Component} from 'react'; import './tree.css'; import Stack from '../utils/util'; class Tree extends Component { constructor(props) { super(props) this.state = { treeData: {}, treeArray: [], treeObj: {}, type: 'tree', parentId: 'pid', id: 'id', value: 'value', label: 'label', children: 'children', checkBox: false } this.checkMap = { 2: 'checked', 1: 'partChecked', 0: '' } } componentWillMount() { if (this.props.config.type.toLowerCase() === 'tree') { this.setState({ treeData: this.props.treeData, ...this.props.config }) } else { this.setState({ treeArray: this.props.treeData, ...this.props.config }) } } componentDidMount() { if (this.state.type.toLowerCase() !== 'tree') { this.factoryArrayData() } else { this.factoryTreeData() } } componentDidUpdate() { } componentWillUnmount() { } factoryArrayData() { let data = this.state.treeArray, obj = {}, rootId = null; data.map((v, i) => { if (v[this.state.parentId] || v[this.state.parentId] === 0) { if (obj[v[this.state.parentId]]) { if (obj[v[this.state.parentId]].children) { obj[v[this.state.parentId]].children.push(v) } else { obj[v[this.state.parentId]].children = [v] } } else { obj[v[this.state.parentId]] = { children: [v] } } } else { rootId = v[this.state.id] } if (obj[v[this.state.id]]) { v.children = obj[v[this.state.id]].children } obj[v[this.state.id]] = v }) this.setState({ treeData: obj[rootId], treeObj: obj }) } factoryTreeData() { let data = this.state.treeData let stack = new Stack(); let obj = {}; stack.push(data); while (stack.top) { let node = stack.pop(); for (let i in node.children) { stack.push(node.children[i]) } obj[node[this.state.id]] = node } this.setState({ treeObj: obj }) } openNode (e, data) { if (e.stopPropagation) { e.stopPropagation(); } else { window.event.cancelBubble = true; } data.open = !data.open this.forceUpdate() } selectNode (e, data) { if (e.stopPropagation) { e.stopPropagation(); } else { window.event.cancelBubble = true; } this.setState({ selectVal: data[this.state.value] }, () => { if (this.props.nodeClick) { this.props.nodeClick(data[this.state.value]) } }) } selectCheckBox (e, data) { if (e.stopPropagation) { e.stopPropagation(); } else { window.event.cancelBubble = true; } let check = data.checked if (data.children && data.children.length) { let stack = new Stack(); stack.push(data); while(stack.top) { let node = stack.pop() for (let i in node.children) { stack.push(node.children[i]) } if (check === 2) { node.checked = 0; } else { node.checked = 2 } } } else { if (check === 2) { data.checked = 0; } else { data.checked = 2 } } if (data[this.state.parentId] || data[this.state.parentId] === 0) { this.updateParentNode(data) } else { this.forceUpdate() if (this.props.selectChange) { this.getCheckedItems() } } } updateParentNode (data) { let par = this.state.treeObj[data[this.state.parentId]], checkLen = 0, partChecked = false; for (let i in par.children) { if (par.children[i].checked === 2) { checkLen++; } else if (par.children[i].checked === 1) { partChecked = true; break; } } if (checkLen === par.children.length) { par.checked = 2 } else if (partChecked || (checkLen < par.children.length && checkLen > 0)) { par.checked = 1; } else { par.checked = 0; } if (this.state.treeObj[par[this.state.parentId]] || this.state.treeObj[par[this.state.parentId]] == 0) { this.updateParentNode(par) } else { this.forceUpdate() if (this.props.selectChange) { this.getCheckedItems() } } } getCheckedItems() { let stack = new Stack (); let checkedArr = []; stack.push(this.state.treeData); while (stack.top) { let node = stack.pop(); for (let i in node.children) { stack.push(node.children[i]) } if (node.checked === 2) { checkedArr.push(node[this.state.value]) } } this.props.selectChange(checkedArr) } renderTreeParent() { let data = this.state.treeData return ( <div className={`parentNode childNode ${data.open?'open':'close'} ${data.children && data.children.length?'':'noChildren'}`}> <span onClick={(e) => this.openNode(e, data)} className="openNode"></span> { this.state.checkBox? <div className={`checkBox ${this.checkMap[data.checked]}`} onClick={(e) => this.selectCheckBox(e, data)}></div>: <div className="fileBox"> <img src="./images/file-icon.png" alt=""/> </div> } <div className={`nodeName ${this.state.selectVal === data[this.state.value]?'active':''}`} onClick={(e) => this.selectNode(e, data)}> {data[this.state.label]} </div> { this.state.treeData.children ? <div className="childList"> {this.renderTreeNode(data)} </div> : null } </div> ) } renderTreeNode(data) { return data.children.map((val, ind) => { return ( <div key={ind} className={`childNode ${val.open?'open':'close'} ${val.children && val.children.length?'':'noChildren'}`}> <span onClick={(e) => this.openNode(e, val)} className="openNode"></span> { this.state.checkBox? <div className={`checkBox ${this.checkMap[val.checked]}`} onClick={(e) => this.selectCheckBox(e, val)}></div>: <div className="fileBox"> <img src="./images/file-icon.png" alt=""/> </div> } {ind === data.children.length - 1? <span className="lastNode"></span>:null } <div className={`nodeName ${this.state.selectVal === val[this.state.value]?'active':''}`} onClick={(e) => this.selectNode(e, val)}> {val[this.state.label]} </div> { val.children ? <div className="childList"> {this.renderTreeNode(val)} </div> : null } </div> ) }) } render() { return ( <div className="tree"> {this.renderTreeParent()} </div> ) } } export default Tree
下面是tree.css
.tree { text-align: left; } .tree .childNode { padding-left: 20px; position: relative; background-color: #ffffff; z-index: 1; } .tree .childNode .checkBox { position: absolute; width: 16px; left: 20px; top: 0; z-index: 2; margin: 7px 10px 0; height: 16px; box-sizing: border-box; border: 1px solid #d2d2d2; vertical-align: text-bottom; font-size: 0; border-radius: 2px; cursor: pointer; } .tree .childNode .checkBox:hover { cursor: pointer; border-color: #5bb976; } .tree .childNode .checkBox.checked { border: 0; background: url(../images/icon-check-green.png) no-repeat center center; background-size: 100% 100%; background: none9; filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='./images/icon-check-green.png', sizingMethod='scale') 9; } .tree .childNode .checkBox.partChecked { border: 0; background: url(../images/part-checked.png) no-repeat center center; background-size: 100% 100%; background: none9; filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='./images/part-checked.png', sizingMethod='scale') 9; } .tree .childNode .nodeName { padding-left: 36px; font-size: 14px; color: #333333; white-space: nowrap; overflow: hidden; line-height: 30px; height: 30px; text-overflow: ellipsis; position: relative; z-index: 1; display: inline-block; padding-right: 10px; } .tree .childNode .nodeName.active { background-color: #DEF1FF; } .tree .childNode .nodeName:hover { text-decoration: underline; cursor: pointer; } .tree .childNode.open .openNode { background: url(../images/department-close.png) no-repeat center center; background-size: 100% 100%; background: none9; filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='./images/department-close.png', sizingMethod='scale') 9; } .tree .childNode.open .childList { display: block; } .tree .childNode.close .openNode { background: url(../images/depart-open.png) no-repeat center center; background-size: 100% 100%; background: none9; filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='./images/depart-open.png', sizingMethod='scale') 9; } .tree .childNode.close .childList { display: none; } .tree .childNode .fileBox { position: absolute; width: 16px; left: 20px; top: 0; margin: 5px 10px 0; z-index: 2; } .tree .childNode .fileBox:hover { cursor: pointer; } .tree .childNode .fileBox img { width: 16px; } .tree .childNode:before { position: absolute; left: -13px; top: 15px; width: 20px; height: 100%; border-top: 1px solid #CFCFCF; border-right: 1px solid #CFCFCF; content: ''; z-index: 1; } .tree .childNode:after { position: absolute; bottom: -12px; left: 7px; width: 1px; height: 30px; z-index: 3; background-color: #ffffff; content: ''; } .tree .childNode.parentNode:before { border-top: none; } .tree .childNode .openNode { position: absolute; z-index: 5; left: 0; top: 8px; width: 14px; height: 14px; } .tree .childNode .openNode:hover { cursor: pointer; } .tree .childNode.noChildren .openNode { width: 10px; height: 10px; top: 10px; left: 7px; background: url(../images/no-child.png) no-repeat center center; background-size: 100% 100%; background: none9; filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='./images/no-child.png', sizingMethod='scale') 9; } .tree .childNode.noChildren .openNode:hover { cursor: default; } .tree .childNode .lastNode { position: absolute; bottom: -15px; left: -13px; width: 1px; height: 100%; z-index: 4; background-color: #ffffff; }
utils里面是封装了一个stack栈,关于js栈的使用请移步js遍历树状数据文章。