UNPKG

@qooxdoo/framework

Version:

The JS Framework for Coders

551 lines (485 loc) 16.8 kB
/* ************************************************************************ qooxdoo - the new era of web development http://qooxdoo.org Copyright: 2004-2008 1&1 Internet AG, Germany, http://www.1und1.de License: MIT: https://opensource.org/licenses/MIT See the LICENSE file in the project's top-level directory for details. Authors: * Sebastian Werner (wpbasti) * Andreas Ecker (ecker) ************************************************************************ */ /** * 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. * * @require(qx.lang.normalize.Array) */ qx.Bootstrap.define("qx.Mixin", { statics: { /* --------------------------------------------------------------------------- PUBLIC API --------------------------------------------------------------------------- */ /** * Define a new mixin. * * Example: * <pre class='javascript'> * qx.Mixin.define("name", * { * include: [SuperMixins], * * properties: { * tabIndex: {type: "number", init: -1} * }, * * members: * { * prop1: "foo", * meth1: function() {}, * meth2: function() {} * } * }); * </pre> * * @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 * accessible 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. For a description of the format of a property definition see * {@link qx.core.Property}.</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> * * @return {qx.Mixin} The configured mixin */ define(name, config) { if (config) { // Normalize include if ( config.include && !(qx.Bootstrap.getClass(config.include) === "Array") ) { config.include = [config.include]; } // Validate incoming data if (qx.core.Environment.get("qx.debug")) { this.__validateConfig(name, config); } // Create Interface from statics var mixin = config.statics ? config.statics : {}; qx.Bootstrap.setDisplayNames(mixin, name); for (var key in mixin) { if (mixin[key] instanceof Function) { mixin[key].$$mixin = mixin; } } // Attach configuration if (config.construct) { mixin.$$constructor = config.construct; qx.Bootstrap.setDisplayName(config.construct, name, "constructor"); } if (config.include) { mixin.$$includes = config.include; } if (config.properties) { mixin.$$properties = config.properties; } if (config.members) { mixin.$$members = config.members; qx.Bootstrap.setDisplayNames(config.members, name + ".prototype"); } 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.objects) { mixin.$$objects = config.objects; } if (config.destruct) { mixin.$$destructor = config.destruct; qx.Bootstrap.setDisplayName(config.destruct, name, "destruct"); } } else { var mixin = {}; } // Add basics mixin.$$type = "Mixin"; mixin.name = name; // Attach toString mixin.toString = this.genericToString; // Assign to namespace mixin.basename = qx.Bootstrap.createNamespace(name, mixin); // Store class reference in global mixin registry this.$$registry[name] = mixin; // Return final mixin return mixin; }, /** * Check compatibility between mixins (including their includes) * * @param mixins {Mixin[]} an array of mixins * @throws {Error} when there is a conflict between the mixins * @return {Boolean} <code>true</code> if the mixin passed the compatibility check */ checkCompatibility(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 {Error} when the given mixin is incompatible to the class * @return {Boolean} true if the mixin is compatible to the given class */ isCompatible(mixin, clazz) { var list = qx.util.OOUtil.getMixins(clazz); list.push(mixin); return qx.Mixin.checkCompatibility(list); }, /** * Returns a mixin by name * * @param name {String} class name to resolve * @return {Class} the class */ getByName(name) { return this.$$registry[name]; }, /** * Determine if mixin exists * * @param name {String} mixin name to check * @return {Boolean} true if mixin exists */ isDefined(name) { return this.getByName(name) !== undefined; }, /** * Determine the number of mixins which are defined * * @return {Number} the number of mixins */ getTotalNumber() { return qx.Bootstrap.objectGetLength(this.$$registry); }, /** * Generates a list of all mixins given plus all the * mixins these includes plus... (deep) * * @param mixins {Mixin[] ? []} List of mixins * @return {Array} List of all mixins */ flatten(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)); } } return list; }, /** * This method is used to determine the base method to call at runtime, and is used * by Mixins where the mixin method calls `this.base()`. It is only required by the * compiler, and not the generator. * * The problem is that while Mixin's cannot override the same methods in a single class, * they can override methods that were implemented in a base base - but the compiler * cannot emit compile-time code which knows the base class method because that depends * on the class that the mixin is mixed-into. * * This method will search the hierarchy of the class at runtime, and figure out the * nearest superclass method to call; the result is cached, and it is acceptable for * a mixin's method to override a method mixed into a superclass. * * Technically, this method should be private - it is internal and no notification will * be given if the API changes. However, because it needs to be called by generated code * in any class, it has to appear as public. Do not use it directly. * * @param clazz {Class} the class that is to be examined * @param mixin {Mixin} the mixin that is calling `this.base` * @param methodName {String} the name of the method in `mixin` that is calling `this.base` * @return {Function} the base class function to call */ baseClassMethod(clazz, mixin, methodName) { if (!qx.core.Environment.get("qx.compiler")) { qx.log.Logger.error( "qx.Mixin.baseClassMethod should not be used except with code compiled by the compiler (ie NOT the generator / python toolchain)" ); } else { if ( clazz.$$mixinBaseClassMethods && clazz.$$mixinBaseClassMethods[mixin.name] !== undefined && clazz.$$mixinBaseClassMethods[mixin.name][methodName] !== undefined ) { return clazz.$$mixinBaseClassMethods[mixin.name][methodName]; } // Find the class which added the mixin; if it is mixed in twice, we pick the super-most class var mixedInAt = null; var mixedInIndex = -1; for ( var searchClass = clazz; searchClass; searchClass = searchClass.superclass ) { if (searchClass.$$flatIncludes) { var pos = searchClass.$$flatIncludes.indexOf(mixin); if (pos > -1) { mixedInAt = searchClass; mixedInIndex = pos; } } } var fn = null; if (mixedInAt) { // Multiple mixins can provide an implementation, in which case the mixin which was // added second's "base" implementation is the first mixin's method for (var i = mixedInIndex - 1; i > -1; i--) { var peerMixin = mixedInAt.$$flatIncludes[i]; if (peerMixin.$$members[methodName]) { fn = peerMixin.$$members[methodName]; break; } } // Try looking in the class itself if (!fn && mixedInAt.prototype[methodName]) { fn = mixedInAt.prototype[methodName]; for (let i = 0; i < mixedInAt.$$flatIncludes.length; i++) { if (!mixedInAt.$$flatIncludes[i].$$members[methodName]) { continue; } fn = fn.base; } } // Try looking in the superclass if (!fn && mixedInAt.superclass) { fn = mixedInAt.superclass.prototype[methodName]; } } // Cache the result if (fn) { if (!clazz.$$mixinBaseClassMethods) { clazz.$$mixinBaseClassMethods = {}; } if (!clazz.$$mixinBaseClassMethods[mixin.name]) { clazz.$$mixinBaseClassMethods[mixin.name] = {}; } clazz.$$mixinBaseClassMethods[mixin.name][methodName] = fn; } else if (qx.core.Environment.get("qx.debug")) { throw new Error( "Cannot find base class method called " + methodName + " for mixin " + mixin.name + ", when viewed from " + clazz.classname ); } return fn; } }, /* --------------------------------------------------------------------------- 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() { return "[Mixin " + this.name + "]"; }, /** Registers all defined mixins */ $$registry: {}, /** @type {Map} allowed keys in mixin definition */ __allowedKeys: qx.core.Environment.select("qx.debug", { true: { include: "object", // Mixin | Mixin[] statics: "object", // Map members: "object", // Map properties: "object", // Map events: "object", // Map destruct: "function", // Function construct: "function", // Function objects: "object" // Map }, default: null }), /** * Validates incoming configuration and checks keys and values * * @signature function(name, config) * @param name {String} The name of the class * @param config {Map} Configuration map */ __validateConfig: qx.core.Environment.select("qx.debug", { true(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 && (["Array", "RegExp", "Date"].indexOf( qx.Bootstrap.getClass(config[key]) ) != -1 || 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(name, config) {} }) } });