Revision of Twine: Improved back and forward buttons in Sugarcane from Wed, 12/11/2013 - 02:58

Update: The Javascript on this page is now built into Twine 1.4! This behaviour is now the default and this page is no longer necessary.

This script allows Sugarcane to use HTML5 history management instead of URL hash strings to alter the browser history. This means that various non-deterministic game state changes (random numbers, player data input, state changes inside <<replace>> macros, etc.) will be properly remembered when you use the browser's Back button. This code also updates <<back>> and <<return>> and the Rewind menu.

Note: due to a conflict in the Twine engine, I've had to include Transition CSS with this script. So, you must also include one of the CSS transitions on that page. The default transition CSS code is as follows:

.transition-in{opacity:0;position:absolute}.passage:not(.transition-out){transition:1s;-webkit-transition:1s}
.transition-out{opacity:0;position:absolute}

Note 2: IE9 users will be given the old hashchange functionality anyway.

Javascript code follows:

(function(){var hasPushState=(typeof window.history.pushState=="function");
History.prototype.display=function(d,b,a){var c=tale.get(d);if(a!="back"){this.history.unshift({passage:c,variables:clone(this.history[0].variables)});
this.history[0].hash=this.save();if(hasPushState&&this.history){if(this.history.length==2&&window.history.state===null){window.history.replaceState(this.history,document.title)
}else{window.history.pushState(this.history,document.title)}}}this.history[0].hash=this.save();
var e=c.render();e.style.visibility="visible";if(a!="offscreen"){var p=$("passages");
for(var j=0;j<p.childNodes.length;j+=1){var q=p.childNodes[j];
q.classList.add("transition-out");setTimeout(function(){if(q.parentNode){q.parentNode.removeChild(q)
}},1000)}e.classList.add("transition-in");setTimeout(function(){e.classList.remove("transition-in")
},1);p.appendChild(e)}if((a=="quietly")||(a=="offscreen")){e.style.visibility="visible"
}if(a!="offscreen"){document.title=tale.title+": "+c.title;this.hash=this.save();
if(!hasPushState){window.location.hash=this.hash}window.scroll(0,0)
}return e};History.prototype.restart=function(){if(hasPushState){window.location.reload()
}else{window.location.hash=""}};macros["return"]=macros.back={handler:function(a,b,e){var el,d="";
var steps=1;if(e[0]){if(e[1]=="steps"){if(isNaN(e[0])){throwError(a,"parameter before 'steps' must be a number.");
return}else{if(e[0]<state.history.length){d=state.history[e[0]].passage.title;
steps=e[0]}}}else{if(tale.get(e[0]).id==undefined){throwError(a,"The "+e[0]+" passage does not exist");
return}for(var c=0;c<state.history.length;c++){if(state.history[c].passage.title==e[0]){d=e[0];
steps=c;break}}}}else{d=state.history[1].passage.title}if(!d){return
}else{el=document.createElement("a");el.className="return";el.onclick=function(){if(b=="back"){if(hasPushState){window.history.back();
return}while(steps>=0){if(state.history.length>1){state.history.shift()
}steps--}}state.display(d)};el.href="javascript:void(0)";el.innerHTML="<b>«</b> "+b[0].toUpperCase()+b.slice(1);
a.appendChild(el)}}};Interface.buildSnapback=function(){var c=false;
removeChildren(document.getElementById("snapbackMenu"));for(var a=state.history.length-1;
a>=0;a--){if(state.history[a].passage&&state.history[a].passage.tags.indexOf("bookmark")!=-1){var b=document.createElement("div");
b.pos=a;b.onclick=function(){var p=this.pos;var n=state.history[p].passage.title;
window.history.go(-(p+1));while(p>=0){if(state.history.length>1){state.history.shift()
}p--}state.display(n)};b.innerHTML=Passage.prototype.excerpt.call(state.history[a].passage);
document.getElementById("snapbackMenu").appendChild(b);c=true
}}if(!c){var b=document.createElement("div");b.innerHTML="<i>No passages available</i>";
document.getElementById("snapbackMenu").appendChild(b)}};History.prototype.init=function(){if(!this.restore()){this.display("Start",null)
}if(!hasPushState){this.hash=window.location.hash;this.interval=window.setInterval(function(){a.watchHash.apply(a)
},250)}};window.onpopstate=function(e){if(e.state===null){return
}if(e.state&&e.state.length>0){state.history=e.state}else{state=new History();
state.init()}state.display(state.history[0].passage.title,null,"back")
};if(hasPushState&&state&&state.interval){clearInterval(state.interval)
}}());

Feel free to report any bugs to @webbedspace.