This commit is contained in:
Ivan
2021-06-07 11:56:04 +08:00
commit c3c9fee2fb
1071 changed files with 195655 additions and 0 deletions

8
packages/menu/index.js Normal file
View File

@@ -0,0 +1,8 @@
import ElMenu from './src/menu';
/* istanbul ignore next */
ElMenu.install = function(Vue) {
Vue.component(ElMenu.name, ElMenu);
};
export default ElMenu;

View File

@@ -0,0 +1,45 @@
<template>
<li class="el-menu-item-group">
<div class="el-menu-item-group__title" :style="{paddingLeft: levelPadding + 'px'}">
<template v-if="!$slots.title">{{title}}</template>
<slot v-else name="title"></slot>
</div>
<ul>
<slot></slot>
</ul>
</li>
</template>
<script>
export default {
name: 'ElMenuItemGroup',
componentName: 'ElMenuItemGroup',
inject: ['rootMenu'],
props: {
title: {
type: String
}
},
data() {
return {
paddingLeft: 20
};
},
computed: {
levelPadding() {
let padding = 20;
let parent = this.$parent;
if (this.rootMenu.collapse) return 20;
while (parent && parent.$options.componentName !== 'ElMenu') {
if (parent.$options.componentName === 'ElSubmenu') {
padding += 20;
}
parent = parent.$parent;
}
return padding;
}
}
};
</script>

View File

@@ -0,0 +1,112 @@
<template>
<li class="el-menu-item"
role="menuitem"
tabindex="-1"
:style="[paddingStyle, itemStyle, { backgroundColor }]"
:class="{
'is-active': active,
'is-disabled': disabled
}"
@click="handleClick"
@mouseenter="onMouseEnter"
@focus="onMouseEnter"
@blur="onMouseLeave"
@mouseleave="onMouseLeave"
>
<el-tooltip
v-if="parentMenu.$options.componentName === 'ElMenu' && rootMenu.collapse && $slots.title"
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>
import Menu from './menu-mixin';
import ElTooltip from 'element-ui/packages/tooltip';
import Emitter from 'element-ui/src/mixins/emitter';
export default {
name: 'ElMenuItem',
componentName: 'ElMenuItem',
mixins: [Menu, Emitter],
components: { ElTooltip },
props: {
index: {
default: null,
validator: val => typeof val === 'string' || val === null
},
route: [String, Object],
disabled: Boolean
},
computed: {
active() {
return this.index === this.rootMenu.activeIndex;
},
hoverBackground() {
return this.rootMenu.hoverBackground;
},
backgroundColor() {
return this.rootMenu.backgroundColor || '';
},
activeTextColor() {
return this.rootMenu.activeTextColor || '';
},
textColor() {
return this.rootMenu.textColor || '';
},
mode() {
return this.rootMenu.mode;
},
itemStyle() {
const style = {
color: this.active ? this.activeTextColor : this.textColor
};
if (this.mode === 'horizontal' && !this.isNested) {
style.borderBottomColor = this.active
? (this.rootMenu.activeTextColor ? this.activeTextColor : '')
: 'transparent';
}
return style;
},
isNested() {
return this.parentMenu !== this.rootMenu;
}
},
methods: {
onMouseEnter() {
if (this.mode === 'horizontal' && !this.rootMenu.backgroundColor) return;
this.$el.style.backgroundColor = this.hoverBackground;
},
onMouseLeave() {
if (this.mode === 'horizontal' && !this.rootMenu.backgroundColor) return;
this.$el.style.backgroundColor = this.backgroundColor;
},
handleClick() {
if (!this.disabled) {
this.dispatch('ElMenu', 'item-click', this);
this.$emit('click', this);
}
}
},
mounted() {
this.parentMenu.addItem(this);
this.rootMenu.addItem(this);
},
beforeDestroy() {
this.parentMenu.removeItem(this);
this.rootMenu.removeItem(this);
}
};
</script>

View File

