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