Update jQuery.hasData to always return a boolean, with unit tests.
[jquery.git] / src / data.js
1 (function( jQuery ) {
2
3 var windowData = {},
4         rbrace = /^(?:\{.*\}|\[.*\])$/;
5
6 jQuery.extend({
7         cache: {},
8
9         // Please use with caution
10         uuid: 0,
11
12         // Unique for each copy of jQuery on the page
13         expando: "jQuery" + jQuery.now(),
14
15         // The following elements throw uncatchable exceptions if you
16         // attempt to add expando properties to them.
17         noData: {
18                 "embed": true,
19                 // Ban all objects except for Flash (which handle expandos)
20                 "object": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",
21                 "applet": true
22         },
23
24         hasData: function( elem ) {
25                 return !elem.nodeType || (!!elem[ jQuery.expando ] && !jQuery.isEmptyObject(jQuery.cache[ elem[jQuery.expando] ]));
26         },
27
28         data: function( elem, name, data ) {
29                 if ( !jQuery.acceptData( elem ) ) {
30                         return;
31                 }
32
33                 elem = elem == window ?
34                         windowData :
35                         elem;
36
37                 var isNode = elem.nodeType,
38                         id = isNode ? elem[ jQuery.expando ] : null,
39                         cache = jQuery.cache, thisCache;
40
41                 if ( isNode && !id && typeof name === "string" && data === undefined ) {
42                         return;
43                 }
44
45                 // Get the data from the object directly
46                 if ( !isNode ) {
47                         cache = elem;
48
49                 // Compute a unique ID for the element
50                 } else if ( !id ) {
51                         elem[ jQuery.expando ] = id = ++jQuery.uuid;
52                 }
53
54                 // Avoid generating a new cache unless none exists and we
55                 // want to manipulate it.
56                 if ( typeof name === "object" ) {
57                         if ( isNode ) {
58                                 cache[ id ] = jQuery.extend(cache[ id ], name);
59
60                         } else {
61                                 jQuery.extend( cache, name );
62                         }
63
64                 } else if ( isNode && !cache[ id ] ) {
65                         cache[ id ] = {};
66                 }
67
68                 thisCache = isNode ? cache[ id ] : cache;
69
70                 // Prevent overriding the named cache with undefined values
71                 if ( data !== undefined ) {
72                         thisCache[ name ] = data;
73                 }
74
75                 return typeof name === "string" ? thisCache[ name ] : thisCache;
76         },
77
78         removeData: function( elem, name ) {
79                 if ( !jQuery.acceptData( elem ) ) {
80                         return;
81                 }
82
83                 elem = elem == window ?
84                         windowData :
85                         elem;
86
87                 var isNode = elem.nodeType,
88                         id = isNode ? elem[ jQuery.expando ] : elem,
89                         cache = jQuery.cache,
90                         thisCache = isNode ? cache[ id ] : id;
91
92                 // If we want to remove a specific section of the element's data
93                 if ( name ) {
94                         if ( thisCache ) {
95                                 // Remove the section of cache data
96                                 delete thisCache[ name ];
97
98                                 // If we've removed all the data, remove the element's cache
99                                 if ( isNode && jQuery.isEmptyObject(thisCache) ) {
100                                         jQuery.removeData( elem );
101                                 }
102                         }
103
104                 // Otherwise, we want to remove all of the element's data
105                 } else {
106                         if ( isNode && jQuery.support.deleteExpando ) {
107                                 delete elem[ jQuery.expando ];
108
109                         } else if ( elem.removeAttribute ) {
110                                 elem.removeAttribute( jQuery.expando );
111
112                         // Completely remove the data cache
113                         } else if ( isNode ) {
114                                 delete cache[ id ];
115
116                         // Remove all fields from the object
117                         } else {
118                                 for ( var n in elem ) {
119                                         delete elem[ n ];
120                                 }
121                         }
122                 }
123         },
124
125         // A method for determining if a DOM node can handle the data expando
126         acceptData: function( elem ) {
127                 if ( elem.nodeName ) {
128                         var match = jQuery.noData[ elem.nodeName.toLowerCase() ];
129
130                         if ( match ) {
131                                 return !(match === true || elem.getAttribute("classid") !== match);
132                         }
133                 }
134
135                 return true;
136         }
137 });
138
139 jQuery.fn.extend({
140         data: function( key, value ) {
141                 var data = null;
142
143                 if ( typeof key === "undefined" ) {
144                         if ( this.length ) {
145                                 data = jQuery.data( this[0] );
146
147                                 if ( this[0].nodeType === 1 ) {
148                                         var attr = this[0].attributes, name;
149                                         for ( var i = 0, l = attr.length; i < l; i++ ) {
150                                                 name = attr[i].name;
151
152                                                 if ( name.indexOf( "data-" ) === 0 ) {
153                                                         name = name.substr( 5 );
154                                                         dataAttr( this[0], name, data[ name ] );
155                                                 }
156                                         }
157                                 }
158                         }
159
160                         return data;
161
162                 } else if ( typeof key === "object" ) {
163                         return this.each(function() {
164                                 jQuery.data( this, key );
165                         });
166                 }
167
168                 var parts = key.split(".");
169                 parts[1] = parts[1] ? "." + parts[1] : "";
170
171                 if ( value === undefined ) {
172                         data = this.triggerHandler("getData" + parts[1] + "!", [parts[0]]);
173
174                         // Try to fetch any internally stored data first
175                         if ( data === undefined && this.length ) {
176                                 data = jQuery.data( this[0], key );
177                                 data = dataAttr( this[0], key, data );
178                         }
179
180                         return data === undefined && parts[1] ?
181                                 this.data( parts[0] ) :
182                                 data;
183
184                 } else {
185                         return this.each(function() {
186                                 var $this = jQuery( this ),
187                                         args = [ parts[0], value ];
188
189                                 $this.triggerHandler( "setData" + parts[1] + "!", args );
190                                 jQuery.data( this, key, value );
191                                 $this.triggerHandler( "changeData" + parts[1] + "!", args );
192                         });
193                 }
194         },
195
196         removeData: function( key ) {
197                 return this.each(function() {
198                         jQuery.removeData( this, key );
199                 });
200         }
201 });
202
203 function dataAttr( elem, key, data ) {
204         // If nothing was found internally, try to fetch any
205         // data from the HTML5 data-* attribute
206         if ( data === undefined && elem.nodeType === 1 ) {
207                 data = elem.getAttribute( "data-" + key );
208
209                 if ( typeof data === "string" ) {
210                         try {
211                                 data = data === "true" ? true :
212                                 data === "false" ? false :
213                                 data === "null" ? null :
214                                 !jQuery.isNaN( data ) ? parseFloat( data ) :
215                                         rbrace.test( data ) ? jQuery.parseJSON( data ) :
216                                         data;
217                         } catch( e ) {}
218
219                         // Make sure we set the data so it isn't changed later
220                         jQuery.data( elem, key, data );
221
222                 } else {
223                         data = undefined;
224                 }
225         }
226
227         return data;
228 }
229
230 })( jQuery );