PageSourceSearch

https://propertyguardianprotection.com/js/jquery.contextMenu.js

js propertyguardianprotection.com collected 2026-09-25 13:04:17 UTC 90,968 bytes, 2,122 lines download raw bytes

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.