This is the 7th day of my participation in the August More Text Challenge

This article explains how Web pages can implement custom menu functionality.

Viewing online Demo

Train of thought

The core idea is: Register the ContextMenu event, cancel the default behavior of the event, and use the Event object to get the coordinates of the cursor relative to the viewport (event.clientX and Event.clienty). Display your own custom div blocks that are not visible at initialization.

implementation

DOM structure

The first is the DOM structure. The structure is as follows:

  • div.page-viewIs the element that registers the ContextMenu event;
  • div.contextmenu-maskIt’s a mask. It covers the whole window. It appears along with the right menu, the function is to prevent users from calling up the right menu, but also can click the button outside the menu. You can also add a background color that is transparent, but this is similar to a popover. Generally speaking, is not set background color.
  • div.contextmenu-contentRight-click the contents of the menu.
<div class="page-view">Click on the area</div>

<div class="contextmenu-mask" style="display: none;"></div>
<div class="contextmenu-content">
  <div class="list">
    <div class="item">copy</div>
    <div class="item">shear</div>
    <div class="item">Paste paste paste paste paste paste paste paste</div>
    <div class="item">select all</div>
  </div>
</div>
Copy the code

CSS styles

.page-view {
  margin: 0 auto;
  width: 90%;
  height: calc(100vh - 30px);
  background-color: azure;
}
/* Mask layer */
.contextmenu-mask {
  position: fixed;
  left: 0;
  right: 0;
  top: 0;
  bottom: 0;
  /* background-color: #000; * /
  /* opacity: .2; * /
  z-index: 45;
}
/* Container for menu contents */
.contextmenu-content {
  position: fixed;
  left: 999999px;
  top: 999999px;
  z-index: 50;
  user-select: none;
}
/* Examples use content */
.list {
  border: 1px solid # 555;
  border-radius: 4px;
  min-width: 180px;
  overflow: hidden; /* Handle rounded corners */
}
.item {
  box-sizing: border-box;
  padding: 0 5px;
  height: 30px;
  line-height: 30px;
  word-break: keep-all; /* is important, otherwise line breaks */
  background-color: #fff;
  cursor: default;
}
.item:hover {
  background-color: dodgerblue;
  color: #fff;
}
Copy the code

Here are a few caveats:

  • .contextmenu-contentIt’s not useddisplay: noneInstead, run away from the window by setting very large left and top. There are reasons for this, which we’ll explain in more detail in the script logic later.
  • .itemYou need to set upword-break: keep-all;. Because when the menu goes out of the window, the width becomes the minimum width, in this case 180px. Only when this property and value are set will the text not wrap and have the desired width.
  • .contextmenu-contentFixed positioning is required, not absolute positioning. Because the left and top elements are set to large values, nooverflow: hiddenThe container element produces a very long scroll bar. Fixed positioning does not.

Script logic

Right click to display menu

Start by undoing the default behavior for menu events in the click area.

Get the coordinates of the cursor, and adjust the coordinates to prevent the menu part from running out of the window and being cut. To do this, we need to get the width of the menu and the width of the window viewable area. In addition, in order to prevent the edge of the menu from clinging to the edge of the window, it is necessary to set a minimum padding value for calculation.

Truncated menu:

Menu attached to the edge of the window:

Take setting the abscissa as an example:

if (e.clientX + contextmenuWidth > document.documentElement.clientWidth - PADDING_RIGHT) {
  finalX = e.clientX - contextmenuWidth
}
Copy the code

When the prediction finds that the current cursor is on the left side of the menu, part of the right side of the menu will be cut, so the current coordinate is taken as the right side of the menu, and the coordinate of the upper left corner is the value of the cursor minus the width of the menu.

The complete code is:

const areaEl = document.querySelector('.page-view')
const mask = document.querySelector('.contextmenu-mask')
const contentEl = document.querySelector('.contextmenu-content')

/ * * * *@param {number} X The upper-left coordinate of the menu to be set x *@param {number} Y top left corner y star@param {number} W menu width *@param {number} H Menu height *@returns {x, y} Adjusted coordinates */
const adjustPos = (x, y, w, h) = > {
  const PADDING_RIGHT = 6  // Leave some space on the right side to prevent direct edge, which is not good
  const PADDING_BOTTOM = 6  // Leave some space at the bottom
  const vw = document.documentElement.clientWidth
  const vh = document.documentElement.clientHeight
  if (x + w > vw - PADDING_RIGHT) x -= w
  if (y + h > vh - PADDING_BOTTOM) y -= h
  return {x, y}
}

const onContextMenu = e= > {
  e.preventDefault()
  const rect = contentEl.getBoundingClientRect()
  // console.log(rect)
  const { x, y } = adjustPos(e.clientX, e.clientY, rect.width, rect.height)
  showContextMenu(x, y)
}

// Block menu events under the specified element
areaEl.addEventListener('contextmenu', onContextMenu, false)
Copy the code

Hide the right-click menu without using the normal display: None; Instead, use left and top with very high values. This is because I want to implement an adaptive width and height right-click menu. Therefore need menu width is high, the dynamic Element needed. GetBoundingClientRect () method, this method needs Element in the DOM tree, and as the visible elements, to get high wide, otherwise can only get two 0.

If you are implementing a menu whose width is manually written and whose height is measured by the number of menu items, the best way to hide the menu is display: None.

Hide the menu and click the menu item

Then click on the mask layer to hide the menu and mask. And click the menu item to execute the corresponding command

const hideContextMenu = () = > {
  mask.style.display = 'none'
  contentEl.style.top = '99999px'
  contentEl.style.left = '99999px'
}

// Click mask to hide
mask.addEventListener('mousedown'.() = > {
  hideContextMenu()
}, false)

// Click menu to hide
contentEl.addEventListener('click'.(e) = > {
  console.log('Click:', e.target.textContent)
  // Run the command corresponding to the menu item
  hideContextMenu()
}, false)
Copy the code

Other things to consider

  • Window shrinking situation: window shrinking will cause the menu at the bottom right to run outside the window area, whether to consider monitoring window events.
  • The logic of re-clicking the right button on the menu: Repositioning the right button in this position is the same as clicking the left button, or not processing, the browser’s native right button menu is displayed, you need to select according to your requirements.

At the end

The logic of implementing a custom menu is not complicated, that is, modifying the behavior of the ContextMenu event to show or hide the divs you wrote. But there are some details that need to be worked out to make a good, bug-free right-click menu.