翼度科技»论坛 编程开发 JavaScript 查看内容

使用Vue封装一个前端通用右键菜单组件

7

主题

7

帖子

21

积分

新手上路

Rank: 1

积分
21
本文将手把手实现一个基于Vue的通用的前端通用右键菜单,具有以下特性:

  • 与业务代码完全解耦
  • 支持嵌套元素的右键菜单
  • 菜单项可灵活配置

实现了一个小demo,演示地址:contextmenu-murex.vercel.app/

为什么要做右键菜单

笔者做过一个思维导图项目,需要能够对思维导图上的节点和画布进行操作,如何实现呢?右键菜单是一个不错的选择,既不占用画布空间,又有丰富的功能可供选择。但问题来了,如何实现这样一个右键菜单:

  • 组件使用方便
  • 与业务代码解耦
  • 针对不同的目标元素展示不同的右键菜单
  • 右键菜单如何定位

组件的设计

比较容易想到的是:
  1. ContextMenu
复制代码
组件传递一个与其关联的容器,右击这个容器则显示右键菜单,这样的话需要向
  1. ContextMenu
复制代码
传递容器的真实DOM元素,这样的方式不够优雅也影响效率。
  1. <ContextMenu :relation="componentA"/>
复制代码
在容器组件中嵌套
  1. ContextMenu
复制代码
组件,这个方式下容器和右键菜单的关联关系不明显,而且更要命的是两者之间产生了耦合,
  1. ContextMenu
复制代码
依赖容器组件的数据。
  1. <div class="componentA">
  2.   <ContextMenu :porps=""/>
  3. </div>
复制代码
那么有没有既能与业务组件解耦,且代码组织优雅的设计方案呢?这里笔者参考了开源组件库里对冒泡(Popover),抽屉(Drawer),下拉菜单(Dropdown)等组件的设计方案,利用插槽将业务组件置于
  1. ContextMenu
复制代码
组件中,然后是右键菜单的具体实现。
  1. <!-- ContextMenu 组件使用 -->
  2. <!-- const menu = [
  3.   { label: '部门' },
  4.   { label: '员工' },
  5.   { label: '角色' },
  6.   { label: '权限' },
  7.   { label: '领导' }
  8. ] -->
  9. <ContextMenu :menu="menu" @select="console.log($event)">
  10.     <!-- 业务组件 -->
  11. </ContextMenu>


  12. <!-- ContextMenu -->
  13. <div ref="container">
  14.   <slot></slot>
  15.   <ul class="context-menu">
  16.     <li></li>
  17.     <!-- 菜单组件实现 -->
  18.   </ul>
  19. </div>
复制代码
  1. ContextMenu
复制代码
的使用上,需要提供菜单配置项,是一个数组,数组元素为必须包含
  1. label
复制代码
属性的对象,选定菜单中某一项,可监听
  1. select
复制代码
事件,然后执行相应的业务逻辑。

组件的布局方式

这个很容易想到,一定是要用固定定位,不管是哪个业务组件触发了右键菜单,其位置一定是相对于视口的。
但问题并不是这样就结束了,要知道默认情况下的固定定位位置相对于视口,但如果其父代中有
  1. tranform
复制代码
的元素,那么固定定位的位置是相对于这个元素的而不是视口。如果没有想到这个特性,就会产生严重的布局问题。
我们可以利用 Vue3 内置的
  1. <Teleport>
复制代码
组件,将右键菜单传送到
  1. body
复制代码
元素,这样无论如何右键菜单的定位位置都是相对于视口的。
  1. <!-- ContextMenu -->
  2. <div ref="container">
  3.   <slot></slot>
  4.   
  5.   <Teleport to="body">
  6.     <ul class="context-menu">
  7.     <li></li>
  8.     <!-- 菜单组件实现 -->
  9.   </ul>
  10.   </Teleport>
  11. </div>
复制代码
菜单组件的位置和可见度

设计好组件了,如何显示组件,并定位菜单的位置呢?
这里我们可以写一个
  1. useContextMenu
复制代码
的 hook,返回位置坐标
  1. x
复制代码
  1. y
复制代码
,以及可见度
  1. visible
复制代码
,并接收一个容器参数,因为需要监听各个需要右键菜单的容器的
  1. contextmenu
