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