Advertisement
Guest User

jQuery Contextmenu - select fix

a guest
Jul 16th, 2013
208
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. /*!
  2.  * jQuery contextMenu - Plugin for simple contextMenu handling
  3.  *
  4.  * Version: git-master
  5.  *
  6.  * Authors: Rodney Rehm, Addy Osmani (patches for FF)
  7.  * Web: http://medialize.github.com/jQuery-contextMenu/
  8.  *
  9.  * Licensed under
  10.  *   MIT License http://www.opensource.org/licenses/mit-license
  11.  *   GPL v3 http://opensource.org/licenses/GPL-3.0
  12.  *
  13.  */
  14.  
  15. (function($, undefined){
  16.    
  17.     // TODO: -
  18.         // ARIA stuff: menuitem, menuitemcheckbox und menuitemradio
  19.         // create <menu> structure if $.support[htmlCommand || htmlMenuitem] and !opt.disableNative
  20.  
  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(){
  27.     var t = false,
  28.         e = document.createElement('div');
  29.    
  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';
  33.            
  34.         e.style.cssText = prop + ': text;';
  35.         if (e.style[propCC] == 'text') {
  36.             t = true;
  37.             return false;
  38.         }
  39.        
  40.         return true;
  41.     });
  42.    
  43.     return t;
  44. })();
  45. */
  46.  
  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++ ) {
  53.             try {
  54.                 $( elem ).triggerHandler( "remove" );
  55.                 // http://bugs.jquery.com/ticket/8235
  56.             } catch( e ) {}
  57.         }
  58.         _cleanData( elems );
  59.     };
  60. }
  61.  
  62. var // currently active contextMenu trigger
  63.     $currentTrigger = null,
  64.     // is contextMenu initialized with at least one menu?
  65.     initialized = false,
  66.     // window handle
  67.     $win = $(window),
  68.     // number of registered menus
  69.     counter = 0,
  70.     // mapping selector to namespace
  71.     namespaces = {},
  72.     // mapping namespace to options
  73.     menus = {},
  74.     // custom command type handlers
  75.     types = {},
  76.     // default values
  77.     defaults = {
  78.         // selector of contextMenu trigger
  79.         selector: null,
  80.         // where to append the menu to
  81.         appendTo: null,
  82.         // method to trigger context menu ["right", "left", "hover"]
  83.         trigger: "right",
  84.         // hide menu when mouse leaves trigger / menu elements
  85.         autoHide: false,
  86.         // ms to wait before showing a hover-triggered context menu
  87.         delay: 200,
  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
  90.         reposition: true,
  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({
  98.                     my: "center top",
  99.                     at: "center bottom",
  100.                     of: this,
  101.                     offset: "0 5",
  102.                     collision: "fit"
  103.                 }).css('display', 'none');
  104.             } else {
  105.                 // determine contextMenu position
  106.                 var offset = this.offset();
  107.                 offset.top += this.outerHeight();
  108.                 offset.left += this.outerWidth() / 2 - $menu.outerWidth() / 2;
  109.                 $menu.css(offset);
  110.             }
  111.         },
  112.         // position menu
  113.         position: function(opt, x, y) {
  114.             var $this = this,
  115.                 offset;
  116.             // determine contextMenu position
  117.             if (!x && !y) {
  118.                 opt.determinePosition.call(this, opt.$menu);
  119.                 return;
  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();
  123.             } else {
  124.                 // x and y are given (by mouse event)
  125.                 offset = {top: y, left: x};
  126.             }
  127.            
  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();
  133.            
  134.             if (offset.top + height > bottom) {
  135.                 offset.top -= height;
  136.             }
  137.            
  138.             if (offset.left + width > right) {
  139.                 offset.left -= width;
  140.             }
  141.            
  142.             opt.$menu.css(offset);
  143.         },
  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({
  150.                     my: "left top",
  151.                     at: "right top",
  152.                     of: this,
  153.                     collision: "flipfit fit"
  154.                 }).css('display', '');
  155.             } else {
  156.                 // determine contextMenu position
  157.                 var offset = {
  158.                     top: 0,
  159.                     left: this.outerWidth()
  160.                 };
  161.                 $menu.css(offset);
  162.             }
  163.         },
  164.         // offset to add to zIndex
  165.         zIndex: 1,
  166.         // show hide animation settings
  167.         animation: {
  168.             duration: 50,
  169.             show: 'slideDown',
  170.             hide: 'slideUp'
  171.         },
  172.         // events
  173.         events: {
  174.             show: $.noop,
  175.             hide: $.noop
  176.         },
  177.         // default callback
  178.         callback: null,
  179.         // list of contextMenu items
  180.         items: {}
  181.     },
  182.     // mouse position for hover activation
  183.     hoveract = {
  184.         timer: null,
  185.         pageX: null,
  186.         pageY: null
  187.     },
  188.     // determine zIndex
  189.     zindex = function($t) {
  190.         var zin = 0,
  191.             $tt = $t;
  192.  
  193.         while (true) {
  194.             zin = Math.max(zin, parseInt($tt.css('z-index'), 10) || 0);
  195.             $tt = $tt.parent();
  196.             if (!$tt || !$tt.length || "html body".indexOf($tt.prop('nodeName').toLowerCase()) > -1 ) {
  197.                 break;
  198.             }
  199.         }
  200.        
  201.         return zin;
  202.     },
  203.     // event handlers
  204.     handle = {
  205.         // abort anything
  206.         abortevent: function(e){
  207.             e.preventDefault();
  208.             e.stopImmediatePropagation();
  209.         },
  210.        
  211.         // contextmenu show dispatcher
  212.         contextmenu: function(e) {
  213.             var $this = $(this);
  214.            
  215.             // disable actual context-menu
  216.             e.preventDefault();
  217.             e.stopImmediatePropagation();
  218.            
  219.             // abort native-triggered events unless we're triggering on right click
  220.             if (e.data.trigger != 'right' && e.originalEvent) {
  221.                 return;
  222.             }
  223.            
  224.             // abort event if menu is visible for this trigger
  225.             if ($this.hasClass('context-menu-active')) {
  226.                 return;
  227.             }
  228.            
  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);
  234.                
  235.                 $currentTrigger = $this;
  236.                 if (e.data.build) {
  237.                     var built = e.data.build($currentTrigger, e);
  238.                     // abort if build() returned false
  239.                     if (built === false) {
  240.                         return;
  241.                     }
  242.                    
  243.                     // dynamically build menu on invocation
  244.                     e.data = $.extend(true, {}, defaults, e.data, built || {});
  245.  
  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");
  251.                         }
  252.                        
  253.                         throw new Error('No Items specified');
  254.                     }
  255.                    
  256.                     // backreference for custom command type creation
  257.                     e.data.$trigger = $currentTrigger;
  258.                    
  259.                     op.create(e.data);
  260.                 }
  261.                 // show menu
  262.                 op.show.call($this, e.data, e.pageX, e.pageY);
  263.             }
  264.         },
  265.         // contextMenu left-click trigger
  266.         click: function(e) {
  267.             e.preventDefault();
  268.             e.stopImmediatePropagation();
  269.             $(this).trigger($.Event("contextmenu", { data: e.data, pageX: e.pageX, pageY: e.pageY }));
  270.         },
  271.         // contextMenu right-click trigger
  272.         mousedown: function(e) {
  273.             // register mouse down
  274.             var $this = $(this);
  275.            
  276.             // hide any previous menus
  277.             if ($currentTrigger && $currentTrigger.length && !$currentTrigger.is($this)) {
  278.                 $currentTrigger.data('contextMenu').$menu.trigger('contextmenu:hide');
  279.             }
  280.            
  281.             // activate on right click
  282.             if (e.button == 2) {
  283.                 $currentTrigger = $this.data('contextMenuActive', true);
  284.             }
  285.         },
  286.         // contextMenu right-click trigger
  287.         mouseup: function(e) {
  288.             // show menu
  289.             var $this = $(this);
  290.             if ($this.data('contextMenuActive') && $currentTrigger && $currentTrigger.length && $currentTrigger.is($this) && !$this.hasClass('context-menu-disabled')) {
  291.                 e.preventDefault();
  292.                 e.stopImmediatePropagation();
  293.                 $currentTrigger = $this;
  294.                 $this.trigger($.Event("contextmenu", { data: e.data, pageX: e.pageX, pageY: e.pageY }));
  295.             }
  296.            
  297.             $this.removeData('contextMenuActive');
  298.         },
  299.         // contextMenu hover trigger
  300.         mouseenter: function(e) {
  301.             var $this = $(this),
  302.                 $related = $(e.relatedTarget),
  303.                 $document = $(document);
  304.            
  305.             // abort if we're coming from a menu
  306.             if ($related.is('.context-menu-list') || $related.closest('.context-menu-list').length) {
  307.                 return;
  308.             }
  309.            
  310.             // abort if a menu is shown
  311.             if ($currentTrigger && $currentTrigger.length) {
  312.                 return;
  313.             }
  314.            
  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 }));
  324.             }, e.data.delay );
  325.         },
  326.         // contextMenu hover trigger
  327.         mousemove: function(e) {
  328.             hoveract.pageX = e.pageX;
  329.             hoveract.pageY = e.pageY;
  330.         },
  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) {
  336.                 return;
  337.             }
  338.            
  339.             try {
  340.                 clearTimeout(hoveract.timer);
  341.             } catch(e) {}
  342.            
  343.             hoveract.timer = null;
  344.         },
  345.        
  346.         // click on layer to hide contextMenu
  347.         layerClick: function(e) {
  348.             var $this = $(this),
  349.                 root = $this.data('contextMenuRoot'),
  350.                 mouseup = false,
  351.                 button = e.button,
  352.                 x = e.pageX,
  353.                 y = e.pageY,
  354.                 target,
  355.                 offset,
  356.                 selectors;
  357.                
  358.             e.preventDefault();
  359.             e.stopImmediatePropagation();
  360.            
  361.             setTimeout(function() {
  362.                 var $window, hideshow, possibleTarget;
  363.                 var triggerAction = ((root.trigger == 'left' && button === 0) || (root.trigger == 'right' && button === 2));
  364.                
  365.                 // find the element that would've been clicked, wasn't the layer in the way
  366.                 if (document.elementFromPoint) {
  367.                     root.$layer.hide();
  368.                     target = document.elementFromPoint(x - $win.scrollLeft(), y - $win.scrollTop());
  369.                     root.$layer.show();
  370.                 }
  371.                
  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);
  376.                             return;
  377.                         }
  378.                     } else {
  379.                         offset = root.$trigger.offset();
  380.                         $window = $(window);
  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) {
  391.                                         // reposition
  392.                                         root.position.call(root.$trigger, root, x, y);
  393.                                         return;
  394.                                     }
  395.                                 }
  396.                             }
  397.                         }
  398.                     }
  399.                 }
  400.                
  401.                 if (target && triggerAction) {
  402.                     root.$trigger.one('contextmenu:hidden', function() {
  403.                         $(target).contextMenu({x: x, y: y});
  404.                     });
  405.                 }
  406.  
  407.                 root.$menu.trigger('contextmenu:hide');
  408.             }, 50);
  409.         },
  410.         // key handled :hover
  411.         keyStop: function(e, opt) {
  412.             if (!opt.isInput) {
  413.                 e.preventDefault();
  414.             }
  415.            
  416.             e.stopPropagation();
  417.         },
  418.         key: function(e) {
  419.             var opt = $currentTrigger.data('contextMenu') || {};
  420.  
  421.             switch (e.keyCode) {
  422.                 case 9:
  423.                 case 38: // up
  424.                     handle.keyStop(e, opt);
  425.                     // if keyCode is [38 (up)] or [9 (tab) with shift]
  426.                     if (opt.isInput) {
  427.                         if (e.keyCode == 9 && e.shiftKey) {
  428.                             e.preventDefault();
  429.                             opt.$selected && opt.$selected.find('input, textarea, select').blur();
  430.                             opt.$menu.trigger('prevcommand');
  431.                             return;
  432.                         } else if (e.keyCode == 38 && opt.$selected.find('input, textarea, select').prop('type') == 'checkbox') {
  433.                             // checkboxes don't capture this key
  434.                             e.preventDefault();
  435.                             return;
  436.                         }
  437.                     } else if (e.keyCode != 9 || e.shiftKey) {
  438.                         opt.$menu.trigger('prevcommand');
  439.                         return;
  440.                     }
  441.                     // omitting break;
  442.                    
  443.                 // case 9: // tab - reached through omitted break;
  444.                 case 40: // down
  445.                     handle.keyStop(e, opt);
  446.                     if (opt.isInput) {
  447.                         if (e.keyCode == 9) {
  448.                             e.preventDefault();
  449.                             opt.$selected && opt.$selected.find('input, textarea, select').blur();
  450.                             opt.$menu.trigger('nextcommand');
  451.                             return;
  452.                         } else if (e.keyCode == 40 && opt.$selected.find('input, textarea, select').prop('type') == 'checkbox') {
  453.                             // checkboxes don't capture this key
  454.                             e.preventDefault();
  455.                             return;
  456.                         }
  457.                     } else {
  458.                         opt.$menu.trigger('nextcommand');
  459.                         return;
  460.                     }
  461.                     break;
  462.                
  463.                 case 37: // left
  464.                     handle.keyStop(e, opt);
  465.                     if (opt.isInput || !opt.$selected || !opt.$selected.length) {
  466.                         break;
  467.                     }
  468.                
  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;
  473.                         return;
  474.                     }
  475.                     break;
  476.                    
  477.                 case 39: // right
  478.                     handle.keyStop(e, opt);
  479.                     if (opt.isInput || !opt.$selected || !opt.$selected.length) {
  480.                         break;
  481.                     }
  482.                    
  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');
  488.                         return;
  489.                     }
  490.                     break;
  491.                
  492.                 case 35: // end
  493.                 case 36: // home
  494.                     if (opt.$selected && opt.$selected.find('input, textarea, select').length) {
  495.                         return;
  496.                     } else {
  497.                         (opt.$selected && opt.$selected.parent() || opt.$menu)
  498.                             .children(':not(.disabled, .not-selectable)')[e.keyCode == 36 ? 'first' : 'last']()
  499.                             .trigger('contextmenu:focus');
  500.                         e.preventDefault();
  501.                         return;
  502.                     }
  503.                     break;
  504.                    
  505.                 case 13: // enter
  506.                     handle.keyStop(e, opt);
  507.                     if (opt.isInput) {
  508.                         if (opt.$selected && !opt.$selected.is('textarea, select')) {
  509.                             e.preventDefault();
  510.                             return;
  511.                         }
  512.                         break;
  513.                     }
  514.                     opt.$selected && opt.$selected.trigger('mouseup');
  515.                     return;
  516.                    
  517.                 case 32: // space
  518.                 case 33: // page up
  519.                 case 34: // page down
  520.                     // prevent browser from scrolling down while menu is visible
  521.                     handle.keyStop(e, opt);
  522.                     return;
  523.                    
  524.                 case 27: // esc
  525.                     handle.keyStop(e, opt);
  526.                     opt.$menu.trigger('contextmenu:hide');
  527.                     return;
  528.                    
  529.                 default: // 0-9, a-z
  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'
  535.                             : 'mouseup'
  536.                         );
  537.                         return;
  538.                     }
  539.                     break;
  540.             }
  541.             // pass event to selected item,
  542.             // stop propagation to avoid endless recursion
  543.             e.stopPropagation();
  544.             opt.$selected && opt.$selected.trigger(e);
  545.         },
  546.  
  547.         // select previous possible command in menu
  548.         prevItem: function(e) {
  549.             e.stopPropagation();
  550.             var opt = $(this).data('contextMenu') || {};
  551.  
  552.             // obtain currently selected menu
  553.             if (opt.$selected) {
  554.                 var $s = opt.$selected;
  555.                 opt = opt.$selected.parent().data('contextMenu') || {};
  556.                 opt.$selected = $s;
  557.             }
  558.            
  559.             var $children = opt.$menu.children(),
  560.                 $prev = !opt.$selected || !opt.$selected.prev().length ? $children.last() : opt.$selected.prev(),
  561.                 $round = $prev;
  562.            
  563.             // skip disabled
  564.             while ($prev.hasClass('disabled') || $prev.hasClass('not-selectable')) {
  565.                 if ($prev.prev().length) {
  566.                     $prev = $prev.prev();
  567.                 } else {
  568.                     $prev = $children.last();
  569.                 }
  570.                 if ($prev.is($round)) {
  571.                     // break endless loop
  572.                     return;
  573.                 }
  574.             }
  575.            
  576.             // leave current
  577.             if (opt.$selected) {
  578.                 handle.itemMouseleave.call(opt.$selected.get(0), e);
  579.             }
  580.            
  581.             // activate next
  582.             handle.itemMouseenter.call($prev.get(0), e);
  583.            
  584.             // focus input
  585.             var $input = $prev.find('input, textarea, select');
  586.             if ($input.length) {
  587.                 $input.focus();
  588.             }
  589.         },
  590.         // select next possible command in menu
  591.         nextItem: function(e) {
  592.             e.stopPropagation();
  593.             var opt = $(this).data('contextMenu') || {};
  594.  
  595.             // obtain currently selected menu
  596.             if (opt.$selected) {
  597.                 var $s = opt.$selected;
  598.                 opt = opt.$selected.parent().data('contextMenu') || {};
  599.                 opt.$selected = $s;
  600.             }
  601.  
  602.             var $children = opt.$menu.children(),
  603.                 $next = !opt.$selected || !opt.$selected.next().length ? $children.first() : opt.$selected.next(),
  604.                 $round = $next;
  605.  
  606.             // skip disabled
  607.             while ($next.hasClass('disabled') || $next.hasClass('not-selectable')) {
  608.                 if ($next.next().length) {
  609.                     $next = $next.next();
  610.                 } else {
  611.                     $next = $children.first();
  612.                 }
  613.                 if ($next.is($round)) {
  614.                     // break endless loop
  615.                     return;
  616.                 }
  617.             }
  618.            
  619.             // leave current
  620.             if (opt.$selected) {
  621.                 handle.itemMouseleave.call(opt.$selected.get(0), e);
  622.             }
  623.            
  624.             // activate next
  625.             handle.itemMouseenter.call($next.get(0), e);
  626.            
  627.             // focus input
  628.             var $input = $next.find('input, textarea, select');
  629.             if ($input.length) {
  630.                 $input.focus();
  631.             }
  632.         },
  633.        
  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'),
  637.                 data = $this.data(),
  638.                 opt = data.contextMenu,
  639.                 root = data.contextMenuRoot;
  640.  
  641.             root.$selected = opt.$selected = $this;
  642.             root.isInput = opt.isInput = true;
  643.         },
  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'),
  647.                 data = $this.data(),
  648.                 opt = data.contextMenu,
  649.                 root = data.contextMenuRoot;
  650.  
  651.             root.isInput = opt.isInput = false;
  652.         },
  653.        
  654.         // :hover on menu
  655.         menuMouseenter: function(e) {
  656.             var root = $(this).data().contextMenuRoot;
  657.             root.hovering = true;
  658.         },
  659.         // :hover on menu
  660.         menuMouseleave: function(e) {
  661.             var root = $(this).data().contextMenuRoot;
  662.             if (root.$layer && root.$layer.is(e.relatedTarget)) {
  663.                 root.hovering = false;
  664.             }
  665.         },
  666.        
  667.         // :hover done manually so key handling is possible
  668.         itemMouseenter: function(e) {
  669.             var $this = $(this),
  670.                 data = $this.data(),
  671.                 opt = data.contextMenu,
  672.                 root = data.contextMenuRoot;
  673.            
  674.             root.hovering = true;
  675.  
  676.             // abort if we're re-entering
  677.             if (e && root.$layer && root.$layer.is(e.relatedTarget)) {
  678.                 e.preventDefault();
  679.                 e.stopImmediatePropagation();
  680.             }
  681.  
  682.             // make sure only one item is selected
  683.             (opt.$menu ? opt : root).$menu
  684.                 .children('.hover').trigger('contextmenu:blur');
  685.  
  686.             if ($this.hasClass('disabled') || $this.hasClass('not-selectable')) {
  687.                 opt.$selected = null;
  688.                 return;
  689.             }
  690.            
  691.             $this.trigger('contextmenu:focus');
  692.         },
  693.         // :hover done manually so key handling is possible
  694.         itemMouseleave: function(e) {
  695.             var $this = $(this),
  696.                 data = $this.data(),
  697.                 opt = data.contextMenu,
  698.                 root = data.contextMenuRoot;
  699.  
  700.             if (root !== opt && root.$layer && root.$layer.is(e.relatedTarget)) {
  701.                 root.$selected && root.$selected.trigger('contextmenu:blur');
  702.                 e.preventDefault();
  703.                 e.stopImmediatePropagation();
  704.                 root.$selected = opt.$selected = opt.$node;
  705.                 return;
  706.             }
  707.            
  708.             $this.trigger('contextmenu:blur');
  709.         },
  710.         // contextMenu item click
  711.         itemClick: function(e) {
  712.             var $this = $(this),
  713.                 data = $this.data(),
  714.                 opt = data.contextMenu,
  715.                 root = data.contextMenuRoot,
  716.                 key = data.contextMenuKey,
  717.                 callback;
  718.  
  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')) {
  721.                 return;
  722.             }
  723.  
  724.             e.preventDefault();
  725.             e.stopImmediatePropagation();
  726.  
  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)) {
  731.                 // default callback
  732.                 callback = root.callback;                
  733.             } else {
  734.                 // no callback, no action
  735.                 return;
  736.             }
  737.  
  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);
  743.             }
  744.         },
  745.         // ignore click events on input elements
  746.         inputClick: function(e) {
  747.             e.stopImmediatePropagation();
  748.         },
  749.        
  750.         // hide <menu>
  751.         hideMenu: function(e, data) {
  752.             var root = $(this).data('contextMenuRoot');
  753.             op.hide.call(root.$trigger, root, data && data.force);
  754.         },
  755.         // focus <command>
  756.         focusItem: function(e) {
  757.             e.stopPropagation();
  758.             var $this = $(this),
  759.                 data = $this.data(),
  760.                 opt = data.contextMenu,
  761.                 root = data.contextMenuRoot;
  762.  
  763.             $this.addClass('hover')
  764.                 .siblings('.hover').trigger('contextmenu:blur');
  765.            
  766.             // remember selected
  767.             opt.$selected = root.$selected = $this;
  768.            
  769.             // position sub-menu - do after show so dumb $.ui.position can keep up
  770.             if (opt.$node) {
  771.                 root.positionSubmenu.call(opt.$node, opt.$menu);
  772.             }
  773.         },
  774.         // blur <command>
  775.         blurItem: function(e) {
  776.             e.stopPropagation();
  777.             var $this = $(this),
  778.                 data = $this.data(),
  779.                 opt = data.contextMenu,
  780.                 root = data.contextMenuRoot;
  781.            
  782.             $this.removeClass('hover');
  783.             opt.$selected = null;
  784.         }
  785.     },
  786.     // operations
  787.     op = {
  788.         show: function(opt, x, y) {
  789.             var $trigger = $(this),
  790.                 offset,
  791.                 css = {};
  792.  
  793.             // hide any open menus
  794.             $('#context-menu-layer').trigger('mousedown');
  795.  
  796.             // backreference for callbacks
  797.             opt.$trigger = $trigger;
  798.  
  799.             // show event
  800.             if (opt.events.show.call($trigger, opt) === false) {
  801.                 $currentTrigger = null;
  802.                 return;
  803.             }
  804.  
  805.             // create or update context menu
  806.             op.update.call($trigger, opt);
  807.            
  808.             // position menu
  809.             opt.position.call($trigger, opt, x, y);
  810.  
  811.             // make sure we're in front
  812.             if (opt.zIndex) {
  813.                 css.zIndex = zindex($trigger) + opt.zIndex;
  814.             }
  815.            
  816.             // add layer
  817.             op.layer.call(opt.$menu, opt, css.zIndex);
  818.            
  819.             // adjust sub-menu zIndexes
  820.             opt.$menu.find('ul').css('zIndex', css.zIndex + 1);
  821.            
  822.             // position and show context menu
  823.             opt.$menu.css( css )[opt.animation.show](opt.animation.duration, function() {
  824.                 $trigger.trigger('contextmenu:visible');
  825.             });
  826.             // make options available and set state
  827.             $trigger
  828.                 .data('contextMenu', opt)
  829.                 .addClass("context-menu-active");
  830.            
  831.             // register key handler
  832.             $(document).off('keydown.contextMenu').on('keydown.contextMenu', handle.key);
  833.             // register autoHide handler
  834.             if (opt.autoHide) {
  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();
  842.                    
  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');
  846.                     }
  847.                 });
  848.             }
  849.         },
  850.         hide: function(opt, force) {
  851.             var $trigger = $(this);
  852.             if (!opt) {
  853.                 opt = $trigger.data('contextMenu') || {};
  854.             }
  855.            
  856.             // hide event
  857.             if (!force && opt.events && opt.events.hide.call($trigger, opt) === false) {
  858.                 return;
  859.             }
  860.            
  861.             // remove options and revert state
  862.             $trigger
  863.                 .removeData('contextMenu')
  864.                 .removeClass("context-menu-active");
  865.            
  866.             if (opt.$layer) {
  867.                 // keep layer for a bit so the contextmenu event can be aborted properly by opera
  868.                 setTimeout((function($layer) {
  869.                     return function(){
  870.                         $layer.remove();
  871.                     };
  872.                 })(opt.$layer), 10);
  873.                
  874.                 try {
  875.                     delete opt.$layer;
  876.                 } catch(e) {
  877.                     opt.$layer = null;
  878.                 }
  879.             }
  880.            
  881.             // remove handle
  882.             $currentTrigger = null;
  883.             // remove selected
  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');
  889.             // hide menu
  890.             opt.$menu && opt.$menu[opt.animation.hide](opt.animation.duration, function (){
  891.                 // tear down dynamically built menu after animation is completed.
  892.                 if (opt.build) {
  893.                     opt.$menu.remove();
  894.                     $.each(opt, function(key, value) {
  895.                         switch (key) {
  896.                             case 'ns':
  897.                             case 'selector':
  898.                             case 'build':
  899.                             case 'trigger':
  900.                                 return true;
  901.  
  902.                             default:
  903.                                 opt[key] = undefined;
  904.                                 try {
  905.                                     delete opt[key];
  906.                                 } catch (e) {}
  907.                                 return true;
  908.                         }
  909.                     });
  910.                 }
  911.                
  912.                 setTimeout(function() {
  913.                     $trigger.trigger('contextmenu:hidden');
  914.                 }, 10);
  915.             });
  916.         },
  917.         create: function(opt, root) {
  918.             if (root === undefined) {
  919.                 root = opt;
  920.             }
  921.             // create contextMenu
  922.             opt.$menu = $('<ul class="context-menu-list"></ul>').addClass(opt.className || "").data({
  923.                 'contextMenu': opt,
  924.                 'contextMenuRoot': root
  925.             });
  926.            
  927.             $.each(['callbacks', 'commands', 'inputs'], function(i,k){
  928.                 opt[k] = {};
  929.                 if (!root[k]) {
  930.                     root[k] = {};
  931.                 }
  932.             });
  933.            
  934.             root.accesskeys || (root.accesskeys = {});
  935.            
  936.             // create contextMenu items
  937.             $.each(opt.items, function(key, item){
  938.                 var $t = $('<li class="context-menu-item"></li>').addClass(item.className || ""),
  939.                     $label = null,
  940.                     $input = null;
  941.                
  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);
  945.                
  946.                 item.$node = $t.data({
  947.                     'contextMenu': opt,
  948.                     'contextMenuRoot': root,
  949.                     'contextMenuKey': key
  950.                 });
  951.                
  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>');
  960.                             break;
  961.                         }
  962.                     }
  963.                 }
  964.                
  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);
  970.                     // register commands
  971.                     $.each([opt, root], function(i,k){
  972.                         k.commands[key] = item;
  973.                         if ($.isFunction(item.callback)) {
  974.                             k.callbacks[key] = item.callback;
  975.                         }
  976.                     });
  977.                 } else {
  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');
  985.                         opt.hasTypes = true;
  986.                         $.each([opt, root], function(i,k){
  987.                             k.commands[key] = item;
  988.                             k.inputs[key] = item;
  989.                         });
  990.                     } else if (item.items) {
  991.                         item.type = 'sub';
  992.                     }
  993.                
  994.                     switch (item.type) {
  995.                         case 'text':
  996.                             $input = $('<input type="text" value="1" name="" value="">')
  997.                                 .attr('name', 'context-menu-input-' + key)
  998.                                 .val(item.value || "")
  999.                                 .appendTo($label);
  1000.                             break;
  1001.                    
  1002.                         case 'textarea':
  1003.                             $input = $('<textarea name=""></textarea>')
  1004.                                 .attr('name', 'context-menu-input-' + key)
  1005.                                 .val(item.value || "")
  1006.                                 .appendTo($label);
  1007.  
  1008.                             if (item.height) {
  1009.                                 $input.height(item.height);
  1010.                             }
  1011.                             break;
  1012.  
  1013.                         case 'checkbox':
  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)
  1018.                                 .prependTo($label);
  1019.                             break;
  1020.  
  1021.                         case 'radio':
  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)
  1026.                                 .prependTo($label);
  1027.                             break;
  1028.                    
  1029.                         case 'select':
  1030.                             $input = $('<select name="">')
  1031.                                 .attr('name', 'context-menu-input-' + key)
  1032.                                 .appendTo($label);
  1033.                             if (item.options) {
  1034.                                 $.each(item.options, function(value, text) {
  1035.                                     $('<option></option>').val(value).text(text).appendTo($input);
  1036.                                 });
  1037.                                 $input.val(item.selected);
  1038.                             }
  1039.                             break;
  1040.                        
  1041.                         case 'sub':
  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;
  1048.                             break;
  1049.                        
  1050.                         case 'html':
  1051.                             $(item.html).appendTo($t);
  1052.                             break;
  1053.                        
  1054.                         default:
  1055.                             $.each([opt, root], function(i,k){
  1056.                                 k.commands[key] = item;
  1057.                                 if ($.isFunction(item.callback)) {
  1058.                                     k.callbacks[key] = item.callback;
  1059.                                 }
  1060.                             });
  1061.                             // FIXME: shouldn't this .html() be a .text()?
  1062.                             $('<span></span>').html(item._name || item.name || "").appendTo($t);
  1063.                             break;
  1064.                     }
  1065.                    
  1066.                     // disable key listener in <input>
  1067.                     if (item.type && item.type != 'sub' && item.type != 'html') {
  1068.                         $input
  1069.                             .on('focus', handle.focusInput)
  1070.                             .on('blur', handle.blurInput);
  1071.                        
  1072.                         if (item.events) {
  1073.                             $input.on(item.events, opt);
  1074.                         }
  1075.                     }
  1076.                
  1077.                     // add icons
  1078.                     if (item.icon) {
  1079.                         $t.addClass("icon icon-" + item.icon);
  1080.                     }
  1081.                 }
  1082.                
  1083.                 // cache contained elements
  1084.                 item.$input = $input;
  1085.                 item.$label = $label;
  1086.  
  1087.                 // attach item to menu
  1088.                 $t.appendTo(opt.$menu);
  1089.                
  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);
  1096.                 }
  1097.             });
  1098.             // attach contextMenu to <body> (to bypass any possible overflow:hidden issues on parents of the trigger element)
  1099.             if (!opt.$node) {
  1100.                 opt.$menu.css('display', 'none').addClass('context-menu-root');
  1101.             }
  1102.             opt.$menu.appendTo(opt.appendTo || document.body);
  1103.         },
  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...
  1108.  
  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
  1115.             $menu.css({
  1116.                 position: 'static',
  1117.                 minWidth: '0px',
  1118.                 maxWidth: '100000px'
  1119.             });
  1120.             // identify width of nested menus
  1121.             $menu.find('> li > ul').each(function() {
  1122.                 op.resize($(this), true);
  1123.             });
  1124.             // reset and apply changes in the end because nested
  1125.             // elements' widths wouldn't be calculatable otherwise
  1126.             if (!nested) {
  1127.                 $menu.find('ul').andSelf().css({
  1128.                     position: '',
  1129.                     display: '',
  1130.                     minWidth: '',
  1131.                     maxWidth: ''
  1132.                 }).width(function() {
  1133.                     return $(this).data('width');
  1134.                 });
  1135.             }
  1136.         },
  1137.         update: function(opt, root) {
  1138.             var $trigger = this;
  1139.             if (root === undefined) {
  1140.                 root = opt;
  1141.                 op.resize(opt.$menu);
  1142.             }
  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;
  1149.  
  1150.                 // dis- / enable item
  1151.                 $item[disabled ? 'addClass' : 'removeClass']('disabled');
  1152.                
  1153.                 if (item.type) {
  1154.                     // dis- / enable input elements
  1155.                     $item.find('input, select, textarea').prop('disabled', disabled);
  1156.                    
  1157.                     // update input states
  1158.                     switch (item.type) {
  1159.                         case 'text':
  1160.                         case 'textarea':
  1161.                             item.$input.val(item.value || "");
  1162.                             break;
  1163.                            
  1164.                         case 'checkbox':
  1165.                         case 'radio':
  1166.                             item.$input.val(item.value || "").prop('checked', !!item.selected);
  1167.                             break;
  1168.                            
  1169.                         case 'select':
  1170.                             item.$input.val(item.selected || "");
  1171.                             break;
  1172.                     }
  1173.                 }
  1174.                
  1175.                 if (item.$menu) {
  1176.                     // update sub-menu
  1177.                     op.update.call($trigger, item, root);
  1178.                 }
  1179.             });
  1180.         },
  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)
  1187.                 .insertBefore(this)
  1188.                 .on('contextmenu', handle.abortevent)
  1189.                 .on('mousedown', handle.layerClick);
  1190.            
  1191.             // IE6 doesn't know position:fixed;
  1192.             if (!$.support.fixedPosition) {
  1193.                 $layer.css({
  1194.                     'position' : 'absolute',
  1195.                     'height' : $(document).height()
  1196.                 });
  1197.             }
  1198.            
  1199.             return $layer;
  1200.         }
  1201.     };
  1202.  
  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+/),
  1206.         keys = [];
  1207.        
  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
  1212.         keys.push(k);
  1213.     }
  1214.    
  1215.     return keys;
  1216. }
  1217.  
  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');
  1236.     }
  1237.    
  1238.     return this;
  1239. };
  1240.  
  1241. // manage contextMenu instances
  1242. $.contextMenu = function(operation, options) {
  1243.     if (typeof operation != 'string') {
  1244.         options = operation;
  1245.         operation = 'create';
  1246.     }
  1247.    
  1248.     if (typeof options == 'string') {
  1249.         options = {selector: options};
  1250.     } else if (options === undefined) {
  1251.         options = {};
  1252.     }
  1253.    
  1254.     // merge with default options
  1255.     var o = $.extend(true, {}, defaults, options || {});
  1256.     var $document = $(document);
  1257.     var $context = $document;
  1258.     var _hasContext = false;
  1259.    
  1260.     if (!o.context || !o.context.length) {
  1261.         o.context = document;
  1262.     } else {
  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;
  1267.     }
  1268.    
  1269.     switch (operation) {
  1270.         case 'create':
  1271.             // no selector no joy
  1272.             if (!o.selector) {
  1273.                 throw new Error('No selector specified');
  1274.             }
  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');
  1278.             }
  1279.             if (!o.build && (!o.items || $.isEmptyObject(o.items))) {
  1280.                 throw new Error('No Items specified');
  1281.             }
  1282.             counter ++;
  1283.             o.ns = '.contextMenu' + counter;
  1284.             if (!_hasContext) {
  1285.                 namespaces[o.selector] = o.ns;
  1286.             }
  1287.             menus[o.ns] = o;
  1288.            
  1289.             // default to right click
  1290.             if (!o.trigger) {
  1291.                 o.trigger = 'right';
  1292.             }
  1293.            
  1294.             if (!initialized) {
  1295.                 // make sure item click is registered first
  1296.                 $document
  1297.                     .on({
  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)
  1306.                     .on({
  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');
  1314.  
  1315.                 initialized = true;
  1316.             }
  1317.            
  1318.             // engage native contextmenu event
  1319.             $context
  1320.                 .on('contextmenu' + o.ns, o.selector, o, handle.contextmenu);
  1321.            
  1322.             if (_hasContext) {
  1323.                 // add remove hook, just in case
  1324.                 $context.on('remove' + o.ns, function() {
  1325.                     $(this).contextMenu("destroy");
  1326.                 });
  1327.             }
  1328.            
  1329.             switch (o.trigger) {
  1330.                 case 'hover':
  1331.                         $context
  1332.                             .on('mouseenter' + o.ns, o.selector, o, handle.mouseenter)
  1333.                             .on('mouseleave' + o.ns, o.selector, o, handle.mouseleave);                    
  1334.                     break;
  1335.                    
  1336.                 case 'left':
  1337.                         $context.on('click' + o.ns, o.selector, o, handle.click);
  1338.                     break;
  1339.                 /*
  1340.                 default:
  1341.                     // http://www.quirksmode.org/dom/events/contextmenu.html
  1342.                     $document
  1343.                         .on('mousedown' + o.ns, o.selector, o, handle.mousedown)
  1344.                         .on('mouseup' + o.ns, o.selector, o, handle.mouseup);
  1345.                     break;
  1346.                 */
  1347.             }
  1348.            
  1349.             // create menu
  1350.             if (!o.build) {
  1351.                 op.create(o);
  1352.             }
  1353.             break;
  1354.        
  1355.         case 'destroy':
  1356.             var $visibleMenu;
  1357.             if (_hasContext) {
  1358.                 // get proper options
  1359.                 var context = o.context;
  1360.                 $.each(menus, function(ns, o) {
  1361.                     if (o.context !== context) {
  1362.                         return true;
  1363.                     }
  1364.                    
  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});
  1368.                     }
  1369.  
  1370.                     try {
  1371.                         if (menus[o.ns].$menu) {
  1372.                             menus[o.ns].$menu.remove();
  1373.                         }
  1374.  
  1375.                         delete menus[o.ns];
  1376.                     } catch(e) {
  1377.                         menus[o.ns] = null;
  1378.                     }
  1379.  
  1380.                     $(o.context).off(o.ns);
  1381.                    
  1382.                     return true;
  1383.                 });
  1384.             } else if (!o.selector) {
  1385.                 $document.off('.contextMenu .contextMenuAutoHide');
  1386.                 $.each(menus, function(ns, o) {
  1387.                     $(o.context).off(o.ns);
  1388.                 });
  1389.                
  1390.                 namespaces = {};
  1391.                 menus = {};
  1392.                 counter = 0;
  1393.                 initialized = false;
  1394.                
  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});
  1400.                 }
  1401.                
  1402.                 try {
  1403.                     if (menus[namespaces[o.selector]].$menu) {
  1404.                         menus[namespaces[o.selector]].$menu.remove();
  1405.                     }
  1406.                    
  1407.                     delete menus[namespaces[o.selector]];
  1408.                 } catch(e) {
  1409.                     menus[namespaces[o.selector]] = null;
  1410.                 }
  1411.                
  1412.                 $document.off(namespaces[o.selector]);
  1413.             }
  1414.             break;
  1415.        
  1416.         case 'html5':
  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() {
  1422.                     if (this.id) {
  1423.                         $.contextMenu({
  1424.                             selector: '[contextmenu=' + this.id +']',
  1425.                             items: $.contextMenu.fromMenu(this)
  1426.                         });
  1427.                     }
  1428.                 }).css('display', 'none');
  1429.             }
  1430.             break;
  1431.        
  1432.         default:
  1433.             throw new Error('Unknown operation "' + operation + '"');
  1434.     }
  1435.    
  1436.     return this;
  1437. };
  1438.  
  1439. // import values into <input> commands
  1440. $.contextMenu.setInputValues = function(opt, data) {
  1441.     if (data === undefined) {
  1442.         data = {};
  1443.     }
  1444.    
  1445.     $.each(opt.inputs, function(key, item) {
  1446.         switch (item.type) {
  1447.             case 'text':
  1448.             case 'textarea':
  1449.                 item.value = data[key] || "";
  1450.                 break;
  1451.  
  1452.             case 'checkbox':
  1453.                 item.selected = data[key] ? true : false;
  1454.                 break;
  1455.                
  1456.             case 'radio':
  1457.                 item.selected = (data[item.radio] || "") == item.value ? true : false;
  1458.                 break;
  1459.            
  1460.             case 'select':
  1461.                 if (data[key]!=undefined ){
  1462.                   item.selected = data[key] || "";
  1463.                 }
  1464.                 break;
  1465.         }
  1466.     });
  1467. };
  1468.  
  1469. // export values from <input> commands
  1470. $.contextMenu.getInputValues = function(opt, data) {
  1471.     if (data === undefined) {
  1472.         data = {};
  1473.     }
  1474.    
  1475.     $.each(opt.inputs, function(key, item) {
  1476.         switch (item.type) {
  1477.             case 'text':
  1478.             case 'textarea':
  1479.             case 'select':
  1480.                 data[key] = item.$input.val();
  1481.                 break;
  1482.  
  1483.             case 'checkbox':
  1484.                 data[key] = item.$input.prop('checked');
  1485.                 break;
  1486.                
  1487.             case 'radio':
  1488.                 if (item.$input.prop('checked')) {
  1489.                     data[item.radio] = item.value;
  1490.                 }
  1491.                 break;
  1492.         }
  1493.     });
  1494.    
  1495.     return data;
  1496. };
  1497.  
  1498. // find <label for="xyz">
  1499. function inputLabel(node) {
  1500.     return (node.id && $('label[for="'+ node.id +'"]').val()) || node.name;
  1501. }
  1502.  
  1503. // convert <menu> to items object
  1504. function menuChildren(items, $children, counter) {
  1505.     if (!counter) {
  1506.         counter = 0;
  1507.     }
  1508.    
  1509.     $children.each(function() {
  1510.         var $node = $(this),
  1511.             node = this,
  1512.             nodeName = this.nodeName.toLowerCase(),
  1513.             label,
  1514.             item;
  1515.        
  1516.         // extract <label><input>
  1517.         if (nodeName == 'label' && $node.find('input, textarea, select').length) {
  1518.             label = $node.text();
  1519.             $node = $node.children().first();
  1520.             node = $node.get(0);
  1521.             nodeName = node.nodeName.toLowerCase();
  1522.         }
  1523.        
  1524.         /*
  1525.          * <menu> accepts flow-content as children. that means <embed>, <canvas> and such are valid menu items.
  1526.          * Not being the sadistic kind, $.contextMenu only accepts:
  1527.          * <command>, <menuitem>, <hr>, <span>, <p> <input [text, radio, checkbox]>, <textarea>, <select> and of course <menu>.
  1528.          * Everything else will be imported as an html node, which is not interfaced with contextMenu.
  1529.          */
  1530.        
  1531.         // http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#concept-command
  1532.         switch (nodeName) {
  1533.             // http://www.whatwg.org/specs/web-apps/current-work/multipage/interactive-elements.html#the-menu-element
  1534.             case 'menu':
  1535.                 item = {name: $node.attr('label'), items: {}};
  1536.                 counter = menuChildren(item.items, $node.children(), counter);
  1537.                 break;
  1538.            
  1539.             // http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#using-the-a-element-to-define-a-command
  1540.             case 'a':
  1541.             // http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#using-the-button-element-to-define-a-command
  1542.             case 'button':
  1543.                 item = {
  1544.                     name: $node.text(),
  1545.                     disabled: !!$node.attr('disabled'),
  1546.                     callback: (function(){ return function(){ $node.click(); }; })()
  1547.                 };
  1548.                 break;
  1549.            
  1550.             // http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#using-the-command-element-to-define-a-command
  1551.  
  1552.             case 'menuitem':
  1553.             case 'command':
  1554.                 switch ($node.attr('type')) {
  1555.                     case undefined:
  1556.                     case 'command':
  1557.                     case 'menuitem':
  1558.                         item = {
  1559.                             name: $node.attr('label'),
  1560.                             disabled: !!$node.attr('disabled'),
  1561.                             callback: (function(){ return function(){ $node.click(); }; })()
  1562.                         };
  1563.                         break;
  1564.                        
  1565.                     case 'checkbox':
  1566.                         item = {
  1567.                             type: 'checkbox',
  1568.                             disabled: !!$node.attr('disabled'),
  1569.                             name: $node.attr('label'),
  1570.                             selected: !!$node.attr('checked')
  1571.                         };
  1572.                         break;
  1573.                        
  1574.                     case 'radio':
  1575.                         item = {
  1576.                             type: 'radio',
  1577.                             disabled: !!$node.attr('disabled'),
  1578.                             name: $node.attr('label'),
  1579.                             radio: $node.attr('radiogroup'),
  1580.                             value: $node.attr('id'),
  1581.                             selected: !!$node.attr('checked')
  1582.                         };
  1583.                         break;
  1584.                        
  1585.                     default:
  1586.                         item = undefined;
  1587.                 }
  1588.                 break;
  1589.  
  1590.             case 'hr':
  1591.                 item = '-------';
  1592.                 break;
  1593.                
  1594.             case 'input':
  1595.                 switch ($node.attr('type')) {
  1596.                     case 'text':
  1597.                         item = {
  1598.                             type: 'text',
  1599.                             name: label || inputLabel(node),
  1600.                             disabled: !!$node.attr('disabled'),
  1601.                             value: $node.val()
  1602.                         };
  1603.                         break;
  1604.                        
  1605.                     case 'checkbox':
  1606.                         item = {
  1607.                             type: 'checkbox',
  1608.                             name: label || inputLabel(node),
  1609.                             disabled: !!$node.attr('disabled'),
  1610.                             selected: !!$node.attr('checked')
  1611.                         };
  1612.                         break;
  1613.                        
  1614.                     case 'radio':
  1615.                         item = {
  1616.                             type: 'radio',
  1617.                             name: label || inputLabel(node),
  1618.                             disabled: !!$node.attr('disabled'),
  1619.                             radio: !!$node.attr('name'),
  1620.                             value: $node.val(),
  1621.                             selected: !!$node.attr('checked')
  1622.                         };
  1623.                         break;
  1624.                    
  1625.                     default:
  1626.                         item = undefined;
  1627.                         break;
  1628.                 }
  1629.                 break;
  1630.                
  1631.             case 'select':
  1632.                 item = {
  1633.                     type: 'select',
  1634.                     name: label || inputLabel(node),
  1635.                     disabled: !!$node.attr('disabled'),
  1636.                     selected: $node.val(),
  1637.                     options: {}
  1638.                 };
  1639.                 $node.children().each(function(){
  1640.                     item.options[this.value] = $(this).text();
  1641.                 });
  1642.                 break;
  1643.                
  1644.             case 'textarea':
  1645.                 item = {
  1646.                     type: 'textarea',
  1647.                     name: label || inputLabel(node),
  1648.                     disabled: !!$node.attr('disabled'),
  1649.                     value: $node.val()
  1650.                 };
  1651.                 break;
  1652.            
  1653.             case 'label':
  1654.                 break;
  1655.            
  1656.             default:
  1657.                 item = {type: 'html', html: $node.clone(true)};
  1658.                 break;
  1659.         }
  1660.        
  1661.         if (item) {
  1662.             counter++;
  1663.             items['key' + counter] = item;
  1664.         }
  1665.     });
  1666.    
  1667.     return counter;
  1668. }
  1669.  
  1670. // convert html5 menu
  1671. $.contextMenu.fromMenu = function(element) {
  1672.     var $this = $(element),
  1673.         items = {};
  1674.        
  1675.     menuChildren(items, $this.children());
  1676.    
  1677.     return items;
  1678. };
  1679.  
  1680. // make defaults accessible
  1681. $.contextMenu.defaults = defaults;
  1682. $.contextMenu.types = types;
  1683. // export internal functions - undocumented, for hacking only!
  1684. $.contextMenu.handle = handle;
  1685. $.contextMenu.op = op;
  1686. $.contextMenu.menus = menus;
  1687.  
  1688. })(jQuery);
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement