wiselinks-artirix 1.3.0

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