Skip to content

Commit 675f7d2

Browse files
committed
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.
1 parent 084a64e commit 675f7d2

File tree

5 files changed

+1439
-107
lines changed

5 files changed

+1439
-107
lines changed

src/jquery/data.js

+325-26
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,343 @@
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, sameKeys, 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+
sameKeys = {};
29+
for ( key in obj ) {
30+
if ( key !== camelCase( key ) ) {
31+
migrateWarn( "data-camelCase",
32+
apiName + " always sets/gets camelCased names: " +
33+
key );
34+
curData[ key ] = obj[ key ];
35+
} else {
36+
sameKeys[ key ] = obj[ key ];
37+
}
38+
}
39+
40+
if ( isInstanceMethod ) {
41+
origData.call( this, sameKeys );
1942
} else {
20-
sameKeys[ key ] = name[ key ];
43+
origData.call( this, elem, sameKeys );
2144
}
45+
46+
return obj;
2247
}
48+
}
2349

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

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

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 ) ) {
58+
// Don't use the instance method here to avoid `data-*` attributes
59+
// detection this early.
60+
curData = origJQueryData( elem );
61+
62+
if ( curData && name in curData ) {
63+
migrateWarn( "data-camelCase",
64+
apiName + " always sets/gets camelCased names: " +
65+
name );
3166

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 ) {
3767
curData[ name ] = value;
68+
69+
// Since the "set" path can have two possible entry points
70+
// return the expected data based on which path was taken.
71+
return value !== undefined ? value : name;
72+
} else {
73+
origJQueryData( elem, name, value );
3874
}
39-
return curData[ name ];
4075
}
4176
}
4277

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

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",

test/runner/flags/modules.js

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export const modules = [
55
"attributes",
66
"css",
77
"data",
8+
"data-jquery-compat",
89
"deferred",
910
"effects",
1011
"event",

0 commit comments

Comments
 (0)