/** * @class ExtMVC.lib.Booter * @extends Ext.util.Observable * Boots an Ext MVC application by loading all application files and launching */ ExtMVC.lib.Booter = Ext.extend(Ext.util.Observable, { /** * @property defaultBootParams * @type Object * Contains default boot parameters (e.g. sets the default environment to 'production') */ defaultBootParams: { environment: 'production' }, constructor: function(config) { config = config || {}; Ext.applyIf(config, this.parseLocationParams()); Ext.apply(this, config, this.defaultBootParams); ExtMVC.lib.Booter.superclass.constructor.apply(this, arguments); this.initEvents(); this.initListeners(); }, /** * The Booter loads some code asynchronously, so uses events to proceed the logic. This sets up * all of the internal event monitoring. */ initListeners: function() { this.on('environment-loaded', this.loadApplicationFiles, this); this.on({ scope : this, 'environment-loaded' : this.loadApplicationFiles, 'application-files-loaded': this.launchApp, 'boot-complete' : this.onBootComplete }); }, /** * Sets up events emitted by this component */ initEvents: function() { this.addEvents( /** * @event before-boot * Called just before boot starts. Use this as a hook to tie in any pre-boot logic * @param {ExtMVC.lib.Booter} this The Booter instance */ 'before-boot', /** * @event boot-complete * Fires when the entire boot sequence has been completed */ 'boot-complete', /** * @event environment-loaded * Fires when environment config data has been retrieved * @param {ExtMVC.Environment} environment the Ext.Environment object */ 'environment-loaded', /** * @event app-files-loaded * Fires when all application files (overrides, config, models, views and controllers) * have been loaded and are available */ 'application-files-loaded', /** * @event application-launched * Fires after the application has been launched */ 'application-launched' ); }, boot: function() { this.fireEvent('before-boot'); if (this.useLoadingMask) this.addLoadingMask(); this.loadEnvironment(); }, /** * Called when the app has been fully booted. Override to provide you own logic (defaults to an empty function) */ onBootComplete: function() {}, /** * Loads up the current environment by loading config/environment.json, and the appropriate file from within * config/environments/ for the current environment (e.g. config/environments/production.json) */ loadEnvironment: function() { Ext.Ajax.request({ url : '/config/environment.json', scope : this, success: function(response, options) { var envName = this.environment; this.environment = new ExtMVC.Environment(Ext.decode(response.responseText)); Ext.Ajax.request({ url : String.format("/config/environments/{0}.json", envName), success: function(response, options) { this.environment.update(Ext.decode(response.responseText)); this.fireEvent('environment-loaded', this.environment); }, scope : this }); }, failure: function() { Ext.Msg.alert( 'Could not load environment', 'The environment could not be found' ); } }); }, /** * Loads all required application files, fires the 'app-files-loaded' event when done * @param {ExtMVC.Environment} environment The ExtMVC.Environment to gather file list from */ loadApplicationFiles: function(env) { this.loadStylesheets(env); //if the 'scripts' property on the Environment is present then models, controllers, plugins etc are ignored if (Ext.isArray(env.scripts)) { //&& env.scripts.length > 0) { if (env.scripts.length == 0) { this.fireEvent('application-files-loaded'); } else { this.loadFiles(env.scripts, false, function() { this.fireEvent('application-files-loaded'); }, this); } return; } var order = ['overrides', 'config', 'plugins', 'models', 'controllers', 'views'], baseFiles = [], pluginFiles = [], modelFiles = [], controllerFiles = [], viewFiles = []; // var groups = { // 'base': {preserveOrder: false, } // }; var underscore = ExtMVC.Inflector.underscore; Ext.each(env.config, function(file) { baseFiles.push(String.format("../{0}.js", file)); }, this); Ext.each(env.plugins, function(file) { pluginFiles.push(String.format("{0}/{1}/{2}-all.js", env.pluginsDir, file, file)); }, this); Ext.each(env.overrides, function(file) { pluginFiles.push(String.format("{0}/{1}.js", env.overridesDir, file)); }, this); Ext.each(env.models, function(file) { modelFiles.push(String.format("{0}/models/{1}.js", env.appDir, underscore(file))); }, this); Ext.each(env.controllers, function(file) { controllerFiles.push(String.format("{0}/controllers/{1}_controller.js", env.appDir, underscore(file))); }, this); Ext.each(env.views, function(viewObj) { Ext.iterate(viewObj, function(dir, fileList) { Ext.each(fileList, function(file) { viewFiles.push(String.format("{0}/views/{1}/{2}.js", env.appDir, dir, file)); }, this); }, this); }, this); var me = this; var doFireEvent = function() { me.fireEvent('application-files-loaded'); }; this.loadFiles(baseFiles, false, function() { this.loadFiles(pluginFiles, false, function() { this.loadFiles(modelFiles, false, function() { this.loadFiles(controllerFiles, true, function() { this.loadFiles(viewFiles, true, function() { doFireEvent(); }); }); }); }); }); }, /** * Once all application files are loaded, this launches the application, hides the loading mask, fires the * 'application-launched' event */ launchApp: function() { ExtMVC.app.onReady(); if (this.useLoadingMask) this.removeLoadingMask(); this.fireEvent('application-launched'); this.fireEvent('boot-complete'); }, /** * @property useLoadingMask * @type Boolean * True to automatically add an application loading mask layer to give the user loading feedback (defaults to false) */ useLoadingMask: false, /** * Adds loading mask HTML elements to the page (called at start of bootup) */ addLoadingMask: function() { var body = Ext.getBody(); body.createChild({ id: 'loading-mask' }); body.createChild({ id: 'loading', cn: [{ cls: 'loading-indicator', html: this.getLoadingMaskMessage() }] }); }, /** * Returns the loading mask message string. Override this to provide your own * @return {String} The message to place inside the loading mask (defaults to "Loading...") */ getLoadingMaskMessage: function() { return "Loading..."; }, /** * @property loadingMaskFadeDelay * @type Number * Number of milliseconds after app launch is called before the loading mask will fade away. * Gives your app a little time to draw its UI (defaults to 250) */ loadingMaskFadeDelay: 250, /** * Fades out the loading mask (called after bootup is complete) */ removeLoadingMask: function() { (function(){ Ext.get('loading').remove(); Ext.get('loading-mask').fadeOut({remove:true}); }).defer(this.loadingMaskFadeDelay); }, /** * @private * Inspects document.location and returns an object containing all of the url params * @return {Object} The url params */ parseLocationParams: function() { var args = window.location.search.split("?")[1], params = {}; /** * Read config data from url parameters */ if (args != undefined) { Ext.each(args.split("&"), function(arg) { var splits = arg.split("="), key = splits[0], value = splits[1]; params[key] = value; }, this); } return params; }, /** * Inserts <link> tags to load stylesheets contained in the environment * @param {ExtMVC.lib.Environment} env The environment to load stylesheets from */ loadStylesheets: function(env) { var body = Ext.getBody(); Ext.each(env.stylesheets, function(filename) { body.createChild({ tag : 'link', rel : 'stylesheet', type: 'text/css', href: this.linkToStylesheet(filename) }); }, this); }, linkToStylesheet: function(filename) { if (filename.match(/^http:/)) { return filename } else if (filename[0] === '/') { return String.format("/public{0}.css", filename); } else { return String.format("/public/stylesheets/{0}.css", filename) } }, /** * Creates and returns a script tag, but does not place it into the document. If a callback function * is passed, this is called when the script has been loaded * @param {String} filename The name of the file to create a script tag for * @param {Function} callback Optional callback, which is called when the script has been loaded * @return {Element} The new script ta */ buildScriptTag: function(filename, callback) { var script = document.createElement('script'); script.type = "text/javascript"; script.src = filename; //IE has a different way of handling <script> loads, so we need to check for it here if (script.readyState) { script.onreadystatechange = function(){ if (script.readyState == "loaded" || script.readyState == "complete") { script.onreadystatechange = null; callback(); } }; } else { script.onload = callback; } return script; }, /** * Loads a given set of application .js files. Calls the callback function when all files have been loaded * Set preserveOrder to true to ensure non-parallel loading of files, if load ordering is important * @param {Array} fileList Array of all files to load * @param {Boolean} preserveOrder True to make files load in serial, one after the other (defaults to false) * @param {Function} callback Callback to call after all files have been loaded * @param {Object} scope The scope to call the callback in */ loadFiles: function(fileList, preserveOrder, callback, scope) { var scope = scope || this, head = document.getElementsByTagName("head")[0], fragment = document.createDocumentFragment(), numFiles = fileList.length, loadedFiles = 0, me = this; if (fileList.length == 0) { callback.call(scope); return; } /** * Loads a particular file from the fileList by index. This is used when preserving order */ var loadFileIndex = function(index) { head.appendChild( me.buildScriptTag(fileList[index], onFileLoaded) ); }; /** * Callback function which is called after each file has been loaded. This calls the callback * passed to loadFiles once the final file in the fileList has been loaded */ var onFileLoaded = function() { loadedFiles ++; //if this was the last file, call the callback, otherwise load the next file if (numFiles == loadedFiles && Ext.isFunction(callback)) { callback.call(scope); } else { if (preserveOrder === true) loadFileIndex(loadedFiles); } }; if (preserveOrder === true) { loadFileIndex.call(this, 0); } else { //load each file (most browsers will do this in parallel) Ext.each(fileList, function(file, index) { fragment.appendChild( this.buildScriptTag(file, onFileLoaded) ); }, this); head.appendChild(fragment); } } }); Ext.onReady(function() { ExtMVC.booter = new ExtMVC.lib.Booter(); ExtMVC.booter.boot(); });