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