Quellcode durchsuchen

Merge branch 'dev' into carbon

Leopoldthecoder vor 8 Jahren
Ursprung
Commit
96f542e024
44 geänderte Dateien mit 945 neuen und 223 gelöschten Zeilen
  1. 1 0
      .gitignore
  2. 2 2
      build/bin/version.js
  3. 1 1
      build/deploy-ci.sh
  4. 1 0
      examples/app.vue
  5. 1 1
      examples/docs/en-US/checkbox.md
  6. 63 2
      examples/docs/en-US/menu.md
  7. 5 0
      examples/docs/en-US/select.md
  8. 90 2
      examples/docs/en-US/tree.md
  9. 1 1
      examples/docs/zh-CN/checkbox.md
  10. 61 2
      examples/docs/zh-CN/menu.md
  11. 5 0
      examples/docs/zh-CN/select.md
  12. 89 2
      examples/docs/zh-CN/tree.md
  13. 1 1
      examples/versions.json
  14. 1 1
      package.json
  15. 24 4
      packages/cascader/src/menu.vue
  16. 21 3
      packages/date-picker/src/basic/month-table.vue
  17. 20 3
      packages/date-picker/src/basic/year-table.vue
  18. 10 2
      packages/date-picker/src/panel/date.vue
  19. 13 1
      packages/date-picker/src/panel/time-select.vue
  20. 1 1
      packages/date-picker/src/picker.vue
  21. 1 1
      packages/form/src/form-item.vue
  22. 2 0
      packages/menu/src/menu-item-group.vue
  23. 13 1
      packages/menu/src/menu-item.vue
  24. 9 4
      packages/menu/src/menu-mixin.js
  25. 84 10
      packages/menu/src/menu.vue
  26. 33 26
      packages/menu/src/submenu.vue
  27. 29 3
      packages/select/src/option.vue
  28. 54 49
      packages/select/src/select.vue
  29. 1 1
      packages/table/src/table-column.js
  30. 1 1
      packages/table/src/table.vue
  31. 2 18
      packages/table/src/util.js
  32. 1 1
      packages/theme-default/package.json
  33. 20 0
      packages/theme-default/src/common/transition.css
  34. 41 1
      packages/theme-default/src/menu.css
  35. 3 0
      packages/tooltip/src/main.js
  36. 49 16
      packages/tree/src/model/node.js
  37. 35 49
      packages/tree/src/model/tree-store.js
  38. 3 11
      packages/tree/src/tree-node.vue
  39. 2 1
      packages/tree/src/tree.vue
  40. 1 1
      src/index.js
  41. 21 0
      src/utils/scroll-into-view.js
  42. 18 0
      src/utils/util.js
  43. 46 0
      test/unit/specs/select.spec.js
  44. 65 0
      test/unit/specs/tree.spec.js

+ 1 - 0
.gitignore

@@ -12,3 +12,4 @@ examples/pages/zh-CN
 fe.element/element-ui
 .npmrc
 coverage
+waiter.config.js

+ 2 - 2
build/bin/version.js

@@ -1,6 +1,6 @@
 var fs = require('fs');
 var path = require('path');
 var version = process.env.VERSION || require('../../package.json').version;
-var content = { '1.0.9': '1.0', '1.1.6': '1.1', '1.2.9': '1.2' };
-if (!content[version]) content[version] = '1.3';
+var content = { '1.0.9': '1.0', '1.1.6': '1.1', '1.2.9': '1.2', '1.3.7': '1.3' };
+if (!content[version]) content[version] = '1.4';
 fs.writeFileSync(path.resolve(__dirname, '../../examples/versions.json'), JSON.stringify(content));

+ 1 - 1
build/deploy-ci.sh

@@ -42,7 +42,7 @@ if [ "$TRAVIS_TAG" ]; then
   export SUB_FOLDER=$(echo "$TRAVIS_TAG" | grep -o -E "\d+\.\d+")
   echo $SUB_FOLDER
 
