1 if(!dojo._hasResource["dojo.back"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
2 dojo._hasResource["dojo.back"] = true;
3 dojo.provide("dojo.back");
7 // summary: Browser history management resources
15 // everyone deals with encoding the hash slightly differently
18 var h = window.location.hash;
19 if(h.charAt(0) == "#"){ h = h.substring(1); }
20 return dojo.isMozilla ? h : decodeURIComponent(h);
25 window.location.hash = encodeURIComponent(h);
26 historyCounter = history.length;
29 // if we're in the test for these methods, expose them on dojo.back. ok'd with alex.
30 if(dojo.exists("tests.back-hash")){
31 back.getHash = getHash;
32 back.setHash = setHash;
35 var initialHref = (typeof(window) !== "undefined") ? window.location.href : "";
36 var initialHash = (typeof(window) !== "undefined") ? getHash() : "";
37 var initialState = null;
39 var locationTimer = null;
40 var bookmarkAnchor = null;
41 var historyIframe = null;
42 var forwardStack = [];
43 var historyStack = [];
44 var moveForward = false;
45 var changingUrl = false;
48 function handleBackButton(){
49 //summary: private method. Do not call this directly.
51 //The "current" page is always at the top of the history stack.
52 //console.debug("handlingBackButton");
53 var current = historyStack.pop();
54 if(!current){ return; }
55 var last = historyStack[historyStack.length-1];
56 if(!last && historyStack.length == 0){
60 if(last.kwArgs["back"]){
61 last.kwArgs["back"]();
62 }else if(last.kwArgs["backButton"]){
63 last.kwArgs["backButton"]();
64 }else if(last.kwArgs["handle"]){
65 last.kwArgs.handle("back");
68 forwardStack.push(current);
69 //console.debug("done handling back");
72 back.goBack = handleBackButton;
74 function handleForwardButton(){
75 //summary: private method. Do not call this directly.
76 //console.debug("handling forward");
77 var last = forwardStack.pop();
79 if(last.kwArgs["forward"]){
80 last.kwArgs.forward();
81 }else if(last.kwArgs["forwardButton"]){
82 last.kwArgs.forwardButton();
83 }else if(last.kwArgs["handle"]){
84 last.kwArgs.handle("forward");
86 historyStack.push(last);
87 //console.debug("done handling forward");
90 back.goForward = handleForwardButton;
92 function createState(url, args, hash){
93 //summary: private method. Do not call this directly.
94 return {"url": url, "kwArgs": args, "urlHash": hash}; //Object
97 function getUrlQuery(url){
98 //summary: private method. Do not call this directly.
99 var segments = url.split("?");
100 if(segments.length < 2){
104 return segments[1]; //String
108 function loadIframeHistory(){
109 //summary: private method. Do not call this directly.
110 var url = (dojo.config["dojoIframeHistoryUrl"] || dojo.moduleUrl("dojo", "resources/iframe_history.html")) + "?" + (new Date()).getTime();
113 dojo.isSafari ? historyIframe.location = url : window.frames[historyIframe.name].location = url;
115 //console.warn("dojo.back: Not initialised. You need to call dojo.back.init() from a <script> block that lives inside the <body> tag.");
120 function checkLocation(){
121 //console.debug("checking url");
123 var hsl = historyStack.length;
125 var hash = getHash();
127 if((hash === initialHash||window.location.href == initialHref)&&(hsl == 1)){
128 // FIXME: could this ever be a forward button?
129 // we can't clear it because we still need to check for forwards. Ugg.
130 // clearInterval(this.locationTimer);
135 // first check to see if we could have gone forward. We always halt on
137 if(forwardStack.length > 0){
138 if(forwardStack[forwardStack.length-1].urlHash === hash){
139 handleForwardButton();
144 // ok, that didn't work, try someplace back in the history stack
145 if((hsl >= 2)&&(historyStack[hsl-2])){
146 if(historyStack[hsl-2].urlHash === hash){
152 if(dojo.isSafari && dojo.isSafari < 3){
153 var hisLen = history.length;
154 if(hisLen > historyCounter) handleForwardButton();
155 else if(hisLen < historyCounter) handleBackButton();
156 historyCounter = hisLen;
159 //console.debug("done checking");
162 back.init = function(){
163 //summary: Initializes the undo stack. This must be called from a <script>
164 // block that lives inside the <body> tag to prevent bugs on IE.
165 if(dojo.byId("dj_history")){ return; } // prevent reinit
166 var src = dojo.config["dojoIframeHistoryUrl"] || dojo.moduleUrl("dojo", "resources/iframe_history.html");
167 document.write('<iframe style="border:0;width:1px;height:1px;position:absolute;visibility:hidden;bottom:0;right:0;" name="dj_history" id="dj_history" src="' + src + '"></iframe>');
170 back.setInitialState = function(/*Object*/args){
172 // Sets the state object and back callback for the very first page
175 // It is recommended that you call this method as part of an event
176 // listener that is registered via dojo.addOnLoad().
178 // See the addToHistory() function for the list of valid args properties.
179 initialState = createState(initialHref, args, initialHash);
182 //FIXME: Make these doc comments not be awful. At least they're not wrong.
183 //FIXME: Would like to support arbitrary back/forward jumps. Have to rework iframeLoaded among other things.
184 //FIXME: is there a slight race condition in moz using change URL with the timer check and when
185 // the hash gets set? I think I have seen a back/forward call in quick succession, but not consistent.
189 dojo.__backArgs = function(kwArgs){
191 // A function to be called when this state is reached via the user
192 // clicking the back button.
193 // forward: Function?
194 // Upon return to this state from the "back, forward" combination
195 // of navigation steps, this function will be called. Somewhat
196 // analgous to the semantic of an "onRedo" event handler.
197 // changeUrl: Boolean?|String?
198 // Boolean indicating whether or not to create a unique hash for
199 // this state. If a string is passed instead, it is used as the
204 back.addToHistory = function(/*dojo.__backArgs*/ args){
206 // adds a state object (args) to the history list.
208 // To support getting back button notifications, the object
209 // argument should implement a function called either "back",
210 // "backButton", or "handle". The string "back" will be passed as
211 // the first and only argument to this callback.
213 // To support getting forward button notifications, the object
214 // argument should implement a function called either "forward",
215 // "forwardButton", or "handle". The string "forward" will be
216 // passed as the first and only argument to this callback.
218 // If you want the browser location string to change, define "changeUrl" on the object. If the
219 // value of "changeUrl" is true, then a unique number will be appended to the URL as a fragment
220 // identifier (http://some.domain.com/path#uniquenumber). If it is any other value that does
221 // not evaluate to false, that value will be used as the fragment identifier. For example,
222 // if changeUrl: 'page1', then the URL will look like: http://some.domain.com/path#page1
225 // | dojo.back.addToHistory({
226 // | back: function(){ console.debug('back pressed'); },
227 // | forward: function(){ console.debug('forward pressed'); },
233 // back button "works" fine, however it's not possible to actually
234 // DETECT that you've moved backwards by inspecting window.location.
235 // Unless there is some other means of locating.
236 // FIXME: perhaps we can poll on history.length?
237 // Safari 2.0.3+ (and probably 1.3.2+):
238 // works fine, except when changeUrl is used. When changeUrl is used,
239 // Safari jumps all the way back to whatever page was shown before
240 // the page that uses dojo.undo.browser support.
242 // back button behavior is macro. It does not move back to the
243 // previous hash value, but to the last full page load. This suggests
244 // that the iframe is the correct way to capture the back button in
246 // Don't test this page using local disk for MSIE. MSIE will not create
247 // a history list for iframe_history.html if served from a file: URL.
248 // The XML served back from the XHR tests will also not be properly
249 // created if served from local disk. Serve the test pages from a web
250 // server to test in that browser.
252 // same behavior as IE 5.5 SP2
254 // the back button will return us to the previous hash on the same
255 // page, thereby not requiring an iframe hack, although we do then
256 // need to run a timer to detect inter-page movement.
258 //If addToHistory is called, then that means we prune the
259 //forward stack -- the user went back, then wanted to
260 //start a new forward path.
266 if(dojo.config["useXDomain"] && !dojo.config["dojoIframeHistoryUrl"]){
267 console.debug("dojo.back: When using cross-domain Dojo builds,"
268 + " please save iframe_history.html to your domain and set djConfig.dojoIframeHistoryUrl"
269 + " to the path on your domain to iframe_history.html");
271 historyIframe = window.frames["dj_history"];
274 bookmarkAnchor = document.createElement("a");
275 dojo.body().appendChild(bookmarkAnchor);
276 bookmarkAnchor.style.display = "none";
278 if(args["changeUrl"]){
279 hash = ""+ ((args["changeUrl"]!==true) ? args["changeUrl"] : (new Date()).getTime());
281 //If the current hash matches the new one, just replace the history object with
282 //this new one. It doesn't make sense to track different state objects for the same
283 //logical URL. This matches the browser behavior of only putting in one history
284 //item no matter how many times you click on the same #hash link, at least in Firefox
285 //and Safari, and there is no reliable way in those browsers to know if a #hash link
286 //has been clicked on multiple times. So making this the standard behavior in all browsers
287 //so that dojo.back's behavior is the same in all browsers.
288 if(historyStack.length == 0 && initialState.urlHash == hash){
289 initialState = createState(url, args, hash);
291 }else if(historyStack.length > 0 && historyStack[historyStack.length - 1].urlHash == hash){
292 historyStack[historyStack.length - 1] = createState(url, args, hash);
297 setTimeout(function() {
301 bookmarkAnchor.href = hash;
304 url = loadIframeHistory();
306 var oldCB = args["back"]||args["backButton"]||args["handle"];
308 //The function takes handleName as a parameter, in case the
309 //callback we are overriding was "handle". In that case,
310 //we will need to pass the handle name to handle.
311 var tcb = function(handleName){
313 setTimeout(function() { setHash(hash); }, 1);
315 //Use apply to set "this" to args, and to try to avoid memory leaks.
316 oldCB.apply(this, [handleName]);
319 //Set interceptor function in the right place.
322 }else if(args["backButton"]){
323 args.backButton = tcb;
324 }else if(args["handle"]){
328 var oldFW = args["forward"]||args["forwardButton"]||args["handle"];
330 //The function takes handleName as a parameter, in case the
331 //callback we are overriding was "handle". In that case,
332 //we will need to pass the handle name to handle.
333 var tfw = function(handleName){
337 if(oldFW){ // we might not actually have one
338 //Use apply to set "this" to args, and to try to avoid memory leaks.
339 oldFW.apply(this, [handleName]);
343 //Set interceptor function in the right place.
346 }else if(args["forwardButton"]){
347 args.forwardButton = tfw;
348 }else if(args["handle"]){
352 }else if(!dojo.isIE){
355 locationTimer = setInterval(checkLocation, 200);
360 url = loadIframeHistory();
363 historyStack.push(createState(url, args, hash));
366 back._iframeLoaded = function(evt, ifrLoc){
368 // private method. Do not call this directly.
369 var query = getUrlQuery(ifrLoc.href);
371 // alert("iframeLoaded");
372 // we hit the end of the history, so we should go back
373 if(historyStack.length == 1){
379 // we were expecting it, so it's not either a forward or backward movement
384 //Check the back stack first, since it is more likely.
385 //Note that only one step back or forward is supported.
386 if(historyStack.length >= 2 && query == getUrlQuery(historyStack[historyStack.length-2].url)){
388 }else if(forwardStack.length > 0 && query == getUrlQuery(forwardStack[forwardStack.length-1].url)){
389 handleForwardButton();