jquery/src/data.js

349 lines
9.5 KiB
JavaScript
Raw Normal View History

2013-02-11 17:39:44 +00:00
/*
Implementation Summary
1. Enforce API surface and semantic compatibility with 1.9.x branch
2. Improve the module's maintainability by reducing the storage
paths to a single mechanism.
3. Use the same single mechanism to support "private" and "user" data.
4. _Never_ expose "private" data to user code (TODO: Drop _data, _removeData)
5. Avoid exposing implementation details on user objects (eg. expando properties)
6. Provide a clear path for implementation upgrade to WeakMap in 2014
*/
var data_user, data_priv,
rbrace = /(?:\{[\s\S]*\}|\[[\s\S]*\])$/,
2011-09-07 14:13:22 +00:00
rmultiDash = /([A-Z])/g;
function Data() {
// Support: Android < 4,
// Old WebKit does not have Object.preventExtensions/freeze method, return new empty object instead
Object.defineProperty( this.cache = {}, 0, {
get: function() {
return {};
}
});
this.expando = jQuery.expando + Math.random();
}
2013-02-11 17:39:44 +00:00
Data.uid = 1;
Data.prototype = {
key: function( owner ) {
// We can accept data for non-element nodes in modern browsers, but we should not, see #8335.
// Always return key for "freezed" object for such cases
if ( !this.accept( owner ) ) {
return 0;
}
var descriptor = {},
// Check if the owner object already has a cache key
unlock = owner[ this.expando ];
// If not, create one
if ( !unlock ) {
unlock = Data.uid++;
// Secure it in a non-enumerable, non-writable property
try {
descriptor[ this.expando ] = { value: unlock };
Object.defineProperties( owner, descriptor );
// Support: Android < 4
// Fallback to a less secure definition
} catch ( e ) {
descriptor[ this.expando ] = unlock;
jQuery.extend( owner, descriptor );
}
2013-02-11 17:39:44 +00:00
}
// Ensure the cache object
2013-02-11 17:39:44 +00:00
if ( !this.cache[ unlock ] ) {
this.cache[ unlock ] = {};
}
return unlock;
},
set: function( owner, data, value ) {
2013-02-27 04:41:57 +00:00
var prop,
// There may be an unlock assigned to this node,
// if there is no entry for this "owner", create one inline
// and set the unlock as though an owner entry had always existed
unlock = this.key( owner ),
cache = this.cache[ unlock ];
2013-02-11 17:39:44 +00:00
// Handle: [ owner, key, value ] args
if ( typeof data === "string" ) {
2013-02-11 17:39:44 +00:00
cache[ data ] = value;
// Handle: [ owner, { properties } ] args
} else {
2013-02-27 04:41:57 +00:00
// Support an expectation from the old data system where plain
// objects used to initialize would be set to the cache by
// reference, instead of having properties and values copied.
// Note, this will kill the connection between
2013-02-11 17:39:44 +00:00
// "this.cache[ unlock ]" and "cache"
if ( jQuery.isEmptyObject( cache ) ) {
2013-02-27 04:41:57 +00:00
this.cache[ unlock ] = data;
// Otherwise, copy the properties one-by-one to the cache object
} else {
for ( prop in data ) {
2013-02-11 17:39:44 +00:00
cache[ prop ] = data[ prop ];
}
}
}
},
get: function( owner, key ) {
2013-02-11 17:39:44 +00:00
// Either a valid cache is found, or will be created.
// New caches will be created and the unlock returned,
// allowing direct access to the newly created
// empty data object. A valid owner object must be provided.
var cache = this.cache[ this.key( owner ) ];
return key === undefined ?
cache : cache[ key ];
},
access: function( owner, key, value ) {
// In cases where either:
//
// 1. No key was specified
// 2. A string key was specified, but no value provided
//
// Take the "read" path and allow the get method to determine
// which value to return, respectively either:
//
// 1. The entire cache object
// 2. The data stored at the key
//
if ( key === undefined ||
((key && typeof key === "string") && value === undefined) ) {
return this.get( owner, key );
}
// [*]When the key is not a string, or both a key and value
// are specified, set or extend (existing objects) with either:
//
// 1. An object of properties
// 2. A key and value
//
this.set( owner, key, value );
// Since the "set" path can have two possible entry points
// return the expected data based on which path was taken[*]
return value !== undefined ? value : key;
},
remove: function( owner, key ) {
2013-02-27 04:41:57 +00:00
var i, name,
unlock = this.key( owner ),
cache = this.cache[ unlock ];
if ( key === undefined ) {
2013-02-27 04:41:57 +00:00
this.cache[ unlock ] = {};
} else {
2013-02-27 04:41:57 +00:00
// Support array or space separated string of keys
if ( jQuery.isArray( key ) ) {
// 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( jQuery.camelCase ) );
} else {
// Try the string as a key before any manipulation
if ( key in cache ) {
name = [ key ];
} else {
2013-02-27 04:41:57 +00:00
// If a key with the spaces exists, use it.
// Otherwise, create an array by matching non-whitespace
name = jQuery.camelCase( key );
name = name in cache ?
[ name ] : ( name.match( core_rnotwhite ) || [] );
}
2013-02-27 04:41:57 +00:00
}
2013-02-27 04:41:57 +00:00
i = name.length;
while ( i-- ) {
delete cache[ name[ i ] ];
}
}
},
accept: function( owner ) {
// Do not set data on non-element because it will not be cleared (#8335).
return owner.nodeType ? owner.nodeType === 1 || owner.nodeType === 9 : true;
},
hasData: function( owner ) {
2013-02-11 17:39:44 +00:00
return !jQuery.isEmptyObject(
this.cache[ owner[ this.expando ] ] || {}
2013-02-11 17:39:44 +00:00
);
},
discard: function( owner ) {
delete this.cache[ this.key( owner ) ];
}
};
// These may be used throughout the jQuery core codebase
data_user = new Data();
data_priv = new Data();
jQuery.extend({
acceptData: data_user.accept,
hasData: function( elem ) {
return data_user.hasData( elem ) || data_priv.hasData( elem );
},
data: function( elem, name, data ) {
return data_user.access( elem, name, data );
},
removeData: function( elem, name ) {
data_user.remove( elem, name );
},
// 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 data_priv.access( elem, name, data );
},
_removeData: function( elem, name ) {
data_priv.remove( elem, name );
2009-11-25 17:09:53 +00:00
}
});
jQuery.fn.extend({
data: function( key, value ) {
var attrs, name,
elem = this[ 0 ],
i = 0,
data = null;
// Gets all values
if ( key === undefined ) {
if ( this.length ) {
data = data_user.get( elem );
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-" ) === 0 ) {
name = jQuery.camelCase( name.substring(5) );
dataAttr( elem, name, data[ name ] );
}
}
data_priv.set( elem, "hasDataAttrs", true );
}
}
return data;
}
// Sets multiple values
if ( typeof key === "object" ) {
return this.each(function() {
data_user.set( this, key );
});
2009-11-25 17:09:53 +00:00
}
return jQuery.access( this, function( value ) {
var data,
camelKey = jQuery.camelCase( key );
// The calling jQuery object (element matches) is not empty
// (and therefore has an element appears at this[ 0 ]) and the
// `value` parameter was not undefined. An empty jQuery object
// will result in `undefined` for elem = this[ 0 ] which will
// throw an exception if an attempt to read a data cache is made.
if ( elem && value === undefined ) {
// 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 get data from the cache
// with the key camelized
data = data_user.get( elem, camelKey );
if ( data !== undefined ) {
return data;
}
// Attempt to "discover" the data in
// HTML5 custom data-* attrs
data = dataAttr( elem, camelKey, undefined );
if ( data !== undefined ) {
return data;
}
// We tried really hard, but the data doesn't exist.
return;
}
// Set the data...
this.each(function() {
// 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 ( key.indexOf("-") !== -1 && data !== undefined ) {
data_user.set( this, key, value );
}
2009-11-25 17:09:53 +00:00
});
}, null, value, arguments.length > 1, null, true );
2009-11-25 17:09:53 +00:00
},
removeData: function( key ) {
return this.each(function() {
data_user.remove( this, key );
2009-11-25 17:09:53 +00:00
});
}
});
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 ) {
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 ) ?
JSON.parse( data ) : data;
} catch( e ) {}
// Make sure we set the data so it isn't changed later
data_user.set( elem, key, data );
} else {
data = undefined;
}
}
return data;
}