Revert data.js rewrite.

Reverts the following commits:

commit f717226b3a
Author: Rick Waldron <waldron.rick@gmail.com>
Date:   Mon Dec 31 18:06:38 2012 -0500

Only splice from internal arrays when item actually exists.

commit b9cdc4136b
Author: Rick Waldron <waldron.rick@gmail.com>
Date:   Mon Dec 31 16:20:35 2012 -0500

Updates to data.js re-write to pass events and manipulation

commit d1de3000c6
Author: Rick Waldron <waldron.rick@gmail.com>
Date:   Mon Dec 31 15:09:45 2012 -0500

2.0: Rewrite data.js
This commit is contained in:
Dave Methvin 2013-01-03 20:39:15 -05:00
parent 0d540c3750
commit 445dbd9d95
5 changed files with 326 additions and 341 deletions

View File

@ -1,97 +1,140 @@
var user, priv, data_user, data_priv, var rbrace = /(?:\{[\s\S]*\}|\[[\s\S]*\])$/,
rbrace = /(?:\{[\s\S]*\}|\[[\s\S]*\])$/,
rmultiDash = /([A-Z])/g; rmultiDash = /([A-Z])/g;
function Data() { function internalData( elem, name, data, pvt /* Internal Use Only */ ){
// Nodes|Objects if ( !jQuery.acceptData( elem ) ) {
this.owners = []; return;
// Data objects }
this.cache = [];
var thisCache, ret,
internalKey = jQuery.expando,
getByName = typeof name === "string",
// We have to handle DOM nodes and JS objects differently because IE6-7
// can't GC object references properly across the DOM-JS boundary
isNode = elem.nodeType,
// Only DOM nodes need the global jQuery cache; JS object data is
// attached directly to the object so GC can occur automatically
cache = isNode ? jQuery.cache : elem,
// Only defining an ID for JS objects if its cache already exists allows
// the code to shortcut on the same path as a DOM node with no cache
id = isNode ? elem[ internalKey ] : elem[ internalKey ] && internalKey;
// Avoid doing any more work than we need to when trying to get data on an
// object that has no data at all
if ( (!id || !cache[id] || (!pvt && !cache[id].data)) && getByName && data === undefined ) {
return;
}
if ( !id ) {
// Only DOM nodes need a new unique ID for each element since their data
// ends up in the global cache
if ( isNode ) {
elem[ internalKey ] = id = core_deletedIds.pop() || jQuery.guid++;
} else {
id = internalKey;
}
}
if ( !cache[ id ] ) {
cache[ id ] = {};
// Avoids exposing jQuery metadata on plain JS objects when the object
// is serialized using JSON.stringify
if ( !isNode ) {
cache[ id ].toJSON = jQuery.noop;
}
}
// An object can be passed to jQuery.data instead of a key/value pair; this gets
// shallow copied over onto the existing cache
if ( typeof name === "object" || typeof name === "function" ) {
if ( pvt ) {
cache[ id ] = jQuery.extend( cache[ id ], name );
} else {
cache[ id ].data = jQuery.extend( cache[ id ].data, name );
}
}
thisCache = cache[ id ];
// jQuery data() is stored in a separate object inside the object's internal data
// cache in order to avoid key collisions between internal data and user-defined
// data.
if ( !pvt ) {
if ( !thisCache.data ) {
thisCache.data = {};
}
thisCache = thisCache.data;
}
if ( data !== undefined ) {
thisCache[ jQuery.camelCase( name ) ] = data;
}
// Check for both converted-to-camel and non-converted data property names
// If a data property was specified
if ( getByName ) {
// First Try to find as-is property data
ret = thisCache[ name ];
// Test for null|undefined property data
if ( ret == null ) {
// Try to find the camelCased property
ret = thisCache[ jQuery.camelCase( name ) ];
}
} else {
ret = thisCache;
}
return ret;
} }
Data.prototype = { function internalRemoveData( elem, name, pvt /* For internal use only */ ){
add: function( owner ) { if ( !jQuery.acceptData( elem ) ) {
this.owners.push( owner ); return;
return (this.cache[ this.owners.length - 1 ] = {});
},
set: function( owner, data, value ) {
var prop,
index = this.owners.indexOf( owner );
if ( index === -1 ) {
this.add( owner );
index = this.owners.length - 1;
} }
if ( typeof data === "string" ) {
this.cache[ index ][ data ] = value; var thisCache, i, l,
isNode = elem.nodeType,
// See jQuery.data for more information
cache = isNode ? jQuery.cache : elem,
id = isNode ? elem[ jQuery.expando ] : jQuery.expando;
// If there is already no cache entry for this object, there is no
// purpose in continuing
if ( !cache[ id ] ) {
return;
}
if ( name ) {
thisCache = pvt ? cache[ id ] : cache[ id ].data;
if ( thisCache ) {
// Support array or space separated string names for data keys
if ( !jQuery.isArray( name ) ) {
// try the string as a key before any manipulation
if ( name in thisCache ) {
name = [ name ];
} else { } else {
if ( jQuery.isEmptyObject( this.cache[ index ] ) ) {
this.cache[ index ] = data;
} else {
for ( prop in data ) {
this.cache[ index ][ prop ] = data[ prop ];
}
}
}
return this;
},
get: function( owner, key ) {
var cache,
index = this.owners.indexOf( owner );
// A valid cache is found, or needs to be created.
// New entries will be added and return the current
// empty data object to be used as a return reference
// return this.add( owner );
cache = index === -1 ?
this.add( owner ) : this.cache[ index ];
return key === undefined ?
cache : cache[ key ];
},
access: function( owner, key, value ) {
if ( value === undefined && (key && typeof key !== "object") ) {
// Assume this is a request to read the cached data
return this.get( owner, key );
} else {
// If only an owner was specified, return the entire
// cache object.
if ( key === undefined ) {
return this.get( owner );
}
// Allow setting or extending (existing objects) with an
// object of properties, or a key and val
this.set( owner, key, value );
return value !== undefined ? value : key;
}
// Otherwise, this is a read request.
return this.get( owner, key );
},
remove: function( owner, key ) {
var i, l, name,
camel = jQuery.camelCase,
index = this.owners.indexOf( owner ),
cache = this.cache[ index ];
if ( key === undefined ) {
cache = {};
} else {
if ( cache ) {
// Support array or space separated string of keys
if ( !Array.isArray( key ) ) {
// Try the string as a key before any manipulation
//
if ( key in cache ) {
name = [ key ];
} else {
// split the camel cased version by spaces unless a key with the spaces exists // split the camel cased version by spaces unless a key with the spaces exists
name = camel( key ); name = jQuery.camelCase( name );
name = name in cache ? if ( name in thisCache ) {
[ name ] : name.split(" "); name = [ name ];
} else {
name = name.split(" ");
}
} }
} else { } else {
// If "name" is an array of keys... // If "name" is an array of keys...
@ -100,83 +143,90 @@ Data.prototype = {
// Since there is no way to tell _how_ a key was added, remove // Since there is no way to tell _how_ a key was added, remove
// both plain key and camelCase key. #12786 // both plain key and camelCase key. #12786
// This will only penalize the array argument path. // This will only penalize the array argument path.
name = key.concat( key.map( camel ) ); name = name.concat( jQuery.map( name, jQuery.camelCase ) );
} }
i = 0;
l = name.length;
for ( ; i < l; i++ ) { for ( i = 0, l = name.length; i < l; i++ ) {
delete cache[ name[i] ]; delete thisCache[ name[i] ];
} }
}
}
this.cache[ index ] = cache;
},
hasData: function( owner ) {
var index = this.owners.indexOf( owner );
if ( index > -1 ) { // If there is no data left in the cache, we want to continue
return !jQuery.isEmptyObject( this.cache[ index ] ); // and let the cache object itself get destroyed
if ( !( pvt ? isEmptyDataObject : jQuery.isEmptyObject )( thisCache ) ) {
return;
}
}
} }
return false;
},
discard: function( owner ) {
var index = this.owners.indexOf( owner );
if ( index >= 0 ) { // See jQuery.data for more information
this.owners.splice( index, 1 ); if ( !pvt ) {
this.cache.splice( index, 1 ); delete cache[ id ].data;
}
return this;
}
};
// This will be used by remove() in manipulation to sever // Don't destroy the parent cache unless the internal data object
// remaining references to node objects. One day we'll replace the dual // had been the only thing left in it
// arrays with a WeakMap and this won't be an issue. if ( !isEmptyDataObject( cache[ id ] ) ) {
function data_discard( owner ) { return;
user.discard( owner ); }
priv.discard( owner ); }
// Destroy the cache
if ( isNode ) {
jQuery.cleanData( [ elem ], true );
// Use delete when supported for expandos or `cache` is not a window per isWindow (#10080)
} else if ( jQuery.support.deleteExpando || cache != cache.window ) {
delete cache[ id ];
// When all else fails, null
} else {
cache[ id ] = null;
}
} }
// These may used throughout the jQuery core codebase
user = data_user = new Data();
priv = data_priv = new Data();
jQuery.extend({ jQuery.extend({
acceptData: function() { cache: {},
return true;
},
// Unique for each copy of jQuery on the page // Unique for each copy of jQuery on the page
// Non-digits removed to match rinlinejQuery // Non-digits removed to match rinlinejQuery
expando: "jQuery" + ( core_version + Math.random() ).replace( /\D/g, "" ), expando: "jQuery" + ( core_version + Math.random() ).replace( /\D/g, "" ),
// The following elements throw uncatchable exceptions if you
// attempt to add expando properties to them.
noData: {
"embed": true,
// Ban all objects except for Flash (which handle expandos)
"object": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",
"applet": true
},
hasData: function( elem ) { hasData: function( elem ) {
return user.hasData( elem ) || priv.hasData( elem ); elem = elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ];
return !!elem && !isEmptyDataObject( elem );
}, },
data: function( elem, name, data ) { data: function( elem, name, data ) {
return user.access( elem, name, data ); return internalData( elem, name, data, false );
}, },
removeData: function( elem, name ) { removeData: function( elem, name ) {
return user.remove( elem, name ); return internalRemoveData( elem, name, false );
}, },
// TODO: Replace all calls to _data and _removeData with direct // For internal use only.
// calls to
//
// priv.access( elem, name, data );
//
// priv.remove( elem, name );
//
_data: function( elem, name, data ) { _data: function( elem, name, data ) {
return priv.access( elem, name, data ); return internalData( elem, name, data, true );
}, },
_removeData: function( elem, name ) { _removeData: function( elem, name ) {
return priv.remove( elem, name ); return internalRemoveData( elem, name, true );
},
// A method for determining if a DOM node can handle the data expando
acceptData: function( elem ) {
var noData = elem.nodeName && jQuery.noData[ elem.nodeName.toLowerCase() ];
// nodes accept data unless otherwise specified; rejection can be conditional
return !noData || noData !== true && elem.getAttribute("classid") === noData;
} }
}); });
@ -190,19 +240,20 @@ jQuery.fn.extend({
// Gets all values // Gets all values
if ( key === undefined ) { if ( key === undefined ) {
if ( this.length ) { if ( this.length ) {
data = user.get( elem ); data = jQuery.data( elem );
if ( elem.nodeType === 1 && !priv.get( elem, "hasDataAttrs" ) ) { if ( elem.nodeType === 1 && !jQuery._data( elem, "parsedAttrs" ) ) {
attrs = elem.attributes; attrs = elem.attributes;
for ( ; i < attrs.length; i++ ) { for ( ; i < attrs.length; i++ ) {
name = attrs[i].name; name = attrs[i].name;
if ( name.indexOf( "data-" ) === 0 ) { if ( !name.indexOf( "data-" ) ) {
name = jQuery.camelCase( name.substring(5) ); name = jQuery.camelCase( name.substring(5) );
dataAttr( elem, name, data[ name ] ); dataAttr( elem, name, data[ name ] );
} }
} }
priv.set( elem, "hasDataAttrs", true ); jQuery._data( elem, "parsedAttrs", true );
} }
} }
@ -212,79 +263,37 @@ jQuery.fn.extend({
// Sets multiple values // Sets multiple values
if ( typeof key === "object" ) { if ( typeof key === "object" ) {
return this.each(function() { return this.each(function() {
user.set( this, key ); jQuery.data( this, key );
}); });
} }
return jQuery.access( this, function( value ) { return jQuery.access( this, function( value ) {
var data,
camelKey = jQuery.camelCase( key );
// Get the Data...
if ( value === undefined ) { if ( value === undefined ) {
// Try to fetch any internally stored data first
// Attempt to get data from the cache return elem ? dataAttr( elem, key, jQuery.data( elem, key ) ) : null;
// with the key as-is
data = user.get( elem, key );
if ( data !== undefined ) {
return data;
} }
// Attempt to "discover" the data in
// HTML5 custom data-* attrs
data = dataAttr( elem, key, undefined );
if ( data !== undefined ) {
return data;
}
// As a last resort, attempt to find
// the data by checking AGAIN, but with
// a camelCased key.
data = user.get( elem, camelKey );
if ( data !== undefined ) {
return data;
}
// We tried really hard, but the data doesn't exist.
return undefined;
}
// Set the data...
this.each(function() { this.each(function() {
// First, attempt to store a copy or reference of any jQuery.data( this, key, value );
// data that might've been store with a camelCased key.
var data = user.get( this, camelKey );
// For HTML5 data-* attribute interop, we have to
// store property names with dashes in a camelCase form.
// This might not apply to all properties...*
user.set( this, camelKey, value );
// *... In the case of properties that might ACTUALLY
// have dashes, we need to also store a copy of that
// unchanged property.
if ( /-/.test( key ) && data !== undefined ) {
user.set( this, key, value );
}
}); });
}, null, value, arguments.length > 1, null, true ); }, null, value, arguments.length > 1, null, true );
}, },
removeData: function( key ) { removeData: function( key ) {
return this.each(function() { return this.each(function() {
user.remove( this, key ); jQuery.removeData( this, key );
}); });
} }
}); });
function dataAttr( elem, key, data ) { function dataAttr( elem, key, data ) {
var name;
// If nothing was found internally, try to fetch any // If nothing was found internally, try to fetch any
// data from the HTML5 data-* attribute // data from the HTML5 data-* attribute
if ( data === undefined && elem.nodeType === 1 ) { if ( data === undefined && elem.nodeType === 1 ) {
name = "data-" + key.replace( rmultiDash, "-$1" ).toLowerCase(); var name = "data-" + key.replace( rmultiDash, "-$1" ).toLowerCase();
data = elem.getAttribute( name ); data = elem.getAttribute( name );
if ( typeof data === "string" ) { if ( typeof data === "string" ) {
@ -294,12 +303,13 @@ function dataAttr( elem, key, data ) {
data === "null" ? null : data === "null" ? null :
// Only convert to a number if it doesn't change the string // Only convert to a number if it doesn't change the string
+data + "" === data ? +data : +data + "" === data ? +data :
rbrace.test( data ) ? rbrace.test( data ) ? jQuery.parseJSON( data ) :
JSON.parse( data ) : data; data;
} catch( e ) {} } catch( e ) {}
// Make sure we set the data so it isn't changed later // Make sure we set the data so it isn't changed later
user.set( elem, key, data ); jQuery.data( elem, key, data );
} else { } else {
data = undefined; data = undefined;
} }
@ -307,3 +317,20 @@ function dataAttr( elem, key, data ) {
return data; return data;
} }
// checks a cache object for emptiness
function isEmptyDataObject( obj ) {
var name;
for ( name in obj ) {
// if the public data object is empty, the private is still empty
if ( name === "data" && jQuery.isEmptyObject( obj[name] ) ) {
continue;
}
if ( name !== "toJSON" ) {
return false;
}
}
return true;
}

View File

@ -16,7 +16,7 @@ jQuery.event = {
t = 0; t = 0;
// Don't attach events to noData or text/comment nodes (allow plain objects tho) // Don't attach events to noData or text/comment nodes (allow plain objects tho)
if ( elem.nodeType === 3 || elem.nodeType === 8 || !types || !handler || !(elemData = data_priv.get( elem )) ) { if ( elem.nodeType === 3 || elem.nodeType === 8 || !types || !handler || !(elemData = jQuery._data( elem )) ) {
return; return;
} }
@ -124,7 +124,7 @@ jQuery.event = {
var tns, type, origType, namespaces, origCount, var tns, type, origType, namespaces, origCount,
j, events, special, eventType, handleObj, j, events, special, eventType, handleObj,
t = 0, t = 0,
elemData = data_priv.hasData( elem ) && data_priv.get( elem ); elemData = jQuery.hasData( elem ) && jQuery._data( elem );
if ( !elemData || !(events = elemData.events) ) { if ( !elemData || !(events = elemData.events) ) {
return; return;

View File

@ -327,7 +327,7 @@ jQuery.fn.extend({
for ( i = 0; i < hasScripts; i++ ) { for ( i = 0; i < hasScripts; i++ ) {
node = scripts[ i ]; node = scripts[ i ];
if ( rscriptType.test( node.type || "" ) && if ( rscriptType.test( node.type || "" ) &&
!data_priv.access( node, "globalEval" ) && jQuery.contains( doc, node ) ) { !jQuery._data( node, "globalEval" ) && jQuery.contains( doc, node ) ) {
if ( node.src ) { if ( node.src ) {
// Hope ajax is available... // Hope ajax is available...
@ -514,16 +514,20 @@ jQuery.extend({
}, },
cleanData: function( elems, /* internal */ acceptData ) { cleanData: function( elems, /* internal */ acceptData ) {
var data, elem, type, var id, data, elem, type,
l = elems.length, l = elems.length,
i = 0, i = 0,
internalKey = jQuery.expando,
cache = jQuery.cache,
special = jQuery.event.special; special = jQuery.event.special;
for ( ; i < l; i++ ) { for ( ; i < l; i++ ) {
elem = elems[ i ]; elem = elems[ i ];
if ( acceptData || jQuery.acceptData( elem ) ) { if ( acceptData || jQuery.acceptData( elem ) ) {
data = data_priv.access( elem );
id = elem[ internalKey ];
data = id && cache[ id ];
if ( data ) { if ( data ) {
for ( type in data.events ) { for ( type in data.events ) {
@ -535,12 +539,14 @@ jQuery.extend({
jQuery.removeEvent( elem, type, data.handle ); jQuery.removeEvent( elem, type, data.handle );
} }
} }
}
}
// Discard any remaining `private` and `user` data // Remove cache only if it was not already removed by jQuery.event.remove
// (Splices the data objects out of the internal cache arrays) if ( cache[ id ] ) {
data_discard( elem ); delete cache[ id ];
delete elem[ internalKey ];
}
}
}
} }
} }
}); });
@ -574,30 +580,24 @@ function setGlobalEval( elems, refElements ) {
i = 0; i = 0;
for ( ; i < l; i++ ) { for ( ; i < l; i++ ) {
data_priv.set( jQuery._data( elems[ i ], "globalEval", !refElements || jQuery._data( refElements[ i ], "globalEval" ) );
elems[ i ], "globalEval", !refElements || data_priv.get( refElements[ i ], "globalEval" )
);
} }
} }
function cloneCopyEvent( src, dest ) { function cloneCopyEvent( src, dest ) {
var i, l, type, pOld, pCur, uOld, uCur, events;
if ( dest.nodeType !== 1 ) { if ( dest.nodeType !== 1 || !jQuery.hasData( src ) ) {
return; return;
} }
// 1. Copy private data: events, handlers, etc. var i, l, type,
if ( data_priv.hasData( src ) ) { oldData = jQuery._data( src ),
pOld = data_priv.access( src ); curData = jQuery._data( dest, oldData ),
pCur = jQuery.extend( {}, pOld ); events = oldData.events;
events = pOld.events;
data_priv.set( dest, pCur );
if ( events ) { if ( events ) {
delete pCur.handle; delete curData.handle;
pCur.events = {}; curData.events = {};
for ( type in events ) { for ( type in events ) {
for ( i = 0, l = events[ type ].length; i < l; i++ ) { for ( i = 0, l = events[ type ].length; i < l; i++ ) {
@ -605,14 +605,10 @@ function cloneCopyEvent( src, dest ) {
} }
} }
} }
}
// 2. Copy user data // make the cloned public data object a copy from the original
if ( data_user.hasData( src ) ) { if ( curData.data ) {
uOld = data_user.access( src ); curData.data = jQuery.extend( {}, curData.data );
uCur = jQuery.extend( {}, uOld );
data_user.set( dest, uCur );
} }
} }

View File

@ -200,8 +200,6 @@ var Globals = (function() {
*/ */
QUnit.expectJqData = function( elems, key ) { QUnit.expectJqData = function( elems, key ) {
var i, elem, expando; var i, elem, expando;
if ( jQuery.cache ) {
QUnit.current_testEnvironment.checkJqData = true; QUnit.current_testEnvironment.checkJqData = true;
if ( elems.jquery && elems.toArray ) { if ( elems.jquery && elems.toArray ) {
@ -243,8 +241,6 @@ var Globals = (function() {
} }
} }
} }
}
}; };
QUnit.config.urlConfig.push( { QUnit.config.urlConfig.push( {
id: "jqdata", id: "jqdata",
@ -262,7 +258,6 @@ var Globals = (function() {
fragmentsLength = 0, fragmentsLength = 0,
cacheLength = 0; cacheLength = 0;
if ( jQuery.cache ) {
// Only look for jQuery data problems if this test actually // Only look for jQuery data problems if this test actually
// provided some information to compare against. // provided some information to compare against.
if ( QUnit.urlParams.jqdata || this.checkJqData ) { if ( QUnit.urlParams.jqdata || this.checkJqData ) {
@ -284,17 +279,13 @@ var Globals = (function() {
// Reset data register // Reset data register
expectedDataKeys = {}; expectedDataKeys = {};
}
// Allow QUnit.reset to clean up any attached elements before checking for leaks // Allow QUnit.reset to clean up any attached elements before checking for leaks
QUnit.reset(); QUnit.reset();
if ( jQuery.cache ) {
for ( i in jQuery.cache ) { for ( i in jQuery.cache ) {
++cacheLength; ++cacheLength;
} }
}
jQuery.fragments = {}; jQuery.fragments = {};

View File

@ -1,38 +1,11 @@
module( "data", { teardown: moduleTeardown }); module("data", { teardown: moduleTeardown });
test( "expando", function() { test("expando", function(){
expect(1); expect(1);
equal(jQuery.expando !== undefined, true, "jQuery is exposing the expando"); equal(jQuery.expando !== undefined, true, "jQuery is exposing the expando");
}); });
test( "jQuery.data & removeData, expected returns", function() {
expect(2);
equal(
jQuery.data( document.body, "hello", "world" ), "world",
"jjQuery.data( elem, key, value ) returns value"
);
equal(
jQuery.removeData( document.body, "hello" ), undefined,
"jjQuery.removeData( elem, key, value ) returns undefined"
);
});
test( "jQuery._data & _removeData, expected returns", function() {
expect(2);
equal(
jQuery._data( document.body, "hello", "world" ), "world",
"jjQuery.data( elem, key, value ) returns value"
);
equal(
jQuery._removeData( document.body, "hello" ), undefined,
"jjQuery.removeData( elem, key, value ) returns undefined"
);
});
function dataTests (elem) { function dataTests (elem) {
var oldCacheLength, dataObj, internalDataObj, expected, actual; var oldCacheLength, dataObj, internalDataObj, expected, actual;
@ -126,7 +99,6 @@ test("jQuery.data(document)", 25, function() {
QUnit.expectJqData(document, "foo"); QUnit.expectJqData(document, "foo");
}); });
/*
test("Expando cleanup", 4, function() { test("Expando cleanup", 4, function() {
var expected, actual, var expected, actual,
div = document.createElement("div"); div = document.createElement("div");
@ -160,8 +132,7 @@ test("Expando cleanup", 4, function() {
// Clean up unattached element // Clean up unattached element
jQuery(div).remove(); jQuery(div).remove();
}); });
*/
/*
test("jQuery.acceptData", function() { test("jQuery.acceptData", function() {
expect(7); expect(7);
@ -179,7 +150,7 @@ test("jQuery.acceptData", function() {
applet.setAttribute("classid", "clsid:8AD9C840-044E-11D1-B3E9-00805F499D93"); applet.setAttribute("classid", "clsid:8AD9C840-044E-11D1-B3E9-00805F499D93");
ok( !jQuery.acceptData( applet ), "applet" ); ok( !jQuery.acceptData( applet ), "applet" );
}); });
*/
test(".data()", function() { test(".data()", function() {
expect(5); expect(5);
@ -494,8 +465,8 @@ test("jQuery.data should follow html5 specification regarding camel casing", fun
div.data("foo-bar", "d"); div.data("foo-bar", "d");
equal( div.data("fooBar"), "d", "Verify updated data-* key (fooBar)" ); equal( div.data("fooBar"), "d", "Verify updated data-* key" );
equal( div.data("foo-bar"), "d", "Verify updated data-* key (foo-bar)" ); equal( div.data("foo-bar"), "d", "Verify updated data-* key" );
div.remove(); div.remove();
}); });