@@ -0,0 +1,44 @@
export default {
inject: ['rootMenu'],
computed: {
indexPath() {
const path = [this.index];
let parent = this.$parent;
while (parent.$options.componentName !== 'ElMenu') {
if (parent.index) {
path.unshift(parent.index);
}
parent = parent.$parent;
}
return path;
},
parentMenu() {
let parent = this.$parent;
while (
parent &&
['ElMenu', 'ElSubmenu'].indexOf(parent.$options.componentName) === -1
) {
parent = parent.$parent;
}
return parent;
},
paddingStyle() {
if (this.rootMenu.mode !== 'vertical') return {};
let padding = 20;
let parent = this.$parent;
if (this.rootMenu.collapse) {
padding = 20;
} else {
while (parent && parent.$options.componentName !== 'ElMenu') {
if (parent.$options.componentName === 'ElSubmenu') {
padding += 20;
}
parent = parent.$parent;
}
}
return {paddingLeft: padding + 'px'};
}
}
};

325
packages/menu/src/menu.vue Normal file
View File

@@ -0,0 +1,325 @@
<script type="text/jsx">
import emitter from 'element-ui/src/mixins/emitter';
import Migrating from 'element-ui/src/mixins/migrating';
import Menubar from 'element-ui/src/utils/menu/aria-menubar';
import { addClass, removeClass, hasClass } from 'element-ui/src/utils/dom';
export default {
name: 'ElMenu',
render (h) {
const component = (
<ul
role="menubar"
key={ +this.collapse }
style={{ backgroundColor: this.backgroundColor || '' }}
class={{
'el-menu--horizontal': this.mode === 'horizontal',
'el-menu--collapse': this.collapse,
"el-menu": true
}}
>
{ this.$slots.default }
</ul>
);
if (this.collapseTransition) {
return (
<el-menu-collapse-transition>
{ component }
</el-menu-collapse-transition>
);
} else {
return component;
}
},
componentName: 'ElMenu',
mixins: [emitter, Migrating],
provide() {
return {
rootMenu: this
};
},
components: {
'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.clientWidth;
addClass(el, 'el-menu--collapse');
} else {
addClass(el, 'el-menu--collapse');
el.dataset.oldOverflow = el.style.overflow;
el.dataset.scrollWidth = el.clientWidth;
removeClass(el, 'el-menu--collapse');
}
el.style.width = el.scrollWidth + 'px';
el.style.overflow = 'hidden';
},
leave(el) {
addClass(el, 'horizontal-collapse-transition');
el.style.width = el.dataset.scrollWidth + 'px';
}
}
};
return createElement('transition', data, context.children);
}
}
},
props: {
mode: {
type: String,
default: 'vertical'
},
defaultActive: {
type: String,
default: ''
},
defaultOpeneds: Array,
uniqueOpened: Boolean,
router: Boolean,
menuTrigger: {
type: String,
default: 'hover'
},
collapse: Boolean,
backgroundColor: String,
textColor: String,
activeTextColor: String,
collapseTransition: {
type: Boolean,
default: true
}
},
data() {
return {
activeIndex: this.defaultActive,
openedMenus: (this.defaultOpeneds && !this.collapse) ? this.defaultOpeneds.slice(0) : [],
items: {},
submenus: {}
};
},
computed: {
hoverBackground() {
return this.backgroundColor ? this.mixColor(this.backgroundColor, 0.2) : '';
},
isMenuPopup() {
return this.mode === 'horizontal' || (this.mode === 'vertical' && this.collapse);
}
},
watch: {
defaultActive(value){
if(!this.items[value]){
this.activeIndex = null
}
this.updateActiveIndex(value)
},
defaultOpeneds(value) {
if (!this.collapse) {
this.openedMenus = value;
}
},
collapse(value) {
if (value) this.openedMenus = [];
this.broadcast('ElSubmenu', 'toggle-collapse', value);
}
},
methods: {
updateActiveIndex(val) {
const item = this.items[val] || this.items[this.activeIndex] || this.items[this.defaultActive];
if (item) {
this.activeIndex = item.index;
this.initOpenedMenu();
} else {
this.activeIndex = null;
}
},
getMigratingConfig() {
return {
props: {
'theme': 'theme is removed.'
}
};
},
getColorChannels(color) {
color = color.replace('#', '');
if (/^[0-9a-fA-F]{3}$/.test(color)) {
color = color.split('');
for (let i = 2; i >= 0; i--) {
color.splice(i, 0, color[i]);
}
color = color.join('');
}
if (/^[0-9a-fA-F]{6}$/.test(color)) {
return {
red: parseInt(color.slice(0, 2), 16),
green: parseInt(color.slice(2, 4), 16),
blue: parseInt(color.slice(4, 6), 16)
};
} else {
return {
red: 255,
green: 255,
blue: 255
};
}
},
mixColor(color, percent) {
let { red, green, blue } = this.getColorChannels(color);
if (percent > 0) { // shade given color
red *= 1 - percent;
green *= 1 - percent;
blue *= 1 - percent;
} else { // tint given color
red += (255 - red) * percent;
green += (255 - green) * percent;
blue += (255 - blue) * percent;
}
return `rgb(${ Math.round(red) }, ${ Math.round(green) }, ${ Math.round(blue) })`;
},
addItem(item) {
this.$set(this.items, item.index, item);
},
removeItem(item) {
delete this.items[item.index];
},
addSubmenu(item) {
this.$set(this.submenus, item.index, item);
},
removeSubmenu(item) {
delete this.submenus[item.index];
},
openMenu(index, indexPath) {
let openedMenus = this.openedMenus;
if (openedMenus.indexOf(index) !== -1) return;
// 将不在该菜单路径下的其余菜单收起
// collapse all menu that are not under current menu item
if (this.uniqueOpened) {
this.openedMenus = openedMenus.filter(index => {
return indexPath.indexOf(index) !== -1;
});
}
this.openedMenus.push(index);
},
closeMenu(index) {
const i = this.openedMenus.indexOf(index);
if (i !== -1) {
this.openedMenus.splice(i, 1);
}
},
handleSubmenuClick(submenu) {
const { index, indexPath } = submenu;
let isOpened = this.openedMenus.indexOf(index) !== -1;
if (isOpened) {
this.closeMenu(index);
this.$emit('close', index, indexPath);
} else {
this.openMenu(index, indexPath);
this.$emit('open', index, indexPath);
}
},
handleItemClick(item) {
const { index, indexPath } = item;
const oldActiveIndex = this.activeIndex;
const hasIndex = item.index !== null;
if (hasIndex) {
this.activeIndex = item.index;
}
this.$emit('select', index, indexPath, item);
if (this.mode === 'horizontal' || this.collapse) {
this.openedMenus = [];
}
if (this.router && hasIndex) {
this.routeToItem(item, (error) => {
this.activeIndex = oldActiveIndex;
if (error) {
// vue-router 3.1.0+ push/replace cause NavigationDuplicated error
// https://github.com/ElemeFE/element/issues/17044
if (error.name === 'NavigationDuplicated') return
console.error(error)
}
});
}
},
// 初始化展开菜单
// initialize opened menu
initOpenedMenu() {
const index = this.activeIndex;
const activeItem = this.items[index];
if (!activeItem || this.mode === 'horizontal' || this.collapse) return;
let indexPath = activeItem.indexPath;
// 展开该菜单项的路径上所有子菜单
// expand all submenus of the menu item
indexPath.forEach(index => {
let submenu = this.submenus[index];
submenu && this.openMenu(index, submenu.indexPath);
});
},
routeToItem(item, onError) {
let route = item.route || item.index;
try {
this.$router.push(route, () => {}, onError);
} catch (e) {
console.error(e);
}
},
open(index) {
const { indexPath } = this.submenus[index.toString()];
indexPath.forEach(i => this.openMenu(i, indexPath));
},
close(index) {
this.closeMenu(index);
}
},
mounted() {
this.initOpenedMenu();
this.$on('item-click', this.handleItemClick);
this.$on('submenu-click', this.handleSubmenuClick);
if (this.mode === 'horizontal') {
new Menubar(this.$el); // eslint-disable-line
}
this.$watch('items', this.updateActiveIndex);
}
};
</script>

