| /******************************************************************************* |
| * Copyright: 2004, 2012 1&1 Internet AG, Germany, http://www.1und1.de, |
| * and EclipseSource |
| * |
| * This program and the accompanying materials are made available under the |
| * terms of the Eclipse Public License v1.0 which accompanies this |
| * distribution, and is available at http://www.eclipse.org/legal/epl-v10.html |
| * |
| * Contributors: |
| * 1&1 Internet AG and others - original API and implementation |
| * EclipseSource - adaptation for the Eclipse Rich Ajax Platform |
| ******************************************************************************/ |
| |
| /** |
| * This class is used to define mixins (similar to mixins in Ruby). |
| * |
| * Mixins are collections of code and variables, which can be merged into |
| * other classes. They are similar to classes but don't support inheritance. |
| * |
| * See the description of the {@link #define} method how a mixin is defined. |
| */ |
| qx.Class.define("qx.Mixin", |
| { |
| statics : |
| { |
| /* |
| --------------------------------------------------------------------------- |
| PUBLIC API |
| --------------------------------------------------------------------------- |
| */ |
| |
| /** |
| * Define a new mixin. |
| * |
| * Example: |
| * <pre class='javascript'> |
| * qx.Mixin.define("name", |
| * { |
| * includes: [SuperMixins], |
| * |
| * properties: { |
| * tabIndex: {type: "number", init: -1} |
| * }, |
| * |
| * members: |
| * { |
| * prop1: "foo", |
| * meth1: function() {}, |
| * meth2: function() {} |
| * } |
| * }); |
| * </pre> |
| * |
| * @type static |
| * @param name {String} name of the mixin |
| * @param config {Map ? null} Mixin definition structure. The configuration map has the following keys: |
| * <table> |
| * <tr><th>Name</th><th>Type</th><th>Description</th></tr> |
| * <tr><th>construct</th><td>Function</td><td>An optional mixin constructor. It is called on instantiation each |
| * class including this mixin. The constructor takes no parameters.</td></tr> |
| * <tr><th>destruct</th><td>Function</td><td>An optional mixin destructor.</td></tr> |
| * <tr><th>include</th><td>Mixin[]</td><td>Array of mixins, which will be merged into the mixin.</td></tr> |
| * <tr><th>statics</th><td>Map</td><td> |
| * Map of statics of the mixin. The statics will not get copied into the target class. They remain |
| * acceccible from the mixin. This is the same behaviour as statics in interfaces ({@link qx.Interface#define}). |
| * </td></tr> |
| * <tr><th>members</th><td>Map</td><td>Map of members of the mixin.</td></tr> |
| * <tr><th>properties</th><td>Map</td><td>Map of property definitions. Format of the map: TODOC</td></tr> |
| * <tr><th>events</th><td>Map</td><td> |
| * Map of events the mixin fires. The keys are the names of the events and the values are |
| * corresponding event type classes. |
| * </td></tr> |
| * </table> |
| */ |
| define : function(name, config) |
| { |
| if (config) |
| { |
| // Normalize include |
| if (config.include && !(config.include instanceof Array)) { |
| config.include = [config.include]; |
| } |
| |
| // Create Interface from statics |
| var mixin = config.statics ? config.statics : {}; |
| for(var key in mixin) { |
| mixin[key].mixin = mixin; |
| } |
| |
| // Attach configuration |
| if (config.construct) { |
| mixin.$$constructor = config.construct; |
| } |
| |
| if (config.include) { |
| mixin.$$includes = config.include; |
| } |
| |
| if (config.properties) { |
| mixin.$$properties = config.properties; |
| } |
| |
| if (config.members) { |
| mixin.$$members = config.members; |
| } |
| |
| for(var key in mixin.$$members) |
| { |
| if (mixin.$$members[key] instanceof Function) { |
| mixin.$$members[key].mixin = mixin; |
| } |
| } |
| |
| if (config.events) { |
| mixin.$$events = config.events; |
| } |
| |
| if (config.destruct) { |
| mixin.$$destructor = config.destruct; |
| } |
| } |
| else |
| { |
| var mixin = {}; |
| } |
| |
| // Add basics |
| mixin.$$type = "Mixin"; |
| mixin.name = name; |
| |
| // Attach toString |
| mixin.toString = this.genericToString; |
| |
| // Assign to namespace |
| mixin.basename = qx.Class.createNamespace(name, mixin); |
| |
| // Store class reference in global mixin registry |
| this.__registry[name] = mixin; |
| |
| // Return final mixin |
| return mixin; |
| }, |
| |
| |
| /** |
| * Check compatiblity between mixins (including their includes) |
| * |
| * @param mixins {Mixin[]} an array of mixins |
| * @throws an exception when there is a conflict between the mixins |
| */ |
| checkCompatibility : function(mixins) |
| { |
| var list = this.flatten(mixins); |
| var len = list.length; |
| |
| if (len < 2) { |
| return true; |
| } |
| |
| var properties = {}; |
| var members = {}; |
| var events = {}; |
| var mixin; |
| |
| for (var i=0; i<len; i++) |
| { |
| mixin = list[i]; |
| |
| for (var key in mixin.events) |
| { |
| if(events[key]) { |
| throw new Error('Conflict between mixin "' + mixin.name + '" and "' + events[key] + '" in member "' + key + '"!'); |
| } |
| |
| events[key] = mixin.name; |
| } |
| |
| for (var key in mixin.properties) |
| { |
| if(properties[key]) { |
| throw new Error('Conflict between mixin "' + mixin.name + '" and "' + properties[key] + '" in property "' + key + '"!'); |
| } |
| |
| properties[key] = mixin.name; |
| } |
| |
| for (var key in mixin.members) |
| { |
| if(members[key]) { |
| throw new Error('Conflict between mixin "' + mixin.name + '" and "' + members[key] + '" in member "' + key + '"!'); |
| } |
| |
| members[key] = mixin.name; |
| } |
| } |
| |
| return true; |
| }, |
| |
| |
| /** |
| * Checks if a class is compatible to the given mixin (no conflicts) |
| * |
| * @param mixin {Mixin} mixin to check |
| * @param clazz {Class} class to check |
| * @throws an exception when the given mixin is incompatible to the class |
| * @return {Boolean} true if the mixin is compatible to the given class |
| */ |
| isCompatible : function(mixin, clazz) |
| { |
| var list = qx.Class.getMixins(clazz); |
| list.push(mixin); |
| return qx.Mixin.checkCompatibility(list); |
| }, |
| |
| |
| /** |
| * Returns a mixin by name |
| * |
| * @type static |
| * @param name {String} class name to resolve |
| * @return {Class} the class |
| */ |
| getByName : function(name) { |
| return this.__registry[name]; |
| }, |
| |
| |
| /** |
| * Determine if mixin exists |
| * |
| * @type static |
| * @name isDefined |
| * @param name {String} mixin name to check |
| * @return {Boolean} true if mixin exists |
| */ |
| isDefined : function(name) { |
| return this.getByName(name) !== undefined; |
| }, |
| |
| |
| /** |
| * Determine the number of mixins which are defined |
| * |
| * @type static |
| * @return {Number} the number of classes |
| */ |
| getTotalNumber : function() { |
| return rwt.util.Object.getLength(this.__registry); |
| }, |
| |
| |
| /** |
| * Generates a list of all mixins given plus all the |
| * mixins these includes plus... (deep) |
| * |
| * @param mixins {Mixin[] ? []} List of mixins |
| * @returns {Array} List of all mixins |
| */ |
| flatten : function(mixins) |
| { |
| if (!mixins) { |
| return []; |
| } |
| |
| // we need to create a copy and not to modify the existing array |
| var list = mixins.concat(); |
| |
| for (var i=0, l=mixins.length; i<l; i++) |
| { |
| if (mixins[i].$$includes) { |
| list.push.apply(list, this.flatten(mixins[i].$$includes)); |
| } |
| } |
| |
| // console.log("Flatten: " + mixins + " => " + list); |
| |
| return list; |
| }, |
| |
| |
| |
| |
| |
| /* |
| --------------------------------------------------------------------------- |
| PRIVATE/INTERNAL API |
| --------------------------------------------------------------------------- |
| */ |
| |
| /** |
| * This method will be attached to all mixins to return |
| * a nice identifier for them. |
| * |
| * @internal |
| * @return {String} The mixin identifier |
| */ |
| genericToString : function() { |
| return "[Mixin " + this.name + "]"; |
| }, |
| |
| |
| /** Registers all defined mixins */ |
| __registry : {}, |
| |
| |
| /** {Map} allowed keys in mixin definition */ |
| __allowedKeys : rwt.util.Variant.select("qx.debug", |
| { |
| "on": |
| { |
| "include" : "object", // Mixin | Mixin[] |
| "statics" : "object", // Map |
| "members" : "object", // Map |
| "properties" : "object", // Map |
| "events" : "object", // Map |
| "destruct" : "function", // Function |
| "construct" : "function" // Function |
| }, |
| |
| "default" : null |
| }), |
| |
| |
| /** |
| * Validates incoming configuration and checks keys and values |
| * |
| * @type static |
| * @param name {String} The name of the class |
| * @param config {Map} Configuration map |
| */ |
| __validateConfig : rwt.util.Variant.select("qx.debug", |
| { |
| "on": function(name, config) |
| { |
| // Validate keys |
| var allowed = this.__allowedKeys; |
| for (var key in config) |
| { |
| if (!allowed[key]) { |
| throw new Error('The configuration key "' + key + '" in mixin "' + name + '" is not allowed!'); |
| } |
| |
| if (config[key] == null) { |
| throw new Error('Invalid key "' + key + '" in mixin "' + name + '"! The value is undefined/null!'); |
| } |
| |
| if (allowed[key] !== null && typeof config[key] !== allowed[key]) { |
| throw new Error('Invalid type of key "' + key + '" in mixin "' + name + '"! The type of the key must be "' + allowed[key] + '"!'); |
| } |
| } |
| |
| // Validate maps |
| var maps = [ "statics", "members", "properties", "events" ]; |
| for (var i=0, l=maps.length; i<l; i++) |
| { |
| var key = maps[i]; |
| |
| if (config[key] !== undefined && (config[key] instanceof Array || config[key] instanceof RegExp || config[key] instanceof Date || config[key].classname !== undefined)) { |
| throw new Error('Invalid key "' + key + '" in mixin "' + name + '"! The value needs to be a map!'); |
| } |
| } |
| |
| // Validate includes |
| if (config.include) |
| { |
| for (var i=0, a=config.include, l=a.length; i<l; i++) |
| { |
| if (a[i] == null) { |
| throw new Error("Includes of mixins must be mixins. The include number '" + (i+1) + "' in mixin '" + name + "'is undefined/null!"); |
| } |
| |
| if (a[i].$$type !== "Mixin") { |
| throw new Error("Includes of mixins must be mixins. The include number '" + (i+1) + "' in mixin '" + name + "'is not a mixin!"); |
| } |
| } |
| |
| this.checkCompatibility(config.include); |
| } |
| }, |
| |
| "default" : function() {} |
| }) |
| } |
| }); |