Bläddra i källkod

Improve Tree:
1. Add props: renderContent, highlightCurrent
2. Fix Bug: tree do not change when data changed

furybean 8 år sedan
förälder
incheckning
785bed20df

+ 3 - 1
examples/docs/zh-cn/tree.md

@@ -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>节点本身是否被选中、节点的子树中是否有被选中的节点 |

+ 5 - 1
packages/theme-default/src/tree.css

@@ -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;
 }

+ 70 - 35
packages/tree/src/model/node.js

@@ -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);

+ 2 - 4
packages/tree/src/model/tree.js

@@ -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);
         }

+ 1 - 1
packages/tree/src/transition.js

@@ -58,7 +58,7 @@ class Transition {
     el.style.paddingTop = el.dataset.oldPaddingTop;
     el.style.paddingBottom = el.dataset.oldPaddingBottom;
   }
-};
+}
 
 export default {
   functional: true,

+ 60 - 20
packages/tree/src/tree-node.vue

@@ -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.');

+ 11 - 3
packages/tree/src/tree.vue

@@ -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
       };
     },