From 3a4e1233aa2acabee0d0267d54c2d1112fbdcad4 Mon Sep 17 00:00:00 2001 From: John Resig Date: Sat, 8 Sep 2007 23:31:23 +0000 Subject: [PATCH] Landing the new expando management code. Completely overhauls how data is associated with elements. Plugins will be most interested in: - jQuery.data(elem) -> Unique ID for the element - jQuery.data(elem, name) -> Named data store for the element - jQuery.data(elem, name, value) -> Saves a value to the named data store - jQuery.removeData(elem) -> Remove the expando and the complete data store - jQuery.removeData(elem, name) -> Removes just this one named data store jQuery's .remove() and .empty() automatically clean up after themselves. Once an element leaves a DOM document their events are no longer intact. Thus, statements like so: {{{ $("#foo").remove().appendTo("#bar"); }}} should be written like so: {{{ $("#foo").appendTo("#bar"); }}} in order to avoid losing the bound events. --- src/core.js | 75 ++++++++++++++++++++++++++++++++++++++++++----- src/event.js | 56 +++++++++++++++++------------------ src/selector.js | 16 +++++----- test/data/testrunner.js | 2 +- test/unit/event.js | 8 ++--- 5 files changed, 109 insertions(+), 48 deletions(-) diff --git a/src/core.js b/src/core.js index c612644..ac45d62 100644 --- a/src/core.js +++ b/src/core.js @@ -238,7 +238,7 @@ jQuery.fn = jQuery.prototype = { var clone = ret.find("*").andSelf(); this.find("*").andSelf().each(function(i) { - var events = this.$events; + var events = jQuery.data(this, "events"); for ( var type in events ) for ( var handler in events[type] ) jQuery.event.add(clone[i], type, events[type][handler], events[type][handler].data); @@ -411,6 +411,8 @@ jQuery.extend = jQuery.fn.extend = function() { return target; }; +var expando = "jQuery" + (new Date()).getTime(), uuid = 0; + jQuery.extend({ noConflict: function(deep) { window.$ = _$; @@ -450,6 +452,58 @@ jQuery.extend({ nodeName: function( elem, name ) { return elem.nodeName && elem.nodeName.toUpperCase() == name.toUpperCase(); }, + + cache: {}, + + data: function( elem, name, data ) { + var id = elem[ expando ]; + + // Compute a unique ID for the element + if ( !id ) + id = elem[ expando ] = ++uuid; + + // Only generate the data cache if we're + // trying to access or manipulate it + if ( name && !jQuery.cache[ id ] ) + jQuery.cache[ id ] = {}; + + // Prevent overriding the named cache with undefined values + if ( data != undefined ) + jQuery.cache[ id ][ name ] = data; + + // Return the named cache data, or the ID for the element + return name ? jQuery.cache[ id ][ name ] : id; + }, + + removeData: function( elem, name ) { + var id = elem[ expando ]; + + // If we want to remove a specific section of the element's data + if ( name ) { + // Remove the section of cache data + delete jQuery.cache[ id ][ name ]; + + // If we've removed all the data, remove the element's cache + name = ""; + for ( name in jQuery.cache[ id ] ) break; + if ( !name ) + jQuery.removeData( elem ); + + // Otherwise, we want to remove all of the element's data + } else { + // Clean up the element expando + try { + delete elem[ expando ]; + } catch(e){ + // IE has trouble directly removing the expando + // but it's ok with using removeAttribute + elem.removeAttribute( expando ); + } + + // Completely remove the data cache + delete jQuery.cache[ id ]; + } + }, // args is for internal usage only each: function( obj, fn, args ) { @@ -836,14 +890,16 @@ jQuery.extend({ }, unique: function(first) { - var r = [], num = jQuery.mergeNum++; + var r = [], done = {}; try { - for ( var i = 0, fl = first.length; i < fl; i++ ) - if ( num != first[i].mergeNum ) { - first[i].mergeNum = num; + for ( var i = 0, fl = first.length; i < fl; i++ ) { + var id = jQuery.data(first[i]); + if ( !done[id] ) { + done[id] = true; r.push(first[i]); } + } } catch(e) { r = first; } @@ -851,8 +907,6 @@ jQuery.extend({ return r; }, - mergeNum: 0, - grep: function(elems, fn, inv) { // If a string is passed in for the function, make a function // for it (a handy shortcut) @@ -977,10 +1031,15 @@ jQuery.each( { jQuery.className[ jQuery.className.has(this,c) ? "remove" : "add" ](this, c); }, remove: function(a){ - if ( !a || jQuery.filter( a, [this] ).r.length ) + if ( !a || jQuery.filter( a, [this] ).r.length ) { + jQuery.removeData( this ); this.parentNode.removeChild( this ); + } }, empty: function() { + // Clean up the cache + jQuery("*", this).each(function(){ jQuery.removeData(this); }); + while ( this.firstChild ) this.removeChild( this.firstChild ); } diff --git a/src/event.js b/src/event.js index 205b510..3fba158 100644 --- a/src/event.js +++ b/src/event.js @@ -41,36 +41,34 @@ jQuery.event = { handler.type = parts[1]; // Init the element's event structure - if (!element.$events) - element.$events = {}; + var events = jQuery.data(element, "events") || jQuery.data(element, "events", {}); - if (!element.$handle) - element.$handle = function() { - // returned undefined or false - var val; - - // Handle the second event of a trigger and when - // an event is called after a page has unloaded - if ( typeof jQuery == "undefined" || jQuery.event.triggered ) - return val; - - val = jQuery.event.handle.apply(element, arguments); - + var handle = jQuery.data(element, "handle", function(){ + // returned undefined or false + var val; + + // Handle the second event of a trigger and when + // an event is called after a page has unloaded + if ( typeof jQuery == "undefined" || jQuery.event.triggered ) return val; - }; + + val = jQuery.event.handle.apply(element, arguments); + + return val; + }); // Get the current list of functions bound to this event - var handlers = element.$events[type]; + var handlers = events[type]; // Init the event handler queue if (!handlers) { - handlers = element.$events[type] = {}; + handlers = events[type] = {}; // And bind the global event handler to the element if (element.addEventListener) - element.addEventListener(type, element.$handle, false); + element.addEventListener(type, handle, false); else - element.attachEvent("on" + type, element.$handle); + element.attachEvent("on" + type, handle); } // Add the function to the element's handler list @@ -85,7 +83,7 @@ jQuery.event = { // Detach an event or set of events from an element remove: function(element, type, handler) { - var events = element.$events, ret, index; + var events = jQuery.data(element, "events"), ret, index; // Namespaced event handlers if ( typeof type == "string" ) { @@ -111,7 +109,7 @@ jQuery.event = { // remove all handlers for the given type else - for ( handler in element.$events[type] ) + for ( handler in events[type] ) // Handle the removal of namespaced events if ( !parts[1] || events[type][handler].type == parts[1] ) delete events[type][handler]; @@ -120,9 +118,9 @@ jQuery.event = { for ( ret in events[type] ) break; if ( !ret ) { if (element.removeEventListener) - element.removeEventListener(type, element.$handle, false); + element.removeEventListener(type, jQuery.data(element, "handle"), false); else - element.detachEvent("on" + type, element.$handle); + element.detachEvent("on" + type, jQuery.data(element, "handle")); ret = null; delete events[type]; } @@ -130,8 +128,10 @@ jQuery.event = { // Remove the expando if it's no longer used for ( ret in events ) break; - if ( !ret ) - element.$handle = element.$events = null; + if ( !ret ) { + jQuery.removeData( element, "events" ); + jQuery.removeData( element, "handle" ); + } } }, @@ -156,8 +156,8 @@ jQuery.event = { data.unshift( this.fix({ type: type, target: element }) ); // Trigger the event - if ( jQuery.isFunction( element.$handle ) ) - val = element.$handle.apply( element, data ); + if ( jQuery.isFunction( jQuery.data(element, "handle") ) ) + val = jQuery.data(element, "handle").apply( element, data ); // Handle triggering native .onfoo handlers if ( !fn && element["on"+type] && element["on"+type].apply( element, data ) === false ) @@ -194,7 +194,7 @@ jQuery.event = { var parts = event.type.split("."); event.type = parts[0]; - var c = this.$events && this.$events[event.type], args = Array.prototype.slice.call( arguments, 1 ); + var c = jQuery.data(this, "events") && jQuery.data(this, "events")[event.type], args = Array.prototype.slice.call( arguments, 1 ); args.unshift( event ); for ( var j in c ) { diff --git a/src/selector.js b/src/selector.js index 33017aa..0dadeb4 100644 --- a/src/selector.js +++ b/src/selector.js @@ -140,17 +140,19 @@ jQuery.extend({ if ( (m = re.exec(t)) != null ) { r = []; - var nodeName = m[2], mergeNum = jQuery.mergeNum++; + var nodeName = m[2], merge = {}; m = m[1]; for ( var j = 0, rl = ret.length; j < rl; j++ ) { var n = m == "~" || m == "+" ? ret[j].nextSibling : ret[j].firstChild; for ( ; n; n = n.nextSibling ) if ( n.nodeType == 1 ) { - if ( m == "~" && n.mergeNum == mergeNum ) break; + var id = jQuery.data(n); + + if ( m == "~" && merge[id] ) break; if (!nodeName || n.nodeName.toUpperCase() == nodeName.toUpperCase() ) { - if ( m == "~" ) n.mergeNum = mergeNum; + if ( m == "~" ) merge[id] = true; r.push( n ); } @@ -346,23 +348,23 @@ jQuery.extend({ // We can get a speed boost by handling nth-child here } else if ( m[1] == ":" && m[2] == "nth-child" ) { - var num = jQuery.mergeNum++, tmp = [], + var merge = {}, tmp = [], test = /(\d*)n\+?(\d*)/.exec( m[3] == "even" && "2n" || m[3] == "odd" && "2n+1" || !/\D/.test(m[3]) && "n+" + m[3] || m[3]), first = (test[1] || 1) - 0, last = test[2] - 0; for ( var i = 0, rl = r.length; i < rl; i++ ) { - var node = r[i], parentNode = node.parentNode; + var node = r[i], parentNode = node.parentNode, id = jQuery.data(parentNode); - if ( num != parentNode.mergeNum ) { + if ( !merge[id] ) { var c = 1; for ( var n = parentNode.firstChild; n; n = n.nextSibling ) if ( n.nodeType == 1 ) n.nodeIndex = c++; - parentNode.mergeNum = num; + merge[id] = true; } var add = false; diff --git a/test/data/testrunner.js b/test/data/testrunner.js index 69377e4..7b7779a 100644 --- a/test/data/testrunner.js +++ b/test/data/testrunner.js @@ -161,7 +161,7 @@ function expect(asserts) { * Resets the test setup. Useful for tests that modify the DOM. */ function reset() { - document.getElementById('main').innerHTML = _config.fixture; + $("#main").html( _config.fixture ); } /** diff --git a/test/unit/event.js b/test/unit/event.js index 334f286..cdf86c6 100644 --- a/test/unit/event.js +++ b/test/unit/event.js @@ -8,8 +8,8 @@ test("bind()", function() { ok( event.data.foo == "bar", "bind() with data, Check value of passed data" ); }; $("#firstp").bind("click", {foo: "bar"}, handler).click().unbind("click", handler); - - ok( !$("#firstp").get(0).$events, "Event handler unbound when using data." ); + + ok( !jQuery.data($("#firstp")[0], "events"), "Event handler unbound when using data." ); reset(); var handler = function(event, data) { @@ -108,11 +108,11 @@ test("unbind(event)", function() { el.click(function() { return; }); el.unbind('change',function(){ return; }); - for (var ret in el[0].$events['click']) break; + for (var ret in jQuery.data(el[0], "events")['click']) break; ok( ret, "Extra handlers weren't accidentally removed." ); el.unbind('click'); - ok( !el[0].$events, "Removed the events expando after all handlers are unbound." ); + ok( !jQuery.data(el[0], "events"), "Removed the events expando after all handlers are unbound." ); }); test("trigger(event, [data], [fn])", function() { -- 1.7.10.4