One of the things I've been missing in the Ajax Control Toolkit, is a context menu extender - so I figured I'd write one myself :) Turns out it was much easier than I anticipated, and after about an hour or so I had something that works fairly well. Basically, it allows you to do something like this:
<asp:Panel
ID="_context"
runat="server"
Style="background-color:#cecece; width:250px; height:250px">
Context area
</asp:Panel>
<asp:Panel
ID="_menu"
runat="server"
Style="border:solid 1px black; background-color:White; padding:4px;">
My Context menu!
</asp:Panel>
<iridescence:ContextMenuExtender
ID="_cmExt"
runat="server"
TargetControlID="_context"
ContextMenuControlID="_menu" />
The extender will then make the TargetControl float wherever the mouse is located when you right-click anywhere inside the configured ContextMenuControl.
You can get the source code at the bottom of this post - below I'll just go through the javascript code that makes it work. As I said before when talking about control extenders, I will assume that you know how to write them - if you don't, check out this tutorial for a better introduction than one I could include here :)
To hook things up, we override the initialize function:
initialize : function()
{
Iridescence.Ajax.ContextMenuBehavior.callBaseMethod(this, 'initialize');
this._contextElement = this.get_element();
this._menuElement = $get(this._contextMenuControlID);
// style the context menu
this._menuElement.style.display = 'none';
this._menuElement.style.position = 'absolute';
// attach event handlers
this._onMouseDownHandler = Function.createDelegate(this, this._onMouseDown);
this._onDocumentContextMenuHandler = Function.createDelegate(this, this._onDocumentContextMenu);
this._onDocumentClickHandler = Function.createDelegate(this, this._onDocumentClick);
$addHandler(this._contextElement, 'mousedown', this._onMouseDownHandler);
$addHandler(document, 'contextmenu', this._onDocumentContextMenuHandler);
$addHandler(document, 'click', this._onDocumentClickHandler);
}
Here we simply get references to the Target and ContextMenuControls, and then add handlers for the events that we need.
First of all, we need to capture the mousedown event of the context area, so that we may show the context menu when the user right clicks inside it. This is handled by the _onMouseDown method:
_onMouseDown : function(e)
{
if (e.button == 2)
{
// calculate current mouse position
var scrollTop = document.body.scrollTop ? document.body.scrollTop : document.documentElement.scrollTop;
var scrollLeft = document.body.scrollLeft ? document.body.scrollLeft : document.documentElement.scrollLeft;
// and move context menu there
this.__menuElement.style.left = e.clientX + scrollLeft + 'px';
this.__menuElement.style.top = e.clientY + scrollTop + 'px';
this.__menuElement.style.display = '';
// set flags
this._menuVisible = true;
this._menuJustShown = true;
}
}
This method checks whether the right mouse button was clicked, and if so we need to figure out the current mouse position (offset by the scroll position), and position the context menu element accordingly, before showing it.
Normally, a right click would cause the browsers context menu to be displayed; we dont want that, as it would hide our custom context menu. Thus we've also hooked up to the contextmenu event of the document element, which is handled by the _onDocumentContextMenu method:
_onDocumentContextMenu : function(e)
{
if(this._menuJustShown)
{
// when our custom context menu is showing, we want to disable the browser context menu
e.preventDefault();
this._menuJustShown = false;
}
else if(this._menuVisible)
{
// user right-clicks anywhere while our custom context menu is visible; hide it
this._hideMenu();
}
}
Here, if our custom context menu was just shown, we prevent the browsers context menu from displaying by calling the preventDefault() method on the event arguments object.
The last functionality we need, is to be able to hide the context menu when the user clicks anwhere outside it after it has been shown. Above, we've solved that for when the user right-clicks - we also need to include a left-click solution. We do this by handling the click event of the document element, which is handled by the _onDocumentClick method:
_onDocumentClick : function(e)
{
if(this._menuVisible && e.button != 2)
{
// user left-clicked anywhere while custom context menu is visible; hide it
this._hideMenu();
}
}
The _hideMenu() function that both these calls is very simple - it just hides the context menu element and sets the _menuVisible flag accordingly:
_hideMenu : function()
{
this._menuElement.style.display = 'none';
this._menuVisible = false;
}
And thats it! The only thing left is to clean up after us, which we do by overriding the dispose method:
dispose : function()
{
// clean up
$removeHandler(this._contextElement, 'mousedown', this._onMouseDownHandler);
$removeHandler(document, 'contextmenu', this._onDocumentContextMenuHandler);
$removeHandler(document, 'click', this._onDocumentClickHandler);
Iridescence.Ajax.ContextMenuBehavior.callBaseMethod(this, 'dispose');
}
Now this is a fairly simplistic context menu extender - one might want to add asynchronous loading and maybe animation support at some point - but still, it's quite impressive how simple the ASP.NET Ajax API makes it to write powerful, reusable components with very little code. Gotta love it :)
Download the complete source code here. (Updated 1. December 2007 - fixed a few bugs in the script).
Update 7. December 2007 - Be sure to check out this post, which shows how to add animation support to the ContextMenuExtender.