boycot 搜索
avatar

boycot

Vue3实现自定义右键菜单

使用 Vue3 实现自定义鼠标右键菜单,支持二级菜单、自定义样式、回调函数等功能,同时提供封装成 Vue 指令调用。

🚀 初步构思

  • 仅用 Vue 构建右键菜单 UI 组件,需利用组件提供 show 方法唤起菜单,在指令中再封装对右键菜单事件的处理
  • 接收一个菜单项数组配置,菜单项中提供点击菜单后回调函数,可以自定义传入回调函数的参数
  • 将菜单 DOM 中的样式尽量抽离出可配置,并利用 CSS 变量注入,可自定义菜单的样式
  • 提供show方法接收x,y定位参数,然后显示菜单,同时要加入检查窗口碰撞后的处理
  • 由于右键菜单的特殊性,可封装组件为单实例,页面上只会存在一个菜单实例,可优化性能

🌈 功能说明

菜单项列表-menuList

menuList作为必传参数,用于渲染出菜单项,接收一个长度至少为 1 的数组,同时可以指定children属性用于渲染二级菜单。

菜单项中的配置尽量做成同时接收函数的方式,以便可以通过传入的参数更快动态配置出不同的菜单。

当前支持以下菜单项配置:

参数说明类型
fn点击菜单后执行的回调,回调参数 1 为用户传入的 params, 参数 2 为点击右键时所在的 HtmlElement 元素(使用 document.elementFromPoint 获取), 参数 3 为指令绑定的当前元素(params, activeEl, bindingEl) => void
label菜单名, 可使用函数,回调参数同 fn 选项String, Function
tips菜单辅助文本(处于右侧的文本),可使用函数,回调参数同 fn 选项String, Function
icon菜单图标的类名(字体图标)String
hidden菜单项是否隐藏,可使用函数,回调参数同 fn 选项Boolean, Function
disabled菜单项是否不可点击,可使用函数,回调参数同 fn 选项Boolean, Function
children子菜单的菜单项数组(配置与此表一致,但目前仅支持二级菜单)Array
line是否为分割线,该值为 True 时,以上设置均失效Boolean

菜单在每次打开时都会调用以下方法去格式化出最终的菜单项:

const formatterFnOption = (
  list: MenuSetting[],
  clickDomEl: HTMLElement,
  el: HTMLElement,
  params: any
): MenuSetting[] => {
  return list.map((item) => {
    if (item.children) {
      // 对子菜单进行递归处理
      item.children = formatterFnOption(item.children, clickDomEl, el, params);
    }
    if (isFunction(item.label)) {
      item.label = item.label(params, clickDomEl, el);
    }
    if (isFunction(item.tips)) {
      item.tips = item.tips(params, clickDomEl, el);
    }
    if (isFunction(item.hidden)) {
      item.hidden = item.hidden(params, clickDomEl, el);
    }
    if (isFunction(item.disabled)) {
      item.disabled = item.disabled(params, clickDomEl, el);
    }
    return item;
  });
};

窗口碰撞处理

菜单弹出时根据传入的坐标当作左上角定位,此时需要检测它是否碰撞到了窗口,当传入坐标加上菜单宽度或高度超出了窗口最大宽高时要进行调整。

const show = async (x = 0, y = 0) => {
  // ...some other code
  await nextTick();
  // 以下代码检测是否碰撞到了窗口
  const { innerWidth: windowWidth, innerHeight: windowHeight } = window;
  const menu = MenuWrapper.value;
  const menuHeight = menu.offsetHeight;
  const menuWidth = props.menuWidth || 200;
  menuLeft.value =
    x + menuWidth + 1 > windowWidth ? windowWidth - menuWidth - 5 : x + 1;
  menuTop.value =
    y + menuHeight + 1 > windowHeight ? windowHeight - menuHeight - 5 : y + 1;
};

因为二级菜单是悬停后才出现的,所以二级菜单的碰撞检测需同样额外去处理。

