includejs
Version:
IncludeJS Resource Builder Tool
564 lines (484 loc) • 14.2 kB
JavaScript
;
void
function(w, d) {
var cfg = {},
bin = {},
isWeb = !! (w.location && w.location.protocol && /^https?:/.test(w.location.protocol)),
handler = {},
regexp = {
name: new RegExp('\\{name\\}', 'g')
},
helper = { /** TODO: improve url handling*/
uri: {
getDir: function(url) {
var index = url.lastIndexOf('/');
return index == -1 ? '' : url.substring(index + 1, -index);
},
/** @obsolete */
resolveCurrent: function() {
var scripts = d.querySelectorAll('script');
return scripts[scripts.length - 1].getAttribute('src');
},
resolveUrl: function(url, parent) {
if (cfg.path && url[0] == '/') {
url = cfg.path + url.substring(1);
}
if (url[0] == '/') {
if (isWeb == false || cfg.lockedToFolder == true) return url.substring(1);
return url;
}
switch (url.substring(0, 4)) {
case 'file':
case 'http':
return url;
}
if (parent != null && parent.location != null) return parent.location + url;
return url;
}
},
extend: function(target, source) {
for (var key in source) target[key] = source[key];
return target;
},
/**
* @arg x :
* 1. string - URL to resource
* 2. array - URLs to resources
* 3. object - {route: x} - route defines the route template to resource,
* it must be set before in include.cfg.
* example:
* include.cfg('net','scripts/net/{name}.js')
* include.js({net: 'downloader'}) // -> will load scipts/net/downloader.js
* @arg namespace - route in case of resource url template, or namespace in case of LazyModule
*
* @arg fn - callback function, which receives namespace|route, url to resource and ?id in case of not relative url
* @arg xpath - xpath string of a lazy object 'object.sub.and.othersub';
*/
eachIncludeItem: function(type, x, fn, namespace, xpath) {
if (x == null) {
console.error('Include Item has no Data', type, namespace);
return;
}
if (type == 'lazy' && xpath == null) {
for (var key in x) this.eachIncludeItem(type, x[key], fn, null, key);
return;
}
if (x instanceof Array) {
for (var i = 0; i < x.length; i++) this.eachIncludeItem(type, x[i], fn, namespace, xpath);
return;
}
if (typeof x === 'object') {
for (var key in x) this.eachIncludeItem(type, x[key], fn, key, xpath);
return;
}
if (typeof x === 'string') {
var route = namespace && cfg[namespace];
if (route) {
namespace += '.' + x;
x = route.replace(regexp.name, x);
}
fn(namespace, x, xpath);
return;
}
console.error('Include Package is invalid', arguments);
},
invokeEach: function(arr, args) {
if (arr == null) return;
if (arr instanceof Array) {
for (var i = 0, x, length = arr.length; x = arr[i], i < length; i++) {
if (typeof x === 'function')(args != null ? x.apply(this, args) : x());
}
}
},
doNothing: function(fn) {
typeof fn == 'function' && fn()
},
reportError: function(e) {
console.error('IncludeJS Error:', e, e.message, e.url);
typeof handler.onerror == 'function' && handler.onerror(e);
},
ensureArray: function(obj, xpath) {
if (!xpath) return obj;
var arr = xpath.split('.');
while (arr.length - 1) {
var key = arr.shift();
obj = obj[key] || (obj[key] = {});
}
return (obj[arr.shift()] = []);
},
xhr: function(url, callback) {
var xhr = new XMLHttpRequest(),
s = Date.now();
xhr.onreadystatechange = function() {
xhr.readyState == 4 && callback && callback(url, xhr.responseText);
}
xhr.open('GET', url, true);
xhr.send();
}
},
events = (function(w, d) {
if (d == null) {
return {
ready: helper.doNothing,
load: helper.doNothing
};
}
var readycollection = [],
loadcollection = null,
readyqueue = null,
timer = Date.now();
d.onreadystatechange = function() {
if (/complete|interactive/g.test(d.readyState) == false) return;
if (timer) console.log('DOMContentLoader', d.readyState, Date.now() - timer, 'ms');
events.ready = (events.readyQueue = helper.doNothing);
helper.invokeEach(readyqueue);
helper.invokeEach(readycollection);
readycollection = null;
readyqueue = null;
if (d.readyState == 'complete') {
events.load = helper.doNothing;
helper.invokeEach(loadcollection);
loadcollection = null;
}
};
return {
ready: function(callback) {
readycollection.unshift(callback);
},
readyQueue: function(callback){
(readyqueue || (readyqueue = [])).push(callback);
},
load: function(callback) {
(loadcollection || (loadcollection = [])).unshift(callback);
}
}
})(w, d);
var IncludeDeferred = Class({
ready: function(callback) {
return this.on(4, function() {
events.ready(callback);
});
},
/** assest loaded and window is loaded */
loaded: function(callback) {
return this.on(4, function() {
events.load(callback);
});
},
/** assest loaded */
done: function(callback) {
return this.on(4, this.resolve.bind(this, callback));
},
resolve: function(callback) {
var r = callback(this.response);
if (r != null) this.obj = r;
}
});
var StateObservable = Class({
Construct: function() {
this.callbacks = [];
},
on: function(state, callback) {
state <= this.state ? callback(this) : this.callbacks.unshift({
state: state,
callback: callback
});
return this;
},
readystatechanged: function(state) {
this.state = state;
for (var i = 0, x, length = this.callbacks.length; x = this.callbacks[i], i < length; i++) {
if (x.state > this.state || x.callback == null) continue;
x.callback(this);
x.callback = null;
}
}
});
var currentParent;
var Include = Class({
setCurrent: function(data) {
currentParent = data;
},
incl: function(type, pckg) {
if (this instanceof Resource) return this.include(type, pckg);
var r = new Resource;
if (currentParent) {
r.id = currentParent.id;
//-r.url = currentParent.url;
r.namespace = currentParent.namespace;
//-currentParent = null;
}
return r.include(type, pckg);
//-return (this instanceof Resource ? this : new Resource).include(type, pckg);
},
js: function(pckg) {
return this.incl('js', pckg);
},
css: function(pckg) {
return this.incl('css', pckg);
},
load: function(pckg) {
return this.incl('load', pckg);
},
ajax: function(pckg) {
return this.incl('ajax', pckg);
},
embed: function(pckg) {
return this.incl('embed', pckg);
},
lazy: function(pckg) {
return this.incl('lazy', pckg);
},
cfg: function(arg) {
switch (typeof arg) {
case 'object':
for (var key in arg) cfg[key] = arg[key];
break;
case 'string':
if (arguments.length == 1) return cfg[arg];
if (arguments.length == 2) cfg[arg] = arguments[1];
break;
case 'undefined':
return cfg;
}
return this;
},
promise: function(namespace) {
var arr = namespace.split('.'),
obj = w;
while (arr.length) {
var key = arr.shift();
obj = obj[key] || (obj[key] = {});
}
return obj;
},
register: function(_bin) {
var onready = [];
for (var key in _bin) {
for (var i = 0; i < _bin[key].length; i++) {
var id = _bin[key][i].id,
url = _bin[key][i].url,
namespace = _bin[key][i].namespace,
resource = new Resource;
resource.state = 4;
resource.namespace = namespace;
resource.type = key;
if (url) {
if (url[0] == '/') url = url.substring(1);
resource.location = helper.uri.getDir(url);
}
switch (key) {
case 'load':
case 'lazy':
resource.state = 0;
events.readyQueue(function(_r, _id) {
var container = d.querySelector('script[data-id="' + _id + '"]');
if (container == null) {
console.error('"%s" Data was not embedded into html', _id);
return;
}
_r.obj = container.innerHTML;
_r.readystatechanged(4);
}.bind(this, resource, id));
break;
};
(bin[key] || (bin[key] = {}))[id] = resource;
}
}
}
});
var hasRewrites = typeof IncludeRewrites != 'undefined',
rewrites = hasRewrites ? IncludeRewrites : null;
var Resource = Class({
Base: Include,
Extends: [IncludeDeferred, StateObservable],
Construct: function(type, url, namespace, xpath, parent, id) {
if (type == null) {
return this;
}
this.namespace = namespace;
this.type = type;
this.xpath = xpath;
this.url = url;
if (url != null) {
this.url = helper.uri.resolveUrl(url, parent);
}
if (id) void(0);
else if (namespace) id = namespace;
else if (url[0] == '/') id = url;
else if (parent && parent.namespace) id = parent.namespace + '/' + url;
else if (parent && parent.location) id = '/' + parent.location.replace(/^[\/]+/, '') + url;
else id = '/' + url;
if (bin[type] && bin[type][id]) {
return bin[type][id];
}
if (hasRewrites == true && rewrites[id] != null) {
url = rewrites[id];
} else {
url = this.url;
}
this.location = helper.uri.getDir(url);
//-console.log('includejs. Load Resource:', id, url);
;
(bin[type] || (bin[type] = {}))[id] = this;
var tag;
switch (type) {
case 'js':
helper.xhr(url, this.onload.bind(this));
if (d != null) {
tag = d.createElement('script');
tag.type = "application/x-included-placeholder";
tag.src = url;
}
break;
case 'ajax':
case 'load':
case 'lazy':
helper.xhr(url, this.onload.bind(this));
break;
case 'css':
this.state = 4;
tag = d.createElement('link');
tag.href = url;
tag.rel = "stylesheet";
tag.type = "text/css";
break;
case 'embed':
tag = d.createElement('script');
tag.type = 'application/javascript';
tag.src = url;
tag.onload = function() {
this.readystatechanged(4);
}.bind(this);
tag.onerror = tag.onload;
break;
}
if (tag != null) {
d.querySelector('head').appendChild(tag);
tag = null;
}
return this;
},
include: function(type, pckg) {
this.state = 0;
if (this.includes == null) this.includes = [];
helper.eachIncludeItem(type, pckg, function(namespace, url, xpath) {
var resource = new Resource(type, url, namespace, xpath, this);
this.includes.push(resource);
resource.index = this.calcIndex(type, namespace);
resource.on(4, this.resourceLoaded.bind(this));
}.bind(this));
return this;
},
calcIndex: function(type, namespace) {
if (this.response == null) this.response = {};
switch (type) {
case 'js':
case 'load':
case 'ajax':
if (this.response[type + 'Index'] == null) this.response[type + 'Index'] = -1;
return ++this.response[type + 'Index'];
}
return -1;
},
wait: function() {
if (this.waits == null) this.waits = [];
if (this._include == null) this._include = this.include;
var data;
this.waits.push((data = []));
this.include = function(type, pckg) {
data.push({
type: type,
pckg: pckg
});
return this;
}
return this;
},
resourceLoaded: function(resource) {
if (this.parsing) return;
if (resource != null && resource.obj != null && resource.obj instanceof Include === false) {
switch (resource.type) {
case 'js':
case 'load':
case 'ajax':
var obj = (this.response[resource.type] || (this.response[resource.type] = []));
if (resource.namespace != null) {
obj = helper.ensureArray(obj, resource.namespace);
}
obj[resource.index] = resource.obj;
break;
}
}
if (this.includes != null && this.includes.length) {
for (var i = 0; i < this.includes.length; i++) if (this.includes[i].state != 4) return;
}
if (this.waits && this.waits.length) {
var data = this.waits.shift();
this.include = this._include;
for (var i = 0; i < data.length; i++) this.include(data[i].type, data[i].pckg);
return;
}
this.readystatechanged((this.state = 4));
},
onload: function(url, response) {
if (!response) {
console.warn('Resource cannt be loaded', this.url);
this.readystatechanged(4);
return;
}
switch (this.type) {
case 'load':
case 'ajax':
this.obj = response;
break;
case 'lazy':
LazyModule.create(this.xpath, response);
break;
case 'js':
this.parsing = true;
try {
__includeEval(response, this);
} catch (error) {
error.url = this.url;
helper.reportError(error);
}
break;
};
this.parsing = false;
this.resourceLoaded(null);
}
});
var LazyModule = {
create: function(xpath, code) {
var arr = xpath.split('.'),
obj = window,
module = arr[arr.length - 1];
while (arr.length > 1) {
var prop = arr.shift();
obj = obj[prop] || (obj[prop] = {});
}
arr = null;
obj.__defineGetter__(module, function() {
delete obj[module];
try {
var r = __includeEval(code, window.include);
if (r != null && r instanceof Resource == false) obj[module] = r;
} catch (error) {
error.xpath = xpath;
helper.reportError(e);
} finally {
code = null;
xpath = null;
return obj[module];
}
});
}
}
w.include = new Include();
w.include.helper = helper;
w.IncludeResource = Resource;
}(window, window.document);
window.__includeEval = function(source, include) {
return eval(source);
}