// ========================================================================== // Project: SproutCore - JavaScript Application Framework // Copyright: ©2006-2011 Strobe Inc. and contributors. // Portions ©2008-2011 Apple Inc. All rights reserved. // License: Licensed under MIT license (see license.js) // ========================================================================== /*jslint evil:true */ sc_require('tasks/task'); SC.LOG_MODULE_LOADING = YES; /** SC.Module is responsible for dynamically loading in JavaScript and other resources. These packages of code and resources, called bundles, can be loaded by your application once it has finished loading, allowing you to reduce the time taken for it to launch. You can explicitly load a module by calling SC.Module.loadModule(), or you can mark a module as prefetched in your Buildfile. In those cases, SproutCore will automatically start to load the bundle once the application has loaded and the user has remained idle for more than one second. */ SC.Module = SC.Object.create(/** @scope SC.Module */ { /** Returns YES if the module is ready; NO if it is not loaded or its dependencies have not yet loaded. @param {String} moduleName the name of the module to check @returns {Boolean} */ isModuleReady: function(moduleName) { var moduleInfo = SC.MODULE_INFO[moduleName] ; return moduleInfo ? !!moduleInfo.isReady : NO ; }, /** Asynchronously loads a module if it is not already loaded. If you pass a function, or a target and action, it will be called once the module has finished loading. If the module you request has dependencies (as specified in the Buildfile) that are not yet loaded, it will load them first before executing the requested module. @param moduleName {String} @param target {Function} @param method {Function} @returns {Boolean} YES if already loaded, NO otherwise */ loadModule: function(moduleName, target, method) { var module = SC.MODULE_INFO[moduleName], callbacks, targets, args = SC.A(arguments).slice(3), log = SC.LOG_MODULE_LOADING, idx, len; // Treat the first parameter as the callback if the target is a function and there is // no method supplied. if (method === undefined && SC.typeOf(target) === SC.T_FUNCTION) { method = target; target = null; } if (log) SC.debug("SC.Module: Attempting to load '%@'", moduleName); // If we couldn't find anything in the SC.MODULE_INFO hash, we don't have any record of the // requested module. if (!module) { throw "SC.Module: could not find module '%@'".fmt(moduleName) ; } // If this module was in the middle of being prefetched, we now need to // execute it immediately when it loads. module.isPrefetching = NO; // If the module is already loaded, execute the callback immediately if SproutCore is loaded, // or else as soon as SC has finished loading. if (module.isLoaded && !module.isWaitingForRunLoop) { if (log) SC.debug("SC.Module: Module '%@' already loaded.", moduleName); // we can't just eval it if its dependencies have not been met... if (!this._dependenciesMetForModule(moduleName)) { // we can't let it return normally here, because we need the module to wait until the end of the run loop. // This is because the module may set up bindings. this._addCallbackForModule(moduleName, target, method, args); this._loadDependenciesForModule(moduleName); return NO; } // If the module has finished loading and we have the string // representation, try to evaluate it now. if (module.source) { if (log) SC.debug("SC.Module: Evaluating JavaScript for module '%@'.", moduleName); this._evaluateStringLoadedModule(module); // we can't let it return normally here, because we need the module to wait until the end of the run loop. // This is because the module may set up bindings. this._addCallbackForModule(moduleName, target, method, args); this.invokeLast(function() { module.isReady = YES; this._moduleDidBecomeReady(moduleName); }); return NO; } if (method) { if (SC.isReady) { SC.Module._invokeCallback(moduleName, target, method, args); } else { // Queue callback for when SC has finished loading. SC.ready(SC.Module, function() { SC.Module._invokeCallback(moduleName, target, method, args); }); } } return YES; } // The module has loaded, but is waiting for the end of the run loop before it is "ready"; // we just need to add the callback. else if (module.isWaitingForRunLoop) { this._addCallbackForModule(moduleName, target, method, args); } // The module is not yet loaded, so register the callback and, if necessary, begin loading // the code. else { if (log) SC.debug("SC.Module: Module '%@' is not loaded, loading now.", moduleName); // If this method is called more than once for the same module before it is finished // loading, we might have multiple callbacks that need to be executed once it loads. this._addCallbackForModule(moduleName, target, method, args); // If this is the first time the module has been requested, determine its dependencies // and begin loading them as well as the JavaScript for this module. if (!module.isLoading) { this._loadDependenciesForModule(moduleName); this._loadCSSForModule(moduleName); this._loadJavaScriptForModule(moduleName); module.isLoading = YES; } return NO; } }, _addCallbackForModule: function(moduleName, target, method, args) { var module = SC.MODULE_INFO[moduleName]; // Retrieve array of callbacks from MODULE_INFO hash. var callbacks = module.callbacks || [] ; if (method) { callbacks.push(function() { SC.Module._invokeCallback(moduleName, target, method, args); }); } module.callbacks = callbacks; }, /** @private Loads a module in string form. If you prefetch a module, its source will be held as a string in memory until SC.Module.loadModule() is called, at which time its JavaScript will be evaluated. You shouldn't call this method directly; instead, mark modules as prefetched in your Buildfile. SproutCore will automatically prefetch those modules once your application has loaded and the user is idle. @param {String} moduleName the name of the module to prefetch */ prefetchModule: function(moduleName) { var module = SC.MODULE_INFO[moduleName]; if (module.isLoading || module.isLoaded) return; if (SC.LOG_MODULE_LOADING) SC.debug("SC.Module: Prefetching module '%@'.", moduleName); this._loadDependenciesForModule(moduleName); this._loadCSSForModule(moduleName); this._loadJavaScriptForModule(moduleName); module.isLoading = YES; module.isPrefetching = YES; }, // .......................................................... // INTERNAL SUPPORT // /** @private If a module is marked for lazy instantiation, this method will execute the closure and call any registered callbacks. */ _executeLazilyInstantiatedModule: function(moduleName, targetName, methodName){ var lazyInfo = SC.LAZY_INSTANTIATION[moduleName]; var target; var method; var idx, len; if (SC.LOG_MODULE_LOADING) { SC.debug("SC.Module: Module '%@' is marked for lazy instantiation, instantiating it now…", moduleName); } len = lazyInfo.length; for (idx = 0; idx < len; idx++) { // Iterate through each function associated with this module, and attempt to execute it. try { lazyInfo[idx](); } catch(e) { SC.Logger.error("SC.Module: Failed to lazily instatiate entry for '%@'".fmt(moduleName)); } } // Free up memory containing the functions once they have been executed. delete SC.LAZY_INSTANTIATION[moduleName]; // Now that we have executed the functions, try to find the target and action for the callback. target = this._targetForTargetName(targetName); method = this._methodForMethodNameInTarget(methodName, target); if (!method) { throw "SC.Module: could not find callback for lazily instantiated module '%@'".fmt(moduleName); } }, /** Evaluates a module's JavaScript if it is stored in string format, then deletes that code from memory. @param {Hash} module the module to evaluate */ _evaluateStringLoadedModule: function(module) { var moduleSource = module.source; // so, force a run loop. jQuery.globalEval(moduleSource); delete module.source; if (module.cssSource) { var el = document.createElement('style'); el.setAttribute('type', 'text/css'); if (el.styleSheet) { el.styleSheet.cssText = module.cssSource; } else { var content = document.createTextNode(module.cssSource); el.appendChild(content); } document.getElementsByTagName('head')[0].appendChild(el); } module.isReady = YES; }, /** @private Creates tags for every CSS resource in a module. @param {String} moduleName the name of the module whose CSS should be loaded */ _loadCSSForModule: function(moduleName) { var head = document.getElementsByTagName('head')[0] ; var module = SC.MODULE_INFO[moduleName]; var styles = module.styles || []; var len = styles.length; var url; var el; var idx; if (!head) head = document.documentElement ; // fix for Opera len = styles.length; for (idx = 0; idx < len; idx++) { url = styles[idx] ; if (url.length > 0) { if (SC.LOG_MODULE_LOADING) SC.debug("SC.Module: Loading CSS file in '%@' -> '%@'", moduleName, url); el = document.createElement('link') ; el.setAttribute('href', url) ; el.setAttribute('rel', "stylesheet") ; el.setAttribute('type', "text/css") ; head.appendChild(el) ; } } el = null; }, _loadJavaScriptForModule: function(moduleName) { var module = SC.MODULE_INFO[moduleName]; var el; var url; var dependencies = module.dependencies; var dependenciesAreLoaded = YES; // If this module has dependencies, determine if they are loaded. if (dependencies && dependencies.length > 0) { dependenciesAreLoaded = this._dependenciesMetForModule(moduleName); } // If the module is prefetched, always load the string representation. if (module.isPrefetched) { url = module.stringURL; } else { if (dependenciesAreLoaded) { // Either we have no dependencies or they've all loaded already, // so just execute the code immediately once it loads. url = module.scriptURL; } else { // Because the dependencies might load after this module, load the // string representation so we can execute it once all dependencies // are in place. url = module.stringURL; } } if (url.length > 0) { if (SC.LOG_MODULE_LOADING) SC.debug("SC.Module: Loading JavaScript file in '%@' -> '%@'", moduleName, url); el = document.createElement('script') ; el.setAttribute('type', "text/javascript") ; el.setAttribute('src', url) ; if (SC.browser.isIE) { el.onreadystatechange = function() { if (this.readyState == 'complete' || this.readyState == 'loaded') { SC.run(function() { SC.Module._moduleDidLoad(moduleName); }); } }; } else { el.onload = function() { SC.run(function(){ SC.Module._moduleDidLoad(moduleName); }); }; } document.body.appendChild(el) ; } }, /** @private Returns YES if all of the dependencies for a module are ready. @param {String} moduleName the name of the module being checked @returns {Boolean} whether the dependencies are loaded */ _dependenciesMetForModule: function(moduleName) { var dependencies = SC.MODULE_INFO[moduleName].dependencies || []; var idx, len = dependencies.length; var dependencyName; var module; for (idx = 0; idx < len; idx++) { dependencyName = dependencies[idx]; module = SC.MODULE_INFO[dependencyName]; if (!module) throw "SC.loadModule: Unable to find dependency %@ for module %@.".fmt(dependencyName, moduleName); if (!module.isReady) { return NO; } } return YES; }, /** Loads all unloaded dependencies for a module, then creates the