1 /**
  2  * @fileOverview
  3  * mixing.enchant.js
  4  * <p>Ein Plugin für enchant.js mit dem es möglich ist, beliebig viele
  5  * {@link enchant.Class} Klassen zusammen zu mischen.
  6  * Um Copy & Paste zu vermeiden, ist es auch möglich Funktionen und
  7  * Properties, die in einem Rezept ({@link enchant.Class.MixingRecipe}) 
  8  * definiert sind zu beliebig vielen Klassen hinzuzufügen.</p>
  9  * <p>Durch dies ist es möglich ein Verhalten ähnlich zu multipler Vererbung zu erlangen.</p>
 10  * <p>Benötigt:<ul>
 11  * <li>enchant.js v0.6 oder höher.</li></ul></p>
 12  * Weiterführende Informationen: {@link enchant.Class.mixClasses}, {@link enchant.Class.MixingRecipe.createFromClass}, 
 13  * {@link enchant.Class.mixClassesFromRecipe}, {@link enchant.Class.MixingRecipe} und 
 14  * {@link enchant.Class.applyMixingRecipe}
 15  * @require enchant.js v0.6+
 16  *
 17  * @version 0.1
 18  * @author UEI Corporation (Kevin Kratzer)
 19  **/
 20 
 21 if (enchant !== undefined) {
 22     (function() {
 23 
 24         /**
 25          * @private
 26          */
 27         var decorateFunctionFactory = function(srcFunction, currentFunctionName) {
 28             return function() {
 29                 var firstResult, secondResult;
 30                 firstResult = this._mixing[currentFunctionName].apply(this,arguments);
 31                 secondResult = srcFunction.apply(this,arguments);
 32                 if(secondResult) {
 33                     return secondResult;
 34                 }
 35                 if(firstResult) {
 36                     return firstResult;
 37                 }
 38             };
 39         };
 40 
 41         /**
 42          * @private
 43          */
 44         var voidFunction = function(){};
 45 
 46         /**
 47          * @private
 48          */
 49         var multipleMixingCombinationFunctionFactory = function(oldFunc,newFunc, key) {
 50             return function() {
 51                 var firstResult = oldFunc.apply(this,arguments);
 52                 var mixingStore = this._mixing[key];
 53                 this._mixing[key] = voidFunction;
 54                 var secondResult = newFunc.apply(this,arguments);
 55                 this._mixing[key] = mixingStore;
 56                 if(secondResult) {
 57                     return secondResult;
 58                 }
 59                 if(firstResult) {
 60                     return firstResult;
 61                 }
 62             };
 63         };
 64 
 65         /**
 66          * @private
 67          */
 68         var createFromPrototypeNonRecursive = function(decorate, override, properties, source, functionOverrideNameList, functionIgnoreNameList, propertyIgnoreNameList) {
 69             for(var key in source) {
 70                 if(source.hasOwnProperty(key)) {
 71                     var descriptor = Object.getOwnPropertyDescriptor(source, key);
 72                     if(descriptor.value && typeof(descriptor.value) === 'function') {
 73                         if((!functionIgnoreNameList || functionIgnoreNameList.indexOf(key) === -1) && key !== 'constructor') {
 74                             if(!functionOverrideNameList || functionOverrideNameList.indexOf(key) === -1) {
 75                                 decorate[key] = (decorateFunctionFactory(source[key],key));
 76                             } else {
 77                                 override[key] = source[key];
 78                             }
 79                         }
 80                     } else {
 81                         if(!propertyIgnoreNameList || propertyIgnoreNameList.indexOf(key) === -1 && key !== '_mixing') {
 82                             properties[key] = descriptor;
 83                         }
 84                     }
 85                 }
 86             }
 87         };
 88 
 89         /**
 90          * @private
 91          */
 92         var createFromPrototype = function(decorate,override,properties,source,onlyOwnProperties, functionOverrideNameList, functionIgnoreNameList, propertyIgnoreNameList) {
 93             if(!onlyOwnProperties && source instanceof Object) {
 94                 createFromPrototype(decorate,override,properties,Object.getPrototypeOf(source),onlyOwnProperties, functionOverrideNameList, functionIgnoreNameList, propertyIgnoreNameList);
 95             }
 96             createFromPrototypeNonRecursive(decorate,override,properties,source, functionOverrideNameList, functionIgnoreNameList, propertyIgnoreNameList);
 97         };
 98 
 99         /**
100          * @private
101          */
102         var getFunctionParams = function(methodString) {
103             if(typeof(methodString) !== 'string') {
104                 methodString = methodString.toString();
105             }
106             return methodString.substring(methodString.indexOf('(')+1,methodString.indexOf(')')).replace(/\s+/,'').split(',');
107         };
108 
109         /*Public Interface */
110         /**
111          * @scope enchant.Class.MixingRecipe.prototype
112          */
113         enchant.Class.MixingRecipe = enchant.Class.create({
114             /**
115              * Erstellt ein neues MixingRecipe, welches beschreibt, auf welche Art und Weise Funktionen und Properties während des Mischens hinzugefügt werden.
116              * Um ein Rezept aus einer bereits existierend Klasse zu erstellen, ist auf {@link enchant.Class.MixingRecipe.createFromClass} zu verweisen.
117              * @class Diese Klasse beschreibt auf welche Art und Weise das mischen mit der Zielklasse durchgeführt wird.
118              * Für diesen Zweck enthählt ein MixingRecipe drei Properties:
119              * <ul><li>decorateMethods (Methoden, welche in der Zielklasse dekoriert werden, siehe Dekorierer Entwurfsmuster)</li>
120              * <li>overrideMethods (Methoden, welche in der Zielklasse überschrieben werden)</li>
121              * <li>overrideProperties (Properties, welche in der Zielklasse redefiniert werden)</li></ul>
122              * <p>Siehe auch: {@link enchant.Class.mixClasses}, {@link enchant.Class.mixClassesFromRecipe} und {@link enchant.Class.applyMixingRecipe}.</p>
123              * @param {Object} decorateMethods Die Methoden, welche in der Zielklasse dekoriert werden, siehe Dekorierer Entwurfsmuster. Auf die dekoriertie Methode kann in der durch das Mixen erstellten Klasse mit Hilfe des _mixing Property zugegriffen werden, z.B. this._mixing.myFunction.apply(this,arguments).<br>(Objekt welches Schlüssel-Wert Paare enthält, Schlüssel := Funktionsname, Wert := Funktion)
124              * @param {Object} overrideMethods Die Methoden, welche in der Zielklasse überschrieben werden.<br>(Objekt welches Schlüssel-Wert Paare enthält, Schlüssel := Funktionsname, Wert := Funktion).
125              * @param {Object} properties Die Properties, welche in der Zielklasse redefiniert werden.<br>(Objekt welches Schlüssel-Wert Paare enthält, Schlüssel := Funktionsname, Wert := Propertydescriptor).
126              * @property {Object} decorateMethods Die Methoden, welche in der Zielklasse dekoriert werden, siehe Dekorierer Entwurfsmuster. Auf die dekoriertie Methode kann in der durch das Mixen erstellten Klasse mit Hilfe des _mixing Property zugegriffen werden, z.B. this._mixing.myFunction.apply(this,arguments).<br>(Objekt welches Schlüssel-Wert Paare enthält, Schlüssel := Funktionsname, Wert := Funktion)
127              * @property {Object} overrideMethods Die Methoden, welche in der Zielklasse überschrieben werden.<br>(Objekt welches Schlüssel-Wert Paare enthält, Schlüssel := Funktionsname, Wert := Funktion).
128              * @property {Object} overrideProperties Die Properties, welche in der Zielklasse redefiniert werden.<br>(Objekt welches Schlüssel-Wert Paare enthält, Schlüssel := Funktionsname, Wert := Propertydescriptor).
129              * @example
130              *      var recipe = new enchant.Class.MixingRecipe({
131              *          add : function(value) {
132              *              this._myValue += 3*value;
133              *              this._mixing.add.apply(this,arguments);
134              *          },
135              *          mult : function(value) {
136              *              this._myValue *= value*7;
137              *              this._mixing.mult.apply(this,arguments);
138              *          }
139              *      },{
140              *          sub : function(value) {
141              *              this._myValue -= 5*value;
142              *          }
143              *      },{
144              *      myProperty : {
145              *          get: function() {
146              *              return 3*this._myPropertyValue;
147              *          },
148              *          set : function(val) {
149              *              this._myPropertyValue = val;
150              *          }
151              *      }});
152              *      var NewClass = enchant.Class.applyMixingRecipe(Class1,recipe);
153              * @extends Object
154              * @constructs
155              */
156             initialize : function(decorateMethods, overrideMethods, properties) {
157                 this.decorateMethods = decorateMethods;
158                 this.overrideMethods = overrideMethods;
159                 this.overrideProperties = properties;
160             }
161         });
162 
163         /**
164          * Nimmt die Methoden und Properties der übergebenen Klasse um ein neues MixingRecipe zu erstellen.
165          * Das Standardverhalten dabei ist, alle Funktionen und Properties der übergebenen Klasse, 
166          * einschließlich der Funktionen und Properties in den Superklassen zu nehmen und diese
167          * in der Zielklasse beim mixen zu dekorieren.<br>Methoden welche dekoriert werden, rufen
168          * automatisch die Methoden der sourceClass und der Zielklasse des Mixens, mit Hilfe des
169          * _mixing Properties, auf. Daher muss dies nicht selbst berücksichtigt werden.
170          * <p>Durch die entsprechenden Argumente der Funktion kann das Standardverhalten zu verändert werden.</p>
171          * 
172          * @param {Function<constructor Funktion die mit enchant.Class erstellt wurde>} sourceClass Die Klasse aus der das MixingRecipe erstellt wird.
173          * @param [boolean] onlyOwnProperties Wenn dieses Argument true ist, werden Funktionen und Properties der Superklassen ignoriert. 
174          * @param [Array<String>] functionOverrideNameList Ein Array welches Namen von Funktionen enthält, welche während des Mixens überschrieben werden sollen. 
175          * @param [Array<String>] functionIgnoreNameList Ein Array welches Namen von Funktionen enthält, welche bei der MixingRecipe erstellung ignoriert werden sollen. 
176          * @param [Array<String>] propertyIgnoreNameList Ein Array welches Namen von Properties enthält, welche bei der MixingRecipe erstellung ignoriert werden sollen.
177          * @returns {enchant.Class.MixingRecipe} Das MixingRecipe, welches aus der Definition der sourceClass erstellt wurde.
178          * @example
179          *      var recipe = enchant.Class.MixingRecipe.createFromClass(Class2, true, 
180          *              ['overrideFunction1','overrideFunction2'],
181          *              ['ignoreFunction1','ignoreFunction2'],
182          *              ['ignoreProperty1','ignorePropterty2']);
183          *      recipe.overrideMethods['additionalFunction'] = new function() {
184          *          console.log('Hello, World');
185          *      }
186          *      recipe.overrideProperties['newProperty'] = {
187          *          get: function() {
188          *              return this._newProperty;
189          *          },
190          *          set : function(val) {
191          *              this._newProperty = val;
192          *          }
193          *      }
194          *      var NewClass = enchant.Class.mixClassesFromRecipe(Class1,Class2,recipe);
195          * @constructs
196          * @static
197          */
198         enchant.Class.MixingRecipe.createFromClass = function(sourceClass, onlyOwnProperties, functionOverrideNameList, functionIgnoreNameList, propertyIgnoreNameList) {
199             var decorate = {};
200             var override = {};
201             var properties = {};
202 
203             var source = sourceClass.prototype;
204             createFromPrototype(decorate,override,properties,source,onlyOwnProperties, functionOverrideNameList, functionIgnoreNameList, propertyIgnoreNameList);
205             return new enchant.Class.MixingRecipe(decorate,override,properties);
206         };
207 
208         /**
209          * Nutzt das gegebene MixingRecipe, wendet es auf die "firstClass" Klasse an und liefert das Ergebnis daraus zurück - das MixingRecipe sollte der secondClass entsprechen.
210          * Eine Standard-Initialisierungsmethode wird hinzugefügt, welche die Initialisierungsmethode beider Klassen aufruft.
211          * Die Signatur für diese Standard-Initialisierungsmethode ist:<br>
212          * ([firstClass Konstruktor Arg 1],...,[firstClass Konstruktor Arg n],[secondClass Konstruktor Arg 1],...[secondClass Konstruktor Arg n])</p>
213          * <p>Beide Klassen werden nicht verändert.</p> Siehe auch: {@link enchant.Class.MixingRecipe}
214          * @param {Function<constructor Funktion die mit enchant.Class erstellt wurde>} firstClass Die Klasse auf die das MixingRecipe angewendet wird.
215          * @param {Function<constructor Funktion die mit enchant.Class erstellt wurde>} secondClass Die Klasse die dem MixingRecipe entpsricht, wird für die Standard-Initialisierungsmethode genutzt.
216          * @param {enchant.Class.MixingRecipe} recipe Das MixingRecipe, welches auf die firstClass Klasse angewendet wird - sollte der secondClass entsprechen.
217          * @param [Function] initializeMethod Falls gegeben, wird diese Methode, anstelle der Standard-Initialisierungsmethode, zum Initialisieren der resultierenden Klasse verwendet. 
218          * @returns {Function<constructor Funktion die mit enchant.Class erstellt wurde>} Die Klasse, welche das Ergebnis des Mixens beider Klassen mit Hilfe des MixingRecipe darstellt.
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          * Erstellt ein MixingRecipe aus der "secondClass" Klasse, wendet dieses auf die "firstClass" Klasse an und liefert das Ergebnis daraus zurück.
243          * Das Standardverhalten dabei ist, alle Funktionen und Properties der "secondClass" Klasse, 
244          * einschließlich der Funktionen und Properties in deren Superklassen zu nehmen und diese
245          * in der Zielklasse beim mixen zu dekorieren.<br>Methoden welche dekoriert werden, rufen
246          * automatisch die Methoden der sourceClass und der Zielklasse des Mixens, mit Hilfe des
247          * _mixing Properties, auf. Daher muss dies nicht selbst berücksichtigt werden.
248          * <p>Des Weiteren wird eine Standard-Initialisierungsmethode, welche die Initialisierungsmethode beider Klassen aufruft, hinzugefügt.
249          * Die Signatur für diese Standard-Initialisierungsmethode ist:<br>
250          * ([firstClass Konstruktor Arg 1],...,[firstClass Konstruktor Arg n],[secondClass Konstruktor Arg 1],...[secondClass Konstruktor Arg n])</p>
251          * <p>Beide Klassen werden nicht verändert.</p> Siehe auch: {@link enchant.Class.MixingRecipe}
252          * 
253          * @param {Function<constructor Funktion die mit enchant.Class erstellt wurde>} firstClass Die Klasse auf die das MixingRecipe angewendet wird.
254          * @param {Function<constructor Funktion die mit enchant.Class erstellt wurde>} secondClass Die Klasse aus der das MixingRecipe erstellt wird.
255          * @param [boolean] onlyOwnProperties Wenn dieses Argument true ist, werden Funktionen und Properties der Superklassen der "secondClass" Klasse ignoriert. 
256          * @param [Function] initializeMethod Falls gegeben, wird diese Methode, anstelle der Standard-Initialisierungsmethode, zum Initialisieren der resultierenden Klasse verwendet. 
257          * @returns {Function<constructor Funktion die mit enchant.Class erstellt wurde>} Die Klasse, welche das Ergebnis des Mixens beider Klassen darstellt.
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          * Wendet das übergebene MixingRecipe auf die Zielklasse an, erstellt eine neue Klassendefinition und liefert diese als Rückgabewert zurück.
271          * Die Zielklasse wird nicht modifiziert.<br>Siehe auch: {@link enchant.Class.MixingRecipe}.
272          * 
273          * @param {Function<constructor Funktion die mit enchant.Class erstellt wurde>} target Die Klasse auf die das MixingRecipe angewendet wird.
274          * @param {enchant.Class.MixingRecipe} source Das MixingRecipe, welches genutzt wird um der Zielklasse neue Funktionalität zu verleihen. 
275          * @returns {Function<constructor Funktion die mit enchant.Class erstellt wurde>} Die Klasse, welche das Ergebnis des Mixens der Zielklasse (target) mit dem Rezept (source) darstellt. 
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