Files
pgadmin4/web/pgadmin/static/js/jQuery-contextMenu/jquery.contextMenu.js
Ashesh Vashi d70535385b Upgraded the jQuery-contextMenu to v2.1.0 (latest stable version) in
order to fix the issue rendering the context menu for longer text.

Needs to do some modification in the existing pgAdmin4 CSS theme to make
it looks like/near the existing look and feel.
2016-03-30 19:08:27 +05:30

1905 lines
78 KiB
JavaScript
Executable File

/*!
* jQuery contextMenu v2.1.0 - Plugin for simple contextMenu handling
*
* Version: v2.1.0
*
* Authors: Björn Brala (SWIS.nl), Rodney Rehm, Addy Osmani (patches for FF)
* Web: http://swisnl.github.io/jQuery-contextMenu/
*
* Copyright (c) 2011-2016 SWIS BV and contributors
*
* Licensed under
* MIT License http://www.opensource.org/licenses/mit-license
* GPL v3 http://opensource.org/licenses/GPL-3.0
*
* Date: 2016-02-28T09:41:24.636Z
*/
(function (factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as anonymous module.
define(['jquery'], factory);
} else if (typeof exports === 'object') {
// Node / CommonJS
factory(require('jquery'));
} else {
// Browser globals.
factory(jQuery);
}
})(function ($) {
'use strict';
// TODO: -
// ARIA stuff: menuitem, menuitemcheckbox und menuitemradio
// create <menu> structure if $.support[htmlCommand || htmlMenuitem] and !opt.disableNative
// determine html5 compatibility
$.support.htmlMenuitem = ('HTMLMenuItemElement' in window);
$.support.htmlCommand = ('HTMLCommandElement' in window);
$.support.eventSelectstart = ('onselectstart' in document.documentElement);
/* // should the need arise, test for css user-select
$.support.cssUserSelect = (function(){
var t = false,
e = document.createElement('div');
$.each('Moz|Webkit|Khtml|O|ms|Icab|'.split('|'), function(i, prefix) {
var propCC = prefix + (prefix ? 'U' : 'u') + 'serSelect',
prop = (prefix ? ('-' + prefix.toLowerCase() + '-') : '') + 'user-select';
e.style.cssText = prop + ': text;';
if (e.style[propCC] == 'text') {
t = true;
return false;
}
return true;
});
return t;
})();
*/
/* jshint ignore:start */
if (!$.ui || !$.widget) {
// duck punch $.cleanData like jQueryUI does to get that remove event
$.cleanData = (function (orig) {
return function (elems) {
var events, elem, i;
for (i = 0; elems[i] != null; i++) {
elem = elems[i];
try {
// Only trigger remove when necessary to save time
events = $._data(elem, 'events');
if (events && events.remove) {
$(elem).triggerHandler('remove');
}
// Http://bugs.jquery.com/ticket/8235
} catch (e) {}
}
orig(elems);
};
})($.cleanData);
}
/* jshint ignore:end */
var // currently active contextMenu trigger
$currentTrigger = null,
// is contextMenu initialized with at least one menu?
initialized = false,
// window handle
$win = $(window),
// number of registered menus
counter = 0,
// mapping selector to namespace
namespaces = {},
// mapping namespace to options
menus = {},
// custom command type handlers
types = {},
// default values
defaults = {
// selector of contextMenu trigger
selector: null,
// where to append the menu to
appendTo: null,
// method to trigger context menu ["right", "left", "hover"]
trigger: 'right',
// hide menu when mouse leaves trigger / menu elements
autoHide: false,
// ms to wait before showing a hover-triggered context menu
delay: 200,
// flag denoting if a second trigger should simply move (true) or rebuild (false) an open menu
// as long as the trigger happened on one of the trigger-element's child nodes
reposition: true,
// Default classname configuration to be able avoid conflicts in frameworks
classNames : {
hover: 'context-menu-hover', // Item hover
disabled: 'context-menu-disabled', // Item disabled
visible: 'context-menu-visible', // Item visible
notSelectable: 'context-menu-not-selectable', // Item not selectable
icon: 'context-menu-icon',
iconEdit: 'context-menu-icon-edit',
iconCut: 'context-menu-icon-cut',
iconCopy: 'context-menu-icon-copy',
iconPaste: 'context-menu-icon-paste',
iconDelete: 'context-menu-icon-delete',
iconAdd: 'context-menu-icon-add',
iconQuit: 'context-menu-icon-quit'
},
// determine position to show menu at
determinePosition: function ($menu) {
// position to the lower middle of the trigger element
if ($.ui && $.ui.position) {
// .position() is provided as a jQuery UI utility
// (...and it won't work on hidden elements)
$menu.css('display', 'block').position({
my: 'center top',
at: 'center bottom',
of: this,
offset: '0 5',
collision: 'fit'
}).css('display', 'none');
} else {
// determine contextMenu position
var offset = this.offset();
offset.top += this.outerHeight();
offset.left += this.outerWidth() / 2 - $menu.outerWidth() / 2;
$menu.css(offset);
}
},
// position menu
position: function (opt, x, y) {
var offset;
// determine contextMenu position
if (!x && !y) {
opt.determinePosition.call(this, opt.$menu);
return;
} else if (x === 'maintain' && y === 'maintain') {
// x and y must not be changed (after re-show on command click)
offset = opt.$menu.position();
} else {
// x and y are given (by mouse event)
offset = {top: y, left: x};
}
// correct offset if viewport demands it
var bottom = $win.scrollTop() + $win.height(),
right = $win.scrollLeft() + $win.width(),
height = opt.$menu.outerHeight(),
width = opt.$menu.outerWidth();
if (offset.top + height > bottom) {
offset.top -= height;
}
if (offset.top < 0) {
offset.top = 0;
}
if (offset.left + width > right) {
offset.left -= width;
}
if (offset.left < 0) {
offset.left = 0;
}
opt.$menu.css(offset);
},
// position the sub-menu
positionSubmenu: function ($menu) {
if ($.ui && $.ui.position) {
// .position() is provided as a jQuery UI utility
// (...and it won't work on hidden elements)
$menu.css('display', 'block').position({
my: 'left top',
at: 'right top',
of: this,
collision: 'flipfit fit'
}).css('display', '');
} else {
// determine contextMenu position
var offset = {
top: 0,
left: this.outerWidth()
};
$menu.css(offset);
}
},
// offset to add to zIndex
zIndex: 1,
// show hide animation settings
animation: {
duration: 50,
show: 'slideDown',
hide: 'slideUp'
},
// events
events: {
show: $.noop,
hide: $.noop
},
// default callback
callback: null,
// list of contextMenu items
items: {}
},
// mouse position for hover activation
hoveract = {
timer: null,
pageX: null,
pageY: null
},
// determine zIndex
zindex = function ($t) {
var zin = 0,
$tt = $t;
while (true) {
zin = Math.max(zin, parseInt($tt.css('z-index'), 10) || 0);
$tt = $tt.parent();
if (!$tt || !$tt.length || 'html body'.indexOf($tt.prop('nodeName').toLowerCase()) > -1) {
break;
}
}
return zin;
},
// event handlers
handle = {
// abort anything
abortevent: function (e) {
e.preventDefault();
e.stopImmediatePropagation();
},
// contextmenu show dispatcher
contextmenu: function (e) {
var $this = $(this);
// disable actual context-menu if we are using the right mouse button as the trigger
if (e.data.trigger === 'right') {
e.preventDefault();
e.stopImmediatePropagation();
}
// abort native-triggered events unless we're triggering on right click
if ((e.data.trigger !== 'right' && e.data.trigger !== 'demand') && e.originalEvent) {
return;
}
// Let the current contextmenu decide if it should show or not based on its own trigger settings
if (e.mouseButton !== undefined && e.data) {
if (!(e.data.trigger === 'left' && e.mouseButton === 0) && !(e.data.trigger === 'right' && e.mouseButton === 2)) {
// Mouse click is not valid.
return;
}
}
// abort event if menu is visible for this trigger
if ($this.hasClass('context-menu-active')) {
return;
}
if (!$this.hasClass('context-menu-disabled')) {
// theoretically need to fire a show event at <menu>
// http://www.whatwg.org/specs/web-apps/current-work/multipage/interactive-elements.html#context-menus
// var evt = jQuery.Event("show", { data: data, pageX: e.pageX, pageY: e.pageY, relatedTarget: this });
// e.data.$menu.trigger(evt);
$currentTrigger = $this;
if (e.data.build) {
var built = e.data.build($currentTrigger, e);
// abort if build() returned false
if (built === false) {
return;
}
// dynamically build menu on invocation
e.data = $.extend(true, {}, defaults, e.data, built || {});
// abort if there are no items to display
if (!e.data.items || $.isEmptyObject(e.data.items)) {
// Note: jQuery captures and ignores errors from event handlers
if (window.console) {
(console.error || console.log).call(console, 'No items specified to show in contextMenu');
}
throw new Error('No Items specified');
}
// backreference for custom command type creation
e.data.$trigger = $currentTrigger;
op.create(e.data);
}
var showMenu = false;
for (var item in e.data.items) {
if (e.data.items.hasOwnProperty(item)) {
var visible;
if ($.isFunction(e.data.items[item].visible)) {
visible = e.data.items[item].visible.call($(e.currentTarget), item, e.data);
} else if (typeof item.visible !== 'undefined') {
visible = e.data.items[item].visible === true;
} else {
visible = true;
}
if (visible) {
showMenu = true;
}
}
}
if (showMenu) {
// show menu
var menuContainer = (e.data.appendTo === null ? $('body') : $(e.data.appendTo));
var srcElement = e.target || e.srcElement || e.originalTarget;
if (e.offsetX !== undefined && e.offsetY !== undefined) {
op.show.call($this, e.data,
$(srcElement).offset().left - menuContainer.offset().left + e.offsetX,
$(srcElement).offset().top - menuContainer.offset().top + e.offsetY);
} else {
op.show.call($this, e.data, e.pageX, e.pageY);
}
}
}
},
// contextMenu left-click trigger
click: function (e) {
e.preventDefault();
e.stopImmediatePropagation();
$(this).trigger($.Event('contextmenu', {data: e.data, pageX: e.pageX, pageY: e.pageY}));
},
// contextMenu right-click trigger
mousedown: function (e) {
// register mouse down
var $this = $(this);
// hide any previous menus
if ($currentTrigger && $currentTrigger.length && !$currentTrigger.is($this)) {
$currentTrigger.data('contextMenu').$menu.trigger('contextmenu:hide');
}
// activate on right click
if (e.button === 2) {
$currentTrigger = $this.data('contextMenuActive', true);
}
},
// contextMenu right-click trigger
mouseup: function (e) {
// show menu
var $this = $(this);
if ($this.data('contextMenuActive') && $currentTrigger && $currentTrigger.length && $currentTrigger.is($this) && !$this.hasClass('context-menu-disabled')) {
e.preventDefault();
e.stopImmediatePropagation();
$currentTrigger = $this;
$this.trigger($.Event('contextmenu', {data: e.data, pageX: e.pageX, pageY: e.pageY}));
}
$this.removeData('contextMenuActive');
},
// contextMenu hover trigger
mouseenter: function (e) {
var $this = $(this),
$related = $(e.relatedTarget),
$document = $(document);
// abort if we're coming from a menu
if ($related.is('.context-menu-list') || $related.closest('.context-menu-list').length) {
return;
}
// abort if a menu is shown
if ($currentTrigger && $currentTrigger.length) {
return;
}
hoveract.pageX = e.pageX;
hoveract.pageY = e.pageY;
hoveract.data = e.data;
$document.on('mousemove.contextMenuShow', handle.mousemove);
hoveract.timer = setTimeout(function () {
hoveract.timer = null;
$document.off('mousemove.contextMenuShow');
$currentTrigger = $this;
$this.trigger($.Event('contextmenu', {
data: hoveract.data,
pageX: hoveract.pageX,
pageY: hoveract.pageY
}));
}, e.data.delay);
},
// contextMenu hover trigger
mousemove: function (e) {
hoveract.pageX = e.pageX;
hoveract.pageY = e.pageY;
},
// contextMenu hover trigger
mouseleave: function (e) {
// abort if we're leaving for a menu
var $related = $(e.relatedTarget);
if ($related.is('.context-menu-list') || $related.closest('.context-menu-list').length) {
return;
}
try {
clearTimeout(hoveract.timer);
} catch (e) {
}
hoveract.timer = null;
},
// click on layer to hide contextMenu
layerClick: function (e) {
var $this = $(this),
root = $this.data('contextMenuRoot'),
button = e.button,
x = e.pageX,
y = e.pageY,
target,
offset;
e.preventDefault();
e.stopImmediatePropagation();
setTimeout(function () {
var $window;
var triggerAction = ((root.trigger === 'left' && button === 0) || (root.trigger === 'right' && button === 2));
// find the element that would've been clicked, wasn't the layer in the way
if (document.elementFromPoint && root.$layer) {
root.$layer.hide();
target = document.elementFromPoint(x - $win.scrollLeft(), y - $win.scrollTop());
root.$layer.show();
}
if (root.reposition && triggerAction) {
if (document.elementFromPoint) {
if (root.$trigger.is(target) || root.$trigger.has(target).length) {
root.position.call(root.$trigger, root, x, y);
return;
}
} else {
offset = root.$trigger.offset();
$window = $(window);
// while this looks kinda awful, it's the best way to avoid
// unnecessarily calculating any positions
offset.top += $window.scrollTop();
if (offset.top <= e.pageY) {
offset.left += $window.scrollLeft();
if (offset.left <= e.pageX) {
offset.bottom = offset.top + root.$trigger.outerHeight();
if (offset.bottom >= e.pageY) {
offset.right = offset.left + root.$trigger.outerWidth();
if (offset.right >= e.pageX) {
// reposition
root.position.call(root.$trigger, root, x, y);
return;
}
}
}
}
}
}
if (target && triggerAction) {
root.$trigger.one('contextmenu:hidden', function () {
$(target).contextMenu({ x: x, y: y, button: button });
});
}
root.$menu.trigger('contextmenu:hide');
}, 50);
},
// key handled :hover
keyStop: function (e, opt) {
if (!opt.isInput) {
e.preventDefault();
}
e.stopPropagation();
},
key: function (e) {
var opt = {};
// Only get the data from $currentTrigger if it exists
if ($currentTrigger) {
opt = $currentTrigger.data('contextMenu') || {};
}
// If the trigger happen on a element that are above the contextmenu do this
if (opt.zIndex === undefined) {
opt.zIndex = 0;
}
var targetZIndex = 0;
var getZIndexOfTriggerTarget = function (target) {
if (target.style.zIndex !== '') {
targetZIndex = target.style.zIndex;
} else {
if (target.offsetParent !== null && target.offsetParent !== undefined) {
getZIndexOfTriggerTarget(target.offsetParent);
}
else if (target.parentElement !== null && target.parentElement !== undefined) {
getZIndexOfTriggerTarget(target.parentElement);
}
}
};
getZIndexOfTriggerTarget(e.target);
// If targetZIndex is heigher then opt.zIndex dont progress any futher.
// This is used to make sure that if you are using a dialog with a input / textarea / contenteditable div
// and its above the contextmenu it wont steal keys events
if (targetZIndex > opt.zIndex) {
return;
}
switch (e.keyCode) {
case 9:
case 38: // up
handle.keyStop(e, opt);
// if keyCode is [38 (up)] or [9 (tab) with shift]
if (opt.isInput) {
if (e.keyCode === 9 && e.shiftKey) {
e.preventDefault();
if(opt.$selected) {
opt.$selected.find('input, textarea, select').blur();
}
opt.$menu.trigger('prevcommand');
return;
} else if (e.keyCode === 38 && opt.$selected.find('input, textarea, select').prop('type') === 'checkbox') {
// checkboxes don't capture this key
e.preventDefault();
return;
}
} else if (e.keyCode !== 9 || e.shiftKey) {
opt.$menu.trigger('prevcommand');
return;
}
break;
// omitting break;
// case 9: // tab - reached through omitted break;
case 40: // down
handle.keyStop(e, opt);
if (opt.isInput) {
if (e.keyCode === 9) {
e.preventDefault();
if(opt.$selected) {
opt.$selected.find('input, textarea, select').blur();
}
opt.$menu.trigger('nextcommand');
return;
} else if (e.keyCode === 40 && opt.$selected.find('input, textarea, select').prop('type') === 'checkbox') {
// checkboxes don't capture this key
e.preventDefault();
return;
}
} else {
opt.$menu.trigger('nextcommand');
return;
}
break;
case 37: // left
handle.keyStop(e, opt);
if (opt.isInput || !opt.$selected || !opt.$selected.length) {
break;
}
if (!opt.$selected.parent().hasClass('context-menu-root')) {
var $parent = opt.$selected.parent().parent();
opt.$selected.trigger('contextmenu:blur');
opt.$selected = $parent;
return;
}
break;
case 39: // right
handle.keyStop(e, opt);
if (opt.isInput || !opt.$selected || !opt.$selected.length) {
break;
}
var itemdata = opt.$selected.data('contextMenu') || {};
if (itemdata.$menu && opt.$selected.hasClass('context-menu-submenu')) {
opt.$selected = null;
itemdata.$selected = null;
itemdata.$menu.trigger('nextcommand');
return;
}
break;
case 35: // end
case 36: // home
if (opt.$selected && opt.$selected.find('input, textarea, select').length) {
return;
} else {
(opt.$selected && opt.$selected.parent() || opt.$menu)
.children(':not(.' + opt.classNames.disabled + ', .' + opt.classNames.notSelectable + ')')[e.keyCode === 36 ? 'first' : 'last']()
.trigger('contextmenu:focus');
e.preventDefault();
return;
}
break;
case 13: // enter
handle.keyStop(e, opt);
if (opt.isInput) {
if (opt.$selected && !opt.$selected.is('textarea, select')) {
e.preventDefault();
return;
}
break;
}
if (typeof opt.$selected !== 'undefined' && opt.$selected !== null) {
opt.$selected.trigger('mouseup');
}
return;
case 32: // space
case 33: // page up
case 34: // page down
// prevent browser from scrolling down while menu is visible
handle.keyStop(e, opt);
return;
case 27: // esc
handle.keyStop(e, opt);
opt.$menu.trigger('contextmenu:hide');
return;
default: // 0-9, a-z
var k = (String.fromCharCode(e.keyCode)).toUpperCase();
if (opt.accesskeys && opt.accesskeys[k]) {
// according to the specs accesskeys must be invoked immediately
opt.accesskeys[k].$node.trigger(opt.accesskeys[k].$menu ? 'contextmenu:focus' : 'mouseup');
return;
}
break;
}
// pass event to selected item,
// stop propagation to avoid endless recursion
e.stopPropagation();
if (typeof opt.$selected !== 'undefined' && opt.$selected !== null) {
opt.$selected.trigger(e);
}
},
// select previous possible command in menu
prevItem: function (e) {
e.stopPropagation();
var opt = $(this).data('contextMenu') || {};
var root = $(this).data('contextMenuRoot') || {};
// obtain currently selected menu
if (opt.$selected) {
var $s = opt.$selected;
opt = opt.$selected.parent().data('contextMenu') || {};
opt.$selected = $s;
}
var $children = opt.$menu.children(),
$prev = !opt.$selected || !opt.$selected.prev().length ? $children.last() : opt.$selected.prev(),
$round = $prev;
// skip disabled or hidden elements
while ($prev.hasClass(root.classNames.disabled) || $prev.hasClass(root.classNames.notSelectable) || $prev.is(':hidden')) {
if ($prev.prev().length) {
$prev = $prev.prev();
} else {
$prev = $children.last();
}
if ($prev.is($round)) {
// break endless loop
return;
}
}
// leave current
if (opt.$selected) {
handle.itemMouseleave.call(opt.$selected.get(0), e);
}
// activate next
handle.itemMouseenter.call($prev.get(0), e);
// focus input
var $input = $prev.find('input, textarea, select');
if ($input.length) {
$input.focus();
}
},
// select next possible command in menu
nextItem: function (e) {
e.stopPropagation();
var opt = $(this).data('contextMenu') || {};
var root = $(this).data('contextMenuRoot') || {};
// obtain currently selected menu
if (opt.$selected) {
var $s = opt.$selected;
opt = opt.$selected.parent().data('contextMenu') || {};
opt.$selected = $s;
}
var $children = opt.$menu.children(),
$next = !opt.$selected || !opt.$selected.next().length ? $children.first() : opt.$selected.next(),
$round = $next;
// skip disabled
while ($next.hasClass(root.classNames.disabled) || $next.hasClass(root.classNames.notSelectable) || $next.is(':hidden')) {
if ($next.next().length) {
$next = $next.next();
} else {
$next = $children.first();
}
if ($next.is($round)) {
// break endless loop
return;
}
}
// leave current
if (opt.$selected) {
handle.itemMouseleave.call(opt.$selected.get(0), e);
}
// activate next
handle.itemMouseenter.call($next.get(0), e);
// focus input
var $input = $next.find('input, textarea, select');
if ($input.length) {
$input.focus();
}
},
// flag that we're inside an input so the key handler can act accordingly
focusInput: function () {
var $this = $(this).closest('.context-menu-item'),
data = $this.data(),
opt = data.contextMenu,
root = data.contextMenuRoot;
root.$selected = opt.$selected = $this;
root.isInput = opt.isInput = true;
},
// flag that we're inside an input so the key handler can act accordingly
blurInput: function () {
var $this = $(this).closest('.context-menu-item'),
data = $this.data(),
opt = data.contextMenu,
root = data.contextMenuRoot;
root.isInput = opt.isInput = false;
},
// :hover on menu
menuMouseenter: function () {
var root = $(this).data().contextMenuRoot;
root.hovering = true;
},
// :hover on menu
menuMouseleave: function (e) {
var root = $(this).data().contextMenuRoot;
if (root.$layer && root.$layer.is(e.relatedTarget)) {
root.hovering = false;
}
},
// :hover done manually so key handling is possible
itemMouseenter: function (e) {
var $this = $(this),
data = $this.data(),
opt = data.contextMenu,
root = data.contextMenuRoot;
root.hovering = true;
// abort if we're re-entering
if (e && root.$layer && root.$layer.is(e.relatedTarget)) {
e.preventDefault();
e.stopImmediatePropagation();
}
// make sure only one item is selected
(opt.$menu ? opt : root).$menu
.children('.' + root.classNames.hover).trigger('contextmenu:blur')
.children('.hover').trigger('contextmenu:blur');
if ($this.hasClass(root.classNames.disabled) || $this.hasClass(root.classNames.notSelectable)) {
opt.$selected = null;
return;
}
$this.trigger('contextmenu:focus');
},
// :hover done manually so key handling is possible
itemMouseleave: function (e) {
var $this = $(this),
data = $this.data(),
opt = data.contextMenu,
root = data.contextMenuRoot;
if (root !== opt && root.$layer && root.$layer.is(e.relatedTarget)) {
if (typeof root.$selected !== 'undefined' && root.$selected !== null) {
root.$selected.trigger('contextmenu:blur');
}
e.preventDefault();
e.stopImmediatePropagation();
root.$selected = opt.$selected = opt.$node;
return;
}
$this.trigger('contextmenu:blur');
},
// contextMenu item click
itemClick: function (e) {
var $this = $(this),
data = $this.data(),
opt = data.contextMenu,
root = data.contextMenuRoot,
key = data.contextMenuKey,
callback;
// abort if the key is unknown or disabled or is a menu
if (!opt.items[key] || $this.is('.' + root.classNames.disabled + ', .context-menu-submenu, .context-menu-separator, .' + root.classNames.notSelectable)) {
return;
}
e.preventDefault();
e.stopImmediatePropagation();
if ($.isFunction(root.callbacks[key]) && Object.prototype.hasOwnProperty.call(root.callbacks, key)) {
// item-specific callback
callback = root.callbacks[key];
} else if ($.isFunction(root.callback)) {
// default callback
callback = root.callback;
} else {
// no callback, no action
return;
}
// hide menu if callback doesn't stop that
if (callback.call(root.$trigger, key, root) !== false) {
root.$menu.trigger('contextmenu:hide');
} else if (root.$menu.parent().length) {
op.update.call(root.$trigger, root);
}
},
// ignore click events on input elements
inputClick: function (e) {
e.stopImmediatePropagation();
},
// hide <menu>
hideMenu: function (e, data) {
var root = $(this).data('contextMenuRoot');
op.hide.call(root.$trigger, root, data && data.force);
},
// focus <command>
focusItem: function (e) {
e.stopPropagation();
var $this = $(this),
data = $this.data(),
opt = data.contextMenu,
root = data.contextMenuRoot;
$this
.addClass([root.classNames.hover, root.classNames.visible].join(' '))
// select other items and included items
.parent().find('.context-menu-item').not($this)
.removeClass(root.classNames.visible)
.filter('.' + root.classNames.hover)
.trigger('contextmenu:blur');
// remember selected
opt.$selected = root.$selected = $this;
// position sub-menu - do after show so dumb $.ui.position can keep up
if (opt.$node) {
root.positionSubmenu.call(opt.$node, opt.$menu);
}
},
// blur <command>
blurItem: function (e) {
e.stopPropagation();
var $this = $(this),
data = $this.data(),
opt = data.contextMenu,
root = data.contextMenuRoot;
if (opt.autoHide) { // for tablets and touch screens this needs to remain
$this.removeClass(root.classNames.visible);
}
$this.removeClass(root.classNames.hover);
opt.$selected = null;
}
},
// operations
op = {
show: function (opt, x, y) {
var $trigger = $(this),
css = {};
// hide any open menus
$('#context-menu-layer').trigger('mousedown');
// backreference for callbacks
opt.$trigger = $trigger;
// show event
if (opt.events.show.call($trigger, opt) === false) {
$currentTrigger = null;
return;
}
// create or update context menu
op.update.call($trigger, opt);
// position menu
opt.position.call($trigger, opt, x, y);
// make sure we're in front
if (opt.zIndex) {
var additionalZValue = opt.zIndex;
// If opt.zIndex is a function, call the function to get the right zIndex.
if (typeof opt.zIndex === 'function') {
additionalZValue = opt.zIndex.call($trigger, opt);
}
css.zIndex = zindex($trigger) + additionalZValue;
}
// add layer
op.layer.call(opt.$menu, opt, css.zIndex);
// adjust sub-menu zIndexes
opt.$menu.find('ul').css('zIndex', css.zIndex + 1);
// position and show context menu
opt.$menu.css(css)[opt.animation.show](opt.animation.duration, function () {
$trigger.trigger('contextmenu:visible');
});
// make options available and set state
$trigger
.data('contextMenu', opt)
.addClass('context-menu-active');
// register key handler
$(document).off('keydown.contextMenu').on('keydown.contextMenu', handle.key);
// register autoHide handler
if (opt.autoHide) {
// mouse position handler
$(document).on('mousemove.contextMenuAutoHide', function (e) {
// need to capture the offset on mousemove,
// since the page might've been scrolled since activation
var pos = $trigger.offset();
pos.right = pos.left + $trigger.outerWidth();
pos.bottom = pos.top + $trigger.outerHeight();
if (opt.$layer && !opt.hovering && (!(e.pageX >= pos.left && e.pageX <= pos.right) || !(e.pageY >= pos.top && e.pageY <= pos.bottom))) {
/* Additional hover check after short time, you might just miss the edge of the menu */
setTimeout(function () {
if (!opt.hovering) { opt.$menu.trigger('contextmenu:hide'); }
}, 50);
}
});
}
},
hide: function (opt, force) {
var $trigger = $(this);
if (!opt) {
opt = $trigger.data('contextMenu') || {};
}
// hide event
if (!force && opt.events && opt.events.hide.call($trigger, opt) === false) {
return;
}
// remove options and revert state
$trigger
.removeData('contextMenu')
.removeClass('context-menu-active');
if (opt.$layer) {
// keep layer for a bit so the contextmenu event can be aborted properly by opera
setTimeout((function ($layer) {
return function () {
$layer.remove();
};
})(opt.$layer), 10);
try {
delete opt.$layer;
} catch (e) {
opt.$layer = null;
}
}
// remove handle
$currentTrigger = null;
// remove selected
opt.$menu.find('.' + opt.classNames.hover).trigger('contextmenu:blur');
opt.$selected = null;
// collapse all submenus
opt.$menu.find('.' + opt.classNames.visible).removeClass(opt.classNames.visible);
// unregister key and mouse handlers
// $(document).off('.contextMenuAutoHide keydown.contextMenu'); // http://bugs.jquery.com/ticket/10705
$(document).off('.contextMenuAutoHide').off('keydown.contextMenu');
// hide menu
if(opt.$menu){
opt.$menu[opt.animation.hide](opt.animation.duration, function () {
// tear down dynamically built menu after animation is completed.
if (opt.build) {
opt.$menu.remove();
$.each(opt, function (key) {
switch (key) {
case 'ns':
case 'selector':
case 'build':
case 'trigger':
return true;
default:
opt[key] = undefined;
try {
delete opt[key];
} catch (e) {
}
return true;
}
});
}
setTimeout(function () {
$trigger.trigger('contextmenu:hidden');
}, 10);
});
}
},
create: function (opt, root) {
if (root === undefined) {
root = opt;
}
// create contextMenu
opt.$menu = $('<ul class="context-menu-list"></ul>').addClass(opt.className || '').data({
'contextMenu': opt,
'contextMenuRoot': root
});
$.each(['callbacks', 'commands', 'inputs'], function (i, k) {
opt[k] = {};
if (!root[k]) {
root[k] = {};
}
});
if(!root.accesskeys){
root.accesskeys = {};
}
function createNameNode(item) {
var $name = $('<span></span>');
if (item._accesskey) {
if (item._beforeAccesskey) {
$name.append(document.createTextNode(item._beforeAccesskey));
}
$('<span></span>')
.addClass('context-menu-accesskey')
.text(item._accesskey)
.appendTo($name);
if (item._afterAccesskey) {
$name.append(document.createTextNode(item._afterAccesskey));
}
} else {
$name.text(item.name);
}
return $name;
}
// create contextMenu items
$.each(opt.items, function (key, item) {
var $t = $('<li class="context-menu-item"></li>').addClass(item.className || ''),
$label = null,
$input = null;
// iOS needs to see a click-event bound to an element to actually
// have the TouchEvents infrastructure trigger the click event
$t.on('click', $.noop);
// Make old school string seperator a real item so checks wont be
// akward later.
if (typeof item === 'string') {
item = { type : 'cm_seperator' };
}
item.$node = $t.data({
'contextMenu': opt,
'contextMenuRoot': root,
'contextMenuKey': key
});
// register accesskey
// NOTE: the accesskey attribute should be applicable to any element, but Safari5 and Chrome13 still can't do that
if (typeof item.accesskey !== 'undefined') {
var aks = splitAccesskey(item.accesskey);
for (var i = 0, ak; ak = aks[i]; i++) {
if (!root.accesskeys[ak]) {
root.accesskeys[ak] = item;
var matched = item.name.match(new RegExp('^(.*?)(' + ak + ')(.*)$', 'i'));
if (matched) {
item._beforeAccesskey = matched[1];
item._accesskey = matched[2];
item._afterAccesskey = matched[3];
}
break;
}
}
}
if (item.type && types[item.type]) {
// run custom type handler
types[item.type].call($t, item, opt, root);
// register commands
$.each([opt, root], function (i, k) {
k.commands[key] = item;
if ($.isFunction(item.callback)) {
k.callbacks[key] = item.callback;
}
});
} else {
// add label for input
if (item.type === 'cm_seperator') {
$t.addClass('context-menu-separator ' + root.classNames.notSelectable);
} else if (item.type === 'html') {
$t.addClass('context-menu-html ' + root.classNames.notSelectable);
} else if (item.type) {
$label = $('<label></label>').appendTo($t);
createNameNode(item).appendTo($label);
$t.addClass('context-menu-input');
opt.hasTypes = true;
$.each([opt, root], function (i, k) {
k.commands[key] = item;
k.inputs[key] = item;
});
} else if (item.items) {
item.type = 'sub';
}
switch (item.type) {
case 'cm_seperator':
break;
case 'text':
$input = $('<input type="text" value="1" name="" value="">')
.attr('name', 'context-menu-input-' + key)
.val(item.value || '')
.appendTo($label);
break;
case 'textarea':
$input = $('<textarea name=""></textarea>')
.attr('name', 'context-menu-input-' + key)
.val(item.value || '')
.appendTo($label);
if (item.height) {
$input.height(item.height);
}
break;
case 'checkbox':
$input = $('<input type="checkbox" value="1" name="" value="">')
.attr('name', 'context-menu-input-' + key)
.val(item.value || '')
.prop('checked', !!item.selected)
.prependTo($label);
break;
case 'radio':
$input = $('<input type="radio" value="1" name="" value="">')
.attr('name', 'context-menu-input-' + item.radio)
.val(item.value || '')
.prop('checked', !!item.selected)
.prependTo($label);
break;
case 'select':
$input = $('<select name="">')
.attr('name', 'context-menu-input-' + key)
.appendTo($label);
if (item.options) {
$.each(item.options, function (value, text) {
$('<option></option>').val(value).text(text).appendTo($input);
});
$input.val(item.selected);
}
break;
case 'sub':
createNameNode(item).appendTo($t);
item.appendTo = item.$node;
op.create(item, root);
$t.data('contextMenu', item).addClass('context-menu-submenu');
item.callback = null;
break;
case 'html':
$(item.html).appendTo($t);
break;
default:
$.each([opt, root], function (i, k) {
k.commands[key] = item;
if ($.isFunction(item.callback)) {
k.callbacks[key] = item.callback;
}
});
createNameNode(item).appendTo($t);
break;
}
// disable key listener in <input>
if (item.type && item.type !== 'sub' && item.type !== 'html' && item.type !== 'cm_seperator') {
$input
.on('focus', handle.focusInput)
.on('blur', handle.blurInput);
if (item.events) {
$input.on(item.events, opt);
}
}
// add icons
if (item.icon) {
if ($.isFunction(item.icon)) {
item._icon = item.icon.call(this, this, $t, key, item);
} else {
item._icon = root.classNames.icon + ' ' + root.classNames.icon + '-' + item.icon;
}
$t.addClass(item._icon);
}
}
// cache contained elements
item.$input = $input;
item.$label = $label;
// attach item to menu
$t.appendTo(opt.$menu);
// Disable text selection
if (!opt.hasTypes && $.support.eventSelectstart) {
// browsers support user-select: none,
// IE has a special event for text-selection
// browsers supporting neither will not be preventing text-selection
$t.on('selectstart.disableTextSelect', handle.abortevent);
}
});
// attach contextMenu to <body> (to bypass any possible overflow:hidden issues on parents of the trigger element)
if (!opt.$node) {
opt.$menu.css('display', 'none').addClass('context-menu-root');
}
opt.$menu.appendTo(opt.appendTo || document.body);
},
resize: function ($menu, nested) {
// determine widths of submenus, as CSS won't grow them automatically
// position:absolute within position:absolute; min-width:100; max-width:200; results in width: 100;
// kinda sucks hard...
// determine width of absolutely positioned element
$menu.css({position: 'absolute', display: 'block'});
// don't apply yet, because that would break nested elements' widths
$menu.data('width', Math.ceil($menu.outerWidth()));
// reset styles so they allow nested elements to grow/shrink naturally
$menu.css({
position: 'static',
minWidth: '0px',
maxWidth: '100000px'
});
// identify width of nested menus
$menu.find('> li > ul').each(function () {
op.resize($(this), true);
});
// reset and apply changes in the end because nested
// elements' widths wouldn't be calculatable otherwise
if (!nested) {
$menu.find('ul').addBack().css({
position: '',
display: '',
minWidth: '',
maxWidth: ''
}).outerWidth(function () {
return $(this).data('width');
});
}
},
update: function (opt, root) {
var $trigger = this;
if (root === undefined) {
root = opt;
op.resize(opt.$menu);
}
// re-check disabled for each item
opt.$menu.children().each(function () {
var $item = $(this),
key = $item.data('contextMenuKey'),
item = opt.items[key],
disabled = ($.isFunction(item.disabled) && item.disabled.call($trigger, key, root)) || item.disabled === true,
visible;
if ($.isFunction(item.visible)) {
visible = item.visible.call($trigger, key, root);
} else if (typeof item.visible !== 'undefined') {
visible = item.visible === true;
} else {
visible = true;
}
$item[visible ? 'show' : 'hide']();
// dis- / enable item
$item[disabled ? 'addClass' : 'removeClass'](root.classNames.disabled);
if ($.isFunction(item.icon)) {
$item.removeClass(item._icon);
item._icon = item.icon.call(this, $trigger, $item, key, item);
$item.addClass(item._icon);
}
if (item.type) {
// dis- / enable input elements
$item.find('input, select, textarea').prop('disabled', disabled);
// update input states
switch (item.type) {
case 'text':
case 'textarea':
item.$input.val(item.value || '');
break;
case 'checkbox':
case 'radio':
item.$input.val(item.value || '').prop('checked', !!item.selected);
break;
case 'select':
item.$input.val(item.selected || '');
break;
}
}
if (item.$menu) {
// update sub-menu
op.update.call($trigger, item, root);
}
});
},
layer: function (opt, zIndex) {
// add transparent layer for click area
// filter and background for Internet Explorer, Issue #23
var $layer = opt.$layer = $('<div id="context-menu-layer" style="position:fixed; z-index:' + zIndex + '; top:0; left:0; opacity: 0; filter: alpha(opacity=0); background-color: #000;"></div>')
.css({height: $win.height(), width: $win.width(), display: 'block'})
.data('contextMenuRoot', opt)
.insertBefore(this)
.on('contextmenu', handle.abortevent)
.on('mousedown', handle.layerClick);
// IE6 doesn't know position:fixed;
if (document.body.style.maxWidth === undefined) { // IE6 doesn't support maxWidth
$layer.css({
'position': 'absolute',
'height': $(document).height()
});
}
return $layer;
}
};
// split accesskey according to http://www.whatwg.org/specs/web-apps/current-work/multipage/editing.html#assigned-access-key
function splitAccesskey(val) {
var t = val.split(/\s+/),
keys = [];
for (var i = 0, k; k = t[i]; i++) {
k = k.charAt(0).toUpperCase(); // first character only
// theoretically non-accessible characters should be ignored, but different systems, different keyboard layouts, ... screw it.
// a map to look up already used access keys would be nice
keys.push(k);
}
return keys;
}
// handle contextMenu triggers
$.fn.contextMenu = function (operation) {
var $t = this, $o = operation;
if (this.length > 0) { // this is not a build on demand menu
if (operation === undefined) {
this.first().trigger('contextmenu');
} else if (operation.x !== undefined && operation.y !== undefined) {
this.first().trigger($.Event('contextmenu', { pageX: operation.x, pageY: operation.y, mouseButton: operation.button }));
} else if (operation === 'hide') {
var $menu = this.first().data('contextMenu') ? this.first().data('contextMenu').$menu : null;
if($menu){
$menu.trigger('contextmenu:hide');
}
} else if (operation === 'destroy') {
$.contextMenu('destroy', {context: this});
} else if ($.isPlainObject(operation)) {
operation.context = this;
$.contextMenu('create', operation);
} else if (operation) {
this.removeClass('context-menu-disabled');
} else if (!operation) {
this.addClass('context-menu-disabled');
}
} else {
$.each(menus, function () {
if (this.selector === $t.selector) {
$o.data = this;
$.extend($o.data, {trigger: 'demand'});
}
});
handle.contextmenu.call($o.target, $o);
}
return this;
};
// manage contextMenu instances
$.contextMenu = function (operation, options) {
if (typeof operation !== 'string') {
options = operation;
operation = 'create';
}
if (typeof options === 'string') {
options = {selector: options};
} else if (options === undefined) {
options = {};
}
// merge with default options
var o = $.extend(true, {}, defaults, options || {});
var $document = $(document);
var $context = $document;
var _hasContext = false;
if (!o.context || !o.context.length) {
o.context = document;
} else {
// you never know what they throw at you...
$context = $(o.context).first();
o.context = $context.get(0);
_hasContext = o.context !== document;
}
switch (operation) {
case 'create':
// no selector no joy
if (!o.selector) {
throw new Error('No selector specified');
}
// make sure internal classes are not bound to
if (o.selector.match(/.context-menu-(list|item|input)($|\s)/)) {
throw new Error('Cannot bind to selector "' + o.selector + '" as it contains a reserved className');
}
if (!o.build && (!o.items || $.isEmptyObject(o.items))) {
throw new Error('No Items specified');
}
counter++;
o.ns = '.contextMenu' + counter;
if (!_hasContext) {
namespaces[o.selector] = o.ns;
}
menus[o.ns] = o;
// default to right click
if (!o.trigger) {
o.trigger = 'right';
}
if (!initialized) {
// make sure item click is registered first
$document
.on({
'contextmenu:hide.contextMenu': handle.hideMenu,
'prevcommand.contextMenu': handle.prevItem,
'nextcommand.contextMenu': handle.nextItem,
'contextmenu.contextMenu': handle.abortevent,
'mouseenter.contextMenu': handle.menuMouseenter,
'mouseleave.contextMenu': handle.menuMouseleave
}, '.context-menu-list')
.on('mouseup.contextMenu', '.context-menu-input', handle.inputClick)
.on({
'mouseup.contextMenu': handle.itemClick,
'contextmenu:focus.contextMenu': handle.focusItem,
'contextmenu:blur.contextMenu': handle.blurItem,
'contextmenu.contextMenu': handle.abortevent,
'mouseenter.contextMenu': handle.itemMouseenter,
'mouseleave.contextMenu': handle.itemMouseleave
}, '.context-menu-item');
initialized = true;
}
// engage native contextmenu event
$context
.on('contextmenu' + o.ns, o.selector, o, handle.contextmenu);
if (_hasContext) {
// add remove hook, just in case
$context.on('remove' + o.ns, function () {
$(this).contextMenu('destroy');
});
}
switch (o.trigger) {
case 'hover':
$context
.on('mouseenter' + o.ns, o.selector, o, handle.mouseenter)
.on('mouseleave' + o.ns, o.selector, o, handle.mouseleave);
break;
case 'left':
$context.on('click' + o.ns, o.selector, o, handle.click);
break;
/*
default:
// http://www.quirksmode.org/dom/events/contextmenu.html
$document
.on('mousedown' + o.ns, o.selector, o, handle.mousedown)
.on('mouseup' + o.ns, o.selector, o, handle.mouseup);
break;
*/
}
// create menu
if (!o.build) {
op.create(o);
}
break;
case 'destroy':
var $visibleMenu;
if (_hasContext) {
// get proper options
var context = o.context;
$.each(menus, function (ns, o) {
if (o.context !== context) {
return true;
}
$visibleMenu = $('.context-menu-list').filter(':visible');
if ($visibleMenu.length && $visibleMenu.data().contextMenuRoot.$trigger.is($(o.context).find(o.selector))) {
$visibleMenu.trigger('contextmenu:hide', {force: true});
}
try {
if (menus[o.ns].$menu) {
menus[o.ns].$menu.remove();
}
delete menus[o.ns];
} catch (e) {
menus[o.ns] = null;
}
$(o.context).off(o.ns);
return true;
});
} else if (!o.selector) {
$document.off('.contextMenu .contextMenuAutoHide');
$.each(menus, function (ns, o) {
$(o.context).off(o.ns);
});
namespaces = {};
menus = {};
counter = 0;
initialized = false;
$('#context-menu-layer, .context-menu-list').remove();
} else if (namespaces[o.selector]) {
$visibleMenu = $('.context-menu-list').filter(':visible');
if ($visibleMenu.length && $visibleMenu.data().contextMenuRoot.$trigger.is(o.selector)) {
$visibleMenu.trigger('contextmenu:hide', {force: true});
}
try {
if (menus[namespaces[o.selector]].$menu) {
menus[namespaces[o.selector]].$menu.remove();
}
delete menus[namespaces[o.selector]];
} catch (e) {
menus[namespaces[o.selector]] = null;
}
$document.off(namespaces[o.selector]);
}
break;
case 'html5':
// if <command> or <menuitem> are not handled by the browser,
// or options was a bool true,
// initialize $.contextMenu for them
if ((!$.support.htmlCommand && !$.support.htmlMenuitem) || (typeof options === 'boolean' && options)) {
$('menu[type="context"]').each(function () {
if (this.id) {
$.contextMenu({
selector: '[contextmenu=' + this.id + ']',
items: $.contextMenu.fromMenu(this)
});
}
}).css('display', 'none');
}
break;
default:
throw new Error('Unknown operation "' + operation + '"');
}
return this;
};
// import values into <input> commands
$.contextMenu.setInputValues = function (opt, data) {
if (data === undefined) {
data = {};
}
$.each(opt.inputs, function (key, item) {
switch (item.type) {
case 'text':
case 'textarea':
item.value = data[key] || '';
break;
case 'checkbox':
item.selected = data[key] ? true : false;
break;
case 'radio':
item.selected = (data[item.radio] || '') === item.value;
break;
case 'select':
item.selected = data[key] || '';
break;
}
});
};
// export values from <input> commands
$.contextMenu.getInputValues = function (opt, data) {
if (data === undefined) {
data = {};
}
$.each(opt.inputs, function (key, item) {
switch (item.type) {
case 'text':
case 'textarea':
case 'select':
data[key] = item.$input.val();
break;
case 'checkbox':
data[key] = item.$input.prop('checked');
break;
case 'radio':
if (item.$input.prop('checked')) {
data[item.radio] = item.value;
}
break;
}
});
return data;
};
// find <label for="xyz">
function inputLabel(node) {
return (node.id && $('label[for="' + node.id + '"]').val()) || node.name;
}
// convert <menu> to items object
function menuChildren(items, $children, counter) {
if (!counter) {
counter = 0;
}
$children.each(function () {
var $node = $(this),
node = this,
nodeName = this.nodeName.toLowerCase(),
label,
item;
// extract <label><input>
if (nodeName === 'label' && $node.find('input, textarea, select').length) {
label = $node.text();
$node = $node.children().first();
node = $node.get(0);
nodeName = node.nodeName.toLowerCase();
}
/*
* <menu> accepts flow-content as children. that means <embed>, <canvas> and such are valid menu items.
* Not being the sadistic kind, $.contextMenu only accepts:
* <command>, <menuitem>, <hr>, <span>, <p> <input [text, radio, checkbox]>, <textarea>, <select> and of course <menu>.
* Everything else will be imported as an html node, which is not interfaced with contextMenu.
*/
// http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#concept-command
switch (nodeName) {
// http://www.whatwg.org/specs/web-apps/current-work/multipage/interactive-elements.html#the-menu-element
case 'menu':
item = {name: $node.attr('label'), items: {}};
counter = menuChildren(item.items, $node.children(), counter);
break;
// http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#using-the-a-element-to-define-a-command
case 'a':
// http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#using-the-button-element-to-define-a-command
case 'button':
item = {
name: $node.text(),
disabled: !!$node.attr('disabled'),
callback: (function () {
return function () {
$node.click();
};
})()
};
break;
// http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#using-the-command-element-to-define-a-command
case 'menuitem':
case 'command':
switch ($node.attr('type')) {
case undefined:
case 'command':
case 'menuitem':
item = {
name: $node.attr('label'),
disabled: !!$node.attr('disabled'),
icon: $node.attr('icon'),
callback: (function () {
return function () {
$node.click();
};
})()
};
break;
case 'checkbox':
item = {
type: 'checkbox',
disabled: !!$node.attr('disabled'),
name: $node.attr('label'),
selected: !!$node.attr('checked')
};
break;
case 'radio':
item = {
type: 'radio',
disabled: !!$node.attr('disabled'),
name: $node.attr('label'),
radio: $node.attr('radiogroup'),
value: $node.attr('id'),
selected: !!$node.attr('checked')
};
break;
default:
item = undefined;
}
break;
case 'hr':
item = '-------';
break;
case 'input':
switch ($node.attr('type')) {
case 'text':
item = {
type: 'text',
name: label || inputLabel(node),
disabled: !!$node.attr('disabled'),
value: $node.val()
};
break;
case 'checkbox':
item = {
type: 'checkbox',
name: label || inputLabel(node),
disabled: !!$node.attr('disabled'),
selected: !!$node.attr('checked')
};
break;
case 'radio':
item = {
type: 'radio',
name: label || inputLabel(node),
disabled: !!$node.attr('disabled'),
radio: !!$node.attr('name'),
value: $node.val(),
selected: !!$node.attr('checked')
};
break;
default:
item = undefined;
break;
}
break;
case 'select':
item = {
type: 'select',
name: label || inputLabel(node),
disabled: !!$node.attr('disabled'),
selected: $node.val(),
options: {}
};
$node.children().each(function () {
item.options[this.value] = $(this).text();
});
break;
case 'textarea':
item = {
type: 'textarea',
name: label || inputLabel(node),
disabled: !!$node.attr('disabled'),
value: $node.val()
};
break;
case 'label':
break;
default:
item = {type: 'html', html: $node.clone(true)};
break;
}
if (item) {
counter++;
items['key' + counter] = item;
}
});
return counter;
}
// convert html5 menu
$.contextMenu.fromMenu = function (element) {
var $this = $(element),
items = {};
menuChildren(items, $this.children());
return items;
};
// make defaults accessible
$.contextMenu.defaults = defaults;
$.contextMenu.types = types;
// export internal functions - undocumented, for hacking only!
$.contextMenu.handle = handle;
$.contextMenu.op = op;
$.contextMenu.menus = menus;
});