MooTools ContextMenu Plugin

Written by David Walsh on January 28, 2009 · 60 Comments
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.

Comments

  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 concider 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. 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. 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 March 4, 2009

    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 March 4, 2009

    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. 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. 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 March 17, 2009

    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. 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. Acutally, I figured it out after reading over your code again. Thx :)

  25. 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 August 12, 2009

    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 August 12, 2009

    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 August 12, 2009

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

  31. Maximiliano August 13, 2009

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

  32. Maximiliano August 13, 2009

    Sorry, i figured out, i can use this

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

  33. jpodnegara November 10, 2009

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

  34. 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. 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. ok, answering myself ;-)

    trigger: ‘click’,

  39. 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. 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. Just figured out how to do that myself.

    Imagine our HTML document has the following two spans:

    Click me
    Click me
    ….

    in your initilaising javascript provide the target in the following format:

    targets: ‘span.mif-tree-name’, //menu only available on spans of classs mif-tree-name

    then, modify your menu.js so that the “startListener” function is as follows:

    //get things started
    startListener: function() {
    /* all elements */
    this.targets.each(function(el) {
    // => Array of links with given class. Provide target in the form: ‘a.external’, where ‘external’ is a class name
    matchingTargets = $$(el);
    /* show the menu */
    matchingTargets.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));
    },

    All the magic is done in these few lines:

    /* all elements */
    this.targets.each(function(el) {
    // => Array of links with given class. Provide target in the form: ‘a.external’, where ‘external’ is a class name
    matchingTargets = $$(el);
    /* show the menu */
    matchingTargets.addEvent(this.options.trigger,function(e) {

    Enjoy!

  43. 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

  44. David Hinckle June 10, 2010

    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!

  45. 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.

  46. Niklas Lehto July 3, 2010

    Brilliant! Have implemented this in a project I’m working on. I’ve added some functionality to decide whether to position the menu above or below and to the left or the right of the cursor, depending on the size of the menu and the viewport. Hope someone finds it useful.

    //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'
    });

  47. Niklas Lehto July 3, 2010

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

  48. Niklas Lehto July 5, 2010

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

  49. fuction 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);

    }

  50. 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();

  51. Greg Clout February 23, 2011

    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.

  52. Wassilios Meletiadis July 27, 2011

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

  53. Wassilios Meletiadis July 27, 2011

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

  54. Wassilios Meletiadis June 13, 2012

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

  55. Biswajit June 21, 2012

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

  56. 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/

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

  58. Simon McGregor January 31, 2013

    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.

Be Heard

Tip: Wrap your code in <pre> tags or link to a GitHub Gist!

Use Code Editor
Older
Implementing jQuery-Like Event Syntax in MooTools
Newer
Animated AJAX Record Deletion Using MooTools