插件窝 干货文章 Vue实现右键菜单组件的超详细教程(支持快捷键)

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

菜单 class event 组件 466    来源:    2024-10-18

在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实现右键菜单组件内容请搜索插件窝以前的文章或继续浏览下面的相关文章希望大家以后多多支持插件窝!