From 98749f73549cbbb1eb1db0916f3f2073e006384e Mon Sep 17 00:00:00 2001 From: zs <zhousong@weben-smart.com> Date: 周三, 30 4月 2025 19:47:32 +0800 Subject: [PATCH] 托盘管理 --- HIAWms/web/src/components/vue3-context-menu/ContextSubMenuWrapper.vue | 326 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 files changed, 326 insertions(+), 0 deletions(-) diff --git a/HIAWms/web/src/components/vue3-context-menu/ContextSubMenuWrapper.vue b/HIAWms/web/src/components/vue3-context-menu/ContextSubMenuWrapper.vue new file mode 100644 index 0000000..a2b969c --- /dev/null +++ b/HIAWms/web/src/components/vue3-context-menu/ContextSubMenuWrapper.vue @@ -0,0 +1,326 @@ +<template> + <div class="mx-menu-ghost-host"> + <Transition + v-if="options.menuTransitionProps" + appear + v-bind="options.menuTransitionProps" + @after-leave="emit('closeAnimFinished')" + > + <ContextSubMenuConstructor + v-if="show" + class="mx-menu-host" + :items="options.items" + :adjustPosition="options.adjustPosition" + :maxWidth="options.maxWidth || MenuConstOptions.defaultMaxWidth" + :minWidth="options.minWidth || MenuConstOptions.defaultMinWidth" + :direction="(options.direction || MenuConstOptions.defaultDirection as MenuPopDirection)" + > + <slot /> + </ContextSubMenuConstructor> + </Transition> + <ContextSubMenuConstructor + v-else-if="show" + class="mx-menu-host" + :items="options.items" + :adjustPosition="options.adjustPosition" + :maxWidth="options.maxWidth || MenuConstOptions.defaultMaxWidth" + :minWidth="options.minWidth || MenuConstOptions.defaultMinWidth" + :direction="(options.direction || MenuConstOptions.defaultDirection as MenuPopDirection)" + > + <slot /> + </ContextSubMenuConstructor> + </div> +</template> + +<script setup lang="ts"> + import { + h, + onBeforeUnmount, + onMounted, + type PropType, + provide, + ref, + renderSlot, + toRefs, + type VNode, + watch, + Transition, + useSlots, + type Ref, + } from "vue"; + import type { + MenuItem, + MenuOptions, + MenuPopDirection, + } from "./ContextMenuDefine"; + import { MenuConstOptions } from "./ContextMenuDefine"; + import { + addOpenedContextMenu, + removeOpenedContextMenu, + } from "./ContextMenuMutex"; + import ContextSubMenuConstructor, { + type SubMenuContext, + type SubMenuParentContext, + } from "./ContextSubMenu.vue"; + + /** + * Context menu component + */ + + export type GlobalHasSlot = (name: string) => boolean; + export type GlobalRenderSlot = ( + name: string, + params: Record<string, unknown> + ) => VNode; + + const props = defineProps({ + /** + * Menu options + */ + options: { + type: Object as PropType<MenuOptions>, + default: null, + }, + /** + * Show menu? + */ + show: { + type: Object as PropType<Ref<boolean>>, + default: null, + }, + /** + * Current container, For calculation only + */ + container: { + type: Object as PropType<HTMLElement>, + default: null, + }, + /** + * Make sure is user set the custom container. + */ + isFullScreenContainer: { + type: Boolean, + default: true, + }, + }); + + const emit = defineEmits(["close", "closeAnimFinished"]); + + const slots = useSlots(); + + const { options, show, container } = toRefs(props); + + onMounted(() => { + if (show.value) openMenu(); + }); + onBeforeUnmount(() => { + removeBodyEvents(); + }); + + watch(show, (v: boolean) => { + if (v) { + openMenu(); + } else { + removeBodyEvents(); + } + }); + + const instance = { + closeMenu, + isClosed, + }; + let closed = false; + + function openMenu() { + installBodyEvents(); + addOpenedContextMenu(instance); + } + function closeMenu(fromItem?: MenuItem | undefined) { + closed = true; + emit("close", fromItem); + if (!options.value.menuTransitionProps) emit("closeAnimFinished"); + removeOpenedContextMenu(instance); + } + function isClosed() { + return closed; + } + + function installBodyEvents() { + setTimeout(() => { + document.addEventListener("click", onBodyClick, true); + document.addEventListener("contextmenu", onBodyClick, true); + document.addEventListener("scroll", onBodyScroll, true); + if (!props.isFullScreenContainer && container.value) + container.value.addEventListener("scroll", onBodyScroll, true); + if (options.value.keyboardControl !== false) + document.addEventListener("keydown", onMenuKeyDown); + }, 50); + } + function removeBodyEvents() { + document.removeEventListener("contextmenu", onBodyClick, true); + document.removeEventListener("click", onBodyClick, true); + document.removeEventListener("scroll", onBodyScroll, true); + if (!props.isFullScreenContainer && container.value) + container.value.removeEventListener("scroll", onBodyScroll, true); + if (options.value.keyboardControl !== false) + document.removeEventListener("keydown", onMenuKeyDown); + } + + //For keyboard event, remember which submenu is active + const currentOpenedMenu = ref<SubMenuContext | null>(); + provide( + "globalSetCurrentSubMenu", + (menu: SubMenuContext | null) => (currentOpenedMenu.value = menu) + ); + + function onMenuKeyDown(e: KeyboardEvent) { + let handled = true; + //Handle keyboard event + switch (e.key) { + case "Escape": { + if (currentOpenedMenu.value?.isTopLevel() === false) { + currentOpenedMenu.value?.closeCurrentSubMenu(); + } else { + closeMenu(); + } + break; + } + case "ArrowDown": + currentOpenedMenu.value?.moveCurrentItemDown(); + break; + case "ArrowUp": + currentOpenedMenu.value?.moveCurrentItemUp(); + break; + case "Home": + currentOpenedMenu.value?.moveCurrentItemFirst(); + break; + case "End": + currentOpenedMenu.value?.moveCurrentItemLast(); + break; + case "ArrowLeft": { + if (!currentOpenedMenu.value?.closeSelfAndActiveParent()) + options.value.onKeyFocusMoveLeft?.(); + break; + } + case "ArrowRight": + if (!currentOpenedMenu.value?.openCurrentItemSubMenu()) + options.value.onKeyFocusMoveRight?.(); + break; + case "Enter": + currentOpenedMenu.value?.triggerCurrentItemClick(e); + break; + default: + handled = false; + break; + } + if (handled && currentOpenedMenu.value) { + e.stopPropagation(); + e.preventDefault(); + } + } + function onBodyScroll() { + //close when docunment scroll + if (options.value.closeWhenScroll !== false) closeMenu(); + } + function onBodyClick(e: MouseEvent) { + checkTargetAndClose(e.target as HTMLElement); + } + function checkTargetAndClose(target: HTMLElement) { + //Loop target , Check whether the currently clicked element belongs to the current menu. + // If yes, it will not be closed + while (target) { + if (target.classList && target.classList.contains("mx-menu-host")) return; + target = target.parentNode as HTMLElement; + } + if (options.value.clickCloseOnOutside !== false) { + //Close menu + removeBodyEvents(); + closeMenu(); + } + } + + //provide globalOptions for child use + provide("globalOptions", options.value); + provide("globalCloseMenu", closeMenu); + provide("globalTheme", options.value?.theme || "light"); + provide("globalIsFullScreenContainer", props.isFullScreenContainer); + provide("globalClickCloseClassName", options.value?.clickCloseClassName); + provide("globalIgnoreClickClassName", options.value?.ignoreClickClassName); + provide("globalIconFontClass", options.value?.iconFontClass || "iconfont"); + provide("globalMenuTransitionProps", options.value?.menuTransitionProps); + //check slot exists + provide("globalHasSlot", (name: string) => { + return slots[name] !== undefined; + }); + //render slot + provide( + "globalRenderSlot", + (name: string, params: Record<string, unknown>) => { + return renderSlot( + slots, + name, + { ...params }, + () => [h("span", "Render slot failed")], + false + ); + } + ); + //provide menuContext for child use + provide("menuContext", { + zIndex: options.value.zIndex || MenuConstOptions.defaultZindex, + container: container.value as unknown as HTMLElement, + adjustPadding: { x: 0, y: 0 }, + getParentAbsY: () => options.value.x, + getParentAbsX: () => options.value.y, + getParentX: () => 0, + getParentY: () => 0, + getParentWidth: () => 0, + getParentHeight: () => 0, + getPositon: () => [options.value.x, options.value.y], + closeOtherSubMenuWithTimeOut: () => { + /* Do nothing */ + }, + checkCloseOtherSubMenuTimeOut: () => false, + addOpenedSubMenu: () => { + /* Do nothing */ + }, + closeOtherSubMenu: () => { + /* Do nothing */ + }, + getParentContext: () => null, + getSubMenuInstanceContext: () => null, + getElement: () => null, + addChildMenuItem: () => { + /* Do nothing */ + }, + removeChildMenuItem: () => { + /* Do nothing */ + }, + markActiveMenuItem: () => { + /* Do nothing */ + }, + markThisOpenedByKeyBoard: () => { + /* Do nothing */ + }, + isOpenedByKeyBoardFlag: () => false, + isMenuItemDataCollectedFlag: () => false, + } as SubMenuParentContext); + + //Expose instance function + defineExpose(instance); +</script> + +<style> + .mx-menu-ghost-host { + position: absolute; + left: 0; + bottom: 0; + right: 0; + top: 0; + overflow: hidden; + pointer-events: none; + } + .mx-menu-ghost-host.information_full_screen { + position: fixed; + } +</style> -- Gitblit v1.9.3