zepto.history.js 51 KB

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