/adei/trunk

To get this branch, use:
bzr branch http://darksoft.org/webbzr/adei/trunk
1 by Suren A. Chilingaryan
Initial import
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:
10
11
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
12
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.
17
*/
18
19
/*
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.
24
25
*/
26
window.dhtmlHistory = {
27
	
28
	/*Public: User-agent booleans*/
29
	isIE: false,
30
	isOpera: false,
31
	isSafari: false,
32
	isKonquerer: false,
33
	isGecko: false,
34
	isSupported: false,
35
	
36
	/*Public: Create the DHTML history infrastructure*/
37
	create: function(options) {
38
		
39
		/*
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
44
		*/
45
46
		var that = this;
47
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") {
56
			this.isOpera = true;
57
			this.isSupported = true;
58
		} else if (typeof document.all !== "undefined") {
59
			this.isIE = true;
60
			this.isSupported = true;
61
		} else if (vendor.indexOf("Apple Computer, Inc.") > -1) {
62
			this.isSafari = true;
63
			this.isSupported = true; //(platform.indexOf("mac") > -1);
64
		} else if (UA.indexOf("gecko") != -1) {
65
			this.isGecko = true;
66
			this.isSupported = true;
67
		}
68
69
		/*Set up the historyStorage object; pass in init parameters*/
70
		window.historyStorage.setup(options);
71
72
		/*Execute browser-specific setup methods*/
73
		if (this.isSafari) {
74
			this.createSafari();
75
		} else if (this.isOpera) {
76
			this.createOpera();
77
		}
78
		
79
		/*Get our initial location*/
80
		var initialHash = this.getCurrentLocation();
81
82
		/*Save it as our current location*/
83
		this.currentLocation = initialHash;
84
85
		/*Now that we have a hash, create IE-specific code*/
86
		if (this.isIE) {
87
			this.createIE(initialHash);
88
		}
89
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*/
93
94
		var unloadHandler = function() {
95
			that.firstLoad = null;
96
		};
97
		
98
		this.addEventListener(window,'unload',unloadHandler);		
99
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*/
102
		if (this.isIE) {
103
			/*The iframe will get loaded on page load, and we want to ignore this fact*/
104
			this.ignoreLocationChange = true;
105
		} else {
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);
111
			} else {
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;
118
			}
119
		}
120
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();
125
		};
126
		setInterval(locationHandler, 100);
127
	},	
128
	
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*/
132
		if (this.isIE) {
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);
139
			}
140
			/*Else if this is a fake onload event*/
141
			else {
142
				this.fireOnNewListener = true;
143
				this.firstLoad = false;   
144
			}
145
		}
146
	},
147
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;
155
		}
156
	},
157
	
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() {
164
				l(window.event);
165
			});
166
		}
167
	},
168
	
169
	/*Public: Add a history point.*/
170
	add: function(newLocation, historyData) {
171
172
		if (this.isSafari) {
173
			
174
			/*Remove any leading hash symbols on newLocation*/
175
			newLocation = this.removeHash(newLocation);
176
177
			/*Store the history data into history storage*/
178
			historyStorage.put(newLocation, historyData);
179
180
			/*Save this as our current location*/
181
			this.currentLocation = newLocation;
182
	
183
			/*Change the browser location*/
184
			window.location.hash = newLocation;
185
		
186
			/*Save this to the Safari form field*/
187
			this.putSafariState(newLocation);
188
189
		} else {
190
			
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.*/
194
			var that = this;
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;
200
				    }
201
				    
202
				    /*End of atomic location change block for IE*/
203
				    that.ieAtomicLocationChange = false;
204
				}
205
206
				/*Remove any leading hash symbols on newLocation*/
207
				newLocation = that.removeHash(newLocation);
208
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;
216
					throw new Error(e); 
217
				}
218
219
				/*Store the history data into history storage*/
220
				historyStorage.put(newLocation, historyData);
221
222
				/*Indicate to the browser to ignore this upcomming location change since we're making it programmatically*/
223
				that.ignoreLocationChange = true;
224
225
				/*Indicate to IE that this is an atomic location change block*/
226
				that.ieAtomicLocationChange = true;
227
228
				/*Save this as our current location*/
229
				that.currentLocation = newLocation;
230
		
231
				/*Change the browser location*/
