关于前端:Vue3-如何实现一个函数式右键菜单ContextMenus

2次阅读

共计 5509 个字符,预计需要花费 14 分钟才能阅读完成。

前言: 最近在公司 PC 端的我的项目中应用到了右键呈现菜单选项这样的一个工作需要,并且本人当初也在实现一个偶尔爆发的 idea(想用前端实现一个 windows 零碎从开机到桌面的 UI),其中也要用到右键弹出菜单这样的一个性能,集体感觉这个实现还不错,特来分享🎁。

tips: 我集体是喜爱应用图文来解说知识点的,相比于间接讲概念,我集体更偏向于应用 费曼学习法 来解说某一个性能的实现过程,因为我也是刚从一只菜鸟走过去,所以我更加分明一个老手在去学习一个全新的常识的时候,他其实不是须要你给他讲实现原理,而是你须要作为一个 “引路人” 让他先简略晓得这个常识是用来干什么的,前面随着他本人一步一步的深刻理解,他会本人缓缓领悟其中的原理。

一. 后期筹备

  1. 咱们须要分明的意识到,这种用户点击 右键 而后 弹出菜单 的动作行为是十分不适宜将组件写死在页面上,而后通过应用 v-show 或者 v-if 去管制它的呈现和隐没的,咱们须要想方法应用函数式去管制它的行为。
  2. 在此之前,你须要筹备两个文件来和我一起实现这个右键菜单。
  3. 预览图:

二. 右键菜单的款式

  1. 菜单款式的书写不是咱们本文的重点,你能够疾速在 Menu.vue 里简略书写你本人喜爱的一个简略 div 即可,咱们的重点是在于如何右键弹出它。你也能够在下方的 源码题目 中间接复制我书写的款式,不过你须要应用 UnoCSS 来反对内敛款式属性。
  2. 如果你不晓得如何应用 Unocss,你能够参考这篇文章的内容 手把手教你实现一个代码仓库外面有具体的过程来帮忙你去实现代码仓库的构建,其中包含了 Unocss 如何引入和应用。)

三. h 函数 和 render 函数的应用

  1. 当初咱们曾经实现了 Menu.vue,文件的内容,接下来咱们须要转头去书写 index.ts 内的内容。
  2. 在此之前,咱们须要引入两个 vue 裸露给咱们的,非常重要的函数。h, 和 render
  3. 如果你之前读过我另外三篇文章,我置信你对这两个函数的应用肯定不生疏,然而为了关照之前没有理解过的读者,我还是会在接下来的内容中简略介绍一下。不过我还是倡议你去看一看上面的实现形式,你肯定会有不一样的播种。
  4. Vue3 如何实现一个 Toast 小弹窗
  5. Vue3 如何实现一个全局搜寻框
  6. Vue3 如何实现一个 Dialog
  7. 接下来我简略的介绍一下,这两个函数的应用形式。你须要晓得一个前提常识,咱们在 template 标签里书写的款式,最终都会被转变成虚构 dom

    这外面书写的 div 其实是和咱们在浏览器里看到的 div “并不是同一个” div,只不过通过 vue 帮咱们进行了解决,让它们的表现形式显得一样了。

  8. template 是通过了怎么的解决呢?其实就是通过了 h 函数。而后 h 函数会返回一个非凡的 JS 对象,这个非凡的对象就是咱们所说的 虚构 dom
  9. 那咱们在这个场景怎么应用呢?首先你须要在 index.ts 文件内引入咱们刚刚书写的右键菜单的款式。而后将这个组件作为 h 函数的第一个参数放入,对,就是这么简略。这个 vnode 就是咱们须要用到的 虚构 dom
  10. 有了 虚构 dom 还不行,咱们得通知 vue 咱们要把这个 虚构 dom 渲染到什么中央,这时候就须要用到 render 函数。render 函数要做的事件比较复杂,不过在这里你只须要简略的晓得。render 函数会将一个 虚构 dom 转换成一个实在的 dom 节点 。既然须要一个 虚构 dom,那我刚刚正好用 h 函数转换了失去了一个,于是咱们自然而然能够写出上面的代码。
  11. 怎么回事?怎么还报错了呢?

    咱们看一下报错信息,发现这个 render 函数须要两个参数,咱们只给了一个。那么第二个参数是什么呢?咱们思考一下,当初这个 dom 曾经被转换成实在的 dom 节点 了,然而目前它不晓得本人应该被渲染到哪里,什么意思呢?其实了解起来很简略。
    就好比你当初是一个外卖员,你到了餐厅取餐,餐厅人员说你去吧,你端着手上的一份外卖餐一脸茫然,我去哪啊?
    就对应着,vue 帮你解决好了这个虚构节点,然而你没通知它应该在哪里去渲染。

  12. 晓得起因就好办了,咱们间接创立一个空的 div,先让 render 用着。

