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