2
* jQuery contextMenu v@VERSION - Plugin for simple contextMenu handling
6
* Authors: Björn Brala (SWIS.nl), Rodney Rehm, Addy Osmani (patches for FF)
7
* Web: http://swisnl.github.io/jQuery-contextMenu/
9
* Copyright (c) 2011-@YEAR SWIS BV and contributors
12
* MIT License http://www.opensource.org/licenses/mit-license
13
* GPL v3 http://opensource.org/licenses/GPL-3.0
19
if (typeof define === 'function' && define.amd) {
20
// AMD. Register as anonymous module.
21
define(['jquery'], factory);
22
} else if (typeof exports === 'object') {
24
factory(require('jquery'));
34
// ARIA stuff: menuitem, menuitemcheckbox und menuitemradio
35
// create <menu> structure if $.support[htmlCommand || htmlMenuitem] and !opt.disableNative
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(){
44
e = document.createElement('div');
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';
50
e.style.cssText = prop + ': text;';
51
if (e.style[propCC] == 'text') {
63
if (!$.ui || !$.widget) {
64
// duck punch $.cleanData like jQueryUI does to get that remove event
65
$.cleanData = (function (orig) {
66
return function (elems) {
68
for (i = 0; (elem = elems[i]) != null; i++) {
70
// Only trigger remove when necessary to save time
71
events = $._data(elem, 'events');
72
if (events && events.remove) {
73
$(elem).triggerHandler('remove');
76
// Http://bugs.jquery.com/ticket/8235
84
var // currently active contextMenu trigger
85
$currentTrigger = null,
86
// is contextMenu initialized with at least one menu?
90
// number of registered menus
92
// mapping selector to namespace
94
// mapping namespace to options
96
// custom command type handlers
100
// selector of contextMenu trigger
102
// where to append the menu to
104
// method to trigger context menu ["right", "left", "hover"]
106
// hide menu when mouse leaves trigger / menu elements
108
// ms to wait before showing a hover-triggered context menu
110
// flag denoting if a second trigger should simply move (true) or rebuild (false) an open menu
111
// as long as the trigger happened on one of the trigger-element's child nodes
114
// Default classname configuration to be able avoid conflicts in frameworks
117
hover: 'hover', // Item hover
118
disabled: 'disabled', // Item disabled
119
visible: 'visible', // Item visible
120
notSelectable: 'not-selectable', // Item not selectable
123
iconEdit: 'icon-edit',
125
iconCopy: 'icon-copy',
126
iconPaste: 'icon-paste',
127
iconDelete: 'icon-delete',
129
iconQuit: 'icon-quit'
132
// determine position to show menu at
133
determinePosition: function ($menu) {
134
// position to the lower middle of the trigger element
135
if ($.ui && $.ui.position) {
136
// .position() is provided as a jQuery UI utility
137
// (...and it won't work on hidden elements)
138
$menu.css('display', 'block').position({
144
}).css('display', 'none');
146
// determine contextMenu position
147
var offset = this.offset();
148
offset.top += this.outerHeight();
149
offset.left += this.outerWidth() / 2 - $menu.outerWidth() / 2;
154
position: function (opt, x, y) {
156
// determine contextMenu position
158
opt.determinePosition.call(this, opt.$menu);
160
} else if (x === 'maintain' && y === 'maintain') {
161
// x and y must not be changed (after re-show on command click)
162
offset = opt.$menu.position();
164
// x and y are given (by mouse event)
165
offset = {top: y, left: x};
168
// correct offset if viewport demands it
169
var bottom = $win.scrollTop() + $win.height(),
170
right = $win.scrollLeft() + $win.width(),
171
height = opt.$menu.outerHeight(),
172
width = opt.$menu.outerWidth();
174
if (offset.top + height > bottom) {
175
offset.top -= height;
178
if (offset.top < 0) {
182
if (offset.left + width > right) {
183
offset.left -= width;
186
opt.$menu.css(offset);
188
// position the sub-menu
189
positionSubmenu: function ($menu) {
190
if ($.ui && $.ui.position) {
191
// .position() is provided as a jQuery UI utility
192
// (...and it won't work on hidden elements)
193
$menu.css('display', 'block').position({
197
collision: 'flipfit fit'
198
}).css('display', '');
200
// determine contextMenu position
203
left: this.outerWidth()
208
// offset to add to zIndex
210
// show hide animation settings
223
// list of contextMenu items
226
// mouse position for hover activation
233
zindex = function ($t) {
238
zin = Math.max(zin, parseInt($tt.css('z-index'), 10) || 0);
240
if (!$tt || !$tt.length || 'html body'.indexOf($tt.prop('nodeName').toLowerCase()) > -1) {
249
abortevent: function (e) {
251
e.stopImmediatePropagation();
253
// contextmenu show dispatcher
254
contextmenu: function (e) {
257
// disable actual context-menu if we are using the right mouse button as the trigger
258
if (e.data.trigger === 'right') {
260
e.stopImmediatePropagation();
263
// abort native-triggered events unless we're triggering on right click
264
if ((e.data.trigger !== 'right' && e.data.trigger !== 'demand') && e.originalEvent) {
268
// abort event if menu is visible for this trigger
269
if ($this.hasClass('context-menu-active')) {
273
if (!$this.hasClass('context-menu-disabled')) {
274
// theoretically need to fire a show event at <menu>
275
// http://www.whatwg.org/specs/web-apps/current-work/multipage/interactive-elements.html#context-menus
276
// var evt = jQuery.Event("show", { data: data, pageX: e.pageX, pageY: e.pageY, relatedTarget: this });
277
// e.data.$menu.trigger(evt);
279
$currentTrigger = $this;
281
var built = e.data.build($currentTrigger, e);
282
// abort if build() returned false
283
if (built === false) {
287
// dynamically build menu on invocation
288
e.data = $.extend(true, {}, defaults, e.data, built || {});
290
// abort if there are no items to display
291
if (!e.data.items || $.isEmptyObject(e.data.items)) {
292
// Note: jQuery captures and ignores errors from event handlers
293
if (window.console) {
294
(console.error || console.log).call(console, 'No items specified to show in contextMenu');
297
throw new Error('No Items specified');
300
// backreference for custom command type creation
301
e.data.$trigger = $currentTrigger;
305
var showMenu = false;
306
for (var item in e.data.items) {
307
if (e.data.items.hasOwnProperty(item)) {
309
if ($.isFunction(e.data.items[item].visible)) {
310
visible = e.data.items[item].visible.call($(e.currentTarget), item, e.data);
311
} else if (typeof item.visible !== 'undefined') {
312
visible = e.data.items[item].visible === true;
323
op.show.call($this, e.data, e.pageX, e.pageY);
327
// contextMenu left-click trigger
328
click: function (e) {
330
e.stopImmediatePropagation();
331
$(this).trigger($.Event('contextmenu', {data: e.data, pageX: e.pageX, pageY: e.pageY}));
333
// contextMenu right-click trigger
334
mousedown: function (e) {
335
// register mouse down
338
// hide any previous menus
339
if ($currentTrigger && $currentTrigger.length && !$currentTrigger.is($this)) {
340
$currentTrigger.data('contextMenu').$menu.trigger('contextmenu:hide');
343
// activate on right click
344
if (e.button === 2) {
345
$currentTrigger = $this.data('contextMenuActive', true);
348
// contextMenu right-click trigger
349
mouseup: function (e) {
352
if ($this.data('contextMenuActive') && $currentTrigger && $currentTrigger.length && $currentTrigger.is($this) && !$this.hasClass('context-menu-disabled')) {
354
e.stopImmediatePropagation();
355
$currentTrigger = $this;
356
$this.trigger($.Event('contextmenu', {data: e.data, pageX: e.pageX, pageY: e.pageY}));
359
$this.removeData('contextMenuActive');
361
// contextMenu hover trigger
362
mouseenter: function (e) {
364
$related = $(e.relatedTarget),
365
$document = $(document);
367
// abort if we're coming from a menu
368
if ($related.is('.context-menu-list') || $related.closest('.context-menu-list').length) {
372
// abort if a menu is shown
373
if ($currentTrigger && $currentTrigger.length) {
377
hoveract.pageX = e.pageX;
378
hoveract.pageY = e.pageY;
379
hoveract.data = e.data;
380
$document.on('mousemove.contextMenuShow', handle.mousemove);
381
hoveract.timer = setTimeout(function () {
382
hoveract.timer = null;
383
$document.off('mousemove.contextMenuShow');
384
$currentTrigger = $this;
385
$this.trigger($.Event('contextmenu', {
387
pageX: hoveract.pageX,
388
pageY: hoveract.pageY
392
// contextMenu hover trigger
393
mousemove: function (e) {
394
hoveract.pageX = e.pageX;
395
hoveract.pageY = e.pageY;
397
// contextMenu hover trigger
398
mouseleave: function (e) {
399
// abort if we're leaving for a menu
400
var $related = $(e.relatedTarget);
401
if ($related.is('.context-menu-list') || $related.closest('.context-menu-list').length) {
406
clearTimeout(hoveract.timer);
410
hoveract.timer = null;
412
// click on layer to hide contextMenu
413
layerClick: function (e) {
415
root = $this.data('contextMenuRoot'),
423
e.stopImmediatePropagation();
425
setTimeout(function () {
427
var triggerAction = ((root.trigger === 'left' && button === 0) || (root.trigger === 'right' && button === 2));
429
// find the element that would've been clicked, wasn't the layer in the way
430
if (document.elementFromPoint) {
432
target = document.elementFromPoint(x - $win.scrollLeft(), y - $win.scrollTop());
436
if (root.reposition && triggerAction) {
437
if (document.elementFromPoint) {
438
if (root.$trigger.is(target) || root.$trigger.has(target).length) {
439
root.position.call(root.$trigger, root, x, y);
443
offset = root.$trigger.offset();
445
// while this looks kinda awful, it's the best way to avoid
446
// unnecessarily calculating any positions
447
offset.top += $window.scrollTop();
448
if (offset.top <= e.pageY) {
449
offset.left += $window.scrollLeft();
450
if (offset.left <= e.pageX) {
451
offset.bottom = offset.top + root.$trigger.outerHeight();
452
if (offset.bottom >= e.pageY) {
453
offset.right = offset.left + root.$trigger.outerWidth();
454
if (offset.right >= e.pageX) {
456
root.position.call(root.$trigger, root, x, y);
465
if (target && triggerAction) {
466
root.$trigger.one('contextmenu:hidden', function () {
467
$(target).contextMenu({x: x, y: y});
471
root.$menu.trigger('contextmenu:hide');
474
// key handled :hover
475
keyStop: function (e, opt) {
486
// Only get the data from $currentTrigger if it exists
487
if ($currentTrigger) {
488
opt = $currentTrigger.data('contextMenu') || {};
494
handle.keyStop(e, opt);
495
// if keyCode is [38 (up)] or [9 (tab) with shift]
497
if (e.keyCode === 9 && e.shiftKey) {
499
opt.$selected && opt.$selected.find('input, textarea, select').blur();
500
opt.$menu.trigger('prevcommand');
502
} else if (e.keyCode === 38 && opt.$selected.find('input, textarea, select').prop('type') === 'checkbox') {
503
// checkboxes don't capture this key
507
} else if (e.keyCode !== 9 || e.shiftKey) {
508
opt.$menu.trigger('prevcommand');
512
// case 9: // tab - reached through omitted break;
514
handle.keyStop(e, opt);
516
if (e.keyCode === 9) {
518
opt.$selected && opt.$selected.find('input, textarea, select').blur();
519
opt.$menu.trigger('nextcommand');
521
} else if (e.keyCode === 40 && opt.$selected.find('input, textarea, select').prop('type') === 'checkbox') {
522
// checkboxes don't capture this key
527
opt.$menu.trigger('nextcommand');
533
handle.keyStop(e, opt);
534
if (opt.isInput || !opt.$selected || !opt.$selected.length) {
538
if (!opt.$selected.parent().hasClass('context-menu-root')) {
539
var $parent = opt.$selected.parent().parent();
540
opt.$selected.trigger('contextmenu:blur');
541
opt.$selected = $parent;
547
handle.keyStop(e, opt);
548
if (opt.isInput || !opt.$selected || !opt.$selected.length) {
552
var itemdata = opt.$selected.data('contextMenu') || {};
553
if (itemdata.$menu && opt.$selected.hasClass('context-menu-submenu')) {
554
opt.$selected = null;
555
itemdata.$selected = null;
556
itemdata.$menu.trigger('nextcommand');
563
if (opt.$selected && opt.$selected.find('input, textarea, select').length) {
566
(opt.$selected && opt.$selected.parent() || opt.$menu)
567
.children(':not(.' + opt.classNames.disabled + ', .' + opt.classNames.notSelectable + ')')[e.keyCode === 36 ? 'first' : 'last']()
568
.trigger('contextmenu:focus');
575
handle.keyStop(e, opt);
577
if (opt.$selected && !opt.$selected.is('textarea, select')) {
583
if (typeof opt.$selected !== 'undefined') {
584
opt.$selected.trigger('mouseup');
590
case 34: // page down
591
// prevent browser from scrolling down while menu is visible
592
handle.keyStop(e, opt);
596
handle.keyStop(e, opt);
597
opt.$menu.trigger('contextmenu:hide');
601
var k = (String.fromCharCode(e.keyCode)).toUpperCase();
602
if (opt.accesskeys && opt.accesskeys[k]) {
603
// according to the specs accesskeys must be invoked immediately
604
opt.accesskeys[k].$node.trigger(opt.accesskeys[k].$menu ? 'contextmenu:focus' : 'mouseup');
609
// pass event to selected item,
610
// stop propagation to avoid endless recursion
612
if (typeof opt.$selected !== 'undefined') {
613
opt.$selected.trigger(e);
616
// select previous possible command in menu
617
prevItem: function (e) {
619
var opt = $(this).data('contextMenu') || {};
620
var root = $(this).data('contextMenuRoot') || {};
622
// obtain currently selected menu
624
var $s = opt.$selected;
625
opt = opt.$selected.parent().data('contextMenu') || {};
629
var $children = opt.$menu.children(),
630
$prev = !opt.$selected || !opt.$selected.prev().length ? $children.last() : opt.$selected.prev(),
634
while ($prev.hasClass(root.classNames.disabled) || $prev.hasClass(root.classNames.notSelectable)) {
635
if ($prev.prev().length) {
636
$prev = $prev.prev();
638
$prev = $children.last();
640
if ($prev.is($round)) {
641
// break endless loop
648
handle.itemMouseleave.call(opt.$selected.get(0), e);
652
handle.itemMouseenter.call($prev.get(0), e);
655
var $input = $prev.find('input, textarea, select');
660
// select next possible command in menu
661
nextItem: function (e) {
663
var opt = $(this).data('contextMenu') || {};
664
var root = $(this).data('contextMenuRoot') || {};
666
// obtain currently selected menu
668
var $s = opt.$selected;
669
opt = opt.$selected.parent().data('contextMenu') || {};
673
var $children = opt.$menu.children(),
674
$next = !opt.$selected || !opt.$selected.next().length ? $children.first() : opt.$selected.next(),
678
while ($next.hasClass(root.classNames.disabled) || $next.hasClass(root.classNames.notSelectable)) {
679
if ($next.next().length) {
680
$next = $next.next();
682
$next = $children.first();
684
if ($next.is($round)) {
685
// break endless loop
692
handle.itemMouseleave.call(opt.$selected.get(0), e);
696
handle.itemMouseenter.call($next.get(0), e);
699
var $input = $next.find('input, textarea, select');
704
// flag that we're inside an input so the key handler can act accordingly
705
focusInput: function () {
706
var $this = $(this).closest('.context-menu-item'),
708
opt = data.contextMenu,
709
root = data.contextMenuRoot;
711
root.$selected = opt.$selected = $this;
712
root.isInput = opt.isInput = true;
714
// flag that we're inside an input so the key handler can act accordingly
715
blurInput: function () {
716
var $this = $(this).closest('.context-menu-item'),
718
opt = data.contextMenu,
719
root = data.contextMenuRoot;
721
root.isInput = opt.isInput = false;
724
menuMouseenter: function () {
725
var root = $(this).data().contextMenuRoot;
726
root.hovering = true;
729
menuMouseleave: function (e) {
730
var root = $(this).data().contextMenuRoot;
731
if (root.$layer && root.$layer.is(e.relatedTarget)) {
732
root.hovering = false;
735
// :hover done manually so key handling is possible
736
itemMouseenter: function (e) {
739
opt = data.contextMenu,
740
root = data.contextMenuRoot;
742
root.hovering = true;
744
// abort if we're re-entering
745
if (e && root.$layer && root.$layer.is(e.relatedTarget)) {
747
e.stopImmediatePropagation();
750
// make sure only one item is selected
751
(opt.$menu ? opt : root).$menu
752
.children('.hover').trigger('contextmenu:blur');
754
if ($this.hasClass(root.classNames.disabled) || $this.hasClass(root.classNames.notSelectable)) {
755
opt.$selected = null;
759
$this.trigger('contextmenu:focus');
761
// :hover done manually so key handling is possible
762
itemMouseleave: function (e) {
765
opt = data.contextMenu,
766
root = data.contextMenuRoot;
768
if (root !== opt && root.$layer && root.$layer.is(e.relatedTarget)) {
769
if (typeof root.$selected !== 'undefined') {
770
root.$selected.trigger('contextmenu:blur');
773
e.stopImmediatePropagation();
774
root.$selected = opt.$selected = opt.$node;
778
$this.trigger('contextmenu:blur');
780
// contextMenu item click
781
itemClick: function (e) {
784
opt = data.contextMenu,
785
root = data.contextMenuRoot,
786
key = data.contextMenuKey,
789
// abort if the key is unknown or disabled or is a menu
790
if (!opt.items[key] || $this.is('.' + root.classNames.disabled + ', .context-menu-submenu, .context-menu-separator, .' + root.classNames.notSelectable)) {
795
e.stopImmediatePropagation();
797
if ($.isFunction(root.callbacks[key]) && Object.prototype.hasOwnProperty.call(root.callbacks, key)) {
798
// item-specific callback
799
callback = root.callbacks[key];
800
} else if ($.isFunction(root.callback)) {
802
callback = root.callback;
804
// no callback, no action
808
// hide menu if callback doesn't stop that
809
if (callback.call(root.$trigger, key, root) !== false) {
810
root.$menu.trigger('contextmenu:hide');
811
} else if (root.$menu.parent().length) {
812
op.update.call(root.$trigger, root);
815
// ignore click events on input elements
816
inputClick: function (e) {
817
e.stopImmediatePropagation();
820
hideMenu: function (e, data) {
821
var root = $(this).data('contextMenuRoot');
822
op.hide.call(root.$trigger, root, data && data.force);
825
focusItem: function (e) {
829
opt = data.contextMenu,
830
root = data.contextMenuRoot;
833
.addClass([root.classNames.hover, root.classNames.visible].join(' '))
835
.removeClass(root.classNames.visible)
836
.filter(root.classNames.hover)
837
.trigger('contextmenu:blur');
840
opt.$selected = root.$selected = $this;
842
// position sub-menu - do after show so dumb $.ui.position can keep up
844
root.positionSubmenu.call(opt.$node, opt.$menu);
848
blurItem: function (e) {
852
opt = data.contextMenu,
853
root = data.contextMenuRoot;
855
if (opt.autoHide) { // for tablets and touch screens this needs to remain
856
$this.removeClass(root.classNames.visible);
858
$this.removeClass(root.classNames.hover);
859
opt.$selected = null;
864
show: function (opt, x, y) {
865
var $trigger = $(this),
868
// hide any open menus
869
$('#context-menu-layer').trigger('mousedown');
871
// backreference for callbacks
872
opt.$trigger = $trigger;
875
if (opt.events.show.call($trigger, opt) === false) {
876
$currentTrigger = null;
880
// create or update context menu
881
op.update.call($trigger, opt);
884
opt.position.call($trigger, opt, x, y);
886
// make sure we're in front
888
css.zIndex = zindex($trigger) + opt.zIndex;
892
op.layer.call(opt.$menu, opt, css.zIndex);
894
// adjust sub-menu zIndexes
895
opt.$menu.find('ul').css('zIndex', css.zIndex + 1);
897
// position and show context menu
898
opt.$menu.css(css)[opt.animation.show](opt.animation.duration, function () {
899
$trigger.trigger('contextmenu:visible');
901
// make options available and set state
903
.data('contextMenu', opt)
904
.addClass('context-menu-active');
906
// register key handler
907
$(document).off('keydown.contextMenu').on('keydown.contextMenu', handle.key);
908
// register autoHide handler
910
// mouse position handler
911
$(document).on('mousemove.contextMenuAutoHide', function (e) {
912
// need to capture the offset on mousemove,
913
// since the page might've been scrolled since activation
914
var pos = $trigger.offset();
915
pos.right = pos.left + $trigger.outerWidth();
916
pos.bottom = pos.top + $trigger.outerHeight();
918
if (opt.$layer && !opt.hovering && (!(e.pageX >= pos.left && e.pageX <= pos.right) || !(e.pageY >= pos.top && e.pageY <= pos.bottom))) {
919
// if mouse in menu...
920
opt.$menu.trigger('contextmenu:hide');
925
hide: function (opt, force) {
926
var $trigger = $(this);
928
opt = $trigger.data('contextMenu') || {};
932
if (!force && opt.events && opt.events.hide.call($trigger, opt) === false) {
936
// remove options and revert state
938
.removeData('contextMenu')
939
.removeClass('context-menu-active');
942
// keep layer for a bit so the contextmenu event can be aborted properly by opera
943
setTimeout((function ($layer) {
957
$currentTrigger = null;
959
opt.$menu.find('.' + opt.classNames.hover).trigger('contextmenu:blur');
960
opt.$selected = null;
961
// unregister key and mouse handlers
962
// $(document).off('.contextMenuAutoHide keydown.contextMenu'); // http://bugs.jquery.com/ticket/10705
963
$(document).off('.contextMenuAutoHide').off('keydown.contextMenu');
965
opt.$menu && opt.$menu[opt.animation.hide](opt.animation.duration, function () {
966
// tear down dynamically built menu after animation is completed.
969
$.each(opt, function (key) {
978
opt[key] = undefined;
988
setTimeout(function () {
989
$trigger.trigger('contextmenu:hidden');
993
create: function (opt, root) {
994
if (root === undefined) {
997
// create contextMenu
998
opt.$menu = $('<ul class="context-menu-list"></ul>').addClass(opt.className || '').data({
1000
'contextMenuRoot': root
1003
$.each(['callbacks', 'commands', 'inputs'], function (i, k) {
1010
root.accesskeys || (root.accesskeys = {});
1012
// create contextMenu items
1013
$.each(opt.items, function (key, item) {
1014
var $t = $('<li class="context-menu-item"></li>').addClass(item.className || ''),
1018
// iOS needs to see a click-event bound to an element to actually
1019
// have the TouchEvents infrastructure trigger the click event
1020
$t.on('click', $.noop);
1022
// Make old school string seperator a real item so checks wont be
1024
if (typeof item === 'string') {
1025
item = { type : 'cm_seperator' };
1028
item.$node = $t.data({
1030
'contextMenuRoot': root,
1031
'contextMenuKey': key
1034
// register accesskey
1035
// NOTE: the accesskey attribute should be applicable to any element, but Safari5 and Chrome13 still can't do that
1036
if (typeof item.accesskey !== 'undefined') {
1037
var aks = splitAccesskey(item.accesskey);
1038
for (var i = 0, ak; ak = aks[i]; i++) {
1039
if (!root.accesskeys[ak]) {
1040
root.accesskeys[ak] = item;
1041
item._name = item.name.replace(new RegExp('(' + ak + ')', 'i'), '<span class="context-menu-accesskey">$1</span>');
1047
if (item.type && types[item.type]) {
1048
// run custom type handler
1049
types[item.type].call($t, item, opt, root);
1050
// register commands
1051
$.each([opt, root], function (i, k) {
1052
k.commands[key] = item;
1053
if ($.isFunction(item.callback)) {
1054
k.callbacks[key] = item.callback;
1058
// add label for input
1059
if (item.type === 'cm_seperator') {
1060
$t.addClass('context-menu-separator ' + opt.classNames.notSelectable);
1061
} else if (item.type === 'html') {
1062
$t.addClass('context-menu-html ' + opt.classNames.notSelectable);
1063
} else if (item.type) {
1064
$label = $('<label></label>').appendTo($t);
1065
$('<span></span>').html(item._name || item.name).appendTo($label);
1066
$t.addClass('context-menu-input');
1067
opt.hasTypes = true;
1068
$.each([opt, root], function (i, k) {
1069
k.commands[key] = item;
1070
k.inputs[key] = item;
1072
} else if (item.items) {
1076
switch (item.type) {
1081
$input = $('<input type="text" value="1" name="" value="">')
1082
.attr('name', 'context-menu-input-' + key)
1083
.val(item.value || '')
1088
$input = $('<textarea name=""></textarea>')
1089
.attr('name', 'context-menu-input-' + key)
1090
.val(item.value || '')
1094
$input.height(item.height);
1099
$input = $('<input type="checkbox" value="1" name="" value="">')
1100
.attr('name', 'context-menu-input-' + key)
1101
.val(item.value || '')
1102
.prop('checked', !!item.selected)
1107
$input = $('<input type="radio" value="1" name="" value="">')
1108
.attr('name', 'context-menu-input-' + item.radio)
1109
.val(item.value || '')
1110
.prop('checked', !!item.selected)
1115
$input = $('<select name="">')
1116
.attr('name', 'context-menu-input-' + key)
1119
$.each(item.options, function (value, text) {
1120
$('<option></option>').val(value).text(text).appendTo($input);
1122
$input.val(item.selected);
1127
$('<span></span>').html(item._name || item.name).appendTo($t);
1128
item.appendTo = item.$node;
1129
op.create(item, root);
1130
$t.data('contextMenu', item).addClass('context-menu-submenu');
1131
item.callback = null;
1135
$(item.html).appendTo($t);
1139
$.each([opt, root], function (i, k) {
1140
k.commands[key] = item;
1141
if ($.isFunction(item.callback)) {
1142
k.callbacks[key] = item.callback;
1145
$('<span></span>').html(item._name || item.name || '').appendTo($t);
1149
// disable key listener in <input>
1150
if (item.type && item.type !== 'sub' && item.type !== 'html' && item.type !== 'cm_seperator') {
1152
.on('focus', handle.focusInput)
1153
.on('blur', handle.blurInput);
1156
$input.on(item.events, opt);
1162
if ($.isFunction(item.icon)) {
1163
item._icon = item.icon.call(this, $t, key, item);
1165
item._icon = opt.classNames.icon + ' ' + opt.classNames.icon + '-' + item.icon;
1168
$t.addClass(item._icon);
1172
// cache contained elements
1173
item.$input = $input;
1174
item.$label = $label;
1176
// attach item to menu
1177
$t.appendTo(opt.$menu);
1179
// Disable text selection
1180
if (!opt.hasTypes && $.support.eventSelectstart) {
1181
// browsers support user-select: none,
1182
// IE has a special event for text-selection
1183
// browsers supporting neither will not be preventing text-selection
1184
$t.on('selectstart.disableTextSelect', handle.abortevent);
1187
// attach contextMenu to <body> (to bypass any possible overflow:hidden issues on parents of the trigger element)
1189
opt.$menu.css('display', 'none').addClass('context-menu-root');
1191
opt.$menu.appendTo(opt.appendTo || document.body);
1193
resize: function ($menu, nested) {
1194
// determine widths of submenus, as CSS won't grow them automatically
1195
// position:absolute within position:absolute; min-width:100; max-width:200; results in width: 100;
1196
// kinda sucks hard...
1198
// determine width of absolutely positioned element
1199
$menu.css({position: 'absolute', display: 'block'});
1200
// don't apply yet, because that would break nested elements' widths
1201
$menu.data('width', Math.ceil($menu.width()));
1202
// reset styles so they allow nested elements to grow/shrink naturally
1206
maxWidth: '100000px'
1208
// identify width of nested menus
1209
$menu.find('> li > ul').each(function () {
1210
op.resize($(this), true);
1212
// reset and apply changes in the end because nested
1213
// elements' widths wouldn't be calculatable otherwise
1215
$menu.find('ul').addBack().css({
1220
}).width(function () {
1221
return $(this).data('width');
1225
update: function (opt, root) {
1226
var $trigger = this;
1227
if (root === undefined) {
1229
op.resize(opt.$menu);
1231
// re-check disabled for each item
1232
opt.$menu.children().each(function () {
1233
var $item = $(this),
1234
key = $item.data('contextMenuKey'),
1235
item = opt.items[key],
1236
disabled = ($.isFunction(item.disabled) && item.disabled.call($trigger, key, root)) || item.disabled === true,
1238
if ($.isFunction(item.visible)) {
1239
visible = item.visible.call($trigger, key, root);
1240
} else if (typeof item.visible !== 'undefined') {
1241
visible = item.visible === true;
1245
$item[visible ? 'show' : 'hide']();
1247
// dis- / enable item
1248
$item[disabled ? 'addClass' : 'removeClass'](root.classNames.disabled);
1250
if ($.isFunction(item.icon)) {
1251
$item.removeClass(item._icon);
1252
item._icon = item.icon.call(this, $trigger, key, item);
1253
$item.addClass(item._icon);
1257
// dis- / enable input elements
1258
$item.find('input, select, textarea').prop('disabled', disabled);
1260
// update input states
1261
switch (item.type) {
1264
item.$input.val(item.value || '');
1269
item.$input.val(item.value || '').prop('checked', !!item.selected);
1273
item.$input.val(item.selected || '');
1280
op.update.call($trigger, item, root);
1284
layer: function (opt, zIndex) {
1285
// add transparent layer for click area
1286
// filter and background for Internet Explorer, Issue #23
1287
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>')
1288
.css({height: $win.height(), width: $win.width(), display: 'block'})
1289
.data('contextMenuRoot', opt)
1291
.on('contextmenu', handle.abortevent)
1292
.on('mousedown', handle.layerClick);
1294
// IE6 doesn't know position:fixed;
1295
if (document.body.style.maxWidth === undefined) { // IE6 doesn't support maxWidth
1297
'position': 'absolute',
1298
'height': $(document).height()
1306
// split accesskey according to http://www.whatwg.org/specs/web-apps/current-work/multipage/editing.html#assigned-access-key
1307
function splitAccesskey(val) {
1308
var t = val.split(/\s+/),
1311
for (var i = 0, k; k = t[i]; i++) {
1312
k = k.charAt(0).toUpperCase(); // first character only
1313
// theoretically non-accessible characters should be ignored, but different systems, different keyboard layouts, ... screw it.
1314
// a map to look up already used access keys would be nice
1321
// handle contextMenu triggers
1322
$.fn.contextMenu = function (operation) {
1323
var $t = this, $o = operation;
1324
if (this.length > 0) { // this is not a build on demand menu
1325
if (operation === undefined) {
1326
this.first().trigger('contextmenu');
1327
} else if (operation.x !== undefined && operation.y !== undefined) {
1328
this.first().trigger($.Event('contextmenu', {pageX: operation.x, pageY: operation.y}));
1329
} else if (operation === 'hide') {
1330
var $menu = this.first().data('contextMenu') ? this.first().data('contextMenu').$menu : null;
1331
$menu && $menu.trigger('contextmenu:hide');
1332
} else if (operation === 'destroy') {
1333
$.contextMenu('destroy', {context: this});
1334
} else if ($.isPlainObject(operation)) {
1335
operation.context = this;
1336
$.contextMenu('create', operation);
1337
} else if (operation) {
1338
this.removeClass('context-menu-disabled');
1339
} else if (!operation) {
1340
this.addClass('context-menu-disabled');
1343
$.each(menus, function () {
1344
if (this.selector === $t.selector) {
1347
$.extend($o.data, {trigger: 'demand'});
1351
handle.contextmenu.call($o.target, $o);
1357
// manage contextMenu instances
1358
$.contextMenu = function (operation, options) {
1359
if (typeof operation !== 'string') {
1360
options = operation;
1361
operation = 'create';
1364
if (typeof options === 'string') {
1365
options = {selector: options};
1366
} else if (options === undefined) {
1370
// merge with default options
1371
var o = $.extend(true, {}, defaults, options || {});
1372
var $document = $(document);
1373
var $context = $document;
1374
var _hasContext = false;
1376
if (!o.context || !o.context.length) {
1377
o.context = document;
1379
// you never know what they throw at you...
1380
$context = $(o.context).first();
1381
o.context = $context.get(0);
1382
_hasContext = o.context !== document;
1385
switch (operation) {
1387
// no selector no joy
1389
throw new Error('No selector specified');
1391
// make sure internal classes are not bound to
1392
if (o.selector.match(/.context-menu-(list|item|input)($|\s)/)) {
1393
throw new Error('Cannot bind to selector "' + o.selector + '" as it contains a reserved className');
1395
if (!o.build && (!o.items || $.isEmptyObject(o.items))) {
1396
throw new Error('No Items specified');
1399
o.ns = '.contextMenu' + counter;
1401
namespaces[o.selector] = o.ns;
1405
// default to right click
1407
o.trigger = 'right';
1411
// make sure item click is registered first
1414
'contextmenu:hide.contextMenu': handle.hideMenu,
1415
'prevcommand.contextMenu': handle.prevItem,
1416
'nextcommand.contextMenu': handle.nextItem,
1417
'contextmenu.contextMenu': handle.abortevent,
1418
'mouseenter.contextMenu': handle.menuMouseenter,
1419
'mouseleave.contextMenu': handle.menuMouseleave
1420
}, '.context-menu-list')
1421
.on('mouseup.contextMenu', '.context-menu-input', handle.inputClick)
1423
'mouseup.contextMenu': handle.itemClick,
1424
'contextmenu:focus.contextMenu': handle.focusItem,
1425
'contextmenu:blur.contextMenu': handle.blurItem,
1426
'contextmenu.contextMenu': handle.abortevent,
1427
'mouseenter.contextMenu': handle.itemMouseenter,
1428
'mouseleave.contextMenu': handle.itemMouseleave
1429
}, '.context-menu-item');
1434
// engage native contextmenu event
1436
.on('contextmenu' + o.ns, o.selector, o, handle.contextmenu);
1439
// add remove hook, just in case
1440
$context.on('remove' + o.ns, function () {
1441
$(this).contextMenu('destroy');
1445
switch (o.trigger) {
1448
.on('mouseenter' + o.ns, o.selector, o, handle.mouseenter)
1449
.on('mouseleave' + o.ns, o.selector, o, handle.mouseleave);
1453
$context.on('click' + o.ns, o.selector, o, handle.click);
1457
// http://www.quirksmode.org/dom/events/contextmenu.html
1459
.on('mousedown' + o.ns, o.selector, o, handle.mousedown)
1460
.on('mouseup' + o.ns, o.selector, o, handle.mouseup);
1474
// get proper options
1475
var context = o.context;
1476
$.each(menus, function (ns, o) {
1477
if (o.context !== context) {
1481
$visibleMenu = $('.context-menu-list').filter(':visible');
1482
if ($visibleMenu.length && $visibleMenu.data().contextMenuRoot.$trigger.is($(o.context).find(o.selector))) {
1483
$visibleMenu.trigger('contextmenu:hide', {force: true});
1487
if (menus[o.ns].$menu) {
1488
menus[o.ns].$menu.remove();
1496
$(o.context).off(o.ns);
1500
} else if (!o.selector) {
1501
$document.off('.contextMenu .contextMenuAutoHide');
1502
$.each(menus, function (ns, o) {
1503
$(o.context).off(o.ns);
1509
initialized = false;
1511
$('#context-menu-layer, .context-menu-list').remove();
1512
} else if (namespaces[o.selector]) {
1513
$visibleMenu = $('.context-menu-list').filter(':visible');
1514
if ($visibleMenu.length && $visibleMenu.data().contextMenuRoot.$trigger.is(o.selector)) {
1515
$visibleMenu.trigger('contextmenu:hide', {force: true});
1519
if (menus[namespaces[o.selector]].$menu) {
1520
menus[namespaces[o.selector]].$menu.remove();
1523
delete menus[namespaces[o.selector]];
1525
menus[namespaces[o.selector]] = null;
1528
$document.off(namespaces[o.selector]);
1533
// if <command> or <menuitem> are not handled by the browser,
1534
// or options was a bool true,
1535
// initialize $.contextMenu for them
1536
if ((!$.support.htmlCommand && !$.support.htmlMenuitem) || (typeof options === 'boolean' && options)) {
1537
$('menu[type="context"]').each(function () {
1540
selector: '[contextmenu=' + this.id + ']',
1541
items: $.contextMenu.fromMenu(this)
1544
}).css('display', 'none');
1549
throw new Error('Unknown operation "' + operation + '"');
1555
// import values into <input> commands
1556
$.contextMenu.setInputValues = function (opt, data) {
1557
if (data === undefined) {
1561
$.each(opt.inputs, function (key, item) {
1562
switch (item.type) {
1565
item.value = data[key] || '';
1569
item.selected = data[key] ? true : false;
1573
item.selected = (data[item.radio] || '') === item.value;
1577
item.selected = data[key] || '';
1583
// export values from <input> commands
1584
$.contextMenu.getInputValues = function (opt, data) {
1585
if (data === undefined) {
1589
$.each(opt.inputs, function (key, item) {
1590
switch (item.type) {
1594
data[key] = item.$input.val();
1598
data[key] = item.$input.prop('checked');
1602
if (item.$input.prop('checked')) {
1603
data[item.radio] = item.value;
1612
// find <label for="xyz">
1613
function inputLabel(node) {
1614
return (node.id && $('label[for="' + node.id + '"]').val()) || node.name;
1617
// convert <menu> to items object
1618
function menuChildren(items, $children, counter) {
1623
$children.each(function () {
1624
var $node = $(this),
1626
nodeName = this.nodeName.toLowerCase(),
1630
// extract <label><input>
1631
if (nodeName === 'label' && $node.find('input, textarea, select').length) {
1632
label = $node.text();
1633
$node = $node.children().first();
1634
node = $node.get(0);
1635
nodeName = node.nodeName.toLowerCase();
1639
* <menu> accepts flow-content as children. that means <embed>, <canvas> and such are valid menu items.
1640
* Not being the sadistic kind, $.contextMenu only accepts:
1641
* <command>, <menuitem>, <hr>, <span>, <p> <input [text, radio, checkbox]>, <textarea>, <select> and of course <menu>.
1642
* Everything else will be imported as an html node, which is not interfaced with contextMenu.
1645
// http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#concept-command
1647
// http://www.whatwg.org/specs/web-apps/current-work/multipage/interactive-elements.html#the-menu-element
1649
item = {name: $node.attr('label'), items: {}};
1650
counter = menuChildren(item.items, $node.children(), counter);
1653
// http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#using-the-a-element-to-define-a-command
1655
// http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#using-the-button-element-to-define-a-command
1659
disabled: !!$node.attr('disabled'),
1660
callback: (function () {
1661
return function () {
1668
// http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#using-the-command-element-to-define-a-command
1672
switch ($node.attr('type')) {
1677
name: $node.attr('label'),
1678
disabled: !!$node.attr('disabled'),
1679
icon: $node.attr('icon'),
1680
callback: (function () {
1681
return function () {
1691
disabled: !!$node.attr('disabled'),
1692
name: $node.attr('label'),
1693
selected: !!$node.attr('checked')
1699
disabled: !!$node.attr('disabled'),
1700
name: $node.attr('label'),
1701
radio: $node.attr('radiogroup'),
1702
value: $node.attr('id'),
1703
selected: !!$node.attr('checked')
1717
switch ($node.attr('type')) {
1721
name: label || inputLabel(node),
1722
disabled: !!$node.attr('disabled'),
1730
name: label || inputLabel(node),
1731
disabled: !!$node.attr('disabled'),
1732
selected: !!$node.attr('checked')
1739
name: label || inputLabel(node),
1740
disabled: !!$node.attr('disabled'),
1741
radio: !!$node.attr('name'),
1743
selected: !!$node.attr('checked')
1756
name: label || inputLabel(node),
1757
disabled: !!$node.attr('disabled'),
1758
selected: $node.val(),
1761
$node.children().each(function () {
1762
item.options[this.value] = $(this).text();
1769
name: label || inputLabel(node),
1770
disabled: !!$node.attr('disabled'),
1779
item = {type: 'html', html: $node.clone(true)};
1785
items['key' + counter] = item;
1792
// convert html5 menu
1793
$.contextMenu.fromMenu = function (element) {
1794
var $this = $(element),
1797
menuChildren(items, $this.children());
1802
// make defaults accessible
1803
$.contextMenu.defaults = defaults;
1804
$.contextMenu.types = types;
1805
// export internal functions - undocumented, for hacking only!
1806
$.contextMenu.handle = handle;
1807
$.contextMenu.op = op;
1808
$.contextMenu.menus = menus;