// steal is a resource loader for JavaScript. It is broken into the following parts:
//
// - Helpers - basic utility methods used internally
// - AOP - aspect oriented code helpers
// - Deferred - a minimal deferred implementation
// - Uri - methods for dealing with urls
// - Api - steal's API
// - Module - an object that represents a resource that is loaded and run and has dependencies.
// - Type - a type systems used to load and run different types of resources
// - Packages - used to define packages
// - Extensions - makes steal pre-load a type based on an extension (ex: .coffee)
// - Mapping - configures steal to load resources in a different location
// - Startup - startup code
// - jQuery - code to make jQuery's readWait work
// - Error Handling - detect scripts failing to load
// - Has option - used to specify that one resources contains multiple other resources
// - Window Load - API for knowing when the window has loaded and all scripts have loaded
// - Interactive - Code for IE
// - Options -
(function (undefined) {
var requestFactory = function () {
return h.win.ActiveXObject ? new ActiveXObject("Microsoft.XMLHTTP") : new XMLHttpRequest();
};
// ## Helpers ##
// The following are a list of helper methods used internally to steal
var h = {
// check that we have a document,
win: (function () {
return this
}).call(null),
// a jQuery-like $.each
each: function (o, cb) {
var i, len;
// weak array detection, but we only use this internally so don't
// pass it weird stuff
if (typeof o.length == 'number') {
for (i = 0, len = o.length; i < len; i++) {
cb.call(o[i], i, o[i], o)
}
} else {
for (i in o) {
if (o.hasOwnProperty(i)) {
cb.call(o[i], i, o[i], o)
}
}
}
return o;
},
// adds the item to the array only if it doesn't currently exist
uniquePush: function (arr, item) {
if (h.inArray(arr, item) === -1) {
return arr.push(item)
}
},
// if o is a string
isString: function (o) {
return typeof o == "string";
},
// if o is a function
isFn: function (o) {
return typeof o == "function";
},
// dummy function
noop: function () {},
endsInSlashRegex: /\/$/,
// creates an element
createElement: function (nodeName) {
return h.doc.createElement(nodeName)
},
// creates a script tag
scriptTag: function () {
var start = h.createElement("script");
start.type = "text/javascript";
return start;
},
// minify-able verstion of getElementsByTagName
getElementsByTagName: function (tag) {
return h.doc.getElementsByTagName(tag);
},
// A function that returns the head element
// creates and caches the lookup for faster
// performance.
head: function () {
var hd = h.getElementsByTagName("head")[0];
if (!hd) {
hd = h.createElement("head");
h.docEl.insertBefore(hd, h.docEl.firstChild);
}
// replace head so it runs fast next time.
h.head = function () {
return hd;
}
return hd;
},
// extends one object with another
extend: function (d, s) {
// only extend if we have something to extend
s && h.each(s, function (k) {
if (s.hasOwnProperty(k)) {
d[k] = s[k];
}
});
return d;
},
// makes an array of things, or a mapping of things
map: function (args, cb) {
var arr = [];
h.each(args, function (i, str) {
arr.push(cb ? (h.isString(cb) ? str[cb] : cb.call(str, str)) : str)
});
return arr;
},
// ## AOP ##
// Aspect oriented programming helper methods are used to
// weave in functionality into steal's API.
// calls `before` before `f` is called.
// steal.complete = before(steal.complete, f)
// `changeArgs=true` makes before return the same args
before: function (f, before, changeArgs) {
return changeArgs ?
function before_changeArgs() {
return f.apply(this, before.apply(this, arguments));
} : function before_args() {
before.apply(this, arguments);
return f.apply(this, arguments);
}
},
// returns a function that calls `after`
// after `f`
after: function (f, after, changeRet) {
return changeRet ?
function after_CRet() {
return after.apply(this, [f.apply(this, arguments)].concat(h.map(arguments)));
} : function after_Ret() {
var ret = f.apply(this, arguments);
after.apply(this, arguments);
return ret;
}
},
/**
* Performs an XHR request
* @param {Object} options
* @param {Function} success
* @param {Function} error
*/
request: function (options, success, error) {
var request = new requestFactory(),
contentType = (options.contentType || "application/x-www-form-urlencoded; charset=utf-8"),
clean = function () {
request = check = clean = null;
},
check = function () {
var status;
if (request && request.readyState === 4) {
status = request.status;
if (status === 500 || status === 404 || status === 2 || request.status < 0 || (!status && request.responseText === "")) {
error && error(request.status);
} else {
success(request.responseText);
}
clean();
}
};
request.open("GET", options.src + '', !(options.async === false));
request.setRequestHeader("Content-type", contentType);
if (request.overrideMimeType) {
request.overrideMimeType(contentType);
}
request.onreadystatechange = check;
try {
request.send(null);
}
catch (e) {
if (clean) {
console.error(e);
error && error();
clean();
}
}
},
matchesId: function (loc, id) {
if (loc === "*") {
return true;
} else if (id.indexOf(loc) === 0) {
return true;
}
},
// are we in production
stealCheck: /steal\.(production\.)?js.*/,
// get script that loaded steal
getStealScriptSrc: function () {
if (!h.doc) {
return;
}
var scripts = h.getElementsByTagName("script"),
script;
// find the steal script and setup initial paths.
h.each(scripts, function (i, s) {
if (h.stealCheck.test(s.src)) {
script = s;
}
});
return script;
},
inArray: function (arr, val) {
for (var i = 0; i < arr.length; i++) {
if (arr[i] === val) {
return i;
}
}
return -1;
},
addEvent: function (elem, type, fn) {
if (elem.addEventListener) {
elem.addEventListener(type, fn, false);
} else if (elem.attachEvent) {
elem.attachEvent("on" + type, fn);
} else {
fn();
}
},
useIEShim: false
}
h.doc = h.win.document;
h.docEl = h.doc && h.doc.documentElement;
h.support = {
// does onerror work in script tags?
error: h.doc && (function () {
var script = h.scriptTag();
script.onerror = h.noop;
return h.isFn(script.onerror) || "onerror" in script;
})(),
// If scripts support interactive ready state.
// This is tested later.
interactive: false,
// use attachEvent for event listening (IE)
attachEvent: h.doc && h.scriptTag().attachEvent
}
// steal's deferred library. It is used through steal
// to support jQuery like API for file loading.
var Deferred = function (func) {
if (!(this instanceof Deferred)) return new Deferred();
// arrays for `done` and `fail` callbacks
this.doneFuncs = [];
this.failFuncs = [];
this.resultArgs = null;
this.status = "";
// check for option function: call it with this as context and as first
// parameter, as specified in jQuery api
func && func.call(this, this);
}
Deferred.when = function () {
var args = h.map(arguments);
if (args.length < 2) {
var obj = args[0];
if (obj && (h.isFn(obj.isResolved) && h.isFn(obj.isRejected))) {
return obj;
} else {
return Deferred().resolve(obj);
}
} else {
var df = Deferred(),
done = 0,
// resolve params: params of each resolve, we need to track down
// them to be able to pass them in the correct order if the master
// needs to be resolved
rp = [];
h.each(args, function (j, arg) {
arg.done(function () {
rp[j] = (arguments.length < 2) ? arguments[0] : arguments;
if (++done == args.length) {
df.resolve.apply(df, rp);
}
}).fail(function () {
df.reject(arguments);
});
});
return df;
}
}
// call resolve functions
var resolveFunc = function (type, status) {
return function (context) {
var args = this.resultArgs = (arguments.length > 1) ? arguments[1] : [];
return this.exec(context, this[type], args, status);
}
},
doneFunc = function (type, status) {
return function () {
var self = this;
h.each(arguments, function (i, v, args) {
if (!v) return;
if (v.constructor === Array) {
args.callee.apply(self, v)
} else {
// immediately call the function if the deferred has been resolved
if (self.status === status) v.apply(this, self.resultArgs || []);
self[type].push(v);
}
});
return this;
}
};
h.extend(Deferred.prototype, {
resolveWith: resolveFunc("doneFuncs", "rs"),
rejectWith: resolveFunc("failFuncs", "rj"),
done: doneFunc("doneFuncs", "rs"),
fail: doneFunc("failFuncs", "rj"),
always: function () {
var args = h.map(arguments);
if (args.length && args[0]) this.done(args[0]).fail(args[0]);
return this;
},
then: function () {
var args = h.map(arguments);
// fail function(s)
if (args.length > 1 && args[1]) this.fail(args[1]);
// done function(s)
if (args.length && args[0]) this.done(args[0]);
return this;
},
isResolved: function () {
return this.status === "rs";
},
isRejected: function () {
return this.status === "rj";
},
reject: function () {
return this.rejectWith(this, arguments);
},
resolve: function () {
return this.resolveWith(this, arguments);
},
exec: function (context, dst, args, st) {
if (this.status !== "") return this;
this.status = st;
h.each(dst, function (i, d) {
d.apply(context, args);
});
return this;
}
});
// ## HELPER METHODS FOR DEFERREDS
// Used to call a method on an object or resolve a
// deferred on it when a group of deferreds is resolved.
//
// whenEach(resources,"complete",resource,"execute")
var whenEach = function (arr, func, obj, func2) {
var deferreds = h.map(arr, func)
return Deferred.when.apply(Deferred, deferreds).then(function () {
if (h.isFn(obj[func2])) {
obj[func2]()
} else {
obj[func2].resolve();
}
});
};
// ## URI ##
/**
* @class steal.URI
* A URL / URI helper for getting information from a URL.
*
* var uri = URI( "http://stealjs.com/index.html" )
* uri.path //-> "/index.html"
*/
var URI = function (url) {
if (this.constructor !== URI) {
return new URI(url);
}
h.extend(this, URI.parse("" + url));
};
// the current url (relative to root, which is relative from page)
// normalize joins from this
//
h.extend(URI, {
// parses a URI into it's basic parts
parse: function (string) {
var uriParts = string.split("?"),
uri = uriParts.shift(),
queryParts = uriParts.join("").split("#"),
protoParts = uri.split("://"),
parts = {
query: queryParts.shift(),
fragment: queryParts.join("#")
},
pathParts;
if (protoParts[1]) {
parts.protocol = protoParts.shift();
pathParts = protoParts[0].split("/");
parts.host = pathParts.shift();
parts.path = "/" + pathParts.join("/");
} else {
parts.path = protoParts[0];
}
return parts;
}
});
/**
* @attribute page
* The location of the page as a URI.
*
* st.URI.page.protocol //-> "http"
*/
URI.page = URI(h.win.location && location.href);
/**
* @attribute cur
*
* The current working directory / path. Anything
* loaded relative will be loaded relative to this.
*/
URI.cur = URI();
/**
* @prototype
*/
h.extend(URI.prototype, {
dir: function () {
var parts = this.path.split("/");
parts.pop();
return URI(this.domain() + parts.join("/"))
},
filename: function () {
return this.path.split("/").pop();
},
ext: function () {
var filename = this.filename();
return (filename.indexOf(".") > -1) ? filename.split(".").pop() : "";
},
domain: function () {
return this.protocol ? this.protocol + "://" + this.host : "";
},
isCrossDomain: function (uri) {
uri = URI(uri || h.win.location.href);
var domain = this.domain(),
uriDomain = uri.domain()
return (domain && uriDomain && domain != uriDomain) || this.protocol === "file" || (domain && !uriDomain);
},
isRelativeToDomain: function () {
return !this.path.indexOf("/");
},
hash: function () {
return this.fragment ? "#" + this.fragment : ""
},
search: function () {
return this.query ? "?" + this.query : ""
},
// like join, but returns a string
add: function (uri) {
return this.join(uri) + '';
},
join: function (uri, min) {
uri = URI(uri);
if (uri.isCrossDomain(this)) {
return uri;
}
if (uri.isRelativeToDomain()) {
return URI(this.domain() + uri)
}
// at this point we either
// - have the same domain
// - this has a domain but uri does not
// - both don't have domains
var left = this.path ? this.path.split("/") : [],
right = uri.path.split("/"),
part = right[0];
//if we are joining from a folder like cookbook/, remove the last empty part
if (this.path.match(/\/$/)) {
left.pop();
}
while (part == ".." && left.length) {
// if we've emptied out, folders, just break
// leaving any additional ../s
if (!left.pop()) {
break;
}
right.shift();
part = right[0];
}
return h.extend(URI(this.domain() + left.concat(right).join("/")), {
query: uri.query
});
},
/**
* For a given path, a given working directory, and file location, update the
* path so it points to a location relative to steal's root.
*
* We want everything relative to steal's root so the same app can work in
* multiple pages.
*
* ./files/a.js = steals a.js
* ./files/a = a/a.js
* files/a = //files/a/a.js
* files/a.js = loads //files/a.js
*/
normalize: function (cur) {
cur = cur ? cur.dir() : URI.cur.dir();
var path = this.path,
res = URI(path);
//if path is rooted from steal's root (DEPRECATED)
if (!path.indexOf("//")) {
res = URI(path.substr(2));
} else if (!path.indexOf("./")) { // should be relative
res = cur.join(path.substr(2));
}
// only if we start with ./ or have a /foo should we join from cur
else if (this.isRelative()) {
res = cur.join(this.domain() + path)
}
res.query = this.query;
return res;
},
isRelative: function () {
return /^[\.|\/]/.test(this.path)
},
// a min path from 2 urls that share the same domain
pathTo: function (uri) {
uri = URI(uri);
var uriParts = uri.path.split("/"),
thisParts = this.path.split("/"),
result = [];
while (uriParts.length && thisParts.length && uriParts[0] == thisParts[0]) {
uriParts.shift();
thisParts.shift();
}
h.each(thisParts, function () {
result.push("../")
})
return URI(result.join("") + uriParts.join("/"));
},
mapJoin: function (url) {
return this.join(URI(url).insertMapping());
},
// helper to go from jquery to jquery/jquery.js
addJS: function () {
var ext = this.ext();
if (!ext) {
// if first character of path is a . or /, just load this file
if (!this.isRelative()) {
this.path += "/" + this.filename();
}
this.path += ".js"
}
return this;
}
});
// This can't be added to the prototype using extend because
// then for some reason IE < 9 won't recognize it.
URI.prototype.toString = function () {
return this.domain() + this.path + this.search() + this.hash();
};
// =============================== MAPPING ===============================
URI.prototype.insertMapping = function () {
// go through mappings
var orig = "" + this,
key, value;
for (key in steal.mappings) {
value = steal.mappings[key]
if (value.test.test(orig)) {
return orig.replace(key, value.path);
}
}
return URI(orig);
};
// --- END URI
/**
* `new ConfigManager(config)` creates configuration profile for the steal context.
* It keeps all config parameters in the instance which allows steal to clone it's
* context.
*
* config.stealConfig is tipically set up in __stealconfig.js__. The available options are:
*
* - map - map an id to another id
* - paths - maps an id to a file
* - root - the path to the "root" folder
* - env - `"development"` or `"production"`
* - types - processor rules for various types
* - ext - behavior rules for extensions
* - urlArgs - extra queryString arguments
* - startFile - the file to load
*
* ## map
*
* Maps an id to another id with a certain scope of other ids. This can be
* used to use different modules within the same id or map ids to another id.
* Example:
*
* st.config({
* map: {
* "*": {
* "jquery/jquery.js": "jquery"
* },
* "compontent1":{
* "underscore" : "underscore1.2"
* },
* "component2":{
* "underscore" : "underscore1.1"
* }
* }
* })
*
* ## paths
*
* Maps an id or matching ids to a url. Each mapping is specified
* by an id or part of the id to match and what that
* part should be replaced with.
*
* st.config({
* paths: {
* // maps everything in a jquery folder like: `jquery/controller`
* // to http://cdn.com/jquery/controller/controller.com
* "jquery/" : "http://cdn.com/jquery/"
*
* // if path does not end with /, it matches only that id
* "jquery" : "https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"
* }
* })
*
* ## root
* ## env
*
* If production, does not load "ignored" scripts and loads production script. If development gives more warnings / errors.
*
* ## types
*
* The types option can specify how a type is loaded.
*
* ## ext
*
* The ext option specifies the default behavior if file is loaded with the
* specified extension. For a given extension, a file that configures the type can be given or
* an existing type. For example, for ejs:
*
* st.config({ext: {"ejs": "can/view/ejs/ejs.js"}})
*
* This tells steal to make sure `can/view/ejs/ejs.js` is executed before any file with
* ".ejs" is executed.
*
*
**/
var ConfigManager = function (options) {
this.stealConfig = {};
this.callbacks = [];
this.attr(ConfigManager.defaults);
this.attr(options)
}
h.extend(ConfigManager.prototype, {
// get or set config.stealConfig attributes
attr: function (config) {
if (!config) { // called as a getter, so just return
return this.stealConfig;
}
if (arguments.length === 1 && typeof config === "string") { // called as a getter, so just return
return this.stealConfig && this.stealConfig[config];
}
this.stealConfig = this.stealConfig || {};
for (var prop in config) {
var value = config[prop];
// if it's a special function
this[prop] ?
// run it
this[prop](value) :
// otherwise set or extend
(typeof value == "object" && this.stealConfig[prop] ?
// extend
h.extend(this.stealConfig[prop], value) :
// set
this.stealConfig[prop] = value);
}
for (var i = 0; i < this.callbacks.length; i++) {
this.callbacks[i](this.stealConfig)
}
return this;
},
// add callbacks which are called after config is changed
on: function (cb) {
this.callbacks.push(cb)
},
// get the current start file
startFile: function (startFile) {
// make sure startFile and production look right
this.stealConfig.startFile = "" + URI(startFile).addJS()
if (!this.stealConfig.production) {
this.stealConfig.production = URI(this.stealConfig.startFile).dir() + "/production.js";
}
},
/**
*
* Read or define the path relative URI's should be referenced from.
*
* window.location //-> "http://foo.com/site/index.html"
* st.URI.root("http://foo.com/app/files/")
* st.root.toString() //-> "../../app/files/"
*/
root: function (relativeURI) {
if (relativeURI !== undefined) {
var root = URI(relativeURI);
// the current folder-location of the page http://foo.com/bar/card
var cleaned = URI.page,
// the absolute location or root
loc = cleaned.join(relativeURI);
// cur now points to the 'root' location, but from the page
URI.cur = loc.pathTo(cleaned)
this.stealConfig.root = root;
return this;
}
this.stealConfig.root = root || URI("");
},
//var stealConfig = configs[configContext];
cloneContext: function () {
return new ConfigManager(h.extend({}, this.stealConfig));
}
})
// ConfigManager's defaults
ConfigManager.defaults = {
types: {},
ext: {},
env: "development",
loadProduction: true,
logLevel: 0,
root: "",
amd: false
};
// ### TYPES ##
/**
* Registers a type. You define the type of the file, the basic type it
* converts to, and a conversion function where you convert the original file
* to JS or CSS. This is modeled after the
* [http://api.jquery.com/extending-ajax/#Converters AJAX converters] in jQuery.
*
* Types are designed to make it simple to switch between steal's development
* and production modes. In development mode, the types are converted
* in the browser to allow devs to see changes as they work. When the app is
* built, these converter functions are run by the build process,
* and the processed text is inserted into the production script, optimized for
* performance.
*
* Here's an example converting files of type .foo to JavaScript. Foo is a
* fake language that saves global variables defined like. A .foo file might
* look like this:
*
* REQUIRED FOO
*
* To define this type, you'd call steal.type like this:
*
* steal.type("foo js", function(options, original, success, error){
* var parts = options.text.split(" ")
* options.text = parts[0]+"='"+parts[1]+"'";
* success();
* });
*
* The method we provide is called with the text of .foo files in options.text.
* We parse the file, create JavaScript and put it in options.text. Couldn't
* be simpler.
*
* Here's an example,
* converting [http://jashkenas.github.com/coffee-script/ coffeescript]
* to JavaScript:
*
* steal.type("coffee js", function(options, original, success, error){
* options.text = CoffeeScript.compile(options.text);
* success();
* });
*
* In this example, any time steal encounters a file with extension .coffee,
* it will call the given converter method. CoffeeScript.compile takes the
* text of the file, converts it from coffeescript to javascript, and saves
* the JavaScript text in options.text.
*
* Similarly, languages on top of CSS, like [http://lesscss.org/ LESS], can
* be converted to CSS:
*
* steal.type("less css", function(options, original, success, error){
* new (less.Parser)({
* optimization: less.optimization,
* paths: []
* }).parse(options.text, function (e, root) {
* options.text = root.toCSS();
* success();
* });
* });
*
* This simple type system could be used to convert any file type to be used
* in your JavaScript app. For example, [http://fdik.org/yml/ yml] could be
* used for configuration. jQueryMX uses steal.type to support JS templates,
* such as EJS, TMPL, and others.
*
* @param {String} type A string that defines the new type being defined and
* the type being converted to, separated by a space, like "coffee js".
*
* There can be more than two steps used in conversion, such as "ejs view js".
* This will define a method that converts .ejs files to .view files. There
* should be another converter for "view js" that makes this final conversion
* to JS.
*
* @param {Function} cb( options, original, success, error ) a callback
* function that converts the new file type to a basic type. This method
* needs to do two things: 1) save the text of the converted file in
* options.text and 2) call success() when the conversion is done (it can work
* asynchronously).
*
* - __options__ - the steal options for this file, including path information
* - __original__ - the original argument passed to steal, which might be a
* path or a function
* - __success__ - a method to call when the file is converted and processed
* successfully
* - __error__ - a method called if the conversion fails or the file doesn't
* exist
*/
ConfigManager.prototype.types = function (types) {
var configTypes = this.stealConfig.types || (this.stealConfig.types = {});
h.each(types, function (type, cb) {
var typs = type.split(" ");
configTypes[typs.shift()] = {
require: cb,
convert: typs
};
});
};
ConfigManager.prototype.require = function (options, success, error) {
// add the src option
// but it is not added to functions
if (options.idToUri) {
var old = options.src;
options.src = this.addSuffix(options.idToUri(options.id));
}
// get the type
var type = this.attr().types[options.type],
converters;
// if this has converters, make it get the text first, then pass it to the type
if (type.convert.length) {
converters = type.convert.slice(0);
converters.unshift("text", options.type)
} else {
converters = [options.type]
}
require(options, converters, success, error, this)
}
ConfigManager.prototype.addSuffix = function (str) {
var suffix = this.attr('suffix')
if (suffix) {
str = (str + '').indexOf('?') > -1 ? str + "&" + suffix : str + "?" + suffix;
}
return str;
}
// Require function. It will be called recursevly until all
// converters are ran. After that `success` callback is ran.
// For instance if we're loading the .less file it will first
// run the `text` converter, then `less` converter and finally
// the `fn` converter.
function require(options, converters, success, error, config) {
var t = converters[0]
var type = config.attr('types')[converters.shift()];
type.require(options, function require_continue_check() {
// if we have more types to convert
if (converters.length) {
require(options, converters, success, error, config)
} else { // otherwise this is the final
success.apply(this, arguments);
}
}, error, config)
};
// =============================== TYPES ===============================
// a clean up script that prevents memory leaks and removes the
// script
var cleanUp = function (elem) {
elem.onreadystatechange = elem.onload = elem.onerror = null;
setTimeout(function () {
h.head().removeChild(elem);
}, 1);
},
// the last inserted script, needed for IE
lastInserted,
// if the state is done
stateCheck = /^loade|c|u/;
var cssCount = 0,
createSheet = h.doc && h.doc.createStyleSheet,
lastSheet, lastSheetOptions;
// Apply all the basic types
ConfigManager.defaults.types = {
"js": function (options, success, error) {
// create a script tag
var script = h.scriptTag(),
callback = function () {
if (!script.readyState || stateCheck.test(script.readyState)) {
cleanUp(script);
success();
}
},
errorTimeout;
// if we have text, just set and insert text
if (options.text) {
// insert
script.text = options.text;
} else {
var src = options.src; //st.idToUri( options.id );
// If we're in IE older than IE9 we need to use
// onreadystatechange to determine when javascript file
// is loaded. Unfortunately this makes it impossible to
// call teh error callback, because it will return
// loaded or completed for the script even if it
// encountered the 404 error
if (h.useIEShim) {
script.onreadystatechange = function () {
if (stateCheck.test(script.readyState)) {
success();
}
}
} else {
script.onload = callback;
// error handling doesn't work on firefox on the filesystem
if (h.support.error && error && src.protocol !== "file") {
script.onerror = error;
}
}
// listen to loaded
script.src = "" + src;
//script.src = options.src = addSuffix(options.src);
//script.async = false;
script.onSuccess = success;
}
// insert the script
lastInserted = script;
h.head().insertBefore(script, h.head().firstChild);
// if text, just call success right away, and clean up
if (options.text) {
callback();
}
},
"fn": function (options, success) {
var ret;
if (!options.skipCallbacks) {
ret = options.fn();
}
success(ret);
},
// request text
"text": function (options, success, error) {
h.request(options, function (text) {
options.text = text;
success(text);
}, error)
},
// loads css files and works around IE's 31 sheet limit
"css": function (options, success, error) {
if (options.text) { // less
var css = h.createElement("style");
css.type = "text/css";
if (css.styleSheet) { // IE
css.styleSheet.cssText = options.text;
} else {
(function (node) {
if (css.childNodes.length) {
if (css.firstChild.nodeValue !== node.nodeValue) {
css.replaceChild(node, css.firstChild);
}
} else {
css.appendChild(node);
}
})(h.doc.createTextNode(options.text));
}
h.head().appendChild(css);
} else {
if (createSheet) {
// IE has a 31 sheet and 31 import per sheet limit
if (!cssCount++) {
lastSheet = h.doc.createStyleSheet(options.src);
lastSheetOptions = options;
} else {
var relative = "" + URI(URI(lastSheetOptions.src).dir()).pathTo(options.src);
lastSheet.addImport(relative);
if (cssCount == 30) {
cssCount = 0;
}
}
success();
return;
}
options = options || {};
var link = h.createElement("link");
link.rel = options.rel || "stylesheet";
link.href = options.src;
link.type = "text/css";
h.head().appendChild(link);
}
success();
}
};
var moduleManager = function (steal, stealModules, interactives, config) {
// ============ MODULE ================
// a map of modules by moduleID
var modules = {},
id = 0;
// this is for methods on a 'steal instance'. A file can be in one of a few states:
// created - the steal instance is created, but we haven't started loading it yet
// this happens when thens are used
// loading - (loading=true) By calling load, this will tell steal to load a file
// loaded - (isLoaded=true) The file has been run, but its dependency files have been completed
// complete - all of this files dependencies have loaded and completed.
// A Module is almost anything. It is different from a module
// as it doesn't represent some unit of functionality, rather
// it represents a unit that can have other units "within" it
// as dependencies. A module can:
//
// - load - load the module to the client so it is available, but don't run it yet
// - run - run the code for the module
// - executed - the code has been run for the module, but all
// dependencies for that module might not have finished
// - completed - all modules within the module have completed
//
// __options__
// `options` can be a string, function, or object.
//
// __properties__
//
// - options - has a number of properties
// - src - a URI to this module that can be loaded from the current page
// - rootSrc - a URI to this module relative to the current root URI.
// - type - the type of module: "fn", "js", "css", etc
// - needs - other modules that must be loaded prior to this module
// - fn - a callback function to run when executed
// - unique - false if this module should be loaded each time
// - waits - this module should wait until all prior scripts have completed before running
// - loaded - a deferred indicating if this module has been loaded to the client
// - run - a deferred indicating if the the code for this module run
// - completed - a deferred indicating if all of this modules dependencies have
// completed
// - dependencies - an array of dependencies
var Module = function (options) {
// an array for dependencies, this is the steal calls this module makes
this.dependencies = [];
// an array of implicit dependencies this steal needs
this.needsDependencies = [];
// id for debugging
this.id = (++id);
// the original options
this.orig = options;
// the parent steal's id
this.curId = steal.cur && steal.cur.options.id;
this.setOptions(options);
// create the deferreds used to manage state
this.loaded = Deferred();
this.run = Deferred();
this.completed = Deferred();
};
Module.pending = [];
// `Module.make` is used to either create
// a new module, or return an existing
// module that matches the options.
Module.make = function (options) {
// create the temporary reasource
var module = new Module(options),
// use `rootSrc` as the definitive ID
id = module.options.id;
// assuming this module should not be created again.
if (module.unique && id) {
// Check if we already have a module for this rootSrc
// Also check with a .js ending because we defer 'type'
// determination until later
if (!modules[id] && !modules[id + ".js"]) {
// If we haven't loaded, cache the module
modules[id] = module;
} else {
// Otherwise get the cached module
existingModule = modules[id];
// If options were passed, copy new properties over.
// Don't copy src, etc because those have already
// been changed to be the right values;
if (!h.isString(options)) {
// extend everything other than id
for (var prop in options) {
if (prop !== "id") {
existingModule.options[prop] = options[prop];
}
}
}
return existingModule;
}
}
return module;
};
// updates the paths of things ...
// use stealModules b/c they are more fuzzy
// a module's id stays the same, but a path might change
//
h.extend(Module.prototype, {
setOptions: function (options) {
var prevOptions = this.options;
// if we have no options, we are the global Module that
// contains all other modules.
if (!options) { //global init cur ...
this.options = {};
this.waits = false;
}
//handle callback functions
else if (h.isFn(options)) {
var uri = URI.cur,
self = this,
cur = steal.cur;
this.options = {
fn: function () {
// Set the URI if there are steals
// within the callback.
URI.cur = uri;
// we should get the current "module"
// check it's listed dependencies and see
// if they have a value
var args = [],
found = false,
dep, value;
// iterate backwards through dependencies
for (var i = cur.dependencies.length; i >= 0; i--) {
dep = cur.dependencies[i];
if (found) {
if (dep === null) {
// //alert("YES")
break;
}
// We need to access the stored stealModules in this order
// - calculated id
// - original name
// - dependency return value otherwise
value = stealModules[dep.options.id] || stealModules[dep.orig] || dep.value;
args.unshift(value);
// what does this do?
}
if (dep === self) {
found = true;
}
}
var ret = options.apply(cur, args);
// if this returns a value, we should register it as a module ...
if (ret) {
// register this module ....
cur.value = ret;
}
return ret;
},
id: uri,
type: "fn"
}
// this has nothing to do with 'loading' options
this.waits = true;
this.unique = false;
} else {
// save the original options
this.options = steal.makeOptions(h.extend({}, options), this.curId);
this.waits = this.options.waits || false;
this.unique = true;
}
// if there are other options we haven't already set, reuse the old ones
for (opt in prevOptions) {
if (!this.options[opt]) {
this.options[opt] = prevOptions[opt];
}
}
if (this.options.id) {
this.options.abort = false;
}
},
// Calling complete indicates that all dependencies have
// been completed for this module
complete: function () {
this.completed.resolve();
},
// After the script has been loaded and run
// - checks what has been stolen (in pending)
// - wires up pendings steal's deferreds to eventually complete this
// - this is where all of steal's complexity is
executed: function (script) {
var myqueue, stel, src = this.options.src,
rootSrc = this.options.rootSrc;
// Set this as the current file so any relative urls
// will load from it.
// rootSrc needs to be the translated path
// we need id vs rootSrc ...
if (this.options.id) {
URI.cur = URI(this.options.id);
}
if (this.exports) {
this.exports()
}
// set this as the current module
steal.cur = this;
// mark yourself as 'loaded'.
this.run.resolve();
// If we are IE, get the queue from interactives.
// It in interactives because you can't use onload to know
// which script is executing.
if (h.support.interactive && src) { /*myqueue = interactives[src];*/
if (interactives[src]) {
myqueue = [];
if (interactives.length) {
for (var i = 0; i < interactives.length; i++) {
if (interactives[i] !== this.orig) {
myqueue.push(interactives[i])
}
}
} else {
if (interactives[src] !== this.orig) {
myqueue = interactives[src];
delete interactives[src];
}
}
}
}
// In other browsers, the queue of items to load is
// what is in pending
if (!myqueue) {
myqueue = Module.pending.slice(0);
Module.pending = [];
}
// if we have nothing, mark us as complete
if (!myqueue.length) {
this.complete();
return;
}
this.addDependencies(myqueue)
this.loadDependencies();
},
// add depenedencies to the module
addDependencies: function (myqueue) {
var self = this,
isProduction = steal.config().env == "production";
this.queue = [];
h.each(myqueue, function (i, item) {
if (item === null) {
self.queue.push(null);
return;
}
if ((isProduction && item.ignore) || (!isProduction && !steal.isRhino && item.prodonly)) {
return;
}
// make a steal object
var stel = Module.make(item);
if (steal.packHash[stel.options.id] && stel.options.type !== 'fn') { // if we are production, and this is a package, mark as loading, but steal package?
steal.has("" + stel.options.id);
stel = steal.make(steal.packHash["" + stel.options.id]);
}
// has to happen before 'needs' for when reversed...
self.queue.push(stel);
});
},
// loads module's dependencies
loadDependencies: function () {
//print("-setting up "+this.options.id)
// now we have to figure out how to wire up our pending steals
var self = this,
// the current
// iterate through the collection and add all the 'needs'
// before fetching...
//print("-instances "+this.options.id)
// The set of modules before the previous "wait" module
priorSet = [],
// The current set of modules after and including the
// previous "wait" module
set = [],
// The first set of modules that we will execute
// right away. This should be the first set of dependencies
// that we can load in parallel. If something has
// a need, the need should be in this set
firstSet = [],
// Should we be adding modules to the
// firstSet
setFirstSet = true;
// Goes through each module and maintains
// a list of the set of modules
// that must be complete before the current
// module (`priorSet`).
h.each(this.queue, function (i, module) {
// add it as a dependency, circular are not allowed
self.dependencies.push(module);
// if there's a wait and it's not the first thing
if ((module === null || module.waits) && set.length) {
// add the current set to `priorSet`
priorSet = priorSet.concat(set);
// empty the current set
set = [];
// we have our firs set of items
setFirstSet = false;
if (module === null) {
return;
}
}
if (module === null) return;
// lets us know this module is currently wired to load
module.isSetupToExecute = true;
// when the priorSet is completed, execute this module
// and when it's needs are done
var waitsOn = priorSet.slice(0);
// if there are needs, this can not be part of the "firstSet"
h.each(module.options.needs || [], function (i, raw) {
var need = Module.make({
id: raw,
idToUri: self.options.idToUri
});
// add the need to the module's dependencies
h.uniquePush(module.needsDependencies, need);
waitsOn.push(need);
// add needs to first set to execute
firstSet.push(need)
});
waitsOn.length && whenEach(waitsOn, "completed", module, "execute");
// what is this used for?
// module.waitedOn = module.waitedOn ? module.waitedOn.concat(priorSet) : priorSet.slice(0);
// add this steal to the current set
set.push(module);
// if we are still on the first set, and this has no needs
if (setFirstSet && (module.options.needs || []).length == 0) {
// add this to the first set of things
firstSet.push(module)
}
// start loading the module if possible
module.load();
});
// when every thing is complete, mark us as completed
priorSet = priorSet.concat(set);
whenEach(priorSet, "completed", self, "completed");
// execute the first set of dependencies
h.each(firstSet, function (i, f) {
f.execute();
});
},
/**
* Loads this steal
*/
load: function (returnScript) {
// if we are already loading / loaded
if (this.loading || this.loaded.isResolved()) {
return;
}
this.loading = true;
this.loaded.resolve();
},
execute: function () {
var self = this;
// if a late need dependency was addded
if (this.lateNeedDependency && !this.lateNeedDependency.completed.isResolved()) {
// call execute again when it's finished
this.lateNeedDependency.completed.then(function () {
self.execute()
})
return;
}
// check types
var raw = this.options,
types = config.attr('types');
// if it's a string, get it's extension and check if
// it is a registered type, if it is ... set the type
if (!raw.type) {
var ext = URI(raw.id).ext();
if (!ext && !types[ext]) {
ext = "js";
}
raw.type = ext;
}
if (!types[raw.type] && steal.config().env == 'development') {
throw "steal.js - type " + raw.type + " has not been loaded.";
} else if (!types[raw.type] && steal.config().env == 'production') {
// if we haven't defined EJS yet and we're in production, its ok, just ignore it
return;
}
var converters = types[raw.type].convert;
raw.buildType = converters.length ? converters[converters.length - 1] : raw.type;
if (!self.loaded.isResolved()) {
self.loaded.resolve();
}
if (!self.executing) {
self.executing = true;
config.require(self.options, function (value) {
self.executed(value);
}, function (error, src) {
var abortFlag = self.options.abort,
errorCb = self.options.error;
// if an error callback was provided, fire it
if (errorCb) {
errorCb.call(self.options);
}
h.win.clearTimeout && h.win.clearTimeout(self.completeTimeout)
// if abort: false, register the script as loaded, and don't throw
if (abortFlag === false) {
self.executed();
return;
}
throw "steal.js : " + self.options.src + " not completed"
});
}
},
updateOptions: function () {
var buildType = this.options.buildType;
var orginalOptions = this.options;
this.setOptions(this.orig);
var newOptions = this.options;
this.options = orginalOptions;
for (opt in newOptions) {
this.options[opt] = newOptions[opt];
}
this.options.buildType = buildType;
},
rewriteIdAndUpdateOptions: function (id) {
// if module is not a function it means it's `src` is changeable
if (this.options.type != "fn") {
// finds module's needs
// TODO this is terrible
var needs = (this.options.needs || []).slice(0),
buildType = this.options.buildType;
this.updateOptions();
var newId = this.options.id;
// this mapping is to move a config'd key
if (id !== newId) {
modules[newId] = this;
// TODO: remove the old one ....
}
this.options.buildType = buildType;
// if a module is set to load
// check if there are new needs
if (this.isSetupToExecute) {
this.addLateDependencies(needs);
}
}
},
addLateDependencies: function (needs) {
var self = this;
// find all `needs` and set up "late dependencies"
// this allows us to steal files that need to load
// special converters without loading these converters
// explicitely:
//
// steal('view.ejs', function(ejsFn){...})
//
// This will load files needed to convert .ejs files
// without explicite steal
h.each(this.options.needs || [], function (i, need) {
if (h.inArray(needs, need) == -1) {
var n = steal.make(need);
n.execute()
self.needsDependencies.push(n);
self.lateNeedDependency = n;
}
})
}
});
// =============================== ERROR HANDLING ===============================
h.extend(Module.prototype, {
load: h.after(Module.prototype.load, function (stel) {
var self = this;
if (h.doc && !self.completed && !self.completeTimeout && !steal.isRhino && (self.options.src.protocol == "file" || !h.support.error)) {
self.completeTimeout = setTimeout(function () {
throw "steal.js : " + self.options.src + " not completed"
}, 5000);
}
}),
complete: h.after(Module.prototype.complete, function () {
this.completeTimeout && clearTimeout(this.completeTimeout)
}),
// if we're about to mark a file as executed, mark its "has" array files as
// executed also
executed: h.before(Module.prototype.executed, function () {
if (this.options.has) {
this.loadHas();
}
}),
/**
* @hide
* Goes through the array of files listed in this.options.has, marks them all as loaded.
* This is used for files like production.css, which, once they load, need to mark the files they
* contain as loaded.
*/
loadHas: function () {
var stel, i, current = URI.cur;
if (this.options.buildType == 'js') {
return;
}
// mark everything in has loaded
h.each(this.options.has, function (i, has) {
// don't want the current file to change, since we're just marking files as loaded
URI.cur = URI(current);
steal.executed(has);
});
}
});
// =========== HAS ARRAY STUFF ============
// Logic that deals with files that have collections of other files within
// them. This is usually a production.css file,
// which when it loads, needs to mark several CSS and LESS files it represents
// as being "loaded". This is done by the production.js file having
// steal({src: "production.css", has: ["file1.css", "file2.css"]
//
// after a steal is created, if its been loaded
// already and has a "has", mark those files as loaded
Module.make = h.after(Module.make, function (stel) {
// if we have things
if (stel.options.has) {
// if we have loaded this already (and we are adding has's)
if (stel.run.isResolved()) {
stel.loadHas();
} else {
// have to mark has as loading and executing (so we don't try to get them)
steal.has.apply(steal, stel.options.has)
}
}
return stel;
}, true);
Module.modules = modules;
return Module;
}
function stealManager(kickoff, config, setStealOnWindow) {
// a startup function that will be called when steal is ready
var interactiveScript,
// key is script name, value is array of pending items
interactives = {},
// empty startup function
startup = function () {};
var st = function () {
// convert arguments into an array
var args = h.map(arguments, function (options) {
if (options) {
var opts = h.isString(options) ? {
id: options
} : options;
if (!opts.idToUri) {
opts.idToUri = st.idToUri
}
return opts;
} else {
return options;
}
});
if (args.length) {
Module.pending.push.apply(Module.pending, args);
// steal.after is called everytime steal is called
// it kicks off loading these files
st.after(args);
// return steal for chaining
}
return st;
};
if (setStealOnWindow) {
h.win.steal = st;
}
// clone steal context
st.clone = function () {
return stealManager(false, config.cloneContext())
}
st.config = function () {
st.config.called = true;
return config.attr.apply(config, arguments)
};
st.require = function () {
return config.require.apply(config, arguments);
}
st.config.called = false;
st._id = Math.floor(1000 * Math.random());
/**
* @function st.getScriptOptions
*
* `steal.getScriptOptions` is used to determine various
* options passed to the steal.js file:
*
* - should we load the production version of the
* (if you use steal.production.js instead of steal.js)
* - parts of the query string to determine `startFile`
* - location of the `root url`
*/
st.getScriptOptions = function (script) {
var options = {},
parts, src, query, startFile, env;
script = script || h.getStealScriptSrc();
if (script) {
// Split on question mark to get query
parts = script.src.split("?");
src = parts.shift();
// // for IE7, where the script.src is always relative
// if(!/\/\//.test(src)){
// var dir = URI.page.dir();
// src = URI(dir.join(src))+"";
// }
query = parts.join("?");
// Split on comma to get startFile and env
parts = query.split(",");
if (src.indexOf("steal.production") > -1) {
options.env = "production";
}
// Grab startFile
startFile = parts[0];
if (startFile) {
if (startFile.indexOf(".js") == -1) {
startFile += "/" + startFile.split("/").pop() + ".js";
}
options.startFile = startFile;
}
// Grab env
env = parts[1];
if (env) {
options.env = env;
}
// Split on / to get rootUrl
parts = src.split("/")
parts.pop();
if (parts[parts.length - 1] == "steal") {
parts.pop();
}
var root = parts.join("/");
options.root = root
}
return options;
};
/**
* @function st.id
*
* Given a resource id passed to `steal( resourceID, currentWorkingId )`, this function converts it to the
* final, unique id. This function can be overwritten
* to change how unique ids are defined, for example, to be more AMD-like.
*
* The following are the default rules.
*
* Given an ID:
*
* 1. Check the id has an extension like _.js_ or _.customext_. If it doesn't:
* 1. Check if the id is relative, meaning it starts with _../_ or _./_. If it is not, add
* "/" plus everything after the last "/". So `foo/bar` becomes `foo/bar/bar`
* 2. Add .js to the id.
* 2. Check if the id is relative, meaning it starts with _../_ or _./_. If it is relative,
* set the id to the id joined from the currentWorkingId.
* 3. Check the
*
*
* `st.id()`
*/
// returns the "rootSrc" id, something that looks like requireJS
// for a given id/path, what is the "REAL" id that should be used
// this is where substituation can happen
st.id = function (id, currentWorkingId, type) {
// id should be like
var uri = URI(id);
uri = uri.addJS().normalize(currentWorkingId ? new URI(currentWorkingId) : null)
// check foo/bar
if (!type) {
type = "js"
}
if (type == "js") {
// if it ends with .js remove it ...
// if it ends
}
// check map config
var map = config.attr().map || {};
// always run past
h.each(map, function (loc, maps) {
// is the current working id matching loc
if (h.matchesId(loc, currentWorkingId)) {
// run maps
h.each(maps, function (part, replaceWith) {
if (("" + uri).indexOf(part) == 0) {
uri = URI(("" + uri).replace(part, replaceWith))
}
})
}
})
return uri;
}
st.amdToId = function (id, currentWorkingId, type) {
var uri = URI(id);
uri = uri.normalize(currentWorkingId ? new URI(currentWorkingId) : null)
// check foo/bar
if (!type) {
type = "js"
}
if (type == "js") {
// if it ends with .js remove it ...
// if it ends
}
// check map config
var map = config.attr().map || {};
// always run past
h.each(map, function (loc, maps) {
// is the current working id matching loc
if (h.matchesId(loc, currentWorkingId)) {
// run maps
h.each(maps, function (part, replaceWith) {
if (("" + uri).indexOf(part) == 0) {
uri = URI(("" + uri).replace(part, replaceWith))
}
})
}
})
return uri;
}
// for a given ID, where should I find this resource
/**
* @function st.idToUri
*
* `steal.idToUri( id, noJoin )` takes an id and returns a URI that
* is the location of the file. It uses the paths option of [config].
* Passing true for `noJoin` does not join from the root URI.
*/
st.idToUri = function (id, noJoin) {
// this is normalize
var paths = config.attr().paths || {},
path;
// always run past
h.each(paths, function (part, replaceWith) {
path = "" + id;
// if path ends in / only check first part of id
if ((h.endsInSlashRegex.test(part) && path.indexOf(part) == 0) ||
// or check if its a full match only
path === part) {
id = URI(path.replace(part, replaceWith));
}
})
return noJoin ? id : config.attr().root.join(id)
}
// for a given AMD id this will return an URI object
/**
* @function st.amdIdToUri
*
* `steal.amdIdToUri( id, noJoin )` takes and AMD id and returns a URI that
* is the location of the file. It uses the paths options of [config].
* Passing true for `noJoin` does not join from that URI.
*/
st.amdIdToUri = function (id, noJoin) {
// this is normalize
var paths = config.attr().paths || {},
path;
// always run past
h.each(paths, function (part, replaceWith) {
path = "" + id;
// if path ends in / only check first part of id
if ((h.endsInSlashRegex.test(part) && path.indexOf(part) == 0) ||
// or check if its a full match only
path === part) {
id = URI(path.replace(part, replaceWith));
}
})
if (/(^|\/)[^\/\.]+$/.test(id)) {
id = URI(id + ".js")
}
return id //noJoin ? id : config().root.join(id)
}
// ## AMD ##
var modules = {
};
// AMD is not available for now. If you want to use AMD features with
// steal you can by setting the `amd` param to true:
//
// steal({
// amd: true
// })
//
// This will expose `define` and `require` functions which can be used
// to load AMD modules
if (config.attr('amd') === true) {
// convert resources to modules ...
// a function is a module definition piece
// you steal(moduleId1, moduleId2, function(module1, module2){});
/**
* @function window.define
*
* AMD compatible `define` function. It is available only if steal's
* `amd` param is set to true:
*
*
*
*/
h.win.define = function (moduleId, dependencies, method) {
if (typeof moduleId == 'function') {
modules[URI.cur + ""] = moduleId();
} else if (!method && dependencies) {
if (typeof dependencies == "function") {
modules[moduleId] = dependencies();
} else {
modules[moduleId] = dependencies;
}
} else if (dependencies && method && !dependencies.length) {
modules[moduleId] = method();
} else {
st.apply(null, h.map(dependencies, function (dependency) {
dependency = typeof dependency === "string" ? {
id: dependency
} : dependency;
dependency.toId = st.amdToId;
dependency.idToUri = st.amdIdToUri;
return dependency;
}).concat(method))
}
}
/**
* @function window.require
*
* AMD compatible require function. It is available only if steal's
* `amd` param is set to true:
*
*
*
*/
h.win.require = function (dependencies, method) {
var depends = h.map(dependencies, function (dependency) {
dependency = typeof dependency === "string" ? {
id: dependency
} : dependency;
dependency.toId = st.amdToId;
dependency.idToUri = st.amdIdToUri;
return dependency;
}).concat([method]);
st.apply(null, depends)
}
h.win.define.amd = {
jQuery: true
}
// expose steal as AMD module
define("steal", [], function () {
return st;
});
define("require", function () {
return require;
})
}
/**
* @add steal
*/
// =============================== STATIC API ===============================
var events = {},
page;
h.extend(st, {
each: h.each,
extend: h.extend,
Deferred: Deferred,
// Currently used a few places
isRhino: h.win.load && h.win.readUrl && h.win.readFile,
/**
* @hide
* Makes options
* @param {Object} options
*/
makeOptions: function (options, curId) {
// convert it to a uri
if (!options.id) {
throw {
message: "no id",
options: options
}
}
options.id = options.toId ? options.toId(options.id, curId) : st.id(options.id, curId);
// set the ext
options.ext = options.id.ext();
options.src = options.idToUri ? options.idToUri(options.id) + "" : steal.idToUri(options.id) + "";
// Check if it's a configured needs
var configedExt = config.attr().ext[options.ext];
// if we have something, but it's not a type
if (configedExt && !config.attr().types[configedExt]) {
if (!options.needs) {
options.needs = [];
}
options.needs.push(configedExt);
}
return options;
},
/**
* Calls steal, but waits until all previous steals
* have completed loading until loading the
* files passed to the arguments:
*
* steal('jquery', 'can/util').then('file/that/depends/on_jquery.js')
*
* In this case first `jquery` and `can/util` will be loaded in parallel,
* and after both are loaded `file/that/depends/on_jquery.js` will be loaded
*/
then: function () {
var args = h.map(arguments);
args.unshift(null)
return st.apply(h.win, args);
},
/**
* `steal.bind( event, handler(eventData...) )` listens to
* events on steal. Typically these are used by various build processes
* to know when steal starts and finish loading resources and their
* dependencies. Listen to an event like:
*
* steal.bind('end', function(rootModule){
* rootModule.dependencies // the first stolen resources.
* })
*
* Steal supports the following events:
*
* - __start__ - steal has started loading a group of resources and their dependencies.
* - __end__ - steal has finished loading a group of resources and their dependencies.
* - __done__ - steal has finished loading the first set of resources and their dependencies.
* - __ready__ - after both steal's "done" event and the `window`'s onload event have fired.
*
* For example, the following html:
*
* @codestart html
* <script src='steal/steal.js'></script>
* <script>
* steal('can/control', function(){
* setTimeout(function(){
* steal('can/model')
* },200)
* })
* </script>
* @codeend
*
* Would fire:
*
* - __start__ - immediately after `steal('can/control')` is called
* - __end__ - after 'can/control', all of it's dependencies, and the callback function have executed and completed.
* - __done__ - fired after the first 'end' event.
* - __ready__ - fired after window.onload and the 'done' event
* - __start__ - immediately after `steal('can/model')` is called
* - __end__ - fired after 'can/model' and all of it's dependencies have fired.
*
*
*
* @param {String} event
* @param {Function} listener
*/
bind: function (event, listener) {
if (!events[event]) {
events[event] = []
}
var special = st.events[event]
if (special && special.add) {
listener = special.add(listener);
}
listener && events[event].push(listener);
return st;
},
/**
* `steal.one(eventName, handler(eventArgs...) )` works just like
* [steal.bind] but immediately unbinds after `handler` is called.
*/
one: function (event, listener) {
return st.bind(event, function () {
listener.apply(this, arguments);
st.unbind(event, arguments.callee);
});
},
events: {},
/**
* `steal.unbind( eventName, handler )` removes an event listener on steal.
* @param {String} event
* @param {Function} listener
*/
unbind: function (event, listener) {
var evs = events[event] || [],
i = 0;
while (i < evs.length) {
if (listener === evs[i]) {
evs.splice(i, 1);
} else {
i++;
}
}
},
trigger: function (event, arg) {
var arr = events[event] || [];
// array items might be removed during each iteration (with unbind),
// so we iterate over a copy
h.each(h.map(arr), function (i, f) {
f(arg);
})
},
/**
* @hide
* Creates resources and marks them as loading so steal doesn't try
* to load them.
*
* steal.has("foo/bar.js","zed/car.js");
*
* This is used when a file has other resources in it.
*/
has: function () {
// we don't use IE's interactive script functionality while
// production scripts are loading
h.support.interactive = false;
h.each(arguments, function (i, arg) {
var stel = Module.make({
id: arg,
idToUri: st.idToUri
});
stel.loading = stel.executing = true;
});
},
make: function (id) {
var opts = (typeof id === "string" ? {
id: id
} : id);
if (!opts.idToUri) {
opts.idToUri = st.idToUri;
}
return Module.make(opts);
},
// a dummy function to add things to after the stel is created, but before executed is called
preexecuted: function () {},
/**
* @hide
* Signals that a resource's JS code has been run. This is used
* when a file has other resources in it.
*
* steal.has("foo/bar.js");
*
* //start code for foo/bar.js
* steal("zed/car.js", function(){ ... });
* steal.executed("foo/bar.js");
*
* When a resource is executed, its dependent resources are loaded and eventually
* executed.
*/
// called when a script has loaded via production
executed: function (name) {
// create the steal, mark it as loading, then as loaded
var resource = Module.make({
id: name,
idToUri: st.idToUri
});
resource.loading = resource.executing = true;
//convert(stel, "complete");
st.preexecuted(resource);
resource.executed()
return st;
},
type: function (type, cb) {
var typs = type.split(" ");
if (!cb) {
return config.attr('types')[typs.shift()].require
}
var types = config.attr('types')
types[typs.shift()] = {
require: cb,
convert: typs
};
config.attr('types', types)
},
request: h.request
});
// Determine if we're running in IE older than IE9. This
// will affect loading strategy for JavaScripts.
h.useIEShim = (function () {
if (st.isRhino) {
return false;
}
var d = document.createElement('div');
d.innerHTML = "";
return !!(h.scriptTag().readyState && d.innerText === "ie");
})()
// ============================== Packages ===============================
/**
* @function packages
* `steal.packages( packageIds... )` defines modules for deferred downloading.
*
* This is used by the build system to build collections of modules that will be downloaded
* after initial page load.
*
* For example, an application that wants to progressively load the contents and
* dependencies of _login/login.js_, _filemanager/filemanager.js_, and _contacts/contacts.js_,
* while immediately loading the current users's data might look like:
*
* steal.packages('login','filemanager','contacts')
* steal('models/user', function(User){
*
* // get the current User
* User.findOne({id: "current"},
*
* // success - they logged in
* function(user){
* if(window.location.hash == "#filemanager"){
* steal('filemanager')
* }
* },
* // error - they are logged out
* function(){
* steal('login', function(){
* new Login(document.body);
* // preload filemanager
*
* })
* })
* })
*
*
* steal.packages('tasks','dashboard','fileman');
*
*/
st.packs = [];
st.packHash = {};
st.packages = function (map) {
if (!arguments.length) {
return st.packs;
} else {
if (typeof map == 'string') {
st.packs.push.apply(st.packs, arguments);
} else {
st.packHash = map;
}
return this;
}
};
var Module = moduleManager(st, modules, interactives, config);
resources = Module.modules;
/**
* Implements shim support for steal
*
* This function sets up shims for steal. It follows RequireJS' syntax:
*
* steal.config({
* shim : {
* jquery: {
* exports: "jQuery"
* }
* }
* })
*
* You can also set function to explicitely return value from the module:
*
* steal.config({
* shim : {
* jquery: {
* exports: function(){
* return window.jQuery;
* }
* }
* }
* })
*
* This enables steal to pass you a value from library that is not wrapped
* with steal() call.
*
* steal('jquery', function(j){
* // j is set to jQuery
* })
*/
st.setupShims = function (shims) {
// Go through all shims
for (var id in shims) {
// Make resource from shim's id. Since steal takes care
// of always returning same resource for same id
// when someone steals resource created in this function
// they will get same object back
var resource = Module.make({
id: id
});
if (typeof shims[id] === "object") {
// set up dependencies of the module
var needs = shims[id].deps || []
var exports = shims[id].exports;
var init = shims[id].init
} else {
needs = shims[id];
}(function (_resource, _needs) {
_resource.options.needs = _needs;
})(resource, needs);
// create resource's exports function. We check for existance
// of this function in `Module.prototype.executed` and if it exitst
// it is called, which sets `value` of the module
resource.exports = (function (_resource, _needs, _exports, _init) {
return function () {
var args = [];
h.each(_needs, function (i, id) {
args.push(Module.make(id).value);
});
if (_init) {
// if module has exports function, call it
_resource.value = _init.apply(null, args);
} else {
// otherwise it's a string so we just return
// object from the window e.g window['jQuery']
_resource.value = h.win[_exports];
}
}
})(resource, needs, exports, init)
}
}
// =============================== STARTUP ===============================
var rootSteal = false;
// essentially ... we need to know when we are on our first steal
// then we need to know when the collection of those steals ends ...
// and, it helps if we use a 'collection' steal because of it's natural
// use for going through the pending queue
//
h.extend(st, {
// modifies src
/*makeOptions : after(steal.makeOptions,function(raw){
raw.src = URI.root().join(raw.rootSrc = URI( raw.rootSrc ).insertMapping());
}),*/
//root mappings to other locations
mappings: {},
/**
* Maps a 'rooted' folder to another location. For instance you could use it
* to map from the `foo/bar` location to the `http://foo.cdn/bar`:
*
* steal.map('foo/bar', 'http://foo.cdn/bar');
*
* @param {String|Object} from the location you want to map from. For example:
* 'foo/bar'
* @param {String} [to] where you want to map this folder too. Ex: 'http://foo.cdn/bar'
* @return {steal}
*/
map: function (from, to) {
if (h.isString(from)) {
st.mappings[from] = {
test: new RegExp("^(\/?" + from + ")([/.]|$)"),
path: to
};
h.each(modules, function (id, module) {
if (module.options.type != "fn") {
// TODO terrible
var buildType = module.options.buildType;
module.updateOptions();
module.options.buildType = buildType;
}
})
} else { // its an object
h.each(from, st.map);
}
return this;
},
// called after steals are added to the pending queue
after: function () {
// if we don't have a current 'top' steal
// we create one and set it up
// to start loading its dependencies (the current pending steals)
if (!rootSteal) {
rootSteal = new Module();
// keep a reference in case it disappears
var cur = rootSteal,
// runs when a steal is starting
go = function () {
// indicates that a collection of steals has started
st.trigger("start", cur);
cur.completed.then(function () {
rootSteal = null;
st.trigger("end", cur);
});
cur.executed();
};
// If we are in the browser, wait a
// brief timeout before executing the rootModule.
// This allows embeded script tags with steal to be part of
// the initial set
if (h.win.setTimeout) {
// we want to insert a "wait" after the current pending
st.pushPending();
setTimeout(function () {
st.popPending();
go();
}, 0)
} else {
// if we are in rhino, start loading dependencies right away
go()
}
}
},
_before: h.before,
_after: h.after
});
(function () {
var myPending;
st.pushPending = function () {
myPending = Module.pending.slice(0);
Module.pending = [];
h.each(myPending, function (i, arg) {
Module.make(arg);
})
}
st.popPending = function () {
Module.pending = Module.pending.length ? myPending.concat(null, Module.pending) : myPending;
}
})();
// =============================== jQuery ===============================
(function () {
var jQueryIncremented = false,
jQ, ready = false;
// check if jQuery loaded after every script load ...
Module.prototype.executed = h.before(Module.prototype.executed, function () {
var $ = h.win.jQuery;
if ($ && "readyWait" in $) {
//Increment jQuery readyWait if ncecessary.
if (!jQueryIncremented) {
jQ = $;
$.readyWait += 1;
jQueryIncremented = true;
}
}
});
// once the current batch is done, fire ready if it hasn't already been done
st.bind("end", function () {
if (jQueryIncremented && !ready) {
jQ.ready(true);
ready = true;
}
})
})();
// =========== DEBUG =========
// var name = function(stel){
// if(stel.options && stel.options.type == "fn"){
// return stel.orig.name? stel.orig.name : stel.options.id+":fn";//(""+stel.orig).substr(0,10)
// }
// return stel.options ? stel.options.id + "": "CONTAINER"
// }
//
// Module.prototype.load = before( Module.prototype.load, function(){
// console.log(" load", name(this), this.loading, steal._id, this.id)
// })
//
// Module.prototype.executed = before(Module.prototype.executed, function(){
// var namer= name(this)
// console.log(" executed", namer, steal._id, this.id)
// })
//
// Module.prototype.complete = before(Module.prototype.complete, function(){
// console.log(" complete", name(this), steal._id, this.id)
// })
// ============= WINDOW LOAD ========
var loaded = {
load: Deferred(),
end: Deferred()
},
firstEnd = false;
h.addEvent(h.win, "load", function () {
loaded.load.resolve();
});
st.one("end", function (collection) {
loaded.end.resolve(collection);
firstEnd = collection;
st.trigger("done", firstEnd)
})
st.firstComplete = loaded.end;
Deferred.when(loaded.load, loaded.end).then(function () {
st.trigger("ready")
st.isReady = true;
});
st.events.done = {
add: function (cb) {
if (firstEnd) {
cb(firstEnd);
return false;
} else {
return cb;
}
}
};
startup = h.after(startup, function () {
// get options from
var urlOptions = st.getScriptOptions();
// A: GET OPTIONS
// 1. get script options
//h.extend(options, ); TODO: remove
// 2. options from a steal object that existed before this steal
// the steal object is copied right away
// h.extend(options, opts);
// 3. if url looks like steal[xyz]=bar, add those to the options
// does this need to be supported anywhere?
// NO - Justin
var search = h.win.location && decodeURIComponent(h.win.location.search);
search && search.replace(/steal\[([^\]]+)\]=([^&]+)/g, function (whoe, prop, val) {
urlOptions[prop] = ~val.indexOf(",") ? val.split(",") : val;
});
// B: DO THINGS WITH OPTIONS
// CALCULATE CURRENT LOCATION OF THINGS ...
config.attr(urlOptions);
var options = config.attr();
// mark things that have already been loaded
h.each(options.executed || [], function (i, stel) {
st.executed(stel)
})
// immediate steals we do
var steals = [];
// add start files first
if (options.startFiles) {
/// this can be a string or an array
steals.push.apply(steals, h.isString(options.startFiles) ? [options.startFiles] : options.startFiles)
options.startFiles = steals.slice(0)
}
// either instrument is in this page (if we're the window opened from
// steal.browser), or its opener has it
// try-catching this so we dont have to build up to the iframe
// instrumentation check
try {
// win.top.steal.instrument is for qunit
// win.top.opener.steal.instrument is for funcunit
if (!options.browser && ((h.win.top && h.win.top.st.instrument) || (h.win.top && h.win.top.opener && h.win.top.opener.steal && h.win.top.opener.st.instrument))) {
// force startFiles to load before instrument
steals.push(h.noop, {
id: "steal/instrument",
waits: true
});
}
} catch (e) {
// This would throw permission denied if
// the child window was from a different domain
}
// we only load things with force = true
if (config.attr().env == "production" && config.attr().loadProduction && config.attr().production) {
st({
id: config.attr().production,
force: true
});
} else {
steals.unshift({
id: "stealconfig.js",
abort: false
});
if (options.loadDev !== false) {
steals.unshift({
id: "steal/dev/dev.js",
ignore: true
});
}
if (options.startFile) {
steals.push(null, options.startFile)
}
}
if (steals.length) {
st.apply(h.win, steals);
}
});
// =========== INTERACTIVE STUFF ===========
// Logic that deals with making steal work with IE. IE executes scripts out of order, so in order to tell which scripts are
// dependencies of another, steal needs to check which is the currently "interactive" script.
var getInteractiveScript = function () {
var scripts = h.getElementsByTagName("script"),
i = scripts.length;
while (i--) {
// if script's readyState is interactive it is the one we want
if (scripts[i].readyState === "interactive") {
return scripts[i];
}
}
},
getCachedInteractiveScript = function () {
if (interactiveScript && interactiveScript.readyState === 'interactive') {
return interactiveScript;
}
if (interactiveScript = getInteractiveScript()) {
return interactiveScript;
}
// check last inserted
if (lastInserted && lastInserted.readyState == 'interactive') {
return lastInserted;
}
return null;
};
h.support.interactive = h.doc && !! getInteractiveScript();
if (h.support.interactive) {
// after steal is called, check which script is "interactive" (for IE)
st.after = h.after(st.after, function () {
// check if disabled by st.loading()
if (!h.support.interactive) {
return;
}
var interactive = getCachedInteractiveScript();
// if no interactive script, this is a steal coming from inside a steal, let complete handle it
if (!interactive || !interactive.src || /steal\.(production|production\.[a-zA-Z0-9\-\.\_]*)*js/.test(interactive.src)) {
return;
}
// get the source of the script
var src = interactive.src;
// create an array to hold all steal calls for this script
if (!interactives[src]) {
interactives[src] = []
}
// add to the list of steals for this script tag
if (src) {
interactives[src].push.apply(interactives[src], Module.pending);
Module.pending = [];
}
})
// This is used for packaged scripts. As the packaged script executes, we grab the
// dependencies that have come so far and assign them to the loaded script
st.preexecuted = h.before(st.preexecuted, function (stel) {
// check if disabled by st.loading()
if (!h.support.interactive) {
return;
}
// get the src name
var src = stel.options.src,
// and the src of the current interactive script
interactiveSrc = getCachedInteractiveScript().src;
interactives[src] = interactives[interactiveSrc];
interactives[interactiveSrc] = null;
})
}
// Use config.on to listen on changes in config. We primarily use this
// to update resources' paths when stealconfig.js is loaded.
config.on(function (configData) {
h.each(resources, function (id, resource) {
resource.rewriteIdAndUpdateOptions(id);
});
// set up shims after ids are updated
if (configData.shim) {
st.setupShims(configData.shim)
}
})
st.File = st.URI = URI;
// if this is a first steal context in the page
// we need to set up the `steal` module so we would
// know steal was loaded.
if (kickoff) {
var stealModule = new Module({
id: "steal"
})
stealModule.value = st;
stealModule.loaded.resolve();
stealModule.run.resolve();
stealModule.executing = true;
stealModule.completed.resolve();
resources[stealModule.options.id] = stealModule;
}
startup();
st.resources = resources;
st.Module = Module;
return st;
}
// create initial steal instance
stealManager(true, new ConfigManager(typeof h.win.steal == "object" ? h.win.steal : {}), true)
})();