From: Marcin Koziej Date: Wed, 9 May 2012 09:13:16 +0000 (+0200) Subject: add a context menu package X-Git-Url: https://git.mdrn.pl/redakcja.git/commitdiff_plain/755ea0d8b59e47d37b91fcbf8d58d46e1cf2f1eb?ds=sidebyside add a context menu package --- diff --git a/redakcja/static/contextmenu/images/cut.png b/redakcja/static/contextmenu/images/cut.png new file mode 100755 index 00000000..f215d6f6 Binary files /dev/null and b/redakcja/static/contextmenu/images/cut.png differ diff --git a/redakcja/static/contextmenu/images/door.png b/redakcja/static/contextmenu/images/door.png new file mode 100755 index 00000000..369fc46e Binary files /dev/null and b/redakcja/static/contextmenu/images/door.png differ diff --git a/redakcja/static/contextmenu/images/page_white_copy.png b/redakcja/static/contextmenu/images/page_white_copy.png new file mode 100755 index 00000000..a9f31a27 Binary files /dev/null and b/redakcja/static/contextmenu/images/page_white_copy.png differ diff --git a/redakcja/static/contextmenu/images/page_white_delete.png b/redakcja/static/contextmenu/images/page_white_delete.png new file mode 100755 index 00000000..af1ecaf2 Binary files /dev/null and b/redakcja/static/contextmenu/images/page_white_delete.png differ diff --git a/redakcja/static/contextmenu/images/page_white_edit.png b/redakcja/static/contextmenu/images/page_white_edit.png new file mode 100755 index 00000000..b93e7760 Binary files /dev/null and b/redakcja/static/contextmenu/images/page_white_edit.png differ diff --git a/redakcja/static/contextmenu/images/page_white_paste.png b/redakcja/static/contextmenu/images/page_white_paste.png new file mode 100755 index 00000000..5b2cbb3f Binary files /dev/null and b/redakcja/static/contextmenu/images/page_white_paste.png differ diff --git a/redakcja/static/contextmenu/index.html b/redakcja/static/contextmenu/index.html new file mode 100644 index 00000000..f099c959 --- /dev/null +++ b/redakcja/static/contextmenu/index.html @@ -0,0 +1,200 @@ + + + + + + jQuery Context Menu Plugin Demo + + + + + + + + + + + + + +

jQuery Context Menu Plugin Demo

+

+ This plugin lets you add context menu functionality to your web applications. +

+ +

+ Tip: Try using your keyboard to make a selection. +

+ +

+ Back to the project page +

+ +

Demo

+ +
+ Right click to view the context menu +
+ +
+ +
+ +
+

+ + +

+ +

+ + +

