前言
hello!各位前端大佬,已经很久没有更新文章了,这阵子萝卜换了座城市也换了份工作,思考了许多回到了自己的家乡开启了新的旅途,那么在新工作期间也遇到了许多富有挑战性的需求,那么这篇文章主要介绍下因为一个右键菜单的功能我的一些收获,话不多说,我们来具体看一下
整体的思路是按照渡一的袁老师提供的思路实现的,但是遗憾的是没有找到源码,所以跟着思路自己实现了一遍这个功能,如果侵权与我联系进行删除。
需求分析
那么我们先来聊一下右键自定义菜单都要有哪些功能:
- 触发鼠标右键事件禁用浏览器默认菜单,展示自定义菜单
- 菜单展示动态菜单项,点击菜单项触发对应事件
- 点击菜单项或者点击菜单以外的区域,自定义菜单进行隐藏
根据上述分析,我们来一步步的实现这个自定义菜单
功能实现
一、初始化项目
基于 Vue3 + Ts 我们来初始化一个项目 npm create vue
选择使用 TypeScript,其他的是否勾选不重要
二、创建自定义菜单组件
在 components 文件夹中创建组件 ContentMenu.vue 进行模板初始化,我们使用这个组件的形式是下图这样的,传入对应自定义菜单数据,展示对应的菜单内容,并且点击菜单项后获取当前选中的菜单内容,而且菜单出现的位置是我们点击的位置。
<ContextMenu class="box" :menu-list="menuList" @select="onSelect">
<div>测试内容</div>
</ContextMenu>
<script setup lang="ts">
import ContextMenu from './components/ContextMenu.vue'
const menuList = [
{ label: '菜单一', value: 1 },
{ label: '菜单二', value: 2 },
{ label: '菜单三', value: 3 }
]
const onSelect = (data: any) => {
console.log(data)
}
</script>
三、实现自定义组件思路
- 首先我们通过分析组件的使用形式不难想出我们要定义一个插槽在自定义组件中写入个性化内容
- 菜单项展示传入内容,所以要有一个循环列表展示传入的列表数据
- 右键组件区域触发 contextmenu 事件,控制自定义菜单显示
- 菜单显示位置通过鼠标点击的位置获取,菜单设置成绝对定位
- 点击菜单项、其他区域或者右键展示浏览器菜单隐藏自定义菜单
四、代码实现组件
组件结构
根据我们使用的形式不能想出结构分为一个插槽和一个菜单列表
<template>
<div ref="menuRef">
<slot></slot>
<teleport to="body">
<div
class="menu-list"
v-if="visible"
ref="menuListRef"
:style="{
width: menuWidth,
position: 'absolute',
left: x + 'px',
top: y + 'px',
background,
fontSize,
color
}"
>
<div
:class="['menu-item', border ? 'border-bottom' : '']"
v-for="item in menuList"
:key="item.value"
@click="selectMenu(item)"
>
{{ item.label }}
</div>
</div>
</teleport>
</div>
</template>
这里使用teleport
是为了防止父级出现transform
属性而影响菜单定位的位置,使用 teleport
让菜单传送到body
下
组件样式
样式这里就不过多描述了,大佬们可以根据自己喜欢的配色自定义样式
.menu-list {
min-width: 80px;
border-radius: 5px;
}
.menu-item {
cursor: pointer;
padding: 5px 10px;
box-sizing: border-box;
}
.border-bottom {
border-bottom: 1px solid #e0e0e0;
}
.menu-item:last-child {
border-bottom: none;
}
.menu-item:hover {
background: #66ccff;
border-radius: 5px;
}
组件逻辑
重点在于菜单的交互逻辑,那么我们定义一个 useContextMenu.ts 的 hooks 编写一下菜单的具体逻辑
import { onMounted, onUnmounted, ref, type Ref } from 'vue'
const useContextMenu = (menuRef: Ref<HTMLElement | null>) => {
const x: Ref<number> = ref(0)
const y: Ref<number> = ref(0)
const visible: Ref<boolean> = ref(false)
const openMenu = (e: MouseEvent) => {
e.stopPropagation()
e.preventDefault()
x.value = e.clientX
y.value = e.clientY
visible.value = true
}
const hideMenu = () => {
visible.value = false
}
onMounted(() => {
const menuDiv = menuRef.value
menuDiv?.addEventListener('contextmenu', openMenu)
window.addEventListener('click', hideMenu, true)
window.addEventListener('contextmenu', hideMenu, true)
})
onUnmounted(() => {
const menuDiv = menuRef.value
menuDiv?.removeEventListener('contextmenu', openMenu)
window.removeEventListener('click', hideMenu)
window.removeEventListener('contextmenu', hideMenu)
})
return {
x,
y,
visible
}
}
export { useContextMenu }
属性 | 说明 |
---|---|
x | 菜单定位的left 值 |
y | 菜单定位的top 值 |
visible | 菜单显示隐藏的控制变量 |
openMenu | 显示菜单的方法 |
hideMenu | 隐藏菜单的方法 |
在 onMounted
中获取传入的 dom
元素,添加 contextmenu
事件调用 openMenu
函数,获取鼠标点击的clientX
、clientY
赋值给定义的 x
、y
变量,这样就能控制菜单展示位置为当前鼠标点击位置,再将 visible
变量改为 true 就可以让菜单展示。
然后还要注册 window 的click
事件和contextmenu
让自定义菜单隐藏掉,最后在 onUnmounted
事件中清除掉注册的事件即可
组件中使用hook并发射事件
import { ref } from 'vue'
import { useContextMenu } from '@/hooks/useContextMenu'
const menuRef = ref<HTMLElement | null>(null)
const { x, y, visible } = useContextMenu(menuRef)
const emits = defineEmits(['select'])
const selectMenu = (item: IMenu) => {
emits('select', item)
}
withDefaults(
defineProps<{
menuList: IMenu[]
background?: string
fontSize?: string
color?: string
menuWidth?: string
border?: boolean
}>(),
{
background: '#333',
fontSize: '14px',
color: '#00FFFF',
menuWidth: '100px',
border: false
}
)
将定义好的 hook 在组件中引用,拿到 x
、y
、visible
,并且为菜单项定义一个click函数,当点击菜单项时,将当前点击的菜单项通过 emit
事件发送出去,这样就可以在父组件中拿到当前所点击的菜单项了
总结
那么经过上面的介绍,一个右键自定义菜单的组件就封装好了,如果文章有不正确的地方,还需大佬批评指正,一起进步!