Vue3 如何实现一个函数式右键菜单(ContextMenus)

news2024/10/2 3:15:17

前言: 最近在公司 PC 端的项目中使用到了右键出现菜单选项这样的一个工作需求,并且自己现在也在实现一个偶然迸发的 idea( 想用前端实现一个 windows 系统从开机到桌面的 UI),其中也要用到右键弹出菜单这样的一个功能,个人觉得这个实现还不错,特来分享🎁。

tips: 我个人是喜欢使用图文来讲解知识点的,相比于直接讲概念,我个人更倾向于使用费曼学习法来讲解某一个功能的实现过程,因为我也是刚从一只菜鸟走过来,所以我更加清楚一个新手在去学习一个全新的知识的时候,他其实不是需要你给他讲实现原理,而是你需要作为一个 “引路人” 让他先简单知道这个知识是用来干什么的,后面随着他自己一步一步的深入了解,他会自己慢慢领悟其中的原理。

一. 前期准备

  1. 我们需要清楚的认识到,这种用户点击右键然后弹出菜单的动作行为是非常不适合将组件写死在页面上,然后通过使用 v-show 或者 v-if 去控制它的出现和消失的,我们需要想办法使用函数式去控制它的行为。

  2. 在此之前,你需要准备两个文件来和我一起实现这个右键菜单。
    image.png

  3. 预览图:
    333.gif

二. 右键菜单的样式

  1. 菜单样式的书写不是我们本文的重点,你可以快速在 Menu.vue 里简单书写你自己喜欢的一个简单 div 即可,我们的重点是在于如何右键弹出它。你也可以在下方的源码标题中直接复制我书写的样式,不过你需要使用 UnoCSS 来支持内敛样式属性。

  2. 如果你不知道如何使用 Unocss,你可以参考这篇文章的内容 手把手教你实现一个代码仓库里面有详细的过程来帮助你去完成代码仓库的构建,其中包括了 Unocss 如何引入和使用。)
    image.png

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

  1. 现在我们已经完成了 Menu.vue,文件的内容,接下来我们需要转头去书写 index.ts 内的内容。

  2. 在此之前,我们需要引入两个 vue 暴露给我们的,十分重要的函数。h,和 render
    image.png

  3. 如果你之前读过我另外三篇文章,我相信你对这两个函数的使用一定不陌生,但是为了照顾之前没有了解过的读者,我还是会在接下来的内容中简单介绍一下。不过我还是建议你去看一看下面的实现方式,你一定会有不一样的收获。

  • Vue3 如何实现一个 Toast 小弹窗
  • Vue3 如何实现一个全局搜索框
  • Vue3 如何实现一个Dialog
  1. 接下来我简单的介绍一下,这两个函数的使用方式。你需要知道一个前提知识,我们在 template 标签里书写的样式,最终都会被转变成虚拟 dom
    image.png
    这里面书写的 div 其实是和我们在浏览器里看到的 div “并不是同一个” div,只不过经过 vue 帮我们进行了处理,让它们的表现形式显得一样了。

  2. template 是经过了怎样的处理呢?其实就是经过了 h 函数。然后 h 函数会返回一个特殊的 JS 对象,这个特殊的对象就是我们所说的虚拟dom

  3. 那我们在这个场景怎么使用呢?首先你需要在 index.ts 文件内引入我们刚刚书写的右键菜单的样式。然后将这个组件作为 h 函数的第一个参数放入,对,就是这么简单。这个 vnode 就是我们需要用到的虚拟 dom
    image.png

  4. 有了虚拟 dom 还不行,我们得告诉 vue 我们要把这个虚拟 dom 渲染到什么地方,这时候就需要用到 render 函数。render 函数要做的事情比较复杂,不过在这里你只需要简单的知道。render 函数会将一个 虚拟dom 转换成一个真实的 dom 节点。既然需要一个虚拟 dom,那我刚刚正好用 h 函数转换了得到了一个,于是我们自然而然可以写出下面的代码。
    image.png

  5. 怎么回事?怎么还报错了呢?
    image.png
    我们看一下报错信息,发现这个 render 函数需要两个参数,我们只给了一个。那么第二个参数是什么呢?我们思考一下,现在这个 dom 已经被转换成真实的 dom 节点了,但是目前它不知道自己应该被渲染到哪里,什么意思呢?其实理解起来很简单。
    就好比你现在是一个外卖员,你到了餐厅取餐,餐厅人员说你去吧,你端着手上的一份外卖餐一脸茫然,我去哪啊?
    就对应着,vue 帮你处理好了这个虚拟节点,但是你没告诉它应该在哪里去渲染。

  6. 知道原因就好办了,我们直接创建一个空的 div,先让 render 用着。
    image.png

