Preface
Since I personally do many projects in the background management, the right-click menu is also a relatively high-frequency component. However, the technology stack I personally use is Vue3. At present, the community does not have a good plug-in to use, so I can only choose to build my own wheels.
Basic content of directory structure
In the initial stage, I divided the menu component into two directories and named itand
,The two components each perform their duties,
Components provide outermost container positioning and hierarchical capabilities,
Provides style and current time event callbacks for each item.
ContextMenu component, I expect to insert it in the body, this is to facilitate component positioning (position
), then at this time you can use the help of Vue3Teleport
Components achieve this effect. Since my business scenario is right-clicking in the table, if I go to each row (tr
) or each cell (td
) All generate a menu component, which will result in multiple menu components in the body. This didn't fit my expectations, so I decided to usev-if
To control the display and hiding of components. The basic HTML structure is as follows:
<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>
Calculate the position of the ContextMenu component (position)
Want to knowContextMenu
Where will the component appear? We need to know how it is used in the component? I'm assuming there is a .vue component
<el-button @contextmenu="contextmenuFun">Button</el-button> <Contextmenu ref="ContextMenuRef"> </Contextmenu> import { ref } from 'vue' const ContextMenuRef = ref() const contextmenuFun = (e) => { (e) }
On the business side, you can see it. I expect a trigger point, whether it is a button or HTML element. This trigger point requires manual call to the show method in the ContextMenu component, and the current trigger event source needs to beevent
) Passed over. Then let's go backContextMenu
It's easy to write in the componentshow
The logic of the method.
const position = ref({ top: 0, left: 0 }) const style = computed(() => { return { left: , top: } }) const show = (e: MouseEvent) => { (e, "e") () = true }
Then the location where the contextMenu appears needs to be calculated dynamically. Note that the point is the location where the occurrence is, and we need to calculate the boundary value.
... // Calculate the offset value of x and yconst calculatePosition = (axis: "X" | "Y", mousePos: number, elSize: number) => { const windowSize = axis === "X" ? : const scrollPos = axis === "X" ? : let pos = mousePos - scrollPos if (pos + elSize > windowSize) { pos = (0, pos - elSize) } return pos + scrollPos } const show = async (e: MouseEvent) => { () = true await nextTick() const el = if (!el) { return } const width = const height = const { pageX: x, pageY: y } = e = calculatePosition("Y", y, height) = calculatePosition("X", x, width) (, "w") } ...
We passedcalculatePosition
Calculate the valid x,y, and use itMake sure the display does not exceed the current screen.
Click outside the menu to hide
How to tell if you click outside the menu to hide it? At this time, you need to use the help of the click objectevent
The event is processed, and the peripheral of the processing click element is used as a hook and nameduseClickOutside
import { onMounted, onBeforeUnmount, Ref } from "vue" function useClickOutside(elementRef: Ref<HTMLElement | null>, callback: (event: MouseEvent) => void): void { const clickOutsideHandler = (event: MouseEvent) => { const el = if (!el || el === || ().includes(el)) { return } callback(event) } onMounted(() => { ("click", clickOutsideHandler) }) onBeforeUnmount(() => { ("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, () => { = false })
At this time, we can click outside the menu to hide the menu, but there will be another problem, that is, if, right-click to expand the menu, when I click a button, I don’t want the menu to be hidden, but I hope it will be displayed all the time. At this time, it needs to be targeteduseClickOutside
Add an extra parameter to control. The menu is not hidden when clicking on an element
In business code, it can be passedignore
Exclude HTML elements
div class="contextMenua" @contextmenu="contextmenu">123</div> <button class="ingoreBtn">Not hidden buttons</button> <ContextMenu ref="contextmenuRef" :ignore="ignore" />
Defining props in contextmenu
interface Props { ignore: string[] } const props = withDefaults(defineProps<Props>(), { ignore: () => [] as string[] }) ... useClickOutside( contextmenuRef, () => { ("w") = false }, { ignore: } ) ...
Added new in useClickOutside functionIgnoreElement
Methods are used to exclude HTML elements
let isIgnore = true const IgnoreElement = (ignore: string[], event: MouseEvent) => { return ((target) => { if (typeof target === "string") { return ((target)).some( (el) => el === || ().includes(el) ) } }) } const clickOutsideHandler = (event: MouseEvent) => { ... if (options?.ignore && > 0) { isIgnore = !IgnoreElement(, event) } if (!isIgnore) { isIgnore = true return } ... }
We passedisIgnore
The variables are marked to determine whether they have experienced it.IgnoreElement
The default call to true will not affect the existing logic. whenisIgnore
When false, we need to turn it into true to prevent the next click from being hidden.
The menu does not scroll with the scrollbar
When our page height exceeds the screen height, a scroll bar will appear. When we right-click a menu for an element, it will appear, and then scroll, we will find that our menu will also move. To solve this situation, a transparent cover can be used to cover the body, causing the original scrolling behavior to be invalidated. In this theory, the HTML structure needs to be adjusted
<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( () => , () => { if () { = "hidden" } else { = } } ) const show = async (e: MouseEvent) => { ... = ... }) useClickOutside( contextmenuRef, () => { = false = false }, { ignore: } ) onMounted(async () => { if () { await nextTick() = const style = () = } }) <style> .contextMenu-wrapper { z-index: 9999; background-color: transparent; &.is-fixed { position: fixed; left: 0; top: 0; width: 100%; height: 100%; } } </style>
Addedis-fixed
Variables are used as identification of whether the masking layer is required. passwatch
Listen to the fixed changes, if true, body'soverflow
If it becomes hidden, if it is closed, restore the default valuedefaultSyleOverFlow
So far, the basic functions of the drop-down menu have been completed, but the following functions have not been completed yet:
- Respond to keyboard events
- Control of hierarchical zIndex
- Multi-level menu (subItem)
This is the end of this article about the example of Vue3 development right-click menu. For more related contents of Vue3 right-click menu, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!