社区编辑申请
注册/登录
前端实现右键自定义菜单
开发 前端
实现自定义菜单的逻辑并不复杂,也就是修改 contextmenu 事件的行为,显示或隐藏自己写的 div。

大家好,我是前端西瓜哥。

本文将讲解 Web 页面如何实现自定义菜单功能。

线上 demo:

https://codepen.io/F-star/pen/WNOvQVQ。

思路

核心思路是:注册 contextmenu 事件,取消该事件的默认行为,然后通过 event 对象拿到光标相对视口的坐标位置(event.clientX 和 event.clientY),通过绝对定位的方式,将自己自定义的初始化时不可见的 div 块显示出来。

实现

DOM 结构

首先是 DOM 结构。结构依次为:

  • div.page-view 为注册 contextmenu 事件的元素。
  • div.contextmenu-mask 是遮罩层,遮住整个窗口。它随右键菜单出现而出现,作用是防止用户调出右键菜单后,还可以点击菜单外的按钮。此外还可以添加有透明度的背景色,但这样效果就类似弹窗了。一般来说,都是不设置底色的。
  • div.contextmenu-content 右键菜单的内容。
<div class="page-view">
点击区域
</div>
<div class="contextmenu-mask" style="display: none;"></div>
<div class="contextmenu-content">
<div class="list">
<div class="item">复制</div>
<div class="item">剪切</div>
<div class="item">粘贴粘贴粘贴粘贴粘贴粘贴粘贴粘贴</div>
<div class="item">全选</div>
</div>
</div>

CSS 样式

.page-view {
margin: 0 auto;
width: 90%;
height: calc(100vh - 30px);
background-color: azure;
}
/* 遮罩层 */
.contextmenu-mask {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
/* background-color: #000; */
/* opacity: .2; */
z-index: 45;
}
/* 菜单内容的容器 */
.contextmenu-content {
position: fixed;
left: 999999px;
top: 999999px;
z-index: 50;
user-select: none;
}
/* 例子使用内容 */
.list {
border: 1px solid #555;
border-radius: 4px;
min-width: 180px;
overflow: hidden; /* 处理圆角 */
}
.item {
box-sizing: border-box;
padding: 0 5px;
height: 30px;
line-height: 30px;
word-break: keep-all; /* 很重要,否则会换行 */
background-color: #fff;
cursor: default;
}
.item:hover {
background-color: dodgerblue;
color: #fff;
}

这里有几个注意点:

  • contextmenu-content 并没有使用 display: none 的方式进行隐藏,而是通过设置非常大的 left 和 top 的方式跑到窗口外的远方。这是有原因的,我们将会在后面的脚本逻辑中进行详细讲解。
  • item 需要设置 word-break: keep-all; 。因为当菜单跑到窗口外时,宽度会变成最小宽度,在这里是 180px。只有设置了该属性和值,才能让文字不换行,得到我们想要的宽度。
  • contextmenu-content 需要使用固定定位,不能使用绝对定位。因为设置了大值的 left 和 top 的元素,对不是 overflow: hidden 的容器元素,会产生一个非常长的滚动条。固定定位则不会。

脚本逻辑

右键显示菜单

首先取消掉点击区域的菜单事件的默认行为。

拿到光标的坐标,为防止菜单部分跑到窗口外,导致被切割,需要对坐标进行调整。对此我们需要再拿到 菜单的宽高、窗口可视区域宽高。

此外为了防止菜单边缘紧贴窗口边缘,效果不美观,需要设置一个 最小 padding 值 参与计算。

被截断的菜单:

紧贴窗口边缘的菜单:

以设置横坐标为例,有:

if (e.clientX + contextmenuWidth > document.documentElement.clientWidth - PADDING_RIGHT) {
finalX = e.clientX - contextmenuWidth
}

这里代码的意思是:当预测发现当前光标作为菜单的左侧时,会导致菜单右侧一部分被切割,就以当前坐标作为菜单的右侧,此时的左上角的坐标为光标减去菜单宽度的值。

完整代码为:

