一、定义和使用
oncontextmenu 事件在元素中用户右击鼠标时触发并打开上下文菜单。
注意:所有浏览器都支持 oncontextmenu 事件, contextmenu 元素只有 Firefox 浏览器支持。
二、语法
HTML 中:
<element oncontextmenu="myScript">
JavaScript 中:
object.oncontextmenu=function(){myScript};
JavaScript 中, 使用 addEventListener() 方法:
object.addEventListener("contextmenu", myScript);
jQuery中,使用on监听事件
$(document).on('contextmenu', myScript);
注意: Internet Explorer 8 及更早 IE 浏览器版本不支持 addEventListener() 。
三、使用细节
是否支持冒泡: | Yes |
是否可以取消: | Yes |
事件类型: | MouseEvent |
支持的 HTML 标签: | 所有 HTML 元素 |
四、案例
这通过一个小案例,在考勤统计列表中,每行中对应某员工这月周期内的每天考勤数据;如果某条记录需要删除,这里通过在记录上直接右击出现删除菜单,操作起来则更为方便。如下图:
![](https://i-blog.csdnimg.cn/blog_migrate/289dbe432c3b8dc68ee4952e6b1059ed.gif)
4.1 自定义右击删除菜单步骤
通过DOM对象监听鼠标点击事件
触发鼠标点击事件,获取鼠标当前浏览器位置
根据当前位置重新调整自定义contextmenu的位置
显示自定义contextmenu
4.2 技术点
这里使用Vue技术框架来做这个案例,统计列表使用是element-ui中的table组件,事件监听和数据获取通过jquery实现,在开发前请将其通过npm进行安装。
//安装element-ui
npm i element-ui -S
//安装jquery
npm i jquery -S
4.3 html代码
这里通过列数据中isDate判断该列是否渲染为对应日期列,渲染日期相对应的考勤记录时,需要追加data-prop和data-id进行数据传递,后续执行删除操作时会使用到。
<template>
<div class="list-wrap">
<el-table
stripe
:data="tableData"
class="list-table"
style="width: 100%">
<el-table-column type="selection" width="55" fixed="left"></el-table-column>
<template v-for="(item, index) in tableColumn">
<el-table-column :prop="item.prop" :label="item.name" :key="index" :width="item.width" v-if="item.isDate">
<template slot-scope="scope">
<span class="date-cell" v-html="scope.row[item.prop]"
:data-prop="item.prop"
:data-id="scope.row['id_'+item.prop]"></span>
</template>
</el-table-column>
<el-table-column :prop="item.prop" :label="item.name" :key="index" :width="item.width" v-else fixed="left"></el-table-column>
</template>
</el-table>
</div>
</template>
<style lang="scss">
@import './index.scss';
</style>
4.4 CSS样式
.date-cell类选择器对考勤记录数据进行样式修改,更改鼠标为小手图标,并禁止选择文本内容,为避免后期右击时会误操作选中内容。
.menu-box类选择器为右击菜单增加基础样式。
.date-cell{ cursor: pointer;
-moz-user-select:none; /*火狐*/
-webkit-user-select:none; /*webkit浏览器*/
-ms-user-select:none; /*IE10*/
-khtml-user-select:none; /*早期浏览器*/
user-select:none;
}
.menu-box{
position: absolute;
ul.list{
background-color: #fff;
border: 1px solid #ccc;
li{
border-top: 1px solid #ccc;
padding: 6px 15px;
font-size: 14px;
line-height: 1.5;
color: #666;
list-style: none;
cursor: pointer;
&:first-child{
border-top: 0;
}
}
}
}
4.5 基础数据
变量tableData用于存储员工考勤数据,变量tableColumn用于存储表格列头部数据定义,在获取考勤记录后,动态追加日期列到变量tableColumn中。
<script>
export default {
data(){
return {
tableData: [],
tableColumn: [
{prop: "name", name: "姓名", width: "80px"},
{prop: "workday", name: "实际到岗/天", width: "100px"},
]
}
},
created() {
},
methods: {
}
}
</script>
此时页面样式如下图:
![](https://i-blog.csdnimg.cn/blog_migrate/04a838b6b05de8a99da7a45c23fedd22.png)
4.6 模拟数据
这里在目录位置新建demo.json文件,将以下数据拷入即可。
[
{"id":1482,"name":"张三","record":["08:30","18:00"],"recorddate":"2023-01-01"},
{"id":1481,"name":"张三","record":["08:30","18:00"],"recorddate":"2023-01-02"},
{"id":1480,"name":"张三","record":["08:30","18:00"],"recorddate":"2023-01-03"},
{"id":1479,"name":"张三","record":["08:30","18:00"],"recorddate":"2023-01-04"},
{"id":1478,"name":"张三","record":["08:30","18:00"],"recorddate":"2023-01-05"},
{"id":1477,"name":"张三","record":["08:30","18:00"],"recorddate":"2023-01-06"},
{"id":1476,"name":"张三","record":["08:30","18:00"],"recorddate":"2023-01-07"},
{"id":1475,"name":"张三","record":["08:30","18:00"],"recorddate":"2023-01-08"},
{"id":1474,"name":"张三","record":["08:30","18:00"],"recorddate":"2023-01-09"},
{"id":1473,"name":"张三","record":["08:30","18:00"],"recorddate":"2023-01-10"},
{"id":1472,"name":"张三","record":["08:30","18:00"],"recorddate":"2023-01-11"},
{"id":1471,"name":"张三","record":["08:30","18:00"],"recorddate":"2023-01-12"},
{"id":1470,"name":"张三","record":["08:30","18:00"],"recorddate":"2023-01-13"},
{"id":1469,"name":"张三","record":["08:30","18:00"],"recorddate":"2023-01-14"},
{"id":1468,"name":"张三","record":["08:30","18:00"],"recorddate":"2023-01-15"},
{"id":1467,"name":"张三","record":["08:30","18:00"],"recorddate":"2023-01-16"},
{"id":1466,"name":"张三","record":["08:30","18:00"],"recorddate":"2023-01-17"},
{"id":1465,"name":"张三","record":["08:30","18:00"],"recorddate":"2023-01-18"},
{"id":1464,"name":"张三","record":["08:30","18:00"],"recorddate":"2023-01-19"},
{"id":1463,"name":"张三","record":["08:30","18:00"],"recorddate":"2023-01-20"},
{"id":1462,"name":"张三","record":["08:30","18:00"],"recorddate":"2023-01-21"},
{"id":1461,"name":"张三","record":["08:30","18:00"],"recorddate":"2023-01-22"},
{"id":1382,"name":"李四","record":["08:30","18:00"],"recorddate":"2023-01-01"},
{"id":1381,"name":"李四","record":["08:30","18:00"],"recorddate":"2023-01-07"},
{"id":1380,"name":"李四","record":["08:30","18:00"],"recorddate":"2023-01-08"},
{"id":1379,"name":"李四","record":["08:30","18:00"],"recorddate":"2023-01-15"},
{"id":1378,"name":"李四","record":["08:30","18:00"],"recorddate":"2023-01-21"},
{"id":1377,"name":"李四","record":["08:30","18:00"],"recorddate":"2023-01-22"},
{"id":1376,"name":"王五","record":["08:30","18:00"],"recorddate":"2023-01-05"},
{"id":1375,"name":"王五","record":["08:30","18:00"],"recorddate":"2023-01-06"},
{"id":1374,"name":"王五","record":["08:30","18:00"],"recorddate":"2023-01-09"},
{"id":1373,"name":"王五","record":["08:30","18:00"],"recorddate":"2023-01-11"},
{"id":1372,"name":"王五","record":["08:30","18:00"],"recorddate":"2023-01-10"},
{"id":1371,"name":"王五","record":["08:30","18:00"],"recorddate":"2023-01-04"},
{"id":1370,"name":"王五","record":["08:30","18:00"],"recorddate":"2023-01-03"},
{"id":1369,"name":"王五","record":["08:30","18:00"],"recorddate":"2023-01-02"}
]
4.7 添加日期列
引入刚才创建好的demo.json文件,导致模拟数据。变量tableData用于存储考勤数据,变量tableColumn用于存储表格列数据,在updateList()函数执行后,获取全部唯一日期数据,和全部用户数据,分别存储到tableData和tableColumn变量中。
由于这里是通过模拟数据实现的功能效果,后期在项目中,相应数据是通过接口查询获取而来的。
<script>
import DemoData from './demo.json'
export default {
data(){
return {
tableData: [],
tableColumn: [
{prop: "name", name: "姓名", width: "80px"},
{prop: "workday", name: "实际到岗/天", width: "100px"},
]
}
},
created() {
this.updateList();
},
methods: {
//加载列表数据
updateList(){
//获取日期列表
let dateColumn = [],
employeeList = [];
//循环获取唯一 列日期 和 唯一 员工信息
DemoData.map(item => {
if(!dateColumn.includes(item.recorddate)){
dateColumn.push(item.recorddate);
}
if(!employeeList.includes(item.name)){
employeeList.push(item.name);
}
});
//升序排序
dateColumn.sort();
//修改列长度,以免name和workday固定列被替换
this.tableColumn.length = 2;
//追加日期列
this.tableColumn = this.tableColumn.concat(dateColumn.map(item => {
return {
prop: item,
name: item,
width: "100px",
isDate: true
}
}));
}
//end
}
}
</script>
以上代码完成后,日期列则可以正常显示出来了,如下图:
![](https://i-blog.csdnimg.cn/blog_migrate/c5f9df5973f33531dd75bb567923a55d.png)
4.8 重组行数据
通过变量employeeList存储的员工信息,遍历查询模拟数据中各自对应的考勤记录,并通过变量dateColumn中存储的日期作为键,存储到对应员工信息中。
<script>
import DemoData from './demo.json'
export default {
data(){
return {
tableData: [],
tableColumn: [
{prop: "name", name: "姓名", width: "80px"},
{prop: "workday", name: "实际到岗/天", width: "100px"},
]
}
},
created() {
this.updateList();
},
methods: {
//加载列表数据
updateList(){
//获取日期列表
let dateColumn = [],
employeeList = [];
//循环获取唯一 列日期 和 唯一 员工信息
DemoData.map(item => {
if(!dateColumn.includes(item.recorddate)){
dateColumn.push(item.recorddate);
}
if(!employeeList.includes(item.name)){
employeeList.push(item.name);
}
});
//升序排序
dateColumn.sort();
//修改列长度,以免name和workday固定列被替换
this.tableColumn.length = 2;
//追加列
this.tableColumn = this.tableColumn.concat(dateColumn.map(item => {
return {
prop: item,
name: item,
width: "100px",
isDate: true
}
}));
//重组员工考勤数据,一行显示考勤记录
this.tableData = employeeList.map(item => {
let nData = {},
tmp = null,
realWorkSize = 0;
//获取对应员工的考勤记录
dateColumn.forEach(key => {
tmp = DemoData.filter(sub => sub.name == item && sub.recorddate == key);
if(tmp.length==1&&tmp[0]['record'].length==2){
//记录考勤数据
nData[key] = tmp[0]['record'][0] + '<br />' + tmp[0]['record'][1];
//保存对应考勤记录的ID
nData['id_'+key] = tmp[0]['id'];
//统计到岗天数
realWorkSize++;
}else{
nData[key] = '-';
}
})
return {
name: item,
workday: realWorkSize,
...nData
}
});
}
//end
}
}
</script>
此时模拟数据中考勤记录都正常显示在页面中了,如下图:
![](https://i-blog.csdnimg.cn/blog_migrate/ac5999a50ef63b524a0dfc0052670259.gif)
五、自定义右击菜单
此时我们可以来实现自定义右击菜单了,这里只简单讲解下实现方法和步骤,如有实际需求可对此功能进行封装处理,这里先用散装代码实现此功能。
5.1 准备工作
首先引入jquery,并且添加禁用右击菜单的鼠标事件,代码如下:
<script>
import DemoData from './demo.json'
import $ from 'jquery'
//记录是否悬浮在指定单元格上
let isHoverCell = false;
//记录悬浮位置考勤记录对象
let hoverData = null;
//右击菜单Dom对象
let MenuBox = null;
//记录上一个数据的ID,用于判断是否需要新打开一个窗口
let tmpPreId = 0;
export default {
data(){
return {
tableData: [],
tableColumn: [
{prop: "name", name: "姓名", width: "80px"},
{prop: "workday", name: "实际到岗/天", width: "100px"},
]
}
},
created() {
this.updateList();
//右击菜单事件
window.oncontextmenu = function(e){
if(isHoverCell){
//取消默认的浏览器自带右键
e.preventDefault();
}
}
//event end
},
methods: {
//...
//end
}
}
</script>
绑定oncontextmenu事件后,当鼠标悬浮在考勤记录上时,则变量isHoverCell值为true,此时右击禁用则会生效。
5.2 判断鼠标位置
在methods中新建cellAddEvent()函数,将其放到updateList()函数执行结束位置,延迟100秒再执行,以防数据未渲染完jquery无法查询到全部考勤记录的DOM节点。
另外,考勤数据会因增删改查等操作,重新渲染,所以每个单元格绑定事件,也会在重新渲染后被释放,需渲染完后重新绑定事件,故cellAddEvent()函数应放应updateList()函数内执行。
<script>
import DemoData from './demo.json'
import $ from 'jquery'
//记录是否悬浮在指定单元格上
let isHoverCell = false;
//记录悬浮位置考勤记录对象
let hoverData = null;
//右击菜单Dom对象
let MenuBox = null;
//记录上一个数据的ID,用于判断是否需要新打开一个窗口
let tmpPreId = 0;
export default {
data(){
return {
tableData: [],
tableColumn: [
{prop: "name", name: "姓名", width: "80px"},
{prop: "workday", name: "实际到岗/天", width: "100px"},
]
}
},
created() {
this.updateList();
//右击菜单事件
window.oncontextmenu = function(e){
if(isHoverCell){
//取消默认的浏览器自带右键
e.preventDefault();
}
}
//event end
},
methods: {
/**
* 为考勤记录添加相应监听事件
*/
cellAddEvent(){
let that = this,
propData = "";
//悬浮事件
$('.date-cell').hover(function(){
isHoverCell = true;
let id = $(this).data('id'),
filterData = DemoData.filter(item => item.id == id);
if(filterData.length==1){
hoverData = {
prop: $(this).data('prop'),
data: filterData[0]
}
}
//if end
}, function(){
isHoverCell = false;
hoverData = null;
});
},
//加载列表数据
updateList(){
//获取日期列表
let dateColumn = [],
employeeList = [];
//循环获取唯一 列日期 和 唯一 员工信息
DemoData.map(item => {
if(!dateColumn.includes(item.recorddate)){
dateColumn.push(item.recorddate);
}
if(!employeeList.includes(item.name)){
employeeList.push(item.name);
}
});
//升序排序
dateColumn.sort();
//修改列长度,以免name和workday固定列被替换
this.tableColumn.length = 2;
//追加列
this.tableColumn = this.tableColumn.concat(dateColumn.map(item => {
return {
prop: item,
name: item,
width: "100px",
isDate: true
}
}));
//重组员工考勤数据,一行显示考勤记录
this.tableData = employeeList.map(item => {
let nData = {},
tmp = null,
realWorkSize = 0;
//获取对应员工的考勤记录
dateColumn.forEach(key => {
tmp = DemoData.filter(sub => sub.name == item && sub.recorddate == key);
if(tmp.length==1&&tmp[0]['record'].length==2){
//记录考勤数据
nData[key] = tmp[0]['record'][0] + '<br />' + tmp[0]['record'][1];
//保存对应考勤记录的ID
nData['id_'+key] = tmp[0]['id'];
//统计到岗天数
realWorkSize++;
}else{
nData[key] = '-';
}
})
return {
name: item,
workday: realWorkSize,
...nData
}
});
setTimeout(() => {
this.cellAddEvent();
}, 100);
}
//end
}
}
</script>
此时大家会发现,鼠标在页面其他位置可以正常右击显示默认菜单,但放到考勤记录上时,右击菜单则失效无法显示了。
5.3 添加自定义菜单
当变量isHoverCell为true时,右击执行rightMenuInitial()函数,通过jquery创建菜单容器后,追加到body中。此时右击则能看到删除按钮了。
<script>
import DemoData from './demo.json'
import $ from 'jquery'
//记录是否悬浮在指定单元格上
let isHoverCell = false;
//记录悬浮位置考勤记录对象
let hoverData = null;
//右击菜单Dom对象
let MenuBox = null;
//记录上一个数据的ID,用于判断是否需要新打开一个窗口
let tmpPreId = 0;
export default {
data(){
return {
tableData: [],
tableColumn: [
{prop: "name", name: "姓名", width: "80px"},
{prop: "workday", name: "实际到岗/天", width: "100px"},
]
}
},
created() {
this.updateList();
let that = this;
//右击菜单事件
window.oncontextmenu = function(e){
if(isHoverCell){
//取消默认的浏览器自带右键
e.preventDefault();
that.rightMenuInitial(e);
}
}
//event end
},
methods: {
/**
* 右击初始化弹框事件
* @param e 右击事件
*/
rightMenuInitial(e){
if(null==hoverData||'undefined'===typeof hoverData.data){
this.$message.info("数据读取失败,刷新后再试~");
return;
}
if(null!=MenuBox) {
//上一个右击区域未变化,则结束后续操作
if(tmpPreId==hoverData.data.id) return;
};
//记录当前ID
tmpPreId = hoverData.data.id;
//创建右击菜单容器
MenuBox = $('<div />').addClass('menu-box');
let that = this,
listData = [
{id: 1, name: "删除", style: "delete"}
],
ulBox = $('<ul />').addClass('list');
//添加列表
MenuBox.append(ulBox);
//循环生成右击菜单列表
for(var i in listData){
ulBox.append($('<li />').text(listData[i]['name']).addClass(listData[i]['style']));
}
//for end
//添加位置
MenuBox.css({
'position': 'absolute',
'left': e.clientX,
'top': e.clientY,
'z-index': 10
});
ulBox.hover(function(){
isHoverCell = true;
}, function(){
isHoverCell = false;
});
/**
* 添加删除事件
*/
ulBox.on('click', 'li.delete', function(){
that.$confirm('确定删除'+ hoverData.data.name + '的' + hoverData.prop+'考勤记录吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
//查询源数据对应位置,将其删除(这里Demo演示,真实环境需要将删除数据ID传给后台进行操作)
let index = DemoData.indexOf(hoverData.data);
DemoData.splice(index, 1);
that.updateList();
}).catch(() => {
that.$message({
type: 'info',
message: '已取消删除'
});
});
});
//追加到页面中
$('body').append(MenuBox);
},
//...
//end
}
}
</script>
5.4 增加移除菜单功能
增加removeMenuBox()函数,用于在不同情况下,需要移除自定义菜单。代码如下:
<script>
import DemoData from './demo.json'
import $ from 'jquery'
//记录是否悬浮在指定单元格上
let isHoverCell = false;
//记录悬浮位置考勤记录对象
let hoverData = null;
//右击菜单Dom对象
let MenuBox = null;
//记录上一个数据的ID,用于判断是否需要新打开一个窗口
let tmpPreId = 0;
export default {
data(){
return {
tableData: [],
tableColumn: [
{prop: "name", name: "姓名", width: "80px"},
{prop: "workday", name: "实际到岗/天", width: "100px"},
]
}
},
created() {
this.updateList();
let that = this;
//右击菜单事件
window.oncontextmenu = function(e){
if(isHoverCell){
//取消默认的浏览器自带右键
e.preventDefault();
that.rightMenuInitial(e);
}
}
//event end
},
methods: {
/**
* 删除右击菜单
* @date 2023/1/30
*/
removeMenuBox(){
if(!MenuBox) return;
MenuBox.remove();
MenuBox = null;
},
/**
* 右击初始化弹框事件
* @param e 右击事件
*/
rightMenuInitial(e){
if(null==hoverData||'undefined'===typeof hoverData.data){
this.$message.info("数据读取失败,刷新后再试~");
return;
}
if(null!=MenuBox) {
this.removeMenuBox();
//上一个右击区域未变化,则结束后续操作
if(tmpPreId==hoverData.data.id) return;
};
//记录当前ID
tmpPreId = hoverData.data.id;
//创建右击菜单容器
MenuBox = $('<div />').addClass('menu-box');
let that = this,
listData = [
{id: 1, name: "删除", style: "delete"}
],
ulBox = $('<ul />').addClass('list');
//添加列表
MenuBox.append(ulBox);
//循环生成右击菜单列表
for(var i in listData){
ulBox.append($('<li />').text(listData[i]['name']).addClass(listData[i]['style']));
}
//for end
//添加位置
MenuBox.css({
'position': 'absolute',
'left': e.clientX,
'top': e.clientY,
'z-index': 10
});
//自定义菜单悬浮事件监听
ulBox.hover(function(){
isHoverCell = true;
}, function(){
isHoverCell = false;
});
/**
* 添加删除事件
*/
ulBox.on('click', 'li.delete', function(){
that.$confirm('确定删除'+ hoverData.data.name + '的' + hoverData.prop+'考勤记录吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
//查询源数据对应位置,将其删除(这里Demo演示,真实环境需要将删除数据ID传给后台进行操作)
let index = DemoData.indexOf(hoverData.data);
DemoData.splice(index, 1);
that.removeMenuBox();
that.updateList();
}).catch(() => {
that.removeMenuBox();
that.$message({
type: 'info',
message: '已取消删除'
});
});
});
//追加到页面中
$('body').append(MenuBox);
},
/**
* 为考勤记录添加相应监听事件
*/
cellAddEvent(){
let that = this,
propData = "";
//悬浮事件
$('.date-cell').hover(function(){
isHoverCell = true;
let id = $(this).data('id'),
filterData = DemoData.filter(item => item.id == id);
if(filterData.length==1){
hoverData = {
prop: $(this).data('prop'),
data: filterData[0]
}
}
// console.log('id', id);e
//if end
}, function(){
isHoverCell = false;
});
//鼠标移出事件,关闭删除菜单
$(document).off('click').on('click', function(){
if(!isHoverCell&&null!=MenuBox){
that.removeMenuBox();
}
});
},
//加载列表数据
updateList(){
//获取日期列表
let dateColumn = [],
employeeList = [];
//循环获取唯一 列日期 和 唯一 员工信息
DemoData.map(item => {
if(!dateColumn.includes(item.recorddate)){
dateColumn.push(item.recorddate);
}
if(!employeeList.includes(item.name)){
employeeList.push(item.name);
}
});
//升序排序
dateColumn.sort();
//修改列长度,以免name和workday固定列被替换
this.tableColumn.length = 2;
//追加列
this.tableColumn = this.tableColumn.concat(dateColumn.map(item => {
return {
prop: item,
name: item,
width: "100px",
isDate: true
}
}));
//重组员工考勤数据,一行显示考勤记录
this.tableData = employeeList.map(item => {
let nData = {},
tmp = null,
realWorkSize = 0;
//获取对应员工的考勤记录
dateColumn.forEach(key => {
tmp = DemoData.filter(sub => sub.name == item && sub.recorddate == key);
if(tmp.length==1&&tmp[0]['record'].length==2){
//记录考勤数据
nData[key] = tmp[0]['record'][0] + '<br />' + tmp[0]['record'][1];
//保存对应考勤记录的ID
nData['id_'+key] = tmp[0]['id'];
//统计到岗天数
realWorkSize++;
}else{
nData[key] = '-';
}
})
return {
name: item,
workday: realWorkSize,
...nData
}
});
setTimeout(() => {
this.cellAddEvent();
}, 100);
}
//end
}
}
</script>
5.5 追加菜单内容
在自定义右击菜单时,已预留了增加更多菜单项位置,如下代码,在变量listData中添加菜单内容即可。
rightMenuInitial(e){
if(null==hoverData||'undefined'===typeof hoverData.data){
this.$message.info("数据读取失败,刷新后再试~");
return;
}
if(null!=MenuBox) {
this.removeMenuBox();
//上一个右击区域未变化,则结束后续操作
if(tmpPreId==hoverData.data.id) return;
};
//记录当前ID
tmpPreId = hoverData.data.id;
//创建右击菜单容器
MenuBox = $('<div />').addClass('menu-box');
let that = this,
listData = [
{id: 1, name: "编辑", style: "edit"},
{id: 2, name: "删除", style: "delete"}
],
ulBox = $('<ul />').addClass('list');
//...
}
如下图,右击时自定义菜单中则出现“编辑”项了,这块功能这里就不作详解,大家可以自行操作。这里就简单讲解下自定义右击菜单和删除功能,仅供大家参考,如有不足之处请见谅!
![](https://i-blog.csdnimg.cn/blog_migrate/779438e43990c8dd009787c72b394de5.png)