1/** 2 * jQuery contextMenu v2.7.0 - Plugin for simple contextMenu handling 3 * 4 * Version: v2.7.0 5 * 6 * Authors: Björn Brala (SWIS.nl), Rodney Rehm, Addy Osmani (patches for FF) 7 * Web: http://swisnl.github.io/jQuery-contextMenu/ 8 * 9 * Copyright (c) 2011-2018 SWIS BV and contributors 10 * 11 * Licensed under 12 * MIT License http://www.opensource.org/licenses/mit-license 13 * 14 * Date: 2018-07-12T09:06:55.786Z 15 */ 16 17// jscs:disable 18/* jshint ignore:start */ 19(function (factory) { 20 if (typeof define === 'function' && define.amd) { 21 // AMD. Register as anonymous module. 22 define(['jquery'], factory); 23 } else if (typeof exports === 'object') { 24 // Node / CommonJS 25 factory(require('jquery')); 26 } else { 27 // Browser globals. 28 factory(jQuery); 29 } 30})(function ($) { 31 32 'use strict'; 33 34 // TODO: - 35 // ARIA stuff: menuitem, menuitemcheckbox und menuitemradio 36 // create <menu> structure if $.support[htmlCommand || htmlMenuitem] and !opt.disableNative 37 38 // determine html5 compatibility 39 $.support.htmlMenuitem = ('HTMLMenuItemElement' in window); 40 $.support.htmlCommand = ('HTMLCommandElement' in window); 41 $.support.eventSelectstart = ('onselectstart' in document.documentElement); 42 /* // should the need arise, test for css user-select 43 $.support.cssUserSelect = (function(){ 44 var t = false, 45 e = document.createElement('div'); 46 47 $.each('Moz|Webkit|Khtml|O|ms|Icab|'.split('|'), function(i, prefix) { 48 var propCC = prefix + (prefix ? 'U' : 'u') + 'serSelect', 49 prop = (prefix ? ('-' + prefix.toLowerCase() + '-') : '') + 'user-select'; 50 51 e.style.cssText = prop + ': text;'; 52 if (e.style[propCC] == 'text') { 53 t = true; 54 return false; 55 } 56 57 return true; 58 }); 59 60 return t; 61 })(); 62 */ 63 64 65 if (!$.ui || !$.widget) { 66 // duck punch $.cleanData like jQueryUI does to get that remove event 67 $.cleanData = (function (orig) { 68 return function (elems) { 69 var events, elem, i; 70 for (i = 0; elems[i] != null; i++) { 71 elem = elems[i]; 72 try { 73 // Only trigger remove when necessary to save time 74 events = $._data(elem, 'events'); 75 if (events && events.remove) { 76 $(elem).triggerHandler('remove'); 77 } 78 79 // Http://bugs.jquery.com/ticket/8235 80 } catch (e) { 81 } 82 } 83 orig(elems); 84 }; 85 })($.cleanData); 86 } 87 /* jshint ignore:end */ 88 // jscs:enable 89 90 var // currently active contextMenu trigger 91 $currentTrigger = null, 92 // is contextMenu initialized with at least one menu? 93 initialized = false, 94 // window handle 95 $win = $(window), 96 // number of registered menus 97 counter = 0, 98 // mapping selector to namespace 99 namespaces = {}, 100 // mapping namespace to options 101 menus = {}, 102 // custom command type handlers 103 types = {}, 104 // default values 105 defaults = { 106 // selector of contextMenu trigger 107 selector: null, 108 // where to append the menu to 109 appendTo: null, 110 // method to trigger context menu ["right", "left", "hover"] 111 trigger: 'right', 112 // hide menu when mouse leaves trigger / menu elements 113 autoHide: false, 114 // ms to wait before showing a hover-triggered context menu 115 delay: 200, 116 // flag denoting if a second trigger should simply move (true) or rebuild (false) an open menu 117 // as long as the trigger happened on one of the trigger-element's child nodes 118 reposition: true, 119 // Flag denoting if a second trigger should close the menu, as long as 120 // the trigger happened on one of the trigger-element's child nodes. 121 // This overrides the reposition option. 122 hideOnSecondTrigger: false, 123 124 //ability to select submenu 125 selectableSubMenu: false, 126 127 // Default classname configuration to be able avoid conflicts in frameworks 128 classNames: { 129 hover: 'context-menu-hover', // Item hover 130 disabled: 'context-menu-disabled', // Item disabled 131 visible: 'context-menu-visible', // Item visible 132 notSelectable: 'context-menu-not-selectable', // Item not selectable 133 134 icon: 'context-menu-icon', 135 iconEdit: 'context-menu-icon-edit', 136 iconCut: 'context-menu-icon-cut', 137 iconCopy: 'context-menu-icon-copy', 138 iconPaste: 'context-menu-icon-paste', 139 iconDelete: 'context-menu-icon-delete', 140 iconAdd: 'context-menu-icon-add', 141 iconQuit: 'context-menu-icon-quit', 142 iconLoadingClass: 'context-menu-icon-loading' 143 }, 144 145 // determine position to show menu at 146 determinePosition: function ($menu) { 147 // position to the lower middle of the trigger element 148 if ($.ui && $.ui.position) { 149 // .position() is provided as a jQuery UI utility 150 // (...and it won't work on hidden elements) 151 $menu.css('display', 'block').position({ 152 my: 'center top', 153 at: 'center bottom', 154 of: this, 155 offset: '0 5', 156 collision: 'fit' 157 }).css('display', 'none'); 158 } else { 159 // determine contextMenu position 160 var offset = this.offset(); 161 offset.top += this.outerHeight(); 162 offset.left += this.outerWidth() / 2 - $menu.outerWidth() / 2; 163 $menu.css(offset); 164 } 165 }, 166 // position menu 167 position: function (opt, x, y) { 168 var offset; 169 // determine contextMenu position 170 if (!x && !y) { 171 opt.determinePosition.call(this, opt.$menu); 172 return; 173 } else if (x === 'maintain' && y === 'maintain') {
174 // x and y must not be changed (after re-show on command click) 175 offset = opt.$menu.position(); 176 } else { 177 // x and y are given (by mouse event) 178 var offsetParentOffset = opt.$menu.offsetParent().offset(); 179 offset = {top: y - offsetParentOffset.top, left: x -offsetParentOffset.left}; 180 } 181 182 // correct offset if viewport demands it 183 var bottom = $win.scrollTop() + $win.height(), 184 right = $win.scrollLeft() + $win.width(), 185 height = opt.$menu.outerHeight(), 186 width = opt.$menu.outerWidth(); 187 188 if (offset.top + height > bottom) { 189 offset.top -= height; 190 } 191 192 if (offset.top < 0) { 193 offset.top = 0; 194 } 195 196 if (offset.left + width > right) { 197 offset.left -= width; 198 } 199 200 if (offset.left < 0) { 201 offset.left = 0; 202 } 203 204 opt.$menu.css(offset); 205 }, 206 // position the sub-menu 207 positionSubmenu: function ($menu) { 208 if (typeof $menu === 'undefined') { 209 // When user hovers over item (which has sub items) handle.focusItem will call this. 210 // but the submenu does not exist yet if opt.items is a promise. just return, will 211 // call positionSubmenu after promise is completed. 212 return; 213 } 214 if ($.ui && $.ui.position) { 215 // .position() is provided as a jQuery UI utility 216 // (...and it won't work on hidden elements) 217 $menu.css('display', 'block').position({ 218 my: 'left top-5', 219 at: 'right top', 220 of: this, 221 collision: 'flipfit fit' 222 }).css('display', ''); 223 } else { 224 // determine contextMenu position 225 var offset = { 226 top: -9, 227 left: this.outerWidth() - 5 228 }; 229 $menu.css(offset); 230 } 231 }, 232 // offset to add to zIndex 233 zIndex: 1, 234 // show hide animation settings 235 animation: { 236 duration: 50, 237 show: 'slideDown', 238 hide: 'slideUp' 239 }, 240 // events 241 events: { 242 show: $.noop, 243 hide: $.noop, 244 activated: $.noop 245 }, 246 // default callback 247 callback: null, 248 // list of contextMenu items 249 items: {} 250 }, 251 // mouse position for hover activation 252 hoveract = { 253 timer: null, 254 pageX: null, 255 pageY: null 256 }, 257 // determine zIndex 258 zindex = function ($t) { 259 var zin = 0, 260 $tt = $t; 261 262 while (true) { 263 zin = Math.max(zin, parseInt($tt.css('z-index'), 10) || 0); 264 $tt = $tt.parent(); 265 if (!$tt || !$tt.length || 'html body'.indexOf($tt.prop('nodeName').toLowerCase()) > -1) { 266 break; 267 } 268 } 269 return zin; 270 }, 271 // event handlers 272 handle = { 273 // abort anything 274 abortevent: function (e) { 275 e.preventDefault(); 276 e.stopImmediatePropagation(); 277 }, 278 // contextmenu show dispatcher 279 contextmenu: function (e) { 280 var $this = $(this); 281 282 // disable actual context-menu if we are using the right mouse button as the trigger 283 if (e.data.trigger === 'right') { 284 e.preventDefault(); 285 e.stopImmediatePropagation(); 286 } 287 288 // abort native-triggered events unless we're triggering on right click 289 if ((e.data.trigger !== 'right' && e.data.trigger !== 'demand') && e.originalEvent) { 290 return; 291 } 292 293 // Let the current contextmenu decide if it should show or not based on its own trigger settings 294 if (typeof e.mouseButton !== 'undefined' && e.data) { 295 if (!(e.data.trigger === 'left' && e.mouseButton === 0) && !(e.data.trigger === 'right' && e.mouseButton === 2)) {
296 // Mouse click is not valid. 297 return; 298 } 299 } 300 301 // abort event if menu is visible for this trigger 302 if ($this.hasClass('context-menu-active')) { 303 return; 304 } 305 306 if (!$this.hasClass('context-menu-disabled')) { 307 // theoretically need to fire a show event at <menu> 308 // http://www.whatwg.org/specs/web-apps/current-work/multipage/interactive-elements.html#context-menus 309 // var evt = jQuery.Event("show", { data: data, pageX: e.pageX, pageY: e.pageY, relatedTarget: this }); 310 // e.data.$menu.trigger(evt); 311 312 $currentTrigger = $this; 313 if (e.data.build) { 314 var built = e.data.build($currentTrigger, e); 315 // abort if build() returned false 316 if (built === false) { 317 return; 318 } 319 320 // dynamically build menu on invocation 321 e.data = $.extend(true, {}, defaults, e.data, built || {}); 322 323 // abort if there are no items to display 324 if (!e.data.items || $.isEmptyObject(e.data.items)) { 325 // Note: jQuery captures and ignores errors from event handlers 326 if (window.console) { 327 (console.error || console.log).call(console, 'No items specified to show in contextMenu'); 328 } 329 330 throw new Error('No Items specified'); 331 } 332 333 // backreference for custom command type creation 334 e.data.$trigger = $currentTrigger; 335 336 op.create(e.data); 337 } 338 op.show.call($this, e.data, e.pageX, e.pageY); 339 } 340 }, 341 // contextMenu left-click trigger 342 click: function (e) { 343 e.preventDefault(); 344 e.stopImmediatePropagation(); 345 $(this).trigger($.Event('contextmenu', {data: e.data, pageX: e.pageX, pageY: e.pageY})); 346 }, 347 // contextMenu right-click trigger 348 mousedown: function (e) { 349 // register mouse down 350 var $this = $(this); 351 352 // hide any previous menus 353 if ($currentTrigger && $currentTrigger.length && !$currentTrigger.is($this)) { 354 $currentTrigger.data('contextMenu').$menu.trigger('contextmenu:hide'); 355 } 356 357 // activate on right click 358 if (e.button === 2) { 359 $currentTrigger = $this.data('contextMenuActive', true); 360 } 361 }, 362 // contextMenu right-click trigger 363 mouseup: function (e) { 364 // show menu 365 var $this = $(this); 366 if ($this.data('contextMenuActive') && $currentTrigger && $currentTrigger.length && $currentTrigger.is($this) && !$this.hasClass('context-menu-disabled')) { 367 e.preventDefault(); 368 e.stopImmediatePropagation(); 369 $currentTrigger = $this; 370 $this.trigger($.Event('contextmenu', {data: e.data, pageX: e.pageX, pageY: e.pageY})); 371 } 372 373 $this.removeData('contextMenuActive'); 374 }, 375 // contextMenu hover trigger 376 mouseenter: function (e) { 377 var $this = $(this), 378 $related = $(e.relatedTarget), 379 $document = $(document); 380 381 // abort if we're coming from a menu 382 if ($related.is('.context-menu-list') || $related.closest('.context-menu-list').length) { 383 return; 384 } 385 386 // abort if a menu is shown 387 if ($currentTrigger && $currentTrigger.length) { 388 return; 389 } 390 391 hoveract.pageX = e.pageX; 392 hoveract.pageY = e.pageY; 393 hoveract.data = e.data; 394 $document.on('mousemove.contextMenuShow', handle.mousemove); 395 hoveract.timer = setTimeout(function () { 396 hoveract.timer = null; 397 $document.off('mousemove.contextMenuShow'); 398 $currentTrigger = $this; 399 $this.trigger($.Event('contextmenu', { 400 data: hoveract.data, 401 pageX: hoveract.pageX, 402 pageY: hoveract.pageY 403 })); 404 }, e.data.delay); 405 }, 406 // contextMenu hover trigger 407 mousemove: function (e) { 408 hoveract.pageX = e.pageX; 409 hoveract.pageY = e.pageY; 410 }, 411 // contextMenu hover trigger 412 mouseleave: function (e) { 413 // abort if we're leaving for a menu 414 var $related = $(e.relatedTarget); 415 if ($related.is('.context-menu-list') || $related.closest('.context-menu-list').length) { 416 return; 417 } 418 419 try { 420 clearTimeout(hoveract.timer); 421 } catch (e) { 422 } 423 424 hoveract.timer = null; 425 }, 426 // click on layer to hide contextMenu 427 layerClick: function (e) { 428 var $this = $(this), 429 root = $this.data('contextMenuRoot'), 430 button = e.button, 431 x = e.pageX, 432 y = e.pageY, 433 fakeClick = x === undefined, 434 target, 435 offset; 436 437 e.preventDefault(); 438 439 setTimeout(function () { 440 // If the click is not real, things break: https://github.com/swisnl/jQuery-contextMenu/issues/132 441 if(fakeClick){ 442 if (root !== null && typeof root !== 'undefined' && root.$menu !== null && typeof root.$menu !== 'undefined') { 443 root.$menu.trigger('contextmenu:hide'); 444 } 445 return; 446 } 447 448 var $window; 449 var triggerAction = ((root.trigger === 'left' && button === 0) || (root.trigger === 'right' && button === 2)); 450 451 // find the element that would've been clicked, wasn't the layer in the way 452 if (document.elementFromPoint && root.$layer) { 453 root.$layer.hide(); 454 target = document.elementFromPoint(x - $win.scrollLeft(), y - $win.scrollTop()); 455 456 // also need to try and focus this element if we're in a contenteditable area,
457 // as the layer will prevent the browser mouse action we want 458 if (target.isContentEditable) { 459 var range = document.createRange(), 460 sel = window.getSelection(); 461 range.selectNode(target); 462 range.collapse(true); 463 sel.removeAllRanges(); 464 sel.addRange(range); 465 } 466 $(target).trigger(e); 467 root.$layer.show(); 468 } 469 470 if (root.hideOnSecondTrigger && triggerAction && root.$menu !== null && typeof root.$menu !== 'undefined') { 471 root.$menu.trigger('contextmenu:hide'); 472 return; 473 } 474 475 if (root.reposition && triggerAction) { 476 if (document.elementFromPoint) { 477 if (root.$trigger.is(target)) { 478 root.position.call(root.$trigger, root, x, y); 479 return; 480 } 481 } else { 482 offset = root.$trigger.offset(); 483 $window = $(window); 484 // while this looks kinda awful, it's the best way to avoid 485 // unnecessarily calculating any positions 486 offset.top += $window.scrollTop(); 487 if (offset.top <= e.pageY) {
488 offset.left += $window.scrollLeft(); 489 if (offset.left <= e.pageX) { 490 offset.bottom = offset.top + root.$trigger.outerHeight(); 491 if (offset.bottom >= e.pageY) { 492 offset.right = offset.left + root.$trigger.outerWidth(); 493 if (offset.right >= e.pageX) { 494 // reposition 495 root.position.call(root.$trigger, root, x, y); 496 return; 497 } 498 } 499 } 500 } 501 } 502 } 503 504 if (target && triggerAction) { 505 root.$trigger.one('contextmenu:hidden', function () { 506 $(target).contextMenu({x: x, y: y, button: button}); 507 }); 508 } 509 510 if (root !== null && typeof root !== 'undefined' && root.$menu !== null && typeof root.$menu !== 'undefined') { 511 root.$menu.trigger('contextmenu:hide'); 512 } 513 }, 50); 514 }, 515 // key handled :hover 516 keyStop: function (e, opt) { 517 if (!opt.isInput) { 518 e.preventDefault(); 519 } 520 521 e.stopPropagation(); 522 }, 523 key: function (e) { 524 525 var opt = {}; 526 527 // Only get the data from $currentTrigger if it exists 528 if ($currentTrigger) { 529 opt = $currentTrigger.data('contextMenu') || {}; 530 } 531 // If the trigger happen on a element that are above the contextmenu do this 532 if (typeof opt.zIndex === 'undefined') { 533 opt.zIndex = 0; 534 } 535 var targetZIndex = 0; 536 var getZIndexOfTriggerTarget = function (target) { 537 if (target.style.zIndex !== '') { 538 targetZIndex = target.style.zIndex; 539 } else { 540 if (target.offsetParent !== null && typeof target.offsetParent !== 'undefined') { 541 getZIndexOfTriggerTarget(target.offsetParent); 542 } 543 else if (target.parentElement !== null && typeof target.parentElement !== 'undefined') { 544 getZIndexOfTriggerTarget(target.parentElement); 545 } 546 } 547 }; 548 getZIndexOfTriggerTarget(e.target); 549 // If targetZIndex is heigher then opt.zIndex dont progress any futher. 550 // This is used to make sure that if you are using a dialog with a input / textarea / contenteditable div 551 // and its above the contextmenu it wont steal keys events 552 if (opt.$menu && parseInt(targetZIndex,10) > parseInt(opt.$menu.css("zIndex"),10)) { 553 return; 554 } 555 switch (e.keyCode) { 556 case 9: 557 case 38: // up 558 handle.keyStop(e, opt); 559 // if keyCode is [38 (up)] or [9 (tab) with shift] 560 if (opt.isInput) { 561 if (e.keyCode === 9 && e.shiftKey) { 562 e.preventDefault(); 563 if (opt.$selected) { 564 opt.$selected.find('input, textarea, select').blur(); 565 } 566 if (opt.$menu !== null && typeof opt.$menu !== 'undefined') { 567 opt.$menu.trigger('prevcommand'); 568 } 569 return; 570 } else if (e.keyCode === 38 && opt.$selected.find('input, textarea, select').prop('type') === 'checkbox') { 571 // checkboxes don't capture this key 572 e.preventDefault(); 573 return; 574 } 575 } else if (e.keyCode !== 9 || e.shiftKey) { 576 if (opt.$menu !== null && typeof opt.$menu !== 'undefined') { 577 opt.$menu.trigger('prevcommand'); 578 } 579 return; 580 } 581 break; 582 // omitting break; 583 // case 9: // tab - reached through omitted break; 584 case 40: // down 585 handle.keyStop(e, opt); 586 if (opt.isInput) { 587 if (e.keyCode === 9) { 588 e.preventDefault(); 589 if (opt.$selected) { 590 opt.$selected.find('input, textarea, select').blur(); 591 } 592 if (opt.$menu !== null && typeof opt.$menu !== 'undefined') {
593 opt.$menu.trigger('nextcommand'); 594 } 595 return; 596 } else if (e.keyCode === 40 && opt.$selected.find('input, textarea, select').prop('type') === 'checkbox') { 597 // checkboxes don't capture this key 598 e.preventDefault(); 599 return; 600 } 601 } else { 602 if (opt.$menu !== null && typeof opt.$menu !== 'undefined') { 603 opt.$menu.trigger('nextcommand'); 604 } 605 return; 606 } 607 break; 608 609 case 37: // left 610 handle.keyStop(e, opt); 611 if (opt.isInput || !opt.$selected || !opt.$selected.length) { 612 break; 613 } 614 615 if (!opt.$selected.parent().hasClass('context-menu-root')) { 616 var $parent = opt.$selected.parent().parent(); 617 opt.$selected.trigger('contextmenu:blur'); 618 opt.$selected = $parent; 619 return; 620 } 621 break; 622 623 case 39: // right 624 handle.keyStop(e, opt); 625 if (opt.isInput || !opt.$selected || !opt.$selected.length) { 626 break; 627 } 628 629 var itemdata = opt.$selected.data('contextMenu') || {}; 630 if (itemdata.$menu && opt.$selected.hasClass('context-menu-submenu')) { 631 opt.$selected = null; 632 itemdata.$selected = null; 633 itemdata.$menu.trigger('nextcommand'); 634 return; 635 } 636 break; 637 638 case 35: // end 639 case 36: // home 640 if (opt.$selected && opt.$selected.find('input, textarea, select').length) { 641 return; 642 } else { 643 (opt.$selected && opt.$selected.parent() || opt.$menu) 644 .children(':not(.' + opt.classNames.disabled + ', .' + opt.classNames.notSelectable + ')')[e.keyCode === 36 ? 'first' : 'last']() 645 .trigger('contextmenu:focus'); 646 e.preventDefault(); 647 return; 648 } 649 break; 650 651 case 13: // enter 652 handle.keyStop(e, opt); 653 if (opt.isInput) { 654 if (opt.$selected && !opt.$selected.is('textarea, select')) { 655 e.preventDefault(); 656 return; 657 } 658 break; 659 } 660 if (typeof opt.$selected !== 'undefined' && opt.$selected !== null) { 661 opt.$selected.trigger('mouseup'); 662 } 663 return; 664 665 case 32: // space 666 case 33: // page up 667 case 34: // page down 668 // prevent browser from scrolling down while menu is visible 669 handle.keyStop(e, opt); 670 return; 671 672 case 27: // esc 673 handle.keyStop(e, opt); 674 if (opt.$menu !== null && typeof opt.$menu !== 'undefined') { 675 opt.$menu.trigger('contextmenu:hide'); 676 } 677 return; 678 679 default: // 0-9, a-z 680 var k = (String.fromCharCode(e.keyCode)).toUpperCase(); 681 if (opt.accesskeys && opt.accesskeys[k]) { 682 // according to the specs accesskeys must be invoked immediately 683 opt.accesskeys[k].$node.trigger(opt.accesskeys[k].$menu ? 'contextmenu:focus' : 'mouseup'); 684 return; 685 } 686 break; 687 } 688 // pass event to selected item, 689 // stop propagation to avoid endless recursion 690 e.stopPropagation(); 691 if (typeof opt.$selected !== 'undefined' && opt.$selected !== null) { 692 opt.$selected.trigger(e); 693 } 694 }, 695 // select previous possible command in menu 696 prevItem: function (e) { 697 e.stopPropagation(); 698 var opt = $(this).data('contextMenu') || {}; 699 var root = $(this).data('contextMenuRoot') || {}; 700 701 // obtain currently selected menu 702 if (opt.$selected) { 703 var $s = opt.$selected; 704 opt = opt.$selected.parent().data('contextMenu') || {}; 705 opt.$selected = $s; 706 } 707 708 var $children = opt.$menu.children(), 709 $prev = !opt.$selected || !opt.$selected.prev().length ? $children.last(
709) : opt.$selected.prev(), 710 $round = $prev; 711 712 // skip disabled or hidden elements 713 while ($prev.hasClass(root.classNames.disabled) || $prev.hasClass(root.classNames.notSelectable) || $prev.is(':hidden')) { 714 if ($prev.prev().length) { 715 $prev = $prev.prev(); 716 } else { 717 $prev = $children.last(); 718 } 719 if ($prev.is($round)) { 720 // break endless loop 721 return; 722 } 723 } 724 725 // leave current 726 if (opt.$selected) { 727 handle.itemMouseleave.call(opt.$selected.get(0), e); 728 } 729 730 // activate next 731 handle.itemMouseenter.call($prev.get(0), e); 732 733 // focus input 734 var $input = $prev.find('input, textarea, select'); 735 if ($input.length) { 736 $input.focus(); 737 } 738 }, 739 // select next possible command in menu 740 nextItem: function (e) { 741 e.stopPropagation(); 742 var opt = $(this).data('contextMenu') || {}; 743 var root = $(this).data('contextMenuRoot') || {}; 744 745 // obtain currently selected menu 746 if (opt.$selected) { 747 var $s = opt.$selected; 748 opt = opt.$selected.parent().data('contextMenu') || {}; 749 opt.$selected = $s; 750 } 751 752 var $children = opt.$menu.children(), 753 $next = !opt.$selected || !opt.$selected.next().length ? $children.first() : opt.$selected.next(), 754 $round = $next; 755 756 // skip disabled 757 while ($next.hasClass(root.classNames.disabled) || $next.hasClass(root.classNames.notSelectable) || $next.is(':hidden')) { 758 if ($next.next().length) { 759 $next = $next.next(); 760 } else { 761 $next = $children.first(); 762 } 763 if ($next.is($round)) { 764 // break endless loop 765 return; 766 } 767 } 768 769 // leave current 770 if (opt.$selected) { 771 handle.itemMouseleave.call(opt.$selected.get(0), e); 772 } 773 774 // activate next 775 handle.itemMouseenter.call($next.get(0), e); 776 777 // focus input 778 var $input = $next.find('input, textarea, select'); 779 if ($input.length) { 780 $input.focus(); 781 } 782 }, 783 // flag that we're inside an input so the key handler can act accordingly 784 focusInput: function () { 785 var $this = $(this).closest('.context-menu-item'), 786 data = $this.data(), 787 opt = data.contextMenu, 788 root = data.contextMenuRoot; 789 790 root.$selected = opt.$selected = $this; 791 root.isInput = opt.isInput = true; 792 }, 793 // flag that we're inside an input so the key handler can act accordingly 794 blurInput: function () { 795 var $this = $(this).closest('.context-menu-item'), 796 data = $this.data(), 797 opt = data.contextMenu, 798 root = data.contextMenuRoot; 799 800 root.isInput = opt.isInput = false; 801 }, 802 // :hover on menu 803 menuMouseenter: function () { 804 var root = $(this).data().contextMenuRoot; 805 root.hovering = true; 806 }, 807 // :hover on menu 808 menuMouseleave: function (e) { 809 var root = $(this).data().contextMenuRoot; 810 if (root.$layer && root.$layer.is(e.relatedTarget)) { 811 root.hovering = false; 812 } 813 }, 814 // :hover done manually so key handling is possible 815 itemMouseenter: function (e) { 816 var $this = $(this), 817 data = $this.data(), 818 opt = data.contextMenu, 819 root = data.contextMenuRoot; 820 821 root.hovering = true; 822 823 // abort if we're re-entering 824 if (e && root.$layer && root.$layer.is(e.relatedTarget)) { 825 e.preventDefault(); 826 e.stopImmediatePropagation(); 827 } 828 829 // make sure only one item is selected 830 (opt.$menu ? opt : root).$menu 831 .children('.' + root.classNames.hover).trigger('contextmenu:blur') 832 .children('.hover').trigger('contextmenu:blur'); 833 834 if ($this.hasClass(root.classNames.disabled) || $this.hasClass(root.classNames.notSelectable)) { 835 opt.$selected = null; 836 return; 837 } 838 839 840 $this.trigger('contextmenu:focus'); 841 }, 842 // :hover done manually so key handling is possible 843 itemMouseleave: function (e) { 844 var $this = $(this), 845 data = $this.data(), 846 opt = data.contextMenu, 847 root = data.contextMenuRoot; 848 849 if (root !== opt && root.$layer && root.$layer.is(e.relatedTarget)) { 850 if (typeof root.$selected !== 'undefined' && root.$selected !== null) { 851 root.$selected.trigger('contextmenu:blur'); 852 } 853 e.preventDefault(); 854 e.stopImmediatePropagation(); 855 root.$selected = opt.$selected = opt.$node;
856 return; 857 } 858 859 if(opt && opt.$menu && opt.$menu.hasClass('context-menu-visible')){ 860 return; 861 } 862 863 $this.trigger('contextmenu:blur'); 864 }, 865 // contextMenu item click 866 itemClick: function (e) { 867 var $this = $(this), 868 data = $this.data(), 869 opt = data.contextMenu, 870 root = data.contextMenuRoot, 871 key = data.contextMenuKey, 872 callback; 873 874 // abort if the key is unknown or disabled or is a menu 875 if (!opt.items[key] || $this.is('.' + root.classNames.disabled + ', .context-menu-separator, .' + root.classNames.notSelectable) || ($this.is('.context-menu-submenu') && root.selectableSubMenu === false )) { 876 return; 877 } 878 879 e.preventDefault(); 880 e.stopImmediatePropagation(); 881 882 if ($.isFunction(opt.callbacks[key]) && Object.prototype.hasOwnProperty.call(opt.callbacks, key)) { 883 // item-specific callback 884 callback = opt.callbacks[key]; 885 } else if ($.isFunction(root.callback)) { 886 // default callback 887 callback = root.callback; 888 } else { 889 // no callback, no action 890 return; 891 } 892 893 // hide menu if callback doesn't stop that 894 if (callback.call(root.$trigger, key, root, e) !== false) { 895 root.$menu.trigger('contextmenu:hide'); 896 } else if (root.$menu.parent().length) { 897 op.update.call(root.$trigger, root); 898 } 899 }, 900 // ignore click events on input elements 901 inputClick: function (e) { 902 e.stopImmediatePropagation(); 903 }, 904 // hide <menu> 905 hideMenu: function (e, data) { 906 var root = $(this).data('contextMenuRoot'); 907 op.hide.call(root.$trigger, root, data && data.force); 908 }, 909 // focus <command> 910 focusItem: function (e) { 911 e.stopPropagation(); 912 var $this = $(this), 913 data = $this.data(), 914 opt = data.contextMenu, 915 root = data.contextMenuRoot; 916 917 if ($this.hasClass(root.classNames.disabled) || $this.hasClass(root.classNames.notSelectable)) { 918 return; 919 } 920 921 $this 922 .addClass([root.classNames.hover, root.classNames.visible].join(' ')) 923 // select other items and included items 924 .parent().find('.context-menu-item').not($this) 925 .removeClass(root.classNames.visible) 926 .filter('.' + root.classNames.hover) 927 .trigger('contextmenu:blur'); 928 929 // remember selected 930 opt.$selected = root.$selected = $this; 931 932 933 if(opt && opt.$node && opt.$node.hasClass('context-menu-submenu')){ 934 opt.$node.addClass(root.classNames.hover); 935 } 936 937 // position sub-menu - do after show so dumb $.ui.position can keep up 938 if (opt.$node) { 939 root.positionSubmenu.call(opt.$node, opt.$menu); 940 } 941 }, 942 // blur <command> 943 blurItem: function (e) { 944 e.stopPropagation(); 945 var $this = $(this), 946 data = $this.data(), 947 opt = data.contextMenu, 948 root = data.contextMenuRoot; 949 950 if (opt.autoHide) { // for tablets and touch screens this needs to remain 951 $this.removeClass(root.classNames.visible); 952 } 953 $this.removeClass(root.classNames.hover); 954 opt.$selected = null; 955 } 956 }, 957 // operations 958 op = { 959 show: function (opt, x, y) { 960 var $trigger = $(this), 961 css = {}; 962 963 // hide any open menus 964 $('#context-menu-layer').trigger('mousedown'); 965 966 // backreference for callbacks 967 opt.$trigger = $trigger; 968 969 // show event 970 if (opt.events.show.call($trigger, opt) === false) { 971 $currentTrigger = null; 972 return; 973 } 974 975 // create or update context menu 976 var hasVisibleItems = op.update.call($trigger, opt); 977 if (hasVisibleItems === false) { 978 $currentTrigger = null; 979 return; 980 } 981 982 // position menu 983 opt.position.call($trigger, opt, x, y); 984 985 // make sure we're in front 986 if (opt.zIndex) { 987 var additionalZValue = opt.zIndex; 988 // If opt.zIndex is a function, call the function to get the right zIndex. 989 if (typeof opt.zIndex === 'function') { 990 additionalZValue = opt.zIndex.call($trigger, opt); 991 } 992 css.zIndex = zindex($trigger) + additionalZValue; 993 } 994 995 // add layer 996 op.layer.call(opt.$menu, opt, css.zIndex); 997 998 // adjust sub-menu zIndexes 999 opt.$menu.find('ul').css('zIndex', css.zIndex + 1); 1000 1001 // position and show context menu 1002 opt.$menu.css(css)[opt.animation.show](opt.animation.duration, function () { 1003 $trigger.trigger('contextmenu:visible'); 1004 1005 op.activated(opt); 1006 opt.events.activated(opt); 1007 }); 1008 // make options available and set state 1009 $trigger 1010 .data('contextMenu', opt) 1011 .addClass('context-menu-active'); 1012 1013 // register key handler 1014 $(document).off('keydown.contextMenu').on('keydown.contextMenu', handle.key); 1015 // register autoHide handler 1016 if (opt.autoHide) { 1017 // mouse position handler 1018 $(document).on('mousemove.contextMenuAutoHide', function (e) { 1019 // need to capture the offset on mousemove, 1020 // since the page might've been scrolled since activation 1021 var pos = $trigger.offset(); 1022 pos.right = pos.left + $trigger.outerWidth(); 1023 pos.bottom = pos.top + $trigger.outerHeight(); 1024
1025 if (opt.$layer && !opt.hovering && (!(e.pageX >= pos.left && e.pageX <= pos.right) || !(e.pageY >= pos.top && e.pageY <= pos.bottom))) { 1026 /* Additional hover check after short time, you might just miss the edge of the menu */ 1027 setTimeout(function () { 1028 if (!opt.hovering && opt.$menu !== null && typeof opt.$menu !== 'undefined') { 1029 opt.$menu.trigger('contextmenu:hide'); 1030 } 1031 }, 50); 1032 } 1033 }); 1034 } 1035 }, 1036 hide: function (opt, force) { 1037 var $trigger = $(this); 1038 if (!opt) { 1039 opt = $trigger.data('contextMenu') || {}; 1040 } 1041 1042 // hide event 1043 if (!force && opt.events && opt.events.hide.call($trigger, opt) === false) { 1044 return; 1045 } 1046 1047 // remove options and revert state 1048 $trigger 1049 .removeData('contextMenu') 1050 .removeClass('context-menu-active'); 1051 1052 if (opt.$layer) { 1053 // keep layer for a bit so the contextmenu event can be aborted properly by opera 1054 setTimeout((function ($layer) { 1055 return function () { 1056 $layer.remove(); 1057 }; 1058 })(opt.$layer), 10); 1059 1060 try { 1061 delete opt.$layer; 1062 } catch (e) { 1063 opt.$layer = null; 1064 } 1065 } 1066 1067 // remove handle 1068 $currentTrigger = null; 1069 // remove selected 1070 opt.$menu.find('.' + opt.classNames.hover).trigger('contextmenu:blur'); 1071 opt.$selected = null; 1072 // collapse all submenus 1073 opt.$menu.find('.' + opt.classNames.visible).removeClass(opt.classNames.visible); 1074 // unregister key and mouse handlers 1075 // $(document).off('.contextMenuAutoHide keydown.contextMenu'); // http://bugs.jquery.com/ticket/10705 1076 $(document).off('.contextMenuAutoHide').off('keydown.contextMenu'); 1077 // hide menu 1078 if (opt.$menu) { 1079 opt.$menu[opt.animation.hide](opt.animation.duration, function () { 1080 // tear down dynamically built menu after animation is completed. 1081 if (opt.build) { 1082 opt.$menu.remove(); 1083 $.each(opt, function (key) { 1084 switch (key) { 1085 case 'ns': 1086 case 'selector': 1087 case 'build': 1088 case 'trigger': 1089 return true; 1090 1091 default: 1092 opt[key] = undefined; 1093 try { 1094 delete opt[key]; 1095 } catch (e) { 1096 } 1097 return true; 1098 } 1099 }); 1100 } 1101 1102 setTimeout(function () { 1103 $trigger.trigger('contextmenu:hidden'); 1104 }, 10); 1105 }); 1106 } 1107 }, 1108 create: function (opt, root) { 1109 if (typeof root === 'undefined') { 1110 root = opt; 1111 } 1112 1113 // create contextMenu 1114 opt.$menu = $('<ul class="context-menu-list"></ul>').addClass(opt.className || '').data({ 1115 'contextMenu': opt, 1116 'contextMenuRoot': root 1117 }); 1118 1119 $.each(['callbacks', 'commands', 'inputs'], function (i, k) { 1120 opt[k] = {}; 1121 if (!root[k]) { 1122 root[k] = {}; 1123 } 1124 }); 1125 1126 if (!root.accesskeys) { 1127 root.accesskeys = {}; 1128 } 1129 1130 function createNameNode(item) {
1131 var $name = $('<span></span>'); 1132 if (item._accesskey) { 1133 if (item._beforeAccesskey) { 1134 $name.append(document.createTextNode(item._beforeAccesskey)); 1135 } 1136 $('<span></span>') 1137 .addClass('context-menu-accesskey') 1138 .text(item._accesskey) 1139 .appendTo($name); 1140 if (item._afterAccesskey) { 1141 $name.append(document.createTextNode(item._afterAccesskey)); 1142 } 1143 } else { 1144 if (item.isHtmlName) { 1145 // restrict use with access keys 1146 if (typeof item.accesskey !== 'undefined') { 1147 throw new Error('accesskeys are not compatible with HTML names and cannot be used together in the same item'); 1148 } 1149 $name.html(item.name); 1150 } else { 1151 $name.text(item.name); 1152 } 1153 } 1154 return $name; 1155 } 1156 1157 // create contextMenu items 1158 $.each(opt.items, function (key, item) { 1159 var $t = $('<li class="context-menu-item"></li>').addClass(item.className || ''), 1160 $label = null, 1161 $input = null; 1162 1163 // iOS needs to see a click-event bound to an element to actually 1164 // have the TouchEvents infrastructure trigger the click event 1165 $t.on('click', $.noop); 1166 1167 // Make old school string seperator a real item so checks wont be 1168 // akward later. 1169 // And normalize 'cm_separator' into 'cm_seperator'. 1170 if (typeof item === 'string' || item.type === 'cm_separator') { 1171 item = {type: 'cm_seperator'}; 1172 } 1173 1174 item.$node = $t.data({ 1175 'contextMenu': opt, 1176 'contextMenuRoot': root, 1177 'contextMenuKey': key 1178 }); 1179 1180 // register accesskey 1181 // NOTE: the accesskey attribute should be applicable to any element, but Safari5 and Chrome13 still can't do that 1182 if (typeof item.accesskey !== 'undefined') { 1183 var aks = splitAccesskey(item.accesskey); 1184 for (var i = 0, ak; ak = aks[i]; i++) { 1185 if (!root.accesskeys[ak]) { 1186 root.accesskeys[ak] = item; 1187 var matched = item.name.match(new RegExp('^(.*?)(' + ak + ')(.*)$', 'i')); 1188 if (matched) { 1189 item._beforeAccesskey = matched[1]; 1190 item._accesskey = matched[2]; 1191 item._afterAccesskey = matched[3]; 1192 } 1193 break; 1194 } 1195 } 1196 } 1197 1198 if (item.type && types[item.type]) { 1199 // run custom type handler 1200 types[item.type].call($t, item, opt, root); 1201 // register commands 1202 $.each([opt, root], function (i, k) { 1203 k.commands[key] = item; 1204 // Overwrite only if undefined or the item is appended to the root. This so it 1205 // doesn't overwrite callbacks of root elements if the name is the same. 1206 if ($.isFunction(item.callback) && (typeof k.callbacks[key] === 'undefined' || typeof opt.type === 'undefined')) { 1207 k.callbacks[key] = item.callback; 1208 } 1209 }); 1210 } else { 1211 // add label for input 1212 if (item.type === 'cm_seperator') { 1213 $t.addClass('context-menu-separator ' + root.classNames.notSelectable); 1214 } else if (item.type === 'html') { 1215 $t.addClass('context-menu-html ' + root.classNames.notSelectable); 1216 } else if (item.type !== 'sub' && item.type) { 1217 $label = $('<label></label>').appendTo($t); 1218 createNameNode(item).appendTo($label); 1219 1220 $t.addClass('context-menu-input'); 1221 opt.hasTypes = true; 1222 $.each([opt, root], function (i, k) { 1223 k.commands[key] = item; 1224 k.inputs[key] = item; 1225 }); 1226 } else if (item.items) { 1227 item.type = 'sub'; 1228 } 1229 1230 switch (item.type) { 1231 case 'cm_seperator': 1232 break; 1233 1234 case 'text': 1235 $input = $('<input type="text" value="1" name="" />') 1236 .attr('name', 'context-menu-input-' + key) 1237 .val(item.value || '') 1238 .appendTo($label); 1239 break; 1240 1241 case 'textarea': 1242 $input = $('<textarea name=""></textarea>') 1243 .attr('name', 'context-menu-input-' + key) 1244 .val(item.value || '') 1245 .appendTo($label); 1246 1247 if (item.height) { 1248 $input.height(item.height); 1249 } 1250 break; 1251 1252 case 'checkbox': 1253 $input = $('<input type="checkbox" value="1" name="" />') 1254 .attr('name', 'context-menu-input-' + key) 1255 .val(item.value || '') 1256 .prop('checked', !!item.selected) 1257 .prependTo($label); 1258 break; 1259 1260 case 'radio': 1261 $input = $('<input type="radio" value="1" name="" />') 1262 .attr('name', 'context-menu-input-' + item.radio) 1263 .val(item.value || '') 1264 .prop('checked', !!item.selected) 1265 .prependTo($label); 1266 break; 1267 1268 case 'select': 1269 $input = $('<select name=""></select>') 1270 .attr('name', 'context-menu-input-' + key) 1271 .appendTo($label); 1272 if (item.options) { 1273 $.each(item.options, function (value, text) { 1274 $('<option></option>').val(value).text(text).appendTo($input); 1275 }); 1276 $input.val(item.selected); 1277 } 1278 break; 1279 1280 case 'sub': 1281 createNameNode(item).appendTo($t); 1282 item.appendTo = item.$node;
1283 $t.data('contextMenu', item).addClass('context-menu-submenu'); 1284 item.callback = null; 1285 1286 // If item contains items, and this is a promise, we should create it later 1287 // check if subitems is of type promise. If it is a promise we need to create 1288 // it later, after promise has been resolved. 1289 if ('function' === typeof item.items.then) { 1290 // probably a promise, process it, when completed it will create the sub menu's. 1291 op.processPromises(item, root, item.items); 1292 } else { 1293 // normal submenu. 1294 op.create(item, root); 1295 } 1296 break; 1297 1298 case 'html': 1299 $(item.html).appendTo($t); 1300 break; 1301 1302 default: 1303 $.each([opt, root], function (i, k) { 1304 k.commands[key] = item; 1305 // Overwrite only if undefined or the item is appended to the root. This so it 1306 // doesn't overwrite callbacks of root elements if the name is the same. 1307 if ($.isFunction(item.callback) && (typeof k.callbacks[key] === 'undefined' || typeof opt.type === 'undefined')) { 1308 k.callbacks[key] = item.callback; 1309 } 1310 }); 1311 createNameNode(item).appendTo($t); 1312 break; 1313 } 1314 1315 // disable key listener in <input> 1316 if (item.type && item.type !== 'sub' && item.type !== 'html' && item.type !== 'cm_seperator') { 1317 $input 1318 .on('focus', handle.focusInput) 1319 .on('blur', handle.blurInput); 1320 1321 if (item.events) { 1322 $input.on(item.events, opt); 1323 } 1324 } 1325 1326 // add icons 1327 if (item.icon) { 1328 if ($.isFunction(item.icon)) { 1329 item._icon = item.icon.call(this, this, $t, key, item); 1330 } else { 1331 if (typeof(item.icon) === 'string' && ( 1332 item.icon.substring(0, 4) === 'fab ' 1333 || item.icon.substring(0, 4) === 'fas ' 1334 || item.icon.substring(0, 4) === 'far ' 1335 || item.icon.substring(0, 4) === 'fal ') 1336 ) { 1337 // to enable font awesome 1338 $t.addClass(root.classNames.icon + ' ' + root.classNames.icon + '--fa5'); 1339 item._icon = $('<i class="' + item.icon + '"></i>'); 1340 } else if (typeof(item.icon) === 'string' && item.icon.substring(0, 3) === 'fa-') { 1341 item._icon = root.classNames.icon + ' ' + root.classNames.icon + '--fa fa ' + item.icon; 1342 } else { 1343 item._icon = root.classNames.icon + ' ' + root.classNames.icon + '-' + item.icon; 1344 } 1345 } 1346 1347 if(typeof(item._icon) === "string"){ 1348 $t.addClass(item._icon); 1349 } else { 1350 $t.prepend(item._icon); 1351 } 1352 } 1353 } 1354
1355 // cache contained elements 1356 item.$input = $input; 1357 item.$label = $label; 1358 1359 // attach item to menu 1360 $t.appendTo(opt.$menu); 1361 1362 // Disable text selection 1363 if (!opt.hasTypes && $.support.eventSelectstart) { 1364 // browsers support user-select: none, 1365 // IE has a special event for text-selection 1366 // browsers supporting neither will not be preventing text-selection 1367 $t.on('selectstart.disableTextSelect', handle.abortevent); 1368 } 1369 }); 1370 // attach contextMenu to <body> (to bypass any possible overflow:hidden issues on parents of the trigger element) 1371 if (!opt.$node) { 1372 opt.$menu.css('display', 'none').addClass('context-menu-root'); 1373 } 1374 opt.$menu.appendTo(opt.appendTo || document.body); 1375 }, 1376 resize: function ($menu, nested) { 1377 var domMenu; 1378 // determine widths of submenus, as CSS won't grow them automatically 1379 // position:absolute within position:absolute; min-width:100; max-width:200; results in width: 100; 1380 // kinda sucks hard... 1381 1382 // determine width of absolutely positioned element 1383 $menu.css({position: 'absolute', display: 'block'}); 1384 // don't apply yet, because that would break nested elements' widths 1385 $menu.data('width', 1386 (domMenu = $menu.get(0)).getBoundingClientRect ? 1387 Math.ceil(domMenu.getBoundingClientRect().width) : 1388 $menu.outerWidth() + 1); // outerWidth() returns rounded pixels 1389 // reset styles so they allow nested elements to grow/shrink naturally 1390 $menu.css({ 1391 position: 'static', 1392 minWidth: '0px', 1393 maxWidth: '100000px' 1394 }); 1395 // identify width of nested menus 1396 $menu.find('> li > ul').each(function () { 1397 op.resize($(this), true); 1398 }); 1399 // reset and apply changes in the end because nested 1400 // elements' widths wouldn't be calculatable otherwise 1401 if (!nested) { 1402 $menu.find('ul').addBack().css({ 1403 position: '', 1404 display: '', 1405 minWidth: '', 1406 maxWidth: '' 1407 }).outerWidth(function () { 1408 return $(this).data('width'); 1409 }); 1410 } 1411 }, 1412 update: function (opt, root) { 1413 var $trigger = this; 1414 if (typeof root === 'undefined') { 1415 root = opt; 1416 op.resize(opt.$menu); 1417 } 1418 1419 var hasVisibleItems = false; 1420 1421 // re-check disabled for each item 1422 opt.$menu.children().each(function () { 1423 var $item = $(this), 1424 key = $item.data('contextMenuKey'), 1425 item = opt.items[key], 1426 disabled = ($.isFunction(item.disabled) && item.disabled.call($trigger, key, root)) || item.disabled === true, 1427 visible; 1428 if ($.isFunction(item.visible)) { 1429 visible = item.visible.call($trigger, key, root); 1430 } else if (typeof item.visible !== 'undefined') { 1431 visible = item.visible === true; 1432 } else { 1433 visible = true; 1434 } 1435 1436 if (visible) { 1437 hasVisibleItems = true; 1438 } 1439 1440 $item[visible ? 'show' : 'hide'](); 1441 1442 // dis- / enable item 1443 $item[disabled ? 'addClass' : 'removeClass'](root.classNames.disabled); 1444 1445 if ($.isFunction(item.icon)) { 1446 $item.removeClass(item._icon); 1447 var iconResult = item.icon.call(this, $trigger, $item, key, item); 1448 if(typeof(iconResult) === "string"){ 1449 $item.addClass(iconResult); 1450 } else { 1451 $item.prepend(iconResult); 1452 } 1453 } 1454 1455 if (item.type) { 1456 // dis- / enable input elements 1457 $item.find('input, select, textarea').prop('disabled', disabled); 1458 1459 // update input states 1460 switch (item.type) { 1461 case 'text': 1462 case 'textarea': 1463 item.$input.val(item.value || ''); 1464 break; 1465 1466 case 'checkbox': 1467 case 'radio': 1468 item.$input.val(item.value || '').prop('checked', !!item.selected); 1469 break; 1470 1471 case 'select': 1472 item.$input.val((item.selected === 0 ? "0" : item.selected) || ''); 1473 break; 1474 } 1475 } 1476 1477 if (item.$menu) { 1478 // update sub-menu 1479 var subMenuHasVisibleItems = op.update.call($trigger, item, root); 1480 if (subMenuHasVisibleItems) { 1481 hasVisibleItems = true; 1482 } 1483 } 1484 }); 1485 return hasVisibleItems; 1486 }, 1487 layer: function (opt, zIndex) { 1488 // add transparent layer for click area 1489 // filter and background for Internet Explorer, Issue #23
1490 var $layer = opt.$layer = $('<div id="context-menu-layer"></div>') 1491 .css({ 1492 height: $win.height(), 1493 width: $win.width(), 1494 display: 'block', 1495 position: 'fixed', 1496 'z-index': zIndex, 1497 top: 0, 1498 left: 0, 1499 opacity: 0, 1500 filter: 'alpha(opacity=0)', 1501 'background-color': '#000' 1502 }) 1503 .data('contextMenuRoot', opt) 1504 .insertBefore(this) 1505 .on('contextmenu', handle.abortevent) 1506 .on('mousedown', handle.layerClick); 1507 1508 // IE6 doesn't know position:fixed; 1509 if (typeof document.body.style.maxWidth === 'undefined') { // IE6 doesn't support maxWidth 1510 $layer.css({ 1511 'position': 'absolute', 1512 'height': $(document).height() 1513 }); 1514 } 1515 1516 return $layer; 1517 }, 1518 processPromises: function (opt, root, promise) { 1519 // Start 1520 opt.$node.addClass(root.classNames.iconLoadingClass); 1521 1522 function completedPromise(opt, root, items) { 1523 // Completed promise (dev called promise.resolve). We now have a list of items which can 1524 // be used to create the rest of the context menu. 1525 if (typeof items === 'undefined') { 1526 // Null result, dev should have checked 1527 errorPromise(undefined);//own error object 1528 } 1529 finishPromiseProcess(opt, root, items); 1530 } 1531 1532 function errorPromise(opt, root, errorItem) { 1533 // User called promise.reject() with an error item, if not, provide own error item. 1534 if (typeof errorItem === 'undefined') { 1535 errorItem = { 1536 "error": { 1537 name: "No items and no error item", 1538 icon: "context-menu-icon context-menu-icon-quit" 1539 } 1540 }; 1541 if (window.console) { 1542 (console.error || console.log).call(console, 'When you reject a promise, provide an "items" object, equal to normal sub-menu items'); 1543 } 1544 } else if (typeof errorItem === 'string') { 1545 errorItem = {"error": {name: errorItem}}; 1546 } 1547 finishPromiseProcess(opt, root, errorItem); 1548 } 1549 1550 function finishPromiseProcess(opt, root, items) { 1551 if (typeof root.$menu === 'undefined' || !root.$menu.is(':visible')) { 1552 return; 1553 } 1554 opt.$node.removeClass(root.classNames.iconLoadingClass); 1555 opt.items = items; 1556 op.create(opt, root, true); // Create submenu 1557 op.update(opt, root); // Correctly update position if user is already hovered over menu item 1558 root.positionSubmenu.call(opt.$node, opt.$menu); // positionSubmenu, will only do anything if user already hovered over menu item that just got new subitems. 1559 } 1560 1561 // Wait for promise completion. .then(success, error, notify) (we don't track notify). Bind the opt 1562 // and root to avoid scope problems 1563 promise.then(completedPromise.bind(this, opt, root), errorPromise.bind(this, opt, root)); 1564 }, 1565 // operation that will run after contextMenu showed on screen 1566 activated: function(opt){ 1567 var $menu = opt.$menu; 1568 var $menuOffset = $menu.offset(); 1569 var winHeight = $(window).height(); 1570 var winScrollTop = $(window).scrollTop(); 1571 var menuHeight = $menu.height(); 1572 if(menuHeight > winHeight){ 1573 $menu.css({ 1574 'height' : winHeight + 'px', 1575 'overflow-x': 'hidden', 1576 'overflow-y': 'auto', 1577 'top': winScrollTop + 'px' 1578 }); 1579 } else if(($menuOffset.top < winScrollTop) || ($menuOffset.top + menuHeight > winScrollTop + winHeight)){ 1580 $menu.css({ 1581 'top': '0px' 1582 }); 1583 } 1584 } 1585 }; 1586 1587 // split accesskey according to http://www.whatwg.org/specs/web-apps/current-work/multipage/editing.html#assigned-access-key 1588 function splitAccesskey(val) { 1589 var t = val.split(/\s+/); 1590 var keys = []; 1591 1592 for (var i = 0, k; k = t[i]; i++) { 1593 k = k.charAt(0).toUpperCase();
1593 // first character only 1594 // theoretically non-accessible characters should be ignored, but different systems, different keyboard layouts, ... screw it. 1595 // a map to look up already used access keys would be nice 1596 keys.push(k); 1597 } 1598 1599 return keys; 1600 } 1601 1602// handle contextMenu triggers 1603 $.fn.contextMenu = function (operation) { 1604 var $t = this, $o = operation; 1605 if (this.length > 0) { // this is not a build on demand menu 1606 if (typeof operation === 'undefined') { 1607 this.first().trigger('contextmenu'); 1608 } else if (typeof operation.x !== 'undefined' && typeof operation.y !== 'undefined') { 1609 this.first().trigger($.Event('contextmenu', { 1610 pageX: operation.x, 1611 pageY: operation.y, 1612 mouseButton: operation.button 1613 })); 1614 } else if (operation === 'hide') { 1615 var $menu = this.first().data('contextMenu') ? this.first().data('contextMenu').$menu : null; 1616 if ($menu) { 1617 $menu.trigger('contextmenu:hide'); 1618 } 1619 } else if (operation === 'destroy') { 1620 $.contextMenu('destroy', {context: this}); 1621 } else if ($.isPlainObject(operation)) { 1622 operation.context = this; 1623 $.contextMenu('create', operation); 1624 } else if (operation) { 1625 this.removeClass('context-menu-disabled'); 1626 } else if (!operation) { 1627 this.addClass('context-menu-disabled'); 1628 } 1629 } else { 1630 $.each(menus, function () { 1631 if (this.selector === $t.selector) { 1632 $o.data = this; 1633 1634 $.extend($o.data, {trigger: 'demand'}); 1635 } 1636 }); 1637 1638 handle.contextmenu.call($o.target, $o); 1639 } 1640 1641 return this; 1642 }; 1643 1644 // manage contextMenu instances 1645 $.contextMenu = function (operation, options) { 1646 if (typeof operation !== 'string') { 1647 options = operation; 1648 operation = 'create'; 1649 } 1650 1651 if (typeof options === 'string') { 1652 options = {selector: options}; 1653 } else if (typeof options === 'undefined') { 1654 options = {}; 1655 } 1656 1657 // merge with default options 1658 var o = $.extend(true, {}, defaults, options || {}); 1659 var $document = $(document); 1660 var $context = $document; 1661 var _hasContext = false; 1662 1663 if (!o.context || !o.context.length) { 1664 o.context = document; 1665 } else { 1666 // you never know what they throw at you... 1667 $context = $(o.context).first(); 1668 o.context = $context.get(0); 1669 _hasContext = !$(o.context).is(document); 1670 } 1671 1672 switch (operation) { 1673 1674 case 'update': 1675 // Updates visibility and such 1676 if(_hasContext){ 1677 op.update($context); 1678 } else { 1679 for(var menu in menus){ 1680 if(menus.hasOwnProperty(menu)){ 1681 op.update(menus[menu]); 1682 } 1683 } 1684 } 1685 break; 1686 1687 case 'create': 1688 // no selector no joy 1689 if (!o.selector) { 1690 throw new Error('No selector specified'); 1691 } 1692 // make sure internal classes are not bound to 1693 if (o.selector.match(/.context-menu-(list|item|input)($|\s)/)) { 1694 throw new Error('Cannot bind to selector "' + o.selector + '" as it contains a reserved className'); 1695 } 1696 if (!o.build && (!o.items || $.isEmptyObject(o.items))) { 1697 throw new Error('No Items specified'); 1698 } 1699 counter++; 1700 o.ns = '.contextMenu' + counter; 1701 if (!_hasContext) { 1702 namespaces[o.selector] = o.ns; 1703 } 1704 menus[o.ns] = o; 1705 1706 // default to right click 1707 if (!o.trigger) { 1708 o.trigger = 'right'; 1709 } 1710 1711 if (!initialized) { 1712 var itemClick = o.itemClickEvent === 'click' ? 'click.contextMenu' : 'mouseup.contextMenu'; 1713 var contextMenuItemObj = { 1714 // 'mouseup.contextMenu': handle.itemClick, 1715 // 'click.contextMenu': handle.itemClick, 1716 'contextmenu:focus.contextMenu': handle.focusItem, 1717 'contextmenu:blur.contextMenu': handle.blurItem, 1718 'contextmenu.contextMenu': handle.abortevent, 1719 'mouseenter.contextMenu': handle.itemMouseenter, 1720 'mouseleave.contextMenu': handle.itemMouseleave 1721 }; 1722 contextMenuItemObj[itemClick] = handle.itemClick; 1723 // make sure item click is registered first 1724 $document 1725 .on({ 1726 'contextmenu:hide.contextMenu': handle.hideMenu, 1727 'prevcommand.contextMenu': handle.prevItem, 1728 'nextcommand.contextMenu': handle.nextItem, 1729 'contextmenu.contextMenu': handle.abortevent, 1730 'mouseenter.contextMenu': handle.menuMouseenter, 1731 'mouseleave.contextMenu': handle.menuMouseleave 1732 }, '.context-menu-list') 1733 .on('mouseup.contextMenu', '.context-menu-input', handle.inputClick) 1734 .on(contextMenuItemObj, '.context-menu-item'); 1735 1736 initialized = true; 1737 } 1738 1739 // engage native contextmenu event 1740 $context 1741 .on('contextmenu' + o.ns, o.selector, o, handle.contextmenu); 1742 1743 if (_hasContext) { 1744 // add remove hook, just in case 1745 $context.on('remove' + o.ns, function () { 1746 $(this).contextMenu('destroy'); 1747 }); 1748 } 1749 1750 switch (o.trigger) { 1751 case 'hover': 1752 $context 1753 .on('mouseenter' + o.ns, o.selector, o, handle.mouseenter) 1754 .on('mouseleave' + o.ns, o.selector, o, handle.mouseleave); 1755 break; 1756 1757 case 'left':
1758 $context.on('click' + o.ns, o.selector, o, handle.click); 1759 break; 1760 case 'touchstart': 1761 $context.on('touchstart' + o.ns, o.selector, o, handle.click); 1762 break; 1763 /* 1764 default: 1765 // http://www.quirksmode.org/dom/events/contextmenu.html 1766 $document 1767 .on('mousedown' + o.ns, o.selector, o, handle.mousedown) 1768 .on('mouseup' + o.ns, o.selector, o, handle.mouseup); 1769 break; 1770 */ 1771 } 1772 1773 // create menu 1774 if (!o.build) { 1775 op.create(o); 1776 } 1777 break; 1778 1779 case 'destroy': 1780 var $visibleMenu; 1781 if (_hasContext) { 1782 // get proper options 1783 var context = o.context; 1784 $.each(menus, function (ns, o) { 1785 1786 if (!o) { 1787 return true; 1788 } 1789 1790 // Is this menu equest to the context called from 1791 if (!$(context).is(o.selector)) { 1792 return true; 1793 } 1794 1795 $visibleMenu = $('.context-menu-list').filter(':visible'); 1796 if ($visibleMenu.length && $visibleMenu.data().contextMenuRoot.$trigger.is($(o.context).find(o.selector))) { 1797 $visibleMenu.trigger('contextmenu:hide', {force: true}); 1798 } 1799 1800 try { 1801 if (menus[o.ns].$menu) { 1802 menus[o.ns].$menu.remove(); 1803 } 1804 1805 delete menus[o.ns]; 1806 } catch (e) { 1807 menus[o.ns] = null; 1808 } 1809 1810 $(o.context).off(o.ns); 1811 1812 return true; 1813 }); 1814 } else if (!o.selector) { 1815 $document.off('.contextMenu .contextMenuAutoHide'); 1816 $.each(menus, function (ns, o) { 1817 $(o.context).off(o.ns); 1818 }); 1819 1820 namespaces = {}; 1821 menus = {}; 1822 counter = 0; 1823 initialized = false; 1824 1825 $('#context-menu-layer, .context-menu-list').remove(); 1826 } else if (namespaces[o.selector]) { 1827 $visibleMenu = $('.context-menu-list').filter(':visible'); 1828 if ($visibleMenu.length && $visibleMenu.data().contextMenuRoot.$trigger.is(o.selector)) { 1829 $visibleMenu.trigger('contextmenu:hide', {force: true}); 1830 } 1831 1832 try { 1833 if (menus[namespaces[o.selector]].$menu) { 1834 menus[namespaces[o.selector]].$menu.remove(); 1835 } 1836 1837 delete menus[namespaces[o.selector]]; 1838 } catch (e) { 1839 menus[namespaces[o.selector]] = null; 1840 } 1841 1842 $document.off(namespaces[o.selector]); 1843 } 1844 break; 1845 1846 case 'html5': 1847 // if <command> and <menuitem> are not handled by the browser, 1848 // or options was a bool true, 1849 // initialize $.contextMenu for them 1850 if ((!$.support.htmlCommand && !$.support.htmlMenuitem) || (typeof options === 'boolean' && options)) { 1851 $('menu[type="context"]').each(function () { 1852 if (this.id) { 1853 $.contextMenu({ 1854 selector: '[contextmenu=' + this.id + ']', 1855 items: $.contextMenu.fromMenu(this) 1856 }); 1857 } 1858 }).css('display', 'none'); 1859 } 1860 break; 1861 1862 default: 1863 throw new Error('Unknown operation "' + operation + '"'); 1864 } 1865 1866 return this; 1867 }; 1868 1869// import values into <input> commands 1870 $.contextMenu.setInputValues = function (opt, data) { 1871 if (typeof data === 'undefined') { 1872 data = {}; 1873 } 1874 1875 $.each(opt.inputs, function (key, item) { 1876 switch (item.type) { 1877 case 'text': 1878 case 'textarea': 1879 item.value = data[key] || ''; 1880 break; 1881 1882 case 'checkbox': 1883 item.selected = data[key] ? true : false;
1884 break; 1885 1886 case 'radio': 1887 item.selected = (data[item.radio] || '') === item.value; 1888 break; 1889 1890 case 'select': 1891 item.selected = data[key] || ''; 1892 break; 1893 } 1894 }); 1895 }; 1896 1897// export values from <input> commands 1898 $.contextMenu.getInputValues = function (opt, data) { 1899 if (typeof data === 'undefined') { 1900 data = {}; 1901 } 1902 1903 $.each(opt.inputs, function (key, item) { 1904 switch (item.type) { 1905 case 'text': 1906 case 'textarea': 1907 case 'select': 1908 data[key] = item.$input.val(); 1909 break; 1910 1911 case 'checkbox': 1912 data[key] = item.$input.prop('checked'); 1913 break; 1914 1915 case 'radio': 1916 if (item.$input.prop('checked')) { 1917 data[item.radio] = item.value; 1918 } 1919 break; 1920 } 1921 }); 1922 1923 return data; 1924 }; 1925 1926// find <label for="xyz"> 1927 function inputLabel(node) { 1928 return (node.id && $('label[for="' + node.id + '"]').val()) || node.name; 1929 } 1930 1931// convert <menu> to items object 1932 function menuChildren(items, $children, counter) { 1933 if (!counter) { 1934 counter = 0; 1935 } 1936 1937 $children.each(function () { 1938 var $node = $(this), 1939 node = this, 1940 nodeName = this.nodeName.toLowerCase(), 1941 label, 1942 item; 1943 1944 // extract <label><input> 1945 if (nodeName === 'label' && $node.find('input, textarea, select').length) { 1946 label = $node.text(); 1947 $node = $node.children().first(); 1948 node = $node.get(0); 1949 nodeName = node.nodeName.toLowerCase(); 1950 } 1951 1952 /* 1953 * <menu> accepts flow-content as children. that means <embed>, <canvas> and such are valid menu items. 1954 * Not being the sadistic kind, $.contextMenu only accepts: 1955 * <command>, <menuitem>, <hr>, <span>, <p> <input [text, radio, checkbox]>, <textarea>, <select> and of course <menu>. 1956 * Everything else will be imported as an html node, which is not interfaced with contextMenu. 1957 */ 1958 1959 // http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#concept-command 1960 switch (nodeName) { 1961 // http://www.whatwg.org/specs/web-apps/current-work/multipage/interactive-elements.html#the-menu-element 1962 case 'menu': 1963 item = {name: $node.attr('label'), items: {}}; 1964 counter = menuChildren(item.items, $node.children(), counter); 1965 break; 1966 1967 // http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#using-the-a-element-to-define-a-command 1968 case 'a': 1969 // http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#using-the-button-element-to-define-a-command 1970 case 'button': 1971 item = { 1972 name: $node.text(), 1973 disabled: !!$node.attr('disabled'), 1974 callback: (function () { 1975 return function () { 1976 $node.get(0).click(); 1977 }; 1978 })() 1979 }; 1980 break; 1981 1982 // http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#using-the-command-element-to-define-a-command 1983 case 'menuitem': 1984 case 'command': 1985 switch ($node.attr('type')) { 1986 case undefined: 1987 case 'command': 1988 case 'menuitem': 1989 item = { 1990 name: $node.attr('label'), 1991 disabled: !!$node.attr('disabled'), 1992 icon: $node.attr('icon'), 1993 callback: (function () { 1994 return function () { 1995 $node.get(0).click(); 1996 }; 1997 })() 1998 }; 1999 break; 2000 2001 case 'checkbox': 2002 item = { 2003 type: 'checkbox', 2004 disabled: !!$node.attr('disabled'), 2005 name: $node.attr('label'), 2006 selected: !!$node.attr('checked') 2007 }; 2008 break; 2009 case 'radio': 2010 item = { 2011 type: 'radio', 2012 disabled: !!$node.attr('disabled'), 2013 name: $node.attr('label'), 2014 radio: $node.attr('radiogroup'), 2015 value: $node.attr('id'), 2016 selected: !!$node.attr('checked') 2017 }; 2018 break; 2019 2020 default: 2021 item = undefined; 2022 } 2023 break; 2024 2025 case 'hr': 2026 item = '-------'; 2027 break; 2028 2029 case 'input': 2030 switch ($node.attr('type')) { 2031 case 'text': 2032 item = { 2033 type: 'text', 2034 name: label || inputLabel(node), 2035 disabled: !!$node.attr('disabled'), 2036 value: $node.val() 2037 }; 2038 break; 2039 2040 case 'checkbox': 2041 item = { 2042 type: 'checkbox', 2043 name: label || inputLabel(node), 2044 disabled: !!$node.attr('disabled'), 2045 selected: !!$node.attr('checked') 2046 }; 2047 break; 2048 2049 case 'radio': 2050 item = { 2051 type: 'radio', 2052 name: label || inputLabel(node), 2053 disabled: !!$node.attr('disabled'), 2054 radio: !!$node.attr('name'), 2055 value: $node.val(), 2056 selected: !!$node.attr('checked') 2057 }; 2058 break; 2059 2060 default: 2061 item = undefined; 2062 break; 2063 } 2064 break; 2065 2066 case 'select': 2067 item = { 2068 type: 'select', 2069 name: label || inputLabel(node), 2070 disabled: !!$node.attr('disabled'), 2071 selected: $node.val(), 2072 options: {} 2073 }; 2074 $node.children().each(function () { 2075 item.options[this.value] = $(this).text(); 2076 }); 2077 break; 2078 2079 case 'textarea': 2080 item = { 2081 type: 'textarea', 2082 name: label || inputLabel(node), 2083 disabled: !!$node.attr('disabled'), 2084 value: $node.val() 2085 }; 2086 break; 2087 2088 case 'label': 2089 break; 2090 2091 default: 2092 item = {type: 'html', html: $node.clone(true)}; 2093 break; 2094 } 2095 2096 if (item) { 2097 counter++; 2098 items['key' + counter] = item; 2099 } 2100 }); 2101 2102 return counter; 2103 } 2104 2105// convert html5 menu 2106 $.contextMenu.fromMenu = function (element) { 2107 var $this = $(element), 2108 items = {}; 2109 2110 menuChildren(items, $this.children()); 2111 2112 return items; 2113 }; 2114 2115// make defaults accessible 2116 $.contextMenu.defaults = defaults; 2117 $.contextMenu.types = types; 2118// export internal functions - undocumented, for hacking only! 2119 $.contextMenu.handle = handle; 2120 $.contextMenu.op = op; 2121 $.contextMenu.menus = menus; 2122});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.