Vue3开发右键菜单的示例详解

 更新时间:2024年03月12日 10:46:17   作者:迷途小羔羊  
右键菜单在项目开发中是属于比较高频的组件了,所以这篇文章小编主要来和大家介绍一下如何利用vue3开发一个右键菜单,有需要的可以参考下

前言

由于我个人做的项目是后台管理项目偏多,右键菜单也是属于比较高频的组件了。但是目前我个人使用的技术栈为Vue3,目前社区还没有很好的插件进行使用,只能被逼无奈选择自己造轮子了。

目录结构基本构成

初始阶段,我把菜单组件分成两个目录,分别命名为ContextMenu.vueContentMenuItem.vue,两个组件各施其职,ContextMenu.vue组件提供最外层容器定位和层级能力,ContentMenuItem.vue提供每项的样式和当前时间事件回调。

ContextMenu.vue

ContextMenu组件,我是期望在能body中进行插入,这是为了方便组件的定位(position),那么这个时候是可以借助Vue3中的Teleport组件实现该效果。由于我的业务场景是在表格中右键,如果我对每行(tr)或者每个单元格(td)都生成一个菜单组件,就会导致body中存在多个菜单组件。这个并不符合我的预期想法,所以我决定使用v-if来控制组件的显示与隐藏。 基本的HTML结构如下:

 <Teleport to="body" v-if="visible">
      <div
        class="contextMenu"
        ref="contextmenuRef"
      >
      </div>
  </Teleport>
  <script lang="ts" setup>
      const visible = ref(false)
  </script>
  <style>
      .contextMenu {
          position: absolute;
          min-width: 150px;
          min-height:100px;
          padding-top: 5px;
          padding-bottom: 8px;
          background-color: #fff;
          border-radius: 4px;
        }
  </style>

计算ContextMenu组件的位置(position)

想要知道ContextMenu组件会出现在什么位置,需要我们知道该组件中是怎么使用的?我假设有个.vue组件

<el-button @contextmenu="contextmenuFun">按钮</el-button>
    <Contextmenu ref="ContextMenuRef">
      
    </Contextmenu>
import { ref } from 'vue'
const ContextMenuRef = ref()
const contextmenuFun = (e) => {
  ContextMenuRef.value.show(e)
}

在业务侧,可以看到。我是期望有个触发点的,无论按钮或者HTML元素也好。这个触发点,需要手动的去调用ContextMenu组件中show方法,并且需要把当前的触发事件源(event)传递过去。那么我们回到ContextMenu组件中就很容易写出show方法的逻辑。

const position = ref({
  top: 0,
  left: 0
})
const style = computed(() => {
  return {
    left: position.value.left,
    top: position.value.top
  }
})

const show = (e: MouseEvent) => {
  console.log(e, "e")
  e.preventDefault()
  visible.value = true
}

那么contextMenu出现的位置则需要我们动态的进行计算,注意点就是出现的位置,我们是需要计算边界值。

...
// 计算x,y的偏移值
const calculatePosition = (axis: "X" | "Y", mousePos: number, elSize: number) => {
  const windowSize = axis === "X" ? window.innerWidth : window.innerHeight
  const scrollPos = axis === "X" ? window.scrollX : window.scrollY

  let pos = mousePos - scrollPos
  if (pos + elSize > windowSize) {
    pos = Math.max(0, pos - elSize)
  }

  return pos + scrollPos
}

const show = async (e: MouseEvent) => {
  e.preventDefault()
  visible.value = true
  await nextTick()
  const el = contextmenuRef.value
  if (!el) {
    return
  }
  const width = el.clientWidth
  const height = el.clientHeight
  const { pageX: x, pageY: y } = e
  position.value.top = calculatePosition("Y", y, height)
  position.value.left = calculatePosition("X", x, width)
  console.log(position.value, "w")
}
...

我们通过calculatePosition计算出有效的x,y,在用Math.max确保显示不会超出当前的屏幕。

点击菜单外部隐藏

