diff --git a/src/jquery/data.js b/src/jquery/data.js index 20e5c8d..ffa6a2c 100644 --- a/src/jquery/data.js +++ b/src/jquery/data.js @@ -1,9 +1,10 @@ import { migratePatchFunc, migrateWarn } from "../main.js"; -import { camelCase } from "../utils.js"; +import { camelCase, patchProto } from "../utils.js"; var rmultiDash = /[A-Z]/g, rnothtmlwhite = /[^\x20\t\r\n\f]+/g, - origJQueryData = jQuery.data; + origJQueryData = jQuery.data, + origJQueryPrivateData = jQuery._data; function unCamelCase( str ) { return str.replace( rmultiDash, "-$&" ).toLowerCase(); @@ -11,7 +12,9 @@ function unCamelCase( str ) { function patchDataCamelCase( origData, options ) { var apiName = options.apiName, - isInstanceMethod = options.isInstanceMethod; + isPrivateData = options.isPrivateData, + isInstanceMethod = options.isInstanceMethod, + origJQueryStaticData = isPrivateData ? origJQueryPrivateData : origJQueryData; function objectSetter( elem, obj ) { var curData, key; @@ -23,7 +26,7 @@ function patchDataCamelCase( origData, options ) { // Don't use the instance method here to avoid `data-*` attributes // detection this early. - curData = origJQueryData( elem ); + curData = origJQueryStaticData( elem ); for ( key in obj ) { if ( key !== camelCase( key ) ) { @@ -56,7 +59,7 @@ function patchDataCamelCase( origData, options ) { // Don't use the instance method here to avoid `data-*` attributes // detection this early. - curData = origJQueryData( elem ); + curData = origJQueryStaticData( elem ); if ( curData && name in curData ) { migrateWarn( "data-camelCase", @@ -66,7 +69,7 @@ function patchDataCamelCase( origData, options ) { curData[ name ] = value; } - origJQueryData( elem, name, value ); + origJQueryStaticData( elem, name, value ); // Since the "set" path can have two possible entry points // return the expected data based on which path was taken. @@ -124,7 +127,7 @@ function patchDataCamelCase( origData, options ) { // Don't use the instance method here to avoid `data-*` attributes // detection this early. - curData = origJQueryData( elem ); + curData = origJQueryStaticData( elem ); if ( curData && name in curData ) { migrateWarn( "data-camelCase", @@ -139,11 +142,13 @@ function patchDataCamelCase( origData, options ) { } function patchRemoveDataCamelCase( origRemoveData, options ) { - var isInstanceMethod = options.isInstanceMethod; + var isPrivateData = options.isPrivateData, + isInstanceMethod = options.isInstanceMethod, + origJQueryStaticData = isPrivateData ? origJQueryPrivateData : origJQueryData; function remove( elem, keys ) { var i, singleKey, unCamelCasedKeys, - curData = jQuery.data( elem ); + curData = origJQueryStaticData( elem ); if ( keys === undefined ) { origRemoveData( elem ); @@ -223,104 +228,64 @@ function patchRemoveDataCamelCase( origRemoveData, options ) { migratePatchFunc( jQuery, "data", patchDataCamelCase( jQuery.data, { apiName: "jQuery.data()", + isPrivateData: false, + isInstanceMethod: false + } ), + "data-camelCase" ); +migratePatchFunc( jQuery, "_data", + patchDataCamelCase( jQuery._data, { + apiName: "jQuery._data()", + isPrivateData: true, isInstanceMethod: false } ), "data-camelCase" ); migratePatchFunc( jQuery.fn, "data", patchDataCamelCase( jQuery.fn.data, { apiName: "jQuery.fn.data()", + isPrivateData: false, isInstanceMethod: true } ), "data-camelCase" ); migratePatchFunc( jQuery, "removeData", patchRemoveDataCamelCase( jQuery.removeData, { + isPrivateData: false, + isInstanceMethod: false + } ), + "data-camelCase" ); +migratePatchFunc( jQuery, "_removeData", + patchRemoveDataCamelCase( jQuery._removeData, { + isPrivateData: true, 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, { + isPrivateData: false, 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, + var warningId = options.warningId, 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 ); + isInstanceMethod = options.isInstanceMethod; - return function jQueryDataProtoPatched() { + return function apiWithProtoPatched() { 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 ); + patchProto( result, { + warningId: warningId, + apiName: apiName + } ); return result; }; @@ -330,15 +295,22 @@ function patchDataProto( original, options ) { // so that each of the two patches can be independently disabled. migratePatchFunc( jQuery, "data", patchDataProto( jQuery.data, { + warningId: "data-null-proto", apiName: "jQuery.data()", - isPrivateData: false, + isInstanceMethod: false + } ), + "data-null-proto" ); +migratePatchFunc( jQuery, "_data", + patchDataProto( jQuery._data, { + warningId: "data-null-proto", + apiName: "jQuery._data()", isInstanceMethod: false } ), "data-null-proto" ); migratePatchFunc( jQuery.fn, "data", patchDataProto( jQuery.fn.data, { + warningId: "data-null-proto", apiName: "jQuery.fn.data()", - isPrivateData: true, isInstanceMethod: true } ), "data-null-proto" ); diff --git a/src/utils.js b/src/utils.js index d51891c..52bf203 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,5 +1,74 @@ +import { migrateWarn } from "./main.js"; + export function camelCase( string ) { return string.replace( /-([a-z])/g, function( _, letter ) { return letter.toUpperCase(); } ); } + +// Insert an additional object in the prototype chain between `objrvy` +// and `Object.prototype`; that intermediate object proxies properties +// to `Object.prototype`, warning about their usage first. +export function patchProto( object, 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 object; + } + + var i, + warningId = options.warningId, + apiName = options.apiName, + + // `Object.prototype` keys are not enumerable so list the + // official ones here. An alternative would be wrapping + // 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 afterward. + intermediateObj = Object.create( null ); + + for ( i = 0; i < objProtoKeys.length; i++ ) { + ( function( key ) { + Object.defineProperty( intermediateObj, key, { + get: function() { + migrateWarn( warningId, + "Accessing properties from " + apiName + + " inherited from Object.prototype is deprecated" ); + return ( key + "__cache" ) in intermediateObj ? + intermediateObj[ key + "__cache" ] : + Object.prototype[ key ]; + }, + set: function( value ) { + migrateWarn( warningId, + "Setting properties from " + apiName + + " inherited from Object.prototype is deprecated" ); + intermediateObj[ key + "__cache" ] = value; + } + } ); + } )( objProtoKeys[ i ] ); + } + + Object.setPrototypeOf( intermediateObj, Object.prototype ); + Object.setPrototypeOf( object, intermediateObj ); + + return object; +} diff --git a/test/unit/jquery/data.js b/test/unit/jquery/data.js index e487f1e..c372b1b 100644 --- a/test/unit/jquery/data.js +++ b/test/unit/jquery/data.js @@ -13,6 +13,12 @@ function camelCase( string ) { return jQuery.data.apply( jQuery, arguments ); } }, + { + apiName: "jQuery._data()", + dataFn: function() { + return jQuery._data.apply( jQuery, arguments ); + } + }, { apiName: "jQuery.fn.data()", dataFn: function( elem ) { @@ -142,66 +148,106 @@ function camelCase( string ) { } ); -QUnit.test( ".removeData()", function( assert ) { - assert.expect( 5 ); +[ + { + apiName: "jQuery.removeData()", + dataFn: function() { + return jQuery.data.apply( jQuery, arguments ); + }, + removeDataFn: function() { + return jQuery.removeData.apply( jQuery, arguments ); + } + }, + { + apiName: "jQuery._removeData()", + dataFn: function() { + return jQuery._data.apply( jQuery, arguments ); + }, + removeDataFn: function() { + return jQuery._removeData.apply( jQuery, arguments ); + } + }, + { + apiName: "jQuery.fn.removeData()", + dataFn: function( elem ) { + var args = Array.prototype.slice.call( arguments, 1 ); + return jQuery.fn.data.apply( jQuery( elem ), args ); + }, + removeDataFn: function( elem ) { + var args = Array.prototype.slice.call( arguments, 1 ); + return jQuery.fn.removeData.apply( jQuery( elem ), args ); + } + } +].forEach( function( params ) { + var apiName = params.apiName, + dataFn = params.dataFn, + removeDataFn = params.removeDataFn; + + QUnit.test( apiName + " camelCased names", function( assert ) { + assert.expect( 7 ); - var div1 = jQuery( "
" ).appendTo( "#qunit-fixture" ), - div2 = jQuery( "
" ).appendTo( "#qunit-fixture" ); + var div1 = jQuery( "
" ).appendTo( "#qunit-fixture" ), + div2 = jQuery( "
" ).appendTo( "#qunit-fixture" ); - // Mixed assignment - div1.add( div2 ) - .data( { "a-a-a": 1, "b-bB": 2, "cCC": 3 } ) - .data( "d-d-d", 4 ) - .data( "e-eE", 5 ) - .data( "fFF", 6 ); + // Mixed assignment + [ div1, div2 ].forEach( function( div ) { + dataFn( div, { "a-a-a": 1, "b-bB": 2, "cCC": 3 } ); + dataFn( div, "d-d-d", 4 ); + dataFn( div, "e-eE", 5 ); + dataFn( div, "fFF", 6 ); + } ); - expectNoWarning( assert, "camelCase args", function() { - div1 - .removeData( "aAA cCC eEE" ) - .removeData( [ "bBB", "dDD", "fFF" ] ); - } ); + expectNoWarning( assert, "camelCase args", function() { + removeDataFn( div1, "aAA cCC eEE" ); + removeDataFn( div1, [ "bBB", "dDD", "fFF" ] ); + } ); - expectWarning( assert, "Not camelCase args originally present", 2, function() { + expectWarning( assert, "Not camelCase args originally present", 2, function() { - // We expect two warnings as only the object-set keys are set - // in their original form. - div2 - .removeData( "a-a-a e-eE" ) - .removeData( [ "d-d-d", "b-bB" ] ); - } ); + // We expect two warnings as only the object-set keys are set + // in their original form. + removeDataFn( div2, "a-a-a e-eE" ); + removeDataFn( div2, [ "d-d-d", "b-bB" ] ); + } ); - expectNoWarning( assert, "Not camelCase args originally missing", function() { - div2 - .removeData( "c-cC" ) - .removeData( [ "f-f-f" ] ); - } ); + expectNoWarning( assert, "Not camelCase args originally missing", function() { + removeDataFn( div2, "c-cC" ); + removeDataFn( div2, [ "f-f-f" ] ); + } ); - // Divergence from jQuery 3.x: partially camelCased keys set in the object - // setter need to be passed in the same form when removing. - div1.removeData( "b-bB" ); + // Divergence from jQuery 3.x: partially camelCased keys set in the object + // setter need to be passed in the same form when removing. + removeDataFn( div1, "b-bB" ); - assert.deepEqual( div1.data(), {}, "Data is empty. (div1)" ); - assert.deepEqual( div2.data(), {}, "Data is empty. (div2)" ); + assert.deepEqual( div1.data(), {}, "Data is empty (div1)" ); + assert.deepEqual( div2.data(), {}, "Data is empty (div2)" ); + assert.deepEqual( jQuery._data( div1 ), {}, "Private data is empty (div1)" ); + assert.deepEqual( jQuery._data( div2 ), {}, "Private data is empty (div2)" ); + } ); } ); QUnit.test( "properties from Object.prototype", function( assert ) { - assert.expect( 6 ); + assert.expect( 8 ); var div = jQuery( "
" ).appendTo( "#qunit-fixture" ); div.data( "foo", "bar" ); + jQuery._data( div[ 0 ], "baz", "qaz" ); expectNoWarning( assert, "Regular properties", function() { assert.strictEqual( div.data( "foo" ), "bar", "data access" ); assert.strictEqual( jQuery.data( div[ 0 ], "foo" ), "bar", "data access (static method)" ); + assert.strictEqual( jQuery._data( div[ 0 ], "baz" ), "qaz", "private data access" ); } ); ( Object.setPrototypeOf ? expectWarning : expectNoWarning - )( assert, "Properties from Object.prototype", 2, function() { + )( assert, "Properties from Object.prototype", 3, function() { assert.ok( div.data().hasOwnProperty( "foo" ), "hasOwnProperty works" ); assert.ok( jQuery.data( div[ 0 ] ).hasOwnProperty( "foo" ), "hasOwnProperty works (static method)" ); + assert.ok( jQuery._data( div[ 0 ] ).hasOwnProperty( "baz" ), + "hasOwnProperty works (private data)" ); } ); } );