X-Git-Url: http://git.ithinksw.org/extjs.git/blobdiff_plain/0494b8d9b9bb03ab6c22b34dae81261e3cd7e3e6..HEAD:/docs/source/Loader.html?ds=inline diff --git a/docs/source/Loader.html b/docs/source/Loader.html index 487b924a..f7e95e37 100644 --- a/docs/source/Loader.html +++ b/docs/source/Loader.html @@ -1,107 +1,1187 @@ + - + The source code - - + + + + - -
/*!
- * Ext JS Library 3.3.1
- * Copyright(c) 2006-2010 Sencha Inc.
- * licensing@sencha.com
- * http://www.sencha.com/license
- */
-
/** - * @class Ext.Loader + +
/**
+ * @class Ext.Loader
  * @singleton
- * Simple class to help load JavaScript files on demand
+ * @author Jacky Nguyen <jacky@sencha.com>
+ * @docauthor Jacky Nguyen <jacky@sencha.com>
+ *
+ * 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.createWidget('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`, e.g.:
+ *
+ *     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.
  */
-Ext.Loader = Ext.apply({}, {
-    
/** - * Loads a given set of .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 order is important - * @param {Array} fileList Array of all files to load - * @param {Function} callback Callback to call after all files have been loaded - * @param {Object} scope The scope to call the callback in - * @param {Boolean} preserveOrder True to make files load in serial, one after the other (defaults to false) - */ - load: function(fileList, callback, scope, preserveOrder) { - var scope = scope || this, - head = document.getElementsByTagName("head")[0], - fragment = document.createDocumentFragment(), - numFiles = fileList.length, - loadedFiles = 0, - me = this; - -
/** - * 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 load 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 && typeof callback == 'function') { - 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); +(function(Manager, Class, flexSetter, alias) { + + var + //<if nonBrowser> + isNonBrowser = typeof window === 'undefined', + isNodeJS = isNonBrowser && (typeof require === 'function'), + isPhantomJS = (typeof phantom !== 'undefined' && phantom.fs), + //</if> + dependencyProperties = ['extend', 'mixins', 'requires'], + Loader; + + Loader = Ext.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: [], + + /** + * Maintain the list of files that have already been handled so that they never get double-loaded + * @private + */ + isFileLoaded: {}, + + /** + * Maintain the list of listeners to execute when all required scripts are fully loaded + * @private + */ + readyListeners: [], + + /** + * Contains optional dependencies to be loaded last + * @private + */ + optionalRequires: [], + + /** + * Map of fully qualified class names to an array of dependent classes. + * @private + */ + requiresMap: {}, + + /** + * @private + */ + numPendingFiles: 0, + + /** + * @private + */ + numLoadedFiles: 0, + + /** @private */ + hasFileLoadError: false, + + /** + * @private + */ + classNameToFilePathMap: {}, + + /** + * @property {String[]} history + * 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. + */ + history: [], + + /** + * Configuration + * @private + */ + config: { + /** + * @cfg {Boolean} enabled + * Whether or not to enable the dynamic dependency loading feature. + */ + enabled: 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 {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, `Other.awesome.Class` + * will simply be loaded from `./Other/awesome/Class.js` + */ + paths: { + 'Ext': '.' + } + }, + + /** + * Set the configuration for the loader. This should be called right after ext-core.js + * (or ext-core-debug.js) is included in the page, e.g.: + * + * <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 {String/Object} name Name of the value to override, or a config object to override multiple values. + * @param {Object} value (optional) The new value to set, needed if first parameter is String. + * @return {Ext.Loader} this + */ + setConfig: function(name, value) { + if (Ext.isObject(name) && arguments.length === 1) { + Ext.Object.merge(this.config, name); + } + else { + this.config[name] = (Ext.isObject(value)) ? Ext.Object.merge(this.config[name], value) : value; + } + + return this; + }, + + /** + * 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 this.config[name]; + } + + return this.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) { + //<if nonBrowser> + if (isNonBrowser) { + if (isNodeJS) { + path = require('fs').realpathSync(path); + } + } + //</if> + this.config.paths[name] = path; + + return this; + }), + + /** + * 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 = this.config.paths, + prefix = this.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(/\/\.\//g, '/') + className.replace(/\./g, "/") + '.js'; + }, + + /** + * @private + * @param {String} className + */ + getPrefix: function(className) { + var paths = this.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; + }, + + /** + * 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 = this.queue.length, + i, item, j, requires; + + if (ln === 0) { + this.triggerReady(); + return; + } + + for (i = 0; i < ln; i++) { + item = this.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 > this.numLoadedFiles) { + continue; + } + + j = 0; + + do { + if (Manager.isCreated(requires[j])) { + // Take out from the queue + Ext.Array.erase(requires, j, 1); + } + else { + j++; + } + } while (j < requires.length); + + if (item.requires.length === 0) { + Ext.Array.erase(this.queue, i, 1); + item.callback.call(item.scope); + this.refreshQueue(); + break; + } + } + } + + return this; + }, + + /** + * Inject a script element to document's head, call onLoad and onError accordingly + * @private + */ + injectScriptElement: function(url, onLoad, onError, scope) { + var script = document.createElement('script'), + me = this, + onLoadFn = function() { + me.cleanupScriptElement(script); + onLoad.call(scope); + }, + onErrorFn = function() { + me.cleanupScriptElement(script); + onError.call(scope); + }; + + script.type = 'text/javascript'; + script.src = url; + script.onload = onLoadFn; + script.onerror = onErrorFn; + script.onreadystatechange = function() { + if (this.readyState === 'loaded' || this.readyState === 'complete') { + onLoadFn(); + } + }; + + this.documentHead.appendChild(script); + + return script; + }, + + /** + * @private + */ + cleanupScriptElement: function(script) { + script.onload = null; + script.onreadystatechange = null; + script.onerror = null; + + return this; + }, + + /** + * Load a script file, supports both asynchronous and synchronous approaches + * + * @param {String} url + * @param {Function} onLoad + * @param {Object} scope + * @param {Boolean} synchronous + * @private + */ + loadScriptFile: function(url, onLoad, onError, scope, synchronous) { + var me = this, + noCacheUrl = url + (this.getConfig('disableCaching') ? ('?' + this.getConfig('disableCachingParam') + '=' + Ext.Date.now()) : ''), + fileName = url.split('/').pop(), + isCrossOriginRestricted = false, + xhr, status, onScriptError; + + scope = scope || this; + + this.isLoading = true; + + if (!synchronous) { + onScriptError = function() { + onError.call(scope, "Failed loading '" + url + "', please verify that the file exists", synchronous); + }; + + if (!Ext.isReady && Ext.onDocumentReady) { + Ext.onDocumentReady(function() { + me.injectScriptElement(noCacheUrl, onLoad, onScriptError, scope); + }); + } + else { + this.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; + + if (!isCrossOriginRestricted) { + isCrossOriginRestricted = (status === 0); + } + + if (isCrossOriginRestricted + //<if isNonBrowser> + && !isPhantomJS + //</if> + ) { + onError.call(this, "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); + } + else if (status >= 200 && status < 300 + //<if isNonBrowser> + || isPhantomJS + //</if> + ) { + // Firebug friendly, file names are still shown even though they're eval'ed code + new Function(xhr.responseText + "\n//@ sourceURL=" + fileName)(); + + onLoad.call(scope); + } + else { + onError.call(this, "Failed loading synchronously via XHR: '" + url + "'; please " + + "verify that the file exists. " + + "XHR status code: " + status, synchronous); + } + + // Prevent potential IE memory leak + xhr = null; + } + }, + + /** + * 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, e.g.: + * + * Ext.exclude('Ext.data.*').require('*'); + * + * Ext.exclude('widget.button*').require('widget.*'); + * + * {@link Ext#exclude Ext.exclude} is alias for {@link Ext.Loader#exclude Ext.Loader.exclude} for convenience. + * + * @param {String/String[]} excludes + * @return {Object} object contains `require` method for chaining + */ + exclude: function(excludes) { + var me = this; + + return { + require: function(expressions, fn, scope) { + return me.require(expressions, fn, scope, excludes); + }, + + syncRequire: function(expressions, fn, scope) { + return me.syncRequire(expressions, fn, scope, excludes); + } + }; + }, + + /** + * 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 Ext.syncRequire} is alias for {@link Ext.Loader#syncRequire Ext.Loader.syncRequire} for convenience. + * + * @param {String/String[]} 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/String[]} excludes (Optional) Classes to be excluded, useful when being used with expressions + */ + syncRequire: function() { + this.syncModeEnabled = true; + this.require.apply(this, arguments); + this.refreshQueue(); + this.syncModeEnabled = false; + }, + + /** + * 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 Ext.require} is alias for {@link Ext.Loader#require Ext.Loader.require} for convenience. + * + * @param {String/String[]} 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/String[]} excludes (Optional) Classes to be excluded, useful when being used with expressions + */ + require: function(expressions, fn, scope, excludes) { + var filePath, expression, exclude, className, excluded = {}, + excludedClassNames = [], + possibleClassNames = [], + possibleClassName, classNames = [], + i, j, ln, subLn; + + expressions = Ext.Array.from(expressions); + excludes = Ext.Array.from(excludes); + + fn = fn || Ext.emptyFn; + + scope = scope || Ext.global; + + 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; + } + } + } + + for (i = 0, ln = expressions.length; i < ln; i++) { + expression = expressions[i]; + + if (typeof expression === 'string' && expression.length > 0) { + possibleClassNames = Manager.getNamesByExpression(expression); + + for (j = 0, subLn = possibleClassNames.length; j < subLn; j++) { + possibleClassName = possibleClassNames[j]; + + if (!excluded.hasOwnProperty(possibleClassName) && !Manager.isCreated(possibleClassName)) { + Ext.Array.include(classNames, possibleClassName); + } + } + } + } + + // If the dynamic dependency feature is not being used, throw an error + // if the dependencies are not defined + if (!this.config.enabled) { + if (classNames.length > 0) { + Ext.Error.raise({ + sourceClass: "Ext.Loader", + sourceMethod: "require", + msg: "Ext.Loader is not enabled, so dependencies cannot be resolved dynamically. " + + "Missing required class" + ((classNames.length > 1) ? "es" : "") + ": " + classNames.join(', ') + }); + } + } + + if (classNames.length === 0) { + fn.call(scope); + return this; + } + + this.queue.push({ + requires: classNames, + callback: fn, + scope: scope + }); + + classNames = classNames.slice(); + + for (i = 0, ln = classNames.length; i < ln; i++) { + className = classNames[i]; + + if (!this.isFileLoaded.hasOwnProperty(className)) { + this.isFileLoaded[className] = false; + + filePath = this.getPath(className); + + this.classNameToFilePathMap[className] = filePath; + + this.numPendingFiles++; + + //<if nonBrowser> + if (isNonBrowser) { + if (isNodeJS) { + require(filePath); + } + // Temporary support for hammerjs + else { + var f = fs.open(filePath), + content = '', + line; + + while (true) { + line = f.readLine(); + if (line.length === 0) { + break; + } + content += line; + } + + f.close(); + eval(content); + } + + this.onFileLoaded(className, filePath); + + if (ln === 1) { + return Manager.get(className); + } + + continue; + } + //</if> + this.loadScriptFile( + filePath, + Ext.Function.pass(this.onFileLoaded, [className, filePath], this), + Ext.Function.pass(this.onFileLoadError, [className, filePath]), + this, + this.syncModeEnabled + ); + } + } + + return this; + }, + + /** + * @private + * @param {String} className + * @param {String} filePath + */ + onFileLoaded: function(className, filePath) { + this.numLoadedFiles++; + + this.isFileLoaded[className] = true; + + this.numPendingFiles--; + + if (this.numPendingFiles === 0) { + this.refreshQueue(); + } + + //<debug> + if (this.numPendingFiles <= 1) { + window.status = "Finished loading all dependencies, onReady fired!"; + } + else { + window.status = "Loading dependencies, " + this.numPendingFiles + " files left..."; + } + //</debug> + + //<debug> + if (!this.syncModeEnabled && this.numPendingFiles === 0 && this.isLoading && !this.hasFileLoadError) { + var queue = this.queue, + requires, + i, ln, j, subLn, missingClasses = [], missingPaths = []; + + for (i = 0, ln = queue.length; i < ln; i++) { + requires = queue[i].requires; + + for (j = 0, subLn = requires.length; j < ln; j++) { + if (this.isFileLoaded[requires[j]]) { + missingClasses.push(requires[j]); + } + } + } + + if (missingClasses.length < 1) { + return; + } + + missingClasses = Ext.Array.filter(missingClasses, function(item) { + return !this.requiresMap.hasOwnProperty(item); + }, this); + + for (i = 0,ln = missingClasses.length; i < ln; i++) { + missingPaths.push(this.classNameToFilePathMap[missingClasses[i]]); + } + + Ext.Error.raise({ + sourceClass: "Ext.Loader", + sourceMethod: "onFileLoaded", + msg: "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) { + this.numPendingFiles--; + this.hasFileLoadError = true; + + //<debug error> + Ext.Error.raise({ + sourceClass: "Ext.Loader", + classToLoad: className, + loadPath: filePath, + loadingType: isSynchronous ? 'synchronous' : 'async', + msg: errorMessage + }); + //</debug> + }, + + /** + * @private + */ + addOptionalRequires: function(requires) { + var optionalRequires = this.optionalRequires, + i, ln, require; + + requires = Ext.Array.from(requires); + + for (i = 0, ln = requires.length; i < ln; i++) { + require = requires[i]; + + Ext.Array.include(optionalRequires, require); + } + + return this; + }, + + /** + * @private + */ + triggerReady: function(force) { + var readyListeners = this.readyListeners, + optionalRequires, listener; + + if (this.isLoading || force) { + this.isLoading = false; + + if (this.optionalRequires.length) { + // Clone then empty the array to eliminate potential recursive loop issue + optionalRequires = Ext.Array.clone(this.optionalRequires); + + // Empty the original array + this.optionalRequires.length = 0; + + this.require(optionalRequires, Ext.Function.pass(this.triggerReady, [true], this), this); + return this; + } + + while (readyListeners.length) { + listener = readyListeners.shift(); + listener.fn.call(listener.scope); + + if (this.isLoading) { + return this; + } + } + } + + return this; + }, + + /** + * Adds 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 (`this`) 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); + }; + } + + if (!this.isLoading) { + fn.call(scope); + } + else { + this.readyListeners.push({ + fn: fn, + scope: scope + }); + } + }, + + /** + * @private + * @param {String} className + */ + historyPush: function(className) { + if (className && this.isFileLoaded.hasOwnProperty(className)) { + Ext.Array.include(this.history, className); + } + + return this; } - }, - - /** - * @private - * 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 + }; + + /** + * @member Ext + * @method require + * @alias Ext.Loader#require */ - 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(); + Ext.require = alias(Loader, 'require'); + + /** + * @member Ext + * @method syncRequire + * @alias Ext.Loader#syncRequire + */ + Ext.syncRequire = alias(Loader, 'syncRequire'); + + /** + * @member Ext + * @method exclude + * @alias Ext.Loader#exclude + */ + Ext.exclude = alias(Loader, 'exclude'); + + /** + * @member Ext + * @method onReady + * @alias Ext.Loader#onReady + */ + 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, continueFn) { + var me = this, + dependencies = [], + className = Manager.getName(cls), + i, j, ln, subLn, value, propertyName, propertyValue; + + /* + Basically 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) { +// Loader.historyPush(className); + return; + } + + //<debug error> + var deadlockPath = [], + requiresMap = Loader.requiresMap, + 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; + + detectDeadlock = function(cls) { + deadlockPath.push(cls); + + if (requiresMap[cls]) { + if (Ext.Array.contains(requiresMap[cls], className)) { + Ext.Error.raise({ + sourceClass: "Ext.Loader", + msg: "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]); + } } }; - } else { - script.onload = callback; - } - - return script; - } -}); -
+ + detectDeadlock(className); + } + + //</debug> + + 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); + }); + + return false; + }, true); + + Class.setDefaultPreprocessorPosition('loader', 'after', 'className'); + + /** + * @cfg {String[]} uses + * @member Ext.Class + * List of classes to load together with this class. These aren't neccessarily loaded before + * this class is instantiated. 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) { + var uses = Ext.Array.from(data.uses), + items = [], + i, ln, item; + + for (i = 0, ln = uses.length; i < ln; i++) { + item = uses[i]; + + if (typeof item === 'string') { + items.push(item); + } + } + + Loader.addOptionalRequires(items); + }); + + Manager.setDefaultPostprocessorPosition('uses', 'last'); + +})(Ext.ClassManager, Ext.Class, Ext.Function.flexSetter, Ext.Function.alias); +
- \ No newline at end of file +