rajax.js
February 24th, 2008 • Uncategorized
// RAjax Namespace
var RAjax = {};
// Basic Logger facility that supports logging to
// an HTMLElement, the Firebug console and alerts.
// If provided with a valid DOM id, it will append
// log messages to this element. If it can't find
// the DOM id you provided, it will try to use
// the Firebug console instead (you can make sure
// that this will be found by installing Firebug Lite
// from http://www.getfirebug.com/lite.html website).
// Finally, if it can't find a console Object to log to,
// and you tell it so by passing true as the
// second parameter to initialize, it will fall back
// to those nasty alerts, that can lead to very unfortunate
// situations dependending on which situations (events)
// you use them!
RAjax.Logger = Class.create({
initialize: function(stream, performAlerts) {
this.stream = (s = $(stream)) ? s : console; // maybe null
this.performAlerts = performAlerts || false;
},
log: function(message) {
if(PERFORM_LOGGING) {
if(this.stream) {
// Firebug console or HTMLElement present
if(this.logsToFirebug()) {
// Logging to Firebug console
this.stream.log(message);
} else {
// Logging to HTMLElement
this.stream.insert({bottom: this.formattedMessage(message)});
}
} else { // no stream present
// fallback to annoying alerts
alert(this.formattedMessage(message));
}
}
},
clear: function() {
if(this.logsToPage()) {
this.stream.update('');
}
},
// override this if you need different formatting
formattedMessage: function(message) {
return "
// Basic RAjax.Request class
RAjax.Request = Class.create({
initialize: function(url, options) {
// URL and Ajax.Request options
this.url = url;
this.options = options;
// remember desired http method
this.options._method = this.options.method;
// workaround prototype Ajax.Request JSON handling
// such that it is possible to perform all HTTP operations
// with JSON data in both directions
if(this.needsJsonParameterAdjustment()) {
this.adjustJsonParameters();
this.adjustJsonUrlParameters();
}
// remember callbacks to be able to call the originals
this.onSuccess = this.options.onSuccess;
this.onFailure = this.options.onFailure;
// mix extended callback dispatcher into Ajax.Request options
Object.extend(this.options, this.callbackDispatcher());
},
fire: function() {
new Ajax.Request(this.url, this.options);
},
// ------------------------------------------------------------------
// ------------------------------------------------------------------
// ------------------------------------------------------------------
// 'real' request method
method: function() {
return this.options.method || 'get';
},
// piggybacked rails '_method'
_method: function() {
return this.options._method || null;
},
isJsonRequest: function() {
// defaults to true if not explicitly specified different
return this.options.json ? this.options.json : true;
},
needsAuthenticityToken: function() {
// if something is about to be changed on the server
return this.method() != 'get';
},
needsSpecialMethodParameter: function() {
// if it's neither GET nor POST (rails '_method' parameter)
return this._method() == 'put' || this._method() == 'delete'
},
needsJsonParameterAdjustment: function() {
// if it's a json request and something will be changed on the server
return this.isJsonRequest() && this.needsAuthenticityToken();
},
needsExtendedCallbacks: function() {
// if this.options include any of the extended callbacks
var keys = Object.keys(this.options);
return RAjax.EXTENDED_CALLBACKS.any(function(c) {
return keys.include(c);
});
},
// by convention, hidden input containing authenticity_token
// must have id="auth-token" (this applies only to Rails 2.0)
authenticityToken: function() {
if(t = $('auth-token')) return "authenticity_token=" + t.getValue();
else return "";
},
adjustJsonUrlParameters: function() {
var needsAuthentication = false;
// add authenticity token
if(this.needsAuthenticityToken()) {
this.url += ("?" + this.authenticityToken());
needsAuthentication = true;
}
// add special method parameter
if(this.needsSpecialMethodParameter()) {
if(needsAuthentication) { // authenticity_token already there
this.url += ("&_method=" + this._method());
} else {
this.url += ("?_method=" + this._method());
}
}
},
adjustJsonParameters: function() {
this.options.method = 'post';
this.options.contentType = "application/json";
this.options.evalScripts = false;
if(this.options.parameters) {
this.options.postBody = Object.toJSON(this.options.parameters);
}
},
callbackDispatcher: function(successCallback, failureCallback) {
var self = this;
return {
onSuccess: function(transport) {
var json = transport.responseText.evalJSON(true);
// original callback
if(self.onSuccess) self.onSuccess(transport, json);
if(self.needsExtendedCallbacks()) {
// detailed
self.onSuccessDispatcher(transport, json);
}
},
onFailure: function(transport) {
var json = transport.responseText.evalJSON(true);
// original callback
if(self.onFailure) self.onFailure(transport, json);
if(self.needsExtendedCallbacks()) {
// detailed callbacks
self.onFailureDispatcher(transport, json);
}
}
};
},
onSuccessDispatcher: function(transport, json) {
if(this._method() == "post" && this.options.onPOSTSuccess)
this.options.onPOSTSuccess(transport, json);
if(this._method() == "get" && this.options.onGETSuccess)
this.options.onGETSuccess(transport, json);
if(this._method() == "put" && this.options.onPUTSuccess)
this.options.onPUTSuccess(transport, json);
if(this._method() == "delete" && this.options.onDELETESuccess)
this.options.onDELETESuccess(transport, json);
},
onFailureDispatcher: function(transport, json) {
if(this._method() == "post" && this.options.onPOSTFailure)
this.options.onPOSTFailure(transport, json);
if(this._method() == "get" && this.options.onGETFailure)
this.options.onGETFailure(transport, json);
if(this._method() == "put" && this.options.onPUTFailure)
this.options.onPUTFailure(transport, json);
if(this._method() == "delete" && this.options.onDELETEFailure)
this.options.onDELETEFailure(transport, json);
}
});
RAjax.AUTH_TOKEN_ID = "auth-token";
RAjax.POST = "POST";
RAjax.GET = "GET";
RAjax.PUT = "PUT";
RAjax.DELETE = "DELETE";
RAjax.Options = { evalScripts: false };
RAjax.POSTOptions = Object.extend({ method: 'post' }, RAjax.Options);
RAjax.GETOptions = Object.extend({ method: 'get' }, RAjax.Options);
RAjax.PUTOptions = Object.extend({ method: 'put' }, RAjax.Options);
RAjax.DELETEOptions = Object.extend({ method: 'delete' }, RAjax.Options);
RAjax.Request.POST = function(url, options) {
new RAjax.Request(url, Object.extend(options, RAjax.POSTOptions)).fire();
}
RAjax.Request.GET = function(url, options) {
new RAjax.Request(url, Object.extend(options, RAjax.GETOptions)).fire();
}
RAjax.Request.PUT = function(url, options) {
new RAjax.Request(url, Object.extend(options, RAjax.PUTOptions)).fire();
}
RAjax.Request.DELETE = function(url, options) {
new RAjax.Request(url, Object.extend(options, RAjax.DELETEOptions)).fire();
}
RAjax.DISPATCH_TABLE = {
POST: RAjax.Request.POST,
GET: RAjax.Request.GET,
PUT: RAjax.Request.PUT,
DELETE: RAjax.Request.DELETE
};
RAjax.ResourceProxy = Class.create({
initialize: function(element, resource) {
this.element = element;
this.resource = resource || this.resourceName();
this.resourceId = this.resourceId();
this.newRecord = this.isNewRecord();
this.authToken = (t = $(this.authTokenId())) ? t.getValue() : null;
},
// dispatch to appropriate operation
fire: function(action, parameters, callbacks) {
logger.debug("RAjax.ResourceProxy#fire " + action);
var params = Object.extend(
{ parameters: parameters || {} },
this.prepareCallbacks(action, callbacks)
);
RAjax.DISPATCH_TABLE[action](this.url(action), params);
},
// bundle action relevant callbacks
prepareCallbacks: function(action, callbacks) {
var self = this;
// handle original callbacks (setting 'this' to proper self)
var cs = {
onSuccess: function(transport, json) {
self.onSuccess.call(self, transport, json);
},
onFailure: function(transport, json) {
self.onFailure.call(self, transport, json);
}
};
cs["on" + action + "Success"] = this.mergedCallback(callbacks.onSuccess);
cs["on" + action + "Failure"] = this.mergedCallback(callbacks.onFailure);
return cs;
},
// callbackObject can either be a Function or any Object
definesMultipleCallbacks: function(callback) {
// TODO investigate why (callback instanceof Object) always returns true
// maybe this has something to do with passing params by reference?
return typeof(callback) != "function";
},
// if callbackObject is an Object, merge all callbacks into one function
// if callbackObject is a Function, return it
mergedCallback: function(callbackObject) {
var self = this; // workaround bug with 'this' in inner functions
if(this.definesMultipleCallbacks(callbackObject)) {
return function(transport, json) {
for(callback in callbackObject) {
// provide self for correct access to 'this' in callback actions
callbackObject[callback].call(self, transport, json);
}
};
} else {
// provide self for correct access to 'this' in callback actions
return function(transport, json) {
callbackObject.call(self, transport, json);
}
}
},
// -------------------------------------------------------------
// -------------------- HELPER METHODS -------------------------
// -------------------------------------------------------------
setElement: function(element) {
this.element = element;
this.resource = this.resourceName();
// TODO better pluralization
this.resourceCollection = this.resource + "s";
},
// override this to deal with different naming conventions
resourceName: function() {
return this.element.name.split('[')[0];
},
// override this to deal with different naming conventions
resourceId: function() {
return this.element.name.split('[')[1].gsub(']', '');
},
// override this to deal with different naming conventions
isNewRecord: function() {
return !!this.resourceId().match(/new/);
},
// change value of RAjax.AUTH_TOKEN_ID or
// override this to return the DOM id of your authenticity_token
authTokenId: function() {
return RAjax.AUTH_TOKEN_ID;
},
// override this to return the value of your input field
value: function() { return null; },
// override this to determine if this resource should be deleted
deleted: function() { return false; },
resourceUrl: function() {
return "/"+ this.resourceCollection +"/"+ this.resourceId();
},
resourceCollectionUrl: function() {
return "/" + this.resourceCollection;
},
url: function(action) {
switch(action) {
case RAjax.POST: return this.resourceCollectionUrl();
case RAjax.GET: return this.resourceUrl();
case RAjax.PUT: return this.resourceUrl();
case RAjax.DELETE: return this.resourceUrl();
}
}
});
RAjax.AutoSave = Behavior.create({
initialize: function(proxy) {
this.proxy = new proxy(this.element);
},
onchange: function() {
// dispatch to appropriate event
this.proxy[this.event()]();
},
event: function() {
if(this.proxy.isNewRecord()) {
return RAjax.POST
} else {
return this.proxy.deleted() ? RAjax.DELETE : RAjax.PUT
}
}
});