2  * jQuery contextMenu - Plugin for simple contextMenu handling
 
   6  * Authors: Rodney Rehm, Addy Osmani (patches for FF)
 
   7  * Web: http://medialize.github.com/jQuery-contextMenu/
 
  10  *   MIT License http://www.opensource.org/licenses/mit-license
 
  11  *   GPL v3 http://opensource.org/licenses/GPL-3.0
 
  15 (function($, undefined){
 
  18         // ARIA stuff: menuitem, menuitemcheckbox und menuitemradio
 
  19         // create <menu> structure if $.support[htmlCommand || htmlMenuitem] and !opt.disableNative
 
  21 // determine html5 compatibility
 
  22 $.support.htmlMenuitem = ('HTMLMenuItemElement' in window);
 
  23 $.support.htmlCommand = ('HTMLCommandElement' in window);
 
  24 $.support.eventSelectstart = ("onselectstart" in document.documentElement);
 
  25 /* // should the need arise, test for css user-select
 
  26 $.support.cssUserSelect = (function(){
 
  28         e = document.createElement('div');
 
  30     $.each('Moz|Webkit|Khtml|O|ms|Icab|'.split('|'), function(i, prefix) {
 
  31         var propCC = prefix + (prefix ? 'U' : 'u') + 'serSelect',
 
  32             prop = (prefix ? ('-' + prefix.toLowerCase() + '-') : '') + 'user-select';
 
  34         e.style.cssText = prop + ': text;';
 
  35         if (e.style[propCC] == 'text') {
 
  47 if (!$.ui || !$.ui.widget) {
 
  48     // duck punch $.cleanData like jQueryUI does to get that remove event
 
  49     // https://github.com/jquery/jquery-ui/blob/master/ui/jquery.ui.widget.js#L16-24
 
  50     var _cleanData = $.cleanData;
 
  51     $.cleanData = function( elems ) {
 
  52         for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) {
 
  54                 $( elem ).triggerHandler( "remove" );
 
  55                 // http://bugs.jquery.com/ticket/8235
 
  62 var // currently active contextMenu trigger
 
  63     $currentTrigger = null,
 
  64     // is contextMenu initialized with at least one menu?
 
  68     // number of registered menus
 
  70     // mapping selector to namespace
 
  72     // mapping namespace to options
 
  74     // custom command type handlers
 
  78         // selector of contextMenu trigger
 
  80         // where to append the menu to
 
  82         // method to trigger context menu ["right", "left", "hover"]
 
  84         // hide menu when mouse leaves trigger / menu elements
 
  86         // ms to wait before showing a hover-triggered context menu
 
  88         // flag denoting if a second trigger should simply move (true) or rebuild (false) an open menu
 
  89         // as long as the trigger happened on one of the trigger-element's child nodes
 
  91         // determine position to show menu at
 
  92         determinePosition: function($menu) {
 
  93             // position to the lower middle of the trigger element
 
  94             if ($.ui && $.ui.position) {
 
  95                 // .position() is provided as a jQuery UI utility
 
  96                 // (...and it won't work on hidden elements)
 
  97                 $menu.css('display', 'block').position({
 
 103                 }).css('display', 'none');
 
 105                 // determine contextMenu position
 
 106                 var offset = this.offset();
 
 107                 offset.top += this.outerHeight();
 
 108                 offset.left += this.outerWidth() / 2 - $menu.outerWidth() / 2;
 
 113         position: function(opt, x, y) {
 
 116             // determine contextMenu position
 
 118                 opt.determinePosition.call(this, opt.$menu);
 
 120             } else if (x === "maintain" && y === "maintain") {
 
 121                 // x and y must not be changed (after re-show on command click)
 
 122                 offset = opt.$menu.position();
 
 124                 // x and y are given (by mouse event)
 
 125                 offset = {top: y, left: x};
 
 128             // correct offset if viewport demands it
 
 129             var bottom = $win.scrollTop() + $win.height(),
 
 130                 right = $win.scrollLeft() + $win.width(),
 
 131                 height = opt.$menu.height(),
 
 132                 width = opt.$menu.width();
 
 134             if (offset.top + height > bottom) {
 
 135                 offset.top -= height;
 
 138             if (offset.left + width > right) {
 
 139                 offset.left -= width;
 
 142             opt.$menu.css(offset);
 
 144         // position the sub-menu
 
 145         positionSubmenu: function($menu) {
 
 146             if ($.ui && $.ui.position) {
 
 147                 // .position() is provided as a jQuery UI utility
 
 148                 // (...and it won't work on hidden elements)
 
 149                 $menu.css('display', 'block').position({
 
 153                     collision: "flipfit fit"
 
 154                 }).css('display', '');
 
 156                 // determine contextMenu position
 
 159                     left: this.outerWidth()
 
 164         // offset to add to zIndex
 
 166         // show hide animation settings
 
 179         // list of contextMenu items
 
 182     // mouse position for hover activation
 
 189     zindex = function($t) {
 
 194             zin = Math.max(zin, parseInt($tt.css('z-index'), 10) || 0);
 
 196             if (!$tt || !$tt.length || "html body".indexOf($tt.prop('nodeName').toLowerCase()) > -1 ) {
 
 206         abortevent: function(e){
 
 208             e.stopImmediatePropagation();
 
 211         // contextmenu show dispatcher
 
 212         contextmenu: function(e) {
 
 215             // disable actual context-menu
 
 217             e.stopImmediatePropagation();
 
 219             // abort native-triggered events unless we're triggering on right click
 
 220             if (e.data.trigger != 'right' && e.originalEvent) {
 
 224             // abort event if menu is visible for this trigger
 
 225             if ($this.hasClass('context-menu-active')) {
 
 229             if (!$this.hasClass('context-menu-disabled')) {
 
 230                 // theoretically need to fire a show event at <menu>
 
 231                 // http://www.whatwg.org/specs/web-apps/current-work/multipage/interactive-elements.html#context-menus
 
 232                 // var evt = jQuery.Event("show", { data: data, pageX: e.pageX, pageY: e.pageY, relatedTarget: this });
 
 233                 // e.data.$menu.trigger(evt);
 
 235                 $currentTrigger = $this;
 
 237                     var built = e.data.build($currentTrigger, e);
 
 238                     // abort if build() returned false
 
 239                     if (built === false) {
 
 243                     // dynamically build menu on invocation
 
 244                     e.data = $.extend(true, {}, defaults, e.data, built || {});
 
 246                     // abort if there are no items to display
 
 247                     if (!e.data.items || $.isEmptyObject(e.data.items)) {
 
 248                         // Note: jQuery captures and ignores errors from event handlers
 
 249                         if (window.console) {
 
 250                             (console.error || console.log)("No items specified to show in contextMenu");
 
 253                         throw new Error('No Items sepcified');
 
 256                     // backreference for custom command type creation
 
 257                     e.data.$trigger = $currentTrigger;
 
 262                 op.show.call($this, e.data, e.pageX, e.pageY);
 
 265         // contextMenu left-click trigger
 
 268             e.stopImmediatePropagation();
 
 269             $(this).trigger($.Event("contextmenu", { data: e.data, pageX: e.pageX, pageY: e.pageY }));
 
 271         // contextMenu right-click trigger
 
 272         mousedown: function(e) {
 
 273             // register mouse down
 
 276             // hide any previous menus
 
 277             if ($currentTrigger && $currentTrigger.length && !$currentTrigger.is($this)) {
 
 278                 $currentTrigger.data('contextMenu').$menu.trigger('contextmenu:hide');
 
 281             // activate on right click
 
 283                 $currentTrigger = $this.data('contextMenuActive', true);
 
 286         // contextMenu right-click trigger
 
 287         mouseup: function(e) {
 
 290             if ($this.data('contextMenuActive') && $currentTrigger && $currentTrigger.length && $currentTrigger.is($this) && !$this.hasClass('context-menu-disabled')) {
 
 292                 e.stopImmediatePropagation();
 
 293                 $currentTrigger = $this;
 
 294                 $this.trigger($.Event("contextmenu", { data: e.data, pageX: e.pageX, pageY: e.pageY }));
 
 297             $this.removeData('contextMenuActive');
 
 299         // contextMenu hover trigger
 
 300         mouseenter: function(e) {
 
 302                 $related = $(e.relatedTarget),
 
 303                 $document = $(document);
 
 305             // abort if we're coming from a menu
 
 306             if ($related.is('.context-menu-list') || $related.closest('.context-menu-list').length) {
 
 310             // abort if a menu is shown
 
 311             if ($currentTrigger && $currentTrigger.length) {
 
 315             hoveract.pageX = e.pageX;
 
 316             hoveract.pageY = e.pageY;
 
 317             hoveract.data = e.data;
 
 318             $document.on('mousemove.contextMenuShow', handle.mousemove);
 
 319             hoveract.timer = setTimeout(function() {
 
 320                 hoveract.timer = null;
 
 321                 $document.off('mousemove.contextMenuShow');
 
 322                 $currentTrigger = $this;
 
 323                 $this.trigger($.Event("contextmenu", { data: hoveract.data, pageX: hoveract.pageX, pageY: hoveract.pageY }));
 
 326         // contextMenu hover trigger
 
 327         mousemove: function(e) {
 
 328             hoveract.pageX = e.pageX;
 
 329             hoveract.pageY = e.pageY;
 
 331         // contextMenu hover trigger
 
 332         mouseleave: function(e) {
 
 333             // abort if we're leaving for a menu
 
 334             var $related = $(e.relatedTarget);
 
 335             if ($related.is('.context-menu-list') || $related.closest('.context-menu-list').length) {
 
 340                 clearTimeout(hoveract.timer);
 
 343             hoveract.timer = null;
 
 346         // click on layer to hide contextMenu
 
 347         layerClick: function(e) {
 
 349                 root = $this.data('contextMenuRoot'),
 
 359             e.stopImmediatePropagation();
 
 361             setTimeout(function() {
 
 362                 var $window, hideshow, possibleTarget;
 
 363                 var triggerAction = ((root.trigger == 'left' && button === 0) || (root.trigger == 'right' && button === 2));
 
 365                 // find the element that would've been clicked, wasn't the layer in the way
 
 366                 if (document.elementFromPoint) {
 
 368                     target = document.elementFromPoint(x - $win.scrollLeft(), y - $win.scrollTop());
 
 372                 if (root.reposition && triggerAction) {
 
 373                     if (document.elementFromPoint) {
 
 374                         if (root.$trigger.is(target) || root.$trigger.has(target).length) {
 
 375                             root.position.call(root.$trigger, root, x, y);
 
 379                         offset = root.$trigger.offset();
 
 381                         // while this looks kinda awful, it's the best way to avoid
 
 382                         // unnecessarily calculating any positions
 
 383                         offset.top += $window.scrollTop();
 
 384                         if (offset.top <= e.pageY) {
 
 385                             offset.left += $window.scrollLeft();
 
 386                             if (offset.left <= e.pageX) {
 
 387                                 offset.bottom = offset.top + root.$trigger.outerHeight();
 
 388                                 if (offset.bottom >= e.pageY) {
 
 389                                     offset.right = offset.left + root.$trigger.outerWidth();
 
 390                                     if (offset.right >= e.pageX) {
 
 392                                         root.position.call(root.$trigger, root, x, y);
 
 401                 if (target && triggerAction) {
 
 402                     root.$trigger.one('contextmenu:hidden', function() {
 
 403                         $(target).contextMenu({x: x, y: y});
 
 407                 root.$menu.trigger('contextmenu:hide');
 
 410         // key handled :hover
 
 411         keyStop: function(e, opt) {
 
 419             var opt = $currentTrigger.data('contextMenu') || {};
 
 424                     handle.keyStop(e, opt);
 
 425                     // if keyCode is [38 (up)] or [9 (tab) with shift]
 
 427                         if (e.keyCode == 9 && e.shiftKey) {
 
 429                             opt.$selected && opt.$selected.find('input, textarea, select').blur();
 
 430                             opt.$menu.trigger('prevcommand');
 
 432                         } else if (e.keyCode == 38 && opt.$selected.find('input, textarea, select').prop('type') == 'checkbox') {
 
 433                             // checkboxes don't capture this key
 
 437                     } else if (e.keyCode != 9 || e.shiftKey) {
 
 438                         opt.$menu.trigger('prevcommand');
 
 443                 // case 9: // tab - reached through omitted break;
 
 445                     handle.keyStop(e, opt);
 
 447                         if (e.keyCode == 9) {
 
 449                             opt.$selected && opt.$selected.find('input, textarea, select').blur();
 
 450                             opt.$menu.trigger('nextcommand');
 
 452                         } else if (e.keyCode == 40 && opt.$selected.find('input, textarea, select').prop('type') == 'checkbox') {
 
 453                             // checkboxes don't capture this key
 
 458                         opt.$menu.trigger('nextcommand');
 
 464                     handle.keyStop(e, opt);
 
 465                     if (opt.isInput || !opt.$selected || !opt.$selected.length) {
 
 469                     if (!opt.$selected.parent().hasClass('context-menu-root')) {
 
 470                         var $parent = opt.$selected.parent().parent();
 
 471                         opt.$selected.trigger('contextmenu:blur');
 
 472                         opt.$selected = $parent;
 
 478                     handle.keyStop(e, opt);
 
 479                     if (opt.isInput || !opt.$selected || !opt.$selected.length) {
 
 483                     var itemdata = opt.$selected.data('contextMenu') || {};
 
 484                     if (itemdata.$menu && opt.$selected.hasClass('context-menu-submenu')) {
 
 485                         opt.$selected = null;
 
 486                         itemdata.$selected = null;
 
 487                         itemdata.$menu.trigger('nextcommand');
 
 494                     if (opt.$selected && opt.$selected.find('input, textarea, select').length) {
 
 497                         (opt.$selected && opt.$selected.parent() || opt.$menu)
 
 498                             .children(':not(.disabled, .not-selectable)')[e.keyCode == 36 ? 'first' : 'last']()
 
 499                             .trigger('contextmenu:focus');
 
 506                     handle.keyStop(e, opt);
 
 508                         if (opt.$selected && !opt.$selected.is('textarea, select')) {
 
 514                     opt.$selected && opt.$selected.trigger('mouseup');
 
 519                 case 34: // page down
 
 520                     // prevent browser from scrolling down while menu is visible
 
 521                     handle.keyStop(e, opt);
 
 525                     handle.keyStop(e, opt);
 
 526                     opt.$menu.trigger('contextmenu:hide');
 
 530                     var k = (String.fromCharCode(e.keyCode)).toUpperCase();
 
 531                     if (opt.accesskeys[k]) {
 
 532                         // according to the specs accesskeys must be invoked immediately
 
 533                         opt.accesskeys[k].$node.trigger(opt.accesskeys[k].$menu
 
 534                             ? 'contextmenu:focus'
 
 541             // pass event to selected item, 
 
 542             // stop propagation to avoid endless recursion
 
 544             opt.$selected && opt.$selected.trigger(e);
 
 547         // select previous possible command in menu
 
 548         prevItem: function(e) {
 
 550             var opt = $(this).data('contextMenu') || {};
 
 552             // obtain currently selected menu
 
 554                 var $s = opt.$selected;
 
 555                 opt = opt.$selected.parent().data('contextMenu') || {};
 
 559             var $children = opt.$menu.children(),
 
 560                 $prev = !opt.$selected || !opt.$selected.prev().length ? $children.last() : opt.$selected.prev(),
 
 564             while ($prev.hasClass('disabled') || $prev.hasClass('not-selectable')) {
 
 565                 if ($prev.prev().length) {
 
 566                     $prev = $prev.prev();
 
 568                     $prev = $children.last();
 
 570                 if ($prev.is($round)) {
 
 571                     // break endless loop
 
 578                 handle.itemMouseleave.call(opt.$selected.get(0), e);
 
 582             handle.itemMouseenter.call($prev.get(0), e);
 
 585             var $input = $prev.find('input, textarea, select');
 
 590         // select next possible command in menu
 
 591         nextItem: function(e) {
 
 593             var opt = $(this).data('contextMenu') || {};
 
 595             // obtain currently selected menu
 
 597                 var $s = opt.$selected;
 
 598                 opt = opt.$selected.parent().data('contextMenu') || {};
 
 602             var $children = opt.$menu.children(),
 
 603                 $next = !opt.$selected || !opt.$selected.next().length ? $children.first() : opt.$selected.next(),
 
 607             while ($next.hasClass('disabled') || $next.hasClass('not-selectable')) {
 
 608                 if ($next.next().length) {
 
 609                     $next = $next.next();
 
 611                     $next = $children.first();
 
 613                 if ($next.is($round)) {
 
 614                     // break endless loop
 
 621                 handle.itemMouseleave.call(opt.$selected.get(0), e);
 
 625             handle.itemMouseenter.call($next.get(0), e);
 
 628             var $input = $next.find('input, textarea, select');
 
 634         // flag that we're inside an input so the key handler can act accordingly
 
 635         focusInput: function(e) {
 
 636             var $this = $(this).closest('.context-menu-item'),
 
 638                 opt = data.contextMenu,
 
 639                 root = data.contextMenuRoot;
 
 641             root.$selected = opt.$selected = $this;
 
 642             root.isInput = opt.isInput = true;
 
 644         // flag that we're inside an input so the key handler can act accordingly
 
 645         blurInput: function(e) {
 
 646             var $this = $(this).closest('.context-menu-item'),
 
 648                 opt = data.contextMenu,
 
 649                 root = data.contextMenuRoot;
 
 651             root.isInput = opt.isInput = false;
 
 655         menuMouseenter: function(e) {
 
 656             var root = $(this).data().contextMenuRoot;
 
 657             root.hovering = true;
 
 660         menuMouseleave: function(e) {
 
 661             var root = $(this).data().contextMenuRoot;
 
 662             if (root.$layer && root.$layer.is(e.relatedTarget)) {
 
 663                 root.hovering = false;
 
 667         // :hover done manually so key handling is possible
 
 668         itemMouseenter: function(e) {
 
 671                 opt = data.contextMenu,
 
 672                 root = data.contextMenuRoot;
 
 674             root.hovering = true;
 
 676             // abort if we're re-entering
 
 677             if (e && root.$layer && root.$layer.is(e.relatedTarget)) {
 
 679                 e.stopImmediatePropagation();
 
 682             // make sure only one item is selected
 
 683             (opt.$menu ? opt : root).$menu
 
 684                 .children('.hover').trigger('contextmenu:blur');
 
 686             if ($this.hasClass('disabled') || $this.hasClass('not-selectable')) {
 
 687                 opt.$selected = null;
 
 691             $this.trigger('contextmenu:focus');
 
 693         // :hover done manually so key handling is possible
 
 694         itemMouseleave: function(e) {
 
 697                 opt = data.contextMenu,
 
 698                 root = data.contextMenuRoot;
 
 700             if (root !== opt && root.$layer && root.$layer.is(e.relatedTarget)) {
 
 701                 root.$selected && root.$selected.trigger('contextmenu:blur');
 
 703                 e.stopImmediatePropagation();
 
 704                 root.$selected = opt.$selected = opt.$node;
 
 708             $this.trigger('contextmenu:blur');
 
 710         // contextMenu item click
 
 711         itemClick: function(e) {
 
 714                 opt = data.contextMenu,
 
 715                 root = data.contextMenuRoot,
 
 716                 key = data.contextMenuKey,
 
 719             // abort if the key is unknown or disabled or is a menu
 
 720             if (!opt.items[key] || $this.is('.disabled, .context-menu-submenu, .context-menu-separator, .not-selectable')) {
 
 725             e.stopImmediatePropagation();
 
 727             if ($.isFunction(root.callbacks[key]) && Object.prototype.hasOwnProperty.call(root.callbacks, key)) {
 
 728                 // item-specific callback
 
 729                 callback = root.callbacks[key];
 
 730             } else if ($.isFunction(root.callback)) {
 
 732                 callback = root.callback;                
 
 734                 // no callback, no action
 
 738             // hide menu if callback doesn't stop that
 
 739             if (callback.call(root.$trigger, key, root) !== false) {
 
 740                 root.$menu.trigger('contextmenu:hide');
 
 741             } else if (root.$menu.parent().length) {
 
 742                 op.update.call(root.$trigger, root);
 
 745         // ignore click events on input elements
 
 746         inputClick: function(e) {
 
 747             e.stopImmediatePropagation();
 
 751         hideMenu: function(e, data) {
 
 752             var root = $(this).data('contextMenuRoot');
 
 753             op.hide.call(root.$trigger, root, data && data.force);
 
 756         focusItem: function(e) {
 
 760                 opt = data.contextMenu,
 
 761                 root = data.contextMenuRoot;
 
 763             $this.addClass('hover')
 
 764                 .siblings('.hover').trigger('contextmenu:blur');
 
 767             opt.$selected = root.$selected = $this;
 
 769             // position sub-menu - do after show so dumb $.ui.position can keep up
 
 771                 root.positionSubmenu.call(opt.$node, opt.$menu);
 
 775         blurItem: function(e) {
 
 779                 opt = data.contextMenu,
 
 780                 root = data.contextMenuRoot;
 
 782             $this.removeClass('hover');
 
 783             opt.$selected = null;
 
 788         show: function(opt, x, y) {
 
 789             var $trigger = $(this),
 
 793             // hide any open menus
 
 794             $('#context-menu-layer').trigger('mousedown');
 
 796             // backreference for callbacks
 
 797             opt.$trigger = $trigger;
 
 800             if (opt.events.show.call($trigger, opt) === false) {
 
 801                 $currentTrigger = null;
 
 805             // create or update context menu
 
 806             op.update.call($trigger, opt);
 
 809             opt.position.call($trigger, opt, x, y);
 
 811             // make sure we're in front
 
 813                 css.zIndex = zindex($trigger) + opt.zIndex;
 
 817             op.layer.call(opt.$menu, opt, css.zIndex);
 
 819             // adjust sub-menu zIndexes
 
 820             opt.$menu.find('ul').css('zIndex', css.zIndex + 1);
 
 822             // position and show context menu
 
 823             opt.$menu.css( css )[opt.animation.show](opt.animation.duration, function() {
 
 824                 $trigger.trigger('contextmenu:visible');
 
 826             // make options available and set state
 
 828                 .data('contextMenu', opt)
 
 829                 .addClass("context-menu-active");
 
 831             // register key handler
 
 832             $(document).off('keydown.contextMenu').on('keydown.contextMenu', handle.key);
 
 833             // register autoHide handler
 
 835                 // mouse position handler
 
 836                 $(document).on('mousemove.contextMenuAutoHide', function(e) {
 
 837                     // need to capture the offset on mousemove,
 
 838                     // since the page might've been scrolled since activation
 
 839                     var pos = $trigger.offset();
 
 840                     pos.right = pos.left + $trigger.outerWidth();
 
 841                     pos.bottom = pos.top + $trigger.outerHeight();
 
 843                     if (opt.$layer && !opt.hovering && (!(e.pageX >= pos.left && e.pageX <= pos.right) || !(e.pageY >= pos.top && e.pageY <= pos.bottom))) {
 
 844                         // if mouse in menu...
 
 845                         opt.$menu.trigger('contextmenu:hide');
 
 850         hide: function(opt, force) {
 
 851             var $trigger = $(this);
 
 853                 opt = $trigger.data('contextMenu') || {};
 
 857             if (!force && opt.events && opt.events.hide.call($trigger, opt) === false) {
 
 861             // remove options and revert state
 
 863                 .removeData('contextMenu')
 
 864                 .removeClass("context-menu-active");
 
 867                 // keep layer for a bit so the contextmenu event can be aborted properly by opera
 
 868                 setTimeout((function($layer) {
 
 882             $currentTrigger = null;
 
 884             opt.$menu.find('.hover').trigger('contextmenu:blur');
 
 885             opt.$selected = null;
 
 886             // unregister key and mouse handlers
 
 887             //$(document).off('.contextMenuAutoHide keydown.contextMenu'); // http://bugs.jquery.com/ticket/10705
 
 888             $(document).off('.contextMenuAutoHide').off('keydown.contextMenu');
 
 890             opt.$menu && opt.$menu[opt.animation.hide](opt.animation.duration, function (){
 
 891                 // tear down dynamically built menu after animation is completed.
 
 894                     $.each(opt, function(key, value) {
 
 903                                 opt[key] = undefined;
 
 912                 setTimeout(function() {
 
 913                     $trigger.trigger('contextmenu:hidden');
 
 917         create: function(opt, root) {
 
 918             if (root === undefined) {
 
 921             // create contextMenu
 
 922             opt.$menu = $('<ul class="context-menu-list"></ul>').addClass(opt.className || "").data({
 
 924                 'contextMenuRoot': root
 
 927             $.each(['callbacks', 'commands', 'inputs'], function(i,k){
 
 934             root.accesskeys || (root.accesskeys = {});
 
 936             // create contextMenu items
 
 937             $.each(opt.items, function(key, item){
 
 938                 var $t = $('<li class="context-menu-item"></li>').addClass(item.className || ""),
 
 942                 // iOS needs to see a click-event bound to an element to actually
 
 943                 // have the TouchEvents infrastructure trigger the click event
 
 944                 $t.on('click', $.noop);
 
 946                 item.$node = $t.data({
 
 948                     'contextMenuRoot': root,
 
 949                     'contextMenuKey': key
 
 952                 // register accesskey
 
 953                 // NOTE: the accesskey attribute should be applicable to any element, but Safari5 and Chrome13 still can't do that
 
 954                 if (item.accesskey) {
 
 955                     var aks = splitAccesskey(item.accesskey);
 
 956                     for (var i=0, ak; ak = aks[i]; i++) {
 
 957                         if (!root.accesskeys[ak]) {
 
 958                             root.accesskeys[ak] = item;
 
 959                             item._name = item.name.replace(new RegExp('(' + ak + ')', 'i'), '<span class="context-menu-accesskey">$1</span>');
 
 965                 if (typeof item == "string") {
 
 966                     $t.addClass('context-menu-separator not-selectable');
 
 967                 } else if (item.type && types[item.type]) {
 
 968                     // run custom type handler
 
 969                     types[item.type].call($t, item, opt, root);
 
 971                     $.each([opt, root], function(i,k){
 
 972                         k.commands[key] = item;
 
 973                         if ($.isFunction(item.callback)) {
 
 974                             k.callbacks[key] = item.callback;
 
 978                     // add label for input
 
 979                     if (item.type == 'html') {
 
 980                         $t.addClass('context-menu-html not-selectable');
 
 981                     } else if (item.type) {
 
 982                         $label = $('<label></label>').appendTo($t);
 
 983                         $('<span></span>').html(item._name || item.name).appendTo($label);
 
 984                         $t.addClass('context-menu-input');
 
 986                         $.each([opt, root], function(i,k){
 
 987                             k.commands[key] = item;
 
 988                             k.inputs[key] = item;
 
 990                     } else if (item.items) {
 
 996                             $input = $('<input type="text" value="1" name="" value="">')
 
 997                                 .attr('name', 'context-menu-input-' + key)
 
 998                                 .val(item.value || "")
 
1003                             $input = $('<textarea name=""></textarea>')
 
1004                                 .attr('name', 'context-menu-input-' + key)
 
1005                                 .val(item.value || "")
 
1009                                 $input.height(item.height);
 
1014                             $input = $('<input type="checkbox" value="1" name="" value="">')
 
1015                                 .attr('name', 'context-menu-input-' + key)
 
1016                                 .val(item.value || "")
 
1017                                 .prop("checked", !!item.selected)
 
1022                             $input = $('<input type="radio" value="1" name="" value="">')
 
1023                                 .attr('name', 'context-menu-input-' + item.radio)
 
1024                                 .val(item.value || "")
 
1025                                 .prop("checked", !!item.selected)
 
1030                             $input = $('<select name="">')
 
1031                                 .attr('name', 'context-menu-input-' + key)
 
1034                                 $.each(item.options, function(value, text) {
 
1035                                     $('<option></option>').val(value).text(text).appendTo($input);
 
1037                                 $input.val(item.selected);
 
1042                             // FIXME: shouldn't this .html() be a .text()?
 
1043                             $('<span></span>').html(item._name || item.name).appendTo($t);
 
1044                             item.appendTo = item.$node;
 
1045                             op.create(item, root);
 
1046                             $t.data('contextMenu', item).addClass('context-menu-submenu');
 
1047                             item.callback = null;
 
1051                             $(item.html).appendTo($t);
 
1055                             $.each([opt, root], function(i,k){
 
1056                                 k.commands[key] = item;
 
1057                                 if ($.isFunction(item.callback)) {
 
1058                                     k.callbacks[key] = item.callback;
 
1061                             // FIXME: shouldn't this .html() be a .text()?
 
1062                             $('<span></span>').html(item._name || item.name || "").appendTo($t);
 
1066                     // disable key listener in <input>
 
1067                     if (item.type && item.type != 'sub' && item.type != 'html') {
 
1069                             .on('focus', handle.focusInput)
 
1070                             .on('blur', handle.blurInput);
 
1073                             $input.on(item.events, opt);
 
1079                         $t.addClass("icon icon-" + item.icon);
 
1083                 // cache contained elements
 
1084                 item.$input = $input;
 
1085                 item.$label = $label;
 
1087                 // attach item to menu
 
1088                 $t.appendTo(opt.$menu);
 
1090                 // Disable text selection
 
1091                 if (!opt.hasTypes && $.support.eventSelectstart) {
 
1092                     // browsers support user-select: none, 
 
1093                     // IE has a special event for text-selection
 
1094                     // browsers supporting neither will not be preventing text-selection
 
1095                     $t.on('selectstart.disableTextSelect', handle.abortevent);
 
1098             // attach contextMenu to <body> (to bypass any possible overflow:hidden issues on parents of the trigger element)
 
1100                 opt.$menu.css('display', 'none').addClass('context-menu-root');
 
1102             opt.$menu.appendTo(opt.appendTo || document.body);
 
1104         resize: function($menu, nested) {
 
1105             // determine widths of submenus, as CSS won't grow them automatically
 
1106             // position:absolute within position:absolute; min-width:100; max-width:200; results in width: 100;
 
1107             // kinda sucks hard...
 
1109             // determine width of absolutely positioned element
 
1110             $menu.css({position: 'absolute', display: 'block'});
 
1111             // don't apply yet, because that would break nested elements' widths
 
1112             // add a pixel to circumvent word-break issue in IE9 - #80
 
1113             $menu.data('width', Math.ceil($menu.width()) + 1);
 
1114             // reset styles so they allow nested elements to grow/shrink naturally
 
1118                 maxWidth: '100000px'
 
1120             // identify width of nested menus
 
1121             $menu.find('> li > ul').each(function() {
 
1122                 op.resize($(this), true);
 
1124             // reset and apply changes in the end because nested
 
1125             // elements' widths wouldn't be calculatable otherwise
 
1127                 $menu.find('ul').andSelf().css({
 
1132                 }).width(function() {
 
1133                     return $(this).data('width');
 
1137         update: function(opt, root) {
 
1138             var $trigger = this;
 
1139             if (root === undefined) {
 
1141                 op.resize(opt.$menu);
 
1143             // re-check disabled for each item
 
1144             opt.$menu.children().each(function(){
 
1145                 var $item = $(this),
 
1146                     key = $item.data('contextMenuKey'),
 
1147                     item = opt.items[key],
 
1148                     disabled = ($.isFunction(item.disabled) && item.disabled.call($trigger, key, root)) || item.disabled === true;
 
1150                 // dis- / enable item
 
1151                 $item[disabled ? 'addClass' : 'removeClass']('disabled');
 
1154                     // dis- / enable input elements
 
1155                     $item.find('input, select, textarea').prop('disabled', disabled);
 
1157                     // update input states
 
1158                     switch (item.type) {
 
1161                             item.$input.val(item.value || "");
 
1166                             item.$input.val(item.value || "").prop('checked', !!item.selected);
 
1170                             item.$input.val(item.selected || "");
 
1177                     op.update.call($trigger, item, root);
 
1181         layer: function(opt, zIndex) {
 
1182             // add transparent layer for click area
 
1183             // filter and background for Internet Explorer, Issue #23
 
1184             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>')
 
1185                 .css({height: $win.height(), width: $win.width(), display: 'block'})
 
1186                 .data('contextMenuRoot', opt)
 
1188                 .on('contextmenu', handle.abortevent)
 
1189                 .on('mousedown', handle.layerClick);
 
1191             // IE6 doesn't know position:fixed;
 
1192             if (!$.support.fixedPosition) {
 
1194                     'position' : 'absolute',
 
1195                     'height' : $(document).height()
 
1203 // split accesskey according to http://www.whatwg.org/specs/web-apps/current-work/multipage/editing.html#assigned-access-key
 
1204 function splitAccesskey(val) {
 
1205     var t = val.split(/\s+/),
 
1208     for (var i=0, k; k = t[i]; i++) {
 
1209         k = k[0].toUpperCase(); // first character only
 
1210         // theoretically non-accessible characters should be ignored, but different systems, different keyboard layouts, ... screw it.
 
1211         // a map to look up already used access keys would be nice
 
1218 // handle contextMenu triggers
 
1219 $.fn.contextMenu = function(operation) {
 
1220     if (operation === undefined) {
 
1221         this.first().trigger('contextmenu');
 
1222     } else if (operation.x && operation.y) {
 
1223         this.first().trigger($.Event("contextmenu", {pageX: operation.x, pageY: operation.y}));
 
1224     } else if (operation === "hide") {
 
1225         var $menu = this.data('contextMenu').$menu;
 
1226         $menu && $menu.trigger('contextmenu:hide');
 
1227     } else if (operation === "destroy") {
 
1228         $.contextMenu("destroy", {context: this});
 
1229     } else if ($.isPlainObject(operation)) {
 
1230         operation.context = this;
 
1231         $.contextMenu("create", operation);
 
1232     } else if (operation) {
 
1233         this.removeClass('context-menu-disabled');
 
1234     } else if (!operation) {
 
1235         this.addClass('context-menu-disabled');
 
1241 // manage contextMenu instances
 
1242 $.contextMenu = function(operation, options) {
 
1243     if (typeof operation != 'string') {
 
1244         options = operation;
 
1245         operation = 'create';
 
1248     if (typeof options == 'string') {
 
1249         options = {selector: options};
 
1250     } else if (options === undefined) {
 
1254     // merge with default options
 
1255     var o = $.extend(true, {}, defaults, options || {});
 
1256     var $document = $(document);
 
1257     var $context = $document;
 
1258     var _hasContext = false;
 
1260     if (!o.context || !o.context.length) {
 
1261         o.context = document;
 
1263         // you never know what they throw at you...
 
1264         $context = $(o.context).first();
 
1265         o.context = $context.get(0);
 
1266         _hasContext = o.context !== document;
 
1269     switch (operation) {
 
1271             // no selector no joy
 
1273                 throw new Error('No selector specified');
 
1275             // make sure internal classes are not bound to
 
1276             if (o.selector.match(/.context-menu-(list|item|input)($|\s)/)) {
 
1277                 throw new Error('Cannot bind to selector "' + o.selector + '" as it contains a reserved className');
 
1279             if (!o.build && (!o.items || $.isEmptyObject(o.items))) {
 
1280                 throw new Error('No Items sepcified');
 
1283             o.ns = '.contextMenu' + counter;
 
1285                 namespaces[o.selector] = o.ns;
 
1289             // default to right click
 
1291                 o.trigger = 'right';
 
1295                 // make sure item click is registered first
 
1298                         'contextmenu:hide.contextMenu': handle.hideMenu,
 
1299                         'prevcommand.contextMenu': handle.prevItem,
 
1300                         'nextcommand.contextMenu': handle.nextItem,
 
1301                         'contextmenu.contextMenu': handle.abortevent,
 
1302                         'mouseenter.contextMenu': handle.menuMouseenter,
 
1303                         'mouseleave.contextMenu': handle.menuMouseleave
 
1304                     }, '.context-menu-list')
 
1305                     .on('mouseup.contextMenu', '.context-menu-input', handle.inputClick)
 
1307                         'mouseup.contextMenu': handle.itemClick,
 
1308                         'contextmenu:focus.contextMenu': handle.focusItem,
 
1309                         'contextmenu:blur.contextMenu': handle.blurItem,
 
1310                         'contextmenu.contextMenu': handle.abortevent,
 
1311                         'mouseenter.contextMenu': handle.itemMouseenter,
 
1312                         'mouseleave.contextMenu': handle.itemMouseleave
 
1313                     }, '.context-menu-item');
 
1318             // engage native contextmenu event
 
1320                 .on('contextmenu' + o.ns, o.selector, o, handle.contextmenu);
 
1323                 // add remove hook, just in case
 
1324                 $context.on('remove' + o.ns, function() {
 
1325                     $(this).contextMenu("destroy");
 
1329             switch (o.trigger) {
 
1332                             .on('mouseenter' + o.ns, o.selector, o, handle.mouseenter)
 
1333                             .on('mouseleave' + o.ns, o.selector, o, handle.mouseleave);                    
 
1337                         $context.on('click' + o.ns, o.selector, o, handle.click);
 
1341                     // http://www.quirksmode.org/dom/events/contextmenu.html
 
1343                         .on('mousedown' + o.ns, o.selector, o, handle.mousedown)
 
1344                         .on('mouseup' + o.ns, o.selector, o, handle.mouseup);
 
1358                 // get proper options 
 
1359                 var context = o.context;
 
1360                 $.each(menus, function(ns, o) {
 
1361                     if (o.context !== context) {
 
1365                     $visibleMenu = $('.context-menu-list').filter(':visible');
 
1366                     if ($visibleMenu.length && $visibleMenu.data().contextMenuRoot.$trigger.is($(o.context).find(o.selector))) {
 
1367                         $visibleMenu.trigger('contextmenu:hide', {force: true});
 
1371                         if (menus[o.ns].$menu) {
 
1372                             menus[o.ns].$menu.remove();
 
1380                     $(o.context).off(o.ns);
 
1384             } else if (!o.selector) {
 
1385                 $document.off('.contextMenu .contextMenuAutoHide');
 
1386                 $.each(menus, function(ns, o) {
 
1387                     $(o.context).off(o.ns);
 
1393                 initialized = false;
 
1395                 $('#context-menu-layer, .context-menu-list').remove();
 
1396             } else if (namespaces[o.selector]) {
 
1397                 $visibleMenu = $('.context-menu-list').filter(':visible');
 
1398                 if ($visibleMenu.length && $visibleMenu.data().contextMenuRoot.$trigger.is(o.selector)) {
 
1399                     $visibleMenu.trigger('contextmenu:hide', {force: true});
 
1403                     if (menus[namespaces[o.selector]].$menu) {
 
1404                         menus[namespaces[o.selector]].$menu.remove();
 
1407                     delete menus[namespaces[o.selector]];
 
1409                     menus[namespaces[o.selector]] = null;
 
1412                 $document.off(namespaces[o.selector]);
 
1417             // if <command> or <menuitem> are not handled by the browser,
 
1418             // or options was a bool true,
 
1419             // initialize $.contextMenu for them
 
1420             if ((!$.support.htmlCommand && !$.support.htmlMenuitem) || (typeof options == "boolean" && options)) {
 
1421                 $('menu[type="context"]').each(function() {
 
1424                             selector: '[contextmenu=' + this.id +']',
 
1425                             items: $.contextMenu.fromMenu(this)
 
1428                 }).css('display', 'none');
 
1433             throw new Error('Unknown operation "' + operation + '"');
 
1439 // import values into <input> commands
 
1440 $.contextMenu.setInputValues = function(opt, data) {
 
1441     if (data === undefined) {
 
1445     $.each(opt.inputs, function(key, item) {
 
1446         switch (item.type) {
 
1449                 item.value = data[key] || "";
 
1453                 item.selected = data[key] ? true : false;
 
1457                 item.selected = (data[item.radio] || "") == item.value ? true : false;
 
1461                 item.selected = data[key] || "";
 
1467 // export values from <input> commands
 
1468 $.contextMenu.getInputValues = function(opt, data) {
 
1469     if (data === undefined) {
 
1473     $.each(opt.inputs, function(key, item) {
 
1474         switch (item.type) {
 
1478                 data[key] = item.$input.val();
 
1482                 data[key] = item.$input.prop('checked');
 
1486                 if (item.$input.prop('checked')) {
 
1487                     data[item.radio] = item.value;
 
1496 // find <label for="xyz">
 
1497 function inputLabel(node) {
 
1498     return (node.id && $('label[for="'+ node.id +'"]').val()) || node.name;
 
1501 // convert <menu> to items object
 
1502 function menuChildren(items, $children, counter) {
 
1507     $children.each(function() {
 
1508         var $node = $(this),
 
1510             nodeName = this.nodeName.toLowerCase(),
 
1514         // extract <label><input>
 
1515         if (nodeName == 'label' && $node.find('input, textarea, select').length) {
 
1516             label = $node.text();
 
1517             $node = $node.children().first();
 
1518             node = $node.get(0);
 
1519             nodeName = node.nodeName.toLowerCase();
 
1523          * <menu> accepts flow-content as children. that means <embed>, <canvas> and such are valid menu items.
 
1524          * Not being the sadistic kind, $.contextMenu only accepts:
 
1525          * <command>, <menuitem>, <hr>, <span>, <p> <input [text, radio, checkbox]>, <textarea>, <select> and of course <menu>.
 
1526          * Everything else will be imported as an html node, which is not interfaced with contextMenu.
 
1529         // http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#concept-command
 
1531             // http://www.whatwg.org/specs/web-apps/current-work/multipage/interactive-elements.html#the-menu-element
 
1533                 item = {name: $node.attr('label'), items: {}};
 
1534                 counter = menuChildren(item.items, $node.children(), counter);
 
1537             // http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#using-the-a-element-to-define-a-command
 
1539             // http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#using-the-button-element-to-define-a-command
 
1543                     disabled: !!$node.attr('disabled'),
 
1544                     callback: (function(){ return function(){ $node.click(); }; })()
 
1548             // http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#using-the-command-element-to-define-a-command
 
1552                 switch ($node.attr('type')) {
 
1557                             name: $node.attr('label'),
 
1558                             disabled: !!$node.attr('disabled'),
 
1559                             callback: (function(){ return function(){ $node.click(); }; })()
 
1566                             disabled: !!$node.attr('disabled'),
 
1567                             name: $node.attr('label'),
 
1568                             selected: !!$node.attr('checked')
 
1575                             disabled: !!$node.attr('disabled'),
 
1576                             name: $node.attr('label'),
 
1577                             radio: $node.attr('radiogroup'),
 
1578                             value: $node.attr('id'),
 
1579                             selected: !!$node.attr('checked')
 
1593                 switch ($node.attr('type')) {
 
1597                             name: label || inputLabel(node),
 
1598                             disabled: !!$node.attr('disabled'),
 
1606                             name: label || inputLabel(node),
 
1607                             disabled: !!$node.attr('disabled'),
 
1608                             selected: !!$node.attr('checked')
 
1615                             name: label || inputLabel(node),
 
1616                             disabled: !!$node.attr('disabled'),
 
1617                             radio: !!$node.attr('name'),
 
1619                             selected: !!$node.attr('checked')
 
1632                     name: label || inputLabel(node),
 
1633                     disabled: !!$node.attr('disabled'),
 
1634                     selected: $node.val(),
 
1637                 $node.children().each(function(){
 
1638                     item.options[this.value] = $(this).text();
 
1645                     name: label || inputLabel(node),
 
1646                     disabled: !!$node.attr('disabled'),
 
1655                 item = {type: 'html', html: $node.clone(true)};
 
1661             items['key' + counter] = item;
 
1668 // convert html5 menu
 
1669 $.contextMenu.fromMenu = function(element) {
 
1670     var $this = $(element),
 
1673     menuChildren(items, $this.children());
 
1678 // make defaults accessible
 
1679 $.contextMenu.defaults = defaults;
 
1680 $.contextMenu.types = types;
 
1681 // export internal functions - undocumented, for hacking only!
 
1682 $.contextMenu.handle = handle;
 
1683 $.contextMenu.op = op;
 
1684 $.contextMenu.menus = menus;