Context Menu Using Raw JavaScript

Objectives

  1. Learn to create cross browser event handler in raw JavaScript.
  2. Learn to create cross browser DOMContentLoaded event handler.
  3. Making use of oncontextmenu event.
  4. Learn to apply Module pattern.
  5. Finally develop a reusable, easy to use, cross browser context menu object.

DemoSource Files

HTML

Let’s first take a look at the HTML required for the context menu.

<ul id="contextMenu">
    <li><a href="#">Copy</a></li>
    <li><a href="#">Edit</a></li>
    <li><a href="#">Delete</a></li>
</ul>

The context menu is nothing more than an unordered list. It can be other element like table as well. Let’s create a target element (in our case table) on which this context menu will work.

<table id="grid">
    <thead>
        <tr>
            <th>Manufacturer</th>
            <th>Phone Model</th>
        </tr>
    </thead>
 
    <tbody>
        <tr>
            <td id="col_1">Samsung</td>
            <td>Galaxy S II</td>
        </tr>
 
        <tr>
            <td>Apple</td>
            <td>iPhone 4</td>
        </tr>
 
        <tr>
            <td id="col_1">HTC</td>
            <td>HD7</td>
        </tr>
    </tbody>
</table>

Above table will be our target on which user can right click to display the #contextMenu written above. Finally a div will appear at the bottom of the page in which we will show the status of operation currently performed by user. This will also help us to understand the flow of code.

<div id="status">Messages goes here...</div>

CSS

Following are few lines of CSS to make the above markup prettier.

Table

The id of our table element is grid and all styles are applied on grid.

#grid th {
	background: #565656;
	color: #fff;
}
 
#grid th, #grid td {
	padding: 6px;
}
 
#grid tr:nth-child(2n) {
	background: #F7F7F7;
}

Context Menu

#contextMenu {
    background: #F9F9F9;
    box-shadow: 0 0 12px rgba( 0, 0, 0, .3 );
    border: 1px solid #ccc;
    display: none;
    position: absolute;
    top: 0; left: 0;
    list-style: none;
    margin: 0;
    padding: 0;
    min-width: 100px;
}
 
#contextMenu li {
    position: relative;
}
 
#contextMenu li:hover {
    background: #444;
}
 
#contextMenu a {
    color: #444;
    display: inline-block;
    padding: 8px;
    text-decoration: none;
    width: 85%;
}
 
#contextMenu li:hover a {
    color: #f9f9f9;
}
 
#contextMenu li:last-child a {
	border-bottom: none;
}

Following CSS properties must exist for #contextMenu for its proper working.

  1. First we are hiding the #contextMenu by setting its display to none.
  2. Set #contextMenu’s position to absolute.
  3. Set top and left properties to 0.

Mostly rest of the styles are for look and feel but above mentioned are mandatory. You can reuse styles for multiple context menus by converting the selectors #contextMenu to .contextMenu and apply contextMenu class to multiple elements.

Status Bar

#status {
    background: #565656;
    border-top: 1px solid #CECECE;
    box-shadow: 0 0 8px rgba(0,0,0,.2);
    color: #fff;
    font-size: 2em;
    padding: 10px;
    position: fixed;
    left: 0;
    bottom: 0;
    width: 100%;
}

Status Bar is placed at the bottom of the page with a fixed position.

Script

Cross Browser Event Handling

I used EventUtil object from book Professional JavaScript for Web Developers, 2nd Edition. This object contain methods to provide cross browser event handling. I modified this object a bit and removed some methods to make it simple. Also an additional method DOMReady is added in this object which adds the DOMContentLoaded event for all browsers.

/*
   EventUtil is cross browser event handling object
   source is taken from book "Pro JavaScript for Web Developers, 2nd Edition"
*/
 
var EventUtil = {
    //DOMReady is not taken from the book
    DOMReady: function( f ) {
        if( document.addEventListener ) {
            document.addEventListener( "DOMContentLoaded", f, false );
        } else {
            window.setTimeout( f, 0 );
        }
    },
 
    addHandler: function( element, type, handler ) {
        if( element.addEventListener ) {
            element.addEventListener( type, handler, false );
        } else if( element.attachEvent ) {
            element.attachEvent( 'on' + type, handler );
        } else {
            element[ 'on' + type ] = handler;
        }
    },
 
    getEventObj: function( e ) {
        return e ? e : window.event;
    },
 
    getEventTarget : function( e ) {
        return e.target ? e.target : e.srcElement;
    },
 
    preventDefault: function( e ) {
        e.preventDefault ? e.preventDefault() : e.returnValue = false;
    }
};

Following is the quick overview of EventUtil.

The first method is DOMReady which first checks for the availability of addEventListener. Almost all the modern browsers which support this method to add events also supports DOMContentLoaded event. For older browsers (especially IE8 and below), you can emulate DOMContentLoaded using setTimeout. The idea for this method was taken from jQuery.ready.

Next method is addHandler. In standards compliant browser, we can use addEventListener to add event handlers. But in IE 8 and below, we need to use attachEvent and some older browsers only support handlers using attributes (e.g. elementName.onclick). This is the flow of addHandler method.

