import { getOwner } from 'ember-owner'; import { tryInvoke } from 'ember-utils'; import { get, set } from 'ember-metal'; import { assert } from '@ember/debug'; import { Object as EmberObject } from 'ember-runtime'; import { location, history, window, userAgent } from 'ember-browser-environment'; import { supportsHashChange, supportsHistory, getPath, getHash, getQuery, getFullPath, replacePath, } from './util'; /** @module @ember/routing */ /** AutoLocation will select the best location option based off browser support with the priority order: history, hash, none. Clean pushState paths accessed by hashchange-only browsers will be redirected to the hash-equivalent and vice versa so future transitions are consistent. Keep in mind that since some of your users will use `HistoryLocation`, your server must serve the Ember app at all the routes you define. Browsers that support the `history` API will use `HistoryLocation`, those that do not, but still support the `hashchange` event will use `HashLocation`, and in the rare case neither is supported will use `NoneLocation`. Example: ```app/router.js Router.map(function() { this.route('posts', function() { this.route('new'); }); }); Router.reopen({ location: 'auto' }); ``` This will result in a posts.new url of `/posts/new` for modern browsers that support the `history` api or `/#/posts/new` for older ones, like Internet Explorer 9 and below. When a user visits a link to your application, they will be automatically upgraded or downgraded to the appropriate `Location` class, with the URL transformed accordingly, if needed. Keep in mind that since some of your users will use `HistoryLocation`, your server must serve the Ember app at all the routes you define. @class AutoLocation @static @protected */ export default EmberObject.extend({ /** @private The browser's `location` object. This is typically equivalent to `window.location`, but may be overridden for testing. @property location @default environment.location */ location: location, /** @private The browser's `history` object. This is typically equivalent to `window.history`, but may be overridden for testing. @since 1.5.1 @property history @default environment.history */ history: history, /** @private The user agent's global variable. In browsers, this will be `window`. @since 1.11 @property global @default window */ global: window, /** @private The browser's `userAgent`. This is typically equivalent to `navigator.userAgent`, but may be overridden for testing. @since 1.5.1 @property userAgent @default environment.history */ userAgent: userAgent, /** @private This property is used by the router to know whether to cancel the routing setup process, which is needed while we redirect the browser. @since 1.5.1 @property cancelRouterSetup @default false */ cancelRouterSetup: false, /** @private Will be pre-pended to path upon state change. @since 1.5.1 @property rootURL @default '/' */ rootURL: '/', /** Called by the router to instruct the location to do any feature detection necessary. In the case of AutoLocation, we detect whether to use history or hash concrete implementations. @private */ detect() { let rootURL = this.rootURL; assert( 'rootURL must end with a trailing forward slash e.g. "/app/"', rootURL.charAt(rootURL.length - 1) === '/' ); let implementation = detectImplementation({ location: this.location, history: this.history, userAgent: this.userAgent, rootURL, documentMode: this.documentMode, global: this.global, }); if (implementation === false) { set(this, 'cancelRouterSetup', true); implementation = 'none'; } let concrete = getOwner(this).lookup(`location:${implementation}`); set(concrete, 'rootURL', rootURL); assert(`Could not find location '${implementation}'.`, !!concrete); set(this, 'concreteImplementation', concrete); }, initState: delegateToConcreteImplementation('initState'), getURL: delegateToConcreteImplementation('getURL'), setURL: delegateToConcreteImplementation('setURL'), replaceURL: delegateToConcreteImplementation('replaceURL'), onUpdateURL: delegateToConcreteImplementation('onUpdateURL'), formatURL: delegateToConcreteImplementation('formatURL'), willDestroy() { let concreteImplementation = get(this, 'concreteImplementation'); if (concreteImplementation) { concreteImplementation.destroy(); } }, }); function delegateToConcreteImplementation(methodName) { return function(...args) { let concreteImplementation = get(this, 'concreteImplementation'); assert( "AutoLocation's detect() method should be called before calling any other hooks.", !!concreteImplementation ); return tryInvoke(concreteImplementation, methodName, args); }; } /* Given the browser's `location`, `history` and `userAgent`, and a configured root URL, this function detects whether the browser supports the [History API](https://developer.mozilla.org/en-US/docs/Web/API/History) and returns a string representing the Location object to use based on its determination. For example, if the page loads in an evergreen browser, this function would return the string "history", meaning the history API and thus HistoryLocation should be used. If the page is loaded in IE8, it will return the string "hash," indicating that the History API should be simulated by manipulating the hash portion of the location. */ function detectImplementation(options) { let { location, userAgent, history, documentMode, global, rootURL } = options; let implementation = 'none'; let cancelRouterSetup = false; let currentPath = getFullPath(location); if (supportsHistory(userAgent, history)) { let historyPath = getHistoryPath(rootURL, location); // If the browser supports history and we have a history path, we can use // the history location with no redirects. if (currentPath === historyPath) { implementation = 'history'; } else if (currentPath.substr(0, 2) === '/#') { history.replaceState({ path: historyPath }, null, historyPath); implementation = 'history'; } else { cancelRouterSetup = true; replacePath(location, historyPath); } } else if (supportsHashChange(documentMode, global)) { let hashPath = getHashPath(rootURL, location); // Be sure we're using a hashed path, otherwise let's switch over it to so // we start off clean and consistent. We'll count an index path with no // hash as "good enough" as well. if (currentPath === hashPath || (currentPath === '/' && hashPath === '/#/')) { implementation = 'hash'; } else { // Our URL isn't in the expected hash-supported format, so we want to // cancel the router setup and replace the URL to start off clean cancelRouterSetup = true; replacePath(location, hashPath); } } if (cancelRouterSetup) { return false; } return implementation; } /** @private Returns the current path as it should appear for HistoryLocation supported browsers. This may very well differ from the real current path (e.g. if it starts off as a hashed URL) */ export function getHistoryPath(rootURL, location) { let path = getPath(location); let hash = getHash(location); let query = getQuery(location); let rootURLIndex = path.indexOf(rootURL); let routeHash, hashParts; assert(`Path ${path} does not start with the provided rootURL ${rootURL}`, rootURLIndex === 0); // By convention, Ember.js routes using HashLocation are required to start // with `#/`. Anything else should NOT be considered a route and should // be passed straight through, without transformation. if (hash.substr(0, 2) === '#/') { // There could be extra hash segments after the route hashParts = hash.substr(1).split('#'); // The first one is always the route url routeHash = hashParts.shift(); // If the path already has a trailing slash, remove the one // from the hashed route so we don't double up. if (path.charAt(path.length - 1) === '/') { routeHash = routeHash.substr(1); } // This is the "expected" final order path += routeHash + query; if (hashParts.length) { path += `#${hashParts.join('#')}`; } } else { path += query + hash; } return path; } /** @private Returns the current path as it should appear for HashLocation supported browsers. This may very well differ from the real current path. @method _getHashPath */ export function getHashPath(rootURL, location) { let path = rootURL; let historyPath = getHistoryPath(rootURL, location); let routePath = historyPath.substr(rootURL.length); if (routePath !== '') { if (routePath[0] !== '/') { routePath = `/${routePath}`; } path += `#${routePath}`; } return path; }