react树状组件

最近在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: none\9;
  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: none\9;
  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: none\9;
  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: none\9;
  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: none\9;
  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遍历树状数据文章。

github项目地址