/*******************************************************************************
 * 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);
		}
	}
});

}));
