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