Tuesday, April 28, 2009

Javascript custom right click (context) menu

Today I will cover another aspect of creating web pages without using browsers' standard behavior and capabilities. I will show you how to make your own right mouse button menu and use it instead of this, that the browser draws by default. This allows you to adapt this menus according to you site's needs and can be very useful for some projects. To achieve that I use the oncontextmenu event trigger, which may not be familiar for some of you, cause it is rarely used. It catches the right mouse button clicks, just like the onclick event works for left mouse button.

This script will use my mouse position capture script to draw the menus at the proper coordinates.

The object constructor will accept three parameters:
  • id - HTML id of the menu)
  • menuItems - an array containing all the menu items which will be drawn
  • parent - optional parameter which defines the element to which the menu will be applied. If this is not set, document.body will be used.
The second parameter needs further explanation. The keys of the array menuItems will be used as the text, which will be displayed in the menu and the values of that array will be the functions that will be executed, when the corresponding menu item is clicked. Values can be either "function() {...}" code or a predefined function name. Maybe this seems a bit blurry, but read on and you will see how it's used.

Here is the object constructor:
function contextMenu(id, menuItems, parent)
{
 //First we need to make sure that we have the obligatory parameters present
 if(typeof id == 'undefined' || typeof menuItems == 'undefined')
  return;
 
 //Remember menu's id cause we will need it in the object's prototypes
 this.id = id;
 
 //If this menu already exists - call a prototype that destroys it, so it can be created again on it's new place.
 //This is executed when we already have the menu drawn on the page and the user has clicked on a new location
 if(document.getElementById(this.id))
  this.destroy();
 
 
 //Create the main menu item - an UL element. I use some styles to make it look pretty much like every browsers' menu, 
 //which will be more familiar to the user
 var menu = document.createElement('ul');
 menu.id = id;
 menu.style.listStyleType = 'none';
 menu.style.margin = 0;
 menu.style.padding = 0;
 menu.style.position = 'absolute';
 menu.style.left = xMousePos;
 menu.style.top = yMousePos;
 menu.style.border = '1px solid #000;';
 menu.style.background = '#eee';
 menu.onmouseover = function() { this.style.cursor = 'pointer'; }
 menu.oncontextmenu = function() { return false; }
 
 //We have the menu created, now we must append it's items.
 for(k in menuItems)
 {
  //Sometimes we need to divide the menus in sections. To do so, I'm using a keyword - 'separator'. 
  //If this keyword is used in any menu item, it will be drawn as a separating line (hr) between 
  //the elements above and beneath it...
  if(menuItems[k] == "separator")
  {
   var delim = document.createElement('hr');
   delim.style.height = '1px';
   delim.style.width = '90%';
   delim.style.margin = 0;
   delim.style.marginLeft = '5%';
   delim.style.padding = 0;
   
   menu.appendChild(delim);
  }
  else //...otherwise we use the text and code from the menuItems array
  {
   //if this is not a valid function - don't create the menu item
   if(typeof menuItems[k] != "function")
    continue;
   
   var menuItem = document.createElement('li');
   menuItem.style.textAlign = 'left';
   menuItem.style.margin = 0;
   menuItem.style.marginLeft = '5px';
   menuItem.style.marginRight = '5px';
   menuItem.style.padding = 0;
   menuItem.innerHTML = k;
   menuItem.onclick = menuItems[k];
   
   menu.appendChild(menuItem);
  }  
 }
 
 
 //Now we have the menu ready we need to append it to it's parent element. 
 //If the third parameter (parent) is omitted - we use the body of the page.
 //Note that the check 'if(typeof parent == "object")' will also fail if we have provided a parent, 
 //but it is not a valid HTML element, thus the document.body will be used instead.  
 if(typeof parent == "object")
  parent.appendChild(menu);
 else
  document.body.appendChild(menu);
 
 //The last thing we need to do is add an event listener to the parent. 
 //We add an onclick event, so when the user clicks outside the menu it will disappear.
 //WARNING! This code overwrites the parent's onclick event listener (if any)! This means 
 //that if you already have a listener defined, it will stop working! Be careful when using 
 //this menu on elements the already have event listeners and always test carefully when implementing!
 menu.parentNode.onclick = methodize(this.destroy, this);
}
This is just a simple menu and the object has just one prototype, which removes the menu from the page:
//The prototype that removes the menu from the page.
contextMenu.prototype.destroy = function()
{
 document.getElementById(this.id).parentNode.removeChild(document.getElementById(this.id));
}
I will extend this object in the future, allowing customization, submenus and other useful functionality. To use the code we need to append an event listener to the oncontextmenu event to each element where the menu will be displayed. We can create a custom function that defines our menu items and creates the menu:
//This is an example function that creates a custom right-click menu
function createContextMenu(id, parent)
{
 var menuItems = new Array();
 
 //You can define a new function using a "function() {...}" code as the value of the array.
 menuItems["Go Back"] = function() { history.go(-1) };
 //You can add a separator by defining a value "separator" for the array element.
 menuItems[0] = "separator";
 menuItems["Reload"] = function() { location.reload(); };
 //You can also use a predefined function - in this case reload(), which is defined in the code that fallows.
 menuItems["My Own Reload"] = reload;
 menuItems[1] = "separator";
 menuItems["Another Function"] = anotherFunction;
 
 new contextMenu(id, menuItems, parent);
 
 return false;
}

