2
Copyright (c) 2007 Brian Dillard and Brad Neuberg:
3
Brian Dillard | Project Lead | bdillard@pathf.com | http://blogs.pathf.com/agileajax/
4
Brad Neuberg | Original Project Creator | http://codinginparadise.org
6
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files
7
(the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge,
8
publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do
9
so, subject to the following conditions:
11
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
13
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
14
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
15
FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
16
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20
dhtmlHistory: An object that provides history, history data, and bookmarking for DHTML and Ajax applications.
23
* the historyStorage object included in this file.
26
window.dhtmlHistory = {
28
/*Public: User-agent booleans*/
36
/*Public: Create the DHTML history infrastructure*/
37
create: function(options) {
40
options - object to store initialization parameters
41
options.debugMode - boolean that causes hidden form fields to be shown for development purposes.
42
options.toJSON - function to override default JSON stringifier
43
options.fromJSON - function to override default JSON parser
48
/*set user-agent flags*/
49
var UA = navigator.userAgent.toLowerCase();
50
var platform = navigator.platform.toLowerCase();
51
var vendor = navigator.vendor || "";
52
if (vendor === "KDE") {
53
this.isKonqueror = true;
54
this.isSupported = false;
55
} else if (typeof window.opera !== "undefined") {
57
this.isSupported = true;
58
} else if (typeof document.all !== "undefined") {
60
this.isSupported = true;
61
} else if (vendor.indexOf("Apple Computer, Inc.") > -1) {
63
this.isSupported = true; //(platform.indexOf("mac") > -1);
64
} else if (UA.indexOf("gecko") != -1) {
66
this.isSupported = true;
69
/*Set up the historyStorage object; pass in init parameters*/
70
window.historyStorage.setup(options);
72
/*Execute browser-specific setup methods*/
75
} else if (this.isOpera) {
79
/*Get our initial location*/
80
var initialHash = this.getCurrentLocation();
82
/*Save it as our current location*/
83
this.currentLocation = initialHash;
85
/*Now that we have a hash, create IE-specific code*/
87
this.createIE(initialHash);
90
/*Add an unload listener for the page; this is needed for FF 1.5+ because this browser caches all dynamic updates to the
91
page, which can break some of our logic related to testing whether this is the first instance a page has loaded or whether
92
it is being pulled from the cache*/
94
var unloadHandler = function() {
95
that.firstLoad = null;
98
this.addEventListener(window,'unload',unloadHandler);
100
/*Determine if this is our first page load; for IE, we do this in this.iframeLoaded(), which is fired on pageload. We do it
101
there because we have no historyStorage at this point, which only exists after the page is finished loading in IE*/
103
/*The iframe will get loaded on page load, and we want to ignore this fact*/
104
this.ignoreLocationChange = true;
106
if (!historyStorage.hasKey(this.PAGELOADEDSTRING)) {
107
/*This is our first page load, so ignore the location change and add our special history entry*/
108
this.ignoreLocationChange = true;
109
this.firstLoad = true;
110
historyStorage.put(this.PAGELOADEDSTRING, true);
112
/*This isn't our first page load, so indicate that we want to pay attention to this location change*/
113
this.ignoreLocationChange = false;
114
/*For browsers other than IE, fire a history change event; on IE, the event will be thrown automatically when its
115
hidden iframe reloads on page load. Unfortunately, we don't have any listeners yet; indicate that we want to fire
116
an event when a listener is added.*/
117
this.fireOnNewListener = true;
121
/*Other browsers can use a location handler that checks at regular intervals as their primary mechanism; we use it for IE as
122
well to handle an important edge case; see checkLocation() for details*/
123
var locationHandler = function() {
124
that.checkLocation();
126
setInterval(locationHandler, 100);
129
/*Public: Initialize our DHTML history. You must call this after the page is finished loading.*/
130
initialize: function() {
131
/*IE needs to be explicitly initialized. IE doesn't autofill form data until the page is finished loading, so we have to wait*/
133
/*If this is the first time this page has loaded*/
134
if (!historyStorage.hasKey(this.PAGELOADEDSTRING)) {
135
/*For IE, we do this in initialize(); for other browsers, we do it in create()*/
136
this.fireOnNewListener = false;
137
this.firstLoad = true;
138
historyStorage.put(this.PAGELOADEDSTRING, true);
140
/*Else if this is a fake onload event*/
142
this.fireOnNewListener = true;
143
this.firstLoad = false;
148
/*Public: Adds a history change listener. Note that only one listener is supported at this time.*/
149
addListener: function(listener) {
150
this.listener = listener;
151
/*If the page was just loaded and we should not ignore it, fire an event to our new listener now*/
152
if (this.fireOnNewListener) {
153
this.fireHistoryEvent(this.currentLocation);
154
this.fireOnNewListener = false;
158
/*Public: Generic utility function for attaching events*/
159
addEventListener: function(o,e,l) {
160
if (o.addEventListener) {
161
o.addEventListener(e,l,false);
162
} else if (o.attachEvent) {
163
o.attachEvent('on'+e,function() {
169
/*Public: Add a history point.*/
170
add: function(newLocation, historyData) {
174
/*Remove any leading hash symbols on newLocation*/
175
newLocation = this.removeHash(newLocation);
177
/*Store the history data into history storage*/
178
historyStorage.put(newLocation, historyData);
180
/*Save this as our current location*/
181
this.currentLocation = newLocation;
183
/*Change the browser location*/
184
window.location.hash = newLocation;
186
/*Save this to the Safari form field*/
187
this.putSafariState(newLocation);
191
/*Most browsers require that we wait a certain amount of time before changing the location, such
192
as 200 MS; rather than forcing external callers to use window.setTimeout to account for this,
193
we internally handle it by putting requests in a queue.*/
195
var addImpl = function() {
196
var fixWaitTime = function() {
197
/*Indicate that the current wait time is now less*/
198
if (that.currentWaitTime > 0) {
199
that.currentWaitTime = that.currentWaitTime - that.waitTime;
202
/*End of atomic location change block for IE*/
203
that.ieAtomicLocationChange = false;
206
/*Remove any leading hash symbols on newLocation*/
207
newLocation = that.removeHash(newLocation);
209
/*IE has a strange bug; if the newLocation is the same as _any_ preexisting id in the
210
document, then the history action gets recorded twice; throw a programmer exception if
211
there is an element with this ID*/
212
if (document.getElementById(newLocation) && that.debugMode) {
213
var e = "Exception: History locations can not have the same value as _any_ IDs that might be in the document,"
214
+ " due to a bug in IE; please ask the developer to choose a history location that does not match any HTML"
215
+ " IDs in this document. The following ID is already taken and cannot be a location: " + newLocation;
219
/*Store the history data into history storage*/
220
historyStorage.put(newLocation, historyData);
222
/*Indicate to the browser to ignore this upcomming location change since we're making it programmatically*/
223
that.ignoreLocationChange = true;
225
/*Indicate to IE that this is an atomic location change block*/
226
that.ieAtomicLocationChange = true;
228
/*Save this as our current location*/
229
that.currentLocation = newLocation;
231
/*Change the browser location*/
232
window.location.hash = newLocation;
234
/*Change the hidden iframe's location if on IE*/
236
that.iframe.src = "blank.html?" + newLocation;
240
window.setTimeout(fixWaitTime, that.waitTime);
242
/*End of atomic location change block for IE*/
243
that.ieAtomicLocationChange = false;
248
this.ieAtomicLocationChange = true;
250
if (this.currentWaitTime) {
251
/*Now queue up this add request*/
252
window.setTimeout(addImpl, this.currentWaitTime);
254
/*Indicate that the next request will have to wait for awhile*/
255
this.currentWaitTime = this.currentWaitTime + this.waitTime;
257
this.currentWaitTime = this.currentWaitTime + this.waitTime;
265
isFirstLoad: function() {
266
return this.firstLoad;
270
getVersion: function() {
274
/*Get browser's current hash location; for Safari, read value from a hidden form field*/
277
getCurrentLocation: function() {
278
return this.getCurrentHash();
280
This causes problems in Safari and looks not really needed,
281
at least Mac 3.0.4 and Win 3.1 working well without special
283
var r = (this.isSafari
284
? this.getSafariState()
285
: this.getCurrentHash()
291
/*Public: Manually parse the current url for a hash; tip of the hat to YUI*/
292
getCurrentHash: function() {
294
var r = this.getIframeHash();
298
var r = window.location.href;
299
var i = r.indexOf("#");
306
/*- - - - - - - - - - - -*/
308
/*Private: Constant for our own internal history event called when the page is loaded*/
309
PAGELOADEDSTRING: "DhtmlHistory_pageLoaded",
311
/*Private: Our history change listener.*/
314
/*Private: MS to wait between add requests - will be reset for certain browsers*/
317
/*Private: MS before an add request can execute*/
320
/*Private: Our current hash location, without the "#" symbol.*/
321
currentLocation: null,
323
/*Private: Hidden iframe used to IE to detect history changes*/
326
/*Private: Flags and DOM references used only by Safari*/
327
safariHistoryStartPoint: null,
331
/*Private: Flag used to keep checkLocation() from doing anything when it discovers location changes we've made ourselves
332
programmatically with the add() method. Basically, add() sets this to true. When checkLocation() discovers it's true,
333
it refrains from firing our listener, then resets the flag to false for next cycle. That way, our listener only gets fired on
334
history change events triggered by the user via back/forward buttons and manual hash changes. This flag also helps us set up
335
IE's special iframe-based method of handling history changes.*/
336
ignoreLocationChange: null,
338
/*Private: A flag that indicates that we should fire a history change event when we are ready, i.e. after we are initialized and
339
we have a history change listener. This is needed due to an edge case in browsers other than IE; if you leave a page entirely
340
then return, we must fire this as a history change event. Unfortunately, we have lost all references to listeners from earlier,
341
because JavaScript clears out.*/
342
fireOnNewListener: null,
344
/*Private: A variable that indicates whether this is the first time this page has been loaded. If you go to a web page, leave it
345
for another one, and then return, the page's onload listener fires again. We need a way to differentiate between the first page
346
load and subsequent ones. This variable works hand in hand with the pageLoaded variable we store into historyStorage.*/
349
/*Private: A variable to handle an important edge case in IE. In IE, if a user manually types an address into their browser's
350
location bar, we must intercept this by calling checkLocation() at regular intervals. However, if we are programmatically
351
changing the location bar ourselves using the add() method, we need to ignore these changes in checkLocation(). Unfortunately,
352
these changes take several lines of code to complete, so for the duration of those lines of code, we set this variable to true.
353
That signals to checkLocation() to ignore the change-in-progress. Once we're done with our chunk of location-change code in
354
add(), we set this back to false. We'll do the same thing when capturing user-entered address changes in checkLocation itself.*/
355
ieAtomicLocationChange: null,
357
/*Private: Create IE-specific DOM nodes and overrides*/
358
createIE: function(initialHash) {
359
/*write out a hidden iframe for IE and set the amount of time to wait between add() requests*/
360
this.waitTime = 400;/*IE needs longer between history updates*/
361
var styles = (historyStorage.debugMode
362
? 'width: 800px;height:80px;border:1px solid black;'
363
: historyStorage.hideStyles
365
var iframeID = "rshHistoryFrame";
367
var iframeHTML = '<iframe frameborder="0" id="' + iframeID + '" style="' + styles + '" src="blank.html?' + initialHash + '"></iframe>';
368
document.write(iframeHTML);
369
this.iframe = document.getElementById(iframeID);
373
var node = document.createElement('iframe');
374
node.setAttribute('frameborder', '0');
375
node.setAttribute('id', iframeID);
376
if (typeof node.style.cssText == "string")
377
node.style.cssText = styles;
379
node.setAttribute('style', styles);
381
node.setAttribute("src", "blank.html?" + initialHash);
384
document.body.appendChild(node);
387
/*Private: Create Opera-specific DOM nodes and overrides*/
388
createOpera: function() {
389
this.waitTime = 400;/*Opera needs longer between history updates*/
391
var imgHTML = '<img src="javascript:location.href=\'javascript:dhtmlHistory.checkLocation();\';" style="' + historyStorage.hideStyles + '" />';
392
document.write(imgHTML);
394
var node = document.createElement('img');
395
if (typeof node.style.cssText == "string")
396
node.style.cssText = historyStorage.hideStyles;
398
node.setAttribute('style', historyStorage.hideStyles);
399
node.setAttribute('src', "javascript:location.href=\'javascript:dhtmlHistory.checkLocation();\';");
401
document.body.appendChild(node);
405
/*Private: Create Safari-specific DOM nodes and overrides*/
406
createSafari: function() {
407
var formID = "rshSafariForm";
408
var stackID = "rshSafariStack";
409
var lengthID = "rshSafariLength";
410
var formStyles = historyStorage.debugMode ? historyStorage.showStyles : historyStorage.hideStyles;
411
var inputStyles = (historyStorage.debugMode
412
? 'width:800px;height:20px;border:1px solid black;margin:0;padding:0;'
413
: historyStorage.hideStyles
417
var safariHTML = '<form id="' + formID + '" style="' + formStyles + '">'
418
+ '<input type="text" style="' + inputStyles + '" id="' + stackID + '" value="[]"/>'
419
+ '<input type="text" style="' + inputStyles + '" id="' + lengthID + '" value=""/>'
421
document.write(safariHTML);
422
this.safariStack = document.getElementById(stackID);
423
this.safariLength = document.getElementById(lengthID);
426
var fnode = document.createElement('form');
427
if (typeof fnode.style.cssText == "string")
428
fnode.style.cssText = formStyles;
430
fnode.setAttribute('style', formStyles);
431
fnode.setAttribute('id', formID);
433
var node = document.createElement('input');
434
node.setAttribute('type', "text");
435
if (typeof node.style.cssText == "string")
436
node.style.cssText = inputStyles;
438
node.setAttribute('style', inputStyles);
439
node.setAttribute('id', stackID);
440
node.setAttribute('value', "[]");
442
fnode.appendChild(node);
443
this.safariStack = node;
445
node = document.createElement('input');
446
node.setAttribute('type', "text");
447
if (typeof node.style.cssText == "string")
448
node.style.cssText = inputStyles;
450
node.setAttribute('style', inputStyles);
451
node.setAttribute('id', lengthID);
452
node.setAttribute('value', "");
454
fnode.appendChild(node);
455
this.safariLength = node;
457
document.body.appendChild(fnode);
459
if (!historyStorage.hasKey(this.PAGELOADEDSTRING)) {
460
this.safariHistoryStartPoint = history.length;
461
this.safariLength.value = this.safariHistoryStartPoint;
463
this.safariHistoryStartPoint = this.safariLength.value;
467
/*Private: Safari method to read the history stack from a hidden form field*/
468
getSafariStack: function() {
469
var r = this.safariStack.value;
470
return historyStorage.fromJSON(r);
473
/*Private: Safari method to read from the history stack*/
474
getSafariState: function() {
475
var stack = this.getSafariStack();
476
var state = stack[history.length - this.safariHistoryStartPoint - 1];
479
/*Private: Safari method to write the history stack to a hidden form field*/
480
putSafariState: function(newLocation) {
481
var stack = this.getSafariStack();
482
stack[history.length - this.safariHistoryStartPoint] = newLocation;
483
this.safariStack.value = historyStorage.toJSON(stack);
486
/*Private: Notify the listener of new history changes.*/
487
fireHistoryEvent: function(newHash) {
488
/*extract the value from our history storage for this hash*/
489
var historyData = historyStorage.get(newHash);
490
/*call our listener*/
491
this.listener.call(null, newHash, historyData);
494
/*Private: See if the browser has changed location. This is the primary history mechanism for Firefox. For IE, we use this to
495
handle an important edge case: if a user manually types in a new hash value into their IE location bar and press enter, we want to
496
to intercept this and notify any history listener.*/
497
checkLocation: function() {
499
/*Ignore any location changes that we made ourselves for browsers other than IE*/
500
if (!this.isIE && this.ignoreLocationChange) {
501
this.ignoreLocationChange = false;
505
/*If we are dealing with IE and we are in the middle of making a location change from an iframe, ignore it*/
506
if (this.isIE && this.ieAtomicLocationChange) {
510
/*Get hash location*/
511
var hash = this.getCurrentLocation();
513
/*Do nothing if there's been no change*/
514
if (hash == this.currentLocation) {
517
// alert(hash + ' - ' + this.currentLocation);
519
/*In IE, users manually entering locations into the browser; we do this by comparing the browser's location against the
520
iframe's location; if they differ, we are dealing with a manual event and need to place it inside our history, otherwise
522
this.ieAtomicLocationChange = true;
525
this.iframe.src = "blank.html?" + hash;
526
window.location.hash = hash;
530
if (this.isIE && this.getIframeHash() != hash) {
531
this.iframe.src = "blank.html?" + hash;
533
else if (this.isIE) {
538
/*Save this new location*/
539
this.currentLocation = hash;
541
this.ieAtomicLocationChange = false;
543
/*Notify listeners of the change*/
544
this.fireHistoryEvent(hash);
547
/*Private: Get the current location of IE's hidden iframe.*/
548
getIframeHash: function() {
549
if (!this.iframe) return "";
551
var doc = this.iframe.contentWindow.document;
552
var hash = String(doc.location.search);
553
if (hash.length == 1 && hash.charAt(0) == "?") {
556
else if (hash.length >= 2 && hash.charAt(0) == "?") {
557
hash = hash.substring(1);
562
/*Private: Remove any leading hash that might be on a location.*/
563
removeHash: function(hashValue) {
565
if (hashValue === null || hashValue === undefined) {
568
else if (hashValue === "") {
571
else if (hashValue.length == 1 && hashValue.charAt(0) == "#") {
574
else if (hashValue.length > 1 && hashValue.charAt(0) == "#") {
575
r = hashValue.substring(1);
583
/*Private: For IE, tell when the hidden iframe has finished loading.*/
584
iframeLoaded: function(newLocation) {
585
/*ignore any location changes that we made ourselves*/
586
if (this.ignoreLocationChange) {
587
this.ignoreLocationChange = false;
591
/*Get the new location*/
592
var hash = String(newLocation.search);
593
if (hash.length == 1 && hash.charAt(0) == "?") {
596
else if (hash.length >= 2 && hash.charAt(0) == "?") {
597
hash = hash.substring(1);
599
/*Keep the browser location bar in sync with the iframe hash*/
600
window.location.hash = hash;
602
/*Notify listeners of the change*/
603
this.fireHistoryEvent(hash);
609
historyStorage: An object that uses a hidden form to store history state across page loads. The mechanism for doing so relies on
610
the fact that browsers save the text in form data for the life of the browser session, which means the text is still there when
611
the user navigates back to the page. This object can be used independently of the dhtmlHistory object for caching of Ajax
615
* json2007.js (included in a separate file) or alternate JSON methods passed in through an options bundle.
617
window.historyStorage = {
619
/*Public: Set up our historyStorage object for use by dhtmlHistory or other objects*/
620
setup: function(options) {
623
options - object to store initialization parameters - passed in from dhtmlHistory or directly into historyStorage
624
options.debugMode - boolean that causes hidden form fields to be shown for development purposes.
625
options.toJSON - function to override default JSON stringifier
626
options.fromJSON - function to override default JSON parser
629
/*process init parameters*/
630
if (typeof options !== "undefined") {
631
if (options.debugMode) {
632
this.debugMode = options.debugMode;
634
if (options.toJSON) {
635
this.toJSON = options.toJSON;
637
if (options.fromJSON) {
638
this.fromJSON = options.fromJSON;
642
/*write a hidden form and textarea into the page; we'll stow our history stack here*/
643
var formID = "rshStorageForm";
644
var textareaID = "rshStorageField";
645
var formStyles = this.debugMode ? historyStorage.showStyles : historyStorage.hideStyles;
646
var textareaStyles = (historyStorage.debugMode
647
? 'width: 800px;height:80px;border:1px solid black;'
648
: historyStorage.hideStyles
652
var textareaHTML = '<form id="' + formID + '" style="' + formStyles + '">'
653
+ '<textarea id="' + textareaID + '" style="' + textareaStyles + '"></textarea>'
655
document.write(textareaHTML);
656
this.storageField = document.getElementById(textareaID);
659
var node = document.createElement('textarea');
660
node.setAttribute('id', textareaID);
661
if (typeof node.style.cssText == "string")
662
node.style.cssText = textareaStyles;
664
node.setAttribute('style', textareaStyles);
666
var fnode = document.createElement('form');
667
fnode.setAttribute('id', formID);
668
if (typeof fnode.style.cssText == "string")
669
fnode.style.cssText = formStyles;
671
fnode.setAttribute('style', formStyles);
672
fnode.appendChild(node);
674
this.storageField = node;
675
document.body.appendChild(fnode);
677
if (typeof window.opera !== "undefined") {
678
this.storageField.focus();/*Opera needs to focus this element before persisting values in it*/
683
put: function(key, value) {
684
this.assertValidKey(key);
685
/*if we already have a value for this, remove the value before adding the new one*/
686
if (this.hasKey(key)) {
689
/*store this new key*/
690
this.storageHash[key] = value;
691
/*save and serialize the hashtable into the form*/
692
this.saveHashTable();
697
this.assertValidKey(key);
698
/*make sure the hash table has been loaded from the form*/
699
this.loadHashTable();
700
var value = this.storageHash[key];
701
if (value === undefined) {
708
remove: function(key) {
709
this.assertValidKey(key);
710
/*make sure the hash table has been loaded from the form*/
711
this.loadHashTable();
713
delete this.storageHash[key];
714
/*serialize and save the hash table into the form*/
715
this.saveHashTable();
718
/*Public: Clears out all saved data.*/
720
this.storageField.value = "";
721
this.storageHash = {};
725
hasKey: function(key) {
726
this.assertValidKey(key);
727
/*make sure the hash table has been loaded from the form*/
728
this.loadHashTable();
729
return (typeof this.storageHash[key] !== "undefined");
733
isValidKey: function(key) {
734
return (typeof key === "string");
737
/*Public - CSS strings utilized by both objects to hide or show behind-the-scenes DOM elements*/
738
showStyles: 'border:0;margin:0;padding:0;',
739
hideStyles: 'left:-1000px;top:-1000px;width:1px;height:1px;border:0;position:absolute;',
741
/*Public - debug mode flag*/
744
/*- - - - - - - - - - - -*/
746
/*Private: Our hash of key name/values.*/
749
/*Private: If true, we have loaded our hash table out of the storage form.*/
752
/*Private: DOM reference to our history field*/
755
/*Private: Assert that a key is valid; throw an exception if it not.*/
756
assertValidKey: function(key) {
757
var isValid = this.isValidKey(key);
758
if (!isValid && this.debugMode) {
759
throw new Error("Please provide a valid key for window.historyStorage. Invalid key = " + key + ".");
763
/*Private: Load the hash table up from the form.*/
764
loadHashTable: function() {
765
if (!this.hashLoaded) {
766
var serializedHashTable = this.storageField.value;
767
if (serializedHashTable !== "" && serializedHashTable !== null) {
768
this.storageHash = this.fromJSON(serializedHashTable);
769
this.hashLoaded = true;
773
/*Private: Save the hash table into the form.*/
774
saveHashTable: function() {
775
this.loadHashTable();
776
var serializedHashTable = this.toJSON(this.storageHash);
777
this.storageField.value = serializedHashTable;
779
/*Private: Bridges for our JSON implementations - both rely on 2007 JSON.org library - can be overridden by options bundle*/
780
toJSON: function(o) {
781
return o.toJSONString();
783
fromJSON: function(s) {
784
return s.parseJSON();