Then comes the getEventObj. This method gets the event object in a cross browser way. In IE8 and below, event object is stored in window.event but in standard compliant browsers, this object is passed as a parameter to the event handler.

Similarly getEventTarget gets the target of the event which is e.target in standard browser and e.srcElement in IE8 and below.

Finally to prevent the default action of an element, we use e.preventDefault() but in IE you need to assign false to e.returnValue.

Context Menu

Let’s now focus on creating reusable, cross browser context menu using raw JavaScript. There’s an event called oncontextmenu which runs when user clicks the right mouse button. It has become part of standard in HTML5 due to the vast support of this event by browsers (even IE6 and 7 supports it). There are other ways as well to detect right click using event object but in this tutorial we will stick with oncontextmenu.

We will use a pattern similar to Module pattern to create this context menu. Now write a function which takes three parameters.

function ContextMenu( element, menu, contextHandler ) {
    var THIS = this
      , contextEnabled = true;
 
    THIS.clickTarget = null;
    THIS.target = document.getElementById( element );
    THIS.menu = document.getElementById( menu );
}

First parameter is target element (table#grid in our case). The second parameter menu is the context menu (ul#contextMenu in our case) which will open when user right clicks on the element. Finally an optional contextHandler which will run after context menu is opened. User/developer can pass null if don’t want to use it.

Note that the above function is actually the constructor and we will instantiate its object using new keyword. It’s good practice to start the name of constructor function with capital letter.

Few variables are defined within the function. First THIS variable is assigned “this” keyword and here this keyword actually points to ContextMenu function (if instantiated using new keyword). Second variable is contextEnabled which is boolean variable and is used to enable or disable context menu.

Note that both of these variables are defined using var keyword. This means both of them are private variables. JavaScript treats function as object so we can define private and public variables in a function. To define public variables, we can use this keyword. So the next three variables are public because they are defined using THIS variable which contains “this” keyword.

Let’s add context menu handler now.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
if( THIS.target ) {
    EventUtil.addHandler( THIS.target, 'contextmenu', function( e ) {
	  if( contextEnabled ) {
            e = EventUtil.getEventObj( e );
            e.target = EventUtil.getEventTarget( e );
 
		//display context menu where right click was pressed
		THIS.menu.style.top = e.clientY + "px";
		THIS.menu.style.left = e.clientX + "px";
		THIS.menu.style.display = "block";
 
		//assign current target to clickTarget
		THIS.clickTarget = e.target;
 
		//call contextHandler if it's a valid function
		if( typeof contextHandler == 'function' ) contextHandler( e );
 
		//stop browser to display its own context menu
		EventUtil.preventDefault( e );
        }
    });
}

LINE 1: We check the existence of public variable target. In other words only assign oncontextmenu event if this target exists.

LINE 2: We are using EventUtil.addHandler to assign an event handler to element (THIS.target). The event handler is an anonymous function passed as a third parameter to this method. This event handler will run when user right clicks on THIS.target and the body of this event handler will actually display context menu. Also note the parameter e passed to this handler is actually event object.

LINE 3: All the code in body is wrapped in an if condition which checks the variable contextEnabled. This variable is used to enable or disable context menu. Later in the code we will create a public function to change this variable’s value to true or false.

LINE 4, 5: We use getEventObj to get the proper value of e in cross browser manner. Similarly on line 5 we are getting the value of target from e object. Here target is the element on which user right clicked. Remember that this is different from THIS.target.

LINE 8: We are modifying the CSS value of top property of THIS.menu. THIS.menu actually contains the ul list which will appear when user will right click on THIS.target. CSS properties can be changed from JavaScript using elementObject.style.propertyName (where propertyName can be top, display, zIndex, etc). Now note that we are assigning e.clientY + “px” to top. e.clientY contains the position of mouse on y axis when right click was pressed. We are assigning the position of mouse to menu because we want to display menu where the user pressed right click.

LINE 9: Similary we are assigning the current x position of mouse to menu.

LINE 10: Now we want to display the menu after setting its top and left values. So we set its display property to “block”.

LINE 13: We assign the e.target element to public variable clickTarget so that user of this function can access the element on which user clicked right mouse button. Remember that e.target will be different from the element parameter passed to ContextMenu function. For example in our case we will pass “grid” to this function as our target element. When user clicks the right mouse button on any cell of this grid, e.target will contain that cell td or th because that’s where the user clicked the button. In short you can say that element parameter is used to restrict the area on which user can right click and display custom context menu and e.target in the oncontextmenu handler contains the exact element on which user right clicked.

LINE 16: The last parameter passed to ContextMenu function was an optional function which user can pass to execute its own code after displaying context menu. Here we check the data type of parameter contextHandler. If it’s a function then execute it by passing the event object.

LINE 19: Finally we prevent the default action of the event by calling preventDefault method in EventUtil. The default action of oncontextmenu event is to display the browser’s context menu and this method will prevent it to display.

We completed the code for displaying custom context menu. What if the user clicks on the menu item or clicks anywhere else on the screen? These are the actions when we must hide the custom context menu. So we write an onclick handler for HTML element (you can access it using document.documentElement) after the oncontextmenu handler with the body of if( THIS.target ).

EventUtil.addHandler( document.documentElement, 'click', function( e ) {
    if( contextEnabled ) {
        THIS.menu.style.display = 'none';
    }
});

There’s nothing complex here. All we are doing is hiding the context menu when user clicks anywhere on the screen. We attached click handler to HTML instead of body because it covers the whole page but the height of body element is limited to its contents.

Finally after the body of if( THIS.target ) and before the closing brace of ContextMenu function, we add a public method from where user can enable/disable context menu.

THIS.EnableContextMenu = function( status ) {
    contextEnabled = status == true ? true : false;
};

Note that the status variable is not assigned directly to contextEnabled because user can pass a value other than true or false. So a comparison is done to make sure the status is true or 1, if yes then assign true to contextEnabled. Otherwise assign false.

Here’s the complete code of ContextMenu function.

function ContextMenu( element, menu, contextHandler ) {
    var THIS = this
      , contextEnabled = true;
 
    THIS.clickTarget = null;
    THIS.target = document.getElementById( element );
    THIS.menu = document.getElementById( menu );
 
    if( THIS.target ) {
        EventUtil.addHandler( THIS.target, 'contextmenu', function( e ) {
            if( contextEnabled ) {
                e = EventUtil.getEventObj( e );
                e.target = EventUtil.getEventTarget( e );
 
                THIS.menu.style.top = e.clientY + "px";
                THIS.menu.style.left = e.clientX + "px";
                THIS.menu.style.display = "block";
 
                THIS.clickTarget = e.target;
 
                if( typeof contextHandler == 'function' ) contextHandler( e );
                EventUtil.preventDefault( e );
            }
        });
 
        EventUtil.addHandler( document.documentElement, 'click', function( e ) {
            if( contextEnabled ) {
                THIS.menu.style.display = 'none';
            }
        });
    }
 
    THIS.EnableContextMenu = function( status ) {
	    contextEnabled = status == true ? true : false;
    };
}

Using ContextMenu Script

ContextMenu function is easy to use. All you need to do is instantiate it using new keyword as shown below.

var status = document.getElementById( 'status' );
 
EventUtil.DOMReady( function() {
    var myContextMenu = new ContextMenu( "grid", 'contextMenu', function( e ) {
        status.innerHTML = 'Right Clicked';
    });
});

For our demo purpose we make use of div#status in which we will display the operation performed by the user. So we assign this element to status variable.

Then we make use of DOMReady method in EventUtil object. The function passed to this method runs when DOM becomes ready (document elements are created and DOM tree is build).

To use ContextMenu function, all we need to do is create a new instance of ContextMenu. Note that we pass three parameters to this constructor. First is the id of target element, second is the id of context menu which will display when user will right click on grid. Finally an optional function which will run after the context menu appears. This function is simply setting the innerHTML of status to Right Clicked.

That’s all you have to do to make use of this script.

Making Context Menu Buttons Functional

Links/buttons in context menu are not functional yet. To make them functional we need to assign event handlers or event delegate to them. Write the following code in function passed to DOMReady method. Also remember that this is not part of ContextMenu script. Adding handlers to context menu buttons is responsibility of developer who is using this script.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var menu = document.getElementById( 'contextMenu' );
 
EventUtil.addHandler( menu, 'click', function( e ) {
    e = getEventObj( e );
    var target = EventUtil.getEventTarget( e );
 
    if( target.nodeName == 'A' ) {
        if( myContextMenu.clickTarget.nodeName == 'TD' )
            status.innerHTML = target.innerHTML + ' clicked on row # ' + myContextMenu.clickTarget.parentNode.rowIndex;
        else
            status.innerHTML = target.innerHTML + ' clicked on ' + myContextMenu.clickTarget.nodeName;
 
        EventUtil.preventDefault( e );
        return false;
    }
});

LINE 1: Assign ul#contextMenu element to variable menu.

LINE 2: Add onclick handler to menu (#contextMenu). Actually this will be used as delegate. Check “Not Using Event Delegation” in my previous post.

LINE 3, 4: Get event object and on next line we get the target element on which user clicked the mouse button. Note that here onclick event only respond to left clicks.

LINE 6: If target element is a link then the body of this condition will display the user action in status div. We are checking this because user may have clicked on “li” instead of “a” element.

LINE 7, 8: Here we make use of myContextMenu to get the target (clickTarget) on which user right clicked. myContextMenu contains the instance or object of ContextMenu. We check that if clickTarget is a TD element then set the innerHTML of status to the link’s innerHTML (for example Copy, Edit, Delete) and string ‘clicked on row #’ and the row index in which the td/cell exist.

LINE 9, 10: Else case can only occur when cell is th (Table Header). In that case we are concatenating the nodeName (TH) instead of row index.

LINE 11, 12: EventUtil.preventDefault and return false is used to prevent the default action of this (onclick) event. The default action of clicking on anchor element is to navigate to a new location according to href attribute. This action will be prevented.

That’s the end of this tutorial. I tried my to explain each and everything and I hope you have learned a lot about writing cross browser, raw JavaScript :)


comments powered by Disqus