1 /**	Manages controllers, providing a means for separating functionality into feature-centric modules.
  2  *	@constructor Creates a new RouteManager instance.
  3  *	@augments puredom.ControllerManager
  4  *	@param {Object} [options]	Hashmap of options to be given to the instance.
  5  *	@param {Boolean} [options.allowTemplateFallback=false]		If no URL templates match, attempt to load by name.
  6  *	@param {Boolean} [options.useBest=false]					If no URL templates match, attempt to load by name.
  7  *	@param {Boolean} [options.allowPartialUrlFallback=false]	Use the longest URL template match, even if it isn't a perfect match.
  8  */
  9 puredom.RouteManager = function(options) {
 10 	var self = this;
 11 	options = options || {};
 12 	puredom.ControllerManager.call(this);
 13 	
 14 	this.allowTemplateFallback = options.allowTemplateFallback===true || options.useBest===true;
 15 	this.allowPartialUrlFallback = options.allowPartialUrlFallback===true;
 16 	
 17 	/**	@ignore */
 18 	this._controllerUpdateState = function(options) {
 19 		self.doStateUpdate(self._routerState, options);
 20 	};
 21 };
 22 
 23 
 24 puredom.inherits(puredom.RouteManager, puredom.ControllerManager);
 25 
 26 
 27 puredom.extend(puredom.RouteManager.prototype, /** @lends puredom.RouteManager# */ {
 28 	
 29 	/** RouteManagers are singular by default, because a browser can only navigate to one URL at a time. */
 30 	singular : true,
 31 	
 32 
 33 	/** @public */
 34 	rewrites : [
 35 		/*
 36 		{
 37 			inbound : {
 38 				pattern : /^\/?urlToChange(?:\/(.*?))?$/gim,
 39 				replace : '/final/url/$1'
 40 			}
 41 			//outbound	: //gim,
 42 		}
 43 		*/
 44 	],
 45 	
 46 
 47 	/**	For use with StateManager */
 48 	restoreState : function(state) {
 49 		if (this.initialized!==true) {
 50 			this._initState = state;
 51 		}
 52 		else {
 53 			if (state && state.current_url) {
 54 				this.route(state.current_url);
 55 			}
 56 			else {
 57 				this.routeDefault();
 58 			}
 59 		}
 60 	},
 61 	
 62 
 63 	/**	For use with StateManager */
 64 	doStateUpdate : function(state, options) {
 65 		var controller,
 66 			templatedUrl;
 67 		this._routerState = state && puredom.extend({}, state);
 68 		if (state && state.current) {
 69 			controller = this.get(state.current);
 70 			delete state.current;
 71 			templatedUrl = this._templateUrl(controller.customUrl || controller.urlTemplate || controller.name, controller);
 72 			if (templatedUrl.substring(0,1)!=='/') {
 73 				templatedUrl = '/' + templatedUrl;
 74 			}
 75 			state.current_url = templatedUrl;
 76 		}
 77 		this.updateState(state, options);
 78 	},
 79 	
 80 	
 81 	/** @override */
 82 	register : function(name, controller) {
 83 		controller.updateState = controller.updateRouterState = this._controllerUpdateState;
 84 		return puredom.ControllerManager.prototype.register.call(this, name, controller);
 85 	},
 86 	
 87 	
 88 	/** Attempt to route the given URL to a controller. */
 89 	route : function(url) {
 90 		var list = this._controllers,
 91 			normUrl = url.replace(/^[#!\/]+/gm,'').replace(/#.+$/gm,''),
 92 			item, i, p, urlTemplate, matches, params,
 93 			partialMatchName;
 94 		
 95 		for (i=0; i<list.length; i++) {
 96 			item = list[i];
 97 			urlTemplate = item.customUrl || item.urlTemplate || item.name;
 98 			matches = {};
 99 			if (this._checkUrlMatch(urlTemplate, url, matches)===true) {
100 				params = {};
101 				for (p in matches) {
102 					if ((p+'').substring(0,7)==='params.') {
103 						params[p.substring(7)] = matches[p];
104 					}
105 				}
106 				return this.load(item.name, {
107 					params : params
108 				});
109 			}
110 			else if (this.allowPartialUrlFallback===true && this._checkUrlMatch(urlTemplate, url, matches, {partial:true})===true) {
111 				partialMatchName = item.name;
112 			}
113 		}
114 		
115 		if (partialMatchName) {
116 			return this.load(partialMatchName, {
117 				params : {}
118 			});
119 		}
120 		
121 		if (this.allowTemplateFallback!==false && this.get(normUrl)) {
122 			return this.load(normUrl, {
123 				params : {}
124 			});
125 		}
126 		
127 		this.fireEvent('routingError', [{
128 			attemptedFallback : this.allowTemplateFallback!==false,
129 			url : url,
130 			type : 'RoutingError'
131 		}]);
132 		
133 		return false;
134 	},
135 	
136 
137 	/** Load a controller */
138 	/*
139 	load : function(name, options) {
140 		this.__super.prototype.load.apply(this, arguments);
141 		this.doStateUpdate();
142 		return this;
143 	},
144 	*/
145 	
146 
147 	/** Attempt to route the given URL to a controller. */
148 	routeDefault : function(url) {
149 		return this.loadDefault();
150 	},
151 	
152 
153 	/** Template a URL using values from the current controller. Non-matched fields are left unchanged.
154 	 *	@private
155 	 *	@param {String} tpl						The URL template
156 	 *	@param {Object} [controller=current]	Explicit controller reference.
157 	 *	@returns {String} templatedUrl
158 	 */
159 	_templateUrl : function(tpl, controller) {
160 		controller = controller || this.current();
161 		return puredom.template(tpl, controller, false);
162 	},
163 
164 	
165 	/** Check if a URL template matches the given URL.
166 	 *	@private
167 	 *	@param {String} urlTemplate		A URL template, as used in Controller.customUrl
168 	 *	@param {String} url				The URL to test
169 	 *	@param {Object} matches			Optional Object to populate with the matched URL segments
170 	 *	@returns {Boolean} didMatch
171 	 */
172 	_checkUrlMatch : function(urlTemplate, url, matches, options) {
173 		var templateSegments = this._getUrlSegments(urlTemplate),
174 			urlSegments = this._getUrlSegments(url),
175 			tplFieldReg = /^\{([^{}]+)\}$/gim,
176 			isMatch = true,
177 			i;
178 		options = options || {};
179 		matches = matches || {};
180 		if (options.partial===true) {
181 			for (i=templateSegments.length; i--; ) {
182 				if (templateSegments[i].match(tplFieldReg)) {
183 					templateSegments.splice(i, 1);
184 				}
185 			}
186 			//console.log(urlSegments, templateSegments);
187 		}
188 		if (urlSegments.length===templateSegments.length) {
189 			for (i=0; i<urlSegments.length; i++) {
190 				if (urlSegments[i]===templateSegments[i] || templateSegments[i].match(tplFieldReg)) {
191 					matches[templateSegments[i].replace(tplFieldReg,'$1')] = urlSegments[i];
192 				}
193 				else {
194 					isMatch = false;
195 					break;
196 				}
197 			}
198 		}
199 		else {
200 			isMatch = false;
201 		}
202 		return isMatch;
203 	},
204 	
205 
206 	/**	Normalize a URL for comparison
207 	 *	@private
208 	 */
209 	_getUrlSegments : function(url) {
210 		var segs = (url+'').split('/'),
211 			i;
212 		for (i=segs.length; i--; ) {
213 			if (segs[i].replace(/(\s|\/)/gm,'').length===0) {
214 				segs.splice(i, 1);
215 			}
216 		}
217 		return segs;
218 	}
219 	
220 });