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