UNPKG

vxe-pc-ui

Version:
485 lines (441 loc) 15.7 kB
import { defineComponent, ref, h, reactive, PropType, inject, resolveComponent, createCommentVNode, watch, VNode, computed, nextTick, onBeforeUnmount, onMounted } from 'vue' import XEUtils from 'xe-utils' import { getConfig, getIcon, createEvent, permission, useSize, globalEvents } from '../../ui' import { toCssUnit } from '../../ui/src/dom' import { getLastZIndex, nextZIndex } from '../../ui/src/utils' import { getSlotVNs } from '../../ui/src/vn' import VxeLoadingComponent from '../../loading/src/loading' import type { VxeMenuDefines, VxeMenuPropTypes, MenuReactData, VxeMenuEmits, MenuMethods, VxeComponentSlotType, VxeLayoutAsidePropTypes, MenuPrivateMethods, MenuPrivateRef, VxeMenuPrivateComputed, VxeMenuConstructor, VxeMenuPrivateMethods, ValueOf, VxeLayoutAsideConstructor, VxeLayoutAsidePrivateMethods } from '../../../types' export default defineComponent({ name: 'VxeMenu', props: { modelValue: [String, Number] as PropType<VxeMenuPropTypes.ModelValue>, expandAll: Boolean as PropType<VxeMenuPropTypes.ExpandAll>, collapsed: { type: Boolean as PropType<VxeMenuPropTypes.Collapsed>, default: null }, collapseFixed: Boolean as PropType<VxeMenuPropTypes.CollapseFixed>, loading: Boolean as PropType<VxeMenuPropTypes.Loading>, options: { type: Array as PropType<VxeMenuPropTypes.Options>, default: () => [] }, size: { type: String as PropType<VxeMenuPropTypes.Size>, default: () => getConfig().image.size || getConfig().size } }, emits: [ 'update:modelValue', 'click' ] as VxeMenuEmits, setup (props, context) { const { emit, slots } = context const xID = XEUtils.uniqueId() const $xeLayoutAside = inject<(VxeLayoutAsideConstructor & VxeLayoutAsidePrivateMethods) | null>('$xeLayoutAside', null) const refElem = ref<HTMLDivElement>() const refCollapseElem = ref<HTMLDivElement>() const { computeSize } = useSize(props) const reactData = reactive<MenuReactData>({ initialized: !!props.collapsed, isEnterCollapse: false, collapseStyle: {}, collapseZindex: 0, activeName: props.modelValue, menuList: [], itemHeight: 1 }) const refMaps: MenuPrivateRef = { refElem } const computeIsCollapsed = computed(() => { const { collapsed } = props if (XEUtils.isBoolean(collapsed)) { return collapsed } if ($xeLayoutAside) { return !!$xeLayoutAside.props.collapsed } return false }) const computeCollapseWidth = computed(() => { let collapseWidth: VxeLayoutAsidePropTypes.CollapseWidth = '' if ($xeLayoutAside) { collapseWidth = $xeLayoutAside.props.collapseWidth || '' } return collapseWidth }) const computeCollapseEnterWidth = computed(() => { let width: VxeLayoutAsidePropTypes.Width = '' if ($xeLayoutAside) { width = $xeLayoutAside.props.width || '' } return width }) const computeMaps: VxeMenuPrivateComputed = { computeSize } const $xeMenu = { xID, props, context, reactData, getRefMaps: () => refMaps, getComputeMaps: () => computeMaps } as unknown as VxeMenuConstructor & VxeMenuPrivateMethods const getMenuTitle = (item: VxeMenuPropTypes.MenuOption) => { return `${item.title || item.name}` } const updateZindex = () => { if (reactData.collapseZindex < getLastZIndex()) { reactData.collapseZindex = nextZIndex() } } const updateActiveMenu = (isDefExpand?: boolean) => { const { activeName } = reactData XEUtils.eachTree(reactData.menuList, (item, index, items, path, parent, nodes) => { if (item.itemKey === activeName) { nodes.forEach(obj => { obj.isActive = true if (isDefExpand) { obj.isExpand = true } }) item.isExactActive = true } else { item.isExactActive = false item.isActive = false } }, { children: 'childList' }) } const updateMenuConfig = () => { const { options, expandAll } = props reactData.menuList = XEUtils.mapTree(options, (item, index, items, path, parent) => { const objItem = { ...item, parentKey: parent ? (parent.name || path.slice(0, path.length - 1).join(',')) : '', level: path.length, itemKey: item.name || path.join(','), isExactActive: false, isActive: false, isExpand: XEUtils.isBoolean(item.expanded) ? item.expanded : !!expandAll, hasChild: item.children && item.children.length > 0 } as VxeMenuDefines.MenuItem return objItem }, { children: 'children', mapChildren: 'childList' }) } const updateCollapseStyle = () => { const { collapseFixed } = props if (collapseFixed) { nextTick(() => { const { isEnterCollapse } = reactData const isCollapsed = computeIsCollapsed.value const collapseEnterWidth = computeCollapseEnterWidth.value const collapseWidth = computeCollapseWidth.value const el = refElem.value if (el) { const clientRect = el.getBoundingClientRect() const parentNode = el.parentNode as HTMLElement reactData.collapseStyle = isCollapsed ? { top: toCssUnit(clientRect.top), left: toCssUnit(clientRect.left), height: toCssUnit(parentNode.clientHeight), width: isEnterCollapse ? (collapseEnterWidth ? toCssUnit(collapseEnterWidth) : '') : (collapseWidth ? toCssUnit(collapseWidth) : ''), zIndex: reactData.collapseZindex } : {} } }) } } const handleCollapseMenu = () => { const { collapseFixed } = props if (collapseFixed) { const { initialized } = reactData const isCollapsed = computeIsCollapsed.value if (isCollapsed) { if (!initialized) { reactData.initialized = true nextTick(() => { const collapseEl = refCollapseElem.value if (collapseEl) { document.body.appendChild(collapseEl) } }) } } reactData.isEnterCollapse = false updateZindex() updateCollapseStyle() } } const handleClickIconCollapse = (evnt: KeyboardEvent, item: VxeMenuDefines.MenuItem) => { const { hasChild, isExpand } = item if (hasChild) { evnt.stopPropagation() evnt.preventDefault() item.isExpand = !isExpand } } const emitModel = (value: any) => { reactData.activeName = value emit('update:modelValue', value) } const handleClickMenu = (evnt: KeyboardEvent, item: VxeMenuDefines.MenuItem) => { const { itemKey, routerLink, hasChild } = item if (routerLink) { emitModel(itemKey) handleMenuMouseleave() } else { if (hasChild) { handleClickIconCollapse(evnt, item) } else { emitModel(itemKey) handleMenuMouseleave() } } dispatchEvent('click', { menu: item }, evnt) } const handleMenuMouseenter = () => { const { collapseStyle } = reactData const collapseEnterWidth = computeCollapseEnterWidth.value reactData.collapseStyle = Object.assign({}, collapseStyle, { width: collapseEnterWidth ? toCssUnit(collapseEnterWidth) : '' }) reactData.isEnterCollapse = true } const handleMenuMouseleave = () => { const { collapseStyle } = reactData const el = refElem.value reactData.collapseStyle = Object.assign({}, collapseStyle, { width: el ? toCssUnit(el.offsetWidth) : '' }) reactData.isEnterCollapse = false } const callSlot = <T>(slotFunc: ((params: T) => VxeComponentSlotType | VxeComponentSlotType[]) | string | null, params: T) => { if (slotFunc) { if (XEUtils.isString(slotFunc)) { slotFunc = slots[slotFunc] || null } if (XEUtils.isFunction(slotFunc)) { return getSlotVNs(slotFunc(params)) } } return [] } const dispatchEvent = (type: ValueOf<VxeMenuEmits>, params: Record<string, any>, evnt: Event | null) => { emit(type, createEvent(evnt, { $menu: $xeMenu }, params)) } const menuMethods: MenuMethods = { dispatchEvent } const menuPrivateMethods: MenuPrivateMethods = { } Object.assign($xeMenu, menuMethods, menuPrivateMethods) const renderMenuTitle = (item: VxeMenuDefines.MenuItem) => { const { icon, isExpand, hasChild, slots: itemSlots } = item const optionSlot = itemSlots ? itemSlots.default : slots.option const title = getMenuTitle(item) const isCollapsed = computeIsCollapsed.value return [ h('div', { class: 'vxe-menu--item-link-icon' }, icon ? [ h('i', { class: icon }) ] : []), optionSlot ? h('div', { class: 'vxe-menu--item-custom-title' }, callSlot(optionSlot, { option: item as any, collapsed: isCollapsed })) : h('div', { class: 'vxe-menu--item-link-title', title }, title), hasChild ? h('div', { class: 'vxe-menu--item-link-collapse', onClick (evnt: KeyboardEvent) { handleClickIconCollapse(evnt, item) } }, [ h('i', { class: isExpand ? getIcon().MENU_ITEM_EXPAND_OPEN : getIcon().MENU_ITEM_EXPAND_CLOSE }) ]) : createCommentVNode() ] } const renderDefaultChildren = (item: VxeMenuDefines.MenuItem): VNode => { const { itemKey, level, hasChild, isActive, isExactActive, isExpand, routerLink, childList } = item const { isEnterCollapse } = reactData const isCollapsed = computeIsCollapsed.value if (item.permissionCode) { if (!permission.checkVisible(item.permissionCode)) { return createCommentVNode() } } return h('div', { key: itemKey, class: ['vxe-menu--item-wrapper', `vxe-menu--item-level${level}`, { 'is--exact-active': isExactActive, 'is--active': isActive, 'is--expand': (!isCollapsed || isEnterCollapse) && isExpand }] }, [ routerLink ? h(resolveComponent('router-link'), { class: 'vxe-menu--item-link', to: routerLink, onClick (evnt: KeyboardEvent) { handleClickMenu(evnt, item) } }, { default: () => renderMenuTitle(item) }) : h('div', { class: 'vxe-menu--item-link', onClick (evnt: KeyboardEvent) { handleClickMenu(evnt, item) } }, renderMenuTitle(item)), hasChild ? h('div', { class: 'vxe-menu--item-group' }, childList.map(child => renderDefaultChildren(child))) : createCommentVNode() ]) } const renderCollapseChildren = (item: VxeMenuDefines.MenuItem): VNode => { const { itemKey, level, hasChild, isActive, isExactActive, routerLink, childList } = item if (item.permissionCode) { if (!permission.checkVisible(item.permissionCode)) { return createCommentVNode() } } return h('div', { key: itemKey, class: ['vxe-menu--item-wrapper', `vxe-menu--item-level${level}`, { 'is--exact-active': isExactActive, 'is--active': isActive }] }, [ routerLink ? h(resolveComponent('router-link'), { class: 'vxe-menu--item-link', to: routerLink, onClick (evnt: KeyboardEvent) { handleClickMenu(evnt, item) } }, { default: () => renderMenuTitle(item) }) : h('div', { class: 'vxe-menu--item-link', onClick (evnt: KeyboardEvent) { handleClickMenu(evnt, item) } }, renderMenuTitle(item)), hasChild ? h('div', { class: 'vxe-menu--item-group' }, childList.map(child => renderDefaultChildren(child))) : createCommentVNode() ]) } const renderVN = () => { const { loading } = props const { initialized, menuList, collapseStyle, isEnterCollapse } = reactData const vSize = computeSize.value const isCollapsed = computeIsCollapsed.value return h('div', { ref: refElem, class: ['vxe-menu', { [`size--${vSize}`]: vSize, 'is--collapsed': isCollapsed, 'is--loading': loading }] }, [ h('div', { class: 'vxe-menu--item-list' }, menuList.map(child => isCollapsed ? renderCollapseChildren(child) : renderDefaultChildren(child))), initialized ? h('div', { ref: refCollapseElem, class: ['vxe-menu--collapse-wrapper', { [`size--${vSize}`]: vSize, 'is--collapsed': isCollapsed, 'is--enter': isEnterCollapse, 'is--loading': loading }], style: collapseStyle, onMouseenter: handleMenuMouseenter, onMouseleave: handleMenuMouseleave }, [ isCollapsed ? h('div', { class: 'vxe-menu--item-list' }, menuList.map(child => renderDefaultChildren(child))) : createCommentVNode() ]) : createCommentVNode(), /** * 加载中 */ h(VxeLoadingComponent, { class: 'vxe-list-view--loading', modelValue: loading }) ]) } const optFlag = ref(0) watch(() => props.options ? props.options.length : -1, () => { optFlag.value++ }) watch(() => props.options, () => { optFlag.value++ }) watch(optFlag, () => { updateMenuConfig() updateActiveMenu(true) }) watch(() => props.modelValue, (val) => { reactData.activeName = val }) watch(() => reactData.activeName, () => { updateActiveMenu(true) }) watch(computeIsCollapsed, () => { handleCollapseMenu() }) onMounted(() => { globalEvents.on($xeMenu, 'resize', updateCollapseStyle) updateCollapseStyle() }) onBeforeUnmount(() => { globalEvents.off($xeMenu, 'resize') const collapseEl = refCollapseElem.value if (collapseEl) { const parentNode = collapseEl.parentNode if (parentNode) { parentNode.removeChild(collapseEl) } } }) updateMenuConfig() updateActiveMenu(true) $xeMenu.renderVN = renderVN return $xeMenu }, render () { return this.renderVN() } })