232
				window.location.hash = newLocation;
233
234
				/*Change the hidden iframe's location if on IE*/
235
				if (that.isIE) {
236
					that.iframe.src = "blank.html?" + newLocation;
237
				}
238
				
239
				if (that.waitTime) {
240
				    window.setTimeout(fixWaitTime, that.waitTime);
241
				} else {
242
				    /*End of atomic location change block for IE*/
243
				    that.ieAtomicLocationChange = false;
244
				}
245
			};
246
			    
247
			    // Check?
248
			this.ieAtomicLocationChange = true;
249
			
250
			if (this.currentWaitTime) {
251
			    /*Now queue up this add request*/
252
			    window.setTimeout(addImpl, this.currentWaitTime);
253
254
			    /*Indicate that the next request will have to wait for awhile*/
255
			    this.currentWaitTime = this.currentWaitTime + this.waitTime;
256
			} else {
257
			    this.currentWaitTime = this.currentWaitTime + this.waitTime;
258
			    addImpl();
259
			}
260
261
		}
262
	},
263
264
	/*Public*/
265
	isFirstLoad: function() {
266
		return this.firstLoad;
267
	},
268
269
	/*Public*/
270
	getVersion: function() {
271
		return "0.6";
272
	},
273
274
	/*Get browser's current hash location; for Safari, read value from a hidden form field*/
275
276
	/*Public*/
277
	getCurrentLocation: function() {
278
		return this.getCurrentHash();
279
	/*
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
282
	    handling of Safari.
283
		var r = (this.isSafari
284
			? this.getSafariState()
285
			: this.getCurrentHash()
286
		);
287
		return r;
288
	*/
289
	},
290
	
291
	/*Public: Manually parse the current url for a hash; tip of the hat to YUI*/
292
    getCurrentHash: function() {
293
	    if (this.isIE) {
294
		var r = this.getIframeHash();
295
		if (r) return r;
296
	    }
297
		
298
		var r = window.location.href;
299
		var i = r.indexOf("#");
300
		return (i >= 0
301
			? r.substr(i+1)
302
			: ""
303
		);
304
    },
305
	
306
	/*- - - - - - - - - - - -*/
307
	
308
	/*Private: Constant for our own internal history event called when the page is loaded*/
309
	PAGELOADEDSTRING: "DhtmlHistory_pageLoaded",
310
	
311
	/*Private: Our history change listener.*/
312
	listener: null,
313
314
	/*Private: MS to wait between add requests - will be reset for certain browsers*/
315
	waitTime: 200,
316
	
317
	/*Private: MS before an add request can execute*/
318
	currentWaitTime: 0,
319
320
	/*Private: Our current hash location, without the "#" symbol.*/
321
	currentLocation: null,
322
323
	/*Private: Hidden iframe used to IE to detect history changes*/
324
	iframe: null,
325
326
	/*Private: Flags and DOM references used only by Safari*/
327
	safariHistoryStartPoint: null,
328
	safariStack: null,
329
	safariLength: null,
330
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,
337
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,
343
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.*/
347
	firstLoad: null,
348
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,
356
	
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
364
		);
365
		var iframeID = "rshHistoryFrame";
366
/*
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);
370
*/
371
372
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;
378
		else
379
		    node.setAttribute('style', styles);
380
		
381
		node.setAttribute("src", "blank.html?" + initialHash);
382
		this.iframe = node;
383
384
		document.body.appendChild(node);
385
	},
386
	
387
	/*Private: Create Opera-specific DOM nodes and overrides*/
388
	createOpera: function() {
389
		this.waitTime = 400;/*Opera needs longer between history updates*/
390
/*
391
		var imgHTML = '<img src="javascript:location.href=\'javascript:dhtmlHistory.checkLocation();\';" style="' + historyStorage.hideStyles + '" />';
392
		document.write(imgHTML);
393
*/
394
		var node = document.createElement('img');
395
		if (typeof node.style.cssText == "string")
396
		    node.style.cssText = historyStorage.hideStyles;
397
		else
398
		    node.setAttribute('style', historyStorage.hideStyles);
399
		node.setAttribute('src', "javascript:location.href=\'javascript:dhtmlHistory.checkLocation();\';");
400
401
		document.body.appendChild(node);
402
403
	},
404
	
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
414
		);
