|
- /*******************************************************************************
- * jquery.ui-contextmenu.js plugin.
- *
- * jQuery plugin that provides a context menu (based on the jQueryUI menu widget).
- *
- * @see https://github.com/mar10/jquery-ui-contextmenu
- *
- * Copyright (c) 2013-2018, Martin Wendt (http://wwWendt.de). Licensed MIT.
- */
-
- (function( factory ) {
- "use strict";
- if ( typeof define === "function" && define.amd ) {
- // AMD. Register as an anonymous module.
- define([ "jquery", "jquery-ui/ui/widgets/menu" ], factory );
- } else {
- // Browser globals
- factory( jQuery );
- }
- }(function( $ ) {
-
- "use strict";
-
- var supportSelectstart = "onselectstart" in document.createElement("div"),
- match = $.ui.menu.version.match(/^(\d)\.(\d+)/),
- uiVersion = {
- major: parseInt(match[1], 10),
- minor: parseInt(match[2], 10)
- },
- isLTE110 = ( uiVersion.major < 2 && uiVersion.minor <= 10 ),
- isLTE111 = ( uiVersion.major < 2 && uiVersion.minor <= 11 );
-
- $.widget("moogle.contextmenu", {
- version: "@VERSION",
- options: {
- addClass: "ui-contextmenu", // Add this class to the outer <ul>
- closeOnWindowBlur: true, // Close menu when window loses focus
- appendTo: "body", // Set keyboard focus to first entry on open
- autoFocus: false, // Set keyboard focus to first entry on open
- autoTrigger: true, // open menu on browser's `contextmenu` event
- delegate: null, // selector
- hide: { effect: "fadeOut", duration: "fast" },
- ignoreParentSelect: true, // Don't trigger 'select' for sub-menu parents
- menu: null, // selector or jQuery pointing to <UL>, or a definition hash
- position: null, // popup positon
- preventContextMenuForPopup: false, // prevent opening the browser's system
- // context menu on menu entries
- preventSelect: false, // disable text selection of target
- show: { effect: "slideDown", duration: "fast" },
- taphold: false, // open menu on taphold events (requires external plugins)
- uiMenuOptions: {}, // Additional options, used when UI Menu is created
- // Events:
- beforeOpen: $.noop, // menu about to open; return `false` to prevent opening
- blur: $.noop, // menu option lost focus
- close: $.noop, // menu was closed
- create: $.noop, // menu was initialized
- createMenu: $.noop, // menu was initialized (original UI Menu)
- focus: $.noop, // menu option got focus
- open: $.noop, // menu was opened
- select: $.noop // menu option was selected; return `false` to prevent closing
- },
- /** Constructor */
- _create: function() {
- var cssText, eventNames, targetId,
- opts = this.options;
-
- this.$headStyle = null;
- this.$menu = null;
- this.menuIsTemp = false;
- this.currentTarget = null;
- this.extraData = {};
- this.previousFocus = null;
-
- if (opts.delegate == null) {
- $.error("ui-contextmenu: Missing required option `delegate`.");
- }
- if (opts.preventSelect) {
- // Create a global style for all potential menu targets
- // If the contextmenu was bound to `document`, we apply the
- // selector relative to the <body> tag instead
- targetId = ($(this.element).is(document) ? $("body")
- : this.element).uniqueId().attr("id");
- cssText = "#" + targetId + " " + opts.delegate + " { " +
- "-webkit-user-select: none; " +
- "-khtml-user-select: none; " +
- "-moz-user-select: none; " +
- "-ms-user-select: none; " +
- "user-select: none; " +
- "}";
- this.$headStyle = $("<style class='moogle-contextmenu-style' />")
- .prop("type", "text/css")
- .appendTo("head");
-
- try {
- this.$headStyle.html(cssText);
- } catch ( e ) {
- // issue #47: fix for IE 6-8
- this.$headStyle[0].styleSheet.cssText = cssText;
- }
- // TODO: the selectstart is not supported by FF?
- if (supportSelectstart) {
- this.element.on("selectstart" + this.eventNamespace, opts.delegate,
- function(event) {
- event.preventDefault();
- });
- }
- }
- this._createUiMenu(opts.menu);
-
- eventNames = "contextmenu" + this.eventNamespace;
- if (opts.taphold) {
- eventNames += " taphold" + this.eventNamespace;
- }
- this.element.on(eventNames, opts.delegate, $.proxy(this._openMenu, this));
- },
- /** Destructor, called on $().contextmenu("destroy"). */
- _destroy: function() {
- this.element.off(this.eventNamespace);
-
- this._createUiMenu(null);
-
- if (this.$headStyle) {
- this.$headStyle.remove();
- this.$headStyle = null;
- }
- },
- /** (Re)Create jQuery UI Menu. */
- _createUiMenu: function(menuDef) {
- var ct, ed,
- opts = this.options;
-
- // Remove temporary <ul> if any
- if (this.isOpen()) {
- // #58: 'replaceMenu' in beforeOpen causing select: to lose ui.target
- ct = this.currentTarget;
- ed = this.extraData;
- // close without animation, to force async mode
- this._closeMenu(true);
- this.currentTarget = ct;
- this.extraData = ed;
- }
- if (this.menuIsTemp) {
- this.$menu.remove(); // this will also destroy ui.menu
- } else if (this.$menu) {
- this.$menu
- .menu("destroy")
- .removeClass(opts.addClass)
- .hide();
- }
- this.$menu = null;
- this.menuIsTemp = false;
- // If a menu definition array was passed, create a hidden <ul>
- // and generate the structure now
- if ( !menuDef ) {
- return;
- } else if ($.isArray(menuDef)) {
- this.$menu = $.moogle.contextmenu.createMenuMarkup(menuDef, null, opts);
- this.menuIsTemp = true;
- }else if ( typeof menuDef === "string" ) {
- this.$menu = $(menuDef);
- } else {
- this.$menu = menuDef;
- }
- // Create - but hide - the jQuery UI Menu widget
- this.$menu
- .hide()
- .addClass(opts.addClass)
- // Create a menu instance that delegates events to our widget
- .menu($.extend(true, {}, opts.uiMenuOptions, {
- items: "> :not(.ui-widget-header)",
- blur: $.proxy(opts.blur, this),
- create: $.proxy(opts.createMenu, this),
- focus: $.proxy(opts.focus, this),
- select: $.proxy(function(event, ui) {
- // User selected a menu entry
- var retval,
- isParent = $.moogle.contextmenu.isMenu(ui.item),
- actionHandler = ui.item.data("actionHandler");
-
- ui.cmd = ui.item.attr("data-command");
- ui.target = $(this.currentTarget);
- ui.extraData = this.extraData;
- // ignore clicks, if they only open a sub-menu
- if ( !isParent || !opts.ignoreParentSelect) {
- retval = this._trigger.call(this, "select", event, ui);
- if ( actionHandler ) {
- retval = actionHandler.call(this, event, ui);
- }
- if ( retval !== false ) {
- this._closeMenu.call(this);
- }
- event.preventDefault();
- }
- }, this)
- }));
- },
- /** Open popup (called on 'contextmenu' event). */
- _openMenu: function(event, recursive) {
- var res, promise, ui,
- opts = this.options,
- posOption = opts.position,
- self = this,
- manualTrigger = !!event.isTrigger;
-
- if ( !opts.autoTrigger && !manualTrigger ) {
- // ignore browser's `contextmenu` events
- return;
- }
- // Prevent browser from opening the system context menu
- event.preventDefault();
-
- this.currentTarget = event.target;
- this.extraData = event._extraData || {};
-
- ui = { menu: this.$menu, target: $(this.currentTarget), extraData: this.extraData,
- originalEvent: event, result: null };
-
- if ( !recursive ) {
- res = this._trigger("beforeOpen", event, ui);
- promise = (ui.result && $.isFunction(ui.result.promise)) ? ui.result : null;
- ui.result = null;
- if ( res === false ) {
- this.currentTarget = null;
- return false;
- } else if ( promise ) {
- // Handler returned a Deferred or Promise. Delay menu open until
- // the promise is resolved
- promise.done(function() {
- self._openMenu(event, true);
- });
- this.currentTarget = null;
- return false;
- }
- ui.menu = this.$menu; // Might have changed in beforeOpen
- }
-
- // Register global event handlers that close the dropdown-menu
- $(document).on("keydown" + this.eventNamespace, function(event) {
- if ( event.which === $.ui.keyCode.ESCAPE ) {
- self._closeMenu();
- }
- }).on("mousedown" + this.eventNamespace + " touchstart" + this.eventNamespace,
- function(event) {
- // Close menu when clicked outside menu
- if ( !$(event.target).closest(".ui-menu-item").length ) {
- self._closeMenu();
- }
- });
- $(window).on("blur" + this.eventNamespace, function(event) {
- if ( opts.closeOnWindowBlur ) {
- self._closeMenu();
- }
- });
-
- // required for custom positioning (issue #18 and #13).
- if ($.isFunction(posOption)) {
- posOption = posOption(event, ui);
- }
- posOption = $.extend({
- my: "left top",
- at: "left bottom",
- // if called by 'open' method, event does not have pageX/Y
- of: (event.pageX === undefined) ? event.target : event,
- collision: "fit"
- }, posOption);
-
- // Update entry statuses from callbacks
- this._updateEntries(this.$menu);
-
- // Finally display the popup
- this.$menu
- .show() // required to fix positioning error
- .css({
- position: "absolute",
- left: 0,
- top: 0
- }).position(posOption)
- .hide(); // hide again, so we can apply nice effects
-
- if ( opts.preventContextMenuForPopup ) {
- this.$menu.on("contextmenu" + this.eventNamespace, function(event) {
- event.preventDefault();
- });
- }
- this._show(this.$menu, opts.show, function() {
- var $first;
-
- // Set focus to first active menu entry
- if ( opts.autoFocus ) {
- self.previousFocus = $(event.target);
- // self.$menu.focus();
- $first = self.$menu
- .children("li.ui-menu-item")
- .not(".ui-state-disabled")
- .first();
- self.$menu.menu("focus", null, $first).focus();
- }
- self._trigger.call(self, "open", event, ui);
- });
- },
- /** Close popup. */
- _closeMenu: function(immediately) {
- var self = this,
- hideOpts = immediately ? false : this.options.hide,
- ui = { menu: this.$menu, target: $(this.currentTarget), extraData: this.extraData };
-
- // Note: we don't want to unbind the 'contextmenu' event
- $(document)
- .off("mousedown" + this.eventNamespace)
- .off("touchstart" + this.eventNamespace)
- .off("keydown" + this.eventNamespace);
- $(window)
- .off("blur" + this.eventNamespace);
-
- self.currentTarget = null; // issue #44 after hide animation is too late
- self.extraData = {};
- if ( this.$menu ) { // #88: widget might have been destroyed already
- this.$menu
- .off("contextmenu" + this.eventNamespace);
- this._hide(this.$menu, hideOpts, function() {
- if ( self.previousFocus ) {
- self.previousFocus.focus();
- self.previousFocus = null;
- }
- self._trigger("close", null, ui);
- });
- } else {
- self._trigger("close", null, ui);
- }
- },
- /** Handle $().contextmenu("option", key, value) calls. */
- _setOption: function(key, value) {
- switch (key) {
- case "menu":
- this.replaceMenu(value);
- break;
- }
- $.Widget.prototype._setOption.apply(this, arguments);
- },
- /** Return ui-menu entry (<LI> tag). */
- _getMenuEntry: function(cmd) {
- return this.$menu.find("li[data-command=" + cmd + "]");
- },
- /** Close context menu. */
- close: function() {
- if (this.isOpen()) {
- this._closeMenu();
- }
- },
- /* Apply status callbacks when menu is opened. */
- _updateEntries: function() {
- var self = this,
- ui = {
- menu: this.$menu, target: $(this.currentTarget), extraData: this.extraData };
-
- $.each(this.$menu.find(".ui-menu-item"), function(i, o) {
- var $entry = $(o),
- fn = $entry.data("disabledHandler"),
- res = fn ? fn({ type: "disabled" }, ui) : null;
-
- ui.item = $entry;
- ui.cmd = $entry.attr("data-command");
- // Evaluate `disabled()` callback
- if ( res != null ) {
- self.enableEntry(ui.cmd, !res);
- self.showEntry(ui.cmd, res !== "hide");
- }
- // Evaluate `title()` callback
- fn = $entry.data("titleHandler"),
- res = fn ? fn({ type: "title" }, ui) : null;
- if ( res != null ) {
- self.setTitle(ui.cmd, "" + res);
- }
- // Evaluate `tooltip()` callback
- fn = $entry.data("tooltipHandler"),
- res = fn ? fn({ type: "tooltip" }, ui) : null;
- if ( res != null ) {
- $entry.attr("title", "" + res);
- }
- });
- },
- /** Enable or disable the menu command. */
- enableEntry: function(cmd, flag) {
- this._getMenuEntry(cmd).toggleClass("ui-state-disabled", (flag === false));
- },
- /** Return ui-menu entry (LI tag) as jQuery object. */
- getEntry: function(cmd) {
- return this._getMenuEntry(cmd);
- },
- /** Return ui-menu entry wrapper as jQuery object.
- UI 1.10: this is the <a> tag inside the LI
- UI 1.11: this is the LI istself
- UI 1.12: this is the <div> tag inside the LI
- */
- getEntryWrapper: function(cmd) {
- return this._getMenuEntry(cmd).find(">[role=menuitem]").addBack("[role=menuitem]");
- },
- /** Return Menu element (UL). */
- getMenu: function() {
- return this.$menu;
- },
- /** Return true if menu is open. */
- isOpen: function() {
- // return this.$menu && this.$menu.is(":visible");
- return !!this.$menu && !!this.currentTarget;
- },
- /** Open context menu on a specific target (must match options.delegate)
- * Optional `extraData` is passed to event handlers as `ui.extraData`.
- */
- open: function(targetOrEvent, extraData) {
- // Fake a 'contextmenu' event
- extraData = extraData || {};
-
- var isEvent = (targetOrEvent && targetOrEvent.type && targetOrEvent.target),
- event = isEvent ? targetOrEvent : {},
- target = isEvent ? targetOrEvent.target : targetOrEvent,
- e = jQuery.Event("contextmenu", {
- target: $(target).get(0),
- pageX: event.pageX,
- pageY: event.pageY,
- originalEvent: isEvent ? targetOrEvent : undefined,
- _extraData: extraData
- });
- return this.element.trigger(e);
- },
- /** Replace the menu altogether. */
- replaceMenu: function(data) {
- this._createUiMenu(data);
- },
- /** Redefine a whole menu entry. */
- setEntry: function(cmd, entry) {
- var $ul,
- $entryLi = this._getMenuEntry(cmd);
-
- if (typeof entry === "string") {
- window.console && window.console.warn(
- "setEntry(cmd, t) with a plain string title is deprecated since v1.18." +
- "Use setTitle(cmd, '" + entry + "') instead.");
- return this.setTitle(cmd, entry);
- }
- $entryLi.empty();
- entry.cmd = entry.cmd || cmd;
- $.moogle.contextmenu.createEntryMarkup(entry, $entryLi);
- if ($.isArray(entry.children)) {
- $ul = $("<ul/>").appendTo($entryLi);
- $.moogle.contextmenu.createMenuMarkup(entry.children, $ul);
- }
- // #110: jQuery UI 1.12: refresh only works when this class is not set:
- $entryLi.removeClass("ui-menu-item");
- this.getMenu().menu("refresh");
- },
- /** Set icon (pass null to remove). */
- setIcon: function(cmd, icon) {
- return this.updateEntry(cmd, { uiIcon: icon });
- },
- /** Set title. */
- setTitle: function(cmd, title) {
- return this.updateEntry(cmd, { title: title });
- },
- // /** Set tooltip (pass null to remove). */
- // setTooltip: function(cmd, tooltip) {
- // this._getMenuEntry(cmd).attr("title", tooltip);
- // },
- /** Show or hide the menu command. */
- showEntry: function(cmd, flag) {
- this._getMenuEntry(cmd).toggle(flag !== false);
- },
- /** Redefine selective attributes of a menu entry. */
- updateEntry: function(cmd, entry) {
- var $icon, $wrapper,
- $entryLi = this._getMenuEntry(cmd);
-
- if ( entry.title !== undefined ) {
- $.moogle.contextmenu.updateTitle($entryLi, "" + entry.title);
- }
- if ( entry.tooltip !== undefined ) {
- if ( entry.tooltip === null ) {
- $entryLi.removeAttr("title");
- } else {
- $entryLi.attr("title", entry.tooltip);
- }
- }
- if ( entry.uiIcon !== undefined ) {
- $wrapper = this.getEntryWrapper(cmd),
- $icon = $wrapper.find("span.ui-icon").not(".ui-menu-icon");
- $icon.remove();
- if ( entry.uiIcon ) {
- $wrapper.append($("<span class='ui-icon' />").addClass(entry.uiIcon));
- }
- }
- if ( entry.hide !== undefined ) {
- $entryLi.toggle(!entry.hide);
- } else if ( entry.show !== undefined ) {
- // Note: `show` is an undocumented variant. `hide: false` is preferred
- $entryLi.toggle(!!entry.show);
- }
- // if ( entry.isHeader !== undefined ) {
- // $entryLi.toggleClass("ui-widget-header", !!entry.isHeader);
- // }
- if ( entry.data !== undefined ) {
- $entryLi.data(entry.data);
- }
-
- // Set/clear class names, but handle ui-state-disabled separately
- if ( entry.disabled === undefined ) {
- entry.disabled = $entryLi.hasClass("ui-state-disabled");
- }
- if ( entry.setClass ) {
- if ( $entryLi.hasClass("ui-menu-item") ) {
- entry.setClass += " ui-menu-item";
- }
- $entryLi.removeClass();
- $entryLi.addClass(entry.setClass);
- } else if ( entry.addClass ) {
- $entryLi.addClass(entry.addClass);
- }
- $entryLi.toggleClass("ui-state-disabled", !!entry.disabled);
- // // #110: jQuery UI 1.12: refresh only works when this class is not set:
- // $entryLi.removeClass("ui-menu-item");
- // this.getMenu().menu("refresh");
- }
- });
-
- /*
- * Global functions
- */
- $.extend($.moogle.contextmenu, {
- /** Convert a menu description into a into a <li> content. */
- createEntryMarkup: function(entry, $parentLi) {
- var $wrapper = null;
-
- $parentLi.attr("data-command", entry.cmd);
-
- if ( !/[^\-\u2014\u2013\s]/.test( entry.title ) ) {
- // hyphen, em dash, en dash: separator as defined by UI Menu 1.10
- $parentLi.text(entry.title);
- } else {
- if ( isLTE110 ) {
- // jQuery UI Menu 1.10 or before required an `<a>` tag
- $wrapper = $("<a/>", {
- html: "" + entry.title,
- href: "#"
- }).appendTo($parentLi);
-
- } else if ( isLTE111 ) {
- // jQuery UI Menu 1.11 preferes to avoid `<a>` tags or <div> wrapper
- $parentLi.html("" + entry.title);
- $wrapper = $parentLi;
-
- } else {
- // jQuery UI Menu 1.12 introduced `<div>` wrappers
- $wrapper = $("<div/>", {
- html: "" + entry.title
- }).appendTo($parentLi);
- }
- if ( entry.uiIcon ) {
- $wrapper.append($("<span class='ui-icon' />").addClass(entry.uiIcon));
- }
- // Store option callbacks in entry's data
- $.each( [ "action", "disabled", "title", "tooltip" ], function(i, attr) {
- if ( $.isFunction(entry[attr]) ) {
- $parentLi.data(attr + "Handler", entry[attr]);
- }
- });
- if ( entry.disabled === true ) {
- $parentLi.addClass("ui-state-disabled");
- }
- if ( entry.isHeader ) {
- $parentLi.addClass("ui-widget-header");
- }
- if ( entry.addClass ) {
- $parentLi.addClass(entry.addClass);
- }
- if ( $.isPlainObject(entry.data) ) {
- $parentLi.data(entry.data);
- }
- if ( typeof entry.tooltip === "string" ) {
- $parentLi.attr("title", entry.tooltip);
- }
- }
- },
- /** Convert a nested array of command objects into a <ul> structure. */
- createMenuMarkup: function(options, $parentUl, opts) {
- var i, menu, $ul, $li,
- appendTo = (opts && opts.appendTo) ? opts.appendTo : "body";
-
- if ( $parentUl == null ) {
- $parentUl = $("<ul class='ui-helper-hidden' />").appendTo(appendTo);
- }
- for (i = 0; i < options.length; i++) {
- menu = options[i];
- $li = $("<li/>").appendTo($parentUl);
-
- $.moogle.contextmenu.createEntryMarkup(menu, $li);
-
- if ( $.isArray(menu.children) ) {
- $ul = $("<ul/>").appendTo($li);
- $.moogle.contextmenu.createMenuMarkup(menu.children, $ul);
- }
- }
- return $parentUl;
- },
- /** Returns true if the menu item has child menu items */
- isMenu: function(item) {
- if ( isLTE110 ) {
- return item.has(">a[aria-haspopup='true']").length > 0;
- } else if ( isLTE111 ) { // jQuery UI 1.11 used no tag wrappers
- return item.is("[aria-haspopup='true']");
- } else {
- return item.has(">div[aria-haspopup='true']").length > 0;
- }
- },
- /** Replace the title of elem', but retain icons andchild entries. */
- replaceFirstTextNodeChild: function(elem, html) {
- var $icons = elem.find(">span.ui-icon,>ul.ui-menu").detach();
-
- elem
- .empty()
- .html(html)
- .append($icons);
- },
- /** Updates the menu item's title */
- updateTitle: function(item, title) {
- if ( isLTE110 ) { // jQuery UI 1.10 and before used <a> tags
- $.moogle.contextmenu.replaceFirstTextNodeChild($("a", item), title);
- } else if ( isLTE111 ) { // jQuery UI 1.11 used no tag wrappers
- $.moogle.contextmenu.replaceFirstTextNodeChild(item, title);
- } else { // jQuery UI 1.12+ introduced <div> tag wrappers
- $.moogle.contextmenu.replaceFirstTextNodeChild($("div", item), title);
- }
- }
- });
-
- }));
|