vue实现右键菜单

7 篇文章 0 订阅
2 篇文章 0 订阅

自己写的右键菜单组件 可以直接安装 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触发方式Stringclick/contextmenu/allcontextmenu
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>

效果

自定义摸版

默认

在这里插入图片描述

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

hobtdto

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值