Vue3開(kāi)發(fā)右鍵菜單的示例詳解
前言
由于我個(gè)人做的項(xiàng)目是后臺(tái)管理項(xiàng)目偏多,右鍵菜單也是屬于比較高頻的組件了。但是目前我個(gè)人使用的技術(shù)棧為Vue3,目前社區(qū)還沒(méi)有很好的插件進(jìn)行使用,只能被逼無(wú)奈選擇自己造輪子了。
目錄結(jié)構(gòu)基本構(gòu)成
初始階段,我把菜單組件分成兩個(gè)目錄,分別命名為ContextMenu.vue和ContentMenuItem.vue,兩個(gè)組件各施其職,ContextMenu.vue組件提供最外層容器定位和層級(jí)能力,ContentMenuItem.vue提供每項(xiàng)的樣式和當(dāng)前時(shí)間事件回調(diào)。
ContextMenu.vue
ContextMenu組件,我是期望在能body中進(jìn)行插入,這是為了方便組件的定位(position),那么這個(gè)時(shí)候是可以借助Vue3中的Teleport組件實(shí)現(xiàn)該效果。由于我的業(yè)務(wù)場(chǎng)景是在表格中右鍵,如果我對(duì)每行(tr)或者每個(gè)單元格(td)都生成一個(gè)菜單組件,就會(huì)導(dǎo)致body中存在多個(gè)菜單組件。這個(gè)并不符合我的預(yù)期想法,所以我決定使用v-if來(lái)控制組件的顯示與隱藏。 基本的HTML結(jié)構(gòu)如下:
<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>
計(jì)算ContextMenu組件的位置(position)
想要知道ContextMenu組件會(huì)出現(xiàn)在什么位置,需要我們知道該組件中是怎么使用的?我假設(shè)有個(gè).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)
}
在業(yè)務(wù)側(cè),可以看到。我是期望有個(gè)觸發(fā)點(diǎn)的,無(wú)論按鈕或者HTML元素也好。這個(gè)觸發(fā)點(diǎn),需要手動(dòng)的去調(diào)用ContextMenu組件中show方法,并且需要把當(dāng)前的觸發(fā)事件源(event)傳遞過(guò)去。那么我們回到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出現(xiàn)的位置則需要我們動(dòng)態(tài)的進(jìn)行計(jì)算,注意點(diǎn)就是出現(xiàn)的位置,我們是需要計(jì)算邊界值。
...
// 計(jì)算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")
}
...
我們通過(guò)calculatePosition計(jì)算出有效的x,y,在用Math.max確保顯示不會(huì)超出當(dāng)前的屏幕。
點(diǎn)擊菜單外部隱藏
如何判斷點(diǎn)擊菜單外部進(jìn)行隱藏呢?這個(gè)時(shí)候,就需要借助點(diǎn)擊對(duì)象中的event事件進(jìn)行處理了,把處理點(diǎn)擊元素外圍作為一個(gè)hook進(jìn)行使用并命名為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
})
這個(gè)時(shí)候我們就能實(shí)現(xiàn)點(diǎn)擊菜單外部讓菜單隱藏了,但是還會(huì)伴隨一個(gè)問(wèn)題,就是如果,我右鍵展開(kāi)了菜單,當(dāng)我去點(diǎn)擊某個(gè)按鈕的時(shí)候,我不希望這個(gè)這個(gè)菜單進(jìn)行隱藏,而是希望一直顯示。這個(gè)時(shí)候,就需要針對(duì)useClickOutside添加一個(gè)額外的參數(shù)進(jìn)行控制。 針對(duì)點(diǎn)擊某個(gè)元素,菜單不隱藏
在業(yè)務(wù)代碼中,可以通過(guò)傳遞ignore進(jìn)行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函數(shù)中新增IgnoreElement方法用來(lái)排除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
}
...
}
我們通過(guò)isIgnore變量進(jìn)行打標(biāo)識(shí),用于判斷是否經(jīng)歷過(guò)IgnoreElement的調(diào)用,默認(rèn)為true,并不會(huì)影響現(xiàn)有邏輯。當(dāng)isIgnore為false的時(shí)候,我們需要把它變成true,防止下次點(diǎn)擊無(wú)法隱藏。
菜單不隨著滾動(dòng)條進(jìn)行滾動(dòng)
當(dāng)我們的頁(yè)面高度超出了屏幕高度時(shí),會(huì)出現(xiàn)滾動(dòng)條的情況,當(dāng)我們對(duì)某個(gè)元素進(jìn)行右鍵菜單的過(guò)程會(huì)出現(xiàn),然后再去進(jìn)行滾動(dòng),會(huì)發(fā)現(xiàn)我們的菜單也會(huì)跟隨著移動(dòng)。為了解決這個(gè)情況,可以使用一個(gè)透明的遮蓋層蓋住body,使得原本的滾動(dòng)行為失效。 在這理論上,需要對(duì)HTML結(jié)構(gòu)進(jìn)行調(diào)整
<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變量作為是否需要遮蓋層的標(biāo)識(shí)。通過(guò)watch監(jiān)聽(tīng)fixed的變化,如果為真的話,則需要body的overflow變成hidden,關(guān)閉了的話恢復(fù)默認(rèn)的值defaultSyleOverFlow
目前為止,就已經(jīng)完成了下拉菜單的基本功能,但是還有以下功能還沒(méi)有完成:
- 響應(yīng)鍵盤事件
- 層級(jí)zIndex的控制
- 多層級(jí)菜單(subItem)
到此這篇關(guān)于Vue3開(kāi)發(fā)右鍵菜單的示例詳解的文章就介紹到這了,更多相關(guān)Vue3右鍵菜單內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
如何測(cè)量vue應(yīng)用運(yùn)行時(shí)的性能
這篇文章主要介紹了如何測(cè)量vue應(yīng)用運(yùn)行時(shí)的性能,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,,需要的朋友可以參考下2019-06-06
vue在使用element組件出現(xiàn)<el-input>標(biāo)簽無(wú)法輸入的問(wèn)題
這篇文章主要介紹了vue在使用element組件出現(xiàn)<el-input>標(biāo)簽無(wú)法輸入的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-04-04
Vue中用watch一次監(jiān)聽(tīng)多個(gè)值變化的示例詳解
在Vue中,watch 本身不能監(jiān)聽(tīng)多個(gè)變量,但我們可以通過(guò)返回具有計(jì)算屬性的對(duì)象然后監(jiān)聽(tīng)該對(duì)象,從而實(shí)現(xiàn)一次性“監(jiān)聽(tīng)多個(gè)變量”,本文給大家介紹了Vue中用watch一次監(jiān)聽(tīng)兩個(gè)值變化的示例,需要的朋友可以參考下2024-01-01
Vuex,iView UI面包屑導(dǎo)航使用擴(kuò)展詳解
今天小編就為大家分享一篇Vuex,iView UI面包屑導(dǎo)航使用擴(kuò)展詳解,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2019-11-11
使用ElementUI el-upload實(shí)現(xiàn)一次性上傳多個(gè)文件
在日常的前端開(kāi)發(fā)中,文件上傳是一個(gè)非常常見(jiàn)的需求,尤其是在用戶需要一次性上傳多個(gè)文件的場(chǎng)景下,ElementUI作為一款非常優(yōu)秀的Vue.js 2.0組件庫(kù),為我們提供了豐富的UI組件,本文介紹了如何使用ElementUI el-upload實(shí)現(xiàn)一次性上傳多個(gè)文件,需要的朋友可以參考下2024-08-08
vue 運(yùn)用mock數(shù)據(jù)的示例代碼
本篇文章主要介紹了vue 運(yùn)用mock數(shù)據(jù)的示例代碼,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-11-11
關(guān)于vue3.0中的this.$router.replace({ path: ''/''})刷新無(wú)效果問(wèn)題
這篇文章主要介紹了關(guān)于vue3.0中的this.$router.replace({ path: '/'})刷新無(wú)效果問(wèn)題,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-01-01
vue2.x 父組件監(jiān)聽(tīng)子組件事件并傳回信息的方法
本篇文章主要介紹了vue2.x 父組件監(jiān)聽(tīng)子組件事件并傳回信息的方法,具有一定的參考價(jià)值,有興趣的可以了解一下2017-07-07

