loleaflet/.eslintignore | 1 loleaflet/Makefile.am | 2 loleaflet/js/jquery.contextMenu.js | 1928 +++++++++++++++++++++++++++++++++++++ 3 files changed, 1930 insertions(+), 1 deletion(-)
New commits: commit 2ea141c02f48d5ec82f69691ef045c7c036f0752 Author: Henry Castro <hcas...@collabora.com> AuthorDate: Thu May 2 16:29:21 2019 -0400 Commit: Henry Castro <hcas...@collabora.com> CommitDate: Thu May 2 18:19:59 2019 -0400 loleaflet: move jquery.contextMenu.js to loleaflet/js for next patch I require to modify the contextMenu to trigger on touch start event. I should do updating archived-packages, but ... Change-Id: I20f1fbda1e7e60f97cd790c055599edcf8da9d1a diff --git a/loleaflet/.eslintignore b/loleaflet/.eslintignore index 6f11545b6..c7d0ae184 100644 --- a/loleaflet/.eslintignore +++ b/loleaflet/.eslintignore @@ -5,3 +5,4 @@ # # This is way too ugly for eslint... **/js/jquery.mCustomScrollbar.js +**/js/jquery.contextMenu.js diff --git a/loleaflet/Makefile.am b/loleaflet/Makefile.am index d4ac0e620..9d46d96a7 100644 --- a/loleaflet/Makefile.am +++ b/loleaflet/Makefile.am @@ -91,7 +91,6 @@ NODE_MODULES_JS =\ node_modules/hammerjs/hammer.min.js \ node_modules/jquery/dist/jquery.js \ node_modules/jquery-mousewheel/jquery.mousewheel.js \ - node_modules/jquery-contextmenu/dist/jquery.contextMenu.js \ node_modules/jquery-ui/jquery-ui.js \ node_modules/smartmenus/dist/jquery.smartmenus.js \ node_modules/autolinker/dist/Autolinker.js \ @@ -102,6 +101,7 @@ NODE_MODULES_JS =\ node_modules/@braintree/sanitize-url/dist.js LOLEAFLET_LIBS_JS =\ + jquery.contextMenu.js \ jquery.mCustomScrollbar.js \ w2ui-1.5.rc1.js diff --git a/loleaflet/js/jquery.contextMenu.js b/loleaflet/js/jquery.contextMenu.js new file mode 100644 index 000000000..881ee67f0 --- /dev/null +++ b/loleaflet/js/jquery.contextMenu.js @@ -0,0 +1,1928 @@ +/*! + * jQuery contextMenu v2.2.3 - Plugin for simple contextMenu handling + * + * Version: v2.2.3 + * + * Authors: Björn Brala (SWIS.nl), Rodney Rehm, Addy Osmani (patches for FF) + * Web: http://swisnl.github.io/jQuery-contextMenu/ + * + * Copyright (c) 2011-2016 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: 2016-07-17T19:36:02.968Z + */ + +(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; + })(); + */ + + /* jshint ignore:start */ + 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; elems[i] != null; i++) { + elem = elems[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); + } + /* jshint ignore:end */ + + 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, + + // Default classname configuration to be able avoid conflicts in frameworks + classNames : { + + hover: 'context-menu-hover', // Item hover + disabled: 'context-menu-disabled', // Item disabled + visible: 'context-menu-visible', // Item visible + notSelectable: 'context-menu-not-selectable', // Item not selectable + + icon: 'context-menu-icon', + iconEdit: 'context-menu-icon-edit', + iconCut: 'context-menu-icon-cut', + iconCopy: 'context-menu-icon-copy', + iconPaste: 'context-menu-icon-paste', + iconDelete: 'context-menu-icon-delete', + iconAdd: 'context-menu-icon-add', + iconQuit: 'context-menu-icon-quit' + }, + + // 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; + } + + if (offset.left < 0) { + offset.left = 0; + } + + 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; + } + + // Let the current contextmenu decide if it should show or not based on its own trigger settings + if (e.mouseButton !== undefined && e.data) { + if (!(e.data.trigger === 'left' && e.mouseButton === 0) && !(e.data.trigger === 'right' && e.mouseButton === 2)) { + // Mouse click is not valid. + 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) { + 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, button: button }); + }); + } + + if (root != null && root.$menu != null) { + 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') || {}; + } + // If the trigger happen on a element that are above the contextmenu do this + if (opt.zIndex === undefined) { + opt.zIndex = 0; + } + var targetZIndex = 0; + var getZIndexOfTriggerTarget = function (target) { + if (target.style.zIndex !== '') { + targetZIndex = target.style.zIndex; + } else { + if (target.offsetParent !== null && target.offsetParent !== undefined) { + getZIndexOfTriggerTarget(target.offsetParent); + } + else if (target.parentElement !== null && target.parentElement !== undefined) { + getZIndexOfTriggerTarget(target.parentElement); + } + } + }; + getZIndexOfTriggerTarget(e.target); + // If targetZIndex is heigher then opt.zIndex dont progress any futher. + // This is used to make sure that if you are using a dialog with a input / textarea / contenteditable div + // and its above the contextmenu it wont steal keys events + if (targetZIndex > opt.zIndex) { + return; + } + 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(); + if(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; + } + break; + // 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(); + if(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(.' + opt.classNames.disabled + ', .' + opt.classNames.notSelectable + ')')[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 !== null) { + 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 !== null) { + opt.$selected.trigger(e); + } + }, + // select previous possible command in menu + prevItem: function (e) { + e.stopPropagation(); + var opt = $(this).data('contextMenu') || {}; + var root = $(this).data('contextMenuRoot') || {}; + + // 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 or hidden elements + while ($prev.hasClass(root.classNames.disabled) || $prev.hasClass(root.classNames.notSelectable) || $prev.is(':hidden')) { + 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') || {}; + var root = $(this).data('contextMenuRoot') || {}; + + // 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(root.classNames.disabled) || $next.hasClass(root.classNames.notSelectable) || $next.is(':hidden')) { + 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('.' + root.classNames.hover).trigger('contextmenu:blur') + .children('.hover').trigger('contextmenu:blur'); + + if ($this.hasClass(root.classNames.disabled) || $this.hasClass(root.classNames.notSelectable)) { + 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 !== null) { + 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('.' + root.classNames.disabled + ', .context-menu-submenu, .context-menu-separator, .' + root.classNames.notSelectable)) { + return; + } + + e.preventDefault(); + e.stopImmediatePropagation(); + + if ($.isFunction(opt.callbacks[key]) && Object.prototype.hasOwnProperty.call(opt.callbacks, key)) { + // item-specific callback + callback = opt.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; + + if ($this.hasClass(root.classNames.disabled) || $this.hasClass(root.classNames.notSelectable)) { + return; + } + + $this + .addClass([root.classNames.hover, root.classNames.visible].join(' ')) + // select other items and included items + .parent().find('.context-menu-item').not($this) + .removeClass(root.classNames.visible) + .filter('.' + root.classNames.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, + root = data.contextMenuRoot; + + if (opt.autoHide) { // for tablets and touch screens this needs to remain + $this.removeClass(root.classNames.visible); + } + $this.removeClass(root.classNames.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) { + var additionalZValue = opt.zIndex; + // If opt.zIndex is a function, call the function to get the right zIndex. + if (typeof opt.zIndex === 'function') { + additionalZValue = opt.zIndex.call($trigger, opt); + } + css.zIndex = zindex($trigger) + additionalZValue; + } + + // 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))) { + /* Additional hover check after short time, you might just miss the edge of the menu */ + setTimeout(function () { + if (!opt.hovering && opt.$menu != null) { opt.$menu.trigger('contextmenu:hide'); } + }, 50); + } + }); + } + }, + 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('.' + opt.classNames.hover).trigger('contextmenu:blur'); + opt.$selected = null; + // collapse all submenus + opt.$menu.find('.' + opt.classNames.visible).removeClass(opt.classNames.visible); + // unregister key and mouse handlers + // $(document).off('.contextMenuAutoHide keydown.contextMenu'); // http://bugs.jquery.com/ticket/10705 + $(document).off('.contextMenuAutoHide').off('keydown.contextMenu'); + // hide menu + if(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] = {}; + } + }); + + if(!root.accesskeys){ + root.accesskeys = {}; + } + + function createNameNode(item) { + var $name = $('<span></span>'); + if (item._accesskey) { + if (item._beforeAccesskey) { + $name.append(document.createTextNode(item._beforeAccesskey)); + } + $('<span></span>') + .addClass('context-menu-accesskey') + .text(item._accesskey) + .appendTo($name); + if (item._afterAccesskey) { + $name.append(document.createTextNode(item._afterAccesskey)); + } + } else { + if (item.isHtmlName) { + // restrict use with access keys + if (typeof item.accesskey !== 'undefined') { + throw new Error('accesskeys are not compatible with HTML names and cannot be used together in the same item'); + } + $name.html(item.name); + } else { + $name.text(item.name); + } + } + return $name; + } + + // 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. + // And normalize 'cm_separator' into 'cm_seperator'. + if (typeof item === 'string' || item.type === 'cm_separator') { + 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; + var matched = item.name.match(new RegExp('^(.*?)(' + ak + ')(.*)$', 'i')); + if (matched) { + item._beforeAccesskey = matched[1]; + item._accesskey = matched[2]; + item._afterAccesskey = matched[3]; + } + 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; + // Overwrite only if undefined or the item is appended to the root. This so it + // doesn't overwrite callbacks of root elements if the name is the same. + if ($.isFunction(item.callback) && (k.callbacks[key] === undefined || opt.type === undefined)) { + k.callbacks[key] = item.callback; + } + }); + } else { + // add label for input + if (item.type === 'cm_seperator') { + $t.addClass('context-menu-separator ' + root.classNames.notSelectable); + } else if (item.type === 'html') { + $t.addClass('context-menu-html ' + root.classNames.notSelectable); + } else if (item.type) { + $label = $('<label></label>').appendTo($t); + createNameNode(item).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 'cm_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': + createNameNode(item).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; + // Overwrite only if undefined or the item is appended to the root. This so it + // doesn't overwrite callbacks of root elements if the name is the same. + if ($.isFunction(item.callback) && (k.callbacks[key] === undefined || opt.type === undefined)) { + k.callbacks[key] = item.callback; + } + }); + createNameNode(item).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, this, $t, key, item); + } else { + if ( typeof(item.icon) === 'string' && item.icon.substring(0,3) == 'fa-' ) { + // to enable font awesome + item._icon = root.classNames.icon + ' ' + root.classNames.icon + '--fa fa ' + item.icon; + } else { + item._icon = root.classNames.icon + ' ' + root.classNames.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) { + var domMenu; + // 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', + (domMenu = $menu.get(0)).getBoundingClientRect ? + Math.ceil(domMenu.getBoundingClientRect().width) : + $menu.outerWidth() + 1); // outerWidth() returns rounded pixels + // 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: '' + }).outerWidth(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'](root.classNames.disabled); + + if ($.isFunction(item.icon)) { + $item.removeClass(item._icon); + item._icon = item.icon.call(this, $trigger, $item, 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 !== undefined && operation.y !== undefined) { + this.first().trigger($.Event('contextmenu', { pageX: operation.x, pageY: operation.y, mouseButton: operation.button })); + } else if (operation === 'hide') { + var $menu = this.first().data('contextMenu') ? this.first().data('contextMenu').$menu : null; + if($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) { + var itemClick = o.itemClickEvent === 'click' ? 'click.contextMenu' : 'mouseup.contextMenu'; + var contextMenuItemObj = { + // 'mouseup.contextMenu': handle.itemClick, + // 'click.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 + }; + contextMenuItemObj[itemClick] = handle.itemClick; + // 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(contextMenuItemObj, '.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; + + +}); _______________________________________________ Libreoffice-commits mailing list libreoffice-comm...@lists.freedesktop.org https://lists.freedesktop.org/mailman/listinfo/libreoffice-commits