Mozilla

HTML5 context menus in Firefox (Screencast and Code)

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:

<section id="noninteractive" contextmenu="imagemenu">
 
  <img src="html5.png" alt="HTML5" id="menudemo">
 
    <menu type="context" id="imagemenu">
      <menuitem label="rotate" onclick="rotate()"
                icon="arrow_rotate_clockwise.png">
      </menuitem>
      <menuitem label="resize" onclick="resize()"
                icon="image-resize.png">
      </menuitem>
      <menu label="share">
        <menuitem label="twitter" onclick="alert('not yet')"></menuitem>
        <menuitem label="facebook" onclick="alert('not yet')"></menuitem>
      </menu>
    </menu>
 
</section>

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 onclick handlers to point to different JavaScript functions to call when the menu item gets activated. The resulting context menu looks like this:

image with a context menu

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:

<section id="noninteractive" contextmenu="countmenu">
  <menu type="context" id="countmenu">
      <menuitem class="wordcount" label="count words"></menuitem>
  </menu>
</section>
 
<section id="interactive" contextmenu="countmenuinteractive">
  <menu type="context" id="countmenuinteractive">
      <menuitem class="wordcount" label="count words"></menuitem>
  </menu>
</section>

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.

context menu item available or not available depending on selection

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

19 comments

Comments are now closed.

  1. Mårten Björk wrote on November 24th, 2011 at 03:09:

    Great news! How accessible is this? For instance, is there any way to indicate that the word count is a result of the action in the contextual menu?

  2. fpiat wrote on November 24th, 2011 at 03:20:

    Can we only had new options or have we methods / attributes for disabling or hiding the browser menu options?

    1. fpiat wrote on November 24th, 2011 at 03:22:

      must read “Can we only add” and not “had”

    2. passcod wrote on November 24th, 2011 at 04:26:

      I think that’d be rather inconvenient, and might actually be a security risk. The first silly use would probably be to disable to “View Source” menu item or the “View Image”/”Save Image” items for pseudo-protection… but I think it could go much further than that, exploit-wise.

      1. Chris Heilmann wrote on November 24th, 2011 at 06:51:

        Yes, this is why for now you can “only add” new items and not override the original ones.

        1. Ronny wrote on November 24th, 2011 at 09:48:

          Hey Chris, thanks for the write-up! This is cool.
          The first thing I thought of when reading this article was “hehe let’s try to replace a default action like Back with some custom action, could be fun”.
          So I guess “only add” is a good call ;-)

      2. Jonas Finnemann Jensen wrote on November 24th, 2011 at 11:07:

        This would be awesome, if I’m writting a web app there’s no reason why “View Source”, “Save Image”, etc. should take up space in the context menu. They’re not useful if a web app, why should I be able to save the image of a button?

        People can always find these options elsewhere, like in a menu or download the source manually.

        My point is that when you force web apps to have all these left-over things that are only useful for when a website is a document, you’re making the web application platform less appealing.

        Trust the web developer, if they want to obscure images, which is as you put it “silly”, they can just do it using js, css or whatever.

        1. Jonas Finnemann Jensen wrote on November 24th, 2011 at 11:19:

          My point is that from a usability point of view, letting people remove default context menu items would make sense.

          And if some web devs, wishes to destroy the default usability by abusing this feature, that’s their loss.

          I agree that alerts with “you can’t right click here” are silly, but why not let the silly people be silly if they want to so badly? :)

          1. dhinesh wrote on September 12th, 2012 at 03:26:

            it was a nice theory

  3. Rodney Rehm wrote on November 24th, 2011 at 05:57:

    I find it pretty interesting, that you guys link to Addy Osmani’s fork of the HTML5 contextmenu polyfill. His repo is out of sync. You’ll find the real deal at http://medialize.github.com/jQuery-contextMenu/ :) – Native Demo here: http://medialize.github.com/jQuery-contextMenu/demo/html5-polyfill-firefox8.html

    I’d be interested in finding out how this happened. Is it because Addy Osmani is a namel well known on the net and I am not? Or did the guy looking for a polyfill just link to the first hit on google?

    Is there a road map regarding your context menu stuff? Currently one cannot (optically) tell aparty controls, checkboxes and radio elements within a context menu. (Amongst other difficulties I have with the native implementation). Who would I bug with my questions?

    1. Chris Heilmann wrote on November 24th, 2011 at 06:49:

      Actually it is because we were talking to each other when I was showing him the native implementation and he went for it. He was the first to tweet back and start the discussion on Google+. Nothing about being famous at all. Great to get your resources and feedback. I will try to find out who is the best person to talk to you about it. Is there a bug open on the issue with checkboxes?

      1. Rodney Rehm wrote on November 24th, 2011 at 06:59:

        Nope, haven’t opened any bugs yet. Should I do that before hand, or wait for some mozillian discussion? And thanks for updating the link :)

        1. Rodney Rehm wrote on November 25th, 2011 at 09:15:

          There you go, I fought my way through bugzilla: https://bugzilla.mozilla.org/show_bug.cgi?id=705292

  4. Luke Dorny wrote on November 24th, 2011 at 10:37:

    Just a nitpic as we discuss html5, usage of the Section element without a heading inside it. If not, it should be a div or something better.
    Otherwise a fascinating new realm of possibilities making your site feel like it is an app, or better. Awesome article.

  5. Daniel Piechnick wrote on November 25th, 2011 at 00:22:

    I can see a lot of different applications for this. Can we expect this in IE anytime soon? :)

    Daniel Piechnick

  6. kangax wrote on November 26th, 2011 at 17:03:

    Just a small tweak — `’contextMenu’ in document.documentElement` would be better, for cases when body doesn’t exist yet.

    Also, what exactly is `”HTMLMenuItemElement” in window` supposed to determine? That menuitem element is supported? It just made me wonder if this kind of relation check is safe — is implementation really required to expose HTMLMenuItemElement interface publicly+globally when adding support for menu item element. Does IDL mention it anywhere?

  7. Isuru wrote on January 11th, 2012 at 20:07:

    Just what I was looking for this script I’m working on! really cool!

  8. icaaq wrote on March 19th, 2012 at 06:16:

    Just a heads up, the menuitem-element is not yet part of the html-specification. Follow the issue here :) https://www.w3.org/Bugs/Public/show_bug.cgi?id=13608

  9. pedz wrote on May 25th, 2012 at 06:10:

    This is very interesting. I bumped into it while surfing trying to answer the following request: I’m looking for suggestions on how to give the user a visual clue that a context menu is available.

    I have a table, like a report, of “hits” in a database. Each is one line, with about 7 columns. Each element has its own context menu. Currently there is no visual clue to users that all those features are available and based upon feedback, people don’t know that they are there.

    And… perhaps I should not even have a context menu at those points but its all I could come up with. The current implementation is circa 2008 using javascript and a hidden UL element that pops up on a right mouse click.

Comments are closed for this article.