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,658 @@
1
+ /**
2
+ * History.js HTML4 Support
3
+ * Depends on the HTML5 Support
4
+ * @author Benjamin Arthur Lupton <contact@balupton.com>
5
+ * @copyright 2010-2011 Benjamin Arthur Lupton <contact@balupton.com>
6
+ * @license New BSD License <http://creativecommons.org/licenses/BSD/>
7
+ */
8
+
9
+ (function(window,undefined){
10
+ "use strict";
11
+
12
+ // ========================================================================
13
+ // Initialise
14
+
15
+ // Localise Globals
16
+ var
17
+ document = window.document, // Make sure we are using the correct document
18
+ setTimeout = window.setTimeout||setTimeout,
19
+ clearTimeout = window.clearTimeout||clearTimeout,
20
+ setInterval = window.setInterval||setInterval,
21
+ History = window.History = window.History||{}; // Public History Object
22
+
23
+ // Check Existence
24
+ if ( typeof History.initHtml4 !== 'undefined' ) {
25
+ throw new Error('History.js HTML4 Support has already been loaded...');
26
+ }
27
+
28
+
29
+ // ========================================================================
30
+ // Initialise HTML4 Support
31
+
32
+ // Initialise HTML4 Support
33
+ History.initHtml4 = function(){
34
+ // Initialise
35
+ if ( typeof History.initHtml4.initialized !== 'undefined' ) {
36
+ // Already Loaded
37
+ return false;
38
+ }
39
+ else {
40
+ History.initHtml4.initialized = true;
41
+ }
42
+
43
+
44
+ // ====================================================================
45
+ // Properties
46
+
47
+ /**
48
+ * History.enabled
49
+ * Is History enabled?
50
+ */
51
+ History.enabled = true;
52
+
53
+
54
+ // ====================================================================
55
+ // Hash Storage
56
+
57
+ /**
58
+ * History.savedHashes
59
+ * Store the hashes in an array
60
+ */
61
+ History.savedHashes = [];
62
+
63
+ /**
64
+ * History.isLastHash(newHash)
65
+ * Checks if the hash is the last hash
66
+ * @param {string} newHash
67
+ * @return {boolean} true
68
+ */
69
+ History.isLastHash = function(newHash){
70
+ // Prepare
71
+ var oldHash = History.getHashByIndex(),
72
+ isLast;
73
+
74
+ // Check
75
+ isLast = newHash === oldHash;
76
+
77
+ // Return isLast
78
+ return isLast;
79
+ };
80
+
81
+ /**
82
+ * History.saveHash(newHash)
83
+ * Push a Hash
84
+ * @param {string} newHash
85
+ * @return {boolean} true
86
+ */
87
+ History.saveHash = function(newHash){
88
+ // Check Hash
89
+ if ( History.isLastHash(newHash) ) {
90
+ return false;
91
+ }
92
+
93
+ // Push the Hash
94
+ History.savedHashes.push(newHash);
95
+
96
+ // Return true
97
+ return true;
98
+ };
99
+
100
+ /**
101
+ * History.getHashByIndex()
102
+ * Gets a hash by the index
103
+ * @param {integer} index
104
+ * @return {string}
105
+ */
106
+ History.getHashByIndex = function(index){
107
+ // Prepare
108
+ var hash = null;
109
+
110
+ // Handle
111
+ if ( typeof index === 'undefined' ) {
112
+ // Get the last inserted
113
+ hash = History.savedHashes[History.savedHashes.length-1];
114
+ }
115
+ else if ( index < 0 ) {
116
+ // Get from the end
117
+ hash = History.savedHashes[History.savedHashes.length+index];
118
+ }
119
+ else {
120
+ // Get from the beginning
121
+ hash = History.savedHashes[index];
122
+ }
123
+
124
+ // Return hash
125
+ return hash;
126
+ };
127
+
128
+
129
+ // ====================================================================
130
+ // Discarded States
131
+
132
+ /**
133
+ * History.discardedHashes
134
+ * A hashed array of discarded hashes
135
+ */
136
+ History.discardedHashes = {};
137
+
138
+ /**
139
+ * History.discardedStates
140
+ * A hashed array of discarded states
141
+ */
142
+ History.discardedStates = {};
143
+
144
+ /**
145
+ * History.discardState(State)
146
+ * Discards the state by ignoring it through History
147
+ * @param {object} State
148
+ * @return {true}
149
+ */
150
+ History.discardState = function(discardedState,forwardState,backState){
151
+ //History.debug('History.discardState', arguments);
152
+ // Prepare
153
+ var discardedStateHash = History.getHashByState(discardedState),
154
+ discardObject;
155
+
156
+ // Create Discard Object
157
+ discardObject = {
158
+ 'discardedState': discardedState,
159
+ 'backState': backState,
160
+ 'forwardState': forwardState
161
+ };
162
+
163
+ // Add to DiscardedStates
164
+ History.discardedStates[discardedStateHash] = discardObject;
165
+
166
+ // Return true
167
+ return true;
168
+ };
169
+
170
+ /**
171
+ * History.discardHash(hash)
172
+ * Discards the hash by ignoring it through History
173
+ * @param {string} hash
174
+ * @return {true}
175
+ */
176
+ History.discardHash = function(discardedHash,forwardState,backState){
177
+ //History.debug('History.discardState', arguments);
178
+ // Create Discard Object
179
+ var discardObject = {
180
+ 'discardedHash': discardedHash,
181
+ 'backState': backState,
182
+ 'forwardState': forwardState
183
+ };
184
+
185
+ // Add to discardedHash
186
+ History.discardedHashes[discardedHash] = discardObject;
187
+
188
+ // Return true
189
+ return true;
190
+ };
191
+
192
+ /**
193
+ * History.discardedState(State)
194
+ * Checks to see if the state is discarded
195
+ * @param {object} State
196
+ * @return {bool}
197
+ */
198
+ History.discardedState = function(State){
199
+ // Prepare
200
+ var StateHash = History.getHashByState(State),
201
+ discarded;
202
+
203
+ // Check
204
+ discarded = History.discardedStates[StateHash]||false;
205
+
206
+ // Return true
207
+ return discarded;
208
+ };
209
+
210
+ /**
211
+ * History.discardedHash(hash)
212
+ * Checks to see if the state is discarded
213
+ * @param {string} State
214
+ * @return {bool}
215
+ */
216
+ History.discardedHash = function(hash){
217
+ // Check
218
+ var discarded = History.discardedHashes[hash]||false;
219
+
220
+ // Return true
221
+ return discarded;
222
+ };
223
+
224
+ /**
225
+ * History.recycleState(State)
226
+ * Allows a discarded state to be used again
227
+ * @param {object} data
228
+ * @param {string} title
229
+ * @param {string} url
230
+ * @return {true}
231
+ */
232
+ History.recycleState = function(State){
233
+ //History.debug('History.recycleState', arguments);
234
+ // Prepare
235
+ var StateHash = History.getHashByState(State);
236
+
237
+ // Remove from DiscardedStates
238
+ if ( History.discardedState(State) ) {
239
+ delete History.discardedStates[StateHash];
240
+ }
241
+
242
+ // Return true
243
+ return true;
244
+ };
245
+
246
+
247
+ // ====================================================================
248
+ // HTML4 HashChange Support
249
+
250
+ if ( History.emulated.hashChange ) {
251
+ /*
252
+ * We must emulate the HTML4 HashChange Support by manually checking for hash changes
253
+ */
254
+
255
+ /**
256
+ * History.hashChangeInit()
257
+ * Init the HashChange Emulation
258
+ */
259
+ History.hashChangeInit = function(){
260
+ // Define our Checker Function
261
+ History.checkerFunction = null;
262
+
263
+ // Define some variables that will help in our checker function
264
+ var lastDocumentHash = '',
265
+ iframeId, iframe,
266
+ lastIframeHash, checkerRunning,
267
+ startedWithHash = Boolean(History.getHash());
268
+
269
+ // Handle depending on the browser
270
+ if ( History.isInternetExplorer() ) {
271
+ // IE6 and IE7
272
+ // We need to use an iframe to emulate the back and forward buttons
273
+
274
+ // Create iFrame
275
+ iframeId = 'historyjs-iframe';
276
+ iframe = document.createElement('iframe');
277
+
278
+ // Adjust iFarme
279
+ iframe.setAttribute('id', iframeId);
280
+ iframe.style.display = 'none';
281
+
282
+ // Append iFrame
283
+ document.body.appendChild(iframe);
284
+
285
+ // Create initial history entry
286
+ iframe.contentWindow.document.open();
287
+ iframe.contentWindow.document.close();
288
+
289
+ // Define some variables that will help in our checker function
290
+ lastIframeHash = '';
291
+ checkerRunning = false;
292
+
293
+ // Define the checker function
294
+ History.checkerFunction = function(){
295
+ // Check Running
296
+ if ( checkerRunning ) {
297
+ return false;
298
+ }
299
+
300
+ // Update Running
301
+ checkerRunning = true;
302
+
303
+ // Fetch
304
+ var documentHash = History.getHash()||'',
305
+ iframeHash = History.getHash(iframe.contentWindow.document)||'';
306
+
307
+ // The Document Hash has changed (application caused)
308
+ if ( documentHash !== lastDocumentHash ) {
309
+ // Equalise
310
+ lastDocumentHash = documentHash;
311
+
312
+ // Create a history entry in the iframe
313
+ if ( iframeHash !== documentHash ) {
314
+ //History.debug('hashchange.checker: iframe hash change', 'documentHash (new):', documentHash, 'iframeHash (old):', iframeHash);
315
+
316
+ // Equalise
317
+ lastIframeHash = iframeHash = documentHash;
318
+
319
+ // Create History Entry
320
+ iframe.contentWindow.document.open();
321
+ iframe.contentWindow.document.close();
322
+
323
+ // Update the iframe's hash
324
+ iframe.contentWindow.document.location.hash = History.escapeHash(documentHash);
325
+ }
326
+
327
+ // Trigger Hashchange Event
328
+ History.Adapter.trigger(window,'hashchange');
329
+ }
330
+
331
+ // The iFrame Hash has changed (back button caused)
332
+ else if ( iframeHash !== lastIframeHash ) {
333
+ //History.debug('hashchange.checker: iframe hash out of sync', 'iframeHash (new):', iframeHash, 'documentHash (old):', documentHash);
334
+
335
+ // Equalise
336
+ lastIframeHash = iframeHash;
337
+
338
+ // If there is no iframe hash that means we're at the original
339
+ // iframe state.
340
+ // And if there was a hash on the original request, the original
341
+ // iframe state was replaced instantly, so skip this state and take
342
+ // the user back to where they came from.
343
+ if (startedWithHash && iframeHash === '') {
344
+ History.back();
345
+ }
346
+ else {
347
+ // Update the Hash
348
+ History.setHash(iframeHash,false);
349
+ }
350
+ }
351
+
352
+ // Reset Running
353
+ checkerRunning = false;
354
+
355
+ // Return true
356
+ return true;
357
+ };
358
+ }
359
+ else {
360
+ // We are not IE
361
+ // Firefox 1 or 2, Opera
362
+
363
+ // Define the checker function
364
+ History.checkerFunction = function(){
365
+ // Prepare
366
+ var documentHash = History.getHash()||'';
367
+
368
+ // The Document Hash has changed (application caused)
369
+ if ( documentHash !== lastDocumentHash ) {
370
+ // Equalise
371
+ lastDocumentHash = documentHash;
372
+
373
+ // Trigger Hashchange Event
374
+ History.Adapter.trigger(window,'hashchange');
375
+ }
376
+
377
+ // Return true
378
+ return true;
379
+ };
380
+ }
381
+
382
+ // Apply the checker function
383
+ History.intervalList.push(setInterval(History.checkerFunction, History.options.hashChangeInterval));
384
+
385
+ // Done
386
+ return true;
387
+ }; // History.hashChangeInit
388
+
389
+ // Bind hashChangeInit
390
+ History.Adapter.onDomLoad(History.hashChangeInit);
391
+
392
+ } // History.emulated.hashChange
393
+
394
+
395
+ // ====================================================================
396
+ // HTML5 State Support
397
+
398
+ // Non-Native pushState Implementation
399
+ if ( History.emulated.pushState ) {
400
+ /*
401
+ * We must emulate the HTML5 State Management by using HTML4 HashChange
402
+ */
403
+
404
+ /**
405
+ * History.onHashChange(event)
406
+ * Trigger HTML5's window.onpopstate via HTML4 HashChange Support
407
+ */
408
+ History.onHashChange = function(event){
409
+ //History.debug('History.onHashChange', arguments);
410
+
411
+ // Prepare
412
+ var currentUrl = ((event && event.newURL) || History.getLocationHref()),
413
+ currentHash = History.getHashByUrl(currentUrl),
414
+ currentState = null,
415
+ currentStateHash = null,
416
+ currentStateHashExits = null,
417
+ discardObject;
418
+
419
+ // Check if we are the same state
420
+ if ( History.isLastHash(currentHash) ) {
421
+ // There has been no change (just the page's hash has finally propagated)
422
+ //History.debug('History.onHashChange: no change');
423
+ History.busy(false);
424
+ return false;
425
+ }
426
+
427
+ // Reset the double check
428
+ History.doubleCheckComplete();
429
+
430
+ // Store our location for use in detecting back/forward direction
431
+ History.saveHash(currentHash);
432
+
433
+ // Expand Hash
434
+ if ( currentHash && History.isTraditionalAnchor(currentHash) ) {
435
+ //History.debug('History.onHashChange: traditional anchor', currentHash);
436
+ // Traditional Anchor Hash
437
+ History.Adapter.trigger(window,'anchorchange');
438
+ History.busy(false);
439
+ return false;
440
+ }
441
+
442
+ // Create State
443
+ currentState = History.extractState(History.getFullUrl(currentHash||History.getLocationHref(),false),true);
444
+
445
+ // Check if we are the same state
446
+ if ( History.isLastSavedState(currentState) ) {
447
+ //History.debug('History.onHashChange: no change');
448
+ // There has been no change (just the page's hash has finally propagated)
449
+ History.busy(false);
450
+ return false;
451
+ }
452
+
453
+ // Create the state Hash
454
+ currentStateHash = History.getHashByState(currentState);
455
+
456
+ // Check if we are DiscardedState
457
+ discardObject = History.discardedState(currentState);
458
+ if ( discardObject ) {
459
+ // Ignore this state as it has been discarded and go back to the state before it
460
+ if ( History.getHashByIndex(-2) === History.getHashByState(discardObject.forwardState) ) {
461
+ // We are going backwards
462
+ //History.debug('History.onHashChange: go backwards');
463
+ History.back(false);
464
+ } else {
465
+ // We are going forwards
466
+ //History.debug('History.onHashChange: go forwards');
467
+ History.forward(false);
468
+ }
469
+ return false;
470
+ }
471
+
472
+ // Push the new HTML5 State
473
+ //History.debug('History.onHashChange: success hashchange');
474
+ History.pushState(currentState.data,currentState.title,currentState.url,false);
475
+
476
+ // End onHashChange closure
477
+ return true;
478
+ };
479
+ History.Adapter.bind(window,'hashchange',History.onHashChange);
480
+
481
+ /**
482
+ * History.pushState(data,title,url)
483
+ * Add a new State to the history object, become it, and trigger onpopstate
484
+ * We have to trigger for HTML4 compatibility
485
+ * @param {object} data
486
+ * @param {string} title
487
+ * @param {string} url
488
+ * @return {true}
489
+ */
490
+ History.pushState = function(data,title,url,queue){
491
+ //History.debug('History.pushState: called', arguments);
492
+
493
+ // Check the State
494
+ if ( History.getHashByUrl(url) ) {
495
+ throw new Error('History.js does not support states with fragement-identifiers (hashes/anchors).');
496
+ }
497
+
498
+ // Handle Queueing
499
+ if ( queue !== false && History.busy() ) {
500
+ // Wait + Push to Queue
501
+ //History.debug('History.pushState: we must wait', arguments);
502
+ History.pushQueue({
503
+ scope: History,
504
+ callback: History.pushState,
505
+ args: arguments,
506
+ queue: queue
507
+ });
508
+ return false;
509
+ }
510
+
511
+ // Make Busy
512
+ History.busy(true);
513
+
514
+ // Fetch the State Object
515
+ var newState = History.createStateObject(data,title,url),
516
+ newStateHash = History.getHashByState(newState),
517
+ oldState = History.getState(false),
518
+ oldStateHash = History.getHashByState(oldState),
519
+ html4Hash = History.getHash();
520
+
521
+ // Store the newState
522
+ History.storeState(newState);
523
+ History.expectedStateId = newState.id;
524
+
525
+ // Recycle the State
526
+ History.recycleState(newState);
527
+
528
+ // Force update of the title
529
+ History.setTitle(newState);
530
+
531
+ // Check if we are the same State
532
+ if ( newStateHash === oldStateHash ) {
533
+ //History.debug('History.pushState: no change', newStateHash);
534
+ History.busy(false);
535
+ return false;
536
+ }
537
+
538
+ // Update HTML4 Hash
539
+ if ( newStateHash !== html4Hash && newStateHash !== History.getShortUrl(History.getLocationHref()) ) {
540
+ //History.debug('History.pushState: update hash', newStateHash, html4Hash);
541
+ History.setHash(newStateHash,false);
542
+ return false;
543
+ }
544
+
545
+ // Update HTML5 State
546
+ History.saveState(newState);
547
+
548
+ // Fire HTML5 Event
549
+ //History.debug('History.pushState: trigger popstate');
550
+ History.Adapter.trigger(window,'statechange');
551
+ History.busy(false);
552
+
553
+ // End pushState closure
554
+ return true;
555
+ };
556
+
557
+ /**
558
+ * History.replaceState(data,title,url)
559
+ * Replace the State and trigger onpopstate
560
+ * We have to trigger for HTML4 compatibility
561
+ * @param {object} data
562
+ * @param {string} title
563
+ * @param {string} url
564
+ * @return {true}
565
+ */
566
+ History.replaceState = function(data,title,url,queue){
567
+ //History.debug('History.replaceState: called', arguments);
568
+
569
+ // Check the State
570
+ if ( History.getHashByUrl(url) ) {
571
+ throw new Error('History.js does not support states with fragement-identifiers (hashes/anchors).');
572
+ }
573
+
574
+ // Handle Queueing
575
+ if ( queue !== false && History.busy() ) {
576
+ // Wait + Push to Queue
577
+ //History.debug('History.replaceState: we must wait', arguments);
578
+ History.pushQueue({
579
+ scope: History,
580
+ callback: History.replaceState,
581
+ args: arguments,
582
+ queue: queue
583
+ });
584
+ return false;
585
+ }
586
+
587
+ // Make Busy
588
+ History.busy(true);
589
+
590
+ // Fetch the State Objects
591
+ var newState = History.createStateObject(data,title,url),
592
+ newStateHash = History.getHashByState(newState),
593
+ oldState = History.getState(false),
594
+ oldStateHash = History.getHashByState(oldState),
595
+ previousState = History.getStateByIndex(-2);
596
+
597
+ // Discard Old State
598
+ History.discardState(oldState,newState,previousState);
599
+
600
+ // If the url hasn't changed, just store and save the state
601
+ // and fire a statechange event to be consistent with the
602
+ // html 5 api
603
+ if ( newStateHash === oldStateHash ) {
604
+ // Store the newState
605
+ History.storeState(newState);
606
+ History.expectedStateId = newState.id;
607
+
608
+ // Recycle the State
609
+ History.recycleState(newState);
610
+
611
+ // Force update of the title
612
+ History.setTitle(newState);
613
+
614
+ // Update HTML5 State
615
+ History.saveState(newState);
616
+
617
+ // Fire HTML5 Event
618
+ //History.debug('History.pushState: trigger popstate');
619
+ History.Adapter.trigger(window,'statechange');
620
+ History.busy(false);
621
+ }
622
+ else {
623
+ // Alias to PushState
624
+ History.pushState(newState.data,newState.title,newState.url,false);
625
+ }
626
+
627
+ // End replaceState closure
628
+ return true;
629
+ };
630
+
631
+ } // History.emulated.pushState
632
+
633
+
634
+
635
+ // ====================================================================
636
+ // Initialise
637
+
638
+ // Non-Native pushState Implementation
639
+ if ( History.emulated.pushState ) {
640
+ /**
641
+ * Ensure initial state is handled correctly
642
+ */
643
+ if ( History.getHash() && !History.emulated.hashChange ) {
644
+ History.Adapter.onDomLoad(function(){
645
+ History.Adapter.trigger(window,'hashchange');
646
+ });
647
+ }
648
+
649
+ } // History.emulated.pushState
650
+
651
+ }; // History.initHtml4
652
+
653
+ // Try to Initialise History
654
+ if ( typeof History.init !== 'undefined' ) {
655
+ History.init();
656
+ }
657
+
658
+ })(window);