Change the way jQuery.data works so that there is no longer a chance of collision...
authorColin Snover <github.com@zetafleet.com>
Sun, 9 Jan 2011 21:52:33 +0000 (15:52 -0600)
committerColin Snover <github.com@zetafleet.com>
Sun, 9 Jan 2011 21:52:33 +0000 (15:52 -0600)
src/attributes.js
src/data.js
src/effects.js
src/event.js
src/manipulation.js
src/queue.js
test/unit/attributes.js
test/unit/data.js
test/unit/effects.js
test/unit/event.js

index fec1323..d37400a 100644 (file)
@@ -133,11 +133,11 @@ jQuery.fn.extend({
                        } else if ( type === "undefined" || type === "boolean" ) {
                                if ( this.className ) {
                                        // store className if set
-                                       jQuery.data( this, "__className__", this.className );
+                                       jQuery._data( this, "__className__", this.className );
                                }
 
                                // toggle whole className
-                               this.className = this.className || value === false ? "" : jQuery.data( this, "__className__" ) || "";
+                               this.className = this.className || value === false ? "" : jQuery._data( this, "__className__" ) || "";
                        }
                });
        },
index 4d1d1bd..a1abc9e 100644 (file)
@@ -1,7 +1,6 @@
 (function( jQuery ) {
 
-var windowData = {},
-       rbrace = /^(?:\{.*\}|\[.*\])$/;
+var rbrace = /^(?:\{.*\}|\[.*\])$/;
 
 jQuery.extend({
        cache: {},
@@ -23,110 +22,163 @@ jQuery.extend({
        },
 
        hasData: function( elem ) {
-               if ( elem.nodeType ) {
-                       elem = jQuery.cache[ elem[jQuery.expando] ];
-               }
+               elem = elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ];
 
                return !!elem && !jQuery.isEmptyObject(elem);
        },
 
-       data: function( elem, name, data ) {
+       data: function( elem, name, data, pvt /* Internal Use Only */ ) {
                if ( !jQuery.acceptData( elem ) ) {
                        return;
                }
 
-               elem = elem == window ?
-                       windowData :
-                       elem;
+               var internalKey = jQuery.expando, getByName = typeof name === "string", thisCache,
+
+                       // 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,
 
-               var isNode = elem.nodeType,
-                       id = isNode ? elem[ jQuery.expando ] : null,
-                       cache = jQuery.cache, thisCache;
+                       // 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[ jQuery.expando ] : elem[ jQuery.expando ] && jQuery.expando;
 
-               if ( isNode && !id && typeof name === "string" && data === undefined ) {
+               // 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 || (pvt && id && !cache[ id ][ internalKey ])) && getByName && data === undefined ) {
                        return;
                }
 
-               // Get the data from the object directly
-               if ( !isNode ) {
-                       cache = elem;
+               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[ jQuery.expando ] = id = ++jQuery.uuid;
+                       } else {
+                               id = jQuery.expando;
+                       }
+               }
 
-               // Compute a unique ID for the element
-               } else if ( !id ) {
-                       elem[ jQuery.expando ] = id = ++jQuery.uuid;
+               if ( !cache[ id ] ) {
+                       cache[ id ] = {};
                }
 
-               // Avoid generating a new cache unless none exists and we
-               // want to manipulate it.
+               // 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" ) {
-                       if ( isNode ) {
+                       if ( pvt ) {
+                               cache[ id ][ internalKey ] = jQuery.extend(cache[ id ][ internalKey ], name);
+                       } else {
                                cache[ id ] = jQuery.extend(cache[ id ], name);
+                       }
+               }
 
-                       } else {
-                               jQuery.extend( cache, name );
+               thisCache = cache[ id ];
+
+               // Internal jQuery data is stored in a separate object inside the object's data
+               // cache in order to avoid key collisions between internal data and user-defined
+               // data
+               if ( pvt ) {
+                       if ( !thisCache[ internalKey ] ) {
+                               thisCache[ internalKey ] = {};
                        }
 
-               } else if ( isNode && !cache[ id ] ) {
-                       cache[ id ] = {};
+                       thisCache = thisCache[ internalKey ];
                }
 
-               thisCache = isNode ? cache[ id ] : cache;
-
-               // Prevent overriding the named cache with undefined values
                if ( data !== undefined ) {
                        thisCache[ name ] = data;
                }
 
-               return typeof name === "string" ? thisCache[ name ] : thisCache;
+               return getByName ? thisCache[ name ] : thisCache;
        },
 
-       removeData: function( elem, name ) {
+       removeData: function( elem, name, pvt /* Internal Use Only */ ) {
                if ( !jQuery.acceptData( elem ) ) {
                        return;
                }
 
-               elem = elem == window ?
-                       windowData :
-                       elem;
+               var internalKey = jQuery.expando, isNode = elem.nodeType,
+
+                       // See jQuery.data for more information
+                       cache = isNode ? jQuery.cache : elem,
+
+                       // See jQuery.data for more information
+                       id = isNode ? elem[ jQuery.expando ] : jQuery.expando;
 
-               var isNode = elem.nodeType,
-                       id = isNode ? elem[ jQuery.expando ] : elem,
-                       cache = jQuery.cache,
-                       thisCache = isNode ? cache[ id ] : id;
+               // If there is already no cache entry for this object, there is no
+               // purpose in continuing
+               if ( !cache[ id ] ) {
+                       return;
+               }
 
-               // If we want to remove a specific section of the element's data
                if ( name ) {
+                       var thisCache = pvt ? cache[ id ][ internalKey ] : cache[ id ];
+
                        if ( thisCache ) {
-                               // Remove the section of cache data
                                delete thisCache[ name ];
 
-                               // If we've removed all the data, remove the element's cache
-                               if ( isNode && jQuery.isEmptyObject(thisCache) ) {
-                                       jQuery.removeData( elem );
+                               // If there is no data left in the cache, we want to continue
+                               // and let the cache object itself get destroyed
+                               if ( !jQuery.isEmptyObject(thisCache) ) {
+                                       return;
                                }
                        }
+               }
+
+               // See jQuery.data for more information
+               if ( pvt ) {
+                       delete cache[ id ][ internalKey ];
+
+                       // Don't destroy the parent cache unless the internal data object
+                       // had been the only thing left in it
+                       if ( !jQuery.isEmptyObject(cache[ id ]) ) {
+                               return;
+                       }
+               }
+
+               var internalCache = cache[ id ][ internalKey ];
 
-               // Otherwise, we want to remove all of the element's data
+               // Browsers that fail expando deletion also refuse to delete expandos on
+               // the window, but it will allow it on all other JS objects; other browsers
+               // don't care
+               if ( jQuery.support.deleteExpando || cache != window ) {
+                       delete cache[ id ];
                } else {
-                       if ( isNode && jQuery.support.deleteExpando ) {
-                               delete elem[ jQuery.expando ];
+                       cache[ id ] = null;
+               }
 
+               // We destroyed the entire user cache at once because it's faster than
+               // iterating through each key, but we need to continue to persist internal
+               // data if it existed
+               if ( internalCache ) {
+                       cache[ id ] = {};
+                       cache[ id ][ internalKey ] = internalCache;
+
+               // Otherwise, we need to eliminate the expando on the node to avoid
+               // false lookups in the cache for entries that no longer exist
+               } else if ( isNode ) {
+                       // IE does not allow us to delete expando properties from nodes,
+                       // nor does it have a removeAttribute function on Document nodes;
+                       // we must handle all of these cases
+                       if ( jQuery.support.deleteExpando ) {
+                               delete elem[ jQuery.expando ];
                        } else if ( elem.removeAttribute ) {
                                elem.removeAttribute( jQuery.expando );
-
-                       // Completely remove the data cache
-                       } else if ( isNode ) {
-                               delete cache[ id ];
-
-                       // Remove all fields from the object
                        } else {
-                               for ( var n in elem ) {
-                                       delete elem[ n ];
-                               }
+                               elem[ jQuery.expando ] = null;
                        }
                }
        },
 
+       // For internal use only.
+       _data: function( elem, name, data ) {
+               return jQuery.data( elem, name, data, true );
+       },
+
        // A method for determining if a DOM node can handle the data expando
        acceptData: function( elem ) {
                if ( elem.nodeName ) {
index bd57ffc..b067539 100644 (file)
@@ -27,7 +27,7 @@ jQuery.fn.extend({
 
                                // Reset the inline display of this element to learn if it is
                                // being hidden by cascaded rules or not
-                               if ( !jQuery.data(elem, "olddisplay") && display === "none" ) {
+                               if ( !jQuery._data(elem, "olddisplay") && display === "none" ) {
                                        display = elem.style.display = "";
                                }
 
@@ -35,7 +35,7 @@ jQuery.fn.extend({
                                // in a stylesheet to whatever the default browser style is
                                // for such an element
                                if ( display === "" && jQuery.css( elem, "display" ) === "none" ) {
-                                       jQuery.data(elem, "olddisplay", defaultDisplay(elem.nodeName));
+                                       jQuery._data(elem, "olddisplay", defaultDisplay(elem.nodeName));
                                }
                        }
 
@@ -46,7 +46,7 @@ jQuery.fn.extend({
                                display = elem.style.display;
 
                                if ( display === "" || display === "none" ) {
-                                       elem.style.display = jQuery.data(elem, "olddisplay") || "";
+                                       elem.style.display = jQuery._data(elem, "olddisplay") || "";
                                }
                        }
 
@@ -62,8 +62,8 @@ jQuery.fn.extend({
                        for ( var i = 0, j = this.length; i < j; i++ ) {
                                var display = jQuery.css( this[i], "display" );
 
-                               if ( display !== "none" && !jQuery.data( this[i], "olddisplay" ) ) {
-                                       jQuery.data( this[i], "olddisplay", display );
+                               if ( display !== "none" && !jQuery._data( this[i], "olddisplay" ) ) {
+                                       jQuery._data( this[i], "olddisplay", display );
                                }
                        }
 
index 675e5ff..7fa5488 100644 (file)
@@ -8,7 +8,8 @@ var rnamespaces = /\.(.*)$/,
        fcleanup = function( nm ) {
                return nm.replace(rescape, "\\$&");
        },
-       focusCounts = { focusin: 0, focusout: 0 };
+       focusCounts = { focusin: 0, focusout: 0 },
+       eventKey = "events";
 
 /*
  * A number of helper functions used for managing events.
@@ -50,7 +51,7 @@ jQuery.event = {
                }
 
                // Init the element's event structure
-               var elemData = jQuery.data( elem );
+               var elemData = jQuery._data( elem );
 
                // If no elemData is found then we must be trying to bind to one of the
                // banned noData elements
@@ -58,10 +59,7 @@ jQuery.event = {
                        return;
                }
 
-               // Use a key less likely to result in collisions for plain JS objects.
-               // Fixes bug #7150.
-               var eventKey = elem.nodeType ? "events" : "__events__",
-                       events = elemData[ eventKey ],
+               var events = elemData[ eventKey ],
                        eventHandle = elemData.handle;
 
                if ( typeof events === "function" ) {
@@ -177,8 +175,7 @@ jQuery.event = {
                }
 
                var ret, type, fn, j, i = 0, all, namespaces, namespace, special, eventType, handleObj, origType,
-                       eventKey = elem.nodeType ? "events" : "__events__",
-                       elemData = jQuery.data( elem ),
+                       elemData = jQuery.hasData( elem ) && jQuery._data( elem ),
                        events = elemData && elemData[ eventKey ];
 
                if ( !elemData || !events ) {
@@ -290,10 +287,10 @@ jQuery.event = {
                        delete elemData.handle;
 
                        if ( typeof elemData === "function" ) {
-                               jQuery.removeData( elem, eventKey );
+                               jQuery.removeData( elem, eventKey, true );
 
                        } else if ( jQuery.isEmptyObject( elemData ) ) {
-                               jQuery.removeData( elem );
+                               jQuery.removeData( elem, undefined, true );
                        }
                }
        },
@@ -325,9 +322,16 @@ jQuery.event = {
 
                                // Only trigger if we've ever bound an event for it
                                if ( jQuery.event.global[ type ] ) {
+                                       // XXX This code smells terrible. event.js should not be directly
+                                       // inspecting the data cache
                                        jQuery.each( jQuery.cache, function() {
-                                               if ( this.events && this.events[type] ) {
-                                                       jQuery.event.trigger( event, data, this.handle.elem );
+                                               // internalKey variable is just used to make it easier to find
+                                               // and potentially change this stuff later; currently it just
+                                               // points to jQuery.expando
+                                               var internalKey = jQuery.expando,
+                                                       internalCache = this[ internalKey ];
+                                               if ( internalCache && internalCache.events && internalCache.events[type] ) {
+                                                       jQuery.event.trigger( event, data, internalCache.handle.elem );
                                                }
                                        });
                                }
@@ -353,8 +357,8 @@ jQuery.event = {
 
                // Trigger the event, it is assumed that "handle" is a function
                var handle = elem.nodeType ?
-                       jQuery.data( elem, "handle" ) :
-                       (jQuery.data( elem, "__events__" ) || {}).handle;
+                       jQuery._data( elem, "handle" ) :
+                       (jQuery._data( elem, eventKey ) || {}).handle;
 
                if ( handle ) {
                        handle.apply( elem, data );
@@ -432,7 +436,7 @@ jQuery.event = {
 
                event.namespace = event.namespace || namespace_sort.join(".");
 
-               events = jQuery.data(this, this.nodeType ? "events" : "__events__");
+               events = jQuery._data(this, eventKey);
 
                if ( typeof events === "function" ) {
                        events = events.events;
@@ -787,12 +791,12 @@ if ( !jQuery.support.changeBubbles ) {
                        return;
                }
 
-               data = jQuery.data( elem, "_change_data" );
+               data = jQuery._data( elem, "_change_data" );
                val = getVal(elem);
 
                // the current data will be also retrieved by beforeactivate
                if ( e.type !== "focusout" || elem.type !== "radio" ) {
-                       jQuery.data( elem, "_change_data", val );
+                       jQuery._data( elem, "_change_data", val );
                }
 
                if ( data === undefined || val === data ) {
@@ -837,7 +841,7 @@ if ( !jQuery.support.changeBubbles ) {
                        // information
                        beforeactivate: function( e ) {
                                var elem = e.target;
-                               jQuery.data( elem, "_change_data", getVal(elem) );
+                               jQuery._data( elem, "_change_data", getVal(elem) );
                        }
                },
 
@@ -986,8 +990,8 @@ jQuery.fn.extend({
 
                return this.click( jQuery.proxy( fn, function( event ) {
                        // Figure out which function to execute
-                       var lastToggle = ( jQuery.data( this, "lastToggle" + fn.guid ) || 0 ) % i;
-                       jQuery.data( this, "lastToggle" + fn.guid, lastToggle + 1 );
+                       var lastToggle = ( jQuery._data( this, "lastToggle" + fn.guid ) || 0 ) % i;
+                       jQuery._data( this, "lastToggle" + fn.guid, lastToggle + 1 );
 
                        // Make sure that clicks stop
                        event.preventDefault();
@@ -1075,7 +1079,7 @@ function liveHandler( event ) {
        var stop, maxLevel, related, match, handleObj, elem, j, i, l, data, close, namespace, ret,
                elems = [],
                selectors = [],
-               events = jQuery.data( this, this.nodeType ? "events" : "__events__" );
+               events = jQuery._data( this, eventKey );
 
        if ( typeof events === "function" ) {
                events = events.events;
index 96caa02..657aef7 100644 (file)
@@ -381,17 +381,24 @@ function cloneCopyEvent(orig, ret) {
                        throw "Cloned data mismatch";
                }
 
-               var oldData = jQuery.data( orig[nodeIndex] ),
-                       curData = jQuery.data( this, oldData ),
-                       events = oldData && oldData.events;
-
-               if ( events ) {
-                       delete curData.handle;
-                       curData.events = {};
-
-                       for ( var type in events ) {
-                               for ( var i = 0, l = events[ type ].length; i < l; i++ ) {
-                                       jQuery.event.add( this, type, events[ type ][ i ], events[ type ][ i ].data );
+               var internalKey = jQuery.expando,
+                       oldData = jQuery.data( orig[nodeIndex] ),
+                       curData = jQuery.data( this, oldData );
+
+               // Switch to use the internal data object, if it exists, for the next
+               // stage of data copying
+               if ( (oldData = oldData[ internalKey ]) ) {
+                       var events = oldData.events;
+                       curData = curData[ internalKey ] = jQuery.extend({}, oldData);
+
+                       if ( events ) {
+                               delete curData.handle;
+                               curData.events = {};
+
+                               for ( var type in events ) {
+                                       for ( var i = 0, l = events[ type ].length; i < l; i++ ) {
+                                               jQuery.event.add( this, type, events[ type ][ i ], events[ type ][ i ].data );
+                                       }
                                }
                        }
                }
@@ -594,8 +601,7 @@ jQuery.extend({
        },
 
        cleanData: function( elems ) {
-               var data, id, cache = jQuery.cache,
-                       special = jQuery.event.special,
+               var data, id, cache = jQuery.cache, internalKey = jQuery.expando, special = jQuery.event.special,
                        deleteExpando = jQuery.support.deleteExpando;
 
                for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) {
@@ -606,13 +612,14 @@ jQuery.extend({
                        id = elem[ jQuery.expando ];
 
                        if ( id ) {
-                               data = cache[ id ];
+                               data = cache[ id ] && cache[ id ][ internalKey ];
 
                                if ( data && data.events ) {
                                        for ( var type in data.events ) {
                                                if ( special[ type ] ) {
                                                        jQuery.event.remove( elem, type );
 
+                                               // This is a shortcut to avoid jQuery.event.remove's overhead
                                                } else {
                                                        jQuery.removeEvent( elem, type, data.handle );
                                                }
index 735b0e1..5fb04df 100644 (file)
@@ -7,7 +7,7 @@ jQuery.extend({
                }
 
                type = (type || "fx") + "queue";
-               var q = jQuery.data( elem, type );
+               var q = jQuery._data( elem, type );
 
                // Speed up dequeue by getting out quickly if this is just a lookup
                if ( !data ) {
@@ -15,7 +15,7 @@ jQuery.extend({
                }
 
                if ( !q || jQuery.isArray(data) ) {
-                       q = jQuery.data( elem, type, jQuery.makeArray(data) );
+                       q = jQuery._data( elem, type, jQuery.makeArray(data) );
 
                } else {
                        q.push( data );
index a1ab581..04f1684 100644 (file)
@@ -703,12 +703,12 @@ var testToggleClass = function(valueObj) {
 
        // toggleClass storage
        e.toggleClass(true);
-       ok( e.get(0).className === "", "Assert class is empty (data was empty)" );
+       ok( e[0].className === "", "Assert class is empty (data was empty)" );
        e.addClass("testD testE");
        ok( e.is(".testD.testE"), "Assert class present" );
        e.toggleClass();
        ok( !e.is(".testD.testE"), "Assert class not present" );
-       ok( e.data('__className__') === 'testD testE', "Assert data was stored" );
+       ok( jQuery._data(e[0], '__className__') === 'testD testE', "Assert data was stored" );
        e.toggleClass();
        ok( e.is(".testD.testE"), "Assert class present (restored from data)" );
        e.toggleClass(false);
@@ -720,11 +720,9 @@ var testToggleClass = function(valueObj) {
        e.toggleClass();
        ok( e.is(".testD.testE"), "Assert class present (restored from data)" );
 
-
-
        // Cleanup
        e.removeClass("testD");
-       e.removeData('__className__');
+       jQuery.removeData(e[0], '__className__', true);
 };
 
 test("toggleClass(String|boolean|undefined[, boolean])", function() {
@@ -785,7 +783,7 @@ test("toggleClass(Fucntion[, boolean]) with incoming value", function() {
 
        // Cleanup
        e.removeClass("test");
-       e.removeData('__className__');
+       jQuery.removeData(e[0], '__className__', true);
 });
 
 test("addClass, removeClass, hasClass", function() {
index 310cd6b..28e19a3 100644 (file)
-module("data");
+module("data", { teardown: moduleTeardown });
 
 test("expando", function(){
-       expect(6);
+       expect(1);
 
        equals("expando" in jQuery, true, "jQuery is exposing the expando");
+});
 
-       var obj = {};
-       equals( jQuery.data(obj), obj, "jQuery.data(obj) returns the object");
-       equals( jQuery.expando in obj, false, "jQuery.data(obj) did not add an expando to the object" );
+function dataTests (elem) {
+       // expect(32)
 
-       obj = {};
-       jQuery.data(obj, 'test');
-       equals( jQuery.expando in obj, false, "jQuery.data(obj,key) did not add an expando to the object" );
+       function getCacheLength() {
+               var cacheLength = 0;
+               for (var i in jQuery.cache) {
+                       ++cacheLength;
+               }
 
-       obj = {};
-       jQuery.data(obj, "foo", "bar");
-       equals( jQuery.expando in obj, false, "jQuery.data(obj,key,value) did not add an expando to the object" );
-       equals( obj.foo, "bar", "jQuery.data(obj,key,value) sets fields directly on the object." );
-});
+               return cacheLength;
+       }
 
-test("jQuery.acceptData", function() {
-       expect(7);
+       equals( jQuery.data(elem, "foo"), undefined, "No data exists initially" );
+       strictEqual( jQuery.hasData(elem), false, "jQuery.hasData agrees no data exists initially" );
 
-       ok( jQuery.acceptData( document ), "document" );
-       ok( jQuery.acceptData( document.documentElement ), "documentElement" );
-       ok( jQuery.acceptData( {} ), "object" );
-       ok( !jQuery.acceptData( document.createElement("embed") ), "embed" );
-       ok( !jQuery.acceptData( document.createElement("applet") ), "applet" );
+       var dataObj = jQuery.data(elem);
+       equals( typeof dataObj, "object", "Calling data with no args gives us a data object reference" );
+       strictEqual( jQuery.data(elem), dataObj, "Calling jQuery.data returns the same data object when called multiple times" );
 
-       var flash = document.createElement("object");
-       flash.setAttribute("classid", "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000");
-       ok( jQuery.acceptData( flash ), "flash" );
+       strictEqual( jQuery.hasData(elem), false, "jQuery.hasData agrees no data exists even when an empty data obj exists" );
 
-       var applet = document.createElement("object");
-       applet.setAttribute("classid", "clsid:8AD9C840-044E-11D1-B3E9-00805F499D93");
-       ok( !jQuery.acceptData( applet ), "applet" );
-});
+       dataObj.foo = "bar";
+       equals( jQuery.data(elem, "foo"), "bar", "Data is readable by jQuery.data when set directly on a returned data object" );
 
-test("jQuery.data", function() {
-       expect(15);
-       var div = document.createElement("div");
+       strictEqual( jQuery.hasData(elem), true, "jQuery.hasData agrees data exists when data exists" );
 
-       ok( jQuery.data(div, "test") === undefined, "Check for no data exists" );
+       jQuery.data(elem, "foo", "baz");
+       equals( jQuery.data(elem, "foo"), "baz", "Data can be changed by jQuery.data" );
+       equals( dataObj.foo, "baz", "Changes made through jQuery.data propagate to referenced data object" );
 
-       jQuery.data(div, "test", "success");
-       equals( jQuery.data(div, "test"), "success", "Check for added data" );
+       jQuery.data(elem, "foo", undefined);
+       equals( jQuery.data(elem, "foo"), "baz", "Data is not unset by passing undefined to jQuery.data" );
 
-       ok( jQuery.data(div, "notexist") === undefined, "Check for no data exists" );
+       jQuery.data(elem, "foo", null);
+       strictEqual( jQuery.data(elem, "foo"), null, "Setting null using jQuery.data works OK" );
 
-       var data = jQuery.data(div);
-       same( data, { "test": "success" }, "Return complete data set" );
+       jQuery.data(elem, "foo", "foo1");
 
-       jQuery.data(div, "test", "overwritten");
-       equals( jQuery.data(div, "test"), "overwritten", "Check for overwritten data" );
+       jQuery.data(elem, { "bar" : "baz", "boom" : "bloz" });
+       strictEqual( jQuery.data(elem, "foo"), "foo1", "Passing an object extends the data object instead of replacing it" );
+       equals( jQuery.data(elem, "boom"), "bloz", "Extending the data object works" );
 
-       jQuery.data(div, "test", undefined);
-       equals( jQuery.data(div, "test"), "overwritten", "Check that data wasn't removed");
+       jQuery._data(elem, "foo", "foo2");
+       equals( jQuery._data(elem, "foo"), "foo2", "Setting internal data works" );
+       equals( jQuery.data(elem, "foo"), "foo1", "Setting internal data does not override user data" );
 
-       jQuery.data(div, "test", null);
-       ok( jQuery.data(div, "test") === null, "Check for null data");
+       var internalDataObj = jQuery.data(elem, jQuery.expando);
+       strictEqual( jQuery._data(elem), internalDataObj, "Internal data object is accessible via jQuery.expando property" );
+       notStrictEqual( dataObj, internalDataObj, "Internal data object is not the same as user data object" );
 
-       jQuery.data(div, "test3", "orig");
-       jQuery.data(div, { "test": "in", "test2": "in2" });
-       equals( jQuery.data(div, "test"), "in", "Verify setting an object in data" );
-       equals( jQuery.data(div, "test2"), "in2", "Verify setting an object in data" );
-       equals( jQuery.data(div, "test3"), "orig", "Verify original not overwritten" );
+       strictEqual( elem.boom, undefined, "Data is never stored directly on the object" );
 
-       var obj = {};
-       jQuery.data( obj, "prop", true );
+       jQuery.removeData(elem, "foo");
+       strictEqual( jQuery.data(elem, "foo"), undefined, "jQuery.removeData removes single properties" );
 
-       ok( obj.prop, "Data is being stored on the object" );
-       equals( jQuery.data( obj, "prop" ), true, "Make sure the right value is retrieved" );
+       jQuery.removeData(elem);
+       strictEqual( jQuery.data(elem, jQuery.expando), internalDataObj, "jQuery.removeData does not remove internal data if it exists" );
 
-       jQuery.data( window, "BAD", true );
-       ok( !window[ jQuery.expando ], "Make sure there is no expando on the window object." );
-       ok( !window.BAD, "And make sure that the property wasn't set directly on the window." );
-       ok( jQuery.data( window, "BAD" ), "Make sure that the value was set." );
-});
+       jQuery.removeData(elem, undefined, true);
 
-test("jQuery.hasData", function() {
-       expect(6);
+       strictEqual( jQuery.data(elem, jQuery.expando), undefined, "jQuery.removeData on internal data works" );
+       strictEqual( jQuery.hasData(elem), false, "jQuery.hasData agrees all data has been removed from object" );
+
+       jQuery._data(elem, "foo", "foo2");
+       strictEqual( jQuery.hasData(elem), true, "jQuery.hasData shows data exists even if it is only internal data" );
+
+       jQuery.data(elem, "foo", "foo1");
+       equals( jQuery._data(elem, "foo"), "foo2", "Setting user data does not override internal data" );
+
+       jQuery.removeData(elem, undefined, true);
+       equals( jQuery.data(elem, "foo"), "foo1", "jQuery.removeData for internal data does not remove user data" );
+
+       if (elem.nodeType) {
+               var oldCacheLength = getCacheLength();
+               jQuery.removeData(elem, "foo");
 
-       function testData(obj) {
-               equals( jQuery.hasData(obj), false, "No data exists" );
-               jQuery.data( obj, "foo", "bar" );
-               equals( jQuery.hasData(obj), true, "Data exists" );
-               jQuery.removeData( obj, "foo" );
-               equals( jQuery.hasData(obj), false, "Data was removed" );
+               equals( getCacheLength(), oldCacheLength - 1, "Removing the last item in the data object destroys it" );
        }
+       else {
+               jQuery.removeData(elem, "foo");
+               var expected, actual;
+
+               if (jQuery.support.deleteExpando) {
+                       expected = false;
+                       actual = jQuery.expando in elem;
+               }
+               else {
+                       expected = null;
+                       actual = elem[ jQuery.expando ];
+               }
+
+               equals( actual, expected, "Removing the last item in the data object destroys it" );
+       }
+
+       jQuery.data(elem, "foo", "foo1");
+       jQuery._data(elem, "foo", "foo2");
+
+       equals( jQuery.data(elem, "foo"), "foo1", "(sanity check) Ensure data is set in user data object" );
+       equals( jQuery._data(elem, "foo"), "foo2", "(sanity check) Ensure data is set in internal data object" );
+
+       jQuery.removeData(elem, "foo", true);
+
+       strictEqual( jQuery.data(elem, jQuery.expando), undefined, "Removing the last item in internal data destroys the internal data object" );
+
+       jQuery._data(elem, "foo", "foo2");
+       equals( jQuery._data(elem, "foo"), "foo2", "(sanity check) Ensure data is set in internal data object" );
 
-       testData(document.createElement('div'));
-       testData({});
+       jQuery.removeData(elem, "foo");
+       equals( jQuery._data(elem, "foo"), "foo2", "(sanity check) jQuery.removeData for user data does not remove internal data" );
+
+       if (elem.nodeType) {
+               oldCacheLength = getCacheLength();
+               jQuery.removeData(elem, "foo", true);
+               equals( getCacheLength(), oldCacheLength - 1, "Removing the last item in the internal data object also destroys the user data object when it is empty" );
+       }
+       else {
+               jQuery.removeData(elem, "foo", true);
+
+               if (jQuery.support.deleteExpando) {
+                       expected = false;
+                       actual = jQuery.expando in elem;
+               }
+               else {
+                       expected = null;
+                       actual = elem[ jQuery.expando ];
+               }
+
+               equals( actual, expected, "Removing the last item in the internal data object also destroys the user data object when it is empty" );
+       }
+}
+
+test("jQuery.data", function() {
+       expect(128);
+
+       var div = document.createElement("div");
+
+       dataTests(div);
+       dataTests({});
+
+       // remove bound handlers from window object to stop potential false positives caused by fix for #5280 in
+       // transports/xhr.js
+       jQuery(window).unbind("unload");
+
+       dataTests(window);
+       dataTests(document);
+
+       // clean up unattached element
+       jQuery(div).remove();
+});
+
+test("jQuery.acceptData", function() {
+       expect(7);
+
+       ok( jQuery.acceptData( document ), "document" );
+       ok( jQuery.acceptData( document.documentElement ), "documentElement" );
+       ok( jQuery.acceptData( {} ), "object" );
+       ok( !jQuery.acceptData( document.createElement("embed") ), "embed" );
+       ok( !jQuery.acceptData( document.createElement("applet") ), "applet" );
+
+       var flash = document.createElement("object");
+       flash.setAttribute("classid", "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000");
+       ok( jQuery.acceptData( flash ), "flash" );
+
+       var applet = document.createElement("object");
+       applet.setAttribute("classid", "clsid:8AD9C840-044E-11D1-B3E9-00805F499D93");
+       ok( !jQuery.acceptData( applet ), "applet" );
 });
 
 test(".data()", function() {
@@ -98,7 +179,6 @@ test(".data()", function() {
 
        var div = jQuery("#foo");
        strictEqual( div.data("foo"), undefined, "Make sure that missing result is undefined" );
-
        div.data("test", "success");
        same( div.data(), {test: "success"}, "data() get the entire data object" );
        strictEqual( div.data("foo"), undefined, "Make sure that missing result is still undefined" );
@@ -107,7 +187,7 @@ test(".data()", function() {
        equals( nodiv.data(), null, "data() on empty set returns null" );
 
        var obj = { foo: "bar" };
-       equals( jQuery(obj).data(), obj, "Retrieve data object from a wrapped JS object (#7524)" );
+       deepEqual( jQuery(obj).data(), {}, "Retrieve data object from a wrapped JS object (#7524)" );
 })
 
 test(".data(String) and .data(String, Object)", function() {
@@ -194,11 +274,14 @@ test(".data(String) and .data(String, Object)", function() {
        equals( $elem.data('null',null).data('null'), null, "null's are preserved");
        equals( $elem.data('emptyString','').data('emptyString'), '', "Empty strings are preserved");
        equals( $elem.data('false',false).data('false'), false, "false's are preserved");
-       equals( $elem.data('exists'), true, "Existing data is returned" );
+       equals( $elem.data('exists'), undefined, "Existing data is not returned" );
 
        // Clean up
        $elem.removeData();
-       ok( jQuery.isEmptyObject( $elem[0] ), "removeData clears the object" );
+       deepEqual( $elem[0], {exists:true}, "removeData does not clear the object" );
+
+       // manually clean up detached elements
+       parent.remove();
 });
 
 test("data-* attributes", function() {
@@ -323,13 +406,17 @@ test(".data(Object)", function() {
 
        var obj = {test:"unset"},
                jqobj = jQuery(obj);
+       jqobj.data("test", "unset");
        jqobj.data({ "test": "in", "test2": "in2" });
-       equals( obj.test, "in", "Verify setting an object on an object extends the object" );
-       equals( obj.test2, "in2", "Verify setting an object on an object extends the object" );
+       equals( jQuery.data(obj).test, "in", "Verify setting an object on an object extends the data object" );
+       equals( obj.test2, undefined, "Verify setting an object on an object does not extend the object" );
+
+       // manually clean up detached elements
+       div.remove();
 });
 
 test("jQuery.removeData", function() {
-       expect(7);
+       expect(6);
        var div = jQuery("#foo")[0];
        jQuery.data(div, "test", "testing");
        jQuery.removeData(div, "test");
@@ -342,10 +429,9 @@ test("jQuery.removeData", function() {
 
        var obj = {};
        jQuery.data(obj, "test", "testing");
-       equals( obj.test, "testing", "verify data on plain object");
+       equals( jQuery(obj).data("test"), "testing", "verify data on plain object");
        jQuery.removeData(obj, "test");
        equals( jQuery.data(obj, "test"), undefined, "Check removal of data on plain object" );
-       equals( obj.test, undefined, "Check removal of data directly from plain object" );
 
        jQuery.data( window, "BAD", true );
        jQuery.removeData( window, "BAD" );
@@ -371,4 +457,4 @@ test(".removeData()", function() {
 
        div.removeData("test.foo");
        equals( div.data("test.foo"), undefined, "Make sure data is intact" );
-});
+});
\ No newline at end of file
index b7b60ab..cf9d131 100644 (file)
@@ -895,7 +895,7 @@ test("hide hidden elements (bug #7141)", function() {
        var div = jQuery("<div style='display:none'></div>").appendTo("#main");
        equals( div.css("display"), "none", "Element is hidden by default" );
        div.hide();
-       ok( !div.data("olddisplay"), "olddisplay is undefined after hiding an already-hidden element" );
+       ok( !jQuery._data(div, "olddisplay"), "olddisplay is undefined after hiding an already-hidden element" );
        div.show();
        equals( div.css("display"), "block", "Show a double-hidden element" );
 
@@ -910,7 +910,7 @@ test("hide hidden elements, with animation (bug #7141)", function() {
        var div = jQuery("<div style='display:none'></div>").appendTo("#main");
        equals( div.css("display"), "none", "Element is hidden by default" );
        div.hide(1, function () {
-               ok( !div.data("olddisplay"), "olddisplay is undefined after hiding an already-hidden element" );
+               ok( !jQuery._data(div, "olddisplay"), "olddisplay is undefined after hiding an already-hidden element" );
                div.show(1, function () {
                        equals( div.css("display"), "block", "Show a double-hidden element" );
                        start();
index b4672a8..cae1a83 100644 (file)
@@ -28,7 +28,7 @@ test("bind(), with data", function() {
        };
        jQuery("#firstp").bind("click", {foo: "bar"}, handler).click().unbind("click", handler);
 
-       ok( !jQuery.data(jQuery("#firstp")[0], "events"), "Event handler unbound when using data." );
+       ok( !jQuery._data(jQuery("#firstp")[0], "events"), "Event handler unbound when using data." );
 });
 
 test("click(), with data", function() {
@@ -39,7 +39,7 @@ test("click(), with data", function() {
        };
        jQuery("#firstp").click({foo: "bar"}, handler).click().unbind("click", handler);
 
-       ok( !jQuery.data(jQuery("#firstp")[0], "events"), "Event handler unbound when using data." );
+       ok( !jQuery._data(jQuery("#firstp")[0], "events"), "Event handler unbound when using data." );
 });
 
 test("bind(), with data, trigger with data", function() {
@@ -505,7 +505,7 @@ test("bind(), with different this object", function() {
                .bind("click", jQuery.proxy(handler1, thisObject)).click().unbind("click", handler1)
                .bind("click", data, jQuery.proxy(handler2, thisObject)).click().unbind("click", handler2);
 
-       ok( !jQuery.data(jQuery("#firstp")[0], "events"), "Event handler unbound when using different this object and data." );
+       ok( !jQuery._data(jQuery("#firstp")[0], "events"), "Event handler unbound when using different this object and data." );
 });
 
 test("bind(name, false), unbind(name, false)", function() {
@@ -547,7 +547,7 @@ test("bind()/trigger()/unbind() on plain object", function() {
                }
        });
 
-       var events = jQuery(obj).data("__events__");
+       var events = jQuery._data(obj, "events");
        ok( events, "Object has events bound." );
        equals( obj.events, undefined, "Events object on plain objects is not events" );
        equals( typeof events, "function", "'events' expando is a function on plain objects." );
@@ -567,7 +567,9 @@ test("bind()/trigger()/unbind() on plain object", function() {
        // Make sure it doesn't complain when no events are found
        jQuery(obj).unbind("test");
 
-       equals( obj.__events__, undefined, "Make sure events object is removed" );
+       equals( obj && obj[ jQuery.expando ] &&
+                   obj[ jQuery.expando ][ jQuery.expando ] &&
+                       obj[ jQuery.expando ][ jQuery.expando ].events, undefined, "Make sure events object is removed" );
 });
 
 test("unbind(type)", function() {
@@ -947,7 +949,7 @@ test("toggle(Function, Function, ...)", function() {
        equals( turn, 2, "Trying toggle with 3 functions, attempt 5 yields 2");
 
        $div.unbind('click',fns[0]);
-       var data = jQuery.data( $div[0], 'events' );
+       var data = jQuery._data( $div[0], 'events' );
        ok( !data, "Unbinding one function from toggle unbinds them all");
 
        // Test Multi-Toggles
@@ -1065,7 +1067,7 @@ test(".live()/.die()", function() {
        equals( clicked, 2, "live with a context" );
 
        // Make sure the event is actually stored on the context
-       ok( jQuery.data(container, "events").live, "live with a context" );
+       ok( jQuery._data(container, "events").live, "live with a context" );
 
        // Test unbinding with a different context
        jQuery("#foo", container).die("click");
@@ -1578,7 +1580,7 @@ test(".delegate()/.undelegate()", function() {
        equals( clicked, 2, "delegate with a context" );
 
        // Make sure the event is actually stored on the context
-       ok( jQuery.data(container, "events").live, "delegate with a context" );
+       ok( jQuery._data(container, "events").live, "delegate with a context" );
 
        // Test unbinding with a different context
        jQuery("#main").undelegate("#foo", "click");
@@ -1907,7 +1909,7 @@ test("window resize", function() {
                ok( true, "Resize event fired." );
        }).resize().unbind("resize");
 
-       ok( !jQuery(window).data("__events__"), "Make sure all the events are gone." );
+       ok( !jQuery._data(window, "__events__"), "Make sure all the events are gone." );
 });
 
 test("focusin bubbles", function() {