const handleMenuMouseEnter = ($event: MouseEvent, item: MenuSetting) => {
  if (item.children && !item.disabled) {
    hoverFlag.value = true;
    const el = $event.currentTarget as HTMLElement;
    if (!el) return;
    const { offsetWidth } = el;
    const subEl = el.querySelector(".__menu__sub__wrapper") as HTMLElement;
    if (!subEl) return;
    // 以下代码检测是否碰撞到了窗口
    const { offsetWidth: subOffsetWidth, offsetHeight: subOffsetHeight } =
      subEl;
    const { innerWidth: windowWidth, innerHeight: windowHeight } = window;
    const { top, left } = el.getBoundingClientRect();
    if (left + offsetWidth + subOffsetWidth > windowWidth - 5) {
      subLeft.value = left - subOffsetWidth + 5;
    } else {
      subLeft.value = left + offsetWidth;
    }
    if (top + subOffsetHeight > windowHeight - 5) {
      subTop.value = windowHeight - subOffsetHeight;
    } else {
      subTop.value = top + 5;
    }
  }
};

二级菜单

自定义样式

菜单样式通过 CSS3 变量控制,通过menuWrapperCssmenuItemCss两个 Props 传入修改 CSS 变量。

let el = MenuWrapper.value;
if (props.menuWrapperCss) {
  Object.keys(props.menuWrapperCss).map((item) => {
    el.style.setProperty(
      `--menu-${item}`,
      props.menuWrapperCss && props.menuWrapperCss[item]
    );
  });
}
if (props.menuItemCss) {
  Object.keys(props.menuItemCss).map((item) => {
    el.style.setProperty(
      `--menu-item-${item}`,
      props.menuItemCss && props.menuItemCss[item]
    );
  });
}

支持的样式如下:

menuWrapperCss-菜单容器CSS设置(点击展开查看)
参数说明类型默认值
background菜单容器背景色String#c8f2f0
boxShadow菜单容器阴影String0 1px 5px #888
padding默认 paddingString5px 0
borderRadius圆角String4px
lineColor分割线颜色String#ccc
lineMargin分割线 MarginString5px 0

menuItemCss-菜单项CSS设置(点击展开查看)
参数说明类型默认值
height每项高度String30px
padding-String0 10px
iconSize图标大小String20px
iconFontSize字体图标字体大小(设置类型为字体图标时可用)String-
iconColor字体图标颜色String#484852
labelColor菜单项标题颜色String#484852
labelFontSize菜单项标题字体大小String14px
tipsColor菜单辅助文字颜色String#889
tipsFontSize菜单辅助文字字体大小String12px
arrowColor指示箭头颜色(出现子菜单时生成)String#484852
arrowSize指示箭头大小(指示箭头为使用 border 生成的三角形)String10px
disabledColor菜单禁用状态时的颜色String#bcc
hoverBackgroundhover 时菜单项的背景色Stringrgba(255,255,255,.8)
hoverLabelColorhover 时菜单项 label 的颜色Stringnull
hoverTipsColorhover 时菜单项 tips 的颜色Stringnull
hoverArrowColorhover 时菜单项 arrow 的颜色Stringnull

对于不支持 CSS 变量的浏览器也可以选择覆盖 CSS 类名实现

⚡ 封装成函数调用

当前只是用 Vue 构建了菜单组件,但并不推荐直接使用组件方式引用。为了方便使用将其封装出一个函数,在需要使用时再调用函数,这种类似ElementUIMessageBox的调用方式。同时函数调用方式可确保页面上只会存在一个菜单实例,可优化性能。

函数调用方式必须在 options 中传入 el(绑定唤起菜单的 Dom 元素)。

function CustomMouseMenu(options: CustomMouseMenuOptions) {
  const className = "__mouse__menu__container";
  let container: HTMLElement;
  if (document.querySelector(`.${className}`)) {
    container = document.querySelector(`.${className}`) as HTMLElement;
  } else {
    container = createClassDom("div", className);
  }
  const vm = h(MouseMenu, options);
  render(vm, container);
  document.body.appendChild(container);
  return vm.component?.proxy as ComponentPublicInstance<typeof MouseMenu>;
}

