1 /**	Manages controllers, providing a means for separating functionality into feature-centric modules.
  2  *	@constructor Creates a new ControllerManager instance.
  3  *	@param {Object} [options]		Hash of options to be given to the instance.
  4  */
  5 puredom.ControllerManager = function(options) {
  6 	puredom.EventEmitter.call(this);
  7 	
  8 	this.controllerOptions = puredom.extend({}, this.controllerOptions);
  9 	this._messageListeners = [];
 10 	this._controllers = [];
 11 	this._current = null;
 12 	
 13 	if (options) {
 14 		if (options.controllerOptions) {
 15 			puredom.extend(this.controllerOptions, options.controllerOptions);
 16 		}
 17 		if (puredom.typeOf(options.singular)==='boolean') {
 18 			this.singular = options.singular;
 19 		}
 20 		if (puredom.typeOf(options.allowLoadDefault)==='boolean') {
 21 			this.allowLoadDefault = options.allowLoadDefault;
 22 		}
 23 	}
 24 };
 25 
 26 puredom.extend(puredom.ControllerManager.prototype, /** @lends puredom.ControllerManager#*/ {
 27 
 28 	/** Options to pass to every controller. */
 29 	controllerOptions : {},
 30 
 31 	
 32 	restoreState : function(state) {
 33 		if (this.initialized!==true) {
 34 			this._initState = state;
 35 		}
 36 		else {
 37 			if (state && state.current) {
 38 				this.load(state.current);
 39 			}
 40 			else {
 41 				this.loadDefault();
 42 			}
 43 		}
 44 	},
 45 
 46 	
 47 	doStateUpdate : function(state, options) {
 48 		if (this.updateState) {
 49 			this.updateState(state, options);
 50 		}
 51 	},
 52 
 53 	
 54 	/**	Initialize the registry. */
 55 	init : function(options) {
 56 		var autoRestore = true;
 57 		if (this.initialized!==true) {
 58 			this.initialized = true;
 59 			if (options) {
 60 				if (options.controllerOptions) {
 61 					puredom.extend(this.controllerOptions, options.controllerOptions);
 62 				}
 63 				if (puredom.typeOf(options.singular)==='boolean') {
 64 					this.singular = options.singular;
 65 				}
 66 				if (puredom.typeOf(options.allowLoadDefault)==='boolean') {
 67 					this.allowLoadDefault = options.allowLoadDefault;
 68 				}
 69 				if (options.autoRestoreOnInit===false) {
 70 					autoRestore = false;
 71 				}
 72 			}
 73 			if (this._initState && autoRestore) {
 74 				this.restoreState(this._initState);
 75 			}
 76 			this._initState = null;
 77 			try {
 78 				delete this._initState;
 79 			}catch(err){}
 80 			if (this.allowLoadDefault!==false && !this.current()) {
 81 				this.loadDefault();
 82 			}
 83 		}
 84 	},
 85 
 86 	
 87 	/**	Destroy the registry. */
 88 	destroy : function() {
 89 		var current, x;
 90 		// supress errors for destructors to avoid chained memory leaks
 91 		try {
 92 			current = this.current();
 93 			if (current) {
 94 				if (current.unload) {
 95 					current.unload();
 96 				}
 97 			}
 98 			for (x=this._controllers.length; x--; ) {
 99 				if (this._controllers[x].destroy) {
100 					this._controllers[x].destroy();
101 				}
102 			}
103 		}catch(err){}
104 		this.controllerOptions = {};
105 		this._controllers = [];
106 		this._messageListeners = [];
107 		this._current = null;
108 	},
109 
110 	
111 	/**	Register a named module. */
112 	register : function(name, controller) {
113 		controller = controller || {};
114 		if (puredom.typeOf(name)==='string') {
115 			controller.name = name;
116 		}
117 		else {
118 			controller = name;
119 		}
120 		this._controllers.push(controller);
121 
122 		this.fireEvent('add', [this.getIdFromName(controller.name)]);
123 	},
124 
125 
126 	/**	Load the given named module. */
127 	load : function(name, options) {
128 		var sandboxController, previousController, params, newController, eventResponse, response, loadResponse, unloadResponse;
129 		
130 		name = (name+'').toLowerCase();
131 		previousController = this.singular===true && this.current();
132 		
133 		if (previousController && previousController.name.toLowerCase()===name) {
134 			if (previousController.handleRepeatLoad) {
135 				previousController.handleRepeatLoad(options || {});
136 			}
137 			return true;
138 		}
139 		
140 		sandboxController = this._createControllerSandbox(name);
141 		params = puredom.extend({
142 				previousController : previousController
143 			}, 
144 			this.controllerOptions || {}, 
145 			options || {},
146 			sandboxController.sandbox
147 		);
148 		newController = name && this.get(name);
149 		
150 		if (newController) {
151 			puredom.extend(newController, sandboxController.sandbox);
152 			if (this.singular===true) {
153 				unloadResponse = this._unloadCurrent();
154 				if (unloadResponse===false) {
155 					return false;
156 				}
157 			}
158 			response = newController;
159 			if (newController.load) {
160 				eventResponse = this.fireEvent('beforeload', [newController.name]);
161 				if (eventResponse===false || (eventResponse.falsey && !eventResponse.truthy)) {
162 					return false;
163 				}
164 				loadResponse = newController.load(params);
165 				if (loadResponse!==null && loadResponse!==undefined) {
166 					response = loadResponse;
167 				}
168 			}
169 			// if the new controller doens't load, go back to the old one
170 			if (loadResponse===false) {
171 				eventResponse = this.fireEvent('loadcancel', [newController.name]);
172 				if (eventResponse===false || (eventResponse.falsey && !eventResponse.truthy)) {
173 					return false;
174 				}
175 				if (this.singular===true && params.previousController) {
176 					this.load(params.previousController.name, options);
177 				}
178 				else {
179 					this.loadDefault(options);
180 				}
181 			}
182 			else {
183 				this._current = this.getIdFromName(name);
184 				this.fireEvent('load', [name]);
185 				this.fireEvent('change', [name]);
186 				this.doStateUpdate({
187 					current : name
188 				});
189 			}
190 			return response;
191 		}
192 		return false;
193 	},
194 
195 
196 	/**	Load the default module (the module with isDefault=true).
197 	 *	@returns {Boolean} defaultWasLoaded
198 	 */
199 	loadDefault : function(options) {
200 		for (var x=this._controllers.length; x--; ) {
201 			if (this._controllers[x].isDefault===true) {
202 				return this.load(this._controllers[x].name, options);
203 			}
204 		}
205 		return false;
206 	},
207 
208 
209 	/** Load the controller that was previously loaded. <br />
210 	 *	<strong>Note:</strong> This is not actual history, it only remembers one item.
211 	 */
212 	loadPrevious : function(options) {
213 		if (this._previousController) {
214 			this.load(this._previousController, options);
215 		}
216 	},
217 	
218 
219 	/**	Unload the current controller if one exists. */
220 	none : function() {
221 		this._unloadCurrent();
222 	},
223 
224 
225 	/**	Reload the current controller if one exists. */
226 	reloadCurrent : function() {
227 		var current = this.current();
228 		
229 		if (current) {
230 			this._unloadCurrent();
231 			this.load(current.name, this.controllerOptions);
232 		}
233 	},
234 
235 
236 	/**	@private */
237 	_unloadCurrent : function() {
238 		var current = this.current(),
239 			time, ret;
240 		if (current && current.unload) {
241 			ret = this.fireEvent('beforeunload', [current.name]);
242 			if (ret===false || (ret.falsey && !ret.truthy)) {
243 				return false;
244 			}
245 			ret = current.unload();
246 			if (ret===false) {
247 				return false;
248 			}
249 			this.fireEvent('unload', [current.name]);
250 			this._current = null;
251 		}
252 	},
253 	
254 
255 	/**	Get the definition for a given named controller.
256 	 *	@param {String} name					The controller name to find
257 	 *	@param {Boolean} [returnIndex=false]	If true, returns the index instead of a reference.
258 	 */
259 	get : function(name, returnIndex) {
260 		name = (name+'').toLowerCase();
261 		for (var x=this._controllers.length; x--; ) {
262 			if (this._controllers[x].name.toLowerCase()===name) {
263 				return returnIndex===true ? x : this._controllers[x];
264 			}
265 		}
266 		return false;
267 	},
268 	
269 
270 	/**	Post a message to the current controller if it exists. */
271 	postMessage : function(type, msgObj) {
272 		var current = this.current();
273 		if (current && current.onmessage) {
274 			//this.fireEvent('postMessage', [type, msgObj]);
275 			current.onmessage(type, msgObj);
276 			return true;
277 		}
278 		return false;
279 	},
280 
281 
282 	/**	Handle a message from a controller.
283 	 *	@private
284 	 */
285 	onMessage : function(type, handler, controller) {
286 		var obj = {
287 			type : (type+'').toLowerCase().replace(/^on/gim,''),
288 			handler : handler
289 		};
290 		if (controller) {
291 			if (puredom.typeOf(controller)==='string') {
292 				obj.controller = controller.toLowerCase();
293 			}
294 			else if (controller.hasOwnProperty('name')) {
295 				obj.controller = (controller.name + '').toLowerCase();
296 			}
297 		}
298 		this._messageListeners.push(obj);
299 	},
300 
301 
302 	/** Get a list of registered controllers
303 	 *	@param {Array} [properties]		Other properties to include in the list from each controller.
304 	 *	@returns {Array} controllerList
305 	 */
306 	getList : function(properties) {
307 		var map = [],
308 			i, j, ob;
309 		properties = (properties || []);
310 		for (i=0; i<this._controllers.length; i++) {
311 			ob = {
312 				name : this._controllers[i].name
313 			};
314 			for (j=0; j<properties.length; j++) {
315 				ob[properties[j]] = this._controllers[i][properties[j]];
316 			}
317 			map.push(ob);
318 		}
319 		return map;
320 	},
321 
322 
323 	/**	Get a reference to the current controller if it exists. */
324 	current : function() {
325 		return puredom.typeOf(this._current)==='number' && this._controllers[this._current] || false;
326 	},
327 	
328 
329 	/**	@private */
330 	getIdFromName : function(name) {
331 		return this.get(name, true);
332 	},
333 
334 
335 	/**	@private */
336 	getNameFromId : function(id) {
337 		var controller = puredom.typeOf(id)==='number' && this._controllers[id];
338 		return controller && controller.name || false;
339 	},
340 
341 
342 	/** @private */
343 	_createControllerSandbox : function(name) {
344 		var controllerManager = this,
345 			sandbox,
346 			sandboxController,
347 			muted = false,
348 			throwListenerControllerError;
349 		
350 		name = (name + '').toLowerCase();
351 		
352 		/** Throw an error from a listener without blocking other listeners */
353 		throwListenerControllerError = function(listener, error) {
354 			var customError = new Error(
355 				'Listener error encountered in ControllerManager#sandbox.postMessage() :: ' + error.message,
356 				error.fileName,
357 				error.lineNumber
358 			);
359 			setTimeout(function() {
360 				var e = customError;
361 				error = customError = listener = null;
362 				throw(e);
363 			}, 1);
364 		};
365 		
366 		/** A sandbox that can be safely passed to a controller */
367 		sandbox = {
368 			controllerManager : controllerManager,
369 			manager : controllerManager,
370 			postMessage : function(type, msgObj) {
371 				var listener, x;
372 				msgObj = puredom.extend({}, msgObj, {
373 					controller	: name,
374 					type		: (type + '').replace(/^on/gim,'')
375 				});
376 				if (!muted) {
377 					controllerManager.fireEvent('message', msgObj);
378 					controllerManager.fireEvent(msgObj.type, msgObj);
379 					for (x=0; x<controllerManager._messageListeners.length; x++) {
380 						listener = controllerManager._messageListeners[x];
381 						if (!listener.controller || listener.controller===name.toLowerCase()) {
382 							try {
383 								listener.handler(msgObj);
384 							} catch(err) {
385 								throwListnerError(listener, err);
386 							}
387 						}
388 					}
389 				}
390 			}
391 		};
392 		
393 		/** A privileged manager/controller for the sandbox */
394 		sandboxController = {
395 			setName : function(newName) {
396 				name = (newName + '').toLowerCase();
397 			},
398 			mute : function() {
399 				muted = true;
400 			},
401 			unmute : function() {
402 				muted = false;
403 			},
404 			destroy : function() {
405 				for (var x in this.sandbox) {
406 					if (this.sandbox.hasOwnProperty(x)) {
407 						this.sandbox[x] = null;
408 					}
409 				}
410 				delete this.sandbox;
411 				controllerManager = null;
412 			},
413 			sandbox : sandbox
414 		};
415 		
416 		/** cleanup pointless refs: */
417 		setTimeout(function() {
418 			sandboxController = sandbox = null;
419 		}, 1);
420 		
421 		return sandboxController;
422 	},
423 
424 
425 	/** @private */
426 	_postMessageFromController : function(type, msgObj) {
427 	},
428 	
429 	/** @private */
430 	_controllers : [],
431 	
432 	/** @private */
433 	_messageListeners : [],
434 
435 	/** @private */
436 	_current : null
437 });
438 
439 
440 puredom.inherits(puredom.ControllerManager, puredom.EventEmitter);
441 
442 
443 
444 
445 
446 
447 
448 
449 
450 
451 
452 
453 
454 
455 
456 
457 /*
458 switchControllerAsync : function(name, callback) {
459 	var self = this,
460 		params = {
461 			previousController : this.currentController(),
462 			parent : this.controllerParent
463 		},
464 		newController = name && this.getController(name),
465 		loadNewController;
466 	
467 	if (newController) {
468 		loadNewController = function() {
469 			newController.load(params);
470 			self._current = self.getControllerIdFromName(name);
471 			self = newController = params = loadNewController = null;
472 		};
473 		if (params.previousController && params.precontrollerController.unload) {
474 			params.previousController.unload(loadNewController);
475 		}
476 		else {
477 			loadNewController();
478 		}
479 		return true;
480 	}
481 	return false;
482 },
483 */
484