right.history.js 51 KB

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