Upgrade to ExtJS 4.0.2 - Released 06/09/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      * Convert 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 {Mixed} value
63      * @param {Boolean} recursive
64      * @markdown
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      *
136      * @param {Object} object The object to encode
137      * @param {Boolean} recursive (optional) Whether or not to interpret the object in recursive format.
138      * (PHP / Ruby on Rails servers and similar). Defaults to false
139      * @return {String} queryString
140      * @markdown
141      */
142     toQueryString: function(object, recursive) {
143         var paramObjects = [],
144             params = [],
145             i, j, ln, paramObject, value;
146
147         for (i in object) {
148             if (object.hasOwnProperty(i)) {
149                 paramObjects = paramObjects.concat(ExtObject.toQueryObjects(i, object[i], recursive));
150             }
151         }
152
153         for (j = 0, ln = paramObjects.length; j < ln; j++) {
154             paramObject = paramObjects[j];
155             value = paramObject.value;
156
157             if (Ext.isEmpty(value)) {
158                 value = '';
159             }
160             else if (Ext.isDate(value)) {
161                 value = Ext.Date.toString(value);
162             }
163
164             params.push(encodeURIComponent(paramObject.name) + '=' + encodeURIComponent(String(value)));
165         }
166
167         return params.join('&');
168     },
169
170     /**
171      * Converts a query string back into an object.
172      *
173 - Non-recursive:
174
175     Ext.Object.fromQueryString(foo=1&bar=2); // returns {foo: 1, bar: 2}
176     Ext.Object.fromQueryString(foo=&bar=2); // returns {foo: null, bar: 2}
177     Ext.Object.fromQueryString(some%20price=%24300); // returns {'some price': '$300'}
178     Ext.Object.fromQueryString(colors=red&colors=green&colors=blue); // returns {colors: ['red', 'green', 'blue']}
179
180 - Recursive:
181
182     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);
183
184     // returns
185     {
186         username: 'Jacky',
187         dateOfBirth: {
188             day: '1',
189             month: '2',
190             year: '1911'
191         },
192         hobbies: ['coding', 'eating', 'sleeping', ['nested', 'stuff']]
193     }
194
195      * @param {String} queryString The query string to decode
196      * @param {Boolean} recursive (Optional) Whether or not to recursively decode the string. This format is supported by
197      * PHP / Ruby on Rails servers and similar. Defaults to false
198      * @return {Object}
199      */
200     fromQueryString: function(queryString, recursive) {
201         var parts = queryString.replace(/^\?/, '').split('&'),
202             object = {},
203             temp, components, name, value, i, ln,
204             part, j, subLn, matchedKeys, matchedName,
205             keys, key, nextKey;
206
207         for (i = 0, ln = parts.length; i < ln; i++) {
208             part = parts[i];
209
210             if (part.length > 0) {
211                 components = part.split('=');
212                 name = decodeURIComponent(components[0]);
213                 value = (components[1] !== undefined) ? decodeURIComponent(components[1]) : '';
214
215                 if (!recursive) {
216                     if (object.hasOwnProperty(name)) {
217                         if (!Ext.isArray(object[name])) {
218                             object[name] = [object[name]];
219                         }
220
221                         object[name].push(value);
222                     }
223                     else {
224                         object[name] = value;
225                     }
226                 }
227                 else {
228                     matchedKeys = name.match(/(\[):?([^\]]*)\]/g);
229                     matchedName = name.match(/^([^\[]+)/);
230
231                     //<debug error>
232                     if (!matchedName) {
233                         Ext.Error.raise({
234                             sourceClass: "Ext.Object",
235                             sourceMethod: "fromQueryString",
236                             queryString: queryString,
237                             recursive: recursive,
238                             msg: 'Malformed query string given, failed parsing name from "' + part + '"'
239                         });
240                     }
241                     //</debug>
242
243                     name = matchedName[0];
244                     keys = [];
245
246                     if (matchedKeys === null) {
247                         object[name] = value;
248                         continue;
249                     }
250
251                     for (j = 0, subLn = matchedKeys.length; j < subLn; j++) {
252                         key = matchedKeys[j];
253                         key = (key.length === 2) ? '' : key.substring(1, key.length - 1);
254                         keys.push(key);
255                     }
256
257                     keys.unshift(name);
258
259                     temp = object;
260
261                     for (j = 0, subLn = keys.length; j < subLn; j++) {
262                         key = keys[j];
263
264                         if (j === subLn - 1) {
265                             if (Ext.isArray(temp) && key === '') {
266                                 temp.push(value);
267                             }
268                             else {
269                                 temp[key] = value;
270                             }
271                         }
272                         else {
273                             if (temp[key] === undefined || typeof temp[key] === 'string') {
274                                 nextKey = keys[j+1];
275
276                                 temp[key] = (Ext.isNumeric(nextKey) || nextKey === '') ? [] : {};
277                             }
278
279                             temp = temp[key];
280                         }
281                     }
282                 }
283             }
284         }
285
286         return object;
287     },
288
289     /**
290      * Iterate through an object and invoke the given callback function for each iteration. The iteration can be stop
291      * by returning `false` in the callback function. For example:
292
293     var person = {
294         name: 'Jacky'
295         hairColor: 'black'
296         loves: ['food', 'sleeping', 'wife']
297     };
298
299     Ext.Object.each(person, function(key, value, myself) {
300         console.log(key + ":" + value);
301
302         if (key === 'hairColor') {
303             return false; // stop the iteration
304         }
305     });
306
307      * @param {Object} object The object to iterate
308      * @param {Function} fn The callback function. Passed arguments for each iteration are:
309
310 - {String} `key`
311 - {Mixed} `value`
312 - {Object} `object` The object itself
313
314      * @param {Object} scope (Optional) The execution scope (`this`) of the callback function
315      * @markdown
316      */
317     each: function(object, fn, scope) {
318         for (var property in object) {
319             if (object.hasOwnProperty(property)) {
320                 if (fn.call(scope || object, property, object[property], object) === false) {
321                     return;
322                 }
323             }
324         }
325     },
326
327     /**
328      * Merges any number of objects recursively without referencing them or their children.
329
330     var extjs = {
331         companyName: 'Ext JS',
332         products: ['Ext JS', 'Ext GWT', 'Ext Designer'],
333         isSuperCool: true
334         office: {
335             size: 2000,
336             location: 'Palo Alto',
337             isFun: true
338         }
339     };
340
341     var newStuff = {
342         companyName: 'Sencha Inc.',
343         products: ['Ext JS', 'Ext GWT', 'Ext Designer', 'Sencha Touch', 'Sencha Animator'],
344         office: {
345             size: 40000,
346             location: 'Redwood City'
347         }
348     };
349
350     var sencha = Ext.Object.merge(extjs, newStuff);
351
352     // extjs and sencha then equals to
353     {
354         companyName: 'Sencha Inc.',
355         products: ['Ext JS', 'Ext GWT', 'Ext Designer', 'Sencha Touch', 'Sencha Animator'],
356         isSuperCool: true
357         office: {
358             size: 30000,
359             location: 'Redwood City'
360             isFun: true
361         }
362     }
363
364      * @param {Object} object,...
365      * @return {Object} merged The object that is created as a result of merging all the objects passed in.
366      * @markdown
367      */
368     merge: function(source, key, value) {
369         if (typeof key === 'string') {
370             if (value && value.constructor === Object) {
371                 if (source[key] && source[key].constructor === Object) {
372                     ExtObject.merge(source[key], value);
373                 }
374                 else {
375                     source[key] = Ext.clone(value);
376                 }
377             }
378             else {
379                 source[key] = value;
380             }
381
382             return source;
383         }
384
385         var i = 1,
386             ln = arguments.length,
387             object, property;
388
389         for (; i < ln; i++) {
390             object = arguments[i];
391
392             for (property in object) {
393                 if (object.hasOwnProperty(property)) {
394                     ExtObject.merge(source, property, object[property]);
395                 }
396             }
397         }
398
399         return source;
400     },
401
402     /**
403      * Returns the first matching key corresponding to the given value.
404      * If no matching value is found, null is returned.
405
406     var person = {
407         name: 'Jacky',
408         loves: 'food'
409     };
410
411     alert(Ext.Object.getKey(sencha, 'loves')); // alerts 'food'
412
413      * @param {Object} object
414      * @param {Object} value The value to find
415      * @markdown
416      */
417     getKey: function(object, value) {
418         for (var property in object) {
419             if (object.hasOwnProperty(property) && object[property] === value) {
420                 return property;
421             }
422         }
423
424         return null;
425     },
426
427     /**
428      * Gets all values of the given object as an array.
429
430     var values = Ext.Object.getValues({
431         name: 'Jacky',
432         loves: 'food'
433     }); // ['Jacky', 'food']
434
435      * @param {Object} object
436      * @return {Array} An array of values from the object
437      * @markdown
438      */
439     getValues: function(object) {
440         var values = [],
441             property;
442
443         for (property in object) {
444             if (object.hasOwnProperty(property)) {
445                 values.push(object[property]);
446             }
447         }
448
449         return values;
450     },
451
452     /**
453      * Gets all keys of the given object as an array.
454
455     var values = Ext.Object.getKeys({
456         name: 'Jacky',
457         loves: 'food'
458     }); // ['name', 'loves']
459
460      * @param {Object} object
461      * @return {Array} An array of keys from the object
462      * @method
463      */
464     getKeys: ('keys' in Object.prototype) ? Object.keys : function(object) {
465         var keys = [],
466             property;
467
468         for (property in object) {
469             if (object.hasOwnProperty(property)) {
470                 keys.push(property);
471             }
472         }
473
474         return keys;
475     },
476
477     /**
478      * Gets the total number of this object's own properties
479
480     var size = Ext.Object.getSize({
481         name: 'Jacky',
482         loves: 'food'
483     }); // size equals 2
484
485      * @param {Object} object
486      * @return {Number} size
487      * @markdown
488      */
489     getSize: function(object) {
490         var size = 0,
491             property;
492
493         for (property in object) {
494             if (object.hasOwnProperty(property)) {
495                 size++;
496             }
497         }
498
499         return size;
500     }
501 };
502
503
504 /**
505  * A convenient alias method for {@link Ext.Object#merge}
506  *
507  * @member Ext
508  * @method merge
509  */
510 Ext.merge = Ext.Object.merge;
511
512 /**
513  * A convenient alias method for {@link Ext.Object#toQueryString}
514  *
515  * @member Ext
516  * @method urlEncode
517  * @deprecated 4.0.0 Use {@link Ext.Object#toQueryString Ext.Object.toQueryString} instead
518  */
519 Ext.urlEncode = function() {
520     var args = Ext.Array.from(arguments),
521         prefix = '';
522
523     // Support for the old `pre` argument
524     if ((typeof args[1] === 'string')) {
525         prefix = args[1] + '&';
526         args[1] = false;
527     }
528
529     return prefix + Ext.Object.toQueryString.apply(Ext.Object, args);
530 };
531
532 /**
533  * A convenient alias method for {@link Ext.Object#fromQueryString}
534  *
535  * @member Ext
536  * @method urlDecode
537  * @deprecated 4.0.0 Use {@link Ext.Object#fromQueryString Ext.Object.fromQueryString} instead
538  */
539 Ext.urlDecode = function() {
540     return Ext.Object.fromQueryString.apply(Ext.Object, arguments);
541 };
542
543 })();
544