reservation plugin - unbound request (unclean
[unfold.git] / portal / static / unbound_reservation_static / js / menu / jquery.contextMenu.js
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                 item.selected = data[key] || "";
1462                 break;
1463         }
1464     });
1465 };
1466
1467 // export values from <input> commands
1468 $.contextMenu.getInputValues = function(opt, data) {
1469     if (data === undefined) {
1470         data = {};
1471     }
1472     
1473     $.each(opt.inputs, function(key, item) {
1474         switch (item.type) {
1475             case 'text':
1476             case 'textarea':
1477             case 'select':
1478                 data[key] = item.$input.val();
1479                 break;
1480
1481             case 'checkbox':
1482                 data[key] = item.$input.prop('checked');
1483                 break;
1484                 
1485             case 'radio':
1486                 if (item.$input.prop('checked')) {
1487                     data[item.radio] = item.value;
1488                 }
1489                 break;
1490         }
1491     });
1492     
1493     return data;
1494 };
1495
1496 // find <label for="xyz">
1497 function inputLabel(node) {
1498     return (node.id && $('label[for="'+ node.id +'"]').val()) || node.name;
1499 }
1500
1501 // convert <menu> to items object
1502 function menuChildren(items, $children, counter) {
1503     if (!counter) {
1504         counter = 0;
1505     }
1506     
1507     $children.each(function() {
1508         var $node = $(this),
1509             node = this,
1510             nodeName = this.nodeName.toLowerCase(),
1511             label,
1512             item;
1513         
1514         // extract <label><input>
1515         if (nodeName == 'label' && $node.find('input, textarea, select').length) {
1516             label = $node.text();
1517             $node = $node.children().first();
1518             node = $node.get(0);
1519             nodeName = node.nodeName.toLowerCase();
1520         }
1521         
1522         /*
1523          * <menu> accepts flow-content as children. that means <embed>, <canvas> and such are valid menu items.
1524          * Not being the sadistic kind, $.contextMenu only accepts:
1525          * <command>, <menuitem>, <hr>, <span>, <p> <input [text, radio, checkbox]>, <textarea>, <select> and of course <menu>.
1526          * Everything else will be imported as an html node, which is not interfaced with contextMenu.
1527          */
1528         
1529         // http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#concept-command
1530         switch (nodeName) {
1531             // http://www.whatwg.org/specs/web-apps/current-work/multipage/interactive-elements.html#the-menu-element
1532             case 'menu':
1533                 item = {name: $node.attr('label'), items: {}};
1534                 counter = menuChildren(item.items, $node.children(), counter);
1535                 break;
1536             
1537             // http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#using-the-a-element-to-define-a-command
1538             case 'a':
1539             // http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#using-the-button-element-to-define-a-command
1540             case 'button':
1541                 item = {
1542                     name: $node.text(),
1543                     disabled: !!$node.attr('disabled'),
1544                     callback: (function(){ return function(){ $node.click(); }; })()
1545                 };
1546                 break;
1547             
1548             // http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#using-the-command-element-to-define-a-command
1549
1550             case 'menuitem':
1551             case 'command':
1552                 switch ($node.attr('type')) {
1553                     case undefined:
1554                     case 'command':
1555                     case 'menuitem':
1556                         item = {
1557                             name: $node.attr('label'),
1558                             disabled: !!$node.attr('disabled'),
1559                             callback: (function(){ return function(){ $node.click(); }; })()
1560                         };
1561                         break;
1562                         
1563                     case 'checkbox':
1564                         item = {
1565                             type: 'checkbox',
1566                             disabled: !!$node.attr('disabled'),
1567                             name: $node.attr('label'),
1568                             selected: !!$node.attr('checked')
1569                         };
1570                         break;
1571                         
1572                     case 'radio':
1573                         item = {
1574                             type: 'radio',
1575                             disabled: !!$node.attr('disabled'),
1576                             name: $node.attr('label'),
1577                             radio: $node.attr('radiogroup'),
1578                             value: $node.attr('id'),
1579                             selected: !!$node.attr('checked')
1580                         };
1581                         break;
1582                         
1583                     default:
1584                         item = undefined;
1585                 }
1586                 break;
1587  
1588             case 'hr':
1589                 item = '-------';
1590                 break;
1591                 
1592             case 'input':
1593                 switch ($node.attr('type')) {
1594                     case 'text':
1595                         item = {
1596                             type: 'text',
1597                             name: label || inputLabel(node),
1598                             disabled: !!$node.attr('disabled'),
1599                             value: $node.val()
1600                         };
1601                         break;
1602                         
1603                     case 'checkbox':
1604                         item = {
1605                             type: 'checkbox',
1606                             name: label || inputLabel(node),
1607                             disabled: !!$node.attr('disabled'),
1608                             selected: !!$node.attr('checked')
1609                         };
1610                         break;
1611                         
1612                     case 'radio':
1613                         item = {
1614                             type: 'radio',
1615                             name: label || inputLabel(node),
1616                             disabled: !!$node.attr('disabled'),
1617                             radio: !!$node.attr('name'),
1618                             value: $node.val(),
1619                             selected: !!$node.attr('checked')
1620                         };
1621                         break;
1622                     
1623                     default:
1624                         item = undefined;
1625                         break;
1626                 }
1627                 break;
1628                 
1629             case 'select':
1630                 item = {
1631                     type: 'select',
1632                     name: label || inputLabel(node),
1633                     disabled: !!$node.attr('disabled'),
1634                     selected: $node.val(),
1635                     options: {}
1636                 };
1637                 $node.children().each(function(){
1638                     item.options[this.value] = $(this).text();
1639                 });
1640                 break;
1641                 
1642             case 'textarea':
1643                 item = {
1644                     type: 'textarea',
1645                     name: label || inputLabel(node),
1646                     disabled: !!$node.attr('disabled'),
1647                     value: $node.val()
1648                 };
1649                 break;
1650             
1651             case 'label':
1652                 break;
1653             
1654             default:
1655                 item = {type: 'html', html: $node.clone(true)};
1656                 break;
1657         }
1658         
1659         if (item) {
1660             counter++;
1661             items['key' + counter] = item;
1662         }
1663     });
1664     
1665     return counter;
1666 }
1667
1668 // convert html5 menu
1669 $.contextMenu.fromMenu = function(element) {
1670     var $this = $(element),
1671         items = {};
1672         
1673     menuChildren(items, $this.children());
1674     
1675     return items;
1676 };
1677
1678 // make defaults accessible
1679 $.contextMenu.defaults = defaults;
1680 $.contextMenu.types = types;
1681 // export internal functions - undocumented, for hacking only!
1682 $.contextMenu.handle = handle;
1683 $.contextMenu.op = op;
1684 $.contextMenu.menus = menus;
1685
1686 })(jQuery);