Upgrade to ExtJS 4.0.7 - Released 10/19/2011
[extjs.git] / src / core / src / class / Loader.js
1 /*
2
3 This file is part of Ext JS 4
4
5 Copyright (c) 2011 Sencha Inc
6
7 Contact:  http://www.sencha.com/contact
8
9 GNU General Public License Usage
10 This file may be used under the terms of the GNU General Public License version 3.0 as published by the Free Software Foundation and appearing in the file LICENSE included in the packaging of this file.  Please review the following information to ensure the GNU General Public License version 3.0 requirements will be met: http://www.gnu.org/copyleft/gpl.html.
11
12 If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
13
14 */
15 /**
16  * @class Ext.Loader
17  * @singleton
18  * @author Jacky Nguyen <jacky@sencha.com>
19  * @docauthor Jacky Nguyen <jacky@sencha.com>
20  *
21  * Ext.Loader is the heart of the new dynamic dependency loading capability in Ext JS 4+. It is most commonly used
22  * via the {@link Ext#require} shorthand. Ext.Loader supports both asynchronous and synchronous loading
23  * approaches, and leverage their advantages for the best development flow. We'll discuss about the pros and cons
24  * of each approach:
25  *
26  * # Asynchronous Loading
27  *
28  * - Advantages:
29  *       + Cross-domain
30  *       + No web server needed: you can run the application via the file system protocol
31  *     (i.e: `file://path/to/your/index.html`)
32  *       + Best possible debugging experience: error messages come with the exact file name and line number
33  *
34  * - Disadvantages:
35  *       + Dependencies need to be specified before-hand
36  *
37  * ### Method 1: Explicitly include what you need:
38  *
39  *     // Syntax
40  *     Ext.require({String/Array} expressions);
41  *
42  *     // Example: Single alias
43  *     Ext.require('widget.window');
44  *
45  *     // Example: Single class name
46  *     Ext.require('Ext.window.Window');
47  *
48  *     // Example: Multiple aliases / class names mix
49  *     Ext.require(['widget.window', 'layout.border', 'Ext.data.Connection']);
50  *
51  *     // Wildcards
52  *     Ext.require(['widget.*', 'layout.*', 'Ext.data.*']);
53  *
54  * ### Method 2: Explicitly exclude what you don't need:
55  *
56  *     // Syntax: Note that it must be in this chaining format.
57  *     Ext.exclude({String/Array} expressions)
58  *        .require({String/Array} expressions);
59  *
60  *     // Include everything except Ext.data.*
61  *     Ext.exclude('Ext.data.*').require('*'); 
62  *
63  *     // Include all widgets except widget.checkbox*,
64  *     // which will match widget.checkbox, widget.checkboxfield, widget.checkboxgroup, etc.
65  *     Ext.exclude('widget.checkbox*').require('widget.*');
66  *
67  * # Synchronous Loading on Demand
68  *
69  * - Advantages:
70  *       + There's no need to specify dependencies before-hand, which is always the convenience of including
71  *     ext-all.js before
72  *
73  * - Disadvantages:
74  *       + Not as good debugging experience since file name won't be shown (except in Firebug at the moment)
75  *       + Must be from the same domain due to XHR restriction
76  *       + Need a web server, same reason as above
77  *
78  * There's one simple rule to follow: Instantiate everything with Ext.create instead of the `new` keyword
79  *
80  *     Ext.create('widget.window', { ... }); // Instead of new Ext.window.Window({...});
81  *
82  *     Ext.create('Ext.window.Window', {}); // Same as above, using full class name instead of alias
83  *
84  *     Ext.widget('window', {}); // Same as above, all you need is the traditional `xtype`
85  *
86  * Behind the scene, {@link Ext.ClassManager} will automatically check whether the given class name / alias has already
87  * existed on the page. If it's not, Ext.Loader will immediately switch itself to synchronous mode and automatic load
88  * the given class and all its dependencies.
89  *
90  * # Hybrid Loading - The Best of Both Worlds
91  *
92  * It has all the advantages combined from asynchronous and synchronous loading. The development flow is simple:
93  *
94  * ### Step 1: Start writing your application using synchronous approach.
95  *
96  * Ext.Loader will automatically fetch all dependencies on demand as they're needed during run-time. For example:
97  *
98  *     Ext.onReady(function(){
99  *         var window = Ext.createWidget('window', {
100  *             width: 500,
101  *             height: 300,
102  *             layout: {
103  *                 type: 'border',
104  *                 padding: 5
105  *             },
106  *             title: 'Hello Dialog',
107  *             items: [{
108  *                 title: 'Navigation',
109  *                 collapsible: true,
110  *                 region: 'west',
111  *                 width: 200,
112  *                 html: 'Hello',
113  *                 split: true
114  *             }, {
115  *                 title: 'TabPanel',
116  *                 region: 'center'
117  *             }]
118  *         });
119  *
120  *         window.show();
121  *     })
122  *
123  * ### Step 2: Along the way, when you need better debugging ability, watch the console for warnings like these:
124  *
125  *     [Ext.Loader] Synchronously loading 'Ext.window.Window'; consider adding Ext.require('Ext.window.Window') before your application's code ClassManager.js:432
126  *     [Ext.Loader] Synchronously loading 'Ext.layout.container.Border'; consider adding Ext.require('Ext.layout.container.Border') before your application's code
127  *
128  * Simply copy and paste the suggested code above `Ext.onReady`, e.g.:
129  *
130  *     Ext.require('Ext.window.Window');
131  *     Ext.require('Ext.layout.container.Border');
132  *
133  *     Ext.onReady(...);
134  *
135  * Everything should now load via asynchronous mode.
136  *
137  * # Deployment
138  *
139  * It's important to note that dynamic loading should only be used during development on your local machines.
140  * During production, all dependencies should be combined into one single JavaScript file. Ext.Loader makes
141  * the whole process of transitioning from / to between development / maintenance and production as easy as
142  * possible. Internally {@link Ext.Loader#history Ext.Loader.history} maintains the list of all dependencies
143  * your application needs in the exact loading sequence. It's as simple as concatenating all files in this
144  * array into one, then include it on top of your application.
145  *
146  * This process will be automated with Sencha Command, to be released and documented towards Ext JS 4 Final.
147  */
148 (function(Manager, Class, flexSetter, alias) {
149
150     var
151         //<if nonBrowser>
152         isNonBrowser = typeof window === 'undefined',
153         isNodeJS = isNonBrowser && (typeof require === 'function'),
154         isPhantomJS = (typeof phantom !== 'undefined' && phantom.fs),
155         //</if>
156         dependencyProperties = ['extend', 'mixins', 'requires'],
157         Loader;
158
159     Loader = Ext.Loader = {
160         /**
161          * @private
162          */
163         documentHead: typeof document !== 'undefined' && (document.head || document.getElementsByTagName('head')[0]),
164
165         /**
166          * Flag indicating whether there are still files being loaded
167          * @private
168          */
169         isLoading: false,
170
171         /**
172          * Maintain the queue for all dependencies. Each item in the array is an object of the format:
173          * {
174          *      requires: [...], // The required classes for this queue item
175          *      callback: function() { ... } // The function to execute when all classes specified in requires exist
176          * }
177          * @private
178          */
179         queue: [],
180
181         /**
182          * Maintain the list of files that have already been handled so that they never get double-loaded
183          * @private
184          */
185         isFileLoaded: {},
186
187         /**
188          * Maintain the list of listeners to execute when all required scripts are fully loaded
189          * @private
190          */
191         readyListeners: [],
192
193         /**
194          * Contains optional dependencies to be loaded last
195          * @private
196          */
197         optionalRequires: [],
198
199         /**
200          * Map of fully qualified class names to an array of dependent classes.
201          * @private
202          */
203         requiresMap: {},
204
205         /**
206          * @private
207          */
208         numPendingFiles: 0,
209
210         /**
211          * @private
212          */
213         numLoadedFiles: 0,
214
215         /** @private */
216         hasFileLoadError: false,
217
218         /**
219          * @private
220          */
221         classNameToFilePathMap: {},
222
223         /**
224          * @property {String[]} history
225          * An array of class names to keep track of the dependency loading order.
226          * This is not guaranteed to be the same everytime due to the asynchronous nature of the Loader.
227          */
228         history: [],
229
230         /**
231          * Configuration
232          * @private
233          */
234         config: {
235             /**
236              * @cfg {Boolean} enabled
237              * Whether or not to enable the dynamic dependency loading feature.
238              */
239             enabled: false,
240
241             /**
242              * @cfg {Boolean} disableCaching
243              * Appends current timestamp to script files to prevent caching.
244              */
245             disableCaching: true,
246
247             /**
248              * @cfg {String} disableCachingParam
249              * The get parameter name for the cache buster's timestamp.
250              */
251             disableCachingParam: '_dc',
252
253             /**
254              * @cfg {Object} paths
255              * The mapping from namespaces to file paths
256              *
257              *     {
258              *         'Ext': '.', // This is set by default, Ext.layout.container.Container will be
259              *                     // loaded from ./layout/Container.js
260              *
261              *         'My': './src/my_own_folder' // My.layout.Container will be loaded from
262              *                                     // ./src/my_own_folder/layout/Container.js
263              *     }
264              *
265              * Note that all relative paths are relative to the current HTML document.
266              * If not being specified, for example, `Other.awesome.Class`
267              * will simply be loaded from `./Other/awesome/Class.js`
268              */
269             paths: {
270                 'Ext': '.'
271             }
272         },
273
274         /**
275          * Set the configuration for the loader. This should be called right after ext-core.js
276          * (or ext-core-debug.js) is included in the page, e.g.:
277          *
278          *     <script type="text/javascript" src="ext-core-debug.js"></script>
279          *     <script type="text/javascript">
280          *       Ext.Loader.setConfig({
281          *           enabled: true,
282          *           paths: {
283          *               'My': 'my_own_path'
284          *           }
285          *       });
286          *     <script>
287          *     <script type="text/javascript">
288          *       Ext.require(...);
289          *
290          *       Ext.onReady(function() {
291          *           // application code here
292          *       });
293          *     </script>
294          *
295          * Refer to config options of {@link Ext.Loader} for the list of possible properties.
296          *
297          * @param {String/Object} name  Name of the value to override, or a config object to override multiple values.
298          * @param {Object} value  (optional) The new value to set, needed if first parameter is String.
299          * @return {Ext.Loader} this
300          */
301         setConfig: function(name, value) {
302             if (Ext.isObject(name) && arguments.length === 1) {
303                 Ext.Object.merge(this.config, name);
304             }
305             else {
306                 this.config[name] = (Ext.isObject(value)) ? Ext.Object.merge(this.config[name], value) : value;
307             }
308
309             return this;
310         },
311
312         /**
313          * Get the config value corresponding to the specified name.
314          * If no name is given, will return the config object.
315          * @param {String} name The config property name
316          * @return {Object}
317          */
318         getConfig: function(name) {
319             if (name) {
320                 return this.config[name];
321             }
322
323             return this.config;
324         },
325
326         /**
327          * Sets the path of a namespace. For Example:
328          *
329          *     Ext.Loader.setPath('Ext', '.');
330          *
331          * @param {String/Object} name See {@link Ext.Function#flexSetter flexSetter}
332          * @param {String} path See {@link Ext.Function#flexSetter flexSetter}
333          * @return {Ext.Loader} this
334          * @method
335          */
336         setPath: flexSetter(function(name, path) {
337             //<if nonBrowser>
338             if (isNonBrowser) {
339                 if (isNodeJS) {
340                     path = require('fs').realpathSync(path);
341                 }
342             }
343             //</if>
344             this.config.paths[name] = path;
345
346             return this;
347         }),
348
349         /**
350          * Translates a className to a file path by adding the the proper prefix and converting the .'s to /'s.
351          * For example:
352          *
353          *     Ext.Loader.setPath('My', '/path/to/My');
354          *
355          *     alert(Ext.Loader.getPath('My.awesome.Class')); // alerts '/path/to/My/awesome/Class.js'
356          *
357          * Note that the deeper namespace levels, if explicitly set, are always resolved first. For example:
358          *
359          *     Ext.Loader.setPath({
360          *         'My': '/path/to/lib',
361          *         'My.awesome': '/other/path/for/awesome/stuff',
362          *         'My.awesome.more': '/more/awesome/path'
363          *     });
364          *
365          *     alert(Ext.Loader.getPath('My.awesome.Class')); // alerts '/other/path/for/awesome/stuff/Class.js'
366          *
367          *     alert(Ext.Loader.getPath('My.awesome.more.Class')); // alerts '/more/awesome/path/Class.js'
368          *
369          *     alert(Ext.Loader.getPath('My.cool.Class')); // alerts '/path/to/lib/cool/Class.js'
370          *
371          *     alert(Ext.Loader.getPath('Unknown.strange.Stuff')); // alerts 'Unknown/strange/Stuff.js'
372          *
373          * @param {String} className
374          * @return {String} path
375          */
376         getPath: function(className) {
377             var path = '',
378                 paths = this.config.paths,
379                 prefix = this.getPrefix(className);
380
381             if (prefix.length > 0) {
382                 if (prefix === className) {
383                     return paths[prefix];
384                 }
385
386                 path = paths[prefix];
387                 className = className.substring(prefix.length + 1);
388             }
389
390             if (path.length > 0) {
391                 path += '/';
392             }
393
394             return path.replace(/\/\.\//g, '/') + className.replace(/\./g, "/") + '.js';
395         },
396
397         /**
398          * @private
399          * @param {String} className
400          */
401         getPrefix: function(className) {
402             var paths = this.config.paths,
403                 prefix, deepestPrefix = '';
404
405             if (paths.hasOwnProperty(className)) {
406                 return className;
407             }
408
409             for (prefix in paths) {
410                 if (paths.hasOwnProperty(prefix) && prefix + '.' === className.substring(0, prefix.length + 1)) {
411                     if (prefix.length > deepestPrefix.length) {
412                         deepestPrefix = prefix;
413                     }
414                 }
415             }
416
417             return deepestPrefix;
418         },
419
420         /**
421          * Refresh all items in the queue. If all dependencies for an item exist during looping,
422          * it will execute the callback and call refreshQueue again. Triggers onReady when the queue is
423          * empty
424          * @private
425          */
426         refreshQueue: function() {
427             var ln = this.queue.length,
428                 i, item, j, requires;
429
430             if (ln === 0) {
431                 this.triggerReady();
432                 return;
433             }
434
435             for (i = 0; i < ln; i++) {
436                 item = this.queue[i];
437
438                 if (item) {
439                     requires = item.requires;
440
441                     // Don't bother checking when the number of files loaded
442                     // is still less than the array length
443                     if (requires.length > this.numLoadedFiles) {
444                         continue;
445                     }
446
447                     j = 0;
448
449                     do {
450                         if (Manager.isCreated(requires[j])) {
451                             // Take out from the queue
452                             Ext.Array.erase(requires, j, 1);
453                         }
454                         else {
455                             j++;
456                         }
457                     } while (j < requires.length);
458
459                     if (item.requires.length === 0) {
460                         Ext.Array.erase(this.queue, i, 1);
461                         item.callback.call(item.scope);
462                         this.refreshQueue();
463                         break;
464                     }
465                 }
466             }
467
468             return this;
469         },
470
471         /**
472          * Inject a script element to document's head, call onLoad and onError accordingly
473          * @private
474          */
475         injectScriptElement: function(url, onLoad, onError, scope) {
476             var script = document.createElement('script'),
477                 me = this,
478                 onLoadFn = function() {
479                     me.cleanupScriptElement(script);
480                     onLoad.call(scope);
481                 },
482                 onErrorFn = function() {
483                     me.cleanupScriptElement(script);
484                     onError.call(scope);
485                 };
486
487             script.type = 'text/javascript';
488             script.src = url;
489             script.onload = onLoadFn;
490             script.onerror = onErrorFn;
491             script.onreadystatechange = function() {
492                 if (this.readyState === 'loaded' || this.readyState === 'complete') {
493                     onLoadFn();
494                 }
495             };
496
497             this.documentHead.appendChild(script);
498
499             return script;
500         },
501
502         /**
503          * @private
504          */
505         cleanupScriptElement: function(script) {
506             script.onload = null;
507             script.onreadystatechange = null;
508             script.onerror = null;
509
510             return this;
511         },
512
513         /**
514          * Load a script file, supports both asynchronous and synchronous approaches
515          *
516          * @param {String} url
517          * @param {Function} onLoad
518          * @param {Object} scope
519          * @param {Boolean} synchronous
520          * @private
521          */
522         loadScriptFile: function(url, onLoad, onError, scope, synchronous) {
523             var me = this,
524                 noCacheUrl = url + (this.getConfig('disableCaching') ? ('?' + this.getConfig('disableCachingParam') + '=' + Ext.Date.now()) : ''),
525                 fileName = url.split('/').pop(),
526                 isCrossOriginRestricted = false,
527                 xhr, status, onScriptError;
528
529             scope = scope || this;
530
531             this.isLoading = true;
532
533             if (!synchronous) {
534                 onScriptError = function() {
535                     onError.call(scope, "Failed loading '" + url + "', please verify that the file exists", synchronous);
536                 };
537
538                 if (!Ext.isReady && Ext.onDocumentReady) {
539                     Ext.onDocumentReady(function() {
540                         me.injectScriptElement(noCacheUrl, onLoad, onScriptError, scope);
541                     });
542                 }
543                 else {
544                     this.injectScriptElement(noCacheUrl, onLoad, onScriptError, scope);
545                 }
546             }
547             else {
548                 if (typeof XMLHttpRequest !== 'undefined') {
549                     xhr = new XMLHttpRequest();
550                 } else {
551                     xhr = new ActiveXObject('Microsoft.XMLHTTP');
552                 }
553
554                 try {
555                     xhr.open('GET', noCacheUrl, false);
556                     xhr.send(null);
557                 } catch (e) {
558                     isCrossOriginRestricted = true;
559                 }
560
561                 status = (xhr.status === 1223) ? 204 : xhr.status;
562
563                 if (!isCrossOriginRestricted) {
564                     isCrossOriginRestricted = (status === 0);
565                 }
566
567                 if (isCrossOriginRestricted
568                 //<if isNonBrowser>
569                 && !isPhantomJS
570                 //</if>
571                 ) {
572                     onError.call(this, "Failed loading synchronously via XHR: '" + url + "'; It's likely that the file is either " +
573                                        "being loaded from a different domain or from the local file system whereby cross origin " +
574                                        "requests are not allowed due to security reasons. Use asynchronous loading with " +
575                                        "Ext.require instead.", synchronous);
576                 }
577                 else if (status >= 200 && status < 300
578                 //<if isNonBrowser>
579                 || isPhantomJS
580                 //</if>
581                 ) {
582                     // Firebug friendly, file names are still shown even though they're eval'ed code
583                     new Function(xhr.responseText + "\n//@ sourceURL=" + fileName)();
584
585                     onLoad.call(scope);
586                 }
587                 else {
588                     onError.call(this, "Failed loading synchronously via XHR: '" + url + "'; please " +
589                                        "verify that the file exists. " +
590                                        "XHR status code: " + status, synchronous);
591                 }
592
593                 // Prevent potential IE memory leak
594                 xhr = null;
595             }
596         },
597
598         /**
599          * Explicitly exclude files from being loaded. Useful when used in conjunction with a broad include expression.
600          * Can be chained with more `require` and `exclude` methods, e.g.:
601          *
602          *     Ext.exclude('Ext.data.*').require('*');
603          *
604          *     Ext.exclude('widget.button*').require('widget.*');
605          *
606          * {@link Ext#exclude Ext.exclude} is alias for {@link Ext.Loader#exclude Ext.Loader.exclude} for convenience.
607          *
608          * @param {String/String[]} excludes
609          * @return {Object} object contains `require` method for chaining
610          */
611         exclude: function(excludes) {
612             var me = this;
613
614             return {
615                 require: function(expressions, fn, scope) {
616                     return me.require(expressions, fn, scope, excludes);
617                 },
618
619                 syncRequire: function(expressions, fn, scope) {
620                     return me.syncRequire(expressions, fn, scope, excludes);
621                 }
622             };
623         },
624
625         /**
626          * Synchronously loads all classes by the given names and all their direct dependencies;
627          * optionally executes the given callback function when finishes, within the optional scope.
628          *
629          * {@link Ext#syncRequire Ext.syncRequire} is alias for {@link Ext.Loader#syncRequire Ext.Loader.syncRequire} for convenience.
630          *
631          * @param {String/String[]} expressions Can either be a string or an array of string
632          * @param {Function} fn (Optional) The callback function
633          * @param {Object} scope (Optional) The execution scope (`this`) of the callback function
634          * @param {String/String[]} excludes (Optional) Classes to be excluded, useful when being used with expressions
635          */
636         syncRequire: function() {
637             this.syncModeEnabled = true;
638             this.require.apply(this, arguments);
639             this.refreshQueue();
640             this.syncModeEnabled = false;
641         },
642
643         /**
644          * Loads all classes by the given names and all their direct dependencies;
645          * optionally executes the given callback function when finishes, within the optional scope.
646          *
647          * {@link Ext#require Ext.require} is alias for {@link Ext.Loader#require Ext.Loader.require} for convenience.
648          *
649          * @param {String/String[]} expressions Can either be a string or an array of string
650          * @param {Function} fn (Optional) The callback function
651          * @param {Object} scope (Optional) The execution scope (`this`) of the callback function
652          * @param {String/String[]} excludes (Optional) Classes to be excluded, useful when being used with expressions
653          */
654         require: function(expressions, fn, scope, excludes) {
655             var filePath, expression, exclude, className, excluded = {},
656                 excludedClassNames = [],
657                 possibleClassNames = [],
658                 possibleClassName, classNames = [],
659                 i, j, ln, subLn;
660
661             expressions = Ext.Array.from(expressions);
662             excludes = Ext.Array.from(excludes);
663
664             fn = fn || Ext.emptyFn;
665
666             scope = scope || Ext.global;
667
668             for (i = 0, ln = excludes.length; i < ln; i++) {
669                 exclude = excludes[i];
670
671                 if (typeof exclude === 'string' && exclude.length > 0) {
672                     excludedClassNames = Manager.getNamesByExpression(exclude);
673
674                     for (j = 0, subLn = excludedClassNames.length; j < subLn; j++) {
675                         excluded[excludedClassNames[j]] = true;
676                     }
677                 }
678             }
679
680             for (i = 0, ln = expressions.length; i < ln; i++) {
681                 expression = expressions[i];
682
683                 if (typeof expression === 'string' && expression.length > 0) {
684                     possibleClassNames = Manager.getNamesByExpression(expression);
685
686                     for (j = 0, subLn = possibleClassNames.length; j < subLn; j++) {
687                         possibleClassName = possibleClassNames[j];
688
689                         if (!excluded.hasOwnProperty(possibleClassName) && !Manager.isCreated(possibleClassName)) {
690                             Ext.Array.include(classNames, possibleClassName);
691                         }
692                     }
693                 }
694             }
695
696             // If the dynamic dependency feature is not being used, throw an error
697             // if the dependencies are not defined
698             if (!this.config.enabled) {
699                 if (classNames.length > 0) {
700                     Ext.Error.raise({
701                         sourceClass: "Ext.Loader",
702                         sourceMethod: "require",
703                         msg: "Ext.Loader is not enabled, so dependencies cannot be resolved dynamically. " +
704                              "Missing required class" + ((classNames.length > 1) ? "es" : "") + ": " + classNames.join(', ')
705                     });
706                 }
707             }
708
709             if (classNames.length === 0) {
710                 fn.call(scope);
711                 return this;
712             }
713
714             this.queue.push({
715                 requires: classNames,
716                 callback: fn,
717                 scope: scope
718             });
719
720             classNames = classNames.slice();
721
722             for (i = 0, ln = classNames.length; i < ln; i++) {
723                 className = classNames[i];
724
725                 if (!this.isFileLoaded.hasOwnProperty(className)) {
726                     this.isFileLoaded[className] = false;
727
728                     filePath = this.getPath(className);
729
730                     this.classNameToFilePathMap[className] = filePath;
731
732                     this.numPendingFiles++;
733
734                     //<if nonBrowser>
735                     if (isNonBrowser) {
736                         if (isNodeJS) {
737                             require(filePath);
738                         }
739                         // Temporary support for hammerjs
740                         else {
741                             var f = fs.open(filePath),
742                                 content = '',
743                                 line;
744
745                             while (true) {
746                                 line = f.readLine();
747                                 if (line.length === 0) {
748                                     break;
749                                 }
750                                 content += line;
751                             }
752
753                             f.close();
754                             eval(content);
755                         }
756
757                         this.onFileLoaded(className, filePath);
758
759                         if (ln === 1) {
760                             return Manager.get(className);
761                         }
762
763                         continue;
764                     }
765                     //</if>
766                     this.loadScriptFile(
767                         filePath,
768                         Ext.Function.pass(this.onFileLoaded, [className, filePath], this),
769                         Ext.Function.pass(this.onFileLoadError, [className, filePath]),
770                         this,
771                         this.syncModeEnabled
772                     );
773                 }
774             }
775
776             return this;
777         },
778
779         /**
780          * @private
781          * @param {String} className
782          * @param {String} filePath
783          */
784         onFileLoaded: function(className, filePath) {
785             this.numLoadedFiles++;
786
787             this.isFileLoaded[className] = true;
788
789             this.numPendingFiles--;
790
791             if (this.numPendingFiles === 0) {
792                 this.refreshQueue();
793             }
794
795             //<debug>
796             if (this.numPendingFiles <= 1) {
797                 window.status = "Finished loading all dependencies, onReady fired!";
798             }
799             else {
800                 window.status = "Loading dependencies, " + this.numPendingFiles + " files left...";
801             }
802             //</debug>
803
804             //<debug>
805             if (!this.syncModeEnabled && this.numPendingFiles === 0 && this.isLoading && !this.hasFileLoadError) {
806                 var queue = this.queue,
807                     requires,
808                     i, ln, j, subLn, missingClasses = [], missingPaths = [];
809
810                 for (i = 0, ln = queue.length; i < ln; i++) {
811                     requires = queue[i].requires;
812
813                     for (j = 0, subLn = requires.length; j < ln; j++) {
814                         if (this.isFileLoaded[requires[j]]) {
815                             missingClasses.push(requires[j]);
816                         }
817                     }
818                 }
819
820                 if (missingClasses.length < 1) {
821                     return;
822                 }
823
824                 missingClasses = Ext.Array.filter(missingClasses, function(item) {
825                     return !this.requiresMap.hasOwnProperty(item);
826                 }, this);
827
828                 for (i = 0,ln = missingClasses.length; i < ln; i++) {
829                     missingPaths.push(this.classNameToFilePathMap[missingClasses[i]]);
830                 }
831
832                 Ext.Error.raise({
833                     sourceClass: "Ext.Loader",
834                     sourceMethod: "onFileLoaded",
835                     msg: "The following classes are not declared even if their files have been " +
836                             "loaded: '" + missingClasses.join("', '") + "'. Please check the source code of their " +
837                             "corresponding files for possible typos: '" + missingPaths.join("', '") + "'"
838                 });
839             }
840             //</debug>
841         },
842
843         /**
844          * @private
845          */
846         onFileLoadError: function(className, filePath, errorMessage, isSynchronous) {
847             this.numPendingFiles--;
848             this.hasFileLoadError = true;
849
850             //<debug error>
851             Ext.Error.raise({
852                 sourceClass: "Ext.Loader",
853                 classToLoad: className,
854                 loadPath: filePath,
855                 loadingType: isSynchronous ? 'synchronous' : 'async',
856                 msg: errorMessage
857             });
858             //</debug>
859         },
860
861         /**
862          * @private
863          */
864         addOptionalRequires: function(requires) {
865             var optionalRequires = this.optionalRequires,
866                 i, ln, require;
867
868             requires = Ext.Array.from(requires);
869
870             for (i = 0, ln = requires.length; i < ln; i++) {
871                 require = requires[i];
872
873                 Ext.Array.include(optionalRequires, require);
874             }
875
876             return this;
877         },
878
879         /**
880          * @private
881          */
882         triggerReady: function(force) {
883             var readyListeners = this.readyListeners,
884                 optionalRequires, listener;
885
886             if (this.isLoading || force) {
887                 this.isLoading = false;
888
889                 if (this.optionalRequires.length) {
890                     // Clone then empty the array to eliminate potential recursive loop issue
891                     optionalRequires = Ext.Array.clone(this.optionalRequires);
892
893                     // Empty the original array
894                     this.optionalRequires.length = 0;
895
896                     this.require(optionalRequires, Ext.Function.pass(this.triggerReady, [true], this), this);
897                     return this;
898                 }
899
900                 while (readyListeners.length) {
901                     listener = readyListeners.shift();
902                     listener.fn.call(listener.scope);
903
904                     if (this.isLoading) {
905                         return this;
906                     }
907                 }
908             }
909
910             return this;
911         },
912
913         /**
914          * Adds new listener to be executed when all required scripts are fully loaded.
915          *
916          * @param {Function} fn The function callback to be executed
917          * @param {Object} scope The execution scope (`this`) of the callback function
918          * @param {Boolean} withDomReady Whether or not to wait for document dom ready as well
919          */
920         onReady: function(fn, scope, withDomReady, options) {
921             var oldFn;
922
923             if (withDomReady !== false && Ext.onDocumentReady) {
924                 oldFn = fn;
925
926                 fn = function() {
927                     Ext.onDocumentReady(oldFn, scope, options);
928                 };
929             }
930
931             if (!this.isLoading) {
932                 fn.call(scope);
933             }
934             else {
935                 this.readyListeners.push({
936                     fn: fn,
937                     scope: scope
938                 });
939             }
940         },
941
942         /**
943          * @private
944          * @param {String} className
945          */
946         historyPush: function(className) {
947             if (className && this.isFileLoaded.hasOwnProperty(className)) {
948                 Ext.Array.include(this.history, className);
949             }
950
951             return this;
952         }
953     };
954
955     /**
956      * @member Ext
957      * @method require
958      * @alias Ext.Loader#require
959      */
960     Ext.require = alias(Loader, 'require');
961
962     /**
963      * @member Ext
964      * @method syncRequire
965      * @alias Ext.Loader#syncRequire
966      */
967     Ext.syncRequire = alias(Loader, 'syncRequire');
968
969     /**
970      * @member Ext
971      * @method exclude
972      * @alias Ext.Loader#exclude
973      */
974     Ext.exclude = alias(Loader, 'exclude');
975
976     /**
977      * @member Ext
978      * @method onReady
979      * @alias Ext.Loader#onReady
980      */
981     Ext.onReady = function(fn, scope, options) {
982         Loader.onReady(fn, scope, true, options);
983     };
984
985     /**
986      * @cfg {String[]} requires
987      * @member Ext.Class
988      * List of classes that have to be loaded before instantiating this class.
989      * For example:
990      *
991      *     Ext.define('Mother', {
992      *         requires: ['Child'],
993      *         giveBirth: function() {
994      *             // we can be sure that child class is available.
995      *             return new Child();
996      *         }
997      *     });
998      */
999     Class.registerPreprocessor('loader', function(cls, data, continueFn) {
1000         var me = this,
1001             dependencies = [],
1002             className = Manager.getName(cls),
1003             i, j, ln, subLn, value, propertyName, propertyValue;
1004
1005         /*
1006         Basically loop through the dependencyProperties, look for string class names and push
1007         them into a stack, regardless of whether the property's value is a string, array or object. For example:
1008         {
1009               extend: 'Ext.MyClass',
1010               requires: ['Ext.some.OtherClass'],
1011               mixins: {
1012                   observable: 'Ext.util.Observable';
1013               }
1014         }
1015         which will later be transformed into:
1016         {
1017               extend: Ext.MyClass,
1018               requires: [Ext.some.OtherClass],
1019               mixins: {
1020                   observable: Ext.util.Observable;
1021               }
1022         }
1023         */
1024
1025         for (i = 0, ln = dependencyProperties.length; i < ln; i++) {
1026             propertyName = dependencyProperties[i];
1027
1028             if (data.hasOwnProperty(propertyName)) {
1029                 propertyValue = data[propertyName];
1030
1031                 if (typeof propertyValue === 'string') {
1032                     dependencies.push(propertyValue);
1033                 }
1034                 else if (propertyValue instanceof Array) {
1035                     for (j = 0, subLn = propertyValue.length; j < subLn; j++) {
1036                         value = propertyValue[j];
1037
1038                         if (typeof value === 'string') {
1039                             dependencies.push(value);
1040                         }
1041                     }
1042                 }
1043                 else if (typeof propertyValue != 'function') {
1044                     for (j in propertyValue) {
1045                         if (propertyValue.hasOwnProperty(j)) {
1046                             value = propertyValue[j];
1047
1048                             if (typeof value === 'string') {
1049                                 dependencies.push(value);
1050                             }
1051                         }
1052                     }
1053                 }
1054             }
1055         }
1056
1057         if (dependencies.length === 0) {
1058 //            Loader.historyPush(className);
1059             return;
1060         }
1061
1062         //<debug error>
1063         var deadlockPath = [],
1064             requiresMap = Loader.requiresMap,
1065             detectDeadlock;
1066
1067         /*
1068         Automatically detect deadlocks before-hand,
1069         will throw an error with detailed path for ease of debugging. Examples of deadlock cases:
1070
1071         - A extends B, then B extends A
1072         - A requires B, B requires C, then C requires A
1073
1074         The detectDeadlock function will recursively transverse till the leaf, hence it can detect deadlocks
1075         no matter how deep the path is.
1076         */
1077
1078         if (className) {
1079             requiresMap[className] = dependencies;
1080
1081             detectDeadlock = function(cls) {
1082                 deadlockPath.push(cls);
1083
1084                 if (requiresMap[cls]) {
1085                     if (Ext.Array.contains(requiresMap[cls], className)) {
1086                         Ext.Error.raise({
1087                             sourceClass: "Ext.Loader",
1088                             msg: "Deadlock detected while loading dependencies! '" + className + "' and '" +
1089                                 deadlockPath[1] + "' " + "mutually require each other. Path: " +
1090                                 deadlockPath.join(' -> ') + " -> " + deadlockPath[0]
1091                         });
1092                     }
1093
1094                     for (i = 0, ln = requiresMap[cls].length; i < ln; i++) {
1095                         detectDeadlock(requiresMap[cls][i]);
1096                     }
1097                 }
1098             };
1099
1100             detectDeadlock(className);
1101         }
1102
1103         //</debug>
1104
1105         Loader.require(dependencies, function() {
1106             for (i = 0, ln = dependencyProperties.length; i < ln; i++) {
1107                 propertyName = dependencyProperties[i];
1108
1109                 if (data.hasOwnProperty(propertyName)) {
1110                     propertyValue = data[propertyName];
1111
1112                     if (typeof propertyValue === 'string') {
1113                         data[propertyName] = Manager.get(propertyValue);
1114                     }
1115                     else if (propertyValue instanceof Array) {
1116                         for (j = 0, subLn = propertyValue.length; j < subLn; j++) {
1117                             value = propertyValue[j];
1118
1119                             if (typeof value === 'string') {
1120                                 data[propertyName][j] = Manager.get(value);
1121                             }
1122                         }
1123                     }
1124                     else if (typeof propertyValue != 'function') {
1125                         for (var k in propertyValue) {
1126                             if (propertyValue.hasOwnProperty(k)) {
1127                                 value = propertyValue[k];
1128
1129                                 if (typeof value === 'string') {
1130                                     data[propertyName][k] = Manager.get(value);
1131                                 }
1132                             }
1133                         }
1134                     }
1135                 }
1136             }
1137
1138             continueFn.call(me, cls, data);
1139         });
1140
1141         return false;
1142     }, true);
1143
1144     Class.setDefaultPreprocessorPosition('loader', 'after', 'className');
1145
1146     /**
1147      * @cfg {String[]} uses
1148      * @member Ext.Class
1149      * List of classes to load together with this class.  These aren't neccessarily loaded before
1150      * this class is instantiated. For example:
1151      *
1152      *     Ext.define('Mother', {
1153      *         uses: ['Child'],
1154      *         giveBirth: function() {
1155      *             // This code might, or might not work:
1156      *             // return new Child();
1157      *
1158      *             // Instead use Ext.create() to load the class at the spot if not loaded already:
1159      *             return Ext.create('Child');
1160      *         }
1161      *     });
1162      */
1163     Manager.registerPostprocessor('uses', function(name, cls, data) {
1164         var uses = Ext.Array.from(data.uses),
1165             items = [],
1166             i, ln, item;
1167
1168         for (i = 0, ln = uses.length; i < ln; i++) {
1169             item = uses[i];
1170
1171             if (typeof item === 'string') {
1172                 items.push(item);
1173             }
1174         }
1175
1176         Loader.addOptionalRequires(items);
1177     });
1178
1179     Manager.setDefaultPostprocessorPosition('uses', 'last');
1180
1181 })(Ext.ClassManager, Ext.Class, Ext.Function.flexSetter, Ext.Function.alias);
1182