四. 右键弹出菜单的实现

  1. 在进行下面的功能之前,你需要知道一个前提知识。
    右键.gif
    如上面的 gif 所示,我们可以看到,浏览器本身是存在默认的右键点击事件的。在这里我们需要取消浏览器自身的右键弹出菜单事件。

  2. 我们再具体一点讲,其实我们需要做的就是替换掉浏览器默认的右键事件。通过查阅 MDN 我们可以得知,window 对象存在一个叫做 contextMenus 的事件。
    image.png

  3. 那接下来就好办了,我们直接替换这个事件为我们的自定义事件即可。(这里阻止默认事件需要调用 e.preventDefault 方法。)
    image.png
    然后我们在随便一个全屏的组件引入这个函数,我们来测试一下,看看效果
    2.gif

  4. 嗯,现在已经不会弹出浏览器默认的菜单了。那么接下来要做的就是如何让我们写好的菜单呈现到页面上。首先第一点,我们需要明确告诉这个组件你的父元素是谁
    我们上面只是临时创造了一个简单的 div,但是目前我们还是没告诉它应该渲染到哪里。处理方法也很简单,这里我提前创建好了一个很简单的页面,并且设置好了一个唯一 ID
    image.png

  5. 那么我们就可以非常轻松的获得这个元素。
    image.png

  6. 现在父元素也有了,只需要将我们的 containerEl 元素放入到 scope 里即可。
    不过你需要知道的是,我们这个元素是不应该出现在正常的文档流里的,因为它的位置是不固定的,所以我们在放进去 scope 元素之前,应该给它处理成绝对定位类型的元素。
    image.png

  7. 对了,这里需要注意,我们需要给 scope 设置一个 relative 属性,来告诉我们的 containerEl 它要在谁的范围内是绝对定位。
    image.png

  8. 接下来我们进入到我们的 scope 组件内引入这个函数,调用一下看看效果。

    image.png
    2.gif
    ok,现在已经实现我们的右键弹出菜单的基本功能了。

五. 菜单位置出现的位置

  1. 在这里我们需要用到 clientX,和 clientY 这两个属性。
    image.png

  2. 如果你是第一次看到这个属性,那么我简单介绍一下。
    image.png
    假设我在屏幕的上点击了一下(类比上图的红点出),那么此时这个点到屏幕最左边的距离就是 clientX,同理到屏幕顶部的距离就是 clientY

  3. 聪明的你一定想到了,那我此时将 containerEltopleft 的值分别设置成这两个属性的值,不就恰好会让菜单出现在我们的右边吗?我们试一下。
    image.png
    然后看看效果:
    3.gif

  4. 目前看起来一切正常,但是我们需要考虑一个边界情况。
    image.png
    当我们距离屏幕右侧过近的时候,此时右键会导致有部分内容被遮挡。所以我们要想办法解决这个边界情况。

六. 解决右侧过近的问题

  1. 不要觉得很难,其实目前我们要做的事情很简单。
    image.png

  2. 如上图,我们仅仅只需要去判断
    scope 的 clientWidth 的长度 - clientX 的长度= 是否大于containerEl 的 offsetWidth ?
    如果大于,则调转 left 的方向为 right ,并设置 right=0px 即可。

  3. 如果上面所说的 offsetWidthclientWidth 你还不了解。我强烈建议你请点击这篇博文先去了解清楚这几个 width 属性到底代表着什么意思,因为对于前端开发来说,这是极其重要的几个属性。如果你之后要接触移动端,那么这是你必须掌握的知识点。
    你必须知道的 clientWdith,scrollWidth,offsetWidth

  4. 既然知道了原理,那么代码写起来就非常简单了,在此之前在这里我们需要调整一下 scope.appendChild 的执行时机。
    image.png
    我们测试一下效果。
    4.gif

七. 增强该函数的健壮性

  1. 目前这个框我们无法确保它的唯一性,所以我们还需要改造一下这个函数。

  2. 增加一个变量 isShow ,我们需要知道当前的 Menu 菜单是否正在展示。
    image.png

  3. containerElconst 声明变为 let 声明。并将创造时机延迟到调用右键时再创建,这样我们就能保证每次右键制造的这个 Menu 组件是都是全新的。(不然就会出现沿用上一次 css 属性,导致样式错乱的 bug )
    image.png

  4. 获取 scope 元素的时机也推迟到用户点击右键的时候再获取。(因为下面的 close 函数也需要用到这个变量)
    image.png

  5. 拆分两个函数,一个打开 openMenu 函数,一个关闭函数 closeMenu

    image.png
    image.png

  6. 最后在 window.oncontextmenu 的匿名函数里去调取这两个函数。
    image.png

  7. 然后我们将这三个变量暴露出去。
    image.png

