自己写的右键菜单组件 可以直接安装 npm i @xxllxx/vue-context-menu
文章目录
.
代码目录结构
├── src
├── components
└── context-menu
├── index.js // 注册
├── utils.js
├── index.vue // 菜单box
└── item.vue // 菜单项
代码
index.js
// src/components/context-menu/index.js
import contextMenu from './index.vue'
import contextMenuItem from './item.vue'
const install = (Vue, config) => {
Vue.component(config.name || contextMenu.name, contextMenu)
Vue.component(config.itemName || contextMenuItem.name, contextMenuItem)
}
if (typeof window !== 'undefined' && window.Vue) {
install(window.Vue)
}
export default {
install,
contextMenu,
contextMenuItem
}
utils.js
// src/components/context-menu/utils.js
export function getElementOffset(element) {
let offset = { left: 0, top: 0 }
let current = element.offsetParent
offset.left += element.offsetLeft
offset.top += element.offsetTop
while (current !== null) {
offset.left += current.offsetLeft
offset.top += current.offsetTop
current = current.offsetParent
}
return offset
}
// 清空菜单
export function clearContextMenu() {
if (document.querySelector('.context-menu-box')) {
let element = document.querySelector('.context-menu-box')
element.remove()
}
}
index.vue
// src/components/context-menu/index.vue
<template>
<div
v-once
style="display: block;z-index: 9999;pointer-events: visible"
class="context-menu-box"
key="context_menu"
>
<ul
ref="menu"
class="list-context-menu-wrapper"
data-hide-modal="0"
style="left: -1000px; top: -1000px; "
>
<slot v-once>
<li style="width:120px" @click="emptyClick" class="empty-text">空菜单</li>
</slot>
</ul>
</div>
</template>
<script>
import { getElementOffset, clearContextMenu } from './utils'
export default {
name: 'context-menu',
props: {
width: {
type: String,
default: ''
},
offset: {
type: Object,
default: null
},
mode: {
type: String,
default: 'contextmenu'
}
},
data() {
return {
modeList: ['contextmenu', 'click', 'all']
}
},
computed: {
trigger: function() {
if (this.modeList.find(t => t == this.mode)) return this.mode
return 'contextmenu'
}
},
mounted() {
this.$nextTick(() => {
let el = this.$el
let parent = this.$el.parentElement
let menu = this.$refs['menu']
let that = this
let offset = this.offset
if (parent) {
if (this.width) {
menu.style.width = this.width
}
parent.removeChild(el)
parent.addEventListener('click', event => {
if (this.trigger == 'contextmenu') clearContextMenu()
})
// 菜单禁用右键菜单
menu.addEventListener('contextmenu', e => {
// e.stopPropagation()
e.preventDefault()
})
const func = function(event) {
event.preventDefault()
event.stopPropagation()
clearContextMenu()
document.body.insertBefore(el, document.body.firstChild)
let x = 0
let y = 0
if (offset && !isNaN(offset.x) && !isNaN(offset.y)) {
let temp = getElementOffset(parent)
x = temp.left + offset.x
y = temp.top + offset.y
} else {
//获取鼠标视口位置
x = document.body.offsetWidth - menu.offsetWidth <= event.clientX ? event.clientX - menu.offsetWidth : event.clientX
y = 0
if (document.body.offsetHeight - menu.offsetHeight <= event.clientY) y = event.clientY - menu.offsetHeight
else y = event.clientY
if (y < 0 && document.body.offsetHeight > menu.offsetHeight) {
y = (document.body.offsetHeight - menu.offsetHeight) / 2
}
}
menu.style.left = x + 'px'
menu.style.top = y + 'px'
}
// 添加菜单事件
switch (this.trigger) {
case 'contextmenu':
parent.addEventListener('contextmenu', func)
break
case 'click':
parent.addEventListener('click', func)
break
case 'all':
parent.addEventListener('contextmenu', func)
parent.addEventListener('click', func)
break
default:
parent.addEventListener('contextmenu', func)
break
}
//#region 删除菜单
// 其他地方左键时取消已打开的菜单
document.addEventListener(
'click',
e => {
clearContextMenu()
},
true
)
// 其他地方右键时取消已打开的菜单
document.addEventListener(
'contextmenu',
e => {
// 菜单右键不取消菜单
if (!e.path.find(t => t.tagName === 'DIV' && t.className === 'context-menu-box')) clearContextMenu()
},
true
)
//#endregion
}
})
},
methods: {
emptyClick(event) {
event.stopPropagation()
}
}
}
</script>
<style lang="scss">
.context-menu-box {
position: absolute;
user-select: none;
.list-context-menu-wrapper {
position: absolute;
padding: 8px 0;
z-index: 999;
background: #fff;
box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.12);
border-radius: 3px;
border: 1px solid #e4e4e4;
box-sizing: content-box;
margin: 0px;
white-space: nowrap;
li {
list-style: none;
}
.menu-item-space {
height: 40px;
line-height: 40px;
}
.empty-text {
height: 60px;
margin: 0px auto;
padding: 0 24px;
display: flex;
justify-content: center;
align-items: center;
color: rgba(0, 0, 0, 0.4);
}
.menu-item {
&:hover {
background: rgba(0, 0, 0, 0.02);
}
&:active {
background: rgba(0, 0, 0, 0.04);
}
&:focus {
outline: -webkit-focus-ring-color auto 1px;
}
text-align: left;
font-size: 14px;
color: rgba(0, 0, 0, 0.88);
font-weight: normal;
font-style: normal;
font-stretch: normal;
letter-spacing: normal;
display: block;
cursor: pointer;
-webkit-tap-highlight-color: rgba(255, 255, 255, 0);
padding: 0 24px;
// 加图标默认样式
img,
svg {
display: inline-block;
width: 14px;
height: 14px;
vertical-align: middle;
margin-right: 9px;
}
}
.seperator {
height: 1px;
padding: 8px 0;
&::before {
content: '';
display: block;
height: 1px;
background: rgba(0, 0, 0, 0.04);
}
}
}
}
</style>
item.vue
// src/components/context-menu/item.vue
<template>
<li
:class="isEmpty? 'menu-item menu-item-space':'seperator'"
:aria-label="label"
tabindex="1"
@click="menuClick"
>
<slot>{{label}}</slot>
</li>
</template>
<script>
export default {
name: 'context-menu-item',
props: {
label: {
type: String,
default: ''
}
},
computed: {
isEmpty: function() {
if (this.label || this.$slots.default) return true
return false
}
},
created() {},
methods: {
menuClick() {
if (this.label) this.$emit('click')
}
}
}
</script>
使用
全局注册
// src/main.js
import Vue from 'vue'
//...
import context from '@/components/context-menu'
// 全局注册
Vue.use(context)
// 重命名组件名称 默认 context-menu 和 context-menu-item
// Vue.use(context, { name: 'contextMenu', itemName: 'contextMenuItem' })
//...
//...
context-menu 属性方法
参数 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
mode | 触发方式 | String | click/contextmenu/all | contextmenu |
width | 菜单宽度 | String | — | 自适应 |
offset | 固定菜单位置,根据父级的偏移量 | Object | {x:120,y:20} | null |
context-menu-item 属性
参数 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
lable | 显示文字 | —— | —— | |
@click | 触发事件 | Function | —— | —— |
例子
直接添加到需要右键菜单的元素下
// src/view/home.vue
<template>
<div>
<div class="homeItem">
1
<context-menu width="120px">
<context-menu-item @click="item2">
<!-- 自定义摸版 -->
<img src="/svg/tag.svg" />
标签
</context-menu-item>
<context-menu-item @click="item2">
<img src="/svg/wallet.svg" />
钱包
</context-menu-item >
<context-menu-item @click="item1">
<img src="/svg/package.svg" />
包裹
</context-menu-item >
<context-menu-item @click="item1">
<img src="/svg/logistics.svg" />
物流
</context-menu-item>
</context-menu >
</div>
<div class="homeItem">
2
<context-menu width="120px">
<context-menu-item @click="item2">2</context-menu-item>
</context-menu>
</div>
</div>
</template>
<script>
export default {
name: 'home',
methods: {
item1() {
alert(1)
},
item2() {
alert(2)
},
item3() {
alert(3)
}
}
}
</script>
<style scoped>
.homeItem {
background: aqua;
width: 120px;
margin-top: 20px;
height: 120px;
}
.menu-item img {
margin-top: -1px;
}
</style>