1 /**
  2  * @fileOverview
  3  * mixing.enchant.js
  4  * <p>複数のクラスを混ぜることができるようにし、同じ関数やプロパティを複数のクラスに追加することができるようにするenchant.jsプラグイン.
  5  * その上、コピーアンドペーストをしないために、製法({@link enchant.Class.MixingRecipe})で定義された関数やプロパティを
  6  * 複数クラスに混ぜることもできるようにする.</p>
  7  * <p>これで複数の継承ようなことができるということ</p>
  8  * <p>必要なもの:</p><ul>
  9  * <li>enchant.js v0.6 以上.</li></ul></p>
 10  * 詳細は{@link enchant.Class.mixClasses}, {@link enchant.Class.MixingRecipe.createFromClass},
 11  * {@link enchant.Class.mixClassesFromRecipe}, {@link enchant.Class.MixingRecipe} や
 12  * {@link enchant.Class.applyMixingRecipe} 参照してください.
 13  * @require enchant.js v0.6+
 14  *
 15  * @version 0.1
 16  * @author UEI Corporation (Kevin Kratzer)
 17  **/
 18 
 19 if (enchant !== undefined) {
 20     (function() {
 21 
 22         /**
 23          * @private
 24          */
 25         var decorateFunctionFactory = function(srcFunction, currentFunctionName) {
 26             return function() {
 27                 var firstResult, secondResult;
 28                 firstResult = this._mixing[currentFunctionName].apply(this,arguments);
 29                 secondResult = srcFunction.apply(this,arguments);
 30                 if(secondResult) {
 31                     return secondResult;
 32                 }
 33                 if(firstResult) {
 34                     return firstResult;
 35                 }
 36             };
 37         };
 38 
 39         /**
 40          * @private
 41          */
 42         var voidFunction = function(){};
 43 
 44         /**
 45          * @private
 46          */
 47         var multipleMixingCombinationFunctionFactory = function(oldFunc,newFunc, key) {
 48             return function() {
 49                 var firstResult = oldFunc.apply(this,arguments);
 50                 var mixingStore = this._mixing[key];
 51                 this._mixing[key] = voidFunction;
 52                 var secondResult = newFunc.apply(this,arguments);
 53                 this._mixing[key] = mixingStore;
 54                 if(secondResult) {
 55                     return secondResult;
 56                 }
 57                 if(firstResult) {
 58                     return firstResult;
 59                 }
 60             };
 61         };
 62 
 63         /**
 64          * @private
 65          */
 66         var createFromPrototypeNonRecursive = function(decorate, override, properties, source, functionOverrideNameList, functionIgnoreNameList, propertyIgnoreNameList) {
 67             for(var key in source) {
 68                 if(source.hasOwnProperty(key)) {
 69                     var descriptor = Object.getOwnPropertyDescriptor(source, key);
 70                     if(descriptor.value && typeof(descriptor.value) === 'function') {
 71                         if((!functionIgnoreNameList || functionIgnoreNameList.indexOf(key) === -1) && key !== 'constructor') {
 72                             if(!functionOverrideNameList || functionOverrideNameList.indexOf(key) === -1) {
 73                                 decorate[key] = (decorateFunctionFactory(source[key],key));
 74                             } else {
 75                                 override[key] = source[key];
 76                             }
 77                         }
 78                     } else {
 79                         if(!propertyIgnoreNameList || propertyIgnoreNameList.indexOf(key) === -1 && key !== '_mixing') {
 80                             properties[key] = descriptor;
 81                         }
 82                     }
 83                 }
 84             }
 85         };
 86 
 87         /**
 88          * @private
 89          */
 90         var createFromPrototype = function(decorate,override,properties,source,onlyOwnProperties, functionOverrideNameList, functionIgnoreNameList, propertyIgnoreNameList) {
 91             if(!onlyOwnProperties && source instanceof Object) {
 92                 createFromPrototype(decorate,override,properties,Object.getPrototypeOf(source),onlyOwnProperties, functionOverrideNameList, functionIgnoreNameList, propertyIgnoreNameList);
 93             }
 94             createFromPrototypeNonRecursive(decorate,override,properties,source, functionOverrideNameList, functionIgnoreNameList, propertyIgnoreNameList);
 95         };
 96 
 97         /**
 98          * @private
 99          */
100         var getFunctionParams = function(methodString) {
101             if(typeof(methodString) !== 'string') {
102                 methodString = methodString.toString();
103             }
104             return methodString.substring(methodString.indexOf('(')+1,methodString.indexOf(')')).replace(/\s+/,'').split(',');
105         };
106 
107         /*Public Interface */
108         /**
109          * @scope enchant.Class.MixingRecipe.prototype
110          */
111         enchant.Class.MixingRecipe = enchant.Class.create({
112             /**
113              * 混ぜる中、どういう風に関数やプロパティが追加されるか設定する新たなMixingRecipeを生成する.
114              * クラスからMixingRecipeを生成することには {@link enchant.Class.MixingRecipe.createFromClass} を参照してください.
115              * @class 混ぜる先にどういう風に混ぜる処理を行うか設定する.
116              * このために、MixingRecipeはプロパティが三つある:
117              * <ul><li>decorateMethods(先の関数をラップする関数、デコレータ・パターンを参照してください)</li>
118              * <li>overrideMethods (先の関数をオーバーライドする関数)</li>
119              * <li>overrideProperties (先のプロパティをオーバーライドするプロパティ)</li></ul>
120              * <p>{@link enchant.Class.mixClasses}、 {@link enchant.Class.mixClassesFromRecipe} や {@link enchant.Class.applyMixingRecipe} を参照してください.</p>
121              * @param {Object} decorateMethods 先の関数をラップする関数、デコレータ・パターンを参照してください. 混ぜる結果のクラスでラップされた関数をアクセスするように_mixingというプロパティがある、例:this._mixing.myFunction.apply(this,arguments).<br>(キーと値のペア持っているオブジェクト、キーは関数名、値は関数).
122              * @param {Object} overrideMethods 先の関数をオーバーライドする関数.<br>(キーと値のペア持っているオブジェクト、キーは関数名、値は関数).
123              * @param {Object} properties 先のプロパティをオーバーライドするプロパティ.<br>(キーと値のペア持っているオブジェクト、キーは関数名、値はプロパティデスクリプタ).
124              * @property {Object} decorateMethods 先の関数をラップする関数、デコレータ・パターンを参照してください. 混ぜる結果のクラスでラップされた関数をアクセスするように_mixingというプロパティがある、例:this._mixing.myFunction.apply(this,arguments).<br>(キーと値のペア持っているオブジェクト、キーは関数名、値は関数).
125              * @property {Object} overrideMethods 先の関数をオーバーライドする関数.<br>(キーと値のペア持っているオブジェクト、キーは関数名、値は関数).
126              * @property {Object} overrideProperties 先のプロパティをオーバーライドするプロパティ.<br>(キーと値のペア持っているオブジェクト、キーは関数名、値はプロパティデスクリプタ).
127              * @example
128              *      var recipe = new enchant.Class.MixingRecipe({
129              *          add : function(value) {
130              *              this._myValue += 3*value;
131              *              this._mixing.add.apply(this,arguments);
132              *          },
133              *          mult : function(value) {
134              *              this._myValue *= value*7;
135              *              this._mixing.mult.apply(this,arguments);
136              *          }
137              *      },{
138              *          sub : function(value) {
139              *              this._myValue -= 5*value;
140              *          }
141              *      },{
142              *      myProperty : {
143              *          get: function() {
144              *              return 3*this._myPropertyValue;
145              *          },
146              *          set : function(val) {
147              *              this._myPropertyValue = val;
148              *          }
149              *      }});
150              *      var NewClass = enchant.Class.applyMixingRecipe(Class1,recipe);
151              * @extends Object
152              * @constructs
153              */
154             initialize : function(decorateMethods, overrideMethods, properties) {
155                 this.decorateMethods = decorateMethods;
156                 this.overrideMethods = overrideMethods;
157                 this.overrideProperties = properties;
158             }
159         });
160 
161         /**
162          * 引数のクルスの関数やプロパティから、MixingRecipeを生成する.
163          * デフォルト振舞はsourceClassの全ての関数やプロパティ、スーパークラスの関数やプロパティも使用して、
164          * 混ぜる先の関数をラップする(decorate).<br>_mixingプロパティでラップされた関数が自動的に、
165          * sourceClassと混ぜる先の関数を呼び出されるので、関係なくてもいい.
166          * <p>対応引数でデフォルト振舞を変更ができる.</p>
167          * 
168          * @param {Function<constructor enchant.Classで生成された関数>} sourceClass このクラスから、MixingRecipeが生成される.
169          * @param [boolean] onlyOwnProperties Trueの場合、スーパークラスの関数やプロパティが無視されない. 
170          * @param [Array<String>] functionOverrideNameList 混ぜるときオーバーライドされる関数名を持っている配列.  
171          * @param [Array<String>] functionIgnoreNameList MixingRecipeを生成するとき無視される関数名を持っている配列.  
172          * @param [Array<String>] propertyIgnoreNameList MixingRecipeを生成するとき無視されるプロパティ名を持っている配列.
173          * @returns {enchant.Class.MixingRecipe} クラス定義から生成されたMixingRecipe.
174          * @example
175          *      var recipe = enchant.Class.MixingRecipe.createFromClass(Class2, true, 
176          *              ['overrideFunction1','overrideFunction2'],
177          *              ['ignoreFunction1','ignoreFunction2'],
178          *              ['ignoreProperty1','ignorePropterty2']);
179          *      recipe.overrideMethods['additionalFunction'] = new function() {
180          *          console.log('Hello, World');
181          *      }
182          *      recipe.overrideProperties['newProperty'] = {
183          *          get: function() {
184          *              return this._newProperty;
185          *          },
186          *          set : function(val) {
187          *              this._newProperty = val;
188          *          }
189          *      }
190          *      var NewClass = enchant.Class.mixClassesFromRecipe(Class1,Class2,recipe);
191          * @constructs
192          * @static
193          */
194         enchant.Class.MixingRecipe.createFromClass = function(sourceClass, onlyOwnProperties, functionOverrideNameList, functionIgnoreNameList, propertyIgnoreNameList) {
195             var decorate = {};
196             var override = {};
197             var properties = {};
198 
199             var source = sourceClass.prototype;
200             createFromPrototype(decorate,override,properties,source,onlyOwnProperties, functionOverrideNameList, functionIgnoreNameList, propertyIgnoreNameList);
201             return new enchant.Class.MixingRecipe(decorate,override,properties);
202         };
203 
204         /**
205          * 設定されたMixingRecipeを使用して、firstClassというクラスに実行して、この結果を戻る。設定されたMixingRecipeはsecondClassというクラスに関係があったほうがいい.
206          * どちらでもクラスの初期化関数を呼び出すデフォルト初期化関数が追加される.
207          * このデフォルト初期か関数の書式は:<br>
208          * ([firstClass コンストラクタ 引数 1],...,[firstClass コンストラクタ 引数 n],[secondClass コンストラクタ 引数 1],...[secondClass コンストラクタ 引数 n])</p>
209          * <p>どちらでもクラスが変更されない</p>{@link enchant.Class.MixingRecipe} を参照してください.
210          * @param {Function<constructor enchant.Classで生成された関数>} firstClass MixingRecipeを実行されるクラス.
211          * @param {Function<constructor enchant.Classで生成された関数>} sourceClass MixingRecipに関係があるクラス。デフォルト初期化関数に使用される。
212          * @param {enchant.Class.MixingRecipe} recipe firstClassに実行される製法。 secondClassに関係があったほうがいい.
213          * @param [Function] initializeMethod 設定すると、新しいクラスの初期化にデフォルト初期化関数が使用されないけど、この関数が使用される. 
214          * @returns {Function<constructor enchant.Classで生成された関数>} 製法でどちらでもクラスを混ぜた結果クラス.
215          * @example
216          *      var MapGroup = enchant.Class.mixClasses(Map, Group,true);
217          *      var map = new MapGroup(16, 16);
218          *      var SpriteLabel = enchant.Class.mixClasses(Sprite, Label,true);
219          *      var kumaLabel = new SpriteLabel(32,32,'Kuma');
220          * @static
221          */
222         enchant.Class.mixClassesFromRecipe = function(firstClass, secondClass, recipe, initializeMethod) {
223             var result = enchant.Class.applyMixingRecipe(firstClass,recipe);
224             var paramLength = getFunctionParams(firstClass.prototype.initialize).length;
225             if(typeof(initializeMethod) !== 'function') {
226                 initializeMethod = function() {
227                     var args = Array.prototype.slice.call(arguments);
228                     secondClass.prototype.initialize.apply(this,args.slice(paramLength));
229                     firstClass.prototype.initialize.apply(this,args.slice(0,paramLength));
230                 };
231             }
232             result.prototype.initialize = initializeMethod;
233             return result;
234         };
235 
236 
237         /**
238          * secondClassというクルスからMixingRecipeを生成して、firstClassというクラスに実行して、この結果を戻る.
239          * デフォルト振舞はsecondClassの全ての関数やプロパティ、スーパークラスの関数やプロパティも使用して、
240          * 混ぜる先の関数をラップする(decorate).<br>_mixingプロパティでラップされた関数が自動的に、
241          * sourceClassと混ぜる先の関数を呼び出されるので、関係なくてもいい.
242          * <p>その上、どちらでもクラスの初期化関数を呼び出すデフォルト初期化関数が追加される.このデフォルト初期か関数の書式は:<br>
243          * ([firstClass コンストラクタ 引数 1],...,[firstClass コンストラクタ 引数 n],[secondClass コンストラクタ 引数 1],...[secondClass コンストラクタ 引数 n])</p>
244          * <p>どちらでもクラスが変更されない</p>{@link enchant.Class.MixingRecipe} を参照してください.
245          * @param {Function<constructor enchant.Classで生成された関数>} firstClass MixingRecipeを実行されるクラス.
246          * @param {Function<constructor enchant.Classで生成された関数>} sourceClass このクラスから、MixingRecipeが生成される.
247          * @param [boolean] onlyOwnProperties Trueの場合、スーパークラスの関数やプロパティが無視されない. 
248          * @param [Function] initializeMethod 設定すると、新しいクラスの初期化にデフォルト初期化関数が使用されないけど、この関数が使用される. 
249          * @returns {Function<constructor enchant.Classで生成された関数>} どちらでもクラスを混ぜた結果クラス.
250          * @example
251          *      var MapGroup = enchant.Class.mixClasses(Map, Group,true);
252          *      var map = new MapGroup(16, 16);
253          *      var SpriteLabel = enchant.Class.mixClasses(Sprite, Label,true);
254          *      var kumaLabel = new SpriteLabel(32,32,'Kuma');
255          * @static
256          */
257         enchant.Class.mixClasses = function(firstClass, secondClass, useOnlyOwnPropertiesForSecondClass, initializeMethod) {
258             return enchant.Class.mixClassesFromRecipe(firstClass,secondClass,enchant.Class.MixingRecipe.createFromClass(secondClass, useOnlyOwnPropertiesForSecondClass, [], ['initialize'], []),initializeMethod);
259         };
260 
261         /**
262          * 設定されたMixingRecipeを先クラスに実行して、新たなクラスを生成してこのクラスを戻る.
263          * 先クラスが変更されない.<br>{@link enchant.Class.MixingRecipe} を参照してください.
264          * 
265          * @param {Function<constructor enchant.Classで生成された関数>} target MixingRecipeを実行される先クラス.
266          * @param {enchant.Class.MixingRecipe} source 先に新しい機能を追加するMixingRecipe.
267          * @returns {Function<constructor enchant.Classで生成された関数>} sourceという製法とtargetというクラスを混ぜた結果クラス.
268          * @example
269          *      var recipe = new enchant.Class.MixingRecipe({
270          *         // ... see enchant.Class.MixingRecipe
271          *      },{
272          *          // ... see enchant.Class.MixingRecipe
273          *      },{
274          *          // ... see enchant.Class.MixingRecipe
275          *      });
276          *      var NewClass = applyMixingRecipe(Class1,recipe);
277          * @static
278          */
279         enchant.Class.applyMixingRecipe = function(target, source) {
280             var result = enchant.Class.create(target,{});
281             target = result.prototype;
282             for(var recipeKey in source) {
283                 if(source.hasOwnProperty(recipeKey)) {
284                     var currentSource = source[recipeKey];
285                     if(recipeKey === 'overrideMethods') {
286                         for(var methodKey in currentSource) {
287                             if(currentSource.hasOwnProperty(methodKey)) {
288                                 target[methodKey] = currentSource[methodKey];
289                                 if(target._mixing && target._mixing[methodKey]) {
290                                     target._mixing[methodKey] = voidFunction;
291                                 }
292                             }
293                         }
294                     } else if(recipeKey === 'overrideProperties') {
295                         for(var propertyKey in currentSource) {
296                             if(currentSource.hasOwnProperty(propertyKey)) {
297                                 Object.defineProperty(target,propertyKey,currentSource[propertyKey]);
298                             }
299                         }
300                     } else if(recipeKey === 'decorateMethods') {
301                         if(!target._mixing) {
302                             target._mixing = {};
303                         }
304                         for(var key in currentSource) {
305                             if(currentSource.hasOwnProperty(key)) {
306                                 var targetHolder = target;
307                                 if(!target[key]) {
308                                     while(targetHolder instanceof Object && !targetHolder[key]) {
309                                         targetHolder = Object.getPrototypeOf(targetHolder);
310                                     }
311                                 }
312                                 if(target._mixing[key]) {
313                                     var newFunc = targetHolder[key];
314                                     target._mixing[key] = (multipleMixingCombinationFunctionFactory(target._mixing[key],newFunc,key));
315                                 } else {
316                                     target._mixing[key] = targetHolder[key];
317                                     if(!target._mixing[key]) {
318                                         target._mixing[key] = voidFunction;
319                                     }
320                                 }
321                                 target[key] = currentSource[key];
322                             }
323                         }
324                     }
325                 }
326             }
327             return result;
328         };
329     })();
330 }
331