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