wiselinks 0.6.3 → 0.6.4

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- wiselinks (0.6.3)
4
+ wiselinks (0.6.4)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
@@ -47,6 +47,7 @@ GEM
47
47
  xpath (~> 1.0.0)
48
48
  childprocess (0.3.9)
49
49
  ffi (~> 1.0, >= 1.0.11)
50
+ closure-compiler (1.1.8)
50
51
  coffee-rails (3.2.2)
51
52
  coffee-script (>= 2.2.0)
52
53
  railties (~> 3.2.0)
@@ -54,6 +55,8 @@ GEM
54
55
  coffee-script-source
55
56
  execjs
56
57
  coffee-script-source (1.5.0)
58
+ coffeelint (0.0.6)
59
+ coffee-script
57
60
  colorize (0.5.8)
58
61
  coveralls (0.6.7)
59
62
  colorize
@@ -170,7 +173,9 @@ PLATFORMS
170
173
  DEPENDENCIES
171
174
  bundler
172
175
  capybara
176
+ closure-compiler
173
177
  coffee-rails
178
+ coffeelint
174
179
  coveralls
175
180
  factory_girl
176
181
  faker
data/README.md CHANGED
@@ -82,6 +82,8 @@ Wiselinks works in all major browsers including browsers that do not support HTM
82
82
 
83
83
  ##Installation
84
84
 
85
+ ### Rails
86
+
85
87
  Add this to your Gemfile:
86
88
 
87
89
  ```ruby
@@ -94,6 +96,10 @@ Then do:
94
96
 
95
97
  Restart your server and you're now using wiselinks!
96
98
 
99
+ ### All others
100
+
101
+ Copy `wiselinks-x.y.z.js` or `wiselinks-x.y.z.min.js` from `build` folder in this project to your project.
102
+
97
103
  ## How does it work?
98
104
 
99
105
  ### CoffeeScript
@@ -252,10 +258,14 @@ Wiselinks can process forms. After submit button is clicked, Wiselinks will perf
252
258
  </div>
253
259
  ```
254
260
 
255
- ### Rendering
261
+ ### Server processing
256
262
 
257
263
  The idea of Wiselinks is that you should render only content that you need in current request. Usually you don't need to reload your stylesheets and javascripts on every request.
258
264
 
265
+ `X-Wiselinks` header is passed with every Wiselinks request. Server should respond with content that should be inserted into `$target`.
266
+
267
+ In Rails after installing Wiselinks gem, all requests that have `X-Wiselinks` header will be automatically processed within 'app/views/layouts/wiselinks' layout, that basically has only `yield` operator. Of course you can override layout name by redefining `wiselinks_layout` method in your controller.
268
+
259
269
  ### Javascript Events
260
270
 
261
271
  While using Wiselinks you **can rely** on `DOMContentLoaded` or `jQuery.ready()` to trigger your JavaScript code, but Wiselinks gives you some additional useful event to deal with the lifecycle of the page:
