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