415
416
/*
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=""/>'
420
		+ '</form>';
421
		document.write(safariHTML);
422
		this.safariStack = document.getElementById(stackID);
423
		this.safariLength = document.getElementById(lengthID);
424
*/
425
426
		var fnode = document.createElement('form');
427
		if (typeof fnode.style.cssText == "string")
428
		    fnode.style.cssText = formStyles;
429
		else
430
		    fnode.setAttribute('style', formStyles);
431
		fnode.setAttribute('id', formID);
432
		
433
		var node = document.createElement('input');
434
		node.setAttribute('type', "text");
435
		if (typeof node.style.cssText == "string")
436
		    node.style.cssText = inputStyles;
437
		else
438
		    node.setAttribute('style', inputStyles);
439
		node.setAttribute('id', stackID);
440
		node.setAttribute('value', "[]");
441
442
		fnode.appendChild(node);
443
		this.safariStack = node;
444
		
445
		node = document.createElement('input');
446
		node.setAttribute('type', "text");
447
		if (typeof node.style.cssText == "string")
448
		    node.style.cssText = inputStyles;
449
		else
450
		    node.setAttribute('style', inputStyles);
451
		node.setAttribute('id', lengthID);
452
		node.setAttribute('value', "");
453
454
		fnode.appendChild(node);
455
		this.safariLength = node;
456
		
457
		document.body.appendChild(fnode);
458
459
		if (!historyStorage.hasKey(this.PAGELOADEDSTRING)) {
460
			this.safariHistoryStartPoint = history.length;
461
			this.safariLength.value = this.safariHistoryStartPoint;
462
		} else {
463
			this.safariHistoryStartPoint = this.safariLength.value;
464
		}
465
	},
466
	
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);
471
	},
472
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];
477
		return state;
478
	},			
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);
484
	},
485
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);
492
	},
493
	
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() {
498
		
499
		/*Ignore any location changes that we made ourselves for browsers other than IE*/
500
		if (!this.isIE && this.ignoreLocationChange) {
501
			this.ignoreLocationChange = false;
502
			return;
503
		}
504
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) {
507
			return;
508
		}
509
		
510
		/*Get hash location*/
511
		var hash = this.getCurrentLocation();
512
513
		/*Do nothing if there's been no change*/
514
		if (hash == this.currentLocation) {
515
			return;
516
		}
517
//		alert(hash + ' - ' + this.currentLocation);
518
		
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
521
		we can return*/
522
		this.ieAtomicLocationChange = true;
523
524
		if (this.isIE) {
525
		    this.iframe.src = "blank.html?" + hash;
526
		    window.location.hash = hash;
527
		}
528
529
/*
530
		if (this.isIE && this.getIframeHash() != hash) {
531
			this.iframe.src = "blank.html?" + hash;
532
		}
533
		else if (this.isIE) {
534
			return;
535
		}
536
*/
537
538
		/*Save this new location*/
539
		this.currentLocation = hash;
540
541
		this.ieAtomicLocationChange = false;
542
543
		/*Notify listeners of the change*/
544
		this.fireHistoryEvent(hash);
545
	},
546
547
	/*Private: Get the current location of IE's hidden iframe.*/
548
	getIframeHash: function() {
549
		if (!this.iframe) return "";
550
		
551
		var doc = this.iframe.contentWindow.document;
552
		var hash = String(doc.location.search);
553
		if (hash.length == 1 && hash.charAt(0) == "?") {
554
			hash = "";
555
		}
556
		else if (hash.length >= 2 && hash.charAt(0) == "?") {
557
			hash = hash.substring(1);
558
		}
559
		return hash;
560
	},
561
562
	/*Private: Remove any leading hash that might be on a location.*/
563
	removeHash: function(hashValue) {
564
		var r;
565
		if (hashValue === null || hashValue === undefined) {
566
			r = null;
567
		}
568
		else if (hashValue === "") {
569
			r = "";
570
		}
571
		else if (hashValue.length == 1 && hashValue.charAt(0) == "#") {
572
			r = "";
573
		}
574
		else if (hashValue.length > 1 && hashValue.charAt(0) == "#") {
575
			r = hashValue.substring(1);
576
		}
577
		else {
578
			r = hashValue;
579
		}
580
		return r;
581
	},
582
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;
588
			return;