-  SUB_FOLDER='1.3'
+  SUB_FOLDER='1.4'
   mkdir $SUB_FOLDER
   rm -rf *.js *.css *.map static
   rm -rf $SUB_FOLDER/**

+ 1 - 0
examples/app.vue

@@ -107,6 +107,7 @@
       background-color: #ECF8FF;
       border-radius: 4px;
       border-left: #50bfff 5px solid;
+      margin-top: 20px;
 
       code {
         background-color: rgba(#fff, .7);

+ 1 - 1
examples/docs/en-US/checkbox.md

@@ -243,7 +243,7 @@ Checkbox with button styles.
 | min     | minimum number of checkbox checked   | number    |       —        |     —    |
 | max     | maximum number of checkbox checked   | number    |       —        |     —    |
 
-### Checkbox-group Events
+### Checkbox Events
 | Event Name | Description | Parameters |
 |---------- |-------- |---------- |
 | change  | triggers when the binding value changes | Event object |

+ 63 - 2
examples/docs/en-US/menu.md

@@ -3,7 +3,7 @@
     .el-menu-demo {
       padding-left: 55px;
     }
-    .el-menu-vertical-demo {
+    .el-menu-vertical-demo:not(.el-menu--collapse) {
       width: 200px;
       min-height: 400px;
     }
@@ -33,7 +33,8 @@
     data() {
       return {
         activeIndex: '1',
-        activeIndex2: '1'
+        activeIndex2: '1',
+        isCollapse: false
       };
     },
     methods: {
@@ -179,10 +180,70 @@ Vertical NavMenu with sub-menus.
 ```
 :::
 
+### Collapse
+
+Vertical NavMenu could be collapsed.
+
+::: demo 
+```html
+<el-radio-group v-model="isCollapse" style="margin-bottom: 20px;">
+  <el-radio-button :label="false">expand</el-radio-button>
+  <el-radio-button :label="true">collapse</el-radio-button>
+</el-radio-group>
+<el-menu default-active="2" class="el-menu-vertical-demo" @open="handleOpen" @close="handleClose" :collapse="isCollapse">
+  <el-submenu index="1">
+    <template slot="title">
+      <i class="el-icon-message"></i>
+      <span slot="title">Navigator One</span>
+    </template>
+    <el-menu-item-group>
+      <span slot="title">Group One</span>
+      <el-menu-item index="1-1">item one</el-menu-item>
+      <el-menu-item index="1-2">item two</el-menu-item>
+    </el-menu-item-group>
+    <el-menu-item-group title="Group Two">
+      <el-menu-item index="1-3">item three</el-menu-item>
+    </el-menu-item-group>
+    <el-submenu index="1-4">
+      <span slot="title">item four</span>
+      <el-menu-item index="1-4-1">item one</el-menu-item>
+    </el-submenu>
+  </el-submenu>
+  <el-menu-item index="2">
+    <i class="el-icon-menu"></i>
+    <span slot="title">Navigator Two</span>
+  </el-menu-item>
+  <el-menu-item index="3">
+    <i class="el-icon-setting"></i>
+    <span slot="title">Navigator Three</span>
+  </el-menu-item>
+</el-menu>
+
+<script>
+  export default {
+    data() {
+      return {
+        isCollapse: false
+      };
+    },
+    methods: {
+      handleOpen(key, keyPath) {
+        console.log(key, keyPath);
+      },
+      handleClose(key, keyPath) {
+        console.log(key, keyPath);
+      }
+    }
+  }
+</script>
+```
+:::
+
 ### Menu Attribute
 | Attribute      | Description          | Type      | Accepted Values       | Default  |
 |---------- |-------- |---------- |-------------  |-------- |
 | mode     | menu display mode   | string  |   horizontal/vertical   | vertical |
+| collapse  | whether the menu is collapsed (available only in vertical mode) | boolean  |   —   | false |
 | theme     | theme color   | string    | light/dark | light |
 | default-active | index of currently active menu | string    | — | — |
 | default-openeds | array that contains keys of currently active sub-menus  | Array    | — | — |

+ 5 - 0
examples/docs/en-US/select.md

@@ -637,11 +637,16 @@ Create and select new items that are not included in select options
 ```
 :::
 
+:::tip
+If the binding value of Select is an object, make sure to assign `value-key` as its unique identity key name.
+:::
+
 ### Select Attributes
 | Attribute      | Description          | Type      | Accepted Values       | Default  |
 |---------- |-------------- |---------- |--------------------------------  |-------- |
 | multiple | whether multiple-select is activated | boolean | — | false |
 | disabled | whether Select is disabled | boolean | — | false |
+| value-key | unique identity key name for value, required when value is an object | string | — | value |
 | size | size of Input | string | large/small/mini | — |
 | clearable | whether single select can be cleared | boolean | — | false |
 | multiple-limit | maximum number of options user can select when `multiple` is `true`. No limit when set to 0 | number | — | 0 |

+ 90 - 2
examples/docs/en-US/tree.md

@@ -93,6 +93,35 @@
     }]
   }];
 
+  const data3 = [{
+    id: 1,
+    label: 'Level one 1',
+    children: [{
+      id: 3,
+      label: 'Level two 2-1',
+      children: [{
+        id: 4,
+        label: 'Level three 3-1-1'
+      }, {
+        id: 5,
+        label: 'Level three 3-1-2',
+        disabled: true
+      }]
+    }, {
+      id: 2,
+      label: 'Level two 2-2',
+      disabled: true,
+      children: [{
+        id: 6,
+        label: 'Level three 3-2-1'
+      }, {
+        id: 7,
+        label: 'Level three 3-2-2',
+        disabled: true
+      }]
+    }]
+  }];
+
   let id = 1000;
 
   const regions = [{
@@ -211,6 +240,7 @@
       return {
         data,
         data2,
+        data3,
         regions,
         defaultProps,
         props,
@@ -363,6 +393,63 @@ Used for node selection. In the following example, data for each layer is acquir
 ```
 :::
 
+### Disabled checkbox
+
+The checkbox of a node can be set as disabled.
+
+::: demo In the example, 'disabled' property is declared in defaultProps, and some nodes are set as 'disabled:true'. The corresponding checkboxes are disabled and can't be clicked.
+```html
+<el-tree
+  :data="data3"
+  :props="defaultProps"
+  show-checkbox
+  @check-change="handleCheckChange">
+</el-tree>
+
+<script>
+  export default {
+    data() {
+      return {
+        data3: [{
+          id: 1,
+          label: 'Level one 1',
+          children: [{
+            id: 3,
+            label: 'Level two 2-1',
+            children: [{
+              id: 4,
+              label: 'Level three 3-1-1'
+            }, {
+              id: 5,
+              label: 'Level three 3-1-2',
+              disabled: true
+            }]
+          }, {
+            id: 2,
+            label: 'Level two 2-2',
+            disabled: true,
+            children: [{
+              id: 6,
+              label: 'Level three 3-2-1'
+            }, {
+              id: 7,
+              label: 'Level three 3-2-2',
+              disabled: true
+            }]
+          }]
+        }],
+        defaultProps: {
+            children: 'children',
+            label: 'label',
+            disabled: 'disabled',
+        },
+      };
+    }
+  };
+</script>
+```
+:::
+
 ### Default expanded and default checked
 Tree nodes can be initially expanded or checked
 
@@ -789,8 +876,9 @@ Only one node among the same level can be expanded at one time.
 ### props
 | Attribute | Description                              | Type   | Accepted Values | Default |
 | --------- | ---------------------------------------- | ------ | --------------- | ------- |
-| label     | specify which key of node object is used as the node's label | string | —               | —       |
-| children  | specify which key of node object is used as the node's subtree | string | —               | —       |
+| label     | specify which key of node object is used as the node's label | string, function(data, node) | —               | —       |
+| children | specify which node object is used as the node's subtree | string, function(data, node) | —               | —       |
+| disabled | specify which node's checkbox disabled |  boolean, function(data, node) | —    | —    |
 
 ### Method
 `Tree` has the following method, which returns the currently selected array of nodes.

+ 1 - 1
examples/docs/zh-CN/checkbox.md

@@ -254,7 +254,7 @@
 | min     | 可被勾选的 checkbox 的最小数量   | number    |       —        |     —    |
 | max     | 可被勾选的 checkbox 的最大数量   | number    |       —        |     —    |
 
-### Checkbox-group Events
+### Checkbox Events
 | 事件名称      | 说明    | 回调参数      |
 |---------- |-------- |---------- |
 | change  | 当绑定值变化时触发的事件 | event 事件对象 |

+ 61 - 2
examples/docs/zh-CN/menu.md

@@ -3,7 +3,7 @@
     .el-menu-demo {
       padding-left: 55px;
     }
-    .el-menu-vertical-demo {
+    .el-menu-vertical-demo:not(.el-menu--collapse) {
       width: 200px;
       min-height: 400px;
     }
@@ -33,7 +33,8 @@
     data() {
       return {
         activeIndex: '1',
-        activeIndex2: '1'
+        activeIndex2: '1',
+        isCollapse: true
       };
     },
     methods: {
@@ -181,10 +182,68 @@
 ```
 :::
 
+### 折叠
+
+::: demo
+```html
+<el-radio-group v-model="isCollapse" style="margin-bottom: 20px;">
+  <el-radio-button :label="false">展开</el-radio-button>
+  <el-radio-button :label="true">收起</el-radio-button>
+</el-radio-group>
+<el-menu default-active="2" class="el-menu-vertical-demo" @open="handleOpen" @close="handleClose" :collapse="isCollapse">
+  <el-submenu index="1">
+    <template slot="title">
+      <i class="el-icon-message"></i>
+      <span slot="title">导航一</span>
+    </template>
+    <el-menu-item-group>
+      <span slot="title">分组一</span>
+      <el-menu-item index="1-1">选项1</el-menu-item>
+      <el-menu-item index="1-2">选项2</el-menu-item>
+    </el-menu-item-group>
+    <el-menu-item-group title="分组2">
+      <el-menu-item index="1-3">选项3</el-menu-item>
+    </el-menu-item-group>
+    <el-submenu index="1-4">
+      <span slot="title">选项4</span>
+      <el-menu-item index="1-4-1">选项1</el-menu-item>
+    </el-submenu>
+  </el-submenu>
+  <el-menu-item index="2">
+    <i class="el-icon-menu"></i>
+    <span slot="title">导航二</span>
+  </el-menu-item>
+  <el-menu-item index="3">
+    <i class="el-icon-setting"></i>
+    <span slot="title">导航三</span>
+  </el-menu-item>
+</el-menu>
+
+<script>
+  export default {
+    data() {
+      return {
+        isCollapse: false
+      };
+    },
+    methods: {
+      handleOpen(key, keyPath) {
+        console.log(key, keyPath);
+      },
+      handleClose(key, keyPath) {
+        console.log(key, keyPath);
+      }
+    }
+  }
+</script>
+```
+:::
+
 ### Menu Attribute
 | 参数      | 说明    | 类型      | 可选值       | 默认值   |
 |---------- |-------- |---------- |-------------  |-------- |
 | mode     | 模式   | string  |   horizontal,vertical   | vertical |
+| collapse  | 是否水平折叠收起菜单(仅在 mode 为 vertical 时可用)| boolean  |   —   | false |
 | theme     | 主题色   | string    | light,dark | light |
 | default-active | 当前激活菜单的 index | string    | — | — |
 | default-openeds | 当前打开的submenu的 key 数组 | Array    | — | — |

+ 5 - 0
examples/docs/zh-CN/select.md

@@ -632,11 +632,16 @@
 ```
 :::
 
+:::tip
+如果 Select 的绑定值为对象类型,请务必指定 `value-key` 作为它的唯一性标识。
+:::
+
 ### Select Attributes 
 | 参数      | 说明          | 类型      | 可选值                           | 默认值  |
 |---------- |-------------- |---------- |--------------------------------  |-------- |
 | multiple | 是否多选 | boolean | — | false |
 | disabled | 是否禁用 | boolean | — | false |
+| value-key | 作为 value 唯一标识的键名,绑定值为对象类型时必填 | string | — | value |
 | size | 输入框尺寸 | string | large/small/mini | — |
 | clearable | 单选时是否可以清空选项 | boolean | — | false |
 | multiple-limit | 多选时用户最多可以选择的项目数,为 0 则不限制 | number | — | 0 |

+ 89 - 2
examples/docs/zh-CN/tree.md

@@ -93,6 +93,35 @@
     }]
   }];
 
+  const data3 = [{
+    id: 1,
+    label: '一级 2',
+    children: [{
+      id: 3,
+      label: '二级 2-1',
+      children: [{
+        id: 4,
+        label: '三级 3-1-1'
+      }, {
+        id: 5,
+        label: '三级 3-1-2',
+        disabled: true
+      }]
+    }, {
+      id: 2,
+      label: '二级 2-2',
+      disabled: true,
+      children: [{
+        id: 6,
+        label: '三级 3-2-1'
+      }, {
+        id: 7,
+        label: '三级 3-2-2',
+        disabled: true
+      }]
+    }]
+  }];
+
   let id = 1000;
 
   const regions = [{
@@ -211,6 +240,7 @@
       return {
         data,
         data2,
+        data3,
         regions,
         defaultProps,
         props,
@@ -427,6 +457,62 @@
 ```
 :::
 
+### 禁用状态
+可将 Tree 的某些节点设置为禁用状态
+
+::: demo 通过`disabled`设置禁用状态。
+```html
+<el-tree
+  :data="data3"
+  show-checkbox
+  node-key="id"
+  :default-expanded-keys="[2, 3]"
+  :default-checked-keys="[5]">
+</el-tree>
+
+<script>
+  export default {
+    data() {
+      return {
+        data3: [{
+          id: 1,
+          label: '一级 2',
+          children: [{
+            id: 3,
+            label: '二级 2-1',
+            children: [{
+              id: 4,
+              label: '三级 3-1-1'
+            }, {
+              id: 5,
+              label: '三级 3-1-2',
+              disabled: true
+            }]
+          }, {
+            id: 2,
+            label: '二级 2-2',
+            disabled: true,
+            children: [{
+              id: 6,
+              label: '三级 3-2-1'
+            }, {
+              id: 7,
+              label: '三级 3-2-2',
+              disabled: true
+            }]
+          }]
+        }],
+        defaultProps: {
+          children: 'children',
+          label: 'label'
+        }
+      };
+    }
+  };
+</script>
+```
+:::
+
 ### 树节点的选择
 
 ::: demo 本例展示如何获取和设置选中节点。获取和设置各有两种方式:通过 node 或通过 key。如果需要通过 key 来获取或设置,则必须设置`node-key`。
@@ -789,8 +875,9 @@
 ### props
 | 参数       | 说明                | 类型     | 可选值  | 默认值  |
 | -------- | ----------------- | ------ | ---- | ---- |
-| label    | 指定节点标签为节点对象的某个属性值 | string | —    | —    |
-| children | 指定子树为节点对象的某个属性值   | string | —    | —    |
+| label    | 指定节点标签为节点对象的某个属性值 | string, function(data, node) | —    | —    |
+| children | 指定子树为节点对象的某个属性值 | string, function(data, node) | —    | —    |
+| disabled | 指定节点选择框是否禁用 |  boolean, function(data, node) | —    | —    |
 
 ### 方法
 `Tree` 拥有如下方法,返回目前被选中的节点数组:

+ 1 - 1
examples/versions.json

@@ -1 +1 @@
-{"1.0.9":"1.0","1.1.6":"1.1","1.2.9":"1.2","1.3.7":"1.3"}
+{"1.0.9":"1.0","1.1.6":"1.1","1.2.9":"1.2","1.3.7":"1.3","1.4.0-beta.1":"1.4"}

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "element-ui",
-  "version": "1.3.7",
+  "version": "1.4.0-beta.1",
   "description": "A Component Library for Vue.js.",
   "main": "lib/element-ui.common.js",
   "files": [

+ 24 - 4
packages/cascader/src/menu.vue

@@ -1,5 +1,6 @@
 <script>
   import { isDef } from 'element-ui/src/utils/shared';
+  import scrollIntoView from 'element-ui/src/utils/scroll-into-view';
 
   export default {
     name: 'ElCascaderMenu',
@@ -94,6 +95,12 @@
         } else {
           this.$emit('activeItemChange', this.activeValue);
         }
+      },
+      scrollMenu(menu) {
+        scrollIntoView(menu, menu.getElementsByClassName('is-active')[0]);
+      },
+      handleMenuEnter() {
+        this.$nextTick(() => this.$refs.menus.forEach(menu => this.scrollMenu(menu)));
       }
     },
 
@@ -121,9 +128,19 @@
                 click: 'click',
                 hover: 'mouseenter'
               }[expandTrigger];
-              events.on[triggerEvent] = () => { this.activeItem(item, menuIndex); };
+              events.on[triggerEvent] = () => {
+                this.activeItem(item, menuIndex);
+                this.$nextTick(() => {
+                  // adjust self and next level
+                  this.scrollMenu(this.$refs.menus[menuIndex]);
+                  this.scrollMenu(this.$refs.menus[menuIndex + 1]);
+                });
+              };
             } else {
-              events.on.click = () => { this.select(item, menuIndex); };
+              events.on.click = () => {
+                this.select(item, menuIndex);
+                this.$nextTick(() => this.scrollMenu(this.$refs.menus[menuIndex]));
+              };
             }
           }
 
@@ -152,19 +169,22 @@
               'el-cascader-menu': true,
               'el-cascader-menu--flexible': isFlat
             }}
-            style={menuStyle}>
+            style={menuStyle}
+            refInFor
+            ref="menus">
             {items}
           </ul>
         );
       });
       return (
-        <transition name="el-zoom-in-top" on-after-leave={this.handleMenuLeave}>
+        <transition name="el-zoom-in-top" on-before-enter={this.handleMenuEnter} on-after-leave={this.handleMenuLeave}>
           <div
             v-show={visible}
             class={[
               'el-cascader-menus',
               popperClass
             ]}
+            ref="wrapper"
           >
             {menus}
           </div>

+ 21 - 3
packages/date-picker/src/basic/month-table.vue

@@ -63,11 +63,29 @@
     methods: {
       getCellStyle(month) {
         const style = {};
-        const date = new Date(this.date);
 
+        var year = this.date.getFullYear();
+        var date = new Date(0);
+        date.setFullYear(year);
         date.setMonth(month);
-        style.disabled = typeof this.disabledDate === 'function' &&
-          this.disabledDate(date);
+        date.setHours(0);
+        var nextMonth = new Date(date);
+        nextMonth.setMonth(month + 1);
+
+        var flag = false;
+        if (typeof this.disabledDate === 'function') {
+
+          while (date < nextMonth) {
+            if (this.disabledDate(date)) {
+              date = new Date(date.getTime() + 8.64e7);
+            } else {
+              break;
+            }
+          }
+          if ((date - nextMonth) === 0) flag = true;
+        }
+
+        style.disabled = flag;
         style.current = this.month === month;
 
         return style;

+ 20 - 3
packages/date-picker/src/basic/year-table.vue

@@ -62,11 +62,28 @@
     methods: {
       getCellStyle(year) {
         const style = {};
-        const date = new Date(this.date);
 
+        var date = new Date(0);
         date.setFullYear(year);
-        style.disabled = typeof this.disabledDate === 'function' &&
-          this.disabledDate(date);
+        date.setHours(0);
+        var nextYear = new Date(date);
+        nextYear.setFullYear(year + 1);
+
+        var flag = false;
+        if (typeof this.disabledDate === 'function') {
+
+          while (date < nextYear) {
+            if (this.disabledDate(date)) {
+              date = new Date(date.getTime() + 8.64e7);
+            } else {
+              break;
+            }
+          }
+          if ((date - nextYear) === 0) flag = true;
+
+        }
+
+        style.disabled = flag;
         style.current = Number(this.year) === year;
 
         return style;

+ 10 - 2
packages/date-picker/src/panel/date.vue

@@ -406,11 +406,11 @@
 
       visibleDate: {
         get() {
-          return formatDate(this.date);
+          return formatDate(this.date, this.dateFormat);
         },
 
         set(val) {
-          const date = parseDate(val, 'yyyy-MM-dd');
+          const date = parseDate(val, this.dateFormat);
           if (!date) {
             return;
           }
@@ -445,6 +445,14 @@
         } else {
           return 'HH:mm:ss';
         }
+      },
+
+      dateFormat() {
+        if (this.format) {
+          return this.format.replace('HH:mm', '').replace(':ss', '').trim();
+        } else {
+          return 'yyyy-MM-dd';
+        }
       }
     }
   };

+ 13 - 1
packages/date-picker/src/panel/time-select.vue

@@ -1,6 +1,7 @@
 <template>
-  <transition name="el-zoom-in-top" @after-leave="$emit('dodestroy')">
+  <transition name="el-zoom-in-top" @before-enter="handleMenuEnter" @after-leave="$emit('dodestroy')">
     <div
+      ref="popper"
       v-show="visible"
       :style="{ width: width + 'px' }"
       :class="popperClass"
@@ -18,6 +19,7 @@
 
 <script type="text/babel">
   import ElScrollbar from 'element-ui/packages/scrollbar';
+  import scrollIntoView from 'element-ui/src/utils/scroll-into-view';
 
   const parseTime = function(time) {
     const values = ('' || time).split(':');
@@ -81,6 +83,7 @@
         } else if (this.maxTime && compareTime(val, this.maxTime) > 0) {
           this.$emit('pick');
         }
+        this.$nextTick(() => this.scrollToOption());
       }
     },
 
@@ -93,6 +96,15 @@
 
       handleClear() {
         this.$emit('pick');
+      },
+
+      scrollToOption(className = 'selected') {
+        const menu = this.$refs.popper.querySelector('.el-picker-panel__content');
+        scrollIntoView(menu, menu.getElementsByClassName(className)[0]);
+      },
+
+      handleMenuEnter() {
+        this.$nextTick(() => this.scrollToOption());
       }
     },
 

+ 1 - 1
packages/date-picker/src/picker.vue

@@ -179,7 +179,7 @@ const TYPE_VALUE_RESOLVER_MAP = {
 };
 const PLACEMENT_MAP = {
   left: 'bottom-start',
-  center: 'bottom-center',
+  center: 'bottom',
   right: 'bottom-end'
 };
 

+ 1 - 1
packages/form/src/form-item.vue

@@ -4,7 +4,7 @@
     'is-validating': validateState === 'validating',
     'is-required': isRequired || required
   }">
-    <label :for="prop" class="el-form-item__label" v-bind:style="labelStyle" v-if="label">
+    <label :for="prop" class="el-form-item__label" v-bind:style="labelStyle" v-if="label || $slots.label">
       <slot name="label">{{label + form.labelSuffix}}</slot>
     </label>
     <div class="el-form-item__content" v-bind:style="contentStyle">

+ 2 - 0
packages/menu/src/menu-item-group.vue

@@ -15,6 +15,7 @@
 
     componentName: 'ElMenuItemGroup',
 
+    inject: ['rootMenu'],
     props: {
       title: {
         type: String
@@ -29,6 +30,7 @@
       levelPadding() {
         let padding = 10;
         let parent = this.$parent;
+        if (this.rootMenu.collapse) return 20;
         while (parent && parent.$options.componentName !== 'ElMenu') {
           if (parent.$options.componentName === 'ElSubmenu') {
             padding += 20;

+ 13 - 1
packages/menu/src/menu-item.vue

@@ -6,7 +6,19 @@
       'is-active': active,
       'is-disabled': disabled
     }">
-    <slot></slot>
+    <el-tooltip
+      v-if="$parent === rootMenu && rootMenu.collapse"
+      effect="dark"
+      placement="right">
+      <div slot="content"><slot name="title"></slot></div>
+      <div style="position: absolute;left: 0;top: 0;height: 100%;width: 100%;display: inline-block;box-sizing: border-box;padding: 0 20px;">
+        <slot></slot>
+      </div>
+    </el-tooltip>
+    <template v-else>
+      <slot></slot>
+      <slot name="title"></slot>
+    </template>
   </li>
 </template>
 <script>

+ 9 - 4
packages/menu/src/menu-mixin.js

@@ -36,11 +36,16 @@ export default {
 
       let padding = 20;
       let parent = this.$parent;
-      while (parent && parent.$options.componentName !== 'ElMenu') {
-        if (parent.$options.componentName === 'ElSubmenu') {
-          padding += 20;
+
+      if (this.rootMenu.collapse) {
+        padding = 20;
+      } else {
+        while (parent && parent.$options.componentName !== 'ElMenu') {
+          if (parent.$options.componentName === 'ElSubmenu') {
+            padding += 20;
+          }
+          parent = parent.$parent;
         }
-        parent = parent.$parent;
       }
       return {paddingLeft: padding + 'px'};
     }

+ 84 - 10
packages/menu/src/menu.vue

@@ -1,15 +1,82 @@
 <template>
-  <ul class="el-menu"
-    :class="{
-      'el-menu--horizontal': mode === 'horizontal',
-      'el-menu--dark': theme === 'dark'
-    }"
-  >
-    <slot></slot>
-  </ul>
+  <el-menu-collapse-transition>
+    <ul class="el-menu"
+      :key="+collapse"
+      :class="{
+        'el-menu--horizontal': mode === 'horizontal',
+        'el-menu--dark': theme === 'dark',
+        'el-menu--collapse': collapse
+      }"
+    >
+      <slot></slot>
+    </ul>
+  </el-menu-collapse-transition>
 </template>
 <script>
+  import Vue from 'vue';
   import emitter from 'element-ui/src/mixins/emitter';
+  import { addClass, removeClass, hasClass } from 'element-ui/src/utils/dom';
+
+  Vue.component('el-menu-collapse-transition', {
+    functional: true,
+    render(createElement, context) {
+      const data = {
+        props: {
+          mode: 'out-in'
+        },
+        on: {
+          beforeEnter(el) {
+            el.style.opacity = 0.2;
+          },
+
+          enter(el) {
+            addClass(el, 'el-opacity-transition');
+            el.style.opacity = 1;
+          },
+
+          afterEnter(el) {
+            removeClass(el, 'el-opacity-transition');
+            el.style.opacity = '';
+          },
+
+          beforeLeave(el) {
+            if (!el.dataset) el.dataset = {};
+
+            if (hasClass(el, 'el-menu--collapse')) {
+              removeClass(el, 'el-menu--collapse');
+              el.dataset.oldOverflow = el.style.overflow;
+              el.dataset.scrollWidth = el.scrollWidth;
+              addClass(el, 'el-menu--collapse');
+            }
+
+            el.style.width = el.scrollWidth + 'px';
+            el.style.overflow = 'hidden';
+          },
+
+          leave(el) {
+            if (!hasClass(el, 'el-menu--collapse')) {
+              addClass(el, 'horizontal-collapse-transition');
+              el.style.width = '64px';
+            } else {
+              addClass(el, 'horizontal-collapse-transition');
+              el.style.width = el.dataset.scrollWidth + 'px';
+            }
+          },
+
+          afterLeave(el) {
+            removeClass(el, 'horizontal-collapse-transition');
+            if (hasClass(el, 'el-menu--collapse')) {
+              el.style.width = el.dataset.scrollWidth + 'px';
+            } else {
+              el.style.width = '64px';
+            }
+            el.style.overflow = el.dataset.oldOverflow;
+          }
+        }
+      };
+      return createElement('transition', data, context.children);
+    }
+  });
 
   export default {
     name: 'ElMenu',
@@ -18,6 +85,12 @@
 
     mixins: [emitter],
 
+    provide() {
+      return {
+        rootMenu: this
+      };
+    },
+
     props: {
       mode: {
         type: String,
@@ -37,7 +110,8 @@
       menuTrigger: {
         type: String,
         default: 'hover'
-      }
+      },
+      collapse: Boolean
     },
     data() {
       return {
@@ -106,7 +180,7 @@
         this.activedIndex = item.index;
         this.$emit('select', index, indexPath, item);
 
-        if (this.mode === 'horizontal') {
+        if (this.mode === 'horizontal' || this.collapse) {
           this.openedMenus = [];
         }
 

+ 33 - 26
packages/menu/src/submenu.vue

@@ -5,18 +5,21 @@
       'is-active': active,
       'is-opened': opened
     }"
+    @mouseenter="handleMouseenter"
+    @mouseleave="handleMouseleave"
   >
-    <div class="el-submenu__title" ref="submenu-title" :style="paddingStyle">
+    <div class="el-submenu__title" ref="submenu-title" @click="handleClick" :style="paddingStyle">
       <slot name="title"></slot>
       <i :class="{
         'el-submenu__icon-arrow': true,
-        'el-icon-arrow-down': rootMenu.mode === 'vertical',
-        'el-icon-caret-bottom': rootMenu.mode === 'horizontal'
+        'el-icon-caret-bottom': rootMenu.mode === 'horizontal',
+        'el-icon-arrow-down': rootMenu.mode === 'vertical' && !rootMenu.collapse,
+        'el-icon-caret-right': rootMenu.mode === 'vertical' && rootMenu.collapse
       }">
       </i>
     </div>
-    <template v-if="rootMenu.mode === 'horizontal'">
-      <transition name="el-zoom-in-top">
+    <template v-if="rootMenu.mode === 'horizontal' || (rootMenu.mode === 'vertical' && rootMenu.collapse)">
+      <transition :name="menuTransitionName">
         <ul class="el-menu" v-show="opened"><slot></slot></ul>
       </transition>
     </template>
@@ -45,6 +48,7 @@
         required: true
       }
     },
+
     data() {
       return {
         timeout: null,
@@ -53,6 +57,9 @@
       };
     },
     computed: {
+      menuTransitionName() {
+        return this.rootMenu.collapse ? 'el-zoom-in-left' : 'el-zoom-in-top';
+      },
       opened() {
         return this.rootMenu.openedMenus.indexOf(this.index) > -1;
       },
@@ -93,37 +100,40 @@
         delete this.submenus[item.index];
       },
       handleClick() {
+        const {rootMenu} = this;
+        if (
+          (rootMenu.menuTrigger === 'hover' && rootMenu.mode === 'horizontal') ||
+          (rootMenu.collapse && rootMenu.mode === 'vertical')
+        ) {
+          return;
+        }
         this.dispatch('ElMenu', 'submenu-click', this);
       },
       handleMouseenter() {
+        const {rootMenu} = this;
+        if (
+          (rootMenu.menuTrigger === 'click' && rootMenu.mode === 'horizontal') ||
+          (!rootMenu.collapse && rootMenu.mode === 'vertical')
+        ) {
+          return;
+        }
         clearTimeout(this.timeout);
         this.timeout = setTimeout(() => {
           this.rootMenu.openMenu(this.index, this.indexPath);
         }, 300);
       },
       handleMouseleave() {
+        const {rootMenu} = this;
+        if (
+          (rootMenu.menuTrigger === 'click' && rootMenu.mode === 'horizontal') ||
+          (!rootMenu.collapse && rootMenu.mode === 'vertical')
+        ) {
+          return;
+        }
         clearTimeout(this.timeout);
         this.timeout = setTimeout(() => {
           this.rootMenu.closeMenu(this.index, this.indexPath);
         }, 300);
-      },
-      initEvents() {
-        let {
-          rootMenu,
-          handleMouseenter,
-          handleMouseleave,
-          handleClick
-        } = this;
-        let triggerElm;
-
-        if (rootMenu.mode === 'horizontal' && rootMenu.menuTrigger === 'hover') {
-          triggerElm = this.$el;
-          triggerElm.addEventListener('mouseenter', handleMouseenter);
-          triggerElm.addEventListener('mouseleave', handleMouseleave);
-        } else {
-          triggerElm = this.$refs['submenu-title'];
-          triggerElm.addEventListener('click', handleClick);
-        }
       }
     },
     created() {
@@ -133,9 +143,6 @@
     beforeDestroy() {
       this.parentMenu.removeSubmenu(this);
       this.rootMenu.removeSubmenu(this);
-    },
-    mounted() {
-      this.initEvents();
     }
   };
 </script>

+ 29 - 3
packages/select/src/option.vue

@@ -17,6 +17,7 @@
 
 <script type="text/babel">
   import Emitter from 'element-ui/src/mixins/emitter';
+  import { getValueByPath } from 'element-ui/src/utils/util';
 
   export default {
     mixins: [Emitter],
@@ -47,8 +48,13 @@
     },
 
     computed: {
+      isObject() {
+        const type = typeof this.value;
+        return type !== 'string' && type !== 'number' && type !== 'boolean';
+      },
+
       currentLabel() {
-        return this.label || ((typeof this.value === 'string' || typeof this.value === 'number') ? this.value : '');
+        return this.label || (this.isObject ? '' : this.value);
       },
 
       currentValue() {
@@ -65,9 +71,9 @@
 
       itemSelected() {
         if (!this.parent.multiple) {
-          return this.value === this.parent.value;
+          return this.isEqual(this.value, this.parent.value);
         } else {
-          return this.parent.value.indexOf(this.value) > -1;
+          return this.contains(this.parent.value, this.value);
         }
       },
 
@@ -92,6 +98,26 @@
     },
 
     methods: {
+      isEqual(a, b) {
+        if (!this.isObject) {
+          return a === b;
+        } else {
+          const valueKey = this.parent.valueKey;
+          return getValueByPath(a, valueKey) === getValueByPath(b, valueKey);
+        }
+      },
+
+      contains(arr = [], target) {
+        if (!this.isObject) {
+          return arr.indexOf(target) > -1;
+        } else {
+          const valueKey = this.parent.valueKey;
+          return arr.some(item => {
+            return getValueByPath(item, valueKey) === getValueByPath(target, valueKey);
+          });
+        }
+      },
+
       handleGroupDisabled(val) {
         this.groupDisabled = val;
       },

+ 54 - 49
packages/select/src/select.vue

@@ -11,7 +11,7 @@
       <transition-group @after-leave="resetInputHeight">
         <el-tag
           v-for="item in selected"
-          :key="item.value"
+          :key="getValueKey(item)"
           closable
           :hit="item.hitState"
           type="primary"
@@ -67,8 +67,8 @@
     </el-input>
     <transition
       name="el-zoom-in-top"
-      @after-leave="doDestroy"
-      @after-enter="handleMenuEnter">
+      @before-enter="handleMenuEnter"
+      @after-leave="doDestroy">
       <el-select-menu
         ref="popper"
         v-show="visible && emptyText !== false">
@@ -105,6 +105,9 @@
   import { addClass, removeClass, hasClass } from 'element-ui/src/utils/dom';
   import { addResizeListener, removeResizeListener } from 'element-ui/src/utils/resize-event';
   import { t } from 'element-ui/src/locale';
+  import scrollIntoView from 'element-ui/src/utils/scroll-into-view';
+  import { getValueByPath } from 'element-ui/src/utils/util';
+
   const sizeMap = {
     'large': 42,
     'small': 30,
@@ -194,7 +197,11 @@
           return t('el.select.placeholder');
         }
       },
-      defaultFirstOption: Boolean
+      defaultFirstOption: Boolean,
+      valueKey: {
+        type: String,
+        default: 'value'
+      }
     },
 
     data() {
@@ -210,13 +217,10 @@
         cachedPlaceHolder: '',
         optionsCount: 0,
         filteredOptionsCount: 0,
-        dropdownUl: null,
         visible: false,
         selectedLabel: '',
         hoverIndex: -1,
         query: '',
-        bottomOverflow: 0,
-        topOverflow: 0,
         optionsAllDisabled: false,
         inputHovering: false,
         currentPlaceholder: ''
@@ -292,7 +296,6 @@
             }
           });
           if (!this.multiple) {
-            this.getOverflows();
             if (this.selected) {
               if (this.filterable && this.allowCreate &&
                 this.createdSelected && this.createdOption) {
@@ -353,44 +356,31 @@
         }
       },
 
-      handleMenuEnter() {
-        if (!this.dropdownUl) {
-          this.dropdownUl = this.$refs.popper.$el.querySelector('.el-select-dropdown__wrap');
-          this.getOverflows();
-        }
-        if (!this.multiple && this.dropdownUl) {
-          this.resetMenuScroll();
-        }
+      scrollToOption(className = 'selected') {
+        const menu = this.$refs.popper.$el.querySelector('.el-select-dropdown__wrap');
+        scrollIntoView(menu, menu.getElementsByClassName(className)[0]);
       },
 
-      getOverflows() {
-        if (this.dropdownUl && this.selected && this.selected.$el) {
-          let selectedRect = this.selected.$el.getBoundingClientRect();
-          let popperRect = this.$refs.popper.$el.getBoundingClientRect();
-          this.bottomOverflow = selectedRect.bottom - popperRect.bottom;
-          this.topOverflow = selectedRect.top - popperRect.top;
-        }
-      },
-
-      resetMenuScroll() {
-        if (this.bottomOverflow > 0) {
-          this.dropdownUl.scrollTop += this.bottomOverflow;
-        } else if (this.topOverflow < 0) {
-          this.dropdownUl.scrollTop += this.topOverflow;
-        }
+      handleMenuEnter() {
+        this.$nextTick(() => this.scrollToOption());
       },
 
       getOption(value) {
         let option;
+        const type = typeof value;
+        const isObject = type !== 'string' && type !== 'number' && type !== 'boolean';
         for (let i = this.cachedOptions.length - 1; i >= 0; i--) {
           const cachedOption = this.cachedOptions[i];
-          if (cachedOption.value === value) {
+          const isEqual = isObject
+            ? getValueByPath(cachedOption.value, this.valueKey) === getValueByPath(value, this.valueKey)
+            : cachedOption.value === value;
+          if (isEqual) {
             option = cachedOption;
             break;
           }
         }
         if (option) return option;
-        const label = typeof value === 'string' || typeof value === 'number'
+        const label = !isObject
           ? value : '';
         let newOption = {
           value: value,
@@ -525,7 +515,7 @@
       handleOptionSelect(option) {
         if (this.multiple) {
           const value = this.value.slice();
-          const optionIndex = value.indexOf(option.value);
+          const optionIndex = this.getValueIndex(value, option.value);
           if (optionIndex > -1) {
             value.splice(optionIndex, 1);
           } else if (this.multipleLimit <= 0 || value.length < this.multipleLimit) {
@@ -541,6 +531,26 @@
           this.$emit('input', option.value);
           this.visible = false;
         }
+        this.$nextTick(() => this.scrollToOption());
+      },
+
+      getValueIndex(arr = [], value) {
+        const type = typeof value;
+        const isObject = type !== 'string' && type !== 'number' && type !== 'boolean';
+        if (!isObject) {
+          return arr.indexOf(value);
+        } else {
+          const valueKey = this.valueKey;
+          let index = -1;
+          arr.some((item, i) => {
+            if (getValueByPath(item, valueKey) === getValueByPath(value, valueKey)) {
+              index = i;
+              return true;
+            }
+            return false;
+          });
+          return index;
+        }
       },
 
       toggleMenu() {
@@ -565,7 +575,6 @@
             if (this.hoverIndex === this.options.length) {
               this.hoverIndex = 0;
             }
-            this.resetScrollTop();
             if (this.options[this.hoverIndex].disabled === true ||
               this.options[this.hoverIndex].groupDisabled === true ||
               !this.options[this.hoverIndex].visible) {
@@ -577,7 +586,6 @@
             if (this.hoverIndex < 0) {
               this.hoverIndex = this.options.length - 1;
             }
-            this.resetScrollTop();
             if (this.options[this.hoverIndex].disabled === true ||
               this.options[this.hoverIndex].groupDisabled === true ||
               !this.options[this.hoverIndex].visible) {
@@ -585,19 +593,7 @@
             }
           }
         }
-      },
-
-      resetScrollTop() {
-        let bottomOverflowDistance = this.options[this.hoverIndex].$el.getBoundingClientRect().bottom -
-          this.$refs.popper.$el.getBoundingClientRect().bottom;
-        let topOverflowDistance = this.options[this.hoverIndex].$el.getBoundingClientRect().top -
-          this.$refs.popper.$el.getBoundingClientRect().top;
-        if (bottomOverflowDistance > 0) {
-          this.dropdownUl.scrollTop += bottomOverflowDistance;
-        }
-        if (topOverflowDistance < 0) {
-          this.dropdownUl.scrollTop += topOverflowDistance;
-        }
+        this.$nextTick(() => this.scrollToOption('hover'));
       },
 
       selectOption() {
@@ -667,6 +663,15 @@
             }
           }
         }
+      },
+
+      getValueKey(item) {
+        const type = typeof item.value;
+        if (type === 'number' || type === 'string') {
+          return item.value;
+        } else {
+          return getValueByPath(item.value, this.valueKey);
+        }
       }
     },
 

+ 1 - 1
packages/table/src/table-column.js

@@ -1,7 +1,7 @@
 import ElCheckbox from 'element-ui/packages/checkbox';
 import ElTag from 'element-ui/packages/tag';
 import objectAssign from 'element-ui/src/utils/merge';
-import { getValueByPath } from './util';
+import { getValueByPath } from 'element-ui/src/utils/util';
 
 let columnIdSeed = 1;
 

+ 1 - 1
packages/table/src/table.vue

@@ -312,7 +312,7 @@
       },
 
       selection() {
-        return this.store.selection;
+        return this.store.states.selection;
       },
 
       columns() {

+ 2 - 18
packages/table/src/util.js

@@ -1,3 +1,5 @@
+import { getValueByPath } from 'element-ui/src/utils/util';
+
 export const getCell = function(event) {
   let cell = event.target;
 
@@ -11,24 +13,6 @@ export const getCell = function(event) {
   return null;
 };
 
-export const getValueByPath = function(object, prop) {
-  prop = prop || '';
-  const paths = prop.split('.');
-  let current = object;
-  let result = null;
-  for (let i = 0, j = paths.length; i < j; i++) {
-    const path = paths[i];
-    if (!current) break;
-
-    if (i === j - 1) {
-      result = current[path];
-      break;
-    }
-    current = current[path];
-  }
-  return result;
-};
-
 const isObject = function(obj) {
   return obj !== null && typeof obj === 'object';
 };

+ 1 - 1
packages/theme-default/package.json

@@ -1,6 +1,6 @@
 {
   "name": "element-theme-default",
-  "version": "1.3.7",
+  "version": "1.4.0-beta.1",
   "description": "Element component default theme.",
   "main": "lib/index.css",
   "style": "lib/index.css",

+ 20 - 0
packages/theme-default/src/common/transition.css

@@ -68,9 +68,25 @@
   transform: scaleY(0);
 }
 
+.el-zoom-in-left-enter-active,
+.el-zoom-in-left-leave-active {
+  opacity: 1;
+  transform: scale(1, 1);
+  transition: var(--md-fade-transition);
+  transform-origin: top left;
+}
+.el-zoom-in-left-enter,
+.el-zoom-in-left-leave-active {
+  opacity: 0;
+  transform: scale(.45, .45);
+}
+
 .collapse-transition {
   transition: 0.3s height ease-in-out, 0.3s padding-top ease-in-out, 0.3s padding-bottom ease-in-out;
 }
+.horizontal-collapse-transition {
+  transition: 0.3s width ease-in-out, 0.3s padding-left ease-in-out, 0.3s padding-right ease-in-out;
+}
 
 .el-list-enter-active,
 .el-list-leave-active {
@@ -79,4 +95,8 @@
 .el-list-enter, .el-list-leave-active {
   opacity: 0;
   transform: translateY(-30px);
+}
+
+.el-opacity-transition {
+  transition: opacity .3s cubic-bezier(.55,0,.1,1);
 }

+ 41 - 1
packages/theme-default/src/menu.css

@@ -23,7 +23,7 @@
     padding-left: 0;
     background-color: var(--menu-item-fill);
     @utils-clearfix;
-    
+
     & li {
       list-style: none;
     }
@@ -137,6 +137,45 @@
         }
       }
     }
+    @m collapse {
+      width: 64px;
+
+      > .el-menu-item,
+      > .el-submenu > .el-submenu__title {
+        text-align: center;
+        [class^="el-icon-"] {
+          margin: 0;
+          vertical-align: middle;
+        }
+        .el-submenu__icon-arrow {
+          display: none;
+        }
+        span {
+          height: 0;
+          width: 0;
+          overflow: hidden;
+          visibility: hidden;
+          display: inline-block;
+        }
+      }
+
+      .el-submenu {
+        position: relative;
+        & .el-menu {
+          position: absolute;
+          margin-left: 5px;
+          top: 0;
+          left: 100%;
+          z-index: 10;
+        }
+
+        &.is-opened {
+          > .el-submenu__title .el-submenu__icon-arrow {
+            transform: none;
+          }
+        }
+      }
+    }
   }
   @b menu-item {
     @extend menu-item;
@@ -175,6 +214,7 @@
       height: 50px;
       line-height: 50px;
       padding: 0 45px;
+      min-width: 200px;
 
       &:hover {
         background-color: var(--color-base-gray);

+ 3 - 0
packages/tooltip/src/main.js

@@ -80,9 +80,12 @@ export default {
     if (!vnode) return vnode;
     const data = vnode.data = vnode.data || {};
     const on = vnode.data.on = vnode.data.on || {};
+    const nativeOn = vnode.data.nativeOn = vnode.data.nativeOn || {};
 
     on.mouseenter = this.addEventHandle(on.mouseenter, () => { this.setExpectedState(true); this.handleShowPopper(); });
     on.mouseleave = this.addEventHandle(on.mouseleave, () => { this.setExpectedState(false); this.debounceClose(); });
+    nativeOn.mouseenter = this.addEventHandle(nativeOn.mouseenter, () => { this.setExpectedState(true); this.handleShowPopper(); });
+    nativeOn.mouseleave = this.addEventHandle(nativeOn.mouseleave, () => { this.setExpectedState(false); this.debounceClose(); });
     data.staticClass = this.concatClass(data.staticClass, 'el-tooltip');
 
     return vnode;

+ 49 - 16
packages/tree/src/model/node.js

@@ -1,28 +1,44 @@
 import objectAssign from 'element-ui/src/utils/merge';
 import { markNodeData, NODE_KEY } from './util';
 
-const reInitChecked = function(node) {
-  const siblings = node.childNodes;
-
+export const getChildState = node => {
   let all = true;
   let none = true;
-
-  for (let i = 0, j = siblings.length; i < j; i++) {
-    const sibling = siblings[i];
-    if (sibling.checked !== true || sibling.indeterminate) {
+  let allWithoutDisable = true;
+  for (let i = 0, j = node.length; i < j; i++) {
+    const n = node[i];
+    if (n.checked !== true || n.indeterminate) {
       all = false;
+      if (!n.disabled) {
+        allWithoutDisable = false;
+      }
     }
-    if (sibling.checked !== false || sibling.indeterminate) {
+    if (n.checked !== false || n.indeterminate) {
       none = false;
     }
   }
 
+  return { all, none, allWithoutDisable, half: !all && !none };
+};
+
+const reInitChecked = function(node) {
+  const {all, none, half} = getChildState(node.childNodes);
   if (all) {
-    node.setChecked(true);
-  } else if (!all && !none) {
-    node.setChecked('half');
+    node.checked = true;
+    node.indeterminate = false;
+  } else if (half) {
+    node.checked = false;
+    node.indeterminate = true;
   } else if (none) {
-    node.setChecked(false);
+    node.checked = false;
+    node.indeterminate = false;
+  }
+
+  const parent = node.parent;
+  if (!parent || parent.level === 0) return;
+
+  if (!node.store.checkStrictly) {
+    reInitChecked(parent);
   }
 };
 
@@ -145,6 +161,10 @@ export default class Node {
     return null;
   }
 
+  get disabled() {
+    return getPropertyFromData(this, 'disabled');
+  }
+
   insertChild(child, index) {
     if (!child) throw new Error('insertChild error: child is required.');
 
@@ -260,16 +280,29 @@ export default class Node {
     this.isLeaf = false;
   }
 
-  setChecked(value, deep) {
+  setChecked(value, deep, recursion, passValue) {
     this.indeterminate = value === 'half';
     this.checked = value === true;
+    let { allWithoutDisable } = getChildState(this.childNodes);
+
+    if (this.childNodes.length && allWithoutDisable) {
+      this.checked = false;
+      value = false;
+    }
 
     const handleDescendants = () => {
       if (deep) {
         const childNodes = this.childNodes;
         for (let i = 0, j = childNodes.length; i < j; i++) {
           const child = childNodes[i];
-          child.setChecked(value !== false, deep);
+          passValue = passValue || value !== false;
+          const isCheck = child.disabled ? child.checked : passValue;
+          child.setChecked(isCheck, deep, true, passValue);
+        }
+        const { half, all } = getChildState(childNodes);
+        if (!all) {
+          this.checked = all;
+          this.indeterminate = half;
         }
       }
     };
@@ -288,7 +321,7 @@ export default class Node {
     const parent = this.parent;
     if (!parent || parent.level === 0) return;
 
-    if (!this.store.checkStrictly) {
+    if (!this.store.checkStrictly && !recursion) {
       reInitChecked(parent);
     }
   }
@@ -337,7 +370,7 @@ export default class Node {
   }
 
   loadData(callback, defaultProps = {}) {
-    if (this.store.lazy === true && this.store.load && !this.loaded && !this.loading) {
+    if (this.store.lazy === true && this.store.load && !this.loaded && (!this.loading || Object.keys(defaultProps).length)) {
       this.loading = true;
 
       const resolve = (children) => {

+ 35 - 49
packages/tree/src/model/tree-store.js

@@ -188,61 +188,47 @@ export default class TreeStore {
   }
 
   _setCheckedKeys(key, leafOnly = false, checkedKeys) {
-    const allNodes = this._getAllNodes();
-    allNodes.sort((a, b) => b.level - a.level);
-
+    const allNodes = this._getAllNodes().sort((a, b) => b.level - a.level);
+    const cache = Object.create(null);
     const keys = Object.keys(checkedKeys);
-    allNodes.forEach((node) => {
-      let checked = keys.indexOf(node.data[key] + '') > -1;
+    allNodes.forEach(node => node.setChecked(false, false));
+    for (let i = 0, j = allNodes.length; i < j; i++) {
+      const node = allNodes[i];
+      const nodeKey = node.data[key].toString();
+      let checked = keys.indexOf(nodeKey) > -1;
+      if (!checked) {
+        if (node.checked && !cache[nodeKey]) {
+          node.setChecked(false, false);
+        }
+        continue;
+      }
 
-      if (!node.isLeaf) {
-        if (!this.checkStrictly) {
-          const childNodes = node.childNodes;
+      let parent = node.parent;
+      while (parent && parent.level > 0) {
+        cache[parent.data[key]] = true;
+        parent = parent.parent;
+      }
 
-          let all = true;
-          let none = true;
+      if (node.isLeaf || this.checkStrictly) {
+        node.setChecked(true, false);
+        continue;
+      }
+      node.setChecked(true, true);
 
-          for (let i = 0, j = childNodes.length; i < j; i++) {
-            const child = childNodes[i];
-            if (child.checked !== true || child.indeterminate) {
-              all = false;
-            }
-            if (child.checked !== false || child.indeterminate) {
-              none = false;
+      if (leafOnly) {
+        node.setChecked(false, false);
+        const traverse = function(node) {
+          const childNodes = node.childNodes;
+          childNodes.forEach((child) => {
+            if (!child.isLeaf) {
+              child.setChecked(false, false);
             }
-          }
-
-          if (all) {
-            node.setChecked(true, !this.checkStrictly);
-          } else if (!all && !none) {
-            checked = checked ? true : 'half';
-            node.setChecked(checked, !this.checkStrictly && checked === true);
-          } else if (none) {
-            node.setChecked(checked, !this.checkStrictly);
-          }
-        } else {
-          node.setChecked(checked, false);
-        }
-
-        if (leafOnly) {
-          node.setChecked(false, false);
-          const traverse = function(node) {
-            const childNodes = node.childNodes;
-
-            childNodes.forEach((child) => {
-              if (!child.isLeaf) {
-                child.setChecked(false, false);
-              }
-              traverse(child);
-            });
-          };
-
-          traverse(node);
-        }
-      } else {
-        node.setChecked(checked, false);
+            traverse(child);
+          });
+        };
+        traverse(node);
       }
-    });
+    }
   }
 
   setCheckedNodes(array, leafOnly = false) {

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

@@ -18,8 +18,8 @@
         v-if="showCheckbox"
         v-model="node.checked"
         :indeterminate="node.indeterminate"
-        @change="handleCheckChange"
-        @click.native.stop="handleUserClick">
+        :disabled="!!node.disabled"
+        @change="handleCheckChange">
       </el-checkbox>
       <span
         v-if="node.loading"
@@ -155,16 +155,8 @@
         }
       },
 
-      handleUserClick() {
-        if (this.node.indeterminate) {
-          this.node.setChecked(this.node.checked, !this.tree.checkStrictly);
-        }
-      },
-
       handleCheckChange(ev) {
-        if (!this.node.indeterminate) {
-          this.node.setChecked(ev.target.checked, !this.tree.checkStrictly);
-        }
+        this.node.setChecked(ev.target.checked, !this.tree.checkStrictly);
       },
 
       handleChildNodeExpand(nodeData, node, instance) {

+ 2 - 1
packages/tree/src/tree.vue

@@ -69,7 +69,8 @@
           return {
             children: 'children',
             label: 'label',
-            icon: 'icon'
+            icon: 'icon',
+            disabled: 'disabled'
           };
         }
       },

+ 1 - 1
src/index.js

@@ -158,7 +158,7 @@ if (typeof window !== 'undefined' && window.Vue) {
 };
 
 module.exports = {
-  version: '1.3.7',
+  version: '1.4.0-beta.1',
   locale: locale.use,
   i18n: locale.i18n,
   install,

+ 21 - 0
src/utils/scroll-into-view.js

@@ -0,0 +1,21 @@
+import Vue from 'vue';
+
+export default function scrollIntoView(container, selected) {
+  if (Vue.prototype.$isServer) return;
+
+  if (!selected) {
+    container.scrollTop = 0;
+    return;
+  }
+
+  const top = selected.offsetTop;
+  const bottom = selected.offsetTop + selected.offsetHeight;
+  const viewRectTop = container.scrollTop;
+  const viewRectBottom = viewRectTop + container.clientHeight;
+
+  if (top < viewRectTop) {
+    container.scrollTop = top;
+  } else if (bottom > viewRectBottom) {
+    container.scrollTop = bottom - container.clientHeight;
+  }
+}

+ 18 - 0
src/utils/util.js

@@ -19,3 +19,21 @@ export function toObject(arr) {
   }
   return res;
 };
+
+export const getValueByPath = function(object, prop) {
+  prop = prop || '';
+  const paths = prop.split('.');
+  let current = object;
+  let result = null;
+  for (let i = 0, j = paths.length; i < j; i++) {
+    const path = paths[i];
+    if (!current) break;
+
+    if (i === j - 1) {
+      result = current[path];
+      break;
+    }
+    current = current[path];
+  }
+  return result;
+};

+ 46 - 0
test/unit/specs/select.spec.js

@@ -113,6 +113,7 @@ describe('Select', () => {
             <el-option
               v-for="item in options"
               :label="item.label"
+              :key="item.value"
               :value="item.value">
             </el-option>
           </el-select>
@@ -147,6 +148,7 @@ describe('Select', () => {
             <el-option
               v-for="item in options"
               :label="item.label"
+              :key="item.value"
               :value="item.value">
               <p>{{item.label}} {{item.value}}</p>
             </el-option>
@@ -226,6 +228,7 @@ describe('Select', () => {
             <el-option
               v-for="item in options"
               :label="item.label"
+              :key="item.value"
               :value="item.value">
             </el-option>
           </el-select>
@@ -285,6 +288,44 @@ describe('Select', () => {
     }, 100);
   });
 
+  it('object typed value', done => {
+    vm = createVue({
+      template: `
+        <div>
+          <el-select v-model="value" value-key="id">
+            <el-option
+              v-for="item in options"
+              :label="item.label"
+              :key="item.id"
+              :value="item">
+            </el-option>
+          </el-select>
+        </div>
+      `,
+
+      data() {
+        return {
+          options: [{
+            id: 1,
+            label: 'label1'
+          }, {
+            id: 2,
+            label: 'label2'
+          }],
+          value: {
+            id: 1,
+            label: 'label1'
+          }
+        };
+      }
+    }, true);
+    setTimeout(() => {
+      expect(vm.$el.querySelector('.el-input__inner').value).to.equal('label1');
+      expect(vm.$el.querySelector('.el-select-dropdown__item').classList.contains('selected'));
+      done();
+    }, 100);
+  });
+
   it('custom el-option template', () => {
     vm = createVue({
       template: `
@@ -293,6 +334,7 @@ describe('Select', () => {
             <el-option
               v-for="item in options"
               :label="item.label"
+              :key="item.value"
               :value="item.value">
               <p>{{item.label}} {{item.value}}</p>
             </el-option>
@@ -320,11 +362,13 @@ describe('Select', () => {
           <el-select v-model="value">
             <el-option-group
               v-for="group in options"
+              :key="group.label"
               :disabled="group.disabled"
               :label="group.label">
               <el-option
                 v-for="item in group.options"
                 :label="item.label"
+                :key="item.value"
                 :value="item.value">
               </el-option>
             </el-option-group>
@@ -416,6 +460,7 @@ describe('Select', () => {
             <el-option
               v-for="item in options"
               :label="item"
+              :key="item.value"
               :value="item"
             />
           </el-select>
@@ -504,6 +549,7 @@ describe('Select', () => {
             <el-option
               v-for="item in options"
               :label="item.label"
+              :key="item.value"
               :value="item.value">
               <p>{{item.label}} {{item.value}}</p>
             </el-option>

+ 65 - 0
test/unit/specs/tree.spec.js

@@ -61,6 +61,61 @@ describe('Tree', () => {
     }, options), true);
   };
 
+  const getDisableTreeVm = (props, options) => {
+    return createVue(Object.assign({
+      template: `
+        <el-tree ref="tree" :data="data" ${ props }></el-tree>
+        `,
+
+      data() {
+        return {
+          defaultExpandedKeys: [],
+          defaultCheckedKeys: [],
+          clickedNode: null,
+          count: 1,
+          data: [{
+            id: 1,
+            label: '一级 1',
+            children: [{
+              id: 11,
+              label: '二级 1-1',
+              children: [{
+                id: 111,
+                label: '三级 1-1',
+                disabled: true
+              }]
+            }]
+          }, {
+            id: 2,
+            label: '一级 2',
+            children: [{
+              id: 21,
+              label: '二级 2-1'
+            }, {
+              id: 22,
+              label: '二级 2-2'
+            }]
+          }, {
+            id: 3,
+            label: '一级 3',
+            children: [{
+              id: 31,
+              label: '二级 3-1'
+            }, {
+              id: 32,
+              label: '二级 3-2'
+            }]
+          }],
+          defaultProps: {
+            children: 'children',
+            label: 'label',
+            disabled: 'disabled'
+          }
+        };
+      }
+    }, options), true);
+  };
+
   const ALL_NODE_COUNT = 9;
 
   it('create', () => {
@@ -344,6 +399,16 @@ describe('Tree', () => {
     }, 0);
   });
 
+  it('set disabled checkbox', done => {
+    vm = getDisableTreeVm(':props="defaultProps" show-checkbox node-key="id"');
+    const node = document.querySelectorAll('.el-tree-node__content')[2];
+    const nodeCheckbox = node.querySelector('.el-checkbox input');
+    vm.$nextTick(() => {
+      expect(nodeCheckbox.disabled).to.equal(true);
+      done();
+    });
+  });
+
   it('check strictly', (done) => {
     vm = getTreeVm(':props="defaultProps" show-checkbox check-strictly');
     const tree = vm.$children[0];