hash.js 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. define(["./_base/kernel", "require", "./_base/config", "./aspect", "./_base/lang", "./topic", "./domReady", "./sniff"],
  2. function(dojo, require, config, aspect, lang, topic, domReady, has){
  3. // module:
  4. // dojo/hash
  5. dojo.hash = function(/* String? */ hash, /* Boolean? */ replace){
  6. // summary:
  7. // Gets or sets the hash string in the browser URL.
  8. // description:
  9. // Handles getting and setting of location.hash.
  10. //
  11. // - If no arguments are passed, acts as a getter.
  12. // - If a string is passed, acts as a setter.
  13. // hash:
  14. // the hash is set - #string.
  15. // replace:
  16. // If true, updates the hash value in the current history
  17. // state instead of creating a new history state.
  18. // returns:
  19. // when used as a getter, returns the current hash string.
  20. // when used as a setter, returns the new hash string.
  21. // example:
  22. // | topic.subscribe("/dojo/hashchange", context, callback);
  23. // |
  24. // | function callback (hashValue){
  25. // | // do something based on the hash value.
  26. // | }
  27. // getter
  28. if(!arguments.length){
  29. return _getHash();
  30. }
  31. // setter
  32. if(hash.charAt(0) == "#"){
  33. hash = hash.substring(1);
  34. }
  35. if(replace){
  36. _replace(hash);
  37. }else{
  38. location.href = "#" + hash;
  39. }
  40. return hash; // String
  41. };
  42. // Global vars
  43. var _recentHash, _ieUriMonitor, _connect,
  44. _pollFrequency = config.hashPollFrequency || 100;
  45. //Internal functions
  46. function _getSegment(str, delimiter){
  47. var i = str.indexOf(delimiter);
  48. return (i >= 0) ? str.substring(i+1) : "";
  49. }
  50. function _getHash(){
  51. return _getSegment(location.href, "#");
  52. }
  53. function _dispatchEvent(){
  54. topic.publish("/dojo/hashchange", _getHash());
  55. }
  56. function _pollLocation(){
  57. if(_getHash() === _recentHash){
  58. return;
  59. }
  60. _recentHash = _getHash();
  61. _dispatchEvent();
  62. }
  63. function _replace(hash){
  64. if(_ieUriMonitor){
  65. if(_ieUriMonitor.isTransitioning()){
  66. setTimeout(lang.hitch(null,_replace,hash), _pollFrequency);
  67. return;
  68. }
  69. var href = _ieUriMonitor.iframe.location.href;
  70. var index = href.indexOf('?');
  71. // main frame will detect and update itself
  72. _ieUriMonitor.iframe.location.replace(href.substring(0, index) + "?" + hash);
  73. return;
  74. }
  75. location.replace("#"+hash);
  76. !_connect && _pollLocation();
  77. }
  78. function IEUriMonitor(){
  79. // summary:
  80. // Determine if the browser's URI has changed or if the user has pressed the
  81. // back or forward button. If so, call _dispatchEvent.
  82. //
  83. // description:
  84. // IE doesn't add changes to the URI's hash into the history unless the hash
  85. // value corresponds to an actual named anchor in the document. To get around
  86. // this IE difference, we use a background IFrame to maintain a back-forward
  87. // history, by updating the IFrame's query string to correspond to the
  88. // value of the main browser location's hash value.
  89. //
  90. // E.g. if the value of the browser window's location changes to
  91. //
  92. // #action=someAction
  93. //
  94. // ... then we'd update the IFrame's source to:
  95. //
  96. // ?action=someAction
  97. //
  98. // This design leads to a somewhat complex state machine, which is
  99. // described below:
  100. //
  101. // ####s1
  102. //
  103. // Stable state - neither the window's location has changed nor
  104. // has the IFrame's location. Note that this is the 99.9% case, so
  105. // we optimize for it.
  106. //
  107. // Transitions: s1, s2, s3
  108. //
  109. // ####s2
  110. //
  111. // Window's location changed - when a user clicks a hyperlink or
  112. // code programmatically changes the window's URI.
  113. //
  114. // Transitions: s4
  115. //
  116. // ####s3
  117. //
  118. // Iframe's location changed as a result of user pressing back or
  119. // forward - when the user presses back or forward, the location of
  120. // the background's iframe changes to the previous or next value in
  121. // its history.
  122. //
  123. // Transitions: s1
  124. //
  125. // ####s4
  126. //
  127. // IEUriMonitor has programmatically changed the location of the
  128. // background iframe, but it's location hasn't yet changed. In this
  129. // case we do nothing because we need to wait for the iframe's
  130. // location to reflect its actual state.
  131. //
  132. // Transitions: s4, s5
  133. //
  134. // ####s5
  135. //
  136. // IEUriMonitor has programmatically changed the location of the
  137. // background iframe, and the iframe's location has caught up with
  138. // reality. In this case we need to transition to s1.
  139. //
  140. // Transitions: s1
  141. //
  142. // The hashchange event is always dispatched on the transition back to s1.
  143. // create and append iframe
  144. var ifr = document.createElement("iframe"),
  145. IFRAME_ID = "dojo-hash-iframe",
  146. ifrSrc = config.dojoBlankHtmlUrl || require.toUrl("./resources/blank.html");
  147. if(config.useXDomain && !config.dojoBlankHtmlUrl){
  148. console.warn("dojo/hash: When using cross-domain Dojo builds,"
  149. + " please save dojo/resources/blank.html to your domain and set djConfig.dojoBlankHtmlUrl"
  150. + " to the path on your domain to blank.html");
  151. }
  152. ifr.id = IFRAME_ID;
  153. ifr.src = ifrSrc + "?" + _getHash();
  154. ifr.style.display = "none";
  155. document.body.appendChild(ifr);
  156. this.iframe = dojo.global[IFRAME_ID];
  157. var recentIframeQuery, transitioning, expectedIFrameQuery, docTitle, ifrOffline,
  158. iframeLoc = this.iframe.location;
  159. function resetState(){
  160. _recentHash = _getHash();
  161. recentIframeQuery = ifrOffline ? _recentHash : _getSegment(iframeLoc.href, "?");
  162. transitioning = false;
  163. expectedIFrameQuery = null;
  164. }
  165. this.isTransitioning = function(){
  166. return transitioning;
  167. };
  168. this.pollLocation = function(){
  169. if(!ifrOffline){
  170. try{
  171. //see if we can access the iframe's location without a permission denied error
  172. var iframeSearch = _getSegment(iframeLoc.href, "?");
  173. //good, the iframe is same origin (no thrown exception)
  174. if(document.title != docTitle){ //sync title of main window with title of iframe.
  175. docTitle = this.iframe.document.title = document.title;
  176. }
  177. }catch(e){
  178. //permission denied - server cannot be reached.
  179. ifrOffline = true;
  180. console.error("dojo/hash: Error adding history entry. Server unreachable.");
  181. }
  182. }
  183. var hash = _getHash();
  184. if(transitioning && _recentHash === hash){
  185. // we're in an iframe transition (s4 or s5)
  186. if(ifrOffline || iframeSearch === expectedIFrameQuery){
  187. // s5 (iframe caught up to main window or iframe offline), transition back to s1
  188. resetState();
  189. _dispatchEvent();
  190. }else{
  191. // s4 (waiting for iframe to catch up to main window)
  192. setTimeout(lang.hitch(this,this.pollLocation),0);
  193. return;
  194. }
  195. }else if(_recentHash === hash && (ifrOffline || recentIframeQuery === iframeSearch)){
  196. // we're in stable state (s1, iframe query == main window hash), do nothing
  197. }else{
  198. // the user has initiated a URL change somehow.
  199. // sync iframe query <-> main window hash
  200. if(_recentHash !== hash){
  201. // s2 (main window location changed), set iframe url and transition to s4
  202. _recentHash = hash;
  203. transitioning = true;
  204. expectedIFrameQuery = hash;
  205. ifr.src = ifrSrc + "?" + expectedIFrameQuery;
  206. ifrOffline = false; //we're updating the iframe src - set offline to false so we can check again on next poll.
  207. setTimeout(lang.hitch(this,this.pollLocation),0); //yielded transition to s4 while iframe reloads.
  208. return;
  209. }else if(!ifrOffline){
  210. // s3 (iframe location changed via back/forward button), set main window url and transition to s1.
  211. location.href = "#" + iframeLoc.search.substring(1);
  212. resetState();
  213. _dispatchEvent();
  214. }
  215. }
  216. setTimeout(lang.hitch(this,this.pollLocation), _pollFrequency);
  217. };
  218. resetState(); // initialize state (transition to s1)
  219. setTimeout(lang.hitch(this,this.pollLocation), _pollFrequency);
  220. }
  221. domReady(function(){
  222. if("onhashchange" in dojo.global && (!has("ie") || (has("ie") >= 8 && document.compatMode != "BackCompat"))){ //need this IE browser test because "onhashchange" exists in IE8 in IE7 mode
  223. _connect = aspect.after(dojo.global,"onhashchange",_dispatchEvent, true);
  224. }else{
  225. if(document.addEventListener){ // Non-IE
  226. _recentHash = _getHash();
  227. setInterval(_pollLocation, _pollFrequency); //Poll the window location for changes
  228. }else if(document.attachEvent){ // IE7-
  229. //Use hidden iframe in versions of IE that don't have onhashchange event
  230. _ieUriMonitor = new IEUriMonitor();
  231. }
  232. // else non-supported browser, do nothing.
  233. }
  234. });
  235. return dojo.hash;
  236. });