Upgrade to ExtJS 4.0.1 - Released 05/18/2011
[extjs.git] / src / core / src / Ext.js
1 /**
2  * @class Ext
3  * @singleton
4  */
5 (function() {
6     var global = this,
7         objectPrototype = Object.prototype,
8         toString = Object.prototype.toString,
9         enumerables = true,
10         enumerablesTest = { toString: 1 },
11         i;
12
13     if (typeof Ext === 'undefined') {
14         global.Ext = {};
15     }
16
17     Ext.global = global;
18
19     for (i in enumerablesTest) {
20         enumerables = null;
21     }
22
23     if (enumerables) {
24         enumerables = ['hasOwnProperty', 'valueOf', 'isPrototypeOf', 'propertyIsEnumerable',
25                        'toLocaleString', 'toString', 'constructor'];
26     }
27
28     /**
29      * An array containing extra enumerables for old browsers
30      * @type Array
31      */
32     Ext.enumerables = enumerables;
33
34     /**
35      * Copies all the properties of config to the specified object.
36      * Note that if recursive merging and cloning without referencing the original objects / arrays is needed, use
37      * {@link Ext.Object#merge} instead.
38      * @param {Object} object The receiver of the properties
39      * @param {Object} config The source of the properties
40      * @param {Object} defaults A different object that will also be applied for default values
41      * @return {Object} returns obj
42      */
43     Ext.apply = function(object, config, defaults) {
44         if (defaults) {
45             Ext.apply(object, defaults);
46         }
47
48         if (object && config && typeof config === 'object') {
49             var i, j, k;
50
51             for (i in config) {
52                 object[i] = config[i];
53             }
54
55             if (enumerables) {
56                 for (j = enumerables.length; j--;) {
57                     k = enumerables[j];
58                     if (config.hasOwnProperty(k)) {
59                         object[k] = config[k];
60                     }
61                 }
62             }
63         }
64
65         return object;
66     };
67
68     Ext.buildSettings = Ext.apply({
69         baseCSSPrefix: 'x-',
70         scopeResetCSS: false
71     }, Ext.buildSettings || {});
72
73     Ext.apply(Ext, {
74         /**
75          * A reusable empty function
76          */
77         emptyFn: function() {},
78
79         baseCSSPrefix: Ext.buildSettings.baseCSSPrefix,
80
81         /**
82          * Copies all the properties of config to object if they don't already exist.
83          * @function
84          * @param {Object} object The receiver of the properties
85          * @param {Object} config The source of the properties
86          * @return {Object} returns obj
87          */
88         applyIf: function(object, config) {
89             var property;
90
91             if (object) {
92                 for (property in config) {
93                     if (object[property] === undefined) {
94                         object[property] = config[property];
95                     }
96                 }
97             }
98
99             return object;
100         },
101
102         /**
103          * Iterates either an array or an object. This method delegates to
104          * {@link Ext.Array#each Ext.Array.each} if the given value is iterable, and {@link Ext.Object#each Ext.Object.each} otherwise.
105          *
106          * @param {Object/Array} object The object or array to be iterated.
107          * @param {Function} fn The function to be called for each iteration. See and {@link Ext.Array#each Ext.Array.each} and
108          * {@link Ext.Object#each Ext.Object.each} for detailed lists of arguments passed to this function depending on the given object
109          * type that is being iterated.
110          * @param {Object} scope (Optional) The scope (`this` reference) in which the specified function is executed.
111          * Defaults to the object being iterated itself.
112          * @markdown
113          */
114         iterate: function(object, fn, scope) {
115             if (Ext.isEmpty(object)) {
116                 return;
117             }
118
119             if (scope === undefined) {
120                 scope = object;
121             }
122
123             if (Ext.isIterable(object)) {
124                 Ext.Array.each.call(Ext.Array, object, fn, scope);
125             }
126             else {
127                 Ext.Object.each.call(Ext.Object, object, fn, scope);
128             }
129         }
130     });
131
132     Ext.apply(Ext, {
133
134         /**
135          * This method deprecated. Use {@link Ext#define Ext.define} instead.
136          * @method
137          * @param {Function} superclass
138          * @param {Object} overrides
139          * @return {Function} The subclass constructor from the <tt>overrides</tt> parameter, or a generated one if not provided.
140          * @deprecated 4.0.0 Use {@link Ext#define Ext.define} instead
141          */
142         extend: function() {
143             // inline overrides
144             var objectConstructor = objectPrototype.constructor,
145                 inlineOverrides = function(o) {
146                 for (var m in o) {
147                     if (!o.hasOwnProperty(m)) {
148                         continue;
149                     }
150                     this[m] = o[m];
151                 }
152             };
153
154             return function(subclass, superclass, overrides) {
155                 // First we check if the user passed in just the superClass with overrides
156                 if (Ext.isObject(superclass)) {
157                     overrides = superclass;
158                     superclass = subclass;
159                     subclass = overrides.constructor !== objectConstructor ? overrides.constructor : function() {
160                         superclass.apply(this, arguments);
161                     };
162                 }
163
164                 //<debug>
165                 if (!superclass) {
166                     Ext.Error.raise({
167                         sourceClass: 'Ext',
168                         sourceMethod: 'extend',
169                         msg: 'Attempting to extend from a class which has not been loaded on the page.'
170                     });
171                 }
172                 //</debug>
173
174                 // We create a new temporary class
175                 var F = function() {},
176                     subclassProto, superclassProto = superclass.prototype;
177
178                 F.prototype = superclassProto;
179                 subclassProto = subclass.prototype = new F();
180                 subclassProto.constructor = subclass;
181                 subclass.superclass = superclassProto;
182
183                 if (superclassProto.constructor === objectConstructor) {
184                     superclassProto.constructor = superclass;
185                 }
186
187                 subclass.override = function(overrides) {
188                     Ext.override(subclass, overrides);
189                 };
190
191                 subclassProto.override = inlineOverrides;
192                 subclassProto.proto = subclassProto;
193
194                 subclass.override(overrides);
195                 subclass.extend = function(o) {
196                     return Ext.extend(subclass, o);
197                 };
198
199                 return subclass;
200             };
201         }(),
202
203         /**
204          * Proxy to {@link Ext.Base#override}. Please refer {@link Ext.Base#override} for further details.
205
206     Ext.define('My.cool.Class', {
207         sayHi: function() {
208             alert('Hi!');
209         }
210     }
211
212     Ext.override(My.cool.Class, {
213         sayHi: function() {
214             alert('About to say...');
215
216             this.callOverridden();
217         }
218     });
219
220     var cool = new My.cool.Class();
221     cool.sayHi(); // alerts 'About to say...'
222                   // alerts 'Hi!'
223
224          * Please note that `this.callOverridden()` only works if the class was previously
225          * created with {@link Ext#define)
226          *
227          * @param {Object} cls The class to override
228          * @param {Object} overrides The list of functions to add to origClass. This should be specified as an object literal
229          * containing one or more methods.
230          * @method override
231          * @markdown
232          */
233         override: function(cls, overrides) {
234             if (cls.prototype.$className) {
235                 return cls.override(overrides);
236             }
237             else {
238                 Ext.apply(cls.prototype, overrides);
239             }
240         }
241     });
242
243     // A full set of static methods to do type checking
244     Ext.apply(Ext, {
245
246         /**
247          * Returns the given value itself if it's not empty, as described in {@link Ext#isEmpty}; returns the default
248          * value (second argument) otherwise.
249          *
250          * @param {Mixed} value The value to test
251          * @param {Mixed} defaultValue The value to return if the original value is empty
252          * @param {Boolean} allowBlank (optional) true to allow zero length strings to qualify as non-empty (defaults to false)
253          * @return {Mixed} value, if non-empty, else defaultValue
254          */
255         valueFrom: function(value, defaultValue, allowBlank){
256             return Ext.isEmpty(value, allowBlank) ? defaultValue : value;
257         },
258
259         /**
260          * Returns the type of the given variable in string format. List of possible values are:
261          *
262          * - `undefined`: If the given value is `undefined`
263          * - `null`: If the given value is `null`
264          * - `string`: If the given value is a string
265          * - `number`: If the given value is a number
266          * - `boolean`: If the given value is a boolean value
267          * - `date`: If the given value is a `Date` object
268          * - `function`: If the given value is a function reference
269          * - `object`: If the given value is an object
270          * - `array`: If the given value is an array
271          * - `regexp`: If the given value is a regular expression
272          * - `element`: If the given value is a DOM Element
273          * - `textnode`: If the given value is a DOM text node and contains something other than whitespace
274          * - `whitespace`: If the given value is a DOM text node and contains only whitespace
275          *
276          * @param {Mixed} value
277          * @return {String}
278          * @markdown
279          */
280         typeOf: function(value) {
281             if (value === null) {
282                 return 'null';
283             }
284
285             var type = typeof value;
286
287             if (type === 'undefined' || type === 'string' || type === 'number' || type === 'boolean') {
288                 return type;
289             }
290
291             var typeToString = toString.call(value);
292
293             switch(typeToString) {
294                 case '[object Array]':
295                     return 'array';
296                 case '[object Date]':
297                     return 'date';
298                 case '[object Boolean]':
299                     return 'boolean';
300                 case '[object Number]':
301                     return 'number';
302                 case '[object RegExp]':
303                     return 'regexp';
304             }
305
306             if (type === 'function') {
307                 return 'function';
308             }
309
310             if (type === 'object') {
311                 if (value.nodeType !== undefined) {
312                     if (value.nodeType === 3) {
313                         return (/\S/).test(value.nodeValue) ? 'textnode' : 'whitespace';
314                     }
315                     else {
316                         return 'element';
317                     }
318                 }
319
320                 return 'object';
321             }
322
323             //<debug error>
324             Ext.Error.raise({
325                 sourceClass: 'Ext',
326                 sourceMethod: 'typeOf',
327                 msg: 'Failed to determine the type of the specified value "' + value + '". This is most likely a bug.'
328             });
329             //</debug>
330         },
331
332         /**
333          * Returns true if the passed value is empty, false otherwise. The value is deemed to be empty if it is either:
334          *
335          * - `null`
336          * - `undefined`
337          * - a zero-length array
338          * - a zero-length string (Unless the `allowEmptyString` parameter is set to `true`)
339          *
340          * @param {Mixed} value The value to test
341          * @param {Boolean} allowEmptyString (optional) true to allow empty strings (defaults to false)
342          * @return {Boolean}
343          * @markdown
344          */
345         isEmpty: function(value, allowEmptyString) {
346             return (value === null) || (value === undefined) || (!allowEmptyString ? value === '' : false) || (Ext.isArray(value) && value.length === 0);
347         },
348
349         /**
350          * Returns true if the passed value is a JavaScript Array, false otherwise.
351          *
352          * @param {Mixed} target The target to test
353          * @return {Boolean}
354          * @method
355          */
356         isArray: ('isArray' in Array) ? Array.isArray : function(value) {
357             return toString.call(value) === '[object Array]';
358         },
359
360         /**
361          * Returns true if the passed value is a JavaScript Date object, false otherwise.
362          * @param {Object} object The object to test
363          * @return {Boolean}
364          */
365         isDate: function(value) {
366             return toString.call(value) === '[object Date]';
367         },
368
369         /**
370          * Returns true if the passed value is a JavaScript Object, false otherwise.
371          * @param {Mixed} value The value to test
372          * @return {Boolean}
373          * @method
374          */
375         isObject: (toString.call(null) === '[object Object]') ?
376         function(value) {
377             return value !== null && value !== undefined && toString.call(value) === '[object Object]' && value.nodeType === undefined;
378         } :
379         function(value) {
380             return toString.call(value) === '[object Object]';
381         },
382
383         /**
384          * Returns true if the passed value is a JavaScript 'primitive', a string, number or boolean.
385          * @param {Mixed} value The value to test
386          * @return {Boolean}
387          */
388         isPrimitive: function(value) {
389             var type = typeof value;
390
391             return type === 'string' || type === 'number' || type === 'boolean';
392         },
393
394         /**
395          * Returns true if the passed value is a JavaScript Function, false otherwise.
396          * @param {Mixed} value The value to test
397          * @return {Boolean}
398          * @method
399          */
400         isFunction:
401         // Safari 3.x and 4.x returns 'function' for typeof <NodeList>, hence we need to fall back to using
402         // Object.prorotype.toString (slower)
403         (typeof document !== 'undefined' && typeof document.getElementsByTagName('body') === 'function') ? function(value) {
404             return toString.call(value) === '[object Function]';
405         } : function(value) {
406             return typeof value === 'function';
407         },
408
409         /**
410          * Returns true if the passed value is a number. Returns false for non-finite numbers.
411          * @param {Mixed} value The value to test
412          * @return {Boolean}
413          */
414         isNumber: function(value) {
415             return typeof value === 'number' && isFinite(value);
416         },
417
418         /**
419          * Validates that a value is numeric.
420          * @param {Mixed} value Examples: 1, '1', '2.34'
421          * @return {Boolean} True if numeric, false otherwise
422          */
423         isNumeric: function(value) {
424             return !isNaN(parseFloat(value)) && isFinite(value);
425         },
426
427         /**
428          * Returns true if the passed value is a string.
429          * @param {Mixed} value The value to test
430          * @return {Boolean}
431          */
432         isString: function(value) {
433             return typeof value === 'string';
434         },
435
436         /**
437          * Returns true if the passed value is a boolean.
438          *
439          * @param {Mixed} value The value to test
440          * @return {Boolean}
441          */
442         isBoolean: function(value) {
443             return typeof value === 'boolean';
444         },
445
446         /**
447          * Returns true if the passed value is an HTMLElement
448          * @param {Mixed} value The value to test
449          * @return {Boolean}
450          */
451         isElement: function(value) {
452             return value ? value.nodeType === 1 : false;
453         },
454
455         /**
456          * Returns true if the passed value is a TextNode
457          * @param {Mixed} value The value to test
458          * @return {Boolean}
459          */
460         isTextNode: function(value) {
461             return value ? value.nodeName === "#text" : false;
462         },
463
464         /**
465          * Returns true if the passed value is defined.
466          * @param {Mixed} value The value to test
467          * @return {Boolean}
468          */
469         isDefined: function(value) {
470             return typeof value !== 'undefined';
471         },
472
473         /**
474          * Returns true if the passed value is iterable, false otherwise
475          * @param {Mixed} value The value to test
476          * @return {Boolean}
477          */
478         isIterable: function(value) {
479             return (value && typeof value !== 'string') ? value.length !== undefined : false;
480         }
481     });
482
483     Ext.apply(Ext, {
484
485         /**
486          * Clone almost any type of variable including array, object, DOM nodes and Date without keeping the old reference
487          * @param {Mixed} item The variable to clone
488          * @return {Mixed} clone
489          */
490         clone: function(item) {
491             if (item === null || item === undefined) {
492                 return item;
493             }
494
495             // DOM nodes
496             // TODO proxy this to Ext.Element.clone to handle automatic id attribute changing
497             // recursively
498             if (item.nodeType && item.cloneNode) {
499                 return item.cloneNode(true);
500             }
501
502             var type = toString.call(item);
503
504             // Date
505             if (type === '[object Date]') {
506                 return new Date(item.getTime());
507             }
508
509             var i, j, k, clone, key;
510
511             // Array
512             if (type === '[object Array]') {
513                 i = item.length;
514
515                 clone = [];
516
517                 while (i--) {
518                     clone[i] = Ext.clone(item[i]);
519                 }
520             }
521             // Object
522             else if (type === '[object Object]' && item.constructor === Object) {
523                 clone = {};
524
525                 for (key in item) {
526                     clone[key] = Ext.clone(item[key]);
527                 }
528
529                 if (enumerables) {
530                     for (j = enumerables.length; j--;) {
531                         k = enumerables[j];
532                         clone[k] = item[k];
533                     }
534                 }
535             }
536
537             return clone || item;
538         },
539
540         /**
541          * @private
542          * Generate a unique reference of Ext in the global scope, useful for sandboxing
543          */
544         getUniqueGlobalNamespace: function() {
545             var uniqueGlobalNamespace = this.uniqueGlobalNamespace;
546
547             if (uniqueGlobalNamespace === undefined) {
548                 var i = 0;
549
550                 do {
551                     uniqueGlobalNamespace = 'ExtSandbox' + (++i);
552                 } while (Ext.global[uniqueGlobalNamespace] !== undefined);
553
554                 Ext.global[uniqueGlobalNamespace] = Ext;
555                 this.uniqueGlobalNamespace = uniqueGlobalNamespace;
556             }
557
558             return uniqueGlobalNamespace;
559         },
560
561         /**
562          * @private
563          */
564         functionFactory: function() {
565             var args = Array.prototype.slice.call(arguments);
566
567             if (args.length > 0) {
568                 args[args.length - 1] = 'var Ext=window.' + this.getUniqueGlobalNamespace() + ';' +
569                     args[args.length - 1];
570             }
571
572             return Function.prototype.constructor.apply(Function.prototype, args);
573         }
574     });
575
576     /**
577      * Old alias to {@link Ext#typeOf}
578      * @deprecated 4.0.0 Use {@link Ext#typeOf} instead
579      * @method
580      */
581     Ext.type = Ext.typeOf;
582
583 })();