src/pyams_skin/resources/js/myams.js
changeset 248 86b71518e457
parent 245 79635dd71cca
child 251 475559f51ff6
equal deleted inserted replaced
247:9f1dded7e725 248:86b71518e457
     1 /*
     1 /*
     2  * MyAMS
     2  * MyAMS
     3  * « My Application Management Skin »
     3  * « My Application Management Skin »
     4  *
     4  *
     5  * $Tag: 0.1.11 $ (rev. 1)
     5  * $Tag$ (rev. 1)
     6  * A bootstrap based application/administration skin
     6  * A bootstrap based application/administration skin
     7  *
     7  *
     8  * Custom administration and application skin tools
     8  * Custom administration and application skin tools
     9  * Released under Zope Public License ZPL 1.1
     9  * Released under Zope Public License ZPL 1.1
    10  * ©2014-2016 Thierry Florac <tflorac@ulthar.net>
    10  * ©2014-2016 Thierry Florac <tflorac@ulthar.net>
    88 	};
    88 	};
    89 
    89 
    90 
    90 
    91 	/**
    91 	/**
    92 	 * JQuery filter on parents class
    92 	 * JQuery filter on parents class
       
    93 	 * This filter is often combined with ":not()" to select DOM objects which don't have
       
    94 	 * parents of a given class.
       
    95 	 * For example:
       
    96 	 *
       
    97 	 *   $('.hint:not(:parents(.nohints))', element);
       
    98 	 *
       
    99 	 * will select all elements with ".hint" class which don't have a parent with '.nohints' class.
    93 	 */
   100 	 */
    94 	$.expr[':'].parents = function(obj, index, meta /*, stack*/) {
   101 	$.expr[':'].parents = function(obj, index, meta /*, stack*/) {
    95 		return $(obj).parents(meta[3]).length > 0;
   102 		return $(obj).parents(meta[3]).length > 0;
    96 	};
   103 	};
    97 
   104 
    98 
   105 
    99 	/**
   106 	/**
   100 	 * JQuery 'scrollbarWidth' function
   107 	 * JQuery 'scrollbarWidth' function
   101 	 * Get width of vertical scrollbar
   108 	 * Get width of default vertical scrollbar
   102 	 */
   109 	 */
   103 	if ($.scrollbarWidth === undefined) {
   110 	if ($.scrollbarWidth === undefined) {
   104 		$.scrollbarWidth = function() {
   111 		$.scrollbarWidth = function() {
   105 			var parent = $('<div style="width: 50px; height: 50px; overflow: auto"><div/></div>').appendTo('body');
   112 			var parent = $('<div style="width: 50px; height: 50px; overflow: auto"><div/></div>').appendTo('body');
   106 			var child = parent.children();
   113 			var child = parent.children();
   114 	/**
   121 	/**
   115 	 * MyAMS JQuery extensions
   122 	 * MyAMS JQuery extensions
   116 	 */
   123 	 */
   117 	$.fn.extend({
   124 	$.fn.extend({
   118 
   125 
   119 		/*
   126 		/**
   120 		 * Check if current object is empty or not
   127 		 * Check if current object is empty or not
   121 		 */
   128 		 */
   122 		exists: function() {
   129 		exists: function() {
   123 			return $(this).length > 0;
   130 			return $(this).length > 0;
   124 		},
   131 		},
   125 
   132 
   126 		/*
   133 		/**
   127 		 * Get object if it supports given CSS class,
   134 		 * Get object if it supports given CSS class,
   128 		 * otherwise looks for parents
   135 		 * otherwise look for parents
   129 		 */
   136 		 */
   130 		objectOrParentWithClass: function(klass) {
   137 		objectOrParentWithClass: function(klass) {
   131 			if (this.hasClass(klass)) {
   138 			if (this.hasClass(klass)) {
   132 				return this;
   139 				return this;
   133 			} else {
   140 			} else {
   134 				return this.parents('.' + klass);
   141 				return this.parents('.' + klass);
   135 			}
   142 			}
   136 		},
   143 		},
   137 
   144 
   138 		/*
   145 		/**
   139 		 * Build an array of attributes of the given selection
   146 		 * Build an array of attributes of the given selection
   140 		 */
   147 		 */
   141 		listattr: function(attr) {
   148 		listattr: function(attr) {
   142 			var result = [];
   149 			var result = [];
   143 			this.each(function() {
   150 			this.each(function() {
   144 				result.push($(this).attr(attr));
   151 				result.push($(this).attr(attr));
   145 			});
   152 			});
   146 			return result;
   153 			return result;
   147 		},
   154 		},
   148 
   155 
   149 		/*
   156 		/**
   150 		 * CSS style function
   157 		 * CSS style function
   151 		 * Code from Aram Kocharyan on stackoverflow.com
   158 		 * Code from Aram Kocharyan on stackoverflow.com
   152 		 */
   159 		 */
   153 		style: function(styleName, value, priority) {
   160 		style: function(styleName, value, priority) {
   154 			// DOM node
   161 			// DOM node
   174 				// Get CSSStyleDeclaration
   181 				// Get CSSStyleDeclaration
   175 				return style;
   182 				return style;
   176 			}
   183 			}
   177 		},
   184 		},
   178 
   185 
   179 		/*
   186 		/**
   180 		 * Remove CSS classes starting with a given prefix
   187 		 * Remove CSS classes starting with a given prefix
   181 		 */
   188 		 */
   182 		removeClassPrefix: function (prefix) {
   189 		removeClassPrefix: function (prefix) {
   183 			this.each(function (i, it) {
   190 			this.each(function (i, it) {
   184 				var classes = it.className.split(" ").map(function(item) {
   191 				var classes = it.className.split(" ").map(function(item) {
   187 				it.className = $.trim(classes.join(" "));
   194 				it.className = $.trim(classes.join(" "));
   188 			});
   195 			});
   189 			return this;
   196 			return this;
   190 		},
   197 		},
   191 
   198 
   192 		/*
   199 		/**
   193 		 * Context menu handler
   200 		 * Context menu handler
   194 		 */
   201 		 */
   195 		contextMenu: function(settings) {
   202 		contextMenu: function(settings) {
   196 
   203 
   197 			function getMenuPosition(mouse, direction, scrollDir) {
   204 			function getMenuPosition(mouse, direction, scrollDir) {
   574 		}
   581 		}
   575 		return globals.document.body.contains(element[0]);
   582 		return globals.document.body.contains(element[0]);
   576 	};
   583 	};
   577 
   584 
   578 	/**
   585 	/**
   579 	 * Get script or CSS file using browser cache
   586 	 * Get target URL matching given source
   580 	 * Script or CSS URLs can include variable names, given between braces, as in
   587 	 *
   581 	 * {MyAMS.baseURL}
   588 	 * Given URL can include variable names (with their namespace), given between braces, as in {MyAMS.baseURL}
   582 	 */
   589 	 */
   583 	MyAMS.getSource = function(url) {
   590 	MyAMS.getSource = function(url) {
   584 		return url.replace(/{[^{}]*}/g, function(match) {
   591 		return url.replace(/{[^{}]*}/g, function(match) {
   585 			return ams.getFunctionByName(match.substr(1, match.length-2));
   592 			return ams.getFunctionByName(match.substr(1, match.length-2));
   586 		});
   593 		});
   587 	};
   594 	};
   588 
   595 
       
   596 	/**
       
   597 	 * Script loader function
       
   598 	 *
       
   599 	 * @param url: script URL
       
   600 	 * @param callback: a callback to be called after script loading
       
   601 	 * @param options: a set of options to be added to AJAX call
       
   602 	 */
   589 	MyAMS.getScript = function(url, callback, options) {
   603 	MyAMS.getScript = function(url, callback, options) {
   590 		if (typeof(callback) === 'object') {
   604 		if (typeof(callback) === 'object') {
   591 			options = callback;
   605 			options = callback;
   592 			callback = null;
   606 			callback = null;
   593 		}
   607 		}
   604 		};
   618 		};
   605 		var settings = $.extend({}, defaults, options);
   619 		var settings = $.extend({}, defaults, options);
   606 		return $.ajax(settings);
   620 		return $.ajax(settings);
   607 	};
   621 	};
   608 
   622 
       
   623 	/**
       
   624 	 * CSS file loader function
       
   625 	 *
       
   626 	 * @param url: CSS file URL
       
   627 	 * @param id: a unique ID given to CSS file
       
   628 	 */
   609 	MyAMS.getCSS = function(url, id) {
   629 	MyAMS.getCSS = function(url, id) {
   610 		var head = $('HEAD');
   630 		var head = $('HEAD');
   611 		var css = $('link[data-ams-id="' + id + '"]', head);
   631 		var css = $('link[data-ams-id="' + id + '"]', head);
   612 		if (css.length === 0) {
   632 		if (css.length === 0) {
   613 			var source = ams.getSource(url);
   633 			var source = ams.getSource(url);
   626 	/**
   646 	/**
   627 	 * Events management
   647 	 * Events management
   628 	 */
   648 	 */
   629 	MyAMS.event = {
   649 	MyAMS.event = {
   630 
   650 
       
   651 		/**
       
   652 		 * Stop current event propagation
       
   653 		 */
   631 		stop: function(event) {
   654 		stop: function(event) {
   632 			if (!event) {
   655 			if (!event) {
   633 				event = window.event;
   656 				event = window.event;
   634 			}
   657 			}
   635 			if (event) {
   658 			if (event) {
   648 	/**
   671 	/**
   649 	 * Browser testing functions; mostly for IE...
   672 	 * Browser testing functions; mostly for IE...
   650 	 */
   673 	 */
   651 	MyAMS.browser = {
   674 	MyAMS.browser = {
   652 
   675 
       
   676 		/**
       
   677 		 * Get IE version
       
   678 		 */
   653 		getInternetExplorerVersion: function() {
   679 		getInternetExplorerVersion: function() {
   654 			var rv = -1;
   680 			var rv = -1;
   655 			if (navigator.appName === "Microsoft Internet Explorer") {
   681 			if (navigator.appName === "Microsoft Internet Explorer") {
   656 				var ua = navigator.userAgent;
   682 				var ua = navigator.userAgent;
   657 				var re = new RegExp("MSIE ([0-9]{1,}[.0-9]{0,})");
   683 				var re = new RegExp("MSIE ([0-9]{1,}[.0-9]{0,})");
   660 				}
   686 				}
   661 			}
   687 			}
   662 			return rv;
   688 			return rv;
   663 		},
   689 		},
   664 
   690 
       
   691 		/**
       
   692 		 * Display alert for old IE version
       
   693 		 */
   665 		checkVersion: function() {
   694 		checkVersion: function() {
   666 			var msg = "You're not using Windows Internet Explorer.";
   695 			var msg = "You're not using Windows Internet Explorer.";
   667 			var ver = this.getInternetExplorerVersion();
   696 			var ver = this.getInternetExplorerVersion();
   668 			if (ver > -1) {
   697 			if (ver > -1) {
   669 				if (ver >= 8) {
   698 				if (ver >= 8) {
   675 			if (globals.alert) {
   704 			if (globals.alert) {
   676 				globals.alert(msg);
   705 				globals.alert(msg);
   677 			}
   706 			}
   678 		},
   707 		},
   679 
   708 
       
   709 		/**
       
   710 		 * Check if IE is in version 8 or lower
       
   711 		 */
   680 		isIE8orlower: function() {
   712 		isIE8orlower: function() {
   681 			var msg = "0";
   713 			var msg = "0";
   682 			var ver = this.getInternetExplorerVersion();
   714 			var ver = this.getInternetExplorerVersion();
   683 			if (ver > -1) {
   715 			if (ver > -1) {
   684 				if (ver >= 9) {
   716 				if (ver >= 9) {
   689 			}
   721 			}
   690 			return msg;
   722 			return msg;
   691 		},
   723 		},
   692 
   724 
   693 
   725 
       
   726 		/**
       
   727 		 * Copy selection to clipboard
       
   728 		 *
       
   729 		 * If 'text' argument is provided, given text is copied to clipboard.
       
   730 		 * Otherwise, text ou event's source is copied.
       
   731 		 * Several methods are tested to do clipboard copy (based on browser features); il copy can't be done,
       
   732 		 * a prompt is displayed to allow user to make a manual copy. 
       
   733 		 */
   694 		copyToClipboard: function(text) {
   734 		copyToClipboard: function(text) {
   695 
   735 
   696 			function doCopy(text) {
   736 			function doCopy(text) {
   697 				var copied = false;
   737 				var copied = false;
   698 				if (window.clipboardData && window.clipboardData.setData) {
   738 				if (window.clipboardData && window.clipboardData.setData) {
   720 									  {
   760 									  {
   721 										  title: text.length > 1
   761 										  title: text.length > 1
   722 											  ? ams.i18n.CLIPBOARD_TEXT_COPY_OK
   762 											  ? ams.i18n.CLIPBOARD_TEXT_COPY_OK
   723 											  : ams.i18n.CLIPBOARD_CHARACTER_COPY_OK,
   763 											  : ams.i18n.CLIPBOARD_CHARACTER_COPY_OK,
   724 										  icon: 'fa fa-fw fa-info-circle font-xs align-top margin-top-10',
   764 										  icon: 'fa fa-fw fa-info-circle font-xs align-top margin-top-10',
   725 										  timeout: 1000
   765 										  timeout: 3000
   726 									  });
   766 									  });
   727 				} else if (globals.prompt) {
   767 				} else if (globals.prompt) {
   728 					globals.prompt(MyAMS.i18n.CLIPBOARD_COPY, text);
   768 					globals.prompt(MyAMS.i18n.CLIPBOARD_COPY, text);
   729 				}
   769 				}
   730 			}
   770 			}
   810 	MyAMS.ajax = {
   850 	MyAMS.ajax = {
   811 
   851 
   812 		/**
   852 		/**
   813 		 * Check for given feature and download script if necessary
   853 		 * Check for given feature and download script if necessary
   814 		 *
   854 		 *
   815 		 * @checker: pointer to a javascript object which will be downloaded in undefined
   855 		 * @param checker: pointer to a javascript object which will be downloaded in undefined
   816 		 * @source: URL of a javascript file containing requested feature
   856 		 * @param source: URL of a javascript file containing requested feature
   817 		 * @callback: pointer to a function which will be called after the script is downloaded. The first
   857 		 * @param callback: pointer to a function which will be called after the script is downloaded. The first
   818 		 *   argument of this callback is a boolean value indicating if the script was just downloaded (true)
   858 		 *   argument of this callback is a boolean value indicating if the script was just downloaded (true)
   819 		 *   or if the requested object was already loaded (false)
   859 		 *   or if the requested object was already loaded (false)
   820 		 * @options: callback options
   860 		 * @param options: callback options
   821 		 */
   861 		 */
   822 		check: function(checker, source, callback, options) {
   862 		check: function(checker, source, callback, options) {
   823 
   863 
   824 			function callCallbacks(firstLoad, options) {
   864 			function callCallbacks(firstLoad, options) {
   825 				if (callback === undefined) {
   865 				if (callback === undefined) {
  1140 			var message;
  1180 			var message;
  1141 			if (result.message) {
  1181 			if (result.message) {
  1142 				message = result.message;
  1182 				message = result.message;
  1143 				if (typeof(message) === 'string') {
  1183 				if (typeof(message) === 'string') {
  1144 					if ((status === 'info') || (status === 'success')) {
  1184 					if ((status === 'info') || (status === 'success')) {
  1145 						ams.skin.smallBox(status,
  1185 						ams.skin.smallBox(status, {
  1146 										  {
       
  1147 											  title: message,
  1186 											  title: message,
  1148 											  icon: 'fa fa-fw fa-info-circle font-xs align-top margin-top-10',
  1187 											  icon: 'fa fa-fw fa-info-circle font-xs align-top margin-top-10',
  1149 											  timeout: 3000
  1188 											  timeout: 3000
  1150 										  });
  1189 										  });
  1151 					} else {
  1190 					} else {
  1158 								   message.body,
  1197 								   message.body,
  1159 								   message.subtitle);
  1198 								   message.subtitle);
  1160 				}
  1199 				}
  1161 			}
  1200 			}
  1162 			if (result.smallbox) {
  1201 			if (result.smallbox) {
  1163 				ams.skin.smallBox(result.smallbox_status || status,
  1202 				ams.skin.smallBox(result.smallbox_status || status, {
  1164 								  {title: result.smallbox,
  1203 									  title: result.smallbox,
  1165 								   icon: 'fa fa-fw fa-info-circle font-xs align-top margin-top-10',
  1204 									  icon: 'fa fa-fw fa-info-circle font-xs align-top margin-top-10',
  1166 								   timeout: 3000});
  1205 									  timeout: 3000
       
  1206 								  });
  1167 			}
  1207 			}
  1168 			if (result.messagebox) {
  1208 			if (result.messagebox) {
  1169 				message = result.messagebox;
  1209 				message = result.messagebox;
  1170 				if (typeof(message) === 'string') {
  1210 				if (typeof(message) === 'string') {
  1171 					ams.skin.messageBox('info',
  1211 					ams.skin.messageBox('info', {
  1172 										{
       
  1173 											title: ams.i18n.ERROR_OCCURED,
  1212 											title: ams.i18n.ERROR_OCCURED,
  1174 											content: message,
  1213 											content: message,
  1175 											timeout: 10000
  1214 											timeout: 10000
  1176 										});
  1215 										});
  1177 				} else {
  1216 				} else {
  1178 					var messageStatus = message.status || 'info';
  1217 					var messageStatus = message.status || 'info';
  1179 					if (messageStatus === 'error' && form && target) {
  1218 					if (messageStatus === 'error' && form && target) {
  1180 						ams.executeFunctionByName(form.data('ams-form-submit-error') || 'MyAMS.form.finalizeSubmitOnError', form, target);
  1219 						ams.executeFunctionByName(form.data('ams-form-submit-error') || 'MyAMS.form.finalizeSubmitOnError', form, target);
  1181 					}
  1220 					}
  1182 					ams.skin.messageBox(messageStatus,
  1221 					ams.skin.messageBox(messageStatus, {
  1183 										{title: message.title || ams.i18n.ERROR_OCCURED,
  1222 											title: message.title || ams.i18n.ERROR_OCCURED,
  1184 										 content: message.content,
  1223 											content: message.content,
  1185 										 icon: message.icon,
  1224 											icon: message.icon,
  1186 										 number: message.number,
  1225 											number: message.number,
  1187 										 timeout: message.timeout === null ? undefined : (message.timeout || 10000)});
  1226 											timeout: message.timeout === null ? undefined : (message.timeout || 10000)
       
  1227 										});
  1188 				}
  1228 				}
  1189 			}
  1229 			}
  1190 			if (result.event) {
  1230 			if (result.event) {
  1191 				form.trigger(result.event, result.event_options);
  1231 				form.trigger(result.event, result.event_options);
  1192 			}
  1232 			}
  2120 						var widget = $('[name="' + widgetData.name + '"]', form);
  2160 						var widget = $('[name="' + widgetData.name + '"]', form);
  2121 						if (!widget.exists()) {
  2161 						if (!widget.exists()) {
  2122 							widget = $('[name="' + widgetData.name + ':list"]', form);
  2162 							widget = $('[name="' + widgetData.name + ':list"]', form);
  2123 						}
  2163 						}
  2124 						if (widget.exists()) {
  2164 						if (widget.exists()) {
       
  2165 							// Update widget state
  2125 							widget.parents('label:first')
  2166 							widget.parents('label:first')
  2126 								  .removeClassPrefix('state-')
  2167 								  .removeClassPrefix('state-')
  2127 								  .addClass('state-error')
  2168 								  .addClass('state-error')
  2128 								  .after('<span for="name" class="state-error">' + widgetData.message + '</span>');
  2169 								  .after('<span for="name" class="state-error">' + widgetData.message + '</span>');
  2129 						}
  2170 						} else {
  2130 						// complete form alert message
  2171 							// complete form alert message
  2131 						if (widgetData.label) {
  2172 							if (widgetData.label) {
  2132 							message.push(widgetData.label + ' : ' + widgetData.message);
  2173 								message.push(widgetData.label + ' : ' + widgetData.message);
       
  2174 							}
  2133 						}
  2175 						}
  2134 						// mark parent tab (if any) with error status
  2176 						// mark parent tab (if any) with error status
  2135 						var tabIndex = widget.parents('.tab-pane').index() + 1;
  2177 						var tabIndex = widget.parents('.tab-pane').index() + 1;
  2136 						if (tabIndex > 0) {
  2178 						if (tabIndex > 0) {
  2137 							var navTabs = $('.nav-tabs', $(widget).parents('.tabforms'));
  2179 							var navTabs = $('.nav-tabs', $(widget).parents('.tabforms'));
  2593 		},
  2635 		},
  2594 
  2636 
  2595 		/** Datetimepicker dialog cleaner callback */
  2637 		/** Datetimepicker dialog cleaner callback */
  2596 		datetimepickerDialogHiddenCallback: function() {
  2638 		datetimepickerDialogHiddenCallback: function() {
  2597 			$('.datepicker, .timepicker, .datetimepicker', this).datetimepicker('destroy');
  2639 			$('.datepicker, .timepicker, .datetimepicker', this).datetimepicker('destroy');
       
  2640 		},
       
  2641 
       
  2642 		/** Set SEO status */
       
  2643 		setSEOStatus: function() {
       
  2644 			var input = $(this);
       
  2645 			var progress = input.siblings('.progress').children('.progress-bar');
       
  2646 			var length = Math.min(input.val().length, 100);
       
  2647 			var status = 'success';
       
  2648 			if (length < 20 || length > 80) {
       
  2649 				status = 'danger';
       
  2650 			} else if (length < 40 || length > 66) {
       
  2651 				status = 'warning';
       
  2652 			}
       
  2653 			progress.removeClassPrefix('progress-bar')
       
  2654 					.addClass('progress-bar')
       
  2655 					.addClass('progress-bar-' + status)
       
  2656 					.css('width', length + '%');
  2598 		}
  2657 		}
  2599 	};
  2658 	};
  2600 
  2659 
  2601 
  2660 
  2602 	/**
  2661 	/**
  5811 			if (button.exists() && button.parent().hasClass('input-file')) {
  5870 			if (button.exists() && button.parent().hasClass('input-file')) {
  5812 				button.next('input[type="text"]').val(input.val());
  5871 				button.next('input[type="text"]').val(input.val());
  5813 			}
  5872 			}
  5814 		});
  5873 		});
  5815 
  5874 
       
  5875 		// Always blur readonly inputs
       
  5876 		$(document).on('focus', 'input[readonly="readonly"]', function() {
       
  5877 			$(this).blur();
       
  5878 		});
       
  5879 
  5816 		// Prevent bootstrap dialog from blocking TinyMCE focus
  5880 		// Prevent bootstrap dialog from blocking TinyMCE focus
  5817 		$(document).on('focusin', function(e) {
  5881 		$(document).on('focusin', function(e) {
  5818 			if ($(e.target).closest('.mce-window').length) {
  5882 			if ($(e.target).closest('.mce-window').length) {
  5819 				e.stopImmediatePropagation();
  5883 				e.stopImmediatePropagation();
  5820 			}
  5884 			}
  5821 		});
  5885 		});
  5822 
  5886 
  5823 		// Disable clicks on disabled tabs
  5887 		// Disable clicks on disabled tabs
  5824 		$("a[data-toggle=tab]", ".nav-tabs").on("click", function(e) {
  5888 		$(document).on("click", '.nav-tabs a[data-toggle=tab]', function(e) {
  5825 			if ($(this).parent('li').hasClass("disabled")) {
  5889 			if ($(this).parent('li').hasClass("disabled")) {
  5826 				e.preventDefault();
  5890 				e.preventDefault();
  5827 				return false;
  5891 				return false;
  5828 			}
  5892 			}
       
  5893 		});
       
  5894 
       
  5895 		// Automatically set orientation of dropdown menus
       
  5896 		$(document).on('show.bs.dropdown', '.btn-group', function() {
       
  5897 			var menu = $(this);
       
  5898 			var ul = menu.children('.dropdown-menu');
       
  5899 			var menuRect = menu.get(0).getBoundingClientRect();
       
  5900 			var position = menuRect.top;
       
  5901 			var buttonHeight = menuRect.height;
       
  5902 			var menuHeight = ul.outerHeight();
       
  5903 			if (position > menuHeight && $(window).height() - position < buttonHeight + menuHeight) {
       
  5904 				menu.addClass("dropup");
       
  5905 			}
       
  5906 		}).on('hidden.bs.dropdown', '.btn-group', function() {
       
  5907 			// always reset after close
       
  5908 			$(this).removeClass('dropup');
  5829 		});
  5909 		});
  5830 
  5910 
  5831 		// Enable tabs dynamic loading
  5911 		// Enable tabs dynamic loading
  5832 		$(document).on('show.bs.tab', function(e) {
  5912 		$(document).on('show.bs.tab', function(e) {
  5833 			var link = $(e.target);
  5913 			var link = $(e.target);