Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
seleniumhq
GitHub Repository: seleniumhq/selenium
Path: blob/trunk/third_party/closure/goog/history/history.js
2868 views
1
// Copyright 2007 The Closure Library Authors. All Rights Reserved.
2
//
3
// Licensed under the Apache License, Version 2.0 (the "License");
4
// you may not use this file except in compliance with the License.
5
// You may obtain a copy of the License at
6
//
7
// http://www.apache.org/licenses/LICENSE-2.0
8
//
9
// Unless required by applicable law or agreed to in writing, software
10
// distributed under the License is distributed on an "AS-IS" BASIS,
11
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
// See the License for the specific language governing permissions and
13
// limitations under the License.
14
15
/**
16
* @fileoverview Browser history stack management class.
17
*
18
* The goog.History object allows a page to create history state without leaving
19
* the current document. This allows users to, for example, hit the browser's
20
* back button without leaving the current page.
21
*
22
* The history object can be instantiated in one of two modes. In user visible
23
* mode, the current history state is shown in the browser address bar as a
24
* document location fragment (the portion of the URL after the '#'). These
25
* addresses can be bookmarked, copied and pasted into another browser, and
26
* modified directly by the user like any other URL.
27
*
28
* If the history object is created in invisible mode, the user can still
29
* affect the state using the browser forward and back buttons, but the current
30
* state is not displayed in the browser address bar. These states are not
31
* bookmarkable or editable.
32
*
33
* It is possible to use both types of history object on the same page, but not
34
* currently recommended due to browser deficiencies.
35
*
36
* Tested to work in:
37
* <ul>
38
* <li>Firefox 1.0-4.0
39
* <li>Internet Explorer 5.5-9.0
40
* <li>Opera 9+
41
* <li>Safari 4+
42
* </ul>
43
*
44
* @author [email protected] (Shawn Brenneman)
45
* @see ../demos/history1.html
46
* @see ../demos/history2.html
47
*/
48
49
/* Some browser specific implementation notes:
50
*
51
* Firefox (through version 2.0.0.1):
52
*
53
* Ideally, navigating inside the hidden iframe could be done using
54
* about:blank#state instead of a real page on the server. Setting the hash on
55
* about:blank creates history entries, but the hash is not recorded and is lost
56
* when the user hits the back button. This is true in Opera as well. A blank
57
* HTML page must be provided for invisible states to be recorded in the iframe
58
* hash.
59
*
60
* After leaving the page with the History object and returning to it (by
61
* hitting the back button from another site), the last state of the iframe is
62
* overwritten. The most recent state is saved in a hidden input field so the
63
* previous state can be restored.
64
*
65
* Firefox does not store the previous value of dynamically generated input
66
* elements. To save the state, the hidden element must be in the HTML document,
67
* either in the original source or added with document.write. If a reference
68
* to the input element is not provided as a constructor argument, then the
69
* history object creates one using document.write, in which case the history
70
* object must be created from a script in the body element of the page.
71
*
72
* Manually editing the address field to a different hash link prevents further
73
* updates to the address bar. The page continues to work as normal, but the
74
* address shown will be incorrect until the page is reloaded.
75
*
76
* NOTE(user): It should be noted that Firefox will URL encode any non-regular
77
* ascii character, along with |space|, ", <, and >, when added to the fragment.
78
* If you expect these characters in your tokens you should consider that
79
* setToken('<b>') would result in the history fragment "%3Cb%3E", and
80
* "esp&eacute;re" would show "esp%E8re". (IE allows unicode characters in the
81
* fragment)
82
*
83
* TODO(user): Should we encapsulate this escaping into the API for visible
84
* history and encode all characters that aren't supported by Firefox? It also
85
* needs to be optional so apps can elect to handle the escaping themselves.
86
*
87
*
88
* Internet Explorer (through version 7.0):
89
*
90
* IE does not modify the history stack when the document fragment is changed.
91
* We create history entries instead by using document.open and document.write
92
* into a hidden iframe.
93
*
94
* IE destroys the history stack when navigating from /foo.html#someFragment to
95
* /foo.html. The workaround is to always append the # to the URL. This is
96
* somewhat unfortunate when loading the page without any # specified, because
97
* a second "click" sound will play on load as the fragment is automatically
98
* appended. If the hash is always present, this can be avoided.
99
*
100
* Manually editing the hash in the address bar in IE6 and then hitting the back
101
* button can replace the page with a blank page. This is a Bad User Experience,
102
* but probably not preventable.
103
*
104
* IE also has a bug when the page is loaded via a server redirect, setting
105
* a new hash value on the window location will force a page reload. This will
106
* happen the first time setToken is called with a new token. The only known
107
* workaround is to force a client reload early, for example by setting
108
* window.location.hash = window.location.hash, which will otherwise be a no-op.
109
*
110
* Internet Explorer 8.0, Webkit 532.1 and Gecko 1.9.2:
111
*
112
* IE8 has introduced the support to the HTML5 onhashchange event, which means
113
* we don't have to do any polling to detect fragment changes. Chrome and
114
* Firefox have added it on their newer builds, wekbit 532.1 and gecko 1.9.2.
115
* http://www.w3.org/TR/html5/history.html
116
* NOTE(goto): it is important to note that the document needs to have the
117
* <!DOCTYPE html> tag to enable the IE8 HTML5 mode. If the tag is not present,
118
* IE8 will enter IE7 compatibility mode (which can also be enabled manually).
119
*
120
* Opera (through version 9.02):
121
*
122
* Navigating through pages at a rate faster than some threshold causes Opera
123
* to cancel all outstanding timeouts and intervals, including the location
124
* polling loop. Since this condition cannot be detected, common input events
125
* are captured to cause the loop to restart.
126
*
127
* location.replace is adding a history entry inside setHash_, despite
128
* documentation that suggests it should not.
129
*
130
*
131
* Safari (through version 2.0.4):
132
*
133
* After hitting the back button, the location.hash property is no longer
134
* readable from JavaScript. This is fixed in later WebKit builds, but not in
135
* currently shipping Safari. For now, the only recourse is to disable history
136
* states in Safari. Pages are still navigable via the History object, but the
137
* back button cannot restore previous states.
138
*
139
* Safari sets history states on navigation to a hashlink, but doesn't allow
140
* polling of the hash, so following actual anchor links in the page will create
141
* useless history entries. Using location.replace does not seem to prevent
142
* this. Not a terribly good user experience, but fixed in later Webkits.
143
*
144
*
145
* WebKit (nightly version 420+):
146
*
147
* This almost works. Returning to a page with an invisible history object does
148
* not restore the old state, however, and there is no pageshow event that fires
149
* in this browser. Holding off on finding a solution for now.
150
*
151
*
152
* HTML5 capable browsers (Firefox 4, Chrome, Safari 5)
153
*
154
* No known issues. The goog.history.Html5History class provides a simpler
155
* implementation more suitable for recent browsers. These implementations
156
* should be merged so the history class automatically invokes the correct
157
* implementation.
158
*/
159
160
161
goog.provide('goog.History');
162
goog.provide('goog.History.Event');
163
goog.provide('goog.History.EventType');
164
165
goog.require('goog.Timer');
166
goog.require('goog.asserts');
167
goog.require('goog.dom');
168
goog.require('goog.dom.InputType');
169
goog.require('goog.dom.safe');
170
/** @suppress {extraRequire} */
171
goog.require('goog.events.Event');
172
goog.require('goog.events.EventHandler');
173
goog.require('goog.events.EventTarget');
174
goog.require('goog.events.EventType');
175
goog.require('goog.history.Event');
176
goog.require('goog.history.EventType');
177
goog.require('goog.html.SafeHtml');
178
goog.require('goog.html.TrustedResourceUrl');
179
goog.require('goog.labs.userAgent.device');
180
goog.require('goog.memoize');
181
goog.require('goog.string');
182
goog.require('goog.string.Const');
183
goog.require('goog.userAgent');
184
185
186
187
/**
188
* A history management object. Can be instantiated in user-visible mode (uses
189
* the address fragment to manage state) or in hidden mode. This object should
190
* be created from a script in the document body before the document has
191
* finished loading.
192
*
193
* To store the hidden states in browsers other than IE, a hidden iframe is
194
* used. It must point to a valid html page on the same domain (which can and
195
* probably should be blank.)
196
*
197
* Sample instantiation and usage:
198
*
199
* <pre>
200
* // Instantiate history to use the address bar for state.
201
* var h = new goog.History();
202
* goog.events.listen(h, goog.history.EventType.NAVIGATE, navCallback);
203
* h.setEnabled(true);
204
*
205
* // Any changes to the location hash will call the following function.
206
* function navCallback(e) {
207
* alert('Navigated to state "' + e.token + '"');
208
* }
209
*
210
* // The history token can also be set from code directly.
211
* h.setToken('foo');
212
* </pre>
213
*
214
* @param {boolean=} opt_invisible True to use hidden history states instead of
215
* the user-visible location hash.
216
* @param {!goog.html.TrustedResourceUrl=} opt_blankPageUrl A URL to a
217
* blank page on the same server. Required if opt_invisible is true.
218
* This URL is also used as the src for the iframe used to track history
219
* state in IE (if not specified the iframe is not given a src attribute).
220
* Access is Denied error may occur in IE7 if the window's URL's scheme
221
* is https, and this URL is not specified.
222
* @param {HTMLInputElement=} opt_input The hidden input element to be used to
223
* store the history token. If not provided, a hidden input element will
224
* be created using document.write.
225
* @param {HTMLIFrameElement=} opt_iframe The hidden iframe that will be used by
226
* IE for pushing history state changes, or by all browsers if opt_invisible
227
* is true. If not provided, a hidden iframe element will be created using
228
* document.write.
229
* @constructor
230
* @extends {goog.events.EventTarget}
231
*/
232
goog.History = function(
233
opt_invisible, opt_blankPageUrl, opt_input, opt_iframe) {
234
goog.events.EventTarget.call(this);
235
236
if (opt_invisible && !opt_blankPageUrl) {
237
throw Error('Can\'t use invisible history without providing a blank page.');
238
}
239
240
var input;
241
if (opt_input) {
242
input = opt_input;
243
} else {
244
var inputId = 'history_state' + goog.History.historyCount_;
245
var inputHtml = goog.html.SafeHtml.create('input', {
246
type: goog.dom.InputType.TEXT,
247
name: inputId,
248
id: inputId,
249
style: goog.string.Const.from('display:none')
250
});
251
goog.dom.safe.documentWrite(document, inputHtml);
252
input = goog.dom.getElement(inputId);
253
}
254
255
/**
256
* An input element that stores the current iframe state. Used to restore
257
* the state when returning to the page on non-IE browsers.
258
* @type {HTMLInputElement}
259
* @private
260
*/
261
this.hiddenInput_ = /** @type {HTMLInputElement} */ (input);
262
263
/**
264
* The window whose location contains the history token fragment. This is
265
* the window that contains the hidden input. It's typically the top window.
266
* It is not necessarily the same window that the js code is loaded in.
267
* @type {Window}
268
* @private
269
*/
270
this.window_ = opt_input ?
271
goog.dom.getWindow(goog.dom.getOwnerDocument(opt_input)) :
272
window;
273
274
/**
275
* The base URL for the hidden iframe. Must refer to a document in the
276
* same domain as the main page.
277
* @type {!goog.html.TrustedResourceUrl|undefined}
278
* @private
279
*/
280
this.iframeSrc_ = opt_blankPageUrl;
281
282
if (goog.userAgent.IE && !opt_blankPageUrl) {
283
if (window.location.protocol == 'https') {
284
this.iframeSrc_ = goog.html.TrustedResourceUrl.fromConstant(
285
goog.string.Const.from('https:///'));
286
} else {
287
this.iframeSrc_ = goog.html.TrustedResourceUrl.fromConstant(
288
goog.string.Const.from('javascript:""'));
289
}
290
}
291
292
/**
293
* A timer for polling the current history state for changes.
294
* @type {goog.Timer}
295
* @private
296
*/
297
this.timer_ = new goog.Timer(goog.History.PollingType.NORMAL);
298
this.registerDisposable(this.timer_);
299
300
/**
301
* True if the state tokens are displayed in the address bar, false for hidden
302
* history states.
303
* @type {boolean}
304
* @private
305
*/
306
this.userVisible_ = !opt_invisible;
307
308
/**
309
* An object to keep track of the history event listeners.
310
* @type {goog.events.EventHandler<!goog.History>}
311
* @private
312
*/
313
this.eventHandler_ = new goog.events.EventHandler(this);
314
315
if (opt_invisible || goog.History.LEGACY_IE) {
316
var iframe;
317
if (opt_iframe) {
318
iframe = opt_iframe;
319
} else {
320
var iframeId = 'history_iframe' + goog.History.historyCount_;
321
// Using a "sandbox" attribute on the iframe might be possible, but
322
// this HTML didn't initially have it and when it was refactored
323
// to SafeHtml it was kept without it.
324
var iframeHtml = goog.html.SafeHtml.createIframe(this.iframeSrc_, null, {
325
id: iframeId,
326
style: goog.string.Const.from('display:none'),
327
sandbox: undefined
328
});
329
goog.dom.safe.documentWrite(document, iframeHtml);
330
iframe = goog.dom.getElement(iframeId);
331
}
332
333
/**
334
* Internet Explorer uses a hidden iframe for all history changes. Other
335
* browsers use the iframe only for pushing invisible states.
336
* @type {HTMLIFrameElement}
337
* @private
338
*/
339
this.iframe_ = /** @type {HTMLIFrameElement} */ (iframe);
340
341
/**
342
* Whether the hidden iframe has had a document written to it yet in this
343
* session.
344
* @type {boolean}
345
* @private
346
*/
347
this.unsetIframe_ = true;
348
}
349
350
if (goog.History.LEGACY_IE) {
351
// IE relies on the hidden input to restore the history state from previous
352
// sessions, but input values are only restored after window.onload. Set up
353
// a callback to poll the value after the onload event.
354
this.eventHandler_.listen(
355
this.window_, goog.events.EventType.LOAD, this.onDocumentLoaded);
356
357
/**
358
* IE-only variable for determining if the document has loaded.
359
* @type {boolean}
360
* @protected
361
*/
362
this.documentLoaded = false;
363
364
/**
365
* IE-only variable for storing whether the history object should be enabled
366
* once the document finishes loading.
367
* @type {boolean}
368
* @private
369
*/
370
this.shouldEnable_ = false;
371
}
372
373
// Set the initial history state.
374
if (this.userVisible_) {
375
this.setHash_(this.getToken(), true);
376
} else {
377
this.setIframeToken_(this.hiddenInput_.value);
378
}
379
380
goog.History.historyCount_++;
381
};
382
goog.inherits(goog.History, goog.events.EventTarget);
383
384
385
/**
386
* Status of when the object is active and dispatching events.
387
* @type {boolean}
388
* @private
389
*/
390
goog.History.prototype.enabled_ = false;
391
392
393
/**
394
* Whether the object is performing polling with longer intervals. This can
395
* occur for instance when setting the location of the iframe when in invisible
396
* mode and the server that is hosting the blank html page is down. In FF, this
397
* will cause the location of the iframe to no longer be accessible, with
398
* permision denied exceptions being thrown on every access of the history
399
* token. When this occurs, the polling interval is elongated. This causes
400
* exceptions to be thrown at a lesser rate while allowing for the history
401
* object to resurrect itself when the html page becomes accessible.
402
* @type {boolean}
403
* @private
404
*/
405
goog.History.prototype.longerPolling_ = false;
406
407
408
/**
409
* The last token set by the history object, used to poll for changes.
410
* @type {?string}
411
* @private
412
*/
413
goog.History.prototype.lastToken_ = null;
414
415
416
/**
417
* Whether the browser supports HTML5 history management's onhashchange event.
418
* {@link http://www.w3.org/TR/html5/history.html}. IE 9 in compatibility mode
419
* indicates that onhashchange is in window, but testing reveals the event
420
* isn't actually fired.
421
* @return {boolean} Whether onhashchange is supported.
422
*/
423
goog.History.isOnHashChangeSupported = goog.memoize(function() {
424
return goog.userAgent.IE ? goog.userAgent.isDocumentModeOrHigher(8) :
425
'onhashchange' in goog.global;
426
});
427
428
429
/**
430
* Whether the current browser is Internet Explorer prior to version 8. Many IE
431
* specific workarounds developed before version 8 are unnecessary in more
432
* current versions.
433
* @type {boolean}
434
*/
435
goog.History.LEGACY_IE =
436
goog.userAgent.IE && !goog.userAgent.isDocumentModeOrHigher(8);
437
438
439
/**
440
* Whether the browser always requires the hash to be present. Internet Explorer
441
* before version 8 will reload the HTML page if the hash is omitted.
442
* @type {boolean}
443
*/
444
goog.History.HASH_ALWAYS_REQUIRED = goog.History.LEGACY_IE;
445
446
447
/**
448
* If not null, polling in the user invisible mode will be disabled until this
449
* token is seen. This is used to prevent a race condition where the iframe
450
* hangs temporarily while the location is changed.
451
* @type {?string}
452
* @private
453
*/
454
goog.History.prototype.lockedToken_ = null;
455
456
457
/** @override */
458
goog.History.prototype.disposeInternal = function() {
459
goog.History.superClass_.disposeInternal.call(this);
460
this.eventHandler_.dispose();
461
this.setEnabled(false);
462
};
463
464
465
/**
466
* Starts or stops the History polling loop. When enabled, the History object
467
* will immediately fire an event for the current location. The caller can set
468
* up event listeners between the call to the constructor and the call to
469
* setEnabled.
470
*
471
* On IE, actual startup may be delayed until the iframe and hidden input
472
* element have been loaded and can be polled. This behavior is transparent to
473
* the caller.
474
*
475
* @param {boolean} enable Whether to enable the history polling loop.
476
*/
477
goog.History.prototype.setEnabled = function(enable) {
478
479
if (enable == this.enabled_) {
480
return;
481
}
482
483
if (goog.History.LEGACY_IE && !this.documentLoaded) {
484
// Wait until the document has actually loaded before enabling the
485
// object or any saved state from a previous session will be lost.
486
this.shouldEnable_ = enable;
487
return;
488
}
489
490
if (enable) {
491
if (goog.userAgent.OPERA) {
492
// Capture events for common user input so we can restart the timer in
493
// Opera if it fails. Yes, this is distasteful. See operaDefibrillator_.
494
this.eventHandler_.listen(
495
this.window_.document, goog.History.INPUT_EVENTS_,
496
this.operaDefibrillator_);
497
} else if (goog.userAgent.GECKO) {
498
// Firefox will not restore the correct state after navigating away from
499
// and then back to the page with the history object. This can be fixed
500
// by restarting the history object on the pageshow event.
501
this.eventHandler_.listen(this.window_, 'pageshow', this.onShow_);
502
}
503
504
// TODO(user): make HTML5 and invisible history work by listening to the
505
// iframe # changes instead of the window.
506
if (goog.History.isOnHashChangeSupported() && this.userVisible_) {
507
this.eventHandler_.listen(
508
this.window_, goog.events.EventType.HASHCHANGE, this.onHashChange_);
509
this.enabled_ = true;
510
this.dispatchEvent(new goog.history.Event(this.getToken(), false));
511
} else if (
512
!(goog.userAgent.IE && !goog.labs.userAgent.device.isMobile()) ||
513
this.documentLoaded) {
514
// Start dispatching history events if all necessary loading has
515
// completed (always true for browsers other than IE.)
516
this.eventHandler_.listen(
517
this.timer_, goog.Timer.TICK, goog.bind(this.check_, this, true));
518
519
this.enabled_ = true;
520
521
// Initialize last token at startup except on IE < 8, where the last token
522
// must only be set in conjunction with IFRAME updates, or the IFRAME will
523
// start out of sync and remove any pre-existing URI fragment.
524
if (!goog.History.LEGACY_IE) {
525
this.lastToken_ = this.getToken();
526
this.dispatchEvent(new goog.history.Event(this.getToken(), false));
527
}
528
529
this.timer_.start();
530
}
531
532
} else {
533
this.enabled_ = false;
534
this.eventHandler_.removeAll();
535
this.timer_.stop();
536
}
537
};
538
539
540
/**
541
* Callback for the window onload event in IE. This is necessary to read the
542
* value of the hidden input after restoring a history session. The value of
543
* input elements is not viewable until after window onload for some reason (the
544
* iframe state is similarly unavailable during the loading phase.) If
545
* setEnabled is called before the iframe has completed loading, the history
546
* object will actually be enabled at this point.
547
* @protected
548
*/
549
goog.History.prototype.onDocumentLoaded = function() {
550
this.documentLoaded = true;
551
552
if (this.hiddenInput_.value) {
553
// Any saved value in the hidden input can only be read after the document
554
// has been loaded due to an IE limitation. Restore the previous state if
555
// it has been set.
556
this.setIframeToken_(this.hiddenInput_.value, true);
557
}
558
559
this.setEnabled(this.shouldEnable_);
560
};
561
562
563
/**
564
* Handler for the Gecko pageshow event. Restarts the history object so that the
565
* correct state can be restored in the hash or iframe.
566
* @param {goog.events.BrowserEvent} e The browser event.
567
* @private
568
*/
569
goog.History.prototype.onShow_ = function(e) {
570
// NOTE(user): persisted is a property passed in the pageshow event that
571
// indicates whether the page is being persisted from the cache or is being
572
// loaded for the first time.
573
if (e.getBrowserEvent()['persisted']) {
574
this.setEnabled(false);
575
this.setEnabled(true);
576
}
577
};
578
579
580
/**
581
* Handles HTML5 onhashchange events on browsers where it is supported.
582
* This is very similar to {@link #check_}, except that it is not executed
583
* continuously. It is only used when
584
* {@code goog.History.isOnHashChangeSupported()} is true.
585
* @param {goog.events.BrowserEvent} e The browser event.
586
* @private
587
*/
588
goog.History.prototype.onHashChange_ = function(e) {
589
var hash = this.getLocationFragment_(this.window_);
590
if (hash != this.lastToken_) {
591
this.update_(hash, true);
592
}
593
};
594
595
596
/**
597
* @return {string} The current token.
598
*/
599
goog.History.prototype.getToken = function() {
600
if (this.lockedToken_ != null) {
601
return this.lockedToken_;
602
} else if (this.userVisible_) {
603
return this.getLocationFragment_(this.window_);
604
} else {
605
return this.getIframeToken_() || '';
606
}
607
};
608
609
610
/**
611
* Sets the history state. When user visible states are used, the URL fragment
612
* will be set to the provided token. Sometimes it is necessary to set the
613
* history token before the document title has changed, in this case IE's
614
* history drop down can be out of sync with the token. To get around this
615
* problem, the app can pass in a title to use with the hidden iframe.
616
* @param {string} token The history state identifier.
617
* @param {string=} opt_title Optional title used when setting the hidden iframe
618
* title in IE.
619
*/
620
goog.History.prototype.setToken = function(token, opt_title) {
621
this.setHistoryState_(token, false, opt_title);
622
};
623
624
625
/**
626
* Replaces the current history state without affecting the rest of the history
627
* stack.
628
* @param {string} token The history state identifier.
629
* @param {string=} opt_title Optional title used when setting the hidden iframe
630
* title in IE.
631
*/
632
goog.History.prototype.replaceToken = function(token, opt_title) {
633
this.setHistoryState_(token, true, opt_title);
634
};
635
636
637
/**
638
* Gets the location fragment for the current URL. We don't use location.hash
639
* directly as the browser helpfully urlDecodes the string for us which can
640
* corrupt the tokens. For example, if we want to store: label/%2Froot it would
641
* be returned as label//root.
642
* @param {Window} win The window object to use.
643
* @return {string} The fragment.
644
* @private
645
*/
646
goog.History.prototype.getLocationFragment_ = function(win) {
647
var href = win.location.href;
648
var index = href.indexOf('#');
649
return index < 0 ? '' : href.substring(index + 1);
650
};
651
652
653
/**
654
* Sets the history state. When user visible states are used, the URL fragment
655
* will be set to the provided token. Setting opt_replace to true will cause the
656
* navigation to occur, but will replace the current history entry without
657
* affecting the length of the stack.
658
*
659
* @param {string} token The history state identifier.
660
* @param {boolean} replace Set to replace the current history entry instead of
661
* appending a new history state.
662
* @param {string=} opt_title Optional title used when setting the hidden iframe
663
* title in IE.
664
* @private
665
*/
666
goog.History.prototype.setHistoryState_ = function(token, replace, opt_title) {
667
if (this.getToken() != token) {
668
if (this.userVisible_) {
669
this.setHash_(token, replace);
670
671
if (!goog.History.isOnHashChangeSupported()) {
672
if (goog.userAgent.IE && !goog.labs.userAgent.device.isMobile()) {
673
// IE must save state using the iframe.
674
this.setIframeToken_(token, replace, opt_title);
675
}
676
}
677
678
// This condition needs to be called even if
679
// goog.History.isOnHashChangeSupported() is true so the NAVIGATE event
680
// fires sychronously.
681
if (this.enabled_) {
682
this.check_(false);
683
}
684
} else {
685
// Fire the event immediately so that setting history is synchronous, but
686
// set a suspendToken so that polling doesn't trigger a 'back'.
687
this.setIframeToken_(token, replace);
688
this.lockedToken_ = this.lastToken_ = this.hiddenInput_.value = token;
689
this.dispatchEvent(new goog.history.Event(token, false));
690
}
691
}
692
};
693
694
695
/**
696
* Sets or replaces the URL fragment. The token does not need to be URL encoded
697
* according to the URL specification, though certain characters (like newline)
698
* are automatically stripped.
699
*
700
* If opt_replace is not set, non-IE browsers will append a new entry to the
701
* history list. Setting the hash does not affect the history stack in IE
702
* (unless there is a pre-existing named anchor for that hash.)
703
*
704
* Older versions of Webkit cannot query the location hash, but it still can be
705
* set. If we detect one of these versions, always replace instead of creating
706
* new history entries.
707
*
708
* window.location.replace replaces the current state from the history stack.
709
* http://www.whatwg.org/specs/web-apps/current-work/#dom-location-replace
710
* http://www.whatwg.org/specs/web-apps/current-work/#replacement-enabled
711
*
712
* @param {string} token The new string to set.
713
* @param {boolean=} opt_replace Set to true to replace the current token
714
* without appending a history entry.
715
* @private
716
*/
717
goog.History.prototype.setHash_ = function(token, opt_replace) {
718
// If the page uses a BASE element, setting location.hash directly will
719
// navigate away from the current document. Also, the original URL path may
720
// possibly change from HTML5 history pushState. To account for these, the
721
// full path is always specified.
722
var loc = this.window_.location;
723
var url = loc.href.split('#')[0];
724
725
// If a hash has already been set, then removing it programmatically will
726
// reload the page. Once there is a hash, we won't remove it.
727
var hasHash = goog.string.contains(loc.href, '#');
728
729
if (goog.History.HASH_ALWAYS_REQUIRED || hasHash || token) {
730
url += '#' + token;
731
}
732
733
if (url != loc.href) {
734
if (opt_replace) {
735
loc.replace(url);
736
} else {
737
loc.href = url;
738
}
739
}
740
};
741
742
743
/**
744
* Sets the hidden iframe state. On IE, this is accomplished by writing a new
745
* document into the iframe. In Firefox, the iframe's URL fragment stores the
746
* state instead.
747
*
748
* Older versions of webkit cannot set the iframe, so ignore those browsers.
749
*
750
* @param {string} token The new string to set.
751
* @param {boolean=} opt_replace Set to true to replace the current iframe state
752
* without appending a new history entry.
753
* @param {string=} opt_title Optional title used when setting the hidden iframe
754
* title in IE.
755
* @private
756
*/
757
goog.History.prototype.setIframeToken_ = function(
758
token, opt_replace, opt_title) {
759
if (this.unsetIframe_ || token != this.getIframeToken_()) {
760
this.unsetIframe_ = false;
761
token = goog.string.urlEncode(token);
762
763
if (goog.userAgent.IE) {
764
// Caching the iframe document results in document permission errors after
765
// leaving the page and returning. Access it anew each time instead.
766
var doc = goog.dom.getFrameContentDocument(this.iframe_);
767
768
doc.open('text/html', opt_replace ? 'replace' : undefined);
769
var iframeSourceHtml = goog.html.SafeHtml.concat(
770
goog.html.SafeHtml.create(
771
'title', {}, (opt_title || this.window_.document.title)),
772
goog.html.SafeHtml.create('body', {}, token));
773
goog.dom.safe.documentWrite(doc, iframeSourceHtml);
774
doc.close();
775
} else {
776
goog.asserts.assertInstanceof(
777
this.iframeSrc_, goog.html.TrustedResourceUrl,
778
'this.iframeSrc_ must be set on calls to setIframeToken_');
779
var url =
780
goog.html.TrustedResourceUrl.unwrap(
781
/** @type {!goog.html.TrustedResourceUrl} */ (this.iframeSrc_)) +
782
'#' + token;
783
784
// In Safari, it is possible for the contentWindow of the iframe to not
785
// be present when the page is loading after a reload.
786
var contentWindow = this.iframe_.contentWindow;
787
if (contentWindow) {
788
if (opt_replace) {
789
contentWindow.location.replace(url);
790
} else {
791
contentWindow.location.href = url;
792
}
793
}
794
}
795
}
796
};
797
798
799
/**
800
* Return the current state string from the hidden iframe. On internet explorer,
801
* this is stored as a string in the document body. Other browsers use the
802
* location hash of the hidden iframe.
803
*
804
* Older versions of webkit cannot access the iframe location, so always return
805
* null in that case.
806
*
807
* @return {?string} The state token saved in the iframe (possibly null if the
808
* iframe has never loaded.).
809
* @private
810
*/
811
goog.History.prototype.getIframeToken_ = function() {
812
if (goog.userAgent.IE) {
813
var doc = goog.dom.getFrameContentDocument(this.iframe_);
814
return doc.body ? goog.string.urlDecode(doc.body.innerHTML) : null;
815
} else {
816
// In Safari, it is possible for the contentWindow of the iframe to not
817
// be present when the page is loading after a reload.
818
var contentWindow = this.iframe_.contentWindow;
819
if (contentWindow) {
820
var hash;
821
822
try {
823
// Iframe tokens are urlEncoded
824
hash = goog.string.urlDecode(this.getLocationFragment_(contentWindow));
825
} catch (e) {
826
// An exception will be thrown if the location of the iframe can not be
827
// accessed (permission denied). This can occur in FF if the the server
828
// that is hosting the blank html page goes down and then a new history
829
// token is set. The iframe will navigate to an error page, and the
830
// location of the iframe can no longer be accessed. Due to the polling,
831
// this will cause constant exceptions to be thrown. In this case,
832
// we enable longer polling. We do not have to attempt to reset the
833
// iframe token because (a) we already fired the NAVIGATE event when
834
// setting the token, (b) we can rely on the locked token for current
835
// state, and (c) the token is still in the history and
836
// accesible on forward/back.
837
if (!this.longerPolling_) {
838
this.setLongerPolling_(true);
839
}
840
841
return null;
842
}
843
844
// There was no exception when getting the hash so turn off longer polling
845
// if it is on.
846
if (this.longerPolling_) {
847
this.setLongerPolling_(false);
848
}
849
850
return hash || null;
851
} else {
852
return null;
853
}
854
}
855
};
856
857
858
/**
859
* Checks the state of the document fragment and the iframe title to detect
860
* navigation changes. If {@code goog.HistoryisOnHashChangeSupported()} is
861
* {@code false}, then this runs approximately twenty times per second.
862
* @param {boolean} isNavigation True if the event was initiated by a browser
863
* action, false if it was caused by a setToken call. See
864
* {@link goog.history.Event}.
865
* @private
866
*/
867
goog.History.prototype.check_ = function(isNavigation) {
868
if (this.userVisible_) {
869
var hash = this.getLocationFragment_(this.window_);
870
if (hash != this.lastToken_) {
871
this.update_(hash, isNavigation);
872
}
873
}
874
875
// Old IE uses the iframe for both visible and non-visible versions.
876
if (!this.userVisible_ || goog.History.LEGACY_IE) {
877
var token = this.getIframeToken_() || '';
878
if (this.lockedToken_ == null || token == this.lockedToken_) {
879
this.lockedToken_ = null;
880
if (token != this.lastToken_) {
881
this.update_(token, isNavigation);
882
}
883
}
884
}
885
};
886
887
888
/**
889
* Updates the current history state with a given token. Called after a change
890
* to the location or the iframe state is detected by poll_.
891
*
892
* @param {string} token The new history state.
893
* @param {boolean} isNavigation True if the event was initiated by a browser
894
* action, false if it was caused by a setToken call. See
895
* {@link goog.history.Event}.
896
* @private
897
*/
898
goog.History.prototype.update_ = function(token, isNavigation) {
899
this.lastToken_ = this.hiddenInput_.value = token;
900
901
if (this.userVisible_) {
902
if (goog.History.LEGACY_IE) {
903
this.setIframeToken_(token);
904
}
905
906
this.setHash_(token);
907
} else {
908
this.setIframeToken_(token);
909
}
910
911
this.dispatchEvent(new goog.history.Event(this.getToken(), isNavigation));
912
};
913
914
915
/**
916
* Sets if the history oject should use longer intervals when polling.
917
*
918
* @param {boolean} longerPolling Whether to enable longer polling.
919
* @private
920
*/
921
goog.History.prototype.setLongerPolling_ = function(longerPolling) {
922
if (this.longerPolling_ != longerPolling) {
923
this.timer_.setInterval(
924
longerPolling ? goog.History.PollingType.LONG :
925
goog.History.PollingType.NORMAL);
926
}
927
this.longerPolling_ = longerPolling;
928
};
929
930
931
/**
932
* Opera cancels all outstanding timeouts and intervals after any rapid
933
* succession of navigation events, including the interval used to detect
934
* navigation events. This function restarts the interval so that navigation can
935
* continue. Ideally, only events which would be likely to cause a navigation
936
* change (mousedown and keydown) would be bound to this function. Since Opera
937
* seems to ignore keydown events while the alt key is pressed (such as
938
* alt-left or right arrow), this function is also bound to the much more
939
* frequent mousemove event. This way, when the update loop freezes, it will
940
* unstick itself as the user wiggles the mouse in frustration.
941
* @private
942
*/
943
goog.History.prototype.operaDefibrillator_ = function() {
944
this.timer_.stop();
945
this.timer_.start();
946
};
947
948
949
/**
950
* List of user input event types registered in Opera to restart the history
951
* timer (@see goog.History#operaDefibrillator_).
952
* @type {Array<string>}
953
* @private
954
*/
955
goog.History.INPUT_EVENTS_ = [
956
goog.events.EventType.MOUSEDOWN, goog.events.EventType.KEYDOWN,
957
goog.events.EventType.MOUSEMOVE
958
];
959
960
961
/**
962
* Counter for the number of goog.History objects that have been instantiated.
963
* Used to create unique IDs.
964
* @type {number}
965
* @private
966
*/
967
goog.History.historyCount_ = 0;
968
969
970
/**
971
* Types of polling. The values are in ms of the polling interval.
972
* @enum {number}
973
*/
974
goog.History.PollingType = {
975
NORMAL: 150,
976
LONG: 10000
977
};
978
979
980
/**
981
* Constant for the history change event type.
982
* @enum {string}
983
* @deprecated Use goog.history.EventType.
984
*/
985
goog.History.EventType = goog.history.EventType;
986
987
988
989
/**
990
* Constant for the history change event type.
991
* @constructor
992
* @deprecated Use goog.history.Event.
993
* @final
994
*/
995
goog.History.Event = goog.history.Event;
996
997