View File

@@ -0,0 +1,349 @@
<script>
import ElCollapseTransition from 'element-ui/src/transitions/collapse-transition';
import menuMixin from './menu-mixin';
import Emitter from 'element-ui/src/mixins/emitter';
import Popper from 'element-ui/src/utils/vue-popper';
const poperMixins = {
props: {
transformOrigin: {
type: [Boolean, String],
default: false
},
offset: Popper.props.offset,
boundariesPadding: Popper.props.boundariesPadding,
popperOptions: Popper.props.popperOptions
},
data: Popper.data,
methods: Popper.methods,
beforeDestroy: Popper.beforeDestroy,
deactivated: Popper.deactivated
};
export default {
name: 'ElSubmenu',
componentName: 'ElSubmenu',
mixins: [menuMixin, Emitter, poperMixins],
components: { ElCollapseTransition },
props: {
index: {
type: String,
required: true
},
showTimeout: {
type: Number,
default: 300
},
hideTimeout: {
type: Number,
default: 300
},
popperClass: String,
disabled: Boolean,
popperAppendToBody: {
type: Boolean,
default: undefined
}
},
data() {
return {
popperJS: null,
timeout: null,
items: {},
submenus: {},
mouseInChild: false
};
},
watch: {
opened(val) {
if (this.isMenuPopup) {
this.$nextTick(_ => {
this.updatePopper();
});
}
}
},
computed: {
// popper option
appendToBody() {
return this.popperAppendToBody === undefined
? this.isFirstLevel
: this.popperAppendToBody;
},
menuTransitionName() {
return this.rootMenu.collapse ? 'el-zoom-in-left' : 'el-zoom-in-top';
},
opened() {
return this.rootMenu.openedMenus.indexOf(this.index) > -1;
},
active() {
let isActive = false;
const submenus = this.submenus;
const items = this.items;
Object.keys(items).forEach(index => {
if (items[index].active) {
isActive = true;
}
});
Object.keys(submenus).forEach(index => {
if (submenus[index].active) {
isActive = true;
}
});
return isActive;
},
hoverBackground() {
return this.rootMenu.hoverBackground;
},
backgroundColor() {
return this.rootMenu.backgroundColor || '';
},
activeTextColor() {
return this.rootMenu.activeTextColor || '';
},
textColor() {
return this.rootMenu.textColor || '';
},
mode() {
return this.rootMenu.mode;
},
isMenuPopup() {
return this.rootMenu.isMenuPopup;
},
titleStyle() {
if (this.mode !== 'horizontal') {
return {
color: this.textColor
};
}
return {
borderBottomColor: this.active
? (this.rootMenu.activeTextColor ? this.activeTextColor : '')
: 'transparent',
color: this.active
? this.activeTextColor
: this.textColor
};
},
isFirstLevel() {
let isFirstLevel = true;
let parent = this.$parent;
while (parent && parent !== this.rootMenu) {
if (['ElSubmenu', 'ElMenuItemGroup'].indexOf(parent.$options.componentName) > -1) {
isFirstLevel = false;
break;
} else {
parent = parent.$parent;
}
}
return isFirstLevel;
}
},
methods: {
handleCollapseToggle(value) {
if (value) {
this.initPopper();
} else {
this.doDestroy();
}
},
addItem(item) {
this.$set(this.items, item.index, item);
},
removeItem(item) {
delete this.items[item.index];
},
addSubmenu(item) {
this.$set(this.submenus, item.index, item);
},
removeSubmenu(item) {
delete this.submenus[item.index];
},
handleClick() {
const { rootMenu, disabled } = this;
if (
(rootMenu.menuTrigger === 'hover' && rootMenu.mode === 'horizontal') ||
(rootMenu.collapse && rootMenu.mode === 'vertical') ||
disabled
) {
return;
}
this.dispatch('ElMenu', 'submenu-click', this);
},
handleMouseenter(event, showTimeout = this.showTimeout) {
if (!('ActiveXObject' in window) && event.type === 'focus' && !event.relatedTarget) {
return;
}
const { rootMenu, disabled } = this;
if (
(rootMenu.menuTrigger === 'click' && rootMenu.mode === 'horizontal') ||
(!rootMenu.collapse && rootMenu.mode === 'vertical') ||
disabled
) {
return;
}
this.dispatch('ElSubmenu', 'mouse-enter-child');
clearTimeout(this.timeout);
this.timeout = setTimeout(() => {
this.rootMenu.openMenu(this.index, this.indexPath);
}, showTimeout);
if (this.appendToBody) {
this.$parent.$el.dispatchEvent(new MouseEvent('mouseenter'));
}
},
handleMouseleave(deepDispatch = false) {
const {rootMenu} = this;
if (
(rootMenu.menuTrigger === 'click' && rootMenu.mode === 'horizontal') ||
(!rootMenu.collapse && rootMenu.mode === 'vertical')
) {
return;
}
this.dispatch('ElSubmenu', 'mouse-leave-child');
clearTimeout(this.timeout);
this.timeout = setTimeout(() => {
!this.mouseInChild && this.rootMenu.closeMenu(this.index);
}, this.hideTimeout);
if (this.appendToBody && deepDispatch) {
if (this.$parent.$options.name === 'ElSubmenu') {
this.$parent.handleMouseleave(true);
}
}
},
handleTitleMouseenter() {
if (this.mode === 'horizontal' && !this.rootMenu.backgroundColor) return;
const title = this.$refs['submenu-title'];
title && (title.style.backgroundColor = this.rootMenu.hoverBackground);
},
handleTitleMouseleave() {
if (this.mode === 'horizontal' && !this.rootMenu.backgroundColor) return;
const title = this.$refs['submenu-title'];
title && (title.style.backgroundColor = this.rootMenu.backgroundColor || '');
},
updatePlacement() {
this.currentPlacement = this.mode === 'horizontal' && this.isFirstLevel
? 'bottom-start'
: 'right-start';
},
initPopper() {
this.referenceElm = this.$el;
this.popperElm = this.$refs.menu;
this.updatePlacement();
}
},
created() {
this.$on('toggle-collapse', this.handleCollapseToggle);
this.$on('mouse-enter-child', () => {
this.mouseInChild = true;
clearTimeout(this.timeout);
});
this.$on('mouse-leave-child', () => {
this.mouseInChild = false;
clearTimeout(this.timeout);
});
},
mounted() {
this.parentMenu.addSubmenu(this);
this.rootMenu.addSubmenu(this);
this.initPopper();
},
beforeDestroy() {
this.parentMenu.removeSubmenu(this);
this.rootMenu.removeSubmenu(this);
},
render(h) {
const {
active,
opened,
paddingStyle,
titleStyle,
backgroundColor,
rootMenu,
currentPlacement,
menuTransitionName,
mode,
disabled,
popperClass,
$slots,
isFirstLevel
} = this;
const popupMenu = (
<transition name={menuTransitionName}>
<div
ref="menu"
v-show={opened}
class={[`el-menu--${mode}`, popperClass]}
on-mouseenter={($event) => this.handleMouseenter($event, 100)}
on-mouseleave={() => this.handleMouseleave(true)}
on-focus={($event) => this.handleMouseenter($event, 100)}>
<ul
role="menu"
class={['el-menu el-menu--popup', `el-menu--popup-${currentPlacement}`]}
style={{ backgroundColor: rootMenu.backgroundColor || '' }}>
{$slots.default}
</ul>
</div>
</transition>
);
const inlineMenu = (
<el-collapse-transition>
<ul
role="menu"
class="el-menu el-menu--inline"
v-show={opened}
style={{ backgroundColor: rootMenu.backgroundColor || '' }}>
{$slots.default}
</ul>
</el-collapse-transition>
);
const submenuTitleIcon = (
rootMenu.mode === 'horizontal' && isFirstLevel ||
rootMenu.mode === 'vertical' && !rootMenu.collapse
) ? 'el-icon-arrow-down' : 'el-icon-arrow-right';
return (
<li
class={{
'el-submenu': true,
'is-active': active,
'is-opened': opened,
'is-disabled': disabled
}}
role="menuitem"
aria-haspopup="true"
aria-expanded={opened}
on-mouseenter={this.handleMouseenter}
on-mouseleave={() => this.handleMouseleave(false)}
on-focus={this.handleMouseenter}
>
<div
class="el-submenu__title"
ref="submenu-title"
on-click={this.handleClick}
on-mouseenter={this.handleTitleMouseenter}
on-mouseleave={this.handleTitleMouseleave}
style={[paddingStyle, titleStyle, { backgroundColor }]}
>
{$slots.title}
<i class={[ 'el-submenu__icon-arrow', submenuTitleIcon ]}></i>
</div>
{this.isMenuPopup ? popupMenu : inlineMenu}
</li>
);
}
};
</script>