2 * jQuery contextMenu - Plugin for simple contextMenu handling
\r
6 * Authors: Rodney Rehm, Addy Osmani (patches for FF)
\r
7 * Web: http://medialize.github.com/jQuery-contextMenu/
\r
10 * MIT License http://www.opensource.org/licenses/mit-license
\r
11 * GPL v3 http://opensource.org/licenses/GPL-3.0
\r
15 (function($, undefined){
\r
18 // ARIA stuff: menuitem, menuitemcheckbox und menuitemradio
\r
19 // create <menu> structure if $.support[htmlCommand || htmlMenuitem] and !opt.disableNative
\r
21 // determine html5 compatibility
\r
22 $.support.htmlMenuitem = ('HTMLMenuItemElement' in window);
\r
23 $.support.htmlCommand = ('HTMLCommandElement' in window);
\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
31 // number of registered menus
\r
33 // mapping selector to namespace
\r
35 // mapping namespace to options
\r
37 // custom command type handlers
\r
41 // selector of contextMenu trigger
\r
43 // where to append the menu to
\r
45 // method to trigger context menu ["right", "left", "hover"]
\r
47 // hide menu when mouse leaves trigger / menu elements
\r
49 // ms to wait before showing a hover-triggered context menu
\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
59 at: "center bottom",
\r
63 }).css('display', 'none');
\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
73 position: function(opt, x, y) {
\r
76 // determine contextMenu position
\r
78 opt.determinePosition.call(this, opt.$menu);
\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
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
90 if (triggerIsFixed) {
\r
91 y -= $win.scrollTop();
\r
92 x -= $win.scrollLeft();
\r
94 offset = {top: y, left: x};
\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
103 if (offset.top + height > bottom) {
\r
104 offset.top -= height;
\r
107 if (offset.left + width > right) {
\r
108 offset.left -= width;
\r
111 opt.$menu.css(offset);
\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
123 }).css('display', '');
\r
125 // determine contextMenu position
\r
126 var offset = this.offset();
\r
128 offset.left += this.outerWidth();
\r
132 // offset to add to zIndex
\r
134 // show hide animation settings
\r
145 // default callback
\r
147 // list of contextMenu items
\r
150 // mouse position for hover activation
\r
156 // determine zIndex
\r
157 zindex = function($t) {
\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
174 abortevent: function(e){
\r
175 e.preventDefault();
\r
176 e.stopImmediatePropagation();
\r
179 // contextmenu show dispatcher
\r
180 contextmenu: function(e) {
\r
181 var $this = $(this);
\r
183 // disable actual context-menu
\r
184 e.preventDefault();
\r
185 e.stopImmediatePropagation();
\r
187 // abort native-triggered events unless we're triggering on right click
\r
188 if (e.data.trigger != 'right' && e.originalEvent) {
\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
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
206 // dynamically build menu on invocation
\r
207 e.data = $.extend(true, defaults, e.data, built || {});
\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
216 throw new Error('No Items sepcified');
\r
219 // backreference for custom command type creation
\r
220 e.data.$trigger = $currentTrigger;
\r
225 op.show.call($this, e.data, e.pageX, e.pageY);
\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
234 // contextMenu right-click trigger
\r
235 mousedown: function(e) {
\r
236 // register mouse down
\r
237 var $this = $(this);
\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
244 // activate on right click
\r
245 if (e.button == 2) {
\r
246 $currentTrigger = $this.data('contextMenuActive', true);
\r
249 // contextMenu right-click trigger
\r
250 mouseup: function(e) {
\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
260 $this.removeData('contextMenuActive');
\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
268 // abort if we're coming from a menu
\r
269 if ($related.is('.context-menu-list') || $related.closest('.context-menu-list').length) {
\r
273 // abort if a menu is shown
\r
274 if ($currentTrigger && $currentTrigger.length) {
\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
289 // contextMenu hover trigger
\r
290 mousemove: function(e) {
\r
291 hoveract.pageX = e.pageX;
\r
292 hoveract.pageY = e.pageY;
\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
303 clearTimeout(hoveract.timer);
\r
306 hoveract.timer = null;
\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
314 e.preventDefault();
\r
315 e.stopImmediatePropagation();
\r
317 if ((root.trigger == 'left' && e.button == 0) || (root.trigger == 'right' && e.button == 2)) {
\r
318 var offset = root.$trigger.offset();
\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
331 root.position.call(root.$trigger, root, e.pageX, e.pageY);
\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
346 // key handled :hover
\r
347 keyStop: function(e, opt) {
\r
348 if (!opt.isInput) {
\r
349 e.preventDefault();
\r
352 e.stopPropagation();
\r
355 var opt = $currentTrigger.data('contextMenu') || {},
\r
356 $children = opt.$menu.children(),
\r
359 switch (e.keyCode) {
\r
362 handle.keyStop(e, opt);
\r
363 // if keyCode is [38 (up)] or [9 (tab) with shift]
\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
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
375 } else if (e.keyCode != 9 || e.shiftKey) {
\r
376 opt.$menu.trigger('prevcommand');
\r
382 handle.keyStop(e, opt);
\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
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
395 opt.$menu.trigger('nextcommand');
\r
401 handle.keyStop(e, opt);
\r
402 if (opt.isInput || !opt.$selected || !opt.$selected.length) {
\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
415 handle.keyStop(e, opt);
\r
416 if (opt.isInput || !opt.$selected || !opt.$selected.length) {
\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
431 if (opt.$selected && opt.$selected.find('input, textarea, select').length) {
\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
443 handle.keyStop(e, opt);
\r
445 if (opt.$selected && !opt.$selected.is('textarea, select')) {
\r
446 e.preventDefault();
\r
451 opt.$selected && opt.$selected.trigger('mouseup');
\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
462 handle.keyStop(e, opt);
\r
463 opt.$menu.trigger('contextmenu:hide');
\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
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
484 // select previous possible command in menu
\r
485 prevItem: function(e) {
\r
486 e.stopPropagation();
\r
487 var opt = $(this).data('contextMenu') || {};
\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
496 var $children = opt.$menu.children(),
\r
497 $prev = !opt.$selected || !opt.$selected.prev().length ? $children.last() : opt.$selected.prev(),
\r
501 while ($prev.hasClass('disabled') || $prev.hasClass('not-selectable')) {
\r
502 if ($prev.prev().length) {
\r
503 $prev = $prev.prev();
\r
505 $prev = $children.last();
\r
507 if ($prev.is($round)) {
\r
508 // break endless loop
\r
514 if (opt.$selected) {
\r
515 handle.itemMouseleave.call(opt.$selected.get(0), e);
\r
519 handle.itemMouseenter.call($prev.get(0), e);
\r
522 var $input = $prev.find('input, textarea, select');
\r
523 if ($input.length) {
\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
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
539 var $children = opt.$menu.children(),
\r
540 $next = !opt.$selected || !opt.$selected.next().length ? $children.first() : opt.$selected.next(),
\r
544 while ($next.hasClass('disabled') || $next.hasClass('not-selectable')) {
\r
545 if ($next.next().length) {
\r
546 $next = $next.next();
\r
548 $next = $children.first();
\r
550 if ($next.is($round)) {
\r
551 // break endless loop
\r
557 if (opt.$selected) {
\r
558 handle.itemMouseleave.call(opt.$selected.get(0), e);
\r
562 handle.itemMouseenter.call($next.get(0), e);
\r
565 var $input = $next.find('input, textarea, select');
\r
566 if ($input.length) {
\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
578 root.$selected = opt.$selected = $this;
\r
579 root.isInput = opt.isInput = true;
\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
588 root.isInput = opt.isInput = false;
\r
592 menuMouseenter: function(e) {
\r
593 var root = $(this).data().contextMenuRoot;
\r
594 root.hovering = true;
\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
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
611 root.hovering = true;
\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
619 // make sure only one item is selected
\r
620 (opt.$menu ? opt : root).$menu
\r
621 .children('.hover').trigger('contextmenu:blur');
\r
623 if ($this.hasClass('disabled') || $this.hasClass('not-selectable')) {
\r
624 opt.$selected = null;
\r
628 $this.trigger('contextmenu:focus');
\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
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
645 $this.trigger('contextmenu:blur');
\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
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
661 e.preventDefault();
\r
662 e.stopImmediatePropagation();
\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
671 // no callback, no action
\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
679 op.update.call(root.$trigger, root);
\r
682 // ignore click events on input elements
\r
683 inputClick: function(e) {
\r
684 e.stopImmediatePropagation();
\r
688 hideMenu: function(e) {
\r
689 var root = $(this).data('contextMenuRoot');
\r
690 op.hide.call(root.$trigger, root);
\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
700 $this.addClass('hover')
\r
701 .siblings('.hover').trigger('contextmenu:blur');
\r
703 // remember selected
\r
704 opt.$selected = root.$selected = $this;
\r
706 // position sub-menu - do after show so dumb $.ui.position can keep up
\r
708 root.positionSubmenu.call(opt.$node, opt.$menu);
\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
719 $this.removeClass('hover');
\r
720 opt.$selected = null;
\r
725 show: function(opt, x, y) {
\r
726 var $this = $(this),
\r
730 // hide any open menus
\r
731 $('#context-menu-layer').trigger('mousedown');
\r
733 // backreference for callbacks
\r
734 opt.$trigger = $this;
\r
737 if (opt.events.show.call($this, opt) === false) {
\r
738 $currentTrigger = null;
\r
742 // create or update context menu
\r
743 op.update.call($this, opt);
\r
746 opt.position.call($this, opt, x, y);
\r
748 // make sure we're in front
\r
750 css.zIndex = zindex($this) + opt.zIndex;
\r
754 op.layer.call(opt.$menu, opt, css.zIndex);
\r
756 // adjust sub-menu zIndexes
\r
757 opt.$menu.find('ul').css('zIndex', css.zIndex + 1);
\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
780 hide: function(opt) {
\r
781 var $this = $(this);
\r
783 opt = $this.data('contextMenu') || {};
\r
787 if (opt.events && opt.events.hide.call($this, opt) === false) {
\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
796 })(opt.$layer), 10);
\r
806 $currentTrigger = null;
\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
814 opt.$menu && opt.$menu[opt.animation.hide](opt.animation.duration);
\r
816 // tear down dynamically built menu
\r
818 opt.$menu.remove();
\r
819 $.each(opt, function(key, value) {
\r
828 opt[key] = undefined;
\r
837 create: function(opt, root) {
\r
838 if (root === undefined) {
\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
847 $.each(['callbacks', 'commands', 'inputs'], function(i,k){
\r
854 root.accesskeys || (root.accesskeys = {});
\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
862 item.$node = $t.data({
\r
863 'contextMenu': opt,
\r
864 'contextMenuRoot': root,
\r
865 'contextMenuKey': key
\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
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
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
906 } else if (item.items) {
\r
910 switch (item.type) {
\r
912 $input = $('<input type="text" value="1" name="context-menu-input-'+ key +'" value="">')
\r
913 .val(item.value || "").appendTo($label);
\r
917 $input = $('<textarea name="context-menu-input-'+ key +'"></textarea>')
\r
918 .val(item.value || "").appendTo($label);
\r
921 $input.height(item.height);
\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
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
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
941 $input.val(item.selected);
\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
954 $(item.html).appendTo($t);
\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
965 $('<span></span>').html(item._name || item.name || "").appendTo($t);
\r
969 // disable key listener in <input>
\r
970 if (item.type && item.type != 'sub' && item.type != 'html') {
\r
972 .on('focus', handle.focusInput)
\r
973 .on('blur', handle.blurInput);
\r
976 $input.on(item.events);
\r
982 $t.addClass("icon icon-" + item.icon);
\r
986 // cache contained elements
\r
987 item.$input = $input;
\r
988 item.$label = $label;
\r
990 // attach item to menu
\r
991 $t.appendTo(opt.$menu);
\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
1002 // attach contextMenu to <body> (to bypass any possible overflow:hidden issues on parents of the trigger element)
\r
1004 opt.$menu.css('display', 'none').addClass('context-menu-root');
\r
1006 opt.$menu.appendTo(opt.appendTo || document.body);
\r
1008 update: function(opt, root) {
\r
1010 if (root === undefined) {
\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
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
1028 // dis- / enable item
\r
1029 $item[disabled ? 'addClass' : 'removeClass']('disabled');
\r
1032 // dis- / enable input elements
\r
1033 $item.find('input, select, textarea').prop('disabled', disabled);
\r
1035 // update input states
\r
1036 switch (item.type) {
\r
1039 item.$input.val(item.value || "");
\r
1044 item.$input.val(item.value || "").prop('checked', !!item.selected);
\r
1048 item.$input.val(item.selected || "");
\r
1054 // update sub-menu
\r
1055 op.update.call($this, item, root);
\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
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
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
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
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
1111 if (typeof options == 'string') {
\r
1112 options = {selector: options};
\r
1113 } else if (options === undefined) {
\r
1117 // merge with default options
\r
1118 var o = $.extend(true, {}, defaults, options || {}),
\r
1119 $body = $body = $(document);
\r
1121 switch (operation) {
\r
1123 // no selector no joy
\r
1124 if (!o.selector) {
\r
1125 throw new Error('No selector specified');
\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
1131 if (!o.build && (!o.items || $.isEmptyObject(o.items))) {
\r
1132 throw new Error('No Items sepcified');
\r
1135 o.ns = '.contextMenu' + counter;
\r
1136 namespaces[o.selector] = o.ns;
\r
1139 if (!initialized) {
\r
1140 // make sure item click is registered first
\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
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
1160 initialized = true;
\r
1163 // engage native contextmenu event
\r
1165 .on('contextmenu' + o.ns, o.selector, o, handle.contextmenu);
\r
1167 switch (o.trigger) {
\r
1170 .on('mouseenter' + o.ns, o.selector, o, handle.mouseenter)
\r
1171 .on('mouseleave' + o.ns, o.selector, o, handle.mouseleave);
\r
1175 $body.on('click' + o.ns, o.selector, o, handle.click);
\r
1179 // http://www.quirksmode.org/dom/events/contextmenu.html
\r
1181 .on('mousedown' + o.ns, o.selector, o, handle.mousedown)
\r
1182 .on('mouseup' + o.ns, o.selector, o, handle.mouseup);
\r
1194 if (!o.selector) {
\r
1195 $body.off('.contextMenu .contextMenuAutoHide');
\r
1196 $.each(namespaces, function(key, value) {
\r
1203 initialized = false;
\r
1205 $('.context-menu-list').remove();
\r
1206 } else if (namespaces[o.selector]) {
\r
1208 if (menus[namespaces[o.selector]].$menu) {
\r
1209 menus[namespaces[o.selector]].$menu.remove();
\r
1212 delete menus[namespaces[o.selector]];
\r
1214 menus[namespaces[o.selector]] = null;
\r
1217 $body.off(namespaces[o.selector]);
\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
1229 selector: '[contextmenu=' + this.id +']',
\r
1230 items: $.contextMenu.fromMenu(this)
\r
1233 }).css('display', 'none');
\r
1238 throw new Error('Unknown operation "' + operation + '"');
\r
1244 // import values into <input> commands
\r
1245 $.contextMenu.setInputValues = function(opt, data) {
\r
1246 if (data === undefined) {
\r
1250 $.each(opt.inputs, function(key, item) {
\r
1251 switch (item.type) {
\r
1254 item.value = data[key] || "";
\r
1258 item.selected = data[key] ? true : false;
\r
1262 item.selected = (data[item.radio] || "") == item.value ? true : false;
\r
1266 item.selected = data[key] || "";
\r
1272 // export values from <input> commands
\r
1273 $.contextMenu.getInputValues = function(opt, data) {
\r
1274 if (data === undefined) {
\r
1278 $.each(opt.inputs, function(key, item) {
\r
1279 switch (item.type) {
\r
1283 data[key] = item.$input.val();
\r
1287 data[key] = item.$input.prop('checked');
\r
1291 if (item.$input.prop('checked')) {
\r
1292 data[item.radio] = item.value;
\r
1301 // find <label for="xyz">
\r
1302 function inputLabel(node) {
\r
1303 return (node.id && $('label[for="'+ node.id +'"]').val()) || node.name;
\r
1306 // convert <menu> to items object
\r
1307 function menuChildren(items, $children, counter) {
\r
1312 $children.each(function() {
\r
1313 var $node = $(this),
\r
1315 nodeName = this.nodeName.toLowerCase(),
\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
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
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
1338 item = {name: $node.attr('label'), items: {}};
\r
1339 menuChildren(item.items, $node.children(), counter);
\r
1342 // http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#using-the-a-element-to-define-a-command
\r
1344 // http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#using-the-button-element-to-define-a-command
\r
1347 name: $node.text(),
\r
1348 disabled: !!$node.attr('disabled'),
\r
1349 callback: (function(){ return function(){ $node.click(); }; })()
\r
1353 // http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#using-the-command-element-to-define-a-command
\r
1357 switch ($node.attr('type')) {
\r
1362 name: $node.attr('label'),
\r
1363 disabled: !!$node.attr('disabled'),
\r
1364 callback: (function(){ return function(){ $node.click(); }; })()
\r
1371 disabled: !!$node.attr('disabled'),
\r
1372 name: $node.attr('label'),
\r
1373 selected: !!$node.attr('checked')
\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
1398 switch ($node.attr('type')) {
\r
1402 name: label || inputLabel(node),
\r
1403 disabled: !!$node.attr('disabled'),
\r
1404 value: $node.val()
\r
1411 name: label || inputLabel(node),
\r
1412 disabled: !!$node.attr('disabled'),
\r
1413 selected: !!$node.attr('checked')
\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
1437 name: label || inputLabel(node),
\r
1438 disabled: !!$node.attr('disabled'),
\r
1439 selected: $node.val(),
\r
1442 $node.children().each(function(){
\r
1443 item.options[this.value] = $(this).text();
\r
1450 name: label || inputLabel(node),
\r
1451 disabled: !!$node.attr('disabled'),
\r
1452 value: $node.val()
\r
1460 item = {type: 'html', html: $node.clone(true)};
\r
1466 items['key' + counter] = item;
\r
1471 // convert html5 menu
\r
1472 $.contextMenu.fromMenu = function(element) {
\r
1473 var $this = $(element),
\r
1476 menuChildren(items, $this.children());
\r
1481 // make defaults accessible
\r
1482 $.contextMenu.defaults = defaults;
\r
1483 $.contextMenu.types = types;
\r