+
+ + + + + diff --git a/redakcja/static/contextmenu/jquery.contextMenu.css b/redakcja/static/contextmenu/jquery.contextMenu.css new file mode 100755 index 00000000..46af2487 --- /dev/null +++ b/redakcja/static/contextmenu/jquery.contextMenu.css @@ -0,0 +1,134 @@ +/*! + * jQuery contextMenu - Plugin for simple contextMenu handling + * + * Version: 1.5.13 + * + * Authors: Rodney Rehm, Addy Osmani (patches for FF) + * Web: http://medialize.github.com/jQuery-contextMenu/ + * + * Licensed under + * MIT License http://www.opensource.org/licenses/mit-license + * GPL v3 http://opensource.org/licenses/GPL-3.0 + * + */ + +.context-menu-list { + margin:0; + padding:0; + + min-width: 120px; + max-width: 250px; + display: inline-block; + position: absolute; + list-style-type: none; + + border: 1px solid #DDD; + background: #EEE; + + -webkit-box-shadow: 0 2px 5px rgba(0, 0, 0, 0.5); + -moz-box-shadow: 0 2px 5px rgba(0, 0, 0, 0.5); + -ms-box-shadow: 0 2px 5px rgba(0, 0, 0, 0.5); + -o-box-shadow: 0 2px 5px rgba(0, 0, 0, 0.5); + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.5); + + font-family: Verdana, Arial, Helvetica, sans-serif; + font-size: 11px; +} + +.context-menu-item { + padding: 2px 2px 2px 24px; + background-color: #EEE; + position: relative; + -moz-user-select: -moz-none; +} + +.context-menu-separator { + padding-bottom:0; + border-bottom: 1px solid #DDD; +} + +.context-menu-item > label { + -moz-user-select: text; +} + +.context-menu-item.hover { + cursor: pointer; + background-color: #39F; +} + +.context-menu-item.disabled { + color: #666; +} + +.context-menu-input.hover, +.context-menu-item.disabled.hover { + cursor: default; + background-color: #EEE; +} + +.context-menu-submenu:after { + content: ">"; + color: #666; + position: absolute; + top: 0; + right: 3px; + z-index: 1; +} + +/* icons + #protip: + In case you want to use sprites for icons (which I would suggest you do) have a look at + http://css-tricks.com/13224-pseudo-spriting/ to get an idea of how to implement + .context-menu-item.icon:before {} + */ +.context-menu-item.icon { min-height: 18px; background-repeat: no-repeat; background-position: 4px 2px; } +.context-menu-item.icon-edit { background-image: url(images/page_white_edit.png); } +.context-menu-item.icon-cut { background-image: url(images/cut.png); } +.context-menu-item.icon-copy { background-image: url(images/page_white_copy.png); } +.context-menu-item.icon-paste { background-image: url(images/page_white_paste.png); } +.context-menu-item.icon-delete { background-image: url(images/page_white_delete.png); } +.context-menu-item.icon-quit { background-image: url(images/door.png); } + +/* vertically align inside labels */ +.context-menu-input > label > * { vertical-align: top; } + +/* position checkboxes and radios as icons */ +.context-menu-input > label > input[type="checkbox"], +.context-menu-input > label > input[type="radio"] { + margin-left: -17px; +} +.context-menu-input > label > span { + margin-left: 5px; +} + +.context-menu-input > label, +.context-menu-input > label > input[type="text"], +.context-menu-input > label > textarea, +.context-menu-input > label > select { + display: block; + width: 100%; + + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + -ms-box-sizing: border-box; + -o-box-sizing: border-box; + box-sizing: border-box; +} + +.context-menu-input > label > textarea { + height: 100px; +} +.context-menu-item > .context-menu-list { + display: none; + /* re-positioned by js */ + right: -5px; + top: 5px; +} + +.context-menu-item.hover > .context-menu-list { + display: block; +} + +.context-menu-accesskey { + text-decoration: underline; +} diff --git a/redakcja/static/contextmenu/jquery.contextMenu.js b/redakcja/static/contextmenu/jquery.contextMenu.js new file mode 100755 index 00000000..53c3fe93 --- /dev/null +++ b/redakcja/static/contextmenu/jquery.contextMenu.js @@ -0,0 +1,1485 @@ +/*! + * jQuery contextMenu - Plugin for simple contextMenu handling + * + * Version: 1.5.13 + * + * Authors: Rodney Rehm, Addy Osmani (patches for FF) + * Web: http://medialize.github.com/jQuery-contextMenu/ + * + * Licensed under + * MIT License http://www.opensource.org/licenses/mit-license + * GPL v3 http://opensource.org/licenses/GPL-3.0 + * + */ + +(function($, undefined){ + + // TODO: - + // ARIA stuff: menuitem, menuitemcheckbox und menuitemradio + // create structure if $.support[htmlCommand || htmlMenuitem] and !opt.disableNative + +// determine html5 compatibility +$.support.htmlMenuitem = ('HTMLMenuItemElement' in window); +$.support.htmlCommand = ('HTMLCommandElement' in window); + +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, + // 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 $this = this, + 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) + var triggerIsFixed = opt.$trigger.parents().andSelf() + .filter(function() { + return $(this).css('position') == "fixed"; + }).length; + + if (triggerIsFixed) { + y -= $win.scrollTop(); + x -= $win.scrollLeft(); + } + 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.height(), + width = opt.$menu.width(); + + if (offset.top + height > bottom) { + offset.top -= height; + } + + 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: "fit" + }).css('display', ''); + } else { + // determine contextMenu position + var offset = this.offset(); + offset.top += 0; + offset.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 || $tt.prop('nodeName').toLowerCase() == 'body') { + 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 + e.preventDefault(); + e.stopImmediatePropagation(); + + // abort native-triggered events unless we're triggering on right click + if (e.data.trigger != 'right' && e.originalEvent) { + return; + } + + if (!$this.hasClass('context-menu-disabled')) { + // theoretically need to fire a show event at + // 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)("No items specified to show in contextMenu"); + } + + throw new Error('No Items sepcified'); + } + + // backreference for custom command type creation + e.data.$trigger = $currentTrigger; + + op.create(e.data); + } + // 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(jQuery.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(jQuery.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(jQuery.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'); + + e.preventDefault(); + e.stopImmediatePropagation(); + + if ((root.trigger == 'left' && e.button == 0) || (root.trigger == 'right' && e.button == 2)) { + var offset = root.$trigger.offset(); + + // 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, e.pageX, e.pageY); + return; + } + } + } + } + } + + // remove only after mouseup has completed + $this.on('mouseup', function(e) { + e.preventDefault(); + e.stopImmediatePropagation(); + root.$menu.trigger('contextmenu:hide'); + }); + }, + // key handled :hover + keyStop: function(e, opt) { + if (!opt.isInput) { + e.preventDefault(); + } + + e.stopPropagation(); + }, + key: function(e) { + var opt = $currentTrigger.data('contextMenu') || {}, + $children = opt.$menu.children(), + $round; + + 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; + } + + case 9: // tab + 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; + } + opt.$selected && 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[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(); + opt.$selected && 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(e) { + 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(e) { + 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(e) { + 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)) { + root.$selected && 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.hasClass('disabled') || $this.hasClass('context-menu-submenu')) { + return; + } + + e.preventDefault(); + e.stopImmediatePropagation(); + + if ($.isFunction(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 { + op.update.call(root.$trigger, root); + } + }, + // ignore click events on input elements + inputClick: function(e) { + e.stopImmediatePropagation(); + }, + + // hide + hideMenu: function(e) { + var root = $(this).data('contextMenuRoot'); + op.hide.call(root.$trigger, root); + }, + // focus + focusItem: function(e) { + e.stopPropagation(); + var $this = $(this), + data = $this.data(), + opt = data.contextMenu, + root = data.contextMenuRoot; + + $this.addClass('hover') + .siblings('.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 + blurItem: function(e) { + e.stopPropagation(); + var $this = $(this), + data = $this.data(), + opt = data.contextMenu, + root = data.contextMenuRoot; + + $this.removeClass('hover'); + opt.$selected = null; + } + }, + // operations + op = { + show: function(opt, x, y) { + var $this = $(this), + offset, + css = {}; + + // hide any open menus + $('#context-menu-layer').trigger('mousedown'); + + // backreference for callbacks + opt.$trigger = $this; + + // show event + if (opt.events.show.call($this, opt) === false) { + $currentTrigger = null; + return; + } + + // create or update context menu + op.update.call($this, opt); + + // position menu + opt.position.call($this, opt, x, y); + + // make sure we're in front + if (opt.zIndex) { + css.zIndex = zindex($this) + 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); + // make options available + $this.data('contextMenu', opt); + // register key handler + $(document).off('keydown.contextMenu').on('keydown.contextMenu', handle.key); + // register autoHide handler + if (opt.autoHide) { + // trigger element coordinates + var pos = $this.position(); + pos.right = pos.left + $this.outerWidth(); + pos.bottom = pos.top + this.outerHeight(); + // mouse position handler + $(document).on('mousemove.contextMenuAutoHide', function(e) { + 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.$layer.trigger('mousedown'); + } + }); + } + }, + hide: function(opt) { + var $this = $(this); + if (!opt) { + opt = $this.data('contextMenu') || {}; + } + + // hide event + if (opt.events && opt.events.hide.call($this, opt) === false) { + return; + } + + 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); + + // tear down dynamically built menu + if (opt.build) { + opt.$menu.remove(); + $.each(opt, function(key, value) { + switch (key) { + case 'ns': + case 'selector': + case 'build': + case 'trigger': + return true; + + default: + opt[key] = undefined; + try { + delete opt[key]; + } catch (e) {} + return true; + } + }); + } + }, + create: function(opt, root) { + if (root === undefined) { + root = opt; + } + // create contextMenu + opt.$menu = $('
    ').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 = $('
  • '), + $label = null, + $input = null; + + 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 (item.accesskey) { + 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'), '$1'); + break; + } + } + } + + if (typeof item == "string") { + $t.addClass('context-menu-separator not-selectable'); + } else 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 == 'html') { + $t.addClass('context-menu-html not-selectable'); + } else if (item.type) { + $label = $('').appendTo($t); + $('').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 'text': + $input = $('') + .val(item.value || "").appendTo($label); + break; + + case 'textarea': + $input = $('') + .val(item.value || "").appendTo($label); + + if (item.height) { + $input.height(item.height); + } + break; + + case 'checkbox': + $input = $('') + .val(item.value || "").prop("checked", !!item.selected).prependTo($label); + break; + + case 'radio': + $input = $('') + .val(item.value || "").prop("checked", !!item.selected).prependTo($label); + break; + + case 'select': + $input = $(' + if (item.type && item.type != 'sub' && item.type != 'html') { + $input + .on('focus', handle.focusInput) + .on('blur', handle.blurInput); + + if (item.events) { + $input.on(item.events); + } + } + + // add icons + if (item.icon) { + $t.addClass("icon icon-" + 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) { + if($.browser.msie) { + $t.on('selectstart.disableTextSelect', handle.abortevent); + } else if(!$.browser.mozilla) { + $t.on('mousedown.disableTextSelect', handle.abortevent); + } + } + }); + // attach contextMenu to (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); + }, + update: function(opt, root) { + var $this = this; + if (root === undefined) { + root = opt; + // determine widths of submenus, as CSS won't grow them automatically + // position:absolute > position:absolute; min-width:100; max-width:200; results in width: 100; + // kinda sucks hard... + opt.$menu.find('ul').andSelf().css({position: 'static', display: 'block'}).each(function(){ + var $this = $(this); + $this.width($this.css('position', 'absolute').width()) + .css('position', 'static'); + }).css({position: '', display: ''}); + } + // 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($this, key, root)) || item.disabled === true; + + // dis- / enable item + $item[disabled ? 'addClass' : 'removeClass']('disabled'); + + 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($this, item, root); + } + }); + }, + layer: function(opt, zIndex) { + // add transparent layer for click area + // filter and background for Internet Explorer, Issue #23 + return opt.$layer = $('
    ') + .css({height: $win.height(), width: $win.width(), display: 'block'}) + .data('contextMenuRoot', opt) + .insertBefore(this) + .on('contextmenu', handle.abortevent) + .on('mousedown', handle.layerClick); + } + }; + +// 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[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) { + if (operation === undefined) { + this.first().trigger('contextmenu'); + } else if (operation.x && operation.y) { + this.first().trigger(jQuery.Event("contextmenu", {pageX: operation.x, pageY: operation.y})); + } else if (operation === "hide") { + var $menu = this.data('contextMenu').$menu; + $menu && $menu.trigger('contextmenu:hide'); + } else if (operation) { + this.removeClass('context-menu-disabled'); + } else if (!operation) { + this.addClass('context-menu-disabled'); + } + + 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 || {}), + $body = $body = $(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 sepcified'); + } + counter ++; + o.ns = '.contextMenu' + counter; + namespaces[o.selector] = o.ns; + menus[o.ns] = o; + + if (!initialized) { + // make sure item click is registered first + $body + .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 + $body + .on('contextmenu' + o.ns, o.selector, o, handle.contextmenu); + + switch (o.trigger) { + case 'hover': + $body + .on('mouseenter' + o.ns, o.selector, o, handle.mouseenter) + .on('mouseleave' + o.ns, o.selector, o, handle.mouseleave); + break; + + case 'left': + $body.on('click' + o.ns, o.selector, o, handle.click); + break; + /* + default: + // http://www.quirksmode.org/dom/events/contextmenu.html + $body + .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': + if (!o.selector) { + $body.off('.contextMenu .contextMenuAutoHide'); + $.each(namespaces, function(key, value) { + $body.off(value); + }); + + namespaces = {}; + menus = {}; + counter = 0; + initialized = false; + + $('.context-menu-list').remove(); + } else if (namespaces[o.selector]) { + 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; + } + + $body.off(namespaces[o.selector]); + } + break; + + case 'html5': + // if or 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 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 ? true : false; + break; + + case 'select': + item.selected = data[key] || ""; + break; + } + }); +}; + +// export values from 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