1<!-- 2@license 3Copyright (c) 2015 The Polymer Project Authors. All rights reserved. 4This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt 5The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt 6The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt 7Code distributed by Google as part of the polymer project is also 8subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt 9--> 10 11<link rel="import" href="../polymer/polymer.html"> 12 13<!-- 14 15The `iron-location` element manages binding to and from the current URL. 16 17iron-location is the first, and lowest level element in the Polymer team's 18routing system. This is a beta release of iron-location as we continue work 19on higher level elements, and as such iron-location may undergo breaking 20changes. 21 22#### Properties 23 24When the URL is: `/search?query=583#details` iron-location's properties will be: 25 26 - path: `'/search'` 27 - query: `'query=583'` 28 - hash: `'details'` 29 30These bindings are bidirectional. Modifying them will in turn modify the URL. 31 32iron-location is only active while it is attached to the document. 33 34#### Links 35 36While iron-location is active in the document it will intercept clicks on links 37within your site, updating the URL pushing the updated URL out through the 38databinding system. iron-location only intercepts clicks with the intent to 39open in the same window, so middle mouse clicks and ctrl/cmd clicks work fine. 40 41You can customize this behavior with the `urlSpaceRegex`. 42 43#### Dwell Time 44 45iron-location protects against accidental history spamming by only adding 46entries to the user's history if the URL stays unchanged for `dwellTime` 47milliseconds. 48 49@demo demo/index.html 50 51 --> 52<script> 53 (function() { 54 'use strict'; 55 56 Polymer({ 57 is: 'iron-location', 58 properties: { 59 /** 60 * The pathname component of the URL. 61 */ 62 path: { 63 type: String, 64 notify: true, 65 value: function() { 66 return window.decodeURIComponent(window.location.pathname); 67 } 68 }, 69 /** 70 * The query string portion of the URL. 71 */ 72 query: { 73 type: String, 74 notify: true, 75 value: function() { 76 return window.location.search.slice(1); 77 } 78 }, 79 /** 80 * The hash component of the URL. 81 */ 82 hash: { 83 type: String, 84 notify: true, 85 value: function() { 86 return window.decodeURIComponent(window.location.hash.slice(1)); 87 } 88 }, 89 /** 90 * If the user was on a URL for less than `dwellTime` milliseconds, it 91 * won't be added to the browser's history, but instead will be replaced 92 * by the next entry. 93 * 94 * This is to prevent large numbers of entries from clogging up the user's 95 * browser history. Disable by setting to a negative number. 96 */ 97 dwellTime: { 98 type: Number, 99 value: 2000 100 }, 101 102 /** 103 * A regexp that defines the set of URLs that should be considered part 104 * of this web app. 105 * 106 * Clicking on a link that matches this regex won't result in a full page 107 * navigation, but will instead just update the URL state in place. 108 * 109 * This regexp is given everything after the origin in an absolute 110 * URL. So to match just URLs that start with /search/ do: 111 * url-space-regex="^/search/" 112 * 113 * @type {string|RegExp} 114 */ 115 urlSpaceRegex: { 116 type: String, 117 value: '' 118 }, 119 120 /** 121 * urlSpaceRegex, but coerced into a regexp. 122 * 123 * @type {RegExp} 124 */ 125 _urlSpaceRegExp: { 126 computed: '_makeRegExp(urlSpaceRegex)' 127 }, 128 129 _lastChangedAt: { 130 type: Number 131 }, 132 133 _initialized: { 134 type: Boolean, 135 value: false 136 } 137 }, 138 hostAttributes: { 139 hidden: true 140 }, 141 observers: [ 142 '_updateUrl(path, query, hash)' 143 ], 144 attached: function() { 145 this.listen(window, 'hashchange', '_hashChanged'); 146 this.listen(window, 'location-changed', '_urlChanged'); 147 this.listen(window, 'popstate', '_urlChanged'); 148 this.listen(/** @type {!HTMLBodyElement} */(document.body), 'click', '_globalOnClick'); 149 // Give a 200ms grace period to make initial redirects without any 150 // additions to the user's history. 151 this._lastChangedAt = window.performance.now() - (this.dwellTime - 200); 152 153 this._initialized = true; 154 this._urlChanged(); 155 }, 156 detached: function() { 157 this.unlisten(window, 'hashchange', '_hashChanged'); 158 this.unlisten(window, 'location-changed', '_urlChanged'); 159 this.unlisten(window, 'popstate', '_urlChanged'); 160 this.unlisten(/** @type {!HTMLBodyElement} */(document.body), 'click', '_globalOnClick'); 161 this._initialized = false; 162 }, 163 _hashChanged: function() { 164 this.hash = window.decodeURIComponent(window.location.hash.substring(1)); 165 }, 166 _urlChanged: function() { 167 // We want to extract all info out of the updated URL before we 168 // try to write anything back into it. 169 // 170 // i.e. without _dontUpdateUrl we'd overwrite the new path with the old 171 // one when we set this.hash. Likewise for query. 172 this._dontUpdateUrl = true; 173 this._hashChanged(); 174 this.path = window.decodeURIComponent(window.location.pathname); 175 this.query = window.location.search.substring(1); 176 this._dontUpdateUrl = false; 177 this._updateUrl(); 178 }, 179 _getUrl: function() { 180 var partiallyEncodedPath = window.encodeURI( 181 this.path).replace(/\#/g, '%23').replace(/\?/g, '%3F'); 182 var partiallyEncodedQuery = ''; 183 if (this.query) { 184 partiallyEncodedQuery = '?' + this.query.replace(/\#/g, '%23'); 185 } 186 var partiallyEncodedHash = ''; 187 if (this.hash) { 188 partiallyEncodedHash = '#' + window.encodeURI(this.hash); 189 } 190 return ( 191 partiallyEncodedPath + partiallyEncodedQuery + partiallyEncodedHash); 192 }, 193 _updateUrl: function() { 194 if (this._dontUpdateUrl || !this._initialized) { 195 return; 196 } 197 if (this.path === window.decodeURIComponent(window.location.pathname) && 198 this.query === window.location.search.substring(1) && 199 this.hash === window.decodeURIComponent( 200 window.location.hash.substring(1))) { 201 // Nothing to do, the current URL is a representation of our properties. 202 return; 203 } 204 var newUrl = this._getUrl(); 205 // Need to use a full URL in case the containing page has a base URI. 206 var fullNewUrl = new URL( 207 newUrl, window.location.protocol + '//' + window.location.host).href; 208 var now = window.performance.now(); 209 var shouldReplace = 210 this._lastChangedAt + this.dwellTime > now; 211 this._lastChangedAt = now; 212 if (shouldReplace) { 213 window.history.replaceState({}, '', fullNewUrl); 214 } else { 215 window.history.pushState({}, '', fullNewUrl); 216 } 217 this.fire('location-changed', {}, {node: window}); 218 }, 219 /** 220 * A necessary evil so that links work as expected. Does its best to 221 * bail out early if possible. 222 * 223 * @param {MouseEvent} event . 224 */ 225 _globalOnClick: function(event) { 226 // If another event handler has stopped this event then there's nothing 227 // for us to do. This can happen e.g. when there are multiple 228 // iron-location elements in a page. 229 if (event.defaultPrevented) { 230 return; 231 } 232 var href = this._getSameOriginLinkHref(event); 233 if (!href) { 234 return; 235 } 236 event.preventDefault(); 237 // If the navigation is to the current page we shouldn't add a history 238 // entry or fire a change event. 239 if (href === window.location.href) { 240 return; 241 } 242 window.history.pushState({}, '', href); 243 this.fire('location-changed', {}, {node: window}); 244 }, 245 /** 246 * Returns the absolute URL of the link (if any) that this click event 247 * is clicking on, if we can and should override the resulting full 248 * page navigation. Returns null otherwise. 249 * 250 * @param {MouseEvent} event . 251 * @return {string?} . 252 */ 253 _getSameOriginLinkHref: function(event) { 254 // We only care about left-clicks. 255 if (event.button !== 0) { 256 return null; 257 } 258 // We don't want modified clicks, where the intent is to open the page 259 // in a new tab. 260 if (event.metaKey || event.ctrlKey) { 261 return null; 262 } 263 var eventPath = Polymer.dom(event).path; 264 var anchor = null; 265 for (var i = 0; i < eventPath.length; i++) { 266 var element = eventPath[i]; 267 if (element.tagName === 'A' && element.href) { 268 anchor = element; 269 break; 270 } 271 } 272 273 // If there's no link there's nothing to do. 274 if (!anchor) { 275 return null; 276 } 277 278 // Target blank is a new tab, don't intercept. 279 if (anchor.target === '_blank') { 280 return null; 281 } 282 // If the link is for an existing parent frame, don't intercept. 283 if ((anchor.target === '_top' || 284 anchor.target === '_parent') && 285 window.top !== window) { 286 return null; 287 } 288 289 var href = anchor.href; 290 291 // It only makes sense for us to intercept same-origin navigations. 292 // pushState/replaceState don't work with cross-origin links. 293 var url; 294 if (document.baseURI != null) { 295 url = new URL(href, /** @type {string} */(document.baseURI)); 296 } else { 297 url = new URL(href); 298 } 299 300 var origin; 301 302 // IE Polyfill 303 if (window.location.origin) { 304 origin = window.location.origin; 305 } else { 306 origin = window.location.protocol + '//' + window.location.hostname; 307 308 if (window.location.port) { 309 origin += ':' + window.location.port; 310 } 311 } 312 313 if (url.origin !== origin) { 314 return null; 315 } 316 var normalizedHref = url.pathname + url.search + url.hash; 317 318 // If we've been configured not to handle this url... don't handle it! 319 if (this._urlSpaceRegExp && 320 !this._urlSpaceRegExp.test(normalizedHref)) { 321 return null; 322 } 323 // Need to use a full URL in case the containing page has a base URI. 324 var fullNormalizedHref = new URL( 325 normalizedHref, window.location.href).href; 326 return fullNormalizedHref; 327 }, 328 _makeRegExp: function(urlSpaceRegex) { 329 return RegExp(urlSpaceRegex); 330 } 331 }); 332 })(); 333</script> 334