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