四. 右键弹出菜单的实现

  1. 在进行上面的性能之前,你须要晓得一个前提常识。

    如下面的 gif 所示,咱们能够看到,浏览器自身是存在默认的右键点击事件的。在这里咱们须要勾销浏览器本身的右键弹出菜单事件。

  2. 咱们再具体一点讲,其实咱们须要做的就是 替换 掉浏览器默认的右键事件。通过查阅 MDN 咱们能够得悉,window 对象存在一个叫做 contextMenus 的事件。
  3. 那接下来就好办了,咱们间接替换这个事件为咱们的自定义事件即可。(这里阻止默认事件须要调用 e.preventDefault 办法。)

    而后咱们在轻易一个全屏的组件引入这个函数,咱们来测试一下,看看成果

  4. 嗯,当初曾经不会弹出浏览器默认的菜单了。那么接下来要做的就是如何让咱们写好的菜单出现到页面上。首先第一点,咱们须要明确通知这个组件你的 父元素是谁
    咱们下面只是长期发明了一个简略的 div,然而目前咱们还是没通知它应该渲染到哪里。解决办法也很简略,这里我提前创立好了一个很简略的页面,并且设置好了一个惟一 ID
  5. 那么咱们就能够十分轻松的取得这个元素。
  6. 当初父元素也有了,只须要将咱们的 containerEl 元素放入到 scope 里即可。
    不过你须要晓得的是,咱们这个元素是不应该呈现在失常的文档流里的,因为它的地位是不固定的,所以咱们在放进去 scope 元素之前,应该给它解决成 相对定位 类型的元素。
  7. 对了,这里须要留神,咱们须要给 scope 设置一个 relative 属性,来通知咱们的 containerEl 它要在谁的范畴内是相对定位。
  8. 接下来咱们进入到咱们的 scope 组件内引入这个函数,调用一下看看成果。

    ok,当初曾经实现咱们的右键弹出菜单的基本功能了。

五. 菜单地位呈现的地位

  1. 在这里咱们须要用到 clientX,和 clientY 这两个属性。
  2. 如果你是第一次看到这个属性,那么我简略介绍一下。

    假如我在屏幕的上点击了一下(类比上图的红点出),那么此时这个点到屏幕最右边的间隔就是 clientX,同理到屏幕顶部的间隔就是 clientY

  3. 聪慧的你肯定想到了,那我此时将 containerEltopleft 的值别离设置成这两个属性的值,不就恰好会让菜单呈现在咱们的左边吗?咱们试一下。

    而后看看成果:

  4. 目前看起来一切正常,然而咱们须要思考一个边界状况。

    当咱们间隔屏幕右侧过近的时候,此时右键会导致有局部内容被遮挡。所以咱们要想方法解决这个边界状况。

六. 解决右侧过近的问题

  1. 不要感觉很难,其实目前咱们要做的事件很简略。
  2. 如上图,咱们仅仅只须要去判断
    scope 的 clientWidth 的长度 – clientX 的长度 = 是否大于 containerEl 的 offsetWidth?
    如果大于,则调转 left 的方向为 right , 并设置 right=0px 即可。
  3. 如果下面所说的 offsetWidthclientWidth 你还不理解。我 强烈建议 你请点击这篇博文先去理解分明这几个 width 属性到底代表着什么意思,因为对于前端开发来说,这是极其重要的几个属性。如果你之后要接触 挪动端 ,那么这是你必须把握的知识点。
    你必须晓得的 clientWdith,scrollWidth,offsetWidth
  4. 既然晓得了原理,那么代码写起来就非常简单了,在此之前在这里咱们须要调整一下 scope.appendChild 的执行机会。

    咱们测试一下成果。

