Vue实现右键菜单组件的超详细教程(支持快捷键)

 更新时间:2024年02月22日 08:27:21   作者:刘在心中-Dennis  
右键菜单组件非常常见,所有的前端开发工程师都会遇到类似的功能组件开发需求,这篇文章主要给大家介绍了关于Vue实现右键菜单组件的超详细教程,文中介绍的方法支持快捷键,需要的朋友可以参考下

在Web应用程序开发中,右键菜单是一个常见的功能需求。它允许用户通过鼠标右键点击元素,弹出一个自定义的菜单,提供一系列操作选项。Vue.js作为一种流行的JavaScript框架,提供了丰富的工具和组件,可以轻松实现各种交互效果,包括右键菜单。本文将向你展示如何使用Vue.js实现一个灵活可定制的右键菜单组件。

使用Vue.js的组件化开发方式来实现右键菜单组件。该组件接受一个选项数组作为参数,每个选项包含菜单项的名称、点击事件、图标和快捷键提示。当用户右键点击某个元素时,组件会根据鼠标位置显示菜单,并响应用户的点击事件。组件还支持快捷键操作,用户可以通过按下指定的组合键来触发对应的菜单项。

图片展示

一.代码说明

1.属性定义

组件代码解析: 首先,我们需要在Vue组件中定义以下数据属性:

  • isContextMenuVisible:控制右键菜单的显示和隐藏。
  • contextMenuStyle:用于设置右键菜单的位置。
  • pressedKeys:存储按下的组合键。
  • timeout:用于清除按键数组的定时器。

2.监听器定义

接下来,我们需要在组件的mounted钩子函数中添加事件监听器,分别监听keydown、keyup和click事件。这些事件用于实现右键菜单的显示、隐藏和快捷键操作。

 mounted() {
        window.addEventListener('keydown', this.handleKeyDown);
        window.addEventListener('keyup', this.handleKeyUp);
        window.addEventListener('click', this.handleClickOutside);
 },
 beforeUnmount() {
        window.removeEventListener('keydown', this.handleKeyDown);
        window.removeEventListener('keyup', this.handleKeyUp);
        window.removeEventListener('click', this.handleClickOutside);
 },

3.方法定义

在组件的methods中,我们定义了以下方法:

showContextMenu(event, options):显示右键菜单。该方法接受鼠标事件对象和选项数组作为参数,并根据鼠标位置计算菜单的位置。

 showContextMenu(event) {
     event.preventDefault(); // 阻止默认右键菜单
     this.isContextMenuVisible = true;

     const menuWidth = 280 // 计算弹窗的宽度,可以根据实际情况获取
     const windowWidth = window.innerWidth;
     const maxLeft = windowWidth - menuWidth; // 弹窗最大允许的 left 值

     let left = event.clientX;
     if (left > maxLeft) {
        left = maxLeft;
     }

     this.contextMenuStyle = {
        top: `${event.clientY}px`,
        left: `${left}px`
       };
},
  • hideContextMenu():隐藏右键菜单。
 hideContextMenu() {
      this.isContextMenuVisible = false;
 },
  • handleOptionClick(action):处理菜单项的点击事件。该方法调用传入的点击事件处理函数,并隐藏右键菜单。
handleOptionClick(action) {
    this.hideContextMenu();
    action(); // 执行传入的方法
},
  • handleKeyDown(event):按键按下事件。该方法将按下的键值存入pressedKeys数组,并设置定时器清空该数组。
  handleKeyDown(event) {
    const key = event.key.toLowerCase();
    if (this.pressedKeys.indexOf(key) === -1) {
       this.pressedKeys.push(key)
    }
  },
  • handleKeyUp(event):按键松开事件。该方法通过调用matchShortcut方法匹配菜单项的快捷键,并执行对应的方法。
 handleKeyUp(event) {
   this.matchShortcut(event);
   this.pressedKeys = [];
 },
  • matchShortcut(event):匹配菜单项的快捷键,并执行对应的方法。
 matchShortcut(event) {
      for (const option of this.options) {
       if (option.shortcut && this.isShortcutPressed(option.shortcutKey)) {
           event.preventDefault(); // 阻止默认快捷键操作
           option.action(); // 执行对应选项的方法
           return;
          }
      }
},
  • isShortcutPressed(shortcutKey):判断按下的组合键是否与菜单项的快捷键匹配。
 isShortcutPressed(shortcutKey) {
   const keys = shortcutKey.toLowerCase().split('+').map(key => key.trim());
   if (keys.length !== this.pressedKeys.length) {
      return false;
   }
   for (const key of keys) {
     if (!this.pressedKeys.includes(key)) {
        return false;
       }
    }
     return true;
},

最后,我们还定义了一个handleClickOutside方法,用于处理点击右键菜单外部的事件,当用户点击菜单外部时,会隐藏菜单。

  handleClickOutside(event) {
     if (!this.$el.contains(event.target)) {
       this.hideContextMenu();
     }
  },

4.完整代码

