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