Skip to content
This repository has been archived by the owner on Aug 14, 2021. It is now read-only.
Martin Wendt edited this page Mar 10, 2019 · 17 revisions

jquery.ui-contextmenu - Tutorial

A jQuery plugin that provides a context menu (based on the standard jQueryUI menu widget).

sample

First, include dependencies:

  • jQuery 1.7+ (1.10 or later recommended)
  • jQuery UI 1.9+ (at least core, widget, menu), 1.11+ recommended
  • One of the ThemeRoller CSS themes or a custom one
  • jquery.ui-contextmenu.js (also available as CDN on jsDelivr, cdnjs), or UNPKG
<head>
    <link href="//code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css" 
        rel="stylesheet" />
    <script src="//code.jquery.com/jquery-3.2.1.min.js"></script>
    <script src="//code.jquery.com/ui/1.12.1/jquery-ui.min.js"></script>
    <script src="assets/jquery.ui-contextmenu.min.js"></script>

Assume we have some HTML elements that we want to attach a popup menu to:

<div id="container">
    <div class="hasmenu">AAA</div>
    <div class="hasmenu">BBB</div>
    <div class="hasmenu">CCC</div>
</div>

Now we can enable a context menu like so:

$("#container").contextmenu({
  delegate: ".hasmenu",
  menu: [
    {title: "Copy", cmd: "copy", uiIcon: "ui-icon-copy"},
    {title: "----"},
    {title: "More", children: [
      {title: "Sub 1", cmd: "sub1", disabled: true},
      {title: "Sub 2", cmd: "sub1"}
      ]}
    ],
  select: function(event, ui) {
    alert("select " + ui.cmd + " on " + ui.target.text());
  }
});

See the API Docs for a list of options.

