UNPKG

@openui5/sap.ui.core

Version:

OpenUI5 Core Library sap.ui.core

576 lines (491 loc) 20.3 kB
/*! * OpenUI5 * (c) Copyright 2009-2023 SAP SE or an SAP affiliate company. * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. */ sap.ui.define([ 'sap/ui/core/library', './HashChanger', "sap/base/Log", "sap/base/util/ObjectPath" ], function(library, HashChanger, Log, ObjectPath) { "use strict"; // shortcut for enum(s) var HistoryDirection = library.routing.HistoryDirection; // when HashChanger fires a hashChange event without a real browser "hashchange" event, the _getDirectionWithState // function can detect this case and keep the previous history direction unchanged var DIRECTION_UNCHANGED = "Direction_Unchanged"; /** * Used to determine the {@link sap.ui.core.routing.HistoryDirection} of the current or a future navigation, * done with a {@link sap.ui.core.routing.Router} or {@link sap.ui.core.routing.HashChanger}. * * <strong>ATTENTION:</strong> this class will not be accurate if someone does hash-replacement without the named classes above. * If you are manipulating the hash directly, this class is not supported anymore. * * @param {sap.ui.core.routing.HashChanger} oHashChanger required, without a HashChanger this class cannot work. The class needs to be aware of the hash-changes. * @public * @class * @alias sap.ui.core.routing.History */ var History = function(oHashChanger) { var that = this; this._iHistoryLength = window.history.length; this.aHistory = []; this._bIsInitial = true; function initHistory(sCurrentHash) { if (History._bUsePushState && !History.getInstance()) { var oState = window.history.state === null ? {} : window.history.state; if (typeof oState === "object") { oState.sap = oState.sap ? oState.sap : {}; if (oState.sap.history && Array.isArray(oState.sap.history) && oState.sap.history[oState.sap.history.length - 1] === sCurrentHash) { History._aStateHistory = oState.sap.history; } else { History._aStateHistory.push(sCurrentHash); oState.sap.history = History._aStateHistory; window.history.replaceState(oState, window.document.title); } } else { Log.debug("Unable to determine HistoryDirection as history.state is already set: " + window.history.state, "sap.ui.core.routing.History"); } } that._reset(); } if (!oHashChanger) { Log.error("sap.ui.core.routing.History constructor was called and it did not get a hashChanger as parameter"); } this._setHashChanger(oHashChanger); if (oHashChanger._initialized) { initHistory(oHashChanger.getHash()); } else { oHashChanger.attachEventOnce("hashChanged", function(oEvent) { initHistory(oEvent.getParameter("newHash")); }); } }; /** * Stores the history of full hashes to compare with window.history.state * @private */ History._aStateHistory = []; /* * Whether the push state API should be used. * * Within iframe, the usage of push state API has to be turned off because some browsers (Chrome, Firefox and Edge) * change the ownership of the last "hashchange" event to the outer frame as soon as the outer frame replaces the * current hash. This makes the state that is saved by using push state API incomplete in both outer and inner * frames. Due to this, the usage of push state can only be done in outer frame. * * @private */ History._bUsePushState = window.self === window.top; /** * Returns the length difference between the history state stored in browser's * pushState and the state maintained in this class. * * The function returns <code>undefined</code> when * <ul> * <li>The current state in browser's history pushState isn't * initialized, for example, between a new hash is set or replaced * and the "hashChange" event is processed by this class</li> * <li>History pushState is already used before UI5 History * is initialized, and UI5 can't maintain the hash history * by using the browser pushState</li> * </ul> * * Once the "hashChange" event is processed by this class, this method always * returns 0. However, before a "hashChange" event reaches this class, it * returns the offset between the new hash and the previous one within the * history state. * * @public * @since 1.70 * @return {int|undefined} The length difference or returns * <code>undefined</code> when browser pushState can't be used at the * moment when this function is called */ History.prototype.getHistoryStateOffset = function() { if (!History._bUsePushState) { return undefined; } var aStateHistory = ObjectPath.get("history.state.sap.history"); if (!Array.isArray(aStateHistory)) { return undefined; } return aStateHistory.length - History._aStateHistory.length; }; /** * Detaches all events and cleans up this instance * * @private */ History.prototype.destroy = function() { this._unRegisterHashChanger(); }; /** * Determines what the navigation direction for a newly given hash would be * It will say Unknown if there is a history foo - bar (current history) - foo * If you now ask for the direction of the hash "foo" you get Unknown because it might be backwards or forwards. * For hash replacements, the history stack will be replaced at this position for the history. * @param {string} [sNewHash] optional, if this parameter is not passed the last hashChange is taken. * @returns {sap.ui.core.routing.HistoryDirection|undefined} Direction for the given hash or <code>undefined</code>, if no navigation has taken place yet. * @public */ History.prototype.getDirection = function(sNewHash) { //no navigation has taken place and someone asks for a direction if (sNewHash !== undefined && this._bIsInitial) { return undefined; } if (sNewHash === undefined) { return this._sCurrentDirection; } return this._getDirection(sNewHash); }; /** * Gets the previous hash in the history. * * If the last direction was Unknown or there was no navigation yet, <code>undefined</code> will be returned. * @returns {string|undefined} Previous hash in the history or <code>undefined</code> * @public */ History.prototype.getPreviousHash = function() { return this.aHistory[this.iHistoryPosition - 1]; }; History.prototype._setHashChanger = function(oHashChanger) { if (this._oHashChanger) { this._unRegisterHashChanger(); } this._oHashChanger = oHashChanger; this._mEventListeners = {}; oHashChanger.getRelevantEventsInfo().forEach(function(oEventInfo) { var sEventName = oEventInfo.name, oParamMapping = oEventInfo.paramMapping || {}, fnListener = this._onHashChange.bind(this, oParamMapping); this._mEventListeners[sEventName] = fnListener; this._oHashChanger.attachEvent(sEventName, fnListener, this); }.bind(this)); this._oHashChanger.attachEvent("hashReplaced", this._hashReplaced, this); this._oHashChanger.attachEvent("hashSet", this._hashSet, this); }; History.prototype._unRegisterHashChanger = function() { if (this._mEventListeners) { var aEventNames = Object.keys(this._mEventListeners); aEventNames.forEach(function(sEventName) { this._oHashChanger.detachEvent(sEventName, this._mEventListeners[sEventName], this); }.bind(this)); delete this._mEventListeners; } this._oHashChanger.detachEvent("hashReplaced", this._hashReplaced, this); this._oHashChanger.detachEvent("hashSet", this._hashSet, this); this._oHashChanger = null; }; /** * Empties the history array, and sets the instance back to the unknown state. * @private */ History.prototype._reset = function() { this.aHistory.length = 0; this.iHistoryPosition = 0; this._bUnknown = true; /* * if the history is reset it should always get the current hash since - * if you go from the Unknown to a defined state and then back is pressed we can be sure that the direction is backwards. * Because the only way from unknown to known state is a new entry in the history. */ this.aHistory[0] = this._oHashChanger.getHash(); }; /** * Determines what the navigation direction for a newly given hash would be * @param {string} sNewHash the new hash * @param {boolean} bHistoryLengthIncreased if the history length has increased compared with the last check * @param {boolean} bCheckHashChangerEvents Checks if the hash was set or replaced by the hashchanger. When getDirection is called by an app this has to be false. * @returns {sap.ui.core.routing.HistoryDirection} The history direction * @private */ History.prototype._getDirection = function(sNewHash, bHistoryLengthIncreased, bCheckHashChangerEvents) { //Next hash was set by the router - it has to be a new entry if (bCheckHashChangerEvents && this._oNextHash && this._oNextHash.sHash === sNewHash) { return HistoryDirection.NewEntry; } //increasing the history length will add entries but we cannot rely on this as only criteria, since the history length is capped if (bHistoryLengthIncreased) { return HistoryDirection.NewEntry; } //we have not had a direction yet and the application did not trigger navigation + the browser history does not increase //the user is navigating in his history but we cannot determine the direction if (this._bUnknown) { return HistoryDirection.Unknown; } //At this point we know the user pressed a native browser navigation button //both directions contain the same hash we don't know the direction if (this.aHistory[this.iHistoryPosition + 1] === sNewHash && this.aHistory[this.iHistoryPosition - 1] === sNewHash) { return HistoryDirection.Unknown; } if (this.aHistory[this.iHistoryPosition - 1] === sNewHash) { return HistoryDirection.Backwards; } if (this.aHistory[this.iHistoryPosition + 1] === sNewHash) { return HistoryDirection.Forwards; } //Nothing hit, return unknown since we cannot determine what happened return HistoryDirection.Unknown; }; /** * Determine HistoryDirection leveraging the full hash as in window.location.hash * and window.history.state. * * @param {string} sHash the complete hash, same as window.location.hash * @return {sap.ui.core.routing.HistoryDirection} The determined HistoryDirection * @private */ History.prototype._getDirectionWithState = function(sHash) { var oState = window.history.state === null ? {} : window.history.state, bBackward, sDirection; if (typeof oState === "object") { if (oState.sap === undefined) { History._aStateHistory.push(sHash); oState.sap = {}; oState.sap.history = History._aStateHistory; window.history.replaceState(oState, document.title); sDirection = HistoryDirection.NewEntry; } else { bBackward = oState.sap.history.every(function(sURL, index) { return sURL === History._aStateHistory[index]; }); // If the state history is identical with the history trace, it means // that a hashChanged event is fired without a real brower hash change. // In this case, the _getDirectionWithState can't be used to determine // the history direction and should keep the direction unchanged if (bBackward && oState.sap.history.length === History._aStateHistory.length) { sDirection = DIRECTION_UNCHANGED; } else { sDirection = bBackward ? HistoryDirection.Backwards : HistoryDirection.Forwards; History._aStateHistory = oState.sap.history; } } } else { Log.debug("Unable to determine HistoryDirection as history.state is already set: " + window.history.state, "sap.ui.core.routing.History"); } return sDirection; }; History.prototype._onHashChange = function(oParamMapping, oEvent) { var sNewHashParamName = oParamMapping.newHash || "newHash", sOldHashParamName = oParamMapping.oldHash || "oldHash", sFullHashParamName = oParamMapping.fullHash || "fullHash"; // Leverage the fullHash parameter if available this._hashChange(oEvent.getParameter(sNewHashParamName), oEvent.getParameter(sOldHashParamName), oEvent.getParameter(sFullHashParamName)); }; /** * Handles a hash change and cleans up the History * * @param {string} sNewHash The new hash * @param {string} sOldHash The old hash * @param {string} sFullHash The full hash * @private */ History.prototype._hashChange = function(sNewHash, sOldHash, sFullHash) { var actualHistoryLength = window.history.length, sDirection; // We don't want to record replaced hashes if (this._oNextHash && this._oNextHash.bWasReplaced && this._oNextHash.sHash === sNewHash) { if (this._oNextHash.sDirection) { sDirection = this._oNextHash.sDirection; } else { // Since a replace has taken place, the current history entry is also replaced this.aHistory[this.iHistoryPosition] = sNewHash; if (sFullHash !== undefined && History._bUsePushState && this === History.getInstance()) { // after the hash is replaced, the history state is cleared. // We need to update the last entry in _aStateHistory and save the // history back to the browser history state History._aStateHistory[History._aStateHistory.length - 1] = sFullHash; window.history.replaceState({ sap: { history: History._aStateHistory } }, window.document.title); } this._oNextHash = null; // reset the direction to Unknown when hash is replaced after history is already initialized if (!this._bIsInitial) { this._sCurrentDirection = HistoryDirection.Unknown; } return; } } // a navigation has taken place so the history is not initial anymore. this._bIsInitial = false; if (sDirection) { this._adaptToDirection(sDirection, { oldHash: sOldHash, newHash: sNewHash, fullHash: sFullHash }); } else { // Extended direction determination with window.history.state // // The enhancement for direction determination is only done for the global // instance because the window.history.state can only be used once for the // new entry determination. Once the window.history.state is changed, it // can't be used again for the same hashchange event to determine the // direction which is the case if additional History instances are created if (!sDirection && sFullHash !== undefined && History._bUsePushState && this === History.getInstance()) { sDirection = this._getDirectionWithState(sFullHash); } // if the hashChange event is fired without a real browser hashchange event, the direction isn't updated if (sDirection === DIRECTION_UNCHANGED) { return; } // if the direction can't be decided by using the state method, the fallback to the legacy method is taken if (!sDirection) { sDirection = this._getDirection(sNewHash, this._iHistoryLength < window.history.length, true); } // We are at a known state of the history now, since we have a new entry / forwards or backwards this._bUnknown = false; switch (sDirection) { case HistoryDirection.Unknown: // We don't know the state of the history, don't record it and set it back to unknown, since we can't say what comes up until the app navigates again this._reset(); break; case HistoryDirection.NewEntry: this.aHistory.splice(this.iHistoryPosition + 1, this.aHistory.length - this.iHistoryPosition - 1, sNewHash); this.iHistoryPosition++; break; case HistoryDirection.Forwards: this.iHistoryPosition++; break; case HistoryDirection.Backwards: this.iHistoryPosition--; break; default: break; } } this._sCurrentDirection = sDirection; // Remember the new history length, after it has been taken into account by getDirection this._iHistoryLength = actualHistoryLength; //the next hash direction was determined - set it back if (this._oNextHash) { this._oNextHash = null; } }; /** * Adapts the internal structure by using the given direction information. * * @param {sap.ui.core.routing.HistoryDirection} sDirection The given navigation direction * @param {object} oHashInfo The object that contains the 'oldHash', 'newHash' and 'fullHash' * @private * */ History.prototype._adaptToDirection = function(sDirection, oHashInfo) { var sFullHash = oHashInfo.fullHash, sNewHash = oHashInfo.newHash, iIndex, oState; if (History._bUsePushState && this === History.getInstance() && sFullHash !== undefined) { switch (sDirection) { case HistoryDirection.NewEntry: case HistoryDirection.Forwards: History._aStateHistory.push(sFullHash); break; case HistoryDirection.Backwards: iIndex = History._aStateHistory.lastIndexOf(sFullHash); if (iIndex !== -1) { History._aStateHistory.splice(iIndex + 1); } else { History._aStateHistory = [sFullHash]; Log.debug("Can't find " + sFullHash + " in " + JSON.stringify(History._aStateHistory)); } break; case HistoryDirection.Unknown: History._aStateHistory[History._aStateHistory.length - 1] = sFullHash; break; default: break; } oState = {}; oState.sap = {}; oState.sap.history = History._aStateHistory; window.history.replaceState(oState, document.title); } switch (sDirection) { case HistoryDirection.NewEntry: this.aHistory.splice(this.iHistoryPosition + 1, this.aHistory.length - this.iHistoryPosition - 1, sNewHash); this.iHistoryPosition += 1; break; case HistoryDirection.Forwards: iIndex = this.aHistory.indexOf(sNewHash, this.iHistoryPosition + 1); if (iIndex !== -1) { this.iHistoryPosition = iIndex; } else { // insert the new hash at the next position after the current history position this.aHistory.splice(this.iHistoryPosition + 1, this.aHistory.length - this.iHistoryPosition - 1, sNewHash); this.iHistoryPosition++; } break; case HistoryDirection.Backwards: iIndex = this.aHistory.lastIndexOf(sNewHash, this.iHistoryPosition - 1); if (iIndex !== -1) { this.iHistoryPosition = iIndex; } else { this.aHistory = [sNewHash]; this.iHistoryPosition = 0; } break; case HistoryDirection.Unknown: this.aHistory[this.iHistoryPosition] = sNewHash; break; default: break; } }; /** * Handles a hash change and cleans up the History * @param {sap.ui.base.Event} oEvent The event containing the hash * @private */ History.prototype._hashSet = function(oEvent) { var sHash = oEvent.getParameter("hash"); if (sHash === undefined) { sHash = oEvent.getParameter("sHash"); } this._hashChangedByApp(sHash, false); }; /** * Handles a hash change and cleans up the History * @param {sap.ui.base.Event} oEvent The event containing the hash * @private */ History.prototype._hashReplaced = function(oEvent) { var sHash = oEvent.getParameter("hash"), sDirection = oEvent.getParameter("direction"); if (sHash === undefined) { sHash = oEvent.getParameter("sHash"); } // When the same hash is replaced, let the direction be set manually // This is needed when switching between iframes. The new iframe needs to be updated with the navigation // direction that is forwarded from the outer frame. if (sHash === this._oHashChanger.getHash() && sDirection) { this._sCurrentDirection = sDirection; } this._hashChangedByApp(sHash, true, sDirection); }; /** * Sets the next hash that is going to happen in the hashChange function - used to determine if the app or the browserHistory/links triggered this navigation * * @param {string} sNewHash The new hash * @param {boolean} bWasReplaced If the hash was replaced * @param {sap.ui.core.routing.HistoryDirection} sDirection The direction of the last navigation */ History.prototype._hashChangedByApp = function(sNewHash, bWasReplaced, sDirection) { this._oNextHash = { sHash : sNewHash, bWasReplaced : bWasReplaced, sDirection: sDirection}; }; var instance; /** * @public * @returns { sap.ui.core.routing.History } a global singleton that gets created as soon as the sap.ui.core.routing.History is required */ History.getInstance = function() { return instance; }; instance = new History(HashChanger.getInstance()); return History; }, /* bExport= */ true);