mootools.history.js 51 KB

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