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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 = [];
}

View File

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

View File

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

View File

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