大家好,我是前端西瓜哥。
本文将讲解 Web 页面如何实现自定义菜单功能。
线上 demo:
思路
核心思路是:注册 contextmenu 事件,取消该事件的默认行为,然后通过 event 对象拿到光标相对视口的坐标位置(event.clientX 和 event.clientY),通过绝对定位的方式,将自己自定义的初始化时不可见的 div 块显示出来。
实现DOM 结构
首先是 DOM 结构。结构依次为:
div.page-view 为注册 contextmenu 事件的元素;div.contextmenu-mask 是遮罩层,遮住整个窗口。它随右键菜单出现而出现,作用是防止用户调出右键菜单后,还可以点击菜单外的按钮。此外还可以添加有透明度的背景色,但这样效果就类似弹窗了。一般来说,都是不设置底色的。div.contextmenu-content 右键菜单的内容。
.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: px; top: px; 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 = 'px' contentEl.style.left = 'px'}// 点击蒙版,隐藏mask.addEventListener('mousedown', () => { hideContextMenu()}, false)// 点击菜单,隐藏contentEl.addEventListener('click', (e) => { console.log('点击:', e.target.textContent) // 执行菜单项对应命令 hideContextMenu()}, false)其他要考虑的地方窗口缩小的情况:窗口缩小会导致右下方的菜单跑到窗口区域外,是否考虑监听窗口事件。菜单上再点右键的逻辑:是以这个位置重新定位右键菜单,还是等同于点击了左键的效果,还是不进行处理,弹出浏览器原生右键菜单,需要根据需求进行选择。结尾
实现自定义菜单的逻辑并不复杂,也就是修改 contextmenu 事件的行为,显示或隐藏自己写的 div。
但里面有些细节需要处理好,才能写出一个没有 bug 的优秀右键菜单。
标签: 菜单
②文章观点仅代表原作者本人不代表本站立场,并不完全代表本站赞同其观点和对其真实性负责。
③文章版权归原作者所有,部分转载文章仅为传播更多信息、受益服务用户之目的,如信息标记有误,请联系站长修正。
④本站一律禁止以任何方式发布或转载任何违法违规的相关信息,如发现本站上有涉嫌侵权/违规及任何不妥的内容,请第一时间反馈。发送邮件到 88667178@qq.com,经核实立即修正或删除。