/
githubmirror
/
amphtml
Обзор
Документация
Войти
/
githubmirror
/
amphtml
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/service/history-impl.js
1 148 строк
30 KB
Viktor Szépe
Fix typos in src/ (#39606)
12 авг 2025, 21:35
Не верифицирован
12 авг 2025, 21:35
fca692e
Код
Авторство
О чём код?
import {Deferred, tryResolve} from '#core/data-structures/promise'; import {map} from '#core/types/object'; import {getHistoryState} from '#core/window/history'; import {Services} from '#service'; import {dev, devAssert} from '#utils/log'; import {getMode} from '../mode'; import { getService, registerServiceBuilder, registerServiceBuilderForDoc, } from '../service-helpers'; /** @private @const {string} */ const TAG_ = 'History'; /** @private @const {string} */ const HISTORY_PROP_ = 'AMP.History'; /** @typedef {number} */ let HistoryIdDef; /** * @typedef {{stackIndex: HistoryIdDef, title: string, fragment: string, data: (!JsonObject|undefined)}} */ let HistoryStateDef; /** * @typedef {{title: (string|undefined), fragment: (string|undefined), url: (string|undefined), canonicalUrl: (string|undefined), data: (!JsonObject|undefined)}} */ let HistoryStateUpdateDef; /** * Wraps the browser's History API for viewer support and necessary polyfills. */ export class History { /** * @param {!./ampdoc-impl.AmpDoc} ampdoc * @param {!HistoryBindingInterface} binding */ constructor(ampdoc, binding) { /** @private @const {!./ampdoc-impl.AmpDoc} */ this.ampdoc_ = ampdoc; /** @private @const {!../service/timer-impl.Timer} */ this.timer_ = Services.timerFor(ampdoc.win); /** @private @const {!HistoryBindingInterface} */ this.binding_ = binding; /** @private {number} */ this.stackIndex_ = 0; /** @private {!Array<!Function|undefined>} */ this.stackOnPop_ = []; /** * @private {!Array<!{ * callback: function():!Promise, * resolve: !Function, * reject: !Function, * trace: (!Error|undefined) * }>} */ this.queue_ = []; this.binding_.setOnStateUpdated(this.onStateUpdated_.bind(this)); } /** @visibleForTesting */ cleanup() { this.binding_.cleanup(); } /** * Pushes new state into history stack with an optional callback to be called * when this state is popped as well as an object with updates to be applied * to the state. * @param {!Function=} opt_onPop * @param {!HistoryStateUpdateDef=} opt_stateUpdate * @return {!Promise<!HistoryIdDef>} */ push(opt_onPop, opt_stateUpdate) { return this.enqueue_(() => { return this.binding_.push(opt_stateUpdate).then((historyState) => { this.onStateUpdated_(historyState); if (opt_onPop) { this.stackOnPop_[historyState.stackIndex] = opt_onPop; } return historyState.stackIndex; }); }, 'push'); } /** * Pops a previously pushed state from the history stack. If onPop callback * has been registered, it will be called with the state that was associated * with the new head state within the history stack. All states coming * after the supplied state will also be popped, and their * callbacks executed in the same fashion. * @param {!HistoryIdDef} stateId * @return {!Promise} */ pop(stateId) { return this.enqueue_(() => { return this.binding_.pop(stateId).then((historyState) => { this.onStateUpdated_(historyState); }); }, 'pop'); } /** * Replaces the current state, optionally specifying updates to the state * object to be associated with the replacement. * @param {!HistoryStateUpdateDef=} opt_stateUpdate * @return {!Promise} */ replace(opt_stateUpdate) { return this.enqueue_( () => this.binding_.replace(opt_stateUpdate), 'replace' ); } /** * Retrieves the current state, containing the current fragment, title, * and amp-bind state. * @return {!Promise<!HistoryStateDef>} */ get() { return this.enqueue_(() => this.binding_.get(), 'get'); } /** * Requests navigation one step back. This first attempts to go back within * the context of this document. * * @param {boolean=} navigate * @return {!Promise} */ goBack(navigate) { return this.enqueue_(() => { if (this.stackIndex_ <= 0 && !navigate) { return Promise.resolve(); } // Pop the current state. The binding will ignore the request if // it cannot satisfy it. return this.binding_.pop(this.stackIndex_).then((historyState) => { this.onStateUpdated_(historyState); }); }, 'goBack'); } /** * Helper method to handle navigation to a local target, e.g. When a user * clicks an anchor link to a local hash - <a href="#section1">Go to section * 1</a>. * * @param {string} target * @return {!Promise} */ replaceStateForTarget(target) { devAssert(target[0] == '#', 'target should start with a #'); const previousHash = this.ampdoc_.win.location.hash; return this.push(() => { this.ampdoc_.win.location.replace(previousHash || '#'); }).then(() => { this.binding_.replaceStateForTarget(target); }); } /** * Get the fragment from the url or the viewer. * Strip leading '#' in the fragment * @return {!Promise<string>} */ getFragment() { return this.binding_.getFragment(); } /** * Update the page url fragment * @param {string} fragment * @return {!Promise} */ updateFragment(fragment) { if (fragment[0] == '#') { fragment = fragment.substr(1); } return this.binding_.updateFragment(fragment); } /** * @param {!HistoryStateDef} historyState * @private */ onStateUpdated_(historyState) { this.stackIndex_ = historyState.stackIndex; this.doPop_(historyState); } /** * @param {!HistoryStateDef} historyState * @private */ doPop_(historyState) { if (this.stackIndex_ >= this.stackOnPop_.length - 1) { return; } const toPop = []; for (let i = this.stackOnPop_.length - 1; i > this.stackIndex_; i--) { if (this.stackOnPop_[i]) { toPop.push(this.stackOnPop_[i]); this.stackOnPop_[i] = undefined; } } this.stackOnPop_.splice(this.stackIndex_ + 1); if (toPop.length > 0) { for (let i = 0; i < toPop.length; i++) { // With the same delay timeouts must observe the order, although // there's no hard requirement in this case to follow the pop order. this.timer_.delay(() => toPop[i](historyState), 1); } } } /** * @param {function():!Promise<RESULT>} callback * @param {string} name * @return {!Promise<RESULT>} * @template RESULT * @private */ enqueue_(callback, name) { const deferred = new Deferred(); const {promise, reject, resolve} = deferred; // TODO(dvoytenko, #8785): cleanup after tracing. const trace = new Error('history trace for ' + name + ': '); this.queue_.push({callback, resolve, reject, trace}); if (this.queue_.length == 1) { this.deque_(); } return promise; } /** * @private */ deque_() { if (this.queue_.length == 0) { return; } const task = this.queue_[0]; let promise; try { promise = task.callback(); } catch (e) { promise = Promise.reject(e); } promise .then( (result) => { task.resolve(result); }, (reason) => { dev().error(TAG_, 'failed to execute a task:', reason); // TODO(dvoytenko, #8785): cleanup after tracing. if (task.trace) { task.trace.message += reason; dev().error(TAG_, task.trace); } task.reject(reason); } ) .then(() => { this.queue_.splice(0, 1); this.deque_(); }); } } /** * HistoryBindingInterface is an interface that defines an underlying technology * behind the {@link History}. * @interface */ class HistoryBindingInterface { /** @protected */ cleanup() {} /** * Configures a callback to be called when the state has been updated. * @param {function(!HistoryStateDef)} unusedCallback * @protected */ setOnStateUpdated(unusedCallback) {} /** * Pushes a new state onto the history stack, optionally specifying the state * object associated with the current state. * Returns a promise that yields the new state. * @param {!HistoryStateUpdateDef=} opt_stateUpdate * @return {!Promise<!HistoryStateDef>} */ push(opt_stateUpdate) {} /** * Pops a previously pushed state from the history stack. All history * states coming after this state will also be popped. * Returns a promise that yields the new state. * @param {number} unusedStackIndex * @return {!Promise<!HistoryStateDef>} */ pop(unusedStackIndex) {} /** * Replaces the current state, optionally specifying updates to the state * object to be associated with the replacement. * Returns a promise that yields the new state. * @param {!HistoryStateUpdateDef=} opt_stateUpdate * @return {!Promise<!HistoryStateDef>} */ replace(opt_stateUpdate) {} /** * Retrieves the current state, containing the current fragment, title, * and amp-bind state. * @return {!Promise<!HistoryStateDef>} */ get() {} /** * Replaces the state for local target navigation. * @param {string} unusedTarget */ replaceStateForTarget(unusedTarget) {} /** * Get the fragment from the url or the viewer. * Strip leading '#' in the fragment * @return {!Promise<string>} */ getFragment() {} /** * Update the page url fragment * @param {string} unusedFragment * @return {!Promise} */ updateFragment(unusedFragment) {} } /** * Implementation of HistoryBindingInterface based on the native window. It uses * window.history properties and events. * * Visible for testing. * * @implements {HistoryBindingInterface} */ export class HistoryBindingNatural_ { /** * @param {!Window} win */ constructor(win) { /** @const {!Window} */ this.win = win; /** @private @const {!../service/timer-impl.Timer} */ this.timer_ = Services.timerFor(win); const {history} = this.win; /** @private {number} */ this.startIndex_ = history.length - 1; const state = getHistoryState(history); if (state && state[HISTORY_PROP_] !== undefined) { this.startIndex_ = Math.min(state[HISTORY_PROP_], this.startIndex_); } /** @private {number} */ this.stackIndex_ = this.startIndex_; /** * @private {{promise: !Promise, resolve: !Function, * reject: !Function}|undefined} */ this.waitingState_; /** @private {?function(!HistoryStateDef)} */ this.onStateUpdated_ = null; // A number of browsers do not support history.state. In this cases, // History will track its own version. See unsupportedState_. /** @private {boolean} @const */ this.supportsState_ = 'state' in history; /** @private {*} */ this.unsupportedState_ = this.historyState_(this.stackIndex_); // There are still browsers who do not support push/replaceState. let pushState, replaceState; if (history.pushState && history.replaceState) { /** @private @const {function(*, string=, string=)|undefined} */ this.origPushState_ = history.originalPushState || history.pushState.bind(history); /** @private @const {function(*, string=, string=)|undefined} */ this.origReplaceState_ = history.originalReplaceState || history.replaceState.bind(history); pushState = (state, opt_title, opt_url) => { this.unsupportedState_ = state; try { this.origPushState_( state, opt_title, // A bug in edge causes paths to become undefined if URL is // undefined, filed here: https://goo.gl/KlImZu opt_url || null ); } catch (e) { dev().error(TAG_, 'pushState failed: ' + e.message); } }; replaceState = (state, opt_title, opt_url) => { this.unsupportedState_ = state; // NOTE: check for `undefined` since IE11 and Edge // unexpectedly coerces it into a `string`. try { if (opt_url !== undefined) { this.origReplaceState_(state, opt_title, opt_url); } else { this.origReplaceState_(state, opt_title); } } catch (e) { dev().error(TAG_, 'replaceState failed: ' + e.message); } }; if (!history.originalPushState) { history.originalPushState = this.origPushState_; } if (!history.originalReplaceState) { history.originalReplaceState = this.origReplaceState_; } } else { pushState = (state, opt_title, opt_url) => { this.unsupportedState_ = state; }; replaceState = (state, opt_title, opt_url) => { this.unsupportedState_ = state; }; } /** @private @const {!Function} */ this.pushState_ = pushState; /** @private @const {!Function} */ this.replaceState_ = replaceState; try { this.replaceState_( this.historyState_(this.stackIndex_, /* replace */ true) ); } catch (e) { dev().error(TAG_, 'Initial replaceState failed: ' + e.message); } history.pushState = this.historyPushState_.bind(this); history.replaceState = this.historyReplaceState_.bind(this); this.popstateHandler_ = (e) => { const event = /** @type {!PopStateEvent} */ (e); const state = /** @type {!JsonObject} */ (event.state); dev().fine( TAG_, 'popstate event: ' + this.win.history.length + ', ' + JSON.stringify(state) ); this.onHistoryEvent_(); }; this.win.addEventListener('popstate', this.popstateHandler_); } /** @override */ cleanup() { if (this.origPushState_) { this.win.history.pushState = this.origPushState_; } if (this.origReplaceState_) { this.win.history.replaceState = this.origReplaceState_; } this.win.removeEventListener('popstate', this.popstateHandler_); } /** * @param {number} stackIndex * @param {boolean=} opt_replace * @return {*} * @private */ historyState_(stackIndex, opt_replace) { const state = map(opt_replace ? this.getState_() : undefined); state[HISTORY_PROP_] = stackIndex; return state; } /** @override */ setOnStateUpdated(callback) { this.onStateUpdated_ = callback; } /** @override */ push(opt_stateUpdate) { return this.whenReady_(() => { const newState = this.mergeStateUpdate_( this.getState_(), opt_stateUpdate || {} ); this.historyPushState_( newState, /* title */ undefined, newState.fragment ? '#' + newState.fragment : undefined ); return tryResolve(() => this.mergeStateUpdate_(newState, {stackIndex: this.stackIndex_}) ); }); } /** @override */ pop(stackIndex) { // On pop, stack is not allowed to go prior to the starting point. stackIndex = Math.max(stackIndex, this.startIndex_); return this.whenReady_(() => { return this.back_(this.stackIndex_ - stackIndex + 1); }).then((newStackIndex) => { return this.mergeStateUpdate_(this.getState_(), { stackIndex: newStackIndex, }); }); } /** @override */ replace(opt_stateUpdate = {}) { return this.whenReady_(() => { const newState = this.mergeStateUpdate_( this.getState_(), opt_stateUpdate || {} ); const url = (newState.url || '').replace(/#.*/, ''); const fragment = newState.fragment ? '#' + newState.fragment : ''; this.historyReplaceState_( newState, newState.title, url || fragment ? url + fragment : undefined ); return tryResolve(() => this.mergeStateUpdate_(newState, {stackIndex: this.stackIndex_}) ); }); } /** @override */ get() { return tryResolve(() => this.mergeStateUpdate_(this.getState_(), { stackIndex: this.stackIndex_, }) ); } /** * @param {number} stackIndex * @return {!Promise} */ backTo(stackIndex) { // On pop, stack is not allowed to go prior to the starting point. stackIndex = Math.max(stackIndex, this.startIndex_); return this.whenReady_(() => { return this.back_(this.stackIndex_ - stackIndex); }); } /** @private */ onHistoryEvent_() { let state = this.getState_(); dev().fine( TAG_, 'history event: ' + this.win.history.length + ', ' + JSON.stringify(state) ); const stackIndex = state ? state[HISTORY_PROP_] : undefined; let newStackIndex = this.stackIndex_; const waitingState = this.waitingState_; this.waitingState_ = undefined; if (newStackIndex > this.win.history.length - 2) { // Make sure stack has enough space. Whether we are going forward or // backward, the stack should have at least one extra cell. newStackIndex = this.win.history.length - 2; this.updateHistoryState_( this.mergeStateUpdate_(state, {stackIndex: newStackIndex}) ); } if (stackIndex == undefined) { // A new navigation forward by the user. newStackIndex = newStackIndex + 1; } else if (stackIndex < this.win.history.length) { // A simple trip back. newStackIndex = stackIndex; } else { // Generally not possible, but for posterity. newStackIndex = this.win.history.length - 1; } // If state index has been updated as the result replace the state. if (!state) { state = {}; } state[HISTORY_PROP_] = newStackIndex; this.replaceState_(state, undefined, undefined); // Update the stack, pop squeezed states. if (newStackIndex != this.stackIndex_) { this.updateHistoryState_( this.mergeStateUpdate_(state, {stackIndex: newStackIndex}) ); } // User navigation is allowed to move past the starting point of // the history stack. if (newStackIndex < this.startIndex_) { this.startIndex_ = newStackIndex; } if (waitingState) { waitingState.resolve(); } } /** @private */ getState_() { if (this.supportsState_) { return getHistoryState(this.win.history); } return this.unsupportedState_; } /** @private */ assertReady_() { devAssert( !this.waitingState_, 'The history must not be in the waiting state' ); } /** * @param {function():!Promise<RESULT>} callback * @return {!Promise<RESULT>} * @template RESULT * @private */ whenReady_(callback) { if (!this.waitingState_) { return callback(); } return this.waitingState_.promise.then(callback, callback); } /** * @return {!Promise} * @private */ wait_() { this.assertReady_(); const deferred = new Deferred(); const {reject, resolve} = deferred; const promise = this.timer_.timeoutPromise(500, deferred.promise); this.waitingState_ = {promise, resolve, reject}; return promise; } /** * @param {number} steps * @return {!Promise} */ back_(steps) { this.assertReady_(); if (steps <= 0) { return Promise.resolve(this.stackIndex_); } this.unsupportedState_ = this.historyState_(this.stackIndex_ - steps); const promise = this.wait_(); this.win.history.go(-steps); return promise.then(() => { return Promise.resolve(this.stackIndex_); }); } /** * @param {*=} state * @param {(string|undefined)=} title * @param {(string|undefined)=} url * @private */ historyPushState_(state, title, url) { this.assertReady_(); if (!state) { state = {}; } let stackIndex = this.stackIndex_ + 1; state[HISTORY_PROP_] = stackIndex; this.pushState_(state, title, url); if (stackIndex != this.win.history.length - 1) { stackIndex = this.win.history.length - 1; state[HISTORY_PROP_] = stackIndex; this.replaceState_(state); } const newState = this.mergeStateUpdate_( /** @type {!HistoryStateDef} */ (state), {stackIndex} ); this.updateHistoryState_(newState); } /** * If this is a hash update the choice of `location.replace` vs * `history.replaceState` is important. Due to bugs, not every browser * triggers `:target` pseudo-class when `replaceState` is called. * See http://www.zachleat.com/web/moving-target/ for more details. * location.replace will trigger a `popstate` event, we temporarily * disable handling it. * @param {string} target * * @override */ replaceStateForTarget(target) { devAssert(target[0] == '#', 'target should start with a #'); this.whenReady_(() => { // location.replace will fire a popstate event which is not a history // event, so temporarily remove the event listener and re-add it after. // As explained above in the function comment, typically we'd just do // replaceState here but in order to trigger :target re-eval we have to // use location.replace. this.win.removeEventListener('popstate', this.popstateHandler_); try { // TODO(mkhatib, #6095): Chrome iOS will add extra states for // location.replace. this.win.location.replace(target); } finally { this.win.addEventListener('popstate', this.popstateHandler_); } this.historyReplaceState_(); return Promise.resolve(); }); } /** * @param {*=} state * @param {(string|undefined)=} title * @param {(string|undefined)=} url * @private */ historyReplaceState_(state, title, url) { this.assertReady_(); if (!state) { state = {}; } const stackIndex = Math.min(this.stackIndex_, this.win.history.length - 1); state[HISTORY_PROP_] = stackIndex; this.replaceState_(state, title, url); const newState = this.mergeStateUpdate_( /** @type {!HistoryStateDef} */ (state), {stackIndex} ); this.updateHistoryState_(newState); } /** * @param {!HistoryStateDef} historyState * @private */ updateHistoryState_(historyState) { this.assertReady_(); historyState.stackIndex = Math.min( historyState.stackIndex, this.win.history.length - 1 ); if (this.stackIndex_ != historyState.stackIndex) { dev().fine( TAG_, 'stack index changed: ' + this.stackIndex_ + ' -> ' + historyState.stackIndex ); this.stackIndex_ = historyState.stackIndex; if (this.onStateUpdated_) { this.onStateUpdated_(historyState); } } } /** @override */ getFragment() { let {hash} = this.win.location; /* Strip leading '#' */ hash = hash.substr(1); return Promise.resolve(hash); } /** @override */ updateFragment(fragment) { return this.replace({fragment}); } /** * @param {?HistoryStateDef} state * @param {!HistoryStateUpdateDef} update * @return {!HistoryStateDef} */ mergeStateUpdate_(state, update) { const mergedData = /** @type {!JsonObject} */ ({ ...((state && state.data) || {}), ...(update.data || {}), }); return /** @type {!HistoryStateDef} */ ({ ...(state || {}), ...update, data: mergedData, }); } } /** * Implementation of HistoryBindingInterface that assumes a virtual history that * relies on viewer's "pushHistory", "popHistory" and "historyPopped" * protocol. * * Visible for testing. * * @implements {HistoryBindingInterface} */ export class HistoryBindingVirtual_ { /** * @param {!Window} win * @param {!./viewer-interface.ViewerInterface} viewer */ constructor(win, viewer) { /** @const {!Window} */ this.win = win; /** @private @const {!./viewer-interface.ViewerInterface} */ this.viewer_ = viewer; /** @private {number} */ this.stackIndex_ = 0; /** @private {?function(!HistoryStateDef)} */ this.onStateUpdated_ = null; /** @private {!UnlistenDef} */ this.unlistenOnHistoryPopped_ = this.viewer_.onMessage( 'historyPopped', (data) => this.onHistoryPopped_(data) ); } /** @override */ replaceStateForTarget(target) { devAssert(target[0] == '#', 'target should start with a #'); this.win.location.replace(target); } /** @override */ cleanup() { this.unlistenOnHistoryPopped_(); } /** @override */ setOnStateUpdated(callback) { this.onStateUpdated_ = callback; } /** * Gets the history state from a response. This checks if `maybeHistoryState` * is a history state, and returns it if so, falling back to `fallbackState` * otherwise. * @param {*} maybeHistoryState * @param {!HistoryStateDef} fallbackState * @param {string} debugId * @return {!HistoryStateDef} * @private */ toHistoryState_(maybeHistoryState, fallbackState, debugId) { if (this.isHistoryState_(maybeHistoryState)) { return /** @type {!HistoryStateDef} */ (maybeHistoryState); } else { dev().warn( TAG_, 'Ignored unexpected "%s" data:', debugId, maybeHistoryState ); } return fallbackState; } /** * @param {*} maybeHistoryState * @return {boolean} */ isHistoryState_(maybeHistoryState) { return !!maybeHistoryState && maybeHistoryState['stackIndex'] !== undefined; } /** * `pushHistory` * * Request: {'stackIndex': string} * Response: undefined | {'stackIndex': string} * * @override */ push(opt_stateUpdate) { const message = /** @type {!JsonObject} */ ({ 'stackIndex': this.stackIndex_ + 1, ...(opt_stateUpdate || {}), }); const push = 'pushHistory'; return this.viewer_ .sendMessageAwaitResponse(push, message) .then((response) => { const fallbackState = /** @type {!HistoryStateDef} */ (message); const newState = this.toHistoryState_(response, fallbackState, push); this.updateHistoryState_(newState); return newState; }); } /** * `popHistory` * * Request: {'stackIndex': string} * Response: undefined | {'stackIndex': string} * * @override */ pop(stackIndex) { if (stackIndex > this.stackIndex_) { return this.get(); } const message = {'stackIndex': this.stackIndex_}; const pop = 'popHistory'; return this.viewer_ .sendMessageAwaitResponse(pop, message) .then((response) => { const fallbackState = /** @type {!HistoryStateDef} */ ({ 'stackIndex': this.stackIndex_ - 1, }); const newState = this.toHistoryState_(response, fallbackState, pop); this.updateHistoryState_(newState); return newState; }); } /** * `replaceHistory` * * Request: {'fragment': string} * Response: undefined | {'stackIndex': string} * * @override */ replace(opt_stateUpdate) { if (opt_stateUpdate && opt_stateUpdate.url) { if (!this.viewer_.hasCapability('fullReplaceHistory')) { // Full URL replacement requested, but not supported by the viewer. // Don't update, and return the current state. const curState = /** @type {!HistoryStateDef} */ ({ 'stackIndex': this.stackIndex_, }); return Promise.resolve(curState); } // replace fragment, only explicit fragment param will be sent. const url = opt_stateUpdate.url.replace(/#.*/, ''); opt_stateUpdate.url = url; } const message = /** @type {!JsonObject} */ ({ 'stackIndex': this.stackIndex_, ...(opt_stateUpdate || {}), }); const replace = 'replaceHistory'; return this.viewer_ .sendMessageAwaitResponse(replace, message, /* cancelUnsent */ true) .then((response) => { const fallbackState = /** @type {!HistoryStateDef} */ (message); const newState = this.toHistoryState_(response, fallbackState, replace); this.updateHistoryState_(newState); return newState; }); } /** * Note: Only returns the current `stackIndex`. * @override */ get() { // Not sure why this type coercion is necessary, but CC complains otherwise. return Promise.resolve( /** @type {!HistoryStateDef} */ ({ data: undefined, fragment: '', stackIndex: this.stackIndex_, title: '', }) ); } /** * `historyPopped` (from viewer) * * Request: {'newStackIndex': number} | {'stackIndex': number} * Response: undefined * * @param {!JsonObject} data * @private */ onHistoryPopped_(data) { if (data['newStackIndex'] !== undefined) { data['stackIndex'] = data['newStackIndex']; } if (this.isHistoryState_(data)) { this.updateHistoryState_(/** @type {!HistoryStateDef} */ (data)); } else { dev().warn(TAG_, 'Ignored unexpected "historyPopped" data:', data); } } /** * @param {!HistoryStateDef} state * @private */ updateHistoryState_(state) { const {stackIndex} = state; if (this.stackIndex_ != stackIndex) { dev().fine(TAG_, `stackIndex: ${this.stackIndex_} -> ${stackIndex}`); this.stackIndex_ = stackIndex; if (this.onStateUpdated_) { this.onStateUpdated_(state); } } } /** * `getFragment` * * Request: undefined * Response: string * * @override */ getFragment() { if (!this.viewer_.hasCapability('fragment')) { return Promise.resolve(''); } return this.viewer_ .sendMessageAwaitResponse( 'getFragment', undefined, /* cancelUnsent */ true ) .then((data) => { if (!data) { return ''; } let hash = dev().assertString(data); /* Strip leading '#'*/ if (hash[0] == '#') { hash = hash.substr(1); } return hash; }); } /** * `replaceHistory` * * Request: {'fragment': string} * Response: undefined | {'stackIndex': string} * * @override */ updateFragment(fragment) { if (!this.viewer_.hasCapability('fragment')) { return Promise.resolve(); } return /** @type {!Promise} */ this.viewer_.sendMessageAwaitResponse( 'replaceHistory', {'fragment': fragment}, /* cancelUnsent */ true ); } } /** * @param {!./ampdoc-impl.AmpDoc} ampdoc * @return {!History} * @private */ function createHistory(ampdoc) { const viewer = Services.viewerForDoc(ampdoc); let binding; if ( viewer.isOvertakeHistory() || getMode(ampdoc.win).test || ampdoc.win.__AMP_TEST_IFRAME ) { binding = new HistoryBindingVirtual_(ampdoc.win, viewer); } else { // Only one global "natural" binding is allowed since it works with the // global history stack. registerServiceBuilder( ampdoc.win, 'global-history-binding', HistoryBindingNatural_ ); binding = getService(ampdoc.win, 'global-history-binding'); } return new History(ampdoc, binding); } /** * @param {!./ampdoc-impl.AmpDoc} ampdoc */ export function installHistoryServiceForDoc(ampdoc) { registerServiceBuilderForDoc(ampdoc, 'history', createHistory); }