因为一个自定义右键菜单的功能,我学到了这些知识

260 阅读4分钟

前言

hello!各位前端大佬,已经很久没有更新文章了,这阵子萝卜换了座城市也换了份工作,思考了许多回到了自己的家乡开启了新的旅途,那么在新工作期间也遇到了许多富有挑战性的需求,那么这篇文章主要介绍下因为一个右键菜单的功能我的一些收获,话不多说,我们来具体看一下

整体的思路是按照渡一的袁老师提供的思路实现的,但是遗憾的是没有找到源码,所以跟着思路自己实现了一遍这个功能,如果侵权与我联系进行删除。

需求分析

那么我们先来聊一下右键自定义菜单都要有哪些功能:

  1.  触发鼠标右键事件禁用浏览器默认菜单,展示自定义菜单
  2.  菜单展示动态菜单项,点击菜单项触发对应事件
  3.  点击菜单项或者点击菜单以外的区域,自定义菜单进行隐藏

根据上述分析,我们来一步步的实现这个自定义菜单

功能实现

一、初始化项目

基于 Vue3 + Ts 我们来初始化一个项目 npm create vue 选择使用 TypeScript,其他的是否勾选不重要

二、创建自定义菜单组件

components 文件夹中创建组件 ContentMenu.vue 进行模板初始化,我们使用这个组件的形式是下图这样的,传入对应自定义菜单数据,展示对应的菜单内容,并且点击菜单项后获取当前选中的菜单内容,而且菜单出现的位置是我们点击的位置。

 <ContextMenu class="box" :menu-list="menuList"  @select="onSelect">
    <div>测试内容</div>
 </ContextMenu>
    
<script setup lang="ts">
    import ContextMenu from './components/ContextMenu.vue'
    const menuList = [
      { label: '菜单一', value: 1 },
      { label: '菜单二', value: 2 },
      { label: '菜单三', value: 3 }
    ]
    const onSelect = (data: any) => {
      console.log(data)
    }
</script>
image.png image.png

三、实现自定义组件思路

  1. 首先我们通过分析组件的使用形式不难想出我们要定义一个插槽在自定义组件中写入个性化内容
  2. 菜单项展示传入内容,所以要有一个循环列表展示传入的列表数据
  3. 右键组件区域触发 contextmenu 事件,控制自定义菜单显示
  4. 菜单显示位置通过鼠标点击的位置获取,菜单设置成绝对定位
  5. 点击菜单项、其他区域或者右键展示浏览器菜单隐藏自定义菜单

四、代码实现组件

组件结构

根据我们使用的形式不能想出结构分为一个插槽和一个菜单列表

<template>
  <div ref="menuRef">
    <slot></slot>
    <teleport to="body">
      <div
        class="menu-list"
        v-if="visible"
        ref="menuListRef"
        :style="{
          width: menuWidth,
          position: 'absolute',
          left: x + 'px',
          top: y + 'px',
          background,
          fontSize,
          color
        }"
      >
        <div
          :class="['menu-item', border ? 'border-bottom' : '']"
          v-for="item in menuList"
          :key="item.value"
          @click="selectMenu(item)"
        >
          {{ item.label }}
        </div>
      </div>
    </teleport>
  </div>
</template>

这里使用teleport是为了防止父级出现transform属性而影响菜单定位的位置,使用 teleport让菜单传送到body

组件样式

样式这里就不过多描述了,大佬们可以根据自己喜欢的配色自定义样式

.menu-list {
  min-width: 80px;
  border-radius: 5px;
}
.menu-item {
  cursor: pointer;
  padding: 5px 10px;
  box-sizing: border-box;
}

.border-bottom {
  border-bottom: 1px solid #e0e0e0;
}
.menu-item:last-child {
  border-bottom: none;
}
.menu-item:hover {
  background: #66ccff;
  border-radius: 5px;
}

组件逻辑

重点在于菜单的交互逻辑,那么我们定义一个 useContextMenu.ts 的 hooks 编写一下菜单的具体逻辑

import { onMounted, onUnmounted, ref, type Ref } from 'vue'

const useContextMenu = (menuRef: Ref<HTMLElement | null>) => {
  const x: Ref<number> = ref(0)
  const y: Ref<number> = ref(0)

  const visible: Ref<boolean> = ref(false)

  const openMenu = (e: MouseEvent) => {
    e.stopPropagation()
    e.preventDefault()
    x.value = e.clientX
    y.value = e.clientY
    visible.value = true
  }

  const hideMenu = () => {
    visible.value = false
  }

  onMounted(() => {
    const menuDiv = menuRef.value
    menuDiv?.addEventListener('contextmenu', openMenu)
    window.addEventListener('click', hideMenu, true)
    window.addEventListener('contextmenu', hideMenu, true)
  })

  onUnmounted(() => {
    const menuDiv = menuRef.value
    menuDiv?.removeEventListener('contextmenu', openMenu)
    window.removeEventListener('click', hideMenu)
    window.removeEventListener('contextmenu', hideMenu)
  })

  return {
    x,
    y,
    visible
  }
}

export { useContextMenu }

属性说明
x菜单定位的left
y菜单定位的top
visible菜单显示隐藏的控制变量
openMenu显示菜单的方法
hideMenu隐藏菜单的方法

onMounted 中获取传入的 dom 元素,添加 contextmenu事件调用 openMenu 函数,获取鼠标点击的clientXclientY赋值给定义的 xy变量,这样就能控制菜单展示位置为当前鼠标点击位置,再将 visible 变量改为 true 就可以让菜单展示。
然后还要注册 windowclick事件和contextmenu让自定义菜单隐藏掉,最后在 onUnmounted 事件中清除掉注册的事件即可

组件中使用hook并发射事件
import { ref } from 'vue'
import { useContextMenu } from '@/hooks/useContextMenu'
const menuRef = ref<HTMLElement | null>(null)
const { x, y, visible } = useContextMenu(menuRef)

const emits = defineEmits(['select'])

const selectMenu = (item: IMenu) => {
  emits('select', item)
}

withDefaults(
  defineProps<{
    menuList: IMenu[]
    background?: string
    fontSize?: string
    color?: string
    menuWidth?: string
    border?: boolean
  }>(),
  {
    background: '#333',
    fontSize: '14px',
    color: '#00FFFF',
    menuWidth: '100px',
    border: false
  }
)

将定义好的 hook 在组件中引用,拿到 xyvisible,并且为菜单项定义一个click函数,当点击菜单项时,将当前点击的菜单项通过 emit 事件发送出去,这样就可以在父组件中拿到当前所点击的菜单项了

总结

那么经过上面的介绍,一个右键自定义菜单的组件就封装好了,如果文章有不正确的地方,还需大佬批评指正,一起进步!