MooTools ContextMenu Plugin

By  on  
dwContext: MooTools Context Menu

ContextMenu is a highly customizable, compact context menu script written with CSS, XHTML, and the MooTools JavaScript framework. ContextMenu allows you to offer stylish, functional context menus on your website.

The XHTML Menu

<ul id="contextmenu">
	<li><a href="#edit" class="edit">Edit</a></li>
	<li class="separator"><a href="#cut" class="cut">Cut</a></li>
	<li><a href="#copy" class="copy">Copy</a></li>
	<li><a href="#paste" class="paste">Paste</a></li>
	<li><a href="#delete" class="delete">Delete</a></li>
	<li class="separator"><a href="#quit" class="quit">Quit</a></li>
</ul>

Use a list of menu items with one link per item. The href attribute is especially important as it must be named the same as the menu's action, which you'll see below.

The Sample CSS

/* context menu specific */
#contextmenu	{ border:1px solid #999; padding:0; background:#eee; width:200px; list-style-type:none; display:none; }
#contextmenu .separator	{ border-top:1px solid #999; }
#contextmenu li	{ margin:0; padding:0; }
#contextmenu li a { display:block; padding:5px 10px 5px 35px; width:155px; font-size:12px; text-decoration:none; font-family:tahoma,arial,sans-serif; color:#000; background-position:8px 8px; background-repeat:no-repeat; }
#contextmenu li a:hover	{ background-color:#ddd; }
#contextmenu li a.disabled { color:#ccc; font-style:italic; }
#contextmenu li a.disabled:hover { background-color:#eee; }

/* context menu items */
#contextmenu li a.edit	{ background-image:url(edit.png); }
#contextmenu li a.cut	{ background-image:url(cut.png); }
#contextmenu li a.copy	{ background-image:url(copy.png); }
#contextmenu li a.paste	{ background-image:url(paste.png); }
#contextmenu li a.delete	{ background-image:url(delete.png); }
#contextmenu li a.quit	{ background-image:url(quit.png); }

Make the CSS look however you'd like. For the purposes of IE6, however, you'll want to set the link widths. Also note that the menu should be initialized as "display:none".

The MooTools JavaScript

var ContextMenu = new Class({

	//implements
	Implements: [Options,Events],

	//options
	options: {
		actions: {},
		menu: 'contextmenu',
		stopEvent: true,
		targets: 'body',
		trigger: 'contextmenu',
		offsets: { x:0, y:0 },
		onShow: $empty,
		onHide: $empty,
		onClick: $empty,
		fadeSpeed: 200
	},
	
	//initialization
	initialize: function(options) {
		//set options
		this.setOptions(options)
		
		//option diffs menu
		this.menu = $(this.options.menu);
		this.targets = $$(this.options.targets);
		
		//fx
		this.fx = new Fx.Tween(this.menu, { property: 'opacity', duration:this.options.fadeSpeed });
		
		//hide and begin the listener
		this.hide().startListener();
		
		//hide the menu
		this.menu.setStyles({ 'position':'absolute','top':'-900000px', 'display':'block' });
	},
	
	//get things started
	startListener: function() {
		/* all elements */
		this.targets.each(function(el) {
			/* show the menu */
			el.addEvent(this.options.trigger,function(e) {
				//enabled?
				if(!this.options.disabled) {
					//prevent default, if told to
					if(this.options.stopEvent) { e.stop(); }
					//record this as the trigger
					this.options.element = $(el);
					//position the menu
					this.menu.setStyles({
						top: (e.page.y + this.options.offsets.y),
						left: (e.page.x + this.options.offsets.x),
						position: 'absolute',
						'z-index': '2000'
					});
					//show the menu
					this.show();
				}
			}.bind(this));
		},this);
		
		/* menu items */
		this.menu.getElements('a').each(function(item) {
			item.addEvent('click',function(e) {
				if(!item.hasClass('disabled')) {
					this.execute(item.get('href').split('#')[1],$(this.options.element));
					this.fireEvent('click',[item,e]);
				}
			}.bind(this));
		},this);
		
		//hide on body click
		$(document.body).addEvent('click', function() {
			this.hide();
		}.bind(this));
	},
	
	//show menu
	show: function(trigger) {
		//this.menu.fade('in');
		this.fx.start(1);
		this.fireEvent('show');
		this.shown = true;
		return this;
	},
	
	//hide the menu
	hide: function(trigger) {
		if(this.shown)
		{
			this.fx.start(0);
			//this.menu.fade('out');
			this.fireEvent('hide');
			this.shown = false;
		}
		return this;
	},
	
	//disable an item
	disableItem: function(item) {
		this.menu.getElements('a[href$=' + item + ']').addClass('disabled');
		return this;
	},
	
	//enable an item
	enableItem: function(item) {
		this.menu.getElements('a[href$=' + item + ']').removeClass('disabled');
		return this;
	},
	
	//diable the entire menu
	disable: function() {
		this.options.disabled = true;
		return this;
	},
	
	//enable the entire menu
	enable: function() {
		this.options.disabled = false;
		return this;
	},
	
	//execute an action
	execute: function(action,element) {
		if(this.options.actions[action]) {
			this.options.actions[action](element,this);
		}
		return this;
	}
	
});

