chromium/third_party/polymer/v3_0/components-chromium/iron-location/iron-location.js

/**
@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);
  }
});