You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1787 lines
71 KiB
1787 lines
71 KiB
/*! |
|
* jQuery contextMenu v@VERSION - Plugin for simple contextMenu handling |
|
* |
|
* Version: v@VERSION |
|
* |
|
* Authors: Björn Brala (SWIS.nl), Rodney Rehm, Addy Osmani (patches for FF) |
|
* Web: http://swisnl.github.io/jQuery-contextMenu/ |
|
* |
|
* Copyright (c) 2011-@YEAR SWIS BV and contributors |
|
* |
|
* Licensed under |
|
* MIT License http://www.opensource.org/licenses/mit-license |
|
* GPL v3 http://opensource.org/licenses/GPL-3.0 |
|
* |
|
* Date: @DATE |
|
*/ |
|
|
|
|
|
(function (factory) { |
|
if (typeof define === 'function' && define.amd) { |
|
// AMD. Register as anonymous module. |
|
define(['jquery'], factory); |
|
} else if (typeof exports === 'object') { |
|
// Node / CommonJS |
|
factory(require('jquery')); |
|
} else { |
|
// Browser globals. |
|
factory(jQuery); |
|
} |
|
})(function ($) { |
|
|
|
'use strict'; |
|
|
|
// TODO: - |
|
// ARIA stuff: menuitem, menuitemcheckbox und menuitemradio |
|
// create <menu> structure if $.support[htmlCommand || htmlMenuitem] and !opt.disableNative |
|
|
|
// determine html5 compatibility |
|
$.support.htmlMenuitem = ('HTMLMenuItemElement' in window); |
|
$.support.htmlCommand = ('HTMLCommandElement' in window); |
|
$.support.eventSelectstart = ('onselectstart' in document.documentElement); |
|
/* // should the need arise, test for css user-select |
|
$.support.cssUserSelect = (function(){ |
|
var t = false, |
|
e = document.createElement('div'); |
|
|
|
$.each('Moz|Webkit|Khtml|O|ms|Icab|'.split('|'), function(i, prefix) { |
|
var propCC = prefix + (prefix ? 'U' : 'u') + 'serSelect', |
|
prop = (prefix ? ('-' + prefix.toLowerCase() + '-') : '') + 'user-select'; |
|
|
|
e.style.cssText = prop + ': text;'; |
|
if (e.style[propCC] == 'text') { |
|
t = true; |
|
return false; |
|
} |
|
|
|
return true; |
|
}); |
|
|
|
return t; |
|
})(); |
|
*/ |
|
|
|
if (!$.ui || !$.widget) { |
|
// duck punch $.cleanData like jQueryUI does to get that remove event |
|
$.cleanData = (function (orig) { |
|
return function (elems) { |
|
var events, elem, i; |
|
for (i = 0; (elem = elems[i]) != null; i++) { |
|
try { |
|
// Only trigger remove when necessary to save time |
|
events = $._data(elem, 'events'); |
|
if (events && events.remove) { |
|
$(elem).triggerHandler('remove'); |
|
} |
|
|
|
// Http://bugs.jquery.com/ticket/8235 |
|
} catch (e) {} |
|
} |
|
orig(elems); |
|
}; |
|
})($.cleanData); |
|
} |
|
|
|
var // currently active contextMenu trigger |
|
$currentTrigger = null, |
|
// is contextMenu initialized with at least one menu? |
|
initialized = false, |
|
// window handle |
|
$win = $(window), |
|
// number of registered menus |
|
counter = 0, |
|
// mapping selector to namespace |
|
namespaces = {}, |
|
// mapping namespace to options |
|
menus = {}, |
|
// custom command type handlers |
|
types = {}, |
|
// default values |
|
defaults = { |
|
// selector of contextMenu trigger |
|
selector: null, |
|
// where to append the menu to |
|
appendTo: null, |
|
// method to trigger context menu ["right", "left", "hover"] |
|
trigger: 'right', |
|
// hide menu when mouse leaves trigger / menu elements |
|
autoHide: false, |
|
// ms to wait before showing a hover-triggered context menu |
|
delay: 200, |
|
// flag denoting if a second trigger should simply move (true) or rebuild (false) an open menu |
|
// as long as the trigger happened on one of the trigger-element's child nodes |
|
reposition: true, |
|
// determine position to show menu at |
|
determinePosition: function ($menu) { |
|
// position to the lower middle of the trigger element |
|
if ($.ui && $.ui.position) { |
|
// .position() is provided as a jQuery UI utility |
|
// (...and it won't work on hidden elements) |
|
$menu.css('display', 'block').position({ |
|
my: 'center top', |
|
at: 'center bottom', |
|
of: this, |
|
offset: '0 5', |
|
collision: 'fit' |
|
}).css('display', 'none'); |
|
} else { |
|
// determine contextMenu position |
|
var offset = this.offset(); |
|
offset.top += this.outerHeight(); |
|
offset.left += this.outerWidth() / 2 - $menu.outerWidth() / 2; |
|
$menu.css(offset); |
|
} |
|
}, |
|
// position menu |
|
position: function (opt, x, y) { |
|
var offset; |
|
// determine contextMenu position |
|
if (!x && !y) { |
|
opt.determinePosition.call(this, opt.$menu); |
|
return; |
|
} else if (x === 'maintain' && y === 'maintain') { |
|
// x and y must not be changed (after re-show on command click) |
|
offset = opt.$menu.position(); |
|
} else { |
|
// x and y are given (by mouse event) |
|
offset = {top: y, left: x}; |
|
} |
|
|
|
// correct offset if viewport demands it |
|
var bottom = $win.scrollTop() + $win.height(), |
|
right = $win.scrollLeft() + $win.width(), |
|
height = opt.$menu.outerHeight(), |
|
width = opt.$menu.outerWidth(); |
|
|
|
if (offset.top + height > bottom) { |
|
offset.top -= height; |
|
} |
|
|
|
if (offset.top < 0) { |
|
offset.top = 0; |
|
} |
|
|
|
if (offset.left + width > right) { |
|
offset.left -= width; |
|
} |
|
|
|
opt.$menu.css(offset); |
|
}, |
|
// position the sub-menu |
|
positionSubmenu: function ($menu) { |
|
if ($.ui && $.ui.position) { |
|
// .position() is provided as a jQuery UI utility |
|
// (...and it won't work on hidden elements) |
|
$menu.css('display', 'block').position({ |
|
my: 'left top', |
|
at: 'right top', |
|
of: this, |
|
collision: 'flipfit fit' |
|
}).css('display', ''); |
|
} else { |
|
// determine contextMenu position |
|
var offset = { |
|
top: 0, |
|
left: this.outerWidth() |
|
}; |
|
$menu.css(offset); |
|
} |
|
}, |
|
// offset to add to zIndex |
|
zIndex: 1, |
|
// show hide animation settings |
|
animation: { |
|
duration: 50, |
|
show: 'slideDown', |
|
hide: 'slideUp' |
|
}, |
|
// events |
|
events: { |
|
show: $.noop, |
|
hide: $.noop |
|
}, |
|
// default callback |
|
callback: null, |
|
// list of contextMenu items |
|
items: {} |
|
}, |
|
// mouse position for hover activation |
|
hoveract = { |
|
timer: null, |
|
pageX: null, |
|
pageY: null |
|
}, |
|
// determine zIndex |
|
zindex = function ($t) { |
|
var zin = 0, |
|
$tt = $t; |
|
|
|
while (true) { |
|
zin = Math.max(zin, parseInt($tt.css('z-index'), 10) || 0); |
|
$tt = $tt.parent(); |
|
if (!$tt || !$tt.length || 'html body'.indexOf($tt.prop('nodeName').toLowerCase()) > -1) { |
|
break; |
|
} |
|
} |
|
return zin; |
|
}, |
|
// event handlers |
|
handle = { |
|
// abort anything |
|
abortevent: function (e) { |
|
e.preventDefault(); |
|
e.stopImmediatePropagation(); |
|
}, |
|
// contextmenu show dispatcher |
|
contextmenu: function (e) { |
|
var $this = $(this); |
|
|
|
// disable actual context-menu if we are using the right mouse button as the trigger |
|
if (e.data.trigger === 'right') { |
|
e.preventDefault(); |
|
e.stopImmediatePropagation(); |
|
} |
|
|
|
// abort native-triggered events unless we're triggering on right click |
|
if ((e.data.trigger !== 'right' && e.data.trigger !== 'demand') && e.originalEvent) { |
|
return; |
|
} |
|
|
|
// abort event if menu is visible for this trigger |
|
if ($this.hasClass('context-menu-active')) { |
|
return; |
|
} |
|
|
|
if (!$this.hasClass('context-menu-disabled')) { |
|
// theoretically need to fire a show event at <menu> |
|
// http://www.whatwg.org/specs/web-apps/current-work/multipage/interactive-elements.html#context-menus |
|
// var evt = jQuery.Event("show", { data: data, pageX: e.pageX, pageY: e.pageY, relatedTarget: this }); |
|
// e.data.$menu.trigger(evt); |
|
|
|
$currentTrigger = $this; |
|
if (e.data.build) { |
|
var built = e.data.build($currentTrigger, e); |
|
// abort if build() returned false |
|
if (built === false) { |
|
return; |
|
} |
|
|
|
// dynamically build menu on invocation |
|
e.data = $.extend(true, {}, defaults, e.data, built || {}); |
|
|
|
// abort if there are no items to display |
|
if (!e.data.items || $.isEmptyObject(e.data.items)) { |
|
// Note: jQuery captures and ignores errors from event handlers |
|
if (window.console) { |
|
(console.error || console.log).call(console, 'No items specified to show in contextMenu'); |
|
} |
|
|
|
throw new Error('No Items specified'); |
|
} |
|
|
|
// backreference for custom command type creation |
|
e.data.$trigger = $currentTrigger; |
|
|
|
op.create(e.data); |
|
} |
|
var showMenu = false; |
|
for (var item in e.data.items) { |
|
if (e.data.items.hasOwnProperty(item)) { |
|
var visible; |
|
if ($.isFunction(e.data.items[item].visible)) { |
|
visible = e.data.items[item].visible.call($(e.currentTarget), item, e.data); |
|
} else if (typeof item.visible !== 'undefined') { |
|
visible = e.data.items[item].visible === true; |
|
} else { |
|
visible = true; |
|
} |
|
if (visible) { |
|
showMenu = true; |
|
} |
|
} |
|
} |
|
if (showMenu) { |
|
// show menu |
|
op.show.call($this, e.data, e.pageX, e.pageY); |
|
} |
|
} |
|
}, |
|
// contextMenu left-click trigger |
|
click: function (e) { |
|
e.preventDefault(); |
|
e.stopImmediatePropagation(); |
|
$(this).trigger($.Event('contextmenu', {data: e.data, pageX: e.pageX, pageY: e.pageY})); |
|
}, |
|
// contextMenu right-click trigger |
|
mousedown: function (e) { |
|
// register mouse down |
|
var $this = $(this); |
|
|
|
// hide any previous menus |
|
if ($currentTrigger && $currentTrigger.length && !$currentTrigger.is($this)) { |
|
$currentTrigger.data('contextMenu').$menu.trigger('contextmenu:hide'); |
|
} |
|
|
|
// activate on right click |
|
if (e.button === 2) { |
|
$currentTrigger = $this.data('contextMenuActive', true); |
|
} |
|
}, |
|
// contextMenu right-click trigger |
|
mouseup: function (e) { |
|
// show menu |
|
var $this = $(this); |
|
if ($this.data('contextMenuActive') && $currentTrigger && $currentTrigger.length && $currentTrigger.is($this) && !$this.hasClass('context-menu-disabled')) { |
|
e.preventDefault(); |
|
e.stopImmediatePropagation(); |
|
$currentTrigger = $this; |
|
$this.trigger($.Event('contextmenu', {data: e.data, pageX: e.pageX, pageY: e.pageY})); |
|
} |
|
|
|
$this.removeData('contextMenuActive'); |
|
}, |
|
// contextMenu hover trigger |
|
mouseenter: function (e) { |
|
var $this = $(this), |
|
$related = $(e.relatedTarget), |
|
$document = $(document); |
|
|
|
// abort if we're coming from a menu |
|
if ($related.is('.context-menu-list') || $related.closest('.context-menu-list').length) { |
|
return; |
|
} |
|
|
|
// abort if a menu is shown |
|
if ($currentTrigger && $currentTrigger.length) { |
|
return; |
|
} |
|
|
|
hoveract.pageX = e.pageX; |
|
hoveract.pageY = e.pageY; |
|
hoveract.data = e.data; |
|
$document.on('mousemove.contextMenuShow', handle.mousemove); |
|
hoveract.timer = setTimeout(function () { |
|
hoveract.timer = null; |
|
$document.off('mousemove.contextMenuShow'); |
|
$currentTrigger = $this; |
|
$this.trigger($.Event('contextmenu', { |
|
data: hoveract.data, |
|
pageX: hoveract.pageX, |
|
pageY: hoveract.pageY |
|
})); |
|
}, e.data.delay); |
|
}, |
|
// contextMenu hover trigger |
|
mousemove: function (e) { |
|
hoveract.pageX = e.pageX; |
|
hoveract.pageY = e.pageY; |
|
}, |
|
// contextMenu hover trigger |
|
mouseleave: function (e) { |
|
// abort if we're leaving for a menu |
|
var $related = $(e.relatedTarget); |
|
if ($related.is('.context-menu-list') || $related.closest('.context-menu-list').length) { |
|
return; |
|
} |
|
|
|
try { |
|
clearTimeout(hoveract.timer); |
|
} catch (e) { |
|
} |
|
|
|
hoveract.timer = null; |
|
}, |
|
// click on layer to hide contextMenu |
|
layerClick: function (e) { |
|
var $this = $(this), |
|
root = $this.data('contextMenuRoot'), |
|
button = e.button, |
|
x = e.pageX, |
|
y = e.pageY, |
|
target, |
|
offset; |
|
|
|
e.preventDefault(); |
|
e.stopImmediatePropagation(); |
|
|
|
setTimeout(function () { |
|
var $window; |
|
var triggerAction = ((root.trigger === 'left' && button === 0) || (root.trigger === 'right' && button === 2)); |
|
|
|
// find the element that would've been clicked, wasn't the layer in the way |
|
if (document.elementFromPoint) { |
|
root.$layer.hide(); |
|
target = document.elementFromPoint(x - $win.scrollLeft(), y - $win.scrollTop()); |
|
root.$layer.show(); |
|
} |
|
|
|
if (root.reposition && triggerAction) { |
|
if (document.elementFromPoint) { |
|
if (root.$trigger.is(target) || root.$trigger.has(target).length) { |
|
root.position.call(root.$trigger, root, x, y); |
|
return; |
|
} |
|
} else { |
|
offset = root.$trigger.offset(); |
|
$window = $(window); |
|
// while this looks kinda awful, it's the best way to avoid |
|
// unnecessarily calculating any positions |
|
offset.top += $window.scrollTop(); |
|
if (offset.top <= e.pageY) { |
|
offset.left += $window.scrollLeft(); |
|
if (offset.left <= e.pageX) { |
|
offset.bottom = offset.top + root.$trigger.outerHeight(); |
|
if (offset.bottom >= e.pageY) { |
|
offset.right = offset.left + root.$trigger.outerWidth(); |
|
if (offset.right >= e.pageX) { |
|
// reposition |
|
root.position.call(root.$trigger, root, x, y); |
|
return; |
|
} |
|
} |
|
} |
|
} |
|
} |
|
} |
|
|
|
if (target && triggerAction) { |
|
root.$trigger.one('contextmenu:hidden', function () { |
|
$(target).contextMenu({x: x, y: y}); |
|
}); |
|
} |
|
|
|
root.$menu.trigger('contextmenu:hide'); |
|
}, 50); |
|
}, |
|
// key handled :hover |
|
keyStop: function (e, opt) { |
|
if (!opt.isInput) { |
|
e.preventDefault(); |
|
} |
|
|
|
e.stopPropagation(); |
|
}, |
|
key: function (e) { |
|
|
|
var opt = {}; |
|
|
|
// Only get the data from $currentTrigger if it exists |
|
if ($currentTrigger) { |
|
opt = $currentTrigger.data('contextMenu') || {}; |
|
} |
|
|
|
switch (e.keyCode) { |
|
case 9: |
|
case 38: // up |
|
handle.keyStop(e, opt); |
|
// if keyCode is [38 (up)] or [9 (tab) with shift] |
|
if (opt.isInput) { |
|
if (e.keyCode === 9 && e.shiftKey) { |
|
e.preventDefault(); |
|
opt.$selected && opt.$selected.find('input, textarea, select').blur(); |
|
opt.$menu.trigger('prevcommand'); |
|
return; |
|
} else if (e.keyCode === 38 && opt.$selected.find('input, textarea, select').prop('type') === 'checkbox') { |
|
// checkboxes don't capture this key |
|
e.preventDefault(); |
|
return; |
|
} |
|
} else if (e.keyCode !== 9 || e.shiftKey) { |
|
opt.$menu.trigger('prevcommand'); |
|
return; |
|
} |
|
// omitting break; |
|
// case 9: // tab - reached through omitted break; |
|
case 40: // down |
|
handle.keyStop(e, opt); |
|
if (opt.isInput) { |
|
if (e.keyCode === 9) { |
|
e.preventDefault(); |
|
opt.$selected && opt.$selected.find('input, textarea, select').blur(); |
|
opt.$menu.trigger('nextcommand'); |
|
return; |
|
} else if (e.keyCode === 40 && opt.$selected.find('input, textarea, select').prop('type') === 'checkbox') { |
|
// checkboxes don't capture this key |
|
e.preventDefault(); |
|
return; |
|
} |
|
} else { |
|
opt.$menu.trigger('nextcommand'); |
|
return; |
|
} |
|
break; |
|
|
|
case 37: // left |
|
handle.keyStop(e, opt); |
|
if (opt.isInput || !opt.$selected || !opt.$selected.length) { |
|
break; |
|
} |
|
|
|
if (!opt.$selected.parent().hasClass('context-menu-root')) { |
|
var $parent = opt.$selected.parent().parent(); |
|
opt.$selected.trigger('contextmenu:blur'); |
|
opt.$selected = $parent; |
|
return; |
|
} |
|
break; |
|
|
|
case 39: // right |
|
handle.keyStop(e, opt); |
|
if (opt.isInput || !opt.$selected || !opt.$selected.length) { |
|
break; |
|
} |
|
|
|
var itemdata = opt.$selected.data('contextMenu') || {}; |
|
if (itemdata.$menu && opt.$selected.hasClass('context-menu-submenu')) { |
|
opt.$selected = null; |
|
itemdata.$selected = null; |
|
itemdata.$menu.trigger('nextcommand'); |
|
return; |
|
} |
|
break; |
|
|
|
case 35: // end |
|
case 36: // home |
|
if (opt.$selected && opt.$selected.find('input, textarea, select').length) { |
|
return; |
|
} else { |
|
(opt.$selected && opt.$selected.parent() || opt.$menu) |
|
.children(':not(.disabled, .not-selectable)')[e.keyCode === 36 ? 'first' : 'last']() |
|
.trigger('contextmenu:focus'); |
|
e.preventDefault(); |
|
return; |
|
} |
|
break; |
|
|
|
case 13: // enter |
|
handle.keyStop(e, opt); |
|
if (opt.isInput) { |
|
if (opt.$selected && !opt.$selected.is('textarea, select')) { |
|
e.preventDefault(); |
|
return; |
|
} |
|
break; |
|
} |
|
if (typeof opt.$selected !== 'undefined') { |
|
opt.$selected.trigger('mouseup'); |
|
} |
|
return; |
|
|
|
case 32: // space |
|
case 33: // page up |
|
case 34: // page down |
|
// prevent browser from scrolling down while menu is visible |
|
handle.keyStop(e, opt); |
|
return; |
|
|
|
case 27: // esc |
|
handle.keyStop(e, opt); |
|
opt.$menu.trigger('contextmenu:hide'); |
|
return; |
|
|
|
default: // 0-9, a-z |
|
var k = (String.fromCharCode(e.keyCode)).toUpperCase(); |
|
if (opt.accesskeys && opt.accesskeys[k]) { |
|
// according to the specs accesskeys must be invoked immediately |
|
opt.accesskeys[k].$node.trigger(opt.accesskeys[k].$menu ? 'contextmenu:focus' : 'mouseup'); |
|
return; |
|
} |
|
break; |
|
} |
|
// pass event to selected item, |
|
// stop propagation to avoid endless recursion |
|
e.stopPropagation(); |
|
if (typeof opt.$selected !== 'undefined') { |
|
opt.$selected.trigger(e); |
|
} |
|
}, |
|
// select previous possible command in menu |
|
prevItem: function (e) { |
|
e.stopPropagation(); |
|
var opt = $(this).data('contextMenu') || {}; |
|
|
|
// obtain currently selected menu |
|
if (opt.$selected) { |
|
var $s = opt.$selected; |
|
opt = opt.$selected.parent().data('contextMenu') || {}; |
|
opt.$selected = $s; |
|
} |
|
|
|
var $children = opt.$menu.children(), |
|
$prev = !opt.$selected || !opt.$selected.prev().length ? $children.last() : opt.$selected.prev(), |
|
$round = $prev; |
|
|
|
// skip disabled |
|
while ($prev.hasClass('disabled') || $prev.hasClass('not-selectable')) { |
|
if ($prev.prev().length) { |
|
$prev = $prev.prev(); |
|
} else { |
|
$prev = $children.last(); |
|
} |
|
if ($prev.is($round)) { |
|
// break endless loop |
|
return; |
|
} |
|
} |
|
|
|
// leave current |
|
if (opt.$selected) { |
|
handle.itemMouseleave.call(opt.$selected.get(0), e); |
|
} |
|
|
|
// activate next |
|
handle.itemMouseenter.call($prev.get(0), e); |
|
|
|
// focus input |
|
var $input = $prev.find('input, textarea, select'); |
|
if ($input.length) { |
|
$input.focus(); |
|
} |
|
}, |
|
// select next possible command in menu |
|
nextItem: function (e) { |
|
e.stopPropagation(); |
|
var opt = $(this).data('contextMenu') || {}; |
|
|
|
// obtain currently selected menu |
|
if (opt.$selected) { |
|
var $s = opt.$selected; |
|
opt = opt.$selected.parent().data('contextMenu') || {}; |
|
opt.$selected = $s; |
|
} |
|
|
|
var $children = opt.$menu.children(), |
|
$next = !opt.$selected || !opt.$selected.next().length ? $children.first() : opt.$selected.next(), |
|
$round = $next; |
|
|
|
// skip disabled |
|
while ($next.hasClass('disabled') || $next.hasClass('not-selectable')) { |
|
if ($next.next().length) { |
|
$next = $next.next(); |
|
} else { |
|
$next = $children.first(); |
|
} |
|
if ($next.is($round)) { |
|
// break endless loop |
|
return; |
|
} |
|
} |
|
|
|
// leave current |
|
if (opt.$selected) { |
|
handle.itemMouseleave.call(opt.$selected.get(0), e); |
|
} |
|
|
|
// activate next |
|
handle.itemMouseenter.call($next.get(0), e); |
|
|
|
// focus input |
|
var $input = $next.find('input, textarea, select'); |
|
if ($input.length) { |
|
$input.focus(); |
|
} |
|
}, |
|
// flag that we're inside an input so the key handler can act accordingly |
|
focusInput: function () { |
|
var $this = $(this).closest('.context-menu-item'), |
|
data = $this.data(), |
|
opt = data.contextMenu, |
|
root = data.contextMenuRoot; |
|
|
|
root.$selected = opt.$selected = $this; |
|
root.isInput = opt.isInput = true; |
|
}, |
|
// flag that we're inside an input so the key handler can act accordingly |
|
blurInput: function () { |
|
var $this = $(this).closest('.context-menu-item'), |
|
data = $this.data(), |
|
opt = data.contextMenu, |
|
root = data.contextMenuRoot; |
|
|
|
root.isInput = opt.isInput = false; |
|
}, |
|
// :hover on menu |
|
menuMouseenter: function () { |
|
var root = $(this).data().contextMenuRoot; |
|
root.hovering = true; |
|
}, |
|
// :hover on menu |
|
menuMouseleave: function (e) { |
|
var root = $(this).data().contextMenuRoot; |
|
if (root.$layer && root.$layer.is(e.relatedTarget)) { |
|
root.hovering = false; |
|
} |
|
}, |
|
// :hover done manually so key handling is possible |
|
itemMouseenter: function (e) { |
|
var $this = $(this), |
|
data = $this.data(), |
|
opt = data.contextMenu, |
|
root = data.contextMenuRoot; |
|
|
|
root.hovering = true; |
|
|
|
// abort if we're re-entering |
|
if (e && root.$layer && root.$layer.is(e.relatedTarget)) { |
|
e.preventDefault(); |
|
e.stopImmediatePropagation(); |
|
} |
|
|
|
// make sure only one item is selected |
|
(opt.$menu ? opt : root).$menu |
|
.children('.hover').trigger('contextmenu:blur'); |
|
|
|
if ($this.hasClass('disabled') || $this.hasClass('not-selectable')) { |
|
opt.$selected = null; |
|
return; |
|
} |
|
|
|
$this.trigger('contextmenu:focus'); |
|
}, |
|
// :hover done manually so key handling is possible |
|
itemMouseleave: function (e) { |
|
var $this = $(this), |
|
data = $this.data(), |
|
opt = data.contextMenu, |
|
root = data.contextMenuRoot; |
|
|
|
if (root !== opt && root.$layer && root.$layer.is(e.relatedTarget)) { |
|
if (typeof root.$selected !== 'undefined') { |
|
root.$selected.trigger('contextmenu:blur'); |
|
} |
|
e.preventDefault(); |
|
e.stopImmediatePropagation(); |
|
root.$selected = opt.$selected = opt.$node; |
|
return; |
|
} |
|
|
|
$this.trigger('contextmenu:blur'); |
|
}, |
|
// contextMenu item click |
|
itemClick: function (e) { |
|
var $this = $(this), |
|
data = $this.data(), |
|
opt = data.contextMenu, |
|
root = data.contextMenuRoot, |
|
key = data.contextMenuKey, |
|
callback; |
|
|
|
// abort if the key is unknown or disabled or is a menu |
|
if (!opt.items[key] || $this.is('.disabled, .context-menu-submenu, .context-menu-separator, .not-selectable')) { |
|
return; |
|
} |
|
|
|
e.preventDefault(); |
|
e.stopImmediatePropagation(); |
|
|
|
if ($.isFunction(root.callbacks[key]) && Object.prototype.hasOwnProperty.call(root.callbacks, key)) { |
|
// item-specific callback |
|
callback = root.callbacks[key]; |
|
} else if ($.isFunction(root.callback)) { |
|
// default callback |
|
callback = root.callback; |
|
} else { |
|
// no callback, no action |
|
return; |
|
} |
|
|
|
// hide menu if callback doesn't stop that |
|
if (callback.call(root.$trigger, key, root) !== false) { |
|
root.$menu.trigger('contextmenu:hide'); |
|
} else if (root.$menu.parent().length) { |
|
op.update.call(root.$trigger, root); |
|
} |
|
}, |
|
// ignore click events on input elements |
|
inputClick: function (e) { |
|
e.stopImmediatePropagation(); |
|
}, |
|
// hide <menu> |
|
hideMenu: function (e, data) { |
|
var root = $(this).data('contextMenuRoot'); |
|
op.hide.call(root.$trigger, root, data && data.force); |
|
}, |
|
// focus <command> |
|
focusItem: function (e) { |
|
e.stopPropagation(); |
|
var $this = $(this), |
|
data = $this.data(), |
|
opt = data.contextMenu, |
|
root = data.contextMenuRoot; |
|
|
|
$this |
|
.addClass('hover visible') |
|
.siblings() |
|
.removeClass('visible') |
|
.filter('.hover') |
|
.trigger('contextmenu:blur'); |
|
|
|
// remember selected |
|
opt.$selected = root.$selected = $this; |
|
|
|
// position sub-menu - do after show so dumb $.ui.position can keep up |
|
if (opt.$node) { |
|
root.positionSubmenu.call(opt.$node, opt.$menu); |
|
} |
|
}, |
|
// blur <command> |
|
blurItem: function (e) { |
|
e.stopPropagation(); |
|
var $this = $(this), |
|
data = $this.data(), |
|
opt = data.contextMenu; |
|
|
|
if (opt.autoHide) { // for tablets and touch screens this needs to remain |
|
$this.removeClass('visible'); |
|
} |
|
$this.removeClass('hover'); |
|
opt.$selected = null; |
|
} |
|
}, |
|
// operations |
|
op = { |
|
show: function (opt, x, y) { |
|
var $trigger = $(this), |
|
css = {}; |
|
|
|
// hide any open menus |
|
$('#context-menu-layer').trigger('mousedown'); |
|
|
|
// backreference for callbacks |
|
opt.$trigger = $trigger; |
|
|
|
// show event |
|
if (opt.events.show.call($trigger, opt) === false) { |
|
$currentTrigger = null; |
|
return; |
|
} |
|
|
|
// create or update context menu |
|
op.update.call($trigger, opt); |
|
|
|
// position menu |
|
opt.position.call($trigger, opt, x, y); |
|
|
|
// make sure we're in front |
|
if (opt.zIndex) { |
|
css.zIndex = zindex($trigger) + opt.zIndex; |
|
} |
|
|
|
// add layer |
|
op.layer.call(opt.$menu, opt, css.zIndex); |
|
|
|
// adjust sub-menu zIndexes |
|
opt.$menu.find('ul').css('zIndex', css.zIndex + 1); |
|
|
|
// position and show context menu |
|
opt.$menu.css(css)[opt.animation.show](opt.animation.duration, function () { |
|
$trigger.trigger('contextmenu:visible'); |
|
}); |
|
// make options available and set state |
|
$trigger |
|
.data('contextMenu', opt) |
|
.addClass('context-menu-active'); |
|
|
|
// register key handler |
|
$(document).off('keydown.contextMenu').on('keydown.contextMenu', handle.key); |
|
// register autoHide handler |
|
if (opt.autoHide) { |
|
// mouse position handler |
|
$(document).on('mousemove.contextMenuAutoHide', function (e) { |
|
// need to capture the offset on mousemove, |
|
// since the page might've been scrolled since activation |
|
var pos = $trigger.offset(); |
|
pos.right = pos.left + $trigger.outerWidth(); |
|
pos.bottom = pos.top + $trigger.outerHeight(); |
|
|
|
if (opt.$layer && !opt.hovering && (!(e.pageX >= pos.left && e.pageX <= pos.right) || !(e.pageY >= pos.top && e.pageY <= pos.bottom))) { |
|
// if mouse in menu... |
|
opt.$menu.trigger('contextmenu:hide'); |
|
} |
|
}); |
|
} |
|
}, |
|
hide: function (opt, force) { |
|
var $trigger = $(this); |
|
if (!opt) { |
|
opt = $trigger.data('contextMenu') || {}; |
|
} |
|
|
|
// hide event |
|
if (!force && opt.events && opt.events.hide.call($trigger, opt) === false) { |
|
return; |
|
} |
|
|
|
// remove options and revert state |
|
$trigger |
|
.removeData('contextMenu') |
|
.removeClass('context-menu-active'); |
|
|
|
if (opt.$layer) { |
|
// keep layer for a bit so the contextmenu event can be aborted properly by opera |
|
setTimeout((function ($layer) { |
|
return function () { |
|
$layer.remove(); |
|
}; |
|
})(opt.$layer), 10); |
|
|
|
try { |
|
delete opt.$layer; |
|
} catch (e) { |
|
opt.$layer = null; |
|
} |
|
} |
|
|
|
// remove handle |
|
$currentTrigger = null; |
|
// remove selected |
|
opt.$menu.find('.hover').trigger('contextmenu:blur'); |
|
opt.$selected = null; |
|
// unregister key and mouse handlers |
|
// $(document).off('.contextMenuAutoHide keydown.contextMenu'); // http://bugs.jquery.com/ticket/10705 |
|
$(document).off('.contextMenuAutoHide').off('keydown.contextMenu'); |
|
// hide menu |
|
opt.$menu && opt.$menu[opt.animation.hide](opt.animation.duration, function () { |
|
// tear down dynamically built menu after animation is completed. |
|
if (opt.build) { |
|
opt.$menu.remove(); |
|
$.each(opt, function (key) { |
|
switch (key) { |
|
case 'ns': |
|
case 'selector': |
|
case 'build': |
|
case 'trigger': |
|
return true; |
|
|
|
default: |
|
opt[key] = undefined; |
|
try { |
|
delete opt[key]; |
|
} catch (e) { |
|
} |
|
return true; |
|
} |
|
}); |
|
} |
|
|
|
setTimeout(function () { |
|
$trigger.trigger('contextmenu:hidden'); |
|
}, 10); |
|
}); |
|
}, |
|
create: function (opt, root) { |
|
if (root === undefined) { |
|
root = opt; |
|
} |
|
// create contextMenu |
|
opt.$menu = $('<ul class="context-menu-list"></ul>').addClass(opt.className || '').data({ |
|
'contextMenu': opt, |
|
'contextMenuRoot': root |
|
}); |
|
|
|
$.each(['callbacks', 'commands', 'inputs'], function (i, k) { |
|
opt[k] = {}; |
|
if (!root[k]) { |
|
root[k] = {}; |
|
} |
|
}); |
|
|
|
root.accesskeys || (root.accesskeys = {}); |
|
|
|
// create contextMenu items |
|
$.each(opt.items, function (key, item) { |
|
var $t = $('<li class="context-menu-item"></li>').addClass(item.className || ''), |
|
$label = null, |
|
$input = null; |
|
|
|
// iOS needs to see a click-event bound to an element to actually |
|
// have the TouchEvents infrastructure trigger the click event |
|
$t.on('click', $.noop); |
|
|
|
// Make old school string seperator a real item so checks wont be |
|
// akward later. |
|
if (typeof item === 'string') { |
|
item = { type : 'cm_seperator' }; |
|
} |
|
|
|
item.$node = $t.data({ |
|
'contextMenu': opt, |
|
'contextMenuRoot': root, |
|
'contextMenuKey': key |
|
}); |
|
|
|
// register accesskey |
|
// NOTE: the accesskey attribute should be applicable to any element, but Safari5 and Chrome13 still can't do that |
|
if (typeof item.accesskey !== 'undefined') { |
|
var aks = splitAccesskey(item.accesskey); |
|
for (var i = 0, ak; ak = aks[i]; i++) { |
|
if (!root.accesskeys[ak]) { |
|
root.accesskeys[ak] = item; |
|
item._name = item.name.replace(new RegExp('(' + ak + ')', 'i'), '<span class="context-menu-accesskey">$1</span>'); |
|
break; |
|
} |
|
} |
|
} |
|
|
|
if (item.type && types[item.type]) { |
|
// run custom type handler |
|
types[item.type].call($t, item, opt, root); |
|
// register commands |
|
$.each([opt, root], function (i, k) { |
|
k.commands[key] = item; |
|
if ($.isFunction(item.callback)) { |
|
k.callbacks[key] = item.callback; |
|
} |
|
}); |
|
} else { |
|
// add label for input |
|
if (item.type === 'cm_seperator') { |
|
$t.addClass('context-menu-separator not-selectable'); |
|
} else if (item.type === 'html') { |
|
$t.addClass('context-menu-html not-selectable'); |
|
} else if (item.type) { |
|
$label = $('<label></label>').appendTo($t); |
|
$('<span></span>').html(item._name || item.name).appendTo($label); |
|
$t.addClass('context-menu-input'); |
|
opt.hasTypes = true; |
|
$.each([opt, root], function (i, k) { |
|
k.commands[key] = item; |
|
k.inputs[key] = item; |
|
}); |
|
} else if (item.items) { |
|
item.type = 'sub'; |
|
} |
|
|
|
switch (item.type) { |
|
case 'seperator': |
|
break; |
|
|
|
case 'text': |
|
$input = $('<input type="text" value="1" name="" value="">') |
|
.attr('name', 'context-menu-input-' + key) |
|
.val(item.value || '') |
|
.appendTo($label); |
|
break; |
|
|
|
case 'textarea': |
|
$input = $('<textarea name=""></textarea>') |
|
.attr('name', 'context-menu-input-' + key) |
|
.val(item.value || '') |
|
.appendTo($label); |
|
|
|
if (item.height) { |
|
$input.height(item.height); |
|
} |
|
break; |
|
|
|
case 'checkbox': |
|
$input = $('<input type="checkbox" value="1" name="" value="">') |
|
.attr('name', 'context-menu-input-' + key) |
|
.val(item.value || '') |
|
.prop('checked', !!item.selected) |
|
.prependTo($label); |
|
break; |
|
|
|
case 'radio': |
|
$input = $('<input type="radio" value="1" name="" value="">') |
|
.attr('name', 'context-menu-input-' + item.radio) |
|
.val(item.value || '') |
|
.prop('checked', !!item.selected) |
|
.prependTo($label); |
|
break; |
|
|
|
case 'select': |
|
$input = $('<select name="">') |
|
.attr('name', 'context-menu-input-' + key) |
|
.appendTo($label); |
|
if (item.options) { |
|
$.each(item.options, function (value, text) { |
|
$('<option></option>').val(value).text(text).appendTo($input); |
|
}); |
|
$input.val(item.selected); |
|
} |
|
break; |
|
|
|
case 'sub': |
|
$('<span></span>').html(item._name || item.name).appendTo($t); |
|
item.appendTo = item.$node; |
|
op.create(item, root); |
|
$t.data('contextMenu', item).addClass('context-menu-submenu'); |
|
item.callback = null; |
|
break; |
|
|
|
case 'html': |
|
$(item.html).appendTo($t); |
|
break; |
|
|
|
default: |
|
$.each([opt, root], function (i, k) { |
|
k.commands[key] = item; |
|
if ($.isFunction(item.callback)) { |
|
k.callbacks[key] = item.callback; |
|
} |
|
}); |
|
$('<span></span>').html(item._name || item.name || '').appendTo($t); |
|
break; |
|
} |
|
|
|
// disable key listener in <input> |
|
if (item.type && item.type !== 'sub' && item.type !== 'html' && item.type !== 'cm_seperator') { |
|
$input |
|
.on('focus', handle.focusInput) |
|
.on('blur', handle.blurInput); |
|
|
|
if (item.events) { |
|
$input.on(item.events, opt); |
|
} |
|
} |
|
|
|
// add icons |
|
if (item.icon) { |
|
if ($.isFunction(item.icon)) { |
|
item._icon = item.icon.call(this, $t, key, item); |
|
} else { |
|
item._icon = 'icon icon-' + item.icon; |
|
} |
|
$t.addClass(item._icon); |
|
} |
|
} |
|
|
|
// cache contained elements |
|
item.$input = $input; |
|
item.$label = $label; |
|
|
|
// attach item to menu |
|
$t.appendTo(opt.$menu); |
|
|
|
// Disable text selection |
|
if (!opt.hasTypes && $.support.eventSelectstart) { |
|
// browsers support user-select: none, |
|
// IE has a special event for text-selection |
|
// browsers supporting neither will not be preventing text-selection |
|
$t.on('selectstart.disableTextSelect', handle.abortevent); |
|
} |
|
}); |
|
// attach contextMenu to <body> (to bypass any possible overflow:hidden issues on parents of the trigger element) |
|
if (!opt.$node) { |
|
opt.$menu.css('display', 'none').addClass('context-menu-root'); |
|
} |
|
opt.$menu.appendTo(opt.appendTo || document.body); |
|
}, |
|
resize: function ($menu, nested) { |
|
// determine widths of submenus, as CSS won't grow them automatically |
|
// position:absolute within position:absolute; min-width:100; max-width:200; results in width: 100; |
|
// kinda sucks hard... |
|
|
|
// determine width of absolutely positioned element |
|
$menu.css({position: 'absolute', display: 'block'}); |
|
// don't apply yet, because that would break nested elements' widths |
|
$menu.data('width', Math.ceil($menu.width())); |
|
// reset styles so they allow nested elements to grow/shrink naturally |
|
$menu.css({ |
|
position: 'static', |
|
minWidth: '0px', |
|
maxWidth: '100000px' |
|
}); |
|
// identify width of nested menus |
|
$menu.find('> li > ul').each(function () { |
|
op.resize($(this), true); |
|
}); |
|
// reset and apply changes in the end because nested |
|
// elements' widths wouldn't be calculatable otherwise |
|
if (!nested) { |
|
$menu.find('ul').addBack().css({ |
|
position: '', |
|
display: '', |
|
minWidth: '', |
|
maxWidth: '' |
|
}).width(function () { |
|
return $(this).data('width'); |
|
}); |
|
} |
|
}, |
|
update: function (opt, root) { |
|
var $trigger = this; |
|
if (root === undefined) { |
|
root = opt; |
|
op.resize(opt.$menu); |
|
} |
|
// re-check disabled for each item |
|
opt.$menu.children().each(function () { |
|
var $item = $(this), |
|
key = $item.data('contextMenuKey'), |
|
item = opt.items[key], |
|
disabled = ($.isFunction(item.disabled) && item.disabled.call($trigger, key, root)) || item.disabled === true, |
|
visible; |
|
if ($.isFunction(item.visible)) { |
|
visible = item.visible.call($trigger, key, root); |
|
} else if (typeof item.visible !== 'undefined') { |
|
visible = item.visible === true; |
|
} else { |
|
visible = true; |
|
} |
|
$item[visible ? 'show' : 'hide'](); |
|
|
|
// dis- / enable item |
|
$item[disabled ? 'addClass' : 'removeClass']('disabled'); |
|
|
|
if ($.isFunction(item.icon)) { |
|
$item.removeClass(item._icon); |
|
item._icon = item.icon.call(this, $trigger, key, item); |
|
$item.addClass(item._icon); |
|
} |
|
|
|
if (item.type) { |
|
// dis- / enable input elements |
|
$item.find('input, select, textarea').prop('disabled', disabled); |
|
|
|
// update input states |
|
switch (item.type) { |
|
case 'text': |
|
case 'textarea': |
|
item.$input.val(item.value || ''); |
|
break; |
|
|
|
case 'checkbox': |
|
case 'radio': |
|
item.$input.val(item.value || '').prop('checked', !!item.selected); |
|
break; |
|
|
|
case 'select': |
|
item.$input.val(item.selected || ''); |
|
break; |
|
} |
|
} |
|
|
|
if (item.$menu) { |
|
// update sub-menu |
|
op.update.call($trigger, item, root); |
|
} |
|
}); |
|
}, |
|
layer: function (opt, zIndex) { |
|
// add transparent layer for click area |
|
// filter and background for Internet Explorer, Issue #23 |
|
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>') |
|
.css({height: $win.height(), width: $win.width(), display: 'block'}) |
|
.data('contextMenuRoot', opt) |
|
.insertBefore(this) |
|
.on('contextmenu', handle.abortevent) |
|
.on('mousedown', handle.layerClick); |
|
|
|
// IE6 doesn't know position:fixed; |
|
if (document.body.style.maxWidth === undefined) { // IE6 doesn't support maxWidth |
|
$layer.css({ |
|
'position': 'absolute', |
|
'height': $(document).height() |
|
}); |
|
} |
|
|
|
return $layer; |
|
} |
|
}; |
|
|
|
// split accesskey according to http://www.whatwg.org/specs/web-apps/current-work/multipage/editing.html#assigned-access-key |
|
function splitAccesskey(val) { |
|
var t = val.split(/\s+/), |
|
keys = []; |
|
|
|
for (var i = 0, k; k = t[i]; i++) { |
|
k = k.charAt(0).toUpperCase(); // first character only |
|
// theoretically non-accessible characters should be ignored, but different systems, different keyboard layouts, ... screw it. |
|
// a map to look up already used access keys would be nice |
|
keys.push(k); |
|
} |
|
|
|
return keys; |
|
} |
|
|
|
// handle contextMenu triggers |
|
$.fn.contextMenu = function (operation) { |
|
var $t = this, $o = operation; |
|
if (this.length > 0) { // this is not a build on demand menu |
|
if (operation === undefined) { |
|
this.first().trigger('contextmenu'); |
|
} else if (operation.x && operation.y) { |
|
this.first().trigger($.Event('contextmenu', {pageX: operation.x, pageY: operation.y})); |
|
} else if (operation === 'hide') { |
|
var $menu = this.first().data('contextMenu') ? this.first().data('contextMenu').$menu : null; |
|
$menu && $menu.trigger('contextmenu:hide'); |
|
} else if (operation === 'destroy') { |
|
$.contextMenu('destroy', {context: this}); |
|
} else if ($.isPlainObject(operation)) { |
|
operation.context = this; |
|
$.contextMenu('create', operation); |
|
} else if (operation) { |
|
this.removeClass('context-menu-disabled'); |
|
} else if (!operation) { |
|
this.addClass('context-menu-disabled'); |
|
} |
|
} else { |
|
$.each(menus, function () { |
|
if (this.selector === $t.selector) { |
|
$o.data = this; |
|
|
|
$.extend($o.data, {trigger: 'demand'}); |
|
} |
|
}); |
|
|
|
handle.contextmenu.call($o.target, $o); |
|
} |
|
|
|
return this; |
|
}; |
|
|
|
// manage contextMenu instances |
|
$.contextMenu = function (operation, options) { |
|
if (typeof operation !== 'string') { |
|
options = operation; |
|
operation = 'create'; |
|
} |
|
|
|
if (typeof options === 'string') { |
|
options = {selector: options}; |
|
} else if (options === undefined) { |
|
options = {}; |
|
} |
|
|
|
// merge with default options |
|
var o = $.extend(true, {}, defaults, options || {}); |
|
var $document = $(document); |
|
var $context = $document; |
|
var _hasContext = false; |
|
|
|
if (!o.context || !o.context.length) { |
|
o.context = document; |
|
} else { |
|
// you never know what they throw at you... |
|
$context = $(o.context).first(); |
|
o.context = $context.get(0); |
|
_hasContext = o.context !== document; |
|
} |
|
|
|
switch (operation) { |
|
case 'create': |
|
// no selector no joy |
|
if (!o.selector) { |
|
throw new Error('No selector specified'); |
|
} |
|
// make sure internal classes are not bound to |
|
if (o.selector.match(/.context-menu-(list|item|input)($|\s)/)) { |
|
throw new Error('Cannot bind to selector "' + o.selector + '" as it contains a reserved className'); |
|
} |
|
if (!o.build && (!o.items || $.isEmptyObject(o.items))) { |
|
throw new Error('No Items specified'); |
|
} |
|
counter++; |
|
o.ns = '.contextMenu' + counter; |
|
if (!_hasContext) { |
|
namespaces[o.selector] = o.ns; |
|
} |
|
menus[o.ns] = o; |
|
|
|
// default to right click |
|
if (!o.trigger) { |
|
o.trigger = 'right'; |
|
} |
|
|
|
if (!initialized) { |
|
// make sure item click is registered first |
|
$document |
|
.on({ |
|
'contextmenu:hide.contextMenu': handle.hideMenu, |
|
'prevcommand.contextMenu': handle.prevItem, |
|
'nextcommand.contextMenu': handle.nextItem, |
|
'contextmenu.contextMenu': handle.abortevent, |
|
'mouseenter.contextMenu': handle.menuMouseenter, |
|
'mouseleave.contextMenu': handle.menuMouseleave |
|
}, '.context-menu-list') |
|
.on('mouseup.contextMenu', '.context-menu-input', handle.inputClick) |
|
.on({ |
|
'mouseup.contextMenu': handle.itemClick, |
|
'contextmenu:focus.contextMenu': handle.focusItem, |
|
'contextmenu:blur.contextMenu': handle.blurItem, |
|
'contextmenu.contextMenu': handle.abortevent, |
|
'mouseenter.contextMenu': handle.itemMouseenter, |
|
'mouseleave.contextMenu': handle.itemMouseleave |
|
}, '.context-menu-item'); |
|
|
|
initialized = true; |
|
} |
|
|
|
// engage native contextmenu event |
|
$context |
|
.on('contextmenu' + o.ns, o.selector, o, handle.contextmenu); |
|
|
|
if (_hasContext) { |
|
// add remove hook, just in case |
|
$context.on('remove' + o.ns, function () { |
|
$(this).contextMenu('destroy'); |
|
}); |
|
} |
|
|
|
switch (o.trigger) { |
|
case 'hover': |
|
$context |
|
.on('mouseenter' + o.ns, o.selector, o, handle.mouseenter) |
|
.on('mouseleave' + o.ns, o.selector, o, handle.mouseleave); |
|
break; |
|
|
|
case 'left': |
|
$context.on('click' + o.ns, o.selector, o, handle.click); |
|
break; |
|
/* |
|
default: |
|
// http://www.quirksmode.org/dom/events/contextmenu.html |
|
$document |
|
.on('mousedown' + o.ns, o.selector, o, handle.mousedown) |
|
.on('mouseup' + o.ns, o.selector, o, handle.mouseup); |
|
break; |
|
*/ |
|
} |
|
|
|
// create menu |
|
if (!o.build) { |
|
op.create(o); |
|
} |
|
break; |
|
|
|
case 'destroy': |
|
var $visibleMenu; |
|
if (_hasContext) { |
|
// get proper options |
|
var context = o.context; |
|
$.each(menus, function (ns, o) { |
|
if (o.context !== context) { |
|
return true; |
|
} |
|
|
|
$visibleMenu = $('.context-menu-list').filter(':visible'); |
|
if ($visibleMenu.length && $visibleMenu.data().contextMenuRoot.$trigger.is($(o.context).find(o.selector))) { |
|
$visibleMenu.trigger('contextmenu:hide', {force: true}); |
|
} |
|
|
|
try { |
|
if (menus[o.ns].$menu) { |
|
menus[o.ns].$menu.remove(); |
|
} |
|
|
|
delete menus[o.ns]; |
|
} catch (e) { |
|
menus[o.ns] = null; |
|
} |
|
|
|
$(o.context).off(o.ns); |
|
|
|
return true; |
|
}); |
|
} else if (!o.selector) { |
|
$document.off('.contextMenu .contextMenuAutoHide'); |
|
$.each(menus, function (ns, o) { |
|
$(o.context).off(o.ns); |
|
}); |
|
|
|
namespaces = {}; |
|
menus = {}; |
|
counter = 0; |
|
initialized = false; |
|
|
|
$('#context-menu-layer, .context-menu-list').remove(); |
|
} else if (namespaces[o.selector]) { |
|
$visibleMenu = $('.context-menu-list').filter(':visible'); |
|
if ($visibleMenu.length && $visibleMenu.data().contextMenuRoot.$trigger.is(o.selector)) { |
|
$visibleMenu.trigger('contextmenu:hide', {force: true}); |
|
} |
|
|
|
try { |
|
if (menus[namespaces[o.selector]].$menu) { |
|
menus[namespaces[o.selector]].$menu.remove(); |
|
} |
|
|
|
delete menus[namespaces[o.selector]]; |
|
} catch (e) { |
|
menus[namespaces[o.selector]] = null; |
|
} |
|
|
|
$document.off(namespaces[o.selector]); |
|
} |
|
break; |
|
|
|
case 'html5': |
|
// if <command> or <menuitem> are not handled by the browser, |
|
// or options was a bool true, |
|
// initialize $.contextMenu for them |
|
if ((!$.support.htmlCommand && !$.support.htmlMenuitem) || (typeof options === 'boolean' && options)) { |
|
$('menu[type="context"]').each(function () { |
|
if (this.id) { |
|
$.contextMenu({ |
|
selector: '[contextmenu=' + this.id + ']', |
|
items: $.contextMenu.fromMenu(this) |
|
}); |
|
} |
|
}).css('display', 'none'); |
|
} |
|
break; |
|
|
|
default: |
|
throw new Error('Unknown operation "' + operation + '"'); |
|
} |
|
|
|
return this; |
|
}; |
|
|
|
// import values into <input> commands |
|
$.contextMenu.setInputValues = function (opt, data) { |
|
if (data === undefined) { |
|
data = {}; |
|
} |
|
|
|
$.each(opt.inputs, function (key, item) { |
|
switch (item.type) { |
|
case 'text': |
|
case 'textarea': |
|
item.value = data[key] || ''; |
|
break; |
|
|
|
case 'checkbox': |
|
item.selected = data[key] ? true : false; |
|
break; |
|
|
|
case 'radio': |
|
item.selected = (data[item.radio] || '') === item.value; |
|
break; |
|
|
|
case 'select': |
|
item.selected = data[key] || ''; |
|
break; |
|
} |
|
}); |
|
}; |
|
|
|
// export values from <input> commands |
|
$.contextMenu.getInputValues = function (opt, data) { |
|
if (data === undefined) { |
|
data = {}; |
|
} |
|
|
|
$.each(opt.inputs, function (key, item) { |
|
switch (item.type) { |
|
case 'text': |
|
case 'textarea': |
|
case 'select': |
|
data[key] = item.$input.val(); |
|
break; |
|
|
|
case 'checkbox': |
|
data[key] = item.$input.prop('checked'); |
|
break; |
|
|
|
case 'radio': |
|
if (item.$input.prop('checked')) { |
|
data[item.radio] = item.value; |
|
} |
|
break; |
|
} |
|
}); |
|
|
|
return data; |
|
}; |
|
|
|
// find <label for="xyz"> |
|
function inputLabel(node) { |
|
return (node.id && $('label[for="' + node.id + '"]').val()) || node.name; |
|
} |
|
|
|
// convert <menu> to items object |
|
function menuChildren(items, $children, counter) { |
|
if (!counter) { |
|
counter = 0; |
|
} |
|
|
|
$children.each(function () { |
|
var $node = $(this), |
|
node = this, |
|
nodeName = this.nodeName.toLowerCase(), |
|
label, |
|
item; |
|
|
|
// extract <label><input> |
|
if (nodeName === 'label' && $node.find('input, textarea, select').length) { |
|
label = $node.text(); |
|
$node = $node.children().first(); |
|
node = $node.get(0); |
|
nodeName = node.nodeName.toLowerCase(); |
|
} |
|
|
|
/* |
|
* <menu> accepts flow-content as children. that means <embed>, <canvas> and such are valid menu items. |
|
* Not being the sadistic kind, $.contextMenu only accepts: |
|
* <command>, <menuitem>, <hr>, <span>, <p> <input [text, radio, checkbox]>, <textarea>, <select> and of course <menu>. |
|
* Everything else will be imported as an html node, which is not interfaced with contextMenu. |
|
*/ |
|
|
|
// http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#concept-command |
|
switch (nodeName) { |
|
// http://www.whatwg.org/specs/web-apps/current-work/multipage/interactive-elements.html#the-menu-element |
|
case 'menu': |
|
item = {name: $node.attr('label'), items: {}}; |
|
counter = menuChildren(item.items, $node.children(), counter); |
|
break; |
|
|
|
// http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#using-the-a-element-to-define-a-command |
|
case 'a': |
|
// http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#using-the-button-element-to-define-a-command |
|
case 'button': |
|
item = { |
|
name: $node.text(), |
|
disabled: !!$node.attr('disabled'), |
|
callback: (function () { |
|
return function () { |
|
$node.click(); |
|
}; |
|
})() |
|
}; |
|
break; |
|
|
|
// http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#using-the-command-element-to-define-a-command |
|
|
|
case 'menuitem': |
|
case 'command': |
|
switch ($node.attr('type')) { |
|
case undefined: |
|
case 'command': |
|
case 'menuitem': |
|
item = { |
|
name: $node.attr('label'), |
|
disabled: !!$node.attr('disabled'), |
|
icon: $node.attr('icon'), |
|
callback: (function () { |
|
return function () { |
|
$node.click(); |
|
}; |
|
})() |
|
}; |
|
break; |
|
|
|
case 'checkbox': |
|
item = { |
|
type: 'checkbox', |
|
disabled: !!$node.attr('disabled'), |
|
name: $node.attr('label'), |
|
selected: !!$node.attr('checked') |
|
}; |
|
break; |
|
case 'radio': |
|
item = { |
|
type: 'radio', |
|
disabled: !!$node.attr('disabled'), |
|
name: $node.attr('label'), |
|
radio: $node.attr('radiogroup'), |
|
value: $node.attr('id'), |
|
selected: !!$node.attr('checked') |
|
}; |
|
break; |
|
|
|
default: |
|
item = undefined; |
|
} |
|
break; |
|
|
|
case 'hr': |
|
item = '-------'; |
|
break; |
|
|
|
case 'input': |
|
switch ($node.attr('type')) { |
|
case 'text': |
|
item = { |
|
type: 'text', |
|
name: label || inputLabel(node), |
|
disabled: !!$node.attr('disabled'), |
|
value: $node.val() |
|
}; |
|
break; |
|
|
|
case 'checkbox': |
|
item = { |
|
type: 'checkbox', |
|
name: label || inputLabel(node), |
|
disabled: !!$node.attr('disabled'), |
|
selected: !!$node.attr('checked') |
|
}; |
|
break; |
|
|
|
case 'radio': |
|
item = { |
|
type: 'radio', |
|
name: label || inputLabel(node), |
|
disabled: !!$node.attr('disabled'), |
|
radio: !!$node.attr('name'), |
|
value: $node.val(), |
|
selected: !!$node.attr('checked') |
|
}; |
|
break; |
|
|
|
default: |
|
item = undefined; |
|
break; |
|
} |
|
break; |
|
|
|
case 'select': |
|
item = { |
|
type: 'select', |
|
name: label || inputLabel(node), |
|
disabled: !!$node.attr('disabled'), |
|
selected: $node.val(), |
|
options: {} |
|
}; |
|
$node.children().each(function () { |
|
item.options[this.value] = $(this).text(); |
|
}); |
|
break; |
|
|
|
case 'textarea': |
|
item = { |
|
type: 'textarea', |
|
name: label || inputLabel(node), |
|
disabled: !!$node.attr('disabled'), |
|
value: $node.val() |
|
}; |
|
break; |
|
|
|
case 'label': |
|
break; |
|
|
|
default: |
|
item = {type: 'html', html: $node.clone(true)}; |
|
break; |
|
} |
|
|
|
if (item) { |
|
counter++; |
|
items['key' + counter] = item; |
|
} |
|
}); |
|
|
|
return counter; |
|
} |
|
|
|
// convert html5 menu |
|
$.contextMenu.fromMenu = function (element) { |
|
var $this = $(element), |
|
items = {}; |
|
|
|
menuChildren(items, $this.children()); |
|
|
|
return items; |
|
}; |
|
|
|
// make defaults accessible |
|
$.contextMenu.defaults = defaults; |
|
$.contextMenu.types = types; |
|
// export internal functions - undocumented, for hacking only! |
|
$.contextMenu.handle = handle; |
|
$.contextMenu.op = op; |
|
$.contextMenu.menus = menus; |
|
});
|
|
|