如何判断点击菜单外部进行隐藏呢?这个时候,就需要借助点击对象中的event事件进行处理了,把处理点击元素外围作为一个hook进行使用并命名为useClickOutside

import { onMounted, onBeforeUnmount, Ref } from "vue"

function useClickOutside(elementRef: Ref<HTMLElement | null>, callback: (event: MouseEvent) => void): void {
  const clickOutsideHandler = (event: MouseEvent) => {
    const el = elementRef.value
    if (!el || el === event.target || event.composedPath().includes(el)) {
      return
    }
    callback(event)
  }

  onMounted(() => {
    window.addEventListener("click", clickOutsideHandler)
  })

  onBeforeUnmount(() => {
    window.removeEventListener("click", clickOutsideHandler)
  })
}

export default useClickOutside
 <div class="contextMenu" ref="contextmenuRef" :style="style">1234</div>
const contextmenuRef = ref<HTMLDivElement | null>(null)
import useClickOutside from "./UseClickOutSide"
useClickOutside(contextmenuRef, () => {
  visible.value = false
})

这个时候我们就能实现点击菜单外部让菜单隐藏了,但是还会伴随一个问题,就是如果,我右键展开了菜单,当我去点击某个按钮的时候,我不希望这个这个菜单进行隐藏,而是希望一直显示。这个时候,就需要针对useClickOutside添加一个额外的参数进行控制。 针对点击某个元素,菜单不隐藏

在业务代码中,可以通过传递ignore进行HTML元素排除

div class="contextMenua" @contextmenu="contextmenu">123</div>
  <button class="ingoreBtn">不隐藏的按钮</button>
  <ContextMenu ref="contextmenuRef" :ignore="ignore" />

在contextmenu中定义props

interface Props {
  ignore: string[]
}
const props = withDefaults(defineProps<Props>(), {
  ignore: () => [] as string[]
})
...
useClickOutside(
  contextmenuRef,
  () => {
    console.log("w")
    visible.value = false
  },
  { ignore: props.ignore }
)
...

在useClickOutside函数中新增IgnoreElement方法用来排除HTML元素

let isIgnore = true
const IgnoreElement = (ignore: string[], event: MouseEvent) => {
    return ignore.some((target) => {
      if (typeof target === "string") {
        return Array.from(window.document.querySelectorAll(target)).some(
          (el) => el === event.target || event.composedPath().includes(el)
        )
      }
    })
  }
  const clickOutsideHandler = (event: MouseEvent) => {
      ...
       if (options?.ignore && options.ignore.length > 0) {
      isIgnore = !IgnoreElement(options.ignore, event)
    }
    if (!isIgnore) {
      isIgnore = true
      return
    }
    ...
  
  }

我们通过isIgnore变量进行打标识,用于判断是否经历过IgnoreElement的调用,默认为true,并不会影响现有逻辑。当isIgnore为false的时候,我们需要把它变成true,防止下次点击无法隐藏。

菜单不随着滚动条进行滚动

当我们的页面高度超出了屏幕高度时,会出现滚动条的情况,当我们对某个元素进行右键菜单的过程会出现,然后再去进行滚动,会发现我们的菜单也会跟随着移动。为了解决这个情况,可以使用一个透明的遮盖层盖住body,使得原本的滚动行为失效。 在这理论上,需要对HTML结构进行调整

 <div class="contextMenu-wrapper" :class="{ 'is-fixed': fixed }">
      <div class="contextMenu" ref="contextmenuRef" :style="style" :class="[popperClass]">1234</div>
    </div>
    interface Props {
      ignore: string[]
      popperClass?: string
      isFixed: boolean
    }
    watch(
      () => fixed.value,
      () => {
        if (fixed.value) {
          document.body.style.overflow = "hidden"
        } else {
          document.body.style.overflow = defaultSyleOverFlow.value
        }
      }
    )
    const show = async (e: MouseEvent) => {
        ...
        fixed.value = props.isFixed
        ...
    })
    useClickOutside(
      contextmenuRef,
      () => {
        visible.value = false
        fixed.value = false
      },
      { ignore: props.ignore }
    )
  onMounted(async () => {
  if (props.isFixed) {
        await nextTick()
        defaultSyleOverFlow.value = document.body.style.overflow
        const style = window.getComputedStyle(document.body)
        defaultSyleOverFlow.value = style.overflow
      }
})  
<style>
.contextMenu-wrapper {
  z-index: 9999;
  background-color: transparent;
  &.is-fixed {
    position: fixed;
    left: 0;
    top: 0;
    width: 100%;
    height: 100%;
  }
}
</style>

