From f73d8786777c7ebcf53a93bf46c2268892a28502 Mon Sep 17 00:00:00 2001
From: schangxiang@126.com <schangxiang@126.com>
Date: 周三, 07 5月 2025 09:14:40 +0800
Subject: [PATCH] 调整项目位置
---
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