import type IBrowserFrame from '../browser/types/IBrowserFrame.js'; import HistoryScrollRestorationEnum from './HistoryScrollRestorationEnum.js'; import * as PropertySymbol from '../PropertySymbol.js'; import BrowserFrameURL from '../browser/utilities/BrowserFrameURL.js'; import DOMExceptionNameEnum from '../exception/DOMExceptionNameEnum.js'; import type BrowserWindow from '../window/BrowserWindow.js'; /** * History API. * * Reference: * https://developer.mozilla.org/en-US/docs/Web/API/History. */ export default class History { #browserFrame: IBrowserFrame | null; #window: BrowserWindow; /** * Constructor. * * @param browserFrame Browser frame. * @param window Owner window. */ constructor(browserFrame: IBrowserFrame, window: BrowserWindow) { if (!browserFrame) { throw new TypeError('Illegal constructor'); } this.#browserFrame = browserFrame; this.#window = window; } /** * Returns the history length. * * @returns History length. */ public get length(): number { return this.#browserFrame?.[PropertySymbol.history].items.length || 0; } /** * Returns an any value representing the state at the top of the history stack. This is a way to look at the state without having to wait for a popstate event. * * @returns State. */ public get state(): object | null { return this.#browserFrame?.[PropertySymbol.history].currentItem.state || null; } /** * Returns scroll restoration. * * @returns Sroll restoration. */ public get scrollRestoration(): HistoryScrollRestorationEnum { return ( this.#browserFrame?.[PropertySymbol.history].currentItem.scrollRestoration || HistoryScrollRestorationEnum.auto ); } /** * Sets scroll restoration. * * @param scrollRestoration Sroll restoration. */ public set scrollRestoration(scrollRestoration: HistoryScrollRestorationEnum) { switch (scrollRestoration) { case HistoryScrollRestorationEnum.auto: case HistoryScrollRestorationEnum.manual: const currentItem = this.#browserFrame?.[PropertySymbol.history].currentItem; if (currentItem) { currentItem.scrollRestoration = scrollRestoration; } break; } } /** * Goes to the previous page in session history. */ public back(): void { if (!this.#window.closed) { this.#browserFrame?.goBack(); } } /** * Goes to the next page in session history. */ public forward(): void { if (!this.#window.closed) { this.#browserFrame?.goForward(); } } /** * Load a specific page from the session history. * * @param delta Delta. * @param _delta */ public go(delta: number): void { if (!this.#window.closed) { this.#browserFrame?.goSteps(delta); } } /** * Pushes the given data onto the session history stack. * * @param state State. * @param _unused Unused. * @param [url] URL. */ public pushState(state: any, _unused: any, url?: string | URL): void { if (!this.#browserFrame || this.#window.closed) { return; } const history = this.#browserFrame?.[PropertySymbol.history]; if (!history) { return; } if (arguments.length < 2) { throw new this.#window.TypeError( `Failed to execute 'pushState' on 'History': 2 arguments required, but only ${arguments.length} present.` ); } const location = this.#window[PropertySymbol.location]; const newURL = url ? BrowserFrameURL.getRelativeURL(this.#browserFrame, url) : location; if (url && newURL.origin !== location.origin) { throw new this.#window.DOMException( `Failed to execute 'pushState' on 'History': A history state object with URL '${url.toString()}' cannot be created in a document with origin '${ location.origin }' and URL '${location.href}'.`, DOMExceptionNameEnum.securityError ); } history.currentItem.popState = true; history.push({ title: this.#window.document.title, href: newURL.href, state, popState: true, scrollRestoration: history.currentItem.scrollRestoration, method: history.currentItem.method || 'GET', formData: history.currentItem.formData || null }); location[PropertySymbol.setURL](this.#browserFrame, history.currentItem.href); } /** * This method modifies the current history entry, replacing it with a new state. * * @param state State. * @param _unused Unused. * @param [url] URL. */ public replaceState(state: any, _unused: any, url?: string | URL): void { if (!this.#browserFrame || this.#window.closed) { return; } const history = this.#browserFrame?.[PropertySymbol.history]; if (!history) { return; } if (arguments.length < 2) { throw new this.#window.TypeError( `Failed to execute 'pushState' on 'History': 2 arguments required, but only ${arguments.length} present.` ); } const location = this.#window[PropertySymbol.location]; const newURL = url ? BrowserFrameURL.getRelativeURL(this.#browserFrame, url) : location; if (url && newURL.origin !== location.origin) { throw new this.#window.DOMException( `Failed to execute 'pushState' on 'History': A history state object with URL '${url.toString()}' cannot be created in a document with origin '${ location.origin }' and URL '${location.href}'.`, DOMExceptionNameEnum.securityError ); } history.replace({ title: this.#window.document.title, href: newURL.href, state, popState: history.currentItem.popState, scrollRestoration: history.currentItem.scrollRestoration, method: history.currentItem.method, formData: history.currentItem.formData }); if (url) { location[PropertySymbol.setURL](this.#browserFrame, history.currentItem.href); } } /** * Destroys the history. * * This will make sure that the History API can't access page data from the next history item. */ public [PropertySymbol.destroy](): void { this.#browserFrame = null; } }