Upgrade to ExtJS 3.0.0 - Released 07/06/2009
[extjs.git] / src / core / Ext-more.js
1 /*!
2  * Ext JS Library 3.0.0
3  * Copyright(c) 2006-2009 Ext JS, LLC
4  * licensing@extjs.com
5  * http://www.extjs.com/license
6  */
7 /**
8  * @class Ext
9  */
10
11 Ext.ns("Ext.grid", "Ext.dd", "Ext.tree", "Ext.form", "Ext.menu",
12        "Ext.state", "Ext.layout", "Ext.app", "Ext.ux", "Ext.chart", "Ext.direct");
13     /**
14      * Namespace alloted for extensions to the framework.
15      * @property ux
16      * @type Object
17      */
18
19 Ext.apply(Ext, function(){
20     var E = Ext, idSeed = 0;
21
22     return {
23         /**
24         * A reusable empty function
25         * @property
26         * @type Function
27         */
28         emptyFn : function(){},
29
30         /**
31          * URL to a 1x1 transparent gif image used by Ext to create inline icons with CSS background images. 
32          * In older versions of IE, this defaults to "http://extjs.com/s.gif" and you should change this to a URL on your server.
33          * For other browsers it uses an inline data URL.
34          * @type String
35          */
36         BLANK_IMAGE_URL : Ext.isIE6 || Ext.isIE7 ?
37                             'http:/' + '/extjs.com/s.gif' :
38                             'data:image/gif;base64,R0lGODlhAQABAID/AMDAwAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==',
39
40         extendX : function(supr, fn){
41             return Ext.extend(supr, fn(supr.prototype));
42         },
43
44         /**
45          * Returns the current HTML document object as an {@link Ext.Element}.
46          * @return Ext.Element The document
47          */
48         getDoc : function(){
49             return Ext.get(document);
50         },
51
52         /**
53          * Returns true if the passed object is a JavaScript date object, otherwise false.
54          * @param {Object} object The object to test
55          * @return {Boolean}
56          */
57         isDate : function(v){
58             return Object.prototype.toString.apply(v) === '[object Date]';
59         },
60
61         /**
62          * Utility method for validating that a value is numeric, returning the specified default value if it is not.
63          * @param {Mixed} value Should be a number, but any type will be handled appropriately
64          * @param {Number} defaultValue The value to return if the original value is non-numeric
65          * @return {Number} Value, if numeric, else defaultValue
66          */
67         num : function(v, defaultValue){
68             v = Number(v === null || typeof v == 'boolean'? NaN : v);
69             return isNaN(v)? defaultValue : v;
70         },
71
72         /**
73          * <p>Utility method for returning a default value if the passed value is empty.</p>
74          * <p>The value is deemed to be empty if it is<div class="mdetail-params"><ul>
75          * <li>null</li>
76          * <li>undefined</li>
77          * <li>an empty array</li>
78          * <li>a zero length string (Unless the <tt>allowBlank</tt> parameter is <tt>true</tt>)</li>
79          * </ul></div>
80          * @param {Mixed} value The value to test
81          * @param {Mixed} defaultValue The value to return if the original value is empty
82          * @param {Boolean} allowBlank (optional) true to allow zero length strings to qualify as non-empty (defaults to false)
83          * @return {Mixed} value, if non-empty, else defaultValue
84          */
85         value : function(v, defaultValue, allowBlank){
86             return Ext.isEmpty(v, allowBlank) ? defaultValue : v;
87         },
88
89         /**
90          * Escapes the passed string for use in a regular expression
91          * @param {String} str
92          * @return {String}
93          */
94         escapeRe : function(s) {
95             return s.replace(/([.*+?^${}()|[\]\/\\])/g, "\\$1");
96         },
97
98         sequence : function(o, name, fn, scope){
99             o[name] = o[name].createSequence(fn, scope);
100         },
101
102         /**
103          * Applies event listeners to elements by selectors when the document is ready.
104          * The event name is specified with an <tt>&#64;</tt> suffix.
105          * <pre><code>
106 Ext.addBehaviors({
107     // add a listener for click on all anchors in element with id foo
108     '#foo a&#64;click' : function(e, t){
109         // do something
110     },
111     
112     // add the same listener to multiple selectors (separated by comma BEFORE the &#64;)
113     '#foo a, #bar span.some-class&#64;mouseover' : function(){
114         // do something
115     }
116 });
117          * </code></pre> 
118          * @param {Object} obj The list of behaviors to apply
119          */
120         addBehaviors : function(o){
121             if(!Ext.isReady){
122                 Ext.onReady(function(){
123                     Ext.addBehaviors(o);
124                 });
125             } else {
126                 var cache = {}, // simple cache for applying multiple behaviors to same selector does query multiple times
127                     parts,
128                     b,
129                     s;
130                 for (b in o) {
131                     if ((parts = b.split('@'))[1]) { // for Object prototype breakers
132                         s = parts[0];
133                         if(!cache[s]){
134                             cache[s] = Ext.select(s);
135                         }
136                         cache[s].on(parts[1], o[b]);
137                     }
138                 }
139                 cache = null;
140             }
141         },
142
143
144         // deprecated
145         combine : function(){
146             var as = arguments, l = as.length, r = [];
147             for(var i = 0; i < l; i++){
148                 var a = as[i];
149                 if(Ext.isArray(a)){
150                     r = r.concat(a);
151                 }else if(a.length !== undefined && !a.substr){
152                     r = r.concat(Array.prototype.slice.call(a, 0));
153                 }else{
154                     r.push(a);
155                 }
156             }
157             return r;
158         },
159
160         /**
161          * Copies a set of named properties fom the source object to the destination object.
162          * <p>example:<pre><code>
163 ImageComponent = Ext.extend(Ext.BoxComponent, {
164     initComponent: function() {
165         this.autoEl = { tag: 'img' };
166         MyComponent.superclass.initComponent.apply(this, arguments);
167         this.initialBox = Ext.copyTo({}, this.initialConfig, 'x,y,width,height');
168     }
169 });
170          * </code></pre> 
171          * @param {Object} The destination object.
172          * @param {Object} The source object.
173          * @param {Array/String} Either an Array of property names, or a comma-delimited list
174          * of property names to copy.
175          * @return {Object} The modified object.
176         */
177         copyTo : function(dest, source, names){
178             if(typeof names == 'string'){
179                 names = names.split(/[,;\s]/);
180             }
181             Ext.each(names, function(name){
182                 if(source.hasOwnProperty(name)){
183                     dest[name] = source[name];
184                 }
185             }, this);
186             return dest;
187         },
188
189         /**
190          * Attempts to destroy any objects passed to it by removing all event listeners, removing them from the
191          * DOM (if applicable) and calling their destroy functions (if available).  This method is primarily
192          * intended for arguments of type {@link Ext.Element} and {@link Ext.Component}, but any subclass of
193          * {@link Ext.util.Observable} can be passed in.  Any number of elements and/or components can be
194          * passed into this function in a single call as separate arguments.
195          * @param {Mixed} arg1 An {@link Ext.Element}, {@link Ext.Component}, or an Array of either of these to destroy
196          * @param {Mixed} arg2 (optional)
197          * @param {Mixed} etc... (optional)
198          */
199         destroy : function(){
200             Ext.each(arguments, function(arg){
201                 if(arg){
202                     if(Ext.isArray(arg)){
203                         this.destroy.apply(this, arg);
204                     }else if(Ext.isFunction(arg.destroy)){
205                         arg.destroy();
206                     }else if(arg.dom){
207                         arg.remove();
208                     }    
209                 }
210             }, this);
211         },
212
213         /**
214          * Attempts to destroy and then remove a set of named properties of the passed object.
215          * @param {Object} o The object (most likely a Component) who's properties you wish to destroy.
216          * @param {Mixed} arg1 The name of the property to destroy and remove from the object.
217          * @param {Mixed} etc... More property names to destroy and remove.
218          */
219         destroyMembers : function(o, arg1, arg2, etc){
220             for(var i = 1, a = arguments, len = a.length; i < len; i++) {
221                 Ext.destroy(o[a[i]]);
222                 delete o[a[i]];
223             }
224         },
225
226         /**
227          * Creates a copy of the passed Array with falsy values removed.
228          * @param {Array/NodeList} arr The Array from which to remove falsy values.
229          * @return {Array} The new, compressed Array.
230          */
231         clean : function(arr){
232             var ret = [];
233             Ext.each(arr, function(v){
234                 if(!!v){
235                     ret.push(v);
236                 }
237             });
238             return ret;
239         },
240
241         /**
242          * Creates a copy of the passed Array, filtered to contain only unique values.
243          * @param {Array} arr The Array to filter
244          * @return {Array} The new Array containing unique values.
245          */
246         unique : function(arr){
247             var ret = [],
248                 collect = {};
249
250             Ext.each(arr, function(v) {
251                 if(!collect[v]){
252                     ret.push(v);
253                 }
254                 collect[v] = true;
255             });
256             return ret;
257         },
258
259         /**
260          * Recursively flattens into 1-d Array. Injects Arrays inline.
261          * @param {Array} arr The array to flatten
262          * @return {Array} The new, flattened array.
263          */
264         flatten : function(arr){
265             var worker = [];
266             function rFlatten(a) {
267                 Ext.each(a, function(v) {
268                     if(Ext.isArray(v)){
269                         rFlatten(v);
270                     }else{
271                         worker.push(v);
272                     }
273                 });
274                 return worker;
275             }
276             return rFlatten(arr);
277         },
278
279         /**
280          * Returns the minimum value in the Array.
281          * @param {Array|NodeList} arr The Array from which to select the minimum value.
282          * @param {Function} comp (optional) a function to perform the comparision which determines minimization.
283          *                   If omitted the "<" operator will be used. Note: gt = 1; eq = 0; lt = -1
284          * @return {Object} The minimum value in the Array.
285          */
286         min : function(arr, comp){
287             var ret = arr[0];
288             comp = comp || function(a,b){ return a < b ? -1 : 1; };
289             Ext.each(arr, function(v) {
290                 ret = comp(ret, v) == -1 ? ret : v;
291             });
292             return ret;
293         },
294
295         /**
296          * Returns the maximum value in the Array
297          * @param {Array|NodeList} arr The Array from which to select the maximum value.
298          * @param {Function} comp (optional) a function to perform the comparision which determines maximization.
299          *                   If omitted the ">" operator will be used. Note: gt = 1; eq = 0; lt = -1
300          * @return {Object} The maximum value in the Array.
301          */
302         max : function(arr, comp){
303             var ret = arr[0];
304             comp = comp || function(a,b){ return a > b ? 1 : -1; };
305             Ext.each(arr, function(v) {
306                 ret = comp(ret, v) == 1 ? ret : v;
307             });
308             return ret;
309         },
310
311         /**
312          * Calculates the mean of the Array
313          * @param {Array} arr The Array to calculate the mean value of.
314          * @return {Number} The mean.
315          */
316         mean : function(arr){
317            return Ext.sum(arr) / arr.length;
318         },
319
320         /**
321          * Calculates the sum of the Array
322          * @param {Array} arr The Array to calculate the sum value of.
323          * @return {Number} The sum.
324          */
325         sum : function(arr){
326            var ret = 0;
327            Ext.each(arr, function(v) {
328                ret += v;
329            });
330            return ret;
331         },
332
333         /**
334          * Partitions the set into two sets: a true set and a false set.
335          * Example: 
336          * Example2: 
337          * <pre><code>
338 // Example 1:
339 Ext.partition([true, false, true, true, false]); // [[true, true, true], [false, false]]
340
341 // Example 2:
342 Ext.partition(
343     Ext.query("p"),
344     function(val){
345         return val.className == "class1"
346     }
347 );
348 // true are those paragraph elements with a className of "class1",
349 // false set are those that do not have that className.
350          * </code></pre>
351          * @param {Array|NodeList} arr The array to partition
352          * @param {Function} truth (optional) a function to determine truth.  If this is omitted the element
353          *                   itself must be able to be evaluated for its truthfulness.
354          * @return {Array} [true<Array>,false<Array>]
355          */
356         partition : function(arr, truth){
357             var ret = [[],[]];
358             Ext.each(arr, function(v, i, a) {
359                 ret[ (truth && truth(v, i, a)) || (!truth && v) ? 0 : 1].push(v);
360             });
361             return ret;
362         },
363
364         /**
365          * Invokes a method on each item in an Array.
366          * <pre><code>
367 // Example:
368 Ext.invoke(Ext.query("p"), "getAttribute", "id");
369 // [el1.getAttribute("id"), el2.getAttribute("id"), ..., elN.getAttribute("id")]
370          * </code></pre>
371          * @param {Array|NodeList} arr The Array of items to invoke the method on.
372          * @param {String} methodName The method name to invoke.
373          * @param {Anything} ... Arguments to send into the method invocation.
374          * @return {Array} The results of invoking the method on each item in the array.
375          */
376         invoke : function(arr, methodName){
377             var ret = [],
378                 args = Array.prototype.slice.call(arguments, 2);
379             Ext.each(arr, function(v,i) {
380                 if (v && typeof v[methodName] == "function") {
381                     ret.push(v[methodName].apply(v, args));
382                 } else {
383                     ret.push(undefined);
384                 }
385             });
386             return ret;
387         },
388
389         /**
390          * Plucks the value of a property from each item in the Array
391          * <pre><code>
392 // Example:
393 Ext.pluck(Ext.query("p"), "className"); // [el1.className, el2.className, ..., elN.className]
394          * </code></pre>
395          * @param {Array|NodeList} arr The Array of items to pluck the value from.
396          * @param {String} prop The property name to pluck from each element.
397          * @return {Array} The value from each item in the Array.
398          */
399         pluck : function(arr, prop){
400             var ret = [];
401             Ext.each(arr, function(v) {
402                 ret.push( v[prop] );
403             });
404             return ret;
405         },
406
407         /**
408          * <p>Zips N sets together.</p>
409          * <pre><code>
410 // Example 1:
411 Ext.zip([1,2,3],[4,5,6]); // [[1,4],[2,5],[3,6]]
412 // Example 2:
413 Ext.zip(
414     [ "+", "-", "+"],
415     [  12,  10,  22],
416     [  43,  15,  96],
417     function(a, b, c){
418         return "$" + a + "" + b + "." + c
419     }
420 ); // ["$+12.43", "$-10.15", "$+22.96"]
421          * </code></pre>
422          * @param {Arrays|NodeLists} arr This argument may be repeated. Array(s) to contribute values.
423          * @param {Function} zipper (optional) The last item in the argument list. This will drive how the items are zipped together.
424          * @return {Array} The zipped set.
425          */
426         zip : function(){
427             var parts = Ext.partition(arguments, function( val ){ return !Ext.isFunction(val); }),
428                 arrs = parts[0],
429                 fn = parts[1][0],
430                 len = Ext.max(Ext.pluck(arrs, "length")),
431                 ret = [];
432
433             for (var i = 0; i < len; i++) {
434                 ret[i] = [];
435                 if(fn){
436                     ret[i] = fn.apply(fn, Ext.pluck(arrs, i));
437                 }else{
438                     for (var j = 0, aLen = arrs.length; j < aLen; j++){
439                         ret[i].push( arrs[j][i] );
440                     }
441                 }
442             }
443             return ret;
444         },
445
446         /**
447          * This is shorthand reference to {@link Ext.ComponentMgr#get}.
448          * Looks up an existing {@link Ext.Component Component} by {@link Ext.Component#id id}
449          * @param {String} id The component {@link Ext.Component#id id}
450          * @return Ext.Component The Component, <tt>undefined</tt> if not found, or <tt>null</tt> if a
451          * Class was found.
452         */
453         getCmp : function(id){
454             return Ext.ComponentMgr.get(id);
455         },
456
457         /**
458          * By default, Ext intelligently decides whether floating elements should be shimmed. If you are using flash,
459          * you may want to set this to true.
460          * @type Boolean
461          */
462         useShims: E.isIE6 || (E.isMac && E.isGecko2),
463
464         // inpired by a similar function in mootools library
465         /**
466          * Returns the type of object that is passed in. If the object passed in is null or undefined it
467          * return false otherwise it returns one of the following values:<div class="mdetail-params"><ul>
468          * <li><b>string</b>: If the object passed is a string</li>
469          * <li><b>number</b>: If the object passed is a number</li>
470          * <li><b>boolean</b>: If the object passed is a boolean value</li>
471          * <li><b>date</b>: If the object passed is a Date object</li>
472          * <li><b>function</b>: If the object passed is a function reference</li>
473          * <li><b>object</b>: If the object passed is an object</li>
474          * <li><b>array</b>: If the object passed is an array</li>
475          * <li><b>regexp</b>: If the object passed is a regular expression</li>
476          * <li><b>element</b>: If the object passed is a DOM Element</li>
477          * <li><b>nodelist</b>: If the object passed is a DOM NodeList</li>
478          * <li><b>textnode</b>: If the object passed is a DOM text node and contains something other than whitespace</li>
479          * <li><b>whitespace</b>: If the object passed is a DOM text node and contains only whitespace</li>
480          * </ul></div>
481          * @param {Mixed} object
482          * @return {String}
483          */
484         type : function(o){
485             if(o === undefined || o === null){
486                 return false;
487             }
488             if(o.htmlElement){
489                 return 'element';
490             }
491             var t = typeof o;
492             if(t == 'object' && o.nodeName) {
493                 switch(o.nodeType) {
494                     case 1: return 'element';
495                     case 3: return (/\S/).test(o.nodeValue) ? 'textnode' : 'whitespace';
496                 }
497             }
498             if(t == 'object' || t == 'function') {
499                 switch(o.constructor) {
500                     case Array: return 'array';
501                     case RegExp: return 'regexp';
502                     case Date: return 'date';
503                 }
504                 if(typeof o.length == 'number' && typeof o.item == 'function') {
505                     return 'nodelist';
506                 }
507             }
508             return t;
509         },
510
511         intercept : function(o, name, fn, scope){
512             o[name] = o[name].createInterceptor(fn, scope);
513         },
514
515         // internal
516         callback : function(cb, scope, args, delay){
517             if(Ext.isFunction(cb)){
518                 if(delay){
519                     cb.defer(delay, scope, args || []);
520                 }else{
521                     cb.apply(scope, args || []);
522                 }
523             }
524         }
525     };
526 }());
527
528 /**
529  * @class Function
530  * These functions are available on every Function object (any JavaScript function).
531  */
532 Ext.apply(Function.prototype, {
533     /**
534      * Create a combined function call sequence of the original function + the passed function.
535      * The resulting function returns the results of the original function.
536      * The passed fcn is called with the parameters of the original function. Example usage:
537      * <pre><code>
538 var sayHi = function(name){
539     alert('Hi, ' + name);
540 }
541
542 sayHi('Fred'); // alerts "Hi, Fred"
543
544 var sayGoodbye = sayHi.createSequence(function(name){
545     alert('Bye, ' + name);
546 });
547
548 sayGoodbye('Fred'); // both alerts show
549 </code></pre>
550      * @param {Function} fcn The function to sequence
551      * @param {Object} scope (optional) The scope of the passed fcn (Defaults to scope of original function or window)
552      * @return {Function} The new function
553      */
554     createSequence : function(fcn, scope){
555         var method = this;
556         return !Ext.isFunction(fcn) ?
557                 this :
558                 function(){
559                     var retval = method.apply(this || window, arguments);
560                     fcn.apply(scope || this || window, arguments);
561                     return retval;
562                 };
563     }
564 });
565
566
567 /**
568  * @class String
569  * These functions are available as static methods on the JavaScript String object.
570  */
571 Ext.applyIf(String, {
572
573     /**
574      * Escapes the passed string for ' and \
575      * @param {String} string The string to escape
576      * @return {String} The escaped string
577      * @static
578      */
579     escape : function(string) {
580         return string.replace(/('|\\)/g, "\\$1");
581     },
582
583     /**
584      * Pads the left side of a string with a specified character.  This is especially useful
585      * for normalizing number and date strings.  Example usage:
586      * <pre><code>
587 var s = String.leftPad('123', 5, '0');
588 // s now contains the string: '00123'
589      * </code></pre>
590      * @param {String} string The original string
591      * @param {Number} size The total length of the output string
592      * @param {String} char (optional) The character with which to pad the original string (defaults to empty string " ")
593      * @return {String} The padded string
594      * @static
595      */
596     leftPad : function (val, size, ch) {
597         var result = String(val);
598         if(!ch) {
599             ch = " ";
600         }
601         while (result.length < size) {
602             result = ch + result;
603         }
604         return result;
605     }
606 });
607
608 /**
609  * Utility function that allows you to easily switch a string between two alternating values.  The passed value
610  * is compared to the current string, and if they are equal, the other value that was passed in is returned.  If
611  * they are already different, the first value passed in is returned.  Note that this method returns the new value
612  * but does not change the current string.
613  * <pre><code>
614 // alternate sort directions
615 sort = sort.toggle('ASC', 'DESC');
616
617 // instead of conditional logic:
618 sort = (sort == 'ASC' ? 'DESC' : 'ASC');
619 </code></pre>
620  * @param {String} value The value to compare to the current string
621  * @param {String} other The new value to use if the string already equals the first value passed in
622  * @return {String} The new value
623  */
624 String.prototype.toggle = function(value, other){
625     return this == value ? other : value;
626 };
627
628 /**
629  * Trims whitespace from either end of a string, leaving spaces within the string intact.  Example:
630  * <pre><code>
631 var s = '  foo bar  ';
632 alert('-' + s + '-');         //alerts "- foo bar -"
633 alert('-' + s.trim() + '-');  //alerts "-foo bar-"
634 </code></pre>
635  * @return {String} The trimmed string
636  */
637 String.prototype.trim = function(){
638     var re = /^\s+|\s+$/g;
639     return function(){ return this.replace(re, ""); };
640 }();
641
642 // here to prevent dependency on Date.js
643 /**
644  Returns the number of milliseconds between this date and date
645  @param {Date} date (optional) Defaults to now
646  @return {Number} The diff in milliseconds
647  @member Date getElapsed
648  */
649 Date.prototype.getElapsed = function(date) {
650     return Math.abs((date || new Date()).getTime()-this.getTime());
651 };
652
653
654 /**
655  * @class Number
656  */
657 Ext.applyIf(Number.prototype, {
658     /**
659      * Checks whether or not the current number is within a desired range.  If the number is already within the
660      * range it is returned, otherwise the min or max value is returned depending on which side of the range is
661      * exceeded.  Note that this method returns the constrained value but does not change the current number.
662      * @param {Number} min The minimum number in the range
663      * @param {Number} max The maximum number in the range
664      * @return {Number} The constrained value if outside the range, otherwise the current value
665      */
666     constrain : function(min, max){
667         return Math.min(Math.max(this, min), max);
668     }
669 });