OJD database committing support
[eidogo-ojd.git] / js / rsh.js
blobe01821dc97a1894af01bee2234fab66f78915b01
1 /*
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
5    
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.
21         
22         dependencies:
23                 * the historyStorage object included in this file.
26 window.dhtmlHistory = {
27         
28         /*Public: User-agent booleans*/
29         isIE: false,
30         isOpera: false,
31         isSafari: false,
32         isSafari3: false,
33         isKonquerer: false,
34         isGecko: false,
35         isSupported: false,
36         
37         /*Public: Create the DHTML history infrastructure*/
38         create: function(options) {
39                 
40                 /*
41                         options - object to store initialization parameters
42                         options.debugMode - boolean that causes hidden form fields to be shown for development purposes.
43                         options.toJSON - function to override default JSON stringifier
44                         options.fromJSON - function to override default JSON parser
45                 */
47                 var that = this;
49                 /*set user-agent flags*/
50                 var UA = navigator.userAgent.toLowerCase();
51                 var platform = navigator.platform.toLowerCase();
52                 var vendor = navigator.vendor || "";
53                 var version = (UA.match(/.+(?:rv|it|ra|ie)[\/: ]([\d.]+)/) || [])[1];
54                 if (vendor === "KDE") {
55                         this.isKonqueror = true;
56                         this.isSupported = false;
57                 } else if (typeof window.opera !== "undefined") {
58                         this.isOpera = true;
59                         this.isSupported = true;
60                 } else if (typeof document.all !== "undefined") {
61                         this.isIE = true;
62                         this.isSupported = true;
63                 } else if (vendor.indexOf("Apple Computer, Inc.") > -1) {
64                     if (parseInt(version, 10) < 420) {
65                         this.isSafari = true;
66                     } else {
67                         this.isSafari3 = true;
68                     }
69                     this.isSupported = (platform.indexOf("mac") > -1);
70                 } else if (UA.indexOf("gecko") != -1) {
71                         this.isGecko = true;
72                         this.isSupported = true;
73                 }
75                 /*Set up the historyStorage object; pass in init parameters*/
76                 window.historyStorage.setup(options);
78                 /*Execute browser-specific setup methods*/
79                 if (this.isSafari) {
80                         this.createSafari();
81                 } else if (this.isOpera) {
82                         this.createOpera();
83                 }
84                 
85                 /*Get our initial location*/
86                 var initialHash = this.getCurrentLocation();
88                 /*Save it as our current location*/
89                 this.currentLocation = initialHash;
91                 /*Now that we have a hash, create IE-specific code*/
92                 if (this.isIE) {
93                         this.createIE(initialHash);
94                 }
96                 /*Add an unload listener for the page; this is needed for FF 1.5+ because this browser caches all dynamic updates to the
97                 page, which can break some of our logic related to testing whether this is the first instance a page has loaded or whether
98                 it is being pulled from the cache*/
100                 var unloadHandler = function() {
101                         that.firstLoad = null;
102                 };
103                 
104                 this.addEventListener(window,'unload',unloadHandler);           
106                 /*Determine if this is our first page load; for IE, we do this in this.iframeLoaded(), which is fired on pageload. We do it
107                 there because we have no historyStorage at this point, which only exists after the page is finished loading in IE*/
108                 if (this.isIE) {
109                         /*The iframe will get loaded on page load, and we want to ignore this fact*/
110                         this.ignoreLocationChange = true;
111                 } else {
112                         if (!historyStorage.hasKey(this.PAGELOADEDSTRING)) {
113                                 /*This is our first page load, so ignore the location change and add our special history entry*/
114                                 this.ignoreLocationChange = true;
115                                 this.firstLoad = true;
116                                 historyStorage.put(this.PAGELOADEDSTRING, true);
117                         } else {
118                                 /*This isn't our first page load, so indicate that we want to pay attention to this location change*/
119                                 this.ignoreLocationChange = false;
120                                 /*For browsers other than IE, fire a history change event; on IE, the event will be thrown automatically when its
121                                 hidden iframe reloads on page load. Unfortunately, we don't have any listeners yet; indicate that we want to fire
122                                 an event when a listener is added.*/
123                                 this.fireOnNewListener = true;
124                         }
125                 }
127                 /*Other browsers can use a location handler that checks at regular intervals as their primary mechanism; we use it for IE as
128                 well to handle an important edge case; see checkLocation() for details*/
129                 var locationHandler = function() {
130                         that.checkLocation();
131                 };
132                 setInterval(locationHandler, 100);
133         },      
134         
135         /*Public: Initialize our DHTML history. You must call this after the page is finished loading.*/
136         initialize: function() {
137                 /*IE needs to be explicitly initialized. IE doesn't autofill form data until the page is finished loading, so we have to wait*/
138                 if (this.isIE) {
139                         /*If this is the first time this page has loaded*/
140                         if (!historyStorage.hasKey(this.PAGELOADEDSTRING)) {
141                                 /*For IE, we do this in initialize(); for other browsers, we do it in create()*/
142                                 this.fireOnNewListener = false;
143                                 this.firstLoad = true;
144                                 historyStorage.put(this.PAGELOADEDSTRING, true);
145                         }
146                         /*Else if this is a fake onload event*/
147                         else {
148                                 this.fireOnNewListener = true;
149                                 this.firstLoad = false;   
150                         }
151                 }
152         },
154         /*Public: Adds a history change listener. Note that only one listener is supported at this time.*/
155         addListener: function(listener) {
156                 this.listener = listener;
157                 /*If the page was just loaded and we should not ignore it, fire an event to our new listener now*/
158                 if (this.fireOnNewListener) {
159                         this.fireHistoryEvent(this.currentLocation);
160                         this.fireOnNewListener = false;
161                 }
162         },
163         
164         /*Public: Generic utility function for attaching events*/
165         addEventListener: function(o,e,l) {
166                 if (o.addEventListener) {
167                         o.addEventListener(e,l,false);
168                 } else if (o.attachEvent) {
169                         o.attachEvent('on'+e,function() {
170                                 l(window.event);
171                         });
172                 }
173         },
174         
175         /*Public: Add a history point.*/
176         add: function(newLocation, historyData) {
177                 
178                 if (this.isSafari) {
179                         
180                         /*Remove any leading hash symbols on newLocation*/
181                         newLocation = this.removeHash(newLocation);
183                         /*Store the history data into history storage*/
184                         historyStorage.put(newLocation, historyData);
186                         /*Save this as our current location*/
187                         this.currentLocation = newLocation;
188         
189                         /*Change the browser location*/
190                         window.location.hash = newLocation;
191                 
192                         /*Save this to the Safari form field*/
193                         this.putSafariState(newLocation);
195                 } else {
196                         
197                         /*Most browsers require that we wait a certain amount of time before changing the location, such
198                         as 200 MS; rather than forcing external callers to use window.setTimeout to account for this,
199                         we internally handle it by putting requests in a queue.*/
200                         var that = this;
201                         var addImpl = function() {
203                                 /*Indicate that the current wait time is now less*/
204                                 if (that.currentWaitTime > 0) {
205                                         that.currentWaitTime = that.currentWaitTime - that.waitTime;
206                                 }
207                         
208                                 /*Remove any leading hash symbols on newLocation*/
209                                 newLocation = that.removeHash(newLocation);
211                                 /*IE has a strange bug; if the newLocation is the same as _any_ preexisting id in the
212                                 document, then the history action gets recorded twice; throw a programmer exception if
213                                 there is an element with this ID*/
214                                 if (document.getElementById(newLocation) && that.debugMode) {
215                                         var e = "Exception: History locations can not have the same value as _any_ IDs that might be in the document,"
216                                         + " due to a bug in IE; please ask the developer to choose a history location that does not match any HTML"
217                                         + " IDs in this document. The following ID is already taken and cannot be a location: " + newLocation;
218                                         throw new Error(e); 
219                                 }
221                                 /*Store the history data into history storage*/
222                                 historyStorage.put(newLocation, historyData);
224                                 /*Indicate to the browser to ignore this upcomming location change since we're making it programmatically*/
225                                 that.ignoreLocationChange = true;
227                                 /*Indicate to IE that this is an atomic location change block*/
228                                 that.ieAtomicLocationChange = true;
230                                 /*Save this as our current location*/
231                                 that.currentLocation = newLocation;
232                 
233                                 /*Change the browser location*/
234                                 window.location.hash = newLocation;
236                                 /*Change the hidden iframe's location if on IE*/
237                                 if (that.isIE) {
238                                         that.iframe.src = "blank.html?" + newLocation;
239                                 }
241                                 /*End of atomic location change block for IE*/
242                                 that.ieAtomicLocationChange = false;
243                         };
245                         /*Now queue up this add request*/
246                         window.setTimeout(addImpl, this.currentWaitTime);
248                         /*Indicate that the next request will have to wait for awhile*/
249                         this.currentWaitTime = this.currentWaitTime + this.waitTime;
250                 }
251         },
253         /*Public*/
254         isFirstLoad: function() {
255                 return this.firstLoad;
256         },
258         /*Public*/
259         getVersion: function() {
260                 return "0.6";
261         },
263         /*Get browser's current hash location; for Safari, read value from a hidden form field*/
265         /*Public*/
266         getCurrentLocation: function() {
267                 var r = (this.isSafari
268                         ? this.getSafariState()
269                         : this.getCurrentHash()
270                 );
271                 return r;
272         },
273         
274         /*Public: Manually parse the current url for a hash; tip of the hat to YUI*/
275     getCurrentHash: function() {
276                 var r = window.location.href;
277                 var i = r.indexOf("#");
278                 return (i >= 0
279                         ? r.substr(i+1)
280                         : ""
281                 );
282     },
283         
284         /*- - - - - - - - - - - -*/
285         
286         /*Private: Constant for our own internal history event called when the page is loaded*/
287         PAGELOADEDSTRING: "DhtmlHistory_pageLoaded",
288         
289         /*Private: Our history change listener.*/
290         listener: null,
292         /*Private: MS to wait between add requests - will be reset for certain browsers*/
293         waitTime: 200,
294         
295         /*Private: MS before an add request can execute*/
296         currentWaitTime: 0,
298         /*Private: Our current hash location, without the "#" symbol.*/
299         currentLocation: null,
301         /*Private: Hidden iframe used to IE to detect history changes*/
302         iframe: null,
304         /*Private: Flags and DOM references used only by Safari*/
305         safariHistoryStartPoint: null,
306         safariStack: null,
307         safariLength: null,
309         /*Private: Flag used to keep checkLocation() from doing anything when it discovers location changes we've made ourselves
310         programmatically with the add() method. Basically, add() sets this to true. When checkLocation() discovers it's true,
311         it refrains from firing our listener, then resets the flag to false for next cycle. That way, our listener only gets fired on
312         history change events triggered by the user via back/forward buttons and manual hash changes. This flag also helps us set up
313         IE's special iframe-based method of handling history changes.*/
314         ignoreLocationChange: null,
316         /*Private: A flag that indicates that we should fire a history change event when we are ready, i.e. after we are initialized and
317         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
318         then return, we must fire this as a history change event. Unfortunately, we have lost all references to listeners from earlier,
319         because JavaScript clears out.*/
320         fireOnNewListener: null,
322         /*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
323         for another one, and then return, the page's onload listener fires again. We need a way to differentiate between the first page
324         load and subsequent ones. This variable works hand in hand with the pageLoaded variable we store into historyStorage.*/
325         firstLoad: null,
327         /*Private: A variable to handle an important edge case in IE. In IE, if a user manually types an address into their browser's
328         location bar, we must intercept this by calling checkLocation() at regular intervals. However, if we are programmatically
329         changing the location bar ourselves using the add() method, we need to ignore these changes in checkLocation(). Unfortunately,
330         these changes take several lines of code to complete, so for the duration of those lines of code, we set this variable to true.
331         That signals to checkLocation() to ignore the change-in-progress. Once we're done with our chunk of location-change code in
332         add(), we set this back to false. We'll do the same thing when capturing user-entered address changes in checkLocation itself.*/
333         ieAtomicLocationChange: null,
334         
335         /*Private: Create IE-specific DOM nodes and overrides*/
336         createIE: function(initialHash) {
337                 /*write out a hidden iframe for IE and set the amount of time to wait between add() requests*/
338                 this.waitTime = 400;/*IE needs longer between history updates*/
339                 var styles = (historyStorage.debugMode
340                         ? 'width: 800px;height:80px;border:1px solid black;'
341                         : historyStorage.hideStyles
342                 );
343                 var iframeID = "rshHistoryFrame";
344                 var iframeHTML = '<iframe frameborder="0" id="' + iframeID + '" style="' + styles + '" src="blank.html?' + initialHash + '"></iframe>';
345                 document.write(iframeHTML);
346                 this.iframe = document.getElementById(iframeID);
347         },
348         
349         /*Private: Create Opera-specific DOM nodes and overrides*/
350         createOpera: function() {
351                 this.waitTime = 400;/*Opera needs longer between history updates*/
352                 var imgHTML = '<img src="javascript:location.href=\'javascript:dhtmlHistory.checkLocation();\';" style="' + historyStorage.hideStyles + '" />';
353                 document.write(imgHTML);
354         },
355         
356         /*Private: Create Safari-specific DOM nodes and overrides*/
357         createSafari: function() {
358                 var formID = "rshSafariForm";
359                 var stackID = "rshSafariStack";
360                 var lengthID = "rshSafariLength";
361                 var formStyles = historyStorage.debugMode ? historyStorage.showStyles : historyStorage.hideStyles;
362                 var inputStyles = (historyStorage.debugMode
363                         ? 'width:800px;height:20px;border:1px solid black;margin:0;padding:0;'
364                         : historyStorage.hideStyles
365                 );
366                 var safariHTML = '<form id="' + formID + '" style="' + formStyles + '">'
367                         + '<input type="text" style="' + inputStyles + '" id="' + stackID + '" value="[]"/>'
368                         + '<input type="text" style="' + inputStyles + '" id="' + lengthID + '" value=""/>'
369                 + '</form>';
370                 document.write(safariHTML);
371                 this.safariStack = document.getElementById(stackID);
372                 this.safariLength = document.getElementById(lengthID);
373                 if (!historyStorage.hasKey(this.PAGELOADEDSTRING)) {
374                         this.safariHistoryStartPoint = history.length;
375                         this.safariLength.value = this.safariHistoryStartPoint;
376                 } else {
377                         this.safariHistoryStartPoint = this.safariLength.value;
378                 }
379         },
380         
381         /*Private: Safari method to read the history stack from a hidden form field*/
382         getSafariStack: function() {
383                 var r = this.safariStack.value;
384                 return historyStorage.fromJSON(r);
385         },
387         /*Private: Safari method to read from the history stack*/
388         getSafariState: function() {
389                 var stack = this.getSafariStack();
390                 var state = stack[history.length - this.safariHistoryStartPoint - 1];
391                 return state;
392         },                      
393         /*Private: Safari method to write the history stack to a hidden form field*/
394         putSafariState: function(newLocation) {
395             var stack = this.getSafariStack();
396             stack[history.length - this.safariHistoryStartPoint] = newLocation;
397             this.safariStack.value = historyStorage.toJSON(stack);
398         },
400         /*Private: Notify the listener of new history changes.*/
401         fireHistoryEvent: function(newHash) {
402                 /*extract the value from our history storage for this hash*/
403                 var historyData = historyStorage.get(newHash);
404                 /*call our listener*/
405                 this.listener.call(null, newHash, historyData);
406         },
407         
408         /*Private: See if the browser has changed location. This is the primary history mechanism for Firefox. For IE, we use this to
409         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
410         to intercept this and notify any history listener.*/
411         checkLocation: function() {
412                 
413                 /*Ignore any location changes that we made ourselves for browsers other than IE*/
414                 if (!this.isIE && this.ignoreLocationChange) {
415                         this.ignoreLocationChange = false;
416                         return;
417                 }
419                 /*If we are dealing with IE and we are in the middle of making a location change from an iframe, ignore it*/
420                 if (!this.isIE && this.ieAtomicLocationChange) {
421                         return;
422                 }
423                 
424                 /*Get hash location*/
425                 var hash = this.getCurrentLocation();
427                 /*Do nothing if there's been no change*/
428                 if (hash == this.currentLocation) {
429                         return;
430                 }
432                 /*In IE, users manually entering locations into the browser; we do this by comparing the browser's location against the
433                 iframe's location; if they differ, we are dealing with a manual event and need to place it inside our history, otherwise
434                 we can return*/
435                 this.ieAtomicLocationChange = true;
437                 if (this.isIE && this.getIframeHash() != hash) {
438                         this.iframe.src = "blank.html?" + hash;
439                 }
440                 else if (this.isIE) {
441                         /*the iframe is unchanged*/
442                         return;
443                 }
445                 /*Save this new location*/
446                 this.currentLocation = hash;
448                 this.ieAtomicLocationChange = false;
450                 /*Notify listeners of the change*/
451                 this.fireHistoryEvent(hash);
452         },
454         /*Private: Get the current location of IE's hidden iframe.*/
455         getIframeHash: function() {
456                 var doc = this.iframe.contentWindow.document;
457                 var hash = String(doc.location.search);
458                 if (hash.length == 1 && hash.charAt(0) == "?") {
459                         hash = "";
460                 }
461                 else if (hash.length >= 2 && hash.charAt(0) == "?") {
462                         hash = hash.substring(1);
463                 }
464                 return hash;
465         },
467         /*Private: Remove any leading hash that might be on a location.*/
468         removeHash: function(hashValue) {
469                 var r;
470                 if (hashValue === null || hashValue === undefined) {
471                         r = null;
472                 }
473                 else if (hashValue === "") {
474                         r = "";
475                 }
476                 else if (hashValue.length == 1 && hashValue.charAt(0) == "#") {
477                         r = "";
478                 }
479                 else if (hashValue.length > 1 && hashValue.charAt(0) == "#") {
480                         r = hashValue.substring(1);
481                 }
482                 else {
483                         r = hashValue;
484                 }
485                 return r;
486         },
488         /*Private: For IE, tell when the hidden iframe has finished loading.*/
489         iframeLoaded: function(newLocation) {
490                 /*ignore any location changes that we made ourselves*/
491                 if (this.ignoreLocationChange) {
492                         this.ignoreLocationChange = false;
493                         return;
494                 }
496                 /*Get the new location*/
497                 var hash = String(newLocation.search);
498                 if (hash.length == 1 && hash.charAt(0) == "?") {
499                         hash = "";
500                 }
501                 else if (hash.length >= 2 && hash.charAt(0) == "?") {
502                         hash = hash.substring(1);
503                 }
504                 /*Keep the browser location bar in sync with the iframe hash*/
505                 window.location.hash = hash;
507                 /*Notify listeners of the change*/
508                 this.fireHistoryEvent(hash);
509         }
514         historyStorage: An object that uses a hidden form to store history state across page loads. The mechanism for doing so relies on
515         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
516         the user navigates back to the page. This object can be used independently of the dhtmlHistory object for caching of Ajax
517         session information.
518         
519         dependencies: 
520                 * json2007.js (included in a separate file) or alternate JSON methods passed in through an options bundle.
522 window.historyStorage = {
523         
524         /*Public: Set up our historyStorage object for use by dhtmlHistory or other objects*/
525         setup: function(options) {
526                 
527                 /*
528                         options - object to store initialization parameters - passed in from dhtmlHistory or directly into historyStorage
529                         options.debugMode - boolean that causes hidden form fields to be shown for development purposes.
530                         options.toJSON - function to override default JSON stringifier
531                         options.fromJSON - function to override default JSON parser
532                 */
533                 
534                 /*process init parameters*/
535                 if (typeof options !== "undefined") {
536                         if (options.debugMode) {
537                                 this.debugMode = options.debugMode;
538                         }
539                         if (options.toJSON) {
540                                 this.toJSON = options.toJSON;
541                         }
542                         if (options.fromJSON) {
543                                 this.fromJSON = options.fromJSON;
544                         }
545                 }               
546                 
547                 /*write a hidden form and textarea into the page; we'll stow our history stack here*/
548                 var formID = "rshStorageForm";
549                 var textareaID = "rshStorageField";
550                 var formStyles = this.debugMode ? historyStorage.showStyles : historyStorage.hideStyles;
551                 var textareaStyles = (historyStorage.debugMode
552                         ? 'width: 800px;height:80px;border:1px solid black;'
553                         : historyStorage.hideStyles
554                 );
555                 var textareaHTML = '<form id="' + formID + '" style="' + formStyles + '">'
556                         + '<textarea id="' + textareaID + '" style="' + textareaStyles + '"></textarea>'
557                 + '</form>';
558                 document.write(textareaHTML);
559                 this.storageField = document.getElementById(textareaID);
560                 if (typeof window.opera !== "undefined") {
561                         this.storageField.focus();/*Opera needs to focus this element before persisting values in it*/
562                 }
563         },
564         
565         /*Public*/
566         put: function(key, value) {
567                 this.assertValidKey(key);
568                 /*if we already have a value for this, remove the value before adding the new one*/
569                 if (this.hasKey(key)) {
570                         this.remove(key);
571                 }
572                 /*store this new key*/
573                 this.storageHash[key] = value;
574                 /*save and serialize the hashtable into the form*/
575                 this.saveHashTable();
576         },
578         /*Public*/
579         get: function(key) {
580                 this.assertValidKey(key);
581                 /*make sure the hash table has been loaded from the form*/
582                 this.loadHashTable();
583                 var value = this.storageHash[key];
584                 if (value === undefined) {
585                         value = null;
586                 }
587                 return value;
588         },
590         /*Public*/
591         remove: function(key) {
592                 this.assertValidKey(key);
593                 /*make sure the hash table has been loaded from the form*/
594                 this.loadHashTable();
595                 /*delete the value*/
596                 delete this.storageHash[key];
597                 /*serialize and save the hash table into the form*/
598                 this.saveHashTable();
599         },
601         /*Public: Clears out all saved data.*/
602         reset: function() {
603                 this.storageField.value = "";
604                 this.storageHash = {};
605         },
607         /*Public*/
608         hasKey: function(key) {
609                 this.assertValidKey(key);
610                 /*make sure the hash table has been loaded from the form*/
611                 this.loadHashTable();
612                 return (typeof this.storageHash[key] !== "undefined");
613         },
615         /*Public*/
616         isValidKey: function(key) {
617                 return (typeof key === "string");
618         },
619         
620         /*Public - CSS strings utilized by both objects to hide or show behind-the-scenes DOM elements*/
621         showStyles: 'border:0;margin:0;padding:0;',
622         hideStyles: 'left:-1000px;top:-1000px;width:1px;height:1px;border:0;position:absolute;',
623         
624         /*Public - debug mode flag*/
625         debugMode: false,
626         
627         /*- - - - - - - - - - - -*/
629         /*Private: Our hash of key name/values.*/
630         storageHash: {},
632         /*Private: If true, we have loaded our hash table out of the storage form.*/
633         hashLoaded: false, 
635         /*Private: DOM reference to our history field*/
636         storageField: null,
638         /*Private: Assert that a key is valid; throw an exception if it not.*/
639         assertValidKey: function(key) {
640                 var isValid = this.isValidKey(key);
641                 if (!isValid && this.debugMode) {
642                         throw new Error("Please provide a valid key for window.historyStorage. Invalid key = " + key + ".");
643                 }
644         },
646         /*Private: Load the hash table up from the form.*/
647         loadHashTable: function() {
648                 if (!this.hashLoaded) { 
649                         var serializedHashTable = this.storageField.value;
650                         if (serializedHashTable !== "" && serializedHashTable !== null) {
651                                 this.storageHash = this.fromJSON(serializedHashTable);
652                                 this.hashLoaded = true;
653                         }
654                 }
655         },
656         /*Private: Save the hash table into the form.*/
657         saveHashTable: function() {
658                 this.loadHashTable();
659                 var serializedHashTable = this.toJSON(this.storageHash);
660                 this.storageField.value = serializedHashTable;
661         },
662         /*Private: Bridges for our JSON implementations - both rely on 2007 JSON.org library - can be overridden by options bundle*/
663         toJSON: function(o) {
664                 return o.toJSONString();
665         },
666         fromJSON: function(s) {
667                 return s.parseJSON();
668         }