hat-trick 0.1.2 → 0.1.3

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