//An example custom function that is called when a menu item is clicked
function reload()
{
 location.reload();
}

//An example custom function that is called when a menu item is clicked
function anotherFunction()
{
 alert('Another Function Code...');
}
This function defines an array, which will be used to draw the menu. Some of the functions inside the menu are created with the code "function() {...}" and others are predefined functions, which are also shown in the code above. Note that the keyword 'separator' is a string, so you can still have a function named separator() and use it within the menu. In order for the menu to be working propery, all values of the array must be valid functions. If you misspell a function name, or set a value that is not a function - the menu item will not be displayed.

Finally, a sample HTML that shows you how to use the object:
<div style='width: 250px; height: 250px; border: 5px solid #333;' oncontextmenu="createContextMenu('myFirstContextMenu', this); return false;">&nbsp;</div>
With this code, a menu will appear at the mouse position when you right click on the div. If you click anywhere outside that div, the page will be working normally. WARNING! When you create a menu with this code it overwrites the parent's onclick event listner (if any)! This means that if you already have a listener defined, it will stop working, which can lead to strange page behavior or brake other scripts' usage! Be careful when using this menu on elements the already have event listeners and always test carefully when implementing!

This is pretty much the simplest menu example. In the future fallow ups to this post I will extend the object to be able to create a menu with sub menus, add the ability to set a custom style for the menu, etc. You can see a demo page here. You can use the script directly from this url.

P.S. I've received a bug report about this script - it isn't working on Internet Explorer! Unfortunately I had no Windows OS around me when I was creating it and haven't tested on IE. I will fix it soon and advice you to wait for the fix before using the script.

3 comments:

martin joseph said...

I copied your code and created an html page. But when i opened it in the browser (ie 8) an error comes up saying xMosPos and yMosPos are not defined. and i dont know what values to give it.Could you help on this topic.

Martin Joseph

Nikoloff said...

Please, read the PS! As I've written in the post, the script isn't working on IE (or maybe it is on IE8, but not on 6 and 7). Unfortunately I don't have any free time recently to fix it. I recommend you to wait a little more for me to take a better look at it and make it work cross-browser.

About the error you are getting - it seems that you didn't include one of my other scripts - http://enikoloff.info/scripts/mousePos.js which sets the values of xMousePos and yMousePos. If you add this script to your page you shouldn't get this error, but for now, I can't guarantee that you won't get any other errors...

Anonymous said...

Nikoloff,

Below is your context menu reworked using prototype which makes the script completely cross browser compatible.

Jerry Sparkman,


var contextMenu = Class.create({
id: null,
destroy: function() {
if (this.id && $(this.id))
$(this.id).remove();
},
createContextMenu: function(e, id, menuItems) {
if (this.id && $(this.id))
$(this.id).remove();

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;
}

var menuContainer = new Element('div', { 'id': this.id })
.setStyle(
{
position: 'absolute',
left: (posx + 2) + 'px',
top: (posy + 2) + 'px',
background: '#ccc',
zIndex: '99999'
});
var menu = new Element('ul')
.setStyle(
{
listStyleType: 'none',
margin: 0,
padding: '0 3px 0 3px',
left: '-2px',
top: '-2px',
border: 'solid 1px #ccc',
background: '#eee',
position: 'relative',
maxWidth: '150px'
});
$(menu).observe('mouseover', function() { this.setStyle({ cursor: 'pointer' }); });
$(menu).observe('contextmenu', function(e) { Event.stop(e); });
$(menuItems).each(function(item) {
if (item == "separator") {
var delim = new Element('hr').setStyle(
{
height: '1px',
margin: 0,
padding: 0
});
menu.insert(delim);
}
else {
if (!item.func)
return;

var menuItem = new Element('li').setStyle(
{
textAlign: 'left',
magin: 0,
padding: '3px 0 3px 0'
});
menuItem.update(item.text);
menuItem.observe('mouseover', function() { $(this).setStyle({ background: '#ccc' }); });
menuItem.observe('mouseout', function() { $(this).setStyle({ background: 'transparent' }); });
menuItem.observe('click', item.func);
menu.insert(menuItem);
}
});
menuContainer.insert(menu);
$(document.body).insert(menuContainer);
$(document.body).observe('click', this.destroy.bind(this));
window.onblur = this.destroy.bind(this);
$(menuContainer).setStyle({ display: 'block' });
},

initialize: function(id, event) {
this.id = id;

var menuItems = new Array();

menuItems.push({ text: "Go Back", func: function() { history.go(-1); } });
menuItems.push("separator");
menuItems.push({ text: "Sign In", func: function() { location = 'your sign in url'; } });
menuItems.push("separator");
menuItems.push({ text: "Another Function", func: function() { alert('Another function code...'); } });

this.createContextMenu(event, id, menuItems);
}
});

$(document.body).observe('contextmenu', function(e) {
Event.stop(e);
new contextMenu('HsnContextMenu', e);
});