aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorRick Waldron <waldron.rick@gmail.com>2013-02-03 15:27:55 -0500
committerRick Waldron <waldron.rick@gmail.com>2013-02-03 15:27:55 -0500
commit7f94a5cc3a167a710576c008da2c186a98ce2dd4 (patch)
treed4a3e8ec0e4385d305676b74a663de3017c48438 /src
parented0e2d1e8a5005a80d0dd127dc4019f696471ad6 (diff)
downloadjquery-7f94a5cc3a167a710576c008da2c186a98ce2dd4.tar.gz
jquery-7f94a5cc3a167a710576c008da2c186a98ce2dd4.zip
2.0: Rewrite data.js (Incl. event, manipulation, tests)
Diffstat (limited to 'src')
-rw-r--r--src/data.js441
-rw-r--r--src/event.js4
-rw-r--r--src/manipulation.js58
3 files changed, 247 insertions, 256 deletions
diff --git a/src/data.js b/src/data.js
index 596341c6a..448decd00 100644
--- a/src/data.js
+++ b/src/data.js
@@ -1,237 +1,201 @@
-var rbrace = /(?:\{[\s\S]*\}|\[[\s\S]*\])$/,
+var data_user, data_priv,
+ rbrace = /(?:\{[\s\S]*\}|\[[\s\S]*\])$/,
rmultiDash = /([A-Z])/g;
-function internalData( elem, name, data, pvt ) {
- if ( !jQuery.acceptData( elem ) ) {
- return;
- }
-
- 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;
- }
+function Data() {
+ // Nodes|Objects
+ this.owners = [];
+ // Data objects
+ this.cache = [];
+}
- 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;
- }
- }
+Data.index = function( array, node ) {
+ return array.indexOf( node );
+};
- 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;
+Data.prototype = {
+ add: function( owner ) {
+ this.owners.push( owner );
+ return (this.cache[ this.owners.length - 1 ] = {});
+ },
+ set: function( owner, data, value ) {
+ var prop,
+ index = Data.index( this.owners, owner );
+
+ // If there is no entry for this "owner", create one inline
+ // and set the index as though an owner entry had always existed
+ if ( index === -1 ) {
+ this.add( owner );
+ index = this.owners.length - 1;
}
- }
+ // Handle: [ owner, key, value ] args
+ if ( typeof data === "string" ) {
+ this.cache[ index ][ data ] = value;
- // 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 );
+ // Handle: [ owner, { properties } ] args
} else {
- cache[ id ].data = jQuery.extend( cache[ id ].data, name );
- }
- }
-
- thisCache = cache[ id ];
+ // In the case where there was actually no "owner" entry and
+ // this.add( owner ) was called to create one, there will be
+ // a corresponding empty plain object in the cache.
+ if ( jQuery.isEmptyObject( this.cache[ index ] ) ) {
+ this.cache[ index ] = data;
- // 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 = {};
+ // Otherwise, copy the properties one-by-one to the cache object
+ } else {
+ for ( prop in data ) {
+ this.cache[ index ][ prop ] = data[ prop ];
+ }
+ }
}
+ return this;
+ },
+ get: function( owner, key ) {
+ var cache,
+ index = Data.index( this.owners, 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 );
+ // This logic was required by expectations made of the
+ // old data system.
+ 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 {
- 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 ) {
+ // If only an owner was specified, return the entire
+ // cache object.
+ if ( key === undefined ) {
+ return this.get( owner );
+ }
- // Try to find the camelCased property
- ret = thisCache[ jQuery.camelCase( name ) ];
+ // 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;
}
- } else {
- ret = thisCache;
- }
-
- return ret;
-}
-
-function internalRemoveData( elem, name, pvt ) {
- if ( !jQuery.acceptData( elem ) ) {
- return;
- }
-
- 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 {
+ // Otherwise, this is a read request.
+ return this.get( owner, key );
+ },
+ remove: function( owner, key ) {
+ var i, l, name,
+ camel = jQuery.camelCase,
+ index = Data.index( this.owners, owner ),
+ cache = this.cache[ index ];
- // split the camel cased version by spaces unless a key with the spaces exists
- name = jQuery.camelCase( name );
- if ( name in thisCache ) {
- name = [ name ];
+ 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 {
- name = name.split(" ");
+ // Split the camel cased version by spaces unless a key with the spaces exists
+ name = camel( key );
+ name = name in cache ?
+ [ name ] : name.split(" ");
}
+ } else {
+ // If "name" is an array of keys...
+ // When data is initially created, via ("key", "val") signature,
+ // keys will be converted to camelCase.
+ // Since there is no way to tell _how_ a key was added, remove
+ // both plain key and camelCase key. #12786
+ // This will only penalize the array argument path.
+ name = key.concat( key.map( camel ) );
}
- } else {
- // If "name" is an array of keys...
- // When data is initially created, via ("key", "val") signature,
- // keys will be converted to camelCase.
- // Since there is no way to tell _how_ a key was added, remove
- // both plain key and camelCase key. #12786
- // This will only penalize the array argument path.
- name = name.concat( jQuery.map( name, jQuery.camelCase ) );
- }
-
- for ( i = 0, l = name.length; i < l; i++ ) {
- delete thisCache[ name[i] ];
- }
+ i = 0;
+ l = name.length;
- // If there is no data left in the cache, we want to continue
- // and let the cache object itself get destroyed
- if ( !( pvt ? isEmptyDataObject : jQuery.isEmptyObject )( thisCache ) ) {
- return;
+ for ( ; i < l; i++ ) {
+ delete cache[ name[i] ];
+ }
}
}
- }
+ this.cache[ index ] = cache;
+ },
+ hasData: function( owner ) {
+ var index = Data.index( this.owners, owner );
- // See jQuery.data for more information
- if ( !pvt ) {
- delete cache[ id ].data;
+ if ( index > -1 ) {
+ return !jQuery.isEmptyObject( this.cache[ index ] );
+ }
+ return false;
+ },
+ discard: function( owner ) {
+ var index = Data.index( this.owners, owner );
- // Don't destroy the parent cache unless the internal data object
- // had been the only thing left in it
- if ( !isEmptyDataObject( cache[ id ] ) ) {
- return;
+ if ( index >= 0 ) {
+ this.owners.splice( index, 1 );
+ this.cache.splice( index, 1 );
}
+ return this;
}
+};
+
+// This will be used by remove()/cleanData() in manipulation to sever
+// remaining references to node objects. One day we'll replace the dual
+// arrays with a WeakMap and this won't be an issue.
+// (Splices the data objects out of the internal cache arrays)
+function data_discard( owner ) {
+ data_user.discard( owner );
+ data_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 ];
+// These may be used throughout the jQuery core codebase
+data_user = new Data();
+data_priv = new Data();
- // When all else fails, null
- } else {
- cache[ id ] = null;
- }
-}
jQuery.extend({
- cache: {},
-
+ // This is no longer relevant to jQuery core, but must remain
+ // supported for the sake of jQuery 1.9.x API surface compatibility.
+ acceptData: function() {
+ return true;
+ },
// Unique for each copy of jQuery on the page
// Non-digits removed to match rinlinejQuery
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 ) {
- elem = elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ];
- return !!elem && !isEmptyDataObject( elem );
+ return data_user.hasData( elem ) || data_priv.hasData( elem );
},
data: function( elem, name, data ) {
- return internalData( elem, name, data );
+ return data_user.access( elem, name, data );
},
removeData: function( elem, name ) {
- return internalRemoveData( elem, name );
+ return data_user.remove( elem, name );
},
- // For internal use only.
+ // TODO: Replace all calls to _data and _removeData with direct
+ // calls to
+ //
+ // data_priv.access( elem, name, data );
+ //
+ // data_priv.remove( elem, name );
+ //
_data: function( elem, name, data ) {
- return internalData( elem, name, data, true );
+ return data_priv.access( elem, name, data );
},
_removeData: function( elem, name ) {
- return internalRemoveData( elem, name, true );
- },
-
- // A method for determining if a DOM node can handle the data expando
- acceptData: function( elem ) {
- // Do not set data on non-element because it will not be cleared (#8335).
- if ( elem.nodeType && elem.nodeType !== 1 && elem.nodeType !== 9 ) {
- return false;
- }
-
- 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;
+ return data_priv.remove( elem, name );
}
});
@@ -245,20 +209,19 @@ jQuery.fn.extend({
// Gets all values
if ( key === undefined ) {
if ( this.length ) {
- data = jQuery.data( elem );
+ data = data_user.get( elem );
- if ( elem.nodeType === 1 && !jQuery._data( elem, "parsedAttrs" ) ) {
+ if ( elem.nodeType === 1 && !data_priv.get( elem, "hasDataAttrs" ) ) {
attrs = elem.attributes;
for ( ; i < attrs.length; i++ ) {
name = attrs[i].name;
- if ( !name.indexOf( "data-" ) ) {
- name = jQuery.camelCase( name.slice(5) );
-
+ if ( name.indexOf( "data-" ) === 0 ) {
+ name = jQuery.camelCase( name.substring(5) );
dataAttr( elem, name, data[ name ] );
}
}
- jQuery._data( elem, "parsedAttrs", true );
+ data_priv.set( elem, "hasDataAttrs", true );
}
}
@@ -268,53 +231,94 @@ jQuery.fn.extend({
// Sets multiple values
if ( typeof key === "object" ) {
return this.each(function() {
- jQuery.data( this, key );
+ data_user.set( this, key );
});
}
return jQuery.access( this, function( value ) {
+ var data,
+ camelKey = jQuery.camelCase( key );
+ // Get the Data...
if ( value === undefined ) {
- // Try to fetch any internally stored data first
- return elem ? dataAttr( elem, key, jQuery.data( elem, key ) ) : null;
+
+ // Attempt to get data from the cache
+ // with the key as-is
+ data = 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 = 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() {
- jQuery.data( this, key, value );
+ // First, attempt to store a copy or reference of any
+ // data that might've been store with a camelCased key.
+ var data = 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...*
+ data_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 ) {
+ data_user.set( this, key, value );
+ }
});
}, null, value, arguments.length > 1, null, true );
},
removeData: function( key ) {
return this.each(function() {
- jQuery.removeData( this, key );
+ data_user.remove( this, key );
});
}
});
function dataAttr( elem, key, data ) {
+ var name;
+
// If nothing was found internally, try to fetch any
// data from the HTML5 data-* attribute
if ( data === undefined && elem.nodeType === 1 ) {
- var name = "data-" + key.replace( rmultiDash, "-$1" ).toLowerCase();
-
+ name = "data-" + key.replace( rmultiDash, "-$1" ).toLowerCase();
data = elem.getAttribute( name );
if ( typeof data === "string" ) {
try {
data = data === "true" ? true :
- data === "false" ? false :
- data === "null" ? null :
- // Only convert to a number if it doesn't change the string
- +data + "" === data ? +data :
- rbrace.test( data ) ? jQuery.parseJSON( data ) :
- data;
+ data === "false" ? false :
+ data === "null" ? null :
+ // Only convert to a number if it doesn't change the string
+ +data + "" === data ? +data :
+ rbrace.test( data ) ?
+ JSON.parse( data ) : data;
} catch( e ) {}
// Make sure we set the data so it isn't changed later
- jQuery.data( elem, key, data );
-
+ data_user.set( elem, key, data );
} else {
data = undefined;
}
@@ -322,20 +326,3 @@ function dataAttr( elem, key, 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;
-}
diff --git a/src/event.js b/src/event.js
index 4d7d6a895..4266a1ddb 100644
--- a/src/event.js
+++ b/src/event.js
@@ -24,7 +24,7 @@ jQuery.event = {
var handleObjIn, eventHandle, tmp,
events, t, handleObj,
special, handlers, type, namespaces, origType,
- elemData = jQuery._data( elem );
+ elemData = data_priv.get( elem );
// Don't attach events to noData or text/comment nodes (but allow plain objects)
if ( !elemData ) {
@@ -130,7 +130,7 @@ jQuery.event = {
var j, origCount, tmp,
events, t, handleObj,
special, handlers, type, namespaces, origType,
- elemData = jQuery.hasData( elem ) && jQuery._data( elem );
+ elemData = data_priv.hasData( elem ) && data_priv.get( elem );
if ( !elemData || !(events = elemData.events) ) {
return;
diff --git a/src/manipulation.js b/src/manipulation.js
index 708d7851d..d50ebddf7 100644
--- a/src/manipulation.js
+++ b/src/manipulation.js
@@ -328,7 +328,7 @@ jQuery.fn.extend({
for ( i = 0; i < hasScripts; i++ ) {
node = scripts[ i ];
if ( rscriptType.test( node.type || "" ) &&
- !jQuery._data( node, "globalEval" ) && jQuery.contains( doc, node ) ) {
+ !data_priv.access( node, "globalEval" ) && jQuery.contains( doc, node ) ) {
if ( node.src ) {
// Hope ajax is available...
@@ -511,11 +511,9 @@ jQuery.extend({
},
cleanData: function( elems, /* internal */ acceptData ) {
- var id, data, elem, type,
+ var data, elem, type,
l = elems.length,
i = 0,
- internalKey = jQuery.expando,
- cache = jQuery.cache,
special = jQuery.event.special;
for ( ; i < l; i++ ) {
@@ -523,8 +521,7 @@ jQuery.extend({
if ( acceptData || jQuery.acceptData( elem ) ) {
- id = elem[ internalKey ];
- data = id && cache[ id ];
+ data = data_priv.access( elem );
if ( data ) {
for ( type in data.events ) {
@@ -536,14 +533,10 @@ jQuery.extend({
jQuery.removeEvent( elem, type, data.handle );
}
}
-
- // Remove cache only if it was not already removed by jQuery.event.remove
- if ( cache[ id ] ) {
- delete cache[ id ];
- delete elem[ internalKey ];
- }
}
}
+ // Discard any remaining `private` and `user` data
+ data_discard( elem );
}
}
});
@@ -576,38 +569,49 @@ function setGlobalEval( elems, refElements ) {
i = 0;
for ( ; i < l; i++ ) {
- jQuery._data( elems[ i ], "globalEval", !refElements || jQuery._data( refElements[ i ], "globalEval" ) );
+ data_priv.set(
+ elems[ i ], "globalEval", !refElements || data_priv.get( refElements[ i ], "globalEval" )
+ );
}
}
function cloneCopyEvent( src, dest ) {
+ var i, l, type, pdataOld, pdataCur, udataOld, udataCur, events;
- if ( dest.nodeType !== 1 || !jQuery.hasData( src ) ) {
+ if ( dest.nodeType !== 1 ) {
return;
}
- var i, l, type,
- oldData = jQuery._data( src ),
- curData = jQuery._data( dest, oldData ),
- events = oldData.events;
+ // 1. Copy private data: events, handlers, etc.
+ if ( data_priv.hasData( src ) ) {
+ pdataOld = data_priv.access( src );
+ pdataCur = jQuery.extend( {}, pdataOld );
+ events = pdataOld.events;
+
+ data_priv.set( dest, pdataCur );
- if ( events ) {
- delete curData.handle;
- curData.events = {};
+ if ( events ) {
+ delete pdataCur.handle;
+ pdataCur.events = {};
- for ( type in events ) {
- for ( i = 0, l = events[ type ].length; i < l; i++ ) {
- jQuery.event.add( dest, type, events[ type ][ i ] );
+ for ( type in events ) {
+ for ( i = 0, l = events[ type ].length; i < l; i++ ) {
+ jQuery.event.add( dest, type, events[ type ][ i ] );
+ }
}
}
}
- // make the cloned public data object a copy from the original
- if ( curData.data ) {
- curData.data = jQuery.extend( {}, curData.data );
+ // 2. Copy user data
+ if ( data_user.hasData( src ) ) {
+ udataOld = data_user.access( src );
+ udataCur = jQuery.extend( {}, udataOld );
+
+ data_user.set( dest, udataCur );
}
}
+
function getAll( context, tag ) {
var ret = context.getElementsByTagName ? context.getElementsByTagName( tag || "*" ) :
context.querySelectorAll ? context.querySelectorAll( tag || "*" ) :