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