| <!DOCTYPE html> |
| <!-- |
| Copyright 2015 The Chromium Authors. All rights reserved. |
| Use of this source code is governed by a BSD-style license that can be |
| found in the LICENSE file. |
| --> |
| |
| <script> |
| 'use strict'; |
| |
| /** |
| * Utility functions for dealing with URIs. |
| */ |
| var uri = (function() { |
| |
| /** |
| * URI controller. |
| * @param {function()} pageStateCallback callback for getting the page state. |
| */ |
| var Controller = function(pageStateCallback) { |
| this.pageStateCallback_ = pageStateCallback; |
| this.stateRequest_ = null; |
| this.stateRequestStatus_ = null; |
| window.addEventListener('popstate', this.onPopstate_, true); |
| }; |
| |
| /** |
| * Unique integer that gets assigned to the source element that fires a page |
| * state change event. This id can be used in handling 'urichange' event |
| * to identify the source element that the event originated from. |
| */ |
| Controller.uniqueIdCounter_ = 0; |
| |
| /** |
| * Gets a incremented integer id. |
| * @return {number} id number. |
| */ |
| Controller.uniqueId = function() { |
| return Controller.uniqueIdCounter_++; |
| }; |
| |
| /** |
| * Sends a request for page state and dispatches 'uriload' event which can be |
| * caught to handle initializing the page. The URI parameters are attached to |
| * the event detail object. |
| */ |
| Controller.prototype.load = function() { |
| var params = uri.getAllParameters(); |
| var sid = uri.getParameter('sid'); |
| if (!params || !sid) { |
| return; |
| } |
| |
| var request = new XMLHttpRequest(); |
| request.onload = function() { |
| var pageState = JSON.parse(request.responseText); |
| var params = uri.getAllParameters(); |
| var uriLoadEvent = new CustomEvent('uriload', { |
| 'detail': { |
| 'params': params, |
| 'state': pageState |
| }, |
| 'bubbles': true, |
| 'cancelable': true |
| }); |
| window.dispatchEvent(uriLoadEvent); |
| }; |
| request.onerror = function() { |
| this.firePageStateRequestEvent_('failed'); |
| }; |
| request.open('get', '/short_uri?sid=' + sid); |
| request.send(); |
| }; |
| |
| /** |
| * Updates URI on page state changes. |
| * |
| * The event passed is expected to have a detail property containing |
| * a dictionary 'params' which is used to update the URI. A request |
| * is made to /short_uri to save the current page state and URI is |
| * updated with the new state ID. Also, target element gets assigned a |
| * 'uniqueid' which can be used in handling 'urichange' event to identify |
| * the source that fired the event. |
| */ |
| Controller.prototype.onPageStateChanged = function(event) { |
| var detail = event.detail; |
| var state = { |
| 'stateName': detail['stateName'], |
| 'params': detail['params'], |
| 'state': detail['state'] |
| }; |
| |
| if (detail.id && detail.id.length > 0) { |
| state['id'] = detail.id; |
| } else if (detail.target) { |
| if (!detail.target.getAttribute('uniqueid')) { |
| detail.target.setAttribute('uniqueid', Controller.uniqueId()); |
| } |
| state['id'] = detail.target.getAttribute('uniqueid'); |
| } |
| if (state['state']) { |
| this.sendShortURIRequest_(state); |
| } else { |
| this.updateUri_(state); |
| } |
| }; |
| |
| /** |
| * Updates URI based on passed state data. |
| * @param {Object} state Dictionary containing state data. |
| */ |
| Controller.prototype.updateUri_ = function(state) { |
| var params = uri.getAllParameters(); |
| if (!state || !state['params']) { |
| params = {}; |
| } else { |
| for (var k in state['params']) { |
| var value = state['params'][k]; |
| if (value) { |
| params[k] = value; |
| } else { |
| delete params[k]; |
| } |
| } |
| } |
| var uri_str = getCurrentPathWithParams(params); |
| if (uri_str == window.location.pathname + uri.getQueryString()) { |
| return; |
| } |
| window.history.pushState(state, '', uri_str); |
| }; |
| |
| /** |
| * Dispatches 'urichange' event on 'popstate' event. |
| * @param {Event} event 'popstate' event, which has a 'state' property set |
| * by a previous call to history.pushState. |
| */ |
| Controller.prototype.onPopstate_ = function(event) { |
| if (!event.state) { |
| return; |
| } |
| var state = event.state; |
| var uriChangeEvent = new CustomEvent('urichange', { |
| 'detail': { |
| 'id': state['id'], |
| 'stateName': state['stateName'], |
| 'params': state['params'], |
| 'state': state['state'] |
| }, |
| 'bubbles': true, |
| 'cancelable': true |
| }); |
| window.dispatchEvent(uriChangeEvent); |
| }; |
| |
| /** |
| * Sends a request to save current page state and update the URI with |
| * the new SID. |
| * @param {Object} state Dictionary containing state data. |
| */ |
| Controller.prototype.sendShortURIRequest_ = function(state) { |
| if (this.stateRequest_) { |
| this.stateRequest_.abort(); |
| } |
| var pageState = this.pageStateCallback_(); |
| if (!pageState) { |
| this.updateUri_(null); |
| return; |
| } |
| |
| var postData = 'page_state=' + |
| encodeURIComponent(JSON.stringify(pageState)); |
| |
| this.firePageStateRequestEvent_('loading'); |
| var request = new XMLHttpRequest(); |
| var self = this; |
| request.onload = function() { |
| var response = JSON.parse(request.responseText); |
| var stateId = response['sid']; |
| state['params'] = state['params'] || {}; |
| state['params']['sid'] = stateId; |
| self.updateUri_(state); |
| self.firePageStateRequestEvent_('complete'); |
| }; |
| request.onerror = function() { |
| self.firePageStateRequestEvent_('failed'); |
| }; |
| request.open('post', '/short_uri', true); |
| request.setRequestHeader('Content-type', |
| 'application/x-www-form-urlencoded'); |
| request.send(postData); |
| this.stateRequest_ = request; |
| }; |
| |
| /** |
| * Dispatches an event for the status of page state request. |
| * @param {string} status Status name. |
| */ |
| Controller.prototype.firePageStateRequestEvent_ = function(status) { |
| var event = new CustomEvent('pagestaterequest', { |
| 'detail': {'status': status} |
| }); |
| window.dispatchEvent(event); |
| }; |
| |
| /** |
| * Gets the named parameter from the URI query string. |
| * @param {string} name The name of the parameter to get. |
| * @param {?string=} opt_default The default to return. |
| * @return {?string} The value of the parameter. |
| */ |
| var getParameter = function(name, opt_default) { |
| name = name.replace(/[\[]/, '\\\[').replace(/[\]]/, '\\\]'); |
| var regex = new RegExp('[\\?&]' + name + '=([^&#]*)'); |
| var results = regex.exec(uri.getQueryString()); |
| if (results != null) { |
| return decodeURIComponent(results[1].replace(/\+/g, ' ')); |
| } |
| return opt_default || null; |
| }; |
| |
| /** |
| * Gets all the URI parameters. Does not support multiple parameters with the |
| * same name. |
| * @return {!Object} Mapping of name->value for URI parameters. |
| */ |
| var getAllParameters = function() { |
| var params = {}; |
| var queryString = uri.getQueryString().replace(/^\?/, ''); |
| if (!queryString) { |
| return {}; |
| } |
| var parts = queryString.split('&'); |
| for (var i = 0; i < parts.length; i++) { |
| var pair = parts[i].split('='); |
| if (pair.length == 1) { |
| params[pair[0]] = ''; |
| } else { |
| params[pair[0]] = decodeURIComponent(pair[1].replace(/\+/g, ' ')); |
| } |
| } |
| return params; |
| }; |
| |
| /** Gets the query string, including "?", in a mockable way. */ |
| var getQueryString = function() { |
| return window.location.search; |
| }; |
| |
| /** |
| * Returns a string which is the current path of the URI with the query |
| * parameters from the given Object. This is suitable for usage with |
| * history.pushState(). |
| * @param {!Object} params Key/value pairs for new query string. |
| * @return {string} New URI which can be passed into history.pushState(). |
| */ |
| var getCurrentPathWithParams = function(params) { |
| var pairs = Object.keys(params).map(function(k) { |
| return k + '=' + encodeURIComponent(params[k]); |
| }); |
| return window.location.pathname + '?' + pairs.join('&'); |
| }; |
| |
| return { |
| Controller: Controller, |
| getParameter: getParameter, |
| getAllParameters: getAllParameters, |
| getCurrentPathWithParams: getCurrentPathWithParams, |
| getQueryString: getQueryString |
| }; |
| })(); |
| |
| </script> |