text: // Avoid `console` errors in browsers that lack a console.
  (function() {
      var method;
      var noop = function () {};
      var methods = [
          'assert', 'clear', 'count', 'debug', 'dir', 'dirxml', 'error',
          'exception', 'group', 'groupCollapsed', 'groupEnd', 'info', 'log',
          'markTimeline', 'profile', 'profileEnd', 'table', 'time', 'timeEnd',
          'timeStamp', 'trace', 'warn'
      ];
      var length = methods.length;
      var console = (window.console = window.console || {});
  
      while (length--) {
          method = methods[length];
  
          // Only stub undefined methods.
          if (!console[method]) {
              console[method] = noop;
          }
      }
  }());
  
  // Place any jQuery/helper plugins in here.
  
  /*!
   * jQuery UI Position v1.10.0
   * http://jqueryui.com
   *
   * Copyright 2013 jQuery Foundation and other contributors
   * Released under the MIT license.
   * http://jquery.org/license
   *
   * http://api.jqueryui.com/position/
   */
  (function( $, undefined ) {
  
  $.ui = $.ui || {};
  
  var cachedScrollbarWidth,
      max = Math.max,
      abs = Math.abs,
      round = Math.round,
      rhorizontal = /left|center|right/,
      rvertical = /top|center|bottom/,
      roffset = /[\+\-]\d+%?/,
      rposition = /^\w+/,
      rpercent = /%$/,
      _position = $.fn.position;
  
  function getOffsets( offsets, width, height ) {
      return [
          parseInt( offsets[ 0 ], 10 ) * ( rpercent.test( offsets[ 0 ] ) ? width / 100 : 1 ),
          parseInt( offsets[ 1 ], 10 ) * ( rpercent.test( offsets[ 1 ] ) ? height / 100 : 1 )
      ];
  }
  
  function parseCss( element, property ) {
      return parseInt( $.css( element, property ), 10 ) || 0;
  }
  
  function getDimensions( elem ) {
      var raw = elem[0];
      if ( raw.nodeType === 9 ) {
          return {
              width: elem.width(),
              height: elem.height(),
              offset: { top: 0, left: 0 }
          };
      }
      if ( $.isWindow( raw ) ) {
          return {
              width: elem.width(),
              height: elem.height(),
              offset: { top: elem.scrollTop(), left: elem.scrollLeft() }
          };
      }
      if ( raw.preventDefault ) {
          return {
              width: 0,
              height: 0,
              offset: { top: raw.pageY, left: raw.pageX }
          };
      }
      return {
          width: elem.outerWidth(),
          height: elem.outerHeight(),
          offset: elem.offset()
      };
  }
  
  $.position = {
      scrollbarWidth: function() {
          if ( cachedScrollbarWidth !== undefined ) {
              return cachedScrollbarWidth;
          }
          var w1, w2,
              div = $( "<div style='display:block;width:50px;height:50px;overflow:hidden;'><div style='height:100px;width:auto;'></div></div>" ),
              innerDiv = div.children()[0];
  
          $( "body" ).append( div );
          w1 = innerDiv.offsetWidth;
          div.css( "overflow", "scroll" );
  
          w2 = innerDiv.offsetWidth;
  
          if ( w1 === w2 ) {
              w2 = div[0].clientWidth;
          }
  
          div.remove();
  
          return (cachedScrollbarWidth = w1 - w2);
      },
      getScrollInfo: function( within ) {
          var overflowX = within.isWindow ? "" : within.element.css( "overflow-x" ),
              overflowY = within.isWindow ? "" : within.element.css( "overflow-y" ),
              hasOverflowX = overflowX === "scroll" ||
                  ( overflowX === "auto" && within.width < within.element[0].scrollWidth ),
              hasOverflowY = overflowY === "scroll" ||
                  ( overflowY === "auto" && within.height < within.element[0].scrollHeight );
          return {
              width: hasOverflowX ? $.position.scrollbarWidth() : 0,
              height: hasOverflowY ? $.position.scrollbarWidth() : 0
          };
      },
      getWithinInfo: function( element ) {
          var withinElement = $( element || window ),
              isWindow = $.isWindow( withinElement[0] );
          return {
              element: withinElement,
              isWindow: isWindow,
              offset: withinElement.offset() || { left: 0, top: 0 },
              scrollLeft: withinElement.scrollLeft(),
              scrollTop: withinElement.scrollTop(),
              width: isWindow ? withinElement.width() : withinElement.outerWidth(),
              height: isWindow ? withinElement.height() : withinElement.outerHeight()
          };
      }
  };
  
  $.fn.position = function( options ) {
      if ( !options || !options.of ) {
          return _position.apply( this, arguments );
      }
  
      // make a copy, we don't want to modify arguments
      options = $.extend( {}, options );
  
      var atOffset, targetWidth, targetHeight, targetOffset, basePosition, dimensions,
          target = $( options.of ),
          within = $.position.getWithinInfo( options.within ),
          scrollInfo = $.position.getScrollInfo( within ),
          collision = ( options.collision || "flip" ).split( " " ),
          offsets = {};
  
      dimensions = getDimensions( target );
      if ( target[0].preventDefault ) {
          // force left top to allow flipping
          options.at = "left top";
      }
      targetWidth = dimensions.width;
      targetHeight = dimensions.height;
      targetOffset = dimensions.offset;
      // clone to reuse original targetOffset later
      basePosition = $.extend( {}, targetOffset );
  
      // force my and at to have valid horizontal and vertical positions
      // if a value is missing or invalid, it will be converted to center
      $.each( [ "my", "at" ], function() {
          var pos = ( options[ this ] || "" ).split( " " ),
              horizontalOffset,
              verticalOffset;
  
          if ( pos.length === 1) {
              pos = rhorizontal.test( pos[ 0 ] ) ?
                  pos.concat( [ "center" ] ) :
                  rvertical.test( pos[ 0 ] ) ?
                      [ "center" ].concat( pos ) :
                      [ "center", "center" ];
          }
          pos[ 0 ] = rhorizontal.test( pos[ 0 ] ) ? pos[ 0 ] : "center";
          pos[ 1 ] = rvertical.test( pos[ 1 ] ) ? pos[ 1 ] : "center";
  
          // calculate offsets
          horizontalOffset = roffset.exec( pos[ 0 ] );
          verticalOffset = roffset.exec( pos[ 1 ] );
          offsets[ this ] = [
              horizontalOffset ? horizontalOffset[ 0 ] : 0,
              verticalOffset ? verticalOffset[ 0 ] : 0
          ];
  
          // reduce to just the positions without the offsets
          options[ this ] = [
              rposition.exec( pos[ 0 ] )[ 0 ],
              rposition.exec( pos[ 1 ] )[ 0 ]
          ];
      });
  
      // normalize collision option
      if ( collision.length === 1 ) {
          collision[ 1 ] = collision[ 0 ];
      }
  
      if ( options.at[ 0 ] === "right" ) {
          basePosition.left += targetWidth;
      } else if ( options.at[ 0 ] === "center" ) {
          basePosition.left += targetWidth / 2;
      }
  
      if ( options.at[ 1 ] === "bottom" ) {
          basePosition.top += targetHeight;
      } else if ( options.at[ 1 ] === "center" ) {
          basePosition.top += targetHeight / 2;
      }
  
      atOffset = getOffsets( offsets.at, targetWidth, targetHeight );
      basePosition.left += atOffset[ 0 ];
      basePosition.top += atOffset[ 1 ];
  
      return this.each(function() {
          var collisionPosition, using,
              elem = $( this ),
              elemWidth = elem.outerWidth(),
              elemHeight = elem.outerHeight(),
              marginLeft = parseCss( this, "marginLeft" ),
              marginTop = parseCss( this, "marginTop" ),
              collisionWidth = elemWidth + marginLeft + parseCss( this, "marginRight" ) + scrollInfo.width,
              collisionHeight = elemHeight + marginTop + parseCss( this, "marginBottom" ) + scrollInfo.height,
              position = $.extend( {}, basePosition ),
              myOffset = getOffsets( offsets.my, elem.outerWidth(), elem.outerHeight() );
  
          if ( options.my[ 0 ] === "right" ) {
              position.left -= elemWidth;
          } else if ( options.my[ 0 ] === "center" ) {
              position.left -= elemWidth / 2;
          }
  
          if ( options.my[ 1 ] === "bottom" ) {
              position.top -= elemHeight;
          } else if ( options.my[ 1 ] === "center" ) {
              position.top -= elemHeight / 2;
          }
  
          position.left += myOffset[ 0 ];
          position.top += myOffset[ 1 ];
  
          // if the browser doesn't support fractions, then round for consistent results
          if ( !$.support.offsetFractions ) {
              position.left = round( position.left );
              position.top = round( position.top );
          }
  
          collisionPosition = {
              marginLeft: marginLeft,
              marginTop: marginTop
          };
  
          $.each( [ "left", "top" ], function( i, dir ) {
              if ( $.ui.position[ collision[ i ] ] ) {
                  $.ui.position[ collision[ i ] ][ dir ]( position, {
                      targetWidth: targetWidth,
                      targetHeight: targetHeight,
                      elemWidth: elemWidth,
                      elemHeight: elemHeight,
                      collisionPosition: collisionPosition,
                      collisionWidth: collisionWidth,
                      collisionHeight: collisionHeight,
                      offset: [ atOffset[ 0 ] + myOffset[ 0 ], atOffset [ 1 ] + myOffset[ 1 ] ],
                      my: options.my,
                      at: options.at,
                      within: within,
                      elem : elem
                  });
              }
          });
  
          if ( options.using ) {
              // adds feedback as second argument to using callback, if present
              using = function( props ) {
                  var left = targetOffset.left - position.left,
                      right = left + targetWidth - elemWidth,
                      top = targetOffset.top - position.top,
                      bottom = top + targetHeight - elemHeight,
                      feedback = {
                          target: {
                              element: target,
                              left: targetOffset.left,
                              top: targetOffset.top,
                              width: targetWidth,
                              height: targetHeight
                          },
                          element: {
                              element: elem,
                              left: position.left,
                              top: position.top,
                              width: elemWidth,
                              height: elemHeight
                          },
                          horizontal: right < 0 ? "left" : left > 0 ? "right" : "center",
                          vertical: bottom < 0 ? "top" : top > 0 ? "bottom" : "middle"
                      };
                  if ( targetWidth < elemWidth && abs( left + right ) < targetWidth ) {
                      feedback.horizontal = "center";
                  }
                  if ( targetHeight < elemHeight && abs( top + bottom ) < targetHeight ) {
                      feedback.vertical = "middle";
                  }
                  if ( max( abs( left ), abs( right ) ) > max( abs( top ), abs( bottom ) ) ) {
                      feedback.important = "horizontal";
                  } else {
                      feedback.important = "vertical";
                  }
                  options.using.call( this, props, feedback );
              };
          }
  
          elem.offset( $.extend( position, { using: using } ) );
      });
  };
  
  $.ui.position = {
      fit: {
          left: function( position, data ) {
              var within = data.within,
                  withinOffset = within.isWindow ? within.scrollLeft : within.offset.left,
                  outerWidth = within.width,
                  collisionPosLeft = position.left - data.collisionPosition.marginLeft,
                  overLeft = withinOffset - collisionPosLeft,
                  overRight = collisionPosLeft + data.collisionWidth - outerWidth - withinOffset,
                  newOverRight;
  
              // element is wider than within
              if ( data.collisionWidth > outerWidth ) {
                  // element is initially over the left side of within
                  if ( overLeft > 0 && overRight <= 0 ) {
                      newOverRight = position.left + overLeft + data.collisionWidth - outerWidth - withinOffset;
                      position.left += overLeft - newOverRight;
                  // element is initially over right side of within
                  } else if ( overRight > 0 && overLeft <= 0 ) {
                      position.left = withinOffset;
                  // element is initially over both left and right sides of within
                  } else {
                      if ( overLeft > overRight ) {
                          position.left = withinOffset + outerWidth - data.collisionWidth;
                      } else {
                          position.left = withinOffset;
                      }
                  }
              // too far left -> align with left edge
              } else if ( overLeft > 0 ) {
                  position.left += overLeft;
              // too far right -> align with right edge
              } else if ( overRight > 0 ) {
                  position.left -= overRight;
              // adjust based on position and margin
              } else {
                  position.left = max( position.left - collisionPosLeft, position.left );
              }
          },
          top: function( position, data ) {
              var within = data.within,
                  withinOffset = within.isWindow ? within.scrollTop : within.offset.top,
                  outerHeight = data.within.height,
                  collisionPosTop = position.top - data.collisionPosition.marginTop,
                  overTop = withinOffset - collisionPosTop,
                  overBottom = collisionPosTop + data.collisionHeight - outerHeight - withinOffset,
                  newOverBottom;
  
              // element is taller than within
              if ( data.collisionHeight > outerHeight ) {
                  // element is initially over the top of within
                  if ( overTop > 0 && overBottom <= 0 ) {
                      newOverBottom = position.top + overTop + data.collisionHeight - outerHeight - withinOffset;
                      position.top += overTop - newOverBottom;
                  // element is initially over bottom of within
                  } else if ( overBottom > 0 && overTop <= 0 ) {
                      position.top = withinOffset;
                  // element is initially over both top and bottom of within
                  } else {
                      if ( overTop > overBottom ) {
                          position.top = withinOffset + outerHeight - data.collisionHeight;
                      } else {
                          position.top = withinOffset;
                      }
                  }
              // too far up -> align with top
              } else if ( overTop > 0 ) {
                  position.top += overTop;
              // too far down -> align with bottom edge
              } else if ( overBottom > 0 ) {
                  position.top -= overBottom;
              // adjust based on position and margin
              } else {
                  position.top = max( position.top - collisionPosTop, position.top );
              }
          }
      },
      flip: {
          left: function( position, data ) {
              var within = data.within,
                  withinOffset = within.offset.left + within.scrollLeft,
                  outerWidth = within.width,
                  offsetLeft = within.isWindow ? within.scrollLeft : within.offset.left,
                  collisionPosLeft = position.left - data.collisionPosition.marginLeft,
                  overLeft = collisionPosLeft - offsetLeft,
                  overRight = collisionPosLeft + data.collisionWidth - outerWidth - offsetLeft,
                  myOffset = data.my[ 0 ] === "left" ?
                      -data.elemWidth :
                      data.my[ 0 ] === "right" ?
                          data.elemWidth :
                          0,
                  atOffset = data.at[ 0 ] === "left" ?
                      data.targetWidth :
                      data.at[ 0 ] === "right" ?
                          -data.targetWidth :
                          0,
                  offset = -2 * data.offset[ 0 ],
                  newOverRight,
                  newOverLeft;
  
              if ( overLeft < 0 ) {
                  newOverRight = position.left + myOffset + atOffset + offset + data.collisionWidth - outerWidth - withinOffset;
                  if ( newOverRight < 0 || newOverRight < abs( overLeft ) ) {
                      position.left += myOffset + atOffset + offset;
                  }
              }
              else if ( overRight > 0 ) {
                  newOverLeft = position.left - data.collisionPosition.marginLeft + myOffset + atOffset + offset - offsetLeft;
                  if ( newOverLeft > 0 || abs( newOverLeft ) < overRight ) {
                      position.left += myOffset + atOffset + offset;
                  }
              }
          },
          top: function( position, data ) {
              var within = data.within,
                  withinOffset = within.offset.top + within.scrollTop,
                  outerHeight = within.height,
                  offsetTop = within.isWindow ? within.scrollTop : within.offset.top,
                  collisionPosTop = position.top - data.collisionPosition.marginTop,
                  overTop = collisionPosTop - offsetTop,
                  overBottom = collisionPosTop + data.collisionHeight - outerHeight - offsetTop,
                  top = data.my[ 1 ] === "top",
                  myOffset = top ?
                      -data.elemHeight :
                      data.my[ 1 ] === "bottom" ?
                          data.elemHeight :
                          0,
                  atOffset = data.at[ 1 ] === "top" ?
                      data.targetHeight :
                      data.at[ 1 ] === "bottom" ?
                          -data.targetHeight :
                          0,
                  offset = -2 * data.offset[ 1 ],
                  newOverTop,
                  newOverBottom;
              if ( overTop < 0 ) {
                  newOverBottom = position.top + myOffset + atOffset + offset + data.collisionHeight - outerHeight - withinOffset;
                  if ( ( position.top + myOffset + atOffset + offset) > overTop && ( newOverBottom < 0 || newOverBottom < abs( overTop ) ) ) {
                      position.top += myOffset + atOffset + offset;
                  }
              }
              else if ( overBottom > 0 ) {
                  newOverTop = position.top -  data.collisionPosition.marginTop + myOffset + atOffset + offset - offsetTop;
                  if ( ( position.top + myOffset + atOffset + offset) > overBottom && ( newOverTop > 0 || abs( newOverTop ) < overBottom ) ) {
                      position.top += myOffset + atOffset + offset;
                  }
              }
          }
      },
      flipfit: {
          left: function() {
              $.ui.position.flip.left.apply( this, arguments );
              $.ui.position.fit.left.apply( this, arguments );
          },
          top: function() {
              $.ui.position.flip.top.apply( this, arguments );
              $.ui.position.fit.top.apply( this, arguments );
          }
      }
  };
  
  // fraction support test
  (function () {
      var testElement, testElementParent, testElementStyle, offsetLeft, i,
          body = document.getElementsByTagName( "body" )[ 0 ],
          div = document.createElement( "div" );
  
      //Create a "fake body" for testing based on method used in jQuery.support
      testElement = document.createElement( body ? "div" : "body" );
      testElementStyle = {
          visibility: "hidden",
          width: 0,
          height: 0,
          border: 0,
          margin: 0,
          background: "none"
      };
      if ( body ) {
          $.extend( testElementStyle, {
              position: "absolute",
              left: "-1000px",
              top: "-1000px"
          });
      }
      for ( i in testElementStyle ) {
          testElement.style[ i ] = testElementStyle[ i ];
      }
      testElement.appendChild( div );
      testElementParent = body || document.documentElement;
      testElementParent.insertBefore( testElement, testElementParent.firstChild );
  
      div.style.cssText = "position: absolute; left: 10.7432222px;";
  
      offsetLeft = $( div ).offset().left;
      $.support.offsetFractions = offsetLeft > 10 && offsetLeft < 11;
  
      testElement.innerHTML = "";
      testElementParent.removeChild( testElement );
  })();
  
  }( jQuery ) );
  
  
  /*!
   * jQuery contextMenu - Plugin for simple contextMenu handling
   *
   * Version: git-master
   *
   * Authors: Rodney Rehm, Addy Osmani (patches for FF)
   * Web: http://medialize.github.com/jQuery-contextMenu/
   *
   * Licensed under
   *   MIT License http://www.opensource.org/licenses/mit-license
   *   GPL v3 http://opensource.org/licenses/GPL-3.0
   *
   */
  
  (function($, undefined){
  
      // TODO: -
          // ARIA stuff: menuitem, menuitemcheckbox und menuitemradio
          // create <menu> structure if $.support[htmlCommand || htmlMenuitem] and !opt.disableNative
  
  // determine html5 compatibility
  $.support.htmlMenuitem = ('HTMLMenuItemElement' in window);
  $.support.htmlCommand = ('HTMLCommandElement' in window);
  $.support.eventSelectstart = ("onselectstart" in document.documentElement);
  /* // should the need arise, test for css user-select
  $.support.cssUserSelect = (function(){
      var t = false,
          e = document.createElement('div');
  
      $.each('Moz|Webkit|Khtml|O|ms|Icab|'.split('|'), function(i, prefix) {
          var propCC = prefix + (prefix ? 'U' : 'u') + 'serSelect',
              prop = (prefix ? ('-' + prefix.toLowerCase() + '-') : '') + 'user-select';
  
          e.style.cssText = prop + ': text;';
          if (e.style[propCC] == 'text') {
              t = true;
              return false;
          }
  
          return true;
      });
  
      return t;
  })();
  */
  
  if (!$.ui || !$.ui.widget) {
      // duck punch $.cleanData like jQueryUI does to get that remove event
      // https://github.com/jquery/jquery-ui/blob/master/ui/jquery.ui.widget.js#L16-24
      var _cleanData = $.cleanData;
      $.cleanData = function( elems ) {
          for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) {
              try {
                  $( elem ).triggerHandler( "remove" );
                  // http://bugs.jquery.com/ticket/8235
              } catch( e ) {}
          }
          _cleanData( elems );
      };
  }
  
  var // currently active contextMenu trigger
      $currentTrigger = null,
      // is contextMenu initialized with at least one menu?
      initialized = false,
      // window handle
      $win = $(window),
      // number of registered menus
      counter = 0,
      // mapping selector to namespace
      namespaces = {},
      // mapping namespace to options
      menus = {},
      // custom command type handlers
      types = {},
      // default values
      defaults = {
          // selector of contextMenu trigger
          selector: null,
          // where to append the menu to
          appendTo: null,
          // method to trigger context menu ["right", "left", "hover"]
          trigger: "right",
          // hide menu when mouse leaves trigger / menu elements
          autoHide: false,
          // ms to wait before showing a hover-triggered context menu
          delay: 200,
          // flag denoting if a second trigger should simply move (true) or rebuild (false) an open menu
          // as long as the trigger happened on one of the trigger-element's child nodes
          reposition: true,
          // determine position to show menu at
          determinePosition: function($menu) {
              // position to the lower middle of the trigger element
              if ($.ui && $.ui.position) {
                  // .position() is provided as a jQuery UI utility
                  // (...and it won't work on hidden elements)
                  $menu.css('display', 'block').position({
                      my: "center top",
                      at: "center bottom",
                      of: this,
                      offset: "0 5",
                      collision: "fit"
                  }).css('display', 'none');
              } else {
                  // determine contextMenu position
                  var offset = this.offset();
                  offset.top += this.outerHeight();
                  offset.left += this.outerWidth() / 2 - $menu.outerWidth() / 2;
                  $menu.css(offset);
              }
          },
          // position menu
          position: function(opt, x, y) {
              var $this = this,
                  offset;
              // determine contextMenu position
              if (!x && !y) {
                  opt.determinePosition.call(this, opt.$menu);
                  return;
              } else if (x === "maintain" && y === "maintain") {
                  // x and y must not be changed (after re-show on command click)
                  offset = opt.$menu.position();
              } else {
                  // x and y are given (by mouse event)
                  offset = {top: y, left: x};
              }
  
              // correct offset if viewport demands it
              var bottom = $win.scrollTop() + $win.height(),
                  right = $win.scrollLeft() + $win.width(),
                  height = opt.$menu.height(),
                  width = opt.$menu.width();
  
              if (offset.top + height > bottom) {
                  offset.top -= height;
              }
  
              if (offset.left + width > right) {
                  offset.left -= width;
              }
  
              opt.$menu.css(offset);
          },
          // position the sub-menu
          positionSubmenu: function($menu) {
              if ($.ui && $.ui.position) {
                  // .position() is provided as a jQuery UI utility
                  // (...and it won't work on hidden elements)
                  $menu.css('display', 'block').position({
                      my: "left top",
                      at: "right top",
                      of: this,
                      collision: "flipfit fit"
                  }).css('display', '');
              } else {
                  // determine contextMenu position
                  var offset = {
                      top: 0,
                      left: this.outerWidth()
                  };
                  $menu.css(offset);
              }
          },
          // offset to add to zIndex
          zIndex: 1,
          // show hide animation settings
          animation: {
              duration: 50,
              show: 'slideDown',
              hide: 'slideUp'
          },
          // events
          events: {
              show: $.noop,
              hide: $.noop
          },
          // default callback
          callback: null,
          // list of contextMenu items
          items: {}
      },
      // mouse position for hover activation
      hoveract = {
          timer: null,
          pageX: null,
          pageY: null
      },
      // determine zIndex
      zindex = function($t) {
          var zin = 0,
              $tt = $t;
  
          while (true) {
              zin = Math.max(zin, parseInt($tt.css('z-index'), 10) || 0);
              $tt = $tt.parent();
              if (!$tt || !$tt.length || "html body".indexOf($tt.prop('nodeName').toLowerCase()) > -1 ) {
                  break;
              }
          }
  
          return zin;
      },
      // event handlers
      handle = {
          // abort anything
          abortevent: function(e){
              e.preventDefault();
              e.stopImmediatePropagation();
          },
  
          // contextmenu show dispatcher
          contextmenu: function(e) {
              var $this = $(this);
  
              // disable actual context-menu
              e.preventDefault();
              e.stopImmediatePropagation();
  
              // abort native-triggered events unless we're triggering on right click
              if (e.data.trigger != 'right' && e.originalEvent) {
                  return;
              }
  
              // abort event if menu is visible for this trigger
              if ($this.hasClass('context-menu-active')) {
                  return;
              }
  
              if (!$this.hasClass('context-menu-disabled')) {
                  // theoretically need to fire a show event at <menu>
                  // http://www.whatwg.org/specs/web-apps/current-work/multipage/interactive-elements.html#context-menus
                  // var evt = jQuery.Event("show", { data: data, pageX: e.pageX, pageY: e.pageY, relatedTarget: this });
                  // e.data.$menu.trigger(evt);
  
                  $currentTrigger = $this;
                  if (e.data.build) {
                      var built = e.data.build($currentTrigger, e);
                      // abort if build() returned false
                      if (built === false) {
                          return;
                      }
  
                      // dynamically build menu on invocation
                      e.data = $.extend(true, {}, defaults, e.data, built || {});
  
                      // abort if there are no items to display
                      if (!e.data.items || $.isEmptyObject(e.data.items)) {
                          // Note: jQuery captures and ignores errors from event handlers
                          if (window.console) {
                              (console.error || console.log)("No items specified to show in contextMenu");
                          }
  
                          throw new Error('No Items specified');
                      }
  
                      // backreference for custom command type creation
                      e.data.$trigger = $currentTrigger;
  
                      op.create(e.data);
                  }
                  // show menu
                  op.show.call($this, e.data, e.pageX, e.pageY);
              }
          },
          // contextMenu left-click trigger
          click: function(e) {
              e.preventDefault();
              e.stopImmediatePropagation();
              $(this).trigger($.Event("contextmenu", { data: e.data, pageX: e.pageX, pageY: e.pageY }));
          },
          // contextMenu right-click trigger
          mousedown: function(e) {
              // register mouse down
              var $this = $(this);
  
              // hide any previous menus
              if ($currentTrigger && $currentTrigger.length && !$currentTrigger.is($this)) {
                  $currentTrigger.data('contextMenu').$menu.trigger('contextmenu:hide');
              }
  
              // activate on right click
              if (e.button == 2) {
                  $currentTrigger = $this.data('contextMenuActive', true);
              }
          },
          // contextMenu right-click trigger
          mouseup: function(e) {
              // show menu
              var $this = $(this);
              if ($this.data('contextMenuActive') && $currentTrigger && $currentTrigger.length && $currentTrigger.is($this) && !$this.hasClass('context-menu-disabled')) {
                  e.preventDefault();
                  e.stopImmediatePropagation();
                  $currentTrigger = $this;
                  $this.trigger($.Event("contextmenu", { data: e.data, pageX: e.pageX, pageY: e.pageY }));
              }
  
              $this.removeData('contextMenuActive');
          },
          // contextMenu hover trigger
          mouseenter: function(e) {
              var $this = $(this),
                  $related = $(e.relatedTarget),
                  $document = $(document);
  
              // abort if we're coming from a menu
              if ($related.is('.context-menu-list') || $related.closest('.context-menu-list').length) {
                  return;
              }
  
              // abort if a menu is shown
              if ($currentTrigger && $currentTrigger.length) {
                  return;
              }
  
              hoveract.pageX = e.pageX;
              hoveract.pageY = e.pageY;
              hoveract.data = e.data;
              $document.on('mousemove.contextMenuShow', handle.mousemove);
              hoveract.timer = setTimeout(function() {
                  hoveract.timer = null;
                  $document.off('mousemove.contextMenuShow');
                  $currentTrigger = $this;
                  $this.trigger($.Event("contextmenu", { data: hoveract.data, pageX: hoveract.pageX, pageY: hoveract.pageY }));
              }, e.data.delay );
          },
          // contextMenu hover trigger
          mousemove: function(e) {
              hoveract.pageX = e.pageX;
              hoveract.pageY = e.pageY;
          },
          // contextMenu hover trigger
          mouseleave: function(e) {
              // abort if we're leaving for a menu
              var $related = $(e.relatedTarget);
              if ($related.is('.context-menu-list') || $related.closest('.context-menu-list').length) {
                  return;
              }
  
              try {
                  clearTimeout(hoveract.timer);
              } catch(e) {}
  
              hoveract.timer = null;
          },
  
          // click on layer to hide contextMenu
          layerClick: function(e) {
              var $this = $(this),
                  root = $this.data('contextMenuRoot'),
                  mouseup = false,
                  button = e.button,
                  x = e.pageX,
                  y = e.pageY,
                  target,
                  offset,
                  selectors;
  
              e.preventDefault();
              e.stopImmediatePropagation();
  
              setTimeout(function() {
                  var $window, hideshow, possibleTarget;
                  var triggerAction = ((root.trigger == 'left' && button === 0) || (root.trigger == 'right' && button === 2));
  
                  // find the element that would've been clicked, wasn't the layer in the way
                  if (document.elementFromPoint) {
                      root.$layer.hide();
                      target = document.elementFromPoint(x - $win.scrollLeft(), y - $win.scrollTop());
                      root.$layer.show();
                  }
  
                  if (root.reposition && triggerAction) {
                      if (document.elementFromPoint) {
                          if (root.$trigger.is(target) || root.$trigger.has(target).length) {
                              root.position.call(root.$trigger, root, x, y);
                              return;
                          }
                      } else {
                          offset = root.$trigger.offset();
                          $window = $(window);
                          // while this looks kinda awful, it's the best way to avoid
                          // unnecessarily calculating any positions
                          offset.top += $window.scrollTop();
                          if (offset.top <= e.pageY) {
                              offset.left += $window.scrollLeft();
                              if (offset.left <= e.pageX) {
                                  offset.bottom = offset.top + root.$trigger.outerHeight();
                                  if (offset.bottom >= e.pageY) {
                                      offset.right = offset.left + root.$trigger.outerWidth();
                                      if (offset.right >= e.pageX) {
                                          // reposition
                                          root.position.call(root.$trigger, root, x, y);
                                          return;
                                      }
                                  }
                              }
                          }
                      }
                  }
  
                  if (target && triggerAction) {
                      root.$trigger.one('contextmenu:hidden', function() {
                          $(target).contextMenu({x: x, y: y});
                      });
                  }
  
                  root.$menu.trigger('contextmenu:hide');
              }, 50);
          },
          // key handled :hover
          keyStop: function(e, opt) {
              if (!opt.isInput) {
                  e.preventDefault();
              }
  
              e.stopPropagation();
          },
          key: function(e) {
              var opt = $currentTrigger.data('contextMenu') || {};
  
              switch (e.keyCode) {
                  case 9:
                  case 38: // up
                      handle.keyStop(e, opt);
                      // if keyCode is [38 (up)] or [9 (tab) with shift]
                      if (opt.isInput) {
                          if (e.keyCode == 9 && e.shiftKey) {
                              e.preventDefault();
                              opt.$selected && opt.$selected.find('input, textarea, select').blur();
                              opt.$menu.trigger('prevcommand');
                              return;
                          } else if (e.keyCode == 38 && opt.$selected.find('input, textarea, select').prop('type') == 'checkbox') {
                              // checkboxes don't capture this key
                              e.preventDefault();
                              return;
                          }
                      } else if (e.keyCode != 9 || e.shiftKey) {
                          opt.$menu.trigger('prevcommand');
                          return;
                      }
                      // omitting break;
  
                  // case 9: // tab - reached through omitted break;
                  case 40: // down
                      handle.keyStop(e, opt);
                      if (opt.isInput) {
                          if (e.keyCode == 9) {
                              e.preventDefault();
                              opt.$selected && opt.$selected.find('input, textarea, select').blur();
                              opt.$menu.trigger('nextcommand');
                              return;
                          } else if (e.keyCode == 40 && opt.$selected.find('input, textarea, select').prop('type') == 'checkbox') {
                              // checkboxes don't capture this key
                              e.preventDefault();
                              return;
                          }
                      } else {
                          opt.$menu.trigger('nextcommand');
                          return;
                      }
                      break;
  
                  case 37: // left
                      handle.keyStop(e, opt);
                      if (opt.isInput || !opt.$selected || !opt.$selected.length) {
                          break;
                      }
  
                      if (!opt.$selected.parent().hasClass('context-menu-root')) {
                          var $parent = opt.$selected.parent().parent();
                          opt.$selected.trigger('contextmenu:blur');
                          opt.$selected = $parent;
                          return;
                      }
                      break;
  
                  case 39: // right
                      handle.keyStop(e, opt);
                      if (opt.isInput || !opt.$selected || !opt.$selected.length) {
                          break;
                      }
  
                      var itemdata = opt.$selected.data('contextMenu') || {};
                      if (itemdata.$menu && opt.$selected.hasClass('context-menu-submenu')) {
                          opt.$selected = null;
                          itemdata.$selected = null;
                          itemdata.$menu.trigger('nextcommand');
                          return;
                      }
                      break;
  
                  case 35: // end
                  case 36: // home
                      if (opt.$selected && opt.$selected.find('input, textarea, select').length) {
                          return;
                      } else {
                          (opt.$selected && opt.$selected.parent() || opt.$menu)
                              .children(':not(.disabled, .not-selectable)')[e.keyCode == 36 ? 'first' : 'last']()
                              .trigger('contextmenu:focus');
                          e.preventDefault();
                          return;
                      }
                      break;
  
                  case 13: // enter
                      handle.keyStop(e, opt);
                      if (opt.isInput) {
                          if (opt.$selected && !opt.$selected.is('textarea, select')) {
                              e.preventDefault();
                              return;
                          }
                          break;
                      }
                      opt.$selected && opt.$selected.trigger('mouseup');
                      return;
  
                  case 32: // space
                  case 33: // page up
                  case 34: // page down
                      // prevent browser from scrolling down while menu is visible
                      handle.keyStop(e, opt);
                      return;
  
                  case 27: // esc
                      handle.keyStop(e, opt);
                      opt.$menu.trigger('contextmenu:hide');
                      return;
  
                  default: // 0-9, a-z
                      var k = (String.fromCharCode(e.keyCode)).toUpperCase();
                      if (opt.accesskeys[k]) {
                          // according to the specs accesskeys must be invoked immediately
                          opt.accesskeys[k].$node.trigger(opt.accesskeys[k].$menu
                              ? 'contextmenu:focus'
                              : 'mouseup'
                          );
                          return;
                      }
                      break;
              }
              // pass event to selected item,
              // stop propagation to avoid endless recursion
              e.stopPropagation();
              opt.$selected && opt.$selected.trigger(e);
          },
  
          // select previous possible command in menu
          prevItem: function(e) {
              e.stopPropagation();
              var opt = $(this).data('contextMenu') || {};
  
              // obtain currently selected menu
              if (opt.$selected) {
                  var $s = opt.$selected;
                  opt = opt.$selected.parent().data('contextMenu') || {};
                  opt.$selected = $s;
              }
  
              var $children = opt.$menu.children(),
                  $prev = !opt.$selected || !opt.$selected.prev().length ? $children.last() : opt.$selected.prev(),
                  $round = $prev;
  
              // skip disabled
              while ($prev.hasClass('disabled') || $prev.hasClass('not-selectable')) {
                  if ($prev.prev().length) {
                      $prev = $prev.prev();
                  } else {
                      $prev = $children.last();
                  }
                  if ($prev.is($round)) {
                      // break endless loop
                      return;
                  }
              }
  
              // leave current
              if (opt.$selected) {
                  handle.itemMouseleave.call(opt.$selected.get(0), e);
              }
  
              // activate next
              handle.itemMouseenter.call($prev.get(0), e);
  
              // focus input
              var $input = $prev.find('input, textarea, select');
              if ($input.length) {
                  $input.focus();
              }
          },
          // select next possible command in menu
          nextItem: function(e) {
              e.stopPropagation();
              var opt = $(this).data('contextMenu') || {};
  
              // obtain currently selected menu
              if (opt.$selected) {
                  var $s = opt.$selected;
                  opt = opt.$selected.parent().data('contextMenu') || {};
                  opt.$selected = $s;
              }
  
              var $children = opt.$menu.children(),
                  $next = !opt.$selected || !opt.$selected.next().length ? $children.first() : opt.$selected.next(),
                  $round = $next;
  
              // skip disabled
              while ($next.hasClass('disabled') || $next.hasClass('not-selectable')) {
                  if ($next.next().length) {
                      $next = $next.next();
                  } else {
                      $next = $children.first();
                  }
                  if ($next.is($round)) {
                      // break endless loop
                      return;
                  }
              }
  
              // leave current
              if (opt.$selected) {
                  handle.itemMouseleave.call(opt.$selected.get(0), e);
              }
  
              // activate next
              handle.itemMouseenter.call($next.get(0), e);
  
              // focus input
              var $input = $next.find('input, textarea, select');
              if ($input.length) {
                  $input.focus();
              }
          },
  
          // flag that we're inside an input so the key handler can act accordingly
          focusInput: function(e) {
              var $this = $(this).closest('.context-menu-item'),
                  data = $this.data(),
                  opt = data.contextMenu,
                  root = data.contextMenuRoot;
  
              root.$selected = opt.$selected = $this;
              root.isInput = opt.isInput = true;
          },
          // flag that we're inside an input so the key handler can act accordingly
          blurInput: function(e) {
              var $this = $(this).closest('.context-menu-item'),
                  data = $this.data(),
                  opt = data.contextMenu,
                  root = data.contextMenuRoot;
  
              root.isInput = opt.isInput = false;
          },
  
          // :hover on menu
          menuMouseenter: function(e) {
              var root = $(this).data().contextMenuRoot;
              root.hovering = true;
          },
          // :hover on menu
          menuMouseleave: function(e) {
              var root = $(this).data().contextMenuRoot;
              if (root.$layer && root.$layer.is(e.relatedTarget)) {
                  root.hovering = false;
              }
          },
  
          // :hover done manually so key handling is possible
          itemMouseenter: function(e) {
              var $this = $(this),
                  data = $this.data(),
                  opt = data.contextMenu,
                  root = data.contextMenuRoot;
  
              root.hovering = true;
  
              // abort if we're re-entering
              if (e && root.$layer && root.$layer.is(e.relatedTarget)) {
                  e.preventDefault();
                  e.stopImmediatePropagation();
              }
  
              // make sure only one item is selected
              (opt.$menu ? opt : root).$menu
                  .children('.hover').trigger('contextmenu:blur');
  
              if ($this.hasClass('disabled') || $this.hasClass('not-selectable')) {
                  opt.$selected = null;
                  return;
              }
  
              $this.trigger('contextmenu:focus');
          },
          // :hover done manually so key handling is possible
          itemMouseleave: function(e) {
              var $this = $(this),
                  data = $this.data(),
                  opt = data.contextMenu,
                  root = data.contextMenuRoot;
  
              if (root !== opt && root.$layer && root.$layer.is(e.relatedTarget)) {
                  root.$selected && root.$selected.trigger('contextmenu:blur');
                  e.preventDefault();
                  e.stopImmediatePropagation();
                  root.$selected = opt.$selected = opt.$node;
                  return;
              }
  
              $this.trigger('contextmenu:blur');
          },
          // contextMenu item click
          itemClick: function(e) {
              var $this = $(this),
                  data = $this.data(),
                  opt = data.contextMenu,
                  root = data.contextMenuRoot,
                  key = data.contextMenuKey,
                  callback;
  
              // abort if the key is unknown or disabled or is a menu
              if (!opt.items[key] || $this.is('.disabled, .context-menu-submenu, .context-menu-separator, .not-selectable')) {
                  return;
              }
  
              e.preventDefault();
              e.stopImmediatePropagation();
  
              if ($.isFunction(root.callbacks[key]) && Object.prototype.hasOwnProperty.call(root.callbacks, key)) {
                  // item-specific callback
                  callback = root.callbacks[key];
              } else if ($.isFunction(root.callback)) {
                  // default callback
                  callback = root.callback;
              } else {
                  // no callback, no action
                  return;
              }
  
              // hide menu if callback doesn't stop that
              if (callback.call(root.$trigger, key, root) !== false) {
                  root.$menu.trigger('contextmenu:hide');
              } else if (root.$menu.parent().length) {
                  op.update.call(root.$trigger, root);
              }
          },
          // ignore click events on input elements
          inputClick: function(e) {
              e.stopImmediatePropagation();
          },
  
          // hide <menu>
          hideMenu: function(e, data) {
              var root = $(this).data('contextMenuRoot');
              op.hide.call(root.$trigger, root, data && data.force);
          },
          // focus <command>
          focusItem: function(e) {
              e.stopPropagation();
              var $this = $(this),
                  data = $this.data(),
                  opt = data.contextMenu,
                  root = data.contextMenuRoot;
  
              $this.addClass('hover')
                  .siblings('.hover').trigger('contextmenu:blur');
  
              // remember selected
              opt.$selected = root.$selected = $this;
  
              // position sub-menu - do after show so dumb $.ui.position can keep up
              if (opt.$node) {
                  root.positionSubmenu.call(opt.$node, opt.$menu);
              }
          },
          // blur <command>
          blurItem: function(e) {
              e.stopPropagation();
              var $this = $(this),
                  data = $this.data(),
                  opt = data.contextMenu,
                  root = data.contextMenuRoot;
  
              $this.removeClass('hover');
              opt.$selected = null;
          }
      },
      // operations
      op = {
          show: function(opt, x, y) {
              var $trigger = $(this),
                  offset,
                  css = {};
  
              // hide any open menus
              $('#context-menu-layer').trigger('mousedown');
  
              // backreference for callbacks
              opt.$trigger = $trigger;
  
              // show event
              if (opt.events.show.call($trigger, opt) === false) {
                  $currentTrigger = null;
                  return;
              }
  
              // create or update context menu
              op.update.call($trigger, opt);
  
              // position menu
              opt.position.call($trigger, opt, x, y);
  
              // make sure we're in front
              if (opt.zIndex) {
                  css.zIndex = zindex($trigger) + opt.zIndex;
              }
  
              // add layer
              op.layer.call(opt.$menu, opt, css.zIndex);
  
              // adjust sub-menu zIndexes
              opt.$menu.find('ul').css('zIndex', css.zIndex + 1);
  
              // position and show context menu
              opt.$menu.css( css )[opt.animation.show](opt.animation.duration, function() {
                  $trigger.trigger('contextmenu:visible');
              });
              // make options available and set state
              $trigger
                  .data('contextMenu', opt)
                  .addClass("context-menu-active");
  
              // register key handler
              $(document).off('keydown.contextMenu').on('keydown.contextMenu', handle.key);
              // register autoHide handler
              if (opt.autoHide) {
                  // mouse position handler
                  $(document).on('mousemove.contextMenuAutoHide', function(e) {
                      // need to capture the offset on mousemove,
                      // since the page might've been scrolled since activation
                      var pos = $trigger.offset();
                      pos.right = pos.left + $trigger.outerWidth();
                      pos.bottom = pos.top + $trigger.outerHeight();
  
                      if (opt.$layer && !opt.hovering && (!(e.pageX >= pos.left && e.pageX <= pos.right) || !(e.pageY >= pos.top && e.pageY <= pos.bottom))) {
                          // if mouse in menu...
                          opt.$menu.trigger('contextmenu:hide');
                      }
                  });
              }
          },
          hide: function(opt, force) {
              var $trigger = $(this);
              if (!opt) {
                  opt = $trigger.data('contextMenu') || {};
              }
  
              // hide event
              if (!force && opt.events && opt.events.hide.call($trigger, opt) === false) {
                  return;
              }
  
              // remove options and revert state
              $trigger
                  .removeData('contextMenu')
                  .removeClass("context-menu-active");
  
              if (opt.$layer) {
                  // keep layer for a bit so the contextmenu event can be aborted properly by opera
                  setTimeout((function($layer) {
                      return function(){
                          $layer.remove();
                      };
                  })(opt.$layer), 10);
  
                  try {
                      delete opt.$layer;
                  } catch(e) {
                      opt.$layer = null;
                  }
              }
  
              // remove handle
              $currentTrigger = null;
              // remove selected
              opt.$menu.find('.hover').trigger('contextmenu:blur');
              opt.$selected = null;
              // unregister key and mouse handlers
              //$(document).off('.contextMenuAutoHide keydown.contextMenu'); // http://bugs.jquery.com/ticket/10705
              $(document).off('.contextMenuAutoHide').off('keydown.contextMenu');
              // hide menu
              opt.$menu && opt.$menu[opt.animation.hide](opt.animation.duration, function (){
                  // tear down dynamically built menu after animation is completed.
                  if (opt.build) {
                      opt.$menu.remove();
                      $.each(opt, function(key, value) {
                          switch (key) {
                              case 'ns':
                              case 'selector':
                              case 'build':
                              case 'trigger':
                                  return true;
  
                              default:
                                  opt[key] = undefined;
                                  try {
                                      delete opt[key];
                                  } catch (e) {}
                                  return true;
                          }
                      });
                  }
  
                  setTimeout(function() {
                      $trigger.trigger('contextmenu:hidden');
                  }, 10);
              });
          },
          create: function(opt, root) {
              if (root === undefined) {
                  root = opt;
              }
              // create contextMenu
              opt.$menu = $('<ul class="context-menu-list"></ul>').addClass(opt.className || "").data({
                  'contextMenu': opt,
                  'contextMenuRoot': root
              });
  
              $.each(['callbacks', 'commands', 'inputs'], function(i,k){
                  opt[k] = {};
                  if (!root[k]) {
                      root[k] = {};
                  }
              });
  
              root.accesskeys || (root.accesskeys = {});
  
              // create contextMenu items
              $.each(opt.items, function(key, item){
                  var $t = $('<li class="context-menu-item"></li>').addClass(item.className || ""),
                      $label = null,
                      $input = null;
  
                  // iOS needs to see a click-event bound to an element to actually
                  // have the TouchEvents infrastructure trigger the click event
                  $t.on('click', $.noop);
  
                  item.$node = $t.data({
                      'contextMenu': opt,
                      'contextMenuRoot': root,
                      'contextMenuKey': key
                  });
  
                  // register accesskey
                  // NOTE: the accesskey attribute should be applicable to any element, but Safari5 and Chrome13 still can't do that
                  if (item.accesskey) {
                      var aks = splitAccesskey(item.accesskey);
                      for (var i=0, ak; ak = aks[i]; i++) {
                          if (!root.accesskeys[ak]) {
                              root.accesskeys[ak] = item;
                              item._name = item.name.replace(new RegExp('(' + ak + ')', 'i'), '<span class="context-menu-accesskey">$1</span>');
                              break;
                          }
                      }
                  }
  
                  if (typeof item == "string") {
                      $t.addClass('context-menu-separator not-selectable');
                  } else if (item.type && types[item.type]) {
                      // run custom type handler
                      types[item.type].call($t, item, opt, root);
                      // register commands
                      $.each([opt, root], function(i,k){
                          k.commands[key] = item;
                          if ($.isFunction(item.callback)) {
                              k.callbacks[key] = item.callback;
                          }
                      });
                  } else {
                      // add label for input
                      if (item.type == 'html') {
                          $t.addClass('context-menu-html not-selectable');
                      } else if (item.type) {
                          $label = $('<label></label>').appendTo($t);
                          $('<span></span>').html(item._name || item.name).appendTo($label);
                          $t.addClass('context-menu-input');
                          opt.hasTypes = true;
                          $.each([opt, root], function(i,k){
                              k.commands[key] = item;
                              k.inputs[key] = item;
                          });
                      } else if (item.items) {
                          item.type = 'sub';
                      }
  
                      switch (item.type) {
                          case 'text':
                              $input = $('<input type="text" value="1" name="" value="">')
                                  .attr('name', 'context-menu-input-' + key)
                                  .val(item.value || "")
                                  .appendTo($label);
                              break;
  
                          case 'textarea':
                              $input = $('<textarea name=""></textarea>')
                                  .attr('name', 'context-menu-input-' + key)
                                  .val(item.value || "")
                                  .appendTo($label);
  
                              if (item.height) {
                                  $input.height(item.height);
                              }
                              break;
  
                          case 'checkbox':
                              $input = $('<input type="checkbox" value="1" name="" value="">')
                                  .attr('name', 'context-menu-input-' + key)
                                  .val(item.value || "")
                                  .prop("checked", !!item.selected)
                                  .prependTo($label);
                              break;
  
                          case 'radio':
                              $input = $('<input type="radio" value="1" name="" value="">')
                                  .attr('name', 'context-menu-input-' + item.radio)
                                  .val(item.value || "")
                                  .prop("checked", !!item.selected)
                                  .prependTo($label);
                              break;
  
                          case 'select':
                              $input = $('<select name="">')
                                  .attr('name', 'context-menu-input-' + key)
                                  .appendTo($label);
                              if (item.options) {
                                  $.each(item.options, function(value, text) {
                                      $('<option></option>').val(value).text(text).appendTo($input);
                                  });
                                  $input.val(item.selected);
                              }
                              break;
  
                          case 'sub':
                              // FIXME: shouldn't this .html() be a .text()?
                              $('<span></span>').html(item._name || item.name).appendTo($t);
                              item.appendTo = item.$node;
                              op.create(item, root);
                              $t.data('contextMenu', item).addClass('context-menu-submenu');
                              item.callback = null;
                              break;
  
                          case 'html':
                              $(item.html).appendTo($t);
                              break;
  
                          default:
                              $.each([opt, root], function(i,k){
                                  k.commands[key] = item;
                                  if ($.isFunction(item.callback)) {
                                      k.callbacks[key] = item.callback;
                                  }
                              });
                              // FIXME: shouldn't this .html() be a .text()?
                              $('<span></span>').html(item._name || item.name || "").appendTo($t);
                              break;
                      }
  
                      // disable key listener in <input>
                      if (item.type && item.type != 'sub' && item.type != 'html') {
                          $input
                              .on('focus', handle.focusInput)
                              .on('blur', handle.blurInput);
  
                          if (item.events) {
                              $input.on(item.events, opt);
                          }
                      }
  
                      // add icons
                      if (item.icon) {
                          $t.addClass("icon icon-" + item.icon);
                      }
                  }
  
                  // cache contained elements
                  item.$input = $input;
                  item.$label = $label;
  
                  // attach item to menu
                  $t.appendTo(opt.$menu);
  
                  // Disable text selection
                  if (!opt.hasTypes && $.support.eventSelectstart) {
                      // browsers support user-select: none,
                      // IE has a special event for text-selection
                      // browsers supporting neither will not be preventing text-selection
                      $t.on('selectstart.disableTextSelect', handle.abortevent);
                  }
              });
              // attach contextMenu to <body> (to bypass any possible overflow:hidden issues on parents of the trigger element)
              if (!opt.$node) {
                  opt.$menu.css('display', 'none').addClass('context-menu-root');
              }
              opt.$menu.appendTo(opt.appendTo || document.body);
          },
          resize: function($menu, nested) {
              // determine widths of submenus, as CSS won't grow them automatically
              // position:absolute within position:absolute; min-width:100; max-width:200; results in width: 100;
              // kinda sucks hard...
  
              // determine width of absolutely positioned element
              $menu.css({position: 'absolute', display: 'block'});
              // don't apply yet, because that would break nested elements' widths
              // add a pixel to circumvent word-break issue in IE9 - #80
              $menu.data('width', Math.ceil($menu.width()) + 1);
              // reset styles so they allow nested elements to grow/shrink naturally
              $menu.css({
                  position: 'static',
                  minWidth: '0px',
                  maxWidth: '100000px'
              });
              // identify width of nested menus
              $menu.find('> li > ul').each(function() {
                  op.resize($(this), true);
              });
              // reset and apply changes in the end because nested
              // elements' widths wouldn't be calculatable otherwise
              if (!nested) {
                  $menu.find('ul').andSelf().css({
                      position: '',
                      display: '',
                      minWidth: '',
                      maxWidth: ''
                  }).width(function() {
                      return $(this).data('width');
                  });
              }
          },
          update: function(opt, root) {
              var $trigger = this;
              if (root === undefined) {
                  root = opt;
                  op.resize(opt.$menu);
              }
              // re-check disabled for each item
              opt.$menu.children().each(function(){
                  var $item = $(this),
                      key = $item.data('contextMenuKey'),
                      item = opt.items[key],
                      disabled = ($.isFunction(item.disabled) && item.disabled.call($trigger, key, root)) || item.disabled === true;
  
                  // dis- / enable item
                  $item[disabled ? 'addClass' : 'removeClass']('disabled');
  
                  if (item.type) {
                      // dis- / enable input elements
                      $item.find('input, select, textarea').prop('disabled', disabled);
  
                      // update input states
                      switch (item.type) {
                          case 'text':
                          case 'textarea':
                              item.$input.val(item.value || "");
                              break;
  
                          case 'checkbox':
                          case 'radio':
                              item.$input.val(item.value || "").prop('checked', !!item.selected);
                              break;
  
                          case 'select':
                              item.$input.val(item.selected || "");
                              break;
                      }
                  }
  
                  if (item.$menu) {
                      // update sub-menu
                      op.update.call($trigger, item, root);
                  }
              });
          },
          layer: function(opt, zIndex) {
              // add transparent layer for click area
              // filter and background for Internet Explorer, Issue #23
              var $layer = opt.$layer = $('<div id="context-menu-layer" style="position:fixed; z-index:' + zIndex + '; top:0; left:0; opacity: 0; filter: alpha(opacity=0); background-color: #000;"></div>')
                  .css({height: $win.height(), width: $win.width(), display: 'block'})
                  .data('contextMenuRoot', opt)
                  .insertBefore(this)
                  .on('contextmenu', handle.abortevent)
                  .on('mousedown', handle.layerClick);
  
              // IE6 doesn't know position:fixed;
              if (!$.support.fixedPosition) {
                  $layer.css({
                      'position' : 'absolute',
                      'height' : $(document).height()
                  });
              }
  
              return $layer;
          }
      };
  
  // split accesskey according to http://www.whatwg.org/specs/web-apps/current-work/multipage/editing.html#assigned-access-key
  function splitAccesskey(val) {
      var t = val.split(/\s+/),
          keys = [];
  
      for (var i=0, k; k = t[i]; i++) {
          k = k[0].toUpperCase(); // first character only
          // theoretically non-accessible characters should be ignored, but different systems, different keyboard layouts, ... screw it.
          // a map to look up already used access keys would be nice
          keys.push(k);
      }
  
      return keys;
  }
  
  // handle contextMenu triggers
  $.fn.contextMenu = function(operation) {
      if (operation === undefined) {
          this.first().trigger('contextmenu');
      } else if (operation.x && operation.y) {
          this.first().trigger($.Event("contextmenu", {pageX: operation.x, pageY: operation.y}));
      } else if (operation === "hide") {
          var $menu = this.data('contextMenu').$menu;
          $menu && $menu.trigger('contextmenu:hide');
      } else if (operation === "destroy") {
          $.contextMenu("destroy", {context: this});
      } else if ($.isPlainObject(operation)) {
          operation.context = this;
          $.contextMenu("create", operation);
      } else if (operation) {
          this.removeClass('context-menu-disabled');
      } else if (!operation) {
          this.addClass('context-menu-disabled');
      }
  
      return this;
  };
  
  // manage contextMenu instances
  $.contextMenu = function(operation, options) {
      if (typeof operation != 'string') {
          options = operation;
          operation = 'create';
      }
  
      if (typeof options == 'string') {
          options = {selector: options};
      } else if (options === undefined) {
          options = {};
      }
  
      // merge with default options
      var o = $.extend(true, {}, defaults, options || {});
      var $document = $(document);
      var $context = $document;
      var _hasContext = false;
  
      if (!o.context || !o.context.length) {
          o.context = document;
      } else {
          // you never know what they throw at you...
          $context = $(o.context).first();
          o.context = $context.get(0);
          _hasContext = o.context !== document;
      }
  
      switch (operation) {
          case 'create':
              // no selector no joy
              if (!o.selector) {
                  throw new Error('No selector specified');
              }
              // make sure internal classes are not bound to
              if (o.selector.match(/.context-menu-(list|item|input)($|\s)/)) {
                  throw new Error('Cannot bind to selector "' + o.selector + '" as it contains a reserved className');
              }
              if (!o.build && (!o.items || $.isEmptyObject(o.items))) {
                  throw new Error('No Items specified');
              }
              counter ++;
              o.ns = '.contextMenu' + counter;
              if (!_hasContext) {
                  namespaces[o.selector] = o.ns;
              }
              menus[o.ns] = o;
  
              // default to right click
              if (!o.trigger) {
                  o.trigger = 'right';
              }
  
              if (!initialized) {
                  // make sure item click is registered first
                  $document
                      .on({
                          'contextmenu:hide.contextMenu': handle.hideMenu,
                          'prevcommand.contextMenu': handle.prevItem,
                          'nextcommand.contextMenu': handle.nextItem,
                          'contextmenu.contextMenu': handle.abortevent,
                          'mouseenter.contextMenu': handle.menuMouseenter,
                          'mouseleave.contextMenu': handle.menuMouseleave
                      }, '.context-menu-list')
                      .on('mouseup.contextMenu', '.context-menu-input', handle.inputClick)
                      .on({
                          'mouseup.contextMenu': handle.itemClick,
                          'contextmenu:focus.contextMenu': handle.focusItem,
                          'contextmenu:blur.contextMenu': handle.blurItem,
                          'contextmenu.contextMenu': handle.abortevent,
                          'mouseenter.contextMenu': handle.itemMouseenter,
                          'mouseleave.contextMenu': handle.itemMouseleave
                      }, '.context-menu-item');
  
                  initialized = true;
              }
  
              // engage native contextmenu event
              $context
                  .on('contextmenu' + o.ns, o.selector, o, handle.contextmenu);
  
              if (_hasContext) {
                  // add remove hook, just in case
                  $context.on('remove' + o.ns, function() {
                      $(this).contextMenu("destroy");
                  });
              }
  
              switch (o.trigger) {
                  case 'hover':
                          $context
                              .on('mouseenter' + o.ns, o.selector, o, handle.mouseenter)
                              .on('mouseleave' + o.ns, o.selector, o, handle.mouseleave);
                      break;
  
                  case 'left':
                          $context.on('click' + o.ns, o.selector, o, handle.click);
                      break;
                  /*
                  default:
                      // http://www.quirksmode.org/dom/events/contextmenu.html
                      $document
                          .on('mousedown' + o.ns, o.selector, o, handle.mousedown)
                          .on('mouseup' + o.ns, o.selector, o, handle.mouseup);
                      break;
                  */
              }
  
              // create menu
              if (!o.build) {
                  op.create(o);
              }
              break;
  
          case 'destroy':
              var $visibleMenu;
              if (_hasContext) {
                  // get proper options
                  var context = o.context;
                  $.each(menus, function(ns, o) {
                      if (o.context !== context) {
                          return true;
                      }
  
                      $visibleMenu = $('.context-menu-list').filter(':visible');
                      if ($visibleMenu.length && $visibleMenu.data().contextMenuRoot.$trigger.is($(o.context).find(o.selector))) {
                          $visibleMenu.trigger('contextmenu:hide', {force: true});
                      }
  
                      try {
                          if (menus[o.ns].$menu) {
                              menus[o.ns].$menu.remove();
                          }
  
                          delete menus[o.ns];
                      } catch(e) {
                          menus[o.ns] = null;
                      }
  
                      $(o.context).off(o.ns);
  
                      return true;
                  });
              } else if (!o.selector) {
                  $document.off('.contextMenu .contextMenuAutoHide');
                  $.each(menus, function(ns, o) {
                      $(o.context).off(o.ns);
                  });
  
                  namespaces = {};
                  menus = {};
                  counter = 0;
                  initialized = false;
  
                  $('#context-menu-layer, .context-menu-list').remove();
              } else if (namespaces[o.selector]) {
                  $visibleMenu = $('.context-menu-list').filter(':visible');
                  if ($visibleMenu.length && $visibleMenu.data().contextMenuRoot.$trigger.is(o.selector)) {
                      $visibleMenu.trigger('contextmenu:hide', {force: true});
                  }
  
                  try {
                      if (menus[namespaces[o.selector]].$menu) {
                          menus[namespaces[o.selector]].$menu.remove();
                      }
  
                      delete menus[namespaces[o.selector]];
                  } catch(e) {
                      menus[namespaces[o.selector]] = null;
                  }
  
                  $document.off(namespaces[o.selector]);
              }
              break;
  
          case 'html5':
              // if <command> or <menuitem> are not handled by the browser,
              // or options was a bool true,
              // initialize $.contextMenu for them
              if ((!$.support.htmlCommand && !$.support.htmlMenuitem) || (typeof options == "boolean" && options)) {
                  $('menu[type="context"]').each(function() {
                      if (this.id) {
                          $.contextMenu({
                              selector: '[contextmenu=' + this.id +']',
                              items: $.contextMenu.fromMenu(this)
                          });
                      }
                  }).css('display', 'none');
              }
              break;
  
          default:
              throw new Error('Unknown operation "' + operation + '"');
      }
  
      return this;
  };
  
  // import values into <input> commands
  $.contextMenu.setInputValues = function(opt, data) {
      if (data === undefined) {
          data = {};
      }
  
      $.each(opt.inputs, function(key, item) {
          switch (item.type) {
              case 'text':
              case 'textarea':
                  item.value = data[key] || "";
                  break;
  
              case 'checkbox':
                  item.selected = data[key] ? true : false;
                  break;
  
              case 'radio':
                  item.selected = (data[item.radio] || "") == item.value ? true : false;
                  break;
  
              case 'select':
                  item.selected = data[key] || "";
                  break;
          }
      });
  };
  
  // export values from <input> commands
  $.contextMenu.getInputValues = function(opt, data) {
      if (data === undefined) {
          data = {};
      }
  
      $.each(opt.inputs, function(key, item) {
          switch (item.type) {
              case 'text':
              case 'textarea':
              case 'select':
                  data[key] = item.$input.val();
                  break;
  
              case 'checkbox':
                  data[key] = item.$input.prop('checked');
                  break;
  
              case 'radio':
                  if (item.$input.prop('checked')) {
                      data[item.radio] = item.value;
                  }
                  break;
          }
      });
  
      return data;
  };
  
  // find <label for="xyz">
  function inputLabel(node) {
      return (node.id && $('label[for="'+ node.id +'"]').val()) || node.name;
  }
  
  // convert <menu> to items object
  function menuChildren(items, $children, counter) {
      if (!counter) {
          counter = 0;
      }
  
      $children.each(function() {
          var $node = $(this),
              node = this,
              nodeName = this.nodeName.toLowerCase(),
              label,
              item;
  
          // extract <label><input>
          if (nodeName == 'label' && $node.find('input, textarea, select').length) {
              label = $node.text();
              $node = $node.children().first();
              node = $node.get(0);
              nodeName = node.nodeName.toLowerCase();
          }
  
          /*
           * <menu> accepts flow-content as children. that means <embed>, <canvas> and such are valid menu items.
           * Not being the sadistic kind, $.contextMenu only accepts:
           * <command>, <menuitem>, <hr>, <span>, <p> <input [text, radio, checkbox]>, <textarea>, <select> and of course <menu>.
           * Everything else will be imported as an html node, which is not interfaced with contextMenu.
           */
  
          // http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#concept-command
          switch (nodeName) {
              // http://www.whatwg.org/specs/web-apps/current-work/multipage/interactive-elements.html#the-menu-element
              case 'menu':
                  item = {name: $node.attr('label'), items: {}};
                  counter = menuChildren(item.items, $node.children(), counter);
                  break;
  
              // http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#using-the-a-element-to-define-a-command
              case 'a':
              // http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#using-the-button-element-to-define-a-command
              case 'button':
                  item = {
                      name: $node.text(),
                      disabled: !!$node.attr('disabled'),
                      callback: (function(){ return function(){ $node.click(); }; })()
                  };
                  break;
  
              // http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#using-the-command-element-to-define-a-command
  
              case 'menuitem':
              case 'command':
                  switch ($node.attr('type')) {
                      case undefined:
                      case 'command':
                      case 'menuitem':
                          item = {
                              name: $node.attr('label'),
                              disabled: !!$node.attr('disabled'),
                              callback: (function(){ return function(){ $node.click(); }; })()
                          };
                          break;
  
                      case 'checkbox':
                          item = {
                              type: 'checkbox',
                              disabled: !!$node.attr('disabled'),
                              name: $node.attr('label'),
                              selected: !!$node.attr('checked')
                          };
                          break;
  
                      case 'radio':
                          item = {
                              type: 'radio',
                              disabled: !!$node.attr('disabled'),
                              name: $node.attr('label'),
                              radio: $node.attr('radiogroup'),
                              value: $node.attr('id'),
                              selected: !!$node.attr('checked')
                          };
                          break;
  
                      default:
                          item = undefined;
                  }
                  break;
  
              case 'hr':
                  item = '-------';
                  break;
  
              case 'input':
                  switch ($node.attr('type')) {
                      case 'text':
                          item = {
                              type: 'text',
                              name: label || inputLabel(node),
                              disabled: !!$node.attr('disabled'),
                              value: $node.val()
                          };
                          break;
  
                      case 'checkbox':
                          item = {
                              type: 'checkbox',
                              name: label || inputLabel(node),
                              disabled: !!$node.attr('disabled'),
                              selected: !!$node.attr('checked')
                          };
                          break;
  
                      case 'radio':
                          item = {
                              type: 'radio',
                              name: label || inputLabel(node),
                              disabled: !!$node.attr('disabled'),
                              radio: !!$node.attr('name'),
                              value: $node.val(),
                              selected: !!$node.attr('checked')
                          };
                          break;
  
                      default:
                          item = undefined;
                          break;
                  }
                  break;
  
              case 'select':
                  item = {
                      type: 'select',
                      name: label || inputLabel(node),
                      disabled: !!$node.attr('disabled'),
                      selected: $node.val(),
                      options: {}
                  };
                  $node.children().each(function(){
                      item.options[this.value] = $(this).text();
                  });
                  break;
  
              case 'textarea':
                  item = {
                      type: 'textarea',
                      name: label || inputLabel(node),
                      disabled: !!$node.attr('disabled'),
                      value: $node.val()
                  };
                  break;
  
              case 'label':
                  break;
  
              default:
                  item = {type: 'html', html: $node.clone(true)};
                  break;
          }
  
          if (item) {
              counter++;
              items['key' + counter] = item;
          }
      });
  
      return counter;
  }
  
  // convert html5 menu
  $.contextMenu.fromMenu = function(element) {
      var $this = $(element),
          items = {};
  
      menuChildren(items, $this.children());
  
      return items;
  };
  
  // make defaults accessible
  $.contextMenu.defaults = defaults;
  $.contextMenu.types = types;
  // export internal functions - undocumented, for hacking only!
  $.contextMenu.handle = handle;
  $.contextMenu.op = op;
  $.contextMenu.menus = menus;
  
  })(jQuery);
  