back.js 14 KB

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