📜 ⬆️ ⬇️

Own context menu using JavaScript

Web applications are now becoming a new step in the development of the web. This is not ordinary information sites. Examples of advanced web applications include Gmail and Dropbox. With the growth of functionality, availability and usefulness of web applications, the need to increase the efficiency of their use grows. This guide will cover the creation of such a useful thing as your own context menu, and in particular:
  1. We will understand what the context menu is and why it is needed.
  2. We implement our context menu using JS and CSS.
  3. We will touch upon the shortcomings and limitations of the approach used, in order to know what problems may caution us when rolling out all this into production.


What is the context menu?


According to Wikipedia , the context menu is the menu that appears when the user interacts with the graphical user interface (by pressing the right mouse button). The context menu contains a limited set of possible actions, which are usually associated with the selected object.

On your computer, right-clicking on the desktop will bring up the context menu of the operating system. From here you can probably create a new folder, get some information and do something else. The context menu in the browser allows, for example, to get information about the page, view its source, save the image, open the link in a new tab, work with the clipboard and all that. Moreover, the set of available actions depends on where you clicked, that is, on the context. This is the standard behavior laid by browser developers [ and extensions to it ].

Web applications are gradually beginning to replace the standard context menus with their own. Excellent examples are all the same Gmail and Dropbox. The only question is how to make your context menu? In the browser, when you click the right mouse button, the contextmenu event fires. We will have to cancel the default behavior and make it so that instead of the standard menu we display our own. It is not so difficult, but we will understand step by step, so it will be quite volume. To begin with, we will create the basic structure of the application so that the example being developed is not completely divorced from reality.
')

Task list


Imagine that we are creating an application that allows you to maintain a list of tasks. I understand, you are probably incredibly tired of all these task lists, but let it be. The application page contains a list of unfinished tasks. For each task, a typical set of CRUD actions is available: get information about the task, add a new one, edit it, delete it.

An example of the result is on CodePen . You can look there immediately, if you are too lazy to read or want to make sure that you are really interested in further reading. In the meantime, proceed to the step by step development of the plan. I will use some modern CSS features and create a simple task list on data attributes. I will also use the reset of styles from Eric Meyer and reset the box-sizing property for all elements in the border-box:
*, *::before, *::after {   box-sizing: border-box; } 

I will not use prefixes in CSS, but an auto-prefixer is included in the demo on CodePen.

Creating a basic structure


Open our HTML-document, distribute the header, the content part with a certain list of tasks and the basement. I will also pull up Font Awesome and Roboto to make the layout a little better. Each task must contain the data-id attribute, which in reality would be taken from the database. Also, each task will contain a list of actions. Here are the important parts of the markup:
 <body>  <ul class="tasks">    <li class="task" data-id="3">      <div class="task__content">        Go To Grocery      </div>      <div class="task__actions">        <i class="fa fa-eye"></i>        <i class="fa fa-edit"></i>        <i class="fa fa-times"></i>      </div>    </li>    <!-- more task items here... -->  </ul>  <script src="main.js"></script> </body> 

