blob: 89acb2861d7d7267e0756883e1a55fd66bb19ff3 [file] [log] [blame]
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
part of the polymer project is also subject to an additional IP rights grant
found at http://polymer.github.io/PATENTS.txt
*/
import '../polymer/polymer_bundled.min.js';
import {Polymer} from '../polymer/polymer_bundled.min.js';
import {dom} from '../polymer/polymer_bundled.min.js';
/**
The `iron-location` element manages binding to and from the current URL.
iron-location is the first, and lowest level element in the Polymer team's
routing system. This is a beta release of iron-location as we continue work
on higher level elements, and as such iron-location may undergo breaking
changes.
#### Properties
When the URL is: `/search?query=583#details` iron-location's properties will be:
- path: `'/search'`
- query: `'query=583'`
- hash: `'details'`
These bindings are bidirectional. Modifying them will in turn modify the URL.
iron-location is only active while it is attached to the document.
#### Links
While iron-location is active in the document it will intercept clicks on links
within your site, updating the URL pushing the updated URL out through the
databinding system. iron-location only intercepts clicks with the intent to
open in the same window, so middle mouse clicks and ctrl/cmd clicks work fine.
You can customize this behavior with the `urlSpaceRegex`.
#### Dwell Time
iron-location protects against accidental history spamming by only adding
entries to the user's history if the URL stays unchanged for `dwellTime`
milliseconds.
@demo demo/index.html
*/
Polymer({
is: 'iron-location',
properties: {
/**
* The pathname component of the URL.
*/
path: {
type: String,
notify: true,
value: function() {
return window.decodeURIComponent(window.location.pathname);
}
},
/**
* The query string portion of the URL.
*/
query: {
type: String,
notify: true,
value: function() {
return window.location.search.slice(1);
}
},
/**
* The hash component of the URL.
*/
hash: {
type: String,
notify: true,
value: function() {
return window.decodeURIComponent(window.location.hash.slice(1));
}
},
/**
* If the user was on a URL for less than `dwellTime` milliseconds, it
* won't be added to the browser's history, but instead will be replaced
* by the next entry.
*
* This is to prevent large numbers of entries from clogging up the user's
* browser history. Disable by setting to a negative number.
*/
dwellTime: {type: Number, value: 2000},
/**
* A regexp that defines the set of URLs that should be considered part
* of this web app.
*
* Clicking on a link that matches this regex won't result in a full page
* navigation, but will instead just update the URL state in place.
*
* This regexp is given everything after the origin in an absolute
* URL. So to match just URLs that start with /search/ do:
* url-space-regex="^/search/"
*
* @type {string|RegExp}
*/
urlSpaceRegex: {type: String, value: ''},
/**
* A flag that specifies whether the spaces in query that would normally be
* encoded as %20 should be encoded as +.
*
* Given an example text "hello world", it is encoded in query as
* - "hello%20world" without the parameter
* - "hello+world" with the parameter
*/
encodeSpaceAsPlusInQuery: {type: Boolean, value: false},
/**
* urlSpaceRegex, but coerced into a regexp.
*
* @type {RegExp}
*/
_urlSpaceRegExp: {computed: '_makeRegExp(urlSpaceRegex)'},
_lastChangedAt: {type: Number},
_initialized: {type: Boolean, value: false}
},
hostAttributes: {hidden: true},
observers: ['_updateUrl(path, query, hash)'],
created: function() {
this.__location = window.location;
},
attached: function() {
this.listen(window, 'hashchange', '_hashChanged');
this.listen(window, 'location-changed', '_urlChanged');
this.listen(window, 'popstate', '_urlChanged');
this.listen(
/** @type {!HTMLBodyElement} */ (document.body),
'click',
'_globalOnClick');
// Give a 200ms grace period to make initial redirects without any
// additions to the user's history.
this._lastChangedAt = window.performance.now() - (this.dwellTime - 200);
this._initialized = true;
this._urlChanged();
},
detached: function() {
this.unlisten(window, 'hashchange', '_hashChanged');
this.unlisten(window, 'location-changed', '_urlChanged');
this.unlisten(window, 'popstate', '_urlChanged');
this.unlisten(
/** @type {!HTMLBodyElement} */ (document.body),
'click',
'_globalOnClick');
this._initialized = false;
},
_hashChanged: function() {
this.hash = window.decodeURIComponent(this.__location.hash.substring(1));
},
_urlChanged: function() {
// We want to extract all info out of the updated URL before we
// try to write anything back into it.
//
// i.e. without _dontUpdateUrl we'd overwrite the new path with the old
// one when we set this.hash. Likewise for query.
this._dontUpdateUrl = true;
this._hashChanged();
this.path = window.decodeURIComponent(this.__location.pathname);
this.query = this.__location.search.substring(1);
this._dontUpdateUrl = false;
this._updateUrl();
},
_getUrl: function() {
var partiallyEncodedPath =
window.encodeURI(this.path).replace(/\#/g, '%23').replace(/\?/g, '%3F');
var partiallyEncodedQuery = '';
if (this.query) {
partiallyEncodedQuery = '?' + this.query.replace(/\#/g, '%23');
if (this.encodeSpaceAsPlusInQuery) {
partiallyEncodedQuery = partiallyEncodedQuery.replace(/\+/g, '%2B')
.replace(/ /g, '+')
.replace(/%20/g, '+');
} else {
// required for edge
partiallyEncodedQuery =
partiallyEncodedQuery.replace(/\+/g, '%2B').replace(/ /g, '%20');
}
}
var partiallyEncodedHash = '';
if (this.hash) {
partiallyEncodedHash = '#' + window.encodeURI(this.hash);
}
return (
partiallyEncodedPath + partiallyEncodedQuery + partiallyEncodedHash);
},
_updateUrl: function() {
if (this._dontUpdateUrl || !this._initialized) {
return;
}
if (this.path === window.decodeURIComponent(this.__location.pathname) &&
this.query === this.__location.search.substring(1) &&
this.hash ===
window.decodeURIComponent(this.__location.hash.substring(1))) {
// Nothing to do, the current URL is a representation of our properties.
return;
}
var newUrl = this._getUrl();
// Need to use a full URL in case the containing page has a base URI.
var fullNewUrl =
new URL(newUrl, this.__location.protocol + '//' + this.__location.host)
.href;
var now = window.performance.now();
var shouldReplace = this._lastChangedAt + this.dwellTime > now;
this._lastChangedAt = now;
if (shouldReplace) {
window.history.replaceState({}, '', fullNewUrl);
} else {
window.history.pushState({}, '', fullNewUrl);
}
this.fire('location-changed', {}, {node: window});
},
/**
* A necessary evil so that links work as expected. Does its best to
* bail out early if possible.
*
* @param {MouseEvent} event .
*/
_globalOnClick: function(event) {
// If another event handler has stopped this event then there's nothing
// for us to do. This can happen e.g. when there are multiple
// iron-location elements in a page.
if (event.defaultPrevented) {
return;
}
var href = this._getSameOriginLinkHref(event);
if (!href) {
return;
}
event.preventDefault();
// If the navigation is to the current page we shouldn't add a history
// entry or fire a change event.
if (href === this.__location.href) {
return;
}
window.history.pushState({}, '', href);
this.fire('location-changed', {}, {node: window});
},
/**
* Returns the absolute URL of the link (if any) that this click event
* is clicking on, if we can and should override the resulting full
* page navigation. Returns null otherwise.
*
* @param {MouseEvent} event .
* @return {string?} .
*/
_getSameOriginLinkHref: function(event) {
// We only care about left-clicks.
if (event.button !== 0) {
return null;
}
// We don't want modified clicks, where the intent is to open the page
// in a new tab.
if (event.metaKey || event.ctrlKey) {
return null;
}
var eventPath = dom(event).path;
var anchor = null;
for (var i = 0; i < eventPath.length; i++) {
var element = eventPath[i];
if (element.tagName === 'A' && element.href) {
anchor = element;
break;
}
}
// If there's no link there's nothing to do.
if (!anchor) {
return null;
}
// Target blank is a new tab, don't intercept.
if (anchor.target === '_blank') {
return null;
}
// If the link is for an existing parent frame, don't intercept.
if ((anchor.target === '_top' || anchor.target === '_parent') &&
window.top !== window) {
return null;
}
// If the link is a download, don't intercept.
if (anchor.download) {
return null;
}
var href = anchor.href;
// It only makes sense for us to intercept same-origin navigations.
// pushState/replaceState don't work with cross-origin links.
var url;
if (document.baseURI != null) {
url = new URL(href, /** @type {string} */ (document.baseURI));
} else {
url = new URL(href);
}
var origin;
// IE Polyfill
if (this.__location.origin) {
origin = this.__location.origin;
} else {
origin = this.__location.protocol + '//' + this.__location.host;
}
var urlOrigin;
if (url.origin) {
urlOrigin = url.origin;
} else {
// IE always adds port number on HTTP and HTTPS on <a>.host but not on
// window.location.host
var urlHost = url.host;
var urlPort = url.port;
var urlProtocol = url.protocol;
var isExtraneousHTTPS = urlProtocol === 'https:' && urlPort === '443';
var isExtraneousHTTP = urlProtocol === 'http:' && urlPort === '80';
if (isExtraneousHTTPS || isExtraneousHTTP) {
urlHost = url.hostname;
}
urlOrigin = urlProtocol + '//' + urlHost;
}
if (urlOrigin !== origin) {
return null;
}
var normalizedHref = url.pathname + url.search + url.hash;
// pathname should start with '/', but may not if `new URL` is not supported
if (normalizedHref[0] !== '/') {
normalizedHref = '/' + normalizedHref;
}
// If we've been configured not to handle this url... don't handle it!
if (this._urlSpaceRegExp && !this._urlSpaceRegExp.test(normalizedHref)) {
return null;
}
// Need to use a full URL in case the containing page has a base URI.
var fullNormalizedHref = new URL(normalizedHref, this.__location.href).href;
return fullNormalizedHref;
},
_makeRegExp: function(urlSpaceRegex) {
return RegExp(urlSpaceRegex);
}
});