esf-core
Version:
EsFramework Javascript core. Contains simple classloader, heavily inspired by ExtJs
592 lines (513 loc) • 21.5 kB
JavaScript
(function () {
var global = this,
//Core class creation functionality
core = {
/**
* Create class (not instance)
* @param source
* @return
*/
create: function (source) {
var newClass = this.getConstructor(source);
core.override(newClass, source);
return newClass;
},
/**
* Extend class
* @param superClass
* @param source
*/
extend : function(superClass, source) {
var newClass = this.getConstructor(
source,
superClass.prototype.constructor
);
var superClone = function() {};
superClone.prototype = superClass.prototype;
newClass.prototype = new superClone();
newClass.prototype.constructor = source.hasOwnProperty("constructor") ? source.constructor : function () {};
// Save reference to superclass
newClass.prototype.superClass = superClass;
// Add callparent method
newClass.prototype.callParent = function () {
// Search which child class is calling this method
var superClass = newClass;
while (superClass.prototype[this.callParent.caller.$name] !== this.callParent.caller) {
superClass = superClass.prototype.superClass;
}
superClass = superClass.prototype.superClass;
// <debug.error>
if (typeof(superClass) == "undefined") {
var callingClass = newClass;
while (callingClass.prototype[this.callParent.caller.$name] !== this.callParent.caller) {
callingClass = callingClass.prototype.superClass;
}
throw "Parent method ("
+ this.callParent.caller.$name+") cant be called from "
+ callingClass.prototype.$name + " because it hasn't a mother class";
}
if (typeof(superClass.prototype[this.callParent.caller.$name]) == "undefined")
throw 'Parent method ('
+ this.callParent.caller.$name+') is not defined in ' + this.$name;
// <debug.error>
/*if (this.callParent.caller.$name == "constructor") {
return superClass.apply(this, arguments);
}*/
return superClass.prototype[this.callParent.caller.$name].apply(this, arguments);
};
if (source)
core.override(newClass, source);
return newClass;
},
/**
* Get the constructor of a class
* @param source object that describes the class
* @param defaultConstructor normally the constructor of the superclass that
* will be used if the source doesn't have one
* @return {Function}
*/
getConstructor: function (source, defaultConstructor) {
return function() {
// Clone instance attributes
for (var property in this) {
if (typeof(this[property]) == 'object') {
this[property] = apply(true, source[property] instanceof Array ? [] : {}, this[property]);
}
}
if (source.hasOwnProperty("constructor"))
return source.constructor.apply(this, arguments);
else if (typeof(defaultConstructor) != "undefined") {
return defaultConstructor.apply(this, arguments);
}
return this;
};
},
/**
* Add attributes from source to prototype of scope
* the scope of this function should be the newly created class
* @private
* @param source
*/
override: function(target, source) {
// <debug.error>
if ( typeof (source) != "object" || !source)
throw new TypeError("Object needed as source.");
// </debug.error>
for (var property in source) {
if (source.hasOwnProperty(property)) {
// Add information about name to method
if (typeof source[property] == 'function') {
source[property].$name = property;
}
target.prototype[property] = source[property];
}
}
}
},
/*
* excerpt from jquery source code (see http://code.jquery.com/jquery-latest.js)
*/
/**
* Copyright 2012 jQuery Foundation and other contributors
* http://jquery.com/
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
apply = function () {
// TODO: make faster and (re)move some vars
var options, name, src, copy, copyIsArray, clone, target = arguments[0] || {},
i = 1,
length = arguments.length,
deep = false,
toString = Object.prototype.toString,
hasOwn = Object.prototype.hasOwnProperty,
push = Array.prototype.push,
slice = Array.prototype.slice,
trim = String.prototype.trim,
indexOf = Array.prototype.indexOf,
class2type = {
"[object Boolean]": "boolean",
"[object Number]": "number",
"[object String]": "string",
"[object Function]": "function",
"[object Array]": "array",
"[object Date]": "date",
"[object RegExp]": "regexp",
"[object Object]": "object"
},
jQuery = {
isFunction: function (obj) {
return jQuery.type(obj) === "function"
},
isArray: Array.isArray ||
function (obj) {
return jQuery.type(obj) === "array"
},
isWindow: function (obj) {
return obj != null && obj == obj.window
},
isNumeric: function (obj) {
return !isNaN(parseFloat(obj)) && isFinite(obj)
},
type: function (obj) {
return obj == null ? String(obj) : class2type[toString.call(obj)] || "object"
},
isPlainObject: function (obj) {
if (!obj || jQuery.type(obj) !== "object" || obj.nodeType) {
return false
}
try {
if (obj.constructor && !hasOwn.call(obj, "constructor") && !hasOwn.call(obj.constructor.prototype, "isPrototypeOf")) {
return false
}
} catch (e) {
return false
}
var key;
for (key in obj) {}
return key === undefined || hasOwn.call(obj, key)
}
};
if (typeof target === "boolean") {
deep = target;
target = arguments[1] || {};
i = 2;
}
if (typeof target !== "object" && !jQuery.isFunction(target)) {
target = {}
}
if (length === i) {
target = this;
--i;
}
for (i; i < length; i++) {
if ((options = arguments[i]) != null) {
for (name in options) {
src = target[name];
copy = options[name];
if (target === copy) {
continue
}
if (deep && copy && (jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)))) {
if (copyIsArray) {
copyIsArray = false;
clone = src && jQuery.isArray(src) ? src : []
} else {
clone = src && jQuery.isPlainObject(src) ? src : {};
}
// WARNING: RECURSION
target[name] = apply(deep, clone, copy);
} else if (copy !== undefined) {
target[name] = copy;
}
}
}
}
return target;
};
/**
* @singleton
*/
module.exports = new (core.create({
/**
* Instances of class definitions
* @private
*/
classes: {},
/**
* Class constructors for different number of args
* @private
*/
instantiators: [],
/**
* Tagged classes
*/
tags: {},
/**
* Singletons
*/
singletons: {},
/**
* Tag filter
*/
tagFilter: {},
/**
* Namespace of the object
*/
_ns: 'Esf',
/**
* Create instance of class
* @param name can be either class or name of a class
* @return {*}
*/
create: function (name) {
// Create instantiator
if (!this.instantiators[arguments.length]) {
var a = [];
for (i = 1; i < arguments.length; i++) {
a.push('a[' + i + ']');
}
this.instantiators[arguments.length] = new Function('cls', 'a', 'return new cls('+a.join(',')+')');
}
// <debug.error>
if (typeof(name) != "function") {
if (!(this.classes[name] || this.getClass(name)))
throw "Can not create undefined class " + name;
if (typeof(this.classes[name].loaded) != "undefined" || this.classes[name].loaded == false)
throw "Can not create class with unloaded dependencies";
}
// </debug.error>
return this.instantiators[arguments.length](typeof(name) == "function" ? name :
(this.classes[name] || this.getClass(name)), arguments);
},
/**
* Define class
* @param name
*/
define: function (name, source, options) {
// Parse args
if (typeof(name) == "object") { // Map args if private class
options = source;
source = name;
name = false; // name arg is boolean false if the definition is a private class
}
if (typeof(options) == "undefined")
options = {};
if (typeof(source) == "function")
source = source();
// Save importand options to keep code clean
var singleton = typeof(options.singleton) !="undefined" && options.singleton==true,
extend = typeof(options.extend) == "string",
cls,
self = this,
args = arguments;
// Create class
// TODO: add multiple class inheritance
if (extend) {
if (!this.getClass(options.extend)) {
// <debug>
if (!this.Loader || !(this.Loader.hasClass(options.extend) && name)) { // Can not load private class asynchronous
throw new Error(
"Superclass `" + options.extend + "` of " + (name || ('private class')) + " not found"
);
}
// <debug>
// Load class asynchronous
this.Loader.require(options.extend, function () {
// Try loading mother class asynchronous
// Class will be available if all dependencies were fetched
self.define.apply(self, args);
});
}
cls = core.extend(this.getClass(options.extend), source);
} else {
cls = core.create(source);
}
// Save class name in prototype of class
if (name) {
cls.prototype.$name = name;
}
// Save that class is a singleton
if (singleton) {
cls.prototype.$singleton = true;
}
// Save that class extends herself (more specifically the already defined instance)
//<debug>
if (name && options.extend == name) {
cls.prototype.$overridden =
typeof(cls.prototype.$overridden=="undefined") ? 0
: ++cls.prototype.$overridden;
}
//<debug>
// Load deps if given
if (typeof(options.deps) != "undefined") {
// <debug.error>
if (!this.Loader)
throw "Can not define class with deps without initialized loader";
// </debug.error>
cls.prototype.deps = {};
this.Loader.npm.require(options.deps, cls.prototype.deps);
}
// Create instance if singleton
if (singleton) {
// <debug.error>
if (!name)
throw new Error("Can not create singleton for private class");
// <debug.error>
if (this.loader) {
this.loader.on('load', function (n) {
if (n==name) {
self.singletons[name] = this.create(cls);
return false; // Stop searching
}
});
} else {
this.singletons[name] = this.create(cls);
}
// TODO: add debug function to notify about timeouts
}
// Map to global namespace or self
if (name) {
var split = name.split('.'),
ns = global;
// If class is in the namespace of the core map it there instead of global
if (split[0] == this._ns) {
split.shift();
ns = this;
}
// TODO: Only map to global namespace if configured
for (var i = 0; i<split.length-1; i++) {
if (typeof(ns[split[i]]) == "undefined")
ns[split[i]] = {};
ns = ns[split[i]];
}
ns[split[split.length-1]] = singleton ? this.singletons[name] : cls;
}
// Save tags if given
if (typeof(options.tags) != "undefined") {
for (var k in options.tags) {
var tagName;
// Parse format of the tag
if (typeof(options.tags[k]) == "object") {
tagName = options.tags[k].name;
var hash = require('es-hash')(options.tags[k]);
// Create array for filter
if (typeof(this.tagFilter[hash]) == "undefined")
this.tagFilter[hash] = [];
// Save filter
this.tagFilter[hash].push(
singleton ? this.singletons[name] : cls
);
} else {
tagName = options.tags[k];
}
// Create array for name
if (typeof(this.tags[tagName]) == "undefined")
this.tags[tagName] = [];
// Save tag
this.tags[tagName].push(
singleton ? this.singletons[name] : cls
);
}
}
// Save defined class
if (name) {
this.classes[name] = cls;
}
// Return
if (!name) {
return cls; // private class definition, no method chainging return prototype instead
}
return this;
},
/**
* Apply given object attributes to class (mostly singleton rather than prototype)
* @param deep {Boolean} optional
* @param {String} target
* @param {Object} object1
*/
apply: function () {
return apply.apply(this, arguments);
},
/**
* Find tagged classes
* @param tagName
* @return {Array}
*/
findTaggedClasses: function (tagName, filter) {
if (typeof(filter) != "undefined") {
filter.name = tagName;
var hash = require('es-hash')(filter);
return typeof(this.tagFilter[hash]) == "undefined" ? [] : this.tagFilter[hash];
}
return typeof(this.tags[tagName]) == "undefined" ? [] : this.tags[tagName];
},
/**
* Alias class method
* @param cls
* @param method
* @return {Function}
*/
alias: function (cls, method) {
if (typeof(cls) == "string") {
cls = this.getSingleton(cls);
if (!cls)
throw "Class (" + cls + ") not found";
}
return function () {
cls[method].apply(cls, arguments);
}
},
/**
* Returns wether class for given name is a singleton
* @param name
* @return {*}
*/
isSingleton: function (name) {
var cls = this.getClass(name);
return cls && cls.prototype.singleton;
},
/**
* Get singelton
* @param name
* @return {Boolean}
*/
getSingleton: function (name) {
return typeof(this.singletons[name]) == "undefined" ? false : this.singletons[name];
},
/**
* Get class prototype (not instance)
* @param name
* @return {*}
*/
getClass: function (name) {
// Esf internal classes
if (typeof(this.classes[name]) != "undefined")
return this.classes[name];
// Search in global ns
var split = name.split('.'),
ns = global;
for (var i = 0; i<split.length-1; i++) {
if (typeof(ns[split[i]]) == "undefined") {
ns = {};
break;
}
ns = ns[split[i]];
}
if (typeof(ns[split[split.length-1]]) == "function") {
return this.classes[name] = ns[split[split.length-1]]; // from global ns
}
// Load class synchronous if defined in autoload
// <debug>
if (this.Loader && this.Loader.isAutoloaded(name)) {
this.Loader.require(name);
if (typeof(this.classes[name]) != "undefined")
return this.classes[name];
}
// /<debug>
// No class found
return false;
},
Loader: false,
globalize: function () {
global.Esf = this;
}
}));
})();