vue实现右键菜单-程序员宅基地

技术标签: plugin  vue  html  javascript  

自己写的右键菜单组件 可以直接安装 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>

效果

自定义摸版

默认

在这里插入图片描述

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/hobtdto/article/details/106035617

智能推荐

**剑指offer面试题68 - II. 二叉树的最近公共祖先**_二叉树的最近公共祖先mianshi-程序员宅基地

文章浏览阅读8.7k次。剑指offer面试题68 - II. 二叉树的最近公共祖先给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”例如,给定如下二叉树: root = [3,5,1,6,2,0,8,null,null,7,4]示例 1:输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = _二叉树的最近公共祖先mianshi

C语言——全局变量、静态本地变量、宏定义_宏定义是全局变量吗-程序员宅基地

文章浏览阅读617次。充实每天的第3个8小时!原来我之前所见到的变量大部分都是定义在函数内部的本地变量(进入函数它就才出现,出了函数就不存在了)全局变量1、定义在函数外的变量—全局变量1>生存期、作用域为全局!2>所有的函数都可以使用、访问。2、全局变量的初始化:1>没有做初始化的全局变量编译器会自动为它分配0值。(而本地变量是随机的一个值!)2>没有初始化的全局指针会得到NULL值。3>只能使用 编译时的已知值 来初始化全局变量#include <s_宏定义是全局变量吗

open failed: EACCES (Permission denied)-程序员宅基地

文章浏览阅读603次。华为H60-L03 系统4.4.2 app更新时遇见open failed: EACCES (Permission denied) 错误,读写权限都已经注册java.io.FileNotFoundException: /storage/emulated/0/download/-------.apk: open failed: EACCES (Permission de

断言函数-RF_断言rf运算-程序员宅基地

文章浏览阅读1.5k次。测试用例的目的是要验证一些操作否符合我们的预期结果,所以在测试用例中,断言函数是必不可少的一项。我们做的每一步操作都会有预期的结果,为了保证操作得到的结果符合预期,我们需要在测试用例中添加断言,来保证实际结果和预期结果一致。那么先让我们来认识一些常用的断言函数:1、should be equal 与should not be equal我们在第一行设置一个变量,并赋值1,第二行,意思是${var}应该等于1运行:会发现只是打印出了变量的值,一般来说,断言函数只起断言作用,符合断言没有任何操作,不_断言rf运算

基础干货:高效卷积,降内存提速度保精度(附论文下载)_计算机视觉研究院的博客-程序员宅基地

文章浏览阅读644次。关注并星标从此不迷路计算机视觉研究院公众号ID|ComputerVisionGzq学习群|扫码在主页获取加入方式论文地址:https://arxiv.org/pdf/1901.01928v..._卷积核搜索空间减少

sql内连接中,等值连接与自然连接的区别-程序员宅基地

文章浏览阅读892次。1. 等值连接中不要求相等属性值的属性名相同,而自然连接要求相等属性值的属性名必须相同,即两关系只有在同名属性才能进行自然连接。如上例R中的C列和S中的D列可进行等值连接,但因为属性名不同,不能进行自然连接。 2. 等值连接不将重复属性去掉,而自然连接去掉重复属性,也可以说,自然连接是去掉重复列的等值连接。如上例R中的B列和S中的B列进行等值连接时,结果有两个重复的属性列B,而进行自然连..._自然连接的属性名可以不同吗

随便推点

solidity-以太坊区块链Truffle-webpack开发入门 (十一) Debug-调试合约_truffle debug 死循环-程序员宅基地

文章浏览阅读182次。调试合约 原文地址truffle整合了debug功能,方便调试合约,跟传统的应用程序的断点调试很相似但是truffle的调试的时机跟传统软件有很大的区别,传统软件的调试是实时的run-time,而truffle的调试是跟合约程序的运行时分离的,你不能够在合约执行的时候调试,也就是说合约的执行不能被调试打断,但是因为合约的transaction操作会返回一个hash值,所以可以根据这个ha..._truffle debug 死循环

Sql语句四舍五入的几种方法-程序员宅基地

文章浏览阅读3.2k次。Access函数Int返回小于或等于指定数值表达式的最大整数。 SQLServer函数FLOOR 返回小于或等于指定数值表达式的最大整数。CEILING 返回大于或等于指定数值表达式的最小整数。项目中常常需要获取经过四舍五入后保留两位小数的数值,根据项目需求或者限制,有时候只能在SQL语句中进行四舍五入。以下是经过整理,在SQL Service 2005/2008中...

做视频影视混剪,这些点要特别注意,做到了能让你收入翻倍-程序员宅基地

文章浏览阅读2k次。前段时间大周测试了一个新号做影视混剪,大概有两个多月了,每天更新一个作品,现在终于开始有收益了,7天有2000多,全是靠的这些剪辑技巧。今天给大家分享一些简单的,坚持做下去你也可以。一、选择平台抖音、快手这一类平台需要积累一定的粉丝量才能接广告、带货、变现等,不推荐新手一开始就主做这种平台,但可以作为一个分发内容的平台。刚开始操作的时候选择一些有播放量就有收益的短视频平台,每天都能看到收益情况,有收益了,自然就有了做下去的动力。头条号、趣头条、百家号、企鹅号、大鱼号等都是开通收益权益后,有播放量就

pygame编写打地鼠游戏(6)_打地鼠pygame background image-程序员宅基地

文章浏览阅读767次。对上一次代码进一步修改,为适应背景修改了部分颜色。'''打地鼠游戏v1.5''''''1. 添加背景图为适应背景图,更改了原来设置的一些颜色的值2. 创建锤子类并实现锤子跟随鼠标功能'''# 导包导库导模块import pygame, sysfrom pygame import *from math import sin, cos, radiansfrom datetime ..._打地鼠pygame background image

C# 后端发送GET\POST请求(以企业微信发送消息为例)_后端请求getsection-程序员宅基地

文章浏览阅读2.1k次。用过企业微信的人应该都知道,如果我们要发送应用消息,首先需要用get方法获取到token,下面是后端发送get请求获取企业微信应用消息的方法:string url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=" + corpid + "&corpsecret=" + corpsecret;//corpid和corpsecret需要从企业微信后台管理里面拿到,具体方法可以看官方文档 HttpWebReq_后端请求getsection