dojox
Version:
Dojo eXtensions, a rollup of many useful sub-projects and varying states of maturity – from very stable and robust, to alpha and experimental. See individual projects contain README files for details.
292 lines (249 loc) • 10.6 kB
JavaScript
define(["dojo/_base/lang", "dojo/_base/declare", "dojo/topic", "dojo/on", "../Controller", "../utils/hash", "dojo/hash"],
function(lang, declare, topic, on, Controller, hash){
// module:
// dojox/app/controllers/HistoryHash
// summary:
// Bind "app-domNode" event on dojox/app application instance,
// Bind "startTransition" event on dojox/app application domNode,
// Bind "/dojo/hashchange" event on window object.
// Maintain history by history hash.
return declare("dojox.app.controllers.HistoryHash", Controller, {
constructor: function(app){
// summary:
// Bind "app-domNode" event on dojox/app application instance,
// Bind "startTransition" event on dojox/app application domNode,
// subscribe "/dojo/hashchange" event.
//
// app:
// dojox/app application instance.
this.events = {
"app-domNode": this.onDomNodeChange
};
if(this.app.domNode){
this.onDomNodeChange({oldNode: null, newNode: this.app.domNode});
}
topic.subscribe("/dojo/hashchange", lang.hitch(this, function(newhash){
this._onHashChange(newhash);
}));
this._historyStack = []; // application history stack
this._historyLen = 0; // current window.history length
this._historyDiff = 0; // the diff of window.history stack and application history stack
this._current = null; // current history item in application history stack
this._next = null; // next history item in application history stack
this._previous = null; // previous history item in application history stack
this._index = 0; // identify current history item's index in application history stack
this._oldHistoryLen = 0;// window.history stack length before hash change
this._newHistoryLen = 0;// window.history stack length after hash change
this._addToHistoryStack = false;
this._startTransitionEvent = false;
// push the default page to the history stack
var currentHash = window.location.hash;
if(currentHash && (currentHash.length > 1)){
currentHash = currentHash.substr(1);
}
this._historyStack.push({
"hash": currentHash,
"url": window.location.href,
"detail": {target:currentHash}
});
this._historyLen = window.history.length;
this._index = this._historyStack.length - 1;
this._current = currentHash;
// get the diff of window.history and application history
this._historyDiff = window.history.length - this._historyStack.length;
},
onDomNodeChange: function(evt){
if(evt.oldNode != null){
this.unbind(evt.oldNode, "startTransition");
}
this.bind(evt.newNode, "startTransition", lang.hitch(this, this.onStartTransition));
},
onStartTransition: function(evt){
// summary:
// Response to dojox/app "startTransition" event.
//
// example:
// Use "dojox/mobile/TransitionEvent" to trigger "startTransition" event, and this function will response the event. For example:
// | var transOpts = {
// | title:"List",
// | target:"items,list",
// | url: "#items,list"
// | params: {"param1":"p1value"}
// | };
// | new TransitionEvent(domNode, transOpts, e).dispatch();
//
// evt: Object
// transition options parameter
var target = evt.detail.target;
var regex = /#(.+)/;
if(!target && regex.test(evt.detail.href)){
target = evt.detail.href.match(regex)[1];
}
var currentHash = evt.detail.url || "#" + target;
if(evt.detail.params){
currentHash = hash.buildWithParams(currentHash, evt.detail.params);
}
this._oldHistoryLen = window.history.length;
// pushState on iOS will not change location bar hash because of security.
// window.history.pushState(evt.detail, title, currentHash);
// history.length will be changed by set location hash
// change url hash, to workaround iOS pushState not change address bar issue.
window.location.hash = currentHash;
// The operation above will trigger hashchange.
// Use _addToHistoryStack flag to indicate the _onHashChange method should add this hash to history stack.
// When add hash to history stack, this flag should be set to false, we do this in _addHistory.
this._addToHistoryStack = true;
//set startTransition event flag to true if the hash change from startTransition event.
this._startTransitionEvent = true;
},
_addHistory: function(hash){
// summary:
// Add hash to application history stack, update history management flags.
//
// hash:
// new hash should be added to _historyStack.
this._historyStack.push({
"hash": hash,
"url": window.location.href,
"detail": {target:hash}
});
this._historyLen = window.history.length;
this._index = this._historyStack.length - 1;
this._previous = this._current;
this._current = hash;
this._next = null;
this._historyDiff = window.history.length - this._historyStack.length;
// In order to make sure _addToHistoryStack flag invalid after add hash to history stack,
// we set this flag to false in every addHistory operation even if it's already false.
this._addToHistoryStack = false;
},
_onHashChange: function(currentHash){
// summary:
// subscribe /dojo/hashchange and do add history, back, forward and go operation.
//
// currentHash:
// the new url hash when /dojo/hashchange is triggered.
if(this._index < 0 || this._index > (window.history.length - 1)){
throw Error("Application history out of management.");
}
this._newHistoryLen = window.history.length;
// Application history stack asynchronized with window.history, refresh application history stack.
if(this._oldHistoryLen > this._newHistoryLen){
//console.log("need to refresh _historyStack, oldLen:"+this._oldHistoryLen+", newLen: "+this._newHistoryLen+", diff:"+this._historyDiff);
this._historyStack.splice((this._newHistoryLen - this._historyDiff - 1), (this._historyStack.length - 1));
// Reset _historyLen to make sure this._historyLen<window.history.length, so it will push this hash to history stack.
this._historyLen = this._historyStack.length;
// Reset this._oldHistoryLen, so it can avoid refresh history stack again in some situation,
// because by doing this, this._oldHistoryLen !== this._newHistoryLen
this._oldHistoryLen = 0;
}
// this._oldHistoryLen === this._newHistoryLen, it maybe need to refresh history stack or do history go, back and forward,
// so we use _addToHistoryStack to identify the refresh operation.
if(this._addToHistoryStack && (this._oldHistoryLen === this._newHistoryLen)){
this._historyStack.splice((this._newHistoryLen - this._historyDiff - 1), (this._historyStack.length - 1));
this._addHistory(currentHash);
// It's a refresh operation, so that's no need to check history go, back or forward, just return.
return;
}
//window.history.length increase, add hash to application history stack.
if(this._historyLen < window.history.length){
this._addHistory(currentHash);
if(!this._startTransitionEvent){
// transition to the target view
this.app.emit("app-transition", {
viewId: hash.getTarget(currentHash),
opts: { params: hash.getParams(currentHash) || {} }
});
}
}else{
if(currentHash == this._current){
// console.log("do nothing.");
}else if(currentHash === this._previous){ // back
this._back(currentHash, this._historyStack[this._index]["detail"]);
}else if(currentHash === this._next){ //forward
this._forward(currentHash, this._historyStack[this._index]["detail"]);
}else{ // go
//search in "back" first, then "forward"
var index = -1;
for(var i = this._index; i > 0; i--){
if(currentHash === this._historyStack[i]["hash"]){
index = i;
break;
}
}
//search in "forward"
if(-1 === index){
for(var i = this._index; i < this._historyStack.length; i++){
if(currentHash === this._historyStack[i]["hash"]){
index = i;
break;
}
}
}
if(0 < index < this._historyStack.length){
this._go(index, (index - this._index));
}else{
this.app.log("go error. index out of history stack.");
}
}
}
// set startTransition event flag to false
this._startTransitionEvent = false;
},
_back: function(currentHash, detail){
this.app.log("back");
this._next = this._historyStack[this._index]["hash"];
this._index--;
if(this._index > 0){
this._previous = this._historyStack[this._index - 1]["hash"];
}else{
this._previous = null;
}
this._current = currentHash;
var target = hash.getTarget(currentHash, this.app.defaultView);
// publish history back event
topic.publish("/app/history/back", {"viewId": target, "detail": detail});
// transition to the target view
this.app.emit("app-transition", {
viewId: target,
opts: lang.mixin({reverse: true}, detail, {"params": hash.getParams(currentHash)})
});
},
_forward: function(currentHash, detail){
this.app.log("forward");
this._previous = this._historyStack[this._index]["hash"];
this._index++;
if(this._index < this._historyStack.length - 1){
this._next = this._historyStack[this._index + 1]["hash"];
}else{
this._next = null;
}
this._current = currentHash;
var target = hash.getTarget(currentHash, this.app.defaultView);
// publish history forward event
topic.publish("/app/history/forward", {"viewId": target, "detail": detail});
// transition to the target view
this.app.emit("app-transition", {
viewId: target,
opts: lang.mixin({reverse: false}, detail, {"params": hash.getParams(currentHash)})
});
},
_go: function(index, step){
if(index < 0 || (index > window.history.length - 1)){
throw Error("Application history.go steps out of management, index: "+index+" length: "+window.history.length);
}
this._index = index;
this._current = this._historyStack[index]["hash"];
this._previous = this._historyStack[index - 1] ? this._historyStack[index - 1]["hash"] : null;
this._next = this._historyStack[index + 1] ? this._historyStack[index + 1]["hash"] : null;
var target = hash.getTarget(this._current, this.app.defaultView);
// publish history go event
topic.publish("/app/history/go", {"viewId": target, "step": step, "detail": this._historyStack[index]["detail"]});
// transition to the target view
this.app.emit("app-transition", {
viewId: target,
opts: lang.mixin({reverse: (step <= 0)}, this._historyStack[index]["detail"], {"params": hash.getParams(this._current)})
});
}
});
});