const areaEl = document.querySelector('.page-view')
const mask = document.querySelector('.contextmenu-mask')
const contentEl = document.querySelector('.contextmenu-content')
/**
*
* @param {number} x 将要设置的菜单的左上角坐标 x
* @param {number} y 左上角 y
* @param {number} w 菜单的宽度
* @param {number} h 菜单的高度
* @returns {x, y} 调整后的坐标
*/
const adjustPos = (x, y, w, h) => {
const PADDING_RIGHT = 6 // 右边留点空位,防止直接贴边了,不好看
const PADDING_BOTTOM = 6 // 底部也留点空位
const vw = document.documentElement.clientWidth
const vh = document.documentElement.clientHeight
if (x + w > vw - PADDING_RIGHT) x -= w
if (y + h > vh - PADDING_BOTTOM) y -= h
return {x, y}
}
const onContextMenu = e => {
e.preventDefault()
const rect = contentEl.getBoundingClientRect()
// console.log(rect)
const { x, y } = adjustPos(e.clientX, e.clientY, rect.width, rect.height)
showContextMenu(x, y)
}
// 阻止指定元素下的菜单事件
areaEl.addEventListener('contextmenu', onContextMenu, false)

隐藏右键菜单没有使用常规的 display: none;,而是改为使用设置了很大值的 left 和 top。这是因为我要实现的是 自适应宽高 的右键菜单。

为此需要动态拿到菜单的宽高,需要用到

Element.getBoundingClientRect() 方法,而这个方法需要元素在 DOM 树中,且为可见元素,才能拿到宽高,否则只能拿到两个 0。

如果你要实现的菜单是手动写死宽度的,高度通过菜单项的数量来计算的,那么隐藏菜单最好的方案是 display: none。

隐藏菜单和点击菜单项

然后就是点击遮罩层,隐藏菜单和遮罩。以及点击菜单项,执行对应的命令

const hideContextMenu = () => {
mask.style.display = 'none'
contentEl.style.top = '99999px'
contentEl.style.left = '99999px'
}
// 点击蒙版,隐藏
mask.addEventListener('mousedown', () => {
hideContextMenu()
}, false)
// 点击菜单,隐藏
contentEl.addEventListener('click', (e) => {
console.log('点击:', e.target.textContent)
// 执行菜单项对应命令
hideContextMenu()
}, false)

其他要考虑的地方

  • 窗口缩小的情况:窗口缩小会导致右下方的菜单跑到窗口区域外,是否考虑监听窗口事件。
  • 菜单上再点右键的逻辑:是以这个位置重新定位右键菜单,还是等同于点击了左键的效果,还是不进行处理,弹出浏览器原生右键菜单,需要根据需求进行选择。

结尾

实现自定义菜单的逻辑并不复杂,也就是修改 contextmenu 事件的行为,显示或隐藏自己写的 div。

但里面有些细节需要处理好,才能写出一个没有 bug 的优秀右键菜单。

责任编辑:姜华 来源: 今日头条
相关推荐

2022-06-07 10:13:22

前端沙箱对象

2022-06-24 07:08:24

OHOS自定义服务

2022-06-21 14:30:16

Vim自定义Linux

2022-04-21 14:29:40

前端文件预览

2022-06-20 15:19:51

前端监控方案

2022-06-16 15:54:32

前端

2022-05-17 08:39:05

VueViteTypeScript

2022-05-30 09:32:07

Spring容器

2022-06-09 09:27:16

前端行业生存

2022-06-23 09:22:57

Vue技巧前端

2022-04-18 17:28:14

React前端

2022-05-26 21:33:09

业务前端测试

2022-06-20 09:01:20

半导体芯片

2022-06-07 14:15:44

Vue开发工具

2022-02-17 09:50:36

Zabbix联动登录LDAP认证

2022-04-14 08:51:49

微服务Redisson分布式锁

2022-06-10 09:00:53

前端项目个JSON

2022-06-16 08:35:10

CSS属性前端

2022-05-07 15:44:45

eTS 开发鸿蒙

2022-05-22 21:23:10

前端监控系统

同话题下的热门内容

手把手教你用装饰器扩展 Python 计时器IOC-Golang 的 AOP 原理与应用Vue 里,多级菜单要如何设计才显得专业?分析了 700 万份工作需求,市场需求最高的八种编程语言是这些Vue 2.7 正式发布,代号为 Naruto手把手教你实现一个 Python 计时器2022 年编程语言趋势:Swift、Kotlin 热度持续增长,收入最高的五种语言竟是它们分布式事务(Seata) 四大模式详解

编辑推荐

太厉害了,终于有人能把TCP/IP协议讲的明明白白了!牛人5次面试腾讯不成功的经验HBase原理–所有Region切分的细节都在这里了Javascript如何监听页面刷新和关闭事件如何搭建一个HTTPS服务端
我收藏的内容
点赞
收藏

51CTO技术栈公众号