|
@@ -1,228 +0,0 @@
|
|
|
-import type { WebContents, LoadURLOptions } from 'electron/main';
|
|
|
-import { EventEmitter } from 'events';
|
|
|
-
|
|
|
-// JavaScript implementation of Chromium's NavigationController.
|
|
|
-// Instead of relying on Chromium for history control, we completely do history
|
|
|
-// control on user land, and only rely on WebContents.loadURL for navigation.
|
|
|
-// This helps us avoid Chromium's various optimizations so we can ensure renderer
|
|
|
-// process is restarted every time.
|
|
|
-export class NavigationController extends EventEmitter {
|
|
|
- currentIndex: number = -1;
|
|
|
- inPageIndex: number = -1;
|
|
|
- pendingIndex: number = -1;
|
|
|
- history: string[] = [];
|
|
|
-
|
|
|
- constructor (private webContents: WebContents) {
|
|
|
- super();
|
|
|
- this.clearHistory();
|
|
|
-
|
|
|
- // webContents may have already navigated to a page.
|
|
|
- if (this.webContents._getURL()) {
|
|
|
- this.currentIndex++;
|
|
|
- this.history.push(this.webContents._getURL());
|
|
|
- }
|
|
|
- this.webContents.on('navigation-entry-committed' as any, (event: Electron.Event, url: string, inPage: boolean, replaceEntry: boolean) => {
|
|
|
- if (this.inPageIndex > -1 && !inPage) {
|
|
|
- // Navigated to a new page, clear in-page mark.
|
|
|
- this.inPageIndex = -1;
|
|
|
- } else if (this.inPageIndex === -1 && inPage && !replaceEntry) {
|
|
|
- // Started in-page navigations.
|
|
|
- this.inPageIndex = this.currentIndex;
|
|
|
- }
|
|
|
- if (this.pendingIndex >= 0) {
|
|
|
- // Go to index.
|
|
|
- this.currentIndex = this.pendingIndex;
|
|
|
- this.pendingIndex = -1;
|
|
|
- this.history[this.currentIndex] = url;
|
|
|
- } else if (replaceEntry) {
|
|
|
- // Non-user initialized navigation.
|
|
|
- this.history[this.currentIndex] = url;
|
|
|
- } else {
|
|
|
- // Normal navigation. Clear history.
|
|
|
- this.history = this.history.slice(0, this.currentIndex + 1);
|
|
|
- this.currentIndex++;
|
|
|
- this.history.push(url);
|
|
|
- }
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
- loadURL (url: string, options?: LoadURLOptions): Promise<void> {
|
|
|
- if (options == null) {
|
|
|
- options = {};
|
|
|
- }
|
|
|
- const p = new Promise<void>((resolve, reject) => {
|
|
|
- const resolveAndCleanup = () => {
|
|
|
- removeListeners();
|
|
|
- resolve();
|
|
|
- };
|
|
|
- const rejectAndCleanup = (errorCode: number, errorDescription: string, url: string) => {
|
|
|
- const err = new Error(`${errorDescription} (${errorCode}) loading '${typeof url === 'string' ? url.substr(0, 2048) : url}'`);
|
|
|
- Object.assign(err, { errno: errorCode, code: errorDescription, url });
|
|
|
- removeListeners();
|
|
|
- reject(err);
|
|
|
- };
|
|
|
- const finishListener = () => {
|
|
|
- resolveAndCleanup();
|
|
|
- };
|
|
|
- const failListener = (event: Electron.Event, errorCode: number, errorDescription: string, validatedURL: string, isMainFrame: boolean) => {
|
|
|
- if (isMainFrame) {
|
|
|
- rejectAndCleanup(errorCode, errorDescription, validatedURL);
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- let navigationStarted = false;
|
|
|
- const navigationListener = (event: Electron.Event, url: string, isSameDocument: boolean, isMainFrame: boolean) => {
|
|
|
- if (isMainFrame) {
|
|
|
- if (navigationStarted && !isSameDocument) {
|
|
|
- // the webcontents has started another unrelated navigation in the
|
|
|
- // main frame (probably from the app calling `loadURL` again); reject
|
|
|
- // the promise
|
|
|
- // We should only consider the request aborted if the "navigation" is
|
|
|
- // actually navigating and not simply transitioning URL state in the
|
|
|
- // current context. E.g. pushState and `location.hash` changes are
|
|
|
- // considered navigation events but are triggered with isSameDocument.
|
|
|
- // We can ignore these to allow virtual routing on page load as long
|
|
|
- // as the routing does not leave the document
|
|
|
- return rejectAndCleanup(-3, 'ERR_ABORTED', url);
|
|
|
- }
|
|
|
- navigationStarted = true;
|
|
|
- }
|
|
|
- };
|
|
|
- const stopLoadingListener = () => {
|
|
|
- // By the time we get here, either 'finish' or 'fail' should have fired
|
|
|
- // if the navigation occurred. However, in some situations (e.g. when
|
|
|
- // attempting to load a page with a bad scheme), loading will stop
|
|
|
- // without emitting finish or fail. In this case, we reject the promise
|
|
|
- // with a generic failure.
|
|
|
- // TODO(jeremy): enumerate all the cases in which this can happen. If
|
|
|
- // the only one is with a bad scheme, perhaps ERR_INVALID_ARGUMENT
|
|
|
- // would be more appropriate.
|
|
|
- rejectAndCleanup(-2, 'ERR_FAILED', url);
|
|
|
- };
|
|
|
- const removeListeners = () => {
|
|
|
- this.webContents.removeListener('did-finish-load', finishListener);
|
|
|
- this.webContents.removeListener('did-fail-load', failListener);
|
|
|
- this.webContents.removeListener('did-start-navigation', navigationListener);
|
|
|
- this.webContents.removeListener('did-stop-loading', stopLoadingListener);
|
|
|
- this.webContents.removeListener('destroyed', stopLoadingListener);
|
|
|
- };
|
|
|
- this.webContents.on('did-finish-load', finishListener);
|
|
|
- this.webContents.on('did-fail-load', failListener);
|
|
|
- this.webContents.on('did-start-navigation', navigationListener);
|
|
|
- this.webContents.on('did-stop-loading', stopLoadingListener);
|
|
|
- this.webContents.on('destroyed', stopLoadingListener);
|
|
|
- });
|
|
|
- // Add a no-op rejection handler to silence the unhandled rejection error.
|
|
|
- p.catch(() => {});
|
|
|
- this.pendingIndex = -1;
|
|
|
- this.webContents._loadURL(url, options);
|
|
|
- this.webContents.emit('load-url', url, options);
|
|
|
- return p;
|
|
|
- }
|
|
|
-
|
|
|
- getURL () {
|
|
|
- if (this.currentIndex === -1) {
|
|
|
- return '';
|
|
|
- } else {
|
|
|
- return this.history[this.currentIndex];
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- stop () {
|
|
|
- this.pendingIndex = -1;
|
|
|
- return this.webContents._stop();
|
|
|
- }
|
|
|
-
|
|
|
- reload () {
|
|
|
- this.pendingIndex = this.currentIndex;
|
|
|
- return this.webContents._loadURL(this.getURL(), {});
|
|
|
- }
|
|
|
-
|
|
|
- reloadIgnoringCache () {
|
|
|
- this.pendingIndex = this.currentIndex;
|
|
|
- return this.webContents._loadURL(this.getURL(), {
|
|
|
- extraHeaders: 'pragma: no-cache\n',
|
|
|
- reloadIgnoringCache: true
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
- canGoBack () {
|
|
|
- return this.getActiveIndex() > 0;
|
|
|
- }
|
|
|
-
|
|
|
- canGoForward () {
|
|
|
- return this.getActiveIndex() < this.history.length - 1;
|
|
|
- }
|
|
|
-
|
|
|
- canGoToIndex (index: number) {
|
|
|
- return index >= 0 && index < this.history.length;
|
|
|
- }
|
|
|
-
|
|
|
- canGoToOffset (offset: number) {
|
|
|
- return this.canGoToIndex(this.currentIndex + offset);
|
|
|
- }
|
|
|
-
|
|
|
- clearHistory () {
|
|
|
- this.history = [];
|
|
|
- this.currentIndex = -1;
|
|
|
- this.pendingIndex = -1;
|
|
|
- this.inPageIndex = -1;
|
|
|
- }
|
|
|
-
|
|
|
- goBack () {
|
|
|
- if (!this.canGoBack()) {
|
|
|
- return;
|
|
|
- }
|
|
|
- this.pendingIndex = this.getActiveIndex() - 1;
|
|
|
- if (this.inPageIndex > -1 && this.pendingIndex >= this.inPageIndex) {
|
|
|
- return this.webContents._goBack();
|
|
|
- } else {
|
|
|
- return this.webContents._loadURL(this.history[this.pendingIndex], {});
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- goForward () {
|
|
|
- if (!this.canGoForward()) {
|
|
|
- return;
|
|
|
- }
|
|
|
- this.pendingIndex = this.getActiveIndex() + 1;
|
|
|
- if (this.inPageIndex > -1 && this.pendingIndex >= this.inPageIndex) {
|
|
|
- return this.webContents._goForward();
|
|
|
- } else {
|
|
|
- return this.webContents._loadURL(this.history[this.pendingIndex], {});
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- goToIndex (index: number) {
|
|
|
- if (!this.canGoToIndex(index)) {
|
|
|
- return;
|
|
|
- }
|
|
|
- this.pendingIndex = index;
|
|
|
- return this.webContents._loadURL(this.history[this.pendingIndex], {});
|
|
|
- }
|
|
|
-
|
|
|
- goToOffset (offset: number) {
|
|
|
- if (!this.canGoToOffset(offset)) {
|
|
|
- return;
|
|
|
- }
|
|
|
- const pendingIndex = this.currentIndex + offset;
|
|
|
- if (this.inPageIndex > -1 && pendingIndex >= this.inPageIndex) {
|
|
|
- this.pendingIndex = pendingIndex;
|
|
|
- return this.webContents._goToOffset(offset);
|
|
|
- } else {
|
|
|
- return this.goToIndex(pendingIndex);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- getActiveIndex () {
|
|
|
- if (this.pendingIndex === -1) {
|
|
|
- return this.currentIndex;
|
|
|
- } else {
|
|
|
- return this.pendingIndex;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- length () {
|
|
|
- return this.history.length;
|
|
|
- }
|
|
|
-}
|