🚀 Preliminary idea

  • Only Vue is used to build the UI component of the right menu. It is necessary to use the component to provide the show method to evoke the menu, and then encapsulate the processing of the right menu event in the instruction
  • Receives a menu item array configuration that provides a menu – after – click callback function. You can customize the parameters passed to the callback function
  • DOM menu style as far as possible out of configurable, and use CSS variable injection, can customize the style of the menu
  • provideshowMethod receivesx,yPosition the parameters, and then display the menu, while adding the check window after the collision processing
  • Due to the special nature of the right-click menu, wrapable components are single instances, and only one menu instance exists on the page, which optimizes performance

🌈 Function

List of menu items -menuList

MenuList is required to render menu items, accepts an array of length at least 1, and can specify the children property to render second-level menus.

As far as possible, the configuration of menu items should be made to receive functions at the same time, so that different menus can be configured more quickly and dynamically by passing in parameters.

Currently, the following menu items are supported:

parameter instructions type
fn Click menu executed after the callback, the callback parameter 1 for the params of the incoming user parameter 2 to right-click the HtmlElement element (using the document. ElementFromPoint access), the binding of the current element parameter 3 for instructions (params, activeEl, bindingEl) => void
label Menu name, function available, callback argument same as fn option String, Function
tips Menu auxiliary text (text on the right), available functions, callback arguments and fn options String, Function
icon Class name of menu icon (font icon) String
hidden Whether or not menu items are hidden, functions can be used, callback arguments are the same as fn options Boolean, Function
disabled If the menu item is unclickable, you can use functions, callback arguments and fn options Boolean, Function
children Array of menu items for submenus (configured the same as this table, but currently only secondary menus are supported) Array
line If the value is True, the preceding Settings are invalid Boolean

Each time the menu is opened, the following methods are called to format the final menu item:

const formatterFnOption = (list: MenuSetting[], clickDomEl: HTMLElement, el: HTMLElement, params: any): MenuSetting[] => {
  return list.map(item= > {
    if (item.children) {
      // Perform recursive processing on submenus
      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;
  });
};
Copy the code
  • Example: Basic menu

Window collision handling

When the menu pops up, it is positioned as the upper left corner according to the passed coordinate. At this time, it needs to detect whether it collids with the window. When the passed coordinate plus the width or height of the menu exceeds the maximum width and height of the window, it should be adjusted.

const show = async (x = 0, y = 0) = > {/ /... some other code
  await nextTick();
  // The following code detects whether a window has been hit
  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;
};
Copy the code

Since the second level menu appears after hovering, collision detection of the second level menu also needs to be handled extra.

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;
    // The following code detects whether a window has been hit
    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; }}};Copy the code
  • Example: Secondary menu

Custom styles

Menu styles are controlled by CSS3 variables, which are passed in to modify CSS variables via menuWrapperCss and menuItemCss Props.

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]);
  });
}
Copy the code

The supported styles are as follows:

MenuWrapperCss – Menu Container CSS Settings (click to expand to view)
parameter instructions type The default value
background Menu container background color String #c8f2f0
boxShadow Menu container Shadow String 0 1px 5px #888
padding The default padding String 5px 0
borderRadius Rounded corners String 4px
lineColor Dividing line color String #ccc
lineMargin Line Margin String 5px 0


MenuItemCss – menuItemCss Settings (click to expand to view)
parameter instructions type The default value
height Each level String 30px
padding String 0 10px
iconSize Size of the icon String 20px
iconFontSize Font icon Font size (available when type is set to font icon) String
iconColor Font icon color String # 484852
labelColor Menu item title color String # 484852
labelFontSize Menu item title font size String 14px
tipsColor Menu auxiliary text color String # 889
tipsFontSize Menu Auxiliary text font size String 12px
arrowColor Indicates arrow color (generated when submenus appear) String # 484852
arrowSize Indicates arrow size (indicates arrows are triangles generated using border) String 10px
disabledColor Menu color when disabled String #bcc
hoverBackground Hover is the background color of a menu item String Rgba (255255255, 8)
hoverLabelColor Hover menu item label color String null
hoverTipsColor The color of the menu item TIPS when hover String null
hoverArrowColor Hover is the color of the menu item arrow String null


  • Example: Custom style

Browsers that do not support CSS variables can also override the CSS class name implementation

âš¡ is wrapped as a function call

Currently only menu components are built with Vue, but direct component references are not recommended. Encapsulate it as a function for ease of use and call the function when needed, similar to MessageBox of ElementUI. The function invocation ensures that only one menu instance exists on the page, which optimizes performance.

The function call method must pass an EL in options (bind the Dom element that evokes the menu).

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>;
}
Copy the code
  • Example: CustomMouseMenu function call

🔥 is wrapped as Vue3 instruction

Because using the command can know in advance which Dom element the menu is bound to, the processing of the right button and contextmenu event is encapsulated in the VUE command, which makes it more convenient to call out the menu. The Vue directive is also the most recommended method for this plug-in.

Since the contextMenu behavior on the mobile side is inconsistent, we can use the long-press event instead. In the instruction package, the process of right click arousing on PC side and long press arousing menu on mobile side is also done.

The principle of instruction implementation is to use the parameters passed in and bound Dom parameters, encapsulate the user’s right click and long press events and use CustomMouseMenu function to call out the menu.

View the source code

The command mode is as follows:

<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>
Copy the code

descr

For performance, instruction encapsulation mode only mounts mounted hooks by default. When the params parameter is passed to the menu function in a usage scenario, it is possible to update the menu when the component is updated, and you can mount the UPDATE as well. Please refer to the following writing method:

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

Use the right-click menu in 🌟ElementPlus table

A common scenario is to right-click a list item in a table to pop up a menu and display different menus through the list item data. Here is an Example of how to manipulate a native table using a Vue command:

  • Example: Directive binding to the native table

Since el-Table in the ElementPlusUI library provides the row-ContextMenu method, it’s easy to extend our right-click menu into el-Table.

With the row-ContextMenu method, you can right-click a pop-up menu in the 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() }Copy the code
  • Example in ElementPlusTable: Demo

✨ Other Instructions

The plugin also supports other configurations, such as menu ICONS, disable modes, and so on.

Configuration parameters (Props/ instruction Value) :

parameter instructions type The default value
el The Dom element triggered (must be passed in when used as a Vue component or as a CustomMenu function)
menuWidth The width of the menu Number 200
menuList Generate an array of menu items. For details, see the following table Array
hasIcon Whether there is a menu icon Boolean false
iconType Type of menu icon (currently only font icon supported) String font-icon
menuWrapperCss For details about the CSS Settings of menu containers, see the following table Object
menuItemCss For details about the CSS Settings of menu items, see the following table Object
params The custom arguments passed to the handler are injected into the first argument of each callback function below any
appendToBody Whether the container is mounted to the body Boolean true
disabled Whether to disable the entire menu and accept a function (params: any) => boolean
useLongPressInMobile On the mobile terminal, it is compatible to use long press events to call out menus, but the long press mode does not support multi-level menus (only instruction mode is supported). Boolean false
longPressDuration Long press event duration, in ms Number 500
injectCloseListener The Listener is automatically injected into the close menu. If the Listener is set to false, you need to handle it yourself Boolean true

The plug-in is included in thehowdyjsFor one of its subcontractors, welcome start

🔗 Links

  • Github
  • Demo