The delegate option defines a CSS selector, which is evaluated for all elements inside the context element (#container in our example).
In order to attach menus to all matching elements on the page that have class="hasmenu", we may use document as context:

$(document).contextmenu({
    delegate: ".hasmenu",
    ...
});

Note: only one contextmenu widget instance can be bound to one element. See the Howto below for a solution to this problem.

The menu options may contain a (nested) array of entry definitions.
See the API Docs for a list of menu entry options.

Instead of handling all menu commands in the select event, it is also possible to attach callbacks directly to menu entries:

$(document).contextmenu({
    delegate: ".hasmenu",
    menu: [
        { title: "Copy", uiIcon: "ui-icon-copy", action: function(event, ui) {
                alert("Copy " + ui.target.text());
             }
         },
        ...
        ]
});

Initialize menu from an existing <ul> element

In this case menu must point to the markup:

$(document).contextmenu({
    delegate: ".hasmenu",
    menu: "#options",
    select: function(event, ui) {
      ...
    }
});

We also have to provide some HTML markup that defines the context menu structure, see jQueryUI menu for details:

<ul id="options" class="ui-helper-hidden">
    <li data-command="copy"><span class="ui-icon ui-icon-copy"></span>Copy</li>
    <li data-command="paste" class="ui-state-disabled">Paste</li>
    <li>----</li>
    <li>More
        <ul>
            <li data-command="sub1">Sub 1</li>
            <li data-command="sub2">Sub 2</li>
        </ul>
    </li>
</ul>

Note: starting with jQuery UI 1.12 the use of wrappers (<div>) in menu items is required:

<ul id="options" class="ui-helper-hidden">
    <li data-command="copy"><div><span class="ui-icon ui-icon-copy"></span>Copy</div></li>
    ...
</ul>

Note: until and including jQuery UI 1.10 the use of anchors (<a>) in menu items was required:

<ul id="options" class="ui-helper-hidden">
    <li data-command="copy"><a href="#"><span class="ui-icon ui-icon-copy"></span>Copy</a>
    ...
</ul>

Modify the menu depending on the context

Often we need to modify the menu before it is displayed, in order to reflect the current context. This can be done in the beforeOpen event:

$(document).contextmenu({
    delegate: ".hasmenu",
    menu: [
        {title: "Cut", cmd: "cut", uiIcon: "ui-icon-scissors"},
        {title: "Copy", cmd: "copy", uiIcon: "ui-icon-copy"},
        {title: "Paste", cmd: "paste", uiIcon: "ui-icon-clipboard", disabled: true },
        ...
        ],
    beforeOpen: function(event, ui) {
        var $menu = ui.menu,
            $target = ui.target,
            extraData = ui.extraData; // optionally passed when menu was opened by call to open()

        // Optionally return false, to prevent opening the menu
//      return false;

        // En/disable single entries
        $(document).contextmenu("enableEntry", "paste", false);
        // Show/hide single entries
        $(document).contextmenu("showEntry", "cut", false);
        // Redefine the title of single entries
        $(document).contextmenu("setEntry", "copy", "Copy '" + $target.text() + "'")
        // Redefine all attributes of single entries
        $(document).contextmenu("setEntry", "cut", {title: "Cuty", uiIcon: "ui-icon-heart", disabled: true});
        // Redefine the whole menu
        $(document).contextmenu("replaceMenu", [{title: "aaa"}, {title: "bbb"}, ...]);
        // Redefine the whole menu from another HTML definition
        $(document).contextmenu("replaceMenu", "#options2");
    },
    ...
});

Starting with v1.17 there is an alternative way to modify the entry status using callbacks.
The disabled and title options accept a function that will be called when the menu is opened:

$(document).contextmenu({
    ...
    menu: [
        {title: "Paste", cmd: "paste", uiIcon: "ui-icon-clipboard",
            disabled: function(event, ui) {
                // return `true` to disable, `"hide"` to remove entry:
                return clipboardEmpty ? true : false;
            } },
        ...

See the API Docs for a list of methods and a list of events.

Tips and Tricks

[Howto] Add right-aligned shortcut hints

Simply add a tag of your choice to the title (for example <kbd>)

$(document).contextmenu({
    delegate: ".hasmenu",
    menu: [
        {title: "Edit title<kbd>[F2]</kbd>", cmd: "rename"}, 
        {title: "Copy <kbd>[Ctrl+C]</kbd>", cmd: "copy"}, ...
        ],

and make it right aligned via CSS:

.ui-menu kbd {
    float: right;
}

[Howto] Enable keyboard control

In order open a context menu with the keyboard, make sure the target elements are tabbable, for example by adding a tabindex="0" attribute. Also make sure the autoFocus: true option is set. This will allow to Use Tab and the Windows Menu keys.

[Howto] Trigger the menu on custom events

By default the menu is triggered by the contextmenu event, which is typically generated by right click or the respective keyboard shortcut.
This behavior may disabled by passing autoTrigger: false.

Example 1: open an existing menu programatically on a button click:

$("#triggerPopupButton").click(function(){
    // Trigger popup menu on the first target element
    $(document).contextmenu("open", $(".hasmenu:first"), {foo: "bar"});
});

The optional third parameter allows to pass extra data to the handlers.
Note: the open command only works for targets that match the delegate selector option.

$(document).contextmenu({
    delegate: ".hasmenu",
    position: function(event, ui) {  // adjust position (since we don't have a mouse position)
        return {my: "left top", at: "left bottom", of: ui.target};
    },
    ...
    beforeOpen: function(event, ui) {
        // We can access extra data that was passed to the `.open()` command.
        if ( ui.extraData.foo === "bar" ) { ... }

        // ui.extraData is maintained throughout the event sequence,
        // so we can pass custom data on...
        ui.extraData.helloFromBO = true;
    },
    open: function(event, ui) {
        // ... ui.extraData.helloFromBO is true here
    },
    select: function(event, ui) {
        // ... ui.extraData.helloFromBO is true here
    },

Example 2: open the menu on a plain left-click instead of right-click:

$(".hasmenu").click(function(event){
    if ( event.which === 1 ) {  // left-click
        // Pass click event to `open()`
        $(document).contextmenu("open", event);
    }
});
$(document).contextmenu({
    delegate: ".hasmenu",
    autoTrigger: false,  // prevent default behavior, i.e. don't open on right-click
    ...
    beforeOpen: function(event, ui) {
    },

[Howto] Modify the menu using an asynchronous request

$(document).contextmenu({
    ...
    beforeOpen: function(event, ui) {
        // Immediate menu changes
        $(document).contextmenu("setEntry", "test", "(loading...)");
        // Menu opens, then we submit a request and wait for the resonse
        $.ajax({
            ...
        }).done(function(data) {
            // Modify the menu from the ajax response. The menu will be updated
            // while open
            $(document).contextmenu("setEntry", "test", {
                title: "New entry", cmd: "test", 
                children: [ ... ]
                });
        });
    },

Alternatively we can delay the opening until the response arrives:

$(document).contextmenu({
    ...
    beforeOpen: function(event, ui) {
        var dfd = new $.Deferred();

        $.ajax({
            ...
        }).done(function(data) {
            // Modify the menu from the ajax response. The menu will be opened
            // afterwards
            $(document).contextmenu("setEntry", "test", {
                title: "New entry", cmd: "test", 
                children: [ ... ]
                });
            dfd.resolve(); // Notify about finished response
        });

        // Return a promise to delay opening until an async response becomes
        // available
        ui.result = dfd.promise();
    },

[Howto] Bind different contextmenus to the same DOM element

This is especially useful if we want to bind contextmenus for different selectors to the document element, in order to make them global:

$(document).contextmenu({
    delegate: ".hasmenu",
    menu: ...,
    select: function(event, ui) {
        alert("select contextmenu 1" + ui.cmd + " on " + ui.target.text());
    }
});

Another call to $(document).contextmenu({...}) would destroy the previous instance, because the jQuery Widget Factory only allows one instance per element.

The solution is to create new widget with another name but identical functionality:

// 1. Create and register another widget that inherits directly from 
//    jquery-ui-contextmenu:
$.widget("moogle.contextmenu2", $.moogle.contextmenu, {});
// 2. Now we can bind this new widget to the same DOM element without
//    destroying a previous widget.
$(document).contextmenu2({
    delegate: ".hasmenu2",
    menu: ...,
    select: function(event, ui) {
        alert("select contextmenu2" + ui.cmd + " on " + ui.target.text());
    }
});

[Howto] Allow Hyphen as Menu Entry Titles

The jQueryUI menu converts every title that starts with whitespace or '-' to an unselectable separator see here.

jquery.ui-contextmenu adapts this behavior.

A possible hack is shown here (requires patching this library and globally changing the jQuery UI Menu behavior).