1 /** 2 * @fileOverview 3 * mixing.enchant.js 4 * <p>A plugin for enchant.js which allows to mix 5 * arbitrary many {@link enchant.Class} classes together. 6 * It is also possible to add functions and properties defined 7 * as a recipe ({@link enchant.Class.MixingRecipe}) to arbitrary many classes to avoid copy and paste.</p> 8 * <p>Through this it is possible to achieve a behavior similar to multiple inheritance</p> 9 * <p>Requires:<ul> 10 * <li>enchant.js v0.6 or later.</li></ul></p> 11 * See also {@link enchant.Class.mixClasses}, {@link enchant.Class.MixingRecipe.createFromClass}, 12 * {@link enchant.Class.mixClassesFromRecipe}, {@link enchant.Class.MixingRecipe} and 13 * {@link enchant.Class.applyMixingRecipe} for an introduction. 14 * @require enchant.js v0.6+ 15 * 16 * @version 0.1 17 * @author UEI Corporation (Kevin Kratzer) 18 **/ 19 20 if (enchant !== undefined) { 21 (function() { 22 23 /** 24 * @private 25 */ 26 var decorateFunctionFactory = function(srcFunction, currentFunctionName) { 27 return function() { 28 var firstResult, secondResult; 29 firstResult = this._mixing[currentFunctionName].apply(this,arguments); 30 secondResult = srcFunction.apply(this,arguments); 31 if(secondResult) { 32 return secondResult; 33 } 34 if(firstResult) { 35 return firstResult; 36 } 37 }; 38 }; 39 40 /** 41 * @private 42 */ 43 var voidFunction = function(){}; 44 45 /** 46 * @private 47 */ 48 var multipleMixingCombinationFunctionFactory = function(oldFunc,newFunc, key) { 49 return function() { 50 var firstResult = oldFunc.apply(this,arguments); 51 var mixingStore = this._mixing[key]; 52 this._mixing[key] = voidFunction; 53 var secondResult = newFunc.apply(this,arguments); 54 this._mixing[key] = mixingStore; 55 if(secondResult) { 56 return secondResult; 57 } 58 if(firstResult) { 59 return firstResult; 60 } 61 }; 62 }; 63 64 /** 65 * @private 66 */ 67 var createFromPrototypeNonRecursive = function(decorate, override, properties, source, functionOverrideNameList, functionIgnoreNameList, propertyIgnoreNameList) { 68 for(var key in source) { 69 if(source.hasOwnProperty(key)) { 70 var descriptor = Object.getOwnPropertyDescriptor(source, key); 71 if(descriptor.value && typeof(descriptor.value) === 'function') { 72 if((!functionIgnoreNameList || functionIgnoreNameList.indexOf(key) === -1) && key !== 'constructor') { 73 if(!functionOverrideNameList || functionOverrideNameList.indexOf(key) === -1) { 74 decorate[key] = (decorateFunctionFactory(source[key],key)); 75 } else { 76 override[key] = source[key]; 77 } 78 } 79 } else { 80 if(!propertyIgnoreNameList || propertyIgnoreNameList.indexOf(key) === -1 && key !== '_mixing') { 81 properties[key] = descriptor; 82 } 83 } 84 } 85 } 86 }; 87 88 /** 89 * @private 90 */ 91 var createFromPrototype = function(decorate,override,properties,source,onlyOwnProperties, functionOverrideNameList, functionIgnoreNameList, propertyIgnoreNameList) { 92 if(!onlyOwnProperties && source instanceof Object) { 93 createFromPrototype(decorate,override,properties,Object.getPrototypeOf(source),onlyOwnProperties, functionOverrideNameList, functionIgnoreNameList, propertyIgnoreNameList); 94 } 95 createFromPrototypeNonRecursive(decorate,override,properties,source, functionOverrideNameList, functionIgnoreNameList, propertyIgnoreNameList); 96 }; 97 98 /** 99 * @private 100 */ 101 var getFunctionParams = function(methodString) { 102 if(typeof(methodString) !== 'string') { 103 methodString = methodString.toString(); 104 } 105 return methodString.substring(methodString.indexOf('(')+1,methodString.indexOf(')')).replace(/\s+/,'').split(','); 106 }; 107 108 /*Public Interface */ 109 /** 110 * @scope enchant.Class.MixingRecipe.prototype 111 */ 112 enchant.Class.MixingRecipe = enchant.Class.create({ 113 /** 114 * Creates a new MixingRecipe which is used for describing in which way functions and properties should be added during the mixing. 115 * To create a recipe from an existing class see {@link enchant.Class.MixingRecipe.createFromClass} 116 * @class This class is describing in which way the mixing will be performed on the target classes. 117 * For this purpose, MixingRecipe contains three properties: 118 * <ul><li>decorateMethods (methods which will be decorated in the target, see decorator pattern)</li> 119 * <li>overrideMethods (methods which will be overriden in the target)</li> 120 * <li>overrideProperties (properties which will be redefined in the target)</li></ul> 121 * <p>See also {@link enchant.Class.mixClasses}, {@link enchant.Class.mixClassesFromRecipe} and {@link enchant.Class.applyMixingRecipe}.</p> 122 * @param {Object} decorateMethods The methods which will be decorated in the target, see decorator pattern. To access methods which have been decorated in the class resulting from mixing the _mixing property can be used, e.g. this._mixing.myFunction.apply(this,arguments).<br>(Object containing key-value pairs, key := function name, value := function). 123 * @param {Object} overrideMethods The methods which will be overriden in the target.<br>(Object containing key-value pairs, key := function name, value := function). 124 * @param {Object} properties The properties which will be redefined in the target.<br>(Object containing key-value pairs, key := function name, value := property descriptor). 125 * @property {Object} decorateMethods The methods which will be decorated in the target, see decorator pattern. To access methods which have been decorated in the class resulting from mixing the _mixing property can be used, e.g. this._mixing.myFunction.apply(this,arguments).<br>(Object containing key-value pairs, key := function name, value := function). 126 * @property {Object} overrideMethods The methods which will be overriden in the target.<br>(Object containing key-value pairs, key := function name, value := function). 127 * @property {Object} overrideProperties The properties which will be redefined in the target.<br>(Object containing key-value pairs, key := function name, value := property descriptor). 128 * @example 129 * var recipe = new enchant.Class.MixingRecipe({ 130 * add : function(value) { 131 * this._myValue += 3*value; 132 * this._mixing.add.apply(this,arguments); 133 * }, 134 * mult : function(value) { 135 * this._myValue *= value*7; 136 * this._mixing.mult.apply(this,arguments); 137 * } 138 * },{ 139 * sub : function(value) { 140 * this._myValue -= 5*value; 141 * } 142 * },{ 143 * myProperty : { 144 * get: function() { 145 * return 3*this._myPropertyValue; 146 * }, 147 * set : function(val) { 148 * this._myPropertyValue = val; 149 * } 150 * }}); 151 * var NewClass = enchant.Class.applyMixingRecipe(Class1,recipe); 152 * @extends Object 153 * @constructs 154 */ 155 initialize : function(decorateMethods, overrideMethods, properties) { 156 this.decorateMethods = decorateMethods; 157 this.overrideMethods = overrideMethods; 158 this.overrideProperties = properties; 159 } 160 }); 161 162 /** 163 * Takes the methods and properties of the given class to create a new MixingRecipe. 164 * The default behavior is to take all functions and properties of the given class 165 * including functions and properties defined in super classes, whereas functions 166 * are set to decorate the mixing target.<br>Methods which are decorated will automatically 167 * call the soureClass method and the mixing target method (using the _mixing property) - 168 * so there is no need to handle this yourself. 169 * <p>To change the default behavior set the corresponding arguments of the function.</p> 170 * 171 * @param {Function<constructor function created with enchant.Class>} sourceClass The class which will be used to create the recipe. 172 * @param [boolean] onlyOwnProperties If set to true, the functions and properties of the super classes will be ignored. 173 * @param [Array<String>] functionOverrideNameList An array containing names of functions which should be set to override functions in the target during mixing. 174 * @param [Array<String>] functionIgnoreNameList An array containing names of functions which should be ignored when creating the recipe. 175 * @param [Array<String>] propertyIgnoreNameList An array containing names of properties which should be ignored when creating the recipe. 176 * @returns {enchant.Class.MixingRecipe} The MixingRecipe created from the definition of the sourceClass. 177 * @example 178 * var recipe = enchant.Class.MixingRecipe.createFromClass(Class2, true, 179 * ['overrideFunction1','overrideFunction2'], 180 * ['ignoreFunction1','ignoreFunction2'], 181 * ['ignoreProperty1','ignorePropterty2']); 182 * recipe.overrideMethods['additionalFunction'] = new function() { 183 * console.log('Hello, World'); 184 * } 185 * recipe.overrideProperties['newProperty'] = { 186 * get: function() { 187 * return this._newProperty; 188 * }, 189 * set : function(val) { 190 * this._newProperty = val; 191 * } 192 * } 193 * var NewClass = enchant.Class.mixClassesFromRecipe(Class1,Class2,recipe); 194 * @constructs 195 * @static 196 */ 197 enchant.Class.MixingRecipe.createFromClass = function(sourceClass, onlyOwnProperties, functionOverrideNameList, functionIgnoreNameList, propertyIgnoreNameList) { 198 var decorate = {}; 199 var override = {}; 200 var properties = {}; 201 202 var source = sourceClass.prototype; 203 createFromPrototype(decorate,override,properties,source,onlyOwnProperties, functionOverrideNameList, functionIgnoreNameList, propertyIgnoreNameList); 204 return new enchant.Class.MixingRecipe(decorate,override,properties); 205 }; 206 207 /** 208 * Uses the given MixingRecipe, applies it to the first class and returns the result - the MixingRecipe should correspond to the secondClass. 209 * A default initialize method will be added which will call the initialize functions of both classes. 210 * The signature for the default initialize method is:<br> 211 * ([firstClass constructor arg 1],...,[firstClass constructor arg n],[secondClass constructor arg1],...[secondClass constructor arg n]) 212 * <p>Both classes will not be modified.</p> See also: {@link enchant.Class.MixingRecipe} 213 * 214 * @param {Function<constructor function created with enchant.Class>} firstClass The class to which the recipe will be applied. 215 * @param {Function<constructor function created with enchant.Class>} secondClass The class which is related to the MixingRecipe, used for the default initialize function. 216 * @param {enchant.Class.MixingRecipe} recipe The recipe which is applied to the first class - should correspond to the secondClass. 217 * @param [Function] initializeMethod If provided, this function will be used to initialize the resulting class instead of the default initialize method. 218 * @returns {Function<constructor function created with enchant.Class>} initializeMethod The class which is the result of mixing both classes using the recipe. 219 * @example 220 * var MapGroup = enchant.Class.mixClasses(Map, Group,true); 221 * var map = new MapGroup(16, 16); 222 * var SpriteLabel = enchant.Class.mixClasses(Sprite, Label,true); 223 * var kumaLabel = new SpriteLabel(32,32,'Kuma'); 224 * @static 225 */ 226 enchant.Class.mixClassesFromRecipe = function(firstClass, secondClass, recipe, initializeMethod) { 227 var result = enchant.Class.applyMixingRecipe(firstClass,recipe); 228 var paramLength = getFunctionParams(firstClass.prototype.initialize).length; 229 if(typeof(initializeMethod) !== 'function') { 230 initializeMethod = function() { 231 var args = Array.prototype.slice.call(arguments); 232 secondClass.prototype.initialize.apply(this,args.slice(paramLength)); 233 firstClass.prototype.initialize.apply(this,args.slice(0,paramLength)); 234 }; 235 } 236 result.prototype.initialize = initializeMethod; 237 return result; 238 }; 239 240 241 /** 242 * Creates an MixingRecipe out of the second class, applies it to the first class and returns the result. 243 * The default behavior is to take all functions and properties of the second class, 244 * including functions and properties defined in its super classes, whereas functions 245 * are set to decorate the mixing target.<br>Methods which are decorated will automatically 246 * call the soureClass method and the mixing target method (using the _mixing property) - 247 * so there is no need to handle this yourself. 248 * <p>Furthermore, a default initialize method will be added which will 249 * call the initialize functions of both classes. The signature for the default initialize method is:<br> 250 * ([firstClass constructor arg 1],...,[firstClass constructor arg n],[secondClass constructor arg 1],...[secondClass constructor arg n])</p> 251 * <p>Both classes will not be modified.</p> See also: {@link enchant.Class.MixingRecipe} 252 * 253 * @param {Function<constructor function created with enchant.Class>} firstClass The class to which the recipe will be applied. 254 * @param {Function<constructor function created with enchant.Class>} secondClass The class from which the recipe will be created 255 * @param [boolean] useOnlyOwnPropertiesForSecondClass If set to true, the functions and properties of the super classes will be ignored during the recipe creation of the secondClass. 256 * @param [Function] initializeMethod If provided, this function will be used to initialize the resulting class instead of the default initialize method. 257 * @returns {Function<constructor function created with enchant.Class>} The class which is the result of mixing both classes. 258 * @example 259 * var MapGroup = enchant.Class.mixClasses(Map, Group,true); 260 * var map = new MapGroup(16, 16); 261 * var SpriteLabel = enchant.Class.mixClasses(Sprite, Label,true); 262 * var kumaLabel = new SpriteLabel(32,32,'Kuma'); 263 * @static 264 */ 265 enchant.Class.mixClasses = function(firstClass, secondClass, useOnlyOwnPropertiesForSecondClass, initializeMethod) { 266 return enchant.Class.mixClassesFromRecipe(firstClass,secondClass,enchant.Class.MixingRecipe.createFromClass(secondClass, useOnlyOwnPropertiesForSecondClass, [], ['initialize'], []),initializeMethod); 267 }; 268 269 /** 270 * Applies the defined MixingRecipe to the target class creating a new class definition which is then returned. 271 * The target class is not modified directly.<br>See also: {@link enchant.Class.MixingRecipe}. 272 * 273 * @param {Function<constructor function created with enchant.Class>} target The class to which the recipe will be applied. 274 * @param {enchant.Class.MixingRecipe} source The MixingRecipe which is used to add new functionality to the target. 275 * @returns {Function<constructor function created with enchant.Class>} The class which is the result of mixing the target class with the source recipe. 276 * @example 277 * var recipe = new enchant.Class.MixingRecipe({ 278 * // ... see enchant.Class.MixingRecipe 279 * },{ 280 * // ... see enchant.Class.MixingRecipe 281 * },{ 282 * // ... see enchant.Class.MixingRecipe 283 * }); 284 * var NewClass = applyMixingRecipe(Class1,recipe); 285 * @static 286 */ 287 enchant.Class.applyMixingRecipe = function(target, source) { 288 var result = enchant.Class.create(target,{}); 289 target = result.prototype; 290 for(var recipeKey in source) { 291 if(source.hasOwnProperty(recipeKey)) { 292 var currentSource = source[recipeKey]; 293 if(recipeKey === 'overrideMethods') { 294 for(var methodKey in currentSource) { 295 if(currentSource.hasOwnProperty(methodKey)) { 296 target[methodKey] = currentSource[methodKey]; 297 if(target._mixing && target._mixing[methodKey]) { 298 target._mixing[methodKey] = voidFunction; 299 } 300 } 301 } 302 } else if(recipeKey === 'overrideProperties') { 303 for(var propertyKey in currentSource) { 304 if(currentSource.hasOwnProperty(propertyKey)) { 305 Object.defineProperty(target,propertyKey,currentSource[propertyKey]); 306 } 307 } 308 } else if(recipeKey === 'decorateMethods') { 309 if(!target._mixing) { 310 target._mixing = {}; 311 } 312 for(var key in currentSource) { 313 if(currentSource.hasOwnProperty(key)) { 314 var targetHolder = target; 315 if(!target[key]) { 316 while(targetHolder instanceof Object && !targetHolder[key]) { 317 targetHolder = Object.getPrototypeOf(targetHolder); 318 } 319 } 320 if(target._mixing[key]) { 321 var newFunc = targetHolder[key]; 322 target._mixing[key] = (multipleMixingCombinationFunctionFactory(target._mixing[key],newFunc,key)); 323 } else { 324 target._mixing[key] = targetHolder[key]; 325 if(!target._mixing[key]) { 326 target._mixing[key] = voidFunction; 327 } 328 } 329 target[key] = currentSource[key]; 330 } 331 } 332 } 333 } 334 } 335 return result; 336 }; 337 })(); 338 } 339