Upgrade to ExtJS 4.0.7 - Released 10/19/2011
[extjs.git] / src / core / src / lang / Object.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.Object
19  *
20  * A collection of useful static methods to deal with objects.
21  *
22  * @singleton
23  */
24
25 (function() {
26
27 var ExtObject = Ext.Object = {
28
29     /**
30      * Converts a `name` - `value` pair to an array of objects with support for nested structures. Useful to construct
31      * query strings. For example:
32      *
33      *     var objects = Ext.Object.toQueryObjects('hobbies', ['reading', 'cooking', 'swimming']);
34      *
35      *     // objects then equals:
36      *     [
37      *         { name: 'hobbies', value: 'reading' },
38      *         { name: 'hobbies', value: 'cooking' },
39      *         { name: 'hobbies', value: 'swimming' },
40      *     ];
41      *
42      *     var objects = Ext.Object.toQueryObjects('dateOfBirth', {
43      *         day: 3,
44      *         month: 8,
45      *         year: 1987,
46      *         extra: {
47      *             hour: 4
48      *             minute: 30
49      *         }
50      *     }, true); // Recursive
51      *
52      *     // objects then equals:
53      *     [
54      *         { name: 'dateOfBirth[day]', value: 3 },
55      *         { name: 'dateOfBirth[month]', value: 8 },
56      *         { name: 'dateOfBirth[year]', value: 1987 },
57      *         { name: 'dateOfBirth[extra][hour]', value: 4 },
58      *         { name: 'dateOfBirth[extra][minute]', value: 30 },
59      *     ];
60      *
61      * @param {String} name
62      * @param {Object/Array} value
63      * @param {Boolean} [recursive=false] True to traverse object recursively
64      * @return {Array}
65      */
66     toQueryObjects: function(name, value, recursive) {
67         var self = ExtObject.toQueryObjects,
68             objects = [],
69             i, ln;
70
71         if (Ext.isArray(value)) {
72             for (i = 0, ln = value.length; i < ln; i++) {
73                 if (recursive) {
74                     objects = objects.concat(self(name + '[' + i + ']', value[i], true));
75                 }
76                 else {
77                     objects.push({
78                         name: name,
79                         value: value[i]
80                     });
81                 }
82             }
83         }
84         else if (Ext.isObject(value)) {
85             for (i in value) {
86                 if (value.hasOwnProperty(i)) {
87                     if (recursive) {
88                         objects = objects.concat(self(name + '[' + i + ']', value[i], true));
89                     }
90                     else {
91                         objects.push({
92                             name: name,
93                             value: value[i]
94                         });
95                     }
96                 }
97             }
98         }
99         else {
100             objects.push({
101                 name: name,
102                 value: value
103             });
104         }
105
106         return objects;
107     },
108
109     /**
110      * Takes an object and converts it to an encoded query string.
111      *
112      * Non-recursive:
113      *
114      *     Ext.Object.toQueryString({foo: 1, bar: 2}); // returns "foo=1&bar=2"
115      *     Ext.Object.toQueryString({foo: null, bar: 2}); // returns "foo=&bar=2"
116      *     Ext.Object.toQueryString({'some price': '$300'}); // returns "some%20price=%24300"
117      *     Ext.Object.toQueryString({date: new Date(2011, 0, 1)}); // returns "date=%222011-01-01T00%3A00%3A00%22"
118      *     Ext.Object.toQueryString({colors: ['red', 'green', 'blue']}); // returns "colors=red&colors=green&colors=blue"
119      *
120      * Recursive:
121      *
122      *     Ext.Object.toQueryString({
123      *         username: 'Jacky',
124      *         dateOfBirth: {
125      *             day: 1,
126      *             month: 2,
127      *             year: 1911
128      *         },
129      *         hobbies: ['coding', 'eating', 'sleeping', ['nested', 'stuff']]
130      *     }, true); // returns the following string (broken down and url-decoded for ease of reading purpose):
131      *     // username=Jacky
132      *     //    &dateOfBirth[day]=1&dateOfBirth[month]=2&dateOfBirth[year]=1911
133      *     //    &hobbies[0]=coding&hobbies[1]=eating&hobbies[2]=sleeping&hobbies[3][0]=nested&hobbies[3][1]=stuff
134      *
135      * @param {Object} object The object to encode
136      * @param {Boolean} [recursive=false] Whether or not to interpret the object in recursive format.
137      * (PHP / Ruby on Rails servers and similar).
138      * @return {String} queryString
139      */
140     toQueryString: function(object, recursive) {
141         var paramObjects = [],
142             params = [],
143             i, j, ln, paramObject, value;
144
145         for (i in object) {
146             if (object.hasOwnProperty(i)) {
147                 paramObjects = paramObjects.concat(ExtObject.toQueryObjects(i, object[i], recursive));
148             }
149         }
150
151         for (j = 0, ln = paramObjects.length; j < ln; j++) {
152             paramObject = paramObjects[j];
153             value = paramObject.value;
154
155             if (Ext.isEmpty(value)) {
156                 value = '';
157             }
158             else if (Ext.isDate(value)) {
159                 value = Ext.Date.toString(value);
160             }
161
162             params.push(encodeURIComponent(paramObject.name) + '=' + encodeURIComponent(String(value)));
163         }
164
165         return params.join('&');
166     },
167
168     /**
169      * Converts a query string back into an object.
170      *
171      * Non-recursive:
172      *
173      *     Ext.Object.fromQueryString(foo=1&bar=2); // returns {foo: 1, bar: 2}
174      *     Ext.Object.fromQueryString(foo=&bar=2); // returns {foo: null, bar: 2}
175      *     Ext.Object.fromQueryString(some%20price=%24300); // returns {'some price': '$300'}
176      *     Ext.Object.fromQueryString(colors=red&colors=green&colors=blue); // returns {colors: ['red', 'green', 'blue']}
177      *
178      * Recursive:
179      *
180      *       Ext.Object.fromQueryString("username=Jacky&dateOfBirth[day]=1&dateOfBirth[month]=2&dateOfBirth[year]=1911&hobbies[0]=coding&hobbies[1]=eating&hobbies[2]=sleeping&hobbies[3][0]=nested&hobbies[3][1]=stuff", true);
181      *     // returns
182      *     {
183      *         username: 'Jacky',
184      *         dateOfBirth: {
185      *             day: '1',
186      *             month: '2',
187      *             year: '1911'
188      *         },
189      *         hobbies: ['coding', 'eating', 'sleeping', ['nested', 'stuff']]
190      *     }
191      *
192      * @param {String} queryString The query string to decode
193      * @param {Boolean} [recursive=false] Whether or not to recursively decode the string. This format is supported by
194      * PHP / Ruby on Rails servers and similar.
195      * @return {Object}
196      */
197     fromQueryString: function(queryString, recursive) {
198         var parts = queryString.replace(/^\?/, '').split('&'),
199             object = {},
200             temp, components, name, value, i, ln,
201             part, j, subLn, matchedKeys, matchedName,
202             keys, key, nextKey;
203
204         for (i = 0, ln = parts.length; i < ln; i++) {
205             part = parts[i];
206
207             if (part.length > 0) {
208                 components = part.split('=');
209                 name = decodeURIComponent(components[0]);
210                 value = (components[1] !== undefined) ? decodeURIComponent(components[1]) : '';
211
212                 if (!recursive) {
213                     if (object.hasOwnProperty(name)) {
214                         if (!Ext.isArray(object[name])) {
215                             object[name] = [object[name]];
216                         }
217
218                         object[name].push(value);
219                     }
220                     else {
221                         object[name] = value;
222                     }
223                 }
224                 else {
225                     matchedKeys = name.match(/(\[):?([^\]]*)\]/g);
226                     matchedName = name.match(/^([^\[]+)/);
227
228                     //<debug error>
229                     if (!matchedName) {
230                         Ext.Error.raise({
231                             sourceClass: "Ext.Object",
232                             sourceMethod: "fromQueryString",
233                             queryString: queryString,
234                             recursive: recursive,
235                             msg: 'Malformed query string given, failed parsing name from "' + part + '"'
236                         });
237                     }
238                     //</debug>
239
240                     name = matchedName[0];
241                     keys = [];
242
243                     if (matchedKeys === null) {
244                         object[name] = value;
245                         continue;
246                     }
247
248                     for (j = 0, subLn = matchedKeys.length; j < subLn; j++) {
249                         key = matchedKeys[j];
250                         key = (key.length === 2) ? '' : key.substring(1, key.length - 1);
251                         keys.push(key);
252                     }
253
254                     keys.unshift(name);
255
256                     temp = object;
257
258                     for (j = 0, subLn = keys.length; j < subLn; j++) {
259                         key = keys[j];
260
261                         if (j === subLn - 1) {
262                             if (Ext.isArray(temp) && key === '') {
263                                 temp.push(value);
264                             }
265                             else {
266                                 temp[key] = value;
267                             }
268                         }
269                         else {
270                             if (temp[key] === undefined || typeof temp[key] === 'string') {
271                                 nextKey = keys[j+1];
272
273                                 temp[key] = (Ext.isNumeric(nextKey) || nextKey === '') ? [] : {};
274                             }
275
276                             temp = temp[key];
277                         }
278                     }
279                 }
280             }
281         }
282
283         return object;
284     },
285
286     /**
287      * Iterates through an object and invokes the given callback function for each iteration.
288      * The iteration can be stopped by returning `false` in the callback function. For example:
289      *
290      *     var person = {
291      *         name: 'Jacky'
292      *         hairColor: 'black'
293      *         loves: ['food', 'sleeping', 'wife']
294      *     };
295      *
296      *     Ext.Object.each(person, function(key, value, myself) {
297      *         console.log(key + ":" + value);
298      *
299      *         if (key === 'hairColor') {
300      *             return false; // stop the iteration
301      *         }
302      *     });
303      *
304      * @param {Object} object The object to iterate
305      * @param {Function} fn The callback function.
306      * @param {String} fn.key
307      * @param {Object} fn.value
308      * @param {Object} fn.object The object itself
309      * @param {Object} [scope] The execution scope (`this`) of the callback function
310      */
311     each: function(object, fn, scope) {
312         for (var property in object) {
313             if (object.hasOwnProperty(property)) {
314                 if (fn.call(scope || object, property, object[property], object) === false) {
315                     return;
316                 }
317             }
318         }
319     },
320
321     /**
322      * Merges any number of objects recursively without referencing them or their children.
323      *
324      *     var extjs = {
325      *         companyName: 'Ext JS',
326      *         products: ['Ext JS', 'Ext GWT', 'Ext Designer'],
327      *         isSuperCool: true
328      *         office: {
329      *             size: 2000,
330      *             location: 'Palo Alto',
331      *             isFun: true
332      *         }
333      *     };
334      *
335      *     var newStuff = {
336      *         companyName: 'Sencha Inc.',
337      *         products: ['Ext JS', 'Ext GWT', 'Ext Designer', 'Sencha Touch', 'Sencha Animator'],
338      *         office: {
339      *             size: 40000,
340      *             location: 'Redwood City'
341      *         }
342      *     };
343      *
344      *     var sencha = Ext.Object.merge(extjs, newStuff);
345      *
346      *     // extjs and sencha then equals to
347      *     {
348      *         companyName: 'Sencha Inc.',
349      *         products: ['Ext JS', 'Ext GWT', 'Ext Designer', 'Sencha Touch', 'Sencha Animator'],
350      *         isSuperCool: true
351      *         office: {
352      *             size: 30000,
353      *             location: 'Redwood City'
354      *             isFun: true
355      *         }
356      *     }
357      *
358      * @param {Object...} object Any number of objects to merge.
359      * @return {Object} merged The object that is created as a result of merging all the objects passed in.
360      */
361     merge: function(source, key, value) {
362         if (typeof key === 'string') {
363             if (value && value.constructor === Object) {
364                 if (source[key] && source[key].constructor === Object) {
365                     ExtObject.merge(source[key], value);
366                 }
367                 else {
368                     source[key] = Ext.clone(value);
369                 }
370             }
371             else {
372                 source[key] = value;
373             }
374
375             return source;
376         }
377
378         var i = 1,
379             ln = arguments.length,
380             object, property;
381
382         for (; i < ln; i++) {
383             object = arguments[i];
384
385             for (property in object) {
386                 if (object.hasOwnProperty(property)) {
387                     ExtObject.merge(source, property, object[property]);
388                 }
389             }
390         }
391
392         return source;
393     },
394
395     /**
396      * Returns the first matching key corresponding to the given value.
397      * If no matching value is found, null is returned.
398      *
399      *     var person = {
400      *         name: 'Jacky',
401      *         loves: 'food'
402      *     };
403      *
404      *     alert(Ext.Object.getKey(person, 'food')); // alerts 'loves'
405      *
406      * @param {Object} object
407      * @param {Object} value The value to find
408      */
409     getKey: function(object, value) {
410         for (var property in object) {
411             if (object.hasOwnProperty(property) && object[property] === value) {
412                 return property;
413             }
414         }
415
416         return null;
417     },
418
419     /**
420      * Gets all values of the given object as an array.
421      *
422      *     var values = Ext.Object.getValues({
423      *         name: 'Jacky',
424      *         loves: 'food'
425      *     }); // ['Jacky', 'food']
426      *
427      * @param {Object} object
428      * @return {Array} An array of values from the object
429      */
430     getValues: function(object) {
431         var values = [],
432             property;
433
434         for (property in object) {
435             if (object.hasOwnProperty(property)) {
436                 values.push(object[property]);
437             }
438         }
439
440         return values;
441     },
442
443     /**
444      * Gets all keys of the given object as an array.
445      *
446      *     var values = Ext.Object.getKeys({
447      *         name: 'Jacky',
448      *         loves: 'food'
449      *     }); // ['name', 'loves']
450      *
451      * @param {Object} object
452      * @return {String[]} An array of keys from the object
453      * @method
454      */
455     getKeys: ('keys' in Object.prototype) ? Object.keys : function(object) {
456         var keys = [],
457             property;
458
459         for (property in object) {
460             if (object.hasOwnProperty(property)) {
461                 keys.push(property);
462             }
463         }
464
465         return keys;
466     },
467
468     /**
469      * Gets the total number of this object's own properties
470      *
471      *     var size = Ext.Object.getSize({
472      *         name: 'Jacky',
473      *         loves: 'food'
474      *     }); // size equals 2
475      *
476      * @param {Object} object
477      * @return {Number} size
478      */
479     getSize: function(object) {
480         var size = 0,
481             property;
482
483         for (property in object) {
484             if (object.hasOwnProperty(property)) {
485                 size++;
486             }
487         }
488
489         return size;
490     }
491 };
492
493
494 /**
495  * A convenient alias method for {@link Ext.Object#merge}.
496  *
497  * @member Ext
498  * @method merge
499  * @alias Ext.Object#merge
500  */
501 Ext.merge = Ext.Object.merge;
502
503 /**
504  * Alias for {@link Ext.Object#toQueryString}.
505  *
506  * @member Ext
507  * @method urlEncode
508  * @alias Ext.Object#toQueryString
509  * @deprecated 4.0.0 Use {@link Ext.Object#toQueryString} instead
510  */
511 Ext.urlEncode = function() {
512     var args = Ext.Array.from(arguments),
513         prefix = '';
514
515     // Support for the old `pre` argument
516     if ((typeof args[1] === 'string')) {
517         prefix = args[1] + '&';
518         args[1] = false;
519     }
520
521     return prefix + Ext.Object.toQueryString.apply(Ext.Object, args);
522 };
523
524 /**
525  * Alias for {@link Ext.Object#fromQueryString}.
526  *
527  * @member Ext
528  * @method urlDecode
529  * @alias Ext.Object#fromQueryString
530  * @deprecated 4.0.0 Use {@link Ext.Object#fromQueryString} instead
531  */
532 Ext.urlDecode = function() {
533     return Ext.Object.fromQueryString.apply(Ext.Object, arguments);
534 };
535
536 })();
537