@@ -318,13 +328,13 @@ So if you want to show a client-side loading spinner, you could listen for `page
318
328
  Wiselinks adds a couple of methods to `ActionDispatch::Request`. These methods are mostly syntax sugar and don't have any complex logic, so you can use them or not.
319
329
 
320
330
  #### #wiselinks? ###
321
- Method returns `true` if current request is initiated by Wiselinks, `false` otherwise.
331
+ Method returns `true` if current request is initiated by Wiselinks (has `X-Wiselinks` header), `false` otherwise.
322
332
 
323
333
  #### #wiselinks_template? ###
324
- Method returns `true` if current request is initiated by Wiselinks and client want to render template, `false` otherwise.
334
+ Method returns `true` if current request is initiated by Wiselinks and client want to render template (`X-Wiselinks != 'partial'`), `false` otherwise.
325
335
 
326
336
  #### #wiselinks_partial? ###
327
- Method returns `true` if current request is initiated by Wiselinks and client want to render partial, `false` otherwise.
337
+ Method returns `true` if current request is initiated by Wiselinks and client want to render partial (`X-Wiselinks == 'partial'`), `false` otherwise.
328
338
 
329
339
  ### Assets change detection
330
340
 
data/Rakefile CHANGED
@@ -3,6 +3,8 @@
3
3
  require 'bundler/gem_tasks'
4
4
  require 'rspec/core/rake_task'
5
5
 
6
+ require 'wiselinks/builder'
7
+
6
8
  RSpec::Core::RakeTask.new('spec')
7
9
 
8
10
  task :default => :spec
@@ -0,0 +1,3502 @@
1
+ /**
2
+ * Wiselinks-0.6.4
3
+ * @copyright 2012-2013 Igor Alexandrov, Alexey Solilin, Julia Egorova, Alexandr Borisov
4
+ * @preserve https://github.com/igor-alexandrov/wiselinks
5
+ */
6
+
7
+ // Generated by CoffeeScript 1.6.3
8
+ (function() {
9
+ var Form, Link, Page, RequestManager, Wiselinks;
10
+
11
+ Form = (function() {
12
+ function Form(page, $form) {
13
+ this.page = page;
14
+ this.$form = $form;
15
+ }
16
+
17
+ Form.prototype.process = function() {
18
+ var $disable, selector, url;
19
+ selector = 'select:not(:disabled),input:not(:disabled)';
20
+ $disable = this.$form.find(selector).filter(function() {
21
+ return !$(this).val();
22
+ });
23
+ $disable.attr('disabled', true);
24
+ url = this._url();
25
+ $disable.attr('disabled', false);
26
+ return this.page.load(url, this.$form.attr("data-target"), this._type());
27
+ };
28
+
29
+ Form.prototype._params = function() {
30
+ var hash, item, name, _i, _len, _ref;
31
+ hash = {};
32
+ _ref = this.$form.serializeArray();
33
+ for (_i = 0, _len = _ref.length; _i < _len; _i++) {
34
+ item = _ref[_i];
35
+ if (item.name !== 'utf8') {
36
+ name = item.name.indexOf('[]', item.name.length - '[]'.length) !== -1 ? item.name.substr(0, item.name.length - 2) : item.name;
37
+ if (hash[name] != null) {
38
+ hash[name] = hash[name] + ("," + item.value);
39
+ } else {
40
+ hash[name] = item.value;
41
+ }
42
+ }
43
+ }
44
+ return hash;
45
+ };
46
+
47
+ Form.prototype._type = function() {
48
+ if (this.$form.attr("data-push") === 'partial') {
49
+ return 'partial';
50
+ } else {
51
+ return 'template';
52
+ }
53
+ };
54
+
55
+ Form.prototype._url = function() {
56
+ var key, serialized, url, value, _ref;
57
+ serialized = [];
58
+ _ref = this._params();
59
+ for (key in _ref) {
60
+ value = _ref[key];
61
+ serialized.push("" + key + "=" + (encodeURIComponent(value)));
62
+ }
63
+ serialized = serialized.join('&');
64
+ url = this.$form.attr("action");
65
+ if (serialized.length > 0) {
66
+ url += "?" + serialized;
67
+ }
68
+ return url;
69
+ };
70
+
71
+ return Form;
72
+
73
+ })();
74
+
75
+ if (window._Wiselinks === void 0) {
76
+ window._Wiselinks = {};
77
+ }
78
+
79
+ window._Wiselinks.Form = Form;
80
+
81
+ Link = (function() {
82
+ function Link(page, $link) {
83
+ this.page = page;
84
+ this.$link = $link;
85
+ }
86
+
87
+ Link.prototype.allows_process = function(event) {
88
+ return !(this._cross_origin_link(event.currentTarget) || this._non_standard_click(event));
89
+ };
90
+
91
+ Link.prototype.process = function() {
92
+ var type;
93
+ type = this.$link.attr("data-push") === 'partial' ? 'partial' : 'template';
94
+ return this.page.load(this.$link.attr("href"), this.$link.attr("data-target"), type);
95
+ };
96
+
97
+ Link.prototype._cross_origin_link = function(link) {
98
+ return (location.protocol !== link.protocol) || (location.host.split(':')[0] !== link.host.split(':')[0]);
99
+ };
100
+
101
+ Link.prototype._non_standard_click = function(event) {
102
+ return event.metaKey || event.ctrlKey || event.shiftKey || event.altKey;
103
+ };
104
+
105
+ return Link;
106
+
107
+ })();
108
+
109
+ if (window._Wiselinks === void 0) {
110
+ window._Wiselinks = {};
111
+ }
112
+
113
+ window._Wiselinks.Link = Link;
114
+
115
+ Page = (function() {
116
+ function Page($target, options) {
117
+ var self;
118
+ this.$target = $target;
119
+ this.options = options;
120
+ self = this;
121
+ this.template_id = new Date().getTime();
122
+ this.request_manager = new _Wiselinks.RequestManager(this.options);
123
+ self._try_target(this.$target);
124
+ if (History.emulated.pushState && this.options.html4 === true) {
125
+ if (window.location.href.indexOf('#!') === -1 && this.options.html4_normalize_path === true && window.location.pathname !== this.options.html4_root_path) {
126
+ window.location.href = "" + window.location.protocol + "// " + window.location.host + " " + this.options.html4_root_path + "#! " + window.location.pathname;
127
+ }
128
+ if (window.location.hash.indexOf('#!') !== -1) {
129
+ self._call(self._make_state(window.location.hash.substring(2)));
130
+ }
131
+ }
132
+ History.Adapter.bind(window, "statechange", function(event, data) {
133
+ var state;
134
+ state = History.getState();
135
+ if (self._template_id_changed(state)) {
136
+ return self._call(self._reset_state(state));
137
+ } else {
138
+ return self._call(state);
139
+ }
140
+ });
141
+ $(document).on('click', 'a[data-push], a[data-replace]', function(event) {
142
+ var link;
143
+ if ((link = new _Wiselinks.Link(self, $(this))).allows_process(event)) {
144
+ event.preventDefault();
145
+ link.process();
146
+ return false;
147
+ }
148
+ });
149
+ $(document).on('submit', 'form[data-push], form[data-replace]', function(event) {
150
+ var form;
151
+ if ((form = new _Wiselinks.Form(self, $(this)))) {
152
+ event.preventDefault();
153
+ form.process();
154
+ return false;
155
+ }
156
+ });
157
+ }
158
+
159
+ Page.prototype.load = function(url, target, render) {
160
+ if (render == null) {
161
+ render = 'template';
162
+ }
163
+ if (render !== 'partial') {
164
+ this.template_id = new Date().getTime();
165
+ }
166
+ if (target != null) {
167
+ this._try_target($(target));
168
+ }
169
+ return History.pushState({
170
+ timestamp: new Date().getTime(),
171
+ template_id: this.template_id,
172
+ render: render,
173
+ target: target,
174
+ referer: window.location.href
175
+ }, document.title, url);
176
+ };
177
+
178
+ Page.prototype.reload = function() {
179
+ return History.replaceState({
180
+ timestamp: new Date().getTime(),
181
+ template_id: this.template_id,
182
+ render: 'template',
183
+ referer: window.location.href
184
+ }, document.title, History.getState().url);
185
+ };
186
+
187
+ Page.prototype._call = function(state) {
188
+ var $target;
189
+ $target = state.data.target != null ? $(state.data.target) : this.$target;
190
+ return this.request_manager.call($target, state);
191
+ };
192
+
193
+ Page.prototype._template_id_changed = function(state) {
194
+ return (state.data.template_id == null) || state.data.template_id !== this.template_id;
195
+ };
196
+
197
+ Page.prototype._make_state = function(url, target, render, referer) {
198
+ if (render == null) {
199
+ render = 'template';
200
+ }
201
+ return {
202
+ url: url,
203
+ data: {
204
+ target: target,
205
+ render: render,
206
+ referer: referer
207
+ }
208
+ };
209
+ };
210
+
211
+ Page.prototype._reset_state = function(state) {
212
+ if (state.data == null) {
213
+ state.data = {};
214
+ }
215
+ state.data.target = null;
216
+ state.data.render = 'template';
217
+ return state;
218
+ };
219
+
220
+ Page.prototype._try_target = function($target) {
221
+ if ($target.length === 0 && this.options.target_missing === 'exception') {
222
+ throw new Error("[Wiselinks] Target missing: `" + $target.selector + "`");
223
+ }
224
+ };
225
+
226
+ return Page;
227
+
228
+ })();
229
+
230
+ if (window._Wiselinks === void 0) {
231
+ window._Wiselinks = {};
232
+ }
233
+
234
+ window._Wiselinks.Page = Page;
235
+
236
+ RequestManager = (function() {
237
+ function RequestManager(options) {
238
+ this.options = options != null ? options : {};
239
+ }
240
+
241
+ RequestManager.prototype.call = function($target, state) {
242
+ var self;
243
+ self = this;
244
+ if (this.redirected != null) {
245
+ this.redirected = null;
246
+ return;
247
+ }
248
+ self._loading($target, state);
249
+ return $.ajax({
250
+ url: state.url,
251
+ headers: {
252
+ 'X-Wiselinks': state.data.render,
253
+ 'X-Wiselinks-Referer': state.data.referer
254
+ },
255
+ dataType: "html"
256
+ }).done(function(data, status, xhr) {
257
+ var assets_digest, url;
258
+ url = xhr.getResponseHeader('X-Wiselinks-Url');
259
+ assets_digest = xhr.getResponseHeader('X-Wiselinks-Assets-Digest');
260
+ if (self._assets_changed(assets_digest)) {
261
+ return window.location.reload(true);
262
+ } else {
263
+ state = History.getState();
264
+ if ((url != null) && url !== state.url) {
265
+ self._redirect_to(url, $target, state, xhr);
266
+ }
267
+ $target.html(data);
268
+ self._title(xhr.getResponseHeader('X-Wiselinks-Title'));
269
+ return self._done($target, status, state.url, data);
270
+ }
271
+ }).fail(function(xhr, status, error) {
272
+ return self._fail($target, status, state.url, error);
273
+ }).always(function(data_or_xhr, status, xhr_or_error) {
274
+ return self._always($target, status, state.url);
275
+ });
276
+ };
277
+
278
+ RequestManager.prototype._assets_changed = function(assets_digest) {
279
+ return (this.options.assets_digest != null) && this.options.assets_digest !== assets_digest;
280
+ };
281
+
282
+ RequestManager.prototype._redirect_to = function(url, $target, state, xhr) {
283
+ if (xhr && xhr.readyState < 4) {
284
+ xhr.onreadystatechange = $.noop;
285
+ xhr.abort();
286
+ }
287
+ this.redirected = true;
288
+ $(document).trigger('page:redirected', [$target, state.data.render, url]);
289
+ return History.replaceState(state.data, document.title, url);
290
+ };
291
+
292
+ RequestManager.prototype._loading = function($target, state) {
293
+ return $(document).trigger('page:loading', [$target, state.data.render, state.url]);
294
+ };
295
+
296
+ RequestManager.prototype._done = function($target, status, state, data) {
297
+ return $(document).trigger('page:done', [$target, status, state.url, data]);
298
+ };
299
+
300
+ RequestManager.prototype._fail = function($target, status, state, error) {
301
+ return $(document).trigger('page:fail', [$target, status, state.url, error]);
302
+ };
303
+
304
+ RequestManager.prototype._always = function($target, status, state) {
305
+ return $(document).trigger('page:always', [$target, status, state.url]);
306
+ };
307
+
308
+ RequestManager.prototype._title = function(value) {
309
+ if (value != null) {
310
+ $(document).trigger('page:title', decodeURI(value));
311
+ return document.title = decodeURI(value);
312
+ }
313
+ };
314
+
315
+ return RequestManager;
316
+
317
+ })();
318
+
319
+ if (window._Wiselinks === void 0) {
320
+ window._Wiselinks = {};
321
+ }
322
+
323
+ window._Wiselinks.RequestManager = RequestManager;
324
+
325
+ Wiselinks = (function() {
326
+ function Wiselinks($target, options) {
327
+ if ($target == null) {
328
+ $target = $('body');
329
+ }
330
+ this.options = options != null ? options : {};
331
+ this._try_jquery();
332
+ this.options = $.extend(this._defaults(), this.options);
333
+ if (this.enabled()) {
334
+ this.page = new _Wiselinks.Page($target, this.options);
335
+ }
336
+ }
337
+
338
+ Wiselinks.prototype.enabled = function() {
339
+ return !History.emulated.pushState || this.options.html4 === true;
340
+ };
341
+
342
+ Wiselinks.prototype.load = function(url, target, render) {
343
+ if (render == null) {
344
+ render = 'template';
345
+ }
346
+ return this.page.load(url, target, render);
347
+ };
348
+
349
+ Wiselinks.prototype.reload = function() {
350
+ return this.page.reload();
351
+ };
352
+
353
+ Wiselinks.prototype._defaults = function() {
354
+ return {
355
+ html4: true,
356
+ html4_root_path: '/',
357
+ html4_normalize_path: true,
358
+ target_missing: null,
359
+ assets_digest: $("meta[name='assets-digest']").attr("content")
360
+ };
361
+ };
362
+
363
+ Wiselinks.prototype._try_jquery = function() {
364
+ if (window.jQuery == null) {
365
+ throw new Error("[Wiselinks] jQuery is not loaded");
366
+ }
367
+ };
368
+
369
+ return Wiselinks;
370
+
371
+ })();
372
+
373
+ window.Wiselinks = Wiselinks;
374
+
375
+ }).call(this);
376
+ /**
377
+ * History.js jQuery Adapter
378
+ * @author Benjamin Arthur Lupton <contact@balupton.com>
379
+ * @copyright 2010-2011 Benjamin Arthur Lupton <contact@balupton.com>
380
+ * @license New BSD License <http://creativecommons.org/licenses/BSD/>
381
+ */
382
+
383
+ // Closure
384
+ (function(window,undefined){
385
+ "use strict";
386
+
387
+ // Localise Globals
388
+ var
389
+ History = window.History = window.History||{},
390
+ jQuery = window.jQuery;
391
+
392
+ // Check Existence
393
+ if ( typeof History.Adapter !== 'undefined' ) {
394
+ throw new Error('History.js Adapter has already been loaded...');
395
+ }
396
+
397
+ // Add the Adapter
398
+ History.Adapter = {
399
+ /**
400
+ * History.Adapter.bind(el,event,callback)
401
+ * @param {Element|string} el
402
+ * @param {string} event - custom and standard events
403
+ * @param {function} callback
404
+ * @return {void}
405
+ */
406
+ bind: function(el,event,callback){
407
+ jQuery(el).bind(event,callback);
408
+ },
409
+
410
+ /**
411
+ * History.Adapter.trigger(el,event)
412
+ * @param {Element|string} el
413
+ * @param {string} event - custom and standard events
414
+ * @param {Object=} extra - a object of extra event data (optional)
415
+ * @return {void}
416
+ */
417
+ trigger: function(el,event,extra){
418
+ jQuery(el).trigger(event,extra);
419
+ },
420
+
421
+ /**
422
+ * History.Adapter.extractEventData(key,event,extra)
423
+ * @param {string} key - key for the event data to extract
424
+ * @param {string} event - custom and standard events
425
+ * @param {Object=} extra - a object of extra event data (optional)
426
+ * @return {mixed}
427
+ */
428
+ extractEventData: function(key,event,extra){
429
+ // jQuery Native then jQuery Custom
430
+ var result = (event && event.originalEvent && event.originalEvent[key]) || (extra && extra[key]) || undefined;
431
+
432
+ // Return
433
+ return result;
434
+ },
435
+
436
+ /**
437
+ * History.Adapter.onDomLoad(callback)
438
+ * @param {function} callback
439
+ * @return {void}
440
+ */
441
+ onDomLoad: function(callback) {
442
+ jQuery(callback);
443
+ }
444
+ };
445
+
446
+ // Try and Initialise History
447
+ if ( typeof History.init !== 'undefined' ) {
448
+ History.init();
449
+ }
450
+
451
+ })(window);
452
+ /**
453
+ * History.js HTML4 Support
454
+ * Depends on the HTML5 Support
455
+ * @author Benjamin Arthur Lupton <contact@balupton.com>
456
+ * @copyright 2010-2011 Benjamin Arthur Lupton <contact@balupton.com>
457
+ * @license New BSD License <http://creativecommons.org/licenses/BSD/>
458
+ */
459
+
460
+ (function(window,undefined){
461
+ "use strict";
462
+
463
+ // ========================================================================
464
+ // Initialise
465
+
466
+ // Localise Globals
467
+ var
468
+ document = window.document, // Make sure we are using the correct document
469
+ setTimeout = window.setTimeout||setTimeout,
470
+ clearTimeout = window.clearTimeout||clearTimeout,
471
+ setInterval = window.setInterval||setInterval,
472
+ History = window.History = window.History||{}; // Public History Object
473
+
474
+ // Check Existence
475
+ if ( typeof History.initHtml4 !== 'undefined' ) {
476
+ throw new Error('History.js HTML4 Support has already been loaded...');
477
+ }
478
+
479
+
480
+ // ========================================================================
481
+ // Initialise HTML4 Support
482
+
483
+ // Initialise HTML4 Support
484
+ History.initHtml4 = function(){
485
+ // Initialise
486
+ if ( typeof History.initHtml4.initialized !== 'undefined' ) {
487
+ // Already Loaded
488
+ return false;
489
+ }
490
+ else {
491
+ History.initHtml4.initialized = true;
492
+ }
493
+
494
+
495
+ // ====================================================================
496
+ // Properties
497
+
498
+ /**
499
+ * History.enabled
500
+ * Is History enabled?
501
+ */
502
+ History.enabled = true;
503
+
504
+
505
+ // ====================================================================
506
+ // Hash Storage
507
+
508
+ /**
509
+ * History.savedHashes
510
+ * Store the hashes in an array
511
+ */
512
+ History.savedHashes = [];
513
+
514
+ /**
515
+ * History.isLastHash(newHash)
516
+ * Checks if the hash is the last hash
517
+ * @param {string} newHash
518
+ * @return {boolean} true
519
+ */
520
+ History.isLastHash = function(newHash){
521
+ // Prepare
522
+ var oldHash = History.getHashByIndex(),
523
+ isLast;
524
+
525
+ // Check
526
+ isLast = newHash === oldHash;
527
+
528
+ // Return isLast
529
+ return isLast;
530
+ };
531
+
532
+ /**
533
+ * History.saveHash(newHash)
534
+ * Push a Hash
535
+ * @param {string} newHash
536
+ * @return {boolean} true
537
+ */
538
+ History.saveHash = function(newHash){
539
+ // Check Hash
540
+ if ( History.isLastHash(newHash) ) {
541
+ return false;
542
+ }
543
+
544
+ // Push the Hash
545
+ History.savedHashes.push(newHash);
546
+
547
+ // Return true
548
+ return true;
549
+ };
550
+
551
+ /**
552
+ * History.getHashByIndex()
553
+ * Gets a hash by the index
554
+ * @param {integer} index
555
+ * @return {string}
556
+ */
557
+ History.getHashByIndex = function(index){
558
+ // Prepare
559
+ var hash = null;
560
+
561
+ // Handle
562
+ if ( typeof index === 'undefined' ) {
563
+ // Get the last inserted
564
+ hash = History.savedHashes[History.savedHashes.length-1];
565
+ }
566
+ else if ( index < 0 ) {
567
+ // Get from the end
568
+ hash = History.savedHashes[History.savedHashes.length+index];
569
+ }
570
+ else {
571
+ // Get from the beginning
572
+ hash = History.savedHashes[index];
573
+ }
574
+
575
+ // Return hash
576
+ return hash;
577
+ };
578
+
579
+
580
+ // ====================================================================
581
+ // Discarded States
582
+
583
+ /**
584
+ * History.discardedHashes
585
+ * A hashed array of discarded hashes
586
+ */
587
+ History.discardedHashes = {};
588
+
589
+ /**
590
+ * History.discardedStates
591
+ * A hashed array of discarded states
592
+ */
593
+ History.discardedStates = {};
594
+
595
+ /**
596
+ * History.discardState(State)
597
+ * Discards the state by ignoring it through History
598
+ * @param {object} State
599
+ * @return {true}
600
+ */
601
+ History.discardState = function(discardedState,forwardState,backState){
602
+ //History.debug('History.discardState', arguments);
603
+ // Prepare
604
+ var discardedStateHash = History.getHashByState(discardedState),
605
+ discardObject;
606
+
607
+ // Create Discard Object
608
+ discardObject = {
609
+ 'discardedState': discardedState,
610
+ 'backState': backState,
611
+ 'forwardState': forwardState
612
+ };
613
+
614
+ // Add to DiscardedStates
615
+ History.discardedStates[discardedStateHash] = discardObject;
616
+
617
+ // Return true
618
+ return true;
619
+ };
620
+
621
+ /**
622
+ * History.discardHash(hash)
623
+ * Discards the hash by ignoring it through History
624
+ * @param {string} hash
625
+ * @return {true}
626
+ */
627
+ History.discardHash = function(discardedHash,forwardState,backState){
628
+ //History.debug('History.discardState', arguments);
629
+ // Create Discard Object
630
+ var discardObject = {
631
+ 'discardedHash': discardedHash,
632
+ 'backState': backState,
633
+ 'forwardState': forwardState
634
+ };
635
+
636
+ // Add to discardedHash
637
+ History.discardedHashes[discardedHash] = discardObject;
638
+
639
+ // Return true
640
+ return true;
641
+ };
642
+
643
+ /**
644
+ * History.discardState(State)
645
+ * Checks to see if the state is discarded
646
+ * @param {object} State
647
+ * @return {bool}
648
+ */
649
+ History.discardedState = function(State){
650
+ // Prepare
651
+ var StateHash = History.getHashByState(State),
652
+ discarded;
653
+
654
+ // Check
655
+ discarded = History.discardedStates[StateHash]||false;
656
+
657
+ // Return true
658
+ return discarded;
659
+ };
660
+
661
+ /**
662
+ * History.discardedHash(hash)
663
+ * Checks to see if the state is discarded
664
+ * @param {string} State
665
+ * @return {bool}
666
+ */
667
+ History.discardedHash = function(hash){
668
+ // Check
669
+ var discarded = History.discardedHashes[hash]||false;
670
+
671
+ // Return true
672
+ return discarded;
673
+ };
674
+
675
+ /**
676
+ * History.recycleState(State)
677
+ * Allows a discarded state to be used again
678
+ * @param {object} data
679
+ * @param {string} title
680
+ * @param {string} url
681
+ * @return {true}
682
+ */
683
+ History.recycleState = function(State){
684
+ //History.debug('History.recycleState', arguments);
685
+ // Prepare
686
+ var StateHash = History.getHashByState(State);
687
+
688
+ // Remove from DiscardedStates
689
+ if ( History.discardedState(State) ) {
690
+ delete History.discardedStates[StateHash];
691
+ }
692
+
693
+ // Return true
694
+ return true;
695
+ };
696
+
697
+
698
+ // ====================================================================
699
+ // HTML4 HashChange Support
700
+
701
+ if ( History.emulated.hashChange ) {
702
+ /*
703
+ * We must emulate the HTML4 HashChange Support by manually checking for hash changes
704
+ */
705
+
706
+ /**
707
+ * History.hashChangeInit()
708
+ * Init the HashChange Emulation
709
+ */
710
+ History.hashChangeInit = function(){
711
+ // Define our Checker Function
712
+ History.checkerFunction = null;
713
+
714
+ // Define some variables that will help in our checker function
715
+ var lastDocumentHash = '',
716
+ iframeId, iframe,
717
+ lastIframeHash, checkerRunning;
718
+
719
+ // Handle depending on the browser
720
+ if ( History.isInternetExplorer() ) {
721
+ // IE6 and IE7
722
+ // We need to use an iframe to emulate the back and forward buttons
723
+
724
+ // Create iFrame
725
+ iframeId = 'historyjs-iframe';
726
+ iframe = document.createElement('iframe');
727
+
728
+ // Adjust iFarme
729
+ iframe.setAttribute('id', iframeId);
730
+ iframe.style.display = 'none';
731
+
732
+ // Append iFrame
733
+ document.body.appendChild(iframe);
734
+
735
+ // Create initial history entry
736
+ iframe.contentWindow.document.open();
737
+ iframe.contentWindow.document.close();
738
+
739
+ // Define some variables that will help in our checker function
740
+ lastIframeHash = '';
741
+ checkerRunning = false;
742
+
743
+ // Define the checker function
744
+ History.checkerFunction = function(){
745
+ // Check Running
746
+ if ( checkerRunning ) {
747
+ return false;
748
+ }
749
+
750
+ // Update Running
751
+ checkerRunning = true;
752
+
753
+ // Fetch
754
+ var documentHash = History.getHash()||'',
755
+ iframeHash = History.unescapeHash(iframe.contentWindow.document.location.hash)||'';
756
+
757
+ // The Document Hash has changed (application caused)
758
+ if ( documentHash !== lastDocumentHash ) {
759
+ // Equalise
760
+ lastDocumentHash = documentHash;
761
+
762
+ // Create a history entry in the iframe
763
+ if ( iframeHash !== documentHash ) {
764
+ //History.debug('hashchange.checker: iframe hash change', 'documentHash (new):', documentHash, 'iframeHash (old):', iframeHash);
765
+
766
+ // Equalise
767
+ lastIframeHash = iframeHash = documentHash;
768
+
769
+ // Create History Entry
770
+ iframe.contentWindow.document.open();
771
+ iframe.contentWindow.document.close();
772
+
773
+ // Update the iframe's hash
774
+ iframe.contentWindow.document.location.hash = History.escapeHash(documentHash);
775
+ }
776
+
777
+ // Trigger Hashchange Event
778
+ History.Adapter.trigger(window,'hashchange');
779
+ }
780
+
781
+ // The iFrame Hash has changed (back button caused)
782
+ else if ( iframeHash !== lastIframeHash ) {
783
+ //History.debug('hashchange.checker: iframe hash out of sync', 'iframeHash (new):', iframeHash, 'documentHash (old):', documentHash);
784
+
785
+ // Equalise
786
+ lastIframeHash = iframeHash;
787
+
788
+ // Update the Hash
789
+ History.setHash(iframeHash,false);
790
+ }
791
+
792
+ // Reset Running
793
+ checkerRunning = false;
794
+
795
+ // Return true
796
+ return true;
797
+ };
798
+ }
799
+ else {
800
+ // We are not IE
801
+ // Firefox 1 or 2, Opera
802
+
803
+ // Define the checker function
804
+ History.checkerFunction = function(){
805
+ // Prepare
806
+ var documentHash = History.getHash();
807
+
808
+ // The Document Hash has changed (application caused)
809
+ if ( documentHash !== lastDocumentHash ) {
810
+ // Equalise
811
+ lastDocumentHash = documentHash;
812
+
813
+ // Trigger Hashchange Event
814
+ History.Adapter.trigger(window,'hashchange');
815
+ }
816
+
817
+ // Return true
818
+ return true;
819
+ };
820
+ }
821
+
822
+ // Apply the checker function
823
+ History.intervalList.push(setInterval(History.checkerFunction, History.options.hashChangeInterval));
824
+
825
+ // Done
826
+ return true;
827
+ }; // History.hashChangeInit
828
+
829
+ // Bind hashChangeInit
830
+ History.Adapter.onDomLoad(History.hashChangeInit);
831
+
832
+ } // History.emulated.hashChange
833
+
834
+
835
+ // ====================================================================
836
+ // HTML5 State Support
837
+
838
+ // Non-Native pushState Implementation
839
+ if ( History.emulated.pushState ) {
840
+ /*
841
+ * We must emulate the HTML5 State Management by using HTML4 HashChange
842
+ */
843
+
844
+ /**
845
+ * History.onHashChange(event)
846
+ * Trigger HTML5's window.onpopstate via HTML4 HashChange Support
847
+ */
848
+ History.onHashChange = function(event){
849
+ //History.debug('History.onHashChange', arguments);
850
+
851
+ // Prepare
852
+ var currentUrl = ((event && event.newURL) || document.location.href),
853
+ currentHash = History.getHashByUrl(currentUrl),
854
+ currentState = null,
855
+ currentStateHash = null,
856
+ currentStateHashExits = null,
857
+ discardObject;
858
+
859
+ // Check if we are the same state
860
+ if ( History.isLastHash(currentHash) ) {
861
+ // There has been no change (just the page's hash has finally propagated)
862
+ //History.debug('History.onHashChange: no change');
863
+ History.busy(false);
864
+ return false;
865
+ }
866
+
867
+ // Reset the double check
868
+ History.doubleCheckComplete();
869
+
870
+ // Store our location for use in detecting back/forward direction
871
+ History.saveHash(currentHash);
872
+
873
+ // Expand Hash
874
+ if ( currentHash && History.isTraditionalAnchor(currentHash) ) {
875
+ //History.debug('History.onHashChange: traditional anchor', currentHash);
876
+ // Traditional Anchor Hash
877
+ History.Adapter.trigger(window,'anchorchange');
878
+ History.busy(false);
879
+ return false;
880
+ }
881
+
882
+ // Create State
883
+ currentState = History.extractState(History.getFullUrl(currentHash||document.location.href,false),true);
884
+
885
+ // Check if we are the same state
886
+ if ( History.isLastSavedState(currentState) ) {
887
+ //History.debug('History.onHashChange: no change');
888
+ // There has been no change (just the page's hash has finally propagated)
889
+ History.busy(false);
890
+ return false;
891
+ }
892
+
893
+ // Create the state Hash
894
+ currentStateHash = History.getHashByState(currentState);
895
+
896
+ // Check if we are DiscardedState
897
+ discardObject = History.discardedState(currentState);
898
+ if ( discardObject ) {
899
+ // Ignore this state as it has been discarded and go back to the state before it
900
+ if ( History.getHashByIndex(-2) === History.getHashByState(discardObject.forwardState) ) {
901
+ // We are going backwards
902
+ //History.debug('History.onHashChange: go backwards');
903
+ History.back(false);
904
+ } else {
905
+ // We are going forwards
906
+ //History.debug('History.onHashChange: go forwards');
907
+ History.forward(false);
908
+ }
909
+ return false;
910
+ }
911
+
912
+ // Push the new HTML5 State
913
+ //History.debug('History.onHashChange: success hashchange');
914
+ History.pushState(currentState.data,currentState.title,currentState.url,false);
915
+
916
+ // End onHashChange closure
917
+ return true;
918
+ };
919
+ History.Adapter.bind(window,'hashchange',History.onHashChange);
920
+
921
+ /**
922
+ * History.pushState(data,title,url)
923
+ * Add a new State to the history object, become it, and trigger onpopstate
924
+ * We have to trigger for HTML4 compatibility
925
+ * @param {object} data
926
+ * @param {string} title
927
+ * @param {string} url
928
+ * @return {true}
929
+ */
930
+ History.pushState = function(data,title,url,queue){
931
+ //History.debug('History.pushState: called', arguments);
932
+
933
+ // Check the State
934
+ if ( History.getHashByUrl(url) ) {
935
+ throw new Error('History.js does not support states with fragement-identifiers (hashes/anchors).');
936
+ }
937
+
938
+ // Handle Queueing
939
+ if ( queue !== false && History.busy() ) {
940
+ // Wait + Push to Queue
941
+ //History.debug('History.pushState: we must wait', arguments);
942
+ History.pushQueue({
943
+ scope: History,
944
+ callback: History.pushState,
945
+ args: arguments,
946
+ queue: queue
947
+ });
948
+ return false;
949
+ }
950
+
951
+ // Make Busy
952
+ History.busy(true);
953
+
954
+ // Fetch the State Object
955
+ var newState = History.createStateObject(data,title,url),
956
+ newStateHash = History.getHashByState(newState),
957
+ oldState = History.getState(false),
958
+ oldStateHash = History.getHashByState(oldState),
959
+ html4Hash = History.getHash();
960
+
961
+ // Store the newState
962
+ History.storeState(newState);
963
+ History.expectedStateId = newState.id;
964
+
965
+ // Recycle the State
966
+ History.recycleState(newState);
967
+
968
+ // Force update of the title
969
+ // History.setTitle(newState);
970
+
971
+ // Check if we are the same State
972
+ if ( newStateHash === oldStateHash ) {
973
+ //History.debug('History.pushState: no change', newStateHash);
974
+ History.busy(false);
975
+ return false;
976
+ }
977
+
978
+ // Update HTML4 Hash
979
+ if ( newStateHash !== html4Hash && newStateHash !== History.getShortUrl(document.location.href) ) {
980
+ //History.debug('History.pushState: update hash', newStateHash, html4Hash);
981
+ History.setHash(newStateHash,false);
982
+ return false;
983
+ }
984
+
985
+ // Update HTML5 State
986
+ History.saveState(newState);
987
+
988
+ // Fire HTML5 Event
989
+ //History.debug('History.pushState: trigger popstate');
990
+ History.Adapter.trigger(window,'statechange');
991
+ History.busy(false);
992
+
993
+ // End pushState closure
994
+ return true;
995
+ };
996
+
997
+ /**
998
+ * History.replaceState(data,title,url)
999
+ * Replace the State and trigger onpopstate
1000
+ * We have to trigger for HTML4 compatibility
1001
+ * @param {object} data
1002
+ * @param {string} title
1003
+ * @param {string} url
1004
+ * @return {true}
1005
+ */
1006
+ History.replaceState = function(data,title,url,queue){
1007
+ //History.debug('History.replaceState: called', arguments);
1008
+
1009
+ // Check the State
1010
+ if ( History.getHashByUrl(url) ) {
1011
+ throw new Error('History.js does not support states with fragement-identifiers (hashes/anchors).');
1012
+ }
1013
+
1014
+ // Handle Queueing
1015
+ if ( queue !== false && History.busy() ) {
1016
+ // Wait + Push to Queue
1017
+ //History.debug('History.replaceState: we must wait', arguments);
1018
+ History.pushQueue({
1019
+ scope: History,
1020
+ callback: History.replaceState,
1021
+ args: arguments,
1022
+ queue: queue
1023
+ });
1024
+ return false;
1025
+ }
1026
+
1027
+ // Make Busy
1028
+ History.busy(true);
1029
+
1030
+ // Fetch the State Objects
1031
+ var newState = History.createStateObject(data,title,url),
1032
+ oldState = History.getState(false),
1033
+ previousState = History.getStateByIndex(-2);
1034
+
1035
+ // Discard Old State
1036
+ History.discardState(oldState,newState,previousState);
1037
+
1038
+ // Alias to PushState
1039
+ History.pushState(newState.data,newState.title,newState.url,false);
1040
+
1041
+ // End replaceState closure
1042
+ return true;
1043
+ };
1044
+
1045
+ } // History.emulated.pushState
1046
+
1047
+
1048
+
1049
+ // ====================================================================
1050
+ // Initialise
1051
+
1052
+ // Non-Native pushState Implementation
1053
+ if ( History.emulated.pushState ) {
1054
+ /**
1055
+ * Ensure initial state is handled correctly
1056
+ */
1057
+ if ( History.getHash() && !History.emulated.hashChange ) {
1058
+ History.Adapter.onDomLoad(function(){
1059
+ History.Adapter.trigger(window,'hashchange');
1060
+ });
1061
+ }
1062
+
1063
+ } // History.emulated.pushState
1064
+
1065
+ }; // History.initHtml4
1066
+
1067
+ // Try and Initialise History
1068
+ if ( typeof History.init !== 'undefined' ) {
1069
+ History.init();
1070
+ }
1071
+
1072
+ })(window);/**
1073
+ * History.js Core
1074
+ * @author Benjamin Arthur Lupton <contact@balupton.com>
1075
+ * @copyright 2010-2011 Benjamin Arthur Lupton <contact@balupton.com>
1076
+ * @license New BSD License <http://creativecommons.org/licenses/BSD/>
1077
+ */
1078
+
1079
+ (function(window,undefined){
1080
+ "use strict";
1081
+
1082
+ // ========================================================================
1083
+ // Initialise
1084
+
1085
+ // Localise Globals
1086
+ var
1087
+ console = window.console||undefined, // Prevent a JSLint complain
1088
+ document = window.document, // Make sure we are using the correct document
1089
+ navigator = window.navigator, // Make sure we are using the correct navigator
1090
+ sessionStorage = window.sessionStorage||false, // sessionStorage
1091
+ setTimeout = window.setTimeout,
1092
+ clearTimeout = window.clearTimeout,
1093
+ setInterval = window.setInterval,
1094
+ clearInterval = window.clearInterval,
1095
+ JSON = window.JSON,
1096
+ alert = window.alert,
1097
+ History = window.History = window.History||{}, // Public History Object
1098
+ history = window.history; // Old History Object
1099
+
1100
+ // MooTools Compatibility
1101
+ JSON.stringify = JSON.stringify||JSON.encode;
1102
+ JSON.parse = JSON.parse||JSON.decode;
1103
+
1104
+ // Check Existence
1105
+ if ( typeof History.init !== 'undefined' ) {
1106
+ throw new Error('History.js Core has already been loaded...');
1107
+ }
1108
+
1109
+ // Initialise History
1110
+ History.init = function(){
1111
+ // Check Load Status of Adapter
1112
+ if ( typeof History.Adapter === 'undefined' ) {
1113
+ return false;
1114
+ }
1115
+
1116
+ // Check Load Status of Core
1117
+ if ( typeof History.initCore !== 'undefined' ) {
1118
+ History.initCore();
1119
+ }
1120
+
1121
+ // Check Load Status of HTML4 Support
1122
+ if ( typeof History.initHtml4 !== 'undefined' ) {
1123
+ History.initHtml4();
1124
+ }
1125
+
1126
+ // Return true
1127
+ return true;
1128
+ };
1129
+
1130
+
1131
+ // ========================================================================
1132
+ // Initialise Core
1133
+
1134
+ // Initialise Core
1135
+ History.initCore = function(){
1136
+ // Initialise
1137
+ if ( typeof History.initCore.initialized !== 'undefined' ) {
1138
+ // Already Loaded
1139
+ return false;
1140
+ }
1141
+ else {
1142
+ History.initCore.initialized = true;
1143
+ }
1144
+
1145
+
1146
+ // ====================================================================
1147
+ // Options
1148
+
1149
+ /**
1150
+ * History.options
1151
+ * Configurable options
1152
+ */
1153
+ History.options = History.options||{};
1154
+
1155
+ /**
1156
+ * History.options.hashChangeInterval
1157
+ * How long should the interval be before hashchange checks
1158
+ */
1159
+ History.options.hashChangeInterval = History.options.hashChangeInterval || 100;
1160
+
1161
+ /**
1162
+ * History.options.safariPollInterval
1163
+ * How long should the interval be before safari poll checks
1164
+ */
1165
+ History.options.safariPollInterval = History.options.safariPollInterval || 500;
1166
+
1167
+ /**
1168
+ * History.options.doubleCheckInterval
1169
+ * How long should the interval be before we perform a double check
1170
+ */
1171
+ History.options.doubleCheckInterval = History.options.doubleCheckInterval || 500;
1172
+
1173
+ /**
1174
+ * History.options.storeInterval
1175
+ * How long should we wait between store calls
1176
+ */
1177
+ History.options.storeInterval = History.options.storeInterval || 1000;
1178
+
1179
+ /**
1180
+ * History.options.busyDelay
1181
+ * How long should we wait between busy events
1182
+ */
1183
+ History.options.busyDelay = History.options.busyDelay || 250;
1184
+
1185
+ /**
1186
+ * History.options.debug
1187
+ * If true will enable debug messages to be logged
1188
+ */
1189
+ History.options.debug = History.options.debug || false;
1190
+
1191
+ /**
1192
+ * History.options.initialTitle
1193
+ * What is the title of the initial state
1194
+ */
1195
+ History.options.initialTitle = History.options.initialTitle || document.title;
1196
+
1197
+
1198
+ // ====================================================================
1199
+ // Interval record
1200
+
1201
+ /**
1202
+ * History.intervalList
1203
+ * List of intervals set, to be cleared when document is unloaded.
1204
+ */
1205
+ History.intervalList = [];
1206
+
1207
+ /**
1208
+ * History.clearAllIntervals
1209
+ * Clears all setInterval instances.
1210
+ */
1211
+ History.clearAllIntervals = function(){
1212
+ var i, il = History.intervalList;
1213
+ if (typeof il !== "undefined" && il !== null) {
1214
+ for (i = 0; i < il.length; i++) {
1215
+ clearInterval(il[i]);
1216
+ }
1217
+ History.intervalList = null;
1218
+ }
1219
+ };
1220
+
1221
+
1222
+ // ====================================================================
1223
+ // Debug
1224
+
1225
+ /**
1226
+ * History.debug(message,...)
1227
+ * Logs the passed arguments if debug enabled
1228
+ */
1229
+ History.debug = function(){
1230
+ if ( (History.options.debug||false) ) {
1231
+ History.log.apply(History,arguments);
1232
+ }
1233
+ };
1234
+
1235
+ /**
1236
+ * History.log(message,...)
1237
+ * Logs the passed arguments
1238
+ */
1239
+ History.log = function(){
1240
+ // Prepare
1241
+ var
1242
+ consoleExists = !(typeof console === 'undefined' || typeof console.log === 'undefined' || typeof console.log.apply === 'undefined'),
1243
+ textarea = document.getElementById('log'),
1244
+ message,
1245
+ i,n,
1246
+ args,arg
1247
+ ;
1248
+
1249
+ // Write to Console
1250
+ if ( consoleExists ) {
1251
+ args = Array.prototype.slice.call(arguments);
1252
+ message = args.shift();
1253
+ if ( typeof console.debug !== 'undefined' ) {
1254
+ console.debug.apply(console,[message,args]);
1255
+ }
1256
+ else {
1257
+ console.log.apply(console,[message,args]);
1258
+ }
1259
+ }
1260
+ else {
1261
+ message = ("\n"+arguments[0]+"\n");
1262
+ }
1263
+
1264
+ // Write to log
1265
+ for ( i=1,n=arguments.length; i<n; ++i ) {
1266
+ arg = arguments[i];
1267
+ if ( typeof arg === 'object' && typeof JSON !== 'undefined' ) {
1268
+ try {
1269
+ arg = JSON.stringify(arg);
1270
+ }
1271
+ catch ( Exception ) {
1272
+ // Recursive Object
1273
+ }
1274
+ }
1275
+ message += "\n"+arg+"\n";
1276
+ }
1277
+
1278
+ // Textarea
1279
+ if ( textarea ) {
1280
+ textarea.value += message+"\n-----\n";
1281
+ textarea.scrollTop = textarea.scrollHeight - textarea.clientHeight;
1282
+ }
1283
+ // No Textarea, No Console
1284
+ else if ( !consoleExists ) {
1285
+ alert(message);
1286
+ }
1287
+
1288
+ // Return true
1289
+ return true;
1290
+ };
1291
+
1292
+
1293
+ // ====================================================================
1294
+ // Emulated Status
1295
+
1296
+ /**
1297
+ * History.getInternetExplorerMajorVersion()
1298
+ * Get's the major version of Internet Explorer
1299
+ * @return {integer}
1300
+ * @license Public Domain
1301
+ * @author Benjamin Arthur Lupton <contact@balupton.com>
1302
+ * @author James Padolsey <https://gist.github.com/527683>
1303
+ */
1304
+ History.getInternetExplorerMajorVersion = function(){
1305
+ var result = History.getInternetExplorerMajorVersion.cached =
1306
+ (typeof History.getInternetExplorerMajorVersion.cached !== 'undefined')
1307
+ ? History.getInternetExplorerMajorVersion.cached
1308
+ : (function(){
1309
+ var v = 3,
1310
+ div = document.createElement('div'),
1311
+ all = div.getElementsByTagName('i');
1312
+ while ( (div.innerHTML = '<!--[if gt IE ' + (++v) + ']><i></i><![endif]-->') && all[0] ) {}
1313
+ return (v > 4) ? v : false;
1314
+ })()
1315
+ ;
1316
+ return result;
1317
+ };
1318
+
1319
+ /**
1320
+ * History.isInternetExplorer()
1321
+ * Are we using Internet Explorer?
1322
+ * @return {boolean}
1323
+ * @license Public Domain
1324
+ * @author Benjamin Arthur Lupton <contact@balupton.com>
1325
+ */
1326
+ History.isInternetExplorer = function(){
1327
+ var result =
1328
+ History.isInternetExplorer.cached =
1329
+ (typeof History.isInternetExplorer.cached !== 'undefined')
1330
+ ? History.isInternetExplorer.cached
1331
+ : Boolean(History.getInternetExplorerMajorVersion())
1332
+ ;
1333
+ return result;
1334
+ };
1335
+
1336
+ /**
1337
+ * History.emulated
1338
+ * Which features require emulating?
1339
+ */
1340
+ History.emulated = {
1341
+ pushState: !Boolean(
1342
+ window.history && window.history.pushState && window.history.replaceState
1343
+ && !(
1344
+ (/ 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) */
1345
+ || (/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 */
1346
+ )
1347
+ ),
1348
+ hashChange: Boolean(
1349
+ !(('onhashchange' in window) || ('onhashchange' in document))
1350
+ ||
1351
+ (History.isInternetExplorer() && History.getInternetExplorerMajorVersion() < 8)
1352
+ )
1353
+ };
1354
+
1355
+ /**
1356
+ * History.enabled
1357
+ * Is History enabled?
1358
+ */
1359
+ History.enabled = !History.emulated.pushState;
1360
+
1361
+ /**
1362
+ * History.bugs
1363
+ * Which bugs are present
1364
+ */
1365
+ History.bugs = {
1366
+ /**
1367
+ * Safari 5 and Safari iOS 4 fail to return to the correct state once a hash is replaced by a `replaceState` call
1368
+ * https://bugs.webkit.org/show_bug.cgi?id=56249
1369
+ */
1370
+ setHash: Boolean(!History.emulated.pushState && navigator.vendor === 'Apple Computer, Inc.' && /AppleWebKit\/5([0-2]|3[0-3])/.test(navigator.userAgent)),
1371
+
1372
+ /**
1373
+ * Safari 5 and Safari iOS 4 sometimes fail to apply the state change under busy conditions
1374
+ * https://bugs.webkit.org/show_bug.cgi?id=42940
1375
+ */
1376
+ safariPoll: Boolean(!History.emulated.pushState && navigator.vendor === 'Apple Computer, Inc.' && /AppleWebKit\/5([0-2]|3[0-3])/.test(navigator.userAgent)),
1377
+
1378
+ /**
1379
+ * MSIE 6 and 7 sometimes do not apply a hash even it was told to (requiring a second call to the apply function)
1380
+ */
1381
+ ieDoubleCheck: Boolean(History.isInternetExplorer() && History.getInternetExplorerMajorVersion() < 8),
1382
+
1383
+ /**
1384
+ * MSIE 6 requires the entire hash to be encoded for the hashes to trigger the onHashChange event
1385
+ */
1386
+ hashEscape: Boolean(History.isInternetExplorer() && History.getInternetExplorerMajorVersion() < 7)
1387
+ };
1388
+
1389
+ /**
1390
+ * History.isEmptyObject(obj)
1391
+ * Checks to see if the Object is Empty
1392
+ * @param {Object} obj
1393
+ * @return {boolean}
1394
+ */
1395
+ History.isEmptyObject = function(obj) {
1396
+ for ( var name in obj ) {
1397
+ return false;
1398
+ }
1399
+ return true;
1400
+ };
1401
+
1402
+ /**
1403
+ * History.cloneObject(obj)
1404
+ * Clones a object and eliminate all references to the original contexts
1405
+ * @param {Object} obj
1406
+ * @return {Object}
1407
+ */
1408
+ History.cloneObject = function(obj) {
1409
+ var hash,newObj;
1410
+ if ( obj ) {
1411
+ hash = JSON.stringify(obj);
1412
+ newObj = JSON.parse(hash);
1413
+ }
1414
+ else {
1415
+ newObj = {};
1416
+ }
1417
+ return newObj;
1418
+ };
1419
+
1420
+
1421
+ // ====================================================================
1422
+ // URL Helpers
1423
+
1424
+ /**
1425
+ * History.getRootUrl()
1426
+ * Turns "http://mysite.com/dir/page.html?asd" into "http://mysite.com"
1427
+ * @return {String} rootUrl
1428
+ */
1429
+ History.getRootUrl = function(){
1430
+ // Create
1431
+ var rootUrl = document.location.protocol+'//'+(document.location.hostname||document.location.host);
1432
+ if ( document.location.port||false ) {
1433
+ rootUrl += ':'+document.location.port;
1434
+ }
1435
+ rootUrl += '/';
1436
+
1437
+ // Return
1438
+ return rootUrl;
1439
+ };
1440
+
1441
+ /**
1442
+ * History.getBaseHref()
1443
+ * Fetches the `href` attribute of the `<base href="...">` element if it exists
1444
+ * @return {String} baseHref
1445
+ */
1446
+ History.getBaseHref = function(){
1447
+ // Create
1448
+ var
1449
+ baseElements = document.getElementsByTagName('base'),
1450
+ baseElement = null,
1451
+ baseHref = '';
1452
+
1453
+ // Test for Base Element
1454
+ if ( baseElements.length === 1 ) {
1455
+ // Prepare for Base Element
1456
+ baseElement = baseElements[0];
1457
+ baseHref = baseElement.href.replace(/[^\/]+$/,'');
1458
+ }
1459
+
1460
+ // Adjust trailing slash
1461
+ baseHref = baseHref.replace(/\/+$/,'');
1462
+ if ( baseHref ) baseHref += '/';
1463
+
1464
+ // Return
1465
+ return baseHref;
1466
+ };
1467
+
1468
+ /**
1469
+ * History.getBaseUrl()
1470
+ * Fetches the baseHref or basePageUrl or rootUrl (whichever one exists first)
1471
+ * @return {String} baseUrl
1472
+ */
1473
+ History.getBaseUrl = function(){
1474
+ // Create
1475
+ var baseUrl = History.getBaseHref()||History.getBasePageUrl()||History.getRootUrl();
1476
+
1477
+ // Return
1478
+ return baseUrl;
1479
+ };
1480
+
1481
+ /**
1482
+ * History.getPageUrl()
1483
+ * Fetches the URL of the current page
1484
+ * @return {String} pageUrl
1485
+ */
1486
+ History.getPageUrl = function(){
1487
+ // Fetch
1488
+ var
1489
+ State = History.getState(false,false),
1490
+ stateUrl = (State||{}).url||document.location.href,
1491
+ pageUrl;
1492
+
1493
+ // Create
1494
+ pageUrl = stateUrl.replace(/\/+$/,'').replace(/[^\/]+$/,function(part,index,string){
1495
+ return (/\!/).test(part) ? part : part+'/';
1496
+ });
1497
+
1498
+ // Return
1499
+ return pageUrl;
1500
+ };
1501
+
1502
+ /**
1503
+ * History.getBasePageUrl()
1504
+ * Fetches the Url of the directory of the current page
1505
+ * @return {String} basePageUrl
1506
+ */
1507
+ History.getBasePageUrl = function(){
1508
+ // Create
1509
+ var basePageUrl = document.location.href.replace(/[#\?].*/,'').replace(/[^\/]+$/,function(part,index,string){
1510
+ return (/[^\/]$/).test(part) ? '' : part;
1511
+ }).replace(/\/+$/,'')+'/';
1512
+
1513
+ // Return
1514
+ return basePageUrl;
1515
+ };
1516
+
1517
+ /**
1518
+ * History.getFullUrl(url)
1519
+ * Ensures that we have an absolute URL and not a relative URL
1520
+ * @param {string} url
1521
+ * @param {Boolean} allowBaseHref
1522
+ * @return {string} fullUrl
1523
+ */
1524
+ History.getFullUrl = function(url,allowBaseHref){
1525
+ // Prepare
1526
+ var fullUrl = url, firstChar = url.substring(0,1);
1527
+ allowBaseHref = (typeof allowBaseHref === 'undefined') ? true : allowBaseHref;
1528
+
1529
+ // Check
1530
+ if ( /[a-z]+\:\/\//.test(url) ) {
1531
+ // Full URL
1532
+ }
1533
+ else if ( firstChar === '/' ) {
1534
+ // Root URL
1535
+ fullUrl = History.getRootUrl()+url.replace(/^\/+/,'');
1536
+ }
1537
+ else if ( firstChar === '#' ) {
1538
+ // Anchor URL
1539
+ fullUrl = History.getPageUrl().replace(/#!*/,'')+url;
1540
+ }
1541
+ else if ( firstChar === '?' ) {
1542
+ // Query URL
1543
+ fullUrl = History.getPageUrl().replace(/[\?#]!*/,'')+url;
1544
+ }
1545
+ else {
1546
+ // Relative URL
1547
+ if ( allowBaseHref ) {
1548
+ fullUrl = History.getBaseUrl()+url.replace(/^(\!\/)+/,'');
1549
+ } else {
1550
+ fullUrl = History.getBasePageUrl()+url.replace(/^(\!\/)+/,'');
1551
+ }
1552
+ // We have an if condition above as we do not want hashes
1553
+ // which are relative to the baseHref in our URLs
1554
+ // as if the baseHref changes, then all our bookmarks
1555
+ // would now point to different locations
1556
+ // whereas the basePageUrl will always stay the same
1557
+ }
1558
+
1559
+ // Return
1560
+ return fullUrl.replace(/\#$/,'');
1561
+ };
1562
+
1563
+ /**
1564
+ * History.getShortUrl(url)
1565
+ * Ensures that we have a relative URL and not a absolute URL
1566
+ * @param {string} url
1567
+ * @return {string} url
1568
+ */
1569
+ History.getShortUrl = function(url){
1570
+ // Prepare
1571
+ var shortUrl = url, baseUrl = History.getBaseUrl(), rootUrl = History.getRootUrl();
1572
+
1573
+ // Trim baseUrl
1574
+ if ( History.emulated.pushState ) {
1575
+ // We are in a if statement as when pushState is not emulated
1576
+ // The actual url these short urls are relative to can change
1577
+ // So within the same session, we the url may end up somewhere different
1578
+ shortUrl = shortUrl.replace(baseUrl,'');
1579
+ }
1580
+
1581
+ // Trim rootUrl
1582
+ shortUrl = shortUrl.replace(rootUrl,'/');
1583
+
1584
+ // Ensure we can still detect it as a state
1585
+ // if ( History.isTraditionalAnchor(shortUrl) ) {
1586
+ // shortUrl = './'+shortUrl;
1587
+ // // shortUrl = '/'+shortUrl;
1588
+ // }
1589
+
1590
+ shortUrl = '!/'+shortUrl;
1591
+
1592
+ // Clean It
1593
+ shortUrl = shortUrl.replace(/^(\!\/)+/g,'!/').replace(/\#$/,'');
1594
+
1595
+ // Return
1596
+ return shortUrl;
1597
+ };
1598
+
1599
+
1600
+ // ====================================================================
1601
+ // State Storage
1602
+
1603
+ /**
1604
+ * History.store
1605
+ * The store for all session specific data
1606
+ */
1607
+ History.store = {};
1608
+
1609
+ /**
1610
+ * History.idToState
1611
+ * 1-1: State ID to State Object
1612
+ */
1613
+ History.idToState = History.idToState||{};
1614
+
1615
+ /**
1616
+ * History.stateToId
1617
+ * 1-1: State String to State ID
1618
+ */
1619
+ History.stateToId = History.stateToId||{};
1620
+
1621
+ /**
1622
+ * History.urlToId
1623
+ * 1-1: State URL to State ID
1624
+ */
1625
+ History.urlToId = History.urlToId||{};
1626
+
1627
+ /**
1628
+ * History.storedStates
1629
+ * Store the states in an array
1630
+ */
1631
+ History.storedStates = History.storedStates||[];
1632
+
1633
+ /**
1634
+ * History.savedStates
1635
+ * Saved the states in an array
1636
+ */
1637
+ History.savedStates = History.savedStates||[];
1638
+
1639
+ /**
1640
+ * History.noramlizeStore()
1641
+ * Noramlize the store by adding necessary values
1642
+ */
1643
+ History.normalizeStore = function(){
1644
+ History.store.idToState = History.store.idToState||{};
1645
+ History.store.urlToId = History.store.urlToId||{};
1646
+ History.store.stateToId = History.store.stateToId||{};
1647
+ };
1648
+
1649
+ /**
1650
+ * History.getState()
1651
+ * Get an object containing the data, title and url of the current state
1652
+ * @param {Boolean} friendly
1653
+ * @param {Boolean} create
1654
+ * @return {Object} State
1655
+ */
1656
+ History.getState = function(friendly,create){
1657
+ // Prepare
1658
+ if ( typeof friendly === 'undefined' ) { friendly = true; }
1659
+ if ( typeof create === 'undefined' ) { create = true; }
1660
+
1661
+ // Fetch
1662
+ var State = History.getLastSavedState();
1663
+
1664
+ // Create
1665
+ if ( !State && create ) {
1666
+ State = History.createStateObject();
1667
+ }
1668
+
1669
+ // Adjust
1670
+ if ( friendly ) {
1671
+ State = History.cloneObject(State);
1672
+ State.url = State.cleanUrl||State.url;
1673
+ }
1674
+
1675
+ // Return
1676
+ return State;
1677
+ };
1678
+
1679
+ /**
1680
+ * History.getIdByState(State)
1681
+ * Gets a ID for a State
1682
+ * @param {State} newState
1683
+ * @return {String} id
1684
+ */
1685
+ History.getIdByState = function(newState){
1686
+
1687
+ // Fetch ID
1688
+ var id = History.extractId(newState.url),
1689
+ str;
1690
+
1691
+ if ( !id ) {
1692
+ // Find ID via State String
1693
+ str = History.getStateString(newState);
1694
+ if ( typeof History.stateToId[str] !== 'undefined' ) {
1695
+ id = History.stateToId[str];
1696
+ }
1697
+ else if ( typeof History.store.stateToId[str] !== 'undefined' ) {
1698
+ id = History.store.stateToId[str];
1699
+ }
1700
+ else {
1701
+ // Generate a new ID
1702
+ while ( true ) {
1703
+ id = (new Date()).getTime() + String(Math.random()).replace(/\D/g,'');
1704
+ if ( typeof History.idToState[id] === 'undefined' && typeof History.store.idToState[id] === 'undefined' ) {
1705
+ break;
1706
+ }
1707
+ }
1708
+
1709
+ // Apply the new State to the ID
1710
+ History.stateToId[str] = id;
1711
+ History.idToState[id] = newState;
1712
+ }
1713
+ }
1714
+
1715
+ // Return ID
1716
+ return id;
1717
+ };
1718
+
1719
+ /**
1720
+ * History.normalizeState(State)
1721
+ * Expands a State Object
1722
+ * @param {object} State
1723
+ * @return {object}
1724
+ */
1725
+ History.normalizeState = function(oldState){
1726
+ // Variables
1727
+ var newState, dataNotEmpty;
1728
+
1729
+ // Prepare
1730
+ if ( !oldState || (typeof oldState !== 'object') ) {
1731
+ oldState = {};
1732
+ }
1733
+
1734
+ // Check
1735
+ if ( typeof oldState.normalized !== 'undefined' ) {
1736
+ return oldState;
1737
+ }
1738
+
1739
+ // Adjust
1740
+ if ( !oldState.data || (typeof oldState.data !== 'object') ) {
1741
+ oldState.data = {};
1742
+ }
1743
+
1744
+ // ----------------------------------------------------------------
1745
+
1746
+ // Create
1747
+ newState = {};
1748
+ newState.normalized = true;
1749
+ newState.title = oldState.title||'';
1750
+ newState.url = History.getFullUrl(History.unescapeString(oldState.url||document.location.href));
1751
+ newState.hash = History.getShortUrl(newState.url);
1752
+ newState.data = History.cloneObject(oldState.data);
1753
+
1754
+ // Fetch ID
1755
+ newState.id = History.getIdByState(newState);
1756
+
1757
+ // ----------------------------------------------------------------
1758
+
1759
+ // Clean the URL
1760
+ newState.cleanUrl = newState.url.replace(/\??\&_suid.*/,'');
1761
+ newState.url = newState.cleanUrl;
1762
+
1763
+ // Check to see if we have more than just a url
1764
+ dataNotEmpty = !History.isEmptyObject(newState.data);
1765
+
1766
+ // Apply
1767
+ if ( newState.title || dataNotEmpty ) {
1768
+ // Add ID to Hash
1769
+ newState.hash = History.getShortUrl(newState.url).replace(/\??\&_suid.*/,'');
1770
+ if ( !/\?/.test(newState.hash) ) {
1771
+ newState.hash += '?';
1772
+ }
1773
+ newState.hash += '&_suid='+newState.id;
1774
+ }
1775
+
1776
+ // Create the Hashed URL
1777
+ newState.hashedUrl = History.getFullUrl(newState.hash);
1778
+
1779
+ // ----------------------------------------------------------------
1780
+
1781
+ // Update the URL if we have a duplicate
1782
+ if ( (History.emulated.pushState || History.bugs.safariPoll) && History.hasUrlDuplicate(newState) ) {
1783
+ newState.url = newState.hashedUrl;
1784
+ }
1785
+
1786
+ // ----------------------------------------------------------------
1787
+
1788
+ // Return
1789
+ return newState;
1790
+ };
1791
+
1792
+ /**
1793
+ * History.createStateObject(data,title,url)
1794
+ * Creates a object based on the data, title and url state params
1795
+ * @param {object} data
1796
+ * @param {string} title
1797
+ * @param {string} url
1798
+ * @return {object}
1799
+ */
1800
+ History.createStateObject = function(data,title,url){
1801
+ // Hashify
1802
+ var State = {
1803
+ 'data': data,
1804
+ 'title': title,
1805
+ 'url': url
1806
+ };
1807
+
1808
+ // Expand the State
1809
+ State = History.normalizeState(State);
1810
+
1811
+ // Return object
1812
+ return State;
1813
+ };
1814
+
1815
+ /**
1816
+ * History.getStateById(id)
1817
+ * Get a state by it's UID
1818
+ * @param {String} id
1819
+ */
1820
+ History.getStateById = function(id){
1821
+ // Prepare
1822
+ id = String(id);
1823
+
1824
+ // Retrieve
1825
+ var State = History.idToState[id] || History.store.idToState[id] || undefined;
1826
+
1827
+ // Return State
1828
+ return State;
1829
+ };
1830
+
1831
+ /**
1832
+ * Get a State's String
1833
+ * @param {State} passedState
1834
+ */
1835
+ History.getStateString = function(passedState){
1836
+ // Prepare
1837
+ var State, cleanedState, str;
1838
+
1839
+ // Fetch
1840
+ State = History.normalizeState(passedState);
1841
+
1842
+ // Clean
1843
+ cleanedState = {
1844
+ data: State.data,
1845
+ title: passedState.title,
1846
+ url: passedState.url
1847
+ };
1848
+
1849
+ // Fetch
1850
+ str = JSON.stringify(cleanedState);
1851
+
1852
+ // Return
1853
+ return str;
1854
+ };
1855
+
1856
+ /**
1857
+ * Get a State's ID
1858
+ * @param {State} passedState
1859
+ * @return {String} id
1860
+ */
1861
+ History.getStateId = function(passedState){
1862
+ // Prepare
1863
+ var State, id;
1864
+
1865
+ // Fetch
1866
+ State = History.normalizeState(passedState);
1867
+
1868
+ // Fetch
1869
+ id = State.id;
1870
+
1871
+ // Return
1872
+ return id;
1873
+ };
1874
+
1875
+ /**
1876
+ * History.getHashByState(State)
1877
+ * Creates a Hash for the State Object
1878
+ * @param {State} passedState
1879
+ * @return {String} hash
1880
+ */
1881
+ History.getHashByState = function(passedState){
1882
+ // Prepare
1883
+ var State, hash;
1884
+
1885
+ // Fetch
1886
+ State = History.normalizeState(passedState);
1887
+
1888
+ // Hash
1889
+ hash = State.hash;
1890
+
1891
+ // Return
1892
+ return hash;
1893
+ };
1894
+
1895
+ /**
1896
+ * History.extractId(url_or_hash)
1897
+ * Get a State ID by it's URL or Hash
1898
+ * @param {string} url_or_hash
1899
+ * @return {string} id
1900
+ */
1901
+ History.extractId = function ( url_or_hash ) {
1902
+ // Prepare
1903
+ var id,parts,url;
1904
+
1905
+ // Extract
1906
+ parts = /(.*)\&_suid=([0-9]+)$/.exec(url_or_hash);
1907
+ url = parts ? (parts[1]||url_or_hash) : url_or_hash;
1908
+ id = parts ? String(parts[2]||'') : '';
1909
+
1910
+ // Return
1911
+ return id||false;
1912
+ };
1913
+
1914
+ /**
1915
+ * History.isTraditionalAnchor
1916
+ * Checks to see if the url is a traditional anchor or not
1917
+ * @param {String} url_or_hash
1918
+ * @return {Boolean}
1919
+ */
1920
+ History.isTraditionalAnchor = function(url_or_hash){
1921
+ // Check
1922
+ var isTraditional = !(/[\/\?\.]/.test(url_or_hash));
1923
+
1924
+ // Return
1925
+ return isTraditional;
1926
+ };
1927
+
1928
+ /**
1929
+ * History.extractState
1930
+ * Get a State by it's URL or Hash
1931
+ * @param {String} url_or_hash
1932
+ * @return {State|null}
1933
+ */
1934
+ History.extractState = function(url_or_hash,create){
1935
+ // Prepare
1936
+ var State = null, id, url;
1937
+ create = create||false;
1938
+
1939
+ // Fetch SUID
1940
+ id = History.extractId(url_or_hash);
1941
+ if ( id ) {
1942
+ State = History.getStateById(id);
1943
+ }
1944
+
1945
+ // Fetch SUID returned no State
1946
+ if ( !State ) {
1947
+ // Fetch URL
1948
+ url = History.getFullUrl(url_or_hash);
1949
+
1950
+ // Check URL
1951
+ id = History.getIdByUrl(url)||false;
1952
+ if ( id ) {
1953
+ State = History.getStateById(id);
1954
+ }
1955
+
1956
+ // Create State
1957
+ if ( !State && create && !History.isTraditionalAnchor(url_or_hash) ) {
1958
+ State = History.createStateObject(null,null,url);
1959
+ }
1960
+ }
1961
+
1962
+ // Return
1963
+ return State;
1964
+ };
1965
+
1966
+ /**
1967
+ * History.getIdByUrl()
1968
+ * Get a State ID by a State URL
1969
+ */
1970
+ History.getIdByUrl = function(url){
1971
+ // Fetch
1972
+ var id = History.urlToId[url] || History.store.urlToId[url] || undefined;
1973
+
1974
+ // Return
1975
+ return id;
1976
+ };
1977
+
1978
+ /**
1979
+ * History.getLastSavedState()
1980
+ * Get an object containing the data, title and url of the current state
1981
+ * @return {Object} State
1982
+ */
1983
+ History.getLastSavedState = function(){
1984
+ return History.savedStates[History.savedStates.length-1]||undefined;
1985
+ };
1986
+
1987
+ /**
1988
+ * History.getLastStoredState()
1989
+ * Get an object containing the data, title and url of the current state
1990
+ * @return {Object} State
1991
+ */
1992
+ History.getLastStoredState = function(){
1993
+ return History.storedStates[History.storedStates.length-1]||undefined;
1994
+ };
1995
+
1996
+ /**
1997
+ * History.hasUrlDuplicate
1998
+ * Checks if a Url will have a url conflict
1999
+ * @param {Object} newState
2000
+ * @return {Boolean} hasDuplicate
2001
+ */
2002
+ History.hasUrlDuplicate = function(newState) {
2003
+ // Prepare
2004
+ var hasDuplicate = false,
2005
+ oldState;
2006
+
2007
+ // Fetch
2008
+ oldState = History.extractState(newState.url);
2009
+
2010
+ // Check
2011
+ hasDuplicate = oldState && oldState.id !== newState.id;
2012
+
2013
+ // Return
2014
+ return hasDuplicate;
2015
+ };
2016
+
2017
+ /**
2018
+ * History.storeState
2019
+ * Store a State
2020
+ * @param {Object} newState
2021
+ * @return {Object} newState
2022
+ */
2023
+ History.storeState = function(newState){
2024
+ // Store the State
2025
+ History.urlToId[newState.url] = newState.id;
2026
+
2027
+ // Push the State
2028
+ History.storedStates.push(History.cloneObject(newState));
2029
+
2030
+ // Return newState
2031
+ return newState;
2032
+ };
2033
+
2034
+ /**
2035
+ * History.isLastSavedState(newState)
2036
+ * Tests to see if the state is the last state
2037
+ * @param {Object} newState
2038
+ * @return {boolean} isLast
2039
+ */
2040
+ History.isLastSavedState = function(newState){
2041
+ // Prepare
2042
+ var isLast = false,
2043
+ newId, oldState, oldId;
2044
+
2045
+ // Check
2046
+ if ( History.savedStates.length ) {
2047
+ newId = newState.id;
2048
+ oldState = History.getLastSavedState();
2049
+ oldId = oldState.id;
2050
+
2051
+ // Check
2052
+ isLast = (newId === oldId);
2053
+ }
2054
+
2055
+ // Return
2056
+ return isLast;
2057
+ };
2058
+
2059
+ /**
2060
+ * History.saveState
2061
+ * Push a State
2062
+ * @param {Object} newState
2063
+ * @return {boolean} changed
2064
+ */
2065
+ History.saveState = function(newState){
2066
+ // Check Hash
2067
+ if ( History.isLastSavedState(newState) ) {
2068
+ return false;
2069
+ }
2070
+
2071
+ // Push the State
2072
+ History.savedStates.push(History.cloneObject(newState));
2073
+
2074
+ // Return true
2075
+ return true;
2076
+ };
2077
+
2078
+ /**
2079
+ * History.getStateByIndex()
2080
+ * Gets a state by the index
2081
+ * @param {integer} index
2082
+ * @return {Object}
2083
+ */
2084
+ History.getStateByIndex = function(index){
2085
+ // Prepare
2086
+ var State = null;
2087
+
2088
+ // Handle
2089
+ if ( typeof index === 'undefined' ) {
2090
+ // Get the last inserted
2091
+ State = History.savedStates[History.savedStates.length-1];
2092
+ }
2093
+ else if ( index < 0 ) {
2094
+ // Get from the end
2095
+ State = History.savedStates[History.savedStates.length+index];
2096
+ }
2097
+ else {
2098
+ // Get from the beginning
2099
+ State = History.savedStates[index];
2100
+ }
2101
+
2102
+ // Return State
2103
+ return State;
2104
+ };
2105
+
2106
+
2107
+ // ====================================================================
2108
+ // Hash Helpers
2109
+
2110
+ /**
2111
+ * History.getHash()
2112
+ * Gets the current document hash
2113
+ * @return {string}
2114
+ */
2115
+ History.getHash = function(){
2116
+ var hash = History.unescapeHash(document.location.hash);
2117
+ return hash;
2118
+ };
2119
+
2120
+ /**
2121
+ * History.unescapeString()
2122
+ * Unescape a string
2123
+ * @param {String} str
2124
+ * @return {string}
2125
+ */
2126
+ History.unescapeString = function(str){
2127
+ // Prepare
2128
+ var result = str,
2129
+ tmp;
2130
+
2131
+ // Unescape hash
2132
+ while ( true ) {
2133
+ tmp = window.decodeURI(result);
2134
+ if ( tmp === result ) {
2135
+ break;
2136
+ }
2137
+ result = tmp;
2138
+ }
2139
+
2140
+ // Return result
2141
+ return result;
2142
+ };
2143
+
2144
+ /**
2145
+ * History.unescapeHash()
2146
+ * normalize and Unescape a Hash
2147
+ * @param {String} hash
2148
+ * @return {string}
2149
+ */
2150
+ History.unescapeHash = function(hash){
2151
+ // Prepare
2152
+ var result = History.normalizeHash(hash);
2153
+
2154
+ // Unescape hash
2155
+ result = History.unescapeString(result);
2156
+
2157
+ // Return result
2158
+ return result;
2159
+ };
2160
+
2161
+ /**
2162
+ * History.normalizeHash()
2163
+ * normalize a hash across browsers
2164
+ * @return {string}
2165
+ */
2166
+ History.normalizeHash = function(hash){
2167
+ // Prepare
2168
+ var result = hash.replace(/[^#]*#/,'').replace(/#!*/, '');
2169
+
2170
+ // Return result
2171
+ return result;
2172
+ };
2173
+
2174
+ /**
2175
+ * History.setHash(hash)
2176
+ * Sets the document hash
2177
+ * @param {string} hash
2178
+ * @return {History}
2179
+ */
2180
+ History.setHash = function(hash,queue){
2181
+ // Prepare
2182
+ var adjustedHash, State, pageUrl;
2183
+
2184
+ // Handle Queueing
2185
+ if ( queue !== false && History.busy() ) {
2186
+ // Wait + Push to Queue
2187
+ //History.debug('History.setHash: we must wait', arguments);
2188
+ History.pushQueue({
2189
+ scope: History,
2190
+ callback: History.setHash,
2191
+ args: arguments,
2192
+ queue: queue
2193
+ });
2194
+ return false;
2195
+ }
2196
+
2197
+ // Log
2198
+ //History.debug('History.setHash: called',hash);
2199
+
2200
+ // Prepare
2201
+ adjustedHash = History.escapeHash(hash);
2202
+
2203
+ // Make Busy + Continue
2204
+ History.busy(true);
2205
+
2206
+ // Check if hash is a state
2207
+ State = History.extractState(hash,true);
2208
+ if ( State && !History.emulated.pushState ) {
2209
+ // Hash is a state so skip the setHash
2210
+ //History.debug('History.setHash: Hash is a state so skipping the hash set with a direct pushState call',arguments);
2211
+
2212
+ // PushState
2213
+ History.pushState(State.data,State.title,State.url,false);
2214
+ }
2215
+ else if ( document.location.hash !== adjustedHash ) {
2216
+ // Hash is a proper hash, so apply it
2217
+
2218
+ // Handle browser bugs
2219
+ if ( History.bugs.setHash ) {
2220
+ // Fix Safari Bug https://bugs.webkit.org/show_bug.cgi?id=56249
2221
+
2222
+ // Fetch the base page
2223
+ pageUrl = History.getPageUrl();
2224
+
2225
+ // Safari hash apply
2226
+ History.pushState(null,null,pageUrl+'#'+adjustedHash,false);
2227
+ }
2228
+ else {
2229
+ // Normal hash apply
2230
+ document.location.hash = adjustedHash;
2231
+ }
2232
+ }
2233
+
2234
+ // Chain
2235
+ return History;
2236
+ };
2237
+
2238
+ /**
2239
+ * History.escape()
2240
+ * normalize and Escape a Hash
2241
+ * @return {string}
2242
+ */
2243
+ History.escapeHash = function(hash){
2244
+ // Prepare
2245
+ var result = History.normalizeHash(hash);
2246
+
2247
+ // Escape hash
2248
+ result = window.encodeURI(result);
2249
+
2250
+ // IE6 Escape Bug
2251
+ if ( !History.bugs.hashEscape ) {
2252
+ // Restore common parts
2253
+ result = result
2254
+ .replace(/\%21/g,'!')
2255
+ .replace(/\%26/g,'&')
2256
+ .replace(/\%3D/g,'=')
2257
+ .replace(/\%3F/g,'?');
2258
+ }
2259
+
2260
+ // Return result
2261
+ return result;
2262
+ };
2263
+
2264
+ /**
2265
+ * History.getHashByUrl(url)
2266
+ * Extracts the Hash from a URL
2267
+ * @param {string} url
2268
+ * @return {string} url
2269
+ */
2270
+ History.getHashByUrl = function(url){
2271
+ // Extract the hash
2272
+ var hash = String(url)
2273
+ .replace(/([^#]*)#?([^#]*)#?(.*)/, '$2')
2274
+ ;
2275
+
2276
+ // Unescape hash
2277
+ hash = History.unescapeHash(hash);
2278
+
2279
+ // Return hash
2280
+ return hash;
2281
+ };
2282
+
2283
+ /**
2284
+ * History.setTitle(title)
2285
+ * Applies the title to the document
2286
+ * @param {State} newState
2287
+ * @return {Boolean}
2288
+ */
2289
+ History.setTitle = function(newState){
2290
+ // Prepare
2291
+ var title = newState.title,
2292
+ firstState;
2293
+
2294
+ // Initial
2295
+ if ( !title ) {
2296
+ firstState = History.getStateByIndex(0);
2297
+ if ( firstState && firstState.url === newState.url ) {
2298
+ title = firstState.title||History.options.initialTitle;
2299
+ }
2300
+ }
2301
+
2302
+ // Apply
2303
+ try {
2304
+ document.getElementsByTagName('title')[0].innerHTML = title.replace('<','&lt;').replace('>','&gt;').replace(' & ',' &amp; ');
2305
+ }
2306
+ catch ( Exception ) { }
2307
+ document.title = title;
2308
+
2309
+ // Chain
2310
+ return History;
2311
+ };
2312
+
2313
+
2314
+ // ====================================================================
2315
+ // Queueing
2316
+
2317
+ /**
2318
+ * History.queues
2319
+ * The list of queues to use
2320
+ * First In, First Out
2321
+ */
2322
+ History.queues = [];
2323
+
2324
+ /**
2325
+ * History.busy(value)
2326
+ * @param {boolean} value [optional]
2327
+ * @return {boolean} busy
2328
+ */
2329
+ History.busy = function(value){
2330
+ // Apply
2331
+ if ( typeof value !== 'undefined' ) {
2332
+ //History.debug('History.busy: changing ['+(History.busy.flag||false)+'] to ['+(value||false)+']', History.queues.length);
2333
+ History.busy.flag = value;
2334
+ }
2335
+ // Default
2336
+ else if ( typeof History.busy.flag === 'undefined' ) {
2337
+ History.busy.flag = false;
2338
+ }
2339
+
2340
+ // Queue
2341
+ if ( !History.busy.flag ) {
2342
+ // Execute the next item in the queue
2343
+ clearTimeout(History.busy.timeout);
2344
+ var fireNext = function(){
2345
+ var i, queue, item;
2346
+ if ( History.busy.flag ) return;
2347
+ for ( i=History.queues.length-1; i >= 0; --i ) {
2348
+ queue = History.queues[i];
2349
+ if ( queue.length === 0 ) continue;
2350
+ item = queue.shift();
2351
+ History.fireQueueItem(item);
2352
+ History.busy.timeout = setTimeout(fireNext,History.options.busyDelay);
2353
+ }
2354
+ };
2355
+ History.busy.timeout = setTimeout(fireNext,History.options.busyDelay);
2356
+ }
2357
+
2358
+ // Return
2359
+ return History.busy.flag;
2360
+ };
2361
+
2362
+ /**
2363
+ * History.busy.flag
2364
+ */
2365
+ History.busy.flag = false;
2366
+
2367
+ /**
2368
+ * History.fireQueueItem(item)
2369
+ * Fire a Queue Item
2370
+ * @param {Object} item
2371
+ * @return {Mixed} result
2372
+ */
2373
+ History.fireQueueItem = function(item){
2374
+ return item.callback.apply(item.scope||History,item.args||[]);
2375
+ };
2376
+
2377
+ /**
2378
+ * History.pushQueue(callback,args)
2379
+ * Add an item to the queue
2380
+ * @param {Object} item [scope,callback,args,queue]
2381
+ */
2382
+ History.pushQueue = function(item){
2383
+ // Prepare the queue
2384
+ History.queues[item.queue||0] = History.queues[item.queue||0]||[];
2385
+
2386
+ // Add to the queue
2387
+ History.queues[item.queue||0].push(item);
2388
+
2389
+ // Chain
2390
+ return History;
2391
+ };
2392
+
2393
+ /**
2394
+ * History.queue (item,queue), (func,queue), (func), (item)
2395
+ * Either firs the item now if not busy, or adds it to the queue
2396
+ */
2397
+ History.queue = function(item,queue){
2398
+ // Prepare
2399
+ if ( typeof item === 'function' ) {
2400
+ item = {
2401
+ callback: item
2402
+ };
2403
+ }
2404
+ if ( typeof queue !== 'undefined' ) {
2405
+ item.queue = queue;
2406
+ }
2407
+
2408
+ // Handle
2409
+ if ( History.busy() ) {
2410
+ History.pushQueue(item);
2411
+ } else {
2412
+ History.fireQueueItem(item);
2413
+ }
2414
+
2415
+ // Chain
2416
+ return History;
2417
+ };
2418
+
2419
+ /**
2420
+ * History.clearQueue()
2421
+ * Clears the Queue
2422
+ */
2423
+ History.clearQueue = function(){
2424
+ History.busy.flag = false;
2425
+ History.queues = [];
2426
+ return History;
2427
+ };
2428
+
2429
+
2430
+ // ====================================================================
2431
+ // IE Bug Fix
2432
+
2433
+ /**
2434
+ * History.stateChanged
2435
+ * States whether or not the state has changed since the last double check was initialised
2436
+ */
2437
+ History.stateChanged = false;
2438
+
2439
+ /**
2440
+ * History.doubleChecker
2441
+ * Contains the timeout used for the double checks
2442
+ */
2443
+ History.doubleChecker = false;
2444
+
2445
+ /**
2446
+ * History.doubleCheckComplete()
2447
+ * Complete a double check
2448
+ * @return {History}
2449
+ */
2450
+ History.doubleCheckComplete = function(){
2451
+ // Update
2452
+ History.stateChanged = true;
2453
+
2454
+ // Clear
2455
+ History.doubleCheckClear();
2456
+
2457
+ // Chain
2458
+ return History;
2459
+ };
2460
+
2461
+ /**
2462
+ * History.doubleCheckClear()
2463
+ * Clear a double check
2464
+ * @return {History}
2465
+ */
2466
+ History.doubleCheckClear = function(){
2467
+ // Clear
2468
+ if ( History.doubleChecker ) {
2469
+ clearTimeout(History.doubleChecker);
2470
+ History.doubleChecker = false;
2471
+ }
2472
+
2473
+ // Chain
2474
+ return History;
2475
+ };
2476
+
2477
+ /**
2478
+ * History.doubleCheck()
2479
+ * Create a double check
2480
+ * @return {History}
2481
+ */
2482
+ History.doubleCheck = function(tryAgain){
2483
+ // Reset
2484
+ History.stateChanged = false;
2485
+ History.doubleCheckClear();
2486
+
2487
+ // Fix IE6,IE7 bug where calling history.back or history.forward does not actually change the hash (whereas doing it manually does)
2488
+ // Fix Safari 5 bug where sometimes the state does not change: https://bugs.webkit.org/show_bug.cgi?id=42940
2489
+ if ( History.bugs.ieDoubleCheck ) {
2490
+ // Apply Check
2491
+ History.doubleChecker = setTimeout(
2492
+ function(){
2493
+ History.doubleCheckClear();
2494
+ if ( !History.stateChanged ) {
2495
+ //History.debug('History.doubleCheck: State has not yet changed, trying again', arguments);
2496
+ // Re-Attempt
2497
+ tryAgain();
2498
+ }
2499
+ return true;
2500
+ },
2501
+ History.options.doubleCheckInterval
2502
+ );
2503
+ }
2504
+
2505
+ // Chain
2506
+ return History;
2507
+ };
2508
+
2509
+
2510
+ // ====================================================================
2511
+ // Safari Bug Fix
2512
+
2513
+ /**
2514
+ * History.safariStatePoll()
2515
+ * Poll the current state
2516
+ * @return {History}
2517
+ */
2518
+ History.safariStatePoll = function(){
2519
+ // Poll the URL
2520
+
2521
+ // Get the Last State which has the new URL
2522
+ var
2523
+ urlState = History.extractState(document.location.href),
2524
+ newState;
2525
+
2526
+ // Check for a difference
2527
+ if ( !History.isLastSavedState(urlState) ) {
2528
+ newState = urlState;
2529
+ }
2530
+ else {
2531
+ return;
2532
+ }
2533
+
2534
+ // Check if we have a state with that url
2535
+ // If not create it
2536
+ if ( !newState ) {
2537
+ //History.debug('History.safariStatePoll: new');
2538
+ newState = History.createStateObject();
2539
+ }
2540
+
2541
+ // Apply the New State
2542
+ //History.debug('History.safariStatePoll: trigger');
2543
+ History.Adapter.trigger(window,'popstate');
2544
+
2545
+ // Chain
2546
+ return History;
2547
+ };
2548
+
2549
+
2550
+ // ====================================================================
2551
+ // State Aliases
2552
+
2553
+ /**
2554
+ * History.back(queue)
2555
+ * Send the browser history back one item
2556
+ * @param {Integer} queue [optional]
2557
+ */
2558
+ History.back = function(queue){
2559
+ //History.debug('History.back: called', arguments);
2560
+
2561
+ // Handle Queueing
2562
+ if ( queue !== false && History.busy() ) {
2563
+ // Wait + Push to Queue
2564
+ //History.debug('History.back: we must wait', arguments);
2565
+ History.pushQueue({
2566
+ scope: History,
2567
+ callback: History.back,
2568
+ args: arguments,
2569
+ queue: queue
2570
+ });
2571
+ return false;
2572
+ }
2573
+
2574
+ // Make Busy + Continue
2575
+ History.busy(true);
2576
+
2577
+ // Fix certain browser bugs that prevent the state from changing
2578
+ History.doubleCheck(function(){
2579
+ History.back(false);
2580
+ });
2581
+
2582
+ // Go back
2583
+ history.go(-1);
2584
+
2585
+ // End back closure
2586
+ return true;
2587
+ };
2588
+
2589
+ /**
2590
+ * History.forward(queue)
2591
+ * Send the browser history forward one item
2592
+ * @param {Integer} queue [optional]
2593
+ */
2594
+ History.forward = function(queue){
2595
+ //History.debug('History.forward: called', arguments);
2596
+
2597
+ // Handle Queueing
2598
+ if ( queue !== false && History.busy() ) {
2599
+ // Wait + Push to Queue
2600
+ //History.debug('History.forward: we must wait', arguments);
2601
+ History.pushQueue({
2602
+ scope: History,
2603
+ callback: History.forward,
2604
+ args: arguments,
2605
+ queue: queue
2606
+ });
2607
+ return false;
2608
+ }
2609
+
2610
+ // Make Busy + Continue
2611
+ History.busy(true);
2612
+
2613
+ // Fix certain browser bugs that prevent the state from changing
2614
+ History.doubleCheck(function(){
2615
+ History.forward(false);
2616
+ });
2617
+
2618
+ // Go forward
2619
+ history.go(1);
2620
+
2621
+ // End forward closure
2622
+ return true;
2623
+ };
2624
+
2625
+ /**
2626
+ * History.go(index,queue)
2627
+ * Send the browser history back or forward index times
2628
+ * @param {Integer} queue [optional]
2629
+ */
2630
+ History.go = function(index,queue){
2631
+ //History.debug('History.go: called', arguments);
2632
+
2633
+ // Prepare
2634
+ var i;
2635
+
2636
+ // Handle
2637
+ if ( index > 0 ) {
2638
+ // Forward
2639
+ for ( i=1; i<=index; ++i ) {
2640
+ History.forward(queue);
2641
+ }
2642
+ }
2643
+ else if ( index < 0 ) {
2644
+ // Backward
2645
+ for ( i=-1; i>=index; --i ) {
2646
+ History.back(queue);
2647
+ }
2648
+ }
2649
+ else {
2650
+ throw new Error('History.go: History.go requires a positive or negative integer passed.');
2651
+ }
2652
+
2653
+ // Chain
2654
+ return History;
2655
+ };
2656
+
2657
+
2658
+ // ====================================================================
2659
+ // HTML5 State Support
2660
+
2661
+ // Non-Native pushState Implementation
2662
+ if ( History.emulated.pushState ) {
2663
+ /*
2664
+ * Provide Skeleton for HTML4 Browsers
2665
+ */
2666
+
2667
+ // Prepare
2668
+ var emptyFunction = function(){};
2669
+ History.pushState = History.pushState||emptyFunction;
2670
+ History.replaceState = History.replaceState||emptyFunction;
2671
+ } // History.emulated.pushState
2672
+
2673
+ // Native pushState Implementation
2674
+ else {
2675
+ /*
2676
+ * Use native HTML5 History API Implementation
2677
+ */
2678
+
2679
+ /**
2680
+ * History.onPopState(event,extra)
2681
+ * Refresh the Current State
2682
+ */
2683
+ History.onPopState = function(event,extra){
2684
+ // Prepare
2685
+ var stateId = false, newState = false, currentHash, currentState;
2686
+
2687
+ // Reset the double check
2688
+ History.doubleCheckComplete();
2689
+
2690
+ // Check for a Hash, and handle apporiatly
2691
+ currentHash = History.getHash();
2692
+ if ( currentHash ) {
2693
+ // Expand Hash
2694
+ currentState = History.extractState(currentHash||document.location.href,true);
2695
+ if ( currentState ) {
2696
+ // We were able to parse it, it must be a State!
2697
+ // Let's forward to replaceState
2698
+ //History.debug('History.onPopState: state anchor', currentHash, currentState);
2699
+ History.replaceState(currentState.data, currentState.title, currentState.url, false);
2700
+ }
2701
+ else {
2702
+ // Traditional Anchor
2703
+ //History.debug('History.onPopState: traditional anchor', currentHash);
2704
+ History.Adapter.trigger(window,'anchorchange');
2705
+ History.busy(false);
2706
+ }
2707
+
2708
+ // We don't care for hashes
2709
+ History.expectedStateId = false;
2710
+ return false;
2711
+ }
2712
+
2713
+ // Ensure
2714
+ stateId = History.Adapter.extractEventData('state',event,extra) || false;
2715
+
2716
+ // Fetch State
2717
+ if ( stateId ) {
2718
+ // Vanilla: Back/forward button was used
2719
+ newState = History.getStateById(stateId);
2720
+ }
2721
+ else if ( History.expectedStateId ) {
2722
+ // Vanilla: A new state was pushed, and popstate was called manually
2723
+ newState = History.getStateById(History.expectedStateId);
2724
+ }
2725
+ else {
2726
+ // Initial State
2727
+ newState = History.extractState(document.location.href);
2728
+ }
2729
+
2730
+ // The State did not exist in our store
2731
+ if ( !newState ) {
2732
+ // Regenerate the State
2733
+ newState = History.createStateObject(null,null,document.location.href);
2734
+ }
2735
+
2736
+ // Clean
2737
+ History.expectedStateId = false;
2738
+
2739
+ // Check if we are the same state
2740
+ if ( History.isLastSavedState(newState) ) {
2741
+ // There has been no change (just the page's hash has finally propagated)
2742
+ //History.debug('History.onPopState: no change', newState, History.savedStates);
2743
+ History.busy(false);
2744
+ return false;
2745
+ }
2746
+
2747
+ // Store the State
2748
+ History.storeState(newState);
2749
+ History.saveState(newState);
2750
+
2751
+ // Force update of the title
2752
+ History.setTitle(newState);
2753
+
2754
+ // Fire Our Event
2755
+ History.Adapter.trigger(window,'statechange');
2756
+ History.busy(false);
2757
+
2758
+ // Return true
2759
+ return true;
2760
+ };
2761
+ History.Adapter.bind(window,'popstate',History.onPopState);
2762
+
2763
+ /**
2764
+ * History.pushState(data,title,url)
2765
+ * Add a new State to the history object, become it, and trigger onpopstate
2766
+ * We have to trigger for HTML4 compatibility
2767
+ * @param {object} data
2768
+ * @param {string} title
2769
+ * @param {string} url
2770
+ * @return {true}
2771
+ */
2772
+ History.pushState = function(data,title,url,queue){
2773
+ //History.debug('History.pushState: called', arguments);
2774
+
2775
+ // Check the State
2776
+ if ( History.getHashByUrl(url) && History.emulated.pushState ) {
2777
+ throw new Error('History.js does not support states with fragement-identifiers (hashes/anchors).');
2778
+ }
2779
+
2780
+ // Handle Queueing
2781
+ if ( queue !== false && History.busy() ) {
2782
+ // Wait + Push to Queue
2783
+ //History.debug('History.pushState: we must wait', arguments);
2784
+ History.pushQueue({
2785
+ scope: History,
2786
+ callback: History.pushState,
2787
+ args: arguments,
2788
+ queue: queue
2789
+ });
2790
+ return false;
2791
+ }
2792
+
2793
+ // Make Busy + Continue
2794
+ History.busy(true);
2795
+
2796
+ // Create the newState
2797
+ var newState = History.createStateObject(data,title,url);
2798
+
2799
+ // Check it
2800
+ if ( History.isLastSavedState(newState) ) {
2801
+ // Won't be a change
2802
+ History.busy(false);
2803
+ }
2804
+ else {
2805
+ // Store the newState
2806
+ History.storeState(newState);
2807
+ History.expectedStateId = newState.id;
2808
+
2809
+ // Push the newState
2810
+ history.pushState(newState.id,newState.title,newState.url);
2811
+
2812
+ // Fire HTML5 Event
2813
+ History.Adapter.trigger(window,'popstate');
2814
+ }
2815
+
2816
+ // End pushState closure
2817
+ return true;
2818
+ };
2819
+
2820
+ /**
2821
+ * History.replaceState(data,title,url)
2822
+ * Replace the State and trigger onpopstate
2823
+ * We have to trigger for HTML4 compatibility
2824
+ * @param {object} data
2825
+ * @param {string} title
2826
+ * @param {string} url
2827
+ * @return {true}
2828
+ */
2829
+ History.replaceState = function(data,title,url,queue){
2830
+ //History.debug('History.replaceState: called', arguments);
2831
+
2832
+ // Check the State
2833
+ if ( History.getHashByUrl(url) && History.emulated.pushState ) {
2834
+ throw new Error('History.js does not support states with fragement-identifiers (hashes/anchors).');
2835
+ }
2836
+
2837
+ // Handle Queueing
2838
+ if ( queue !== false && History.busy() ) {
2839
+ // Wait + Push to Queue
2840
+ //History.debug('History.replaceState: we must wait', arguments);
2841
+ History.pushQueue({
2842
+ scope: History,
2843
+ callback: History.replaceState,
2844
+ args: arguments,
2845
+ queue: queue
2846
+ });
2847
+ return false;
2848
+ }
2849
+
2850
+ // Make Busy + Continue
2851
+ History.busy(true);
2852
+
2853
+ // Create the newState
2854
+ var newState = History.createStateObject(data,title,url);
2855
+
2856
+ // Check it
2857
+ if ( History.isLastSavedState(newState) ) {
2858
+ // Won't be a change
2859
+ History.busy(false);
2860
+ }
2861
+ else {
2862
+ // Store the newState
2863
+ History.storeState(newState);
2864
+ History.expectedStateId = newState.id;
2865
+
2866
+ // Push the newState
2867
+ history.replaceState(newState.id,newState.title,newState.url);
2868
+
2869
+ // Fire HTML5 Event
2870
+ History.Adapter.trigger(window,'popstate');
2871
+ }
2872
+
2873
+ // End replaceState closure
2874
+ return true;
2875
+ };
2876
+
2877
+ } // !History.emulated.pushState
2878
+
2879
+
2880
+ // ====================================================================
2881
+ // Initialise
2882
+
2883
+ /**
2884
+ * Load the Store
2885
+ */
2886
+ if ( sessionStorage ) {
2887
+ // Fetch
2888
+ try {
2889
+ History.store = JSON.parse(sessionStorage.getItem('History.store'))||{};
2890
+ }
2891
+ catch ( err ) {
2892
+ History.store = {};
2893
+ }
2894
+
2895
+ // Normalize
2896
+ History.normalizeStore();
2897
+ }
2898
+ else {
2899
+ // Default Load
2900
+ History.store = {};
2901
+ History.normalizeStore();
2902
+ }
2903
+
2904
+ /**
2905
+ * Clear Intervals on exit to prevent memory leaks
2906
+ */
2907
+ History.Adapter.bind(window,"beforeunload",History.clearAllIntervals);
2908
+ History.Adapter.bind(window,"unload",History.clearAllIntervals);
2909
+
2910
+ /**
2911
+ * Create the initial State
2912
+ */
2913
+ History.saveState(History.storeState(History.extractState(document.location.href,true)));
2914
+
2915
+ /**
2916
+ * Bind for Saving Store
2917
+ */
2918
+ if ( sessionStorage ) {
2919
+ // When the page is closed
2920
+ History.onUnload = function(){
2921
+ // Prepare
2922
+ var currentStore, item;
2923
+
2924
+ // Fetch
2925
+ try {
2926
+ currentStore = JSON.parse(sessionStorage.getItem('History.store'))||{};
2927
+ }
2928
+ catch ( err ) {
2929
+ currentStore = {};
2930
+ }
2931
+
2932
+ // Ensure
2933
+ currentStore.idToState = currentStore.idToState || {};
2934
+ currentStore.urlToId = currentStore.urlToId || {};
2935
+ currentStore.stateToId = currentStore.stateToId || {};
2936
+
2937
+ // Sync
2938
+ for ( item in History.idToState ) {
2939
+ if ( !History.idToState.hasOwnProperty(item) ) {
2940
+ continue;
2941
+ }
2942
+ currentStore.idToState[item] = History.idToState[item];
2943
+ }
2944
+ for ( item in History.urlToId ) {
2945
+ if ( !History.urlToId.hasOwnProperty(item) ) {
2946
+ continue;
2947
+ }
2948
+ currentStore.urlToId[item] = History.urlToId[item];
2949
+ }
2950
+ for ( item in History.stateToId ) {
2951
+ if ( !History.stateToId.hasOwnProperty(item) ) {
2952
+ continue;
2953
+ }
2954
+ currentStore.stateToId[item] = History.stateToId[item];
2955
+ }
2956
+
2957
+ // Update
2958
+ History.store = currentStore;
2959
+ History.normalizeStore();
2960
+
2961
+ // Store
2962
+ sessionStorage.setItem('History.store',JSON.stringify(currentStore));
2963
+ };
2964
+
2965
+ // For Internet Explorer
2966
+ History.intervalList.push(setInterval(History.onUnload,History.options.storeInterval));
2967
+
2968
+ // For Other Browsers
2969
+ History.Adapter.bind(window,'beforeunload',History.onUnload);
2970
+ History.Adapter.bind(window,'unload',History.onUnload);
2971
+
2972
+ // Both are enabled for consistency
2973
+ }
2974
+
2975
+ // Non-Native pushState Implementation
2976
+ if ( !History.emulated.pushState ) {
2977
+ // Be aware, the following is only for native pushState implementations
2978
+ // If you are wanting to include something for all browsers
2979
+ // Then include it above this if block
2980
+
2981
+ /**
2982
+ * Setup Safari Fix
2983
+ */
2984
+ if ( History.bugs.safariPoll ) {
2985
+ History.intervalList.push(setInterval(History.safariStatePoll, History.options.safariPollInterval));
2986
+ }
2987
+
2988
+ /**
2989
+ * Ensure Cross Browser Compatibility
2990
+ */
2991
+ if ( navigator.vendor === 'Apple Computer, Inc.' || (navigator.appCodeName||'') === 'Mozilla' ) {
2992
+ /**
2993
+ * Fix Safari HashChange Issue
2994
+ */
2995
+
2996
+ // Setup Alias
2997
+ History.Adapter.bind(window,'hashchange',function(){
2998
+ History.Adapter.trigger(window,'popstate');
2999
+ });
3000
+
3001
+ // Initialise Alias
3002
+ if ( History.getHash() ) {
3003
+ History.Adapter.onDomLoad(function(){
3004
+ History.Adapter.trigger(window,'hashchange');
3005
+ });
3006
+ }
3007
+ }
3008
+
3009
+ } // !History.emulated.pushState
3010
+
3011
+
3012
+ }; // History.initCore
3013
+
3014
+ // Try and Initialise History
3015
+ History.init();
3016
+
3017
+ })(window);/*
3018
+ json2.js
3019
+ 2012-10-08
3020
+
3021
+ Public Domain.
3022
+
3023
+ NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
3024
+
3025
+ See http://www.JSON.org/js.html
3026
+
3027
+
3028
+ This code should be minified before deployment.
3029
+ See http://javascript.crockford.com/jsmin.html
3030
+
3031
+ USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO
3032
+ NOT CONTROL.
3033
+
3034
+
3035
+ This file creates a global JSON object containing two methods: stringify
3036
+ and parse.
3037
+
3038
+ JSON.stringify(value, replacer, space)
3039
+ value any JavaScript value, usually an object or array.
3040
+
3041
+ replacer an optional parameter that determines how object
3042
+ values are stringified for objects. It can be a
3043
+ function or an array of strings.
3044
+
3045
+ space an optional parameter that specifies the indentation
3046
+ of nested structures. If it is omitted, the text will
3047
+ be packed without extra whitespace. If it is a number,
3048
+ it will specify the number of spaces to indent at each
3049
+ level. If it is a string (such as '\t' or '&nbsp;'),
3050
+ it contains the characters used to indent at each level.
3051
+
3052
+ This method produces a JSON text from a JavaScript value.
3053
+
3054
+ When an object value is found, if the object contains a toJSON
3055
+ method, its toJSON method will be called and the result will be
3056
+ stringified. A toJSON method does not serialize: it returns the
3057
+ value represented by the name/value pair that should be serialized,
3058
+ or undefined if nothing should be serialized. The toJSON method
3059
+ will be passed the key associated with the value, and this will be
3060
+ bound to the value
3061
+
3062
+ For example, this would serialize Dates as ISO strings.
3063
+
3064
+ Date.prototype.toJSON = function (key) {
3065
+ function f(n) {
3066
+ // Format integers to have at least two digits.
3067
+ return n < 10 ? '0' + n : n;
3068
+ }
3069
+
3070
+ return this.getUTCFullYear() + '-' +
3071
+ f(this.getUTCMonth() + 1) + '-' +
3072
+ f(this.getUTCDate()) + 'T' +
3073
+ f(this.getUTCHours()) + ':' +
3074
+ f(this.getUTCMinutes()) + ':' +
3075
+ f(this.getUTCSeconds()) + 'Z';
3076
+ };
3077
+
3078
+ You can provide an optional replacer method. It will be passed the
3079
+ key and value of each member, with this bound to the containing
3080
+ object. The value that is returned from your method will be
3081
+ serialized. If your method returns undefined, then the member will
3082
+ be excluded from the serialization.
3083
+
3084
+ If the replacer parameter is an array of strings, then it will be
3085
+ used to select the members to be serialized. It filters the results
3086
+ such that only members with keys listed in the replacer array are
3087
+ stringified.
3088
+
3089
+ Values that do not have JSON representations, such as undefined or
3090
+ functions, will not be serialized. Such values in objects will be
3091
+ dropped; in arrays they will be replaced with null. You can use
3092
+ a replacer function to replace those with JSON values.
3093
+ JSON.stringify(undefined) returns undefined.
3094
+
3095
+ The optional space parameter produces a stringification of the
3096
+ value that is filled with line breaks and indentation to make it
3097
+ easier to read.
3098
+
3099
+ If the space parameter is a non-empty string, then that string will
3100
+ be used for indentation. If the space parameter is a number, then
3101
+ the indentation will be that many spaces.
3102
+
3103
+ Example:
3104
+
3105
+ text = JSON.stringify(['e', {pluribus: 'unum'}]);
3106
+ // text is '["e",{"pluribus":"unum"}]'
3107
+
3108
+
3109
+ text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t');
3110
+ // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]'
3111
+
3112
+ text = JSON.stringify([new Date()], function (key, value) {
3113
+ return this[key] instanceof Date ?
3114
+ 'Date(' + this[key] + ')' : value;
3115
+ });
3116
+ // text is '["Date(---current time---)"]'
3117
+
3118
+
3119
+ JSON.parse(text, reviver)
3120
+ This method parses a JSON text to produce an object or array.
3121
+ It can throw a SyntaxError exception.
3122
+
3123
+ The optional reviver parameter is a function that can filter and
3124
+ transform the results. It receives each of the keys and values,
3125
+ and its return value is used instead of the original value.
3126
+ If it returns what it received, then the structure is not modified.
3127
+ If it returns undefined then the member is deleted.
3128
+
3129
+ Example:
3130
+
3131
+ // Parse the text. Values that look like ISO date strings will
3132
+ // be converted to Date objects.
3133
+
3134
+ myData = JSON.parse(text, function (key, value) {
3135
+ var a;
3136
+ if (typeof value === 'string') {
3137
+ a =
3138
+ /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
3139
+ if (a) {
3140
+ return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4],
3141
+ +a[5], +a[6]));
3142
+ }
3143
+ }
3144
+ return value;
3145
+ });
3146
+
3147
+ myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) {
3148
+ var d;
3149
+ if (typeof value === 'string' &&
3150
+ value.slice(0, 5) === 'Date(' &&
3151
+ value.slice(-1) === ')') {
3152
+ d = new Date(value.slice(5, -1));
3153
+ if (d) {
3154
+ return d;
3155
+ }
3156
+ }
3157
+ return value;
3158
+ });
3159
+
3160
+
3161
+ This is a reference implementation. You are free to copy, modify, or
3162
+ redistribute.
3163
+ */
3164
+
3165
+ /*jslint evil: true, regexp: true */
3166
+
3167
+ /*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply,
3168
+ call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours,
3169
+ getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join,
3170
+ lastIndex, length, parse, prototype, push, replace, slice, stringify,
3171
+ test, toJSON, toString, valueOf
3172
+ */
3173
+
3174
+
3175
+ // Create a JSON object only if one does not already exist. We create the
3176
+ // methods in a closure to avoid creating global variables.
3177
+
3178
+ if (typeof JSON !== 'object') {
3179
+ JSON = {};
3180
+ }
3181
+
3182
+ (function () {
3183
+ 'use strict';
3184
+
3185
+ function f(n) {
3186
+ // Format integers to have at least two digits.
3187
+ return n < 10 ? '0' + n : n;
3188
+ }
3189
+
3190
+ if (typeof Date.prototype.toJSON !== 'function') {
3191
+
3192
+ Date.prototype.toJSON = function (key) {
3193
+
3194
+ return isFinite(this.valueOf())
3195
+ ? this.getUTCFullYear() + '-' +
3196
+ f(this.getUTCMonth() + 1) + '-' +
3197
+ f(this.getUTCDate()) + 'T' +
3198
+ f(this.getUTCHours()) + ':' +
3199
+ f(this.getUTCMinutes()) + ':' +
3200
+ f(this.getUTCSeconds()) + 'Z'
3201
+ : null;
3202
+ };
3203
+
3204
+ String.prototype.toJSON =
3205
+ Number.prototype.toJSON =
3206
+ Boolean.prototype.toJSON = function (key) {
3207
+ return this.valueOf();
3208
+ };
3209
+ }
3210
+
3211
+ var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
3212
+ escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
3213
+ gap,
3214
+ indent,
3215
+ meta = { // table of character substitutions
3216
+ '\b': '\\b',
3217
+ '\t': '\\t',
3218
+ '\n': '\\n',
3219
+ '\f': '\\f',
3220
+ '\r': '\\r',
3221
+ '"' : '\\"',
3222
+ '\\': '\\\\'
3223
+ },
3224
+ rep;
3225
+
3226
+
3227
+ function quote(string) {
3228
+
3229
+ // If the string contains no control characters, no quote characters, and no
3230
+ // backslash characters, then we can safely slap some quotes around it.
3231
+ // Otherwise we must also replace the offending characters with safe escape
3232
+ // sequences.
3233
+
3234
+ escapable.lastIndex = 0;
3235
+ return escapable.test(string) ? '"' + string.replace(escapable, function (a) {
3236
+ var c = meta[a];
3237
+ return typeof c === 'string'
3238
+ ? c
3239
+ : '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
3240
+ }) + '"' : '"' + string + '"';
3241
+ }
3242
+
3243
+
3244
+ function str(key, holder) {
3245
+
3246
+ // Produce a string from holder[key].
3247
+
3248
+ var i, // The loop counter.
3249
+ k, // The member key.
3250
+ v, // The member value.
3251
+ length,
3252
+ mind = gap,
3253
+ partial,
3254
+ value = holder[key];
3255
+
3256
+ // If the value has a toJSON method, call it to obtain a replacement value.
3257
+
3258
+ if (value && typeof value === 'object' &&
3259
+ typeof value.toJSON === 'function') {
3260
+ value = value.toJSON(key);
3261
+ }
3262
+
3263
+ // If we were called with a replacer function, then call the replacer to
3264
+ // obtain a replacement value.
3265
+
3266
+ if (typeof rep === 'function') {
3267
+ value = rep.call(holder, key, value);
3268
+ }
3269
+
3270
+ // What happens next depends on the value's type.
3271
+
3272
+ switch (typeof value) {
3273
+ case 'string':
3274
+ return quote(value);
3275
+
3276
+ case 'number':
3277
+
3278
+ // JSON numbers must be finite. Encode non-finite numbers as null.
3279
+
3280
+ return isFinite(value) ? String(value) : 'null';
3281
+
3282
+ case 'boolean':
3283
+ case 'null':
3284
+
3285
+ // If the value is a boolean or null, convert it to a string. Note:
3286
+ // typeof null does not produce 'null'. The case is included here in
3287
+ // the remote chance that this gets fixed someday.
3288
+
3289
+ return String(value);
3290
+
3291
+ // If the type is 'object', we might be dealing with an object or an array or
3292
+ // null.
3293
+
3294
+ case 'object':
3295
+
3296
+ // Due to a specification blunder in ECMAScript, typeof null is 'object',
3297
+ // so watch out for that case.
3298
+
3299
+ if (!value) {
3300
+ return 'null';
3301
+ }
3302
+
3303
+ // Make an array to hold the partial results of stringifying this object value.
3304
+
3305
+ gap += indent;
3306
+ partial = [];
3307
+
3308
+ // Is the value an array?
3309
+
3310
+ if (Object.prototype.toString.apply(value) === '[object Array]') {
3311
+
3312
+ // The value is an array. Stringify every element. Use null as a placeholder
3313
+ // for non-JSON values.
3314
+
3315
+ length = value.length;
3316
+ for (i = 0; i < length; i += 1) {
3317
+ partial[i] = str(i, value) || 'null';
3318
+ }
3319
+
3320
+ // Join all of the elements together, separated with commas, and wrap them in
3321
+ // brackets.
3322
+
3323
+ v = partial.length === 0
3324
+ ? '[]'
3325
+ : gap
3326
+ ? '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']'
3327
+ : '[' + partial.join(',') + ']';
3328
+ gap = mind;
3329
+ return v;
3330
+ }
3331
+
3332
+ // If the replacer is an array, use it to select the members to be stringified.
3333
+
3334
+ if (rep && typeof rep === 'object') {
3335
+ length = rep.length;
3336
+ for (i = 0; i < length; i += 1) {
3337
+ if (typeof rep[i] === 'string') {
3338
+ k = rep[i];
3339
+ v = str(k, value);
3340
+ if (v) {
3341
+ partial.push(quote(k) + (gap ? ': ' : ':') + v);
3342
+ }
3343
+ }
3344
+ }
3345
+ } else {
3346
+
3347
+ // Otherwise, iterate through all of the keys in the object.
3348
+
3349
+ for (k in value) {
3350
+ if (Object.prototype.hasOwnProperty.call(value, k)) {
3351
+ v = str(k, value);
3352
+ if (v) {
3353
+ partial.push(quote(k) + (gap ? ': ' : ':') + v);
3354
+ }
3355
+ }
3356
+ }
3357
+ }
3358
+
3359
+ // Join all of the member texts together, separated with commas,
3360
+ // and wrap them in braces.
3361
+
3362
+ v = partial.length === 0
3363
+ ? '{}'
3364
+ : gap
3365
+ ? '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}'
3366
+ : '{' + partial.join(',') + '}';
3367
+ gap = mind;
3368
+ return v;
3369
+ }
3370
+ }
3371
+
3372
+ // If the JSON object does not yet have a stringify method, give it one.
3373
+
3374
+ if (typeof JSON.stringify !== 'function') {
3375
+ JSON.stringify = function (value, replacer, space) {
3376
+
3377
+ // The stringify method takes a value and an optional replacer, and an optional
3378
+ // space parameter, and returns a JSON text. The replacer can be a function
3379
+ // that can replace values, or an array of strings that will select the keys.
3380
+ // A default replacer method can be provided. Use of the space parameter can
3381
+ // produce text that is more easily readable.
3382
+
3383
+ var i;
3384
+ gap = '';
3385
+ indent = '';
3386
+
3387
+ // If the space parameter is a number, make an indent string containing that
3388
+ // many spaces.
3389
+
3390
+ if (typeof space === 'number') {
3391
+ for (i = 0; i < space; i += 1) {
3392
+ indent += ' ';
3393
+ }
3394
+
3395
+ // If the space parameter is a string, it will be used as the indent string.
3396
+
3397
+ } else if (typeof space === 'string') {
3398
+ indent = space;
3399
+ }
3400
+
3401
+ // If there is a replacer, it must be a function or an array.
3402
+ // Otherwise, throw an error.
3403
+
3404
+ rep = replacer;
3405
+ if (replacer && typeof replacer !== 'function' &&
3406
+ (typeof replacer !== 'object' ||
3407
+ typeof replacer.length !== 'number')) {
3408
+ throw new Error('JSON.stringify');
3409
+ }
3410
+
3411
+ // Make a fake root object containing our value under the key of ''.
3412
+ // Return the result of stringifying the value.
3413
+
3414
+ return str('', {'': value});
3415
+ };
3416
+ }
3417
+
3418
+
3419
+ // If the JSON object does not yet have a parse method, give it one.
3420
+
3421
+ if (typeof JSON.parse !== 'function') {
3422
+ JSON.parse = function (text, reviver) {
3423
+
3424
+ // The parse method takes a text and an optional reviver function, and returns
3425
+ // a JavaScript value if the text is a valid JSON text.
3426
+
3427
+ var j;
3428
+
3429
+ function walk(holder, key) {
3430
+
3431
+ // The walk method is used to recursively walk the resulting structure so
3432
+ // that modifications can be made.
3433
+
3434
+ var k, v, value = holder[key];
3435
+ if (value && typeof value === 'object') {
3436
+ for (k in value) {
3437
+ if (Object.prototype.hasOwnProperty.call(value, k)) {
3438
+ v = walk(value, k);
3439
+ if (v !== undefined) {
3440
+ value[k] = v;
3441
+ } else {
3442
+ delete value[k];
3443
+ }
3444
+ }
3445
+ }
3446
+ }
3447
+ return reviver.call(holder, key, value);
3448
+ }
3449
+
3450
+
3451
+ // Parsing happens in four stages. In the first stage, we replace certain
3452
+ // Unicode characters with escape sequences. JavaScript handles many characters
3453
+ // incorrectly, either silently deleting them, or treating them as line endings.
3454
+
3455
+ text = String(text);
3456
+ cx.lastIndex = 0;
3457
+ if (cx.test(text)) {
3458
+ text = text.replace(cx, function (a) {
3459
+ return '\\u' +
3460
+ ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
3461
+ });
3462
+ }
3463
+
3464
+ // In the second stage, we run the text against regular expressions that look
3465
+ // for non-JSON patterns. We are especially concerned with '()' and 'new'
3466
+ // because they can cause invocation, and '=' because it can cause mutation.
3467
+ // But just to be safe, we want to reject all unexpected forms.
3468
+
3469
+ // We split the second stage into 4 regexp operations in order to work around
3470
+ // crippling inefficiencies in IE's and Safari's regexp engines. First we
3471
+ // replace the JSON backslash pairs with '@' (a non-JSON character). Second, we
3472
+ // replace all simple value tokens with ']' characters. Third, we delete all
3473
+ // open brackets that follow a colon or comma or that begin the text. Finally,
3474
+ // we look to see that the remaining characters are only whitespace or ']' or
3475
+ // ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval.
3476
+
3477
+ if (/^[\],:{}\s]*$/
3478
+ .test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@')
3479
+ .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']')
3480
+ .replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {
3481
+
3482
+ // In the third stage we use the eval function to compile the text into a
3483
+ // JavaScript structure. The '{' operator is subject to a syntactic ambiguity
3484
+ // in JavaScript: it can begin a block or an object literal. We wrap the text
3485
+ // in parens to eliminate the ambiguity.
3486
+
3487
+ j = eval('(' + text + ')');
3488
+
3489
+ // In the optional fourth stage, we recursively walk the new structure, passing
3490
+ // each name/value pair to a reviver function for possible transformation.
3491
+
3492
+ return typeof reviver === 'function'
3493
+ ? walk({'': j}, '')
3494
+ : j;
3495
+ }
3496
+
3497
+ // If the text is not JSON parseable, then a SyntaxError is thrown.
3498
+
3499
+ throw new SyntaxError('JSON.parse');
3500
+ };
3501
+ }
3502
+ }());