复制代码
事件。
这里需要注意位置坐标的要结合菜单height 和 width 来判断是否会相对视口越界,如果越界则自适应定位位置。

  1. import { ref, onMounted, onUnmounted } from "vue";

  2. export function useContextmenu(container) {
  3.   const visible = ref(false);
  4.   const x = ref(0);
  5.   const y = ref(0);

  6.   onMounted(() => {
  7.     container.value.addEventListener("contextmenu", showMenu);
  8.     // 把事件注册到捕获阶段,改变触发不同元素相同事件的触发顺序
  9.     window.addEventListener("contextmenu", hideMenu, true);
  10.     window.addEventListener("click", hideMenu);
  11.   });

  12.   onUnmounted(() => {
  13.     container.value.removeEventListener("contextmenu", showMenu);
  14.   });

  15.   function showMenu(e) {
  16.     e.preventDefault();
  17.     e.stopPropagation();
  18.     visible.value = true;

  19.     nextTick(() => {
  20.       const { clientX, clientY } = e;
  21.       const menuContainer = document.querySelector(".context-menu");
  22.       const { clientWidth: menuWidth, clientHeight: menuHeight } =
  23.         menuContainer;
  24.       const isOverPortWidth = clientX + menuWidth > window.innerWidth;
  25.       const isOverPortHeight = clientY + menuHeight > window.innerHeight;

  26.       if (isOverPortWidth) {
  27.         x.value = clientX - menuWidth;
  28.         y.value = clientY;
  29.       }
  30.       if (isOverPortHeight) {
  31.         x.value = clientX;
  32.         y.value = clientY - menuHeight;
  33.       }
  34.       if (!isOverPortHeight && !isOverPortWidth) {
  35.         x.value = clientX;
  36.         y.value = clientY;
  37.       }
  38.     });
  39.   }

  40.   function hideMenu(e) {
  41.     visible.value = false;
  42.   }
  43.   return { visible, x, y };
  44. }
复制代码
这里控制右键菜单的显示和隐藏还是需要注意一些细节的,比如需要利用事件捕获改变事件的触发顺序,以及阻止冒泡,防止嵌套元素中出现重复右键菜单。

组件动画

这里要实现一个高度由 0 过渡到 h 的效果,利用
  1. <Transition>
复制代码
来实现,但有一个问题是:过渡效果是无法识别
  1. height: auto
复制代码
的,也就是高度无法从 0 过渡到
  1. auto
复制代码
,那么就无法仅通过 CSS 来实现过渡动画,我们可以利用
  1. <Transition>
复制代码
的 JS 钩子函数,来手动计算子元素撑开的高度,然后在触发下一次渲染更新前手动设置
  1. height
复制代码
  1. function handleEnter(el) {
  2.   // 手动计算auto下撑开的容器高度
  3.   el.style.height = 'auto'
  4.   // 这里需要减去多余的padding
  5.   const h = el.clientHeight - 12
  6.   // 高度回归为0 否则没有过渡效果
  7.   el.style.height = 0 + 'px'

  8.   // 渲染下一帧之前,复制过渡和计算出的高度
  9.   requestAnimationFrame(() => {
  10.     el.style.height = h + 'px'
  11.     el.style.transition = '.3s'
  12.   })
  13. }

  14.   // 进入动画结束后,关闭过渡,否则关闭菜单时有时延
  15.   function handdleAfterEnter(el) {
  16.     el.style.transition = 'none'
  17.   }
  18. </script>

  19. <template>
  20.   <div ref="container">
  21.    
  22.     <slot></slot>
  23.    
  24.     <Teleport to="body">
  25.       <Transition @enter="handleEnter" @after-enter="handdleAfterEnter">
  26.         <ul class="context-menu" >
  27.           <li></li>
  28.         </ul>
  29.       </Transition>
  30.     </Teleport>
  31.   </div>
  32. </template>
复制代码
总结

好了,以上就是设计一个通用右键菜单组件的所有注意要点了,可以看到细节还是有一些的,比如:

  • 组件的设计方案
  • 固定定位的问题
  • 事件触发模型
  • 菜单定位越界控制
  • 组件的auto高度过渡动画。
其实还有一种设计方案是函数式组件,利用 Vue API的
  1. h
复制代码
函数将 SFC 渲染为
  1. VNode
复制代码
,然后调用
  1. render
复制代码
方法将真实dom进行挂载,也支持菜单项的配置和业务解耦。
最后,奉上源码:github.com/Jabinuu/contextmenu,如果有用的话欢迎 Star
以上就是使用Vue封装一个前端通用右键菜单组件的详细内容,更多关于Vue右键菜单组件的资料请关注脚本之家其它相关文章!

来源:https://www.jb51.net/javascript/316206dzv.htm
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x

举报 回复 使用道具