2020-11-12 14:43:55 +08:00
|
|
|
<script lang="ts">
|
|
|
|
import {
|
|
|
|
defineComponent,
|
|
|
|
getCurrentInstance,
|
|
|
|
watch,
|
|
|
|
computed,
|
|
|
|
ref,
|
|
|
|
provide,
|
|
|
|
Ref,
|
|
|
|
onMounted,
|
|
|
|
ComputedRef,
|
2020-12-02 15:53:25 +08:00
|
|
|
isRef,
|
2021-08-31 15:31:48 +08:00
|
|
|
h,
|
|
|
|
withDirectives,
|
|
|
|
nextTick,
|
2020-11-12 14:43:55 +08:00
|
|
|
} from 'vue'
|
|
|
|
import mitt from 'mitt'
|
2021-08-31 15:31:48 +08:00
|
|
|
import { Resize } from '@element-plus/directives'
|
2021-08-24 13:36:48 +08:00
|
|
|
import Menubar from '@element-plus/utils/menu/menu-bar'
|
2021-08-31 15:31:48 +08:00
|
|
|
import ElMenuCollapseTransition from './menu-collapse-transition.vue'
|
|
|
|
import ElSubMenu from './submenu.vue'
|
|
|
|
import useMenuColor from './useMenuColor'
|
|
|
|
|
|
|
|
import type {
|
2020-11-12 14:43:55 +08:00
|
|
|
IMenuProps,
|
|
|
|
RootMenuProvider,
|
|
|
|
RegisterMenuItem,
|
|
|
|
SubMenuProvider,
|
2021-08-31 15:31:48 +08:00
|
|
|
} from './menu.type'
|
2020-11-12 14:43:55 +08:00
|
|
|
|
|
|
|
export default defineComponent({
|
|
|
|
name: 'ElMenu',
|
2021-08-31 15:31:48 +08:00
|
|
|
directives: {
|
|
|
|
Resize,
|
|
|
|
},
|
2020-11-12 14:43:55 +08:00
|
|
|
components: {
|
|
|
|
ElMenuCollapseTransition,
|
2021-08-31 15:31:48 +08:00
|
|
|
ElSubMenu,
|
2020-11-12 14:43:55 +08:00
|
|
|
},
|
|
|
|
props: {
|
|
|
|
mode: {
|
|
|
|
type: String,
|
|
|
|
default: 'vertical',
|
|
|
|
},
|
|
|
|
defaultActive: {
|
|
|
|
type: String,
|
|
|
|
default: '',
|
|
|
|
},
|
|
|
|
defaultOpeneds: Array,
|
|
|
|
uniqueOpened: Boolean,
|
|
|
|
router: Boolean,
|
|
|
|
menuTrigger: {
|
|
|
|
type: String,
|
|
|
|
default: 'hover',
|
|
|
|
},
|
|
|
|
collapse: Boolean,
|
|
|
|
backgroundColor: { type: String },
|
|
|
|
textColor: { type: String },
|
|
|
|
activeTextColor: { type: String },
|
|
|
|
collapseTransition: {
|
|
|
|
type: Boolean,
|
|
|
|
default: true,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
emits: ['close', 'open', 'select'],
|
2021-08-31 15:31:48 +08:00
|
|
|
setup(props: IMenuProps, { emit, slots }) {
|
2020-11-12 14:43:55 +08:00
|
|
|
// data
|
|
|
|
const openedMenus = ref(
|
|
|
|
props.defaultOpeneds && !props.collapse
|
|
|
|
? props.defaultOpeneds.slice(0)
|
|
|
|
: [],
|
|
|
|
)
|
|
|
|
const instance = getCurrentInstance()
|
|
|
|
const activeIndex = ref(props.defaultActive)
|
|
|
|
const items = ref({})
|
|
|
|
const submenus = ref({})
|
|
|
|
const alteredCollapse = ref(false)
|
|
|
|
const rootMenuEmitter = mitt()
|
2020-12-01 17:34:03 +08:00
|
|
|
const router = instance.appContext.config.globalProperties.$router
|
2021-08-31 15:31:48 +08:00
|
|
|
const menu = ref(null)
|
|
|
|
const filteredSlot = ref(slots.default?.())
|
2020-11-12 14:43:55 +08:00
|
|
|
|
2021-07-02 17:04:34 +08:00
|
|
|
const hoverBackground = useMenuColor(props)
|
2020-11-12 14:43:55 +08:00
|
|
|
|
|
|
|
// computed
|
|
|
|
const isMenuPopup = computed(() => {
|
|
|
|
return (
|
|
|
|
props.mode === 'horizontal' ||
|
|
|
|
(props.mode === 'vertical' && props.collapse)
|
|
|
|
)
|
|
|
|
})
|
|
|
|
|
|
|
|
// methods
|
|
|
|
|
|
|
|
const initializeMenu = () => {
|
|
|
|
const index = activeIndex.value
|
|
|
|
const activeItem = items.value[index]
|
|
|
|
if (!activeItem || props.mode === 'horizontal' || props.collapse) return
|
|
|
|
|
|
|
|
let indexPath = activeItem.indexPath
|
|
|
|
|
|
|
|
// 展开该菜单项的路径上所有子菜单
|
|
|
|
// expand all submenus of the menu item
|
|
|
|
indexPath.forEach(index => {
|
|
|
|
let submenu = submenus.value[index]
|
|
|
|
submenu && openMenu(index, submenu?.indexPath)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
const addSubMenu = (item: RegisterMenuItem) => {
|
|
|
|
submenus.value[item.index] = item
|
|
|
|
}
|
|
|
|
|
|
|
|
const removeSubMenu = (item: RegisterMenuItem) => {
|
|
|
|
delete submenus.value[item.index]
|
|
|
|
}
|
|
|
|
|
|
|
|
const addMenuItem = (item: RegisterMenuItem) => {
|
|
|
|
items.value[item.index] = item
|
|
|
|
}
|
|
|
|
|
|
|
|
const removeMenuItem = (item: RegisterMenuItem) => {
|
|
|
|
delete items.value[item.index]
|
|
|
|
}
|
|
|
|
|
2020-12-02 15:53:25 +08:00
|
|
|
const openMenu = (index: string, indexPath?: Ref<string[]> | string[]) => {
|
2020-11-12 14:43:55 +08:00
|
|
|
if (openedMenus.value.includes(index)) return
|
|
|
|
// 将不在该菜单路径下的其余菜单收起
|
|
|
|
// collapse all menu that are not under current menu item
|
|
|
|
if (props.uniqueOpened) {
|
|
|
|
openedMenus.value = openedMenus.value.filter((index: string) => {
|
2020-12-02 15:53:25 +08:00
|
|
|
return (
|
|
|
|
(isRef(indexPath) ? indexPath.value : indexPath).indexOf(index) !==
|
|
|
|
-1
|
|
|
|
)
|
2020-11-12 14:43:55 +08:00
|
|
|
})
|
|
|
|
}
|
|
|
|
openedMenus.value.push(index)
|
|
|
|
}
|
|
|
|
|
|
|
|
const closeMenu = index => {
|
|
|
|
const i = openedMenus.value.indexOf(index)
|
|
|
|
if (i !== -1) {
|
|
|
|
openedMenus.value.splice(i, 1)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const open = index => {
|
2020-12-10 16:20:15 +08:00
|
|
|
const { indexPath } = submenus.value[index.toString()]
|
2020-11-12 14:43:55 +08:00
|
|
|
indexPath.forEach(i => openMenu(i, indexPath))
|
|
|
|
}
|
|
|
|
|
|
|
|
const close = index => {
|
|
|
|
closeMenu(index)
|
|
|
|
}
|
|
|
|
|
2021-08-25 10:03:36 +08:00
|
|
|
const handleSubMenuClick = submenu => {
|
2020-11-12 14:43:55 +08:00
|
|
|
const { index, indexPath } = submenu
|
|
|
|
let isOpened = openedMenus.value.includes(index)
|
|
|
|
|
|
|
|
if (isOpened) {
|
|
|
|
closeMenu(index)
|
2021-08-31 15:31:48 +08:00
|
|
|
emit('close', index, indexPath.value)
|
2020-11-12 14:43:55 +08:00
|
|
|
} else {
|
|
|
|
openMenu(index, indexPath)
|
2021-08-31 15:31:48 +08:00
|
|
|
emit('open', index, indexPath.value)
|
2020-11-12 14:43:55 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const handleItemClick = (item: {
|
|
|
|
index: string
|
|
|
|
indexPath: ComputedRef<string[]>
|
|
|
|
route?: any
|
|
|
|
}) => {
|
|
|
|
const { index, indexPath } = item
|
|
|
|
const hasIndex = item.index !== null
|
2021-07-15 15:18:02 +08:00
|
|
|
const emitParams = [index, indexPath.value, item]
|
2020-11-12 14:43:55 +08:00
|
|
|
|
|
|
|
if (props.mode === 'horizontal' || props.collapse) {
|
|
|
|
openedMenus.value = []
|
|
|
|
}
|
2020-12-01 17:34:03 +08:00
|
|
|
|
2021-07-15 15:18:02 +08:00
|
|
|
if (!hasIndex) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if (props.router && router) {
|
2021-07-13 16:57:05 +08:00
|
|
|
let route = item.route || item.index
|
2021-07-15 15:18:02 +08:00
|
|
|
const routerResult = router
|
|
|
|
.push(route)
|
2021-07-13 16:57:05 +08:00
|
|
|
.then(navigationResult => {
|
2021-07-15 15:18:02 +08:00
|
|
|
if (!navigationResult) {
|
|
|
|
activeIndex.value = item.index
|
2021-07-13 16:57:05 +08:00
|
|
|
}
|
2021-07-15 15:18:02 +08:00
|
|
|
return navigationResult
|
2021-07-13 16:57:05 +08:00
|
|
|
})
|
2021-08-31 15:31:48 +08:00
|
|
|
emit('select', ...emitParams.concat(routerResult))
|
2021-07-15 15:18:02 +08:00
|
|
|
} else {
|
|
|
|
activeIndex.value = item.index
|
2021-08-31 15:31:48 +08:00
|
|
|
emit('select', ...emitParams)
|
2020-12-01 17:34:03 +08:00
|
|
|
}
|
|
|
|
}
|
2020-11-12 14:43:55 +08:00
|
|
|
|
2021-08-31 15:31:48 +08:00
|
|
|
const updateActiveIndex = (val: string) => {
|
2020-11-12 14:43:55 +08:00
|
|
|
const itemsInData = items.value
|
|
|
|
const item =
|
|
|
|
itemsInData[val] ||
|
|
|
|
itemsInData[activeIndex.value] ||
|
|
|
|
itemsInData[props.defaultActive]
|
|
|
|
|
|
|
|
if (item) {
|
|
|
|
activeIndex.value = item.index
|
|
|
|
initializeMenu()
|
|
|
|
} else {
|
|
|
|
// Can't find item when collapsing
|
|
|
|
// and activeIndex shouldn't be changed when 'collapse' was changed.
|
|
|
|
// Then reset 'alteredCollapse' immediately.
|
|
|
|
if (!alteredCollapse.value) {
|
|
|
|
activeIndex.value = null
|
|
|
|
} else {
|
|
|
|
alteredCollapse.value = false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-09-01 15:43:57 +08:00
|
|
|
const flattedChildren = children => {
|
|
|
|
const temp = Array.isArray(children) ? children : [children]
|
|
|
|
const res = []
|
|
|
|
temp.forEach(child => {
|
|
|
|
if (Array.isArray(child.children)) {
|
|
|
|
res.push(...flattedChildren(child.children))
|
|
|
|
} else {
|
|
|
|
res.push(child)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
return res
|
|
|
|
}
|
|
|
|
|
2021-08-31 15:31:48 +08:00
|
|
|
const updateFilteredSlot = async () => {
|
|
|
|
filteredSlot.value = slots.default?.()
|
|
|
|
await nextTick()
|
|
|
|
if (props.mode === 'horizontal') {
|
|
|
|
const items = Array.from(menu.value.childNodes).filter((item: HTMLElement) => item.nodeName !== '#text' || item.nodeValue) as [HTMLElement]
|
2021-09-01 15:43:57 +08:00
|
|
|
const originalSlot = flattedChildren(slots.default?.()) || []
|
|
|
|
if (items.length === originalSlot.length) {
|
2021-08-31 15:31:48 +08:00
|
|
|
const moreItemWidth = 64
|
|
|
|
const paddingLeft = parseInt(getComputedStyle(menu.value).paddingLeft)
|
|
|
|
const paddingRight = parseInt(getComputedStyle(menu.value).paddingRight)
|
|
|
|
const menuWidth = menu.value.clientWidth - paddingLeft - paddingRight
|
|
|
|
let calcWidth = 0
|
|
|
|
let sliceIndex = 0
|
|
|
|
items.forEach((item, index) => {
|
|
|
|
calcWidth += item.offsetWidth || 0
|
|
|
|
if (calcWidth <= menuWidth - moreItemWidth) {
|
|
|
|
sliceIndex = index + 1
|
|
|
|
}
|
|
|
|
})
|
2021-09-01 15:43:57 +08:00
|
|
|
const defaultSlot = originalSlot.slice(0, sliceIndex)
|
|
|
|
const moreSlot = originalSlot.slice(sliceIndex)
|
2021-08-31 15:31:48 +08:00
|
|
|
if (moreSlot?.length) {
|
|
|
|
filteredSlot.value = [
|
|
|
|
...defaultSlot,
|
|
|
|
h(ElSubMenu, {
|
|
|
|
index: 'sub-menu-more',
|
2021-09-01 15:43:57 +08:00
|
|
|
class: 'el-sub-menu__hide-arrow',
|
2021-08-31 15:31:48 +08:00
|
|
|
}, {
|
|
|
|
title: () => h('i', { class: ['el-icon-more', 'el-sub-menu__icon-more'] }),
|
|
|
|
default: () => moreSlot,
|
|
|
|
}),
|
|
|
|
]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const handleResize = () => {
|
2021-09-01 15:43:57 +08:00
|
|
|
updateFilteredSlot()
|
2021-08-31 15:31:48 +08:00
|
|
|
}
|
|
|
|
|
2020-11-12 14:43:55 +08:00
|
|
|
// watch
|
2021-08-31 15:31:48 +08:00
|
|
|
watch(() => slots.default?.(), () => {
|
|
|
|
updateFilteredSlot()
|
|
|
|
})
|
2020-11-12 14:43:55 +08:00
|
|
|
|
|
|
|
watch(
|
|
|
|
() => props.defaultActive,
|
|
|
|
currentActive => {
|
2021-03-28 12:46:58 +08:00
|
|
|
if (!items.value[currentActive]) {
|
2020-11-12 14:43:55 +08:00
|
|
|
activeIndex.value = ''
|
|
|
|
}
|
|
|
|
updateActiveIndex(currentActive)
|
|
|
|
},
|
|
|
|
)
|
|
|
|
|
|
|
|
watch(items.value, () => {
|
2021-08-31 15:31:48 +08:00
|
|
|
initializeMenu()
|
2020-11-12 14:43:55 +08:00
|
|
|
})
|
|
|
|
|
|
|
|
watch(
|
|
|
|
() => props.collapse,
|
|
|
|
(value, prev) => {
|
|
|
|
if (value !== prev) {
|
|
|
|
alteredCollapse.value = true
|
|
|
|
}
|
|
|
|
if (value) openedMenus.value = []
|
|
|
|
rootMenuEmitter.emit(
|
|
|
|
'rootMenu:toggle-collapse',
|
|
|
|
Boolean(props.collapse),
|
|
|
|
)
|
|
|
|
},
|
|
|
|
)
|
|
|
|
|
|
|
|
// provide
|
|
|
|
provide<RootMenuProvider>('rootMenu', {
|
|
|
|
props,
|
|
|
|
openedMenus,
|
|
|
|
items,
|
|
|
|
submenus,
|
|
|
|
hoverBackground,
|
|
|
|
activeIndex,
|
|
|
|
isMenuPopup,
|
|
|
|
|
|
|
|
methods: {
|
|
|
|
addMenuItem,
|
|
|
|
removeMenuItem,
|
|
|
|
addSubMenu,
|
|
|
|
removeSubMenu,
|
|
|
|
openMenu,
|
|
|
|
closeMenu,
|
|
|
|
},
|
|
|
|
rootMenuEmit: rootMenuEmitter.emit,
|
|
|
|
rootMenuOn: rootMenuEmitter.on,
|
|
|
|
})
|
|
|
|
provide<SubMenuProvider>(`subMenu:${instance.uid}`, {
|
|
|
|
addSubMenu,
|
|
|
|
removeSubMenu,
|
|
|
|
})
|
|
|
|
|
|
|
|
// lifecycle
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
initializeMenu()
|
|
|
|
rootMenuEmitter.on('menuItem:item-click', handleItemClick)
|
2021-08-25 10:03:36 +08:00
|
|
|
rootMenuEmitter.on('submenu:submenu-click', handleSubMenuClick)
|
2020-11-12 14:43:55 +08:00
|
|
|
if (props.mode === 'horizontal') {
|
|
|
|
new Menubar(instance.vnode.el)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
return {
|
|
|
|
hoverBackground,
|
|
|
|
isMenuPopup,
|
2021-08-31 15:31:48 +08:00
|
|
|
menu,
|
|
|
|
filteredSlot,
|
2020-11-12 14:43:55 +08:00
|
|
|
|
|
|
|
props,
|
|
|
|
|
|
|
|
open,
|
|
|
|
close,
|
2021-08-31 15:31:48 +08:00
|
|
|
handleResize,
|
|
|
|
}
|
|
|
|
},
|
|
|
|
render() {
|
2021-09-01 15:43:57 +08:00
|
|
|
const directives = this.mode === 'horizontal' ? [[Resize, this.handleResize]] : []
|
2021-08-31 15:31:48 +08:00
|
|
|
const menu = withDirectives(h('ul', {
|
|
|
|
key: String(this.collapse),
|
|
|
|
role: 'menubar',
|
|
|
|
ref: 'menu',
|
|
|
|
style: { backgroundColor: this.backgroundColor || '' },
|
|
|
|
class: {
|
|
|
|
'el-menu': true,
|
|
|
|
'el-menu--horizontal': this.mode === 'horizontal',
|
|
|
|
'el-menu--collapse': this.collapse,
|
|
|
|
},
|
2021-09-01 15:43:57 +08:00
|
|
|
}, [this.filteredSlot]), directives)
|
2021-08-31 15:31:48 +08:00
|
|
|
|
|
|
|
if (this.collapseTransition) {
|
|
|
|
return h(ElMenuCollapseTransition, () => menu)
|
2020-11-12 14:43:55 +08:00
|
|
|
}
|
2021-08-31 15:31:48 +08:00
|
|
|
return menu
|
2020-11-12 14:43:55 +08:00
|
|
|
},
|
|
|
|
})
|
|
|
|
</script>
|