八. 右键菜单的使用方法

  1. 我们进到 scope.vue 组件内,引入。
    image.png

  2. 这样我们既可以通过右键创建这个菜单栏,也可以自己在合适的时间去做一些逻辑判断手动打开。

  3. 效果如下
    5.gif

源码

  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>
  1. 这是 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
image.png
我会在之后一直更新类似的内容,包括拖拽的实现。
如果你觉得本文对你有帮助,还希望点个赞

赠人玫瑰,手有余香🌹

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/338049.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

通讯录文件操作化

宝子&#xff0c;你不点个赞吗&#xff1f;不评个论吗&#xff1f;不收个藏吗&#xff1f; 最后的最后&#xff0c;关注我&#xff0c;关注我&#xff0c;关注我&#xff0c;你会看到更多有趣的博客哦&#xff01;&#xff01;&#xff01; 喵喵喵&#xff0c;你对我真的很重…

几个chatGPT的难题,关于语言转换

不同语言代码的移植一直以来是程序员面临的难题&#xff0c;最近问了问chatGPT能否解决这个问题。编写一个程序&#xff0c;实现c语言函数转换为php函数答&#xff1a;这是一个非常困难的问题&#xff0c;因为两种语言的语法、结构和标准库都不相同。如果您希望完成这个任务&am…

MySql服务多版本之间的切换

从网上总结的经验&#xff0c;然后根据自己所遇到的问题合并记录一下&#xff0c;方便日后再次需要用到 MySql服务多版本同时运行 步骤 1、如果你电脑上已经有一个mysql版本&#xff0c;例如mysql-5.7.39-winx64&#xff0c;它占据了3306端口。此时如果你想下仔另一版本&…

活动星投票紫砂新青年制作一个投票活动

“紫砂新青年”网络评选投票_免费链接投票_作品投票通道_扫码投票怎样进行现在来说&#xff0c;公司、企业、学校更多的想借助短视频推广自己。通过微信投票小程序&#xff0c;网友们就可以通过手机拍视频上传视频参加活动&#xff0c;而短视频微信投票评选活动既可以给用户发挥…

6年自动化测试,终于进华为了,年薪25w其实也并非触不可及

我的职业生涯开始和大多数测试人一样&#xff0c;开始接触都是纯功能界面测试&#xff0c;第一份测试工作就是在电商公司做功能测试&#xff0c;工作忙忙碌碌&#xff0c;每天在各种业务需求学习和点点中度过&#xff0c;过了好几年发现自己还只是一个功能测试工程师&#xff0…

锐捷(十四)mpls vxn optionc的关键问题所在和具体问题分析

用锐捷的设备搭建mpls vxn optionc的基础版和带RR的版本&#xff0c;在控制平面和转发平免上分析mpls vxn optionc的关键问题所在和具体问题分析。一 基础mpls vxn optionc&#xff1a;核心&#xff1a;两pe之间之间建立MP EBGP邻居&#xff0c;从而直接传递路由解放了ASBR。关…

LeetCode-1223-掷骰子模拟

1、动态规划法 我们可以利用数组dp[i][j][k]dp[i][j][k]dp[i][j][k]来表示当我们已经投过iii次骰子&#xff0c;其中第iii次投出的骰子是jjj&#xff0c;此时连续投出骰子jjj的次数为kkk。因此我们可以根据上一轮中得到的状态dp[i−1][j][k]dp[i-1][j][k]dp[i−1][j][k]&#…

最小二乘支持向量机”在学习偏微分方程 (PDE) 解方面的应用(Matlab代码实现)

目录 &#x1f4a5;1 概述 &#x1f4da;2 运行结果 &#x1f389;3 参考文献 &#x1f468;‍&#x1f4bb;4 Matlab代码 &#x1f4a5;1 概述 本代码说明了“最小二乘支持向量机”在学习偏微分方程 &#xff08;PDE&#xff09; 解方面的应用。提供了一个示例&#xff0c…

加盟管理系统挑选法则,看完不怕被坑!

经营服装连锁店铺究竟有多难&#xff1f;小编已经不止一次听到身边的老板&#xff0c;抱怨加盟连锁店铺难以管理了&#xff0c;但同时呢&#xff0c;也听到了很多作为加盟商的老板&#xff0c;抱怨总部给的支持和管理不到位。服装加盟店铺管理&#xff0c;到底有哪些难点呢&…

