Allow empty value in metadata
[redakcja.git] / redakcja / static / contextmenu / jquery.contextMenu.js
1 /*!\r
2  * jQuery contextMenu - Plugin for simple contextMenu handling\r
3  *\r
4  * Version: 1.5.13\r
5  *\r
6  * Authors: Rodney Rehm, Addy Osmani (patches for FF)\r
7  * Web: http://medialize.github.com/jQuery-contextMenu/\r
8  *\r
9  * Licensed under\r
10  *   MIT License http://www.opensource.org/licenses/mit-license\r
11  *   GPL v3 http://opensource.org/licenses/GPL-3.0\r
12  *\r
13  */\r
14 \r
15 (function($, undefined){\r
16     \r
17     // TODO: -\r
18         // ARIA stuff: menuitem, menuitemcheckbox und menuitemradio\r
19         // create <menu> structure if $.support[htmlCommand || htmlMenuitem] and !opt.disableNative\r
20 \r
21 // determine html5 compatibility\r
22 $.support.htmlMenuitem = ('HTMLMenuItemElement' in window);\r
23 $.support.htmlCommand = ('HTMLCommandElement' in window);\r
24 \r
25 var // currently active contextMenu trigger\r
26     $currentTrigger = null,\r
27     // is contextMenu initialized with at least one menu?\r
28     initialized = false,\r
29     // window handle\r
30     $win = $(window),\r
31     // number of registered menus\r
32     counter = 0,\r
33     // mapping selector to namespace\r
34     namespaces = {},\r
35     // mapping namespace to options\r
36     menus = {},\r
37     // custom command type handlers\r
38     types = {},\r
39     // default values\r
40     defaults = {\r
41         // selector of contextMenu trigger\r
42         selector: null,\r
43         // where to append the menu to\r
44         appendTo: null,\r
45         // method to trigger context menu ["right", "left", "hover"]\r
46         trigger: "right",\r
47         // hide menu when mouse leaves trigger / menu elements\r
48         autoHide: false,\r
49         // ms to wait before showing a hover-triggered context menu\r
50         delay: 200,\r
51         // determine position to show menu at\r
52         determinePosition: function($menu) {\r
53             // position to the lower middle of the trigger element\r
54             if ($.ui && $.ui.position) {\r
55                 // .position() is provided as a jQuery UI utility\r
56                 // (...and it won't work on hidden elements)\r
57                 $menu.css('display', 'block').position({\r
58                     my: "center top",\r
59                     at: "center bottom",\r
60                     of: this,\r
61                     offset: "0 5",\r
62                     collision: "fit"\r
63                 }).css('display', 'none');\r
64             } else {\r
65                 // determine contextMenu position\r
66                 var offset = this.offset();\r
67                 offset.top += this.outerHeight();\r
68                 offset.left += this.outerWidth() / 2 - $menu.outerWidth() / 2;\r
69                 $menu.css(offset);\r
70             }\r
71         },\r
72         // position menu\r
73         position: function(opt, x, y) {\r
74             var $this = this,\r
75                 offset;\r
76             // determine contextMenu position\r
77             if (!x && !y) {\r
78                 opt.determinePosition.call(this, opt.$menu);\r
79                 return;\r
80             } else if (x === "maintain" && y === "maintain") {\r
81                 // x and y must not be changed (after re-show on command click)\r
82                 offset = opt.$menu.position();\r
83             } else {\r
84                 // x and y are given (by mouse event)\r
85                 var triggerIsFixed = opt.$trigger.parents().andSelf()\r
86                     .filter(function() {\r
87                         return $(this).css('position') == "fixed";\r
88                     }).length;\r
89 \r
90                 if (triggerIsFixed) {\r
91                     y -= $win.scrollTop();\r
92                     x -= $win.scrollLeft();\r
93                 }\r
94                 offset = {top: y, left: x};\r
95             }\r
96             \r
97             // correct offset if viewport demands it\r
98             var bottom = $win.scrollTop() + $win.height(),\r
99                 right = $win.scrollLeft() + $win.width(),\r
100                 height = opt.$menu.height(),\r
101                 width = opt.$menu.width();\r
102             \r
103             if (offset.top + height > bottom) {\r
104                 offset.top -= height;\r
105             }\r
106             \r
107             if (offset.left + width > right) {\r
108                 offset.left -= width;\r
109             }\r
110             \r
111             opt.$menu.css(offset);\r
112         },\r
113         // position the sub-menu\r
114         positionSubmenu: function($menu) {\r
115             if ($.ui && $.ui.position) {\r
116                 // .position() is provided as a jQuery UI utility\r
117                 // (...and it won't work on hidden elements)\r
118                 $menu.css('display', 'block').position({\r
119                     my: "left top",\r
120                     at: "right top",\r
121                     of: this,\r
122                     collision: "fit"\r
123                 }).css('display', '');\r
124             } else {\r
125                 // determine contextMenu position\r
126                 var offset = this.offset();\r
127                 offset.top += 0;\r
128                 offset.left += this.outerWidth();\r
129                 $menu.css(offset);\r
130             }\r
131         },\r
132         // offset to add to zIndex\r
133         zIndex: 1,\r
134         // show hide animation settings\r
135         animation: {\r
136             duration: 50,\r
137             show: 'slideDown',\r
138             hide: 'slideUp'\r
139         },\r
140         // events\r
141         events: {\r
142             show: $.noop,\r
143             hide: $.noop\r
144         },\r
145         // default callback\r
146         callback: null,\r
147         // list of contextMenu items\r
148         items: {}\r
149     },\r
150     // mouse position for hover activation\r
151     hoveract = {\r
152         timer: null,\r
153         pageX: null,\r
154         pageY: null\r
155     },\r
156     // determine zIndex\r
157     zindex = function($t) {\r
158         var zin = 0,\r
159             $tt = $t;\r
160 \r
161         while (true) {\r
162             zin = Math.max(zin, parseInt($tt.css('z-index'), 10) || 0);\r
163             $tt = $tt.parent();\r
164             if (!$tt || !$tt.length || $tt.prop('nodeName').toLowerCase() == 'body') {\r
165                 break;\r
166             }\r
167         }\r
168         \r
169         return zin;\r
170     },\r
171     // event handlers\r
172     handle = {\r
173         // abort anything\r
174         abortevent: function(e){\r
175             e.preventDefault();\r
176             e.stopImmediatePropagation();\r
177         },\r
178         \r
179         // contextmenu show dispatcher\r
180         contextmenu: function(e) {\r
181             var $this = $(this);\r
182             \r
183             // disable actual context-menu\r
184             e.preventDefault();\r
185             e.stopImmediatePropagation();\r
186             \r
187             // abort native-triggered events unless we're triggering on right click\r
188             if (e.data.trigger != 'right' && e.originalEvent) {\r
189                 return;\r
190             }\r
191             \r
192             if (!$this.hasClass('context-menu-disabled')) {\r
193                 // theoretically need to fire a show event at <menu>\r
194                 // http://www.whatwg.org/specs/web-apps/current-work/multipage/interactive-elements.html#context-menus\r
195                 // var evt = jQuery.Event("show", { data: data, pageX: e.pageX, pageY: e.pageY, relatedTarget: this });\r
196                 // e.data.$menu.trigger(evt);\r
197                 \r
198                 $currentTrigger = $this;\r
199                 if (e.data.build) {\r
200                     var built = e.data.build($currentTrigger, e);\r
201                     // abort if build() returned false\r
202                     if (built === false) {\r
203                         return;\r
204                     }\r
205                     \r
206                     // dynamically build menu on invocation\r
207                     e.data = $.extend(true, defaults, e.data, built || {});\r
208 \r
209                     // abort if there are no items to display\r
210                     if (!e.data.items || $.isEmptyObject(e.data.items)) {\r
211                         // Note: jQuery captures and ignores errors from event handlers\r
212                         if (window.console) {\r
213                             (console.error || console.log)("No items specified to show in contextMenu");\r
214                         }\r
215                         \r
216                         throw new Error('No Items sepcified');\r
217                     }\r
218                     \r
219                     // backreference for custom command type creation\r
220                     e.data.$trigger = $currentTrigger;\r
221                     \r
222                     op.create(e.data);\r
223                 }\r
224                 // show menu\r
225                 op.show.call($this, e.data, e.pageX, e.pageY);\r
226             }\r
227         },\r
228         // contextMenu left-click trigger\r
229         click: function(e) {\r
230             e.preventDefault();\r
231             e.stopImmediatePropagation();\r
232             $(this).trigger(jQuery.Event("contextmenu", { data: e.data, pageX: e.pageX, pageY: e.pageY }));\r
233         },\r
234         // contextMenu right-click trigger\r
235         mousedown: function(e) {\r
236             // register mouse down\r
237             var $this = $(this);\r
238             \r
239             // hide any previous menus\r
240             if ($currentTrigger && $currentTrigger.length && !$currentTrigger.is($this)) {\r
241                 $currentTrigger.data('contextMenu').$menu.trigger('contextmenu:hide');\r
242             }\r
243             \r
244             // activate on right click\r
245             if (e.button == 2) {\r
246                 $currentTrigger = $this.data('contextMenuActive', true);\r
247             }\r
248         },\r
249         // contextMenu right-click trigger\r
250         mouseup: function(e) {\r
251             // show menu\r
252             var $this = $(this);\r
253             if ($this.data('contextMenuActive') && $currentTrigger && $currentTrigger.length && $currentTrigger.is($this) && !$this.hasClass('context-menu-disabled')) {\r
254                 e.preventDefault();\r
255                 e.stopImmediatePropagation();\r
256                 $currentTrigger = $this;\r
257                 $this.trigger(jQuery.Event("contextmenu", { data: e.data, pageX: e.pageX, pageY: e.pageY }));\r
258             }\r
259             \r
260             $this.removeData('contextMenuActive');\r
261         },\r
262         // contextMenu hover trigger\r
263         mouseenter: function(e) {\r
264             var $this = $(this),\r
265                 $related = $(e.relatedTarget),\r
266                 $document = $(document);\r
267             \r
268             // abort if we're coming from a menu\r
269             if ($related.is('.context-menu-list') || $related.closest('.context-menu-list').length) {\r
270                 return;\r
271             }\r
272             \r
273             // abort if a menu is shown\r
274             if ($currentTrigger && $currentTrigger.length) {\r
275                 return;\r
276             }\r
277             \r
278             hoveract.pageX = e.pageX;\r
279             hoveract.pageY = e.pageY;\r
280             hoveract.data = e.data;\r
281             $document.on('mousemove.contextMenuShow', handle.mousemove);\r
282             hoveract.timer = setTimeout(function() {\r
283                 hoveract.timer = null;\r
284                 $document.off('mousemove.contextMenuShow');\r
285                 $currentTrigger = $this;\r
286                 $this.trigger(jQuery.Event("contextmenu", { data: hoveract.data, pageX: hoveract.pageX, pageY: hoveract.pageY }));\r
287             }, e.data.delay );\r
288         },\r
289         // contextMenu hover trigger\r
290         mousemove: function(e) {\r
291             hoveract.pageX = e.pageX;\r
292             hoveract.pageY = e.pageY;\r
293         },\r
294         // contextMenu hover trigger\r
295         mouseleave: function(e) {\r
296             // abort if we're leaving for a menu\r
297             var $related = $(e.relatedTarget);\r
298             if ($related.is('.context-menu-list') || $related.closest('.context-menu-list').length) {\r
299                 return;\r
300             }\r
301             \r
302             try {\r
303                 clearTimeout(hoveract.timer);\r
304             } catch(e) {}\r
305             \r
306             hoveract.timer = null;\r
307         },\r
308         \r
309         // click on layer to hide contextMenu\r
310         layerClick: function(e) {\r
311             var $this = $(this),\r
312                 root = $this.data('contextMenuRoot');\r
313                 \r
314             e.preventDefault();\r
315             e.stopImmediatePropagation();\r
316             \r
317             if ((root.trigger == 'left' && e.button == 0) || (root.trigger == 'right' && e.button == 2)) {\r
318                 var offset = root.$trigger.offset();\r
319                 \r
320                 // while this looks kinda awful, it's the best way to avoid\r
321                 // unnecessarily calculating any positions\r
322                 offset.top += $(window).scrollTop();\r
323                 if (offset.top <= e.pageY) {\r
324                     offset.left += $(window).scrollLeft();\r
325                     if (offset.left <= e.pageX) {\r
326                         offset.bottom = offset.top + root.$trigger.outerHeight();\r
327                         if (offset.bottom >= e.pageY) {\r
328                             offset.right = offset.left + root.$trigger.outerWidth();\r
329                             if (offset.right >= e.pageX) {\r
330                                 // reposition\r
331                                 root.position.call(root.$trigger, root, e.pageX, e.pageY);\r
332                                 return;\r
333                             }\r
334                         }\r
335                     }\r
336                 }\r
337             } \r
338             \r
339             // remove only after mouseup has completed\r
340             $this.on('mouseup', function(e) {\r
341                 e.preventDefault();\r
342                 e.stopImmediatePropagation();\r
343                 root.$menu.trigger('contextmenu:hide');\r
344             });\r
345         },\r
346         // key handled :hover\r
347         keyStop: function(e, opt) {\r
348             if (!opt.isInput) {\r
349                 e.preventDefault();\r
350             }\r
351             \r
352             e.stopPropagation();\r
353         },\r
354         key: function(e) {\r
355             var opt = $currentTrigger.data('contextMenu') || {},\r
356                 $children = opt.$menu.children(),\r
357                 $round;\r
358 \r
359             switch (e.keyCode) {\r
360                 case 9:\r
361                 case 38: // up\r
362                     handle.keyStop(e, opt);\r
363                     // if keyCode is [38 (up)] or [9 (tab) with shift]\r
364                     if (opt.isInput) {\r
365                         if (e.keyCode == 9 && e.shiftKey) {\r
366                             e.preventDefault();\r
367                             opt.$selected && opt.$selected.find('input, textarea, select').blur();\r
368                             opt.$menu.trigger('prevcommand');\r
369                             return;\r
370                         } else if (e.keyCode == 38 && opt.$selected.find('input, textarea, select').prop('type') == 'checkbox') {\r
371                             // checkboxes don't capture this key\r
372                             e.preventDefault();\r
373                             return;\r
374                         }\r
375                     } else if (e.keyCode != 9 || e.shiftKey) {\r
376                         opt.$menu.trigger('prevcommand');\r
377                         return;\r
378                     }\r
379                     \r
380                 case 9: // tab\r
381                 case 40: // down\r
382                     handle.keyStop(e, opt);\r
383                     if (opt.isInput) {\r
384                         if (e.keyCode == 9) {\r
385                             e.preventDefault();\r
386                             opt.$selected && opt.$selected.find('input, textarea, select').blur();\r
387                             opt.$menu.trigger('nextcommand');\r
388                             return;\r
389                         } else if (e.keyCode == 40 && opt.$selected.find('input, textarea, select').prop('type') == 'checkbox') {\r
390                             // checkboxes don't capture this key\r
391                             e.preventDefault();\r
392                             return;\r
393                         }\r
394                     } else {\r
395                         opt.$menu.trigger('nextcommand');\r
396                         return;\r
397                     }\r
398                     break;\r
399                 \r
400                 case 37: // left\r
401                     handle.keyStop(e, opt);\r
402                     if (opt.isInput || !opt.$selected || !opt.$selected.length) {\r
403                         break;\r
404                     }\r
405                 \r
406                     if (!opt.$selected.parent().hasClass('context-menu-root')) {\r
407                         var $parent = opt.$selected.parent().parent();\r
408                         opt.$selected.trigger('contextmenu:blur');\r
409                         opt.$selected = $parent;\r
410                         return;\r
411                     }\r
412                     break;\r
413                     \r
414                 case 39: // right\r
415                     handle.keyStop(e, opt);\r
416                     if (opt.isInput || !opt.$selected || !opt.$selected.length) {\r
417                         break;\r
418                     }\r
419                     \r
420                     var itemdata = opt.$selected.data('contextMenu') || {};\r
421                     if (itemdata.$menu && opt.$selected.hasClass('context-menu-submenu')) {\r
422                         opt.$selected = null;\r
423                         itemdata.$selected = null;\r
424                         itemdata.$menu.trigger('nextcommand');\r
425                         return;\r
426                     }\r
427                     break;\r
428                 \r
429                 case 35: // end\r
430                 case 36: // home\r
431                     if (opt.$selected && opt.$selected.find('input, textarea, select').length) {\r
432                         return;\r
433                     } else {\r
434                         (opt.$selected && opt.$selected.parent() || opt.$menu)\r
435                             .children(':not(.disabled, .not-selectable)')[e.keyCode == 36 ? 'first' : 'last']()\r
436                             .trigger('contextmenu:focus');\r
437                         e.preventDefault();\r
438                         return;\r
439                     }\r
440                     break;\r
441                     \r
442                 case 13: // enter\r
443                     handle.keyStop(e, opt);\r
444                     if (opt.isInput) {\r
445                         if (opt.$selected && !opt.$selected.is('textarea, select')) {\r
446                             e.preventDefault();\r
447                             return;\r
448                         }\r
449                         break;\r
450                     }\r
451                     opt.$selected && opt.$selected.trigger('mouseup');\r
452                     return;\r
453                     \r
454                 case 32: // space\r
455                 case 33: // page up\r
456                 case 34: // page down\r
457                     // prevent browser from scrolling down while menu is visible\r
458                     handle.keyStop(e, opt);\r
459                     return;\r
460                     \r
461                 case 27: // esc\r
462                     handle.keyStop(e, opt);\r
463                     opt.$menu.trigger('contextmenu:hide');\r
464                     return;\r
465                     \r
466                 default: // 0-9, a-z\r
467                     var k = (String.fromCharCode(e.keyCode)).toUpperCase();\r
468                     if (opt.accesskeys[k]) {\r
469                         // according to the specs accesskeys must be invoked immediately\r
470                         opt.accesskeys[k].$node.trigger(opt.accesskeys[k].$menu\r
471                             ? 'contextmenu:focus'\r
472                             : 'mouseup'\r
473                         );\r
474                         return;\r
475                     }\r
476                     break;\r
477             }\r
478             // pass event to selected item, \r
479             // stop propagation to avoid endless recursion\r
480             e.stopPropagation();\r
481             opt.$selected && opt.$selected.trigger(e);\r
482         },\r
483 \r
484         // select previous possible command in menu\r
485         prevItem: function(e) {\r
486             e.stopPropagation();\r
487             var opt = $(this).data('contextMenu') || {};\r
488 \r
489             // obtain currently selected menu\r
490             if (opt.$selected) {\r
491                 var $s = opt.$selected;\r
492                 opt = opt.$selected.parent().data('contextMenu') || {};\r
493                 opt.$selected = $s;\r
494             }\r
495             \r
496             var $children = opt.$menu.children(),\r
497                 $prev = !opt.$selected || !opt.$selected.prev().length ? $children.last() : opt.$selected.prev(),\r
498                 $round = $prev;\r
499             \r
500             // skip disabled\r
501             while ($prev.hasClass('disabled') || $prev.hasClass('not-selectable')) {\r
502                 if ($prev.prev().length) {\r
503                     $prev = $prev.prev();\r
504                 } else {\r
505                     $prev = $children.last();\r
506                 }\r
507                 if ($prev.is($round)) {\r
508                     // break endless loop\r
509                     return;\r
510                 }\r
511             }\r
512             \r
513             // leave current\r
514             if (opt.$selected) {\r
515                 handle.itemMouseleave.call(opt.$selected.get(0), e);\r
516             }\r
517             \r
518             // activate next\r
519             handle.itemMouseenter.call($prev.get(0), e);\r
520             \r
521             // focus input\r
522             var $input = $prev.find('input, textarea, select');\r
523             if ($input.length) {\r
524                 $input.focus();\r
525             }\r
526         },\r
527         // select next possible command in menu\r
528         nextItem: function(e) {\r
529             e.stopPropagation();\r
530             var opt = $(this).data('contextMenu') || {};\r
531 \r
532             // obtain currently selected menu\r
533             if (opt.$selected) {\r
534                 var $s = opt.$selected;\r
535                 opt = opt.$selected.parent().data('contextMenu') || {};\r
536                 opt.$selected = $s;\r
537             }\r
538 \r
539             var $children = opt.$menu.children(),\r
540                 $next = !opt.$selected || !opt.$selected.next().length ? $children.first() : opt.$selected.next(),\r
541                 $round = $next;\r
542 \r
543             // skip disabled\r
544             while ($next.hasClass('disabled') || $next.hasClass('not-selectable')) {\r
545                 if ($next.next().length) {\r
546                     $next = $next.next();\r
547                 } else {\r
548                     $next = $children.first();\r
549                 }\r
550                 if ($next.is($round)) {\r
551                     // break endless loop\r
552                     return;\r
553                 }\r
554             }\r
555             \r
556             // leave current\r
557             if (opt.$selected) {\r
558                 handle.itemMouseleave.call(opt.$selected.get(0), e);\r
559             }\r
560             \r
561             // activate next\r
562             handle.itemMouseenter.call($next.get(0), e);\r
563             \r
564             // focus input\r
565             var $input = $next.find('input, textarea, select');\r
566             if ($input.length) {\r
567                 $input.focus();\r
568             }\r
569         },\r
570         \r
571         // flag that we're inside an input so the key handler can act accordingly\r
572         focusInput: function(e) {\r
573             var $this = $(this).closest('.context-menu-item'),\r
574                 data = $this.data(),\r
575                 opt = data.contextMenu,\r
576                 root = data.contextMenuRoot;\r
577 \r
578             root.$selected = opt.$selected = $this;\r
579             root.isInput = opt.isInput = true;\r
580         },\r
581         // flag that we're inside an input so the key handler can act accordingly\r
582         blurInput: function(e) {\r
583             var $this = $(this).closest('.context-menu-item'),\r
584                 data = $this.data(),\r
585                 opt = data.contextMenu,\r
586                 root = data.contextMenuRoot;\r
587 \r
588             root.isInput = opt.isInput = false;\r
589         },\r
590         \r
591         // :hover on menu\r
592         menuMouseenter: function(e) {\r
593             var root = $(this).data().contextMenuRoot;\r
594             root.hovering = true;\r
595         },\r
596         // :hover on menu\r
597         menuMouseleave: function(e) {\r
598             var root = $(this).data().contextMenuRoot;\r
599             if (root.$layer && root.$layer.is(e.relatedTarget)) {\r
600                 root.hovering = false;\r
601             }\r
602         },\r
603         \r
604         // :hover done manually so key handling is possible\r
605         itemMouseenter: function(e) {\r
606             var $this = $(this),\r
607                 data = $this.data(),\r
608                 opt = data.contextMenu,\r
609                 root = data.contextMenuRoot;\r
610             \r
611             root.hovering = true;\r
612 \r
613             // abort if we're re-entering\r
614             if (e && root.$layer && root.$layer.is(e.relatedTarget)) {\r
615                 e.preventDefault();\r
616                 e.stopImmediatePropagation();\r
617             }\r
618 \r
619             // make sure only one item is selected\r
620             (opt.$menu ? opt : root).$menu\r
621                 .children('.hover').trigger('contextmenu:blur');\r
622 \r
623             if ($this.hasClass('disabled') || $this.hasClass('not-selectable')) {\r
624                 opt.$selected = null;\r
625                 return;\r
626             }\r
627             \r
628             $this.trigger('contextmenu:focus');\r
629         },\r
630         // :hover done manually so key handling is possible\r
631         itemMouseleave: function(e) {\r
632             var $this = $(this),\r
633                 data = $this.data(),\r
634                 opt = data.contextMenu,\r
635                 root = data.contextMenuRoot;\r
636 \r
637             if (root !== opt && root.$layer && root.$layer.is(e.relatedTarget)) {\r
638                 root.$selected && root.$selected.trigger('contextmenu:blur');\r
639                 e.preventDefault();\r
640                 e.stopImmediatePropagation();\r
641                 root.$selected = opt.$selected = opt.$node;\r
642                 return;\r
643             }\r
644             \r
645             $this.trigger('contextmenu:blur');\r
646         },\r
647         // contextMenu item click\r
648         itemClick: function(e) {\r
649             var $this = $(this),\r
650                 data = $this.data(),\r
651                 opt = data.contextMenu,\r
652                 root = data.contextMenuRoot,\r
653                 key = data.contextMenuKey,\r
654                 callback;\r
655 \r
656             // abort if the key is unknown or disabled or is a menu\r
657             if (!opt.items[key] || $this.hasClass('disabled') || $this.hasClass('context-menu-submenu')) {\r
658                 return;\r
659             }\r
660 \r
661             e.preventDefault();\r
662             e.stopImmediatePropagation();\r
663 \r
664             if ($.isFunction(root.callbacks[key])) {\r
665                 // item-specific callback\r
666                 callback = root.callbacks[key];\r
667             } else if ($.isFunction(root.callback)) {\r
668                 // default callback\r
669                 callback = root.callback;                \r
670             } else {\r
671                 // no callback, no action\r
672                 return;\r
673             }\r
674 \r
675             // hide menu if callback doesn't stop that\r
676             if (callback.call(root.$trigger, key, root) !== false) {\r
677                 root.$menu.trigger('contextmenu:hide');\r
678             } else {\r
679                 op.update.call(root.$trigger, root);\r
680             }\r
681         },\r
682         // ignore click events on input elements\r
683         inputClick: function(e) {\r
684             e.stopImmediatePropagation();\r
685         },\r
686         \r
687         // hide <menu>\r
688         hideMenu: function(e) {\r
689             var root = $(this).data('contextMenuRoot');\r
690             op.hide.call(root.$trigger, root);\r
691         },\r
692         // focus <command>\r
693         focusItem: function(e) {\r
694             e.stopPropagation();\r
695             var $this = $(this),\r
696                 data = $this.data(),\r
697                 opt = data.contextMenu,\r
698                 root = data.contextMenuRoot;\r
699 \r
700             $this.addClass('hover')\r
701                 .siblings('.hover').trigger('contextmenu:blur');\r
702             \r
703             // remember selected\r
704             opt.$selected = root.$selected = $this;\r
705             \r
706             // position sub-menu - do after show so dumb $.ui.position can keep up\r
707             if (opt.$node) {\r
708                 root.positionSubmenu.call(opt.$node, opt.$menu);\r
709             }\r
710         },\r
711         // blur <command>\r
712         blurItem: function(e) {\r
713             e.stopPropagation();\r
714             var $this = $(this),\r
715                 data = $this.data(),\r
716                 opt = data.contextMenu,\r
717                 root = data.contextMenuRoot;\r
718             \r
719             $this.removeClass('hover');\r
720             opt.$selected = null;\r
721         }\r
722     },\r
723     // operations\r
724     op = {\r
725         show: function(opt, x, y) {\r
726             var $this = $(this),\r
727                 offset,\r
728                 css = {};\r
729 \r
730             // hide any open menus\r
731             $('#context-menu-layer').trigger('mousedown');\r
732 \r
733             // backreference for callbacks\r
734             opt.$trigger = $this;\r
735 \r
736             // show event\r
737             if (opt.events.show.call($this, opt) === false) {\r
738                 $currentTrigger = null;\r
739                 return;\r
740             }\r
741             \r
742             // create or update context menu\r
743             op.update.call($this, opt);\r
744             \r
745             // position menu\r
746             opt.position.call($this, opt, x, y);\r
747 \r
748             // make sure we're in front\r
749             if (opt.zIndex) {\r
750                 css.zIndex = zindex($this) + opt.zIndex;\r
751             }\r
752             \r
753             // add layer\r
754             op.layer.call(opt.$menu, opt, css.zIndex);\r
755             \r
756             // adjust sub-menu zIndexes\r
757             opt.$menu.find('ul').css('zIndex', css.zIndex + 1);\r
758             \r
759             // position and show context menu\r
760             opt.$menu.css( css )[opt.animation.show](opt.animation.duration);\r
761             // make options available\r
762             $this.data('contextMenu', opt);\r
763             // register key handler\r
764             $(document).off('keydown.contextMenu').on('keydown.contextMenu', handle.key);\r
765             // register autoHide handler\r
766             if (opt.autoHide) {\r
767                 // trigger element coordinates\r
768                 var pos = $this.position();\r
769                 pos.right = pos.left + $this.outerWidth();\r
770                 pos.bottom = pos.top + this.outerHeight();\r
771                 // mouse position handler\r
772                 $(document).on('mousemove.contextMenuAutoHide', function(e) {\r
773                     if (opt.$layer && !opt.hovering && (!(e.pageX >= pos.left && e.pageX <= pos.right) || !(e.pageY >= pos.top && e.pageY <= pos.bottom))) {\r
774                         // if mouse in menu...\r
775                         opt.$layer.trigger('mousedown');\r
776                     }\r
777                 });\r
778             }\r
779         },\r
780         hide: function(opt) {\r
781             var $this = $(this);\r
782             if (!opt) {\r
783                 opt = $this.data('contextMenu') || {};\r
784             }\r
785             \r
786             // hide event\r
787             if (opt.events && opt.events.hide.call($this, opt) === false) {\r
788                 return;\r
789             }\r
790             \r
791             if (opt.$layer) {\r
792                 // keep layer for a bit so the contextmenu event can be aborted properly by opera\r
793                 setTimeout((function($layer){ return function(){\r
794                         $layer.remove();\r
795                     };\r
796                 })(opt.$layer), 10);\r
797                 \r
798                 try {\r
799                     delete opt.$layer;\r
800                 } catch(e) {\r
801                     opt.$layer = null;\r
802                 }\r
803             }\r
804             \r
805             // remove handle\r
806             $currentTrigger = null;\r
807             // remove selected\r
808             opt.$menu.find('.hover').trigger('contextmenu:blur');\r
809             opt.$selected = null;\r
810             // unregister key and mouse handlers\r
811             //$(document).off('.contextMenuAutoHide keydown.contextMenu'); // http://bugs.jquery.com/ticket/10705\r
812             $(document).off('.contextMenuAutoHide').off('keydown.contextMenu');\r
813             // hide menu\r
814             opt.$menu && opt.$menu[opt.animation.hide](opt.animation.duration);\r
815             \r
816             // tear down dynamically built menu\r
817             if (opt.build) {\r
818                 opt.$menu.remove();\r
819                 $.each(opt, function(key, value) {\r
820                     switch (key) {\r
821                         case 'ns':\r
822                         case 'selector':\r
823                         case 'build':\r
824                         case 'trigger':\r
825                             return true;\r
826 \r
827                         default:\r
828                             opt[key] = undefined;\r
829                             try {\r
830                                 delete opt[key];\r
831                             } catch (e) {}\r
832                             return true;\r
833                    }\r
834                 });\r
835             }\r
836         },\r
837         create: function(opt, root) {\r
838             if (root === undefined) {\r
839                 root = opt;\r
840             }\r
841             // create contextMenu\r
842             opt.$menu = $('<ul class="context-menu-list ' + (opt.className || "") + '"></ul>').data({\r
843                 'contextMenu': opt,\r
844                 'contextMenuRoot': root\r
845             });\r
846             \r
847             $.each(['callbacks', 'commands', 'inputs'], function(i,k){\r
848                 opt[k] = {};\r
849                 if (!root[k]) {\r
850                     root[k] = {};\r
851                 }\r
852             });\r
853             \r
854             root.accesskeys || (root.accesskeys = {});\r
855             \r
856             // create contextMenu items\r
857             $.each(opt.items, function(key, item){\r
858                 var $t = $('<li class="context-menu-item ' + (item.className || "") +'"></li>'),\r
859                     $label = null,\r
860                     $input = null;\r
861                 \r
862                 item.$node = $t.data({\r
863                     'contextMenu': opt,\r
864                     'contextMenuRoot': root,\r
865                     'contextMenuKey': key\r
866                 });\r
867                 \r
868                 // register accesskey\r
869                 // NOTE: the accesskey attribute should be applicable to any element, but Safari5 and Chrome13 still can't do that\r
870                 if (item.accesskey) {\r
871                     var aks = splitAccesskey(item.accesskey);\r
872                     for (var i=0, ak; ak = aks[i]; i++) {\r
873                         if (!root.accesskeys[ak]) {\r
874                             root.accesskeys[ak] = item;\r
875                             item._name = item.name.replace(new RegExp('(' + ak + ')', 'i'), '<span class="context-menu-accesskey">$1</span>');\r
876                             break;\r
877                         }\r
878                     }\r
879                 }\r
880                 \r
881                 if (typeof item == "string") {\r
882                     $t.addClass('context-menu-separator not-selectable');\r
883                 } else if (item.type && types[item.type]) {\r
884                     // run custom type handler\r
885                     types[item.type].call($t, item, opt, root);\r
886                     // register commands\r
887                     $.each([opt, root], function(i,k){\r
888                         k.commands[key] = item;\r
889                         if ($.isFunction(item.callback)) {\r
890                             k.callbacks[key] = item.callback;\r
891                         }\r
892                     });\r
893                 } else {\r
894                     // add label for input\r
895                     if (item.type == 'html') {\r
896                         $t.addClass('context-menu-html not-selectable');\r
897                     } else if (item.type) {\r
898                         $label = $('<label></label>').appendTo($t);\r
899                         $('<span></span>').html(item._name || item.name).appendTo($label);\r
900                         $t.addClass('context-menu-input');\r
901                         opt.hasTypes = true;\r
902                         $.each([opt, root], function(i,k){\r
903                             k.commands[key] = item;\r
904                             k.inputs[key] = item;\r
905                         });\r
906                     } else if (item.items) {\r
907                         item.type = 'sub';\r
908                     }\r
909                 \r
910                     switch (item.type) {\r
911                         case 'text':\r
912                             $input = $('<input type="text" value="1" name="context-menu-input-'+ key +'" value="">')\r
913                                 .val(item.value || "").appendTo($label);\r
914                             break;\r
915                     \r
916                         case 'textarea':\r
917                             $input = $('<textarea name="context-menu-input-'+ key +'"></textarea>')\r
918                                 .val(item.value || "").appendTo($label);\r
919 \r
920                             if (item.height) {\r
921                                 $input.height(item.height);\r
922                             }\r
923                             break;\r
924 \r
925                         case 'checkbox':\r
926                             $input = $('<input type="checkbox" value="1" name="context-menu-input-'+ key +'" value="">')\r
927                                 .val(item.value || "").prop("checked", !!item.selected).prependTo($label);\r
928                             break;\r
929 \r
930                         case 'radio':\r
931                             $input = $('<input type="radio" value="1" name="context-menu-input-'+ item.radio +'" value="">')\r
932                                 .val(item.value || "").prop("checked", !!item.selected).prependTo($label);\r
933                             break;\r
934                     \r
935                         case 'select':\r
936                             $input = $('<select name="context-menu-input-'+ key +'">').appendTo($label);\r
937                             if (item.options) {\r
938                                 $.each(item.options, function(value, text) {\r
939                                     $('<option></option>').val(value).text(text).appendTo($input);\r
940                                 });\r
941                                 $input.val(item.selected);\r
942                             }\r
943                             break;\r
944                         \r
945                         case 'sub':\r
946                             $('<span></span>').html(item._name || item.name).appendTo($t);\r
947                             item.appendTo = item.$node;\r
948                             op.create(item, root);\r
949                             $t.data('contextMenu', item).addClass('context-menu-submenu');\r
950                             item.callback = null;\r
951                             break;\r
952                         \r
953                         case 'html':\r
954                             $(item.html).appendTo($t);\r
955                             break;\r
956                         \r
957                         default:\r
958                             $.each([opt, root], function(i,k){\r
959                                 k.commands[key] = item;\r
960                                 if ($.isFunction(item.callback)) {\r
961                                     k.callbacks[key] = item.callback;\r
962                                 }\r
963                             });\r
964                             \r
965                             $('<span></span>').html(item._name || item.name || "").appendTo($t);\r
966                             break;\r
967                     }\r
968                     \r
969                     // disable key listener in <input>\r
970                     if (item.type && item.type != 'sub' && item.type != 'html') {\r
971                         $input\r
972                             .on('focus', handle.focusInput)\r
973                             .on('blur', handle.blurInput);\r
974                         \r
975                         if (item.events) {\r
976                             $input.on(item.events);\r
977                         }\r
978                     }\r
979                 \r
980                     // add icons\r
981                     if (item.icon) {\r
982                         $t.addClass("icon icon-" + item.icon);\r
983                     }\r
984                 }\r
985                 \r
986                 // cache contained elements\r
987                 item.$input = $input;\r
988                 item.$label = $label;\r
989 \r
990                 // attach item to menu\r
991                 $t.appendTo(opt.$menu);\r
992                 \r
993                 // Disable text selection\r
994                 if (!opt.hasTypes) {\r
995                     if($.browser.msie) {\r
996                         $t.on('selectstart.disableTextSelect', handle.abortevent);\r
997                     } else if(!$.browser.mozilla) {\r
998                         $t.on('mousedown.disableTextSelect', handle.abortevent);\r
999                     }\r
1000                 }\r
1001             });\r
1002             // attach contextMenu to <body> (to bypass any possible overflow:hidden issues on parents of the trigger element)\r
1003             if (!opt.$node) {\r
1004                 opt.$menu.css('display', 'none').addClass('context-menu-root');\r
1005             }\r
1006             opt.$menu.appendTo(opt.appendTo || document.body);\r
1007         },\r
1008         update: function(opt, root) {\r
1009             var $this = this;\r
1010             if (root === undefined) {\r
1011                 root = opt;\r
1012                 // determine widths of submenus, as CSS won't grow them automatically\r
1013                 // position:absolute > position:absolute; min-width:100; max-width:200; results in width: 100;\r
1014                 // kinda sucks hard...\r
1015                 opt.$menu.find('ul').andSelf().css({position: 'static', display: 'block'}).each(function(){\r
1016                     var $this = $(this);\r
1017                     $this.width($this.css('position', 'absolute').width())\r
1018                         .css('position', 'static');\r
1019                 }).css({position: '', display: ''});\r
1020             }\r
1021             // re-check disabled for each item\r
1022             opt.$menu.children().each(function(){\r
1023                 var $item = $(this),\r
1024                     key = $item.data('contextMenuKey'),\r
1025                     item = opt.items[key],\r
1026                     disabled = ($.isFunction(item.disabled) && item.disabled.call($this, key, root)) || item.disabled === true;\r
1027 \r
1028                 // dis- / enable item\r
1029                 $item[disabled ? 'addClass' : 'removeClass']('disabled');\r
1030                 \r
1031                 if (item.type) {\r
1032                     // dis- / enable input elements\r
1033                     $item.find('input, select, textarea').prop('disabled', disabled);\r
1034                     \r
1035                     // update input states\r
1036                     switch (item.type) {\r
1037                         case 'text':\r
1038                         case 'textarea':\r
1039                             item.$input.val(item.value || "");\r
1040                             break;\r
1041                             \r
1042                         case 'checkbox':\r
1043                         case 'radio':\r
1044                             item.$input.val(item.value || "").prop('checked', !!item.selected);\r
1045                             break;\r
1046                             \r
1047                         case 'select':\r
1048                             item.$input.val(item.selected || "");\r
1049                             break;\r
1050                     }\r
1051                 }\r
1052                 \r
1053                 if (item.$menu) {\r
1054                     // update sub-menu\r
1055                     op.update.call($this, item, root);\r
1056                 }\r
1057             });\r
1058         },\r
1059         layer: function(opt, zIndex) {\r
1060             // add transparent layer for click area\r
1061             // filter and background for Internet Explorer, Issue #23\r
1062             return 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>')\r
1063                 .css({height: $win.height(), width: $win.width(), display: 'block'})\r
1064                 .data('contextMenuRoot', opt)\r
1065                 .insertBefore(this)\r
1066                 .on('contextmenu', handle.abortevent)\r
1067                 .on('mousedown', handle.layerClick);\r
1068         }\r
1069     };\r
1070 \r
1071 // split accesskey according to http://www.whatwg.org/specs/web-apps/current-work/multipage/editing.html#assigned-access-key\r
1072 function splitAccesskey(val) {\r
1073     var t = val.split(/\s+/),\r
1074         keys = [];\r
1075         \r
1076     for (var i=0, k; k = t[i]; i++) {\r
1077         k = k[0].toUpperCase(); // first character only\r
1078         // theoretically non-accessible characters should be ignored, but different systems, different keyboard layouts, ... screw it.\r
1079         // a map to look up already used access keys would be nice\r
1080         keys.push(k);\r
1081     }\r
1082     \r
1083     return keys;\r
1084 }\r
1085 \r
1086 // handle contextMenu triggers\r
1087 $.fn.contextMenu = function(operation) {\r
1088     if (operation === undefined) {\r
1089         this.first().trigger('contextmenu');\r
1090     } else if (operation.x && operation.y) {\r
1091         this.first().trigger(jQuery.Event("contextmenu", {pageX: operation.x, pageY: operation.y}));\r
1092     } else if (operation === "hide") {\r
1093         var $menu = this.data('contextMenu').$menu;\r
1094         $menu && $menu.trigger('contextmenu:hide');\r
1095     } else if (operation) {\r
1096         this.removeClass('context-menu-disabled');\r
1097     } else if (!operation) {\r
1098         this.addClass('context-menu-disabled');\r
1099     }\r
1100     \r
1101     return this;\r
1102 };\r
1103 \r
1104 // manage contextMenu instances\r
1105 $.contextMenu = function(operation, options) {\r
1106     if (typeof operation != 'string') {\r
1107         options = operation;\r
1108         operation = 'create';\r
1109     }\r
1110     \r
1111     if (typeof options == 'string') {\r
1112         options = {selector: options};\r
1113     } else if (options === undefined) {\r
1114         options = {};\r
1115     }\r
1116     \r
1117     // merge with default options\r
1118     var o = $.extend(true, {}, defaults, options || {}),\r
1119         $body = $body = $(document);\r
1120     \r
1121     switch (operation) {\r
1122         case 'create':\r
1123             // no selector no joy\r
1124             if (!o.selector) {\r
1125                 throw new Error('No selector specified');\r
1126             }\r
1127             // make sure internal classes are not bound to\r
1128             if (o.selector.match(/.context-menu-(list|item|input)($|\s)/)) {\r
1129                 throw new Error('Cannot bind to selector "' + o.selector + '" as it contains a reserved className');\r
1130             }\r
1131             if (!o.build && (!o.items || $.isEmptyObject(o.items))) {\r
1132                 throw new Error('No Items sepcified');\r
1133             }\r
1134             counter ++;\r
1135             o.ns = '.contextMenu' + counter;\r
1136             namespaces[o.selector] = o.ns;\r
1137             menus[o.ns] = o;\r
1138             \r
1139             if (!initialized) {\r
1140                 // make sure item click is registered first\r
1141                 $body\r
1142                     .on({\r
1143                         'contextmenu:hide.contextMenu': handle.hideMenu,\r
1144                         'prevcommand.contextMenu': handle.prevItem,\r
1145                         'nextcommand.contextMenu': handle.nextItem,\r
1146                         'contextmenu.contextMenu': handle.abortevent,\r
1147                         'mouseenter.contextMenu': handle.menuMouseenter,\r
1148                         'mouseleave.contextMenu': handle.menuMouseleave\r
1149                     }, '.context-menu-list')\r
1150                     .on('mouseup.contextMenu', '.context-menu-input', handle.inputClick)\r
1151                     .on({\r
1152                         'mouseup.contextMenu': handle.itemClick,\r
1153                         'contextmenu:focus.contextMenu': handle.focusItem,\r
1154                         'contextmenu:blur.contextMenu': handle.blurItem,\r
1155                         'contextmenu.contextMenu': handle.abortevent,\r
1156                         'mouseenter.contextMenu': handle.itemMouseenter,\r
1157                         'mouseleave.contextMenu': handle.itemMouseleave\r
1158                     }, '.context-menu-item');\r
1159 \r
1160                 initialized = true;\r
1161             }\r
1162             \r
1163             // engage native contextmenu event\r
1164             $body\r
1165                 .on('contextmenu' + o.ns, o.selector, o, handle.contextmenu);\r
1166             \r
1167             switch (o.trigger) {\r
1168                 case 'hover':\r
1169                         $body\r
1170                             .on('mouseenter' + o.ns, o.selector, o, handle.mouseenter)\r
1171                             .on('mouseleave' + o.ns, o.selector, o, handle.mouseleave);                    \r
1172                     break;\r
1173                     \r
1174                 case 'left':\r
1175                         $body.on('click' + o.ns, o.selector, o, handle.click);\r
1176                     break;\r
1177                 /*\r
1178                 default:\r
1179                     // http://www.quirksmode.org/dom/events/contextmenu.html\r
1180                     $body\r
1181                         .on('mousedown' + o.ns, o.selector, o, handle.mousedown)\r
1182                         .on('mouseup' + o.ns, o.selector, o, handle.mouseup);\r
1183                     break;\r
1184                 */\r
1185             }\r
1186             \r
1187             // create menu\r
1188             if (!o.build) {\r
1189                 op.create(o);\r
1190             }\r
1191             break;\r
1192         \r
1193         case 'destroy':\r
1194             if (!o.selector) {\r
1195                 $body.off('.contextMenu .contextMenuAutoHide');\r
1196                 $.each(namespaces, function(key, value) {\r
1197                     $body.off(value);\r
1198                 });\r
1199                 \r
1200                 namespaces = {};\r
1201                 menus = {};\r
1202                 counter = 0;\r
1203                 initialized = false;\r
1204                 \r
1205                 $('.context-menu-list').remove();\r
1206             } else if (namespaces[o.selector]) {\r
1207                 try {\r
1208                     if (menus[namespaces[o.selector]].$menu) {\r
1209                         menus[namespaces[o.selector]].$menu.remove();\r
1210                     }\r
1211                     \r
1212                     delete menus[namespaces[o.selector]];\r
1213                 } catch(e) {\r
1214                     menus[namespaces[o.selector]] = null;\r
1215                 }\r
1216                 \r
1217                 $body.off(namespaces[o.selector]);\r
1218             }\r
1219             break;\r
1220         \r
1221         case 'html5':\r
1222             // if <command> or <menuitem> are not handled by the browser,\r
1223             // or options was a bool true,\r
1224             // initialize $.contextMenu for them\r
1225             if ((!$.support.htmlCommand && !$.support.htmlMenuitem) || (typeof options == "boolean" && options)) {\r
1226                 $('menu[type="context"]').each(function() {\r
1227                     if (this.id) {\r
1228                         $.contextMenu({\r
1229                             selector: '[contextmenu=' + this.id +']',\r
1230                             items: $.contextMenu.fromMenu(this)\r
1231                         });\r
1232                     }\r
1233                 }).css('display', 'none');\r
1234             }\r
1235             break;\r
1236         \r
1237         default:\r
1238             throw new Error('Unknown operation "' + operation + '"');\r
1239     }\r
1240     \r
1241     return this;\r
1242 };\r
1243 \r
1244 // import values into <input> commands\r
1245 $.contextMenu.setInputValues = function(opt, data) {\r
1246     if (data === undefined) {\r
1247         data = {};\r
1248     }\r
1249     \r
1250     $.each(opt.inputs, function(key, item) {\r
1251         switch (item.type) {\r
1252             case 'text':\r
1253             case 'textarea':\r
1254                 item.value = data[key] || "";\r
1255                 break;\r
1256 \r
1257             case 'checkbox':\r
1258                 item.selected = data[key] ? true : false;\r
1259                 break;\r
1260                 \r
1261             case 'radio':\r
1262                 item.selected = (data[item.radio] || "") == item.value ? true : false;\r
1263                 break;\r
1264             \r
1265             case 'select':\r
1266                 item.selected = data[key] || "";\r
1267                 break;\r
1268         }\r
1269     });\r
1270 };\r
1271 \r
1272 // export values from <input> commands\r
1273 $.contextMenu.getInputValues = function(opt, data) {\r
1274     if (data === undefined) {\r
1275         data = {};\r
1276     }\r
1277     \r
1278     $.each(opt.inputs, function(key, item) {\r
1279         switch (item.type) {\r
1280             case 'text':\r
1281             case 'textarea':\r
1282             case 'select':\r
1283                 data[key] = item.$input.val();\r
1284                 break;\r
1285 \r
1286             case 'checkbox':\r
1287                 data[key] = item.$input.prop('checked');\r
1288                 break;\r
1289                 \r
1290             case 'radio':\r
1291                 if (item.$input.prop('checked')) {\r
1292                     data[item.radio] = item.value;\r
1293                 }\r
1294                 break;\r
1295         }\r
1296     });\r
1297     \r
1298     return data;\r
1299 };\r
1300 \r
1301 // find <label for="xyz">\r
1302 function inputLabel(node) {\r
1303     return (node.id && $('label[for="'+ node.id +'"]').val()) || node.name;\r
1304 }\r
1305 \r
1306 // convert <menu> to items object\r
1307 function menuChildren(items, $children, counter) {\r
1308     if (!counter) {\r
1309         counter = 0;\r
1310     }\r
1311     \r
1312     $children.each(function() {\r
1313         var $node = $(this),\r
1314             node = this,\r
1315             nodeName = this.nodeName.toLowerCase(),\r
1316             label,\r
1317             item;\r
1318         \r
1319         // extract <label><input>\r
1320         if (nodeName == 'label' && $node.find('input, textarea, select').length) {\r
1321             label = $node.text();\r
1322             $node = $node.children().first();\r
1323             node = $node.get(0);\r
1324             nodeName = node.nodeName.toLowerCase();\r
1325         }\r
1326         \r
1327         /*\r
1328          * <menu> accepts flow-content as children. that means <embed>, <canvas> and such are valid menu items.\r
1329          * Not being the sadistic kind, $.contextMenu only accepts:\r
1330          * <command>, <menuitem>, <hr>, <span>, <p> <input [text, radio, checkbox]>, <textarea>, <select> and of course <menu>.\r
1331          * Everything else will be imported as an html node, which is not interfaced with contextMenu.\r
1332          */\r
1333         \r
1334         // http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#concept-command\r
1335         switch (nodeName) {\r
1336             // http://www.whatwg.org/specs/web-apps/current-work/multipage/interactive-elements.html#the-menu-element\r
1337             case 'menu':\r
1338                 item = {name: $node.attr('label'), items: {}};\r
1339                 menuChildren(item.items, $node.children(), counter);\r
1340                 break;\r
1341             \r
1342             // http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#using-the-a-element-to-define-a-command\r
1343             case 'a':\r
1344             // http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#using-the-button-element-to-define-a-command\r
1345             case 'button':\r
1346                 item = {\r
1347                     name: $node.text(),\r
1348                     disabled: !!$node.attr('disabled'),\r
1349                     callback: (function(){ return function(){ $node.click(); }; })()\r
1350                 };\r
1351                 break;\r
1352             \r
1353             // http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#using-the-command-element-to-define-a-command\r
1354 \r
1355             case 'menuitem':\r
1356             case 'command':\r
1357                 switch ($node.attr('type')) {\r
1358                     case undefined:\r
1359                     case 'command':\r
1360                     case 'menuitem':\r
1361                         item = {\r
1362                             name: $node.attr('label'),\r
1363                             disabled: !!$node.attr('disabled'),\r
1364                             callback: (function(){ return function(){ $node.click(); }; })()\r
1365                         };\r
1366                         break;\r
1367                         \r
1368                     case 'checkbox':\r
1369                         item = {\r
1370                             type: 'checkbox',\r
1371                             disabled: !!$node.attr('disabled'),\r
1372                             name: $node.attr('label'),\r
1373                             selected: !!$node.attr('checked')\r
1374                         };\r
1375                         break;\r
1376                         \r
1377                     case 'radio':\r
1378                         item = {\r
1379                             type: 'radio',\r
1380                             disabled: !!$node.attr('disabled'),\r
1381                             name: $node.attr('label'),\r
1382                             radio: $node.attr('radiogroup'),\r
1383                             value: $node.attr('id'),\r
1384                             selected: !!$node.attr('checked')\r
1385                         };\r
1386                         break;\r
1387                         \r
1388                     default:\r
1389                         item = undefined;\r
1390                 }\r
1391                 break;\r
1392  \r
1393             case 'hr':\r
1394                 item = '-------';\r
1395                 break;\r
1396                 \r
1397             case 'input':\r
1398                 switch ($node.attr('type')) {\r
1399                     case 'text':\r
1400                         item = {\r
1401                             type: 'text',\r
1402                             name: label || inputLabel(node),\r
1403                             disabled: !!$node.attr('disabled'),\r
1404                             value: $node.val()\r
1405                         };\r
1406                         break;\r
1407                         \r
1408                     case 'checkbox':\r
1409                         item = {\r
1410                             type: 'checkbox',\r
1411                             name: label || inputLabel(node),\r
1412                             disabled: !!$node.attr('disabled'),\r
1413                             selected: !!$node.attr('checked')\r
1414                         };\r
1415                         break;\r
1416                         \r
1417                     case 'radio':\r
1418                         item = {\r
1419                             type: 'radio',\r
1420                             name: label || inputLabel(node),\r
1421                             disabled: !!$node.attr('disabled'),\r
1422                             radio: !!$node.attr('name'),\r
1423                             value: $node.val(),\r
1424                             selected: !!$node.attr('checked')\r
1425                         };\r
1426                         break;\r
1427                     \r
1428                     default:\r
1429                         item = undefined;\r
1430                         break;\r
1431                 }\r
1432                 break;\r
1433                 \r
1434             case 'select':\r
1435                 item = {\r
1436                     type: 'select',\r
1437                     name: label || inputLabel(node),\r
1438                     disabled: !!$node.attr('disabled'),\r
1439                     selected: $node.val(),\r
1440                     options: {}\r
1441                 };\r
1442                 $node.children().each(function(){\r
1443                     item.options[this.value] = $(this).text();\r
1444                 });\r
1445                 break;\r
1446                 \r
1447             case 'textarea':\r
1448                 item = {\r
1449                     type: 'textarea',\r
1450                     name: label || inputLabel(node),\r
1451                     disabled: !!$node.attr('disabled'),\r
1452                     value: $node.val()\r
1453                 };\r
1454                 break;\r
1455             \r
1456             case 'label':\r
1457                 break;\r
1458             \r
1459             default:\r
1460                 item = {type: 'html', html: $node.clone(true)};\r
1461                 break;\r
1462         }\r
1463         \r
1464         if (item) {\r
1465             counter++;\r
1466             items['key' + counter] = item;\r
1467         }\r
1468     });\r
1469 }\r
1470 \r
1471 // convert html5 menu\r
1472 $.contextMenu.fromMenu = function(element) {\r
1473     var $this = $(element),\r
1474         items = {};\r
1475         \r
1476     menuChildren(items, $this.children());\r
1477     \r
1478     return items;\r
1479 };\r
1480 \r
1481 // make defaults accessible\r
1482 $.contextMenu.defaults = defaults;\r
1483 $.contextMenu.types = types;\r
1484 \r
1485 })(jQuery);\r