• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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