/** * eGroupWare eTemplate2 - JS code for implementing inheritance * * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @package etemplate * @subpackage api * @link http://www.egroupware.org * @author Andreas Stöckel * @copyright Stylite 2011 * @version $Id$ */ "use strict"; /*egw:uses et2_core_common; */ /** * Usage of the JS inheritance system * ---------------------------------- * * To create a class write * * MyClass = Class.extend([interfaces, ] functions); * * where "interfaces" is a single interface or an array of interfaces and * functions an object containing the functions the class implements. * * An interface has to be created in the following way: * * var IBreathingObject = new Interface({ * breath: function() {} * }); * * var Human = Class.extend(IBreathingObject, { * walk: function() { * console.log("Walking"); * }, * speak: function(_words) { * console.log(_words); * } * }); * * As "Human" does not implement the function "breath", "Human" is treated as * abstract. Trying to create an instance of "Human" will throw an exception. * However * * Human.prototype.implements(IBreathingObject); * * will return true. Lets create a specific class of "Human": * * var ChuckNorris = Human.extend({ * breath: function() { * console.log("Chuck Norris does not breath, he holds air hostage."); * }, * speak: function(_words) { * console.warn("Chuck Norris says:"); * this._super(_words); * } * }); */ // The following code is mostly taken from // http://ejohn.org/blog/simple-javascript-inheritance/ // some parts were slightly changed for better understanding. Added possiblity // to use interfaces. /* Simple JavaScript Inheritance * By John Resig http://ejohn.org/ * MIT Licensed */ // Inspired by base2 and Prototype (function(){ var initializing = false; /** * Turn this to "true" to track creation and destruction of elements */ var getMem_freeMem_trace = false; var tracedObjects = {}; // Check whether "function decompilation" works - fnTest is normally used to // check whether a var fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/; // Base "Class" for interfaces - needed to check whether an object is an // interface this.Interface = function(fncts) { for (var key in fncts) { this[key] = fncts[key]; } }; /** * The addInterfaceFunctions function adds all interface functions the class has * to implement to the class prototype. */ function addInterfaceFunctions(prototype, interfaces) { // Remember all interface functions in the prototype var ifaces = ((typeof prototype["_ifacefuncs"] == "undefined") ? [] : prototype["_ifacefuncs"]); prototype["_ifacefuncs"] = []; for (var i = 0; i < interfaces.length; i++) { var iface = interfaces[i]; if (iface instanceof Interface) { for (var key in iface) { prototype["_ifacefuncs"].push(key); } } else { throw("Interfaces must be instance of Interface!"); } } for (var i = 0; i < ifaces.length; i++) { prototype["_ifacefuncs"].push(ifaces[i]); } }; function addAttributeFunctions(prototype, _super) { function _copyMerge(_new, _old) { var result = {}; // Copy the new object if (typeof _new != "undefined") { for (var key in _new) { result[key] = _new[key]; } } // Merge the old object for (var key in _old) { if (typeof result[key] == "undefined") { result[key] = _old[key]; } } return result; } var attributes = {}; // Copy the old attributes for (var key in prototype.attributes) { attributes[key] = _copyMerge({}, prototype.attributes[key]); } // Add the old attributes to the new ones. If the attributes already // exist, they are merged. for (var key in _super.attributes) { var _old = _super.attributes[key]; var _new = {}; attributes[key] = _copyMerge(attributes[key], _old); } // Validate the attributes for (var key in attributes) { et2_validateAttrib(key, attributes[key]); } prototype.attributes = attributes; }; function classExtend(interfaces, prop) { if (typeof prop == "undefined") { prop = interfaces; interfaces = []; } // If a single interface is given, encapsulate it in an array if (!(interfaces instanceof Array)) { interfaces = [interfaces]; } if (typeof prop.attributes == "undefined") { prop.attributes = {}; } var _super = this.prototype; // Instantiate a base class (but only create the instance, // don't run the init constructor) initializing = true; var prototype = new this(); initializing = false; // Copy the properties over onto the new prototype for (var name in prop) { // Check if we're overwriting an existing function and check whether // the function actually uses "_super" - the RegExp test function // silently converts the funciton prop[name] to a string. if (typeof prop[name] == "function" && typeof _super[name] == "function" && fnTest.test(prop[name])) { prototype[name] = (function(name, fn){ return function() { var tmp = this._super; // Add a new ._super() method that is the same method // but on the super-class this._super = _super[name]; // The method only need to be bound temporarily, so we // remove it when we're done executing var ret = fn.apply(this, arguments); this._super = tmp; return ret; }; })(name, prop[name]); } else { prototype[name] = prop[name]; } } // Add the interface functions and the "implements" function to the // prototype addInterfaceFunctions(prototype, interfaces); // Merge the attributes and create the functions corresponding to the // attributes addAttributeFunctions(prototype, _super); // The dummy class constructor function Class() { // All construction is actually done in the init method if (!initializing) { // Check whether the object implements all interface functions for (var i = 0; i < this._ifacefuncs.length; i++) { var func = this._ifacefuncs[i]; if (!(typeof this[func] == "function")) { throw("Trying to create abstract object, interface " + "function '" + func + "' not implemented."); } } // Do some tracing of the getMem_freeMem_trace is activated if (getMem_freeMem_trace) { this.__OBJ_UID = "obj_" + et2_uniqueId(); var className = this.className(); tracedObjects[this.__OBJ_UID] = { "created": new Date().getTime(), "class": className } et2_debug("log", "*" + this.__OBJ_UID + " (" + className + ")"); } if (this.init) { this.init.apply(this, arguments); } } } // Populate our constructed prototype object Class.prototype = prototype; // Enforce the constructor to be what we expect Class.prototype.constructor = Class; // And make this class extendable Class.extend = classExtend; return Class; }; // The base Class implementation (does nothing) this.Class = function(){}; // Create a new Class that inherits from this class. The first parameter // is an array which defines a set of interfaces the object has to // implement. An interface is simply an object with named functions. Class.extend = classExtend; // The base class has no attributes Class.prototype.attributes = {}; // Add the basic functions /** * Destructor function - it calls "destroy" if it has been defined and then * deletes all keys of this element, so that any access to this element will * eventually throw an exception, making it easier to hunt down memory leaks. */ Class.prototype.free = function() { if (this.destroy) { this.destroy(); } // Trace the freeing of the object if (getMem_freeMem_trace) { delete(tracedObjects[this.__OBJ_UID]); et2_debug("log", "-" + this.__OBJ_UID); } // Delete every object entry for (var key in this) { delete(this[key]); } // Don't raise an exception when attempting to free an element multiple // times. this.free = function() {}; }; // Some debug functions for memory leak hunting if (getMem_freeMem_trace) { /** * Prints a list of all objects UIDs which have not been freed yet. */ Class.prototype.showTrace = function() { console.log(tracedObjects); }, /** * VERY slow - for debugging only! */ Class.prototype.className = function() { for (var key in window) { if (key.substr(0, 3) == "et2" && this.constructor == window[key]) { return key; } } return "?"; } } /** * Returns the value of the given attribute. If the property does not * exist, an error message is issued. */ Class.prototype.getAttribute = function(_name) { if (typeof this.attributes[_name] != "undefined" && !this.attributes[_name].ignore) { if (typeof this["get_" + _name] == "function") { return this["get_" + _name](); } else { return this[_name]; } } else { et2_debug("error", this, "Attribute '" + _name + "' does not exist!"); } }; /** * The setAttribute function sets the attribute with the given name to * the given value. _override defines, whether this[_name] will be set, * if this key already exists. _override defaults to true. A warning * is issued if the attribute does not exist. */ Class.prototype.setAttribute = function(_name, _value, _override) { if (typeof this.attributes[_name] != "undefined") { if (!this.attributes[_name].ignore) { if (typeof _override == "undefined") { _override = true; } var val = et2_checkType(_value, this.attributes[_name].type, _name); if (typeof this["set_" + _name] == "function") { this["set_" + _name](val); } else if (_override || typeof this[_name] == "undefined") { this[_name] = val; } } } else { et2_debug("warn", this, "Attribute '" + _name + "' does not exist!"); } }; /** * generateAttributeSet sanitizes the given associative array of attributes * (by passing each entry to "et2_checkType" and checking for existance of * the attribute) and adds the default values to the associative array. * * @param _attrs is the associative array containing the attributes. */ Class.prototype.generateAttributeSet = function(_attrs) { // Sanity check and validation for (var key in _attrs) { if (typeof this.attributes[key] != "undefined") { if (!this.attributes[key].ignore) { _attrs[key] = et2_checkType(_attrs[key], this.attributes[key].type, key); } } else { // Key does not exist - delete it and issue a warning delete(_attrs[key]); et2_debug("warn", this, "Attribute '" + key + "' does not exist!"); } } // Include default values or already set values for this attribute for (var key in this.attributes) { if (typeof _attrs[key] == "undefined") { var _default = this.attributes[key]["default"]; if (_default == et2_no_init) { _default = undefined; } _attrs[key] = _default; } } return _attrs; }; /** * The initAttributes function sets the attributes to their default * values. The attributes are not overwritten, which means, that the * default is only set, if either a setter exists or this[propName] does * not exist yet. */ Class.prototype.initAttributes = function(_attrs) { for (var key in _attrs) { if (typeof this.attributes[key] != "undefined" && !this.attributes[key].ignore && !(_attrs[key] == undefined)) { this.setAttribute(key, _attrs[key], false); } } }; /** * The implements function can be used to check whether the object * implements the given interface. */ Class.prototype.implements = function(_iface) { for (var key in _iface) { if (this._ifacefuncs.indexOf(key) < 0) { return false; } } return true; }; /** * The instanceOf function can be used to check for both - classes and * interfaces. Please don't change the case of this function as this * affects IE and Opera support. */ Class.prototype.instanceOf = function(_obj) { if (_obj instanceof Interface) { return this.implements(_obj); } else { return this instanceof _obj; } }; }).call(window);