Applying JavaScript-only events to the abstraction object of XMLHttpRequest
/**
* Custom Event equivalent
*/
function CustomEvent() { }
CustomEvent.prototype = {
type : 'custom'
}
// Custom EventTarget equivalent
function EventDispatcher() { }
EventDispatcher.prototype = {
// An object literal to store arrays of listeners by type
events : {},
// If it supports the type, add the listener (capture ignored)
addEventListener : function(type, listener, capture) {
if (this.events[type]) {
this.events[type].push(listener);
}
},
// If it supports the type, remove the listener (capture ignored)
removeEventListener : function(type, listener, capture) {
if (this.events[type] == undefined) {
return;
}
var index = this.events[type].indexOf(listener);
if (this.events[type][index]) {
this.events[type].splice(index, 1);
}
},
// Cycle through all of the event listeners, passing the event to the callbacks
dispatchEvent : function(type, event) {
if (this.events[type]) {
for (var i in this.events[type]) {
if (typeof this.events[type][i] == 'function') {
this.events[type][i](event);
// Accepts an array of the contextual object and the function to call
} else if (typeof this.events[type][i] == 'object') {
this.events[type][i][1].call(this.events[type][i][0], event);
}
}
}
}
}
// A CustomEvent to pass AjaxRequests when loaded
function AjaxEvent(request) {
this.request = request;
}
AjaxEvent.prototype = new CustomEvent;
AjaxEvent.prototype.type = 'ajax';
AjaxEvent.prototype.request = null;
// Instantiated by the AjaxRequestManager, not directly
function AjaxRequest(id) {
this.id = id;
// If the browser follows the standard
if (window.XMLHttpRequest) {
this.xhr = new XMLHttpRequest();
// ...otherwise, if Internet Explorer < 7
} else if (window.ActiveXObject) {
this.xhr = new ActiveXObject('Microsoft.XMLHTTP');
}
// Callback for this.xhr.onreadystatechanged
this.xhr.onreadystatechange = new Function("AjaxRequest.prototype.stateChanged.apply(requests["+id+"], arguments);");
}
AjaxRequest.prototype = new EventDispatcher;
// Event dispatching
AjaxRequest.prototype.events = {
abort:[],
load:[],
open:[],
send:[]
};
// Used to emulate this meaning this
AjaxRequest.prototype.id = null;
AjaxRequest.prototype.xhr = null;
AjaxRequest.prototype.aborted = false;
// Store variable/value pairs for the GET request
AjaxRequest.prototype.get = {};
// Store variable/value pairs for the POST request
AjaxRequest.prototype.post = {};
// Decide whether or not to send this.post
AjaxRequest.prototype.method = 'POST';
// Callback for this.xhr.onreadystatechanged
AjaxRequest.prototype.stateChanged = function() {
// Only continue if finished returning
if (this.xhr.readyState == 4) {
// Only continue if status OK
if (this.xhr.status == 200) {
var e = new AjaxEvent(this);
this.dispatchEvent('load', e);
}
}
}
// Simple alias to abort the call
AjaxRequest.prototype.abort = function() {
this.aborted = true;
var event = new AjaxEvent(this);
event.returned = this.xhr.abort();
this.dispatchEvent('abort', event);
return event.returned;
}
// Alias to this.xhr.open, which stores the method in
// order to decide whether to bother concatinating
// this.post into url-encoded string form. Note: this
// only takes the baseurl as its url, since it encodes
// and concatinates this.get into the GET parameters.
AjaxRequest.prototype.open = function(method, url) {
this.method = method.toUpperCase();
var real_get = this.urlEncodeObject(this.get);
url += '?' + real_get;
var event = new AjaxEvent(this);
event.returned = this.xhr.open(
this.method,
url,
arguments[2], // async
arguments[3], // user
arguments[4] // pass
);
this.dispatchEvent('open', event);
return event.returned;
}
// Simple alias to this.xhr.send, adjusting this.post
// depending on the request method specified.
AjaxRequest.prototype.send = function() {
if (this.aborted) {
return false;
}
var real_post = '';
var event = new AjaxEvent(this);
if (this.method == 'POST') {
this.xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
real_post = this.urlEncodeObject(this.post);
event.returned = this.xhr.send(real_post);
} else {
event.returned = this.xhr.send();
}
this.dispatchEvent('send', event);
return event.returned;
}
// Non-recursive serialization from object to
// url-encoded values
AjaxRequest.prototype.urlEncodeObject = function(obj) {
var first = true;
var string = '';
for (i in obj) {
var temp_obj = obj[i];
// No need to toString() a string literal.
// In fact, that would corrupt the value.
if (typeof temp_obj != 'string') {
temp_obj = temp_obj.toString();
}
temp_key = encodeURIComponent(i);
temp_obj = encodeURIComponent(temp_obj);
if (first) {
first = false;
string += temp_key + '=' + temp_obj;
} else {
string += '&' + temp_key + '=' + temp_obj;
}
}
return string;
}
// Manage pool of AjaxRequest instances
function AjaxRequestManager() { }
AjaxRequestManager.prototype = {
// Array of AjaxRequest instances
requests : [],
// Event listeners to auto-add to new requests
events : AjaxRequest.prototype.events,
// Factory-type function to instanciate AjaxRequests
createAjaxRequest : function() {
var new_id = ++requests.length;
try {
requests[new_id] = new AjaxRequest(new_id);
requests[new_id].events = this.events;
return requests[new_id];
} catch (e) {
alert(e);
// Clean up junk reference if necessary
if (requests[new_id]) {
requests.pop();
}
return false;
}
},
// Garbage collection
eliminateAjaxRequest : function(id) {
if (!requests[id]) {
return false;
}
// Call abort in case of current activity
requests[id].abort();
// First, delete the reference
requests.splice(id, 1);
// Then, adjust the references of the remaining
// objects to match their new indices
while (id < requests.length) {
requests[id++].id--;
}
return true;
},
// Provide a method to cancel all active and pending requests
abortAll : function() {
for (var i = 0; i < window.requests.length; i++) {
if (window.requests[i]) {
window.requests[i].abort();
}
}
},
// Auto-add listeners to AjaxRequest events
addEventListener : function(type, listener, capture) {
EventDispatcher.prototype.addEventListener.call(this, type, listener);
},
// If it supports the type, remove the listener (capture ignored)
removeEventListener : function(type, listener, capture) {
EventDispatcher.prototype.removeEventListener.call(this, type, listener);
}
}