1 /**
  2  * @author Gillis Haasnoot <gillis.haasnoot@gmail.com>
  3  * @package Banana.Util
  4  * @summary UrlManager
  5  */
  6 
  7 /** @namespace Banana.Util.UrlManager */
  8 goog.provide('Banana.Util.UrlManager');
  9 
 10 /**
 11  * Url Manager. It allows a user to manager url parameters in the achor part of the url.
 12  * It provides the functionality to set url parameters, listen to changes in the url and 
 13  * mark and read history points
 14  * 
 15  * @constructor
 16  */
 17 Banana.Util.UrlManager = (function()
 18 {
 19 	var initials = [];
 20 	var listeners = {};
 21 	var paramHistory = {};
 22 	var delim = "&";
 23 	var members = {};
 24 	var started = false;
 25 	var timeout; 
 26 	var timeoutLength = 20;
 27 	var handler = null;
 28 	var urlCheckHandler = null;
 29 	var running = false;
 30 	
 31 	/**
 32 	 * @ignore
 33 	 * @return object
 34 	 */
 35 	var getUrlObject = function()
 36 	{
 37 		var url = getBrowserUrl(); 
 38 		var urlObject = {};
 39 		var key; 
 40 		var value; 
 41 		var itemData;
 42 		
 43 		url = url.split(delim);
 44 		
 45 		for (var i = url.length-1; i>=0; i--) {
 46 			itemData = url[i].split("=");
 47 			key = itemData[0];
 48 			urlObject[key] = itemData.slice(1).join("=");
 49 		}
 50 		return urlObject;
 51 	};
 52 	
 53 	
 54 	/**
 55 	 * sets the browser url. for ie we do this in a iframe
 56 	 * @ignore
 57 	 */
 58 	var setBrowserUrl = function(newUrl) 
 59 	{
 60 		if (navigator.appName == 'Microsoft Internet Explorer' && document.getElementById("URLFrame")) 
 61 		{
 62 			document.frames["URLFrame"].location.replace(document.frames["URLFrame"].location.pathname + "?" + location.hash.slice(1));
 63 			
 64 			document.getElementById("URLFrame").setAttribute("src", document.frames["URLFrame"].location.pathname + "?" + newUrl);
 65 			location.hash = newUrl;
 66 		}
 67 		else 
 68 		{
 69 			location.hash = newUrl;
 70 		
 71 		}
 72 	};
 73 	
 74 	/**
 75 	 * @return string url of the browser. for ie we use the iframe
 76 	 * @ignore
 77 	 */
 78 	var getBrowserUrl = function()
 79 	{
 80 		if (navigator.appName == 'Microsoft Internet Explorer' && document.getElementById("URLFrame")) {
 81 				
 82 			location.hash = document.frames["URLFrame"].location.search.slice(1);
 83 			return document.frames["URLFrame"].location.search.slice(1);
 84 		}
 85 		else 
 86 		{
 87 			return location.hash.slice(1);
 88 		}
 89 	};
 90 	
 91 	
 92 	/**
 93 	 * updates url according to current registered modules
 94 	 * @ignore
 95 	 */
 96 	var updateBrowserUrl = function() 
 97 	{
 98 		var newUrl = "";
 99 
100 		//to ensure we have always sorted url params.
101 		//we do this to prevent situations with 2 equal url params in 2 different sequences
102 		members = Banana.Util.sortObjectKeys(members);
103 
104 		for (var key in members)
105 		{
106 			if (members[key].bhmValue)
107 			{
108 				newUrl = newUrl + (newUrl.length === 0 ? "" : delim);
109 				newUrl = newUrl + key + "=" + members[key].bhmValue;
110 			}
111 		}
112 		
113 		//dont register duplicated urls
114 		if (newUrl ==getBrowserUrl()) return;
115 		
116 		setBrowserUrl(newUrl);
117 	};
118 	
119 	/**
120 	 * Starts checking the url for changes
121 	 * @ignore
122 	 */
123 	var startChecking = function()
124 	{
125 		stopHandler = false;
126 		
127 		if (!running)
128 		{
129 			startHandler();
130 		}
131 	};
132 	
133 	/**
134 	 * Starts the handler for url change detection.
135 	 * If a change in the achor part of the url is dedected we trigger a url.{key} event
136 	 * @ignore
137 	 */
138 	var startHandler = function()
139 	{
140 		running = true;
141 
142 		if (stopHandler)
143 		{
144 			clearTimeout(urlCheckHandler);
145 			stopHandler = false;
146 			return;
147 		}
148 		
149 		if (!handler)
150 		{
151 			/**
152 			 * @ignore
153 			 */
154 			handler = function()
155 			{
156 				var currentURL = getUrlObject();
157 				var currentURLVal;
158 				for (var key in members) 
159 				{		
160 					currentURLVal = currentURL[key] || "";
161 					if (!members[key] || !currentURL[key]) continue;
162 					
163 					if (members[key].bhmValue != currentURLVal) 
164 					{
165 						members[key].bhmValue = currentURLVal;
166 						jQuery(document).trigger("url." + key, [currentURLVal, key]);
167 					}
168 				}
169 				
170 				startHandler();
171 			};
172 		}
173 			
174 		urlCheckHandler = setTimeout(handler, 20);
175 	};
176 	
177 	/**
178 	 * Stops checking
179 	 * @ignore
180 	 */
181 	var stopChecking = function() 
182 	{	
183 		stopHandler = true;
184 		clearTimeout(urlCheckHandler);	
185 		urlCheckHandler = null;
186 		running = false;
187 	}
188 	
189 	
190 	/**
191 	 * public properties
192 	 */
193 	return({
194 		/**
195 		 * registers module in the url. This doesn't mean that we see it in the url. 
196 		 * A value needs to be set first.
197 		 * 
198 		 * @param {String} name of the url param
199 		 */
200 		registerModule : function(name)
201 		{
202 			if (!members[name])
203 			{
204 				members[name] = {};
205 			}
206 			
207 			if (getUrlObject()[name])
208 			{
209 				members[name].bhmValue =  getUrlObject()[name]; //w
210 			}
211 			else
212 			{
213 				members[name].bhmValue = "";
214 			}
215 		},
216 		
217 		/**
218 		 * Auto registers modules by looking at the current url
219 		 * All parameters in the achor part will be registered as a module
220 		 */
221 		autoRegisterModules : function()
222 		{
223 			params = getUrlObject();
224 			
225 			for(param in params)
226 			{
227 				if (param)
228 				{
229 					this.registerModule(param);
230 				}
231 			}
232 		},
233 		
234 		/**
235 		 * Stop checking the url for changes
236 		 */
237 		stopChecking : function()
238 		{
239 			stopChecking();
240 		},
241 		
242 		/**
243 		 * Start checking the url for changes
244 		 */
245 		startChecking : function()
246 		{
247 			startChecking();
248 		},
249 		
250 		/**
251 		 * unregisters all modules. Change in url params pointing to removed modules
252 		 * will not result in a trigger change event
253 		 */
254 		removeModules : function()
255 		{
256 			for (var key in members)
257 			{
258 				stopChecking();
259 				this.unlistenModule(key);
260 			}
261 			members = {};		
262 		},
263 		
264 		/**
265 		 * Clears the url by first removing all the modules (no change event can occur) 
266 		 * and then remove all the external registered listeners.
267 		 */
268 		clearUrl : function()
269 		{
270 			this.removeModules();
271 			//remove rest of registered listeners
272 			this.removeListeners();
273 		},
274 		
275 		/**
276 		 * removes module from url
277 		 * 
278 		 * @param {String} name of the param which should be removed from the url
279 		 * @param {boolean} when true we only remove the param from the url. It stays registered.
280 		 */
281 		removeModule : function(name,hideOnly)
282 		{
283 			stopChecking();
284 
285 			if (!hideOnly)
286 			{
287 				this.unlistenModule(name);
288 				delete members[name];
289 			}
290 			else
291 			{
292 				members[name]['bhmValue'] = '';
293 			}
294 			
295 			updateBrowserUrl();
296 			for (var key in members)
297 			{
298 				return startChecking();
299 			}
300 		},
301 		
302 		/**
303 		 * sets module value in the url.
304 		 * Calling this method will result in a visible url change. 
305 		 * 
306 		 * @param {String} name of the url parameter
307 		 * @param {String} value 
308 		 * @param when true we dont update the url itself. so it wont trigger a change event
309 		 */
310 		setModule : function(name,value,dontUpdateUrl)
311 		{
312 			if (!name )return;
313 			dontUpdateUrl = dontUpdateUrl || false;
314 
315 			if (!members[name]) return false;
316 			
317 			stopChecking();
318 		
319 			members[name].bhmValue = value;
320 			
321 			if (!dontUpdateUrl)
322 			{
323 				if (members[name] && this.getModule[name] != value)
324 				{
325 					updateBrowserUrl();
326 					jQuery(document).trigger("url." + name, [value, name]);
327 				}
328 			}
329 				
330 			startChecking();
331 		},
332 		
333 		/**
334 		 * forces url to be updated
335 		 * Handy when you register multiple modules without updating url to prevent multiple
336 		 * browser history moments 
337 		 */
338 		updateUrl : function()
339 		{
340 			updateBrowserUrl();
341 		},
342 		
343 		/**
344 		 * gets the value of a registered module.
345 		 * We always try to fetch the internally registered value first. 
346 		 * @return {String} param name of in the url
347 		 */
348 		getModule : function(param)
349 		{
350 			//first we try to fetch a value from members array
351 			//we could have set an url param without affecting the url itself.
352 			//this is the array where the value should be in
353 			if (members[param] && members[param].bhmValue)
354 			{
355 				return members[param].bhmValue;
356 			}
357 			//otherwise just fetch it from the url
358 			else if (getUrlObject()[param])
359 			{
360 				return getUrlObject()[param];
361 			}
362 
363 			return null;
364 		},
365 		
366 		/**
367 		 * saves module history 
368 		 */
369 		saveModuleHistory : function()
370 		{
371 			paramHistory[this.getModule('section')] = getUrlObject();	
372 		},
373 
374 		/**
375 		 * @return Object returns module history
376 		 */
377 		getModuleHistory : function(name)
378 		{
379 			return paramHistory[name];
380 		},
381 		
382 		/**
383 		 * gets current browser url.
384 		 * @return {String}
385 		 */
386 		getBrowserUrl : function()
387 		{
388 			return getBrowserUrl();
389 		},
390 	
391 		/**
392 		 * gets complete history
393 		 * @return {Array}
394 		 */
395 		getHistory : function()
396 		{
397 			return paramHistory;
398 		},
399 		
400 		/**
401 		 * binds a function to a url change event. when  
402 		 * 
403 		 * @param {String} name of the url parameter to listen on
404 		 * @param {Function} fn callback function when change is detected
405 		 * @param {mixed} data optional
406 		 */
407 		listenModule : function(name, fn, data)
408 		{
409 			if (!data) {data = {}}
410 			stopChecking(name);
411 			
412 			jQuery(document).bind("url." + name,data, fn);
413 			listeners[name] = true;
414 			startChecking(name);
415 		},
416 		
417 		/**
418 		 * unbinds all functions from change event
419 		 * 
420 		 * @param {String} name of the url parameter
421 		 */
422 		unlistenModule : function(name) 
423 		{
424 			//TODO: Could be done per function instead of removing all binds
425 			jQuery(document).unbind("url." + name);
426 			delete listeners[name];
427 		},
428 		
429 		/**
430 		 * Removes all listeners
431 		 */
432 		removeListeners : function()
433 		{
434 			for (l in listeners)
435 			{
436 				if (typeof(listeners[l]) == 'function') { continue; }
437 
438 				this.unlistenModule(l);
439 			}
440 
441 			listeners = [];
442 		},
443 		
444 		/**
445 		 * @return {Object} of url key values
446 		 */
447 		getURLObject : function()
448 		{
449 			return getUrlObject();
450 		},
451 		
452 		/**
453 		 * Helper to construct url string from complex object
454 		 * @param {String} section
455 		 * @param {Object} params
456 		 * 
457 		 * @return {String} url
458 		 */
459 		createUrlString : function(section,params)
460 		{
461 			if (params)
462 			{
463 				//merge section into the params
464 				params.section = section;		
465 				
466 				params = Banana.Util.sortObjectKeys(params);
467 							
468 				var url = "";
469 				for(var j in params)
470 				{
471 					url +="&"+j+'='+params[j];
472 				}
473 				
474 				//remove first & and return url
475 				return url.substr(1,url.length);
476 			}
477 			else
478 			{
479 				return 'section='+section;
480 			}
481 		}	
482 	});
483 }());