Sequenzia/lib/assets/javascripts/moe-legacy/url-hash.js
2013-10-26 18:06:58 -05:00

310 lines
8.6 KiB
JavaScript
Executable File

UrlHashHandler = function()
{
this.observers = new Hash();
this.normalize = function(h) { }
this.denormalize = function(h) { }
this.deferred_sets = [];
this.deferred_replace = false;
this.current_hash = this.parse(this.get_raw_hash());
this.normalize(this.current_hash);
/* The last value received by the hashchange event: */
this.last_hashchange = this.current_hash.clone();
this.hashchange_event = this.hashchange_event.bindAsEventListener(this);
Element.observe(window, "hashchange", this.hashchange_event);
}
UrlHashHandler.prototype.fire_observers = function(old_hash, new_hash)
{
var all_keys = old_hash.keys();
all_keys = all_keys.concat(new_hash.keys());
all_keys = all_keys.uniq();
var changed_hash_keys = [];
all_keys.each(function(key) {
var old_value = old_hash.get(key);
var new_value = new_hash.get(key);
if(old_value != new_value)
changed_hash_keys.push(key);
}.bind(this));
var observers_to_call = [];
changed_hash_keys.each(function(key) {
var observers = this.observers.get(key);
if(observers == null)
return;
observers_to_call = observers_to_call.concat(observers);
}.bind(this));
var universal_observers = this.observers.get(null);
if(universal_observers != null)
observers_to_call = observers_to_call.concat(universal_observers);
observers_to_call.each(function(observer) {
observer(changed_hash_keys, old_hash, new_hash);
});
}
/*
* Set handlers to normalize and denormalize the URL hash.
*
* Denormalizing a URL hash can convert the URL hash to something clearer for URLs. Normalizing
* it reverses any denormalization, giving names to parameters.
*
* For example, if a normalized URL is
*
* http://www.example.com/app#show?id=1
*
* where the hash is {"": "show", id: "1"}, a denormalized URL may be
*
* http://www.example.com/app#show/1
*
* The denormalize callback will only be called with normalized input. The normalize callback
* may receive any combination of normalized or denormalized input.
*/
UrlHashHandler.prototype.set_normalize = function(norm, denorm)
{
this.normalize = norm;
this.denormalize = denorm;
this.normalize(this.current_hash);
this.set_all(this.current_hash.clone());
}
UrlHashHandler.prototype.hashchange_event = function(event)
{
var old_hash = this.last_hashchange.clone();
this.normalize(old_hash);
var raw = this.get_raw_hash();
var new_hash = this.parse(raw);
this.normalize(new_hash);
this.current_hash = new_hash.clone();
this.last_hashchange = new_hash.clone();
this.fire_observers(old_hash, new_hash);
}
/*
* Parse a hash, returning a Hash.
*
* #a/b?c=d&e=f -> {"": 'a/b', c: 'd', e: 'f'}
*/
UrlHashHandler.prototype.parse = function(hash)
{
if(hash == null)
hash = "";
if(hash.substr(0, 1) == "#")
hash = hash.substr(1);
var hash_path = hash.split("?", 1)[0];
var hash_query = hash.substr(hash_path.length+1);
hash_path = window.decodeURIComponent(hash_path);
var query_params = new Hash();
query_params.set("", hash_path);
if(hash_query != "")
{
var hash_query_values = hash_query.split("&");
for(var i = 0; i < hash_query_values.length; ++i)
{
var keyval = hash_query_values[i]; /* a=b */
var key = keyval.split("=", 1)[0];
/* If the key is blank, eg. "#path?a=b&=d", then ignore the value. It'll overwrite
* the path, which is confusing and never what's wanted. */
if(key == "")
continue;
var value = keyval.substr(key.length+1);
key = window.decodeURIComponent(key);
value = window.decodeURIComponent(value);
query_params.set(key, value);
}
}
return query_params;
}
UrlHashHandler.prototype.construct = function(hash)
{
var s = "#";
var path = hash.get("");
if(path != null)
{
/* For the path portion, we only need to escape the params separator ? and the escape
* character % itself. Don't use encodeURIComponent; it'll encode far more than necessary. */
path = path.replace(/%/g, "%25").replace(/\?/g, "%3f");
s += path;
}
var params = [];
hash.each(function(k) {
var key = k[0], value = k[1];
if(key == "")
return;
if(value == null)
return;
key = window.encodeURIComponent(key);
value = window.encodeURIComponent(value);
params.push(key + "=" + value);
});
if(params.length != 0)
s += "?" + params.join("&");
return s;
}
UrlHashHandler.prototype.get_raw_hash = function()
{
/*
* Firefox doesn't handle window.location.hash correctly; it decodes the contents,
* where all other browsers give us the correct data. http://stackoverflow.com/questions/1703552
*/
var pre_hash_part = window.location.href.split("#", 1)[0];
return window.location.href.substr(pre_hash_part.length);
}
UrlHashHandler.prototype.set_raw_hash = function(hash)
{
var query_params = this.parse(hash);
this.set_all(query_params);
}
UrlHashHandler.prototype.get = function(key)
{
return this.current_hash.get(key);
}
/*
* Set keys in the URL hash.
*
* UrlHash.set({id: 50});
*
* If replace is true and the History API is available, replace the state instead
* of pushing it.
*/
UrlHashHandler.prototype.set = function(hash, replace)
{
var new_hash = this.current_hash.merge(hash);
this.normalize(new_hash);
this.set_all(new_hash, replace);
}
/*
* Each call to UrlHash.set() will immediately set the new hash, which will create a new
* browser history slot. This isn't always wanted. When several changes are being made
* in response to a single action, all changes should be made simultaeously, so only a
* single history slot is created. Making only a single call to set() is difficult when
* these changes are made by unrelated parts of code.
*
* Defer changes to the URL hash. If several calls are made in quick succession, buffer
* the changes. When a short timer expires, make all changes at once. This will never
* happen before the current Javascript call completes, because timers will never interrupt
* running code.
*
* UrlHash.set() doesn't do this, because set() guarantees that the hashchange event will
* be fired and complete before the function returns.
*
* If replace is true and the History API is available, replace the state instead of pushing
* it. If any set_deferred call consolidated into a single update has replace = false, the
* new state will be pushed.
*/
UrlHashHandler.prototype.set_deferred = function(hash, replace)
{
this.deferred_sets.push(hash);
if(replace)
this.deferred_replace = true;
var set = function()
{
this.deferred_set_timer = null;
var new_hash = this.current_hash;
this.deferred_sets.each(function(m) {
new_hash = new_hash.merge(m);
});
this.normalize(new_hash);
this.set_all(new_hash, this.deferred_replace);
this.deferred_sets = [];
this.hashchange_event(null);
this.deferred_replace = false;
}.bind(this);
if(this.deferred_set_timer == null)
this.deferred_set_timer = set.defer();
}
UrlHashHandler.prototype.set_all = function(query_params, replace)
{
query_params = query_params.clone();
this.normalize(query_params);
this.current_hash = query_params.clone();
this.denormalize(query_params);
var new_hash = this.construct(query_params);
if(window.location.hash != new_hash)
{
/* If the History API is available, use it to support URL replacement. FF4.0's pushState
* is broken; don't use it. */
if(window.history && window.history.replaceState && window.history.pushState &&
!navigator.userAgent.match("Firefox/[45]\."))
{
var url = window.location.protocol + "//" + window.location.host + window.location.pathname + new_hash;
if(replace)
window.history.replaceState({}, window.title, url);
else
window.history.pushState({}, window.title, url);
}
else
{
window.location.hash = new_hash;
}
}
/* Explicitly fire the hashchange event, so it's handled quickly even if the browser
* doesn't support the event. It's harmless if we get this event multiple times due
* to the browser delivering it normally due to our change. */
this.hashchange_event(null);
}
/* Observe changes to the specified key. If key is null, watch for all changes. */
UrlHashHandler.prototype.observe = function(key, func)
{
var observers = this.observers.get(key);
if(observers == null)
{
observers = [];
this.observers.set(key, observers);
}
if(observers.indexOf(func) != -1)
return;
observers.push(func);
}
UrlHashHandler.prototype.stopObserving = function(key, func)
{
var observers = this.observers.get(key);
if(observers == null)
return;
observers = observers.without(func);
this.observers.set(key, observers);
}
UrlHash = new UrlHashHandler();