1 /**
  2  * @fileOverview jQuery setPosition Plugin to place elements on the page
  3  * @author Pavel Yaschenko, 05.2010
  4  * @version 0.5
  5  */
  6 
  7 // draft examples of usage
  8 // jQuery('#tooltip').setPosition('#aaa',{from:'LB', to:'AA'});
  9 // jQuery('#bbb').bind("click",function(e){jQuery('#tooltip').setPosition(e);});
 10 // TODO: clear code
 11 // TODO: optimization
 12 
 13 // jQuery(target).setPosition(source,[params])
 14 // source:
 15 //			jQuery selector
 16 //			object {id:}
 17 //			object {left:,top:,width:,height} // all properties are optimal
 18 //			jQuery object
 19 //			dom element
 20 //			event
 21 //
 22 //	params:
 23 //			type: string // position type
 24 //			collision: string // not implemented
 25 //			offset: array [x,y] // implemented only for noPositionType
 26 //			from: string // place target relative of source
 27 //			to: string // direction for target
 28 
 29 	/**
 30 	  * @name jQuery
 31 	  * @namespace jQuery 
 32 	  * */
 33 
 34 (function($) {
 35 	/**
 36      * Place DOM element relative to another element or using position parameters. Elements with style.display='none' also supported.
 37      * 
 38      * @example jQuery('#tooltip').setPosition('#myDiv',{from:'LB', to:'AA'});
 39      * @example jQuery('#myClickDiv').bind("click",function(e){jQuery('#tooltip').setPosition(e);});
 40      *
 41      * @function
 42      * @name jQuery#setPosition
 43      * 
 44      * @param {object} source - object that provides information about new position. <p>
 45      * accepts:
 46      * <ul>
 47      * 		<li>jQuery selector or object</li>
 48      * 		<li>object with id: <code>{id:'myDiv'}</code></li>
 49      * 		<li>object with region settings: <code>{left:0, top:0, width:100, height:100}</code></li>
 50      * 		<li>DOM Element</li>
 51      * 		<li>Event object</li>
 52      * </ul>
 53      * </p>
 54      * @param {object} params - position parameters:
 55      * <dl><dt>
 56      * @param {string} [params.type] - position type that defines positioning and auto positioning rules ["TOOLTIP","DROPDOWN"]</dt><dt>
 57      * @param {string} [params.collision] - not implemented yet</dt><dt>
 58      * @param {array} [params.offset] - provides array(2) with x and y for manually position definition<br/>
 59      * affects only if "type", "from" and "to" not defined</dt><dt>
 60      * @param {string} [params.from] - place target relative of source // draft definition</dt><dt>
 61      * @param {string} [params.to] - direction for target // draft definition</dt>
 62      * </blockquote>
 63      *
 64      * @return {jQuery} jQuery wrapped DOM elements
 65      * */
 66 	$.fn.setPosition = function(source, params) {
 67 		var stype = typeof source;
 68 		if (stype == "object" || stype == "string") {
 69 			var rect = {};
 70 			if (source.type) {
 71 				rect = getPointerRect(source);
 72 			} else if (stype == "string" || source.nodeType || source instanceof jQuery) {
 73 				rect = getElementRect(source);
 74 			} else if (source.id) {
 75 				rect = getElementRect(document.getElementById(source.id));
 76 			} else {
 77 				rect = source;
 78 			}
 79 			
 80 			var params = params || {};
 81 			var def = params.type || params.from || params.to ? $.PositionTypes[params.type || defaultType] : {noPositionType:true};
 82 			
 83 			var options =  $.extend({}, defaults, def, params);
 84 			return this.each(function() {
 85 					element = $(this);
 86 					//alert(rect.left+" "+rect.top+" "+rect.width+" "+rect.height);
 87 					position(rect, element, options);
 88 				});
 89 		}
 90 		return this;
 91 	};
 92 	
 93 	var defaultType = "TOOLTIP";
 94 	var defaults = {
 95 		collision: "",
 96 		offset: [0,0]
 97 	};
 98 	var re = /^(left|right)-(top|buttom|auto)$/i;
 99 	
100 	// TODO: make it private
101 	$.PositionTypes = {
102 		// horisontal constants: L-left, R-right, C-center, A-auto
103 		// vertical constants:   T-top, B-bottom, M-middle, A-auto
104 		// for auto: list of joinPoint-Direction pairs
105 		TOOLTIP: {from:"AA", to:"AA", auto:["RTRT", "RBRT", "LTRT", "RTLT", "LTLT", "LBLT", "RTRB", "RBRB", "LBRB", "RBLB"]},
106 		DROPDOWN:{from:"AA", to:"AA", auto:["LBRB", "LTRT", "RBLB", "RTLT"]}
107 	};
108 	
109 	/** 
110 	  * Add or replace position type rules for auto positioning.
111 	  * Does not fully determinated with parameters yet, only draft version.
112 	  * 
113 	  * @function
114 	  * @name jQuery.addPositionType
115 	  * @param {string} type - name of position rules
116 	  * @param {object} option - options of position rules
117 	  * */
118 	$.addPositionType = function (type, options) {
119 		// TODO: change [options] to [from, to, auto]
120 		/*var obj = {};
121 		if (match=from.match(re))!=null ) {
122 			obj.from = [ match[1]=='right' ? 'R' : 'L', match[2]=='bottom' ? 'B' : 'T'];
123 		}
124 		if (match=to.match(re))!=null ) {
125 			obj.to = [ match[1]=='right' ? 'R' : match[1]=='left' ? 'L' : 'A', match[2]=='bottom' ? 'B' : match[2]=='top' ? 'T' : 'A'];
126 		}*/
127 		$.PositionTypes[type] = options;
128 	}
129     
130     function getPointerRect (event) {
131 		var e = $.event.fix(event);
132 		return {width: 0, height: 0, left: e.pageX, top: e.pageY};
133 	};
134 
135 	function getElementRect (element) {
136 		var jqe = $(element);
137 		var offset = jqe.offset();
138 		return {width: jqe.width(), height: jqe.height(), left: Math.floor(offset.left), top: Math.floor(offset.top)};
139 	};
140 	
141 	function checkCollision (elementRect, windowRect) {
142 		// return 0 if elementRect in windowRect without collision
143 		if (elementRect.left >= windowRect.left &&
144 			elementRect.top >= windowRect.top &&
145 			elementRect.right <= windowRect.right &&  
146 			elementRect.bottom <= windowRect.bottom)
147 			return 0;
148 		// return collision squire
149 		var rect = {left:   (elementRect.left>windowRect.left ? elementRect.left : windowRect.left),
150 					top:    (elementRect.top>windowRect.top ? elementRect.top : windowRect.top),
151 					right:  (elementRect.right<windowRect.right ? elementRect.right : windowRect.right),
152 					bottom: (elementRect.bottom<windowRect.bottom ? elementRect.bottom : windowRect.bottom)};
153 		return (rect.right-rect.left) * (rect.bottom-rect.top);
154 	};
155 	
156 	//function fromLeft() {
157 	/*
158 	 * params: {
159 	 * 	left,top,width,height, //baseRect
160 	 * 	ox,oy, //rectoffset
161 	 * 	w,h // elementDim
162 	 * }
163 	 */
164 	/*	return this.left;
165 	}
166 	
167 	function fromRight(params) {
168 			return this.left + this.width - this.w;
169 	}
170 	
171 	function (params) {
172 		var rect = {left:fromLeft.call(params), right:fromRight.call(params), top:}
173 	}*/
174 	
175 	function getPositionRect(baseRect, rectOffset, elementDim, pos) {
176 		var rect = {};
177 		// TODO: add support for center and middle // may be middle rename to center too
178 		// TODO: add rectOffset support
179 		
180 		var v = pos.charAt(0);
181 		if (v=='L') {
182 			rect.left = baseRect.left// + rectOffset.left;
183 		} else if (v=='R') {
184 			rect.left = baseRect.left + baseRect.width;// - rectOffset.left;
185 		}
186 		
187 		v = pos.charAt(1);
188 		if (v=='T') {
189 			rect.top = baseRect.top// + rectOffset.left;
190 		} else if (v=='B') {
191 			rect.top = baseRect.top + baseRect.height;// - rectOffset.left;
192 		}
193 		
194 		v = pos.charAt(2);
195 		if (v=='L') {
196 			rect.right = rect.left;
197 			rect.left -= elementDim.width;
198 		} else if (v=='R') {
199 			rect.right = rect.left + elementDim.width;
200 		}		
201 		
202 		v = pos.charAt(3);
203 		if (v=='T') {
204 			rect.bottom = rect.top;
205 			rect.top -= elementDim.height;
206 		} else if (v=='B') {
207 			rect.bottom = rect.top + elementDim.height;
208 		}
209 		
210 		return rect;
211 	}
212 	
213 	function __mergePos(s1,s2) {
214 		var result = "";
215 		var ch;
216 		while (result.length < s1.length) {
217 			ch = s1.charAt(result.length);
218 			result += ch == 'A' ? s2.charAt(result.length) : ch; 
219 		}
220 		return result;
221 	}
222 	
223 	function calculatePosition (baseRect, rectOffset, windowRect, elementDim, options) {
224 
225 		var theBest = {square:0};
226 		var rect;
227 		var s;
228 		var ox, oy;
229 		var p = options.from+options.to;
230 		
231 		if (p.indexOf('A')<0) {
232 			return getPositionRect(baseRect, rectOffset, elementDim, p);
233 		} else {
234 			var flag = p=="AAAA";
235 			var pos;
236 			for (var i = 0; i<options.auto.length; i++) {
237 				
238 				// TODO: draft functional
239 				pos = flag ? options.auto[i] : __mergePos(p, options.auto[i]);
240 				rect = getPositionRect(baseRect, rectOffset, elementDim, pos);
241 				ox = rect.left; oy = rect.top;
242 				s = checkCollision(rect, windowRect);
243 				if (s!=0) {
244 					if (ox>=0 && oy>=0 && theBest.square<s) theBest = {x:ox, y:oy, square:s};
245 				} else break;
246 			}
247 			if (s!=0 && (ox<0 || oy<0 || theBest.square>s)) {
248 				ox=theBest.x; oy=theBest.y
249 			}
250 		}
251 		
252 		return {left:ox, top:oy};
253 	}
254 	
255 	function position (rect, element, options) {
256 		var width = element.width();
257 		var height = element.height();
258 		
259 		rect.width = rect.width || 0;
260 		rect.height = rect.height || 0;
261 		
262 		var left = parseInt(element.css('left'),10);
263 		if (isNaN(left) || left==0) {
264 			left = 0;
265 			element.css('left', '0px');
266 		}
267 		if (isNaN(rect.left)) rect.left = left;
268 		
269 		var top = parseInt(element.css('top'),10);
270 		if (isNaN(top) || top==0) {
271 			top = 0;
272 			element.css('top', '0px');
273 		}
274 		if (isNaN(rect.top)) rect.top = top;
275 		
276 		var pos = {};
277 		if (options.noPositionType) {
278 			pos.left = rect.left + rect.width + options.offset[0];
279 			pos.top = rect.top + options.offset[1];
280 		} else {
281 			var jqw = $(window);
282 			var winRect = {left:jqw.scrollLeft(), top:jqw.scrollTop()};
283 			winRect.right = winRect.left + jqw.width();
284 			winRect.bottom = winRect.top + jqw.height();
285 		
286 			pos = calculatePosition(rect, options.offset, winRect, {width:width, height:height}, options);		
287 		}
288 		
289 		// jQuery does not support to get offset for hidden elements
290 		var hideElement=false;
291 		var eVisibility;
292 		var e;
293 		if (element.css("display")=="none") {
294 			hideElement=true;
295 			e = element.get(0);
296 			eVisibility = e.style.visibility;
297 			e.style.visibility = 'hidden';
298 			e.style.display = 'block';
299 		}
300 		
301 		var elementOffset = element.offset();
302 		
303 		if (hideElement) {
304 			e.style.visibility = eVisibility;
305 			e.style.display = 'none';
306 		}
307 		
308 		pos.left += left - Math.floor(elementOffset.left);
309 		pos.top += top - Math.floor(elementOffset.top);	
310 
311 		if (left!=pos.left) {
312 			element.css('left', (pos.left + 'px'));
313 		}
314 		if (top!=pos.top) {
315 			element.css('top', (pos.top + 'px'));
316 		}
317 	};
318 
319 })(jQuery);
320 
321