<template>
    <div id="contextMenu" v-show="isContextMenuVisible" :style="{ top: contextMenuStyle.top, left: contextMenuStyle.left }"
        class="context-menu">
        <div v-for="(option, index) in options" :key="index" @click="handleOptionClick(option.action)"
            class="context-menu-option">
            <span class="icon">{{ option.icon }}</span> <!-- 增加图标 -->
            <span>{{ option.name }}</span>
            <span class="shortcut">{{ option.shortcut }}</span> <!-- 增加快捷键提示 -->
        </div>
    </div>
</template>
  
<script>
import { ref, nextTick } from 'vue'
/**
 * 右键菜单组件
 * options : 菜单配置信息  
 * 
 * option: { name: 菜单项名称, action: 引用组件内需要调用的事件, icon: 菜单图标  shortcut: 快捷键提示, shortcutKey: 快捷键按钮组合,特殊符号需要使用英文名称 },
 *         { name: '上一页', action: this.prevPage, shortcut: "Alt+向上箭头", shortcutKey: 'alt + arrowup' },
 * 
 * 引用组件 定义方法:  this.$refs.contextMenu.showContextMenu(event, this.contextMenuOption);
 * 
 */

export default {
    props: {
        options: {
            type: Array,
            required: true,
            default: []
        },
    },
    data() {
        return {
            isContextMenuVisible: false,
            contextMenuStyle: {
                top: '0px',
                left: '0px'
            },
            pressedKeys: [],
            timeout: null,
        };
    },
    mounted() {
        window.addEventListener('keydown', this.handleKeyDown);
        window.addEventListener('keyup', this.handleKeyUp);
        window.addEventListener('click', this.handleClickOutside);
    },
    beforeUnmount() {
        window.removeEventListener('keydown', this.handleKeyDown);
        window.removeEventListener('keyup', this.handleKeyUp);
        window.removeEventListener('click', this.handleClickOutside);
    },
    methods: {
        showContextMenu(event, options) {
            event.preventDefault(); // 阻止默认右键菜单
            this.isContextMenuVisible = true;

            const menuWidth = 280 // 计算弹窗的宽度,可以根据实际情况获取
            const windowWidth = window.innerWidth;
            const maxLeft = windowWidth - menuWidth; // 弹窗最大允许的 left 值

            let left = event.clientX;
            if (left > maxLeft) {
                left = maxLeft;
            }

            this.contextMenuStyle = {
                top: `${event.clientY}px`,
                left: `${left}px`
            };
            // this.options = options;
        },
        hideContextMenu() {
            this.isContextMenuVisible = false;
        },
        handleOptionClick(action) {
            this.hideContextMenu();
            action(); // 执行传入的方法
        },

        /**
         * 按键按下事件
         * @param {*} event 
         * 
         * 1s内将按下的组合键装入数组,避免冲突
         * 
         */
        handleKeyDown(event) {
            this.pressedKeys.push(event.key.toLowerCase());
            clearTimeout(this.timeout);
            this.timeout = setTimeout(() => {
                this.pressedKeys = [];
            }, 1200);
        },

        /**
         * 按键松开事件
         * @param {*} event 
         * 
         */
        handleKeyUp(event) {
            this.matchShortcut(event);
        },

        /**
         * 用于快捷键匹配菜单项并执行相应的方法
         * @param {*} event 
         */
        matchShortcut(event) {
            for (const option of this.options) {
                if (option.shortcut && this.isShortcutPressed(option.shortcutKey)) {
                    event.preventDefault(); // 阻止默认快捷键操作
                    option.action(); // 执行对应选项的方法
                    return;
                }
            }
        },

        /**
         * 按下按键 匹配菜单项快捷键
         * @param {*} shortcutKey 
         */
        isShortcutPressed(shortcutKey) {
            const keys = shortcutKey.toLowerCase().split('+').map(key => key.trim());
            if (keys.length !== this.pressedKeys.length) {
                return false;
            }
            for (const key of keys) {
                if (!this.pressedKeys.includes(key)) {
                    return false;
                }
            }
            return true;
        },

        handleClickOutside(event) {
            if (!this.$el.contains(event.target)) {
                this.hideContextMenu();
            }
        },
    },

};
</script>
  
<style>
.context-menu {
    position: fixed;
    z-index: 1000;
    min-width: 150px;
    max-width: 300px;
    background-color: white;
    border: none;
    border-radius: 3px;
    box-shadow: 0 0 5px #ccc;
}

/* .context-menu-option {
    height: 30px;
    font-size: 14px;
    padding: 5px 5px;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
} */


.context-menu-option {
    display: flex;
    font-size: 12px;
    align-items: center;
    justify-content: center;
    padding: 10px 5px;
    cursor: pointer;
}

.context-menu-option:not(:last-child) {
    border-bottom: 1px solid #eee;
}

.icon {
    margin-right: 20px;
    /* 控制图标与文字之间的间距 */
}