🔥 封装成 Vue3 指令方式调用

因为使用指令可以提前知道菜单要绑定到哪个 Dom 元素中,把右键及 contextmenu 事件的处理封装在 vue 指令中,使用 Vue 指令可以更方便的调出菜单。Vue 指令也是本插件最推荐的方法。

因为移动端下的 contextmenu 行为不一致,我们可以采用长按事件代替。在指令封装中,同时做了 PC 端右键唤起与移动端长按唤起菜单的处理。

指令实现原理主要是利用传入的参数与绑定的 Dom 等参数,封装用户的右键与长按事件并利用CustomMouseMenu函数唤出菜单。

查看源码

指令方式使用如下:

<template>
  <div v-mouse-menu="options">Dom</div>
</template>
<script>
  import { MouseMenuDirective } from "@howdyjs/mouse-menu";
  export default {
    directive: {
      MouseMenu: MouseMenuDirective,
    },
    setup() {
      return {
        options: {}, // Some Options
      };
    },
  };
</script>

备注说明

为了性能,指令封装模式默认只对 mounted 钩子进行挂载。
当使用场景中有 params 参数传入菜单函数,有可能需要在组件更新时更新菜单,这时可以同时把 update 也挂载上。可参考以下写法:

import { MouseMenuDirective } from "@howdyjs/mouse-menu";
export default {
  directive: {
    MouseMenu: {
      ...MouseMenuDirective,
      updated: MouseMenuDirective.mounted,
    },
  },
};

🌟ElementPlus table 中使用右键菜单

比较常用的一个场景是在表格中右键列表项弹出菜单,并通过列表项数据显示不同的菜单。以下提供一个 Vue 指令方式操作原生表格的 Example:

由于ElementPlusUI 库中的el-table提供了row-contextmenu方法,这样可以很方便的让我们的右键菜单扩展到 el-table 中。

只要对row-contextmenu方法进行处理,就可以在 el-table 中实现右键弹出菜单功能。

showMouseMenu(row, column, event) {
  const { x, y } = event
  const ctx = CustomMouseMenu({
    el: event.currentTarget,
    params: row,
    ...this.menuOptions
  })
  ctx.show(x, y)
  event.preventDefault()
}
  • Example in ElementPlusTable: Demo

element-plus-table-mouse-menu.gif

✨ 其他说明

插件还支持其他配置,例如菜单图标,禁用模式等。

配置参数(Props/指令 Value):

参数说明类型默认值
el触发的 Dom 元素(以 Vue 组件方式或 CustomMenu 函数方式使用时必须传入)--
menuWidth菜单宽度Number200
menuList生成菜单项的数组,具体配置参考下表Array-
hasIcon是否有菜单图标Booleanfalse
iconType菜单图标的类型(目前仅支持字体图标)Stringfont-icon
menuWrapperCss菜单容器的 CSS 设置,具体配置参考下表Object-
menuItemCss菜单项的 CSS 设置,具体配置参考下表Object-
params传给处理函数的自定义参数,会注入到下方各回调函数的首个参数中any-
appendToBody容器是否挂载到 body 上Booleantrue
disabled是否禁用整个菜单,接收一个函数(params: any) => boolean-
useLongPressInMobile移动端下兼容使用长按事件唤出菜单, 但长按模式不支持多级菜单(仅支持指令方式)Booleanfalse
longPressDuration长按事件需持续时间,单位 msNumber500
injectCloseListener自动注入关闭菜单的 Listener,设为 false 时需自行处理Booleantrue

该插件收录在howdyjs中,为其一个分包,欢迎 start

