wiselinks 0.7.3 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- wiselinks (0.7.3)
4
+ wiselinks (1.0.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
@@ -33,7 +33,7 @@ GEM
33
33
  activesupport (3.2.13)
34
34
  i18n (= 0.6.1)
35
35
  multi_json (~> 1.0)
36
- addressable (2.3.4)
36
+ addressable (2.3.5)
37
37
  arel (3.0.2)
38
38
  builder (3.0.4)
39
39
  capybara (2.1.0)
@@ -49,7 +49,7 @@ GEM
49
49
  coffee-script (2.2.0)
50
50
  coffee-script-source
51
51
  execjs
52
- coffee-script-source (1.6.2)
52
+ coffee-script-source (1.6.3)
53
53
  coffeelint (0.0.6)
54
54
  coffee-script
55
55
  colorize (0.5.8)
@@ -77,7 +77,7 @@ GEM
77
77
  mime-types (~> 1.16)
78
78
  treetop (~> 1.4.8)
79
79
  mime-types (1.23)
80
- mini_portile (0.5.0)
80
+ mini_portile (0.5.1)
81
81
  multi_json (1.7.7)
82
82
  nokogiri (1.6.0)
83
83
  mini_portile (~> 0.5.0)
@@ -109,22 +109,22 @@ GEM
109
109
  json (~> 1.4)
110
110
  rest-client (1.6.7)
111
111
  mime-types (>= 1.16)
112
- rspec (2.13.0)
113
- rspec-core (~> 2.13.0)
114
- rspec-expectations (~> 2.13.0)
115
- rspec-mocks (~> 2.13.0)
116
- rspec-core (2.13.1)
117
- rspec-expectations (2.13.0)
112
+ rspec (2.14.0)
113
+ rspec-core (~> 2.14.0)
114
+ rspec-expectations (~> 2.14.0)
115
+ rspec-mocks (~> 2.14.0)
116
+ rspec-core (2.14.2)
117
+ rspec-expectations (2.14.0)
118
118
  diff-lcs (>= 1.1.3, < 2.0)
119
- rspec-mocks (2.13.1)
120
- rspec-rails (2.13.2)
119
+ rspec-mocks (2.14.1)
120
+ rspec-rails (2.14.0)
121
121
  actionpack (>= 3.0)
122
122
  activesupport (>= 3.0)
123
123
  railties (>= 3.0)
124
- rspec-core (~> 2.13.0)
125
- rspec-expectations (~> 2.13.0)
126
- rspec-mocks (~> 2.13.0)
127
- safe_yaml (0.9.3)
124
+ rspec-core (~> 2.14.0)
125
+ rspec-expectations (~> 2.14.0)
126
+ rspec-mocks (~> 2.14.0)
127
+ safe_yaml (0.9.4)
128
128
  shoulda (3.5.0)
129
129
  shoulda-context (~> 1.0, >= 1.0.1)
130
130
  shoulda-matchers (>= 1.4.1, < 3.0)
data/README.md CHANGED
@@ -7,9 +7,9 @@
7
7
 
8
8
  #Wiselinks
9
9
 
10
- Wiselinks makes following links and submitting some forms in your web application faster.
10
+ Wiselinks makes following links and submitting forms in your web application faster.
11
11
 
12
- You may find Wiselinks similar to [Turbolinks](https://github.com/rails/turbolinks) or [Pjax](https://github.com/defunkt/jquery-pjax), but Wiselinks have several rather important differences from both projects. We tried to make Wiselinks as easy to use as Turbolinks are but also as configurable as Pjax.
12
+ You may find Wiselinks similar to [Turbolinks](https://github.com/rails/turbolinks) or [Pjax](https://github.com/defunkt/jquery-pjax), but Wiselinks works as a whitelist rather than blacklist. We tried to make Wiselinks as easy to use as Turbolinks are but also as configurable as Pjax.
13
13
 
14
14
  Try Wiselinks online in our **demo application**:
15
15
 
@@ -17,7 +17,7 @@ Try Wiselinks online in our **demo application**:
17
17
 
18
18
  ## Compatibility
19
19
 
20
- **Please be advised that Javascript events in wiselinks-0.5.0 are not backward compatible.**
20
+ **Please be advised that Javascript events in wiselinks-0.5.0 are not backward compatible.**
21
21
 
22
22
  Wiselinks uses [History.js](https://github.com/balupton/History.js/) library to perform requests.
23
23
 
@@ -36,7 +36,7 @@ Wiselinks works in all major browsers including browsers that do not support HTM
36
36
  </thead>
37
37
  <tbody>
38
38
  <tr>
39
- <td>Work in HTML5 browsers</td>
39
+ <td>Work in HTML5 browsers</td>
40
40
  <td><strong>Yes</strong></td>
41
41
  <td>Yes</td>
42
42
  <td>Yes</td>
@@ -54,31 +54,31 @@ Wiselinks works in all major browsers including browsers that do not support HTM
54
54
  <td>No, degrades to normal request processing</td>
55
55
  </tr>
56
56
  <tr>
57
- <td>Form processing</td>
57
+ <td>Form processing</td>
58
58
  <td><strong>Yes</strong></td>
59
59
  <td>No</td>
60
60
  <td>Yes (experimental feature)</td>
61
61
  </tr>
62
62
  <tr>
63
- <td>Form blank values exclusion</td>
63
+ <td>Form blank values exclusion</td>
64
64
  <td><strong>Yes</strong> (optional)</td>
65
65
  <td>No</td>
66
66
  <td>No</td>
67
67
  </tr>
68
68
  <tr>
69
- <td>Form values optimization</td>
69
+ <td>Form values optimization</td>
70
70
  <td><strong>Yes</strong></td>
71
71
  <td>No</td>
72
72
  <td>No</td>
73
73
  </tr>
74
74
  <tr>
75
- <td>Assets change detection</td>
75
+ <td>Assets change detection</td>
76
76
  <td><strong>Yes</strong>, by calculating assets MD5 hash on boot</td>
77
77
  <td>Yes, by parsing document head on every request</td>
78
78
  <td>No</td>
79
79
  </tr>
80
80
  <tr>
81
- <td>30x HTTP redirects processing</td>
81
+ <td>30x HTTP redirects processing</td>
82
82
  <td><strong>Yes</strong></td>
83
83
  <td>No</td>
84
84
  <td>Yes</td>
@@ -97,7 +97,7 @@ gem 'wiselinks'
97
97
  ```
98
98
 
99
99
  Then do:
100
-
100
+
101
101
  bundle install
102
102
 
103
103
  Restart your server and you're now using wiselinks!
@@ -111,44 +111,44 @@ Copy `wiselinks-x.y.z.js` or `wiselinks-x.y.z.min.js` from `build` folder in thi
111
111
  ### CoffeeScript
112
112
 
113
113
  Create Wiselinks object in your `application.js.coffee`:
114
-
115
- ```coffeescript
114
+
115
+ ```coffeescript
116
116
  #= require jquery
117
117
  #= require wiselinks
118
118
 
119
119
  $(document).ready ->
120
120
  window.wiselinks = new Wiselinks()
121
- ```
121
+ ```
122
122
 
123
123
  You can disable HTML4 browsers support easily:
124
124
 
125
- ```coffeescript
125
+ ```coffeescript
126
126
  #= require jquery
127
127
  #= require wiselinks
128
128
 
129
129
  $(document).ready ->
130
130
  window.wiselinks = new Wiselinks($('body'), html4: false )
131
- ```
131
+ ```
132
132
 
133
133
 
134
134
  Or you can add some more options, if you want:
135
135
 
136
- ```coffeescript
136
+ ```coffeescript
137
137
  #= require jquery
138
138
  #= require jquery.role
139
139
  #= require wiselinks
140
140
 
141
141
  $(document).ready ->
142
- # DOM element with id = "content" will be replaced after data load.
142
+ # DOM element with id = "content" will be replaced after data load.
143
143
  window.wiselinks = new Wiselinks($('#content'))
144
-
144
+
145
145
  $(document).off('page:loading').on(
146
146
  'page:loading'
147
147
  (event, $target, render, url) ->
148
148
  console.log("Loading: #{url} to #{$target.selector} within '#{render}'")
149
149
  # code to start loading animation
150
150
  )
151
-
151
+
152
152
  $(document).off('page:redirected').on(
153
153
  'page:redirected'
154
154
  (event, $target, render, url) ->
@@ -183,11 +183,11 @@ Click on links with `data-push` attribute will fire History.pushState() event.
183
183
  Data from the request will replace content of the container that was passed to Wiselinks (default is `$('body')`)
184
184
 
185
185
 
186
- ```html
186
+ ```html
187
187
  <ul class="menu">
188
188
  <li>
189
189
  <a href="/" data-push="true">Home</a>
190
- </li>
190
+ </li>
191
191
  <li>
192
192
  <a href="/contacts" data-push="true">Contacts</a>
193
193
  </li>
@@ -200,17 +200,17 @@ Data from the request will replace content of the container that was passed to W
200
200
  ```html
201
201
  <div class="dialog">
202
202
  <a href="/step2" data-replace="true">Proceed to the next step</a>
203
- </div>
203
+ </div>
204
204
  ```
205
205
 
206
206
  Click on following links will fire History.pushState() event.
207
207
  Data from the request will be pasted into `<div id="catalog">`. This configuration is widely when you have list of items that are paginated, sorted or maybe grouped by some attributes and you want to update only these items and nothing more on page.
208
208
 
209
- ```html
209
+ ```html
210
210
  <ul class="pagination">
211
211
  <li>
212
212
  <span>1</span>
213
- </li>
213
+ </li>
214
214
  <li>
215
215
  <a href="/?page=2" data-push="true" data-target="#catalog">2</a>
216
216
  </li>
@@ -225,10 +225,10 @@ Data from the request will be pasted into `<div id="catalog">`. This configurati
225
225
  <ul class="sort">
226
226
  <li>
227
227
  <a href="/?sort=title" data-push="true" data-target="#catalog">Sort by Title</a>
228
- </li>
228
+ </li>
229
229
  <li>
230
230
  <a href="/?sort=price" data-push="true" data-target="#catalog">Sort by Price</a>
231
- </li>
231
+ </li>
232
232
  </ul>
233
233
 
234
234
  <div id="catalog">
@@ -245,7 +245,7 @@ Wiselinks can process forms. After submit button is clicked, Wiselinks will perf
245
245
  <div class="filter">
246
246
  <form action="/" method="get" data-push="true" data-target="@catalog">
247
247
  <input type="text" size="30" name="title" id="title">
248
-
248
+
249
249
  <select name="scope" id="scope">
250
250
  <option value="">All Tax Liens</option>
251
251
  <option value="accruing">Accruing Interest</option>
@@ -253,14 +253,14 @@ Wiselinks can process forms. After submit button is clicked, Wiselinks will perf
253
253
  <option value="closed">Closed</option>
254
254
  <option value="trashed">Trash</option>
255
255
  </select>
256
-
256
+
257
257
  <input type="submit" value="Find" name="commit">
258
258
  </form>
259
259
  </div>
260
260
 
261
261
  <div role="catalog">
262
262
  <!-- the list of your items -->
263
- ...
263
+ ...
264
264
  </div>
265
265
  ```
266
266
  **data-include-blank-url-params**
@@ -313,7 +313,7 @@ Event is triggered before the `XMLHttpRequest` is initialised and performed.
313
313
 
314
314
  **page:redirected ($target, render, url)**
315
315
 
316
- Event is triggered when you were redirected during `XMLHttpRequest` (with HTTP 30x status).
316
+ Event is triggered when you were redirected during `XMLHttpRequest` (with HTTP 30x status).
317
317
  * *$target* – jQuery object in which result of the request will be inserted;
318
318
 
319
319
  * *url* - URL where you have been redirected to;
@@ -360,13 +360,13 @@ So if you want to show a client-side loading spinner, you could listen for `page
360
360
  Wiselinks adds a couple of methods to `ActionDispatch::Request`. These methods are mostly syntax sugar and don't have any complex logic, so you can use them or not.
361
361
 
362
362
  #### #wiselinks? ###
363
- Method returns `true` if current request is initiated by Wiselinks (has `X-Wiselinks` header), `false` otherwise.
363
+ Method returns `true` if current request is initiated by Wiselinks (has `X-Wiselinks` header), `false` otherwise.
364
364
 
365
365
  #### #wiselinks_template? ###
366
- Method returns `true` if current request is initiated by Wiselinks and client want to render template (`X-Wiselinks != 'partial'`), `false` otherwise.
366
+ Method returns `true` if current request is initiated by Wiselinks and client want to render template (`X-Wiselinks != 'partial'`), `false` otherwise.
367
367
 
368
368
  #### #wiselinks_partial? ###
369
- Method returns `true` if current request is initiated by Wiselinks and client want to render partial (`X-Wiselinks == 'partial'`), `false` otherwise.
369
+ Method returns `true` if current request is initiated by Wiselinks and client want to render partial (`X-Wiselinks == 'partial'`), `false` otherwise.
370
370
 
371
371
  ### Assets change detection
372
372
 
@@ -388,7 +388,7 @@ Now Wiselinks will track changes of your assets and if anything will change, you
388
388
 
389
389
  Wiselinks handles page titles by passing `X-Wiselinks-Title` header with response. To set this header you can use `wiselinks_title` helper (in Rails).
390
390
 
391
- ```html
391
+ ```html
392
392
  <% wiselinks_title("Wiselinks is awesome") %>
393
393
 
394
394
  <div>
@@ -407,7 +407,7 @@ Wiselinks follows 30x HTTP redirects. Location is updated in browser with `X-Wis
407
407
 
408
408
  By default, if Wiselinks cannot find target that you specified during initialization, it will fail silently. But you can override this behaviour:
409
409
 
410
- ```coffeescript
410
+ ```coffeescript
411
411
  #= require jquery
412
412
  #= require wiselinks
413
413
 
@@ -416,7 +416,7 @@ $(document).ready ->
416
416
  $('something that does not exist'),
417
417
  target_missing: 'exception'
418
418
  )
419
- ```
419
+ ```
420
420
 
421
421
  `[Wiselinks] Target missing` exception will be thrown. This also works for `data-target` attributes.
422
422
 
@@ -475,4 +475,3 @@ Contributors:
475
475
 
476
476
  It is free software, and may be redistributed under the terms specified in the LICENSE file.
477
477
 
478
-
@@ -13,13 +13,28 @@ class Link
13
13
 
14
14
  @page.load(@$link.attr("href"), @$link.data('target'), type)
15
15
 
16
- # We split host because IE returns host with port and other browsers not
17
- #
18
16
  _cross_origin_link: (link) ->
19
- (location.protocol != link.protocol) ||
20
- (location.port != link.port) ||
17
+ this._different_protocol(link) ||
18
+ this._different_host(link) ||
19
+ this._different_port(link)
20
+
21
+ _different_protocol: (link) ->
22
+ location.protocol != link.protocol
23
+
24
+ # IE returns host with port and all other browsers return host without port
25
+ #
26
+ _different_host: (link) ->
21
27
  (location.host.split(':')[0] != link.host.split(':')[0])
22
28
 
29
+ # IE returns for link.port "80" but the location.port is ""
30
+ # Stupid but "modern" browsers return correct values
31
+ #
32
+ _different_port: (link) ->
33
+ port_equals = (location.port == link.port) ||
34
+ (location.port == "" && link.port == "80")
35
+
36
+ !port_equals
37
+
23
38
  _non_standard_click: (event) ->
24
39
  event.metaKey || event.ctrlKey || event.shiftKey || event.altKey
25
40
 
@@ -10,14 +10,11 @@ class Page
10
10
  self._try_target(@$target)
11
11
 
12
12
  if History.emulated.pushState && @options.html4 == true
13
- if window.location.href.indexOf('#!') == -1 &&
13
+ if window.location.href.indexOf('#.') == -1 &&
14
14
  @options.html4_normalize_path == true &&
15
15
  window.location.pathname != @options.html4_root_path
16
16
 
17
- window.location.href = "#{window.location.protocol}//#{window.location.host}#{@options.html4_root_path}#!#{window.location.pathname}"
18
-
19
- if window.location.hash.indexOf('#!') != -1
20
- self._call(self._make_state(window.location.hash.substring(2)))
17
+ window.location.href = "#{window.location.protocol}//#{window.location.host}#{@options.html4_root_path}#.#{window.location.pathname}"
21
18
 
22
19
  History.Adapter.bind(
23
20
  window,
@@ -56,7 +53,6 @@ class Page
56
53
 
57
54
  if target?
58
55
  this._try_target($(target))
59
-
60
56
  History.pushState({
61
57
  timestamp: (new Date().getTime()),
62
58
  template_id: @template_id,
@@ -32,20 +32,19 @@ class RequestManager
32
32
  window.location.reload(true)
33
33
  else
34
34
  state = History.getState()
35
-
36
- if url? && (url != self._normalize(window.location.href))
35
+ if url? && (url != self._normalize(state.url))
37
36
  self._redirect_to(url, $target, state, xhr)
38
37
 
39
38
  $target.html(data)
40
39
 
41
40
  self._title(xhr.getResponseHeader('X-Wiselinks-Title'))
42
- self._done($target, status, state.url, data)
41
+ self._done($target, status, state, data)
43
42
  ).fail(
44
43
  (xhr, status, error) ->
45
- self._fail($target, status, state.url, error)
44
+ self._fail($target, status, state, error)
46
45
  ).always(
47
46
  (data_or_xhr, status, xhr_or_error)->
48
- self._always($target, status, state.url)
47
+ self._always($target, status, state)
49
48
  )
50
49
 
51
50
  _normalize: (url) ->
@@ -67,16 +66,16 @@ class RequestManager
67
66
  History.replaceState(state.data, document.title, url)
68
67
 
69
68
  _loading: ($target, state) ->
70
- $(document).trigger('page:loading', [$target, state.data.render, state.url])
69
+ $(document).trigger('page:loading', [$target, state.data.render, decodeURI(state.url)])
71
70
 
72
- _done: ($target, status, url, data) ->
73
- $(document).trigger('page:done', [$target, status, url, data])
71
+ _done: ($target, status, state, data) ->
72
+ $(document).trigger('page:done', [$target, status, decodeURI(state.url), data])
74
73
 
75
- _fail: ($target, status, url, error) ->
76
- $(document).trigger('page:fail', [$target, status, url, error])
74
+ _fail: ($target, status, state, error) ->
75
+ $(document).trigger('page:fail', [$target, status, decodeURI(state.url), error])
77
76
 
78
- _always: ($target, status, url) ->
79
- $(document).trigger('page:always', [$target, status, url])
77
+ _always: ($target, status, state) ->
78
+ $(document).trigger('page:always', [$target, status, decodeURI(state.url)])
80
79
 
81
80
  _title: (value) ->
82
81
  if value?
@@ -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 '&nbsp;'),
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);