+++ /dev/null
-/*!\r
- * jQuery contextMenu - Plugin for simple contextMenu handling\r
- *\r
- * Version: 1.5.13\r
- *\r
- * Authors: Rodney Rehm, Addy Osmani (patches for FF)\r
- * Web: http://medialize.github.com/jQuery-contextMenu/\r
- *\r
- * Licensed under\r
- * MIT License http://www.opensource.org/licenses/mit-license\r
- * GPL v3 http://opensource.org/licenses/GPL-3.0\r
- *\r
- */\r
-\r
-(function($, undefined){\r
- \r
- // TODO: -\r
- // ARIA stuff: menuitem, menuitemcheckbox und menuitemradio\r
- // create <menu> structure if $.support[htmlCommand || htmlMenuitem] and !opt.disableNative\r
-\r
-// determine html5 compatibility\r
-$.support.htmlMenuitem = ('HTMLMenuItemElement' in window);\r
-$.support.htmlCommand = ('HTMLCommandElement' in window);\r
-\r
-var // currently active contextMenu trigger\r
- $currentTrigger = null,\r
- // is contextMenu initialized with at least one menu?\r
- initialized = false,\r
- // window handle\r
- $win = $(window),\r
- // number of registered menus\r
- counter = 0,\r
- // mapping selector to namespace\r
- namespaces = {},\r
- // mapping namespace to options\r
- menus = {},\r
- // custom command type handlers\r
- types = {},\r
- // default values\r
- defaults = {\r
- // selector of contextMenu trigger\r
- selector: null,\r
- // where to append the menu to\r
- appendTo: null,\r
- // method to trigger context menu ["right", "left", "hover"]\r
- trigger: "right",\r
- // hide menu when mouse leaves trigger / menu elements\r
- autoHide: false,\r
- // ms to wait before showing a hover-triggered context menu\r
- delay: 200,\r
- // determine position to show menu at\r
- determinePosition: function($menu) {\r
- // position to the lower middle of the trigger element\r
- if ($.ui && $.ui.position) {\r
- // .position() is provided as a jQuery UI utility\r
- // (...and it won't work on hidden elements)\r
- $menu.css('display', 'block').position({\r
- my: "center top",\r
- at: "center bottom",\r
- of: this,\r
- offset: "0 5",\r
- collision: "fit"\r
- }).css('display', 'none');\r
- } else {\r
- // determine contextMenu position\r
- var offset = this.offset();\r
- offset.top += this.outerHeight();\r
- offset.left += this.outerWidth() / 2 - $menu.outerWidth() / 2;\r
- $menu.css(offset);\r
- }\r
- },\r
- // position menu\r
- position: function(opt, x, y) {\r
- var $this = this,\r
- offset;\r
- // determine contextMenu position\r
- if (!x && !y) {\r
- opt.determinePosition.call(this, opt.$menu);\r
- return;\r
- } else if (x === "maintain" && y === "maintain") {\r
- // x and y must not be changed (after re-show on command click)\r
- offset = opt.$menu.position();\r
- } else {\r
- // x and y are given (by mouse event)\r
- var triggerIsFixed = opt.$trigger.parents().andSelf()\r
- .filter(function() {\r
- return $(this).css('position') == "fixed";\r
- }).length;\r
-\r
- if (triggerIsFixed) {\r
- y -= $win.scrollTop();\r
- x -= $win.scrollLeft();\r
- }\r
- offset = {top: y, left: x};\r
- }\r
- \r
- // correct offset if viewport demands it\r
- var bottom = $win.scrollTop() + $win.height(),\r
- right = $win.scrollLeft() + $win.width(),\r
- height = opt.$menu.height(),\r
- width = opt.$menu.width();\r
- \r
- if (offset.top + height > bottom) {\r
- offset.top -= height;\r
- }\r
- \r
- if (offset.left + width > right) {\r
- offset.left -= width;\r
- }\r
- \r
- opt.$menu.css(offset);\r
- },\r
- // position the sub-menu\r
- positionSubmenu: function($menu) {\r
- if ($.ui && $.ui.position) {\r
- // .position() is provided as a jQuery UI utility\r
- // (...and it won't work on hidden elements)\r
- $menu.css('display', 'block').position({\r
- my: "left top",\r
- at: "right top",\r
- of: this,\r
- collision: "fit"\r
- }).css('display', '');\r
- } else {\r
- // determine contextMenu position\r
- var offset = this.offset();\r
- offset.top += 0;\r
- offset.left += this.outerWidth();\r
- $menu.css(offset);\r
- }\r
- },\r
- // offset to add to zIndex\r
- zIndex: 1,\r
- // show hide animation settings\r
- animation: {\r
- duration: 50,\r
- show: 'slideDown',\r
- hide: 'slideUp'\r
- },\r
- // events\r
- events: {\r
- show: $.noop,\r
- hide: $.noop\r
- },\r
- // default callback\r
- callback: null,\r
- // list of contextMenu items\r
- items: {}\r
- },\r
- // mouse position for hover activation\r
- hoveract = {\r
- timer: null,\r
- pageX: null,\r
- pageY: null\r
- },\r
- // determine zIndex\r
- zindex = function($t) {\r
- var zin = 0,\r
- $tt = $t;\r
-\r
- while (true) {\r
- zin = Math.max(zin, parseInt($tt.css('z-index'), 10) || 0);\r
- $tt = $tt.parent();\r
- if (!$tt || !$tt.length || $tt.prop('nodeName').toLowerCase() == 'body') {\r
- break;\r
- }\r
- }\r
- \r
- return zin;\r
- },\r
- // event handlers\r
- handle = {\r
- // abort anything\r
- abortevent: function(e){\r
- e.preventDefault();\r
- e.stopImmediatePropagation();\r
- },\r
- \r
- // contextmenu show dispatcher\r
- contextmenu: function(e) {\r
- var $this = $(this);\r
- \r
- // disable actual context-menu\r
- e.preventDefault();\r
- e.stopImmediatePropagation();\r
- \r
- // abort native-triggered events unless we're triggering on right click\r
- if (e.data.trigger != 'right' && e.originalEvent) {\r
- return;\r
- }\r
- \r
- if (!$this.hasClass('context-menu-disabled')) {\r
- // theoretically need to fire a show event at <menu>\r
- // http://www.whatwg.org/specs/web-apps/current-work/multipage/interactive-elements.html#context-menus\r
- // var evt = jQuery.Event("show", { data: data, pageX: e.pageX, pageY: e.pageY, relatedTarget: this });\r
- // e.data.$menu.trigger(evt);\r
- \r
- $currentTrigger = $this;\r
- if (e.data.build) {\r
- var built = e.data.build($currentTrigger, e);\r
- // abort if build() returned false\r
- if (built === false) {\r
- return;\r
- }\r
- \r
- // dynamically build menu on invocation\r
- e.data = $.extend(true, defaults, e.data, built || {});\r
-\r
- // abort if there are no items to display\r
- if (!e.data.items || $.isEmptyObject(e.data.items)) {\r
- // Note: jQuery captures and ignores errors from event handlers\r
- if (window.console) {\r
- (console.error || console.log)("No items specified to show in contextMenu");\r
- }\r
- \r
- throw new Error('No Items sepcified');\r
- }\r
- \r
- // backreference for custom command type creation\r
- e.data.$trigger = $currentTrigger;\r
- \r
- op.create(e.data);\r
- }\r
- // show menu\r
- op.show.call($this, e.data, e.pageX, e.pageY);\r
- }\r
- },\r
- // contextMenu left-click trigger\r
- click: function(e) {\r
- e.preventDefault();\r
- e.stopImmediatePropagation();\r
- $(this).trigger(jQuery.Event("contextmenu", { data: e.data, pageX: e.pageX, pageY: e.pageY }));\r
- },\r
- // contextMenu right-click trigger\r
- mousedown: function(e) {\r
- // register mouse down\r
- var $this = $(this);\r
- \r
- // hide any previous menus\r
- if ($currentTrigger && $currentTrigger.length && !$currentTrigger.is($this)) {\r
- $currentTrigger.data('contextMenu').$menu.trigger('contextmenu:hide');\r
- }\r
- \r
- // activate on right click\r
- if (e.button == 2) {\r
- $currentTrigger = $this.data('contextMenuActive', true);\r
- }\r
- },\r
- // contextMenu right-click trigger\r
- mouseup: function(e) {\r
- // show menu\r
- var $this = $(this);\r
- if ($this.data('contextMenuActive') && $currentTrigger && $currentTrigger.length && $currentTrigger.is($this) && !$this.hasClass('context-menu-disabled')) {\r
- e.preventDefault();\r
- e.stopImmediatePropagation();\r
- $currentTrigger = $this;\r
- $this.trigger(jQuery.Event("contextmenu", { data: e.data, pageX: e.pageX, pageY: e.pageY }));\r
- }\r
- \r
- $this.removeData('contextMenuActive');\r
- },\r
- // contextMenu hover trigger\r
- mouseenter: function(e) {\r
- var $this = $(this),\r
- $related = $(e.relatedTarget),\r
- $document = $(document);\r
- \r
- // abort if we're coming from a menu\r
- if ($related.is('.context-menu-list') || $related.closest('.context-menu-list').length) {\r
- return;\r
- }\r
- \r
- // abort if a menu is shown\r
- if ($currentTrigger && $currentTrigger.length) {\r
- return;\r
- }\r
- \r
- hoveract.pageX = e.pageX;\r
- hoveract.pageY = e.pageY;\r
- hoveract.data = e.data;\r
- $document.on('mousemove.contextMenuShow', handle.mousemove);\r
- hoveract.timer = setTimeout(function() {\r
- hoveract.timer = null;\r
- $document.off('mousemove.contextMenuShow');\r
- $currentTrigger = $this;\r
- $this.trigger(jQuery.Event("contextmenu", { data: hoveract.data, pageX: hoveract.pageX, pageY: hoveract.pageY }));\r
- }, e.data.delay );\r
- },\r
- // contextMenu hover trigger\r
- mousemove: function(e) {\r
- hoveract.pageX = e.pageX;\r
- hoveract.pageY = e.pageY;\r
- },\r
- // contextMenu hover trigger\r
- mouseleave: function(e) {\r
- // abort if we're leaving for a menu\r
- var $related = $(e.relatedTarget);\r
- if ($related.is('.context-menu-list') || $related.closest('.context-menu-list').length) {\r
- return;\r
- }\r
- \r
- try {\r
- clearTimeout(hoveract.timer);\r
- } catch(e) {}\r
- \r
- hoveract.timer = null;\r
- },\r
- \r
- // click on layer to hide contextMenu\r
- layerClick: function(e) {\r
- var $this = $(this),\r
- root = $this.data('contextMenuRoot');\r
- \r
- e.preventDefault();\r
- e.stopImmediatePropagation();\r
- \r
- if ((root.trigger == 'left' && e.button == 0) || (root.trigger == 'right' && e.button == 2)) {\r
- var offset = root.$trigger.offset();\r
- \r
- // while this looks kinda awful, it's the best way to avoid\r
- // unnecessarily calculating any positions\r
- offset.top += $(window).scrollTop();\r
- if (offset.top <= e.pageY) {\r
- offset.left += $(window).scrollLeft();\r
- if (offset.left <= e.pageX) {\r
- offset.bottom = offset.top + root.$trigger.outerHeight();\r
- if (offset.bottom >= e.pageY) {\r
- offset.right = offset.left + root.$trigger.outerWidth();\r
- if (offset.right >= e.pageX) {\r
- // reposition\r
- root.position.call(root.$trigger, root, e.pageX, e.pageY);\r
- return;\r
- }\r
- }\r
- }\r
- }\r
- } \r
- \r
- // remove only after mouseup has completed\r
- $this.on('mouseup', function(e) {\r
- e.preventDefault();\r
- e.stopImmediatePropagation();\r
- root.$menu.trigger('contextmenu:hide');\r
- });\r
- },\r
- // key handled :hover\r
- keyStop: function(e, opt) {\r
- if (!opt.isInput) {\r
- e.preventDefault();\r
- }\r
- \r
- e.stopPropagation();\r
- },\r
- key: function(e) {\r
- var opt = $currentTrigger.data('contextMenu') || {},\r
- $children = opt.$menu.children(),\r
- $round;\r
-\r
- switch (e.keyCode) {\r
- case 9:\r
- case 38: // up\r
- handle.keyStop(e, opt);\r
- // if keyCode is [38 (up)] or [9 (tab) with shift]\r
- if (opt.isInput) {\r
- if (e.keyCode == 9 && e.shiftKey) {\r
- e.preventDefault();\r
- opt.$selected && opt.$selected.find('input, textarea, select').blur();\r
- opt.$menu.trigger('prevcommand');\r
- return;\r
- } else if (e.keyCode == 38 && opt.$selected.find('input, textarea, select').prop('type') == 'checkbox') {\r
- // checkboxes don't capture this key\r
- e.preventDefault();\r
- return;\r
- }\r
- } else if (e.keyCode != 9 || e.shiftKey) {\r
- opt.$menu.trigger('prevcommand');\r
- return;\r
- }\r
- \r
- case 9: // tab\r
- case 40: // down\r
- handle.keyStop(e, opt);\r
- if (opt.isInput) {\r
- if (e.keyCode == 9) {\r
- e.preventDefault();\r
- opt.$selected && opt.$selected.find('input, textarea, select').blur();\r
- opt.$menu.trigger('nextcommand');\r
- return;\r
- } else if (e.keyCode == 40 && opt.$selected.find('input, textarea, select').prop('type') == 'checkbox') {\r
- // checkboxes don't capture this key\r
- e.preventDefault();\r
- return;\r
- }\r
- } else {\r
- opt.$menu.trigger('nextcommand');\r
- return;\r
- }\r
- break;\r
- \r
- case 37: // left\r
- handle.keyStop(e, opt);\r
- if (opt.isInput || !opt.$selected || !opt.$selected.length) {\r
- break;\r
- }\r
- \r
- if (!opt.$selected.parent().hasClass('context-menu-root')) {\r
- var $parent = opt.$selected.parent().parent();\r
- opt.$selected.trigger('contextmenu:blur');\r
- opt.$selected = $parent;\r
- return;\r
- }\r
- break;\r
- \r
- case 39: // right\r
- handle.keyStop(e, opt);\r
- if (opt.isInput || !opt.$selected || !opt.$selected.length) {\r
- break;\r
- }\r
- \r
- var itemdata = opt.$selected.data('contextMenu') || {};\r
- if (itemdata.$menu && opt.$selected.hasClass('context-menu-submenu')) {\r
- opt.$selected = null;\r
- itemdata.$selected = null;\r
- itemdata.$menu.trigger('nextcommand');\r
- return;\r
- }\r
- break;\r
- \r
- case 35: // end\r
- case 36: // home\r
- if (opt.$selected && opt.$selected.find('input, textarea, select').length) {\r
- return;\r
- } else {\r
- (opt.$selected && opt.$selected.parent() || opt.$menu)\r
- .children(':not(.disabled, .not-selectable)')[e.keyCode == 36 ? 'first' : 'last']()\r
- .trigger('contextmenu:focus');\r
- e.preventDefault();\r
- return;\r
- }\r
- break;\r
- \r
- case 13: // enter\r
- handle.keyStop(e, opt);\r
- if (opt.isInput) {\r
- if (opt.$selected && !opt.$selected.is('textarea, select')) {\r
- e.preventDefault();\r
- return;\r
- }\r
- break;\r
- }\r
- opt.$selected && opt.$selected.trigger('mouseup');\r
- return;\r
- \r
- case 32: // space\r
- case 33: // page up\r
- case 34: // page down\r
- // prevent browser from scrolling down while menu is visible\r
- handle.keyStop(e, opt);\r
- return;\r
- \r
- case 27: // esc\r
- handle.keyStop(e, opt);\r
- opt.$menu.trigger('contextmenu:hide');\r
- return;\r
- \r
- default: // 0-9, a-z\r
- var k = (String.fromCharCode(e.keyCode)).toUpperCase();\r
- if (opt.accesskeys[k]) {\r
- // according to the specs accesskeys must be invoked immediately\r
- opt.accesskeys[k].$node.trigger(opt.accesskeys[k].$menu\r
- ? 'contextmenu:focus'\r
- : 'mouseup'\r
- );\r
- return;\r
- }\r
- break;\r
- }\r
- // pass event to selected item, \r
- // stop propagation to avoid endless recursion\r
- e.stopPropagation();\r
- opt.$selected && opt.$selected.trigger(e);\r
- },\r
-\r
- // select previous possible command in menu\r
- prevItem: function(e) {\r
- e.stopPropagation();\r
- var opt = $(this).data('contextMenu') || {};\r
-\r
- // obtain currently selected menu\r
- if (opt.$selected) {\r
- var $s = opt.$selected;\r
- opt = opt.$selected.parent().data('contextMenu') || {};\r
- opt.$selected = $s;\r
- }\r
- \r
- var $children = opt.$menu.children(),\r
- $prev = !opt.$selected || !opt.$selected.prev().length ? $children.last() : opt.$selected.prev(),\r
- $round = $prev;\r
- \r
- // skip disabled\r
- while ($prev.hasClass('disabled') || $prev.hasClass('not-selectable')) {\r
- if ($prev.prev().length) {\r
- $prev = $prev.prev();\r
- } else {\r
- $prev = $children.last();\r
- }\r
- if ($prev.is($round)) {\r
- // break endless loop\r
- return;\r
- }\r
- }\r
- \r
- // leave current\r
- if (opt.$selected) {\r
- handle.itemMouseleave.call(opt.$selected.get(0), e);\r
- }\r
- \r
- // activate next\r
- handle.itemMouseenter.call($prev.get(0), e);\r
- \r
- // focus input\r
- var $input = $prev.find('input, textarea, select');\r
- if ($input.length) {\r
- $input.focus();\r
- }\r
- },\r
- // select next possible command in menu\r
- nextItem: function(e) {\r
- e.stopPropagation();\r
- var opt = $(this).data('contextMenu') || {};\r
-\r
- // obtain currently selected menu\r
- if (opt.$selected) {\r
- var $s = opt.$selected;\r
- opt = opt.$selected.parent().data('contextMenu') || {};\r
- opt.$selected = $s;\r
- }\r
-\r
- var $children = opt.$menu.children(),\r
- $next = !opt.$selected || !opt.$selected.next().length ? $children.first() : opt.$selected.next(),\r
- $round = $next;\r
-\r
- // skip disabled\r
- while ($next.hasClass('disabled') || $next.hasClass('not-selectable')) {\r
- if ($next.next().length) {\r
- $next = $next.next();\r
- } else {\r
- $next = $children.first();\r
- }\r
- if ($next.is($round)) {\r
- // break endless loop\r
- return;\r
- }\r
- }\r
- \r
- // leave current\r
- if (opt.$selected) {\r
- handle.itemMouseleave.call(opt.$selected.get(0), e);\r
- }\r
- \r
- // activate next\r
- handle.itemMouseenter.call($next.get(0), e);\r
- \r
- // focus input\r
- var $input = $next.find('input, textarea, select');\r
- if ($input.length) {\r
- $input.focus();\r
- }\r
- },\r
- \r
- // flag that we're inside an input so the key handler can act accordingly\r
- focusInput: function(e) {\r
- var $this = $(this).closest('.context-menu-item'),\r
- data = $this.data(),\r
- opt = data.contextMenu,\r
- root = data.contextMenuRoot;\r
-\r
- root.$selected = opt.$selected = $this;\r
- root.isInput = opt.isInput = true;\r
- },\r
- // flag that we're inside an input so the key handler can act accordingly\r
- blurInput: function(e) {\r
- var $this = $(this).closest('.context-menu-item'),\r
- data = $this.data(),\r
- opt = data.contextMenu,\r
- root = data.contextMenuRoot;\r
-\r
- root.isInput = opt.isInput = false;\r
- },\r
- \r
- // :hover on menu\r
- menuMouseenter: function(e) {\r
- var root = $(this).data().contextMenuRoot;\r
- root.hovering = true;\r
- },\r
- // :hover on menu\r
- menuMouseleave: function(e) {\r
- var root = $(this).data().contextMenuRoot;\r
- if (root.$layer && root.$layer.is(e.relatedTarget)) {\r
- root.hovering = false;\r
- }\r
- },\r
- \r
- // :hover done manually so key handling is possible\r
- itemMouseenter: function(e) {\r
- var $this = $(this),\r
- data = $this.data(),\r
- opt = data.contextMenu,\r
- root = data.contextMenuRoot;\r
- \r
- root.hovering = true;\r
-\r
- // abort if we're re-entering\r
- if (e && root.$layer && root.$layer.is(e.relatedTarget)) {\r
- e.preventDefault();\r
- e.stopImmediatePropagation();\r
- }\r
-\r
- // make sure only one item is selected\r
- (opt.$menu ? opt : root).$menu\r
- .children('.hover').trigger('contextmenu:blur');\r
-\r
- if ($this.hasClass('disabled') || $this.hasClass('not-selectable')) {\r
- opt.$selected = null;\r
- return;\r
- }\r
- \r
- $this.trigger('contextmenu:focus');\r
- },\r
- // :hover done manually so key handling is possible\r
- itemMouseleave: function(e) {\r
- var $this = $(this),\r
- data = $this.data(),\r
- opt = data.contextMenu,\r
- root = data.contextMenuRoot;\r
-\r
- if (root !== opt && root.$layer && root.$layer.is(e.relatedTarget)) {\r
- root.$selected && root.$selected.trigger('contextmenu:blur');\r
- e.preventDefault();\r
- e.stopImmediatePropagation();\r
- root.$selected = opt.$selected = opt.$node;\r
- return;\r
- }\r
- \r
- $this.trigger('contextmenu:blur');\r
- },\r
- // contextMenu item click\r
- itemClick: function(e) {\r
- var $this = $(this),\r
- data = $this.data(),\r
- opt = data.contextMenu,\r
- root = data.contextMenuRoot,\r
- key = data.contextMenuKey,\r
- callback;\r
-\r
- // abort if the key is unknown or disabled or is a menu\r
- if (!opt.items[key] || $this.hasClass('disabled') || $this.hasClass('context-menu-submenu')) {\r
- return;\r
- }\r
-\r
- e.preventDefault();\r
- e.stopImmediatePropagation();\r
-\r
- if ($.isFunction(root.callbacks[key])) {\r
- // item-specific callback\r
- callback = root.callbacks[key];\r
- } else if ($.isFunction(root.callback)) {\r
- // default callback\r
- callback = root.callback; \r
- } else {\r
- // no callback, no action\r
- return;\r
- }\r
-\r
- // hide menu if callback doesn't stop that\r
- if (callback.call(root.$trigger, key, root) !== false) {\r
- root.$menu.trigger('contextmenu:hide');\r
- } else {\r
- op.update.call(root.$trigger, root);\r
- }\r
- },\r
- // ignore click events on input elements\r
- inputClick: function(e) {\r
- e.stopImmediatePropagation();\r
- },\r
- \r
- // hide <menu>\r
- hideMenu: function(e) {\r
- var root = $(this).data('contextMenuRoot');\r
- op.hide.call(root.$trigger, root);\r
- },\r
- // focus <command>\r
- focusItem: function(e) {\r
- e.stopPropagation();\r
- var $this = $(this),\r
- data = $this.data(),\r
- opt = data.contextMenu,\r
- root = data.contextMenuRoot;\r
-\r
- $this.addClass('hover')\r
- .siblings('.hover').trigger('contextmenu:blur');\r
- \r
- // remember selected\r
- opt.$selected = root.$selected = $this;\r
- \r
- // position sub-menu - do after show so dumb $.ui.position can keep up\r
- if (opt.$node) {\r
- root.positionSubmenu.call(opt.$node, opt.$menu);\r
- }\r
- },\r
- // blur <command>\r
- blurItem: function(e) {\r
- e.stopPropagation();\r
- var $this = $(this),\r
- data = $this.data(),\r
- opt = data.contextMenu,\r
- root = data.contextMenuRoot;\r
- \r
- $this.removeClass('hover');\r
- opt.$selected = null;\r
- }\r
- },\r
- // operations\r
- op = {\r
- show: function(opt, x, y) {\r
- var $this = $(this),\r
- offset,\r
- css = {};\r
-\r
- // hide any open menus\r
- $('#context-menu-layer').trigger('mousedown');\r
-\r
- // backreference for callbacks\r
- opt.$trigger = $this;\r
-\r
- // show event\r
- if (opt.events.show.call($this, opt) === false) {\r
- $currentTrigger = null;\r
- return;\r
- }\r
- \r
- // create or update context menu\r
- op.update.call($this, opt);\r
- \r
- // position menu\r
- opt.position.call($this, opt, x, y);\r
-\r
- // make sure we're in front\r
- if (opt.zIndex) {\r
- css.zIndex = zindex($this) + opt.zIndex;\r
- }\r
- \r
- // add layer\r
- op.layer.call(opt.$menu, opt, css.zIndex);\r
- \r
- // adjust sub-menu zIndexes\r
- opt.$menu.find('ul').css('zIndex', css.zIndex + 1);\r
- \r
- // position and show context menu\r
- opt.$menu.css( css )[opt.animation.show](opt.animation.duration);\r
- // make options available\r
- $this.data('contextMenu', opt);\r
- // register key handler\r
- $(document).off('keydown.contextMenu').on('keydown.contextMenu', handle.key);\r
- // register autoHide handler\r
- if (opt.autoHide) {\r
- // trigger element coordinates\r
- var pos = $this.position();\r
- pos.right = pos.left + $this.outerWidth();\r
- pos.bottom = pos.top + this.outerHeight();\r
- // mouse position handler\r
- $(document).on('mousemove.contextMenuAutoHide', function(e) {\r
- if (opt.$layer && !opt.hovering && (!(e.pageX >= pos.left && e.pageX <= pos.right) || !(e.pageY >= pos.top && e.pageY <= pos.bottom))) {\r
- // if mouse in menu...\r
- opt.$layer.trigger('mousedown');\r
- }\r
- });\r
- }\r
- },\r
- hide: function(opt) {\r
- var $this = $(this);\r
- if (!opt) {\r
- opt = $this.data('contextMenu') || {};\r
- }\r
- \r
- // hide event\r
- if (opt.events && opt.events.hide.call($this, opt) === false) {\r
- return;\r
- }\r
- \r
- if (opt.$layer) {\r
- // keep layer for a bit so the contextmenu event can be aborted properly by opera\r
- setTimeout((function($layer){ return function(){\r
- $layer.remove();\r
- };\r
- })(opt.$layer), 10);\r
- \r
- try {\r
- delete opt.$layer;\r
- } catch(e) {\r
- opt.$layer = null;\r
- }\r
- }\r
- \r
- // remove handle\r
- $currentTrigger = null;\r
- // remove selected\r
- opt.$menu.find('.hover').trigger('contextmenu:blur');\r
- opt.$selected = null;\r
- // unregister key and mouse handlers\r
- //$(document).off('.contextMenuAutoHide keydown.contextMenu'); // http://bugs.jquery.com/ticket/10705\r
- $(document).off('.contextMenuAutoHide').off('keydown.contextMenu');\r
- // hide menu\r
- opt.$menu && opt.$menu[opt.animation.hide](opt.animation.duration);\r
- \r
- // tear down dynamically built menu\r
- if (opt.build) {\r
- opt.$menu.remove();\r
- $.each(opt, function(key, value) {\r
- switch (key) {\r
- case 'ns':\r
- case 'selector':\r
- case 'build':\r
- case 'trigger':\r
- return true;\r
-\r
- default:\r
- opt[key] = undefined;\r
- try {\r
- delete opt[key];\r
- } catch (e) {}\r
- return true;\r
- }\r
- });\r
- }\r
- },\r
- create: function(opt, root) {\r
- if (root === undefined) {\r
- root = opt;\r
- }\r
- // create contextMenu\r
- opt.$menu = $('<ul class="context-menu-list ' + (opt.className || "") + '"></ul>').data({\r
- 'contextMenu': opt,\r
- 'contextMenuRoot': root\r
- });\r
- \r
- $.each(['callbacks', 'commands', 'inputs'], function(i,k){\r
- opt[k] = {};\r
- if (!root[k]) {\r
- root[k] = {};\r
- }\r
- });\r
- \r
- root.accesskeys || (root.accesskeys = {});\r
- \r
- // create contextMenu items\r
- $.each(opt.items, function(key, item){\r
- var $t = $('<li class="context-menu-item ' + (item.className || "") +'"></li>'),\r
- $label = null,\r
- $input = null;\r
- \r
- item.$node = $t.data({\r
- 'contextMenu': opt,\r
- 'contextMenuRoot': root,\r
- 'contextMenuKey': key\r
- });\r
- \r
- // register accesskey\r
- // NOTE: the accesskey attribute should be applicable to any element, but Safari5 and Chrome13 still can't do that\r
- if (item.accesskey) {\r
- var aks = splitAccesskey(item.accesskey);\r
- for (var i=0, ak; ak = aks[i]; i++) {\r
- if (!root.accesskeys[ak]) {\r
- root.accesskeys[ak] = item;\r
- item._name = item.name.replace(new RegExp('(' + ak + ')', 'i'), '<span class="context-menu-accesskey">$1</span>');\r
- break;\r
- }\r
- }\r
- }\r
- \r
- if (typeof item == "string") {\r
- $t.addClass('context-menu-separator not-selectable');\r
- } else if (item.type && types[item.type]) {\r
- // run custom type handler\r
- types[item.type].call($t, item, opt, root);\r
- // register commands\r
- $.each([opt, root], function(i,k){\r
- k.commands[key] = item;\r
- if ($.isFunction(item.callback)) {\r
- k.callbacks[key] = item.callback;\r
- }\r
- });\r
- } else {\r
- // add label for input\r
- if (item.type == 'html') {\r
- $t.addClass('context-menu-html not-selectable');\r
- } else if (item.type) {\r
- $label = $('<label></label>').appendTo($t);\r
- $('<span></span>').html(item._name || item.name).appendTo($label);\r
- $t.addClass('context-menu-input');\r
- opt.hasTypes = true;\r
- $.each([opt, root], function(i,k){\r
- k.commands[key] = item;\r
- k.inputs[key] = item;\r
- });\r
- } else if (item.items) {\r
- item.type = 'sub';\r
- }\r
- \r
- switch (item.type) {\r
- case 'text':\r
- $input = $('<input type="text" value="1" name="context-menu-input-'+ key +'" value="">')\r
- .val(item.value || "").appendTo($label);\r
- break;\r
- \r
- case 'textarea':\r
- $input = $('<textarea name="context-menu-input-'+ key +'"></textarea>')\r
- .val(item.value || "").appendTo($label);\r
-\r
- if (item.height) {\r
- $input.height(item.height);\r
- }\r
- break;\r
-\r
- case 'checkbox':\r
- $input = $('<input type="checkbox" value="1" name="context-menu-input-'+ key +'" value="">')\r
- .val(item.value || "").prop("checked", !!item.selected).prependTo($label);\r
- break;\r
-\r
- case 'radio':\r
- $input = $('<input type="radio" value="1" name="context-menu-input-'+ item.radio +'" value="">')\r
- .val(item.value || "").prop("checked", !!item.selected).prependTo($label);\r
- break;\r
- \r
- case 'select':\r
- $input = $('<select name="context-menu-input-'+ key +'">').appendTo($label);\r
- if (item.options) {\r
- $.each(item.options, function(value, text) {\r
- $('<option></option>').val(value).text(text).appendTo($input);\r
- });\r
- $input.val(item.selected);\r
- }\r
- break;\r
- \r
- case 'sub':\r
- $('<span></span>').html(item._name || item.name).appendTo($t);\r
- item.appendTo = item.$node;\r
- op.create(item, root);\r
- $t.data('contextMenu', item).addClass('context-menu-submenu');\r
- item.callback = null;\r
- break;\r
- \r
- case 'html':\r
- $(item.html).appendTo($t);\r
- break;\r
- \r
- default:\r
- $.each([opt, root], function(i,k){\r
- k.commands[key] = item;\r
- if ($.isFunction(item.callback)) {\r
- k.callbacks[key] = item.callback;\r
- }\r
- });\r
- \r
- $('<span></span>').html(item._name || item.name || "").appendTo($t);\r
- break;\r
- }\r
- \r
- // disable key listener in <input>\r
- if (item.type && item.type != 'sub' && item.type != 'html') {\r
- $input\r
- .on('focus', handle.focusInput)\r
- .on('blur', handle.blurInput);\r
- \r
- if (item.events) {\r
- $input.on(item.events);\r
- }\r
- }\r
- \r
- // add icons\r
- if (item.icon) {\r
- $t.addClass("icon icon-" + item.icon);\r
- }\r
- }\r
- \r
- // cache contained elements\r
- item.$input = $input;\r
- item.$label = $label;\r
-\r
- // attach item to menu\r
- $t.appendTo(opt.$menu);\r
- \r
- // Disable text selection\r
- if (!opt.hasTypes) {\r
- if($.browser.msie) {\r
- $t.on('selectstart.disableTextSelect', handle.abortevent);\r
- } else if(!$.browser.mozilla) {\r
- $t.on('mousedown.disableTextSelect', handle.abortevent);\r
- }\r
- }\r
- });\r
- // attach contextMenu to <body> (to bypass any possible overflow:hidden issues on parents of the trigger element)\r
- if (!opt.$node) {\r
- opt.$menu.css('display', 'none').addClass('context-menu-root');\r
- }\r
- opt.$menu.appendTo(opt.appendTo || document.body);\r
- },\r
- update: function(opt, root) {\r
- var $this = this;\r
- if (root === undefined) {\r
- root = opt;\r
- // determine widths of submenus, as CSS won't grow them automatically\r
- // position:absolute > position:absolute; min-width:100; max-width:200; results in width: 100;\r
- // kinda sucks hard...\r
- opt.$menu.find('ul').andSelf().css({position: 'static', display: 'block'}).each(function(){\r
- var $this = $(this);\r
- $this.width($this.css('position', 'absolute').width())\r
- .css('position', 'static');\r
- }).css({position: '', display: ''});\r
- }\r
- // re-check disabled for each item\r
- opt.$menu.children().each(function(){\r
- var $item = $(this),\r
- key = $item.data('contextMenuKey'),\r
- item = opt.items[key],\r
- disabled = ($.isFunction(item.disabled) && item.disabled.call($this, key, root)) || item.disabled === true;\r
-\r
- // dis- / enable item\r
- $item[disabled ? 'addClass' : 'removeClass']('disabled');\r
- \r
- if (item.type) {\r
- // dis- / enable input elements\r
- $item.find('input, select, textarea').prop('disabled', disabled);\r
- \r
- // update input states\r
- switch (item.type) {\r
- case 'text':\r
- case 'textarea':\r
- item.$input.val(item.value || "");\r
- break;\r
- \r
- case 'checkbox':\r
- case 'radio':\r
- item.$input.val(item.value || "").prop('checked', !!item.selected);\r
- break;\r
- \r
- case 'select':\r
- item.$input.val(item.selected || "");\r
- break;\r
- }\r
- }\r
- \r
- if (item.$menu) {\r
- // update sub-menu\r
- op.update.call($this, item, root);\r
- }\r
- });\r
- },\r
- layer: function(opt, zIndex) {\r
- // add transparent layer for click area\r
- // filter and background for Internet Explorer, Issue #23\r
- return 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>')\r
- .css({height: $win.height(), width: $win.width(), display: 'block'})\r
- .data('contextMenuRoot', opt)\r
- .insertBefore(this)\r
- .on('contextmenu', handle.abortevent)\r
- .on('mousedown', handle.layerClick);\r
- }\r
- };\r
-\r
-// split accesskey according to http://www.whatwg.org/specs/web-apps/current-work/multipage/editing.html#assigned-access-key\r
-function splitAccesskey(val) {\r
- var t = val.split(/\s+/),\r
- keys = [];\r
- \r
- for (var i=0, k; k = t[i]; i++) {\r
- k = k[0].toUpperCase(); // first character only\r
- // theoretically non-accessible characters should be ignored, but different systems, different keyboard layouts, ... screw it.\r
- // a map to look up already used access keys would be nice\r
- keys.push(k);\r
- }\r
- \r
- return keys;\r
-}\r
-\r
-// handle contextMenu triggers\r
-$.fn.contextMenu = function(operation) {\r
- if (operation === undefined) {\r
- this.first().trigger('contextmenu');\r
- } else if (operation.x && operation.y) {\r
- this.first().trigger(jQuery.Event("contextmenu", {pageX: operation.x, pageY: operation.y}));\r
- } else if (operation === "hide") {\r
- var $menu = this.data('contextMenu').$menu;\r
- $menu && $menu.trigger('contextmenu:hide');\r
- } else if (operation) {\r
- this.removeClass('context-menu-disabled');\r
- } else if (!operation) {\r
- this.addClass('context-menu-disabled');\r
- }\r
- \r
- return this;\r
-};\r
-\r
-// manage contextMenu instances\r
-$.contextMenu = function(operation, options) {\r
- if (typeof operation != 'string') {\r
- options = operation;\r
- operation = 'create';\r
- }\r
- \r
- if (typeof options == 'string') {\r
- options = {selector: options};\r
- } else if (options === undefined) {\r
- options = {};\r
- }\r
- \r
- // merge with default options\r
- var o = $.extend(true, {}, defaults, options || {}),\r
- $body = $body = $(document);\r
- \r
- switch (operation) {\r
- case 'create':\r
- // no selector no joy\r
- if (!o.selector) {\r
- throw new Error('No selector specified');\r
- }\r
- // make sure internal classes are not bound to\r
- if (o.selector.match(/.context-menu-(list|item|input)($|\s)/)) {\r
- throw new Error('Cannot bind to selector "' + o.selector + '" as it contains a reserved className');\r
- }\r
- if (!o.build && (!o.items || $.isEmptyObject(o.items))) {\r
- throw new Error('No Items sepcified');\r
- }\r
- counter ++;\r
- o.ns = '.contextMenu' + counter;\r
- namespaces[o.selector] = o.ns;\r
- menus[o.ns] = o;\r
- \r
- if (!initialized) {\r
- // make sure item click is registered first\r
- $body\r
- .on({\r
- 'contextmenu:hide.contextMenu': handle.hideMenu,\r
- 'prevcommand.contextMenu': handle.prevItem,\r
- 'nextcommand.contextMenu': handle.nextItem,\r
- 'contextmenu.contextMenu': handle.abortevent,\r
- 'mouseenter.contextMenu': handle.menuMouseenter,\r
- 'mouseleave.contextMenu': handle.menuMouseleave\r
- }, '.context-menu-list')\r
- .on('mouseup.contextMenu', '.context-menu-input', handle.inputClick)\r
- .on({\r
- 'mouseup.contextMenu': handle.itemClick,\r
- 'contextmenu:focus.contextMenu': handle.focusItem,\r
- 'contextmenu:blur.contextMenu': handle.blurItem,\r
- 'contextmenu.contextMenu': handle.abortevent,\r
- 'mouseenter.contextMenu': handle.itemMouseenter,\r
- 'mouseleave.contextMenu': handle.itemMouseleave\r
- }, '.context-menu-item');\r
-\r
- initialized = true;\r
- }\r
- \r
- // engage native contextmenu event\r
- $body\r
- .on('contextmenu' + o.ns, o.selector, o, handle.contextmenu);\r
- \r
- switch (o.trigger) {\r
- case 'hover':\r
- $body\r
- .on('mouseenter' + o.ns, o.selector, o, handle.mouseenter)\r
- .on('mouseleave' + o.ns, o.selector, o, handle.mouseleave); \r
- break;\r
- \r
- case 'left':\r
- $body.on('click' + o.ns, o.selector, o, handle.click);\r
- break;\r
- /*\r
- default:\r
- // http://www.quirksmode.org/dom/events/contextmenu.html\r
- $body\r
- .on('mousedown' + o.ns, o.selector, o, handle.mousedown)\r
- .on('mouseup' + o.ns, o.selector, o, handle.mouseup);\r
- break;\r
- */\r
- }\r
- \r
- // create menu\r
- if (!o.build) {\r
- op.create(o);\r
- }\r
- break;\r
- \r
- case 'destroy':\r
- if (!o.selector) {\r
- $body.off('.contextMenu .contextMenuAutoHide');\r
- $.each(namespaces, function(key, value) {\r
- $body.off(value);\r
- });\r
- \r
- namespaces = {};\r
- menus = {};\r
- counter = 0;\r
- initialized = false;\r
- \r
- $('.context-menu-list').remove();\r
- } else if (namespaces[o.selector]) {\r
- try {\r
- if (menus[namespaces[o.selector]].$menu) {\r
- menus[namespaces[o.selector]].$menu.remove();\r
- }\r
- \r
- delete menus[namespaces[o.selector]];\r
- } catch(e) {\r
- menus[namespaces[o.selector]] = null;\r
- }\r
- \r
- $body.off(namespaces[o.selector]);\r
- }\r
- break;\r
- \r
- case 'html5':\r
- // if <command> or <menuitem> are not handled by the browser,\r
- // or options was a bool true,\r
- // initialize $.contextMenu for them\r
- if ((!$.support.htmlCommand && !$.support.htmlMenuitem) || (typeof options == "boolean" && options)) {\r
- $('menu[type="context"]').each(function() {\r
- if (this.id) {\r
- $.contextMenu({\r
- selector: '[contextmenu=' + this.id +']',\r
- items: $.contextMenu.fromMenu(this)\r
- });\r
- }\r
- }).css('display', 'none');\r
- }\r
- break;\r
- \r
- default:\r
- throw new Error('Unknown operation "' + operation + '"');\r
- }\r
- \r
- return this;\r
-};\r
-\r
-// import values into <input> commands\r
-$.contextMenu.setInputValues = function(opt, data) {\r
- if (data === undefined) {\r
- data = {};\r
- }\r
- \r
- $.each(opt.inputs, function(key, item) {\r
- switch (item.type) {\r
- case 'text':\r
- case 'textarea':\r
- item.value = data[key] || "";\r
- break;\r
-\r
- case 'checkbox':\r
- item.selected = data[key] ? true : false;\r
- break;\r
- \r
- case 'radio':\r
- item.selected = (data[item.radio] || "") == item.value ? true : false;\r
- break;\r
- \r
- case 'select':\r
- item.selected = data[key] || "";\r
- break;\r
- }\r
- });\r
-};\r
-\r
-// export values from <input> commands\r
-$.contextMenu.getInputValues = function(opt, data) {\r
- if (data === undefined) {\r
- data = {};\r
- }\r
- \r
- $.each(opt.inputs, function(key, item) {\r
- switch (item.type) {\r
- case 'text':\r
- case 'textarea':\r
- case 'select':\r
- data[key] = item.$input.val();\r
- break;\r
-\r
- case 'checkbox':\r
- data[key] = item.$input.prop('checked');\r
- break;\r
- \r
- case 'radio':\r
- if (item.$input.prop('checked')) {\r
- data[item.radio] = item.value;\r
- }\r
- break;\r
- }\r
- });\r
- \r
- return data;\r
-};\r
-\r
-// find <label for="xyz">\r
-function inputLabel(node) {\r
- return (node.id && $('label[for="'+ node.id +'"]').val()) || node.name;\r
-}\r
-\r
-// convert <menu> to items object\r
-function menuChildren(items, $children, counter) {\r
- if (!counter) {\r
- counter = 0;\r
- }\r
- \r
- $children.each(function() {\r
- var $node = $(this),\r
- node = this,\r
- nodeName = this.nodeName.toLowerCase(),\r
- label,\r
- item;\r
- \r
- // extract <label><input>\r
- if (nodeName == 'label' && $node.find('input, textarea, select').length) {\r
- label = $node.text();\r
- $node = $node.children().first();\r
- node = $node.get(0);\r
- nodeName = node.nodeName.toLowerCase();\r
- }\r
- \r
- /*\r
- * <menu> accepts flow-content as children. that means <embed>, <canvas> and such are valid menu items.\r
- * Not being the sadistic kind, $.contextMenu only accepts:\r
- * <command>, <menuitem>, <hr>, <span>, <p> <input [text, radio, checkbox]>, <textarea>, <select> and of course <menu>.\r
- * Everything else will be imported as an html node, which is not interfaced with contextMenu.\r
- */\r
- \r
- // http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#concept-command\r
- switch (nodeName) {\r
- // http://www.whatwg.org/specs/web-apps/current-work/multipage/interactive-elements.html#the-menu-element\r
- case 'menu':\r
- item = {name: $node.attr('label'), items: {}};\r
- menuChildren(item.items, $node.children(), counter);\r
- break;\r
- \r
- // http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#using-the-a-element-to-define-a-command\r
- case 'a':\r
- // http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#using-the-button-element-to-define-a-command\r
- case 'button':\r
- item = {\r
- name: $node.text(),\r
- disabled: !!$node.attr('disabled'),\r
- callback: (function(){ return function(){ $node.click(); }; })()\r
- };\r
- break;\r
- \r
- // http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#using-the-command-element-to-define-a-command\r
-\r
- case 'menuitem':\r
- case 'command':\r
- switch ($node.attr('type')) {\r
- case undefined:\r
- case 'command':\r
- case 'menuitem':\r
- item = {\r
- name: $node.attr('label'),\r
- disabled: !!$node.attr('disabled'),\r
- callback: (function(){ return function(){ $node.click(); }; })()\r
- };\r
- break;\r
- \r
- case 'checkbox':\r
- item = {\r
- type: 'checkbox',\r
- disabled: !!$node.attr('disabled'),\r
- name: $node.attr('label'),\r
- selected: !!$node.attr('checked')\r
- };\r
- break;\r
- \r
- case 'radio':\r
- item = {\r
- type: 'radio',\r
- disabled: !!$node.attr('disabled'),\r
- name: $node.attr('label'),\r
- radio: $node.attr('radiogroup'),\r
- value: $node.attr('id'),\r
- selected: !!$node.attr('checked')\r
- };\r
- break;\r
- \r
- default:\r
- item = undefined;\r
- }\r
- break;\r
- \r
- case 'hr':\r
- item = '-------';\r
- break;\r
- \r
- case 'input':\r
- switch ($node.attr('type')) {\r
- case 'text':\r
- item = {\r
- type: 'text',\r
- name: label || inputLabel(node),\r
- disabled: !!$node.attr('disabled'),\r
- value: $node.val()\r
- };\r
- break;\r
- \r
- case 'checkbox':\r
- item = {\r
- type: 'checkbox',\r
- name: label || inputLabel(node),\r
- disabled: !!$node.attr('disabled'),\r
- selected: !!$node.attr('checked')\r
- };\r
- break;\r
- \r
- case 'radio':\r
- item = {\r
- type: 'radio',\r
- name: label || inputLabel(node),\r
- disabled: !!$node.attr('disabled'),\r
- radio: !!$node.attr('name'),\r
- value: $node.val(),\r
- selected: !!$node.attr('checked')\r
- };\r
- break;\r
- \r
- default:\r
- item = undefined;\r
- break;\r
- }\r
- break;\r
- \r
- case 'select':\r
- item = {\r
- type: 'select',\r
- name: label || inputLabel(node),\r
- disabled: !!$node.attr('disabled'),\r
- selected: $node.val(),\r
- options: {}\r
- };\r
- $node.children().each(function(){\r
- item.options[this.value] = $(this).text();\r
- });\r
- break;\r
- \r
- case 'textarea':\r
- item = {\r
- type: 'textarea',\r
- name: label || inputLabel(node),\r
- disabled: !!$node.attr('disabled'),\r
- value: $node.val()\r
- };\r
- break;\r
- \r
- case 'label':\r
- break;\r
- \r
- default:\r
- item = {type: 'html', html: $node.clone(true)};\r
- break;\r
- }\r
- \r
- if (item) {\r
- counter++;\r
- items['key' + counter] = item;\r
- }\r
- });\r
-}\r
-\r
-// convert html5 menu\r
-$.contextMenu.fromMenu = function(element) {\r
- var $this = $(element),\r
- items = {};\r
- \r
- menuChildren(items, $this.children());\r
- \r
- return items;\r
-};\r
-\r
-// make defaults accessible\r
-$.contextMenu.defaults = defaults;\r
-$.contextMenu.types = types;\r
-\r
-})(jQuery);\r