From 27ba504441037666e787ded85b4af2f65be65c17 Mon Sep 17 00:00:00 2001 From: schangxiang@126.com <schangxiang@126.com> Date: 周二, 29 4月 2025 18:06:07 +0800 Subject: [PATCH] Merge branch 'master' of http://222.71.245.114:9086/r/HIA24016N_PipeLineDemo --- HIAWms/web/src/components/vue3-context-menu/ContextSubMenu.vue | 555 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 files changed, 555 insertions(+), 0 deletions(-) diff --git a/HIAWms/web/src/components/vue3-context-menu/ContextSubMenu.vue b/HIAWms/web/src/components/vue3-context-menu/ContextSubMenu.vue new file mode 100644 index 0000000..b3daeac --- /dev/null +++ b/HIAWms/web/src/components/vue3-context-menu/ContextSubMenu.vue @@ -0,0 +1,555 @@ +<template> + <div + :class="'mx-context-menu ' + (options.customClass ? options.customClass : '') + ' ' + globalTheme" + :style="{ + maxWidth: (maxWidth ? solveNumberOrStringSize(maxWidth) : `${constOptions.defaultMaxWidth}px`), + minWidth: minWidth ? solveNumberOrStringSize(minWidth) : `${constOptions.defaultMinWidth}px`, + maxHeight: overflow && maxHeight > 0 ? `${maxHeight}px` : undefined, + zIndex: zIndex, + left: `${position.x}px`, + top: `${position.y}px`, + }" + data-type="ContextSubMenu" + @click="onSubMenuBodyClick" + @wheel="onMouseWhell" + > + <!--Child menu items--> + <div + :class="[ 'mx-context-menu-items' ]" + ref="menu" + :style="{ + top: `${scrollValue}px`, + }" + > + <slot> + <div v-if="overflow && options.updownButtonSpaceholder" class="mx-context-menu-updown placeholder"></div> + <template v-for="(item, i) in items" :key="i" > + <ContextMenuSeparator v-if="item.hidden !== true && item.divided === 'up'" /> + <ContextMenuSeparator v-if="item.hidden !== true && item.divided === 'self'" /> + <!--Menu Item--> + <ContextMenuItem + v-else + :clickHandler="item.onClick ? (e) => item.onClick!(e) : undefined" + :disabled="item.disabled" + :hidden="item.hidden" + :icon="item.icon" + :iconFontClass="item.iconFontClass" + :svgIcon="item.svgIcon" + :svgProps="item.svgProps" + :label="item.label" + :customRender="(item.customRender as Function)" + :customClass="item.customClass" + :checked="item.checked" + :shortcut="item.shortcut" + :clickClose="item.clickClose" + :clickableWhenHasChildren="item.clickableWhenHasChildren" + :preserveIconWidth="item.preserveIconWidth !== undefined ? item.preserveIconWidth : options.preserveIconWidth" + :showRightArrow="item.children && item.children.length > 0" + :hasChildren="item.children && item.children.length > 0" + :rawMenuItem="item" + @sub-menu-open="item.onSubMenuOpen" + @sub-menu-close="item.onSubMenuClose" + > + <template v-if="item.children && item.children.length > 0" #submenu> + <!--Sub menu--> + <ContextSubMenu + :items="item.children" + :maxWidth="item.maxWidth" + :minWidth="item.minWidth" + :adjustPosition="item.adjustSubMenuPosition !== undefined ? item.adjustSubMenuPosition : options.adjustPosition" + :direction="item.direction !== undefined ? item.direction : options.direction" + /> + </template> + </ContextMenuItem> + <!--Separator--> + <!--Custom render--> + <ContextMenuSeparator v-if="item.hidden !== true && (item.divided === 'down' || item.divided === true)" /> + </template> + <div v-if="overflow && options.updownButtonSpaceholder" class="mx-context-menu-updown placeholder"></div> + </slot> + </div> + + <!--Scroll button host--> + <div + class="mx-context-menu-scroll" + ref="scroll" + > + <!--Updown scroll button--> + <div + v-show="overflow" + ref="upScrollButton" + :class="'mx-context-menu-updown mx-context-no-clickable up' + (overflow && scrollValue < 0 ? '' : ' disabled')" + @click="onScroll(false)" + @wheel="onMouseWhellMx" + > + <ContextMenuIconRight /> + </div> + <div + v-show="overflow" + :class="'mx-context-menu-updown mx-context-no-clickable down' + (overflow && scrollValue > -scrollHeight ? '' : ' disabled')" + @click="onScroll(true)" + @wheel="onMouseWhellMx" + > + <ContextMenuIconRight /> + </div> + </div> + </div> +</template> + +<script lang="ts"> +import { defineComponent, inject, nextTick, onMounted, type PropType, provide, ref, toRefs } from 'vue' +import type { MenuOptions, MenuItem, ContextMenuPositionData, MenuPopDirection } from './ContextMenuDefine' +import type { GlobalHasSlot, GlobalRenderSlot } from './ContextMenu.vue' +import { MenuConstOptions } from './ContextMenuDefine' +import { getLeft, getTop, solveNumberOrStringSize } from './ContextMenuUtils' +import ContextMenuItem from './ContextMenuItem.vue' +import ContextMenuSeparator from './ContextMenuSeparator.vue' +import ContextMenuIconRight from './ContextMenuIconRight.vue' + +//The internal info context for menu item +export interface MenuItemContext { + focus: () => void, + blur: () => void, + showSubMenu: () => boolean, + getElement: () => HTMLElement|undefined, + isDisabledOrHidden: () => boolean, + click: (e: MouseEvent|KeyboardEvent) => void, +} + +//The internal info context for submenu instance +export interface SubMenuContext { + isTopLevel: () => boolean; + closeSelfAndActiveParent: () => boolean, + openCurrentItemSubMenu: () => boolean, + closeCurrentSubMenu: () => void, + moveCurrentItemFirst: () => void, + moveCurrentItemLast: () => void, + moveCurrentItemDown: () => void, + moveCurrentItemUp: () => void, + focusCurrentItem: () => void, + triggerCurrentItemClick: (e: KeyboardEvent|MouseEvent) => void, +} + +//The internal info context for submenu +export interface SubMenuParentContext { + //Props + container: HTMLElement; + zIndex: number; + adjustPadding: { x: number, y: number }, + + //Position control + getParentWidth: () => number; + getParentHeight: () => number; + getParentX: () => number; + getParentY: () => number; + getParentAbsX: () => number; + getParentAbsY: () => number; + getPositon: () => [number,number]; + + //SubMenu mutex + addOpenedSubMenu: (closeFn: () => void) => void; + closeOtherSubMenu: () => void; + closeOtherSubMenuWithTimeOut: () => void; + checkCloseOtherSubMenuTimeOut: () => boolean; + + //Item control + addChildMenuItem: (item: MenuItemContext, index?: number) => void; + removeChildMenuItem: (item: MenuItemContext) => void; + markActiveMenuItem: (item: MenuItemContext, updateState?: boolean) => void; + markThisOpenedByKeyBoard: () => void; + isOpenedByKeyBoardFlag: () => boolean; + isMenuItemDataCollectedFlag: () => boolean; + + //Other + getSubMenuInstanceContext: () => SubMenuContext|null; + getParentContext: () => SubMenuParentContext|null; + getElement: () => HTMLElement|null; +} + +/** + * Submenu container + */ +export default defineComponent({ + name: 'ContextSubMenu', + components: { + ContextMenuItem, + ContextMenuSeparator, + ContextMenuIconRight + }, + props: { + /** + * Items from options + */ + items: { + type: Object as PropType<Array<MenuItem>>, + default: null + }, + /** + * Max width for this submenu + */ + maxWidth: { + type: [String, Number], + default: 0, + }, + /** + * Min width for this submenu + */ + minWidth: { + type: [String, Number], + default: 0, + }, + /** + * Specifies should submenu adjust it position + * when the menu exceeds the screen. The default is true + */ + adjustPosition: { + type: Boolean, + default: true, + }, + /** + * Menu direction + */ + direction: { + type: String as PropType<MenuPopDirection>, + default: 'br', + }, + }, + setup(props) { + + //#region Injects + + const parentContext = inject('menuContext') as SubMenuParentContext; + const options = inject('globalOptions') as MenuOptions; + const globalHasSlot = inject('globalHasSlot') as GlobalHasSlot; + const globalRenderSlot = inject('globalRenderSlot') as GlobalRenderSlot; + const globalTheme = inject('globalTheme') as string; + + //#endregion + + const { zIndex, getParentWidth, getParentHeight } = parentContext; + const { adjustPosition } = toRefs(props); + + const menu = ref<HTMLElement>(); + const scroll = ref<HTMLElement>(); + const upScrollButton = ref<HTMLElement>(); + const openedSubMenuClose = [] as (() => void)[]; + + //#region Keyboard control context + + const globalSetCurrentSubMenu = inject('globalSetCurrentSubMenu') as (menu: SubMenuContext|null) => void; + + const menuItems = [] as MenuItemContext[]; + let currentItem = null as MenuItemContext|null; + let leaveTimeout = 0; + + function blurCurrentMenu() { + if (currentItem) + currentItem.blur(); + } + + function setAndFocusNotDisableItem(isDown: boolean, startIndex?: number) { + if (isDown) { + for(let i = startIndex !== undefined ? startIndex : 0; i < menuItems.length; i++) { + if (!menuItems[i].isDisabledOrHidden()) { + setAndFocusCurrentMenu(i); + break; + } + } + } else { + for(let i = startIndex !== undefined ? startIndex : (menuItems.length - 1); i >= 0; i--) { + if (!menuItems[i].isDisabledOrHidden()) { + setAndFocusCurrentMenu(i); + break; + } + } + } + } + function setAndFocusCurrentMenu(index?: number) { + if (currentItem) + blurCurrentMenu(); + if (index !== undefined) + currentItem = menuItems[Math.max(0, Math.min(index, menuItems.length - 1))]; + if (!currentItem) + return; + + //Focus item + currentItem.focus(); + + //Scroll to current item + if (overflow.value) { + const element = currentItem.getElement(); + if (element) { + scrollValue.value = Math.min(Math.max(-scrollHeight.value, -element.offsetTop - element.offsetHeight + maxHeight.value), 0); + } + } + } + function onSubMenuBodyClick() { + //Mouse click can set current focused submenu + globalSetCurrentSubMenu(thisMenuInsContext); + } + + const thisMenuInsContext : SubMenuContext = { + isTopLevel: () => parentContext.getParentContext() === null, + closeSelfAndActiveParent: () => { + const parent = thisMenuContext.getParentContext(); + if (parent) { + parent.closeOtherSubMenu(); + const conext = parent.getSubMenuInstanceContext() + if (conext) { + conext.focusCurrentItem(); + return true; + } + } + return false; + }, + closeCurrentSubMenu: () => thisMenuContext.getParentContext()?.closeOtherSubMenu(), + moveCurrentItemFirst: () => setAndFocusNotDisableItem(true), + moveCurrentItemLast: () => setAndFocusNotDisableItem(false), + moveCurrentItemDown: () => setAndFocusNotDisableItem(true, (currentItem ? (menuItems.indexOf(currentItem) + 1) : 0)), + moveCurrentItemUp: () => setAndFocusNotDisableItem(false, (currentItem ? (menuItems.indexOf(currentItem) - 1) : 0)), + focusCurrentItem: () => setAndFocusCurrentMenu(), + openCurrentItemSubMenu: () => { + if (currentItem) + return currentItem?.showSubMenu() + return false; + }, + triggerCurrentItemClick: (e) => currentItem?.click(e), + }; + + let isOpenedByKeyBoardFlag = false; + let isMenuItemDataCollectedFlag = false; + + //#endregion + + //#region Menu control context + + //provide menuContext for child use + const thisMenuContext : SubMenuParentContext = { + zIndex: zIndex + 1, + container: parentContext.container, + adjustPadding: options.adjustPadding as { x: number, y: number } || MenuConstOptions.defaultAdjustPadding, + getParentWidth: () => menu.value?.offsetWidth || 0, + getParentHeight: () => menu.value?.offsetHeight || 0, + getParentX: () => position.value.x, + getParentY: () => position.value.y, + getParentAbsX: () => menu.value ? getLeft(menu.value, parentContext.container) : 0, + getParentAbsY: () => menu.value ? getTop(menu.value, parentContext.container) : 0, + getPositon: () => [0,0], + addOpenedSubMenu(closeFn: () => void) { + openedSubMenuClose.push(closeFn); + }, + closeOtherSubMenu() { + openedSubMenuClose.forEach(k => k()); + openedSubMenuClose.splice(0, openedSubMenuClose.length); + globalSetCurrentSubMenu(thisMenuInsContext); + }, + checkCloseOtherSubMenuTimeOut() { + if (leaveTimeout) { + clearTimeout(leaveTimeout); + leaveTimeout = 0; + return true; + } + return false; + }, + closeOtherSubMenuWithTimeOut() { + leaveTimeout = setTimeout(() => { + leaveTimeout = 0; + this.closeOtherSubMenu(); + }, 200) as unknown as number; //Add a delay, the user will not hide the menu when moving too fast + }, + addChildMenuItem: (item: MenuItemContext, index?: number) => { + if (index === undefined) + menuItems.push(item); + else + menuItems.splice(index, 0, item); + }, + removeChildMenuItem: (item: MenuItemContext) => { + menuItems.splice(menuItems.indexOf(item), 1); + }, + markActiveMenuItem: (item: MenuItemContext, updateState = false) => { + blurCurrentMenu(); + currentItem = item; + if (updateState) + setAndFocusCurrentMenu(); + }, + markThisOpenedByKeyBoard: () => { + isOpenedByKeyBoardFlag = true; + }, + isOpenedByKeyBoardFlag: () => { + if (isOpenedByKeyBoardFlag) { + isOpenedByKeyBoardFlag = false; + return true; + } + return false; + }, + isMenuItemDataCollectedFlag: () => isMenuItemDataCollectedFlag, + getElement: () => menu.value || null, + getParentContext: () => parentContext, + getSubMenuInstanceContext: () => thisMenuInsContext, + }; + provide('menuContext', thisMenuContext); + + //#endregion + + const scrollValue = ref(0); + const scrollHeight = ref(0); + + //Scroll the items + function onScroll(down : boolean) { + if (down) + scrollValue.value = Math.min(Math.max(scrollValue.value - 50, -scrollHeight.value), 0); + else + scrollValue.value = Math.min(scrollValue.value + 50, 0); + } + + function onMouseWhellMx(e: WheelEvent) { + e.preventDefault(); + e.stopPropagation(); + onScroll (e.deltaY > 0); + } + function onMouseWhell(e: WheelEvent) { + if (options.mouseScroll) { + e.preventDefault(); + e.stopPropagation(); + onScroll (e.deltaY > 0); + } + } + + const overflow = ref(false); + const position = ref({ x: 0, y: 0 } as ContextMenuPositionData) + const maxHeight = ref(0); + + onMounted(() => { + const pos = parentContext.getPositon(); + position.value = { + x: pos[0] ?? options.xOffset ?? 0, + y: pos[1] ?? options.yOffset ?? 0, + }; + + //Mark current item submenu is open + globalSetCurrentSubMenu(thisMenuInsContext); + + nextTick(() => { + const menuEl = menu.value; + + //adjust submenu position + if (menuEl && scroll.value) { + + const { container } = parentContext; + + const parentWidth = getParentWidth?.() ?? 0; + const parentHeight = getParentHeight?.() ?? 0; + + const fillPaddingX = typeof parentContext.adjustPadding === 'number' ? parentContext.adjustPadding : (parentContext.adjustPadding?.x ?? 0); + const fillPaddingYAlways = typeof parentContext.adjustPadding === 'number' ? parentContext.adjustPadding : (parentContext.adjustPadding?.y ?? 0); + const fillPaddingY = parentHeight > 0 ? fillPaddingYAlways : 0; + + const windowHeight = document.documentElement.scrollHeight; + const windowWidth = document.documentElement.scrollWidth; + + const avliableWidth = Math.min(windowWidth, container.offsetWidth); + const avliableHeight = Math.min(windowHeight, container.offsetHeight); + + let absX = getLeft(menuEl, container), + absY = getTop(menuEl, container); + + //set x positon + if (props.direction.includes('l')) { + position.value.x -= menuEl.offsetWidth + fillPaddingX; //left + } + else if (props.direction.includes('r')) { + position.value.x += parentWidth + fillPaddingX; //right + } + else { + position.value.x += parentWidth / 2; + position.value.x -= (menuEl.offsetWidth + fillPaddingX) / 2; //center + } + + //set y positon + if (props.direction.includes('t')) { + position.value.y -= menuEl.offsetHeight + fillPaddingYAlways * 2; //top + } + else if (props.direction.includes('b')) { + position.value.y -= fillPaddingYAlways; //bottom + } + else { + position.value.y -= (menuEl.offsetHeight + fillPaddingYAlways) / 2; //center + } + + //Overflow adjust + if (adjustPosition.value) { + nextTick(() => { + absX = getLeft(menuEl, container); + absY = getTop(menuEl, container); + + const xOverflow = (absX + menuEl.offsetWidth) - (avliableWidth); + const yOverflow = (absY + menuEl.offsetHeight + fillPaddingY * 2) - (avliableHeight); + + overflow.value = yOverflow > 0; + scrollHeight.value = menuEl.offsetHeight - avliableHeight + fillPaddingY * 2 /* Padding */; + + if (xOverflow > 0) {//X overflow + const ox = parentWidth + menuEl.offsetWidth - fillPaddingX; + const maxSubWidth = absX; + if (ox > maxSubWidth) + position.value.x -= maxSubWidth; + else + position.value.x -= ox; + } + + if (overflow.value) { //Y overflow + const oy = yOverflow; + const maxSubHeight = absY; + if (oy > maxSubHeight) + position.value.y -= maxSubHeight - fillPaddingY; + else + position.value.y -= oy - fillPaddingY; + maxHeight.value = (avliableHeight - fillPaddingY * 2); + } else { + maxHeight.value = 0; + } + }); + } + } + + //Focus this submenu + menuEl?.focus({ + preventScroll: true + }); + + //Is this submenu opened by keyboard? If yes then select first item + if (parentContext.isOpenedByKeyBoardFlag()) + setAndFocusNotDisableItem(true); + + isMenuItemDataCollectedFlag = true; + }); + }); + + return { + menu, + scroll, + options, + zIndex, + constOptions: MenuConstOptions, + scrollValue, + upScrollButton, + overflow, + position, + scrollHeight, + maxHeight, + globalHasSlot, + globalRenderSlot, + globalTheme, + onScroll, + onSubMenuBodyClick, + onMouseWhell, + onMouseWhellMx, + solveNumberOrStringSize, + } + } +}) +</script> + +<style lang="scss"> +@import "./ContextMenu.scss"; +</style> \ No newline at end of file -- Gitblit v1.9.3