]>
Commit | Line | Data |
---|---|---|
2f01fe57 | 1 | /* |
81bea17a | 2 | Copyright (c) 2004-2011, The Dojo Foundation All Rights Reserved. |
2f01fe57 AD |
3 | Available via Academic Free License >= 2.1 OR the modified BSD license. |
4 | see: http://dojotoolkit.org/license for details | |
5 | */ | |
6 | ||
7 | ||
a089699c AD |
8 | if(!dojo._hasResource["dojo.back"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. |
9 | dojo._hasResource["dojo.back"] = true; | |
2f01fe57 | 10 | dojo.provide("dojo.back"); |
a089699c | 11 | |
81bea17a AD |
12 | dojo.getObject("back", true, dojo); |
13 | ||
a089699c AD |
14 | /*===== |
15 | dojo.back = { | |
16 | // summary: Browser history management resources | |
2f01fe57 | 17 | } |
a089699c AD |
18 | =====*/ |
19 | ||
20 | ||
81bea17a AD |
21 | (function(){ |
22 | var back = dojo.back, | |
a089699c AD |
23 | |
24 | // everyone deals with encoding the hash slightly differently | |
25 | ||
81bea17a | 26 | getHash= back.getHash= function(){ |
a089699c AD |
27 | var h = window.location.hash; |
28 | if(h.charAt(0) == "#"){ h = h.substring(1); } | |
81bea17a AD |
29 | return dojo.isMozilla ? h : decodeURIComponent(h); |
30 | }, | |
a089699c | 31 | |
81bea17a | 32 | setHash= back.setHash= function(h){ |
a089699c AD |
33 | if(!h){ h = ""; } |
34 | window.location.hash = encodeURIComponent(h); | |
35 | historyCounter = history.length; | |
81bea17a AD |
36 | }; |
37 | ||
a089699c AD |
38 | var initialHref = (typeof(window) !== "undefined") ? window.location.href : ""; |
39 | var initialHash = (typeof(window) !== "undefined") ? getHash() : ""; | |
40 | var initialState = null; | |
41 | ||
42 | var locationTimer = null; | |
43 | var bookmarkAnchor = null; | |
44 | var historyIframe = null; | |
45 | var forwardStack = []; | |
46 | var historyStack = []; | |
47 | var moveForward = false; | |
48 | var changingUrl = false; | |
49 | var historyCounter; | |
50 | ||
51 | function handleBackButton(){ | |
52 | //summary: private method. Do not call this directly. | |
53 | ||
54 | //The "current" page is always at the top of the history stack. | |
55 | var current = historyStack.pop(); | |
56 | if(!current){ return; } | |
57 | var last = historyStack[historyStack.length-1]; | |
58 | if(!last && historyStack.length == 0){ | |
59 | last = initialState; | |
60 | } | |
61 | if(last){ | |
62 | if(last.kwArgs["back"]){ | |
63 | last.kwArgs["back"](); | |
64 | }else if(last.kwArgs["backButton"]){ | |
65 | last.kwArgs["backButton"](); | |
66 | }else if(last.kwArgs["handle"]){ | |
67 | last.kwArgs.handle("back"); | |
68 | } | |
69 | } | |
70 | forwardStack.push(current); | |
71 | } | |
72 | ||
73 | back.goBack = handleBackButton; | |
74 | ||
75 | function handleForwardButton(){ | |
76 | //summary: private method. Do not call this directly. | |
77 | var last = forwardStack.pop(); | |
78 | if(!last){ return; } | |
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"); | |
85 | } | |
86 | historyStack.push(last); | |
87 | } | |
88 | ||
89 | back.goForward = handleForwardButton; | |
90 | ||
91 | function createState(url, args, hash){ | |
92 | //summary: private method. Do not call this directly. | |
93 | return {"url": url, "kwArgs": args, "urlHash": hash}; //Object | |
94 | } | |
95 | ||
96 | function getUrlQuery(url){ | |
97 | //summary: private method. Do not call this directly. | |
98 | var segments = url.split("?"); | |
99 | if(segments.length < 2){ | |
100 | return null; //null | |
101 | } | |
102 | else{ | |
103 | return segments[1]; //String | |
104 | } | |
105 | } | |
106 | ||
107 | function loadIframeHistory(){ | |
108 | //summary: private method. Do not call this directly. | |
109 | var url = (dojo.config["dojoIframeHistoryUrl"] || dojo.moduleUrl("dojo", "resources/iframe_history.html")) + "?" + (new Date()).getTime(); | |
110 | moveForward = true; | |
111 | if(historyIframe){ | |
112 | dojo.isWebKit ? historyIframe.location = url : window.frames[historyIframe.name].location = url; | |
113 | }else{ | |
114 | //console.warn("dojo.back: Not initialised. You need to call dojo.back.init() from a <script> block that lives inside the <body> tag."); | |
115 | } | |
116 | return url; //String | |
117 | } | |
118 | ||
119 | function checkLocation(){ | |
120 | if(!changingUrl){ | |
121 | var hsl = historyStack.length; | |
122 | ||
123 | var hash = getHash(); | |
124 | ||
125 | if((hash === initialHash||window.location.href == initialHref)&&(hsl == 1)){ | |
126 | // FIXME: could this ever be a forward button? | |
127 | // we can't clear it because we still need to check for forwards. Ugg. | |
128 | // clearInterval(this.locationTimer); | |
129 | handleBackButton(); | |
130 | return; | |
131 | } | |
132 | ||
133 | // first check to see if we could have gone forward. We always halt on | |
134 | // a no-hash item. | |
135 | if(forwardStack.length > 0){ | |
136 | if(forwardStack[forwardStack.length-1].urlHash === hash){ | |
137 | handleForwardButton(); | |
138 | return; | |
139 | } | |
140 | } | |
141 | ||
142 | // ok, that didn't work, try someplace back in the history stack | |
143 | if((hsl >= 2)&&(historyStack[hsl-2])){ | |
144 | if(historyStack[hsl-2].urlHash === hash){ | |
145 | handleBackButton(); | |
146 | return; | |
147 | } | |
148 | } | |
a089699c AD |
149 | } |
150 | }; | |
151 | ||
152 | back.init = function(){ | |
81bea17a | 153 | //summary: Initializes the undo stack. This must be called from a <script> |
a089699c AD |
154 | // block that lives inside the <body> tag to prevent bugs on IE. |
155 | // description: | |
156 | // Only call this method before the page's DOM is finished loading. Otherwise | |
157 | // it will not work. Be careful with xdomain loading or djConfig.debugAtAllCosts scenarios, | |
158 | // in order for this method to work, dojo.back will need to be part of a build layer. | |
159 | if(dojo.byId("dj_history")){ return; } // prevent reinit | |
160 | var src = dojo.config["dojoIframeHistoryUrl"] || dojo.moduleUrl("dojo", "resources/iframe_history.html"); | |
161 | if (dojo._postLoad) { | |
162 | console.error("dojo.back.init() must be called before the DOM has loaded. " | |
163 | + "If using xdomain loading or djConfig.debugAtAllCosts, include dojo.back " | |
164 | + "in a build layer."); | |
165 | } else { | |
166 | 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>'); | |
167 | } | |
168 | }; | |
169 | ||
170 | back.setInitialState = function(/*Object*/args){ | |
81bea17a | 171 | //summary: |
a089699c AD |
172 | // Sets the state object and back callback for the very first page |
173 | // that is loaded. | |
174 | //description: | |
175 | // It is recommended that you call this method as part of an event | |
176 | // listener that is registered via dojo.addOnLoad(). | |
177 | //args: Object | |
178 | // See the addToHistory() function for the list of valid args properties. | |
179 | initialState = createState(initialHref, args, initialHash); | |
180 | }; | |
181 | ||
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. | |
186 | ||
187 | ||
188 | /*===== | |
189 | dojo.__backArgs = function(kwArgs){ | |
190 | // back: Function? | |
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 | |
200 | // hash. | |
201 | } | |
202 | =====*/ | |
203 | ||
204 | back.addToHistory = function(/*dojo.__backArgs*/ args){ | |
81bea17a AD |
205 | // summary: |
206 | // adds a state object (args) to the history list. | |
a089699c AD |
207 | // description: |
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. | |
81bea17a | 212 | // |
a089699c AD |
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. | |
217 | // | |
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 | |
223 | // | |
224 | // There are problems with using dojo.back with semantically-named fragment identifiers | |
225 | // ("hash values" on an URL). In most browsers it will be hard for dojo.back to know | |
226 | // distinguish a back from a forward event in those cases. For back/forward support to | |
227 | // work best, the fragment ID should always be a unique value (something using new Date().getTime() | |
228 | // for example). If you want to detect hash changes using semantic fragment IDs, then | |
229 | // consider using dojo.hash instead (in Dojo 1.4+). | |
230 | // | |
231 | // example: | |
232 | // | dojo.back.addToHistory({ | |
233 | // | back: function(){ console.log('back pressed'); }, | |
234 | // | forward: function(){ console.log('forward pressed'); }, | |
235 | // | changeUrl: true | |
236 | // | }); | |
237 | ||
238 | // BROWSER NOTES: | |
81bea17a | 239 | // Safari 1.2: |
a089699c AD |
240 | // back button "works" fine, however it's not possible to actually |
241 | // DETECT that you've moved backwards by inspecting window.location. | |
242 | // Unless there is some other means of locating. | |
243 | // FIXME: perhaps we can poll on history.length? | |
244 | // Safari 2.0.3+ (and probably 1.3.2+): | |
245 | // works fine, except when changeUrl is used. When changeUrl is used, | |
246 | // Safari jumps all the way back to whatever page was shown before | |
247 | // the page that uses dojo.undo.browser support. | |
248 | // IE 5.5 SP2: | |
249 | // back button behavior is macro. It does not move back to the | |
250 | // previous hash value, but to the last full page load. This suggests | |
251 | // that the iframe is the correct way to capture the back button in | |
252 | // these cases. | |
81bea17a AD |
253 | // Don't test this page using local disk for MSIE. MSIE will not create |
254 | // a history list for iframe_history.html if served from a file: URL. | |
255 | // The XML served back from the XHR tests will also not be properly | |
256 | // created if served from local disk. Serve the test pages from a web | |
a089699c AD |
257 | // server to test in that browser. |
258 | // IE 6.0: | |
259 | // same behavior as IE 5.5 SP2 | |
260 | // Firefox 1.0+: | |
261 | // the back button will return us to the previous hash on the same | |
262 | // page, thereby not requiring an iframe hack, although we do then | |
263 | // need to run a timer to detect inter-page movement. | |
264 | ||
265 | //If addToHistory is called, then that means we prune the | |
266 | //forward stack -- the user went back, then wanted to | |
267 | //start a new forward path. | |
81bea17a | 268 | forwardStack = []; |
a089699c AD |
269 | |
270 | var hash = null; | |
271 | var url = null; | |
272 | if(!historyIframe){ | |
273 | if(dojo.config["useXDomain"] && !dojo.config["dojoIframeHistoryUrl"]){ | |
274 | console.warn("dojo.back: When using cross-domain Dojo builds," | |
275 | + " please save iframe_history.html to your domain and set djConfig.dojoIframeHistoryUrl" | |
276 | + " to the path on your domain to iframe_history.html"); | |
277 | } | |
278 | historyIframe = window.frames["dj_history"]; | |
279 | } | |
280 | if(!bookmarkAnchor){ | |
281 | bookmarkAnchor = dojo.create("a", {style: {display: "none"}}, dojo.body()); | |
282 | } | |
283 | if(args["changeUrl"]){ | |
284 | hash = ""+ ((args["changeUrl"]!==true) ? args["changeUrl"] : (new Date()).getTime()); | |
285 | ||
286 | //If the current hash matches the new one, just replace the history object with | |
287 | //this new one. It doesn't make sense to track different state objects for the same | |
288 | //logical URL. This matches the browser behavior of only putting in one history | |
289 | //item no matter how many times you click on the same #hash link, at least in Firefox | |
290 | //and Safari, and there is no reliable way in those browsers to know if a #hash link | |
291 | //has been clicked on multiple times. So making this the standard behavior in all browsers | |
292 | //so that dojo.back's behavior is the same in all browsers. | |
293 | if(historyStack.length == 0 && initialState.urlHash == hash){ | |
294 | initialState = createState(url, args, hash); | |
295 | return; | |
296 | }else if(historyStack.length > 0 && historyStack[historyStack.length - 1].urlHash == hash){ | |
297 | historyStack[historyStack.length - 1] = createState(url, args, hash); | |
298 | return; | |
299 | } | |
300 | ||
301 | changingUrl = true; | |
81bea17a AD |
302 | setTimeout(function() { |
303 | setHash(hash); | |
304 | changingUrl = false; | |
a089699c AD |
305 | }, 1); |
306 | bookmarkAnchor.href = hash; | |
307 | ||
308 | if(dojo.isIE){ | |
309 | url = loadIframeHistory(); | |
310 | ||
311 | var oldCB = args["back"]||args["backButton"]||args["handle"]; | |
312 | ||
313 | //The function takes handleName as a parameter, in case the | |
314 | //callback we are overriding was "handle". In that case, | |
315 | //we will need to pass the handle name to handle. | |
316 | var tcb = function(handleName){ | |
317 | if(getHash() != ""){ | |
318 | setTimeout(function() { setHash(hash); }, 1); | |
319 | } | |
320 | //Use apply to set "this" to args, and to try to avoid memory leaks. | |
321 | oldCB.apply(this, [handleName]); | |
322 | }; | |
323 | ||
324 | //Set interceptor function in the right place. | |
325 | if(args["back"]){ | |
326 | args.back = tcb; | |
327 | }else if(args["backButton"]){ | |
328 | args.backButton = tcb; | |
329 | }else if(args["handle"]){ | |
330 | args.handle = tcb; | |
331 | } | |
332 | ||
333 | var oldFW = args["forward"]||args["forwardButton"]||args["handle"]; | |
334 | ||
335 | //The function takes handleName as a parameter, in case the | |
336 | //callback we are overriding was "handle". In that case, | |
337 | //we will need to pass the handle name to handle. | |
338 | var tfw = function(handleName){ | |
339 | if(getHash() != ""){ | |
340 | setHash(hash); | |
341 | } | |
342 | if(oldFW){ // we might not actually have one | |
343 | //Use apply to set "this" to args, and to try to avoid memory leaks. | |
344 | oldFW.apply(this, [handleName]); | |
345 | } | |
346 | }; | |
347 | ||
348 | //Set interceptor function in the right place. | |
349 | if(args["forward"]){ | |
350 | args.forward = tfw; | |
351 | }else if(args["forwardButton"]){ | |
352 | args.forwardButton = tfw; | |
353 | }else if(args["handle"]){ | |
354 | args.handle = tfw; | |
355 | } | |
356 | ||
357 | }else if(!dojo.isIE){ | |
358 | // start the timer | |
359 | if(!locationTimer){ | |
360 | locationTimer = setInterval(checkLocation, 200); | |
361 | } | |
362 | ||
363 | } | |
364 | }else{ | |
365 | url = loadIframeHistory(); | |
366 | } | |
367 | ||
368 | historyStack.push(createState(url, args, hash)); | |
369 | }; | |
370 | ||
371 | back._iframeLoaded = function(evt, ifrLoc){ | |
81bea17a | 372 | //summary: |
a089699c AD |
373 | // private method. Do not call this directly. |
374 | var query = getUrlQuery(ifrLoc.href); | |
81bea17a | 375 | if(query == null){ |
a089699c AD |
376 | // alert("iframeLoaded"); |
377 | // we hit the end of the history, so we should go back | |
378 | if(historyStack.length == 1){ | |
379 | handleBackButton(); | |
380 | } | |
381 | return; | |
382 | } | |
383 | if(moveForward){ | |
384 | // we were expecting it, so it's not either a forward or backward movement | |
385 | moveForward = false; | |
386 | return; | |
387 | } | |
388 | ||
389 | //Check the back stack first, since it is more likely. | |
390 | //Note that only one step back or forward is supported. | |
391 | if(historyStack.length >= 2 && query == getUrlQuery(historyStack[historyStack.length-2].url)){ | |
392 | handleBackButton(); | |
393 | }else if(forwardStack.length > 0 && query == getUrlQuery(forwardStack[forwardStack.length-1].url)){ | |
394 | handleForwardButton(); | |
395 | } | |
396 | }; | |
397 | })(); | |
398 | ||
2f01fe57 | 399 | } |