添加了is-fixed变量作为是否需要遮盖层的标识。通过watch监听fixed的变化,如果为真的话,则需要body的overflow变成hidden,关闭了的话恢复默认的值defaultSyleOverFlow

目前为止,就已经完成了下拉菜单的基本功能,但是还有以下功能还没有完成:

  • 响应键盘事件
  • 层级zIndex的控制
  • 多层级菜单(subItem)

到此这篇关于Vue3开发右键菜单的示例详解的文章就介绍到这了,更多相关Vue3右键菜单内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • vue子组件实时获取父组件的数据实现

    vue子组件实时获取父组件的数据实现

    本文主要介绍了vue子组件实时获取父组件的数据实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-12-12
  • Vue列表如何实现滚动到指定位置样式改变效果

    Vue列表如何实现滚动到指定位置样式改变效果

    这篇文章主要介绍了Vue列表实现滚动到指定位置样式改变效果,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-05-05
  • vue导入.md文件的步骤(markdown转HTML)

    vue导入.md文件的步骤(markdown转HTML)

    这篇文章主要介绍了vue导入.md文件的步骤(markdown转HTML),帮助大家更好的理解和使用vue框架,感兴趣的朋友可以了解下
    2020-12-12
  • vuex store 缓存存储原理分析

    vuex store 缓存存储原理分析

    这篇文章主要介绍了vuex store 缓存存储原理,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-07-07
  • vue计算属性及函数的选择

    vue计算属性及函数的选择

    这篇文章主要介绍了vue计算属性及函数的选择,文章围绕主题的相关资料展开详细介绍,需要的小伙伴可以参考一下
    2022-05-05
  • vue实现网络图片瀑布流 + 下拉刷新 + 上拉加载更多(步骤详解)

    vue实现网络图片瀑布流 + 下拉刷新 + 上拉加载更多(步骤详解)

    这篇文章主要介绍了vue实现网络图片瀑布流 + 下拉刷新 + 上拉加载更多,本文分步骤通过实例代码给大家介绍的非常详细,具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-01-01
  • Vue如何使用ElementUI对表单元素进行自定义校验及踩坑

    Vue如何使用ElementUI对表单元素进行自定义校验及踩坑

    有一些验证不是通过input select这样的受控组件来触发验证条件的 ,可以通过自定义验证的方法来触发,下面这篇文章主要给大家介绍了关于Vue如何使用ElementUI对表单元素进行自定义校验及踩坑的相关资料,需要的朋友可以参考下
    2023-02-02
  • 使用axios请求接口,几种content-type的区别详解

    使用axios请求接口,几种content-type的区别详解

    今天小编就为大家分享一篇使用axios请求接口,几种content-type的区别详解,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2019-10-10
  • Vue项目使用CDN优化首屏加载问题

    Vue项目使用CDN优化首屏加载问题

    这篇文章主要介绍了Vue项目使用CDN优化首屏加载问题,本文以vue、vuex、vue-touter为例,给大家介绍处理流程,需要的朋友可以参考下
    2018-04-04
  • 如何通过Vue自带服务器实现Ajax请求跨域(vue-cli)

    如何通过Vue自带服务器实现Ajax请求跨域(vue-cli)

    从A页面访问到B页面,并且要获取到B页面上的数据,而两个页面所在的端口、协议和域名中哪怕有一个不对等,那么这种行为就叫跨域,这篇文章给大家介绍如何通过Vue自带服务器实现Ajax请求跨域(vue-cli),感兴趣的朋友一起看看吧
    2023-10-10

最新评论