mirror of https://github.com/ElemeFE/element
				
				
				
			Improve Tree:
1. Add props: renderContent, highlightCurrent 2. Fix Bug: tree do not change when data changedpull/470/head
							parent
							
								
									baf3d192d8
								
							
						
					
					
						commit
						785bed20df
					
				| 
						 | 
				
			
			@ -235,6 +235,8 @@
 | 
			
		|||
| props | 配置选项,具体看下表 | object | — | — |
 | 
			
		||||
| load | 加载子树数据的方法 | function(node, resolve) | — | — |
 | 
			
		||||
| show-checkbox | 节点是否可被选择 | boolean | — | false |
 | 
			
		||||
| render-content | 树节点的内容区的渲染 Function,会传入两个参数,h 与 { node: node }。 | Function | - | - |
 | 
			
		||||
| highlight-current | 是否高亮当前选中节点,默认值是 false。| boolean | - | false |
 | 
			
		||||
 | 
			
		||||
### props
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -253,5 +255,5 @@
 | 
			
		|||
### Events
 | 
			
		||||
| 事件名称      | 说明    | 回调参数      |
 | 
			
		||||
|---------- |-------- |---------- |
 | 
			
		||||
| node-click  | 节点被点击时的回调 | 传递给 `data` 属性的数组中该节点所对应的对象 |
 | 
			
		||||
| node-click  | 节点被点击时的回调 | 共三个参数,依次为:传递给 `data` 属性的数组中该节点所对应的对象、节点对应的 Node、节点组件本身。 |
 | 
			
		||||
| check-change  | 节点选中状态发生变化时的回调 | 共三个参数,依次为:传递给 `data` 属性的数组中该节点所对应的对象、<br>节点本身是否被选中、节点的子树中是否有被选中的节点 |
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,7 +3,6 @@
 | 
			
		|||
 | 
			
		||||
