colorgy_style 0.0.0.2 → 0.0.0.4

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