// @tag foundation,core // @require ClassManager.js // @define Ext.Loader /** * @author Jacky Nguyen <jacky@sencha.com> * @docauthor Jacky Nguyen <jacky@sencha.com> * @class Ext.Loader * * Ext.Loader is the heart of the new dynamic dependency loading capability in Ext JS 4+. It is most commonly used * via the {@link Ext#require} shorthand. Ext.Loader supports both asynchronous and synchronous loading * approaches, and leverage their advantages for the best development flow. We'll discuss about the pros and cons of each approach: * * # Asynchronous Loading # * * - Advantages: * + Cross-domain * + No web server needed: you can run the application via the file system protocol (i.e: `file://path/to/your/index * .html`) * + Best possible debugging experience: error messages come with the exact file name and line number * * - Disadvantages: * + Dependencies need to be specified before-hand * * ### Method 1: Explicitly include what you need: ### * * // Syntax * Ext.require({String/Array} expressions); * * // Example: Single alias * Ext.require('widget.window'); * * // Example: Single class name * Ext.require('Ext.window.Window'); * * // Example: Multiple aliases / class names mix * Ext.require(['widget.window', 'layout.border', 'Ext.data.Connection']); * * // Wildcards * Ext.require(['widget.*', 'layout.*', 'Ext.data.*']); * * ### Method 2: Explicitly exclude what you don't need: ### * * // Syntax: Note that it must be in this chaining format. * Ext.exclude({String/Array} expressions) * .require({String/Array} expressions); * * // Include everything except Ext.data.* * Ext.exclude('Ext.data.*').require('*'); * * // Include all widgets except widget.checkbox*, * // which will match widget.checkbox, widget.checkboxfield, widget.checkboxgroup, etc. * Ext.exclude('widget.checkbox*').require('widget.*'); * * # Synchronous Loading on Demand # * * - Advantages: * + There's no need to specify dependencies before-hand, which is always the convenience of including ext-all.js * before * * - Disadvantages: * + Not as good debugging experience since file name won't be shown (except in Firebug at the moment) * + Must be from the same domain due to XHR restriction * + Need a web server, same reason as above * * There's one simple rule to follow: Instantiate everything with Ext.create instead of the `new` keyword * * Ext.create('widget.window', { ... }); // Instead of new Ext.window.Window({...}); * * Ext.create('Ext.window.Window', {}); // Same as above, using full class name instead of alias * * Ext.widget('window', {}); // Same as above, all you need is the traditional `xtype` * * Behind the scene, {@link Ext.ClassManager} will automatically check whether the given class name / alias has already * existed on the page. If it's not, Ext.Loader will immediately switch itself to synchronous mode and automatic load the given * class and all its dependencies. * * # Hybrid Loading - The Best of Both Worlds # * * It has all the advantages combined from asynchronous and synchronous loading. The development flow is simple: * * ### Step 1: Start writing your application using synchronous approach. * * Ext.Loader will automatically fetch all dependencies on demand as they're needed during run-time. For example: * * Ext.onReady(function(){ * var window = Ext.widget('window', { * width: 500, * height: 300, * layout: { * type: 'border', * padding: 5 * }, * title: 'Hello Dialog', * items: [{ * title: 'Navigation', * collapsible: true, * region: 'west', * width: 200, * html: 'Hello', * split: true * }, { * title: 'TabPanel', * region: 'center' * }] * }); * * window.show(); * }) * * ### Step 2: Along the way, when you need better debugging ability, watch the console for warnings like these: ### * * [Ext.Loader] Synchronously loading 'Ext.window.Window'; consider adding Ext.require('Ext.window.Window') before your application's code * ClassManager.js:432 * [Ext.Loader] Synchronously loading 'Ext.layout.container.Border'; consider adding Ext.require('Ext.layout.container.Border') before your application's code * * Simply copy and paste the suggested code above `Ext.onReady`, i.e: * * Ext.require('Ext.window.Window'); * Ext.require('Ext.layout.container.Border'); * * Ext.onReady(...); * * Everything should now load via asynchronous mode. * * # Deployment # * * It's important to note that dynamic loading should only be used during development on your local machines. * During production, all dependencies should be combined into one single JavaScript file. Ext.Loader makes * the whole process of transitioning from / to between development / maintenance and production as easy as * possible. Internally {@link Ext.Loader#history Ext.Loader.history} maintains the list of all dependencies your application * needs in the exact loading sequence. It's as simple as concatenating all files in this array into one, * then include it on top of your application. * * This process will be automated with Sencha Command, to be released and documented towards Ext JS 4 Final. * * @singleton */ Ext.Loader = new function() { var Loader = this, Manager = Ext.ClassManager, Class = Ext.Class, flexSetter = Ext.Function.flexSetter, alias = Ext.Function.alias, pass = Ext.Function.pass, defer = Ext.Function.defer, arrayErase = Ext.Array.erase, //<if nonBrowser> isNonBrowser = typeof window == 'undefined', isNodeJS = isNonBrowser && (typeof require == 'function'), isJsdb = isNonBrowser && typeof system != 'undefined' && system.program.search(/jsdb/) !== -1, isPhantomJS = (typeof phantom != 'undefined' && phantom.fs), //</if> dependencyProperties = ['extend', 'mixins', 'requires'], isInHistory = {}, history = [], slashDotSlashRe = /\/\.\//g, dotRe = /\./g, setPathCount = 0; Ext.apply(Loader, { /** * @private */ isInHistory: isInHistory, /** * An array of class names to keep track of the dependency loading order. * This is not guaranteed to be the same everytime due to the asynchronous * nature of the Loader. * * @property {Array} history */ history: history, /** * Configuration * @private */ config: { /** * @cfg {Boolean} enabled * Whether or not to enable the dynamic dependency loading feature. */ enabled: false, /** * @cfg {Boolean} scriptChainDelay * millisecond delay between asynchronous script injection (prevents stack overflow on some user agents) * 'false' disables delay but potentially increases stack load. */ scriptChainDelay : false, /** * @cfg {Boolean} disableCaching * Appends current timestamp to script files to prevent caching. */ disableCaching: true, /** * @cfg {String} disableCachingParam * The get parameter name for the cache buster's timestamp. */ disableCachingParam: '_dc', /** * @cfg {Boolean} garbageCollect * True to prepare an asynchronous script tag for garbage collection (effective only * if {@link #preserveScripts preserveScripts} is false) */ garbageCollect : false, /** * @cfg {Object} paths * The mapping from namespaces to file paths * * { * 'Ext': '.', // This is set by default, Ext.layout.container.Container will be * // loaded from ./layout/Container.js * * 'My': './src/my_own_folder' // My.layout.Container will be loaded from * // ./src/my_own_folder/layout/Container.js * } * * Note that all relative paths are relative to the current HTML document. * If not being specified, for example, <code>Other.awesome.Class</code> * will simply be loaded from <code>./Other/awesome/Class.js</code> */ paths: { 'Ext': '.' }, /** * @cfg {Boolean} preserveScripts * False to remove and optionally {@link #garbageCollect garbage-collect} asynchronously loaded scripts, * True to retain script element for browser debugger compatibility and improved load performance. */ preserveScripts : true, /** * @cfg {String} scriptCharset * Optional charset to specify encoding of dynamic script content. */ scriptCharset : undefined }, /** * Set the configuration for the loader. This should be called right after ext-(debug).js * is included in the page, and before Ext.onReady. i.e: * * <script type="text/javascript" src="ext-core-debug.js"></script> * <script type="text/javascript"> * Ext.Loader.setConfig({ * enabled: true, * paths: { * 'My': 'my_own_path' * } * }); * </script> * <script type="text/javascript"> * Ext.require(...); * * Ext.onReady(function() { * // application code here * }); * </script> * * Refer to config options of {@link Ext.Loader} for the list of possible properties * * @param {Object} config The config object to override the default values * @return {Ext.Loader} this */ setConfig: function(name, value) { if (Ext.isObject(name) && arguments.length === 1) { Ext.merge(Loader.config, name); if ('paths' in name) { Ext.app.collectNamespaces(name.paths); } } else { Loader.config[name] = (Ext.isObject(value)) ? Ext.merge(Loader.config[name], value) : value; if (name === 'paths') { Ext.app.collectNamespaces(value); } } return Loader; }, /** * Get the config value corresponding to the specified name. If no name is given, will return the config object * @param {String} name The config property name * @return {Object} */ getConfig: function(name) { if (name) { return Loader.config[name]; } return Loader.config; }, /** * Sets the path of a namespace. * For Example: * * Ext.Loader.setPath('Ext', '.'); * * @param {String/Object} name See {@link Ext.Function#flexSetter flexSetter} * @param {String} [path] See {@link Ext.Function#flexSetter flexSetter} * @return {Ext.Loader} this * @method */ setPath: flexSetter(function(name, path) { Loader.config.paths[name] = path; Ext.app.namespaces[name] = true; setPathCount++; return Loader; }), /** * Sets a batch of path entries * * @param {Object } paths a set of className: path mappings * @return {Ext.Loader} this */ addClassPathMappings: function(paths) { var name; if(setPathCount == 0){ Loader.config.paths = paths; } else { for(name in paths){ Loader.config.paths[name] = paths[name]; } } setPathCount++; return Loader; }, /** * Translates a className to a file path by adding the * the proper prefix and converting the .'s to /'s. For example: * * Ext.Loader.setPath('My', '/path/to/My'); * * alert(Ext.Loader.getPath('My.awesome.Class')); // alerts '/path/to/My/awesome/Class.js' * * Note that the deeper namespace levels, if explicitly set, are always resolved first. For example: * * Ext.Loader.setPath({ * 'My': '/path/to/lib', * 'My.awesome': '/other/path/for/awesome/stuff', * 'My.awesome.more': '/more/awesome/path' * }); * * alert(Ext.Loader.getPath('My.awesome.Class')); // alerts '/other/path/for/awesome/stuff/Class.js' * * alert(Ext.Loader.getPath('My.awesome.more.Class')); // alerts '/more/awesome/path/Class.js' * * alert(Ext.Loader.getPath('My.cool.Class')); // alerts '/path/to/lib/cool/Class.js' * * alert(Ext.Loader.getPath('Unknown.strange.Stuff')); // alerts 'Unknown/strange/Stuff.js' * * @param {String} className * @return {String} path */ getPath: function(className) { var path = '', paths = Loader.config.paths, prefix = Loader.getPrefix(className); if (prefix.length > 0) { if (prefix === className) { return paths[prefix]; } path = paths[prefix]; className = className.substring(prefix.length + 1); } if (path.length > 0) { path += '/'; } return path.replace(slashDotSlashRe, '/') + className.replace(dotRe, "/") + '.js'; }, /** * @private * @param {String} className */ getPrefix: function(className) { var paths = Loader.config.paths, prefix, deepestPrefix = ''; if (paths.hasOwnProperty(className)) { return className; } for (prefix in paths) { if (paths.hasOwnProperty(prefix) && prefix + '.' === className.substring(0, prefix.length + 1)) { if (prefix.length > deepestPrefix.length) { deepestPrefix = prefix; } } } return deepestPrefix; }, /** * @private * @param {String} className */ isAClassNameWithAKnownPrefix: function(className) { var prefix = Loader.getPrefix(className); // we can only say it's really a class if className is not equal to any known namespace return prefix !== '' && prefix !== className; }, /** * Loads all classes by the given names and all their direct dependencies; optionally executes * the given callback function when finishes, within the optional scope. * * {@link Ext#require} is alias for {@link Ext.Loader#require}. * * @param {String/Array} expressions Can either be a string or an array of string * @param {Function} fn (Optional) The callback function * @param {Object} scope (Optional) The execution scope (`this`) of the callback function * @param {String/Array} excludes (Optional) Classes to be excluded, useful when being used with expressions */ require: function(expressions, fn, scope, excludes) { if (fn) { fn.call(scope); } }, /** * Synchronously loads all classes by the given names and all their direct dependencies; optionally * executes the given callback function when finishes, within the optional scope. * * {@link Ext#syncRequire} is alias for {@link Ext.Loader#syncRequire}. * * @param {String/Array} expressions Can either be a string or an array of string * @param {Function} fn (Optional) The callback function * @param {Object} scope (Optional) The execution scope (`this`) of the callback function * @param {String/Array} excludes (Optional) Classes to be excluded, useful when being used with expressions */ syncRequire: function() {}, /** * Explicitly exclude files from being loaded. Useful when used in conjunction with a broad include expression. * Can be chained with more `require` and `exclude` methods, eg: * * Ext.exclude('Ext.data.*').require('*'); * * Ext.exclude('widget.button*').require('widget.*'); * * {@link Ext#exclude} is alias for {@link Ext.Loader#exclude}. * * @param {Array} excludes * @return {Object} object contains `require` method for chaining */ exclude: function(excludes) { return { require: function(expressions, fn, scope) { return Loader.require(expressions, fn, scope, excludes); }, syncRequire: function(expressions, fn, scope) { return Loader.syncRequire(expressions, fn, scope, excludes); } }; }, /** * Add a new listener to be executed when all required scripts are fully loaded * * @param {Function} fn The function callback to be executed * @param {Object} scope The execution scope (<code>this</code>) of the callback function * @param {Boolean} withDomReady Whether or not to wait for document dom ready as well */ onReady: function(fn, scope, withDomReady, options) { var oldFn; if (withDomReady !== false && Ext.onDocumentReady) { oldFn = fn; fn = function() { Ext.onDocumentReady(oldFn, scope, options); }; } fn.call(scope); } }); //<feature classSystem.loader> var queue = [], isClassFileLoaded = {}, isFileLoaded = {}, classNameToFilePathMap = {}, scriptElements = {}, readyListeners = [], usedClasses = [], requiresMap = {}, comparePriority = function(listenerA, listenerB) { return listenerB.priority - listenerA.priority; }; Ext.apply(Loader, { /** * @private */ documentHead: typeof document != 'undefined' && (document.head || document.getElementsByTagName('head')[0]), /** * Flag indicating whether there are still files being loaded * @private */ isLoading: false, /** * Maintain the queue for all dependencies. Each item in the array is an object of the format: * * { * requires: [...], // The required classes for this queue item * callback: function() { ... } // The function to execute when all classes specified in requires exist * } * * @private */ queue: queue, /** * Maintain the list of files that have already been handled so that they never get double-loaded * @private */ isClassFileLoaded: isClassFileLoaded, /** * @private */ isFileLoaded: isFileLoaded, /** * Maintain the list of listeners to execute when all required scripts are fully loaded * @private */ readyListeners: readyListeners, /** * Contains classes referenced in `uses` properties. * @private */ optionalRequires: usedClasses, /** * Map of fully qualified class names to an array of dependent classes. * @private */ requiresMap: requiresMap, /** * @private */ numPendingFiles: 0, /** * @private */ numLoadedFiles: 0, /** @private */ hasFileLoadError: false, /** * @private */ classNameToFilePathMap: classNameToFilePathMap, /** * The number of scripts loading via loadScript. * @private */ scriptsLoading: 0, /** * @private */ syncModeEnabled: false, scriptElements: scriptElements, /** * Refresh all items in the queue. If all dependencies for an item exist during looping, * it will execute the callback and call refreshQueue again. Triggers onReady when the queue is * empty * @private */ refreshQueue: function() { var ln = queue.length, i, item, j, requires; // When the queue of loading classes reaches zero, trigger readiness if (!ln && !Loader.scriptsLoading) { return Loader.triggerReady(); } for (i = 0; i < ln; i++) { item = queue[i]; if (item) { requires = item.requires; // Don't bother checking when the number of files loaded // is still less than the array length if (requires.length > Loader.numLoadedFiles) { continue; } // Remove any required classes that are loaded for (j = 0; j < requires.length; ) { if (Manager.isCreated(requires[j])) { // Take out from the queue arrayErase(requires, j, 1); } else { j++; } } // If we've ended up with no required classes, call the callback if (item.requires.length === 0) { arrayErase(queue, i, 1); item.callback.call(item.scope); Loader.refreshQueue(); break; } } } return Loader; }, /** * Inject a script element to document's head, call onLoad and onError accordingly * @private */ injectScriptElement: function(url, onLoad, onError, scope, charset) { var script = document.createElement('script'), dispatched = false, config = Loader.config, onLoadFn = function() { if(!dispatched) { dispatched = true; script.onload = script.onreadystatechange = script.onerror = null; if (typeof config.scriptChainDelay == 'number') { //free the stack (and defer the next script) defer(onLoad, config.scriptChainDelay, scope); } else { onLoad.call(scope); } Loader.cleanupScriptElement(script, config.preserveScripts === false, config.garbageCollect); } }, onErrorFn = function(arg) { defer(onError, 1, scope); //free the stack Loader.cleanupScriptElement(script, config.preserveScripts === false, config.garbageCollect); }; script.type = 'text/javascript'; script.onerror = onErrorFn; charset = charset || config.scriptCharset; if (charset) { script.charset = charset; } /* * IE9 Standards mode (and others) SHOULD follow the load event only * (Note: IE9 supports both onload AND readystatechange events) */ if ('addEventListener' in script ) { script.onload = onLoadFn; } else if ('readyState' in script) { // for <IE9 Compatability script.onreadystatechange = function() { if ( this.readyState == 'loaded' || this.readyState == 'complete' ) { onLoadFn(); } }; } else { script.onload = onLoadFn; } script.src = url; (Loader.documentHead || document.getElementsByTagName('head')[0]).appendChild(script); return script; }, /** * @private */ removeScriptElement: function(url) { if (scriptElements[url]) { Loader.cleanupScriptElement(scriptElements[url], true, !!Loader.getConfig('garbageCollect')); delete scriptElements[url]; } return Loader; }, /** * @private */ cleanupScriptElement: function(script, remove, collect) { var prop; script.onload = script.onreadystatechange = script.onerror = null; if (remove) { Ext.removeNode(script); // Remove, since its useless now if (collect) { for (prop in script) { try { if (prop != 'src') { // If we set the src property to null IE // will try and request a script at './null' script[prop] = null; } delete script[prop]; // and prepare for GC } catch (cleanEx) { //ignore } } } } return Loader; }, /** * Loads the specified script URL and calls the supplied callbacks. If this method * is called before {@link Ext#isReady}, the script's load will delay the transition * to ready. This can be used to load arbitrary scripts that may contain further * {@link Ext#require Ext.require} calls. * * @param {Object/String} options The options object or simply the URL to load. * @param {String} options.url The URL from which to load the script. * @param {Function} [options.onLoad] The callback to call on successful load. * @param {Function} [options.onError] The callback to call on failure to load. * @param {Object} [options.scope] The scope (`this`) for the supplied callbacks. */ loadScript: function (options) { var config = Loader.getConfig(), isString = typeof options == 'string', url = isString ? options : options.url, onError = !isString && options.onError, onLoad = !isString && options.onLoad, scope = !isString && options.scope, onScriptError = function() { Loader.numPendingFiles--; Loader.scriptsLoading--; if (onError) { onError.call(scope, "Failed loading '" + url + "', please verify that the file exists"); } if (Loader.numPendingFiles + Loader.scriptsLoading === 0) { Loader.refreshQueue(); } }, onScriptLoad = function () { Loader.numPendingFiles--; Loader.scriptsLoading--; if (onLoad) { onLoad.call(scope); } if (Loader.numPendingFiles + Loader.scriptsLoading === 0) { Loader.refreshQueue(); } }, src; Loader.isLoading = true; Loader.numPendingFiles++; Loader.scriptsLoading++; src = config.disableCaching ? (url + '?' + config.disableCachingParam + '=' + Ext.Date.now()) : url; scriptElements[url] = Loader.injectScriptElement(src, onScriptLoad, onScriptError); }, /** * Load a script file, supports both asynchronous and synchronous approaches * @private */ loadScriptFile: function(url, onLoad, onError, scope, synchronous) { if (isFileLoaded[url]) { return Loader; } var config = Loader.getConfig(), noCacheUrl = url + (config.disableCaching ? ('?' + config.disableCachingParam + '=' + Ext.Date.now()) : ''), isCrossOriginRestricted = false, xhr, status, onScriptError, debugSourceURL = ""; scope = scope || Loader; Loader.isLoading = true; if (!synchronous) { onScriptError = function() { //<debug error> onError.call(scope, "Failed loading '" + url + "', please verify that the file exists", synchronous); //</debug> }; scriptElements[url] = Loader.injectScriptElement(noCacheUrl, onLoad, onScriptError, scope); } else { if (typeof XMLHttpRequest != 'undefined') { xhr = new XMLHttpRequest(); } else { xhr = new ActiveXObject('Microsoft.XMLHTTP'); } try { xhr.open('GET', noCacheUrl, false); xhr.send(null); } catch (e) { isCrossOriginRestricted = true; } status = (xhr.status === 1223) ? 204 : (xhr.status === 0 && ((self.location || {}).protocol == 'file:' || (self.location || {}).protocol == 'ionp:')) ? 200 : xhr.status; isCrossOriginRestricted = isCrossOriginRestricted || (status === 0); if (isCrossOriginRestricted //<if isNonBrowser> && !isPhantomJS //</if> ) { //<debug error> onError.call(Loader, "Failed loading synchronously via XHR: '" + url + "'; It's likely that the file is either " + "being loaded from a different domain or from the local file system whereby cross origin " + "requests are not allowed due to security reasons. Use asynchronous loading with " + "Ext.require instead.", synchronous); //</debug> } else if ((status >= 200 && status < 300) || (status === 304) //<if isNonBrowser> || isPhantomJS //</if> ) { // Debugger friendly, file names are still shown even though they're eval'ed code // Breakpoints work on both Firebug and Chrome's Web Inspector if (!Ext.isIE) { debugSourceURL = "\n//@ sourceURL=" + url; } Ext.globalEval(xhr.responseText + debugSourceURL); onLoad.call(scope); } else { //<debug> onError.call(Loader, "Failed loading synchronously via XHR: '" + url + "'; please " + "verify that the file exists. " + "XHR status code: " + status, synchronous); //</debug> } // Prevent potential IE memory leak xhr = null; } }, // documented above syncRequire: function() { var syncModeEnabled = Loader.syncModeEnabled; if (!syncModeEnabled) { Loader.syncModeEnabled = true; } Loader.require.apply(Loader, arguments); if (!syncModeEnabled) { Loader.syncModeEnabled = false; } Loader.refreshQueue(); }, // documented above require: function(expressions, fn, scope, excludes) { var excluded = {}, included = {}, excludedClassNames = [], possibleClassNames = [], classNames = [], references = [], callback, syncModeEnabled, filePath, expression, exclude, className, possibleClassName, i, j, ln, subLn; if (excludes) { // Convert possible single string to an array. excludes = (typeof excludes === 'string') ? [ excludes ] : excludes; for (i = 0,ln = excludes.length; i < ln; i++) { exclude = excludes[i]; if (typeof exclude == 'string' && exclude.length > 0) { excludedClassNames = Manager.getNamesByExpression(exclude); for (j = 0,subLn = excludedClassNames.length; j < subLn; j++) { excluded[excludedClassNames[j]] = true; } } } } // Convert possible single string to an array. expressions = (typeof expressions === 'string') ? [ expressions ] : (expressions ? expressions : []); if (fn) { if (fn.length > 0) { callback = function() { var classes = [], i, ln; for (i = 0,ln = references.length; i < ln; i++) { classes.push(Manager.get(references[i])); } return fn.apply(this, classes); }; } else { callback = fn; } } else { callback = Ext.emptyFn; } scope = scope || Ext.global; for (i = 0,ln = expressions.length; i < ln; i++) { expression = expressions[i]; if (typeof expression == 'string' && expression.length > 0) { possibleClassNames = Manager.getNamesByExpression(expression); subLn = possibleClassNames.length; for (j = 0; j < subLn; j++) { possibleClassName = possibleClassNames[j]; if (excluded[possibleClassName] !== true) { references.push(possibleClassName); if (!Manager.isCreated(possibleClassName) && !included[possibleClassName]) { included[possibleClassName] = true; classNames.push(possibleClassName); } } } } } // If the dynamic dependency feature is not being used, throw an error // if the dependencies are not defined if (classNames.length > 0) { if (!Loader.config.enabled) { throw new Error("Ext.Loader is not enabled, so dependencies cannot be resolved dynamically. " + "Missing required class" + ((classNames.length > 1) ? "es" : "") + ": " + classNames.join(', ')); } } else { callback.call(scope); return Loader; } syncModeEnabled = Loader.syncModeEnabled; if (!syncModeEnabled) { queue.push({ requires: classNames.slice(), // this array will be modified as the queue is processed, // so we need a copy of it callback: callback, scope: scope }); } ln = classNames.length; for (i = 0; i < ln; i++) { className = classNames[i]; filePath = Loader.getPath(className); // If we are synchronously loading a file that has already been asychronously loaded before // we need to destroy the script tag and revert the count // This file will then be forced loaded in synchronous if (syncModeEnabled && isClassFileLoaded.hasOwnProperty(className)) { if (!isClassFileLoaded[className]) { Loader.numPendingFiles--; Loader.removeScriptElement(filePath); delete isClassFileLoaded[className]; } } if (!isClassFileLoaded.hasOwnProperty(className)) { isClassFileLoaded[className] = false; classNameToFilePathMap[className] = filePath; Loader.numPendingFiles++; Loader.loadScriptFile( filePath, pass(Loader.onFileLoaded, [className, filePath], Loader), pass(Loader.onFileLoadError, [className, filePath], Loader), Loader, syncModeEnabled ); } } if (syncModeEnabled) { callback.call(scope); if (ln === 1) { return Manager.get(className); } } return Loader; }, /** * @private * @param {String} className * @param {String} filePath */ onFileLoaded: function(className, filePath) { var loaded = isClassFileLoaded[className]; Loader.numLoadedFiles++; isClassFileLoaded[className] = true; isFileLoaded[filePath] = true; // In FF, when we sync load something that has had a script tag inserted, the load event may // sometimes fire even if we clean it up and set it to null, so check if we're already loaded here. if (!loaded) { Loader.numPendingFiles--; } if (Loader.numPendingFiles === 0) { Loader.refreshQueue(); } //<debug> if (!Loader.syncModeEnabled && Loader.numPendingFiles === 0 && Loader.isLoading && !Loader.hasFileLoadError) { var missingClasses = [], missingPaths = [], requires, i, ln, j, subLn; for (i = 0,ln = queue.length; i < ln; i++) { requires = queue[i].requires; for (j = 0,subLn = requires.length; j < subLn; j++) { if (isClassFileLoaded[requires[j]]) { missingClasses.push(requires[j]); } } } if (missingClasses.length < 1) { return; } missingClasses = Ext.Array.filter(Ext.Array.unique(missingClasses), function(item) { return !requiresMap.hasOwnProperty(item); }, Loader); if (missingClasses.length < 1) { return; } for (i = 0,ln = missingClasses.length; i < ln; i++) { missingPaths.push(classNameToFilePathMap[missingClasses[i]]); } throw new Error("The following classes are not declared even if their files have been " + "loaded: '" + missingClasses.join("', '") + "'. Please check the source code of their " + "corresponding files for possible typos: '" + missingPaths.join("', '")); } //</debug> }, /** * @private */ onFileLoadError: function(className, filePath, errorMessage, isSynchronous) { Loader.numPendingFiles--; Loader.hasFileLoadError = true; //<debug error> throw new Error("[Ext.Loader] " + errorMessage); //</debug> }, /** * @private * Ensure that any classes referenced in the `uses` property are loaded. */ addUsedClasses: function (classes) { var cls, i, ln; if (classes) { classes = (typeof classes == 'string') ? [classes] : classes; for (i = 0, ln = classes.length; i < ln; i++) { cls = classes[i]; if (typeof cls == 'string' && !Ext.Array.contains(usedClasses, cls)) { usedClasses.push(cls); } } } return Loader; }, /** * @private */ triggerReady: function() { var listener, refClasses = usedClasses; if (Loader.isLoading) { Loader.isLoading = false; if (refClasses.length !== 0) { // Clone then empty the array to eliminate potential recursive loop issue refClasses = refClasses.slice(); usedClasses.length = 0; // this may immediately call us back if all 'uses' classes // have been loaded Loader.require(refClasses, Loader.triggerReady, Loader); return Loader; } } Ext.Array.sort(readyListeners, comparePriority); // this method can be called with Loader.isLoading either true or false // (can be called with false when all 'uses' classes are already loaded) // this may bypass the above if condition while (readyListeners.length && !Loader.isLoading) { // calls to refreshQueue may re-enter triggerReady // so we cannot necessarily iterate the readyListeners array listener = readyListeners.shift(); listener.fn.call(listener.scope); } return Loader; }, // Documented above already onReady: function(fn, scope, withDomReady, options) { var oldFn; if (withDomReady !== false && Ext.onDocumentReady) { oldFn = fn; fn = function() { Ext.onDocumentReady(oldFn, scope, options); }; } if (!Loader.isLoading) { fn.call(scope); } else { readyListeners.push({ fn: fn, scope: scope, priority: (options && options.priority) || 0 }); } }, /** * @private * @param {String} className */ historyPush: function(className) { if (className && isClassFileLoaded.hasOwnProperty(className) && !isInHistory[className]) { isInHistory[className] = true; history.push(className); } return Loader; } }); /** * Turns on or off the "cache buster" applied to dynamically loaded scripts. Normally * dynamically loaded scripts have an extra query parameter appended to avoid stale * cached scripts. This method can be used to disable this mechanism, and is primarily * useful for testing. This is done using a cookie. * @param {Boolean} disable True to disable the cache buster. * @param {String} [path="/"] An optional path to scope the cookie. * @private */ Ext.disableCacheBuster = function (disable, path) { var date = new Date(); date.setTime(date.getTime() + (disable ? 10*365 : -1) * 24*60*60*1000); date = date.toGMTString(); document.cookie = 'ext-cache=1; expires=' + date + '; path='+(path || '/'); }; //<if nonBrowser> if (isNonBrowser) { if (isNodeJS) { Ext.apply(Loader, { syncModeEnabled: true, setPath: flexSetter(function(name, path) { path = require('fs').realpathSync(path); Loader.config.paths[name] = path; return Loader; }), loadScriptFile: function(filePath, onLoad, onError, scope, synchronous) { require(filePath); onLoad.call(scope); } }); } else if (isJsdb) { Ext.apply(Loader, { syncModeEnabled: true, loadScriptFile: function(filePath, onLoad, onError, scope, synchronous) { load(filePath); onLoad.call(scope); } }); } } //</if> //</feature> /** * @member Ext * @method require * @inheritdoc Ext.Loader#require */ Ext.require = alias(Loader, 'require'); /** * @member Ext * @method syncRequire * @inheritdoc Ext.Loader#syncRequire */ Ext.syncRequire = alias(Loader, 'syncRequire'); /** * Convenient shortcut to {@link Ext.Loader#exclude} * @member Ext * @method exclude * @inheritdoc Ext.Loader#exclude */ Ext.exclude = alias(Loader, 'exclude'); /** * @member Ext * @method onReady * @ignore */ Ext.onReady = function(fn, scope, options) { Loader.onReady(fn, scope, true, options); }; /** * @cfg {String[]} requires * @member Ext.Class * List of classes that have to be loaded before instantiating this class. * For example: * * Ext.define('Mother', { * requires: ['Child'], * giveBirth: function() { * // we can be sure that child class is available. * return new Child(); * } * }); */ Class.registerPreprocessor('loader', function(cls, data, hooks, continueFn) { //<debug> Ext.classSystemMonitor && Ext.classSystemMonitor(cls, 'Ext.Loader#loaderPreprocessor', arguments); //</debug> var me = this, dependencies = [], dependency, className = Manager.getName(cls), i, j, ln, subLn, value, propertyName, propertyValue, requiredMap, requiredDep; /* Loop through the dependencyProperties, look for string class names and push them into a stack, regardless of whether the property's value is a string, array or object. For example: { extend: 'Ext.MyClass', requires: ['Ext.some.OtherClass'], mixins: { observable: 'Ext.util.Observable'; } } which will later be transformed into: { extend: Ext.MyClass, requires: [Ext.some.OtherClass], mixins: { observable: Ext.util.Observable; } } */ for (i = 0,ln = dependencyProperties.length; i < ln; i++) { propertyName = dependencyProperties[i]; if (data.hasOwnProperty(propertyName)) { propertyValue = data[propertyName]; if (typeof propertyValue == 'string') { dependencies.push(propertyValue); } else if (propertyValue instanceof Array) { for (j = 0, subLn = propertyValue.length; j < subLn; j++) { value = propertyValue[j]; if (typeof value == 'string') { dependencies.push(value); } } } else if (typeof propertyValue != 'function') { for (j in propertyValue) { if (propertyValue.hasOwnProperty(j)) { value = propertyValue[j]; if (typeof value == 'string') { dependencies.push(value); } } } } } } if (dependencies.length === 0) { return; } //<feature classSystem.loader> //<debug error> var deadlockPath = [], detectDeadlock; /* Automatically detect deadlocks before-hand, will throw an error with detailed path for ease of debugging. Examples of deadlock cases: - A extends B, then B extends A - A requires B, B requires C, then C requires A The detectDeadlock function will recursively transverse till the leaf, hence it can detect deadlocks no matter how deep the path is. */ if (className) { requiresMap[className] = dependencies; //<debug> requiredMap = Loader.requiredByMap || (Loader.requiredByMap = {}); for (i = 0,ln = dependencies.length; i < ln; i++) { dependency = dependencies[i]; (requiredMap[dependency] || (requiredMap[dependency] = [])).push(className); } //</debug> detectDeadlock = function(cls) { deadlockPath.push(cls); if (requiresMap[cls]) { if (Ext.Array.contains(requiresMap[cls], className)) { throw new Error("Deadlock detected while loading dependencies! '" + className + "' and '" + deadlockPath[1] + "' " + "mutually require each other. Path: " + deadlockPath.join(' -> ') + " -> " + deadlockPath[0]); } for (i = 0,ln = requiresMap[cls].length; i < ln; i++) { detectDeadlock(requiresMap[cls][i]); } } }; detectDeadlock(className); } //</debug> //</feature> Loader.require(dependencies, function() { for (i = 0,ln = dependencyProperties.length; i < ln; i++) { propertyName = dependencyProperties[i]; if (data.hasOwnProperty(propertyName)) { propertyValue = data[propertyName]; if (typeof propertyValue == 'string') { data[propertyName] = Manager.get(propertyValue); } else if (propertyValue instanceof Array) { for (j = 0, subLn = propertyValue.length; j < subLn; j++) { value = propertyValue[j]; if (typeof value == 'string') { data[propertyName][j] = Manager.get(value); } } } else if (typeof propertyValue != 'function') { for (var k in propertyValue) { if (propertyValue.hasOwnProperty(k)) { value = propertyValue[k]; if (typeof value == 'string') { data[propertyName][k] = Manager.get(value); } } } } } } continueFn.call(me, cls, data, hooks); }); return false; }, true, 'after', 'className'); //<feature classSystem.loader> /** * @cfg {String[]} uses * @member Ext.Class * List of optional classes to load together with this class. These aren't neccessarily loaded before * this class is created, but are guaranteed to be available before Ext.onReady listeners are * invoked. For example: * * Ext.define('Mother', { * uses: ['Child'], * giveBirth: function() { * // This code might, or might not work: * // return new Child(); * * // Instead use Ext.create() to load the class at the spot if not loaded already: * return Ext.create('Child'); * } * }); */ Manager.registerPostprocessor('uses', function(name, cls, data) { //<debug> Ext.classSystemMonitor && Ext.classSystemMonitor(cls, 'Ext.Loader#usesPostprocessor', arguments); //</debug> var uses = data.uses; if (uses) { Loader.addUsedClasses(uses); } }); Manager.onCreated(Loader.historyPush); //</feature> }; // simple mechanism for automated means of injecting large amounts of dependency info // at the appropriate time in the load cycle if (Ext._classPathMetadata) { Ext.Loader.addClassPathMappings(Ext._classPathMetadata); Ext._classPathMetadata = null; } // initalize the default path of the framework (function() { var scripts = document.getElementsByTagName('script'), currentScript = scripts[scripts.length - 1], src = currentScript.src, path = src.substring(0, src.lastIndexOf('/') + 1), Loader = Ext.Loader; //<debug> if(src.indexOf("/platform/core/src/class/") != -1) { path = path + "../../../../extjs/"; } else if(src.indexOf("/core/src/class/") != -1) { path = path + "../../../"; } //</debug> Loader.setConfig({ enabled: true, disableCaching: //<debug> (/[?&](?:cache|disableCacheBuster)\b/i.test(location.search) || /(^|[ ;])ext-cache=1/.test(document.cookie)) ? false : //</debug> true, paths: { 'Ext': path + 'src' } }); })(); // allows a tools like dynatrace to deterministically detect onReady state by invoking // a callback (intended for external consumption) Ext._endTime = new Date().getTime(); if (Ext._beforereadyhandler){ Ext._beforereadyhandler(); }