2 *contextMenu.js v 1.3.0
3 *Author: Sudhanshu Yadav
5 *Copyright (c) 2013 Sudhanshu Yadav.
6 *Dual licensed under the MIT and GPL licenses
8 ;(function ($, window, document, undefined) {
11 $.fn.contextMenu = function (method, selector, option) {
14 if (!methods[method]) {
21 //need to check for array object
23 if (!((selector instanceof Array) || (typeof selector === 'string') || (selector.nodeType) || (selector.jquery))) {
29 if ((selector instanceof Array) && (method != 'update')) {
33 var myoptions = option;
34 if (method != 'update') {
35 option = iMethods.optionOtimizer(method, option);
36 myoptions = $.extend({}, $.fn.contextMenu.defaults, option);
37 if (!myoptions.baseTrigger) {
38 myoptions.baseTrigger = this;
41 methods[method].call(this, selector, myoptions);
44 $.fn.contextMenu.defaults = {
45 triggerOn: 'click', //avaliable options are all event related mouse plus enter option
46 displayAround: 'cursor', // cursor or trigger
52 closeOther: true, //to close other already opened context menu
55 sizeStyle: 'auto', //allowed values are auto and content (popup size will be according content size)
56 position: 'auto', //allowed values are top, left, bottom and right
57 closeOnClick: true, //close context menu on click/ trigger of any item in menu
60 onOpen: function (data, event) {},
61 afterOpen: function (data, event) {},
62 onClose: function (data, event) {}
66 menu: function (selector, option) {
67 var trigger = $(this);
68 selector = iMethods.createMenuList(trigger, selector, option);
69 iMethods.contextMenuBind.call(this, selector, option, 'menu');
71 popup: function (selector, option) {
72 $(selector).addClass('iw-contextMenu');
73 iMethods.contextMenuBind.call(this, selector, option, 'popup');
75 update: function (selector, option) {
77 this.each(function () {
79 menuData = trgr.data('iw-menuData');
80 //refresh if any new element is added
82 self.contextMenu('refresh');
83 menuData = trgr.data('iw-menuData');
86 var menu = menuData.menu;
87 if (typeof selector === 'object') {
89 for (var i = 0; i < selector.length; i++) {
90 var name = selector[i].name,
91 disable = selector[i].disable,
92 fun = selector[i].fun,
93 img = selector[i].img,
94 title = selector[i].title,
95 className = selector[i].className,
96 elm = menu.children('li').filter(function () {
97 return $(this).contents().filter(function () {
98 return this.nodeType == 3;
101 subMenu = selector[i].subMenu;
103 //toggle disable if provided on update method
104 disable != undefined && (disable ? elm.addClass('iw-mDisable') : elm.removeClass('iw-mDisable'));
106 //bind new function if provided
107 fun && elm.unbind('click.contextMenu').bind('click.contextMenu',fun);
110 title != undefined && elm.attr('title',title);
113 className!= undefined && elm.attr('class',className);
117 var imgIcon = elm.find('.iw-mIcon');
119 imgIcon[0].src = img;
122 elm.prepend('<img src="' + img + '" align="absmiddle" class="iw-mIcon" />');
128 elm.contextMenu('update', subMenu);
134 iMethods.onOff(menu);
135 menuData.option = $.extend({}, menuData.option, option);
136 trgr.data('iw-menuData', menuData);
138 //bind event again if trigger option has changed.
139 var eventType = menuData.option.triggerOn;
141 if (eventType != option.triggerOn) {
142 trgr.unbind('.contextMenu');
144 trgr.bind(eventType + '.contextMenu', iMethods.eventHandler);
149 refresh: function () {
150 var menuData = this.filter(function () {
151 return !!$(this).data('iw-menuData');
152 }).data('iw-menuData'),
153 newElm = this.filter(function () {
154 return !$(this).data('iw-menuData');
156 //to change basetrigger on refresh
157 menuData.option.baseTrigger = this;
158 iMethods.contextMenuBind.call(newElm, menuData.menuSelector, menuData.option);
160 open: function(sel,data){
162 var e = data.event || new Event('click');
163 if(data.top) e.clientY = data.top;
164 if(data.left) e.clientX = data.left;
165 this.each(function(){
166 iMethods.eventHandler.call(this,e);
169 //to force context menu to close
171 var menuData = this.data('iw-menuData');
173 iMethods.closeContextMenu(menuData.option, this, menuData.menu, null);
176 //to get value of a key
177 value: function (key) {
178 var menuData = this.data('iw-menuData');
180 return menuData[key];
181 } else if (menuData.option) {
182 return menuData.option[key];
186 destroy: function () {
187 this.each(function () {
189 menuId = trgr.data('iw-menuData').menuId,
190 menu = $('.iw-contextMenu[menuId=' + menuId + ']'),
191 menuData = menu.data('iw-menuData');
193 //Handle the situation of dynamically added element.
194 if (!menuData) return;
197 if (menuData.noTrigger == 1) {
198 if (menu.hasClass('iw-created')) {
201 menu.removeClass('iw-contextMenu ' + menuId)
202 .removeAttr('menuId').removeData('iw-menuData');
203 //to destroy submenus
204 menu.find('li.iw-mTrigger').contextMenu('destroy');
207 menuData.noTrigger--;
208 menu.data('iw-menuData', menuData);
210 trgr.unbind('.contextMenu').removeClass('iw-mTrigger').removeData('iw-menuData');
215 contextMenuBind: function (selector, option, method) {
218 menuData = menu.data('iw-menuData');
221 if (menu.length == 0) {
222 menu = trigger.find(selector);
223 if (menu.length == 0) {
228 if (method == 'menu') {
229 iMethods.menuHover(menu);
232 var baseTrigger = option.baseTrigger;
237 if (!baseTrigger.data('iw-menuData')) {
238 menuId = Math.ceil(Math.random() * 100000);
239 baseTrigger.data('iw-menuData', {
243 menuId = baseTrigger.data('iw-menuData').menuId;
245 //create clone menu to calculate exact height and width.
246 var cloneMenu = menu.clone();
247 cloneMenu.appendTo('body');
251 'menuWidth': cloneMenu.outerWidth(true),
252 'menuHeight': cloneMenu.outerHeight(true),
258 //to set data on selector
259 menu.data('iw-menuData', menuData).attr('menuId', menuId);
263 menuData.noTrigger++;
264 menu.data('iw-menuData', menuData);
267 //to set data on trigger
268 trigger.addClass('iw-mTrigger').data('iw-menuData', {
269 'menuId': menuData.menuId,
272 'menuSelector': selector,
278 if (option.triggerOn == 'hover') {
279 eventType = 'mouseenter';
280 //hover out if display is of context menu is on hover
281 if (baseTrigger.index(trigger) != -1) {
282 baseTrigger.add(menu).bind('mouseleave.contextMenu', function (e) {
283 if ($(e.relatedTarget).closest('.iw-contextMenu').length == 0) {
284 $('.iw-contextMenu[menuId="' + menuData.menuId + '"]').hide(100);
290 eventType = option.triggerOn;
293 trigger.delegate('input,a,.needs-click','click',function(e){ e.stopImmediatePropagation()});
296 trigger.bind(eventType + '.contextMenu', iMethods.eventHandler);
298 //to stop bubbling in menu
299 menu.bind('click mouseenter', function (e) {
303 menu.delegate('li', 'click', function (e) {
304 if (option.closeOnClick) iMethods.closeContextMenu(option, trigger, menu, e);
307 eventHandler: function (e) {
309 var trigger = $(this),
310 trgrData = trigger.data('iw-menuData'),
311 menu = trgrData.menu,
312 menuData = menu.data('iw-menuData'),
313 option = trgrData.option,
314 cntnmnt = option.containment,
320 cntWin = cntnmnt == window,
321 btChck = option.baseTrigger.index(trigger) == -1;
323 //to close previous open menu.
324 if (!btChck && option.closeOther) {
325 $('.iw-contextMenu').css('display', 'none');
328 //to reset already selected menu item
329 menu.find('.iw-mSelected').removeClass('iw-mSelected');
332 option.onOpen.call(this, clbckData, e);
335 var cObj = $(cntnmnt),
336 cHeight = cObj.innerHeight(),
337 cWidth = cObj.innerWidth(),
340 menuHeight = menuData.menuHeight,
341 menuWidth = menuData.menuWidth,
347 verAdjust = va = parseInt(option.verAdjust),
348 horAdjust = ha = parseInt(option.horAdjust);
351 cTop = cObj.offset().top;
352 cLeft = cObj.offset().left;
354 //to add relative position if no position is defined on containment
355 if (cObj.css('position') == 'static') {
356 cObj.css('position', 'relative');
361 if (option.sizeStyle == 'auto') {
362 menuHeight = Math.min(menuHeight, cHeight);
363 menuWidth = Math.min(menuWidth, cWidth);
364 menuWidth = menuWidth + 20;
367 if (option.displayAround == 'cursor') {
368 left = cntWin ? e.clientX : e.clientX + $(window).scrollLeft() - cLeft;
369 top = cntWin ? e.clientY : e.clientY + $(window).scrollTop() - cTop;
370 bottomMenu = top + menuHeight;
371 rightMenu = left + menuWidth;
372 //max height and width of context menu
373 if (bottomMenu > cHeight) {
374 if ((top - menuHeight) < 0) {
375 if ((bottomMenu - cHeight) < (menuHeight - top)) {
376 top = cHeight - menuHeight;
383 top = top - menuHeight;
387 if (rightMenu > cWidth) {
388 if ((left - menuWidth) < 0) {
389 if ((rightMenu - cWidth) < (menuWidth - left)) {
390 left = cWidth - menuWidth;
397 left = left - menuWidth;
401 } else if (option.displayAround == 'trigger') {
402 var triggerHeight = trigger.outerHeight(true),
403 triggerWidth = trigger.outerWidth(true),
404 triggerLeft = cntWin ? trigger.offset().left - cObj.scrollLeft() : trigger.offset().left - cLeft,
405 triggerTop = cntWin ? trigger.offset().top - cObj.scrollTop() : trigger.offset().top - cTop,
406 leftShift = triggerWidth;
408 left = triggerLeft + triggerWidth;
412 bottomMenu = top + menuHeight;
413 rightMenu = left + menuWidth;
414 //max height and width of context menu
415 if (bottomMenu > cHeight) {
416 if ((top - menuHeight) < 0) {
417 if ((bottomMenu - cHeight) < (menuHeight - top)) {
418 top = cHeight - menuHeight;
425 top = top - menuHeight + triggerHeight;
429 if (rightMenu > cWidth) {
430 if ((left - menuWidth) < 0) {
431 if ((rightMenu - cWidth) < (menuWidth - left)) {
432 left = cWidth - menuWidth;
434 leftShift = -triggerWidth;
441 left = left - menuWidth - triggerWidth;
443 leftShift = -triggerWidth;
447 if (option.position == 'top') {
448 menuHeight = Math.min(menuData.menuHeight, triggerTop);
449 top = triggerTop - menuHeight;
451 left = left - leftShift;
452 } else if (option.position == 'left') {
453 menuWidth = Math.min(menuData.menuWidth, triggerLeft);
454 left = triggerLeft - menuWidth;
456 } else if (option.position == 'bottom') {
457 menuHeight = Math.min(menuData.menuHeight, (cHeight - triggerTop - triggerHeight));
458 top = triggerTop + triggerHeight;
460 left = left - leftShift;
461 } else if (option.position == 'right') {
462 menuWidth = Math.min(menuData.menuWidth, (cWidth - triggerLeft - triggerWidth));
463 left = triggerLeft + triggerWidth;
467 //to draw contextMenu
468 var outerLeftRight = menu.outerWidth(true) - menu.width(),
469 outerTopBottom = menu.outerHeight(true) - menu.height();
472 //applying css property
474 'position': (cntWin || btChck) ? 'fixed' : 'absolute',
475 'display': 'inline-block',
478 'overflow-y': menuHeight != menuData.menuHeight ? 'auto' : 'hidden',
479 'overflow-x': menuWidth != menuData.menuWidth ? 'auto' : 'hidden'
482 if (option.sizeStyle == 'auto') {
483 cssObj.height = menuHeight - outerTopBottom + 'px';
484 cssObj.width = menuWidth - outerLeftRight + 'px';
487 //to get position from offset parent
488 if (option.left != 'auto') {
489 left = iMethods.getPxSize(option.left, cWidth);
491 if (option.top != 'auto') {
492 top = iMethods.getPxSize(option.top, cHeight);
495 var oParPos = trigger.offsetParent().offset();
497 left = left + cLeft - $(window).scrollLeft();
498 top = top + cTop - $(window).scrollTop();
500 left = left - (cLeft - oParPos.left);
501 top = top - (cTop - oParPos.top);
504 cssObj.left = left + ha + 'px';
505 cssObj.top = top + va + 'px';
509 //to call after open call back
510 option.afterOpen.call(this, clbckData, e);
513 //to add current menu class
514 if (trigger.closest('.iw-contextMenu').length == 0) {
515 $('.iw-curMenu').removeClass('iw-curMenu');
516 menu.addClass('iw-curMenu');
524 method: trgrData.method
526 $('html').unbind('click', iMethods.clickEvent).click(dataParm, iMethods.clickEvent);
527 $(document).unbind('keydown', iMethods.keyEvent).keydown(dataParm, iMethods.keyEvent);
528 if (option.winEventClose) {
529 $(window).bind('scroll resize', dataParm, iMethods.scrollEvent);
533 scrollEvent: function (e) {
534 iMethods.closeContextMenu(e.data.option, e.data.trigger, e.data.menu, e);
537 clickEvent: function (e) {
538 var button = e.data.trigger.get(0);
540 if ((button !== e.target) && ($(e.target).closest('.iw-contextMenu').length == 0)) {
541 iMethods.closeContextMenu(e.data.option, e.data.trigger, e.data.menu, e);
544 keyEvent: function (e) {
546 var menu = e.data.menu,
547 option = e.data.option,
549 // handle cursor keys
551 iMethods.closeContextMenu(option, e.data.trigger, menu, e);
553 if (e.data.method == 'menu') {
554 var curMenu = $('.iw-curMenu'),
555 optList = curMenu.children('li:not(.iw-mDisable)'),
556 selected = optList.filter('.iw-mSelected'),
557 index = optList.index(selected),
558 focusOn = function (elm) {
559 selected.removeClass('iw-mSelected');
560 elm.addClass('iw-mSelected');
562 first = function () {
563 focusOn(optList.filter(':first'));
566 focusOn(optList.filter(':last'));
569 focusOn(optList.filter(':eq(' + (index + 1) + ')'));
572 focusOn(optList.filter(':eq(' + (index - 1) + ')'));
574 subMenu = function () {
575 var menuData = selected.data('iw-menuData');
577 selected.triggerHandler('mouseenter.contextMenu');
578 var selector = menuData.menu;
579 selector.addClass('iw-curMenu');
580 curMenu.removeClass('iw-curMenu');
582 optList = curMenu.children('li:not(.iw-mDisable)');
583 selected = optList.filter('.iw-mSelected');
587 parMenu = function () {
588 var selector = curMenu.data('iw-menuData').trigger;
589 var parMenu = selector.closest('.iw-contextMenu');
590 if (parMenu.length != 0) {
591 curMenu.removeClass('iw-curMenu').css('display', 'none');
592 parMenu.addClass('iw-curMenu');
600 (index == optList.length - 1 || selected.length == 0) ? first(): next();
603 (index == 0 || selected.length == 0) ? last(): prev();
620 closeContextMenu: function (option, trigger, menu, e) {
622 //unbind all events from top DOM
623 $(document).unbind('keydown', iMethods.keyEvent);
624 $('html').unbind('click', iMethods.clickEvent);
625 $(window).unbind('scroll resize', iMethods.scrollEvent);
626 $('.iw-contextMenu').hide();
629 //call close function
630 option.onClose.call(this, {
635 getPxSize: function (size, of) {
639 if (size.indexOf('%') != -1) {
640 return parseInt(size) * of / 100;
642 return parseInt(size);
645 menuHover: function (menu) {
646 menu.children('li').bind('mouseenter', function (e) {
648 $('.iw-curMenu').removeClass('iw-curMenu');
649 menu.addClass('iw-curMenu');
651 var selected = menu.find('li.iw-mSelected'),
652 submenu = selected.find('.iw-contextMenu');
653 if ((submenu.length != 0) && (selected[0] != this)) {
656 selected.removeClass('iw-mSelected');
657 $(this).addClass('iw-mSelected');
660 createMenuList: function (trgr, selector, option) {
661 var baseTrigger = option.baseTrigger,
662 randomNum = Math.floor(Math.random() * 10000);
663 if ((typeof selector == 'object') && (!selector.nodeType) && (!selector.jquery)) {
664 var menuList = $('<ul class="iw-contextMenu iw-created iw-cm-menu" id="iw-contextMenu' + randomNum + '"></ul>');
665 for (var i = 0; i < selector.length; i++) {
666 var selObj = selector[i],
669 subMenu = selObj.subMenu,
670 img = selObj.img || '',
671 title = selObj.title || "",
672 className = selObj.className || "",
673 disable = selObj.disable,
674 list = $('<li title="' + title + '" class="' + className + '">' + name + '</li>');
676 list.prepend('<img src="' + img + '" align="absmiddle" class="iw-mIcon" />');
681 list.addClass('iw-mDisable');
684 list.bind('click.contextMenu', fun);
687 menuList.append(list);
689 list.append('<div class="iw-cm-arrow-right" />');
690 iMethods.subMenu(list, subMenu, baseTrigger, option);
693 if (baseTrigger.index(trgr[0]) == -1) {
694 trgr.append(menuList);
696 var par = option.containment == window ? 'body' : option.containment;
697 $(par).append(menuList);
700 iMethods.onOff($('#iw-contextMenu' + randomNum));
701 return '#iw-contextMenu' + randomNum;
702 } else if ($(selector).length != 0) {
703 var element = $(selector);
704 element.removeClass('iw-contextMenuCurrent')
705 .addClass('iw-contextMenu iw-cm-menu iw-contextMenu' + randomNum)
706 .attr('menuId', 'iw-contextMenu' + randomNum)
707 .css('display', 'none');
710 element.find('ul').each(function (index, element) {
711 var subMenu = $(this),
712 parent = subMenu.parent('li');
713 parent.append('<div class="iw-cm-arrow-right" />');
714 subMenu.addClass('iw-contextMenuCurrent');
715 iMethods.subMenu(parent, '.iw-contextMenuCurrent', baseTrigger, option);
717 iMethods.onOff($('.iw-contextMenu' + randomNum));
718 return '.iw-contextMenu' + randomNum;
721 subMenu: function (trigger, selector, baseTrigger, option) {
722 trigger.contextMenu('menu', selector, {
724 displayAround: 'trigger',
726 baseTrigger: baseTrigger,
727 containment: option.containment
730 onOff: function (menu) {
732 menu.find('.iw-mOverlay').remove();
733 menu.find('.iw-mDisable').each(function () {
735 list.append('<div class="iw-mOverlay"/>');
736 list.find('.iw-mOverlay').bind('click mouseenter', function (event) {
737 event.stopPropagation();
743 optionOtimizer: function (method, option) {
747 if (method == 'menu') {
748 if (!option.mouseClick) {
749 option.mouseClick = 'right';
752 if ((option.mouseClick == 'right') && (option.triggerOn == 'click')) {
753 option.triggerOn = 'contextmenu';
756 if ($.inArray(option.triggerOn, ['hover', 'mouseenter', 'mouseover', 'mouseleave', 'mouseout', 'focusin', 'focusout']) != -1) {
757 option.displayAround = 'trigger';
762 })(jQuery, window, document);