Merge branch 'storage'
This commit is contained in:
commit
f6a2cdb067
|
@ -1,3 +1,7 @@
|
|||
* Alter event system to use new element storage API rather than have its own global hashtable. (Andrew Dupont)
|
||||
|
||||
* Add Element#store and Element#retrieve for safe, hash-backed storage of element metadata (no memory leaks). Also add Element#getStorage for working with the element's storage hash directly. Hat tip: Mootools. (ZenCocoon, Andrew Dupont)
|
||||
|
||||
* Fix issue where certain versions of Safari treat class names case-insensitively in Selector/$$ queries. (Andrew Dupont, kangax, Brice)
|
||||
|
||||
* Fix issue where Function#argumentNames returned incorrect results in IE when comments were intermixed with argument names. (Christophe Porteneuve, T.J. Crowder)
|
||||
|
|
|
@ -1205,3 +1205,44 @@ document.viewport = {
|
|||
window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop);
|
||||
}
|
||||
};
|
||||
|
||||
Element.Storage = {
|
||||
UID: 1
|
||||
};
|
||||
|
||||
Element.addMethods({
|
||||
getStorage: function(element) {
|
||||
if (!(element = $(element))) return;
|
||||
|
||||
var uid;
|
||||
if (element === window) {
|
||||
uid = 0;
|
||||
} else {
|
||||
if (Object.isUndefined(element._prototypeUID))
|
||||
element._prototypeUID = [Element.Storage.UID++];
|
||||
uid = element._prototypeUID[0];
|
||||
}
|
||||
|
||||
if (!Element.Storage[uid])
|
||||
Element.Storage[uid] = $H();
|
||||
|
||||
return Element.Storage[uid];
|
||||
},
|
||||
|
||||
store: function(element, key, value) {
|
||||
if (!(element = $(element))) return;
|
||||
element.getStorage().set(key, value);
|
||||
},
|
||||
|
||||
retrieve: function(element, key, defaultValue) {
|
||||
if (!(element = $(element))) return;
|
||||
var hash = Element.getStorage(element), value = hash.get(key);
|
||||
|
||||
if (Object.isUndefined(value)) {
|
||||
hash.set(key, defaultValue);
|
||||
value = defaultValue;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
});
|
||||
|
|
186
src/dom/event.js
186
src/dom/event.js
|
@ -162,67 +162,61 @@
|
|||
Event.extend = Prototype.K;
|
||||
}
|
||||
|
||||
function _getEventID(element) {
|
||||
if (element._prototypeEventID) return element._prototypeEventID[0];
|
||||
return element._prototypeEventID = [++_getEventID.id];
|
||||
}
|
||||
_getEventID.id = 1;
|
||||
|
||||
function _getDOMEventName(eventName) {
|
||||
if (eventName && eventName.include(':')) return 'dataavailable';
|
||||
return eventName;
|
||||
}
|
||||
|
||||
function _getCacheForID(id) {
|
||||
return Event.cache[id] = Event.cache[id] || { };
|
||||
}
|
||||
|
||||
function _getRespondersForEvent(id, eventName) {
|
||||
var c = _getCacheForID(id);
|
||||
return c[eventName] = c[eventName] || [];
|
||||
}
|
||||
|
||||
function _createResponder(element, eventName, handler) {
|
||||
var id = _getEventID(element), r = _getRespondersForEvent(id, eventName);
|
||||
// We don't set a default on the call to Element#retrieve so that we can
|
||||
// handle the element's "virgin" state.
|
||||
var registry = Element.retrieve(element, 'prototype_event_registry');
|
||||
|
||||
if (Object.isUndefined(registry)) {
|
||||
// First time we've handled this element. Put it into the cache.
|
||||
CACHE.push(element);
|
||||
registry = Element.retrieve(element, 'prototype_event_registry', $H());
|
||||
}
|
||||
|
||||
var respondersForEvent = registry.get(eventName);
|
||||
if (Object.isUndefined()) {
|
||||
respondersForEvent = [];
|
||||
registry.set(eventName, respondersForEvent);
|
||||
}
|
||||
|
||||
// Work around the issue that permits a handler to be attached more than
|
||||
// once to the same element & event type.
|
||||
if (r.pluck('handler').include(handler)) return false;
|
||||
|
||||
var responder = function(event) {
|
||||
if (!Event || !Event.extend ||
|
||||
// If it's a custom event, but not the _correct_ custom event, ignore it.
|
||||
(!Object.isUndefined(event.eventName) && event.eventName !== eventName))
|
||||
return false;
|
||||
|
||||
Event.extend(event);
|
||||
handler.call(element, event);
|
||||
};
|
||||
if (respondersForEvent.pluck('handler').include(handler)) return false;
|
||||
|
||||
var responder;
|
||||
if (eventName.include(":")) {
|
||||
// Custom event.
|
||||
responder = function(event) {
|
||||
// If it's not a custom event, ignore it.
|
||||
if (Object.isUndefined(event.eventName))
|
||||
return false;
|
||||
|
||||
// If it's a custom event, but not the _correct_ custom event, ignore it.
|
||||
if (event.eventName !== eventName)
|
||||
return false;
|
||||
|
||||
Event.extend(event);
|
||||
handler.call(element, event);
|
||||
};
|
||||
} else {
|
||||
// Ordinary event.
|
||||
responder = function(event) {
|
||||
Event.extend(event);
|
||||
handler.call(element, event);
|
||||
};
|
||||
}
|
||||
|
||||
responder.handler = handler;
|
||||
r.push(responder);
|
||||
respondersForEvent.push(responder);
|
||||
return responder;
|
||||
}
|
||||
|
||||
function _findResponder(id, eventName, handler) {
|
||||
var r = _getRespondersForEvent(id, eventName);
|
||||
return r.find(function(responder) {
|
||||
return responder.handler === handler;
|
||||
});
|
||||
}
|
||||
|
||||
function _destroyResponder(id, eventName, handler) {
|
||||
var c = _getCacheForID(id);
|
||||
if (Object.isUndefined(c[eventName])) return false;
|
||||
c[eventName] = c[eventName].without(_findResponder(id, eventName, handler));
|
||||
}
|
||||
|
||||
function _destroyCache() {
|
||||
for (var id in Event.cache) {
|
||||
for (var eventName in Event.cache[id])
|
||||
Event.cache[id][eventName] = null;
|
||||
}
|
||||
|
||||
function _destroyCache() {
|
||||
for (var i = 0, length = CACHE.length; i < length; i++)
|
||||
Event.stopObserving(CACHE[i]);
|
||||
}
|
||||
|
||||
var CACHE = [];
|
||||
|
||||
// Internet Explorer needs to remove event handlers on page unload
|
||||
// in order to avoid memory leaks.
|
||||
|
@ -238,54 +232,94 @@
|
|||
|
||||
function observe(element, eventName, handler) {
|
||||
element = $(element);
|
||||
var name = _getDOMEventName(eventName),
|
||||
responder = _createResponder(element, eventName, handler);
|
||||
|
||||
|
||||
var responder = _createResponder(element, eventName, handler);
|
||||
|
||||
if (!responder) return element;
|
||||
|
||||
if (element.addEventListener)
|
||||
element.addEventListener(name, responder, false);
|
||||
else
|
||||
element.attachEvent("on" + name, responder);
|
||||
if (eventName.include(':')) {
|
||||
// Custom event.
|
||||
if (element.addEventListener)
|
||||
element.addEventListener("dataavailable", responder, false);
|
||||
else {
|
||||
// We observe two IE-proprietarty events: one for custom events that
|
||||
// bubble and one for custom events that do not bubble.
|
||||
element.attachEvent("ondataavailable", responder);
|
||||
element.attachEvent("onfilterchange", responder);
|
||||
}
|
||||
} else {
|
||||
// Ordinary event.
|
||||
if (element.addEventListener)
|
||||
element.addEventListener(eventName, responder, false);
|
||||
else
|
||||
element.attachEvent("on" + eventName, responder);
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
function stopObserving(element, eventName, handler) {
|
||||
element = $(element);
|
||||
var id = _getEventID(element), name = _getDOMEventName(eventName);
|
||||
|
||||
var registry = Element.retrieve(element, 'prototype_event_registry');
|
||||
|
||||
if (Object.isUndefined(registry)) return element;
|
||||
|
||||
if (eventName && !handler) {
|
||||
// If an event name is passed without a handler, we stop observing all
|
||||
// handlers of that type.
|
||||
_getRespondersForEvent(id, eventName).each(function(r) {
|
||||
element.stopObserving(eventName, r.handler);
|
||||
var responders = registry.get(eventName);
|
||||
|
||||
if (Object.isUndefined(responders)) return element;
|
||||
|
||||
responders.each( function(r) {
|
||||
Element.stopObserving(element, eventName, r.handler);
|
||||
});
|
||||
return element;
|
||||
} else if (!eventName) {
|
||||
// If both the event name and the handler are omitted, we stop observing
|
||||
// _all_ handlers on the element.
|
||||
Object.keys(_getCacheForID(id)).each(function(eventName) {
|
||||
element.stopObserving(eventName);
|
||||
registry.each( function(pair) {
|
||||
var eventName = pair.key, responders = pair.value;
|
||||
|
||||
responders.each( function(r) {
|
||||
Element.stopObserving(element, eventName, r.handler);
|
||||
});
|
||||
});
|
||||
return element;
|
||||
}
|
||||
|
||||
var responder = _findResponder(id, eventName, handler);
|
||||
|
||||
var responders = registry.get(eventName);
|
||||
var responder = responders.find( function(r) { return r.handler === handler; });
|
||||
if (!responder) return element;
|
||||
|
||||
if (element.removeEventListener)
|
||||
element.removeEventListener(name, responder, false);
|
||||
else
|
||||
element.detachEvent('on' + name, responder);
|
||||
|
||||
_destroyResponder(id, eventName, handler);
|
||||
|
||||
if (eventName.include(':')) {
|
||||
// Custom event.
|
||||
if (element.removeEventListener)
|
||||
element.removeEventListener("dataavailable", responder, false);
|
||||
else {
|
||||
element.detachEvent("ondataavailable", responder);
|
||||
element.detachEvent("onfilterchange", responder);
|
||||
}
|
||||
} else {
|
||||
// Ordinary event.
|
||||
if (element.removeEventListener)
|
||||
element.removeEventListener(eventName, responder, false);
|
||||
else
|
||||
element.detachEvent('on' + eventName, responder);
|
||||
}
|
||||
|
||||
registry.set(eventName, responders.without(responder));
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
function fire(element, eventName, memo) {
|
||||
function fire(element, eventName, memo, bubble) {
|
||||
element = $(element);
|
||||
|
||||
if (Object.isUndefined(bubble))
|
||||
bubble = true;
|
||||
|
||||
if (element == document && document.createEvent && !element.dispatchEvent)
|
||||
element = document.documentElement;
|
||||
|
||||
|
@ -295,7 +329,7 @@
|
|||
event.initEvent('dataavailable', true, true);
|
||||
} else {
|
||||
event = document.createEventObject();
|
||||
event.eventType = 'ondataavailable';
|
||||
event.eventType = bubble ? 'ondataavailable' : 'onfilterchange';
|
||||
}
|
||||
|
||||
event.eventName = eventName;
|
||||
|
@ -332,7 +366,7 @@
|
|||
});
|
||||
|
||||
// Export to the global scope.
|
||||
if (window.Event) Object.extend(window.Event, Event)
|
||||
if (window.Event) Object.extend(window.Event, Event);
|
||||
else window.Event = Event;
|
||||
})();
|
||||
|
||||
|
|
|
@ -1365,6 +1365,23 @@ new Test.Unit.Runner({
|
|||
constants.each(function(pair) {
|
||||
this.assertEqual(Node[pair.key], pair.value);
|
||||
}, this);
|
||||
},
|
||||
|
||||
testElementStorage: function() {
|
||||
var element = $('test-empty');
|
||||
element.store('foo', 'bar');
|
||||
this.assertEqual("bar", element.retrieve("foo"), "Setting and reading a property");
|
||||
element.store('foo', 'thud');
|
||||
this.assertEqual("thud", element.retrieve("foo"), "Re-setting and reading property");
|
||||
|
||||
element.store('bar', 'narf');
|
||||
this.assertEnumEqual($w('foo bar'), element.getStorage().keys(), "Getting the storage hash");
|
||||
element.getStorage().unset('bar');
|
||||
this.assertEnumEqual($w('foo'), element.getStorage().keys(), "Getting the storage hash after unsetting a key");
|
||||
|
||||
|
||||
var clonedElement = $('test-empty').cloneNode(false);
|
||||
this.assert(!('_prototypeUID' in clonedElement), "Cloning a node should not confuse the storage engine");
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -141,16 +141,20 @@ new Test.Unit.Runner({
|
|||
var span = $("span"), observer = function() { }, eventID;
|
||||
|
||||
span.observe("test:somethingHappened", observer);
|
||||
eventID = span._prototypeEventID;
|
||||
|
||||
this.assert(Event.cache[eventID]);
|
||||
this.assert(Object.isArray(Event.cache[eventID]["test:somethingHappened"]));
|
||||
this.assertEqual(1, Event.cache[eventID]["test:somethingHappened"].length);
|
||||
var registry = span.getStorage().get('prototype_event_registry');
|
||||
|
||||
this.assert(registry);
|
||||
this.assert(Object.isArray(registry.get('test:somethingHappened')));
|
||||
this.assertEqual(1, registry.get('test:somethingHappened').length);
|
||||
|
||||
span.stopObserving("test:somethingHappened", observer);
|
||||
this.assert(Event.cache[eventID]);
|
||||
this.assert(Object.isArray(Event.cache[eventID]["test:somethingHappened"]));
|
||||
this.assertEqual(0, Event.cache[eventID]["test:somethingHappened"].length);
|
||||
|
||||
registry = span.getStorage().get('prototype_event_registry');
|
||||
|
||||
this.assert(registry);
|
||||
this.assert(Object.isArray(registry.get('test:somethingHappened')));
|
||||
this.assertEqual(0, registry.get('test:somethingHappened').length);
|
||||
},
|
||||
|
||||
testObserveAndStopObservingAreChainable: function() {
|
||||
|
|
Loading…
Reference in New Issue