back.js 14 KB

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