import { defaultHomepage } from "discourse/lib/utilities"; /** @module Discourse */ const get = Ember.get, set = Ember.set; let popstateFired = false; const supportsHistoryState = window.history && "state" in window.history; const popstateCallbacks = []; /** `Ember.DiscourseLocation` implements the location API using the browser's `history.pushState` API. @class DiscourseLocation @namespace Discourse @extends Ember.Object */ const DiscourseLocation = Ember.Object.extend({ init() { set(this, "location", get(this, "location") || window.location); this.initState(); }, /** @private Used to set state on first call to setURL @method initState */ initState() { const history = get(this, "history") || window.history; if (history && history.scrollRestoration) { history.scrollRestoration = "manual"; } set(this, "history", history); let url = this.formatURL(this.getURL()); const loc = get(this, "location"); if (loc && loc.hash) { url += loc.hash; } this.replaceState(url); }, /** Will be pre-pended to path upon state change @property rootURL @default '/' */ rootURL: "/", /** @private Returns the current `location.pathname` without rootURL @method getURL */ getURL() { const location = get(this, "location"); let url = location.pathname; url = url.replace(new RegExp(`^${Discourse.BaseUri}`), ""); const search = location.search || ""; url += search; return url; }, /** @private Uses `history.pushState` to update the url without a page reload. @method setURL @param path {String} */ setURL(path) { const state = this.getState(); path = this.formatURL(path); if (state && state.path !== path) { const paths = [path, state.path]; if (!(paths.includes("/") && paths.includes(`/${defaultHomepage()}`))) { this.pushState(path); } } }, /** @private Uses `history.replaceState` to update the url without a page reload or history modification. @method replaceURL @param path {String} */ replaceURL(path) { const state = this.getState(); path = this.formatURL(path); if (state && state.path !== path) { this.replaceState(path); } }, /** @private Get the current `history.state` Polyfill checks for native browser support and falls back to retrieving from a private _historyState constiable @method getState */ getState() { return supportsHistoryState ? get(this, "history").state : this._historyState; }, /** @private Pushes a new state @method pushState @param path {String} */ pushState(path) { const state = { path: path }; // store state if browser doesn't support `history.state` if (!supportsHistoryState) { this._historyState = state; } else { get(this, "history").pushState(state, null, path); } // used for webkit workaround this._previousURL = this.getURL(); }, /** @private Replaces the current state @method replaceState @param path {String} */ replaceState(path) { const state = { path: path }; // store state if browser doesn't support `history.state` if (!supportsHistoryState) { this._historyState = state; } else { get(this, "history").replaceState(state, null, path); } // used for webkit workaround this._previousURL = this.getURL(); }, /** @private Register a callback to be invoked whenever the browser history changes, including using forward and back buttons. @method onUpdateURL @param callback {Function} */ onUpdateURL(callback) { const guid = Ember.guidFor(this), self = this; Ember.$(window).on("popstate.ember-location-" + guid, function() { // Ignore initial page load popstate event in Chrome if (!popstateFired) { popstateFired = true; if (self.getURL() === self._previousURL) { return; } } const url = self.getURL(); popstateCallbacks.forEach(function(cb) { cb(url); }); callback(url); }); }, /** @private Used when using `{{action}}` helper. The url is always appended to the rootURL. @method formatURL @param url {String} */ formatURL(url) { let rootURL = get(this, "rootURL"); if (url !== "") { rootURL = rootURL.replace(/\/$/, ""); if (rootURL.length > 0 && url.indexOf(rootURL + "/") === 0) { rootURL = ""; } } return rootURL + url; }, willDestroy() { const guid = Ember.guidFor(this); Ember.$(window).off("popstate.ember-location-" + guid); } }); export default DiscourseLocation;