1 /* 2 * ***** BEGIN LICENSE BLOCK ***** 3 * Zimbra Collaboration Suite Web Client 4 * Copyright (C) 2007, 2009, 2010, 2012, 2013, 2014, 2016 Synacor, Inc. 5 * 6 * The contents of this file are subject to the Common Public Attribution License Version 1.0 (the "License"); 7 * you may not use this file except in compliance with the License. 8 * You may obtain a copy of the License at: https://www.zimbra.com/license 9 * The License is based on the Mozilla Public License Version 1.1 but Sections 14 and 15 10 * have been added to cover use of software over a computer network and provide for limited attribution 11 * for the Original Developer. In addition, Exhibit A has been modified to be consistent with Exhibit B. 12 * 13 * Software distributed under the License is distributed on an "AS IS" basis, 14 * WITHOUT WARRANTY OF ANY KIND, either express or implied. 15 * See the License for the specific language governing rights and limitations under the License. 16 * The Original Code is Zimbra Open Source Web Client. 17 * The Initial Developer of the Original Code is Zimbra, Inc. All rights to the Original Code were 18 * transferred by Zimbra, Inc. to Synacor, Inc. on September 14, 2015. 19 * 20 * All portions of the code are Copyright (C) 2007, 2009, 2010, 2012, 2013, 2014, 2016 Synacor, Inc. All Rights Reserved. 21 * ***** END LICENSE BLOCK ***** 22 */ 23 /* 24 Derived from "Really Simple History", by Brad Neuberg. Its copyright follows: 25 26 Copyright (c) 2005, Brad Neuberg, bkn3@columbia.edu 27 http://codinginparadise.org 28 29 Permission is hereby granted, free of charge, to any person obtaining 30 a copy of this software and associated documentation files (the "Software"), 31 to deal in the Software without restriction, including without limitation 32 the rights to use, copy, modify, merge, publish, distribute, sublicense, 33 and/or sell copies of the Software, and to permit persons to whom the 34 Software is furnished to do so, subject to the following conditions: 35 36 The above copyright notice and this permission notice shall be 37 included in all copies or substantial portions of the Software. 38 39 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 40 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 41 OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 42 IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 43 CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT 44 OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR 45 THE USE OR OTHER DEALINGS IN THE SOFTWARE. 46 */ 47 48 /** 49 * Initializes history support. 50 * @constructor 51 * @class 52 * This singleton class provides support for handling history changes via the Back 53 * and Forward buttons. Since an Ajax application represents a single URL, hitting 54 * the Back button will ordinarily unload the app, which is usually not what the user 55 * wants to do. Changing the hash value in the browser's location will affect the 56 * browser history, but not the content. IE also uses a hidden iframe to track history. 57 * <p> 58 * The code below is a stripped-down version of Brad Neuberg's Really Simple History 59 * (see copyright above). Support for history storage has been removed.</p> 60 * 61 * @author Conrad Damon 62 * 63 * TODO: - add enable() 64 * 65 * @private 66 */ 67 AjxHistoryMgr = function() { 68 69 this.currentLocation = null; // Our current hash location, without the "#" symbol. 70 this.listener = null; // Our history change listener. */ 71 this.iframe = null; // A hidden IFrame we use in Internet Explorer to detect history changes. 72 this.ignoreLocationChange = null; // Indicates to the browser whether to ignore location changes. 73 74 // The amount of time in ms to wait between add requests. 75 this.WAIT_TIME = AjxEnv.isIE ? 400 : 200; 76 77 this.currentWaitTime = 0; // Time in ms to wait before calling setTimeout to add a location 78 79 /** A variable to handle an important edge case in Internet 80 Explorer. In IE, if a user manually types an address into 81 their browser's location bar, we must intercept this by 82 continuously checking the location bar with a timer 83 interval. However, if we manually change the location 84 bar ourselves programmatically, when using our hidden 85 iframe, we need to ignore these changes. Unfortunately, 86 these changes are not atomic, so we surround them with 87 the variable 'ieAtomicLocationChange', that if true, 88 means we are programmatically setting the location and 89 should ignore this atomic chunked change. */ 90 this.ieAtomicLocationChange = null; 91 92 this._eventMgr = new AjxEventMgr(); 93 this._evt = new AjxEvent(); 94 95 this._initialize(); 96 } 97 98 // Name of the file that has content for the iframe 99 AjxHistoryMgr.BLANK_FILE = "blankHistory.html"; 100 101 // ID for the iframe 102 AjxHistoryMgr.IFRAME_ID = "DhtmlHistoryFrame"; 103 104 105 /** 106 * Adds a history change listener. 107 */ 108 AjxHistoryMgr.prototype.addListener = 109 function(listener) { 110 return this._eventMgr.addListener(AjxEvent.HISTORY, listener); 111 }; 112 113 /** 114 * Removes a history change listener. 115 */ 116 AjxHistoryMgr.prototype.removeListener = 117 function(listener) { 118 return this._eventMgr.removeListener(AjxEvent.HISTORY, listener); 119 }; 120 121 /** 122 * Most browsers require that we wait a certain amount of time before changing the 123 * location, such as 200 milliseconds; rather than forcing external callers to use 124 * window.setTimeout to account for this to prevent bugs, we internally handle this 125 * detail by using a 'currentWaitTime' variable and have requests wait in line 126 */ 127 AjxHistoryMgr.prototype.add = 128 function(newLocation) { 129 130 var self = this; 131 var addImpl = function() { 132 133 // indicate that the current wait time is now less 134 if (self.currentWaitTime > 0) { 135 self.currentWaitTime = self.currentWaitTime - self.WAIT_TIME; 136 } 137 138 // remove any leading hash symbols on newLocation 139 newLocation = self._removeHash(newLocation); 140 141 // IE has a strange bug; if the newLocation 142 // is the same as _any_ preexisting id in the 143 // document, then the history action gets recorded 144 // twice; throw a programmer exception if there is 145 // an element with this ID 146 if (AjxEnv.isIE) { 147 if (document.getElementById(newLocation)) { 148 throw new DwtException("AjxHistoryMgr: location has same ID as DOM element"); 149 } 150 } 151 152 // indicate to the browser to ignore this upcoming location change 153 self.ignoreLocationChange = true; 154 155 // indicate to IE that this is an atomic location change block 156 this.ieAtomicLocationChange = true; 157 158 // save this as our current location 159 self.currentLocation = newLocation; 160 161 // change the browser location 162 window.location.hash = newLocation; 163 164 // change the hidden iframe's location if on IE 165 if (AjxEnv.isIE) { 166 self.iframe.src = AjxHistoryMgr.BLANK_FILE + "?" + newLocation; 167 } 168 169 // end of atomic location change block for IE 170 this.ieAtomicLocationChange = false; 171 }; 172 173 // queue up requests 174 window.setTimeout(addImpl, this.currentWaitTime); 175 176 // indicate that the next request will have to wait for awhile 177 this.currentWaitTime = this.currentWaitTime + self.WAIT_TIME; 178 }; 179 180 /** 181 * Returns the current hash value that is in the browser's 182 * location bar, removing leading # symbols if they are present. 183 */ 184 AjxHistoryMgr.prototype.getCurrentLocation = 185 function() { 186 return this._removeHash(window.location.hash); 187 }; 188 189 /** 190 * Creates the DHTML history infrastructure. 191 */ 192 AjxHistoryMgr.prototype._initialize = 193 function() { 194 195 // get our initial location 196 var initialHash = this.getCurrentLocation(); 197 198 // save this as our current location 199 this.currentLocation = initialHash; 200 201 // write out a hidden iframe for IE and 202 // set the amount of time to wait between add() requests 203 if (AjxEnv.isIE) { 204 DBG.println(AjxDebug.DBG2, "Creating iframe for IE: " + AjxHistoryMgr.BLANK_FILE); 205 var html = []; 206 var i = 0; 207 html[i++] = "<iframe style='border: 0px; width: 1px; "; 208 html[i++] = "height: 1px; position: absolute; bottom: 0px; "; 209 html[i++] = "right: 0px; visibility: visible;' "; 210 html[i++] = "id='" + AjxHistoryMgr.IFRAME_ID + "' "; 211 html[i++] = "src='" + AjxHistoryMgr.BLANK_FILE + "?" + initialHash + "'>"; 212 html[i++] = "</iframe>"; 213 214 var htmlElement = document.createElement("div"); 215 document.body.appendChild(htmlElement); 216 htmlElement.innerHTML = html.join(""); 217 } 218 219 if (AjxEnv.isIE) { 220 this.iframe = document.getElementById(AjxHistoryMgr.IFRAME_ID); 221 } 222 223 // other browsers can use a location handler that checks 224 // at regular intervals as their primary mechanism; 225 // we use it for Internet Explorer as well to handle 226 // an important edge case; see _checkLocation() for 227 // details 228 var self = this; 229 var locationHandler = function() { 230 self._checkLocation(); 231 }; 232 window.onhashchange = locationHandler; 233 }; 234 235 /** 236 * Checks if the browsers has changed location. This is the primary history mechanism 237 * for Firefox. For Internet Explorer, we use this to handle an important edge case: 238 * if a user manually types in a new hash value into their Internet Explorer location 239 * bar and press enter, we want to intercept this and notify any history listener. 240 */ 241 AjxHistoryMgr.prototype._checkLocation = 242 function() { 243 // ignore any location changes that we made ourselves 244 // for browsers other than Internet Explorer 245 if (!AjxEnv.isIE && this.ignoreLocationChange) { 246 this.ignoreLocationChange = false; 247 return; 248 } 249 250 // if we are dealing with Internet Explorer 251 // and we are in the middle of making a location 252 // change from an iframe, ignore it 253 if (!AjxEnv.isIE && this.ieAtomicLocationChange) { 254 return; 255 } 256 257 // get hash location 258 var hash = this.getCurrentLocation(); 259 260 // see if there has been a change 261 if (hash == this.currentLocation) { return; } 262 263 // on Internet Explorer, we need to intercept users manually 264 // entering locations into the browser; we do this by comparing 265 // the browsers location against the iframes location; if they 266 // differ, we are dealing with a manual event and need to 267 // place it inside our history, otherwise we can return 268 this.ieAtomicLocationChange = true; 269 270 if (AjxEnv.isIE && this._getIFrameHash() != hash) { 271 this.iframe.src = AjxHistoryMgr.BLANK_FILE + "?" + hash; 272 } else if (AjxEnv.isIE) { 273 // the iframe is unchanged 274 return; 275 } 276 277 // save this new location 278 this.currentLocation = hash; 279 280 this.ieAtomicLocationChange = false; 281 282 // notify listeners of the change 283 this._evt.data = hash; 284 this._eventMgr.notifyListeners(AjxEvent.HISTORY, this._evt); 285 }; 286 287 /** 288 * Gets the current location of the hidden IFrames 289 * that is stored as history. For Internet Explorer. 290 */ 291 AjxHistoryMgr.prototype._getIFrameHash = 292 function() { 293 // get the new location 294 var historyFrame = document.getElementById(AjxHistoryMgr.IFRAME_ID); 295 var doc = historyFrame.contentWindow.document; 296 var hash = new String(doc.location.search); 297 298 if (hash.length == 1 && hash.charAt(0) == "?") { 299 hash = ""; 300 } else if (hash.length >= 2 && hash.charAt(0) == "?") { 301 hash = hash.substring(1); 302 } 303 304 return hash; 305 }; 306 307 /** 308 * Removes any leading hash that might be on a location. 309 */ 310 AjxHistoryMgr.prototype._removeHash = 311 function(hashValue) { 312 if (hashValue == null || hashValue == undefined) { 313 return null; 314 } else if (hashValue == "") { 315 return ""; 316 } else if (hashValue.length == 1 && hashValue.charAt(0) == "#") { 317 return ""; 318 } else if (hashValue.length > 1 && hashValue.charAt(0) == "#") { 319 return hashValue.substring(1); 320 } else { 321 return hashValue; 322 } 323 }; 324 325 /** 326 * For IE, says when the hidden iframe has finished loading. 327 */ 328 AjxHistoryMgr.prototype.iframeLoaded = 329 function(newLocation) { 330 331 // ignore any location changes that we made ourselves 332 if (this.ignoreLocationChange) { 333 this.ignoreLocationChange = false; 334 return; 335 } 336 337 // get the new location 338 var hash = new String(newLocation.search); 339 if (hash.length == 1 && hash.charAt(0) == "?") { 340 hash = ""; 341 } else if (hash.length >= 2 && hash.charAt(0) == "?") { 342 hash = hash.substring(1); 343 } 344 345 // move to this location in the browser location bar 346 window.location.hash = hash; 347 348 // notify listeners of the change 349 this._evt.data = hash; 350 this._eventMgr.notifyListeners(AjxEvent.HISTORY, this._evt); 351 }; 352