Course: JavaScript

Progress (0%)

  1. Foundation

  2. Numbers

  3. Strings

  4. Conditions

  5. Loops

  6. Arrays

  7. Functions

  8. Objects

  9. Exceptions

  10. HTML DOM

  11. CSSOM

  12. Events

  13. Drag and Drop

  14. opt Touch Events

  15. Misc

  16. Project: Analog Clock

Exercise: Right-Click Menu

Exercise 58 Average

Prerequisites for the exercise

  1. JavaScript Mouse Events
  2. All previous chapters

Objective

Implement a custom right-click menu on a web page.

Description

We all work with context menus often while panning around a webpage. They are sometimes also referred to as right-click menus, owing to the fact that they show up when the mouse is right-clicked.

Shown below is an example of a right-click menu in Chrome:

The native context menu on Google Chrome.
The native context menu on Google Chrome.

If you carefully notice it on most of the browsers, the context menu always tries to be aligned with the pointer.

More specfically, the top-left edge of the menu gets aligned with the pointer when there is enough room for the menu to be shown in this way; otherwise, it's bottom edge aligns with the pointer while its right edge aligns with the right edge of the viewport.

It simply depends upon the position of the pointer as to how the menu gets positioned.

In this exercise, you have to implement a custom context menu that gets displayed just like a native context menu, as detailed above.

Your context menu doesn't have to have a whole list of options to select — it should simply be a rectangular box with any dummy option inside it. Let's just say that the width of the menu must be 200px while its height must be 300px.

Note that your code must NOT use these hard-coded dimensions; instead, it must obtain them at runtime so that if we want to change the dimensions, we don't have to make any changes to the JavaScript code.

The sole purpose of this exercise is to get the positioning of the custom menu, and other behavior associated with it, right.

Here's how the final output should look:

Live Example

View Solution

New file

Inside the directory you created for this course on JavaScript, create a new folder called Exercise-58-Right-Click-Menu and put the .html solution files for this exercise within it.

Solution

Let's start by deciding on the HTML for our custom context menu, before we think about its CSS and then the JavaScript.

Which element can we use to denote a context menu?

Well, one obvious option is to go with the plain old <div>, but we won't be doing so. Instead, we'll go with a <menu> element.

As per the HTML standard:

The <menu> element is simply a semantic alternative to <ul> to express an unordered list of commands (a "toolbar").

This goes well with our idea of a custom menu with a list of commands to execute. Great!

Now, with the HTML element finalized, let's move over to defining the CSS for the menu.

The styles would be pretty simple, as shown below:

menu {
   position: absolute;
   z-index: 100;
   width: 200px;
   height: 300px;

   /* Override default styles of <menu> */
   list-style-type: none;
   margin: 0;

   /* Make it look more like a 'menu' */
   background-color: #fafafa;
   border: 1px solid #e1e1e1;
   padding: 15px;
   box-sizing: border-box; /* Prevent padding from affecting width and height */
}

The position: absolute declaration makes sure that we can easily move around the menu, while z-index: 100 makes sure that the menu appears on top of all other elements on the page. Furthermore, as instructed in the exercise's description, the width and height are defined to be 200px and 300px, respectively.

