修身养性,知行合一

  • 首页
  • 爱码
    • 系统
    • 数据库
    • JavaScript
    • CSharp
    • Python
  • 生活
    • 文化
    • 美食
  • 杂谈
  • 关于
修身养性,知行合一
码字,杂谈
  1. 首页
  2. 爱码
  3. 前端
  4. JavaScript
  5. Vue
  6. 正文

封装一个极简的右键菜单

2022年12月28日 2121点热度 0人点赞 0条评论

使用 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>

现在已经可以看到菜单了。

file

但我们需要的是在点击时出现,所以还要添加一个控制变量:

onContextMenu(e) {
    e.preventDefault();

    this.x = e.offsetX;
    this.y = e.offsetY;
    this.show = true;
}

同时在菜单组件中,使用 v-show="show" 来控制它即可。

file

响应问题

现在可以正常显示了,但是出现后,无法消失。

思考消失条件:

  • 左键点击其他地方
  • 右键点击其他地方

所以,我们要在 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,会影响菜单的展示

这里只是列举两项,还有很多情况,甚至还有一些边界情况等等 ,所以真正封装一个菜单,要考虑的内容远比这些多很多。

本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可
标签: JavaScript vue
最后更新:2022年12月28日

jeremyjone

这个人很懒,什么都没留下

打赏 点赞
< 上一篇
下一篇 >

文章评论

取消回复

文章目录
  • 希望的方式
  • 效果图
  • 思路
  • 实现
    • 1、获取父组件
    • 展示我们的菜单
    • 响应问题
    • 配置菜单选项
    • 杂项
  • 完整代码
  • 说明
最新 热点 随机
最新 热点 随机
node-sass 的安装 解决端口被占的问题 vue3 组件 Props 的声明方式 给 div 添加选中状态 请求的取消 rgb 颜色小数兼容问题
node-sass 的安装
Vue3 动态添加图片路径 真丶深入理解JavaScript异步编程(一):异步 Vuex之Getters详解 Proxy - JavaScript IdentityServer4深入使用(二)-- 认证与授权(下) LINQ 语句中格式化日期

(っ•̀ω•́)っ✎⁾⁾ 开心每一天

COPYRIGHT © 2021 jeremyjone.com. ALL RIGHTS RESERVED.

THEME KRATOS MADE BY VTROIS

京ICP备19012859号-1

京公网安备 11010802028585号