The ContextMenu plugin offers numerous options:

  • actions: a collection of actions (functions) to be executed when a corresponding menu item is clicked
  • menu: the ID of the element that represents the menu XHTML
  • stopEvent: do you want the element's default action to be stopped when the menu is triggered to display? (defaults to true)
  • targets: element(s) that should show the menu when triggered (defaults to the document body)
  • trigger: event that triggers the menu to display (defaults to "contextmenu", or right-click)
  • offsets: an {x,y} object with corresponding x and y offsets (x and y both default to 0)
  • onShow: a function to execute when the menu is shown
  • onHide: a function to execute when the menu is hidden
  • onClick: a function to execute when a menu item is clicked

Beyond these initial options, the ContextMenu class also provide some useful methods:

  • disable: disables the context menu
  • enable: enables the context menu
  • disableItem: disables a given menu item
  • enableItem: enables a given menu item
The Sample Usage
window.addEvent('domready', function() {

	//create a context menu
	var context = new ContextMenu({
		targets: 'a', //menu only available on links
		menu: 'contextmenu',
		actions: {
			copy: function(element,ref) { //copy action changes the element's color to green and disables the menu
				element.setStyle('color','#090');
				ref.disable();
			}
		},
		offsets: { x:2, y:2 }
	});
	
	//sample usages of the enable/disable functionality
	$('enable').addEvent('click',function(e) { e.stop(); context.enable(); });
	$('disable').addEvent('click',function(e) { e.stop(); context.disable(); });
	$('enable-copy').addEvent('click',function(e) { e.stop(); context.enableItem('copy'); });
	$('disable-copy').addEvent('click',function(e) { e.stop(); context.disableItem('copy'); });
	
});

The most dynamic part of the ContextMenu instance is the actions option, where you define what action should be taken per menu item. The action is passed the element clicked on and the reference to the context menu. My above example defines the copy action. When you click the "Copy" context menu item, I turn the text color green and disable the context menu. You may define one action per menu item.

This is version 1 of ContextMenu. I'd like to implement a few more features in the future, including:

  • Multi-level menus
  • A core set of actions with corresponding functionality.
  • addItem and removeItem methods

Have suggestions for a version 2? Share them!

ContextMenu is inspired by jQuery Context Menu Plugin.

Recent Features

Incredible Demos

  • By
    Submit Button Enabling

    "Enabling" you ask? Yes. We all know how to disable the submit upon form submission and the reasons for doing so, but what about re-enabling the submit button after an allotted amount of time. After all, what if the user presses the "stop"...

  • By
    JavaScript Copy to Clipboard

    "Copy to clipboard" functionality is something we all use dozens of times daily but the client side API around it has always been lacking; some older APIs and browser implementations required a scary "are you sure?"-style dialog before the content would be copied to clipboard -- not great for...