@component-namespace el {
 | 
			
		||||
  @b tree {
 | 
			
		||||
    overflow: auto;
 | 
			
		||||
    cursor: default;
 | 
			
		||||
    background: #ffffff;
 | 
			
		||||
    border: 1px solid #d3dce6;
 | 
			
		||||
| 
						 | 
				
			
			@ -82,6 +81,11 @@
 | 
			
		|||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.el-tree--highlight-current .el-tree-node.is-current > .el-tree-node__content {
 | 
			
		||||
  background-color: #eff7ff;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.collapse-transition {
 | 
			
		||||
    transition: 0.3s height ease-in-out, 0.3s padding-top ease-in-out, 0.3s padding-bottom ease-in-out;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,8 +1,8 @@
 | 
			
		|||
let idSeed = 0;
 | 
			
		||||
let nodeIdSeed = 0;
 | 
			
		||||
import objectAssign from 'object-assign';
 | 
			
		||||
 | 
			
		||||
const reInitChecked = function(node) {
 | 
			
		||||
  const siblings = node.children;
 | 
			
		||||
  const siblings = node.childNodes;
 | 
			
		||||
 | 
			
		||||
  let all = true;
 | 
			
		||||
  let none = true;
 | 
			
		||||
| 
						 | 
				
			
			@ -42,7 +42,7 @@ const getPropertyFromData = function(node, prop) {
 | 
			
		|||
 | 
			
		||||
export default class Node {
 | 
			
		||||
  constructor(options) {
 | 
			
		||||
    this.id = idSeed++;
 | 
			
		||||
    this.id = nodeIdSeed++;
 | 
			
		||||
    this.text = null;
 | 
			
		||||
    this.checked = false;
 | 
			
		||||
    this.indeterminate = false;
 | 
			
		||||
| 
						 | 
				
			
			@ -61,7 +61,7 @@ export default class Node {
 | 
			
		|||
    // internal
 | 
			
		||||
    this.level = -1;
 | 
			
		||||
    this.loaded = false;
 | 
			
		||||
    this.children = [];
 | 
			
		||||
    this.childNodes = [];
 | 
			
		||||
    this.loading = false;
 | 
			
		||||
 | 
			
		||||
    if (this.parent) {
 | 
			
		||||
| 
						 | 
				
			
			@ -74,8 +74,17 @@ export default class Node {
 | 
			
		|||
  }
 | 
			
		||||
 | 
			
		||||
  setData(data) {
 | 
			
		||||
    if (!Array.isArray(data) && !data.$treeNodeId) {
 | 
			
		||||
      Object.defineProperty(data, '$treeNodeId', {
 | 
			
		||||
        value: this.id,
 | 
			
		||||
        enumerable: false,
 | 
			
		||||
        configurable: false,
 | 
			
		||||
        writable: false
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.data = data;
 | 
			
		||||
    this.children = [];
 | 
			
		||||
    this.childNodes = [];
 | 
			
		||||
 | 
			
		||||
    let children;
 | 
			
		||||
    if (this.level === -1 && this.data instanceof Array) {
 | 
			
		||||
| 
						 | 
				
			
			@ -85,14 +94,7 @@ export default class Node {
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    for (let i = 0, j = children.length; i < j; i++) {
 | 
			
		||||
      const child = children[i];
 | 
			
		||||
      this.insertChild(new Node({
 | 
			
		||||
        data: child,
 | 
			
		||||
        parent: this,
 | 
			
		||||
        lazy: this.lazy,
 | 
			
		||||
        load: this.load,
 | 
			
		||||
        props: this.props
 | 
			
		||||
      }));
 | 
			
		||||
      this.insertChild({ data: children[i] });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -107,26 +109,47 @@ export default class Node {
 | 
			
		|||
  insertChild(child, index) {
 | 
			
		||||
    if (!child) throw new Error('insertChild error: child is required.');
 | 
			
		||||
 | 
			
		||||
    if (!child instanceof Node) {
 | 
			
		||||
      throw new Error('insertChild error: child should an instance of Node.');
 | 
			
		||||
    if (!(child instanceof Node)) {
 | 
			
		||||
      objectAssign(child, {
 | 
			
		||||
        parent: this,
 | 
			
		||||
        lazy: this.lazy,
 | 
			
		||||
        load: this.load,
 | 
			
		||||
        props: this.props
 | 
			
		||||
      });
 | 
			
		||||
      child = new Node(child);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    child.parent = this;
 | 
			
		||||
    child.level = this.level + 1;
 | 
			
		||||
 | 
			
		||||
    if (typeof index === 'undefined') {
 | 
			
		||||
      this.children.push(child);
 | 
			
		||||
      this.childNodes.push(child);
 | 
			
		||||
    } else {
 | 
			
		||||
      this.children.splice(index, 0, child);
 | 
			
		||||
      this.childNodes.splice(index, 0, child);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  removeChild(child) {
 | 
			
		||||
    const index = this.children.indexOf(child);
 | 
			
		||||
    const index = this.childNodes.indexOf(child);
 | 
			
		||||
 | 
			
		||||
    if (index > -1) {
 | 
			
		||||
      child.parent = null;
 | 
			
		||||
      this.children.splice(child, index);
 | 
			
		||||
      this.childNodes.splice(index, 1);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  removeChildByData(data) {
 | 
			
		||||
    let nodeIndex = -1;
 | 
			
		||||
    let targetNode = null;
 | 
			
		||||
    this.childNodes.forEach((node, index) => {
 | 
			
		||||
      if (node.data === data) {
 | 
			
		||||
        nodeIndex = index;
 | 
			
		||||
        targetNode = node;
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (nodeIndex > -1) {
 | 
			
		||||
      targetNode.parent = null;
 | 
			
		||||
      this.childNodes.splice(nodeIndex, 1);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -147,13 +170,7 @@ export default class Node {
 | 
			
		|||
 | 
			
		||||
  doCreateChildren(array, defaultProps = {}) {
 | 
			
		||||
    array.forEach((item) => {
 | 
			
		||||
      const node = new Node(objectAssign({
 | 
			
		||||
        data: item,
 | 
			
		||||
        lazy: this.lazy,
 | 
			
		||||
        load: this.load,
 | 
			
		||||
        props: this.props
 | 
			
		||||
      }, defaultProps));
 | 
			
		||||
      this.insertChild(node);
 | 
			
		||||
      this.insertChild(objectAssign({ data: item }, defaultProps));
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -170,9 +187,9 @@ export default class Node {
 | 
			
		|||
  }
 | 
			
		||||
 | 
			
		||||
  hasChild() {
 | 
			
		||||
    const children = this.children;
 | 
			
		||||
    const childNodes = this.childNodes;
 | 
			
		||||
    if (!this.lazy || (this.lazy === true && this.loaded === true)) {
 | 
			
		||||
      return children && children.length > 0;
 | 
			
		||||
      return childNodes && childNodes.length > 0;
 | 
			
		||||
    }
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			@ -183,9 +200,9 @@ export default class Node {
 | 
			
		|||
 | 
			
		||||
    const handleDeep = () => {
 | 
			
		||||
      if (deep) {
 | 
			
		||||
        const children = this.children;
 | 
			
		||||
        for (let i = 0, j = children.length; i < j; i++) {
 | 
			
		||||
          const child = children[i];
 | 
			
		||||
        const childNodes = this.childNodes;
 | 
			
		||||
        for (let i = 0, j = childNodes.length; i < j; i++) {
 | 
			
		||||
          const child = childNodes[i];
 | 
			
		||||
          child.setChecked(value !== false, deep);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
| 
						 | 
				
			
			@ -225,15 +242,33 @@ export default class Node {
 | 
			
		|||
    return data[children];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  updateChildren() {
 | 
			
		||||
    const newData = this.getChildren() || [];
 | 
			
		||||
    const oldData = this.childNodes.map((node) => node.data);
 | 
			
		||||
 | 
			
		||||
    const newDataMap = {};
 | 
			
		||||
    const newNodes = [];
 | 
			
		||||
 | 
			
		||||
    newData.forEach((item, index) => {
 | 
			
		||||
      if (item.$treeNodeId) {
 | 
			
		||||
        newDataMap[item.$treeNodeId] = { index, data: item };
 | 
			
		||||
      } else {
 | 
			
		||||
        newNodes.push({ index, data: item });
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    oldData.forEach((item) => { if (!newDataMap[item.$treeNodeId]) this.removeChildByData(item); });
 | 
			
		||||
    newNodes.forEach(({ index, data }) => this.insertChild({ data }, index));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  loadData(callback, defaultProps = {}) {
 | 
			
		||||
    if (this.lazy === true && this.load && !this.loaded) {
 | 
			
		||||
      this.loading = true;
 | 
			
		||||
 | 
			
		||||
      const loadFn = this.load;
 | 
			
		||||
      const resolve = (children) => {
 | 
			
		||||
        this.loaded = true;
 | 
			
		||||
        this.loading = false;
 | 
			
		||||
        this.children = [];
 | 
			
		||||
        this.childNodes = [];
 | 
			
		||||
 | 
			
		||||
        this.doCreateChildren(children, defaultProps);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -242,7 +277,7 @@ export default class Node {
 | 
			
		|||
        }
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      loadFn(this, resolve);
 | 
			
		||||
      this.load(this, resolve);
 | 
			
		||||
    } else {
 | 
			
		||||
      if (callback) {
 | 
			
		||||
        callback.call(this);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,8 +8,6 @@ export default class Tree {
 | 
			
		|||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this._isTree = true;
 | 
			
		||||
 | 
			
		||||
    this.root = new Node({
 | 
			
		||||
      data: this.data,
 | 
			
		||||
      lazy: this.lazy,
 | 
			
		||||
| 
						 | 
				
			
			@ -28,9 +26,9 @@ export default class Tree {
 | 
			
		|||
  getCheckedNodes(leafOnly) {
 | 
			
		||||
    const checkedNodes = [];
 | 
			
		||||
    const walk = function(node) {
 | 
			
		||||
      const children = node.root ? node.root.children : node.children;
 | 
			
		||||
      const childNodes = node.root ? node.root.childNodes : node.childNodes;
 | 
			
		||||
 | 
			
		||||
      children.forEach(function(child) {
 | 
			
		||||
      childNodes.forEach(function(child) {
 | 
			
		||||
        if ((!leafOnly && child.checked) || (leafOnly && child.isLeaf && child.checked)) {
 | 
			
		||||
          checkedNodes.push(child.data);
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -58,7 +58,7 @@ class Transition {
 | 
			
		|||
    el.style.paddingTop = el.dataset.oldPaddingTop;
 | 
			
		||||
    el.style.paddingBottom = el.dataset.oldPaddingBottom;
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  functional: true,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,29 +1,42 @@
 | 
			
		|||
<template>
 | 
			
		||||
  <div class="el-tree-node"
 | 
			
		||||
     :class="{ expanded: childrenRendered && expanded }">
 | 
			
		||||
    <div class="el-tree-node__content" :style="{ 'padding-left': node.level * 16 + 'px' }"
 | 
			
		||||
         @click="handleExpandIconClick">
 | 
			
		||||
      <span class="el-tree-node__expand-icon"
 | 
			
		||||
        :class="{ 'is-leaf': node.isLeaf, expanded: !node.isLeaf && expanded }"
 | 
			
		||||
        ></span>
 | 
			
		||||
      <el-checkbox v-if="showCheckbox" :indeterminate="node.indeterminate" v-model="node.checked" @change="handleCheckChange" @click.native="handleUserClick"></el-checkbox>
 | 
			
		||||
    @click.stop="handleClick"
 | 
			
		||||
    :class="{ expanded: childNodeRendered && expanded, 'is-current': $tree.currentNode === _self }">
 | 
			
		||||
    <div class="el-tree-node__content"
 | 
			
		||||
      :style="{ 'padding-left': node.level * 16 + 'px' }"
 | 
			
		||||
      @click="handleExpandIconClick">
 | 
			
		||||
      <span
 | 
			
		||||
        class="el-tree-node__expand-icon"
 | 
			
		||||
        :class="{ 'is-leaf': node.isLeaf, expanded: !node.isLeaf && expanded }">
 | 
			
		||||
      </span>
 | 
			
		||||
      <el-checkbox
 | 
			
		||||
        v-if="showCheckbox"
 | 
			
		||||
        v-model="node.checked"
 | 
			
		||||
        :indeterminate="node.indeterminate"
 | 
			
		||||
        @change="handleCheckChange"
 | 
			
		||||
        @click.native="handleUserClick">
 | 
			
		||||
      </el-checkbox>
 | 
			
		||||
      <span
 | 
			
		||||
        v-if="node.loading"
 | 
			
		||||
        class="el-tree-node__icon el-icon-loading"
 | 
			
		||||
      >
 | 
			
		||||
        class="el-tree-node__icon el-icon-loading">
 | 
			
		||||
      </span>
 | 
			
		||||
      <span class="el-tree-node__label" v-html="node.label"></span>
 | 
			
		||||
      <node-content :node="node"></node-content>
 | 
			
		||||
    </div>
 | 
			
		||||
    <collapse-transition>
 | 
			
		||||
      <div class="el-tree-node__children"
 | 
			
		||||
      <div
 | 
			
		||||
        class="el-tree-node__children"
 | 
			
		||||
        v-show="expanded">
 | 
			
		||||
        <el-tree-node v-for="child in node.children" :node="child"></el-tree-node>
 | 
			
		||||
        <el-tree-node
 | 
			
		||||
          :render-content="renderContent"
 | 
			
		||||
          v-for="child in node.childNodes"
 | 
			
		||||
          :node="child">
 | 
			
		||||
        </el-tree-node>
 | 
			
		||||
      </div>
 | 
			
		||||
    </collapse-transition>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script type="text/ecmascript-6">
 | 
			
		||||
<script type="text/jsx">
 | 
			
		||||
  import CollapseTransition from './transition';
 | 
			
		||||
 | 
			
		||||
  export default {
 | 
			
		||||
| 
						 | 
				
			
			@ -34,18 +47,35 @@
 | 
			
		|||
        default() {
 | 
			
		||||
          return {};
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      },
 | 
			
		||||
      props: {},
 | 
			
		||||
      renderContent: Function
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    components: {
 | 
			
		||||
      CollapseTransition
 | 
			
		||||
      CollapseTransition,
 | 
			
		||||
      NodeContent: {
 | 
			
		||||
        props: {
 | 
			
		||||
          node: {
 | 
			
		||||
            required: true
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        render(h) {
 | 
			
		||||
          const parent = this.$parent;
 | 
			
		||||
          return (
 | 
			
		||||
            parent.renderContent
 | 
			
		||||
              ? parent.renderContent.call(parent._renderProxy, h, { _self: parent.$parent.$vnode.context, node: this.node })
 | 
			
		||||
              : <span class="el-tree-node__label">{ this.node.label }</span>
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    data() {
 | 
			
		||||
      return {
 | 
			
		||||
        $tree: null,
 | 
			
		||||
        expanded: false,
 | 
			
		||||
        childrenRendered: false,
 | 
			
		||||
        childNodeRendered: false,
 | 
			
		||||
        showCheckbox: false,
 | 
			
		||||
        oldChecked: null,
 | 
			
		||||
        oldIndeterminate: null
 | 
			
		||||
| 
						 | 
				
			
			@ -71,21 +101,25 @@
 | 
			
		|||
        this.indeterminate = indeterminate;
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      handleClick() {
 | 
			
		||||
        this.$tree.currentNode = this;
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      handleExpandIconClick(event) {
 | 
			
		||||
        let target = event.target;
 | 
			
		||||
        if (target.tagName.toUpperCase() !== 'DIV' &&
 | 
			
		||||
                target.parentNode.nodeName.toUpperCase() !== 'DIV' ||
 | 
			
		||||
                target.nodeName.toUpperCase() === 'LABLE') return;
 | 
			
		||||
          target.parentNode.nodeName.toUpperCase() !== 'DIV' ||
 | 
			
		||||
          target.nodeName.toUpperCase() === 'LABEL') return;
 | 
			
		||||
        if (this.expanded) {
 | 
			
		||||
          this.node.collapse();
 | 
			
		||||
          this.expanded = false;
 | 
			
		||||
        } else {
 | 
			
		||||
          this.node.expand(() => {
 | 
			
		||||
            this.expanded = true;
 | 
			
		||||
            this.childrenRendered = true;
 | 
			
		||||
            this.childNodeRendered = true;
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
        this.$tree.$emit('node-click', this.node.data);
 | 
			
		||||
        this.$tree.$emit('node-click', this.node.data, this.node, this);
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      handleUserClick() {
 | 
			
		||||
| 
						 | 
				
			
			@ -111,6 +145,12 @@
 | 
			
		|||
      }
 | 
			
		||||
 | 
			
		||||
      const tree = this.$tree;
 | 
			
		||||
      const props = this.props || {};
 | 
			
		||||
      const childrenKey = props['children'] || 'children';
 | 
			
		||||
 | 
			
		||||
      this.$watch(`node.data.${childrenKey}`, () => {
 | 
			
		||||
        this.node.updateChildren();
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      if (!tree) {
 | 
			
		||||
        console.warn('Can not find node\'s tree.');
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,11 @@
 | 
			
		|||
<template>
 | 
			
		||||
  <div class="el-tree">
 | 
			
		||||
    <el-tree-node v-for="child in tree.root.children" :node="child"></el-tree-node>
 | 
			
		||||
  <div class="el-tree" :class="{ 'el-tree--highlight-current': highlightCurrent }">
 | 
			
		||||
    <el-tree-node
 | 
			
		||||
      v-for="child in tree.root.childNodes"
 | 
			
		||||
      :node="child"
 | 
			
		||||
      :props="props"
 | 
			
		||||
      :render-content="renderContent">
 | 
			
		||||
    </el-tree-node>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -14,6 +19,7 @@
 | 
			
		|||
      data: {
 | 
			
		||||
        type: Array
 | 
			
		||||
      },
 | 
			
		||||
      renderContent: Function,
 | 
			
		||||
      showCheckbox: {
 | 
			
		||||
        type: Boolean,
 | 
			
		||||
        default: false
 | 
			
		||||
| 
						 | 
				
			
			@ -31,6 +37,7 @@
 | 
			
		|||
        type: Boolean,
 | 
			
		||||
        default: false
 | 
			
		||||
      },
 | 
			
		||||
      highlightCurrent: Boolean,
 | 
			
		||||
      load: {
 | 
			
		||||
        type: Function
 | 
			
		||||
      }
 | 
			
		||||
| 
						 | 
				
			
			@ -49,7 +56,8 @@
 | 
			
		|||
 | 
			
		||||
    data() {
 | 
			
		||||
      return {
 | 
			
		||||
        tree: {}
 | 
			
		||||
        tree: {},
 | 
			
		||||
        currentNode: null
 | 
			
		||||
      };
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue