jquery.history.js 51 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121
  1. /**
  2. * History.js jQuery Adapter
  3. * @author Benjamin Arthur Lupton <contact@balupton.com>
  4. * @copyright 2010-2011 Benjamin Arthur Lupton <contact@balupton.com>
  5. * @license New BSD License <http://creativecommons.org/licenses/BSD/>
  6. */
  7. // Closure
  8. (function(window,undefined){
  9. "use strict";
  10. // Localise Globals
  11. var
  12. History = window.History = window.History||{},
  13. jQuery = window.jQuery;
  14. // Check Existence
  15. if ( typeof History.Adapter !== 'undefined' ) {
  16. throw new Error('History.js Adapter has already been loaded...');
  17. }
  18. // Add the Adapter
  19. History.Adapter = {
  20. /**
  21. * History.Adapter.bind(el,event,callback)
  22. * @param {Element|string} el
  23. * @param {string} event - custom and standard events
  24. * @param {function} callback
  25. * @return {void}
  26. */
  27. bind: function(el,event,callback){
  28. jQuery(el).bind(event,callback);
  29. },
  30. /**
  31. * History.Adapter.trigger(el,event)
  32. * @param {Element|string} el
  33. * @param {string} event - custom and standard events
  34. * @param {Object=} extra - a object of extra event data (optional)
  35. * @return {void}
  36. */
  37. trigger: function(el,event,extra){
  38. jQuery(el).trigger(event,extra);
  39. },
  40. /**
  41. * History.Adapter.extractEventData(key,event,extra)
  42. * @param {string} key - key for the event data to extract
  43. * @param {string} event - custom and standard events
  44. * @param {Object=} extra - a object of extra event data (optional)
  45. * @return {mixed}
  46. */
  47. extractEventData: function(key,event,extra){
  48. // jQuery Native then jQuery Custom
  49. var result = (event && event.originalEvent && event.originalEvent[key]) || (extra && extra[key]) || undefined;
  50. // Return
  51. return result;
  52. },
  53. /**
  54. * History.Adapter.onDomLoad(callback)
  55. * @param {function} callback
  56. * @return {void}
  57. */
  58. onDomLoad: function(callback) {
  59. jQuery(callback);
  60. }
  61. };
  62. // Try and Initialise History
  63. if ( typeof History.init !== 'undefined' ) {
  64. History.init();
  65. }
  66. })(window);
  67. /**
  68. * History.js Core
  69. * @author Benjamin Arthur Lupton <contact@balupton.com>
  70. * @copyright 2010-2011 Benjamin Arthur Lupton <contact@balupton.com>
  71. * @license New BSD License <http://creativecommons.org/licenses/BSD/>
  72. */
  73. (function(window,undefined){
  74. "use strict";
  75. // ========================================================================
  76. // Initialise
  77. // Localise Globals
  78. var
  79. console = window.console||undefined, // Prevent a JSLint complain
  80. document = window.document, // Make sure we are using the correct document
  81. navigator = window.navigator, // Make sure we are using the correct navigator
  82. sessionStorage = window.sessionStorage||false, // sessionStorage
  83. setTimeout = window.setTimeout,
  84. clearTimeout = window.clearTimeout,
  85. setInterval = window.setInterval,
  86. clearInterval = window.clearInterval,
  87. JSON = window.JSON,
  88. alert = window.alert,
  89. History = window.History = window.History||{}, // Public History Object
  90. history = window.history; // Old History Object
  91. try {
  92. sessionStorage.setItem('TEST', '1');
  93. sessionStorage.removeItem('TEST');
  94. } catch(e) {
  95. sessionStorage = false;
  96. }
  97. // MooTools Compatibility
  98. JSON.stringify = JSON.stringify||JSON.encode;
  99. JSON.parse = JSON.parse||JSON.decode;
  100. // Check Existence
  101. if ( typeof History.init !== 'undefined' ) {
  102. throw new Error('History.js Core has already been loaded...');
  103. }
  104. // Initialise History
  105. History.init = function(options){
  106. // Check Load Status of Adapter
  107. if ( typeof History.Adapter === 'undefined' ) {
  108. return false;
  109. }
  110. // Check Load Status of Core
  111. if ( typeof History.initCore !== 'undefined' ) {
  112. History.initCore();
  113. }
  114. // Check Load Status of HTML4 Support
  115. if ( typeof History.initHtml4 !== 'undefined' ) {
  116. History.initHtml4();
  117. }
  118. // Return true
  119. return true;
  120. };
  121. // ========================================================================
  122. // Initialise Core
  123. // Initialise Core
  124. History.initCore = function(options){
  125. // Initialise
  126. if ( typeof History.initCore.initialized !== 'undefined' ) {
  127. // Already Loaded
  128. return false;
  129. }
  130. else {
  131. History.initCore.initialized = true;
  132. }
  133. // ====================================================================
  134. // Options
  135. /**
  136. * History.options
  137. * Configurable options
  138. */
  139. History.options = History.options||{};
  140. /**
  141. * History.options.hashChangeInterval
  142. * How long should the interval be before hashchange checks
  143. */
  144. History.options.hashChangeInterval = History.options.hashChangeInterval || 100;
  145. /**
  146. * History.options.safariPollInterval
  147. * How long should the interval be before safari poll checks
  148. */
  149. History.options.safariPollInterval = History.options.safariPollInterval || 500;
  150. /**
  151. * History.options.doubleCheckInterval
  152. * How long should the interval be before we perform a double check
  153. */
  154. History.options.doubleCheckInterval = History.options.doubleCheckInterval || 500;
  155. /**
  156. * History.options.disableSuid
  157. * Force History not to append suid
  158. */
  159. History.options.disableSuid = History.options.disableSuid || false;
  160. /**
  161. * History.options.storeInterval
  162. * How long should we wait between store calls
  163. */
  164. History.options.storeInterval = History.options.storeInterval || 1000;
  165. /**
  166. * History.options.busyDelay
  167. * How long should we wait between busy events
  168. */
  169. History.options.busyDelay = History.options.busyDelay || 250;
  170. /**
  171. * History.options.debug
  172. * If true will enable debug messages to be logged
  173. */
  174. History.options.debug = History.options.debug || false;
  175. /**
  176. * History.options.initialTitle
  177. * What is the title of the initial state
  178. */
  179. History.options.initialTitle = History.options.initialTitle || document.title;
  180. /**
  181. * History.options.html4Mode
  182. * If true, will force HTMl4 mode (hashtags)
  183. */
  184. History.options.html4Mode = History.options.html4Mode || false;
  185. /**
  186. * History.options.delayInit
  187. * Want to override default options and call init manually.
  188. */
  189. History.options.delayInit = History.options.delayInit || false;
  190. // ====================================================================
  191. // Interval record
  192. /**
  193. * History.intervalList
  194. * List of intervals set, to be cleared when document is unloaded.
  195. */
  196. History.intervalList = [];
  197. /**
  198. * History.clearAllIntervals
  199. * Clears all setInterval instances.
  200. */
  201. History.clearAllIntervals = function(){
  202. var i, il = History.intervalList;
  203. if (typeof il !== "undefined" && il !== null) {
  204. for (i = 0; i < il.length; i++) {
  205. clearInterval(il[i]);
  206. }
  207. History.intervalList = null;
  208. }
  209. };
  210. // ====================================================================
  211. // Debug
  212. /**
  213. * History.debug(message,...)
  214. * Logs the passed arguments if debug enabled
  215. */
  216. History.debug = function(){
  217. if ( (History.options.debug||false) ) {
  218. History.log.apply(History,arguments);
  219. }
  220. };
  221. /**
  222. * History.log(message,...)
  223. * Logs the passed arguments
  224. */
  225. History.log = function(){
  226. // Prepare
  227. var
  228. consoleExists = !(typeof console === 'undefined' || typeof console.log === 'undefined' || typeof console.log.apply === 'undefined'),
  229. textarea = document.getElementById('log'),
  230. message,
  231. i,n,
  232. args,arg
  233. ;
  234. // Write to Console
  235. if ( consoleExists ) {
  236. args = Array.prototype.slice.call(arguments);
  237. message = args.shift();
  238. if ( typeof console.debug !== 'undefined' ) {
  239. console.debug.apply(console,[message,args]);
  240. }
  241. else {
  242. console.log.apply(console,[message,args]);
  243. }
  244. }
  245. else {
  246. message = ("\n"+arguments[0]+"\n");
  247. }
  248. // Write to log
  249. for ( i=1,n=arguments.length; i<n; ++i ) {
  250. arg = arguments[i];
  251. if ( typeof arg === 'object' && typeof JSON !== 'undefined' ) {
  252. try {
  253. arg = JSON.stringify(arg);
  254. }
  255. catch ( Exception ) {
  256. // Recursive Object
  257. }
  258. }
  259. message += "\n"+arg+"\n";
  260. }
  261. // Textarea
  262. if ( textarea ) {
  263. textarea.value += message+"\n-----\n";
  264. textarea.scrollTop = textarea.scrollHeight - textarea.clientHeight;
  265. }
  266. // No Textarea, No Console
  267. else if ( !consoleExists ) {
  268. alert(message);
  269. }
  270. // Return true
  271. return true;
  272. };
  273. // ====================================================================
  274. // Emulated Status
  275. /**
  276. * History.getInternetExplorerMajorVersion()
  277. * Get's the major version of Internet Explorer
  278. * @return {integer}
  279. * @license Public Domain
  280. * @author Benjamin Arthur Lupton <contact@balupton.com>
  281. * @author James Padolsey <https://gist.github.com/527683>
  282. */
  283. History.getInternetExplorerMajorVersion = function(){
  284. var result = History.getInternetExplorerMajorVersion.cached =
  285. (typeof History.getInternetExplorerMajorVersion.cached !== 'undefined')
  286. ? History.getInternetExplorerMajorVersion.cached
  287. : (function(){
  288. var v = 3,
  289. div = document.createElement('div'),
  290. all = div.getElementsByTagName('i');
  291. while ( (div.innerHTML = '<!--[if gt IE ' + (++v) + ']><i></i><![endif]-->') && all[0] ) {}
  292. return (v > 4) ? v : false;
  293. })()
  294. ;
  295. return result;
  296. };
  297. /**
  298. * History.isInternetExplorer()
  299. * Are we using Internet Explorer?
  300. * @return {boolean}
  301. * @license Public Domain
  302. * @author Benjamin Arthur Lupton <contact@balupton.com>
  303. */
  304. History.isInternetExplorer = function(){
  305. var result =
  306. History.isInternetExplorer.cached =
  307. (typeof History.isInternetExplorer.cached !== 'undefined')
  308. ? History.isInternetExplorer.cached
  309. : Boolean(History.getInternetExplorerMajorVersion())
  310. ;
  311. return result;
  312. };
  313. /**
  314. * History.emulated
  315. * Which features require emulating?
  316. */
  317. if (History.options.html4Mode) {
  318. History.emulated = {
  319. pushState : true,
  320. hashChange: true
  321. };
  322. }
  323. else {
  324. History.emulated = {
  325. pushState: !Boolean(
  326. window.history && window.history.pushState && window.history.replaceState
  327. && !(
  328. (/ Mobile\/([1-7][a-z]|(8([abcde]|f(1[0-8]))))/i).test(navigator.userAgent) /* disable for versions of iOS before version 4.3 (8F190) */
  329. || (/AppleWebKit\/5([0-2]|3[0-2])/i).test(navigator.userAgent) /* disable for the mercury iOS browser, or at least older versions of the webkit engine */
  330. )
  331. ),
  332. hashChange: Boolean(
  333. !(('onhashchange' in window) || ('onhashchange' in document))
  334. ||
  335. (History.isInternetExplorer() && History.getInternetExplorerMajorVersion() < 8)
  336. )
  337. };
  338. }
  339. /**
  340. * History.enabled
  341. * Is History enabled?
  342. */
  343. History.enabled = !History.emulated.pushState;
  344. /**
  345. * History.bugs
  346. * Which bugs are present
  347. */
  348. History.bugs = {
  349. /**
  350. * Safari 5 and Safari iOS 4 fail to return to the correct state once a hash is replaced by a `replaceState` call
  351. * https://bugs.webkit.org/show_bug.cgi?id=56249
  352. */
  353. setHash: Boolean(!History.emulated.pushState && navigator.vendor === 'Apple Computer, Inc.' && /AppleWebKit\/5([0-2]|3[0-3])/.test(navigator.userAgent)),
  354. /**
  355. * Safari 5 and Safari iOS 4 sometimes fail to apply the state change under busy conditions
  356. * https://bugs.webkit.org/show_bug.cgi?id=42940
  357. */
  358. safariPoll: Boolean(!History.emulated.pushState && navigator.vendor === 'Apple Computer, Inc.' && /AppleWebKit\/5([0-2]|3[0-3])/.test(navigator.userAgent)),
  359. /**
  360. * MSIE 6 and 7 sometimes do not apply a hash even it was told to (requiring a second call to the apply function)
  361. */
  362. ieDoubleCheck: Boolean(History.isInternetExplorer() && History.getInternetExplorerMajorVersion() < 8),
  363. /**
  364. * MSIE 6 requires the entire hash to be encoded for the hashes to trigger the onHashChange event
  365. */
  366. hashEscape: Boolean(History.isInternetExplorer() && History.getInternetExplorerMajorVersion() < 7)
  367. };
  368. /**
  369. * History.isEmptyObject(obj)
  370. * Checks to see if the Object is Empty
  371. * @param {Object} obj
  372. * @return {boolean}
  373. */
  374. History.isEmptyObject = function(obj) {
  375. for ( var name in obj ) {
  376. if ( obj.hasOwnProperty(name) ) {
  377. return false;
  378. }
  379. }
  380. return true;
  381. };
  382. /**
  383. * History.cloneObject(obj)
  384. * Clones a object and eliminate all references to the original contexts
  385. * @param {Object} obj
  386. * @return {Object}
  387. */
  388. History.cloneObject = function(obj) {
  389. var hash,newObj;
  390. if ( obj ) {
  391. hash = JSON.stringify(obj);
  392. newObj = JSON.parse(hash);
  393. }
  394. else {
  395. newObj = {};
  396. }
  397. return newObj;
  398. };
  399. // ====================================================================
  400. // URL Helpers
  401. /**
  402. * History.getRootUrl()
  403. * Turns "http://mysite.com/dir/page.html?asd" into "http://mysite.com"
  404. * @return {String} rootUrl
  405. */
  406. History.getRootUrl = function(){
  407. // Create
  408. var rootUrl = document.location.protocol+'//'+(document.location.hostname||document.location.host);
  409. if ( document.location.port||false ) {
  410. rootUrl += ':'+document.location.port;
  411. }
  412. rootUrl += '/';
  413. // Return
  414. return rootUrl;
  415. };
  416. /**
  417. * History.getBaseHref()
  418. * Fetches the `href` attribute of the `<base href="...">` element if it exists
  419. * @return {String} baseHref
  420. */
  421. History.getBaseHref = function(){
  422. // Create
  423. var
  424. baseElements = document.getElementsByTagName('base'),
  425. baseElement = null,
  426. baseHref = '';
  427. // Test for Base Element
  428. if ( baseElements.length === 1 ) {
  429. // Prepare for Base Element
  430. baseElement = baseElements[0];
  431. baseHref = baseElement.href.replace(/[^\/]+$/,'');
  432. }
  433. // Adjust trailing slash
  434. baseHref = baseHref.replace(/\/+$/,'');
  435. if ( baseHref ) baseHref += '/';
  436. // Return
  437. return baseHref;
  438. };
  439. /**
  440. * History.getBaseUrl()
  441. * Fetches the baseHref or basePageUrl or rootUrl (whichever one exists first)
  442. * @return {String} baseUrl
  443. */
  444. History.getBaseUrl = function(){
  445. // Create
  446. var baseUrl = History.getBaseHref()||History.getBasePageUrl()||History.getRootUrl();
  447. // Return
  448. return baseUrl;
  449. };
  450. /**
  451. * History.getPageUrl()
  452. * Fetches the URL of the current page
  453. * @return {String} pageUrl
  454. */
  455. History.getPageUrl = function(){
  456. // Fetch
  457. var
  458. State = History.getState(false,false),
  459. stateUrl = (State||{}).url||History.getLocationHref(),
  460. pageUrl;
  461. // Create
  462. pageUrl = stateUrl.replace(/\/+$/,'').replace(/[^\/]+$/,function(part,index,string){
  463. return (/\./).test(part) ? part : part+'/';
  464. });
  465. // Return
  466. return pageUrl;
  467. };
  468. /**
  469. * History.getBasePageUrl()
  470. * Fetches the Url of the directory of the current page
  471. * @return {String} basePageUrl
  472. */
  473. History.getBasePageUrl = function(){
  474. // Create
  475. var basePageUrl = (History.getLocationHref()).replace(/[#\?].*/,'').replace(/[^\/]+$/,function(part,index,string){
  476. return (/[^\/]$/).test(part) ? '' : part;
  477. }).replace(/\/+$/,'')+'/';
  478. // Return
  479. return basePageUrl;
  480. };
  481. /**
  482. * History.getFullUrl(url)
  483. * Ensures that we have an absolute URL and not a relative URL
  484. * @param {string} url
  485. * @param {Boolean} allowBaseHref
  486. * @return {string} fullUrl
  487. */
  488. History.getFullUrl = function(url,allowBaseHref){
  489. // Prepare
  490. var fullUrl = url, firstChar = url.substring(0,1);
  491. allowBaseHref = (typeof allowBaseHref === 'undefined') ? true : allowBaseHref;
  492. // Check
  493. if ( /[a-z]+\:\/\//.test(url) ) {
  494. // Full URL
  495. }
  496. else if ( firstChar === '/' ) {
  497. // Root URL
  498. fullUrl = History.getRootUrl()+url.replace(/^\/+/,'');
  499. }
  500. else if ( firstChar === '#' ) {
  501. // Anchor URL
  502. fullUrl = History.getPageUrl().replace(/#.*/,'')+url;
  503. }
  504. else if ( firstChar === '?' ) {
  505. // Query URL
  506. fullUrl = History.getPageUrl().replace(/[\?#].*/,'')+url;
  507. }
  508. else {
  509. // Relative URL
  510. if ( allowBaseHref ) {
  511. fullUrl = History.getBaseUrl()+url.replace(/^(\.\/)+/,'');
  512. } else {
  513. fullUrl = History.getBasePageUrl()+url.replace(/^(\.\/)+/,'');
  514. }
  515. // We have an if condition above as we do not want hashes
  516. // which are relative to the baseHref in our URLs
  517. // as if the baseHref changes, then all our bookmarks
  518. // would now point to different locations
  519. // whereas the basePageUrl will always stay the same
  520. }
  521. // Return
  522. return fullUrl.replace(/\#$/,'');
  523. };
  524. /**
  525. * History.getShortUrl(url)
  526. * Ensures that we have a relative URL and not a absolute URL
  527. * @param {string} url
  528. * @return {string} url
  529. */
  530. History.getShortUrl = function(url){
  531. // Prepare
  532. var shortUrl = url, baseUrl = History.getBaseUrl(), rootUrl = History.getRootUrl();
  533. // Trim baseUrl
  534. if ( History.emulated.pushState ) {
  535. // We are in a if statement as when pushState is not emulated
  536. // The actual url these short urls are relative to can change
  537. // So within the same session, we the url may end up somewhere different
  538. shortUrl = shortUrl.replace(baseUrl,'');
  539. }
  540. // Trim rootUrl
  541. shortUrl = shortUrl.replace(rootUrl,'/');
  542. // Ensure we can still detect it as a state
  543. if ( History.isTraditionalAnchor(shortUrl) ) {
  544. shortUrl = './'+shortUrl;
  545. }
  546. // Clean It
  547. shortUrl = shortUrl.replace(/^(\.\/)+/g,'./').replace(/\#$/,'');
  548. // Return
  549. return shortUrl;
  550. };
  551. /**
  552. * History.getLocationHref(document)
  553. * Returns a normalized version of document.location.href
  554. * accounting for browser inconsistencies, etc.
  555. *
  556. * This URL will be URI-encoded and will include the hash
  557. *
  558. * @param {object} document
  559. * @return {string} url
  560. */
  561. History.getLocationHref = function(doc) {
  562. doc = doc || document;
  563. // most of the time, this will be true
  564. if (doc.URL === doc.location.href)
  565. return doc.location.href;
  566. // some versions of webkit URI-decode document.location.href
  567. // but they leave document.URL in an encoded state
  568. if (doc.location.href === decodeURIComponent(doc.URL))
  569. return doc.URL;
  570. // FF 3.6 only updates document.URL when a page is reloaded
  571. // document.location.href is updated correctly
  572. if (doc.location.hash && decodeURIComponent(doc.location.href.replace(/^[^#]+/, "")) === doc.location.hash)
  573. return doc.location.href;
  574. if (doc.URL.indexOf('#') == -1 && doc.location.href.indexOf('#') != -1)
  575. return doc.location.href;
  576. return doc.URL || doc.location.href;
  577. };
  578. // ====================================================================
  579. // State Storage
  580. /**
  581. * History.store
  582. * The store for all session specific data
  583. */
  584. History.store = {};
  585. /**
  586. * History.idToState
  587. * 1-1: State ID to State Object
  588. */
  589. History.idToState = History.idToState||{};
  590. /**
  591. * History.stateToId
  592. * 1-1: State String to State ID
  593. */
  594. History.stateToId = History.stateToId||{};
  595. /**
  596. * History.urlToId
  597. * 1-1: State URL to State ID
  598. */
  599. History.urlToId = History.urlToId||{};
  600. /**
  601. * History.storedStates
  602. * Store the states in an array
  603. */
  604. History.storedStates = History.storedStates||[];
  605. /**
  606. * History.savedStates
  607. * Saved the states in an array
  608. */
  609. History.savedStates = History.savedStates||[];
  610. /**
  611. * History.noramlizeStore()
  612. * Noramlize the store by adding necessary values
  613. */
  614. History.normalizeStore = function(){
  615. History.store.idToState = History.store.idToState||{};
  616. History.store.urlToId = History.store.urlToId||{};
  617. History.store.stateToId = History.store.stateToId||{};
  618. };
  619. /**
  620. * History.getState()
  621. * Get an object containing the data, title and url of the current state
  622. * @param {Boolean} friendly
  623. * @param {Boolean} create
  624. * @return {Object} State
  625. */
  626. History.getState = function(friendly,create){
  627. // Prepare
  628. if ( typeof friendly === 'undefined' ) { friendly = true; }
  629. if ( typeof create === 'undefined' ) { create = true; }
  630. // Fetch
  631. var State = History.getLastSavedState();
  632. // Create
  633. if ( !State && create ) {
  634. State = History.createStateObject();
  635. }
  636. // Adjust
  637. if ( friendly ) {
  638. State = History.cloneObject(State);
  639. State.url = State.cleanUrl||State.url;
  640. }
  641. // Return
  642. return State;
  643. };
  644. /**
  645. * History.getIdByState(State)
  646. * Gets a ID for a State
  647. * @param {State} newState
  648. * @return {String} id
  649. */
  650. History.getIdByState = function(newState){
  651. // Fetch ID
  652. var id = History.extractId(newState.url),
  653. str;
  654. if ( !id ) {
  655. // Find ID via State String
  656. str = History.getStateString(newState);
  657. if ( typeof History.stateToId[str] !== 'undefined' ) {
  658. id = History.stateToId[str];
  659. }
  660. else if ( typeof History.store.stateToId[str] !== 'undefined' ) {
  661. id = History.store.stateToId[str];
  662. }
  663. else {
  664. // Generate a new ID
  665. while ( true ) {
  666. id = (new Date()).getTime() + String(Math.random()).replace(/\D/g,'');
  667. if ( typeof History.idToState[id] === 'undefined' && typeof History.store.idToState[id] === 'undefined' ) {
  668. break;
  669. }
  670. }
  671. // Apply the new State to the ID
  672. History.stateToId[str] = id;
  673. History.idToState[id] = newState;
  674. }
  675. }
  676. // Return ID
  677. return id;
  678. };
  679. /**
  680. * History.normalizeState(State)
  681. * Expands a State Object
  682. * @param {object} State
  683. * @return {object}
  684. */
  685. History.normalizeState = function(oldState){
  686. // Variables
  687. var newState, dataNotEmpty;
  688. // Prepare
  689. if ( !oldState || (typeof oldState !== 'object') ) {
  690. oldState = {};
  691. }
  692. // Check
  693. if ( typeof oldState.normalized !== 'undefined' ) {
  694. return oldState;
  695. }
  696. // Adjust
  697. if ( !oldState.data || (typeof oldState.data !== 'object') ) {
  698. oldState.data = {};
  699. }
  700. // ----------------------------------------------------------------
  701. // Create
  702. newState = {};
  703. newState.normalized = true;
  704. newState.title = oldState.title||'';
  705. newState.url = History.getFullUrl(oldState.url?oldState.url:(History.getLocationHref()));
  706. newState.hash = History.getShortUrl(newState.url);
  707. newState.data = History.cloneObject(oldState.data);
  708. // Fetch ID
  709. newState.id = History.getIdByState(newState);
  710. // ----------------------------------------------------------------
  711. // Clean the URL
  712. newState.cleanUrl = newState.url.replace(/\??\&_suid.*/,'');
  713. newState.url = newState.cleanUrl;
  714. // Check to see if we have more than just a url
  715. dataNotEmpty = !History.isEmptyObject(newState.data);
  716. // Apply
  717. if ( (newState.title || dataNotEmpty) && History.options.disableSuid !== true ) {
  718. // Add ID to Hash
  719. newState.hash = History.getShortUrl(newState.url).replace(/\??\&_suid.*/,'');
  720. if ( !/\?/.test(newState.hash) ) {
  721. newState.hash += '?';
  722. }
  723. newState.hash += '&_suid='+newState.id;
  724. }
  725. // Create the Hashed URL
  726. newState.hashedUrl = History.getFullUrl(newState.hash);
  727. // ----------------------------------------------------------------
  728. // Update the URL if we have a duplicate
  729. if ( (History.emulated.pushState || History.bugs.safariPoll) && History.hasUrlDuplicate(newState) ) {
  730. newState.url = newState.hashedUrl;
  731. }
  732. // ----------------------------------------------------------------
  733. // Return
  734. return newState;
  735. };
  736. /**
  737. * History.createStateObject(data,title,url)
  738. * Creates a object based on the data, title and url state params
  739. * @param {object} data
  740. * @param {string} title
  741. * @param {string} url
  742. * @return {object}
  743. */
  744. History.createStateObject = function(data,title,url){
  745. // Hashify
  746. var State = {
  747. 'data': data,
  748. 'title': title,
  749. 'url': url
  750. };
  751. // Expand the State
  752. State = History.normalizeState(State);
  753. // Return object
  754. return State;
  755. };
  756. /**
  757. * History.getStateById(id)
  758. * Get a state by it's UID
  759. * @param {String} id
  760. */
  761. History.getStateById = function(id){
  762. // Prepare
  763. id = String(id);
  764. // Retrieve
  765. var State = History.idToState[id] || History.store.idToState[id] || undefined;
  766. // Return State
  767. return State;
  768. };
  769. /**
  770. * Get a State's String
  771. * @param {State} passedState
  772. */
  773. History.getStateString = function(passedState){
  774. // Prepare
  775. var State, cleanedState, str;
  776. // Fetch
  777. State = History.normalizeState(passedState);
  778. // Clean
  779. cleanedState = {
  780. data: State.data,
  781. title: passedState.title,
  782. url: passedState.url
  783. };
  784. // Fetch
  785. str = JSON.stringify(cleanedState);
  786. // Return
  787. return str;
  788. };
  789. /**
  790. * Get a State's ID
  791. * @param {State} passedState
  792. * @return {String} id
  793. */
  794. History.getStateId = function(passedState){
  795. // Prepare
  796. var State, id;
  797. // Fetch
  798. State = History.normalizeState(passedState);
  799. // Fetch
  800. id = State.id;
  801. // Return
  802. return id;
  803. };
  804. /**
  805. * History.getHashByState(State)
  806. * Creates a Hash for the State Object
  807. * @param {State} passedState
  808. * @return {String} hash
  809. */
  810. History.getHashByState = function(passedState){
  811. // Prepare
  812. var State, hash;
  813. // Fetch
  814. State = History.normalizeState(passedState);
  815. // Hash
  816. hash = State.hash;
  817. // Return
  818. return hash;
  819. };
  820. /**
  821. * History.extractId(url_or_hash)
  822. * Get a State ID by it's URL or Hash
  823. * @param {string} url_or_hash
  824. * @return {string} id
  825. */
  826. History.extractId = function ( url_or_hash ) {
  827. // Prepare
  828. var id,parts,url, tmp;
  829. // Extract
  830. // If the URL has a #, use the id from before the #
  831. if (url_or_hash.indexOf('#') != -1)
  832. {
  833. tmp = url_or_hash.split("#")[0];
  834. }
  835. else
  836. {
  837. tmp = url_or_hash;
  838. }
  839. parts = /(.*)\&_suid=([0-9]+)$/.exec(tmp);
  840. url = parts ? (parts[1]||url_or_hash) : url_or_hash;
  841. id = parts ? String(parts[2]||'') : '';
  842. // Return
  843. return id||false;
  844. };
  845. /**
  846. * History.isTraditionalAnchor
  847. * Checks to see if the url is a traditional anchor or not
  848. * @param {String} url_or_hash
  849. * @return {Boolean}
  850. */
  851. History.isTraditionalAnchor = function(url_or_hash){
  852. // Check
  853. var isTraditional = !(/[\/\?\.]/.test(url_or_hash));
  854. // Return
  855. return isTraditional;
  856. };
  857. /**
  858. * History.extractState
  859. * Get a State by it's URL or Hash
  860. * @param {String} url_or_hash
  861. * @return {State|null}
  862. */
  863. History.extractState = function(url_or_hash,create){
  864. // Prepare
  865. var State = null, id, url;
  866. create = create||false;
  867. // Fetch SUID
  868. id = History.extractId(url_or_hash);
  869. if ( id ) {
  870. State = History.getStateById(id);
  871. }
  872. // Fetch SUID returned no State
  873. if ( !State ) {
  874. // Fetch URL
  875. url = History.getFullUrl(url_or_hash);
  876. // Check URL
  877. id = History.getIdByUrl(url)||false;
  878. if ( id ) {
  879. State = History.getStateById(id);
  880. }
  881. // Create State
  882. if ( !State && create && !History.isTraditionalAnchor(url_or_hash) ) {
  883. State = History.createStateObject(null,null,url);
  884. }
  885. }
  886. // Return
  887. return State;
  888. };
  889. /**
  890. * History.getIdByUrl()
  891. * Get a State ID by a State URL
  892. */
  893. History.getIdByUrl = function(url){
  894. // Fetch
  895. var id = History.urlToId[url] || History.store.urlToId[url] || undefined;
  896. // Return
  897. return id;
  898. };
  899. /**
  900. * History.getLastSavedState()
  901. * Get an object containing the data, title and url of the current state
  902. * @return {Object} State
  903. */
  904. History.getLastSavedState = function(){
  905. return History.savedStates[History.savedStates.length-1]||undefined;
  906. };
  907. /**
  908. * History.getLastStoredState()
  909. * Get an object containing the data, title and url of the current state
  910. * @return {Object} State
  911. */
  912. History.getLastStoredState = function(){
  913. return History.storedStates[History.storedStates.length-1]||undefined;
  914. };
  915. /**
  916. * History.hasUrlDuplicate
  917. * Checks if a Url will have a url conflict
  918. * @param {Object} newState
  919. * @return {Boolean} hasDuplicate
  920. */
  921. History.hasUrlDuplicate = function(newState) {
  922. // Prepare
  923. var hasDuplicate = false,
  924. oldState;
  925. // Fetch
  926. oldState = History.extractState(newState.url);
  927. // Check
  928. hasDuplicate = oldState && oldState.id !== newState.id;
  929. // Return
  930. return hasDuplicate;
  931. };
  932. /**
  933. * History.storeState
  934. * Store a State
  935. * @param {Object} newState
  936. * @return {Object} newState
  937. */
  938. History.storeState = function(newState){
  939. // Store the State
  940. History.urlToId[newState.url] = newState.id;
  941. // Push the State
  942. History.storedStates.push(History.cloneObject(newState));
  943. // Return newState
  944. return newState;
  945. };
  946. /**
  947. * History.isLastSavedState(newState)
  948. * Tests to see if the state is the last state
  949. * @param {Object} newState
  950. * @return {boolean} isLast
  951. */
  952. History.isLastSavedState = function(newState){
  953. // Prepare
  954. var isLast = false,
  955. newId, oldState, oldId;
  956. // Check
  957. if ( History.savedStates.length ) {
  958. newId = newState.id;
  959. oldState = History.getLastSavedState();
  960. oldId = oldState.id;
  961. // Check
  962. isLast = (newId === oldId);
  963. }
  964. // Return
  965. return isLast;
  966. };
  967. /**
  968. * History.saveState
  969. * Push a State
  970. * @param {Object} newState
  971. * @return {boolean} changed
  972. */
  973. History.saveState = function(newState){
  974. // Check Hash
  975. if ( History.isLastSavedState(newState) ) {
  976. return false;
  977. }
  978. // Push the State
  979. History.savedStates.push(History.cloneObject(newState));
  980. // Return true
  981. return true;
  982. };
  983. /**
  984. * History.getStateByIndex()
  985. * Gets a state by the index
  986. * @param {integer} index
  987. * @return {Object}
  988. */
  989. History.getStateByIndex = function(index){
  990. // Prepare
  991. var State = null;
  992. // Handle
  993. if ( typeof index === 'undefined' ) {
  994. // Get the last inserted
  995. State = History.savedStates[History.savedStates.length-1];
  996. }
  997. else if ( index < 0 ) {
  998. // Get from the end
  999. State = History.savedStates[History.savedStates.length+index];
  1000. }
  1001. else {
  1002. // Get from the beginning
  1003. State = History.savedStates[index];
  1004. }
  1005. // Return State
  1006. return State;
  1007. };
  1008. /**
  1009. * History.getCurrentIndex()
  1010. * Gets the current index
  1011. * @return (integer)
  1012. */
  1013. History.getCurrentIndex = function(){
  1014. // Prepare
  1015. var index = null;
  1016. // No states saved
  1017. if(History.savedStates.length < 1) {
  1018. index = 0;
  1019. }
  1020. else {
  1021. index = History.savedStates.length-1;
  1022. }
  1023. return index;
  1024. };
  1025. // ====================================================================
  1026. // Hash Helpers
  1027. /**
  1028. * History.getHash()
  1029. * @param {Location=} location
  1030. * Gets the current document hash
  1031. * Note: unlike location.hash, this is guaranteed to return the escaped hash in all browsers
  1032. * @return {string}
  1033. */
  1034. History.getHash = function(doc){
  1035. var url = History.getLocationHref(doc),
  1036. hash;
  1037. hash = History.getHashByUrl(url);
  1038. return hash;
  1039. };
  1040. /**
  1041. * History.unescapeHash()
  1042. * normalize and Unescape a Hash
  1043. * @param {String} hash
  1044. * @return {string}
  1045. */
  1046. History.unescapeHash = function(hash){
  1047. // Prepare
  1048. var result = History.normalizeHash(hash);
  1049. // Unescape hash
  1050. result = decodeURIComponent(result);
  1051. // Return result
  1052. return result;
  1053. };
  1054. /**
  1055. * History.normalizeHash()
  1056. * normalize a hash across browsers
  1057. * @return {string}
  1058. */
  1059. History.normalizeHash = function(hash){
  1060. // Prepare
  1061. var result = hash.replace(/[^#]*#/,'').replace(/#.*/, '');
  1062. // Return result
  1063. return result;
  1064. };
  1065. /**
  1066. * History.setHash(hash)
  1067. * Sets the document hash
  1068. * @param {string} hash
  1069. * @return {History}
  1070. */
  1071. History.setHash = function(hash,queue){
  1072. // Prepare
  1073. var State, pageUrl;
  1074. // Handle Queueing
  1075. if ( queue !== false && History.busy() ) {
  1076. // Wait + Push to Queue
  1077. //History.debug('History.setHash: we must wait', arguments);
  1078. History.pushQueue({
  1079. scope: History,
  1080. callback: History.setHash,
  1081. args: arguments,
  1082. queue: queue
  1083. });
  1084. return false;
  1085. }
  1086. // Log
  1087. //History.debug('History.setHash: called',hash);
  1088. // Make Busy + Continue
  1089. History.busy(true);
  1090. // Check if hash is a state
  1091. State = History.extractState(hash,true);
  1092. if ( State && !History.emulated.pushState ) {
  1093. // Hash is a state so skip the setHash
  1094. //History.debug('History.setHash: Hash is a state so skipping the hash set with a direct pushState call',arguments);
  1095. // PushState
  1096. History.pushState(State.data,State.title,State.url,false);
  1097. }
  1098. else if ( History.getHash() !== hash ) {
  1099. // Hash is a proper hash, so apply it
  1100. // Handle browser bugs
  1101. if ( History.bugs.setHash ) {
  1102. // Fix Safari Bug https://bugs.webkit.org/show_bug.cgi?id=56249
  1103. // Fetch the base page
  1104. pageUrl = History.getPageUrl();
  1105. // Safari hash apply
  1106. History.pushState(null,null,pageUrl+'#'+hash,false);
  1107. }
  1108. else {
  1109. // Normal hash apply
  1110. document.location.hash = hash;
  1111. }
  1112. }
  1113. // Chain
  1114. return History;
  1115. };
  1116. /**
  1117. * History.escape()
  1118. * normalize and Escape a Hash
  1119. * @return {string}
  1120. */
  1121. History.escapeHash = function(hash){
  1122. // Prepare
  1123. var result = History.normalizeHash(hash);
  1124. // Escape hash
  1125. result = window.encodeURIComponent(result);
  1126. // IE6 Escape Bug
  1127. if ( !History.bugs.hashEscape ) {
  1128. // Restore common parts
  1129. result = result
  1130. .replace(/\%21/g,'!')
  1131. .replace(/\%26/g,'&')
  1132. .replace(/\%3D/g,'=')
  1133. .replace(/\%3F/g,'?');
  1134. }
  1135. // Return result
  1136. return result;
  1137. };
  1138. /**
  1139. * History.getHashByUrl(url)
  1140. * Extracts the Hash from a URL
  1141. * @param {string} url
  1142. * @return {string} url
  1143. */
  1144. History.getHashByUrl = function(url){
  1145. // Extract the hash
  1146. var hash = String(url)
  1147. .replace(/([^#]*)#?([^#]*)#?(.*)/, '$2')
  1148. ;
  1149. // Unescape hash
  1150. hash = History.unescapeHash(hash);
  1151. // Return hash
  1152. return hash;
  1153. };
  1154. /**
  1155. * History.setTitle(title)
  1156. * Applies the title to the document
  1157. * @param {State} newState
  1158. * @return {Boolean}
  1159. */
  1160. History.setTitle = function(newState){
  1161. // Prepare
  1162. var title = newState.title,
  1163. firstState;
  1164. // Initial
  1165. if ( !title ) {
  1166. firstState = History.getStateByIndex(0);
  1167. if ( firstState && firstState.url === newState.url ) {
  1168. title = firstState.title||History.options.initialTitle;
  1169. }
  1170. }
  1171. // Apply
  1172. try {
  1173. document.getElementsByTagName('title')[0].innerHTML = title.replace('<','&lt;').replace('>','&gt;').replace(' & ',' &amp; ');
  1174. }
  1175. catch ( Exception ) { }
  1176. document.title = title;
  1177. // Chain
  1178. return History;
  1179. };
  1180. // ====================================================================
  1181. // Queueing
  1182. /**
  1183. * History.queues
  1184. * The list of queues to use
  1185. * First In, First Out
  1186. */
  1187. History.queues = [];
  1188. /**
  1189. * History.busy(value)
  1190. * @param {boolean} value [optional]
  1191. * @return {boolean} busy
  1192. */
  1193. History.busy = function(value){
  1194. // Apply
  1195. if ( typeof value !== 'undefined' ) {
  1196. //History.debug('History.busy: changing ['+(History.busy.flag||false)+'] to ['+(value||false)+']', History.queues.length);
  1197. History.busy.flag = value;
  1198. }
  1199. // Default
  1200. else if ( typeof History.busy.flag === 'undefined' ) {
  1201. History.busy.flag = false;
  1202. }
  1203. // Queue
  1204. if ( !History.busy.flag ) {
  1205. // Execute the next item in the queue
  1206. clearTimeout(History.busy.timeout);
  1207. var fireNext = function(){
  1208. var i, queue, item;
  1209. if ( History.busy.flag ) return;
  1210. for ( i=History.queues.length-1; i >= 0; --i ) {
  1211. queue = History.queues[i];
  1212. if ( queue.length === 0 ) continue;
  1213. item = queue.shift();
  1214. History.fireQueueItem(item);
  1215. History.busy.timeout = setTimeout(fireNext,History.options.busyDelay);
  1216. }
  1217. };
  1218. History.busy.timeout = setTimeout(fireNext,History.options.busyDelay);
  1219. }
  1220. // Return
  1221. return History.busy.flag;
  1222. };
  1223. /**
  1224. * History.busy.flag
  1225. */
  1226. History.busy.flag = false;
  1227. /**
  1228. * History.fireQueueItem(item)
  1229. * Fire a Queue Item
  1230. * @param {Object} item
  1231. * @return {Mixed} result
  1232. */
  1233. History.fireQueueItem = function(item){
  1234. return item.callback.apply(item.scope||History,item.args||[]);
  1235. };
  1236. /**
  1237. * History.pushQueue(callback,args)
  1238. * Add an item to the queue
  1239. * @param {Object} item [scope,callback,args,queue]
  1240. */
  1241. History.pushQueue = function(item){
  1242. // Prepare the queue
  1243. History.queues[item.queue||0] = History.queues[item.queue||0]||[];
  1244. // Add to the queue
  1245. History.queues[item.queue||0].push(item);
  1246. // Chain
  1247. return History;
  1248. };
  1249. /**
  1250. * History.queue (item,queue), (func,queue), (func), (item)
  1251. * Either firs the item now if not busy, or adds it to the queue
  1252. */
  1253. History.queue = function(item,queue){
  1254. // Prepare
  1255. if ( typeof item === 'function' ) {
  1256. item = {
  1257. callback: item
  1258. };
  1259. }
  1260. if ( typeof queue !== 'undefined' ) {
  1261. item.queue = queue;
  1262. }
  1263. // Handle
  1264. if ( History.busy() ) {
  1265. History.pushQueue(item);
  1266. } else {
  1267. History.fireQueueItem(item);
  1268. }
  1269. // Chain
  1270. return History;
  1271. };
  1272. /**
  1273. * History.clearQueue()
  1274. * Clears the Queue
  1275. */
  1276. History.clearQueue = function(){
  1277. History.busy.flag = false;
  1278. History.queues = [];
  1279. return History;
  1280. };
  1281. // ====================================================================
  1282. // IE Bug Fix
  1283. /**
  1284. * History.stateChanged
  1285. * States whether or not the state has changed since the last double check was initialised
  1286. */
  1287. History.stateChanged = false;
  1288. /**
  1289. * History.doubleChecker
  1290. * Contains the timeout used for the double checks
  1291. */
  1292. History.doubleChecker = false;
  1293. /**
  1294. * History.doubleCheckComplete()
  1295. * Complete a double check
  1296. * @return {History}
  1297. */
  1298. History.doubleCheckComplete = function(){
  1299. // Update
  1300. History.stateChanged = true;
  1301. // Clear
  1302. History.doubleCheckClear();
  1303. // Chain
  1304. return History;
  1305. };
  1306. /**
  1307. * History.doubleCheckClear()
  1308. * Clear a double check
  1309. * @return {History}
  1310. */
  1311. History.doubleCheckClear = function(){
  1312. // Clear
  1313. if ( History.doubleChecker ) {
  1314. clearTimeout(History.doubleChecker);
  1315. History.doubleChecker = false;
  1316. }
  1317. // Chain
  1318. return History;
  1319. };
  1320. /**
  1321. * History.doubleCheck()
  1322. * Create a double check
  1323. * @return {History}
  1324. */
  1325. History.doubleCheck = function(tryAgain){
  1326. // Reset
  1327. History.stateChanged = false;
  1328. History.doubleCheckClear();
  1329. // Fix IE6,IE7 bug where calling history.back or history.forward does not actually change the hash (whereas doing it manually does)
  1330. // Fix Safari 5 bug where sometimes the state does not change: https://bugs.webkit.org/show_bug.cgi?id=42940
  1331. if ( History.bugs.ieDoubleCheck ) {
  1332. // Apply Check
  1333. History.doubleChecker = setTimeout(
  1334. function(){
  1335. History.doubleCheckClear();
  1336. if ( !History.stateChanged ) {
  1337. //History.debug('History.doubleCheck: State has not yet changed, trying again', arguments);
  1338. // Re-Attempt
  1339. tryAgain();
  1340. }
  1341. return true;
  1342. },
  1343. History.options.doubleCheckInterval
  1344. );
  1345. }
  1346. // Chain
  1347. return History;
  1348. };
  1349. // ====================================================================
  1350. // Safari Bug Fix
  1351. /**
  1352. * History.safariStatePoll()
  1353. * Poll the current state
  1354. * @return {History}
  1355. */
  1356. History.safariStatePoll = function(){
  1357. // Poll the URL
  1358. // Get the Last State which has the new URL
  1359. var
  1360. urlState = History.extractState(History.getLocationHref()),
  1361. newState;
  1362. // Check for a difference
  1363. if ( !History.isLastSavedState(urlState) ) {
  1364. newState = urlState;
  1365. }
  1366. else {
  1367. return;
  1368. }
  1369. // Check if we have a state with that url
  1370. // If not create it
  1371. if ( !newState ) {
  1372. //History.debug('History.safariStatePoll: new');
  1373. newState = History.createStateObject();
  1374. }
  1375. // Apply the New State
  1376. //History.debug('History.safariStatePoll: trigger');
  1377. History.Adapter.trigger(window,'popstate');
  1378. // Chain
  1379. return History;
  1380. };
  1381. // ====================================================================
  1382. // State Aliases
  1383. /**
  1384. * History.back(queue)
  1385. * Send the browser history back one item
  1386. * @param {Integer} queue [optional]
  1387. */
  1388. History.back = function(queue){
  1389. //History.debug('History.back: called', arguments);
  1390. // Handle Queueing
  1391. if ( queue !== false && History.busy() ) {
  1392. // Wait + Push to Queue
  1393. //History.debug('History.back: we must wait', arguments);
  1394. History.pushQueue({
  1395. scope: History,
  1396. callback: History.back,
  1397. args: arguments,
  1398. queue: queue
  1399. });
  1400. return false;
  1401. }
  1402. // Make Busy + Continue
  1403. History.busy(true);
  1404. // Fix certain browser bugs that prevent the state from changing
  1405. History.doubleCheck(function(){
  1406. History.back(false);
  1407. });
  1408. // Go back
  1409. history.go(-1);
  1410. // End back closure
  1411. return true;
  1412. };
  1413. /**
  1414. * History.forward(queue)
  1415. * Send the browser history forward one item
  1416. * @param {Integer} queue [optional]
  1417. */
  1418. History.forward = function(queue){
  1419. //History.debug('History.forward: called', arguments);
  1420. // Handle Queueing
  1421. if ( queue !== false && History.busy() ) {
  1422. // Wait + Push to Queue
  1423. //History.debug('History.forward: we must wait', arguments);
  1424. History.pushQueue({
  1425. scope: History,
  1426. callback: History.forward,
  1427. args: arguments,
  1428. queue: queue
  1429. });
  1430. return false;
  1431. }
  1432. // Make Busy + Continue
  1433. History.busy(true);
  1434. // Fix certain browser bugs that prevent the state from changing
  1435. History.doubleCheck(function(){
  1436. History.forward(false);
  1437. });
  1438. // Go forward
  1439. history.go(1);
  1440. // End forward closure
  1441. return true;
  1442. };
  1443. /**
  1444. * History.go(index,queue)
  1445. * Send the browser history back or forward index times
  1446. * @param {Integer} queue [optional]
  1447. */
  1448. History.go = function(index,queue){
  1449. //History.debug('History.go: called', arguments);
  1450. // Prepare
  1451. var i;
  1452. // Handle
  1453. if ( index > 0 ) {
  1454. // Forward
  1455. for ( i=1; i<=index; ++i ) {
  1456. History.forward(queue);
  1457. }
  1458. }
  1459. else if ( index < 0 ) {
  1460. // Backward
  1461. for ( i=-1; i>=index; --i ) {
  1462. History.back(queue);
  1463. }
  1464. }
  1465. else {
  1466. throw new Error('History.go: History.go requires a positive or negative integer passed.');
  1467. }
  1468. // Chain
  1469. return History;
  1470. };
  1471. // ====================================================================
  1472. // HTML5 State Support
  1473. // Non-Native pushState Implementation
  1474. if ( History.emulated.pushState ) {
  1475. /*
  1476. * Provide Skeleton for HTML4 Browsers
  1477. */
  1478. // Prepare
  1479. var emptyFunction = function(){};
  1480. History.pushState = History.pushState||emptyFunction;
  1481. History.replaceState = History.replaceState||emptyFunction;
  1482. } // History.emulated.pushState
  1483. // Native pushState Implementation
  1484. else {
  1485. /*
  1486. * Use native HTML5 History API Implementation
  1487. */
  1488. /**
  1489. * History.onPopState(event,extra)
  1490. * Refresh the Current State
  1491. */
  1492. History.onPopState = function(event,extra){
  1493. // Prepare
  1494. var stateId = false, newState = false, currentHash, currentState;
  1495. // Reset the double check
  1496. History.doubleCheckComplete();
  1497. // Check for a Hash, and handle apporiatly
  1498. currentHash = History.getHash();
  1499. if ( currentHash ) {
  1500. // Expand Hash
  1501. currentState = History.extractState(currentHash||History.getLocationHref(),true);
  1502. if ( currentState ) {
  1503. // We were able to parse it, it must be a State!
  1504. // Let's forward to replaceState
  1505. //History.debug('History.onPopState: state anchor', currentHash, currentState);
  1506. History.replaceState(currentState.data, currentState.title, currentState.url, false);
  1507. }
  1508. else {
  1509. // Traditional Anchor
  1510. //History.debug('History.onPopState: traditional anchor', currentHash);
  1511. History.Adapter.trigger(window,'anchorchange');
  1512. History.busy(false);
  1513. }
  1514. // We don't care for hashes
  1515. History.expectedStateId = false;
  1516. return false;
  1517. }
  1518. // Ensure
  1519. stateId = History.Adapter.extractEventData('state',event,extra) || false;
  1520. // Fetch State
  1521. if ( stateId ) {
  1522. // Vanilla: Back/forward button was used
  1523. newState = History.getStateById(stateId);
  1524. }
  1525. else if ( History.expectedStateId ) {
  1526. // Vanilla: A new state was pushed, and popstate was called manually
  1527. newState = History.getStateById(History.expectedStateId);
  1528. }
  1529. else {
  1530. // Initial State
  1531. newState = History.extractState(History.getLocationHref());
  1532. }
  1533. // The State did not exist in our store
  1534. if ( !newState ) {
  1535. // Regenerate the State
  1536. newState = History.createStateObject(null,null,History.getLocationHref());
  1537. }
  1538. // Clean
  1539. History.expectedStateId = false;
  1540. // Check if we are the same state
  1541. if ( History.isLastSavedState(newState) ) {
  1542. // There has been no change (just the page's hash has finally propagated)
  1543. //History.debug('History.onPopState: no change', newState, History.savedStates);
  1544. History.busy(false);
  1545. return false;
  1546. }
  1547. // Store the State
  1548. History.storeState(newState);
  1549. History.saveState(newState);
  1550. // Force update of the title
  1551. History.setTitle(newState);
  1552. // Fire Our Event
  1553. History.Adapter.trigger(window,'statechange');
  1554. History.busy(false);
  1555. // Return true
  1556. return true;
  1557. };
  1558. History.Adapter.bind(window,'popstate',History.onPopState);
  1559. /**
  1560. * History.pushState(data,title,url)
  1561. * Add a new State to the history object, become it, and trigger onpopstate
  1562. * We have to trigger for HTML4 compatibility
  1563. * @param {object} data
  1564. * @param {string} title
  1565. * @param {string} url
  1566. * @return {true}
  1567. */
  1568. History.pushState = function(data,title,url,queue){
  1569. //History.debug('History.pushState: called', arguments);
  1570. // Check the State
  1571. if ( History.getHashByUrl(url) && History.emulated.pushState ) {
  1572. throw new Error('History.js does not support states with fragement-identifiers (hashes/anchors).');
  1573. }
  1574. // Handle Queueing
  1575. if ( queue !== false && History.busy() ) {
  1576. // Wait + Push to Queue
  1577. //History.debug('History.pushState: we must wait', arguments);
  1578. History.pushQueue({
  1579. scope: History,
  1580. callback: History.pushState,
  1581. args: arguments,
  1582. queue: queue
  1583. });
  1584. return false;
  1585. }
  1586. // Make Busy + Continue
  1587. History.busy(true);
  1588. // Create the newState
  1589. var newState = History.createStateObject(data,title,url);
  1590. // Check it
  1591. if ( History.isLastSavedState(newState) ) {
  1592. // Won't be a change
  1593. History.busy(false);
  1594. }
  1595. else {
  1596. // Store the newState
  1597. History.storeState(newState);
  1598. History.expectedStateId = newState.id;
  1599. // Push the newState
  1600. history.pushState(newState.id,newState.title,newState.url);
  1601. // Fire HTML5 Event
  1602. History.Adapter.trigger(window,'popstate');
  1603. }
  1604. // End pushState closure
  1605. return true;
  1606. };
  1607. /**
  1608. * History.replaceState(data,title,url)
  1609. * Replace the State and trigger onpopstate
  1610. * We have to trigger for HTML4 compatibility
  1611. * @param {object} data
  1612. * @param {string} title
  1613. * @param {string} url
  1614. * @return {true}
  1615. */
  1616. History.replaceState = function(data,title,url,queue){
  1617. //History.debug('History.replaceState: called', arguments);
  1618. // Check the State
  1619. if ( History.getHashByUrl(url) && History.emulated.pushState ) {
  1620. throw new Error('History.js does not support states with fragement-identifiers (hashes/anchors).');
  1621. }
  1622. // Handle Queueing
  1623. if ( queue !== false && History.busy() ) {
  1624. // Wait + Push to Queue
  1625. //History.debug('History.replaceState: we must wait', arguments);
  1626. History.pushQueue({
  1627. scope: History,
  1628. callback: History.replaceState,
  1629. args: arguments,
  1630. queue: queue
  1631. });
  1632. return false;
  1633. }
  1634. // Make Busy + Continue
  1635. History.busy(true);
  1636. // Create the newState
  1637. var newState = History.createStateObject(data,title,url);
  1638. // Check it
  1639. if ( History.isLastSavedState(newState) ) {
  1640. // Won't be a change
  1641. History.busy(false);
  1642. }
  1643. else {
  1644. // Store the newState
  1645. History.storeState(newState);
  1646. History.expectedStateId = newState.id;
  1647. // Push the newState
  1648. history.replaceState(newState.id,newState.title,newState.url);
  1649. // Fire HTML5 Event
  1650. History.Adapter.trigger(window,'popstate');
  1651. }
  1652. // End replaceState closure
  1653. return true;
  1654. };
  1655. } // !History.emulated.pushState
  1656. // ====================================================================
  1657. // Initialise
  1658. /**
  1659. * Load the Store
  1660. */
  1661. if ( sessionStorage ) {
  1662. // Fetch
  1663. try {
  1664. History.store = JSON.parse(sessionStorage.getItem('History.store'))||{};
  1665. }
  1666. catch ( err ) {
  1667. History.store = {};
  1668. }
  1669. // Normalize
  1670. History.normalizeStore();
  1671. }
  1672. else {
  1673. // Default Load
  1674. History.store = {};
  1675. History.normalizeStore();
  1676. }
  1677. /**
  1678. * Clear Intervals on exit to prevent memory leaks
  1679. */
  1680. History.Adapter.bind(window,"unload",History.clearAllIntervals);
  1681. /**
  1682. * Create the initial State
  1683. */
  1684. History.saveState(History.storeState(History.extractState(History.getLocationHref(),true)));
  1685. /**
  1686. * Bind for Saving Store
  1687. */
  1688. if ( sessionStorage ) {
  1689. // When the page is closed
  1690. History.onUnload = function(){
  1691. // Prepare
  1692. var currentStore, item, currentStoreString;
  1693. // Fetch
  1694. try {
  1695. currentStore = JSON.parse(sessionStorage.getItem('History.store'))||{};
  1696. }
  1697. catch ( err ) {
  1698. currentStore = {};
  1699. }
  1700. // Ensure
  1701. currentStore.idToState = currentStore.idToState || {};
  1702. currentStore.urlToId = currentStore.urlToId || {};
  1703. currentStore.stateToId = currentStore.stateToId || {};
  1704. // Sync
  1705. for ( item in History.idToState ) {
  1706. if ( !History.idToState.hasOwnProperty(item) ) {
  1707. continue;
  1708. }
  1709. currentStore.idToState[item] = History.idToState[item];
  1710. }
  1711. for ( item in History.urlToId ) {
  1712. if ( !History.urlToId.hasOwnProperty(item) ) {
  1713. continue;
  1714. }
  1715. currentStore.urlToId[item] = History.urlToId[item];
  1716. }
  1717. for ( item in History.stateToId ) {
  1718. if ( !History.stateToId.hasOwnProperty(item) ) {
  1719. continue;
  1720. }
  1721. currentStore.stateToId[item] = History.stateToId[item];
  1722. }
  1723. // Update
  1724. History.store = currentStore;
  1725. History.normalizeStore();
  1726. // In Safari, going into Private Browsing mode causes the
  1727. // Session Storage object to still exist but if you try and use
  1728. // or set any property/function of it it throws the exception
  1729. // "QUOTA_EXCEEDED_ERR: DOM Exception 22: An attempt was made to
  1730. // add something to storage that exceeded the quota." infinitely
  1731. // every second.
  1732. currentStoreString = JSON.stringify(currentStore);
  1733. try {
  1734. // Store
  1735. sessionStorage.setItem('History.store', currentStoreString);
  1736. }
  1737. catch (e) {
  1738. if (e.code === DOMException.QUOTA_EXCEEDED_ERR) {
  1739. if (sessionStorage.length) {
  1740. // Workaround for a bug seen on iPads. Sometimes the quota exceeded error comes up and simply
  1741. // removing/resetting the storage can work.
  1742. sessionStorage.removeItem('History.store');
  1743. sessionStorage.setItem('History.store', currentStoreString);
  1744. } else {
  1745. // Otherwise, we're probably private browsing in Safari, so we'll ignore the exception.
  1746. }
  1747. } else {
  1748. throw e;
  1749. }
  1750. }
  1751. };
  1752. // For Internet Explorer
  1753. History.intervalList.push(setInterval(History.onUnload,History.options.storeInterval));
  1754. // For Other Browsers
  1755. History.Adapter.bind(window,'beforeunload',History.onUnload);
  1756. History.Adapter.bind(window,'unload',History.onUnload);
  1757. // Both are enabled for consistency
  1758. }
  1759. // Non-Native pushState Implementation
  1760. if ( !History.emulated.pushState ) {
  1761. // Be aware, the following is only for native pushState implementations
  1762. // If you are wanting to include something for all browsers
  1763. // Then include it above this if block
  1764. /**
  1765. * Setup Safari Fix
  1766. */
  1767. if ( History.bugs.safariPoll ) {
  1768. History.intervalList.push(setInterval(History.safariStatePoll, History.options.safariPollInterval));
  1769. }
  1770. /**
  1771. * Ensure Cross Browser Compatibility
  1772. */
  1773. if ( navigator.vendor === 'Apple Computer, Inc.' || (navigator.appCodeName||'') === 'Mozilla' ) {
  1774. /**
  1775. * Fix Safari HashChange Issue
  1776. */
  1777. // Setup Alias
  1778. History.Adapter.bind(window,'hashchange',function(){
  1779. History.Adapter.trigger(window,'popstate');
  1780. });
  1781. // Initialise Alias
  1782. if ( History.getHash() ) {
  1783. History.Adapter.onDomLoad(function(){
  1784. History.Adapter.trigger(window,'hashchange');
  1785. });
  1786. }
  1787. }
  1788. } // !History.emulated.pushState
  1789. }; // History.initCore
  1790. // Try to Initialise History
  1791. if (!History.options || !History.options.delayInit) {
  1792. History.init();
  1793. }
  1794. })(window);