-
Notifications
You must be signed in to change notification settings - Fork 79
Home
A jQuery plugin that provides a context menu (based on the standard jQueryUI menu widget).
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());
}
},
...
]
});
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>
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.
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;
}
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.
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) {
},
$(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();
},
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());
}
});
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).