The next two styles override the default user-agent styling associated with <menu> elements (remember that they're similar to <ul>).

The rest of the styles, lines 12 - 15, are purely meant to make the menu look like a 'menu'. You can improvise however you want to in these styles, for instance, by applying a box-shadow or maybe giving the menu a dark background color, and so on.

With the element and its CSS done with, it's now time to move over to the logic of showing/hiding the menu.

We'll start by setting up a contextmenu event handler on window that listens to the right-click of the mouse and then showcases a <menu> element upon its occurrence.

Before that, however, let's make an important decision.

Should the <menu> element always be a part of the DOM and shown/hidden via class toggles, or should it actually be added/removed from the DOM?

Well, this is a preferential decision more than being a factual one. If you want to, you could use class toggles on <menu> to show/hide it. Similarly, if you want to, you could add/remove the element node from the DOM to accomplish the same behavior.

We'll go with the latter, i.e. add/remove the <menu> element from the the DOM, because we feel that there's no point of keeping the element when there really is no menu shown on the webpage.

With this crucial decision made, let's get back to our contextmenu event handler's definition:

window.addEventListener('contextmenu', function(e) {
   e.preventDefault();
});

A call to preventDefault() is necessary so to prevent the native context menu from showing up as we right-click on the webpage.

First off, we have to create the <menu> element node and then append it to <body>. Also we have to put some content inside the <menu>, which we'll craft hypothetically:

window.addEventListener('contextmenu', function(e) {
   e.preventDefault();

   var menuElement = document.createElement('menu');
   menuElement.innerHTML = 'A dummy option';
   document.body.appendChild(menuElement);
});

Next up, we have to figure out the coordinates of the mouse pointer, and then determine the position of the contextmenu, correspondingly.

This first requires us to compute the dimensions of the menu. Once that is done, we have to add both of them to the x and y coordinates of the mouse pointer. The resulting values then ought to be compared with the dimensions of the viewport.

If the x coordinate of the pointer added to the width of the menu exceeds the viewport's width, the menu's right edge has to align with the right edge of the viewport, as happens natively, speaking of the horizontal positioning of the menu.

Similarly, if the y coordinate of the pointer added to the height of the menu exceeds the viewport's height, the menu's bottom edge has to align with the mouse pointer (NOT with the bottom edge of the viewport), speaking of the vertical positioning of the menu.

Altogether we get to the following code:

window.addEventListener('contextmenu', function(e) {
   e.preventDefault();

   var menuElement = document.createElement('menu');
   menuElement.innerHTML = 'A dummy option';
   document.body.appendChild(menuElement);

   // Determining the positioning of the <menu>.
   var menuWidth = menuElement.offsetWidth;
   var menuHeight = menuElement.offsetHeight;
   var clientX = e.clientX;
   var clientY = e.clientY;

   var left = clientX + 1;
   if (left + menuWidth > innerWidth) {
      left = innerWidth - menuWidth;
   }

   var top = clientY + 1;
   if (top + menuHeight > innerHeight) {
      top = innerHeight - menuHeight;
   }

   menuElement.style.left = `${left}px`;
   menuElement.style.top = `${top}px`;
});

The positioning of the <menu> element is governed by the left and top style properties. The value of each of these properties is determined separately, as can be seen in the code above.

Let's now try running the example.

Live Example

As we right-click anywhere on the webpage, the menu shows up correctly. So far, we're doing good.

Now, we have to code the logic to hide the menu when a click is made anywhere on the document, outside the menu.

Precisely speaking, we have to track a mousedown event, not an actual click. Fortunately, this is really simple. We saw a similar example in the section Application of stopPropagation() in the chapter JavaScript Events — Propagation, where we used two event handlers, one on the dropdown element and one on window.

The dropdown's handler called stopPropagation() in order to prevent the mousedown event from bubbling up to the window, whereas the handler of window served to hide the dropdown.

We'll now implement a similar logic for our custom menu.

Consider the following code at the very end of our contextmenu handler:

window.addEventListener('contextmenu', function(e) {
   ...

   window.addEventListener('mousedown', windowMouseDownHandler);
   menuElement.addEventListener('mousedown', mouseDownHandler);
});

We register two mousedown handlers: one on menuElement and another one on window.

The handler for window, windowMouseDownHandler(), as shown below,

function windowMouseDownHandler(e) {
   menuElement.parentNode.removeChild(menuElement);
}

executes when mousedown occurs anywhere outside the menu, and serves to hide this menu element.

Because windowMouseDownHandler() wants access to the menuElement variable, we ought to transform it from being a local variable of the contextmenu handler to being a global variable. This is accomplished below:

var menuElement; window.addEventListener('contextmenu', function(e) { e.preventDefault();
menuElement = document.createElement('menu'); ... window.addEventListener('mousedown', windowMouseDownHandler); menuElement.addEventListener('mousedown', mouseDownHandler); });

The handler for menuElement, mouseDownHandler(), on the other hand, as shown below,

function mouseDownHandler(e) {
   e.stopPropagation();
   e.preventDefault();
}

calls stopPropagation() in order to prevent the event from reaching window, thus keeping the menu intact when mousedown occurs anywhere inside the <menu>.

The preventDefault() invocation above is simply meant to prevent the selection of content inside the menu as we drag and move the mouse pointer inside it.

Before we try out the code, if you think about it, there's absolutely no point of retaining the mousedown handler on window, i.e. windowMouseDownHandler(), once our menu is hidden (by virtue of its own execution) — we can, and ideally should, remove it.

That's what we do below:

function windowMouseDownHandler(e) {
   menuElement.parentNode.removeChild(menuElement);
window.removeEventListener('mosuedown', windowMouseDownHandler); }

Now, let's try the code that we've written so far:

Live Example

A right-click shows the menu; a mousedown inside it does nothing; a mousedown outside it hides it.

This is amazing! We're heading in the right direction.

Now, at this point, we might feel we're done, but that's not the case. There are a couple more tests to do before we could make the final judgement that our custom menu replicates the behavior of the browser's native menu.

So what are these tests?

Well, once the menu is shown, let's try right-clicking anywhere outside it. Ideally we want this right-click event (as we just press the mouse) to hide the menu and then show it as soon as we release the right mouse button.

As we test this functionality in the link above, it works absolutely fine. This gets one checkbox ticked.

Over to the next checkbox.

Let's test following behavior now: once the menu is shown, initiate a right-click inside the menu and then take the pointer outside the menu before releasing the mouse button. Ideally, nothing should happen in this case, as the mousedown event originated inside the menu itself.

As before, let's open up the link and start testing:

Live Example

Uh oh! There's some problem in this functionality. As we initiate a right-click inside the menu and then release it when the pointer it taken outside the menu, a new menu gets displayed. This is erroneous behavior.

Now before we could rectify it, we obviously need to understand its cause.

What do you think, why is a new menu shown?

When a right-click originates inside the menu, since the press of the mouse's right button also constitutes a mousedown event, our mouseDownHandler() function executes, preventing the event from propagating to window, where the menu is removed from the DOM. Thereafter, as the mouse button is released, the contextmenu event occurs, ultimately creating a new menu.

And we have the cause right in front of us — the execution of the contextmenu handler is the culprit. We ought to prevent it from executing.

Let's see how to do this.

When the mousedown event gets dispatched on <menu>, we don't want any subsequent contextmenu from occurring on window. Right? Likewise, one thought that comes to the mind is the remove the contextmenu handler from window upon the occurrence of mousedown on menuElement. In this way, the right-click-release action would still fire contextmenu, but there will be no handler to be invoked.

But keep in mind that this contextmenu handler needs to be registered again at some point. The question is: at what point?

And this requires a good amount of thinking.

At what point should we re-register the removed contextmenu handler on window?

After giving it a lot of thought, we come to the following options:

  1. Re-register the contextmenu handler on the mouseup event on window.
  2. Re-register the contextmenu handler on the mousedown event on window.

The former is pointless because if we reinstate the contextmenu handler upon window's mouseup event, since contextmenu occurs after mouseup, the whole right-click action (originating inside the <menu>) would've ended up at contextmenu, which is contrary to our desired behavior. So a complete NO to this first option.

The latter is a variable approach, as the contextmenu handler's reinstatement happens on a subsequent mousedown on window. Hence, a right-click initiated inside <menu> won't end up at a contextmenu event.

Just as we want.

Let's now refactor our code to accomplish all these ideas.

First off, because we need to be able to remove the contextmenu handler, we have to create a named function for it, as done below in the form of windowContextMenuHandler():

function windowContextMenuHandler(e) {
   e.preventDefault();
   ...
}
               
window.addEventListener('contextmenu', windowContextMenuHandler);

Just cut-and-paste the previous anonymous handler function into the global environment and call it windowContextMenuHandler.

Moving on, the next thing to do is to remove this windowContextMenuHandler() handler from window when mousedown occurs inside <menu>. This simply means to go inside the mouseDownHandler() function and make the desired addition:

function mouseDownHandler(e) {
   e.stopPropagation();
   e.preventDefault();

   // When mousedown occurs inside the <menu>, we don't need any
   // subsequent contextmenu from occurring on window.
window.removeEventListener('contextmenu', windowContextMenuHandler); }

The last thing left to do is to reinstate windowContextMenuHandler() on window. This happens inside the mousedown handler set up on window, i.e. windowMouseDownHandler(), as shown below:

function windowMouseDownHandler(e) {
   menuElement.parentNode.removeChild(menuElement);

   // When mousedown occurs outside the <menu>, re-register the
   // contextmenu handler.
window.addEventListener('contextmenu', windowContextMenuHandler); }

addEventListener() saves us!

At this stage, you might be thinking that if a mousedown never occured inside <menu>, but instead happened always outside it, the whole code above would end up registering a new contextmenu handler on window.

Well, this is right, but thanks to the way addEventListener() works, it won't be a problem.

Essentially, addEventListener() first checks the provided handler function in the list of all event handlers registered for the underlying element node, and if it already exists in the list, it doesn't add it again.

For us, this means that if the contextmenu handler isn't removed from window, and if windowMouseDownHandler() executes, we won't be getting two contextmenu handlers registered; there will always be just one handler and that'll be windowContextMenuHandler().

Isn't this amazing?

Now, let's test the previous functionality again:

Live Example

This time, we don't indeed see a new menu, but we do see the native menu appear. This should NOT be so.

How to solve this?

Well, since the exercise requires us to completely override the default context menu from appearing on the webpage, we can do a clever trick here (which is absolutely practical). That is, to set up a separate contextmenu handler on window that only calls preventDefault(). Our contextmenu logic should be separate from this handler.

Moreover, this one-liner handler never gets cleared up.

So, even when our windowContextMenuHandler() handler is removed from window, the right-click action (originating inside the <menu>) doesn't end up showing the browser's native menu, thanks to this disparate handler, preventing the native menu from showing up in any case.

Let's define this handler now:

window.addEventListener('contextmenu', function(e) {
// This handler is only meant to call preventDefault().
e.preventDefault();
}); window.addEventListener('contextmenu', windowContextMenuHandler);

Also we have to refactor our existing contextmenu handler, windowContextMenuHandler(), by removing the call to preventDefault() from it:

function windowContextMenuHandler(e) {
   e.preventDefault();

   menuElement = document.createElement('menu');
   ...
}

And now, let's retest the code:

Live Example

Voila! It works flawlessly.

This completes the exercise.