David Walsh Blog

MooTools ContextMenu Plugin

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:

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

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:

Have suggestions for a version 2? Share them!

ContextMenu is inspired by jQuery Context Menu Plugin.