Menu: add collapse (#5941)

* feature menu collapse

* Update menu.md
pull/5899/merge
baiyaaaaa 2017-07-20 12:44:52 +08:00 committed by 杨奕
parent daa4f83e4f
commit c73eeed291
9 changed files with 327 additions and 56 deletions

View File

@ -3,7 +3,7 @@
.el-menu-demo { .el-menu-demo {
padding-left: 55px; padding-left: 55px;
} }
.el-menu-vertical-demo { .el-menu-vertical-demo:not(.el-menu--collapse) {
width: 200px; width: 200px;
min-height: 400px; min-height: 400px;
} }
@ -33,7 +33,8 @@
data() { data() {
return { return {
activeIndex: '1', activeIndex: '1',
activeIndex2: '1' activeIndex2: '1',
isCollapse: false
}; };
}, },
methods: { 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 ### Menu Attribute
| Attribute | Description | Type | Accepted Values | Default | | Attribute | Description | Type | Accepted Values | Default |
|---------- |-------- |---------- |------------- |-------- | |---------- |-------- |---------- |------------- |-------- |
| mode | menu display mode | string | horizontal/vertical | vertical | | 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 | | theme | theme color | string | light/dark | light |
| default-active | index of currently active menu | string | — | — | | default-active | index of currently active menu | string | — | — |
| default-openeds | array that contains keys of currently active sub-menus | Array | — | — | | default-openeds | array that contains keys of currently active sub-menus | Array | — | — |

View File

@ -3,7 +3,7 @@
.el-menu-demo { .el-menu-demo {
padding-left: 55px; padding-left: 55px;
} }
.el-menu-vertical-demo { .el-menu-vertical-demo:not(.el-menu--collapse) {
width: 200px; width: 200px;
min-height: 400px; min-height: 400px;
} }
@ -33,7 +33,8 @@
data() { data() {
return { return {
activeIndex: '1', activeIndex: '1',
activeIndex2: '1' activeIndex2: '1',
isCollapse: true
}; };
}, },
methods: { 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 ### Menu Attribute
| 参数 | 说明 | 类型 | 可选值 | 默认值 | | 参数 | 说明 | 类型 | 可选值 | 默认值 |
|---------- |-------- |---------- |------------- |-------- | |---------- |-------- |---------- |------------- |-------- |
| mode | 模式 | string | horizontal,vertical | vertical | | mode | 模式 | string | horizontal,vertical | vertical |
| collapse | 是否水平折叠收起菜单(仅在 mode 为 vertical 时可用)| boolean | — | false |
| theme | 主题色 | string | light,dark | light | | theme | 主题色 | string | light,dark | light |
| default-active | 当前激活菜单的 index | string | — | — | | default-active | 当前激活菜单的 index | string | — | — |
| default-openeds | 当前打开的submenu的 key 数组 | Array | — | — | | default-openeds | 当前打开的submenu的 key 数组 | Array | — | — |

View File

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

View File

@ -6,7 +6,19 @@
'is-active': active, 'is-active': active,
'is-disabled': disabled 'is-disabled': disabled
}"> }">
<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> <slot></slot>
</div>
</el-tooltip>
<template v-else>
<slot></slot>
<slot name="title"></slot>
</template>
</li> </li>
</template> </template>
<script> <script>

View File

@ -1,4 +1,5 @@
export default { export default {
inject: ['rootMenu'],
computed: { computed: {
indexPath() { indexPath() {
var path = [this.index]; var path = [this.index];
@ -11,16 +12,6 @@ export default {
} }
return path; return path;
}, },
rootMenu() {
var parent = this.$parent;
while (
parent &&
parent.$options.componentName !== 'ElMenu'
) {
parent = parent.$parent;
}
return parent;
},
parentMenu() { parentMenu() {
let parent = this.$parent; let parent = this.$parent;
while ( while (
@ -36,12 +27,17 @@ export default {
let padding = 20; let padding = 20;
let parent = this.$parent; let parent = this.$parent;
if (this.rootMenu.collapse) {
padding = 20;
} else {
while (parent && parent.$options.componentName !== 'ElMenu') { while (parent && parent.$options.componentName !== 'ElMenu') {
if (parent.$options.componentName === 'ElSubmenu') { if (parent.$options.componentName === 'ElSubmenu') {
padding += 20; padding += 20;
} }
parent = parent.$parent; parent = parent.$parent;
} }
}
return {paddingLeft: padding + 'px'}; return {paddingLeft: padding + 'px'};
} }
} }

View File

@ -1,15 +1,82 @@
<template> <template>
<el-menu-collapse-transition>
<ul class="el-menu" <ul class="el-menu"
:key="collapse"
:class="{ :class="{
'el-menu--horizontal': mode === 'horizontal', 'el-menu--horizontal': mode === 'horizontal',
'el-menu--dark': theme === 'dark' 'el-menu--dark': theme === 'dark',
'el-menu--collapse': collapse
}" }"
> >
<slot></slot> <slot></slot>
</ul> </ul>
</el-menu-collapse-transition>
</template> </template>
<script> <script>
import Vue from 'vue';
import emitter from 'element-ui/src/mixins/emitter'; 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 { export default {
name: 'ElMenu', name: 'ElMenu',
@ -18,6 +85,12 @@
mixins: [emitter], mixins: [emitter],
provide() {
return {
rootMenu: this
};
},
props: { props: {
mode: { mode: {
type: String, type: String,
@ -37,7 +110,8 @@
menuTrigger: { menuTrigger: {
type: String, type: String,
default: 'hover' default: 'hover'
} },
collapse: Boolean
}, },
data() { data() {
return { return {
@ -106,7 +180,7 @@
this.activedIndex = item.index; this.activedIndex = item.index;
this.$emit('select', index, indexPath, item); this.$emit('select', index, indexPath, item);
if (this.mode === 'horizontal') { if (this.mode === 'horizontal' || this.collapse) {
this.openedMenus = []; this.openedMenus = [];
} }

View File

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

View File

@ -68,9 +68,25 @@
transform: scaleY(0); 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 { .collapse-transition {
transition: 0.3s height ease-in-out, 0.3s padding-top ease-in-out, 0.3s padding-bottom ease-in-out; 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-enter-active,
.el-list-leave-active { .el-list-leave-active {
@ -80,3 +96,7 @@
opacity: 0; opacity: 0;
transform: translateY(-30px); transform: translateY(-30px);
} }
.el-opacity-transition {
transition: opacity .3s cubic-bezier(.55,0,.1,1);
}

View File

@ -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 { @b menu-item {
@extend menu-item; @extend menu-item;
@ -175,6 +214,7 @@
height: 50px; height: 50px;
line-height: 50px; line-height: 50px;
padding: 0 45px; padding: 0 45px;
min-width: 200px;
&:hover { &:hover {
background-color: var(--color-base-gray); background-color: var(--color-base-gray);