summaryrefslogtreecommitdiff
path: root/lib/dojo/hash.js
blob: 6ef74b5216a45abd75d010f6e3827dcc4a1414a9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
/*
	Copyright (c) 2004-2010, The Dojo Foundation All Rights Reserved.
	Available via Academic Free License >= 2.1 OR the modified BSD license.
	see: http://dojotoolkit.org/license for details
*/


if(!dojo._hasResource["dojo.hash"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
dojo._hasResource["dojo.hash"] = true;
dojo.provide("dojo.hash");
//TODOC: where does this go?
// summary:
//		Methods for monitoring and updating the hash in the browser URL.
//
// example:
//		dojo.subscribe("/dojo/hashchange", context, callback);
//
//		function callback (hashValue){
//			// do something based on the hash value.
// 		}

(function(){
	dojo.hash = function(/* String? */ hash, /* Boolean? */ replace){
		//	summary:
		//		Gets or sets the hash string.
		//	description:
		//		Handles getting and setting of location.hash.
		//		 - If no arguments are passed, acts as a getter.
		//		 - If a string is passed, acts as a setter.
		//	hash: 
		//		String: the hash is set - #string.
		//	replace:
		//		Boolean: If true, updates the hash value in the current history 
		//			state instead of creating a new history state.
		//	returns:
		//		when used as a getter, returns the current hash string.
		//		when used as a setter, returns the new hash string.
		
		// getter
		if(!arguments.length){
			return _getHash();
		}
		// setter
		if(hash.charAt(0) == "#"){
			hash = hash.substring(1);
		}
		if(replace){
			_replace(hash);
		}else{
			location.href = "#" + hash;
		}
		return hash; // String
	}

	// Global vars
	var _recentHash = null,
		_ieUriMonitor = null,
		_pollFrequency = dojo.config.hashPollFrequency || 100;

	//Internal functions
	function _getSegment(str, delimiter){
		var i = str.indexOf(delimiter);
		return (i >= 0) ? str.substring(i+1) : "";  
	}
	
	function _getHash(){
		return _getSegment(location.href, "#");
	}

	function _dispatchEvent(){
		dojo.publish("/dojo/hashchange", [_getHash()]);
	}

	function _pollLocation(){
		if(_getHash() === _recentHash){
			return;
		}
		_recentHash = _getHash();
		_dispatchEvent();
	}
	
	function _replace(hash){
		if(_ieUriMonitor){
			if(_ieUriMonitor.isTransitioning()){
				setTimeout(dojo.hitch(null,_replace,hash), _pollFrequency);
				return;
			}
			var href = _ieUriMonitor.iframe.location.href;
			var index = href.indexOf('?');
			// main frame will detect and update itself
			_ieUriMonitor.iframe.location.replace(href.substring(0, index) + "?" + hash);
			return;
		}
		location.replace("#"+hash);
		_pollLocation();
	}

	function IEUriMonitor(){
		// summary:
		//		Determine if the browser's URI has changed or if the user has pressed the 
		//		back or forward button. If so, call _dispatchEvent.
		//
		//	description:
		//		IE doesn't add changes to the URI's hash into the history unless the hash
		//		value corresponds to an actual named anchor in the document. To get around
		//      this IE difference, we use a background IFrame to maintain a back-forward
		//		history, by updating the IFrame's query string to correspond to the
		//		value of the main browser location's hash value.
		//
		//		E.g. if the value of the browser window's location changes to
		//
		//		#action=someAction
		//
		//		... then we'd update the IFrame's source to:
		//
		//		?action=someAction
		//
		//		This design leads to a somewhat complex state machine, which is
		//		described below:
		//
		//		s1: Stable state - neither the window's location has changed nor
		//			has the IFrame's location. Note that this is the 99.9% case, so
		//			we optimize for it.
		//			Transitions: s1, s2, s3
		//		s2: Window's location changed - when a user clicks a hyperlink or
		//			code programmatically changes the window's URI.
		//			Transitions: s4
		//		s3: Iframe's location changed as a result of user pressing back or
		//			forward - when the user presses back or forward, the location of
		//			the background's iframe changes to the previous or next value in
		//			its history.
		//			Transitions: s1
		//		s4: IEUriMonitor has programmatically changed the location of the
		//			background iframe, but it's location hasn't yet changed. In this
		//			case we do nothing because we need to wait for the iframe's
		//			location to reflect its actual state.
		//			Transitions: s4, s5
		//		s5:	IEUriMonitor has programmatically changed the location of the
		//			background iframe, and the iframe's location has caught up with
		//			reality. In this case we need to transition to s1.
		//			Transitions: s1
		//
		//		The hashchange event is always dispatched on the transition back to s1.
		//

		// create and append iframe
		var ifr = document.createElement("iframe"),
			IFRAME_ID = "dojo-hash-iframe",
			ifrSrc = dojo.config.dojoBlankHtmlUrl || dojo.moduleUrl("dojo", "resources/blank.html");
		ifr.id = IFRAME_ID;
		ifr.src = ifrSrc + "?" + _getHash();
		ifr.style.display = "none";
		document.body.appendChild(ifr);

		this.iframe = dojo.global[IFRAME_ID];
		var recentIframeQuery, transitioning, expectedIFrameQuery, docTitle, ifrOffline,
			iframeLoc = this.iframe.location;

		function resetState(){
			_recentHash = _getHash();
			recentIframeQuery = ifrOffline ? _recentHash : _getSegment(iframeLoc.href, "?");
			transitioning = false;
			expectedIFrameQuery = null;
		}

		this.isTransitioning = function(){
			return transitioning;
		}
		
		this.pollLocation = function(){
			if(!ifrOffline) {
				try{
					//see if we can access the iframe's location without a permission denied error
					var iframeSearch = _getSegment(iframeLoc.href, "?");
					//good, the iframe is same origin (no thrown exception)
					if(document.title != docTitle){ //sync title of main window with title of iframe.
						docTitle = this.iframe.document.title = document.title;
					}
				}catch(e){
					//permission denied - server cannot be reached.
					ifrOffline = true;
					console.error("dojo.hash: Error adding history entry. Server unreachable.");
				}
			}
			var hash = _getHash();
			if(transitioning && _recentHash === hash){
				// we're in an iframe transition (s4 or s5)
				if(ifrOffline || iframeSearch === expectedIFrameQuery){
					// s5 (iframe caught up to main window or iframe offline), transition back to s1
					resetState();
					_dispatchEvent();
				}else{
					// s4 (waiting for iframe to catch up to main window)
					setTimeout(dojo.hitch(this,this.pollLocation),0);
					return;
				}
			}else if(_recentHash === hash && (ifrOffline || recentIframeQuery === iframeSearch)){
				// we're in stable state (s1, iframe query == main window hash), do nothing
			}else{
				// the user has initiated a URL change somehow.
				// sync iframe query <-> main window hash
				if(_recentHash !== hash){
					// s2 (main window location changed), set iframe url and transition to s4
					_recentHash = hash;
					transitioning = true;
					expectedIFrameQuery = hash;
					ifr.src = ifrSrc + "?" + expectedIFrameQuery;
					ifrOffline = false;	//we're updating the iframe src - set offline to false so we can check again on next poll.
					setTimeout(dojo.hitch(this,this.pollLocation),0); //yielded transition to s4 while iframe reloads.
					return;
				}else if(!ifrOffline){
					// s3 (iframe location changed via back/forward button), set main window url and transition to s1.
					location.href = "#" + iframeLoc.search.substring(1);
					resetState();
					_dispatchEvent();
				}
			}
			setTimeout(dojo.hitch(this,this.pollLocation), _pollFrequency);
		}
		resetState(); // initialize state (transition to s1)
		setTimeout(dojo.hitch(this,this.pollLocation), _pollFrequency);
	}
	dojo.addOnLoad(function(){
		if("onhashchange" in dojo.global && (!dojo.isIE || (dojo.isIE >= 8 && document.compatMode != "BackCompat"))){	//need this IE browser test because "onhashchange" exists in IE8 in IE7 mode
			dojo.connect(dojo.global,"onhashchange",_dispatchEvent);
		}else{
			if(document.addEventListener){ // Non-IE
				_recentHash = _getHash();
				setInterval(_pollLocation, _pollFrequency); //Poll the window location for changes
			}else if(document.attachEvent){ // IE7-
				//Use hidden iframe in versions of IE that don't have onhashchange event
				_ieUriMonitor = new IEUriMonitor();
			} 
			// else non-supported browser, do nothing.
		}
	});
})();

}