334 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			HTML
		
	
	
	
			
		
		
	
	
			334 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			HTML
		
	
	
	
<!--
 | 
						|
@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';
 | 
						|
 | 
						|
    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: ''
 | 
						|
        },
 | 
						|
 | 
						|
        /**
 | 
						|
         * 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)'
 | 
						|
      ],
 | 
						|
      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(window.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(window.location.pathname);
 | 
						|
        this.query = window.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');
 | 
						|
        }
 | 
						|
        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(window.location.pathname) &&
 | 
						|
            this.query === window.location.search.substring(1) &&
 | 
						|
            this.hash === window.decodeURIComponent(
 | 
						|
                window.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, window.location.protocol + '//' + window.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 === window.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;
 | 
						|
        }
 | 
						|
 | 
						|
        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 (window.location.origin) {
 | 
						|
          origin = window.location.origin;
 | 
						|
        } else {
 | 
						|
          origin = window.location.protocol + '//' + window.location.hostname;
 | 
						|
 | 
						|
          if (window.location.port) {
 | 
						|
            origin += ':' + window.location.port;
 | 
						|
          }
 | 
						|
        }
 | 
						|
 | 
						|
        if (url.origin !== origin) {
 | 
						|
          return null;
 | 
						|
        }
 | 
						|
        var normalizedHref = url.pathname + url.search + url.hash;
 | 
						|
 | 
						|
        // 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, window.location.href).href;
 | 
						|
        return fullNormalizedHref;
 | 
						|
      },
 | 
						|
      _makeRegExp: function(urlSpaceRegex) {
 | 
						|
        return RegExp(urlSpaceRegex);
 | 
						|
      }
 | 
						|
    });
 | 
						|
  })();
 | 
						|
</script>
 |