Friday, March 6, 2015

What's on the Menu Today?

With such widespread use of touch-screen devices today, I have to wonder if the mouse will be a forgotten input device in 20 years. Some people probably wish it was already forgotten. It tends to be less efficient than using keyboard shortcuts (if you can remember them all). But I don't find it all that terrible and still make use of right-click context menus in my user interfaces.

I've used both Dojo and ExtJS context menus in the past, but for my comic editor I use the jQuery contextMenu plugin. I've found it to be very responsive and relatively lightweight at 65k, but it feels a little awkward to use at times. One of its best features is that the menu object is created only once and can be bound to any number of objects. This makes very efficient use of the DOM, but the menus are also immutable. I thought that a bit odd at first but was easy enough to work around.

The basic use is simple. Here is an example web page.

<!doctype html>

<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Menus</title>
  <link href="/css/jquery.contextMenu.css" rel="stylesheet">
  <link href="/css/jquery-ui-1.10.4.min.css" rel="stylesheet">
</head>

<body>

  <p>This text should not have a menu.</p>

  <p class="demo">This text should have a menu.</p>

  <p>This text should not have a menu.</p>

  <p>This text says nothing.</p>

</body>

<script src="/js/jquery-1.11.2.min.js"></script>
<script src="/js/jquery.ui.position.js"></script>
<script src="/js/jquery.contextMenu.js"></script>
<script>

</html>

Note that in addition to the JavaScript for contextMenu, there is a CSS file for it you should include as well. Also, jQuery UI Position is recommended.

To create a right-click menu that will bind to every element with the "demo" class, you can do this...

$.contextMenu({

    zIndex: 100,
    selector: ".demo",
    items: {
        item1: {
            name: "Item 1",
            type: "text",
            value: "nothing"
        },
        item2: {
            name: "Second Item",
            type: "text",
            value: ""
        },
        sep1: "----------",
        item3: {
            name: "Another Item",
            type: "text",
            value: "also nothing"
        },
        sep2: "----------",
        someAction: {
            name: "Take Some Action",
            callback: function(key, options) {
                console.log("take action");
            }
        },
    },
    events: {
        show: function(opt) {
            var mData = $.contextMenu.getInputValues(opt, this.data());
            mData.item2 = "222222";
            $.contextMenu.setInputValues(opt, mData);
            console.log("menu shown");
        },
        hide: function(opt) {
            var mData = $.contextMenu.getInputValues(opt, this.data());
            console.log("item 1 when hidden: " + mData.item1);
        }
    }

});

Calling $.contextMenu creates a menu that will be activated whenever you right-click on an element that matches the selector value. In this case, it will be all elements with the demo class. The menu items are called out as fields in the items object. Note that you can use multiple separators, but they each have to have unique names. Each item can specify a name and optionally a type. The value of type will determine how that item is presented in the menu. In my example, "text" as a type results in a text box. If you just want action items to click on in your menu (things like "cut" and "paste" for example), leave off the type attribute and use callback instead. Specifying a function here enables your code to be called whenever that menu item is clicked on. Clicking on an item as causes the menu to be immediately hidden as well.

The events object gives you the ability to set listeners on the menu's show and hide events. In these functions, you can read the write the values for menu items that happen to be form controls such as input boxes. In my example's show function, I call $.contextMenu.getInputValues(opt, this.data()) to get an object that represents the data values of all the input fields in the menu. I know, that's not exactly the most obvious way to get the data, but it is what it is. Once you have the object, you can inspect and/or change the values. In the show function, it might make sense for you to set a value before the menu is displayed to the user. I do that by setting item2's value to "222222". In the data object, you reference the values by the names you gave the menu items (item1, item2, etc.). I leave the other values alone. When the menu is displayed, the value for "Second Item" should be all those 2's even though the value is blank in the menu definition. In the hide listener, it often makes sense to do something with the values that were entered by the user. Getting the data object is the same here. In my example, I just log the value of item1. If you change the text on the menu before hiding it, you should see your text print out to the console.

The results look like this.


I mentioned near the top about how the menus can't be changed after they are created. So what do you do if you need to make serious changes to the menu structure at runtime? The easiest thing to do is to destroy the menu and create a new one. This is what I do when images are added to my image selector menu and I need to recreate it. There doesn't appear to be any drawback to this method, as long as it is easy for you to run your menu-creation function again.

To blow away a specific menu, call "destroy" and specify the selector:

$.contextMenu( "destroy", "demo" );

To destroy all context menus, you can make the same call without specifying a selector.

$.contextMenu( "destroy" );

There's lots more you can do with these context menus, so I encourage you to experiment with it. While you can't see them while viewing the comics on Amphibian.com, you can trust me when I tell you they are there for me when I create them.

None of this really answers the question of what to do with right-click menus in situations where there is no mouse, but honestly I'm still thinking about the best way to handle that. Right now, I just don't edit the comics on a phone or tablet. Primitive, I know.

Amphibian.com comic for 6 March 2015