source: Dev/branches/rest-dojo-ui/client/dojo/back.js @ 256

Last change on this file since 256 was 256, checked in by hendrikvanantwerpen, 13 years ago

Reworked project structure based on REST interaction and Dojo library. As
soon as this is stable, the old jQueryUI branch can be removed (it's
kept for reference).

File size: 13.9 KB
RevLine 
[256]1define(["./_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/*=====
10dojo.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});
Note: See TracBrowser for help on using the repository browser.