blob: a1ae19775f1661e8f7143eb222a8df2867e7c64c [file] [log] [blame]
/*******************************************************************************
* 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() {}
})
}
});