589
		}
590
591
		/*Get the new location*/
592
		var hash = String(newLocation.search);
593
		if (hash.length == 1 && hash.charAt(0) == "?") {
594
			hash = "";
595
		}
596
		else if (hash.length >= 2 && hash.charAt(0) == "?") {
597
			hash = hash.substring(1);
598
		}
599
		/*Keep the browser location bar in sync with the iframe hash*/
600
		window.location.hash = hash;
601
602
		/*Notify listeners of the change*/
603
		this.fireHistoryEvent(hash);
604
	}
605
606
};
607
608
/*
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
612
	session information.
613
	
614
	dependencies: 
615
		* json2007.js (included in a separate file) or alternate JSON methods passed in through an options bundle.
616
*/
617
window.historyStorage = {
618
	
619
	/*Public: Set up our historyStorage object for use by dhtmlHistory or other objects*/
620
	setup: function(options) {
621
		
622
		/*
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
627
		*/
628
		
629
		/*process init parameters*/
630
		if (typeof options !== "undefined") {
631
			if (options.debugMode) {
632
				this.debugMode = options.debugMode;
633
			}
634
			if (options.toJSON) {
635
				this.toJSON = options.toJSON;
636
			}
637
			if (options.fromJSON) {
638
				this.fromJSON = options.fromJSON;
639
			}
640
		}		
641
		
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
649
		);
650
		
651
		/*
652
		var textareaHTML = '<form id="' + formID + '" style="' + formStyles + '">'
653
			+ '<textarea id="' + textareaID + '" style="' + textareaStyles + '"></textarea>'
654
		+ '</form>';
655
		document.write(textareaHTML);
656
		this.storageField = document.getElementById(textareaID);
657
		*/
658
		
659
		var node = document.createElement('textarea');
660
		node.setAttribute('id', textareaID);
661
		if (typeof node.style.cssText == "string")
662
		    node.style.cssText = textareaStyles;
663
		else
664
		    node.setAttribute('style', textareaStyles);
665
666
		var fnode = document.createElement('form');
667
		fnode.setAttribute('id', formID);
668
		if (typeof fnode.style.cssText == "string")
669
		    fnode.style.cssText = formStyles;
670
		else
671
		    fnode.setAttribute('style', formStyles);
672
		fnode.appendChild(node);
673
674
		this.storageField = node;
675
		document.body.appendChild(fnode);
676
677
		if (typeof window.opera !== "undefined") {
678
			this.storageField.focus();/*Opera needs to focus this element before persisting values in it*/
679
		}
680
	},
681
	
682
	/*Public*/
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)) {
687
			this.remove(key);
688
		}
689
		/*store this new key*/
690
		this.storageHash[key] = value;
691
		/*save and serialize the hashtable into the form*/
692
		this.saveHashTable();
693
	},
694
695
	/*Public*/
696
	get: function(key) {
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) {
702
			value = null;
703
		}
704
		return value;
705
	},
706
707
	/*Public*/
708
	remove: function(key) {
709
		this.assertValidKey(key);
710
		/*make sure the hash table has been loaded from the form*/
711
		this.loadHashTable();
712
		/*delete the value*/
713
		delete this.storageHash[key];
714
		/*serialize and save the hash table into the form*/
715
		this.saveHashTable();
716
	},
717
718
	/*Public: Clears out all saved data.*/
719
	reset: function() {
720
		this.storageField.value = "";
721
		this.storageHash = {};
722
	},
723
724
	/*Public*/
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");
730
	},
731
732
	/*Public*/
733
	isValidKey: function(key) {
734
		return (typeof key === "string");
735
	},
736
	
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;',
740
	
741
	/*Public - debug mode flag*/
742
	debugMode: false,
743
	
744
	/*- - - - - - - - - - - -*/
745
746
	/*Private: Our hash of key name/values.*/
747
	storageHash: {},
748
749
	/*Private: If true, we have loaded our hash table out of the storage form.*/
750
	hashLoaded: false, 
751
752
	/*Private: DOM reference to our history field*/
753
	storageField: null,
754
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 + ".");
760
		}
761
	},
762
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;
770
			}
771
		}
772
	},
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;
778
	},
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();
782
	},
783
	fromJSON: function(s) {
784
		return s.parseJSON();
785
	}
786
};