记录--Vue 右键菜单的秘密:自适应位置的实现方法
这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助
下图这个情景,你是否也遇到过?
当你右键点击网页上的某个元素时,弹出的菜单被屏幕边缘遮挡了,导致你无法看清或选择菜单项?
上图中右键菜单的选项并不是固定不变的,它会根据不同的元素或场景来显示不同的选项。
也就是说,菜单的内容和大小都是动态生成的,而不是预先设定好的。
这就给我们调整菜单位置带来了一定的难度,不过当你看完这篇文章所有的问题都不再是问题。
分析问题
遇事不决先画图,我们要解决的问题本质上就是菜单生成的位置,所以我们画个图来找一下头绪:
我们通过上图可以知道,菜单能否在视口中放得下,取决于两个条件:
windowW(视口宽度) - mouseX(鼠标 x 坐标) > menuW(菜单宽度)
windowH(视口高度) - mouseY(鼠标 y 坐标) > menuH(菜单高度)
当同时满足这两个条件的时候说明菜单放得下,那我们就要思考如果不满足条件的时候怎么办了。
如果不满足条件一说明宽度放不下,那我们就让菜单生成到鼠标的左边 mouseX - menuW
,就像下图这样。
如果不满足条件二说明高度放不下,那我们就让菜单贴底 windowH - menuH
,像这样。
那如果两个条件都不满足,就同时应用两个解决办法。
解决问题
先来看一下现在的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | <template> <div ref = "containerRef" > <slot></slot> <Teleport to= "body" > <div v- if = "showMenu" class = "context-menu" :style="{ left: mouseX + 'px' , top: mouseY + 'px' , }"> <div class = "menu-list" > <div @click= "handleClick(item)" class = "menu-item" v- for = "(item, i) in menu" :key= "item.label" > {{ item.label }} </div> </div> </div> </Teleport> </div> </template> <script setup> import { ref } from 'vue' ; import useContextMenu from './useContextMenu' ; const props = defineProps({ menu: { type: Array, default : () => [], }, }); const containerRef = ref ( null ); const { mouseX, mouseY, showMenu } = useContextMenu(containerRef); function handleClick() { showMenu.value = false ; } </script> |
看到我们现在是直接将鼠标的坐标赋值给了菜单,那么接下来就要给菜单一个经过计算的合适位置。
我们知道视口的大小、鼠标的位置、菜单的大小都是会变化的,所以这几个数据都要是响应式。
现在仅仅知道鼠标的位置,还需要知道视口与菜单的大小。
视口大小我们写一个函数来监听视口大小的变化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | import { ref } from "vue" ; const windowW = ref (document.documentElement.clientWidth); const windowH = ref (document.documentElement.clientHeight); window.addEventListener( "resize" , () => { windowW.value = document.documentElement.clientWidth; windowH.value = document.documentElement.clientHeight; }); export default function () { return { windowW, windowH, }; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | const map = new WeakMap(); const ob = new ResizeObserver((entries) => { for ( const entry of entries) { // 这个元素对应的回调函数? const handler = map. get (entry.target); if (handler) { const box = entry.borderBoxSize[0]; handler({ width: box.inlineSize, height: box.blockSize, }); } } }); export default { mounted(el, binding) { // 监视尺寸变化 ob.observe(el); map. set (el, binding.value); }, unmounted(el) { // 取消监听 ob.unobserve(el); }, }; |
现在这些值我们都已经知道了,我们去实现一下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | <template> <div ref = "containerRef" > <slot></slot> <Teleport to= "body" > <!-- 将计算好的位置赋值给菜单 --> <div v- if = "showMenu" class = "context-menu" :style="{ left: pos.posX + 'px' , top: pos.posY + 'px' , }"> <!-- 指令为全局指令,在菜单上使用指令来监听菜单尺寸的变化并触发函数 --> <div v-size-ob= "handleSize" class = "menu-list" > <div @click= "handleClick(item)" class = "menu-item" v- for = "(item, i) in menu" :key= "item.label" > {{ item.label }} </div> </div> </div> </Teleport> </div> </template> <script setup> import { ref } from 'vue' ; import useContextMenu from './useContextMenu' ; import { computed } from '@vue/reactivity' ; // 引入监听视口大小的函数 import useViewport from './useViewport' ; const props = defineProps({ menu: { type: Array, default : () => [], }, }); const containerRef = ref ( null ); const { mouseX, mouseY, showMenu } = useContextMenu(containerRef); // 声明两个响应式变量,用来记录菜单大小的变化。 const menuW = ref (0); const menuH = ref (0); function handleSize({ width, height }) { menuW.value = width; menuH.value = height; } // 获得视口的大小 const { windowW, windowH } = useViewport(); // 计算属性,用来计算菜单合适的位置 const pos = computed(() => { let posX = mouseX.value; let posY = mouseY.value; // 宽度放不下生成新的位置 if (mouseX.value > windowW.value - menuW.value) { posX = mouseX.value - menuW.value } // 高度放不下生成新的位置 if (mouseY.value > windowH.value - menuH.value) { posY = windowH.value - menuH.value } return { posX, posY, }; }); function handleClick() { showMenu.value = false ; } </script> |
我们现在来看一下效果如何。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 【.NET】调用本地 Deepseek 模型
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库