.shortcut {
    margin-right: 10px;
    margin-left: 40px;
    /* 将快捷键提示放置在右侧 */
    text-align: right;
    /* 文字靠右显示 */
}


.context-menu-option:hover {
    background-color: #f0f0f0;
}
</style>
  

二. 组件使用

1. 取消默认监听器

页面需要添加监听器取消浏览器默认的右键菜单

// 在页面加载时添加事件监听器
document.addEventListener('contextmenu', function (event) {
    event.preventDefault(); // 取消默认的右键菜单行为
});

2.页面引用

使用右键菜单组件: 要在你的Vue项目中使用右键菜单组件,需要完成以下步骤: 

将上述代码保存为一个名为ContextMenu.vue的组件文件。

在需要使用右键菜单的组件中,引入ContextMenu组件并注册。

在data属性中定义一个选项数组,包含所有菜单项的配置信息。

在需要触发右键菜单的元素上,添加@contextmenu事件,调用showContextMenu方法显示菜单。

<template>
  <div>
    <!-- 此处为触发右键菜单的元素 -->
    <div @contextmenu="handleRightClick">
       右键点击我
    </div>
    
    <!-- 引入ContextMenu组件 -->
    <ContextMenu ref="contextMenu" :options="options" />
  </div>
</template>

<script>
import ContextMenu from './ContextMenu.vue';

// 在页面加载时添加事件监听器
document.addEventListener('contextmenu', function (event) {
    event.preventDefault(); // 取消默认的右键菜单行为
});

export default {
  components: {
    ContextMenu,
  },
  data() {
    return {
     contextMenuOption: [  // 右键菜单选项,shortcutKey需要按键的英文名称 ...
                { name: '上一页', action: this.prevPage, shortcut: "Alt+向上箭头", shortcutKey: 'alt + arrowup' },
                { name: '下一页', action: this.nextPage, shortcut: "Alt+向下箭头", shortcutKey: 'alt + arrowdown' },
       ],
    };
  },
 methods: {
      handleRightClick(event) {
         this.$refs.contextMenu.showContextMenu(event, this.contextMenuOption);
      },
}
};
</script>

这个组件提供了灵活的配置选项,可以满足不同场景下的需求。可以根据自己的项目需求进行定制和扩展 

总结

到此这篇关于Vue实现右键菜单组件的文章就介绍到这了,更多相关Vue实现右键菜单组件内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • vue跳转同一路由报错的问题及解决

    vue跳转同一路由报错的问题及解决

    这篇文章主要介绍了vue跳转同一路由报错的问题及解决方案,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-04-04
  • vue前端优雅展示后端十万条数据面试点剖析

    vue前端优雅展示后端十万条数据面试点剖析

    这篇文章主要为大家介绍了vue前端优雅展示后端十万条数据的考点剖析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-07-07
  • 实现一个VUE响应式属性装饰器详析

    实现一个VUE响应式属性装饰器详析

    这篇文章主要介绍了实现一个VUE响应式属性装饰器详析,文章通过围绕主题展开详细的内容介绍,具有一定的参考价值,需要的小伙伴可以参考一下
    2022-09-09
  • vuex在vite&vue3中的简单使用说明

    vuex在vite&vue3中的简单使用说明

    这篇文章主要介绍了vuex在vite&vue3中的简单使用说明,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-06-06
  • vue中父组件通过props向子组件传递数据但子组件接收不到解决办法

    vue中父组件通过props向子组件传递数据但子组件接收不到解决办法

    大家都知道可以使用props将父组件的数据传给子组件,下面这篇文章主要给大家介绍了关于vue中父组件通过props向子组件传递数据但子组件接收不到的解决办法,文中通过代码介绍的非常详细,需要的朋友可以参考下
    2024-01-01
  • 从0开始学Vue

    从0开始学Vue

    从零开始学Vue,通过一些例子,让大家概览一些基本的概念和特性,理解Vue的基础知识,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2016-10-10
  • vue项目中使用vue-i18n报错的解决方法

    vue项目中使用vue-i18n报错的解决方法

    这篇文章主要给大家介绍了关于vue项目中使用vue-i18n报错的解决方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-01-01
  • 基于Ant-design-vue的Modal弹窗 封装 命令式与Hooks用法

    基于Ant-design-vue的Modal弹窗 封装 命令式与Hooks用法

    这篇文章主要给大家介绍了基于Ant-design-vue的Modal弹窗封装命令式与Hooks用法,文中有详细的代码示例,具有一定的参考价值,感兴趣的同学可以借鉴阅读
    2023-06-06
  • vue+element开发使用el-select不能回显的处理方案

    vue+element开发使用el-select不能回显的处理方案

    这篇文章主要介绍了vue+element开发使用el-select不能回显的处理方案,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-07-07
  • 一篇文章带你使用Typescript封装一个Vue组件(简单易懂)

    一篇文章带你使用Typescript封装一个Vue组件(简单易懂)

    这篇文章主要介绍了使用Typescript封装一个Vue组件,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-06-06

最新评论