BFS广度优先遍历——Acwing 844. 走迷宫

1.BFS简介我们可以将bfs当做一个成熟稳重的人&#xff0c;一个眼观六路耳听八方的人&#xff0c;他每次搜索都是一层层的搜索&#xff0c;从第一层扩散到最后一层&#xff0c;BFS可以用来解决最短路问题。2.基本思想从初始状态S开始&#xff0c;利用规则&#xff0c;生成所有可…

window11 安装node及配置环境变量

一、安装环境 本教程演示的环境&#xff1a; 系统&#xff1a;win 11 64位 node.js下载地址: http://nodejs.cn/ node.js版本&#xff1a;长期支持版本&#xff08;本教程基于16.15.0&#xff09; 点击选中图标下载到电脑本地即可。 二、安装步骤 1、双击安装包&#xff0c;一…

华为10年经验测试工程师,整理出来的python自动化测试实战

前言 全书共分11章&#xff0c;第一章是基础&#xff0c;了selenium家谱&#xff0c;各种组件之间的关系以及一些必备知识。第二章告诉如何开始用python IDLE写程序以及自动化测试环境的搭建。第三章是webdriver API&#xff0c;我花了相当多时间对原先的文档&#xff0c;冗余…

HTML5之HTML基础学习笔记

列表标签 列表的应用场景 场景&#xff1a;在网页中按照行展示关联性的内容&#xff0c;如&#xff1a;新闻列表、排行榜、账单等特点&#xff1a;按照行的方式&#xff0c;整齐显示内容种类&#xff1a;无序列表、有序列表、自定义列表 这是老师PPT上的内容&#xff0c; 列表…

day10_面向对象基础

今日内容 零、 复习昨日 一、面向对象的概念 二、面向对象编程 三、内存图 零、 复习昨日 见晨考题 每日一数组题 写一个方法 用于合并两个int类型的数组 合并法则如下 {1,2,5,8,9}{1,3,0}---->{1,2,5,8,9,1,3,0} package com.qf.array;import java.util.Arrays;/*** --- 天…

基于Java+SpringBoot+Vue+uniapp前后端分离图书阅读系统设计与实现

博主介绍&#xff1a;✌全网粉丝3W&#xff0c;全栈开发工程师&#xff0c;从事多年软件开发&#xff0c;在大厂呆过。持有软件中级、六级等证书。可提供微服务项目搭建、毕业项目实战、项目定制✌ 博主作品&#xff1a;《微服务实战》专栏是本人的实战经验总结&#xff0c;《S…

MySQL在Linux上的四种安装方式

目录 前言 一、仓库安装 二、本地安装 三、容器安装 四、源码安装 前言 博主的配置信息&#xff1a; Windows版本&#xff1a;Win10 VMware虚拟机版本&#xff1a;Vmware Workstation Pro 17 Linux版本&#xff1a;Red Hat Enterprise Linux 9.1 MySQL版本&#xff1a;My…

一篇文章搞懂Cookie

目录 1 什么是Cookie 2 创建Cookie 3 浏览器查看Cookie 3.1 浏览器查看Cookie的第一种方式 3.2 浏览器查看Cookie的第二种方式 4 获取Cookie 5 修改Cookie 6 Cookie编码与解码 6.1 创建带中文Cookie 6.2 读取带中文Cookie 6.3 获取中文Cookie请求效果 6.4 解决创建和…

grafana9 使用消息模板配置发送企业微信(wecom)

一、grafana9告警设置&#xff1a; 1、进入告警消息模板介面 2、grafana 消息模板设置 template name : API_msg_tpl #名字随便 {{ define "myalert" }} **警报时间:** {{ .StartsAt.Format "2006-01-02 15:04:05 " }} {{ if gt (len .Labels) 0 }}**…

毕业5年,从月薪3000到年薪40w,我掌握了那些核心技能?(建议收藏)

大家好&#xff0c;我是静静~~是一枚一线大厂的测试开发工程师很多读者私信问我&#xff0c;自己时间不短了&#xff0c;随着工作年限的不断增长&#xff0c;感觉自己的技术水平与自己的工作年限严重不符。想跳槽出去换个新环境吧&#xff0c;又感觉自己的能力达不到心仪公司的…

Python_pytorch

python_pytorch 小土堆pytotch学习视频链接 from的是一个个的包&#xff08;package) import 的是一个个的py文件(file.py) 所使用的一般是文件中的类(.class) 第一步实例化所使用的类,然后调用类中的方法&#xff08;def) Dataset 数据集处理 import os from PIL impo…