Upgrade to ExtJS 4.0.2 - Released 06/09/2011
[extjs.git] / src / core / src / class / Class.js
1 /*
2
3 This file is part of Ext JS 4
4
5 Copyright (c) 2011 Sencha Inc
6
7 Contact:  http://www.sencha.com/contact
8
9 GNU General Public License Usage
10 This file may be used under the terms of the GNU General Public License version 3.0 as published by the Free Software Foundation and appearing in the file LICENSE included in the packaging of this file.  Please review the following information to ensure the GNU General Public License version 3.0 requirements will be met: http://www.gnu.org/copyleft/gpl.html.
11
12 If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
13
14 */
15 /**
16  * @author Jacky Nguyen <jacky@sencha.com>
17  * @docauthor Jacky Nguyen <jacky@sencha.com>
18  * @class Ext.Class
19  * 
20  * Handles class creation throughout the whole framework. Note that most of the time {@link Ext#define Ext.define} should
21  * be used instead, since it's a higher level wrapper that aliases to {@link Ext.ClassManager#create}
22  * to enable namespacing and dynamic dependency resolution.
23  * 
24  * # Basic syntax: #
25  * 
26  *     Ext.define(className, properties);
27  * 
28  * in which `properties` is an object represent a collection of properties that apply to the class. See
29  * {@link Ext.ClassManager#create} for more detailed instructions.
30  * 
31  *     Ext.define('Person', {
32  *          name: 'Unknown',
33  * 
34  *          constructor: function(name) {
35  *              if (name) {
36  *                  this.name = name;
37  *              }
38  * 
39  *              return this;
40  *          },
41  * 
42  *          eat: function(foodType) {
43  *              alert("I'm eating: " + foodType);
44  * 
45  *              return this;
46  *          }
47  *     });
48  * 
49  *     var aaron = new Person("Aaron");
50  *     aaron.eat("Sandwich"); // alert("I'm eating: Sandwich");
51  * 
52  * Ext.Class has a powerful set of extensible {@link Ext.Class#registerPreprocessor pre-processors} which takes care of
53  * everything related to class creation, including but not limited to inheritance, mixins, configuration, statics, etc.
54  * 
55  * # Inheritance: #
56  * 
57  *     Ext.define('Developer', {
58  *          extend: 'Person',
59  * 
60  *          constructor: function(name, isGeek) {
61  *              this.isGeek = isGeek;
62  * 
63  *              // Apply a method from the parent class' prototype
64  *              this.callParent([name]);
65  * 
66  *              return this;
67  * 
68  *          },
69  * 
70  *          code: function(language) {
71  *              alert("I'm coding in: " + language);
72  * 
73  *              this.eat("Bugs");
74  * 
75  *              return this;
76  *          }
77  *     });
78  * 
79  *     var jacky = new Developer("Jacky", true);
80  *     jacky.code("JavaScript"); // alert("I'm coding in: JavaScript");
81  *                               // alert("I'm eating: Bugs");
82  * 
83  * See {@link Ext.Base#callParent} for more details on calling superclass' methods
84  * 
85  * # Mixins: #
86  * 
87  *     Ext.define('CanPlayGuitar', {
88  *          playGuitar: function() {
89  *             alert("F#...G...D...A");
90  *          }
91  *     });
92  * 
93  *     Ext.define('CanComposeSongs', {
94  *          composeSongs: function() { ... }
95  *     });
96  * 
97  *     Ext.define('CanSing', {
98  *          sing: function() {
99  *              alert("I'm on the highway to hell...")
100  *          }
101  *     });
102  * 
103  *     Ext.define('Musician', {
104  *          extend: 'Person',
105  * 
106  *          mixins: {
107  *              canPlayGuitar: 'CanPlayGuitar',
108  *              canComposeSongs: 'CanComposeSongs',
109  *              canSing: 'CanSing'
110  *          }
111  *     })
112  * 
113  *     Ext.define('CoolPerson', {
114  *          extend: 'Person',
115  * 
116  *          mixins: {
117  *              canPlayGuitar: 'CanPlayGuitar',
118  *              canSing: 'CanSing'
119  *          },
120  * 
121  *          sing: function() {
122  *              alert("Ahem....");
123  * 
124  *              this.mixins.canSing.sing.call(this);
125  * 
126  *              alert("[Playing guitar at the same time...]");
127  * 
128  *              this.playGuitar();
129  *          }
130  *     });
131  * 
132  *     var me = new CoolPerson("Jacky");
133  * 
134  *     me.sing(); // alert("Ahem...");
135  *                // alert("I'm on the highway to hell...");
136  *                // alert("[Playing guitar at the same time...]");
137  *                // alert("F#...G...D...A");
138  * 
139  * # Config: #
140  * 
141  *     Ext.define('SmartPhone', {
142  *          config: {
143  *              hasTouchScreen: false,
144  *              operatingSystem: 'Other',
145  *              price: 500
146  *          },
147  * 
148  *          isExpensive: false,
149  * 
150  *          constructor: function(config) {
151  *              this.initConfig(config);
152  * 
153  *              return this;
154  *          },
155  * 
156  *          applyPrice: function(price) {
157  *              this.isExpensive = (price > 500);
158  * 
159  *              return price;
160  *          },
161  * 
162  *          applyOperatingSystem: function(operatingSystem) {
163  *              if (!(/^(iOS|Android|BlackBerry)$/i).test(operatingSystem)) {
164  *                  return 'Other';
165  *              }
166  * 
167  *              return operatingSystem;
168  *          }
169  *     });
170  * 
171  *     var iPhone = new SmartPhone({
172  *          hasTouchScreen: true,
173  *          operatingSystem: 'iOS'
174  *     });
175  * 
176  *     iPhone.getPrice(); // 500;
177  *     iPhone.getOperatingSystem(); // 'iOS'
178  *     iPhone.getHasTouchScreen(); // true;
179  *     iPhone.hasTouchScreen(); // true
180  * 
181  *     iPhone.isExpensive; // false;
182  *     iPhone.setPrice(600);
183  *     iPhone.getPrice(); // 600
184  *     iPhone.isExpensive; // true;
185  * 
186  *     iPhone.setOperatingSystem('AlienOS');
187  *     iPhone.getOperatingSystem(); // 'Other'
188  * 
189  * # Statics: #
190  * 
191  *     Ext.define('Computer', {
192  *          statics: {
193  *              factory: function(brand) {
194  *                 // 'this' in static methods refer to the class itself
195  *                  return new this(brand);
196  *              }
197  *          },
198  * 
199  *          constructor: function() { ... }
200  *     });
201  * 
202  *     var dellComputer = Computer.factory('Dell');
203  * 
204  * Also see {@link Ext.Base#statics} and {@link Ext.Base#self} for more details on accessing
205  * static properties within class methods
206  *
207  */
208 (function() {
209
210     var Class,
211         Base = Ext.Base,
212         baseStaticProperties = [],
213         baseStaticProperty;
214
215     for (baseStaticProperty in Base) {
216         if (Base.hasOwnProperty(baseStaticProperty)) {
217             baseStaticProperties.push(baseStaticProperty);
218         }
219     }
220
221     /**
222      * @method constructor
223      * Creates new class.
224      * @param {Object} classData An object represent the properties of this class
225      * @param {Function} createdFn Optional, the callback function to be executed when this class is fully created.
226      * Note that the creation process can be asynchronous depending on the pre-processors used.
227      * @return {Ext.Base} The newly created class
228      */
229     Ext.Class = Class = function(newClass, classData, onClassCreated) {
230         if (typeof newClass !== 'function') {
231             onClassCreated = classData;
232             classData = newClass;
233             newClass = function() {
234                 return this.constructor.apply(this, arguments);
235             };
236         }
237
238         if (!classData) {
239             classData = {};
240         }
241
242         var preprocessorStack = classData.preprocessors || Class.getDefaultPreprocessors(),
243             registeredPreprocessors = Class.getPreprocessors(),
244             index = 0,
245             preprocessors = [],
246             preprocessor, preprocessors, staticPropertyName, process, i, j, ln;
247
248         for (i = 0, ln = baseStaticProperties.length; i < ln; i++) {
249             staticPropertyName = baseStaticProperties[i];
250             newClass[staticPropertyName] = Base[staticPropertyName];
251         }
252
253         delete classData.preprocessors;
254
255         for (j = 0, ln = preprocessorStack.length; j < ln; j++) {
256             preprocessor = preprocessorStack[j];
257
258             if (typeof preprocessor === 'string') {
259                 preprocessor = registeredPreprocessors[preprocessor];
260
261                 if (!preprocessor.always) {
262                     if (classData.hasOwnProperty(preprocessor.name)) {
263                         preprocessors.push(preprocessor.fn);
264                     }
265                 }
266                 else {
267                     preprocessors.push(preprocessor.fn);
268                 }
269             }
270             else {
271                 preprocessors.push(preprocessor);
272             }
273         }
274
275         classData.onClassCreated = onClassCreated;
276
277         classData.onBeforeClassCreated = function(cls, data) {
278             onClassCreated = data.onClassCreated;
279
280             delete data.onBeforeClassCreated;
281             delete data.onClassCreated;
282
283             cls.implement(data);
284
285             if (onClassCreated) {
286                 onClassCreated.call(cls, cls);
287             }
288         };
289
290         process = function(cls, data) {
291             preprocessor = preprocessors[index++];
292
293             if (!preprocessor) {
294                 data.onBeforeClassCreated.apply(this, arguments);
295                 return;
296             }
297
298             if (preprocessor.call(this, cls, data, process) !== false) {
299                 process.apply(this, arguments);
300             }
301         };
302
303         process.call(Class, newClass, classData);
304
305         return newClass;
306     };
307
308     Ext.apply(Class, {
309
310         /** @private */
311         preprocessors: {},
312
313         /**
314          * Register a new pre-processor to be used during the class creation process
315          *
316          * @member Ext.Class registerPreprocessor
317          * @param {String} name The pre-processor's name
318          * @param {Function} fn The callback function to be executed. Typical format:
319
320     function(cls, data, fn) {
321         // Your code here
322
323         // Execute this when the processing is finished.
324         // Asynchronous processing is perfectly ok
325         if (fn) {
326             fn.call(this, cls, data);
327         }
328     });
329
330          * Passed arguments for this function are:
331          *
332          * - `{Function} cls`: The created class
333          * - `{Object} data`: The set of properties passed in {@link Ext.Class} constructor
334          * - `{Function} fn`: The callback function that <b>must</b> to be executed when this pre-processor finishes,
335          * regardless of whether the processing is synchronous or aynchronous
336          *
337          * @return {Ext.Class} this
338          * @markdown
339          */
340         registerPreprocessor: function(name, fn, always) {
341             this.preprocessors[name] = {
342                 name: name,
343                 always: always ||  false,
344                 fn: fn
345             };
346
347             return this;
348         },
349
350         /**
351          * Retrieve a pre-processor callback function by its name, which has been registered before
352          *
353          * @param {String} name
354          * @return {Function} preprocessor
355          */
356         getPreprocessor: function(name) {
357             return this.preprocessors[name];
358         },
359
360         getPreprocessors: function() {
361             return this.preprocessors;
362         },
363
364         /**
365          * Retrieve the array stack of default pre-processors
366          *
367          * @return {Function} defaultPreprocessors
368          */
369         getDefaultPreprocessors: function() {
370             return this.defaultPreprocessors || [];
371         },
372
373         /**
374          * Set the default array stack of default pre-processors
375          *
376          * @param {Array} preprocessors
377          * @return {Ext.Class} this
378          */
379         setDefaultPreprocessors: function(preprocessors) {
380             this.defaultPreprocessors = Ext.Array.from(preprocessors);
381
382             return this;
383         },
384
385         /**
386          * Insert this pre-processor at a specific position in the stack, optionally relative to
387          * any existing pre-processor. For example:
388
389     Ext.Class.registerPreprocessor('debug', function(cls, data, fn) {
390         // Your code here
391
392         if (fn) {
393             fn.call(this, cls, data);
394         }
395     }).insertDefaultPreprocessor('debug', 'last');
396
397          * @param {String} name The pre-processor name. Note that it needs to be registered with
398          * {@link Ext#registerPreprocessor registerPreprocessor} before this
399          * @param {String} offset The insertion position. Four possible values are:
400          * 'first', 'last', or: 'before', 'after' (relative to the name provided in the third argument)
401          * @param {String} relativeName
402          * @return {Ext.Class} this
403          * @markdown
404          */
405         setDefaultPreprocessorPosition: function(name, offset, relativeName) {
406             var defaultPreprocessors = this.defaultPreprocessors,
407                 index;
408
409             if (typeof offset === 'string') {
410                 if (offset === 'first') {
411                     defaultPreprocessors.unshift(name);
412
413                     return this;
414                 }
415                 else if (offset === 'last') {
416                     defaultPreprocessors.push(name);
417
418                     return this;
419                 }
420
421                 offset = (offset === 'after') ? 1 : -1;
422             }
423
424             index = Ext.Array.indexOf(defaultPreprocessors, relativeName);
425
426             if (index !== -1) {
427                 Ext.Array.splice(defaultPreprocessors, Math.max(0, index + offset), 0, name);
428             }
429
430             return this;
431         }
432     });
433
434     /**
435      * @cfg {String} extend
436      * The parent class that this class extends. For example:
437      *
438      *     Ext.define('Person', {
439      *         say: function(text) { alert(text); }
440      *     });
441      *
442      *     Ext.define('Developer', {
443      *         extend: 'Person',
444      *         say: function(text) { this.callParent(["print "+text]); }
445      *     });
446      */
447     Class.registerPreprocessor('extend', function(cls, data) {
448         var extend = data.extend,
449             base = Ext.Base,
450             basePrototype = base.prototype,
451             prototype = function() {},
452             parent, i, k, ln, staticName, parentStatics,
453             parentPrototype, clsPrototype;
454
455         if (extend && extend !== Object) {
456             parent = extend;
457         }
458         else {
459             parent = base;
460         }
461
462         parentPrototype = parent.prototype;
463
464         prototype.prototype = parentPrototype;
465         clsPrototype = cls.prototype = new prototype();
466
467         if (!('$class' in parent)) {
468             for (i in basePrototype) {
469                 if (!parentPrototype[i]) {
470                     parentPrototype[i] = basePrototype[i];
471                 }
472             }
473         }
474
475         clsPrototype.self = cls;
476
477         cls.superclass = clsPrototype.superclass = parentPrototype;
478
479         delete data.extend;
480
481         // Statics inheritance
482         parentStatics = parentPrototype.$inheritableStatics;
483
484         if (parentStatics) {
485             for (k = 0, ln = parentStatics.length; k < ln; k++) {
486                 staticName = parentStatics[k];
487
488                 if (!cls.hasOwnProperty(staticName)) {
489                     cls[staticName] = parent[staticName];
490                 }
491             }
492         }
493
494         // Merge the parent class' config object without referencing it
495         if (parentPrototype.config) {
496             clsPrototype.config = Ext.Object.merge({}, parentPrototype.config);
497         }
498         else {
499             clsPrototype.config = {};
500         }
501
502         if (clsPrototype.$onExtended) {
503             clsPrototype.$onExtended.call(cls, cls, data);
504         }
505
506         if (data.onClassExtended) {
507             clsPrototype.$onExtended = data.onClassExtended;
508             delete data.onClassExtended;
509         }
510
511     }, true);
512
513     /**
514      * @cfg {Object} statics
515      * List of static methods for this class. For example:
516      *
517      *     Ext.define('Computer', {
518      *          statics: {
519      *              factory: function(brand) {
520      *                  // 'this' in static methods refer to the class itself
521      *                  return new this(brand);
522      *              }
523      *          },
524      *
525      *          constructor: function() { ... }
526      *     });
527      *
528      *     var dellComputer = Computer.factory('Dell');
529      */
530     Class.registerPreprocessor('statics', function(cls, data) {
531         var statics = data.statics,
532             name;
533
534         for (name in statics) {
535             if (statics.hasOwnProperty(name)) {
536                 cls[name] = statics[name];
537             }
538         }
539
540         delete data.statics;
541     });
542
543     /**
544      * @cfg {Object} inheritableStatics
545      * List of inheritable static methods for this class.
546      * Otherwise just like {@link #statics} but subclasses inherit these methods.
547      */
548     Class.registerPreprocessor('inheritableStatics', function(cls, data) {
549         var statics = data.inheritableStatics,
550             inheritableStatics,
551             prototype = cls.prototype,
552             name;
553
554         inheritableStatics = prototype.$inheritableStatics;
555
556         if (!inheritableStatics) {
557             inheritableStatics = prototype.$inheritableStatics = [];
558         }
559
560         for (name in statics) {
561             if (statics.hasOwnProperty(name)) {
562                 cls[name] = statics[name];
563                 inheritableStatics.push(name);
564             }
565         }
566
567         delete data.inheritableStatics;
568     });
569
570     /**
571      * @cfg {Object} mixins
572      * List of classes to mix into this class. For example:
573      *
574      *     Ext.define('CanSing', {
575      *          sing: function() {
576      *              alert("I'm on the highway to hell...")
577      *          }
578      *     });
579      *
580      *     Ext.define('Musician', {
581      *          extend: 'Person',
582      *
583      *          mixins: {
584      *              canSing: 'CanSing'
585      *          }
586      *     })
587      */
588     Class.registerPreprocessor('mixins', function(cls, data) {
589         cls.mixin(data.mixins);
590
591         delete data.mixins;
592     });
593
594     /**
595      * @cfg {Object} config
596      * List of configuration options with their default values, for which automatically
597      * accessor methods are generated.  For example:
598      *
599      *     Ext.define('SmartPhone', {
600      *          config: {
601      *              hasTouchScreen: false,
602      *              operatingSystem: 'Other',
603      *              price: 500
604      *          },
605      *          constructor: function(cfg) {
606      *              this.initConfig(cfg);
607      *          }
608      *     });
609      *
610      *     var iPhone = new SmartPhone({
611      *          hasTouchScreen: true,
612      *          operatingSystem: 'iOS'
613      *     });
614      *
615      *     iPhone.getPrice(); // 500;
616      *     iPhone.getOperatingSystem(); // 'iOS'
617      *     iPhone.getHasTouchScreen(); // true;
618      *     iPhone.hasTouchScreen(); // true
619      */
620     Class.registerPreprocessor('config', function(cls, data) {
621         var prototype = cls.prototype;
622
623         Ext.Object.each(data.config, function(name) {
624             var cName = name.charAt(0).toUpperCase() + name.substr(1),
625                 pName = name,
626                 apply = 'apply' + cName,
627                 setter = 'set' + cName,
628                 getter = 'get' + cName;
629
630             if (!(apply in prototype) && !data.hasOwnProperty(apply)) {
631                 data[apply] = function(val) {
632                     return val;
633                 };
634             }
635
636             if (!(setter in prototype) && !data.hasOwnProperty(setter)) {
637                 data[setter] = function(val) {
638                     var ret = this[apply].call(this, val, this[pName]);
639
640                     if (ret !== undefined) {
641                         this[pName] = ret;
642                     }
643
644                     return this;
645                 };
646             }
647
648             if (!(getter in prototype) && !data.hasOwnProperty(getter)) {
649                 data[getter] = function() {
650                     return this[pName];
651                 };
652             }
653         });
654
655         Ext.Object.merge(prototype.config, data.config);
656         delete data.config;
657     });
658
659     Class.setDefaultPreprocessors(['extend', 'statics', 'inheritableStatics', 'mixins', 'config']);
660
661     // Backwards compatible
662     Ext.extend = function(subclass, superclass, members) {
663         if (arguments.length === 2 && Ext.isObject(superclass)) {
664             members = superclass;
665             superclass = subclass;
666             subclass = null;
667         }
668
669         var cls;
670
671         if (!superclass) {
672             Ext.Error.raise("Attempting to extend from a class which has not been loaded on the page.");
673         }
674
675         members.extend = superclass;
676         members.preprocessors = ['extend', 'mixins', 'config', 'statics'];
677
678         if (subclass) {
679             cls = new Class(subclass, members);
680         }
681         else {
682             cls = new Class(members);
683         }
684
685         cls.prototype.override = function(o) {
686             for (var m in o) {
687                 if (o.hasOwnProperty(m)) {
688                     this[m] = o[m];
689                 }
690             }
691         };
692
693         return cls;
694     };
695
696 })();
697