You may not know it, but the HTML5 specifications go beyond what we put in the pages and also define how parts of the browser should become available to developers with HTML, CSS and JavaScript. One of these parts of the specs are context menus, or “right click menus”. Using HTML5 and a menu element you can add new options to these without having to write a browser add-on. In Firefox 8 (the current one) we have support for those. See the following screencast for a context menu demo.
The image example is pretty simple and was actually written by Paul Rouget as a demo in the original Firefox bug request. The main core is the HTML of it:
As you can see you link the menu
element to an element via its ID. The contextmenu
attribute then points to this one. Each menu can have several menuitems
. Each of those gets a textual label and a possible icon. You can also nest menu
elements to create multiple layer menus. Here, we add inline
The functionality is simple, all the rotate()
and resize()
functions do is add class names to the image using querySelector
and classList
:
function rotate() {
document.querySelector('#menudemo').classList.toggle('rotate');
}
function resize() {
document.querySelector('#menudemo').classList.toggle('resize');
}
The real effect is in CSS transforms and transitions. As the image has an ID of menudemo
here is what is needed in CSS to rotate and resize:
#menudemo {
-moz-transition: 0.2s;
width:200px;
}
#menudemo.rotate {
-moz-transform: rotate(90deg);
}
#menudemo.resize {
-moz-transform: scale(0.7);
}
#menudemo.resize.rotate {
-moz-transform: scale(0.7) rotate(90deg);
}
Notice that in a real product we should of course add the other browser prefixes and go prefix-less but as the functionality now only works in Firefox, this is enough for this demo.
Detecting support and visual hinting
Now, as this is extending the normal user offerings in the browser we need to make it obvious that there is a right-click menu available. In CSS3, there is a context-menu
cursor available to us. When context menus are available, this should be shown:
.contextmenu #menudemo, .contextmenu .demo {
cursor: context-menu;
}
We test the browser for support by checking for contextmenu on the body element and for HTMLMenuItemElement
in the window (this has been added as a pull request to Modernizr, too).
if ('contextMenu' in document.body && 'HTMLMenuItemElement' in window) {
document.documentElement.classList.add('contextmenu');
} else {
return;
}
Wouldn’t HTMLMenuItemElement
be enough? Yes, but a real context menu should only offer functionality when it is sensible, and that is where contextMenu
comes in.
Turning menuitems on and off depending on functionality
As a slightly more complex example, let’s add a “count words” functionality to the document. For this, we generate a counter element that will become a tooltip when the words were counted:
var counter = document.createElement('span');
counter.id = 'counter';
counter.className = 'hide';
document.body.appendChild(counter);
counter.addEventListener('click', function(ev){
this.className = 'hide';
},false);
This one is hidden by default and becomes visible when the hide
class is removed. To make it smooth, we use a transition:
#counter{
position: absolute;
background: rgba(0,0,0,0.7);
padding:.5em 1em;
color: #fff;
font-weight:bold;
border-radius: 5px;
-moz-transition: opacity 0.4s;
}
#counter.hide{
opacity: 0;
}
We start with two sections with context menus:
We then loop through all the menuitems
with the class wordcount
and apply the functionality.
var wordcountmenus = document.querySelectorAll('.wordcount'),
i = wordcountmenus.length;
while (i--) {
wordcountmenus[i].addEventListener('click', function(ev){
// add functionality
}, false);
}
We need to find out what has been selected in the page. We do this by using getSelection()
and splitting its string version at whitespace. We then show the counter by removing the hide
class name.
var wordcountmenus = document.querySelectorAll('.wordcount'),
i = wordcountmenus.length;
while (i--) {
wordcountmenus[i].addEventListener('click', function(ev){
var text = document.getSelection(),
count = text.toString().split(/s/).length;
counter.innerHTML = count + ' words';
counter.className = '';
}, false);
}
You can see this in action in the second context menu demo. Now, the issue with this (as explained in the screencast) is that it always counts the words, regardless of the user having selected some text. What we want is the menu only to be active when there is text selected.
So in order to make our menu only become available when it makes sense we check if there is a selection in the document. Every context menu fires an event called contextmenu
when it opens. So all we need to do is to subscribe to this event.
When something is selected in the document document.getSelection().isCollapsed
is true. Otherwise it is false, so all we need to do is to enable or disable the menu item accordingly:
document.querySelector('#interactive').addEventListener(
'contextmenu', function(ev) {
this.querySelector('.wordcount').disabled =
document.getSelection().isCollapsed;
},
false);
The last thing to solve is the position of the mouse to position the counter element. As the menu selection event doesn’t give us the mouse position we need to add a contextmenu
handler to the whole document that positions the counter invisibly behind the menu when it is opened:
document.body.addEventListener(
'contextmenu', function(ev) {
counter.style.left = ev.pageX + 'px';
counter.style.top = ev.pageY + 'px';
counter.className = 'hide';
},
false);
Further reading and resources
- The full code demo
- The Firefox bug request with lots of discussion
- Context Menus on the WHATWG
- A longer discussion on Google+ about context menus
- A jQuery Polyfill for contextmenu
About Chris Heilmann
Evangelist for HTML5 and open web. Let's fix this!
19 comments