If you are using CodePen, you can enable the autoprefixer and the CSS reset connection in the settings. Otherwise, you have to do everything by hand, if you have not yet automated this process. Do not forget that our goal is to create a context menu, so action processing will not be implemented. And now let's add some more CSS:
 /* tasks */ .tasks {  list-style: none;  margin: 0;  padding: 0; } .task {  display: flex;  justify-content: space-between;  padding: 12px 0;  border-bottom: solid 1px #dfdfdf; } .task:last-child {  border-bottom: none; } 

A complete set of styles (and everything else) is presented at CodePen. And here are the most important parts of the code, markup and design. But let's finally get closer to our context menu.

Outline our context menu - markup.


The basis of our menu is the same as that of any other menu - an unordered list nested in the nav tag. Each action will be represented as a list item with a link. Each link is responsible for a specific action. As I mentioned earlier, we need three actions in the context menu:
  1. View task.
  2. Editing a task.
  3. Delete a task.

Throw markup:
 <nav class="context-menu">  <ul class="context-menu__items">    <li class="context-menu__item">      <a href="#" class="context-menu__link">        <i class="fa fa-eye"></i> View Task      </a>    </li>    <li class="context-menu__item">      <a href="#" class="context-menu__link">        <i class="fa fa-edit"></i> Edit Task      </a>    </li>    <li class="context-menu__item">      <a href="#" class="context-menu__link">        <i class="fa fa-times"></i> Delete Task      </a>    </li>  </ul> </nav> 

If you have no idea where to put this markup, place it in front of the closing body tag. Before turning to CSS, let's clarify a couple of points:
  1. We want the context menu to appear where the right click was executed, that is, it needs absolute positioning. Therefore, it is not necessary to place its markup in a container with relative positioning.
  2. We will need some variables or attributes so that we can determine which task the selected action belongs to.

And now the styles.

Putting our menu in order - CSS


The need for absolute positioning of the menu under development has already been mentioned. In addition, set the z-index property to 10. Do not forget that your application may require a different value. These are not all possible styles, other beautiful things are brought in the demo, but they already depend on your needs and are not mandatory. Before moving on to JS, we’ll make the menu invisible by default and add an extra class to display it.
 .context-menu {  display: none;  position: absolute;  z-index: 10; } .context-menu--active {  display: block; } 

Expand our context menu - JavaScript


Let's start by looking at how to register a contextmenu event. Open the self-executing function and catch the event on the entire document. We will also log an event in the console to get some information:
 (function() {  "use strict";  document.addEventListener( "contextmenu", function(e) {    console.log(e);  }); })(); 

If you open the console and click somewhere with the right mouse button, you will see that the event is actually displayed there. There are a lot of different information that we can use. We are especially interested in coordinates. Before canceling the default behavior, let's take into account that this should not be done on the entire document, but only on the elements of the task list. With this in mind, you will need to perform the following steps:
  1. You will need to loop through all the items in the task list and add a contextmenu event handler to each of them.
  2. For each handler, cancel the default behavior.
  3. We will log the event and the item to which it belongs to the console.

In general, we do something like this:
 (function() {  "use strict";  var taskItems = document.querySelectorAll(".task");  for ( var i = 0, len = taskItems.length; i < len; i++ ) {    var taskItem = taskItems[i];    contextMenuListener(taskItem);  }  function contextMenuListener(el) {    el.addEventListener( "contextmenu", function(e) {      console.log(e, el);    });  } })(); 

If you look at the console, you can see that a unique event is triggered with each click on an item from the task list. Now, in addition to canceling the default behavior, we implement the display of the context menu by adding an auxiliary class to it.

But first, let's add an ID to the menu to make it easier to get through JS. Also add a menuState and menu state variable and a variable with the active class. Three variables turned out:
 var menu = document.querySelector("#context-menu"); var menuState = 0; var active = "context-menu--active"; 

We go further. Let's review the contextMenuListener function and add toggleMenuOn, which displays the menu:
 function contextMenuListener(el) {  el.addEventListener( "contextmenu", function(e) {    e.preventDefault();    toggleMenuOn();  }); } function toggleMenuOn() {  if ( menuState !== 1 ) {    menuState = 1;    menu.classList.add(active);  } } 

At the moment, the right mouse button you can already call our context menu. But it cannot be said that it works correctly. Firstly, it is not at all where we would like. To solve the problem will need a little math. Secondly, it is not yet possible to close this menu. Taking into account how ordinary context menus work, I would like our implementation to close when you click a menu and press Escape. In addition, when right clicked outside our menu, it should close, and instead, the default menu should be opened. Let's try to solve it all.

Refactoring our code


It is obvious that for all actions there will be three main events:
  1. contextmenu - Checking the status and opening the context menu.
  2. click - Hide menu.
  3. keyup - Handling keystrokes. In this manual, only ESC is interesting.

We will also need some auxiliary functions, so we’ll add a section for them to the code. Thus we have:
Lot of code
 (function() {  "use strict";  ///////////////////////////////////////  ///////////////////////////////////////  //  // HELPER   FUNCTIONS  //    //  ///////////////////////////////////////  ///////////////////////////////////////  /**   * Some helper functions here.   *    .   */  ///////////////////////////////////////  ///////////////////////////////////////  //  // CORE   FUNCTIONS  //    //  ///////////////////////////////////////  ///////////////////////////////////////  /**   * Variables.   * .   */  var taskItemClassName = 'task';  var menu = document.querySelector("#context-menu");  var menuState = 0;  var activeClassName = "context-menu--active";  /**   * Initialise our application's code.   *    .   */  function init() {    contextListener();    clickListener();    keyupListener();  }  /**   * Listens for contextmenu events.   *   contextmenu.   */  function contextListener() {  }  /**   * Listens for click events.   *   click.   */  function clickListener() {  }  /**   * Listens for keyup events.   *   keyup.   */  function keyupListener() {  }  /**   * Turns the custom context menu on.   *   .   */  function toggleMenuOn() {    if ( menuState !== 1 ) {      menuState = 1;      menu.classList.add(activeClassName);    }  }  /**   * Run the app.   *  .   */  init(); })(); 

Now we do not iterate over the list items. Instead, we will handle the contextmenu event throughout the document, checking whether it is one of the tasks. Therefore, the taskItemClassName variable is entered. We will do this using the convenience function clickInsideElement, which takes two parameters:
  1. The event itself is checked.
  2. The name of the class to compare. If an event occurred inside an element having the specified class, or the parent of this element has such a class, then you need to return this element.

Here is the first auxiliary function:
 function clickInsideElement( e, className ) {  var el = e.srcElement || e.target;  if ( el.classList.contains(className) ) {    return el;  } else {    while ( el = el.parentNode ) {      if ( el.classList && el.classList.contains(className) ) {        return el;      }    }  }  return false; } 

Let's go back and edit contextListener:
 function contextListener() {  document.addEventListener( "contextmenu", function(e) {    if ( clickInsideElement( e, taskItemClassName ) ) {      e.preventDefault();      toggleMenuOn();    }  }); } 

Having a helper function that does some of the dirty work for us, and catches the contextmenu event on the entire document, we can now close the menu when we click outside it. To do this, add the toggleMenuOff function and edit the contextListener:
 function contextListener() {  document.addEventListener( "contextmenu", function(e) {    if ( clickInsideElement( e, taskItemClassName ) ) {      e.preventDefault();      toggleMenuOn();    } else {      toggleMenuOff();    }  }); } function toggleMenuOff() {  if ( menuState !== 0 ) {    menuState = 0;    menu.classList.remove(activeClassName);  } } 

Now right click on the list item. And after - somewhere else in the document. Voila! Our menu closed and opened the standard. Then we will do something similar for the click event so that not from one right button it closes:
 function clickListener() {  document.addEventListener( "click", function(e) {    var button = e.which || e.button;    if ( button === 1 ) {      toggleMenuOff();    }  }); } 

This piece of code is slightly different from the previous one, because Firefox. After the right mouse button is pressed, the click event is triggered in Firefox, so here we have to additionally make sure that the click is actually the left button. Now the menu does not blink with a right click. Let's add a similar handler to pressing the ESC key:
 function keyupListener() {  window.onkeyup = function(e) {    if ( e.keyCode === 27 ) {      toggleMenuOff();    }  } } 

We got a menu that opens and closes as intended, interacting with the user in a natural way. Let's finally position the menu and try to handle the events inside it.

Positioning our context menu


With current HTML and CSS, our menu is displayed at the bottom of the screen. But we would like it to appear where the click occurred. Let's fix this annoying omission. First, add another auxiliary function that gets the exact coordinates of the click. Let's call it getPosition and try to make it process different browser quirks:
 function getPosition(e) {  var posx = 0;  var posy = 0;  if (!e) var e = window.event;  if (e.pageX || e.pageY) {    posx = e.pageX;    posy = e.pageY;  } else if (e.clientX || e.clientY) {    posx = e.clientX + document.body.scrollLeft +                       document.documentElement.scrollLeft;    posy = e.clientY + document.body.scrollTop +                       document.documentElement.scrollTop;  }  return {    x: posx,    y: posy  } } 

Our first step in menu positioning is the preparation of three variables. Add them to the appropriate code block:
 var menuPosition; var menuPositionX; var menuPositionY; 

Create a positionMenu function that takes a single argument, the event. For now, let it print the menu coordinates to the console:
 function positionMenu(e) {  menuPosition = getPosition(e);  console.log(menuPosition); } 

Edit contextListener to start the positioning process:
 function contextListener() {  document.addEventListener( "contextmenu", function(e) {    if ( clickInsideElement( e, taskItemClassName ) ) {      e.preventDefault();      toggleMenuOn();      positionMenu(e);    } else {      toggleMenuOff();    }  }); } 

Poke the context menu again and look at the console. Make sure that the position is really available and logged. We can use inline styles to set top and left properties via JS. Here is the new version of positionMenu:
 function positionMenu(e) {  menuPosition = getPosition(e);  menuPositionX = menuPosition.x + "px";  menuPositionY = menuPosition.y + "px";  menu.style.left = menuPositionX;  menu.style.top = menuPositionY; } 

Shout now everywhere. The menu appears everywhere! It's awesome, but there are a couple of things that need to be resolved:
  1. What happens if the user clicks close to the right edge of the window? The context menu will go beyond it.
  2. What to do if the user resizes the window when the context menu is open? There is the same problem. Dropbox solves this problem by hiding the x-axis overflow (x-overflow: hidden).

Let's solve the first problem. We use JS to determine the width and height of our menu, and check that the menu fits completely. Otherwise, a little shift it. This will require a bit of mathematics and reflection, but we will do it simply and step by step. First, check the width and height of the window. Then find the width and height of the menu. And after we make sure that the difference between the coordinates of the click and the width of the indented window is greater than the width of the menu. And do the same for height. If the menu does not fit on the screen, adjust its coordinates. Start by adding two variables:
 var menuWidth; var menuHeight; 

As you remember, by default our menu is hidden, so you can't just take and calculate its size. In our case, the menu is static, but in actual use its contents may vary depending on the context, so it’s better to calculate the width and height at the time of opening. We obtain the necessary values ​​inside the positionMenu function:
 menuWidth = menu.offsetWidth; menuHeight = menu.offsetHeight; 

We introduce two more variables, but this time for window sizes:
 var windowWidth; var windowHeight; 

We calculate their values ​​in a similar way:
 windowWidth = window.innerWidth; windowHeight = window.innerHeight; 

Ultimately, let's assume that we want to show the menu no closer than 4 pixels from the edge of the window. You can compare the values, as I said above, and adjust the position something like this:
 var clickCoords; var clickCoordsX; var clickCoordsY; // updated positionMenu function function positionMenu(e) {  clickCoords = getPosition(e);  clickCoordsX = clickCoords.x;  clickCoordsY = clickCoords.y;  menuWidth = menu.offsetWidth + 4;  menuHeight = menu.offsetHeight + 4;  windowWidth = window.innerWidth;  windowHeight = window.innerHeight;  if ( (windowWidth - clickCoordsX) < menuWidth ) {    menu.style.left = windowWidth - menuWidth + "px";  } else {    menu.style.left = clickCoordsX + "px";  }  if ( (windowHeight - clickCoordsY) < menuHeight ) {    menu.style.top = windowHeight - menuHeight + "px";  } else {    menu.style.top = clickCoordsY + "px";  } } 

Now our menu behaves quite well. It remains to do something with resize the window. I have already said what Dropbox is doing, but instead we will close the context menu. [ This behavior is much closer to the standard ] Add the following line to the init function:
 resizeListener(); 

And write the function itself:
 function resizeListener() {  window.onresize = function(e) {    toggleMenuOff();  }; } 

Sumptuously.

We cling events to context menu items


If your application is more complicated than this example, and you have planned the dynamic contents of the context menu, you will have to go further into what is going on to find out the missing details yourself. In our application, everything is simpler, and there is only one menu with a constant set of actions. Thus, you can quickly check which item was selected and process this selection. In our example, simply save the selected item to a variable and write its data-id and selected action to the console. To do this, edit the menu layout:
 <nav id="context-menu" class="context-menu">  <ul class="context-menu__items">    <li class="context-menu__item">      <a href="#" class="context-menu__link" data-action="View">        <i class="fa fa-eye"></i> View Task      </a>    </li>    <li class="context-menu__item">      <a href="#" class="context-menu__link" data-action="Edit">        <i class="fa fa-edit"></i> Edit Task      </a>    </li>    <li class="context-menu__item">      <a href="#" class="context-menu__link" data-action="Delete">        <i class="fa fa-times"></i> Delete Task      </a>    </li>  </ul> </nav> 

Next, let's cache all the necessary objects:
 var contextMenuClassName = "context-menu"; var contextMenuItemClassName = "context-menu__item"; var contextMenuLinkClassName = "context-menu__link"; var contextMenuActive = "context-menu--active"; var taskItemClassName = "task"; var taskItemInContext; var clickCoords; var clickCoordsX; var clickCoordsY; var menu = document.querySelector("#context-menu"); var menuItems = menu.querySelectorAll(".context-menu__item"); var menuState = 0; var menuWidth; var menuHeight; var menuPosition; var menuPositionX; var menuPositionY; var windowWidth; var windowHeight; 

There is a taskItemInContext variable, which is assigned a value when right-clicking on a list item. We need it for logging item IDs. Also new class names have appeared. Now go through the functionality.

The initialization function remains the same. The first change affects contextListener, because we want to save the item on which the user clicked in the taskItemInContext, and the clickInsideElement function returns it:
 function contextListener() {  document.addEventListener( "contextmenu", function(e) {    taskItemInContext = clickInsideElement( e, taskItemClassName );    if ( taskItemInContext ) {      e.preventDefault();      toggleMenuOn();      positionMenu(e);    } else {      taskItemInContext = null;      toggleMenuOff();    }  }); } 

We reset it to null if the right click does not occur on the list item. Well, take on clickListener. As I mentioned earlier, for simplicity, we will simply output information to the console. Now when the click event is caught, several checks are made and the menu closes. Let's make adjustments and start processing the click within the context menu, performing some action and only then closing the menu:
 function clickListener() {  document.addEventListener( "click", function(e) {    var clickeElIsLink = clickInsideElement( e, contextMenuLinkClassName );    if ( clickeElIsLink ) {      e.preventDefault();      menuItemListener( clickeElIsLink );    } else {      var button = e.which || e.button;      if ( button === 1 ) {        toggleMenuOff();      }    }  }); } 

You may have noticed that the menuItemListener function is being called. We will define it a bit later. The keyupListener, resizeListener and positionMenu functions are left unchanged. The toggleMenuOn and toggleMenuOff functions will be edited slightly by changing the variable names for better readability of the code:
 function toggleMenuOn() {  if ( menuState !== 1 ) {    menuState = 1;    menu.classList.add( contextMenuActive );  } } function toggleMenuOff() {  if ( menuState !== 0 ) {    menuState = 0;    menu.classList.remove( contextMenuActive );  } } 

Finally, implement the menuItemListener:
 function menuItemListener( link ) {  console.log( "Task ID - " +                taskItemInContext.getAttribute("data-id") +                ", Task action - " + link.getAttribute("data-action"));  toggleMenuOff(); } 

This functionality development ends.

Some notes


Before we finish, let's take some points into account:
  1. Throughout the article, I mentioned the right click as an event to open the context menu. Not all are right-handed and not all have standard mouse settings. But regardless of this, the contextmenu event acts precisely in accordance with the settings of the mouse, and is not rigidly tied to the right button.
  2. Another important point is that we have considered only full-fledged desktop web applications with the mouse as an input device. Users can use the keyboard or mobile devices, so do not forget to provide alternative ways of interaction to maintain a friendly interface.

Big question


I have highlighted this problem in a separate paragraph, because it is really important after all that we have done. Ask yourself: do you really need your own context menu? Such things are cool, but before using them, you need to make sure that it is really useful in your case. Usually, users expect the usual behavior of the application. For example, after a right click on a photo, they expect to be able to save it, copy the link, etc. The lack of necessary items in the custom menu may upset them.

Browser Compatibility


The tutorial used some modern pieces from CSS and JS, namely display: flex in styles and classList to switch classes in JS. It is also worth mentioning that HTML5 tags are used. If backward compatibility with older browsers is required, you will have to implement it yourself. The examples in this guide are tested in the following browsers:

Conclusion and demo


If you have thought carefully about everything and are sure that your application needs this functionality, you can use the developed menu. Of course, it may require any changes, but this guide describes the development process in detail, so it should be not so difficult to implement your amendments.

GitHub , , . CodePen .

From translator


Translation in places is quite free, but not to the detriment of the meaning or content. Everything that does not relate directly to the original, made in the notes.
With suggestions, suggestions and comments, as usual, in drugs.

Source: https://habr.com/ru/post/258167/


All Articles