Discussion

  1. David,

    Great job. I love how context menus improve usability specially in admin UIs.

  2. Nice plugin! A little note on which license you’re releasing this under would be nice :-) I will consider using this one on some of my admin panels, if the license is right, hehe :-)

    There’s a couple of things I’d personally alter, though most of these are simply code style differences.
    I like to keep the indentation to a minimum, and I also like to take advantage of the way mootools handles groups of elements.

    For example, instead of doing this.targets.each and then running a item.addEvent on that, you could simply run addEvent on this.targets instead. The only difference here would be that you’d refer to this.target instead of item, or set item to this.target. Also, instead of doing if(!this.options.disabled) { indented code below }, you could check it the other way around, and return.. so it becomes if(this.options.disabled) return;

    All of these things are, as I said, merely code style, so these are just tips if you want to cut down on the indenting and possibly save a few lines of code. I posted the altered code on your pastebin if you are interested.

  3. @Rexxars: Thank you for the input and I’ll look at the licenses. Hadn’t thought about it. Like everything else on this site, use it however you’d like!

  4. Ryan

    Wow … definitely using this on a few of my sites.

  5. Hi
    This is not working in opera… because of opera of course.
    As I remember there is a setting in opera to allow or not right click, but most of the users will have the standard settings.
    So i think you can detect opera and add something like ctrl+click to launch the menu.

  6. Wow, I’ve been waiting for this for so long!
    Unfortunately it doesn’t work on Opera because of how annoying opera is, I am sure you’ll find a solution for this sometime soon, or like rborn suggested! Nonetheless, its a great class!
    Thanks :)

  7. @rborn: The beauty is that you can choose the trigger. You could make it double-click if you need Opera users to be able to use the menu.

  8. Excellent, David! I know a lot of places where something like this could be used.

  9. Is it possible to have the menu close when you left click outside of the region?

  10. Updated. Thanks Mark!

  11. It looks fantastic. Looking forward an opportunity to use in one of my projects. Some feedback tho:
    – I’m getting errors when running in IE (“Object doesn’t support this property or method” kind of error on lines 144 and 2705);
    – Displays fine in FF and Safari. I don’t use Opera in my desktop;
    – It didn’t copy the selected text in any browser(Safari and FF) I tested.

    keep up the good work :)

  12. @Thiago: It wasn’t supposed to copy text to your clipboard — it’s supposed to turn text green and disable the menu. Also, I’ve just cured the IE issue.

  13. bioule

    Hi, really nice work !
    I used it in a complex interface. It needs to be initialized several times in it’s lifetime. So, when I call for the second time the constructor, the menu will execute two times the expected function…
    So, I just removed the events just before add them
    At line 64 of ContextMenu Class, I just added this code:

    /* menu items */ 
    this.menu.getElements('a').each(function(item){
        item.removeEvents();
    });
    

    Thank you again for your class :)

  14. David Hinckle

    Nice script David. I can’t figure out how to keep the script from appending the url when clicking on a menu item. Is there a way??? Thanks in advance.

  15. @David Hinckle: What do you mean by this? I’m not following.

  16. David Hinckle

    Sorry, I’m probably being too anal about this to begin with. Using your demo as an example, when one clicks on the edit link, “#edit” is appended to the end of the url. I’d like to do away with this. Thanks again. DH

    • I suggest using the “name” attribute instead of the “href” on the link to avoid the “#action” being addded to the url. Easy fix, as follows:

      In your HTML :

      	Edit
      	Cut
      	Copy
      	Paste
      	Delete
      	Quit
      

      then in line 68 0f the script:

      /* menu items */
      this.menu.getElements('a').each(function(item) {
      	item.addEvent('click',function(e) {
      		if(!item.hasClass('disabled')) {
      			this.execute(item.get('name'),$(this.options.element));
      			this.fireEvent('click',[item,e]);
      		}
      	}.bind(this));
      },this);
      

      Enjoy!

  17. scott

    Hey David…I’m having the same problem as @David Hinckle. My script keeps appending “#edit” or whatever the “click” event is to the end of my URL string. Normally this wouldn’t be a problem, but it messes up when I throw .htaccess into the mix.

    Example:

    Say I’ve got an url: http://www.whatever.com/services/ <== using .htaccess for clean url

    When the “click” event is added: http://www.whatever.com/services/#edit <== appended to url after “click” event…page won’t load.

    I hope I’ve explained this well enough…and suggestions?

    Thanks…great site btw.

  18. scott

    Nevermind….I figured it out. I added e.stop(); to the “click” event function.

  19. Not working in Opera.

  20. @Ahmed Alfy: Use a different “trigger”, like double click.

  21. Jose Gonzalez

    Wow David, your work is impressive like always!!

    i’ll try it ;D!

  22. It’s not working perfectly in safari on OSX.

    The menu will show, but you can’t use it as it should be. It’s ignoring the mouseover. You have to click on the menu first and hold down the mouseclick, move the cursor away from the menu, release the mouseclick and now you can use it.

    Anyway.. nice menu! Keep up the good work!

  23. Ben

    Hey, just want to say I love this tool! I was actually wondering if it was somehow possible to pass the element to the onShow event, so I could change the menu based on the elements properties before the menu actually showed?

    Thanks!

  24. Ben

    Acutally, I figured it out after reading over your code again. Thx :)

  25. Kyle

    Hi David,

    I’m not sure if anyone else noticed or was bothered by this, but when you right-click to get the context menu, in everything but opera apparently, there’s the potential that it will highlight text. You can prevent that by adding this to the show() function:

    if(document.selection && document.selection.empty) {
        document.selection.empty();
    } else if(window.getSelection) {
        window.getSelection().removeAllRanges();
    }
    
  26. This is something I would like to see done in JQuery.

  27. Maximiliano

    Hi There, i wonder if there is any way to put this context menu to every row in a table, of couse each item has to have a different menu so i can get for example a data from the row.

  28. @Maximiliano: Yes, so set the “targets” to “#MyTable tr”

  29. Maximiliano

    Hi Again, the thing your mention it works, so im trying right now to gather one element from the Table or something , can you write something that could help me?

  30. Maximiliano

    I make it works with element.cells[1].innerText

  31. Maximiliano

    There is anyway to know the element using the onShow Event?

  32. Maximiliano

    Sorry, i figured out, i can use this

    onShow: function(){
      alert(this.options.element.cells[1].innerText);
    }
  33. jpodnegara

    great one David..!! thanks.. ^_^ but does it work on omnigrid ?

  34. bosko

    hello

    wery nice script david. i want to use on whole body (wherever its right clicked) not on only one DIV … its there possible to make that?

    thank you

  35. Just thought I’d let you know that I’ve added an adapted version of this to my PDF annotation Moodle module, so thanks for making this code available!

    (FYI the link to the Moodle module is above)

  36. Alfred

    Hi,

    sorry – looking at the code -but seems i’m stupid. Where you trigger right mouse click?

    How to bring up contextmenu with a left-mouse click ??

    Thanks

  37. @Alfred: It’s the “trigger” option. You’d want:

    trigger: ‘click’

  38. Alfred

    ok, answering myself ;-)

    trigger: ‘click’,

  39. Alfred

    LOL – Thanks – i see you answered same :-)

  40. Hi,

    I havent tried it out but I would like to know if this menu can be initialized many times on one page on different objects i.e. on right click, a different menu (with varying links on it) based on which objects is right clicked.

    Thanx

  41. Nestor

    Thanks for the plugin. That works magic. The only thing I would like to add is to being able to specify not only element type, but perhaps a class. For example, a page may have a myriad of links , but what if I only want to attach the menu to those belonging to a specific class, eg
    How can I do that?
    Thx again!

  42. Nestor

    Oh, dear, my HTML code got parsed…
    What I wanted to say was:

    imagine you have two spans: (I replaced “” with “@”)

    @span class=”mif-tree-name”@ Click me @/span@
    @span class=”some-class”@ Click me @/span@

    Then if you do as described above, only the first span will have the JS context menu attached; the second one will have browser-default context menu

    Regards

  43. David Hinckle

    Hi David,

    I’m attempting to use your contextMenu class yet again but have a question. If I rightclick on a tr (targets: ‘tbody tr’), how do I get the contextmenu to close when I mouseout of the same row? THANKS AGAIN!

  44. It’s very good.
    I like this.
    Thanks for share.
    And I wrote something to introduce this project for my readers.
    You can find the post about this in my website.
    If something is wrong,pls figure it out.thanks.

  45. Niklas Lehto

    Btw, it’s tested to work in recent PC versions of Firefox, Explorer, Chrome, Opera and Safari.

  46. Niklas Lehto

    One further revision. I found myself in need of adding and removing trigger items, so I added two methods providing this functionality, and broke another method up in two. Here are the changes (with my previous revisions included):

    	//get things started
    	startListener: function(){
    		//all elemnts
    		this.attachToTargets(this.targets);
    
    		//menu items
    		this.menu.getElements('a').each(function(item){
    			item.addEvent('click',function(e){
    				if(!item.hasClass('disabled')){
    					this.execute(item.get('href').split('#')[1],document.id(this.options.element));
    					this.fireEvent('click',[item,e]);
    				}
    			}.bind(this));
    		},this);
    
    		//hide on body click
    		document.id(document.body).addEvents({
    			'click': function(){
    				this.hide();
    			}.bind(this),
    			'contextmenu': function(){
    				this.hide();
    			}.bind(this)
    		});
    	},
    
    	// add trigger event to all targets
    	attachToTargets: function(targets){
    		targets.each(function(el){
    			//show the menu
    			el.addEvent(this.options.trigger,function(e){
    				//enabled?
    				if(!this.options.disabled){
    					//prevent default, if told to
    					if(this.options.stopEvent) { e.stop(); }
    					//record this as the trigger
    					this.options.element = document.id(el);
    					//calculate height of the menu
    					menuSize = this.menu.measure(function(){
    					    return this.getSize();
    					});
    					//decide where to position the menu
    					posY =	((e.page.y + this.options.offsets.y + menuSize.y) < window.getSize().y)
    							? e.page.y + this.options.offsets.y
    							: e.page.y - this.options.offsets.y - menuSize.y;
    					posX =	((e.page.x + this.options.offsets.x + menuSize.x) < window.getSize().x)
    							? e.page.x + this.options.offsets.x
    							: e.page.x - this.options.offsets.x - menuSize.x;
    					//position the menu
    					this.menu.setStyles({
    						top: posY,
    						left: posX,
    						position: 'absolute',
    						'z-index': '2000'
    					});
    					//show the menu
    					this.show();
    				}
    			}.bind(this));
    		},this);
    	},
    
    	//add item(s)
    	addItems: function(items){
    		this.attachToTargets($$(items));
    	},
    
    	//remove item(s)
    	removeItems: function(items){
    		$$(items).removeEvents(this.options.trigger);
    	},
    
  47. Ann
    function contextdemo() {
    alert("hi");
    var menu=contextMenu.createElement('menu');
    var itemNode=contextMenu.createElement('item');
    var itemtext=contextMenu.createTextNode("search");
    var itemattr=contextMenu.createAttribute('onactivate');
    itemtext.appendChild(itemattr);
    itemNode.appendChild(itemtext);
    menu.appendChild(itemNode);
    }
    
  48. Roman

    Hello folks

    The following code works fine for me to reinitialize the context menu:

    context.targets.removeEvents('contextmenu');
    context.targets = $$('.myNewTargets');
    context.menu.getElements('a').removeEvents('click');
    context.startListener();
  49. Greg Clout

    Fantastic component!
    The only trouble im having at present is that the click event is conflicting with the sortables class.
    After opening the menu I can no loger drag and drop in my sortable lists.

  50. Wassilios Meletiadis

    Very nice plugin!

    I needed the ability to define my own z-index for the contextmenu. Therefore I made the
    following changes:

    var ContextMenu = new Class({
        options: {
    	    actions: {},
    	    ...
    	    fadeSpeed: 200,                             // don't forget to add a comma
    	    // somewhere near line 29
    	    zIndex : 2000                               // set the default z-index
        },
        ...
    
        ...
        startListener: function() {
            this.targets.each(function(el) {
                ...
    
                this.menu.setStyles({
                    ...
                    position: 'absolute',               // don't forget to add a comma
                    // somewhere near line 67
                    'z-index': this.options.zIndex      // apply the z-index
                });
                ...
            }
        }
    });
    

    After doing these changes one can do this:

    new ContextMenu({
        actions : {
            ...
        },
        ...
        zIndex : 3000
    });
    
  51. Wassilios Meletiadis

    Damn! I’ve forgotten to use the code-Tags. :(
    Could you please fix my post for better readability. Thanks!

  52. Wassilios Meletiadis

    Like Ben and Maximiliano I needed to know the clicked element inside the show-method.
    Therefore I made the following changes:

    ...
    //show the menu
    this.show(this.options.element);
    ...

    ...
    show: function(clicked) {
    this.fx.start(1);
    this.fireEvent('show', clicked);
    ...
    }

  53. Biswajit

    I have found one small problem ,i.e we can right click again on context menu and if we right click at any place in the dom context menu does not get hide,
    Figured an easy fix,
    just add this after line 90,

    Cheerzzzzz :)


    $(document.body).addEvent('contextmenu', function(e) {
    this.hide();
    }.bind(this));

  54. Dean

    Fantastic script Thanks David.
    I have given the menu more depth for anyone who is in need of it.
    http://jsfiddle.net/DeanOutlaw/hHLCm/6/

  55. Saf1

    Hello,
    can someone help customzing my copy past version, copying a certain li from lul1l to lul2l

  56. Simon McGregor

    The demo page for this plugin:

    http://davidwalsh.name/demo/moo-context-menu.php

    doesn’t appear to work.

    1. The context menu pops up in the wrong screen location (significantly right of and below the click point),
    2. The “Copy” menu item does not appear in the proper place in the menu: it appears as a separate object (in a blue rounded rectangle) which overlaps with the menu.
    3. The icons for each of the other menu items are clipped at the bottom (this is a minor issue).

    The same behaviour occurs in four different browsers (Google Chrome Version 24.0.1312.56 m, Firefox 18.0.1, Opera 12.12, and IE 9.0.8112.16421) so it looks like the menu plugin is broken.

  57. Great plugin!

    Have you thought about putting this in MooTools Forge?

  58. Ha, never mind, I see that it’s there!

Wrap your code in <pre class="{language}"></pre> tags, link to a GitHub gist, JSFiddle fiddle, or CodePen pen to embed!