history.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. /**
  2. * Durandal 2.1.0 Copyright (c) 2012 Blue Spire Consulting, Inc. All Rights Reserved.
  3. * Available via the MIT license.
  4. * see: http://durandaljs.com or https://github.com/BlueSpire/Durandal for details.
  5. */
  6. /**
  7. * This module is based on Backbone's core history support. It abstracts away the low level details of working with browser history and url changes in order to provide a solid foundation for a router.
  8. * @module history
  9. * @requires system
  10. * @requires jquery
  11. */
  12. define(['durandal/system', 'jquery'], function (system, $) {
  13. // Cached regex for stripping a leading hash/slash and trailing space.
  14. var routeStripper = /^[#\/]|\s+$/g;
  15. // Cached regex for stripping leading and trailing slashes.
  16. var rootStripper = /^\/+|\/+$/g;
  17. // Cached regex for detecting MSIE.
  18. var isExplorer = /msie [\w.]+/;
  19. // Cached regex for removing a trailing slash.
  20. var trailingSlash = /\/$/;
  21. // Update the hash location, either replacing the current entry, or adding
  22. // a new one to the browser history.
  23. function updateHash(location, fragment, replace) {
  24. if (replace) {
  25. var href = location.href.replace(/(javascript:|#).*$/, '');
  26. if (history.history.replaceState) {
  27. history.history.replaceState({}, document.title, href + '#' + fragment); // using history.replaceState instead of location.replace to work around chrom bug
  28. } else {
  29. location.replace(href + '#' + fragment);
  30. }
  31. } else {
  32. // Some browsers require that `hash` contains a leading #.
  33. location.hash = '#' + fragment;
  34. }
  35. };
  36. /**
  37. * @class HistoryModule
  38. * @static
  39. */
  40. var history = {
  41. /**
  42. * The setTimeout interval used when the browser does not support hash change events.
  43. * @property {string} interval
  44. * @default 50
  45. */
  46. interval: 50,
  47. /**
  48. * Indicates whether or not the history module is actively tracking history.
  49. * @property {string} active
  50. */
  51. active: false
  52. };
  53. // Ensure that `History` can be used outside of the browser.
  54. if (typeof window !== 'undefined') {
  55. history.location = window.location;
  56. history.history = window.history;
  57. }
  58. /**
  59. * Gets the true hash value. Cannot use location.hash directly due to a bug in Firefox where location.hash will always be decoded.
  60. * @method getHash
  61. * @param {string} [window] The optional window instance
  62. * @return {string} The hash.
  63. */
  64. history.getHash = function(window) {
  65. var match = (window || history).location.href.match(/#(.*)$/);
  66. return match ? match[1] : '';
  67. };
  68. /**
  69. * Get the cross-browser normalized URL fragment, either from the URL, the hash, or the override.
  70. * @method getFragment
  71. * @param {string} fragment The fragment.
  72. * @param {boolean} forcePushState Should we force push state?
  73. * @return {string} he fragment.
  74. */
  75. history.getFragment = function(fragment, forcePushState) {
  76. if (fragment == null) {
  77. if (history._hasPushState || !history._wantsHashChange || forcePushState) {
  78. fragment = history.location.pathname + history.location.search;
  79. var root = history.root.replace(trailingSlash, '');
  80. if (!fragment.indexOf(root)) {
  81. fragment = fragment.substr(root.length);
  82. }
  83. } else {
  84. fragment = history.getHash();
  85. }
  86. }
  87. return fragment.replace(routeStripper, '');
  88. };
  89. /**
  90. * Activate the hash change handling, returning `true` if the current URL matches an existing route, and `false` otherwise.
  91. * @method activate
  92. * @param {HistoryOptions} options.
  93. * @return {boolean|undefined} Returns true/false from loading the url unless the silent option was selected.
  94. */
  95. history.activate = function(options) {
  96. if (history.active) {
  97. system.error("History has already been activated.");
  98. }
  99. history.active = true;
  100. // Figure out the initial configuration. Do we need an iframe?
  101. // Is pushState desired ... is it available?
  102. history.options = system.extend({}, { root: '/' }, history.options, options);
  103. history.root = history.options.root;
  104. history._wantsHashChange = history.options.hashChange !== false;
  105. history._wantsPushState = !!history.options.pushState;
  106. history._hasPushState = !!(history.options.pushState && history.history && history.history.pushState);
  107. var fragment = history.getFragment();
  108. var docMode = document.documentMode;
  109. var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7));
  110. // Normalize root to always include a leading and trailing slash.
  111. history.root = ('/' + history.root + '/').replace(rootStripper, '/');
  112. if (oldIE && history._wantsHashChange) {
  113. history.iframe = $('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo('body')[0].contentWindow;
  114. history.navigate(fragment, false);
  115. }
  116. // Depending on whether we're using pushState or hashes, and whether
  117. // 'onhashchange' is supported, determine how we check the URL state.
  118. if (history._hasPushState) {
  119. $(window).on('popstate', history.checkUrl);
  120. } else if (history._wantsHashChange && ('onhashchange' in window) && !oldIE) {
  121. $(window).on('hashchange', history.checkUrl);
  122. } else if (history._wantsHashChange) {
  123. history._checkUrlInterval = setInterval(history.checkUrl, history.interval);
  124. }
  125. // Determine if we need to change the base url, for a pushState link
  126. // opened by a non-pushState browser.
  127. history.fragment = fragment;
  128. var loc = history.location;
  129. var atRoot = loc.pathname.replace(/[^\/]$/, '$&/') === history.root;
  130. // Transition from hashChange to pushState or vice versa if both are requested.
  131. if (history._wantsHashChange && history._wantsPushState) {
  132. // If we've started off with a route from a `pushState`-enabled
  133. // browser, but we're currently in a browser that doesn't support it...
  134. if (!history._hasPushState && !atRoot) {
  135. history.fragment = history.getFragment(null, true);
  136. history.location.replace(history.root + history.location.search + '#' + history.fragment);
  137. // Return immediately as browser will do redirect to new url
  138. return true;
  139. // Or if we've started out with a hash-based route, but we're currently
  140. // in a browser where it could be `pushState`-based instead...
  141. } else if (history._hasPushState && atRoot && loc.hash) {
  142. this.fragment = history.getHash().replace(routeStripper, '');
  143. this.history.replaceState({}, document.title, history.root + history.fragment + loc.search);
  144. }
  145. }
  146. if (!history.options.silent) {
  147. return history.loadUrl(options.startRoute);
  148. }
  149. };
  150. /**
  151. * Disable history, perhaps temporarily. Not useful in a real app, but possibly useful for unit testing Routers.
  152. * @method deactivate
  153. */
  154. history.deactivate = function() {
  155. $(window).off('popstate', history.checkUrl).off('hashchange', history.checkUrl);
  156. clearInterval(history._checkUrlInterval);
  157. history.active = false;
  158. };
  159. /**
  160. * Checks the current URL to see if it has changed, and if it has, calls `loadUrl`, normalizing across the hidden iframe.
  161. * @method checkUrl
  162. * @return {boolean} Returns true/false from loading the url.
  163. */
  164. history.checkUrl = function() {
  165. var current = history.getFragment();
  166. if (current === history.fragment && history.iframe) {
  167. current = history.getFragment(history.getHash(history.iframe));
  168. }
  169. if (current === history.fragment) {
  170. return false;
  171. }
  172. if (history.iframe) {
  173. history.navigate(current, false);
  174. }
  175. history.loadUrl();
  176. };
  177. /**
  178. * Attempts to load the current URL fragment. A pass-through to options.routeHandler.
  179. * @method loadUrl
  180. * @return {boolean} Returns true/false from the route handler.
  181. */
  182. history.loadUrl = function(fragmentOverride) {
  183. var fragment = history.fragment = history.getFragment(fragmentOverride);
  184. return history.options.routeHandler ?
  185. history.options.routeHandler(fragment) :
  186. false;
  187. };
  188. /**
  189. * Save a fragment into the hash history, or replace the URL state if the
  190. * 'replace' option is passed. You are responsible for properly URL-encoding
  191. * the fragment in advance.
  192. * The options object can contain `trigger: false` if you wish to not have the
  193. * route callback be fired, or `replace: true`, if
  194. * you wish to modify the current URL without adding an entry to the history.
  195. * @method navigate
  196. * @param {string} fragment The url fragment to navigate to.
  197. * @param {object|boolean} options An options object with optional trigger and replace flags. You can also pass a boolean directly to set the trigger option. Trigger is `true` by default.
  198. * @return {boolean} Returns true/false from loading the url.
  199. */
  200. history.navigate = function(fragment, options) {
  201. if (!history.active) {
  202. return false;
  203. }
  204. if(options === undefined) {
  205. options = {
  206. trigger: true
  207. };
  208. }else if(system.isBoolean(options)) {
  209. options = {
  210. trigger: options
  211. };
  212. }
  213. fragment = history.getFragment(fragment || '');
  214. if (history.fragment === fragment) {
  215. return;
  216. }
  217. history.fragment = fragment;
  218. var url = history.root + fragment;
  219. // Don't include a trailing slash on the root.
  220. if(fragment === '' && url !== '/') {
  221. url = url.slice(0, -1);
  222. }
  223. // If pushState is available, we use it to set the fragment as a real URL.
  224. if (history._hasPushState) {
  225. history.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, url);
  226. // If hash changes haven't been explicitly disabled, update the hash
  227. // fragment to store history.
  228. } else if (history._wantsHashChange) {
  229. updateHash(history.location, fragment, options.replace);
  230. if (history.iframe && (fragment !== history.getFragment(history.getHash(history.iframe)))) {
  231. // Opening and closing the iframe tricks IE7 and earlier to push a
  232. // history entry on hash-tag change. When replace is true, we don't
  233. // want history.
  234. if (!options.replace) {
  235. history.iframe.document.open().close();
  236. }
  237. updateHash(history.iframe.location, fragment, options.replace);
  238. }
  239. // If you've told us that you explicitly don't want fallback hashchange-
  240. // based history, then `navigate` becomes a page refresh.
  241. } else {
  242. return history.location.assign(url);
  243. }
  244. if (options.trigger) {
  245. return history.loadUrl(fragment);
  246. }
  247. };
  248. /**
  249. * Navigates back in the browser history.
  250. * @method navigateBack
  251. */
  252. history.navigateBack = function() {
  253. history.history.back();
  254. };
  255. /**
  256. * @class HistoryOptions
  257. * @static
  258. */
  259. /**
  260. * The function that will be called back when the fragment changes.
  261. * @property {function} routeHandler
  262. */
  263. /**
  264. * The url root used to extract the fragment when using push state.
  265. * @property {string} root
  266. */
  267. /**
  268. * Use hash change when present.
  269. * @property {boolean} hashChange
  270. * @default true
  271. */
  272. /**
  273. * Use push state when present.
  274. * @property {boolean} pushState
  275. * @default false
  276. */
  277. /**
  278. * Prevents loading of the current url when activating history.
  279. * @property {boolean} silent
  280. * @default false
  281. */
  282. return history;
  283. });