jquery-historyjs 0.1.0

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