UNPKG

webdash-readme-preview

Version:
442 lines (369 loc) 13.8 kB
<!-- @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 --> <link rel="import" href="../polymer/polymer.html"> <!-- 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 --> <script> (function() { 'use strict'; var workingURL; var urlDoc, urlBase, anchor; /** * @param {string} path * @param {string=} base * @return {!URL|!HTMLAnchorElement} */ function resolveURL(path, base) { if (workingURL === undefined) { workingURL = false; try { var u = new URL('b', 'http://a'); u.pathname = 'c%20d'; workingURL = (u.href === 'http://a/c%20d'); workingURL = workingURL && (new URL('http://www.google.com/?foo bar').href === 'http://www.google.com/?foo%20bar'); } catch (e) {} } if (workingURL) { return new URL(path, base); } if (!urlDoc) { urlDoc = document.implementation.createHTMLDocument('url'); urlBase = urlDoc.createElement('base'); urlDoc.head.appendChild(urlBase); anchor = /** @type {HTMLAnchorElement}*/(urlDoc.createElement('a')); } urlBase.href = base; anchor.href = path.replace(/ /g, '%20'); return anchor; } 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, '+'); } } 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 = resolveURL(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 = Polymer.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 = resolveURL(href, /** @type {string} */(document.baseURI)); } else { url = resolveURL(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 = resolveURL( normalizedHref, this.__location.href).href; return fullNormalizedHref; }, _makeRegExp: function(urlSpaceRegex) { return RegExp(urlSpaceRegex); } }); })(); </script>