/**
* @author Jacky Nguyen
* @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.require([
'widget.window',
'widget.button',
'layout.fit'
]);
Ext.onReady(function() {
var window = Ext.widget('window', {
width: 500,
height: 300,
layout: 'fit',
items: {
xtype: 'button',
text: 'Hello World',
handler: function() { alert(this.text) }
}
});
window.show();
});
*
*
* @singleton
*/
(function(Manager, Class, flexSetter) {
var defaultClassPreprocessors = Class.getDefaultPreprocessors(),
Loader;
Ext.Loader = {
/**
* Flag indicating whether there are still files being loaded
* @private
*/
isLoading: false,
/**
* Maintain the queue for all dependencies. Each item in the array is an object of the format:
* {
* requires: [...], // The required classes for this queue item
* callback: function() { ... } // The function to execute when all classes specified in requires exist
* }
* @private
*/
queue: [],
/**
* Maintain the list of files that have already been handled so that they never get double-loaded
* @private
*/
isFileLoaded: {},
/**
* Maintain the list of listeners to execute when all required scripts are fully loaded
* @private
*/
readyListeners: [],
/**
* Contains all class names that are ever required via {@link Ext.Loader.require}
* @private
*/
requireHistory: {},
/**
* Contains optional dependencies to be loaded last
* @private
*/
optionalRequires: [],
/**
* Map of fully qualified class names to an array of dependent classes.
* @private
*/
requiresMap: {},
/**
* @private
*/
numPendingFiles: 0,
/**
* @private
*/
numLoadedFiles: 0,
/**
* @private
*/
classNameToFilePathMap: {},
/**
* 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 history
* @type Array
*/
history: [],
/**
* Configuration
* @private
*/
config: {
/**
* Whether or not to enable the dynamic dependency loading feature
* Defaults to false
* @cfg {Boolean} enabled
*/
enabled: false,
/**
* Whether or not to enable automatic deadlock detection, very useful
* during development
* Defaults to true
* @cfg {Boolean} enableDeadlockDetection
*/
enableDeadlockDetection: true,
/**
* Whether or not to enable automatic deadlock detection, very useful
* during development
* Defaults to true
* @cfg {Boolean} enableDeadlockDetection
*/
enableCacheBuster: true,
/**
* The mapping from namespaces to file paths
*
* {
* 'Ext': './src' // This is set by default, Ext.layout.Container will refer to ./src/layout/Container.js
* 'My': './src/my_own_folder' // My.layout.Container will refer to ./src/my_own_folder/layout/Container.js
* }
*
*
* If not being specified, for example, Other.awesome.Class
* will simply refer to ./Other/awesome/Class.js
* @cfg {Object} paths
*/
paths: {
'Ext': '.'
}
},
/**
* Set the configuration for the loader. This should be called right after ext-core.js
* (or ext-core-debug.js) is included in the page, 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 {@link Ext.Loader.config} for the list of possible properties
*
* @param {Object} config The config object to override the default values in {@link Ext.Loader.config}
* @return {Ext.Loader} this
*/
setConfig: function(name, value) {
if (Ext.isObject(name) && arguments.length === 1) {
Ext.merge(this.config, name);
}
else {
this.config[name] = (Ext.isObject(value)) ? Ext.merge(this.config[name], value) : value;
}
return this;
},
/**
* Get the config value corresponding to the specified name. If no name is given, will return the config object
* @param {String} name The config property name
* @return {Object/Mixed}
*/
getConfig: function(name) {
if (name) {
return this.config[name];
}
return this.config;
},
/**
* Sets the path of a namespace.
* For Example:
* Ext.Loader.setPath('Ext', '.');
* Indicates that any classes with the top level object "Ext" will be found
* at the root basePath.
* @param {String/Object} name See {@link Ext.Function.flexSetter flexSetter}
* @param {String} path See {@link Ext.Function.flexSetter flexSetter}
*/
setPath: flexSetter(function(name, path) {
this.config.paths[name] = path;
return this;
}),
/**
* Translates a className to a path to load the file from by prefixing
* the proper prefix and converting the .'s to /'s.
*
* For example:
* ("Ext.layout.Layout" => "./src/Ext/layout/Layout.js")
*
* @param {String} className
* @return {String} path
*/
getPath: function(className) {
var path = '',
paths = this.config.paths,
prefix, deepestPrefix = '';
if (paths.hasOwnProperty(className)) {
return paths[className];
}
for (prefix in paths) {
if (paths.hasOwnProperty(prefix) && prefix === className.substring(0, prefix.length)) {
if (prefix.length > deepestPrefix.length) {
deepestPrefix = prefix;
}
}
}
path += paths[deepestPrefix];
className = className.substring(deepestPrefix.length + 1);
path = path + "/" + className.replace(/\./g, "/") + '.js';
path = path.replace(/\/\.\//g, '/');
return path;
},
/**
* 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 manager = Manager,
ln = this.queue.length,
i, item, j, requires;
if (ln === 0) {
this.triggerReady();
return;
}
for (i = 0; i < ln; i++) {
item = this.queue[i];
if (item) {
requires = item.requires;
// Don't bother checking when the number of files loaded
// is still less than the array length
if (requires.length > this.numLoadedFiles) {
continue;
}
j = 0;
do {
if (manager.exist(requires[j])) {
requires.splice(j, 1);
}
else {
j++;
}
} while (j < requires.length);
if (item.requires.length === 0) {
this.queue.splice(i, 1);
item.callback.call(item.scope);
this.refreshQueue();
break;
}
}
}
},
/**
* @private
*/
injectScriptElement: function(url, onLoad, onError, scope) {
var script = document.createElement('script'),
head = document.head || document.getElementsByTagName('head')[0],
isLoaded = false,
onLoadFn = function() {
if (!isLoaded) {
isLoaded = true;
onLoad.call(scope);
}
};
Ext.apply(script, {
type: 'text/javascript',
src: url,
onload: onLoadFn,
onerror: onError,
onreadystatechange: function() {
if (this.readyState === 'loaded' || this.readyState === 'complete') {
onLoadFn();
}
}
});
head.appendChild(script);
return script;
},
/**
* Load a script file, supports both asynchronous and synchronous approaches
* @param {String} url
* @param {Function} onLoad
* @param {Scope} scope
* @param {Boolean} synchronous
* @private
*/
loadScriptFile: function(url, onLoad, scope, synchronous) {
var me = this,
noCacheUrl = url + (this.getConfig('enableCacheBuster') ? '?' + Ext.Date.now() : ''),
fileName = url.split('/').pop(),
xhr, status, onScriptError;
scope = scope || this;
this.isLoading = true;
if (!synchronous) {
onScriptError = function() {
me.onFileLoadError.call(me, {
message: "Failed loading '" + url + "', please verify that it exists",
url: url,
synchronous: false
});
};
if (!Ext.isReady) {
Ext.onDocumentReady(function() {
me.injectScriptElement(noCacheUrl, onLoad, onScriptError, scope);
});
}
else {
this.injectScriptElement(noCacheUrl, onLoad, onScriptError, scope);
}
}
else {
if (typeof XMLHttpRequest !== 'undefined') {
xhr = new XMLHttpRequest();
} else {
xhr = new ActiveXObject('Microsoft.XMLHTTP');
}
xhr.open('GET', noCacheUrl, false);
xhr.send(null);
status = (xhr.status === 1223) ? 204 : xhr.status;
if (status >= 200 && status < 300) {
// Firebug friendly, file names are still shown even though they're eval'ed code
new Function(xhr.responseText + "\n//@ sourceURL=" + fileName)();
onLoad.call(scope);
}
else {
this.onFileLoadError.call(this, {
message: "Failed loading synchronously via XHR: '" + url + "'; please " +
"verify that the file exists. " +
"XHR status code: " + status,
url: url,
synchronous: true
});
}
}
},
/**
* @private
*/
onFileLoadError: function(error) {
//
throw new Error("[Ext.Loader] " + error.message);
//
},
/**
* Explicitly exclude
*/
exclude: function(excludes) {
var me = this;
return {
require: function(expressions, fn, scope) {
return me.require(expressions, fn, scope, excludes);
}
};
},
/**
* 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.require} for convenience
* @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) Stuff to be excluded, useful when being used with expressions
* @private
*/
require: function(expressions, fn, scope, excludes) {
var filePath, expression, exclude, className, excluded = {},
excludedClassNames = [],
possibleClassNames = [],
possibleClassName, classNames = [],
me = this,
i, j, ln, subLn, onFileLoaded;
expressions = Ext.Array.from(expressions);
excludes = Ext.Array.from(excludes);
fn = fn || Ext.emptyFn;
scope = scope || Ext.global;
for (i = 0, ln = excludes.length; i < ln; i++) {
exclude = excludes[i];
if (Ext.isString(exclude) && exclude.length > 0) {
excludedClassNames = Manager.getNamesByExpression(exclude);
for (j = 0, subLn = excludedClassNames.length; j < subLn; j++) {
excluded[excludedClassNames[j]] = true;
}
}
}
for (i = 0, ln = expressions.length; i < ln; i++) {
expression = expressions[i];
if (Ext.isString(expression) && expression.length > 0) {
possibleClassNames = Manager.getNamesByExpression(expression);
for (j = 0, subLn = possibleClassNames.length; j < subLn; j++) {
possibleClassName = possibleClassNames[j];
if (!excluded.hasOwnProperty(possibleClassName) && !Manager.exist(possibleClassName)) {
Ext.Array.include(classNames, possibleClassName);
}
}
}
}
//
// If the dynamic dependency feature is not being used, throw an error
// if the dependencies are not defined
if (!this.config.enabled) {
if (classNames.length > 0) {
throw new Error("[Ext.Loader][not enabled] Missing required class" + ((classNames.length > 1) ? "es" : "") +
": " + classNames.join(', '));
}
}
//
if (classNames.length === 0) {
fn.call(scope);
return this;
}
this.queue.push({
requires: classNames,
callback: fn,
scope: scope
});
classNames = classNames.slice();
for (i = 0, ln = classNames.length; i < ln; i++) {
className = classNames[i];
if (!(this.isFileLoaded.hasOwnProperty(className) && this.isFileLoaded[className] === true)) {
this.requireHistory[className] = true;
this.isFileLoaded[className] = true;
filePath = this.getPath(className);
this.classNameToFilePathMap[className] = filePath;
this.numPendingFiles++;
if (this.numLoadedFiles === 0) {
this.startLoadingTime = Ext.Date.now();
}
this.loadScriptFile(filePath, Ext.Function.pass(this.onFileLoaded, [className, filePath], this), this, this.syncModeEnabled);
}
}
return this;
},
onFileLoaded: function(className, filePath) {
this.numLoadedFiles++;
//
window.status = "Loaded: " + className + " (" + this.numLoadedFiles + " total)";
//
this.numPendingFiles--;
if (this.numPendingFiles === 0) {
this.refreshQueue();
}
//
if (this.numPendingFiles === 0 && this.isLoading) {
var queue = this.queue,
requires,
i, ln, j, subLn, missingClasses = [], missingPaths = [];
for (i = 0, ln = queue.length; i < ln; i++) {
requires = queue[i].requires;
for (j = 0, subLn = requires.length; j < ln; j++) {
if (this.isFileLoaded[requires[j]]) {
missingClasses.push(requires[j]);
}
}
}
if (missingClasses.length < 1) {
return;
}
for (i = 0, ln = missingClasses.length; i < ln; i++) {
missingPaths.push(this.classNameToFilePathMap[missingClasses[i]]);
}
throw new Error("[Ext.Loader] 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(', '));
}
//
},
/**
* @private
*/
addOptionalRequires: function(requires) {
var optionalRequires = this.optionalRequires,
i, ln, require;
requires = Ext.Array.from(requires);
for (i = 0, ln = requires.length; i < ln; i++) {
require = requires[i];
Ext.Array.include(optionalRequires, require);
}
return this;
},
/**
* @private
*/
triggerReady: function(force) {
var readyListeners = this.readyListeners,
optionalRequires, listener;
if (this.isLoading || force) {
this.isLoading = false;
if (this.optionalRequires.length) {
// Clone then empty the array to eliminate potential recursive loop issue
optionalRequires = Ext.Array.clone(this.optionalRequires);
// Empty the original array
this.optionalRequires.length = 0;
this.require(optionalRequires, Ext.Function.pass(this.triggerReady, [true], this), this);
return this;
}
//
window.status = "All dependencies are loaded. (" + this.numLoadedFiles + " files in " +
((Ext.Date.now() - this.startLoadingTime) / 1000)+"s | using " +
Math.round(((this.numLoadedFiles / Ext.Object.getSize(Manager.maps.nameToAliases)) * 100)) + "% of the whole library)";
//
while (readyListeners.length) {
listener = readyListeners.shift();
listener.fn.call(listener.scope);
}
}
return this;
},
/**
* 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 (this
) of the callback function
* @param {Boolean} withDomReady Whether or not to wait for document dom ready as well
*/
onReady: function(fn, scope, withDomReady, options) {
var me = this,
oldFn;
if (withDomReady !== false && Ext.onDocumentReady) {
oldFn = fn;
fn = function() {
Ext.onDocumentReady(oldFn, scope, options);
};
}
if (!this.isLoading) {
fn.call(scope);
}
else {
this.readyListeners.push({
fn: fn,
scope: scope
});
}
},
historyPush: function(className) {
if (className && this.requireHistory.hasOwnProperty(className)) {
Ext.Array.include(this.history, className);
}
},
/**
* @private
*/
enableSyncMode: function(isEnabled) {
this.syncModeEnabled = isEnabled;
}
};
Loader = Ext.Loader;
/**
* Convenient shortcut to {@link Ext.Loader#require}
* @member Ext
* @method require
*/
Ext.require = Ext.Function.alias(Loader, 'require');
/**
* Convenient shortcut to {@link Ext.Loader#exclude}
* @member Ext
* @method exclude
*/
Ext.exclude = Ext.Function.alias(Loader, 'exclude');
Class.registerPreprocessor('loader', function(cls, data, fn) {
var me = this,
dependencyProperties = ['extend', 'mixins', 'requires'],
dependencies = [],
className = Manager.getName(cls),
requiresMap = Loader.requiresMap,
i, j, ln, subLn, value, propertyName, propertyValue, deadlockPath = [], detectDeadlock;
// Basically loop through the dependencyProperties, look for string class names and push
// them into a stack, regardless of whether the property's value is a string, array or object. For example:
// {
// extend: 'Ext.MyClass',
// requires: ['Ext.some.OtherClass'],
// mixins: {
// observable: 'Ext.util.Observable';
// }
// }
// which will later be transformed into:
// {
// extend: Ext.MyClass,
// requires: [Ext.some.OtherClass],
// mixins: {
// observable: Ext.util.Observable;
// }
// }
for (i = 0, ln = dependencyProperties.length; i < ln; i++) {
propertyName = dependencyProperties[i];
if (data.hasOwnProperty(propertyName)) {
propertyValue = data[propertyName];
if (Ext.isString(propertyValue)) {
dependencies.push(propertyValue);
}
else if (Ext.isArray(propertyValue)) {
for (j = 0, subLn = propertyValue.length; j < subLn; j++) {
value = propertyValue[j];
if (Ext.isString(value)) {
dependencies.push(value);
}
}
}
else {
for (j in propertyValue) {
if (propertyValue.hasOwnProperty(j)) {
value = propertyValue[j];
if (Ext.isString(value)) {
dependencies.push(value);
}
}
}
}
}
}
//
/**
* 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 && Loader.getConfig('enableDeadlockDetection')) {
requiresMap[className] = dependencies;
detectDeadlock = function(cls) {
deadlockPath.push(cls);
if (requiresMap[cls]) {
if (Ext.Array.contains(requiresMap[cls], className)) {
throw new Error("[Ext.Loader] Deadlock detected! '" + className + "' and '" + deadlockPath[1] + "' " +
"mutually require each others. Path: " + deadlockPath.join(' -> ') +
" -> " + deadlockPath[0]);
}
for (i = 0, ln = requiresMap[cls].length; i < ln; i++) {
detectDeadlock(requiresMap[cls][i]);
}
}
};
detectDeadlock(className);
}
//
Ext.require(dependencies, function() {
Loader.historyPush(className);
for (i = 0, ln = dependencyProperties.length; i < ln; i++) {
propertyName = dependencyProperties[i];
if (data.hasOwnProperty(propertyName)) {
propertyValue = data[propertyName];
if (Ext.isString(propertyValue)) {
data[propertyName] = Manager.get(propertyValue);
}
else if (Ext.isArray(propertyValue)) {
for (j = 0, subLn = propertyValue.length; j < subLn; j++) {
value = propertyValue[j];
if (Ext.isString(value)) {
data[propertyName][j] = Manager.get(value);
}
}
}
else {
for (var k in propertyValue) {
if (propertyValue.hasOwnProperty(k)) {
value = propertyValue[k];
if (Ext.isString(value)) {
data[propertyName][k] = Manager.get(value);
}
}
}
}
}
}
if (fn) {
fn.call(me, cls, data);
}
});
}).insertDefaultPreprocessor('loader', 'after', 'className');
Manager.registerPostprocessor('uses', function(name, cls, data, fn) {
if (data.uses) {
var uses = Ext.Array.from(data.uses);
uses = Ext.Array.filter(uses, function(use) {
return Ext.isString(use);
});
Loader.addOptionalRequires(uses);
}
if (fn) {
fn.call(this, name, cls, data);
}
}).insertDefaultPostprocessor('uses', 'last');
})(Ext.ClassManager, Ext.Class, Ext.Function.flexSetter);