      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:'bottom-left', to:'auto-auto'});
      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 (stype == "string" || source.nodeType || source instanceof jQuery || typeof source.length!="undefined") {
     71 					rect = getElementRect(source);
     72 			} else if (source.type) {
     73 				rect = getPointerRect(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 			if (!options.noPositionType) {
     85 				if (options.from.length>2) {
     86 					options.from = positionDefinition[options.from.toLowerCase()];
     87 				}
     88 				if (options.to.length>2) {
     89 					options.to = positionDefinition[options.to.toLowerCase()];
     90 				}
     91 			}
     92 			return this.each(function() {
     93 					element = $(this);
     94 					//alert(rect.left+" "+rect.top+" "+rect.width+" "+rect.height);
     95 					position(rect, element, options);
     96 				});
     97 		}
     98 		return this;
     99 	};
    100
    101 	var defaultType = "TOOLTIP";
    102 	var defaults = {
    103 		collision: "",
    104 		offset: [0,0]
    105 	};
    106 	var re = /^(left|right)-(top|buttom|auto)$/i;
    107
    108 	// TODO: make it private
    109 	var positionDefinition = {
    110 		'top-left':'LT',
    111 		'top-right':'RT',
    112 		'bottom-left':'LB',
    113 		'bottom-right':'RB',
    114 		'top-auto':'AT',
    115 		'bottom-auto':'AB',
    116 		'auto-left':'LA',
    117 		'auto-right':'RA',
    118 		'auto-auto':'AA'
    119 	};
    120 	$.PositionTypes = {
    121 		// horisontal constants: L-left, R-right, C-center, A-auto
    122 		// vertical constants:   T-top, B-bottom, M-middle, A-auto
    123 		// for auto: list of joinPoint-Direction pairs
    124 		TOOLTIP: {from:"AA", to:"AA", auto:["RTRT", "RBRT", "LTRT", "RTLT", "LTLT", "LBLT", "RTRB", "RBRB", "LBRB", "RBLB"]},
    125 		DROPDOWN:{from:"AA", to:"AA", auto:["LBRB", "LTRT", "RBLB", "RTLT"]},
    126         DDMENUGROUP:{from:"AA", to:"AA", auto:["RTRB", "RBRT", "LTLB", "LBLT"]}
    127 	};
    128
    129 	/**
    130 	  * Add or replace position type rules for auto positioning.
    131 	  * Does not fully determinated with parameters yet, only draft version.
    132 	  *
    133 	  * @function
    134 	  * @name jQuery.addPositionType
    135 	  * @param {string} type - name of position rules
    136 	  * @param {object} option - options of position rules
    137 	  * */
    138 	$.addPositionType = function (type, options) {
    139 		// TODO: change [options] to [from, to, auto]
    140 		/*var obj = {};
    141 		if (match=from.match(re))!=null ) {
    142 			obj.from = [ match[1]=='right' ? 'R' : 'L', match[2]=='bottom' ? 'B' : 'T'];
    143 		}
    144 		if (match=to.match(re))!=null ) {
    145 			obj.to = [ match[1]=='right' ? 'R' : match[1]=='left' ? 'L' : 'A', match[2]=='bottom' ? 'B' : match[2]=='top' ? 'T' : 'A'];
    146 		}*/
    147 		$.PositionTypes[type] = options;
    148 	}
    149
    150     function getPointerRect (event) {
    151 		var e = $.event.fix(event);
    152 		return {width: 0, height: 0, left: e.pageX, top: e.pageY};
    153 	};
    154
    155 	function getElementRect (element) {
    156 		var jqe = $(element);
    157 		var offset = jqe.offset();
    158 		var rect = {width: jqe.outerWidth(), height: jqe.outerHeight(), left: Math.floor(offset.left), top: Math.floor(offset.top)};
    159 		if (jqe.length>1) {
    160 			var width, height, offset;
    161 			var e;
    162 			for (var i=1;i<jqe.length;i++) {
    163 				e = jqe.eq(i);
    164 				if (e.css('display')=="none") continue;
    165 				width = e.outerWidth();
    166 				height = e.outerHeight();
    167 				offset = e.offset();
    168 				var d = rect.left - offset.left;
    169 				if (d<0) {
    170 					if (width-d > rect.width) rect.width = width - d;
    171 				} else {
    172 					rect.width += d;
    173 				}
    174 				var d = rect.top - offset.top;
    175 				if (d<0) {
    176 					if (height-d > rect.height) rect.height = height -d;
    177 				} else {
    178 					rect.height += d;
    179 				}
    180 				if (offset.left < rect.left) rect.left = offset.left;
    181 				if (offset.top < rect.top) rect.top = offset.top;
    182 			}
    183 		}
    184
    185 		return rect;
    186 	};
    187
    188 	function checkCollision (elementRect, windowRect) {
    189 		// return 0 if elementRect in windowRect without collision
    190 		if (elementRect.left >= windowRect.left &&
    191 			elementRect.top >= windowRect.top &&
    192 			elementRect.right <= windowRect.right &&
    193 			elementRect.bottom <= windowRect.bottom)
    194 			return 0;
    195 		// return collision squire
    196 		var rect = {left:   (elementRect.left>windowRect.left ? elementRect.left : windowRect.left),
    197 					top:    (elementRect.top>windowRect.top ? elementRect.top : windowRect.top)};
    198 		rect.right = elementRect.right<windowRect.right ? (elementRect.right==elementRect.left ? rect.left : elementRect.right) : windowRect.right;
    199 		rect.bottom = elementRect.bottom<windowRect.bottom ? (elementRect.bottom==elementRect.top ? rect.top : elementRect.bottom) : windowRect.bottom;
    200
    201 		return (rect.right-rect.left) * (rect.bottom-rect.top);
    202 	};
    203
    204 	//function fromLeft() {
    205 	/*
    206 	 * params: {
    207 	 * 	left,top,width,height, //baseRect
    208 	 * 	ox,oy, //rectoffset
    209 	 * 	w,h // elementDim
    210 	 * }
    211 	 */
    212 	/*	return this.left;
    213 	}
    214
    215 	function fromRight(params) {
    216 			return this.left + this.width - this.w;
    217 	}
    218
    219 	function (params) {
    220 		var rect = {left:fromLeft.call(params), right:fromRight.call(params), top:}
    221 	}*/
    222
    223 	function getPositionRect(baseRect, rectOffset, elementDim, pos) {
    224 		var rect = {};
    225 		// TODO: add support for center and middle // may be middle rename to center too
    226
    227 		var v = pos.charAt(0);
    228 		if (v=='L') {
    229 			rect.left = baseRect.left;
    230 		} else if (v=='R') {
    231 			rect.left = baseRect.left + baseRect.width;
    232 		}
    233
    234 		v = pos.charAt(1);
    235 		if (v=='T') {
    236 			rect.top = baseRect.top;
    237 		} else if (v=='B') {
    238 			rect.top = baseRect.top + baseRect.height;
    239 		}
    240
    241 		v = pos.charAt(2);
    242 		if (v=='L') {
    243 			rect.left -= rectOffset[0];
    244 			rect.right = rect.left;
    245 			rect.left -= elementDim.width;
    246 		} else if (v=='R') {
    247 			rect.left += rectOffset[0];
    248 			rect.right = rect.left + elementDim.width;
    249 		}
    250
    251 		v = pos.charAt(3);
    252 		if (v=='T') {
    253 			rect.top -= rectOffset[1];
    254 			rect.bottom = rect.top;
    255 			rect.top -= elementDim.height;
    256 		} else if (v=='B') {
    257 			rect.top += rectOffset[1];
    258 			rect.bottom = rect.top + elementDim.height;
    259 		}
    260
    261 		return rect;
    262 	}
    263
    264 	function __mergePos(s1,s2) {
    265 		var result = "";
    266 		var ch;
    267 		while (result.length < s1.length) {
    268 			ch = s1.charAt(result.length);
    269 			result += ch == 'A' ? s2.charAt(result.length) : ch;
    270 		}
    271 		return result;
    272 	}
    273
    274 	function calculatePosition (baseRect, rectOffset, windowRect, elementDim, options) {
    275
    276 		var theBest = {square:0};
    277 		var rect;
    278 		var s;
    279 		var ox, oy;
    280 		var p = options.from+options.to;
    281
    282 		if (p.indexOf('A')<0) {
    283 			return getPositionRect(baseRect, rectOffset, elementDim, p);
    284 		} else {
    285 			var flag = p=="AAAA";
    286 			var pos;
    287 			for (var i = 0; i<options.auto.length; i++) {
    288
    289 				// TODO: draft functional
    290 				pos = flag ? options.auto[i] : __mergePos(p, options.auto[i]);
    291 				rect = getPositionRect(baseRect, rectOffset, elementDim, pos);
    292 				ox = rect.left; oy = rect.top;
    293 				s = checkCollision(rect, windowRect);
    294 				if (s!=0) {
    295 					if (ox>=0 && oy>=0 && theBest.square<s) theBest = {x:ox, y:oy, square:s};
    296 				} else break;
    297 			}
    298 			if (s!=0 && (ox<0 || oy<0 || theBest.square>s)) {
    299 				ox=theBest.x; oy=theBest.y
    300 			}
    301 		}
    302
    303 		return {left:ox, top:oy};
    304 	}
    305
    306 	function position (rect, element, options) {
    307 		var width = element.width();
    308 		var height = element.height();
    309
    310 		rect.width = rect.width || 0;
    311 		rect.height = rect.height || 0;
    312
    313 		var left = parseInt(element.css('left'),10);
    314 		if (isNaN(left) || left==0) {
    315 			left = 0;
    316 			element.css('left', '0px');
    317 		}
    318 		if (isNaN(rect.left)) rect.left = left;
    319
    320 		var top = parseInt(element.css('top'),10);
    321 		if (isNaN(top) || top==0) {
    322 			top = 0;
    323 			element.css('top', '0px');
    324 		}
    325 		if (isNaN(rect.top)) rect.top = top;
    326
    327 		var pos = {};
    328 		if (options.noPositionType) {
    329 			pos.left = rect.left + rect.width + options.offset[0];
    330 			pos.top = rect.top + options.offset[1];
    331 		} else {
    332 			var jqw = $(window);
    333 			var winRect = {left:jqw.scrollLeft(), top:jqw.scrollTop()};
    334 			winRect.right = winRect.left + jqw.width();
    335 			winRect.bottom = winRect.top + jqw.height();
    336
    337 			pos = calculatePosition(rect, options.offset, winRect, {width:width, height:height}, options);
    338 		}
    339
    340 		// jQuery does not support to get offset for hidden elements
    341 		var hideElement=false;
    342 		var eVisibility;
    343 		var e;
    344 		if (element.css("display")=="none") {
    345 			hideElement=true;
    346 			e = element.get(0);
    347 			eVisibility = e.style.visibility;
    348 			e.style.visibility = 'hidden';
    349 			e.style.display = 'block';
    350 		}
    351
    352 		var elementOffset = element.offset();
    353
    354 		if (hideElement) {
    355 			e.style.visibility = eVisibility;
    356 			e.style.display = 'none';
    357 		}
    358
    359 		pos.left += left - Math.floor(elementOffset.left);
    360 		pos.top += top - Math.floor(elementOffset.top);
    361
    362 		if (left!=pos.left) {
    363 			element.css('left', (pos.left + 'px'));
    364 		}
    365 		if (top!=pos.top) {
    366 			element.css('top', (pos.top + 'px'));
    367 		}
    368 	};
    369
    370 })(jQuery);
    371
    372