diff --git a/src/jquery/data.js b/src/jquery/data.js
index 07430ce6..20e5c8d7 100644
--- a/src/jquery/data.js
+++ b/src/jquery/data.js
@@ -1,44 +1,344 @@
import { migratePatchFunc, migrateWarn } from "../main.js";
import { camelCase } from "../utils.js";
-var origData = jQuery.data;
+var rmultiDash = /[A-Z]/g,
+ rnothtmlwhite = /[^\x20\t\r\n\f]+/g,
+ origJQueryData = jQuery.data;
-migratePatchFunc( jQuery, "data", function( elem, name, value ) {
- var curData, sameKeys, key;
+function unCamelCase( str ) {
+ return str.replace( rmultiDash, "-$&" ).toLowerCase();
+}
- // Name can be an object, and each entry in the object is meant to be set as data
- if ( name && typeof name === "object" && arguments.length === 2 ) {
+function patchDataCamelCase( origData, options ) {
+ var apiName = options.apiName,
+ isInstanceMethod = options.isInstanceMethod;
- curData = jQuery.hasData( elem ) && origData.call( this, elem );
- sameKeys = {};
- for ( key in name ) {
- if ( key !== camelCase( key ) ) {
- migrateWarn( "data-camelCase",
- "jQuery.data() always sets/gets camelCased names: " + key );
- curData[ key ] = name[ key ];
+ function objectSetter( elem, obj ) {
+ var curData, key;
+
+ // Name can be an object, and each entry in the object is meant
+ // to be set as data.
+ // Let the original method handle the case of a missing elem.
+ if ( elem ) {
+
+ // Don't use the instance method here to avoid `data-*` attributes
+ // detection this early.
+ curData = origJQueryData( elem );
+
+ for ( key in obj ) {
+ if ( key !== camelCase( key ) ) {
+ migrateWarn( "data-camelCase",
+ apiName + " always sets/gets camelCased names: " +
+ key );
+ curData[ key ] = obj[ key ];
+ }
+ }
+
+ // Pass the keys handled above to the original API as well
+ // so that both the camelCase & initial keys are saved.
+ if ( isInstanceMethod ) {
+ origData.call( this, obj );
} else {
- sameKeys[ key ] = name[ key ];
+ origData.call( this, elem, obj );
}
+
+ return obj;
}
+ }
- origData.call( this, elem, sameKeys );
+ function singleSetter( elem, name, value ) {
+ var curData;
- return name;
- }
+ // If the name is transformed, look for the un-transformed name
+ // in the data object.
+ // Let the original method handle the case of a missing elem.
+ if ( elem ) {
- // If the name is transformed, look for the un-transformed name in the data object
- if ( name && typeof name === "string" && name !== camelCase( name ) ) {
+ // Don't use the instance method here to avoid `data-*` attributes
+ // detection this early.
+ curData = origJQueryData( elem );
+
+ if ( curData && name in curData ) {
+ migrateWarn( "data-camelCase",
+ apiName + " always sets/gets camelCased names: " +
+ name );
- curData = jQuery.hasData( elem ) && origData.call( this, elem );
- if ( curData && name in curData ) {
- migrateWarn( "data-camelCase",
- "jQuery.data() always sets/gets camelCased names: " + name );
- if ( arguments.length > 2 ) {
curData[ name ] = value;
}
- return curData[ name ];
+
+ origJQueryData( elem, name, value );
+
+ // Since the "set" path can have two possible entry points
+ // return the expected data based on which path was taken.
+ return value !== undefined ? value : name;
}
}
- return origData.apply( this, arguments );
-}, "data-camelCase" );
+ return function jQueryDataPatched( elem, name, value ) {
+ var curData,
+ that = this,
+
+ // Support: IE 9 only
+ // IE 9 doesn't support strict mode and later modifications of
+ // parameters also modify the arguments object in sloppy mode.
+ // We need the original arguments so save them here.
+ args = Array.prototype.slice.call( arguments ),
+
+ adjustedArgsLength = args.length;
+
+ if ( isInstanceMethod ) {
+ value = name;
+ name = elem;
+ elem = that[ 0 ];
+ adjustedArgsLength++;
+ }
+
+ if ( name && typeof name === "object" && adjustedArgsLength === 2 ) {
+ if ( isInstanceMethod ) {
+ return that.each( function() {
+ objectSetter.call( that, this, name );
+ } );
+ } else {
+ return objectSetter.call( that, elem, name );
+ }
+ }
+
+ // If the name is transformed, look for the un-transformed name
+ // in the data object.
+ // Let the original method handle the case of a missing elem.
+ if ( name && typeof name === "string" && name !== camelCase( name ) &&
+ adjustedArgsLength > 2 ) {
+
+ if ( isInstanceMethod ) {
+ return that.each( function() {
+ singleSetter.call( that, this, name, value );
+ } );
+ } else {
+ return singleSetter.call( that, elem, name, value );
+ }
+ }
+
+ if ( elem && name && typeof name === "string" &&
+ name !== camelCase( name ) &&
+ adjustedArgsLength === 2 ) {
+
+ // Don't use the instance method here to avoid `data-*` attributes
+ // detection this early.
+ curData = origJQueryData( elem );
+
+ if ( curData && name in curData ) {
+ migrateWarn( "data-camelCase",
+ apiName + " always sets/gets camelCased names: " +
+ name );
+ return curData[ name ];
+ }
+ }
+
+ return origData.apply( this, args );
+ };
+}
+
+function patchRemoveDataCamelCase( origRemoveData, options ) {
+ var isInstanceMethod = options.isInstanceMethod;
+
+ function remove( elem, keys ) {
+ var i, singleKey, unCamelCasedKeys,
+ curData = jQuery.data( elem );
+
+ if ( keys === undefined ) {
+ origRemoveData( elem );
+ return;
+ }
+
+ // Support array or space separated string of keys
+ if ( !Array.isArray( keys ) ) {
+
+ // If a key with the spaces exists, use it.
+ // Otherwise, create an array by matching non-whitespace
+ keys = keys in curData ?
+ [ keys ] :
+ ( keys.match( rnothtmlwhite ) || [] );
+ }
+
+ // Remove:
+ // * the original keys as passed
+ // * their "unCamelCased" version
+ // * their camelCase version
+ // These may be three distinct values for each key!
+ // jQuery 3.x only removes camelCase versions by default. However, in this patch
+ // we set the original keys in the mass-setter case and if the key already exists
+ // so without removing the "unCamelCased" versions the following would be broken:
+ // ```js
+ // elem.data( { "a-a": 1 } ).removeData( "aA" );
+ // ```
+ // Unfortunately, we'll still hit this issue for partially camelCased keys, e.g.:
+ // ```js
+ // elem.data( { "a-aA": 1 } ).removeData( "aAA" );
+ // ```
+ // won't work with this patch. We consider this an edge case, but to make sure that
+ // at least piggybacking works:
+ // ```js
+ // elem.data( { "a-aA": 1 } ).removeData( "a-aA" );
+ // ```
+ // we also remove the original key. Hence, all three are needed.
+ // The original API already removes the camelCase versions, though, so let's defer
+ // to it.
+ unCamelCasedKeys = keys.map( unCamelCase );
+
+ i = keys.length;
+ while ( i-- ) {
+ singleKey = keys[ i ];
+ if ( singleKey !== camelCase( singleKey ) && singleKey in curData ) {
+ migrateWarn( "data-camelCase",
+ "jQuery" + ( isInstanceMethod ? ".fn" : "" ) +
+ ".data() always sets/gets camelCased names: " +
+ singleKey );
+ }
+ delete curData[ singleKey ];
+ }
+
+ // Don't warn when removing "unCamelCased" keys; we're already printing
+ // a warning when setting them and the fix is needed there, not in
+ // the `.removeData()` call.
+ i = unCamelCasedKeys.length;
+ while ( i-- ) {
+ delete curData[ unCamelCasedKeys[ i ] ];
+ }
+
+ origRemoveData( elem, keys );
+ }
+
+ return function jQueryRemoveDataPatched( elem, key ) {
+ if ( isInstanceMethod ) {
+ key = elem;
+ return this.each( function() {
+ remove( this, key );
+ } );
+ } else {
+ remove( elem, key );
+ }
+ };
+}
+
+migratePatchFunc( jQuery, "data",
+ patchDataCamelCase( jQuery.data, {
+ apiName: "jQuery.data()",
+ isInstanceMethod: false
+ } ),
+ "data-camelCase" );
+migratePatchFunc( jQuery.fn, "data",
+ patchDataCamelCase( jQuery.fn.data, {
+ apiName: "jQuery.fn.data()",
+ isInstanceMethod: true
+ } ),
+ "data-camelCase" );
+
+migratePatchFunc( jQuery, "removeData",
+ patchRemoveDataCamelCase( jQuery.removeData, {
+ isInstanceMethod: false
+ } ),
+ "data-camelCase" );
+
+migratePatchFunc( jQuery.fn, "removeData",
+
+ // No, it's not a typo - we're intentionally passing
+ // the static method here as we need something working on
+ // a single element.
+ patchRemoveDataCamelCase( jQuery.removeData, {
+ isInstanceMethod: true
+ } ),
+ "data-camelCase" );
+
+
+function patchDataProto( original, options ) {
+
+ // Support: IE 9 - 10 only, iOS 7 - 8 only
+ // Older IE doesn't have a way to change an existing prototype.
+ // Just return the original method there.
+ // Older WebKit supports `__proto__` but not `Object.setPrototypeOf`.
+ // To avoid complicating code, don't patch the API there either.
+ if ( !Object.setPrototypeOf ) {
+ return original;
+ }
+
+ var i,
+ apiName = options.apiName,
+ isInstanceMethod = options.isInstanceMethod,
+
+ // `Object.prototype` keys are not enumerable so list the
+ // official ones here. An alternative would be wrapping
+ // data objects with a Proxy but that creates additional issues
+ // like breaking object identity on subsequent calls.
+ objProtoKeys = [
+ "__proto__",
+ "__defineGetter__",
+ "__defineSetter__",
+ "__lookupGetter__",
+ "__lookupSetter__",
+ "hasOwnProperty",
+ "isPrototypeOf",
+ "propertyIsEnumerable",
+ "toLocaleString",
+ "toString",
+ "valueOf"
+ ],
+
+ // Use a null prototype at the beginning so that we can define our
+ // `__proto__` getter & setter. We'll reset the prototype afterwards.
+ intermediateDataObj = Object.create( null );
+
+ for ( i = 0; i < objProtoKeys.length; i++ ) {
+ ( function( key ) {
+ Object.defineProperty( intermediateDataObj, key, {
+ get: function() {
+ migrateWarn( "data-null-proto",
+ "Accessing properties from " + apiName +
+ " inherited from Object.prototype is deprecated" );
+ return ( key + "__cache" ) in intermediateDataObj ?
+ intermediateDataObj[ key + "__cache" ] :
+ Object.prototype[ key ];
+ },
+ set: function( value ) {
+ migrateWarn( "data-null-proto",
+ "Setting properties from " + apiName +
+ " inherited from Object.prototype is deprecated" );
+ intermediateDataObj[ key + "__cache" ] = value;
+ }
+ } );
+ } )( objProtoKeys[ i ] );
+ }
+
+ Object.setPrototypeOf( intermediateDataObj, Object.prototype );
+
+ return function jQueryDataProtoPatched() {
+ var result = original.apply( this, arguments );
+
+ if ( arguments.length !== ( isInstanceMethod ? 0 : 1 ) || result === undefined ) {
+ return result;
+ }
+
+ // Insert an additional object in the prototype chain between `result`
+ // and `Object.prototype`; that intermediate object proxies properties
+ // to `Object.prototype`, warning about their usage first.
+ Object.setPrototypeOf( result, intermediateDataObj );
+
+ return result;
+ };
+}
+
+// Yes, we are patching jQuery.data twice; here & above. This is necessary
+// so that each of the two patches can be independently disabled.
+migratePatchFunc( jQuery, "data",
+ patchDataProto( jQuery.data, {
+ apiName: "jQuery.data()",
+ isPrivateData: false,
+ isInstanceMethod: false
+ } ),
+ "data-null-proto" );
+migratePatchFunc( jQuery.fn, "data",
+ patchDataProto( jQuery.fn.data, {
+ apiName: "jQuery.fn.data()",
+ isPrivateData: true,
+ isInstanceMethod: true
+ } ),
+ "data-null-proto" );
diff --git a/test/data/testinit.js b/test/data/testinit.js
index 450f0f19..c4fd7df7 100644
--- a/test/data/testinit.js
+++ b/test/data/testinit.js
@@ -63,6 +63,7 @@
"unit/jquery/attributes.js",
"unit/jquery/css.js",
"unit/jquery/data.js",
+ "unit/jquery/data-jquery-compat.js",
"unit/jquery/deferred.js",
"unit/jquery/effects.js",
"unit/jquery/event.js",
diff --git a/test/unit/jquery/data-jquery-compat.js b/test/unit/jquery/data-jquery-compat.js
new file mode 100644
index 00000000..29a30554
--- /dev/null
+++ b/test/unit/jquery/data-jquery-compat.js
@@ -0,0 +1,949 @@
+// This module contains a copy of most of the `data` tests from
+// jQuery's `3.x-stable` (with some modifications) to make sure our patches
+// don't break compatibility with jQuery.
+
+/* eslint-disable max-len */
+// Disable `max-len` due to too many violations.
+
+QUnit.module( "data-jquery-compat", {
+ beforeEach: function() {
+
+ var template = "" +
+ "
" +
+ "
Everything inside the red border is inside a div with id='foo'.
" +
+ "
" +
+ "\n" +
+ "";
+
+ jQuery( "#qunit-fixture" ).append( template );
+ }
+} );
+
+QUnit.test( "expando", function( assert ) {
+ assert.expect( 1 );
+
+ assert.equal( jQuery.expando !== undefined, true, "jQuery is exposing the expando" );
+} );
+
+QUnit.test( "jQuery.data & removeData, expected returns", function( assert ) {
+ assert.expect( 4 );
+ var elem = document.body;
+
+ assert.equal(
+ jQuery.data( elem, "hello", "world" ), "world",
+ "jQuery.data( elem, key, value ) returns value"
+ );
+ assert.equal(
+ jQuery.data( elem, "hello" ), "world",
+ "jQuery.data( elem, key ) returns value"
+ );
+ assert.deepEqual(
+ jQuery.data( elem, { goodnight: "moon" } ), { goodnight: "moon" },
+ "jQuery.data( elem, obj ) returns obj"
+ );
+ assert.equal(
+ jQuery.removeData( elem, "hello" ), undefined,
+ "jQuery.removeData( elem, key, value ) returns undefined"
+ );
+
+} );
+
+QUnit.test( "jQuery._data & _removeData, expected returns", function( assert ) {
+ assert.expect( 4 );
+ var elem = document.body;
+
+ assert.equal(
+ jQuery._data( elem, "hello", "world" ), "world",
+ "jQuery._data( elem, key, value ) returns value"
+ );
+ assert.equal(
+ jQuery._data( elem, "hello" ), "world",
+ "jQuery._data( elem, key ) returns value"
+ );
+ assert.deepEqual(
+ jQuery._data( elem, { goodnight: "moon" } ), { goodnight: "moon" },
+ "jQuery._data( elem, obj ) returns obj"
+ );
+ assert.equal(
+ jQuery._removeData( elem, "hello" ), undefined,
+ "jQuery._removeData( elem, key, value ) returns undefined"
+ );
+} );
+
+QUnit.test( "jQuery.hasData no side effects", function( assert ) {
+ assert.expect( 1 );
+ var obj = {};
+
+ jQuery.hasData( obj );
+
+ assert.equal( Object.getOwnPropertyNames( obj ).length, 0,
+ "No data expandos where added when calling jQuery.hasData(o)"
+ );
+} );
+
+function dataTests( elem, assert ) {
+ var dataObj, internalDataObj;
+
+ assert.equal( jQuery.data( elem, "foo" ), undefined, "No data exists initially" );
+ assert.strictEqual( jQuery.hasData( elem ), false, "jQuery.hasData agrees no data exists initially" );
+
+ dataObj = jQuery.data( elem );
+ assert.equal( typeof dataObj, "object", "Calling data with no args gives us a data object reference" );
+ assert.strictEqual( jQuery.data( elem ), dataObj, "Calling jQuery.data returns the same data object when called multiple times" );
+
+ assert.strictEqual( jQuery.hasData( elem ), false, "jQuery.hasData agrees no data exists even when an empty data obj exists" );
+
+ dataObj.foo = "bar";
+ if ( !jQuery.data( elem, "foo" ) ) {
+ debugger;
+ }
+ assert.equal( jQuery.data( elem, "foo" ), "bar", "Data is readable by jQuery.data when set directly on a returned data object" );
+
+ assert.strictEqual( jQuery.hasData( elem ), true, "jQuery.hasData agrees data exists when data exists" );
+
+ jQuery.data( elem, "foo", "baz" );
+ assert.equal( jQuery.data( elem, "foo" ), "baz", "Data can be changed by jQuery.data" );
+ assert.equal( dataObj.foo, "baz", "Changes made through jQuery.data propagate to referenced data object" );
+
+ jQuery.data( elem, "foo", undefined );
+ assert.equal( jQuery.data( elem, "foo" ), "baz", "Data is not unset by passing undefined to jQuery.data" );
+
+ jQuery.data( elem, "foo", null );
+ assert.strictEqual( jQuery.data( elem, "foo" ), null, "Setting null using jQuery.data works OK" );
+
+ jQuery.data( elem, "foo", "foo1" );
+
+ jQuery.data( elem, { "bar": "baz", "boom": "bloz" } );
+ assert.strictEqual( jQuery.data( elem, "foo" ), "foo1", "Passing an object extends the data object instead of replacing it" );
+ assert.equal( jQuery.data( elem, "boom" ), "bloz", "Extending the data object works" );
+
+ jQuery._data( elem, "foo", "foo2", true );
+ assert.equal( jQuery._data( elem, "foo" ), "foo2", "Setting internal data works" );
+ assert.equal( jQuery.data( elem, "foo" ), "foo1", "Setting internal data does not override user data" );
+
+ internalDataObj = jQuery._data( elem );
+ assert.ok( internalDataObj, "Internal data object exists" );
+ assert.notStrictEqual( dataObj, internalDataObj, "Internal data object is not the same as user data object" );
+
+ assert.strictEqual( elem.boom, undefined, "Data is never stored directly on the object" );
+
+ jQuery.removeData( elem, "foo" );
+ assert.strictEqual( jQuery.data( elem, "foo" ), undefined, "jQuery.removeData removes single properties" );
+
+ jQuery.removeData( elem );
+ assert.strictEqual( jQuery._data( elem ), internalDataObj, "jQuery.removeData does not remove internal data if it exists" );
+
+ jQuery.data( elem, "foo", "foo1" );
+ jQuery._data( elem, "foo", "foo2" );
+
+ assert.equal( jQuery.data( elem, "foo" ), "foo1", "(sanity check) Ensure data is set in user data object" );
+ assert.equal( jQuery._data( elem, "foo" ), "foo2", "(sanity check) Ensure data is set in internal data object" );
+
+ assert.strictEqual( jQuery._data( elem, jQuery.expando ), undefined, "Removing the last item in internal data destroys the internal data object" );
+
+ jQuery._data( elem, "foo", "foo2" );
+ assert.equal( jQuery._data( elem, "foo" ), "foo2", "(sanity check) Ensure data is set in internal data object" );
+
+ jQuery.removeData( elem, "foo" );
+ assert.equal( jQuery._data( elem, "foo" ), "foo2", "(sanity check) jQuery.removeData for user data does not remove internal data" );
+}
+
+QUnit.test( "jQuery.data(div)", function( assert ) {
+ assert.expect( 25 );
+
+ var div = document.createElement( "div" );
+
+ dataTests( div, assert );
+} );
+
+QUnit.test( "jQuery.data({})", function( assert ) {
+ assert.expect( 25 );
+
+ dataTests( {}, assert );
+} );
+
+QUnit.test( "jQuery.data(window)", function( assert ) {
+ assert.expect( 25 );
+
+ try {
+
+ // Remove bound handlers from window object to stop potential false
+ // positives caused by fix for trac-5280 in transports/xhr.js.
+ jQuery( window ).off( "unload" );
+
+ dataTests( window, assert );
+ } finally {
+ jQuery.removeData( window );
+ jQuery._removeData( window );
+ }
+} );
+
+QUnit.test( "jQuery.data(document)", function( assert ) {
+ assert.expect( 25 );
+
+ dataTests( document, assert );
+} );
+
+QUnit.test( "jQuery.data(