| | |
| | | /> |
| | | </div> |
| | | <CollapseTransition> |
| | | <ul :class="prefixCls" v-show="opened"> |
| | | <ul :class="prefixCls" v-show="state.opened"> |
| | | <slot></slot> |
| | | </ul> |
| | | </CollapseTransition> |
| | |
| | | placement="right" |
| | | :overlayClassName="`${prefixCls}-menu-popover`" |
| | | v-else |
| | | :visible="getIsOpend" |
| | | @visible-change="handleVisibleChange" |
| | | :open="getIsOpend" |
| | | @on-open-change="handleVisibleChange" |
| | | :overlayStyle="getOverlayStyle" |
| | | :overlayInnerStyle="{ padding: 0 }" |
| | | :align="{ offset: [0, 0] }" |
| | | > |
| | | <div :class="getSubClass" v-bind="getEvents(false)"> |
| | |
| | | /> |
| | | </div> |
| | | <!-- eslint-disable-next-line --> |
| | | <template #content v-show="opened"> |
| | | <template #content v-show="state.opened"> |
| | | <div v-bind="getEvents(true)"> |
| | | <ul :class="[prefixCls, `${prefixCls}-${getTheme}`, `${prefixCls}-popup`]"> |
| | | <slot></slot> |
| | |
| | | </li> |
| | | </template> |
| | | |
| | | <script lang="ts"> |
| | | <script lang="ts" setup> |
| | | import { type TimeoutHandle, type Recordable } from '@vben/types'; |
| | | import type { CSSProperties, PropType } from 'vue'; |
| | | import type { SubMenuProvider } from './types'; |
| | | import { |
| | | defineComponent, |
| | | computed, |
| | | unref, |
| | | getCurrentInstance, |
| | | toRefs, |
| | | reactive, |
| | | provide, |
| | | onBeforeMount, |
| | | inject, |
| | | } from 'vue'; |
| | | import { useDesign } from '/@/hooks/web/useDesign'; |
| | | import { propTypes } from '/@/utils/propTypes'; |
| | | import { useDesign } from '@/hooks/web/useDesign'; |
| | | import { propTypes } from '@/utils/propTypes'; |
| | | import { useMenuItem } from './useMenu'; |
| | | import { useSimpleRootMenuContext } from './useSimpleMenuContext'; |
| | | import { CollapseTransition } from '/@/components/Transition'; |
| | | import { CollapseTransition } from '@/components/Transition'; |
| | | import Icon from '@/components/Icon/Icon.vue'; |
| | | import { Popover } from 'ant-design-vue'; |
| | | import { isBoolean, isObject } from '/@/utils/is'; |
| | | import { mitt } from '/@/utils/mitt'; |
| | | import { isBoolean, isObject } from '@/utils/is'; |
| | | import { mitt } from '@/utils/mitt'; |
| | | |
| | | defineOptions({ name: 'SubMenu' }); |
| | | |
| | | const props = defineProps({ |
| | | name: { |
| | | type: [String, Number] as PropType<string | number>, |
| | | required: true, |
| | | }, |
| | | disabled: propTypes.bool, |
| | | collapsedShowTitle: propTypes.bool, |
| | | }); |
| | | |
| | | const DELAY = 200; |
| | | export default defineComponent({ |
| | | name: 'SubMenu', |
| | | components: { |
| | | Icon, |
| | | CollapseTransition, |
| | | Popover, |
| | | }, |
| | | props: { |
| | | name: { |
| | | type: [String, Number] as PropType<string | number>, |
| | | required: true, |
| | | const instance = getCurrentInstance(); |
| | | |
| | | const state = reactive({ |
| | | active: false, |
| | | opened: false, |
| | | }); |
| | | |
| | | const data = reactive({ |
| | | timeout: null as TimeoutHandle | null, |
| | | mouseInChild: false, |
| | | isChild: false, |
| | | }); |
| | | |
| | | const { getParentSubMenu, getItemStyle, getParentMenu, getParentList } = useMenuItem(instance); |
| | | |
| | | const { prefixCls } = useDesign('menu'); |
| | | |
| | | const subMenuEmitter = mitt(); |
| | | |
| | | const { rootMenuEmitter } = useSimpleRootMenuContext(); |
| | | |
| | | const { |
| | | addSubMenu: parentAddSubmenu, |
| | | removeSubMenu: parentRemoveSubmenu, |
| | | removeAll: parentRemoveAll, |
| | | getOpenNames: parentGetOpenNames, |
| | | isRemoveAllPopup, |
| | | sliceIndex, |
| | | level, |
| | | props: rootProps, |
| | | handleMouseleave: parentHandleMouseleave, |
| | | } = inject<SubMenuProvider>(`subMenu:${getParentMenu.value?.uid}`)!; |
| | | |
| | | const getClass = computed(() => { |
| | | return [ |
| | | `${prefixCls}-submenu`, |
| | | { |
| | | [`${prefixCls}-opened`]: state.opened, |
| | | [`${prefixCls}-submenu-disabled`]: props.disabled, |
| | | [`${prefixCls}-submenu-has-parent-submenu`]: unref(getParentSubMenu), |
| | | [`${prefixCls}-child-item-active`]: state.active, |
| | | }, |
| | | disabled: propTypes.bool, |
| | | collapsedShowTitle: propTypes.bool, |
| | | }, |
| | | setup(props) { |
| | | const instance = getCurrentInstance(); |
| | | ]; |
| | | }); |
| | | |
| | | const state = reactive({ |
| | | active: false, |
| | | opened: false, |
| | | const getAccordion = computed(() => rootProps.accordion); |
| | | const getCollapse = computed(() => rootProps.collapse); |
| | | const getTheme = computed(() => rootProps.theme); |
| | | |
| | | const getOverlayStyle = computed((): CSSProperties => { |
| | | return { |
| | | minWidth: '200px', |
| | | }; |
| | | }); |
| | | |
| | | const getIsOpend = computed(() => { |
| | | const name = props.name; |
| | | if (unref(getCollapse)) { |
| | | return parentGetOpenNames().includes(name); |
| | | } |
| | | return state.opened; |
| | | }); |
| | | |
| | | const getSubClass = computed(() => { |
| | | const isActive = rootProps.activeSubMenuNames.includes(props.name); |
| | | return [ |
| | | `${prefixCls}-submenu-title`, |
| | | { |
| | | [`${prefixCls}-submenu-active`]: isActive, |
| | | [`${prefixCls}-submenu-active-border`]: isActive && level === 0, |
| | | [`${prefixCls}-submenu-collapse`]: unref(getCollapse) && level === 0, |
| | | }, |
| | | ]; |
| | | }); |
| | | |
| | | function getEvents(deep: boolean) { |
| | | if (!unref(getCollapse)) { |
| | | return {}; |
| | | } |
| | | return { |
| | | onMouseenter: handleMouseenter, |
| | | onMouseleave: () => handleMouseleave(deep), |
| | | }; |
| | | } |
| | | |
| | | function handleClick() { |
| | | const { disabled } = props; |
| | | if (disabled || unref(getCollapse)) return; |
| | | const opened = state.opened; |
| | | |
| | | if (unref(getAccordion)) { |
| | | const { uidList } = getParentList(); |
| | | rootMenuEmitter.emit('on-update-opened', { |
| | | opend: false, |
| | | parent: instance?.parent, |
| | | uidList: uidList, |
| | | }); |
| | | |
| | | const data = reactive({ |
| | | timeout: null as TimeoutHandle | null, |
| | | mouseInChild: false, |
| | | isChild: false, |
| | | } else { |
| | | rootMenuEmitter.emit('open-name-change', { |
| | | name: props.name, |
| | | opened: !opened, |
| | | }); |
| | | } |
| | | state.opened = !opened; |
| | | } |
| | | |
| | | const { getParentSubMenu, getItemStyle, getParentMenu, getParentList } = |
| | | useMenuItem(instance); |
| | | function handleMouseenter() { |
| | | const disabled = props.disabled; |
| | | if (disabled) return; |
| | | |
| | | const { prefixCls } = useDesign('menu'); |
| | | subMenuEmitter.emit('submenu:mouse-enter-child'); |
| | | |
| | | const subMenuEmitter = mitt(); |
| | | const index = parentGetOpenNames().findIndex((item) => item === props.name); |
| | | |
| | | const { rootMenuEmitter } = useSimpleRootMenuContext(); |
| | | sliceIndex(index); |
| | | |
| | | const { |
| | | addSubMenu: parentAddSubmenu, |
| | | removeSubMenu: parentRemoveSubmenu, |
| | | removeAll: parentRemoveAll, |
| | | getOpenNames: parentGetOpenNames, |
| | | isRemoveAllPopup, |
| | | sliceIndex, |
| | | level, |
| | | props: rootProps, |
| | | handleMouseleave: parentHandleMouseleave, |
| | | } = inject<SubMenuProvider>(`subMenu:${getParentMenu.value?.uid}`)!; |
| | | const isRoot = level === 0 && parentGetOpenNames().length === 2; |
| | | if (isRoot) { |
| | | parentRemoveAll(); |
| | | } |
| | | data.isChild = parentGetOpenNames().includes(props.name); |
| | | clearTimeout(data.timeout!); |
| | | data.timeout = setTimeout(() => { |
| | | parentAddSubmenu(props.name); |
| | | }, DELAY); |
| | | } |
| | | |
| | | const getClass = computed(() => { |
| | | return [ |
| | | `${prefixCls}-submenu`, |
| | | { |
| | | [`${prefixCls}-item-active`]: state.active, |
| | | [`${prefixCls}-opened`]: state.opened, |
| | | [`${prefixCls}-submenu-disabled`]: props.disabled, |
| | | [`${prefixCls}-submenu-has-parent-submenu`]: unref(getParentSubMenu), |
| | | [`${prefixCls}-child-item-active`]: state.active, |
| | | }, |
| | | ]; |
| | | }); |
| | | function handleMouseleave(deepDispatch = false) { |
| | | const parentName = getParentMenu.value?.props.name; |
| | | if (!parentName) { |
| | | isRemoveAllPopup.value = true; |
| | | } |
| | | |
| | | const getAccordion = computed(() => rootProps.accordion); |
| | | const getCollapse = computed(() => rootProps.collapse); |
| | | const getTheme = computed(() => rootProps.theme); |
| | | if (parentGetOpenNames().slice(-1)[0] === props.name) { |
| | | data.isChild = false; |
| | | } |
| | | |
| | | const getOverlayStyle = computed((): CSSProperties => { |
| | | return { |
| | | minWidth: '200px', |
| | | }; |
| | | }); |
| | | |
| | | const getIsOpend = computed(() => { |
| | | const name = props.name; |
| | | if (unref(getCollapse)) { |
| | | return parentGetOpenNames().includes(name); |
| | | } |
| | | return state.opened; |
| | | }); |
| | | |
| | | const getSubClass = computed(() => { |
| | | const isActive = rootProps.activeSubMenuNames.includes(props.name); |
| | | return [ |
| | | `${prefixCls}-submenu-title`, |
| | | { |
| | | [`${prefixCls}-submenu-active`]: isActive, |
| | | [`${prefixCls}-submenu-active-border`]: isActive && level === 0, |
| | | [`${prefixCls}-submenu-collapse`]: unref(getCollapse) && level === 0, |
| | | }, |
| | | ]; |
| | | }); |
| | | |
| | | function getEvents(deep: boolean) { |
| | | if (!unref(getCollapse)) { |
| | | return {}; |
| | | } |
| | | return { |
| | | onMouseenter: handleMouseenter, |
| | | onMouseleave: () => handleMouseleave(deep), |
| | | }; |
| | | } |
| | | |
| | | function handleClick() { |
| | | const { disabled } = props; |
| | | if (disabled || unref(getCollapse)) return; |
| | | const opened = state.opened; |
| | | |
| | | if (unref(getAccordion)) { |
| | | const { uidList } = getParentList(); |
| | | rootMenuEmitter.emit('on-update-opened', { |
| | | opend: false, |
| | | parent: instance?.parent, |
| | | uidList: uidList, |
| | | }); |
| | | } else { |
| | | rootMenuEmitter.emit('open-name-change', { |
| | | name: props.name, |
| | | opened: !opened, |
| | | }); |
| | | } |
| | | state.opened = !opened; |
| | | } |
| | | |
| | | function handleMouseenter() { |
| | | const disabled = props.disabled; |
| | | if (disabled) return; |
| | | |
| | | subMenuEmitter.emit('submenu:mouse-enter-child'); |
| | | |
| | | const index = parentGetOpenNames().findIndex((item) => item === props.name); |
| | | |
| | | sliceIndex(index); |
| | | |
| | | const isRoot = level === 0 && parentGetOpenNames().length === 2; |
| | | if (isRoot) { |
| | | subMenuEmitter.emit('submenu:mouse-leave-child'); |
| | | if (data.timeout) { |
| | | clearTimeout(data.timeout!); |
| | | data.timeout = setTimeout(() => { |
| | | if (isRemoveAllPopup.value) { |
| | | parentRemoveAll(); |
| | | } else if (!data.mouseInChild) { |
| | | parentRemoveSubmenu(props.name); |
| | | } |
| | | data.isChild = parentGetOpenNames().includes(props.name); |
| | | clearTimeout(data.timeout!); |
| | | data.timeout = setTimeout(() => { |
| | | parentAddSubmenu(props.name); |
| | | }, DELAY); |
| | | }, DELAY); |
| | | } |
| | | if (deepDispatch) { |
| | | if (getParentSubMenu.value) { |
| | | parentHandleMouseleave?.(true); |
| | | } |
| | | } |
| | | } |
| | | |
| | | function handleMouseleave(deepDispatch = false) { |
| | | const parentName = getParentMenu.value?.props.name; |
| | | if (!parentName) { |
| | | isRemoveAllPopup.value = true; |
| | | } |
| | | onBeforeMount(() => { |
| | | subMenuEmitter.on('submenu:mouse-enter-child', () => { |
| | | data.mouseInChild = true; |
| | | isRemoveAllPopup.value = false; |
| | | clearTimeout(data.timeout!); |
| | | }); |
| | | subMenuEmitter.on('submenu:mouse-leave-child', () => { |
| | | if (data.isChild) return; |
| | | data.mouseInChild = false; |
| | | clearTimeout(data.timeout!); |
| | | }); |
| | | |
| | | if (parentGetOpenNames().slice(-1)[0] === props.name) { |
| | | data.isChild = false; |
| | | rootMenuEmitter.on( |
| | | 'on-update-opened', |
| | | (data: boolean | (string | number)[] | Recordable<any>) => { |
| | | if (unref(getCollapse)) return; |
| | | if (isBoolean(data)) { |
| | | state.opened = data; |
| | | return; |
| | | } |
| | | |
| | | subMenuEmitter.emit('submenu:mouse-leave-child'); |
| | | if (data.timeout) { |
| | | clearTimeout(data.timeout!); |
| | | data.timeout = setTimeout(() => { |
| | | if (isRemoveAllPopup.value) { |
| | | parentRemoveAll(); |
| | | } else if (!data.mouseInChild) { |
| | | parentRemoveSubmenu(props.name); |
| | | } |
| | | }, DELAY); |
| | | } |
| | | if (deepDispatch) { |
| | | if (getParentSubMenu.value) { |
| | | parentHandleMouseleave?.(true); |
| | | if (isObject(data) && rootProps.accordion) { |
| | | const { opend, parent, uidList } = data as Recordable<any>; |
| | | if (parent === instance?.parent) { |
| | | state.opened = opend; |
| | | } else if (!uidList.includes(instance?.uid)) { |
| | | state.opened = false; |
| | | } |
| | | return; |
| | | } |
| | | |
| | | if (props.name && Array.isArray(data)) { |
| | | state.opened = (data as (string | number)[]).includes(props.name); |
| | | } |
| | | }, |
| | | ); |
| | | |
| | | rootMenuEmitter.on('on-update-active-name:submenu', (data: number[]) => { |
| | | if (instance?.uid) { |
| | | state.active = data.includes(instance?.uid); |
| | | } |
| | | }); |
| | | }); |
| | | |
| | | onBeforeMount(() => { |
| | | subMenuEmitter.on('submenu:mouse-enter-child', () => { |
| | | data.mouseInChild = true; |
| | | isRemoveAllPopup.value = false; |
| | | clearTimeout(data.timeout!); |
| | | }); |
| | | subMenuEmitter.on('submenu:mouse-leave-child', () => { |
| | | if (data.isChild) return; |
| | | data.mouseInChild = false; |
| | | clearTimeout(data.timeout!); |
| | | }); |
| | | function handleVisibleChange(visible: boolean) { |
| | | state.opened = visible; |
| | | } |
| | | |
| | | rootMenuEmitter.on( |
| | | 'on-update-opened', |
| | | (data: boolean | (string | number)[] | Recordable<any>) => { |
| | | if (unref(getCollapse)) return; |
| | | if (isBoolean(data)) { |
| | | state.opened = data; |
| | | return; |
| | | } |
| | | if (isObject(data) && rootProps.accordion) { |
| | | const { opend, parent, uidList } = data as Recordable<any>; |
| | | if (parent === instance?.parent) { |
| | | state.opened = opend; |
| | | } else if (!uidList.includes(instance?.uid)) { |
| | | state.opened = false; |
| | | } |
| | | return; |
| | | } |
| | | |
| | | if (props.name && Array.isArray(data)) { |
| | | state.opened = (data as (string | number)[]).includes(props.name); |
| | | } |
| | | }, |
| | | ); |
| | | |
| | | rootMenuEmitter.on('on-update-active-name:submenu', (data: number[]) => { |
| | | if (instance?.uid) { |
| | | state.active = data.includes(instance?.uid); |
| | | } |
| | | }); |
| | | }); |
| | | |
| | | function handleVisibleChange(visible: boolean) { |
| | | state.opened = visible; |
| | | } |
| | | |
| | | // provide |
| | | provide<SubMenuProvider>(`subMenu:${instance?.uid}`, { |
| | | addSubMenu: parentAddSubmenu, |
| | | removeSubMenu: parentRemoveSubmenu, |
| | | getOpenNames: parentGetOpenNames, |
| | | removeAll: parentRemoveAll, |
| | | isRemoveAllPopup, |
| | | sliceIndex, |
| | | level: level + 1, |
| | | handleMouseleave, |
| | | props: rootProps, |
| | | }); |
| | | |
| | | return { |
| | | getClass, |
| | | prefixCls, |
| | | getCollapse, |
| | | getItemStyle, |
| | | handleClick, |
| | | handleVisibleChange, |
| | | getParentSubMenu, |
| | | getOverlayStyle, |
| | | getTheme, |
| | | getIsOpend, |
| | | getEvents, |
| | | getSubClass, |
| | | ...toRefs(state), |
| | | ...toRefs(data), |
| | | }; |
| | | }, |
| | | // provide |
| | | provide<SubMenuProvider>(`subMenu:${instance?.uid}`, { |
| | | addSubMenu: parentAddSubmenu, |
| | | removeSubMenu: parentRemoveSubmenu, |
| | | getOpenNames: parentGetOpenNames, |
| | | removeAll: parentRemoveAll, |
| | | isRemoveAllPopup, |
| | | sliceIndex, |
| | | level: level + 1, |
| | | handleMouseleave, |
| | | props: rootProps, |
| | | }); |
| | | </script> |