Browse Source

Menu: add collapse (#5941)

* feature menu collapse

* Update menu.md
baiyaaaaa 8 years ago
parent
commit
c73eeed291

+ 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    | — | — |

+ 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    | — | — |

+ 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>

+ 10 - 14
packages/menu/src/menu-mixin.js

@@ -1,4 +1,5 @@
 export default {
+  inject: ['rootMenu'],
   computed: {
     indexPath() {
       var path = [this.index];
@@ -11,16 +12,6 @@ export default {
       }
       return path;
     },
-    rootMenu() {
-      var parent = this.$parent;
-      while (
-        parent &&
-        parent.$options.componentName !== 'ElMenu'
-      ) {
-        parent = parent.$parent;
-      }
-      return parent;
-    },
     parentMenu() {
       let parent = this.$parent;
       while (
@@ -36,11 +27,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>

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