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