七. 加强该函数的健壮性

  1. 目前这个框咱们无奈确保它的唯一性,所以咱们还须要革新一下这个函数。
  2. 减少一个变量 isShow,咱们须要晓得以后的 Menu 菜单是否正在展现。
  3. containerElconst 申明变为 let 申明。并将发明机会提早到调用右键时再创立,这样咱们就能保障每次右键制作的这个 Menu 组件是都是全新的。(不然就会呈现沿用上一次 css 属性,导致款式错乱的 bug)
  4. 获取 scope 元素的机会也推延到用户点击右键的时候再获取。(因为上面的 close 函数也须要用到这个变量)
  5. 拆分两个函数,一个关上 openMenu 函数,一个敞开函数 closeMenu

  6. 最初在 window.oncontextmenu 的匿名函数里去调取这两个函数。
  7. 而后咱们将这三个变量裸露进来。

八. 右键菜单的应用办法

  1. 咱们进到 scope.vue 组件内,引入。
  2. 这样咱们既能够通过右键创立这个菜单栏,也能够本人在适合的工夫去做一些逻辑判断手动关上。
  3. 成果如下

源码

  1. Menu.vue 的源码。

    <script lang="ts" setup>
    import {ref} from "vue"
    
    const menuItemsGroup = [
      {name: "查看(V)",
     arrow: true,
     action: () => {console.log("查看")
     },
      },
      {name: "排序形式(O)",
     arrow: false,
     action: () => {console.log("刷新")
     },
      },
      {name: "刷新(E)",
     arrow: false,
     action: () => {console.log("刷新")
     },
      },
      {name: "粘贴(P)",
     arrow: false,
     action: () => {console.log("刷新")
     },
      },
      {name: "粘贴快捷方式(S)",
     arrow: false,
     action: () => {console.log("刷新")
     },
      },
      {name: "新建(W)",
     arrow: false,
     action: () => {console.log("刷新")
     },
      },
      {name: "个性化(R)",
     arrow: false,
     action: () => {console.log("刷新")
     },
      },
    ]
    </script>
    <template>
      <div
     class="w-17rem bg-#ECECEC flex flex-col py-0.5rem shadow-[4px_4px_5px_2px_rgba(0,0,0,0.3)]"
      >
     <div
       v-for="(item, i) in menuItemsGroup"
       :key="i"
       @click="item.action"
       class="w-full h-2.5rem px-3rem text-1.5rem leading-2.5rem text-black hover:bg-white mb-0.3rem"
       :class="[3, 5, 6].includes(i) ? `b-t-1px b-gray` : `static`"
     >
       <span>{{item.name}}</span>
     </div>
      </div>
    </template>
  2. 这是 openContextMenus 的源码。
import {h, render} from "vue"

import Menu from "./Menu.vue"

export function openContextMenus() {
  let isShow = false
  let scope: HTMLElement | null // 拿到桌面元素
  let containerEl: HTMLDivElement // 创立一个容器元素,给 render 先用着

  window.oncontextmenu = function (e: MouseEvent) {e.preventDefault()
    if (isShow) closeMenu()
    openMenu(e)
  }

  //tips: open the menu
  function openMenu(e: MouseEvent) {scope = document.getElementById("PCDesktop")
    containerEl = document.createElement("div")
    const vnode = h(Menu)
    render(vnode, containerEl) // 将 vnode 传递给 render 函数

    containerEl.style.position = "absolute"

    scope?.appendChild(containerEl) // 1. 为了拿到 offsetWidth,因为只有呈现在浏览器才会产生 offsetWidth 属性值,咱们须要先渲染出实在 dom

    const {offsetWidth} = containerEl //2 . 取出 containerEl 的实在宽度
    const {clientWidth} = scope! //3. 获取父元素的 clientWidth 筹备进行计算
    const {clientX, clientY} = e //4. 取出 click 时鼠标的坐标

    const _X = clientWidth - clientX > offsetWidth ? "left" : "right" // 调整方向
    const _X_offset = clientWidth - clientX // 如果是须要显示在右边,则须要获取以后的差值

    containerEl.style.top = `${clientY}px`
    containerEl.style[_X] = _X === "left" ? `${clientX}px` : `${_X_offset}px`
    isShow = true
  }

  //tips: close the menu
  function closeMenu() {if (isShow) {render(null, containerEl)
      scope?.removeChild(containerEl)
      console.log("分明")
      isShow = false
    }
  }
  return {
    isShow,
    openMenu,
    closeMenu,
  }
}

结语

最近在实现一个 window 的全套 UI,代码开源到了 github

我会在之后始终更新相似的内容,包含拖拽的实现。
如果你感觉本文对你有帮忙,还心愿点个赞

赠人玫瑰,手有余香🌹

正文完
 0