Skip to content

Commit 2c286d1

Browse files
authored
Data: Patch camelCase behavior of $.fn.data, warn about Object.prototype
Changes: 1. Patch not only `jQuery.data()`, but also `jQuery.fn.data()`. 2. Patch `jQuery.removeData()` & `jQuery.fn.removeData()` to work in most cases when different keys with the same camelCase representation were passed to the data setter and later to `removeData`. 3. Warn about using properties inherited from `Object.prototype` on data objects. Closes gh-559 Ref gh-561
1 parent d1b121b commit 2c286d1

File tree

5 files changed

+1469
-107
lines changed

5 files changed

+1469
-107
lines changed

src/jquery/data.js

+326-26
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,344 @@
11
import { migratePatchFunc, migrateWarn } from "../main.js";
22
import { camelCase } from "../utils.js";
33

4-
var origData = jQuery.data;
4+
var rmultiDash = /[A-Z]/g,
5+
rnothtmlwhite = /[^\x20\t\r\n\f]+/g,
6+
origJQueryData = jQuery.data;
57

6-
migratePatchFunc( jQuery, "data", function( elem, name, value ) {
7-
var curData, sameKeys, key;
8+
function unCamelCase( str ) {
9+
return str.replace( rmultiDash, "-$&" ).toLowerCase();
10+
}
811

9-
// Name can be an object, and each entry in the object is meant to be set as data
10-
if ( name && typeof name === "object" && arguments.length === 2 ) {
12+
function patchDataCamelCase( origData, options ) {
13+
var apiName = options.apiName,
14+
isInstanceMethod = options.isInstanceMethod;
1115

12-
curData = jQuery.hasData( elem ) && origData.call( this, elem );
13-
sameKeys = {};
14-
for ( key in name ) {
15-
if ( key !== camelCase( key ) ) {
16-
migrateWarn( "data-camelCase",
17-
"jQuery.data() always sets/gets camelCased names: " + key );
18-
curData[ key ] = name[ key ];
16+
function objectSetter( elem, obj ) {
17+
var curData, key;
18+
19+
// Name can be an object, and each entry in the object is meant
20+
// to be set as data.
21+
// Let the original method handle the case of a missing elem.
22+
if ( elem ) {
23+
24+
// Don't use the instance method here to avoid `data-*` attributes
25+
// detection this early.
26+
curData = origJQueryData( elem );
27+
28+
for ( key in obj ) {
29+
if ( key !== camelCase( key ) ) {
30+
migrateWarn( "data-camelCase",
31+
apiName + " always sets/gets camelCased names: " +
32+
key );
33+
curData[ key ] = obj[ key ];
34+
}
35+
}
36+
37+
// Pass the keys handled above to the original API as well
38+
// so that both the camelCase & initial keys are saved.
39+
if ( isInstanceMethod ) {
40+
origData.call( this, obj );
1941
} else {
20-
sameKeys[ key ] = name[ key ];
42+
origData.call( this, elem, obj );
2143
}
44+
45+
return obj;
2246
}
47+
}
2348

24-
origData.call( this, elem, sameKeys );
49+
function singleSetter( elem, name, value ) {
50+
var curData;
2551

26-
return name;
27-
}
52+
// If the name is transformed, look for the un-transformed name
53+
// in the data object.
54+
// Let the original method handle the case of a missing elem.
55+
if ( elem ) {
2856

29-
// If the name is transformed, look for the un-transformed name in the data object
30-
if ( name && typeof name === "string" && name !== camelCase( name ) ) {
57+
// Don't use the instance method here to avoid `data-*` attributes
58+
// detection this early.
59+
curData = origJQueryData( elem );
60+
61+
if ( curData && name in curData ) {
62+
migrateWarn( "data-camelCase",
63+
apiName + " always sets/gets camelCased names: " +
64+
name );
3165

32-
curData = jQuery.hasData( elem ) && origData.call( this, elem );
33-
if ( curData && name in curData ) {
34-
migrateWarn( "data-camelCase",
35-
"jQuery.data() always sets/gets camelCased names: " + name );
36-
if ( arguments.length > 2 ) {
3766
curData[ name ] = value;
3867
}
39-
return curData[ name ];
68+
69+
origJQueryData( elem, name, value );
70+
71+
// Since the "set" path can have two possible entry points
72+
// return the expected data based on which path was taken.
73+
return value !== undefined ? value : name;
4074
}
4175
}
4276

43-
return origData.apply( this, arguments );
44-
}, "data-camelCase" );
77+
return function jQueryDataPatched( elem, name, value ) {
78+
var curData,
79+
that = this,
80+
81+
// Support: IE 9 only
82+
// IE 9 doesn't support strict mode and later modifications of
83+
// parameters also modify the arguments object in sloppy mode.
84+
// We need the original arguments so save them here.
85+
args = Array.prototype.slice.call( arguments ),
86+
87+
adjustedArgsLength = args.length;
88+
89+
if ( isInstanceMethod ) {
90+
value = name;
91+
name = elem;
92+
elem = that[ 0 ];
93+
adjustedArgsLength++;
94+
}
95+
96+
if ( name && typeof name === "object" && adjustedArgsLength === 2 ) {
97+
if ( isInstanceMethod ) {
98+
return that.each( function() {
99+
objectSetter.call( that, this, name );
100+
} );
101+
} else {
102+
return objectSetter.call( that, elem, name );
103+
}
104+
}
105+
106+
// If the name is transformed, look for the un-transformed name
107+
// in the data object.
108+
// Let the original method handle the case of a missing elem.
109+
if ( name && typeof name === "string" && name !== camelCase( name ) &&
110+
adjustedArgsLength > 2 ) {
111+
112+
if ( isInstanceMethod ) {
113+
return that.each( function() {
114+
singleSetter.call( that, this, name, value );
115+
} );
116+
} else {
117+
return singleSetter.call( that, elem, name, value );
118+
}
119+
}
120+
121+
if ( elem && name && typeof name === "string" &&
122+
name !== camelCase( name ) &&
123+
adjustedArgsLength === 2 ) {
124+
125+
// Don't use the instance method here to avoid `data-*` attributes
126+
// detection this early.
127+
curData = origJQueryData( elem );
128+
129+
if ( curData && name in curData ) {
130+
migrateWarn( "data-camelCase",
131+
apiName + " always sets/gets camelCased names: " +
132+
name );
133+
return curData[ name ];
134+
}
135+
}
136+
137+
return origData.apply( this, args );
138+
};
139+
}
140+
141+
function patchRemoveDataCamelCase( origRemoveData, options ) {
142+
var isInstanceMethod = options.isInstanceMethod;
143+
144+
function remove( elem, keys ) {
145+
var i, singleKey, unCamelCasedKeys,
146+
curData = jQuery.data( elem );
147+
148+
if ( keys === undefined ) {
149+
origRemoveData( elem );
150+
return;
151+
}
152+
153+
// Support array or space separated string of keys
154+
if ( !Array.isArray( keys ) ) {
155+
156+
// If a key with the spaces exists, use it.
157+
// Otherwise, create an array by matching non-whitespace
158+
keys = keys in curData ?
159+
[ keys ] :
160+
( keys.match( rnothtmlwhite ) || [] );
161+
}
162+
163+
// Remove:
164+
// * the original keys as passed
165+
// * their "unCamelCased" version
166+
// * their camelCase version
167+
// These may be three distinct values for each key!
168+
// jQuery 3.x only removes camelCase versions by default. However, in this patch
169+
// we set the original keys in the mass-setter case and if the key already exists
170+
// so without removing the "unCamelCased" versions the following would be broken:
171+
// ```js
172+
// elem.data( { "a-a": 1 } ).removeData( "aA" );
173+
// ```
174+
// Unfortunately, we'll still hit this issue for partially camelCased keys, e.g.:
175+
// ```js
176+
// elem.data( { "a-aA": 1 } ).removeData( "aAA" );
177+
// ```
178+
// won't work with this patch. We consider this an edge case, but to make sure that
179+
// at least piggybacking works:
180+
// ```js
181+
// elem.data( { "a-aA": 1 } ).removeData( "a-aA" );
182+
// ```
183+
// we also remove the original key. Hence, all three are needed.
184+
// The original API already removes the camelCase versions, though, so let's defer
185+
// to it.
186+
unCamelCasedKeys = keys.map( unCamelCase );
187+
188+
i = keys.length;
189+
while ( i-- ) {
190+
singleKey = keys[ i ];
191+
if ( singleKey !== camelCase( singleKey ) && singleKey in curData ) {
192+
migrateWarn( "data-camelCase",
193+
"jQuery" + ( isInstanceMethod ? ".fn" : "" ) +
194+
".data() always sets/gets camelCased names: " +
195+
singleKey );
196+
}
197+
delete curData[ singleKey ];
198+
}
199+
200+
// Don't warn when removing "unCamelCased" keys; we're already printing
201+
// a warning when setting them and the fix is needed there, not in
202+
// the `.removeData()` call.
203+
i = unCamelCasedKeys.length;
204+
while ( i-- ) {
205+
delete curData[ unCamelCasedKeys[ i ] ];
206+
}
207+
208+
origRemoveData( elem, keys );
209+
}
210+
211+
return function jQueryRemoveDataPatched( elem, key ) {
212+
if ( isInstanceMethod ) {
213+
key = elem;
214+
return this.each( function() {
215+
remove( this, key );
216+
} );
217+
} else {
218+
remove( elem, key );
219+
}
220+
};
221+
}
222+
223+
migratePatchFunc( jQuery, "data",
224+
patchDataCamelCase( jQuery.data, {
225+
apiName: "jQuery.data()",
226+
isInstanceMethod: false
227+
} ),
228+
"data-camelCase" );
229+
migratePatchFunc( jQuery.fn, "data",
230+
patchDataCamelCase( jQuery.fn.data, {
231+
apiName: "jQuery.fn.data()",
232+
isInstanceMethod: true
233+
} ),
234+
"data-camelCase" );
235+
236+
migratePatchFunc( jQuery, "removeData",
237+
patchRemoveDataCamelCase( jQuery.removeData, {
238+
isInstanceMethod: false
239+
} ),
240+
"data-camelCase" );
241+
242+
migratePatchFunc( jQuery.fn, "removeData",
243+
244+
// No, it's not a typo - we're intentionally passing
245+
// the static method here as we need something working on
246+
// a single element.
247+
patchRemoveDataCamelCase( jQuery.removeData, {
248+
isInstanceMethod: true
249+
} ),
250+
"data-camelCase" );
251+
252+
253+
function patchDataProto( original, options ) {
254+
255+
// Support: IE 9 - 10 only, iOS 7 - 8 only
256+
// Older IE doesn't have a way to change an existing prototype.
257+
// Just return the original method there.
258+
// Older WebKit supports `__proto__` but not `Object.setPrototypeOf`.
259+
// To avoid complicating code, don't patch the API there either.
260+
if ( !Object.setPrototypeOf ) {
261+
return original;
262+
}
263+
264+
var i,
265+
apiName = options.apiName,
266+
isInstanceMethod = options.isInstanceMethod,
267+
268+
// `Object.prototype` keys are not enumerable so list the
269+
// official ones here. An alternative would be wrapping
270+
// data objects with a Proxy but that creates additional issues
271+
// like breaking object identity on subsequent calls.
272+
objProtoKeys = [
273+
"__proto__",
274+
"__defineGetter__",
275+
"__defineSetter__",
276+
"__lookupGetter__",
277+
"__lookupSetter__",
278+
"hasOwnProperty",
279+
"isPrototypeOf",
280+
"propertyIsEnumerable",
281+
"toLocaleString",
282+
"toString",
283+
"valueOf"
284+
],
285+
286+
// Use a null prototype at the beginning so that we can define our
287+
// `__proto__` getter & setter. We'll reset the prototype afterwards.
288+
intermediateDataObj = Object.create( null );
289+
290+
for ( i = 0; i < objProtoKeys.length; i++ ) {
291+
( function( key ) {
292+
Object.defineProperty( intermediateDataObj, key, {
293+
get: function() {
294+
migrateWarn( "data-null-proto",
295+
"Accessing properties from " + apiName +
296+
" inherited from Object.prototype is deprecated" );
297+
return ( key + "__cache" ) in intermediateDataObj ?
298+
intermediateDataObj[ key + "__cache" ] :
299+
Object.prototype[ key ];
300+
},
301+
set: function( value ) {
302+
migrateWarn( "data-null-proto",
303+
"Setting properties from " + apiName +
304+
" inherited from Object.prototype is deprecated" );
305+
intermediateDataObj[ key + "__cache" ] = value;
306+
}
307+
} );
308+
} )( objProtoKeys[ i ] );
309+
}
310+
311+
Object.setPrototypeOf( intermediateDataObj, Object.prototype );
312+
313+
return function jQueryDataProtoPatched() {
314+
var result = original.apply( this, arguments );
315+
316+
if ( arguments.length !== ( isInstanceMethod ? 0 : 1 ) || result === undefined ) {
317+
return result;
318+
}
319+
320+
// Insert an additional object in the prototype chain between `result`
321+
// and `Object.prototype`; that intermediate object proxies properties
322+
// to `Object.prototype`, warning about their usage first.
323+
Object.setPrototypeOf( result, intermediateDataObj );
324+
325+
return result;
326+
};
327+
}
328+
329+
// Yes, we are patching jQuery.data twice; here & above. This is necessary
330+
// so that each of the two patches can be independently disabled.
331+
migratePatchFunc( jQuery, "data",
332+
patchDataProto( jQuery.data, {
333+
apiName: "jQuery.data()",
334+
isPrivateData: false,
335+
isInstanceMethod: false
336+
} ),
337+
"data-null-proto" );
338+
migratePatchFunc( jQuery.fn, "data",
339+
patchDataProto( jQuery.fn.data, {
340+
apiName: "jQuery.fn.data()",
341+
isPrivateData: true,
342+
isInstanceMethod: true
343+
} ),
344+
"data-null-proto" );

test/data/testinit.js

+1
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
"unit/jquery/attributes.js",
6464
"unit/jquery/css.js",
6565
"unit/jquery/data.js",
66+
"unit/jquery/data-jquery-compat.js",
6667
"unit/jquery/deferred.js",
6768
"unit/jquery/effects.js",
6869
"unit/jquery/event.js",

0 commit comments

Comments
 (0)