乳白
杏仁黄
茉莉黄
麦秆黄
油菜花黄
佛手黄
篾黄
葵扇黄
柠檬黄
金瓜黄
藤黄
酪黄
香水玫瑰黄
淡密黄
大豆黄
素馨黄
向日葵黄
雅梨黄
黄连黄
金盏黄
蛋壳黄
肉色
鹅掌黄
鸡蛋黄
鼬黄
榴萼黄
淡橘橙
枇杷黄
橙皮黄
北瓜黄
杏黄
雄黄
万寿菊黄
菊蕾白
秋葵黄
硫华黄
柚黄
芒果黄
蒿黄
姜黄
香蕉黄
草黄
新禾绿
月灰
淡灰绿
草灰绿
苔绿
碧螺春绿
燕羽灰
蟹壳灰
潭水绿
橄榄绿
蚌肉白
豆汁黄
淡茧黄
乳鸭黄
荔肉白
象牙黄
炒米黄
鹦鹉冠黄
木瓜黄
浅烙黄
莲子白
谷黄
栀子黄
芥黄
银鼠灰
尘灰
枯绿
鲛青
粽叶绿
灰绿
鹤灰
淡松烟
暗海水绿
棕榈绿
米色
淡肉色
麦芽糖黄
琥珀黄
甘草黄
初熟杏黄
浅驼色
沙石黄
虎皮黄
土黄
百灵鸟灰
山鸡黄
龟背黄
苍黄
莱阳梨黄
蜴蜊绿
松鼠灰
橄榄灰
蟹壳绿
古铜绿
焦茶绿
粉白
落英淡粉
瓜瓤粉
蜜黄
金叶黄
金莺黄
鹿角棕
凋叶棕
玳瑁黄
软木黄
风帆黄
桂皮淡棕
猴毛灰
山鸡褐
驼色
茶褐
古铜褐
荷花白
玫瑰粉
橘橙
美人焦橙
润红
淡桃红
海螺橙
桃红
颊红
淡罂粟红
晨曦红
蟹壳红
金莲花橙
草莓红
龙睛鱼红
蜻蜓红
大红
柿红
榴花红
银朱
朱红
鲑鱼红
金黄
鹿皮褐
醉瓜肉
麂棕
淡银灰
淡赭
槟榔综
银灰
海鸥灰
淡咖啡
岩石棕
芒果棕
石板灰
珠母灰
丁香棕
咖啡
筍皮棕
燕颔红
玉粉红
金驼
铁棕
蛛网灰
淡可可棕
中红灰
淡土黄
淡豆沙
椰壳棕
淡铁灰
中灰驼
淡栗棕
可可棕
柞叶棕
野蔷薇红
菠萝红
藕荷
陶瓷红
晓灰
余烬红
火砖红
火泥棕
绀红
橡树棕
海报灰
玫瑰灰
火山棕
豆沙
淡米粉
初桃粉红
介壳淡粉红
淡藏花红
瓜瓤红
芙蓉红
莓酱红
法螺红
落霞红
淡玫瑰灰
蟹蝥红
火岩棕
赭石
暗驼棕
酱棕
栗棕
洋水仙红
谷鞘红
苹果红
铁水红
桂红
极光红
粉红
舌红
曲红
红汞红
淡绯
无花果红
榴子红
胭脂红
合欢红
春梅红
香叶红
珊瑚红
萝卜红
淡茜红
艳红
淡菽红
鱼鳃红
樱桃红
淡蕊香红
石竹红
草茉莉红
茶花红
枸枢红
秋海棠红
丽春红
夕阳红
鹤顶红
鹅血石红
覆盆子红
貂紫
暗玉紫
栗紫
葡萄酱紫
牡丹粉红
山茶红
海棠红
玉红
高粱红
满江红
枣红
葡萄紫
酱紫
淡曙红
唐菖蒲红
鹅冠红
莓红
枫叶红
苋菜红
烟红
暗紫苑红
殷红
猪肝紫
金鱼紫
草珠红
淡绛红
品红
凤仙花红
粉团花红
夹竹桃红
榲桲红
姜红
莲瓣红
水红
报春红
月季红
豇豆红
霞光红
松叶牡丹红
喜蛋红
鼠鼻红
尖晶玉红
山黎豆红
锦葵红
鼠背灰
甘蔗紫
石竹紫
苍蝇灰
卵石紫
李紫
茄皮紫
吊钟花红
兔眼红
紫荆红
菜头紫
鹞冠紫
葡萄酒红
磨石紫
檀紫
火鹅紫
墨紫
晶红
扁豆花红
白芨红
嫩菱红
菠根红
酢酱草红
洋葱紫
海象紫
绀紫
古铜紫
石蕊红
芍药耕红
藏花红
初荷红
马鞭草紫
丁香淡紫
丹紫红
玫瑰红
淡牵牛紫
凤信紫
萝兰紫
玫瑰紫
藤萝紫
槿紫
蕈紫
桔梗紫
魏紫
芝兰紫
菱锰红
龙须红
蓟粉红
电气石红
樱草紫
芦穗灰
隐红灰
苋菜紫
芦灰
暮云灰
斑鸠灰
淡藤萝紫
淡青紫
青蛤壳紫
豆蔻紫
扁豆紫
芥花紫
青莲
芓紫
葛巾紫
牵牛紫
紫灰
龙睛鱼紫
荸荠紫
古鼎灰
乌梅紫
深牵牛紫
银白
芡食白
远山紫
淡蓝紫
山梗紫
螺甸紫
玛瑙灰
野菊紫
满天星紫
锌灰
野葡萄紫
剑锋紫
龙葵紫
暗龙胆紫
晶石紫
暗蓝紫
景泰蓝
尼罗蓝
远天蓝
星蓝
羽扇豆蓝
花青
睛蓝
虹蓝
湖水蓝
秋波蓝
涧石蓝
潮蓝
群青
霁青
碧青
宝石蓝
天蓝
柏林蓝
海青
钴蓝
鸢尾蓝
牵牛花蓝
飞燕草蓝
品蓝
银鱼白
安安蓝
鱼尾灰
鲸鱼灰
海参灰
沙鱼灰
钢蓝
云水蓝
晴山蓝
靛青
大理石灰
海涛蓝
蝶翅蓝
海军蓝
水牛灰
牛角灰
燕颔蓝
云峰白
井天蓝
云山蓝
釉蓝
鸥蓝
搪磁蓝
月影白
星灰
淡蓝灰
鷃蓝
嫩灰
战舰灰
瓦罐灰
青灰
鸽蓝
钢青
暗蓝
月白
海天蓝
清水蓝
瀑布蓝
蔚蓝
孔雀蓝
甸子蓝
石绿
竹篁绿
粉绿
美蝶绿
毛绿
蔻梢绿
麦苗绿
蛙绿
铜绿
竹绿
蓝绿
穹灰
翠蓝
胆矾蓝
樫鸟蓝
闪蓝
冰山蓝
虾壳青
晚波蓝
蜻蜓蓝
玉鈫蓝
垩灰
夏云灰
苍蓝
黄昏灰
灰蓝
深灰蓝
玉簪绿
青矾绿
草原远绿
梧枝绿
浪花绿
海王绿
亚丁绿
镍灰
明灰
淡绿灰
飞泉绿
狼烟灰
绿灰
苍绿
深海绿
长石灰
苷蓝绿
莽丛绿
淡翠绿
明绿
田园绿
翠绿
淡绿
葱绿
孔雀绿
艾绿
蟾绿
宫殿绿
松霜绿
蛋白石绿
薄荷绿
瓦松绿
荷叶绿
田螺绿
白屈菜绿
河豚灰
蒽油绿
槲寄生绿
云杉绿
嫩菊绿
艾背绿
嘉陵水绿
玉髓绿
鲜绿
宝石绿
海沬绿
姚黄
橄榄石绿
水绿
芦苇绿
槐花黄绿
苹果绿
芽绿
蝶黄
橄榄黄绿
鹦鹉绿
油绿
象牙白
汉白玉
雪白
鱼肚白
珍珠灰
浅灰
铅灰
中灰
瓦灰
夜灰
雁灰
深灰