mirror of
https://gitee.com/element-plus/element-plus.git
synced 2024-11-30 18:27:40 +08:00
refactor(components): refactor menu (#3639)
* refactor(menu): rename to kebab-case * refactor(menu): rename RootMenuProvider to MenuProvider * refactor(menu): rename menu.type to types * refactor(menu): extract menu props and emits * refactor(menu): change sub-menu.vue to ts * refactor(menu): extract menu-item-group props and emits * refactor(menu): extract menu-item props and emits * refactor(menu): extract sub-menu props and emits * refactor(menu): rename type RegisterMenuItem to MenuItemRegistered * refactor(menu): MenuProvider ref to reactive * refactor(menu): MenuProvider remove methods * refactor(menu): change submenus to subMenus * refactor(menu): remove RootMenuProps type * refactor(menu): MenuProvider improve types & rename submenu to subMenu * refactor(menu): menu add block to provide * refactor(menu): menu improve expose * refactor(menu): menu improve render types * refactor(menu): menu refactor types & change handle(Sub)MenuItemClick params * refactor(menu): menu refactor types * refactor(menu): menu-item-group improve types * refactor(menu): menu-item improve types * refactor(menu): sub-menu improve types * refactor(menu): use-menu improve types * refactor(menu): sub-menu fix types * refactor(menu): menu-collapse-transition improve types * refactor(menu): menu-item-group improve template * refactor(menu): menu-item rename emit param * refactor(menu): finally improve types * fix lint * chore: re-order import * chore: remove reactive
This commit is contained in:
parent
8e29813609
commit
c68d59c6a0
@ -4,9 +4,9 @@ import { sleep } from '@element-plus/test-utils'
|
||||
import { rAF } from '@element-plus/test-utils/tick'
|
||||
|
||||
import Menu from '../src/menu'
|
||||
import MenuGroup from '../src/menuItemGroup.vue'
|
||||
import MenuItem from '../src/menuItem.vue'
|
||||
import SubMenu from '../src/submenu.vue'
|
||||
import MenuGroup from '../src/menu-item-group.vue'
|
||||
import MenuItem from '../src/menu-item.vue'
|
||||
import SubMenu from '../src/sub-menu'
|
||||
|
||||
const _mount = (template: string, options = {}) =>
|
||||
mount({
|
||||
@ -119,7 +119,7 @@ describe('menu', () => {
|
||||
default-active="2"
|
||||
class="el-menu-vertical-demo"
|
||||
>
|
||||
<el-sub-menu index="1">
|
||||
<el-sub-menu index="1" ref="subMenu">
|
||||
<template #title>
|
||||
<i class="el-icon-location"></i>
|
||||
<span>导航一</span>
|
||||
@ -153,11 +153,13 @@ describe('menu', () => {
|
||||
},
|
||||
}
|
||||
)
|
||||
const elSubMenu = wrapper.findComponent({ name: 'ElSubMenu' })
|
||||
const elSubMenu = wrapper.findComponent({ ref: 'subMenu' })
|
||||
const instance: any = elSubMenu.vm
|
||||
|
||||
const button = wrapper.find('button')
|
||||
button.trigger('click')
|
||||
|
||||
await nextTick()
|
||||
const instance = elSubMenu.vm as any
|
||||
expect(instance.opened).toBeTruthy()
|
||||
})
|
||||
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { withInstall, withNoopInstall } from '@element-plus/utils/with-install'
|
||||
|
||||
import Menu from './src/menu'
|
||||
import MenuItem from './src/menuItem.vue'
|
||||
import MenuItemGroup from './src/menuItemGroup.vue'
|
||||
import SubMenu from './src/submenu.vue'
|
||||
import MenuItem from './src/menu-item.vue'
|
||||
import MenuItemGroup from './src/menu-item-group.vue'
|
||||
import SubMenu from './src/sub-menu'
|
||||
|
||||
export const ElMenu = withInstall(Menu, {
|
||||
MenuItem,
|
||||
@ -15,4 +15,8 @@ export const ElMenuItem = withNoopInstall(MenuItem)
|
||||
export const ElMenuItemGroup = withNoopInstall(MenuItemGroup)
|
||||
export const ElSubMenu = withNoopInstall(SubMenu)
|
||||
|
||||
export * from './src/menu.type'
|
||||
export * from './src/menu'
|
||||
export * from './src/menu-item'
|
||||
export * from './src/menu-item-group'
|
||||
export * from './src/sub-menu'
|
||||
export * from './src/types'
|
||||
|
@ -1,58 +1,59 @@
|
||||
<template>
|
||||
<transition mode="out-in" v-on="on">
|
||||
<slot></slot>
|
||||
<transition mode="out-in" v-bind="listeners">
|
||||
<slot />
|
||||
</transition>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
import { addClass, removeClass, hasClass } from '@element-plus/utils/dom'
|
||||
|
||||
import type { TransitionProps, BaseTransitionProps } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ElMenuCollapseTransition',
|
||||
setup() {
|
||||
return {
|
||||
on: {
|
||||
beforeEnter(el: HTMLElement) {
|
||||
el.style.opacity = '0.2'
|
||||
},
|
||||
|
||||
enter(el: HTMLElement, done) {
|
||||
addClass(el, 'el-opacity-transition')
|
||||
el.style.opacity = '1'
|
||||
done()
|
||||
},
|
||||
|
||||
afterEnter(el: HTMLElement) {
|
||||
removeClass(el, 'el-opacity-transition')
|
||||
el.style.opacity = ''
|
||||
},
|
||||
|
||||
beforeLeave(el: HTMLElement) {
|
||||
if (!el.dataset) {
|
||||
;(el as any).dataset = {}
|
||||
}
|
||||
|
||||
if (hasClass(el, 'el-menu--collapse')) {
|
||||
removeClass(el, 'el-menu--collapse')
|
||||
el.dataset.oldOverflow = el.style.overflow
|
||||
el.dataset.scrollWidth = el.clientWidth.toString()
|
||||
addClass(el, 'el-menu--collapse')
|
||||
} else {
|
||||
addClass(el, 'el-menu--collapse')
|
||||
el.dataset.oldOverflow = el.style.overflow
|
||||
el.dataset.scrollWidth = el.clientWidth.toString()
|
||||
removeClass(el, 'el-menu--collapse')
|
||||
}
|
||||
|
||||
el.style.width = `${el.scrollWidth}px`
|
||||
el.style.overflow = 'hidden'
|
||||
},
|
||||
|
||||
leave(el: HTMLElement) {
|
||||
addClass(el, 'horizontal-collapse-transition')
|
||||
el.style.width = `${el.dataset.scrollWidth}px`
|
||||
},
|
||||
const listeners = {
|
||||
onBeforeEnter: (el) => (el.style.opacity = '0.2'),
|
||||
onEnter(el, done) {
|
||||
addClass(el, 'el-opacity-transition')
|
||||
el.style.opacity = '1'
|
||||
done()
|
||||
},
|
||||
|
||||
onAfterEnter(el) {
|
||||
removeClass(el, 'el-opacity-transition')
|
||||
el.style.opacity = ''
|
||||
},
|
||||
|
||||
onBeforeLeave(el) {
|
||||
if (!el.dataset) {
|
||||
;(el as any).dataset = {}
|
||||
}
|
||||
|
||||
if (hasClass(el, 'el-menu--collapse')) {
|
||||
removeClass(el, 'el-menu--collapse')
|
||||
el.dataset.oldOverflow = el.style.overflow
|
||||
el.dataset.scrollWidth = el.clientWidth.toString()
|
||||
addClass(el, 'el-menu--collapse')
|
||||
} else {
|
||||
addClass(el, 'el-menu--collapse')
|
||||
el.dataset.oldOverflow = el.style.overflow
|
||||
el.dataset.scrollWidth = el.clientWidth.toString()
|
||||
removeClass(el, 'el-menu--collapse')
|
||||
}
|
||||
|
||||
el.style.width = `${el.scrollWidth}px`
|
||||
el.style.overflow = 'hidden'
|
||||
},
|
||||
|
||||
onLeave(el: HTMLElement) {
|
||||
addClass(el, 'horizontal-collapse-transition')
|
||||
el.style.width = `${el.dataset.scrollWidth}px`
|
||||
},
|
||||
} as BaseTransitionProps<HTMLElement> as TransitionProps
|
||||
|
||||
return {
|
||||
listeners,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
6
packages/components/menu/src/menu-item-group.ts
Normal file
6
packages/components/menu/src/menu-item-group.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import type { ExtractPropTypes } from 'vue'
|
||||
|
||||
export const menuItemGroupProps = {
|
||||
title: String,
|
||||
} as const
|
||||
export type MenuItemGroupProps = ExtractPropTypes<typeof menuItemGroupProps>
|
53
packages/components/menu/src/menu-item-group.vue
Normal file
53
packages/components/menu/src/menu-item-group.vue
Normal file
@ -0,0 +1,53 @@
|
||||
<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" />
|
||||
</div>
|
||||
<ul>
|
||||
<slot />
|
||||
</ul>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed, getCurrentInstance, inject } from 'vue'
|
||||
import { throwError } from '@element-plus/utils/error'
|
||||
import { menuItemGroupProps } from './menu-item-group'
|
||||
|
||||
import type { MenuProvider } from './types'
|
||||
|
||||
const COMPONENT_NAME = 'ElMenuItemGroup'
|
||||
|
||||
export default defineComponent({
|
||||
name: COMPONENT_NAME,
|
||||
|
||||
props: menuItemGroupProps,
|
||||
|
||||
setup() {
|
||||
const instance = getCurrentInstance()!
|
||||
const menu = inject<MenuProvider>('rootMenu')
|
||||
if (!menu) throwError(COMPONENT_NAME, 'can not inject root menu')
|
||||
|
||||
const levelPadding = computed(() => {
|
||||
if (menu.props.collapse) return 20
|
||||
let padding = 20
|
||||
let parent = instance.parent
|
||||
while (parent && parent.type.name !== 'ElMenu') {
|
||||
if (parent.type.name === 'ElSubMenu') {
|
||||
padding += 20
|
||||
}
|
||||
parent = parent.parent
|
||||
}
|
||||
return padding
|
||||
})
|
||||
|
||||
return {
|
||||
levelPadding,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
24
packages/components/menu/src/menu-item.ts
Normal file
24
packages/components/menu/src/menu-item.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { buildProp, definePropType } from '@element-plus/utils/props'
|
||||
import { isString } from '@element-plus/utils/util'
|
||||
|
||||
import type { ExtractPropTypes } from 'vue'
|
||||
import type { RouteLocationRaw } from 'vue-router'
|
||||
import type { MenuItemRegistered } from './types'
|
||||
|
||||
export const menuItemProps = {
|
||||
index: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
route: buildProp({
|
||||
type: definePropType<RouteLocationRaw>([String, Object]),
|
||||
}),
|
||||
disabled: Boolean,
|
||||
} as const
|
||||
export type MenuItemProps = ExtractPropTypes<typeof menuItemProps>
|
||||
|
||||
export const menuItemEmits = {
|
||||
click: (item: MenuItemRegistered) =>
|
||||
isString(item.index) && Array.isArray(item.indexPath),
|
||||
}
|
||||
export type MenuItemEmits = typeof menuItemEmits
|
128
packages/components/menu/src/menu-item.vue
Normal file
128
packages/components/menu/src/menu-item.vue
Normal file
@ -0,0 +1,128 @@
|
||||
<template>
|
||||
<li
|
||||
class="el-menu-item"
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
:style="paddingStyle"
|
||||
:class="{
|
||||
'is-active': active,
|
||||
'is-disabled': disabled,
|
||||
}"
|
||||
@click="handleClick"
|
||||
>
|
||||
<el-tooltip
|
||||
v-if="
|
||||
parentMenu.type.name === 'ElMenu' &&
|
||||
rootMenu.props.collapse &&
|
||||
$slots.title
|
||||
"
|
||||
:effect="Effect.DARK"
|
||||
placement="right"
|
||||
>
|
||||
<template #content>
|
||||
<slot name="title" />
|
||||
</template>
|
||||
<div
|
||||
:style="{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
display: 'inline-block',
|
||||
boxSizing: 'border-box',
|
||||
padding: '0 20px',
|
||||
}"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</el-tooltip>
|
||||
<template v-else>
|
||||
<slot />
|
||||
<slot name="title" />
|
||||
</template>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {
|
||||
defineComponent,
|
||||
computed,
|
||||
onMounted,
|
||||
onBeforeUnmount,
|
||||
inject,
|
||||
getCurrentInstance,
|
||||
toRef,
|
||||
reactive,
|
||||
} from 'vue'
|
||||
import ElTooltip from '@element-plus/components/tooltip'
|
||||
import { Effect } from '@element-plus/components/popper'
|
||||
import { throwError } from '@element-plus/utils/error'
|
||||
import useMenu from './use-menu'
|
||||
import { menuItemEmits, menuItemProps } from './menu-item'
|
||||
|
||||
import type { MenuItemRegistered, MenuProvider, SubMenuProvider } from './types'
|
||||
|
||||
const COMPONENT_NAME = 'ElMenuItem'
|
||||
export default defineComponent({
|
||||
name: COMPONENT_NAME,
|
||||
components: {
|
||||
ElTooltip,
|
||||
},
|
||||
|
||||
props: menuItemProps,
|
||||
emits: menuItemEmits,
|
||||
|
||||
setup(props, { emit }) {
|
||||
const instance = getCurrentInstance()!
|
||||
const rootMenu = inject<MenuProvider>('rootMenu')
|
||||
if (!rootMenu) throwError(COMPONENT_NAME, 'can not inject root menu')
|
||||
|
||||
const { parentMenu, paddingStyle, indexPath } = useMenu(
|
||||
instance,
|
||||
toRef(props, 'index')
|
||||
)
|
||||
|
||||
const subMenu = inject<SubMenuProvider>(`subMenu:${parentMenu.value.uid}`)
|
||||
if (!subMenu) throwError(COMPONENT_NAME, 'can not inject sub menu')
|
||||
|
||||
const active = computed(() => props.index === rootMenu.activeIndex)
|
||||
const item: MenuItemRegistered = reactive({
|
||||
index: props.index,
|
||||
indexPath,
|
||||
active,
|
||||
})
|
||||
|
||||
const handleClick = () => {
|
||||
if (!props.disabled) {
|
||||
rootMenu.handleMenuItemClick({
|
||||
index: props.index,
|
||||
indexPath: indexPath.value,
|
||||
route: props.route,
|
||||
})
|
||||
emit('click', item)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
subMenu.addSubMenu(item)
|
||||
rootMenu.addMenuItem(item)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
subMenu.removeSubMenu(item)
|
||||
rootMenu.removeMenuItem(item)
|
||||
})
|
||||
|
||||
return {
|
||||
Effect,
|
||||
parentMenu,
|
||||
rootMenu,
|
||||
paddingStyle,
|
||||
active,
|
||||
|
||||
handleClick,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
@ -6,70 +6,102 @@ import {
|
||||
ref,
|
||||
provide,
|
||||
onMounted,
|
||||
isRef,
|
||||
h,
|
||||
withDirectives,
|
||||
reactive,
|
||||
} from 'vue'
|
||||
import { Resize } from '@element-plus/directives'
|
||||
import Menubar from '@element-plus/utils/menu/menu-bar'
|
||||
import { buildProp, definePropType, mutable } from '@element-plus/utils/props'
|
||||
import { isString, isObject } from '@element-plus/utils/util'
|
||||
import ElMenuCollapseTransition from './menu-collapse-transition.vue'
|
||||
import ElSubMenu from './submenu.vue'
|
||||
import ElSubMenu from './sub-menu'
|
||||
import { useMenuCssVar } from './use-menu-css-var'
|
||||
import type { VNode, Ref, ComputedRef } from 'vue'
|
||||
import type {
|
||||
IMenuProps,
|
||||
RootMenuProvider,
|
||||
RegisterMenuItem,
|
||||
SubMenuProvider,
|
||||
} from './menu.type'
|
||||
|
||||
import type { MenuItemClicked, MenuProvider, SubMenuProvider } from './types'
|
||||
import type { NavigationFailure, Router } from 'vue-router'
|
||||
import type { VNode, ExtractPropTypes, VNodeNormalizedChildren } from 'vue'
|
||||
|
||||
export const menuProps = {
|
||||
mode: buildProp({
|
||||
type: String,
|
||||
values: ['horizontal', 'vertical'],
|
||||
default: 'vertical',
|
||||
} as const),
|
||||
defaultActive: buildProp({
|
||||
type: String,
|
||||
default: '',
|
||||
} as const),
|
||||
defaultOpeneds: buildProp({
|
||||
type: definePropType<string[]>(Array),
|
||||
default: () => mutable([] as const),
|
||||
}),
|
||||
uniqueOpened: Boolean,
|
||||
router: Boolean,
|
||||
menuTrigger: buildProp({
|
||||
type: String,
|
||||
values: ['hover', 'click'],
|
||||
default: 'hover',
|
||||
} as const),
|
||||
collapse: Boolean,
|
||||
backgroundColor: String,
|
||||
textColor: String,
|
||||
activeTextColor: String,
|
||||
collapseTransition: buildProp({
|
||||
type: Boolean,
|
||||
default: true,
|
||||
} as const),
|
||||
} as const
|
||||
export type MenuProps = ExtractPropTypes<typeof menuProps>
|
||||
|
||||
const checkIndexPath = (indexPath: unknown): indexPath is string[] =>
|
||||
Array.isArray(indexPath) && indexPath.every((path) => isString(path))
|
||||
|
||||
export const menuEmits = {
|
||||
close: (index: string, indexPath: string[]) =>
|
||||
isString(index) && checkIndexPath(indexPath),
|
||||
|
||||
open: (index: string, indexPath: string[]) =>
|
||||
isString(index) && checkIndexPath(indexPath),
|
||||
|
||||
select: (
|
||||
index: string,
|
||||
indexPath: string[],
|
||||
item: MenuItemClicked,
|
||||
routerResult?: Promise<void | NavigationFailure>
|
||||
) =>
|
||||
isString(index) &&
|
||||
checkIndexPath(indexPath) &&
|
||||
isObject(item) &&
|
||||
(routerResult === undefined || routerResult instanceof Promise),
|
||||
}
|
||||
export type MenuEmits = typeof menuEmits
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ElMenu',
|
||||
|
||||
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'],
|
||||
props: menuProps,
|
||||
emits: menuEmits,
|
||||
|
||||
setup(props, { emit, slots, expose }) {
|
||||
const instance = getCurrentInstance()!
|
||||
const router = instance.appContext.config.globalProperties.$router as Router
|
||||
const menu = ref<HTMLUListElement>()
|
||||
|
||||
setup(props: IMenuProps, { emit, slots, expose }) {
|
||||
// data
|
||||
const openedMenus = ref(
|
||||
const openedMenus = ref<MenuProvider['openedMenus']>(
|
||||
props.defaultOpeneds && !props.collapse
|
||||
? props.defaultOpeneds.slice(0)
|
||||
: []
|
||||
)
|
||||
const instance = getCurrentInstance()
|
||||
const activeIndex = ref(props.defaultActive)
|
||||
const items = ref({})
|
||||
const submenus = ref({})
|
||||
const activeIndex = ref<MenuProvider['activeIndex']>(props.defaultActive)
|
||||
const items = ref<MenuProvider['items']>({})
|
||||
const subMenus = ref<MenuProvider['subMenus']>({})
|
||||
|
||||
const alteredCollapse = ref(false)
|
||||
const router = instance.appContext.config.globalProperties.$router
|
||||
const menu = ref(null)
|
||||
|
||||
// computed
|
||||
const isMenuPopup = computed(() => {
|
||||
const isMenuPopup = computed<MenuProvider['isMenuPopup']>(() => {
|
||||
return (
|
||||
props.mode === 'horizontal' ||
|
||||
(props.mode === 'vertical' && props.collapse)
|
||||
@ -77,111 +109,80 @@ export default defineComponent({
|
||||
})
|
||||
|
||||
// methods
|
||||
|
||||
const initializeMenu = () => {
|
||||
const index = activeIndex.value
|
||||
const activeItem = items.value[index]
|
||||
const initMenu = () => {
|
||||
const activeItem = activeIndex.value && items.value[activeIndex.value]
|
||||
if (!activeItem || props.mode === 'horizontal' || props.collapse) return
|
||||
|
||||
const indexPath = activeItem.indexPath
|
||||
|
||||
// 展开该菜单项的路径上所有子菜单
|
||||
// expand all submenus of the menu item
|
||||
// expand all subMenus of the menu item
|
||||
indexPath.forEach((index) => {
|
||||
const submenu = submenus.value[index]
|
||||
submenu && openMenu(index, submenu?.indexPath)
|
||||
const 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]
|
||||
}
|
||||
|
||||
const openMenu = (index: string, indexPath?: Ref<string[]> | string[]) => {
|
||||
const openMenu: MenuProvider['openMenu'] = (index, indexPath) => {
|
||||
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) => {
|
||||
return (
|
||||
(isRef(indexPath) ? indexPath.value : indexPath).indexOf(index) !==
|
||||
-1
|
||||
)
|
||||
})
|
||||
openedMenus.value = openedMenus.value.filter((index: string) =>
|
||||
indexPath.includes(index)
|
||||
)
|
||||
}
|
||||
openedMenus.value.push(index)
|
||||
}
|
||||
|
||||
const closeMenu = (index) => {
|
||||
const closeMenu: MenuProvider['closeMenu'] = (index) => {
|
||||
const i = openedMenus.value.indexOf(index)
|
||||
if (i !== -1) {
|
||||
openedMenus.value.splice(i, 1)
|
||||
}
|
||||
}
|
||||
|
||||
const open = (index) => {
|
||||
const { indexPath } = submenus.value[index.toString()]
|
||||
indexPath.forEach((i) => openMenu(i, indexPath))
|
||||
}
|
||||
|
||||
const close = (index) => {
|
||||
closeMenu(index)
|
||||
}
|
||||
|
||||
const handleSubMenuClick = (submenu) => {
|
||||
const { index, indexPath } = submenu
|
||||
const handleSubMenuClick: MenuProvider['handleSubMenuClick'] = ({
|
||||
index,
|
||||
indexPath,
|
||||
}) => {
|
||||
const isOpened = openedMenus.value.includes(index)
|
||||
|
||||
if (isOpened) {
|
||||
closeMenu(index)
|
||||
emit('close', index, indexPath.value)
|
||||
emit('close', index, indexPath)
|
||||
} else {
|
||||
openMenu(index, indexPath)
|
||||
emit('open', index, indexPath.value)
|
||||
emit('open', index, indexPath)
|
||||
}
|
||||
}
|
||||
|
||||
const handleMenuItemClick = (item: {
|
||||
index: string
|
||||
indexPath: ComputedRef<string[]>
|
||||
route?: any
|
||||
}) => {
|
||||
const { index, indexPath } = item
|
||||
const hasIndex = item.index !== null
|
||||
const emitParams = [index, indexPath.value, item]
|
||||
|
||||
const handleMenuItemClick: MenuProvider['handleMenuItemClick'] = (
|
||||
menuItem
|
||||
) => {
|
||||
if (props.mode === 'horizontal' || props.collapse) {
|
||||
openedMenus.value = []
|
||||
}
|
||||
|
||||
if (!hasIndex) {
|
||||
return
|
||||
}
|
||||
const { index, indexPath } = menuItem
|
||||
if (index === undefined || indexPath === undefined) return
|
||||
|
||||
if (props.router && router) {
|
||||
const route = item.route || item.index
|
||||
const routerResult = router.push(route).then((navigationResult) => {
|
||||
if (!navigationResult) {
|
||||
activeIndex.value = item.index
|
||||
}
|
||||
return navigationResult
|
||||
const route = menuItem.route || index
|
||||
const routerResult = router.push(route).then((res) => {
|
||||
if (!res) activeIndex.value = index
|
||||
return res
|
||||
})
|
||||
emit('select', ...emitParams.concat(routerResult))
|
||||
emit(
|
||||
'select',
|
||||
index,
|
||||
indexPath,
|
||||
{ index, indexPath, route },
|
||||
routerResult
|
||||
)
|
||||
} else {
|
||||
activeIndex.value = item.index
|
||||
emit('select', ...emitParams)
|
||||
activeIndex.value = index
|
||||
emit('select', index, indexPath, { index, indexPath })
|
||||
}
|
||||
}
|
||||
|
||||
@ -189,24 +190,24 @@ export default defineComponent({
|
||||
const itemsInData = items.value
|
||||
const item =
|
||||
itemsInData[val] ||
|
||||
itemsInData[activeIndex.value] ||
|
||||
(activeIndex.value && itemsInData[activeIndex.value]) ||
|
||||
itemsInData[props.defaultActive]
|
||||
|
||||
if (item) {
|
||||
activeIndex.value = item.index
|
||||
initializeMenu()
|
||||
initMenu()
|
||||
} 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
|
||||
activeIndex.value = undefined
|
||||
} else {
|
||||
alteredCollapse.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
const handleResize = () => instance.proxy.$forceUpdate()
|
||||
const handleResize = () => instance.proxy!.$forceUpdate()
|
||||
|
||||
watch(
|
||||
() => props.defaultActive,
|
||||
@ -218,9 +219,7 @@ export default defineComponent({
|
||||
}
|
||||
)
|
||||
|
||||
watch(items.value, () => {
|
||||
initializeMenu()
|
||||
})
|
||||
watch(items.value, () => initMenu())
|
||||
|
||||
watch(
|
||||
() => props.collapse,
|
||||
@ -233,55 +232,78 @@ export default defineComponent({
|
||||
)
|
||||
|
||||
// provide
|
||||
provide<RootMenuProvider>('rootMenu', {
|
||||
props,
|
||||
openedMenus,
|
||||
items,
|
||||
submenus,
|
||||
activeIndex,
|
||||
isMenuPopup,
|
||||
{
|
||||
const addSubMenu: MenuProvider['addSubMenu'] = (item) => {
|
||||
subMenus.value[item.index] = item
|
||||
}
|
||||
|
||||
methods: {
|
||||
addMenuItem,
|
||||
removeMenuItem,
|
||||
const removeSubMenu: MenuProvider['removeSubMenu'] = (item) => {
|
||||
delete subMenus.value[item.index]
|
||||
}
|
||||
|
||||
const addMenuItem: MenuProvider['addMenuItem'] = (item) => {
|
||||
items.value[item.index] = item
|
||||
}
|
||||
|
||||
const removeMenuItem: MenuProvider['removeMenuItem'] = (item) => {
|
||||
delete items.value[item.index]
|
||||
}
|
||||
provide<MenuProvider>(
|
||||
'rootMenu',
|
||||
reactive({
|
||||
props,
|
||||
openedMenus,
|
||||
items,
|
||||
subMenus,
|
||||
activeIndex,
|
||||
isMenuPopup,
|
||||
|
||||
addMenuItem,
|
||||
removeMenuItem,
|
||||
addSubMenu,
|
||||
removeSubMenu,
|
||||
openMenu,
|
||||
closeMenu,
|
||||
handleMenuItemClick,
|
||||
handleSubMenuClick,
|
||||
})
|
||||
)
|
||||
provide<SubMenuProvider>(`subMenu:${instance.uid}`, {
|
||||
addSubMenu,
|
||||
removeSubMenu,
|
||||
openMenu,
|
||||
closeMenu,
|
||||
handleMenuItemClick,
|
||||
handleSubMenuClick,
|
||||
},
|
||||
})
|
||||
provide<SubMenuProvider>(`subMenu:${instance.uid}`, {
|
||||
addSubMenu,
|
||||
removeSubMenu,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// lifecycle
|
||||
|
||||
onMounted(() => {
|
||||
initializeMenu()
|
||||
initMenu()
|
||||
if (props.mode === 'horizontal') {
|
||||
new Menubar(instance.vnode.el)
|
||||
new Menubar(instance.vnode.el!)
|
||||
}
|
||||
})
|
||||
|
||||
expose({
|
||||
open,
|
||||
close,
|
||||
})
|
||||
{
|
||||
const open = (index: string) => {
|
||||
const { indexPath } = subMenus.value[index]
|
||||
indexPath.forEach((i) => openMenu(i, indexPath))
|
||||
}
|
||||
expose({
|
||||
open,
|
||||
close: closeMenu,
|
||||
})
|
||||
}
|
||||
|
||||
const flattedChildren = (children) => {
|
||||
const temp = Array.isArray(children) ? children : [children]
|
||||
const res = []
|
||||
temp.forEach((child) => {
|
||||
const flattedChildren = (children: VNodeNormalizedChildren) => {
|
||||
const vnodes = Array.isArray(children) ? children : [children]
|
||||
const result: any[] = []
|
||||
vnodes.forEach((child: any) => {
|
||||
if (Array.isArray(child.children)) {
|
||||
res.push(...flattedChildren(child.children))
|
||||
result.push(...flattedChildren(child.children))
|
||||
} else {
|
||||
res.push(child)
|
||||
result.push(child)
|
||||
}
|
||||
})
|
||||
return res
|
||||
return result
|
||||
}
|
||||
|
||||
const useVNodeResize = (vnode: VNode) =>
|
||||
@ -290,12 +312,10 @@ export default defineComponent({
|
||||
: vnode
|
||||
return () => {
|
||||
let slot = slots.default?.() ?? []
|
||||
const showMore = []
|
||||
const vShowMore: VNode[] = []
|
||||
|
||||
if (props.mode === 'horizontal' && menu.value) {
|
||||
const items = Array.from(
|
||||
(menu.value as Node | undefined)?.childNodes ?? []
|
||||
).filter(
|
||||
const items = Array.from(menu.value?.childNodes ?? []).filter(
|
||||
(item) => item.nodeName !== '#text' || item.nodeValue
|
||||
) as HTMLElement[]
|
||||
const originalSlot = flattedChildren(slot)
|
||||
@ -317,11 +337,11 @@ export default defineComponent({
|
||||
sliceIndex = index + 1
|
||||
}
|
||||
})
|
||||
const defaultSlot = originalSlot.slice(0, sliceIndex)
|
||||
const moreSlot = originalSlot.slice(sliceIndex)
|
||||
if (moreSlot?.length) {
|
||||
slot = defaultSlot
|
||||
showMore.push(
|
||||
const slotDefault = originalSlot.slice(0, sliceIndex)
|
||||
const slotMore = originalSlot.slice(sliceIndex)
|
||||
if (slotMore?.length) {
|
||||
slot = slotDefault
|
||||
vShowMore.push(
|
||||
h(
|
||||
ElSubMenu,
|
||||
{
|
||||
@ -333,7 +353,7 @@ export default defineComponent({
|
||||
h('i', {
|
||||
class: ['el-icon-more', 'el-sub-menu__icon-more'],
|
||||
}),
|
||||
default: () => moreSlot,
|
||||
default: () => slotMore,
|
||||
}
|
||||
)
|
||||
)
|
||||
@ -342,7 +362,7 @@ export default defineComponent({
|
||||
|
||||
const ulStyle = useMenuCssVar(props)
|
||||
|
||||
const vnodeMenu = useVNodeResize(
|
||||
const vMenu = useVNodeResize(
|
||||
h(
|
||||
'ul',
|
||||
{
|
||||
@ -356,15 +376,15 @@ export default defineComponent({
|
||||
'el-menu--collapse': props.collapse,
|
||||
},
|
||||
},
|
||||
[...slot.map((vnode) => useVNodeResize(vnode)), ...showMore]
|
||||
[...slot.map((vnode) => useVNodeResize(vnode)), ...vShowMore]
|
||||
)
|
||||
)
|
||||
|
||||
if (props.collapseTransition && props.mode === 'vertical') {
|
||||
return h(ElMenuCollapseTransition, () => vnodeMenu)
|
||||
return h(ElMenuCollapseTransition, () => vMenu)
|
||||
}
|
||||
|
||||
return vnodeMenu
|
||||
return vMenu
|
||||
}
|
||||
},
|
||||
})
|
||||
|
@ -1,103 +0,0 @@
|
||||
import type { Ref, ComputedRef } from 'vue'
|
||||
|
||||
export interface RegisterMenuItem {
|
||||
index: string
|
||||
indexPath: ComputedRef<string[]>
|
||||
active: ComputedRef<boolean>
|
||||
}
|
||||
|
||||
export interface RootMenuData {
|
||||
activeIndex: string
|
||||
openedMenus: unknown[]
|
||||
items: any
|
||||
submenus: any
|
||||
isMenuPopup: boolean
|
||||
}
|
||||
|
||||
export interface RootMenuProvider {
|
||||
openedMenus: Ref<RootMenuData['openedMenus']>
|
||||
items: Ref<RootMenuData['items']>
|
||||
submenus: Ref<RootMenuData['submenus']>
|
||||
activeIndex: Ref<RootMenuData['activeIndex']>
|
||||
hoverBackground: Ref<string>
|
||||
isMenuPopup: Ref<RootMenuData['isMenuPopup']>
|
||||
|
||||
props: Readonly<Partial<RootMenuProps>>
|
||||
methods: {
|
||||
addMenuItem: (item: RegisterMenuItem) => void
|
||||
removeMenuItem: (item: RegisterMenuItem) => void
|
||||
addSubMenu: (item: RegisterMenuItem) => void
|
||||
removeSubMenu: (item: RegisterMenuItem) => void
|
||||
openMenu: (index: string, indexPath: Ref<string[]>) => void
|
||||
closeMenu: (index: string) => void
|
||||
handleMenuItemClick: (item: {
|
||||
index: string
|
||||
indexPath: ComputedRef<string[]>
|
||||
route?: any
|
||||
}) => void
|
||||
handleSubMenuClick: (submenu: {
|
||||
index: string
|
||||
indexPath: ComputedRef<string[]>
|
||||
}) => void
|
||||
}
|
||||
}
|
||||
|
||||
export interface SubMenuProvider {
|
||||
addSubMenu: (item: RegisterMenuItem) => void
|
||||
removeSubMenu: (item: RegisterMenuItem) => void
|
||||
handleMouseleave?: (deepDispatch: boolean) => void
|
||||
}
|
||||
|
||||
// root menu
|
||||
export interface IMenuProps {
|
||||
mode?: string | 'vertical' | 'horizontal'
|
||||
defaultActive?: string
|
||||
defaultOpeneds?: unknown[]
|
||||
uniqueOpened?: boolean
|
||||
router?: boolean
|
||||
menuTrigger?: string | 'hover' | 'click'
|
||||
collapse?: boolean
|
||||
backgroundColor?: string
|
||||
textColor?: string
|
||||
activeTextColor?: string
|
||||
collapseTransition?: boolean
|
||||
}
|
||||
|
||||
export interface RootMenuProps {
|
||||
mode: string
|
||||
defaultActive: string
|
||||
defaultOpeneds: unknown[]
|
||||
uniqueOpened: boolean
|
||||
router: boolean
|
||||
menuTrigger: string
|
||||
collapse: boolean
|
||||
backgroundColor: string
|
||||
textColor: string
|
||||
activeTextColor: string
|
||||
collapseTransition: boolean
|
||||
}
|
||||
|
||||
// submenu
|
||||
|
||||
export interface ISubMenuProps {
|
||||
index: string
|
||||
showTimeout?: number
|
||||
hideTimeout?: number
|
||||
popperClass?: string
|
||||
disabled?: boolean
|
||||
popperAppendToBody?: boolean
|
||||
}
|
||||
|
||||
// menuItem
|
||||
|
||||
export interface IMenuItemProps {
|
||||
index: string
|
||||
route: string | Record<string, unknown>
|
||||
disabled: boolean
|
||||
}
|
||||
|
||||
// menuGroup
|
||||
|
||||
export interface IMenuGroupProps {
|
||||
title: string
|
||||
}
|
@ -1,128 +0,0 @@
|
||||
<template>
|
||||
<li
|
||||
class="el-menu-item"
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
:style="paddingStyle"
|
||||
:class="{
|
||||
'is-active': active,
|
||||
'is-disabled': disabled,
|
||||
}"
|
||||
@click="handleClick"
|
||||
>
|
||||
<el-tooltip
|
||||
v-if="
|
||||
parentMenu.type.name === 'ElMenu' &&
|
||||
rootMenu.props.collapse &&
|
||||
slots.title
|
||||
"
|
||||
:effect="Effect.DARK"
|
||||
placement="right"
|
||||
>
|
||||
<template #content>
|
||||
<slot name="title"></slot>
|
||||
</template>
|
||||
<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 lang="ts">
|
||||
import {
|
||||
defineComponent,
|
||||
computed,
|
||||
onMounted,
|
||||
onBeforeUnmount,
|
||||
inject,
|
||||
getCurrentInstance,
|
||||
} from 'vue'
|
||||
import ElTooltip from '@element-plus/components/tooltip'
|
||||
import { Effect } from '@element-plus/components/popper'
|
||||
import useMenu from './use-menu'
|
||||
|
||||
import type { RootMenuProvider, SubMenuProvider } from './menu.type'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ElMenuItem',
|
||||
|
||||
// componentName: 'ElMenuItem',
|
||||
|
||||
components: { ElTooltip },
|
||||
|
||||
props: {
|
||||
index: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
route: [String, Object],
|
||||
disabled: Boolean,
|
||||
},
|
||||
emits: ['click'],
|
||||
setup(props, { emit, slots }) {
|
||||
const instance = getCurrentInstance()
|
||||
const rootMenu = inject<RootMenuProvider>('rootMenu')
|
||||
const { parentMenu, paddingStyle, indexPath } = useMenu(
|
||||
instance,
|
||||
computed(() => props.index)
|
||||
)
|
||||
const { addSubMenu, removeSubMenu } = inject<SubMenuProvider>(
|
||||
`subMenu:${parentMenu.value.uid}`
|
||||
)
|
||||
|
||||
const active = computed(() => {
|
||||
return props.index === rootMenu.activeIndex.value
|
||||
})
|
||||
|
||||
const handleClick = () => {
|
||||
if (!props.disabled) {
|
||||
rootMenu.methods.handleMenuItemClick({
|
||||
index: props.index,
|
||||
indexPath,
|
||||
route: props.route,
|
||||
})
|
||||
emit('click', {
|
||||
index: props.index,
|
||||
indexPath: indexPath.value,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
addSubMenu({ index: props.index, indexPath, active })
|
||||
rootMenu.methods.addMenuItem({ index: props.index, indexPath, active })
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
removeSubMenu({ index: props.index, indexPath, active })
|
||||
rootMenu.methods.removeMenuItem({ index: props.index, indexPath, active })
|
||||
})
|
||||
|
||||
return {
|
||||
Effect,
|
||||
parentMenu,
|
||||
rootMenu,
|
||||
slots,
|
||||
|
||||
paddingStyle,
|
||||
active,
|
||||
handleClick,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
@ -1,67 +0,0 @@
|
||||
<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 lang="ts">
|
||||
import {
|
||||
defineComponent,
|
||||
computed,
|
||||
getCurrentInstance,
|
||||
inject,
|
||||
reactive,
|
||||
} from 'vue'
|
||||
|
||||
import type { IMenuGroupProps, RootMenuProvider } from './menu.type'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ElMenuItemGroup',
|
||||
componentName: 'ElMenuItemGroup',
|
||||
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
setup(props: IMenuGroupProps, { slots }) {
|
||||
// data
|
||||
const data = reactive({
|
||||
paddingLeft: 20,
|
||||
})
|
||||
const instance = getCurrentInstance()
|
||||
// computed
|
||||
const levelPadding = computed(() => {
|
||||
let padding = 20
|
||||
let parent = instance.parent
|
||||
if (rootProps.collapse) return 20
|
||||
while (parent && parent.type.name !== 'ElMenu') {
|
||||
if (parent.type.name === 'ElSubMenu') {
|
||||
padding += 20
|
||||
}
|
||||
parent = parent.parent
|
||||
}
|
||||
return padding
|
||||
})
|
||||
|
||||
// inject
|
||||
const { props: rootProps } = inject<RootMenuProvider>('rootMenu')
|
||||
|
||||
return {
|
||||
data,
|
||||
levelPadding,
|
||||
props,
|
||||
slots,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
422
packages/components/menu/src/sub-menu.ts
Normal file
422
packages/components/menu/src/sub-menu.ts
Normal file
@ -0,0 +1,422 @@
|
||||
import {
|
||||
defineComponent,
|
||||
computed,
|
||||
ref,
|
||||
provide,
|
||||
inject,
|
||||
getCurrentInstance,
|
||||
watch,
|
||||
onMounted,
|
||||
onBeforeUnmount,
|
||||
withDirectives,
|
||||
Fragment,
|
||||
vShow,
|
||||
h,
|
||||
reactive,
|
||||
} from 'vue'
|
||||
import { useTimeoutFn } from '@vueuse/core'
|
||||
import ElCollapseTransition from '@element-plus/components/collapse-transition'
|
||||
import ElPopper from '@element-plus/components/popper'
|
||||
import { buildProp } from '@element-plus/utils/props'
|
||||
import { throwError } from '@element-plus/utils/error'
|
||||
import useMenu from './use-menu'
|
||||
import { useMenuCssVar } from './use-menu-css-var'
|
||||
import type { Placement } from '@element-plus/components/popper'
|
||||
|
||||
import type { ExtractPropTypes, VNodeArrayChildren, CSSProperties } from 'vue'
|
||||
import type { MenuProvider, SubMenuProvider } from './types'
|
||||
|
||||
export const subMenuProps = {
|
||||
index: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
showTimeout: buildProp({
|
||||
type: Number,
|
||||
default: 300,
|
||||
} as const),
|
||||
hideTimeout: buildProp({
|
||||
type: Number,
|
||||
default: 300,
|
||||
} as const),
|
||||
popperClass: String,
|
||||
disabled: Boolean,
|
||||
popperAppendToBody: buildProp({
|
||||
type: Boolean,
|
||||
default: undefined,
|
||||
} as const),
|
||||
} as const
|
||||
export type SubMenuProps = ExtractPropTypes<typeof subMenuProps>
|
||||
|
||||
const COMPONENT_NAME = 'ElSubMenu'
|
||||
export default defineComponent({
|
||||
name: COMPONENT_NAME,
|
||||
props: subMenuProps,
|
||||
|
||||
setup(props, { slots, expose }) {
|
||||
const instance = getCurrentInstance()!
|
||||
const { paddingStyle, indexPath, parentMenu } = useMenu(
|
||||
instance,
|
||||
computed(() => props.index)
|
||||
)
|
||||
|
||||
// inject
|
||||
const rootMenu = inject<MenuProvider>('rootMenu')
|
||||
if (!rootMenu) throwError(COMPONENT_NAME, 'can not inject root menu')
|
||||
|
||||
const subMenu = inject<SubMenuProvider>(`subMenu:${parentMenu.value!.uid}`)
|
||||
if (!subMenu) throwError(COMPONENT_NAME, 'can not inject sub menu')
|
||||
|
||||
const items = ref<MenuProvider['items']>({})
|
||||
const subMenus = ref<MenuProvider['subMenus']>({})
|
||||
|
||||
let timeout: (() => void) | undefined
|
||||
const currentPlacement = ref<Placement | ''>('')
|
||||
const mouseInChild = ref(false)
|
||||
const verticalTitleRef = ref<HTMLDivElement>()
|
||||
const vPopper = ref()
|
||||
|
||||
// computed
|
||||
const subMenuTitleIcon = computed(() => {
|
||||
return (mode.value === 'horizontal' && isFirstLevel.value) ||
|
||||
(mode.value === 'vertical' && !rootMenu.props.collapse)
|
||||
? 'el-icon-arrow-down'
|
||||
: 'el-icon-arrow-right'
|
||||
})
|
||||
const isFirstLevel = computed(() => {
|
||||
let isFirstLevel = true
|
||||
let parent = instance.parent
|
||||
while (parent && parent.type.name !== 'ElMenu') {
|
||||
if (['ElSubMenu', 'ElMenuItemGroup'].includes(parent.type.name!)) {
|
||||
isFirstLevel = false
|
||||
break
|
||||
} else {
|
||||
parent = parent.parent
|
||||
}
|
||||
}
|
||||
return isFirstLevel
|
||||
})
|
||||
const appendToBody = computed(() => {
|
||||
return props.popperAppendToBody === undefined
|
||||
? isFirstLevel.value
|
||||
: Boolean(props.popperAppendToBody)
|
||||
})
|
||||
const menuTransitionName = computed(() =>
|
||||
rootMenu.props.collapse ? 'el-zoom-in-left' : 'el-zoom-in-top'
|
||||
)
|
||||
const fallbackPlacements = computed<Placement[]>(() =>
|
||||
mode.value === 'horizontal' && isFirstLevel.value
|
||||
? [
|
||||
'bottom-start',
|
||||
'bottom-end',
|
||||
'top-start',
|
||||
'top-end',
|
||||
'right-start',
|
||||
'left-start',
|
||||
]
|
||||
: [
|
||||
'right-start',
|
||||
'left-start',
|
||||
'bottom-start',
|
||||
'bottom-end',
|
||||
'top-start',
|
||||
'top-end',
|
||||
]
|
||||
)
|
||||
const opened = computed(() => rootMenu.openedMenus.includes(props.index))
|
||||
const active = computed(() => {
|
||||
let isActive = false
|
||||
|
||||
Object.values(items.value).forEach((item) => {
|
||||
if (item.active) {
|
||||
isActive = true
|
||||
}
|
||||
})
|
||||
|
||||
Object.values(subMenus.value).forEach((subItem) => {
|
||||
if (subItem.active) {
|
||||
isActive = true
|
||||
}
|
||||
})
|
||||
|
||||
return isActive
|
||||
})
|
||||
|
||||
const backgroundColor = computed(() => rootMenu.props.backgroundColor || '')
|
||||
const activeTextColor = computed(() => rootMenu.props.activeTextColor || '')
|
||||
const textColor = computed(() => rootMenu.props.textColor || '')
|
||||
const mode = computed(() => rootMenu.props.mode)
|
||||
const item = reactive({
|
||||
index: props.index,
|
||||
indexPath,
|
||||
active,
|
||||
})
|
||||
|
||||
const titleStyle = computed<CSSProperties>(() => {
|
||||
if (mode.value !== 'horizontal') {
|
||||
return {
|
||||
color: textColor.value,
|
||||
}
|
||||
}
|
||||
return {
|
||||
borderBottomColor: active.value
|
||||
? rootMenu.props.activeTextColor
|
||||
? activeTextColor.value
|
||||
: ''
|
||||
: 'transparent',
|
||||
color: active.value ? activeTextColor.value : textColor.value,
|
||||
}
|
||||
})
|
||||
|
||||
// methods
|
||||
const doDestroy = () => vPopper.value?.doDestroy()
|
||||
|
||||
const handleCollapseToggle = (value: boolean) => {
|
||||
if (value) {
|
||||
updatePlacement()
|
||||
} else {
|
||||
doDestroy()
|
||||
}
|
||||
}
|
||||
|
||||
const handleClick = () => {
|
||||
if (
|
||||
(rootMenu.props.menuTrigger === 'hover' &&
|
||||
rootMenu.props.mode === 'horizontal') ||
|
||||
(rootMenu.props.collapse && rootMenu.props.mode === 'vertical') ||
|
||||
props.disabled
|
||||
)
|
||||
return
|
||||
|
||||
rootMenu.handleSubMenuClick({
|
||||
index: props.index,
|
||||
indexPath: indexPath.value,
|
||||
active: active.value,
|
||||
})
|
||||
}
|
||||
|
||||
const handleMouseenter = (
|
||||
event: MouseEvent | FocusEvent,
|
||||
showTimeout = props.showTimeout
|
||||
) => {
|
||||
if (event.type === 'focus' && !event.relatedTarget) {
|
||||
return
|
||||
}
|
||||
if (
|
||||
(rootMenu.props.menuTrigger === 'click' &&
|
||||
rootMenu.props.mode === 'horizontal') ||
|
||||
(!rootMenu.props.collapse && rootMenu.props.mode === 'vertical') ||
|
||||
props.disabled
|
||||
) {
|
||||
return
|
||||
}
|
||||
mouseInChild.value = true
|
||||
|
||||
timeout?.()
|
||||
;({ stop: timeout } = useTimeoutFn(
|
||||
() => rootMenu.openMenu(props.index, indexPath.value),
|
||||
showTimeout
|
||||
))
|
||||
|
||||
if (appendToBody.value) {
|
||||
parentMenu.value.vnode.el?.dispatchEvent(new MouseEvent('mouseenter'))
|
||||
}
|
||||
}
|
||||
|
||||
const handleMouseleave = (deepDispatch = false) => {
|
||||
if (
|
||||
(rootMenu.props.menuTrigger === 'click' &&
|
||||
rootMenu.props.mode === 'horizontal') ||
|
||||
(!rootMenu.props.collapse && rootMenu.props.mode === 'vertical')
|
||||
) {
|
||||
return
|
||||
}
|
||||
mouseInChild.value = false
|
||||
timeout?.()
|
||||
;({ stop: timeout } = useTimeoutFn(
|
||||
() => !mouseInChild.value && rootMenu.closeMenu(props.index),
|
||||
props.hideTimeout
|
||||
))
|
||||
|
||||
if (appendToBody.value && deepDispatch) {
|
||||
if (instance.parent?.type.name === 'ElSubMenu') {
|
||||
subMenu.handleMouseleave?.(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const updatePlacement = () => {
|
||||
currentPlacement.value =
|
||||
mode.value === 'horizontal' && isFirstLevel.value
|
||||
? 'bottom-start'
|
||||
: 'right-start'
|
||||
}
|
||||
|
||||
watch(
|
||||
() => rootMenu.props.collapse,
|
||||
(value) => handleCollapseToggle(Boolean(value))
|
||||
)
|
||||
|
||||
// provide
|
||||
{
|
||||
const addSubMenu: SubMenuProvider['addSubMenu'] = (item) => {
|
||||
subMenus.value[item.index] = item
|
||||
}
|
||||
const removeSubMenu: SubMenuProvider['removeSubMenu'] = (item) => {
|
||||
delete subMenus.value[item.index]
|
||||
}
|
||||
provide<SubMenuProvider>(`subMenu:${instance.uid}`, {
|
||||
addSubMenu,
|
||||
removeSubMenu,
|
||||
handleMouseleave,
|
||||
})
|
||||
}
|
||||
|
||||
// expose
|
||||
expose({
|
||||
opened,
|
||||
})
|
||||
|
||||
// lifecycle
|
||||
onMounted(() => {
|
||||
rootMenu.addSubMenu(item)
|
||||
subMenu.addSubMenu(item)
|
||||
updatePlacement()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
subMenu.removeSubMenu(item)
|
||||
rootMenu.removeSubMenu(item)
|
||||
})
|
||||
|
||||
return () => {
|
||||
const titleTag: VNodeArrayChildren = [
|
||||
slots.title?.(),
|
||||
h('i', {
|
||||
class: ['el-sub-menu__icon-arrow', subMenuTitleIcon.value],
|
||||
}),
|
||||
]
|
||||
|
||||
const ulStyle = useMenuCssVar(rootMenu.props)
|
||||
|
||||
// this render function is only used for bypass `Vue`'s compiler caused patching issue.
|
||||
// temporarily mark ElPopper as any due to type inconsistency.
|
||||
const child = rootMenu.isMenuPopup
|
||||
? h(
|
||||
// TODO: correct popper's type.
|
||||
ElPopper as any,
|
||||
{
|
||||
ref: vPopper,
|
||||
manualMode: true,
|
||||
visible: opened.value,
|
||||
effect: 'light',
|
||||
pure: true,
|
||||
offset: 6,
|
||||
showArrow: false,
|
||||
popperClass: props.popperClass,
|
||||
placement: currentPlacement.value,
|
||||
appendToBody: appendToBody.value,
|
||||
fallbackPlacements: fallbackPlacements.value,
|
||||
transition: menuTransitionName.value,
|
||||
gpuAcceleration: false,
|
||||
},
|
||||
{
|
||||
default: () =>
|
||||
h(
|
||||
'div',
|
||||
{
|
||||
class: [`el-menu--${mode.value}`, props.popperClass],
|
||||
onMouseenter: (evt: MouseEvent) =>
|
||||
handleMouseenter(evt, 100),
|
||||
onMouseleave: () => handleMouseleave(true),
|
||||
onFocus: (evt: FocusEvent) => handleMouseenter(evt, 100),
|
||||
},
|
||||
[
|
||||
h(
|
||||
'ul',
|
||||
{
|
||||
class: [
|
||||
'el-menu el-menu--popup',
|
||||
`el-menu--popup-${currentPlacement.value}`,
|
||||
],
|
||||
style: ulStyle.value,
|
||||
},
|
||||
[slots.default?.()]
|
||||
),
|
||||
]
|
||||
),
|
||||
trigger: () =>
|
||||
h(
|
||||
'div',
|
||||
{
|
||||
class: 'el-sub-menu__title',
|
||||
style: [
|
||||
paddingStyle.value,
|
||||
titleStyle.value,
|
||||
{ backgroundColor: backgroundColor.value },
|
||||
],
|
||||
onClick: handleClick,
|
||||
},
|
||||
titleTag
|
||||
),
|
||||
}
|
||||
)
|
||||
: h(Fragment, {}, [
|
||||
h(
|
||||
'div',
|
||||
{
|
||||
class: 'el-sub-menu__title',
|
||||
style: [
|
||||
paddingStyle.value,
|
||||
titleStyle.value,
|
||||
{ backgroundColor: backgroundColor.value },
|
||||
],
|
||||
ref: verticalTitleRef,
|
||||
onClick: handleClick,
|
||||
},
|
||||
titleTag
|
||||
),
|
||||
h(
|
||||
ElCollapseTransition,
|
||||
{},
|
||||
{
|
||||
default: () =>
|
||||
withDirectives(
|
||||
h(
|
||||
'ul',
|
||||
{
|
||||
role: 'menu',
|
||||
class: 'el-menu el-menu--inline',
|
||||
style: ulStyle.value,
|
||||
},
|
||||
[slots.default?.()]
|
||||
),
|
||||
[[vShow, opened.value]]
|
||||
),
|
||||
}
|
||||
),
|
||||
])
|
||||
|
||||
return h(
|
||||
'li',
|
||||
{
|
||||
class: [
|
||||
'el-sub-menu',
|
||||
{
|
||||
'is-active': active.value,
|
||||
'is-opened': opened.value,
|
||||
'is-disabled': props.disabled,
|
||||
},
|
||||
],
|
||||
role: 'menuitem',
|
||||
ariaHaspopup: true,
|
||||
ariaExpanded: opened.value,
|
||||
onMouseenter: handleMouseenter,
|
||||
onMouseleave: () => handleMouseleave(true),
|
||||
onFocus: handleMouseenter,
|
||||
},
|
||||
[child]
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
@ -1,483 +0,0 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
defineComponent,
|
||||
computed,
|
||||
ref,
|
||||
provide,
|
||||
inject,
|
||||
getCurrentInstance,
|
||||
reactive,
|
||||
watch,
|
||||
onMounted,
|
||||
onBeforeUnmount,
|
||||
withDirectives,
|
||||
Fragment,
|
||||
vShow,
|
||||
h,
|
||||
} from 'vue'
|
||||
import ElCollapseTransition from '@element-plus/components/collapse-transition'
|
||||
import ElPopper from '@element-plus/components/popper'
|
||||
import useMenu from './use-menu'
|
||||
|
||||
import { useMenuCssVar } from './use-menu-css-var'
|
||||
import type {
|
||||
ISubMenuProps,
|
||||
RootMenuProvider,
|
||||
SubMenuProvider,
|
||||
} from './menu.type'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ElSubMenu',
|
||||
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,
|
||||
},
|
||||
},
|
||||
setup(props: ISubMenuProps) {
|
||||
const data = reactive({
|
||||
popperJS: null,
|
||||
timeout: null,
|
||||
items: {},
|
||||
submenus: {},
|
||||
currentPlacement: '',
|
||||
mouseInChild: false,
|
||||
opened: false,
|
||||
})
|
||||
const verticalTitleRef = ref<HTMLElement>(null)
|
||||
const popperVnode = ref(null)
|
||||
|
||||
// instance
|
||||
const instance = getCurrentInstance()
|
||||
const { paddingStyle, indexPath, parentMenu } = useMenu(
|
||||
instance,
|
||||
computed(() => props.index)
|
||||
)
|
||||
|
||||
// inject
|
||||
const {
|
||||
openedMenus,
|
||||
isMenuPopup,
|
||||
methods: rootMethods,
|
||||
props: rootProps,
|
||||
methods: { closeMenu },
|
||||
} = inject<RootMenuProvider>('rootMenu')
|
||||
|
||||
const {
|
||||
addSubMenu: parentAddSubMenu,
|
||||
removeSubMenu: parentRemoveSubMenu,
|
||||
handleMouseleave: parentHandleMouseleave,
|
||||
} = inject<SubMenuProvider>(`subMenu:${parentMenu.value.uid}`)
|
||||
|
||||
// computed
|
||||
const submenuTitleIcon = computed(() => {
|
||||
return (mode.value === 'horizontal' && isFirstLevel.value) ||
|
||||
(mode.value === 'vertical' && !rootProps.collapse)
|
||||
? 'el-icon-arrow-down'
|
||||
: 'el-icon-arrow-right'
|
||||
})
|
||||
const isFirstLevel = computed(() => {
|
||||
let isFirstLevel = true
|
||||
let parent = instance.parent
|
||||
while (parent && parent.type.name !== 'ElMenu') {
|
||||
if (['ElSubMenu', 'ElMenuItemGroup'].includes(parent.type.name)) {
|
||||
isFirstLevel = false
|
||||
break
|
||||
} else {
|
||||
parent = parent.parent
|
||||
}
|
||||
}
|
||||
return isFirstLevel
|
||||
})
|
||||
const appendToBody = computed(() => {
|
||||
return props.popperAppendToBody === undefined
|
||||
? isFirstLevel.value
|
||||
: Boolean(props.popperAppendToBody)
|
||||
})
|
||||
const menuTransitionName = computed(() => {
|
||||
return rootProps.collapse ? 'el-zoom-in-left' : 'el-zoom-in-top'
|
||||
})
|
||||
const fallbackPlacements = computed(() =>
|
||||
mode.value === 'horizontal' && isFirstLevel.value
|
||||
? [
|
||||
'bottom-start',
|
||||
'bottom-end',
|
||||
'top-start',
|
||||
'top-end',
|
||||
'right-start',
|
||||
'left-start',
|
||||
]
|
||||
: [
|
||||
'right-start',
|
||||
'left-start',
|
||||
'bottom-start',
|
||||
'bottom-end',
|
||||
'top-start',
|
||||
'top-end',
|
||||
]
|
||||
)
|
||||
const opened = computed(() => {
|
||||
return openedMenus.value.includes(props.index)
|
||||
})
|
||||
const active = computed(() => {
|
||||
let isActive = false
|
||||
const submenus = data.submenus
|
||||
const items = data.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
|
||||
})
|
||||
|
||||
const backgroundColor = computed(() => {
|
||||
return rootProps.backgroundColor || ''
|
||||
})
|
||||
const activeTextColor = computed(() => {
|
||||
return rootProps.activeTextColor || ''
|
||||
})
|
||||
const textColor = computed(() => {
|
||||
return rootProps.textColor || ''
|
||||
})
|
||||
const mode = computed(() => {
|
||||
return rootProps.mode
|
||||
})
|
||||
const titleStyle = computed(() => {
|
||||
if (mode.value !== 'horizontal') {
|
||||
return {
|
||||
color: textColor.value,
|
||||
}
|
||||
}
|
||||
return {
|
||||
borderBottomColor: active.value
|
||||
? rootProps.activeTextColor
|
||||
? activeTextColor.value
|
||||
: ''
|
||||
: 'transparent',
|
||||
color: active.value ? activeTextColor.value : textColor.value,
|
||||
}
|
||||
})
|
||||
|
||||
const doDestroy = () => {
|
||||
popperVnode.value?.doDestroy()
|
||||
}
|
||||
|
||||
// methods
|
||||
|
||||
const handleCollapseToggle = (value) => {
|
||||
if (value) {
|
||||
updatePlacement()
|
||||
} else {
|
||||
doDestroy()
|
||||
}
|
||||
}
|
||||
const addItem = (item) => {
|
||||
data.items[item.index] = item
|
||||
}
|
||||
const removeItem = (item) => {
|
||||
delete data.items[item.index]
|
||||
}
|
||||
const addSubMenu = (item) => {
|
||||
data.submenus[item.index] = item
|
||||
}
|
||||
const removeSubMenu = (item) => {
|
||||
delete data.submenus[item.index]
|
||||
}
|
||||
const handleClick = () => {
|
||||
const disabled = props.disabled
|
||||
if (
|
||||
(rootProps.menuTrigger === 'hover' &&
|
||||
rootProps.mode === 'horizontal') ||
|
||||
(rootProps.collapse && rootProps.mode === 'vertical') ||
|
||||
disabled
|
||||
) {
|
||||
return
|
||||
}
|
||||
rootMethods.handleSubMenuClick({ index: props.index, indexPath })
|
||||
}
|
||||
const handleMouseenter = (event, showTimeout = props.showTimeout) => {
|
||||
if (
|
||||
!('ActiveXObject' in window) &&
|
||||
event.type === 'focus' &&
|
||||
!event.relatedTarget
|
||||
) {
|
||||
return
|
||||
}
|
||||
const disabled = props.disabled
|
||||
if (
|
||||
(rootProps.menuTrigger === 'click' &&
|
||||
rootProps.mode === 'horizontal') ||
|
||||
(!rootProps.collapse && rootProps.mode === 'vertical') ||
|
||||
disabled
|
||||
) {
|
||||
return
|
||||
}
|
||||
data.mouseInChild = true
|
||||
clearTimeout(data.timeout)
|
||||
data.timeout = setTimeout(() => {
|
||||
rootMethods.openMenu(props.index, indexPath)
|
||||
}, showTimeout)
|
||||
|
||||
if (appendToBody.value) {
|
||||
parentMenu.value.vnode.el.dispatchEvent(new MouseEvent('mouseenter'))
|
||||
}
|
||||
}
|
||||
const handleMouseleave = (deepDispatch = false) => {
|
||||
if (
|
||||
(rootProps.menuTrigger === 'click' &&
|
||||
rootProps.mode === 'horizontal') ||
|
||||
(!rootProps.collapse && rootProps.mode === 'vertical')
|
||||
) {
|
||||
return
|
||||
}
|
||||
data.mouseInChild = false
|
||||
clearTimeout(data.timeout)
|
||||
data.timeout = setTimeout(() => {
|
||||
!data.mouseInChild && closeMenu(props.index)
|
||||
}, props.hideTimeout)
|
||||
|
||||
if (appendToBody.value && deepDispatch) {
|
||||
if (instance.parent.type.name === 'ElSubMenu') {
|
||||
parentHandleMouseleave(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const updatePlacement = () => {
|
||||
data.currentPlacement =
|
||||
mode.value === 'horizontal' && isFirstLevel.value
|
||||
? 'bottom-start'
|
||||
: 'right-start'
|
||||
}
|
||||
|
||||
watch(
|
||||
() => rootProps.collapse,
|
||||
(value) => {
|
||||
handleCollapseToggle(Boolean(value))
|
||||
}
|
||||
)
|
||||
|
||||
// provide
|
||||
provide<SubMenuProvider>(`subMenu:${instance.uid}`, {
|
||||
addSubMenu,
|
||||
removeSubMenu,
|
||||
handleMouseleave,
|
||||
})
|
||||
|
||||
// lifecycle
|
||||
onMounted(() => {
|
||||
rootMethods.addSubMenu({
|
||||
index: props.index,
|
||||
indexPath,
|
||||
active,
|
||||
})
|
||||
parentAddSubMenu({
|
||||
index: props.index,
|
||||
indexPath,
|
||||
active,
|
||||
})
|
||||
updatePlacement()
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
parentRemoveSubMenu({
|
||||
index: props.index,
|
||||
indexPath,
|
||||
active,
|
||||
})
|
||||
rootMethods.removeSubMenu({
|
||||
index: props.index,
|
||||
indexPath,
|
||||
active,
|
||||
})
|
||||
})
|
||||
|
||||
return {
|
||||
data,
|
||||
props,
|
||||
mode,
|
||||
active,
|
||||
isMenuPopup,
|
||||
opened,
|
||||
paddingStyle,
|
||||
titleStyle,
|
||||
backgroundColor,
|
||||
rootProps,
|
||||
menuTransitionName,
|
||||
fallbackPlacements,
|
||||
submenuTitleIcon,
|
||||
appendToBody,
|
||||
|
||||
handleClick,
|
||||
handleMouseenter,
|
||||
handleMouseleave,
|
||||
|
||||
addItem,
|
||||
removeItem,
|
||||
addSubMenu,
|
||||
removeSubMenu,
|
||||
|
||||
popperVnode,
|
||||
verticalTitleRef,
|
||||
}
|
||||
},
|
||||
render() {
|
||||
const titleTag = [
|
||||
this.$slots.title?.(),
|
||||
h(
|
||||
'i',
|
||||
{
|
||||
class: ['el-sub-menu__icon-arrow', this.submenuTitleIcon],
|
||||
},
|
||||
null
|
||||
),
|
||||
]
|
||||
|
||||
const ulStyle = useMenuCssVar(this.rootProps)
|
||||
|
||||
// this render function is only used for bypass `Vue`'s compiler caused patching issue.
|
||||
// temporaryly mark ElPopper as any due to type inconsistency.
|
||||
// TODO: correct popper's type.
|
||||
const child = this.isMenuPopup
|
||||
? h(
|
||||
ElPopper as any,
|
||||
{
|
||||
ref: 'popperVNode',
|
||||
manualMode: true,
|
||||
visible: this.opened,
|
||||
'onUpdate:visible': (val: boolean) => (this.opened = val),
|
||||
effect: 'light',
|
||||
pure: true,
|
||||
offset: 6,
|
||||
showArrow: false,
|
||||
popperClass: this.popperClass,
|
||||
placement: this.data.currentPlacement,
|
||||
appendToBody: this.appendToBody,
|
||||
fallbackPlacements: this.fallbackPlacements,
|
||||
transition: this.menuTransitionName,
|
||||
gpuAcceleration: false,
|
||||
},
|
||||
{
|
||||
default: () =>
|
||||
h(
|
||||
'div',
|
||||
{
|
||||
ref: 'menu',
|
||||
class: [`el-menu--${this.mode}`, this.popperClass],
|
||||
onMouseenter: ($event: Event) =>
|
||||
this.handleMouseenter($event, 100),
|
||||
onMouseleave: () => this.handleMouseleave(true),
|
||||
onFocus: ($event: Event) =>
|
||||
this.handleMouseenter($event, 100),
|
||||
},
|
||||
[
|
||||
h(
|
||||
'ul',
|
||||
{
|
||||
class: [
|
||||
'el-menu el-menu--popup',
|
||||
`el-menu--popup-${this.data.currentPlacement}`,
|
||||
],
|
||||
style: ulStyle.value,
|
||||
},
|
||||
[this.$slots.default?.()]
|
||||
),
|
||||
]
|
||||
),
|
||||
trigger: () =>
|
||||
h(
|
||||
'div',
|
||||
{
|
||||
class: 'el-sub-menu__title',
|
||||
style: [
|
||||
this.paddingStyle,
|
||||
this.titleStyle,
|
||||
{ backgroundColor: this.backgroundColor },
|
||||
],
|
||||
onClick: this.handleClick,
|
||||
},
|
||||
titleTag
|
||||
),
|
||||
}
|
||||
)
|
||||
: h(Fragment, {}, [
|
||||
h(
|
||||
'div',
|
||||
{
|
||||
class: 'el-sub-menu__title',
|
||||
style: [
|
||||
this.paddingStyle,
|
||||
this.titleStyle,
|
||||
{ backgroundColor: this.backgroundColor },
|
||||
],
|
||||
ref: 'verticalTitleRef',
|
||||
onClick: this.handleClick,
|
||||
},
|
||||
titleTag
|
||||
),
|
||||
h(
|
||||
ElCollapseTransition,
|
||||
{},
|
||||
{
|
||||
default: () =>
|
||||
withDirectives(
|
||||
h(
|
||||
'ul',
|
||||
{
|
||||
role: 'menu',
|
||||
class: 'el-menu el-menu--inline',
|
||||
style: ulStyle.value,
|
||||
},
|
||||
[this.$slots.default?.()]
|
||||
),
|
||||
[[vShow, this.opened]]
|
||||
),
|
||||
}
|
||||
),
|
||||
])
|
||||
|
||||
return h(
|
||||
'li',
|
||||
{
|
||||
class: [
|
||||
'el-sub-menu',
|
||||
{
|
||||
'is-active': this.active,
|
||||
'is-opened': this.opened,
|
||||
'is-disabled': this.disabled,
|
||||
},
|
||||
],
|
||||
role: 'menuitem',
|
||||
ariaHaspopup: true,
|
||||
ariaExpanded: this.opened,
|
||||
onMouseenter: this.handleMouseenter,
|
||||
onMouseleave: () => this.handleMouseleave(true),
|
||||
onFocus: this.handleMouseenter,
|
||||
},
|
||||
[child]
|
||||
)
|
||||
},
|
||||
})
|
||||
</script>
|
39
packages/components/menu/src/types.ts
Normal file
39
packages/components/menu/src/types.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import type { RouteLocationRaw } from 'vue-router'
|
||||
import type { MenuProps } from './menu'
|
||||
|
||||
export interface MenuItemRegistered {
|
||||
index: string
|
||||
indexPath: string[]
|
||||
active: boolean
|
||||
}
|
||||
export interface MenuItemClicked {
|
||||
index: string
|
||||
indexPath: string[]
|
||||
route?: RouteLocationRaw
|
||||
}
|
||||
|
||||
export interface MenuProvider {
|
||||
openedMenus: string[]
|
||||
items: Record<string, MenuItemRegistered>
|
||||
subMenus: Record<string, MenuItemRegistered>
|
||||
activeIndex?: string
|
||||
isMenuPopup: boolean
|
||||
props: MenuProps
|
||||
|
||||
addMenuItem: (item: MenuItemRegistered) => void
|
||||
removeMenuItem: (item: MenuItemRegistered) => void
|
||||
addSubMenu: (item: MenuItemRegistered) => void
|
||||
removeSubMenu: (item: MenuItemRegistered) => void
|
||||
|
||||
openMenu: (index: string, indexPath: string[]) => void
|
||||
closeMenu: (index: string) => void
|
||||
|
||||
handleMenuItemClick: (item: MenuItemClicked) => void
|
||||
handleSubMenuClick: (subMenu: MenuItemRegistered) => void
|
||||
}
|
||||
|
||||
export interface SubMenuProvider {
|
||||
addSubMenu: (item: MenuItemRegistered) => void
|
||||
removeSubMenu: (item: MenuItemRegistered) => void
|
||||
handleMouseleave?: (deepDispatch: boolean) => void
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
import { computed } from 'vue'
|
||||
import { darken } from '@element-plus/utils/color'
|
||||
|
||||
import type { IMenuProps } from './menu.type'
|
||||
import type { MenuProps } from './menu'
|
||||
|
||||
export default function useMenuColor(props: IMenuProps) {
|
||||
export default function useMenuColor(props: MenuProps) {
|
||||
const menuBarColor = computed(() => {
|
||||
const color = props.backgroundColor
|
||||
if (!color) {
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { computed } from 'vue'
|
||||
import useMenuColor from './use-menu-color'
|
||||
|
||||
import type { IMenuProps } from './menu.type'
|
||||
import type { MenuProps } from './menu'
|
||||
|
||||
export const useMenuCssVar = (props: IMenuProps) => {
|
||||
export const useMenuCssVar = (props: MenuProps) => {
|
||||
return computed(() => {
|
||||
return {
|
||||
'--el-menu-text-color': props.textColor || '',
|
||||
|
@ -1,34 +1,36 @@
|
||||
import { computed, inject } from 'vue'
|
||||
import { throwError } from '@element-plus/utils/error'
|
||||
|
||||
import type { ComponentInternalInstance, ComputedRef } from 'vue'
|
||||
import type { RootMenuProvider } from './menu.type'
|
||||
import type { ComponentInternalInstance, Ref, CSSProperties } from 'vue'
|
||||
import type { MenuProvider } from './types'
|
||||
|
||||
export default function useMenu(
|
||||
instance: ComponentInternalInstance,
|
||||
currentIndex: ComputedRef<string>
|
||||
currentIndex: Ref<string>
|
||||
) {
|
||||
const rootMenu = inject<RootMenuProvider>('rootMenu')
|
||||
const rootMenu = inject<MenuProvider>('rootMenu')
|
||||
if (!rootMenu) throwError('useMenu', 'can not inject root menu')
|
||||
|
||||
const indexPath = computed(() => {
|
||||
let parent = instance.parent
|
||||
let parent = instance.parent!
|
||||
const path = [currentIndex.value]
|
||||
while (parent.type.name !== 'ElMenu') {
|
||||
if (parent.props.index) {
|
||||
path.unshift(parent.props.index as string)
|
||||
}
|
||||
parent = parent.parent
|
||||
parent = parent.parent!
|
||||
}
|
||||
return path
|
||||
})
|
||||
|
||||
const parentMenu = computed(() => {
|
||||
let parent = instance.parent
|
||||
while (parent && ['ElMenu', 'ElSubMenu'].indexOf(parent.type.name) === -1) {
|
||||
while (parent && !['ElMenu', 'ElSubMenu'].includes(parent.type.name!)) {
|
||||
parent = parent.parent
|
||||
}
|
||||
return parent
|
||||
return parent!
|
||||
})
|
||||
const paddingStyle = computed(() => {
|
||||
const paddingStyle = computed<CSSProperties>(() => {
|
||||
let parent = instance.parent
|
||||
if (rootMenu.props.mode !== 'vertical') return {}
|
||||
|
||||
@ -46,5 +48,10 @@ export default function useMenu(
|
||||
}
|
||||
return { paddingLeft: `${padding}px` }
|
||||
})
|
||||
return { parentMenu, paddingStyle, indexPath }
|
||||
|
||||
return {
|
||||
parentMenu,
|
||||
paddingStyle,
|
||||
indexPath,
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user