使用 element-ui 的时候,没有右键菜单,是个很头疼的事情。使用插件,很多功能又不能很好的兼容,于是快速封装一个,和项目贴合度 100%。
希望的方式
我希望在使需要的区域实现可以出现右键菜单,那么在这个区域的组件中添加一个右键菜单组件即可,通过属性配置菜单内容。
效果图
思路
右键菜单组件获取父组件,给父组件添加右键事件,然后渲染菜单选项。
嗯,就是这么简单。
实现
1、获取父组件
首先,要封装一个组件 ContextMenu
,在挂载后,给父组件添加右键事件。
mounted() {
this.$el.parentElement?.addEventListener('contextmenu', () => {
// 这里将要实现菜单的出现
// 首先,要阻止默认事件
e.preventDefault();
})
}
然后我们在目标组件中添加该组件。
<div>
<ContextMenu />
</div>
此时,目标右键已经不会有默认浏览器菜单出现了。我们实现了第一步。
展示我们的菜单
菜单需要出现在我们点击的位置,所以很自然,需要一个绝对定位。通过右键点击事件,获取具体的 x/y 的位置。
onContextMenu(e) {
e.preventDefault();
this.x = e.offsetX;
this.y = e.offsetY;
}
<div class="relative">
<div class="absolute" :style="{top: `${y}px`, left: `${x}px`}">
菜单内容
</div>
</div>
现在已经可以看到菜单了。
但我们需要的是在点击时出现,所以还要添加一个控制变量:
onContextMenu(e) {
e.preventDefault();
this.x = e.offsetX;
this.y = e.offsetY;
this.show = true;
}
同时在菜单组件中,使用 v-show="show"
来控制它即可。
响应问题
现在可以正常显示了,但是出现后,无法消失。
思考消失条件:
- 左键点击其他地方
- 右键点击其他地方
所以,我们要在 window
上直接挂载点击事件,将 show
置为 false
。
mounted() {
window.addEventListener('click', () => {
this.show = false;
});
window.addEventListener('contextmenu', () => {
this.show = false;
});
}
同时不要忘记在注销组件时注销事件:
beforeDestroy() {
window.removeEventListener('click', () => {
this.show = false;
});
window.removeEventListener('contextmenu', () => {
this.show = false;
});
}
此时还有两个问题:
- 点击菜单时会有穿透问题
- 右键菜单不响应了
第一个问题,通过在组件上添加 `@click.stop.prevent
解决。
第二个问题,是因为 window
上与父组件的 contextmenu
事件冲突,可以通过 setTimeout
来解决,再加一个加载动画效果,实现更符合菜单的效果。
onContextMenu(e) {
e.preventDefault();
this.x = e.offsetX;
this.y = e.offsetY;
setTimeout(() => {
this.show = true;
}, 100);
}
配置菜单选项
这里常用的有两种方式:
- 属性配置
- 插槽直接编写
我这里采用属性配置,定义菜单的通用类型:
Array<{
label: string;
action?: () => void;
}>
再定义一个 prop
来接收菜单配置:
props: {
menu: Array
}
然后,使用时添加内容即可:
<ContextMenu :menu="menu" />
杂项
剩下就是调整菜单的样式了,这里不赘述。
完整代码
<template>
<div class="ContextMenu relative" v-show="menu.length > 0">
<transition name="el-fade-in">
<div
class="absolute gap-y-xs menu-wrapper"
:style="{
top: `${y}px`,
left: `${x}px`,
backgroundColor: 'white',
borderRadius: '4px',
padding: '6px 16px'
}"
v-show="show"
@contextmenu.stop.prevent
@click.stop.prevent
>
<el-link
v-for="(item, index) in menu"
class="nowrap"
:key="index"
:underline="false"
@click.stop="
() => {
show = false;
item?.action();
}
"
>
{{ item.label }}
</el-link>
</div>
</transition>
</div>
</template>
<script lang="ts">
import Vue, { PropType } from 'vue';
interface IMenu {
label: string;
action?: () => void;
icon?: string;
}
export default Vue.extend({
name: 'ContextMenu',
props: {
menu: Array as PropType<Array<IMenu>>
},
data() {
return {
x: 0,
y: 0,
show: false
};
},
mounted() {
window.addEventListener('click', () => {
this.show = false;
});
window.addEventListener('contextmenu', () => {
this.show = false;
});
this.$el.parentElement?.addEventListener('contextmenu', this.onContextMenu);
},
beforeDestroy() {
window.removeEventListener('click', () => {
this.show = false;
});
window.removeEventListener('contextmenu', () => {
this.show = false;
});
},
methods: {
onContextMenu(e: MouseEvent) {
e.preventDefault();
this.x = e.offsetX;
this.y = e.offsetY;
setTimeout(() => {
this.show = true;
}, 100);
}
}
});
</script>
<style lang="scss" scoped>
.ContextMenu {
.menu-wrapper {
& > * {
display: block;
}
}
}
</style>
说明
这只是提供一个最小化实现方式的思路,它的问题本身还有很多,比如:
- 父组件存在位置样式,会影响菜单的位置
- 父组件设置了
overflow: hidden
,会影响菜单的展示
这里只是列举两项,还有很多情况,甚至还有一些边界情况等等 ,所以真正封装一个菜单,要考虑的内容远比这些多很多。
文章评论