123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243 |
- 'use strict';
- const { ipcMainInternal } = require('@electron/internal/browser/ipc-main-internal');
- // The history operation in renderer is redirected to browser.
- ipcMainInternal.on('ELECTRON_NAVIGATION_CONTROLLER_GO_BACK', function (event) {
- event.sender.goBack();
- });
- ipcMainInternal.on('ELECTRON_NAVIGATION_CONTROLLER_GO_FORWARD', function (event) {
- event.sender.goForward();
- });
- ipcMainInternal.on('ELECTRON_NAVIGATION_CONTROLLER_GO_TO_OFFSET', function (event, offset) {
- event.sender.goToOffset(offset);
- });
- ipcMainInternal.on('ELECTRON_NAVIGATION_CONTROLLER_LENGTH', function (event) {
- event.returnValue = event.sender.length();
- });
- // JavaScript implementation of Chromium's NavigationController.
- // Instead of relying on Chromium for history control, we compeletely 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 everytime.
- const NavigationController = (function () {
- function NavigationController (webContents) {
- this.webContents = webContents;
- 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', (event, url, inPage, replaceEntry) => {
- 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);
- }
- });
- }
- NavigationController.prototype.loadURL = function (url, options) {
- if (options == null) {
- options = {};
- }
- const p = new Promise((resolve, reject) => {
- const resolveAndCleanup = () => {
- removeListeners();
- resolve();
- };
- const rejectAndCleanup = (errorCode, errorDescription, url) => {
- 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, errorCode, errorDescription, validatedURL, isMainFrame, frameProcessId, frameRoutingId) => {
- if (isMainFrame) {
- rejectAndCleanup(errorCode, errorDescription, validatedURL);
- }
- };
- let navigationStarted = false;
- const navigationListener = (event, url, isSameDocument, isMainFrame, frameProcessId, frameRoutingId, navigationId) => {
- 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.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);
- });
- // 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;
- };
- NavigationController.prototype.getURL = function () {
- if (this.currentIndex === -1) {
- return '';
- } else {
- return this.history[this.currentIndex];
- }
- };
- NavigationController.prototype.stop = function () {
- this.pendingIndex = -1;
- return this.webContents._stop();
- };
- NavigationController.prototype.reload = function () {
- this.pendingIndex = this.currentIndex;
- return this.webContents._loadURL(this.getURL(), {});
- };
- NavigationController.prototype.reloadIgnoringCache = function () {
- this.pendingIndex = this.currentIndex;
- return this.webContents._loadURL(this.getURL(), {
- extraHeaders: 'pragma: no-cache\n',
- reloadIgnoringCache: true
- });
- };
- NavigationController.prototype.canGoBack = function () {
- return this.getActiveIndex() > 0;
- };
- NavigationController.prototype.canGoForward = function () {
- return this.getActiveIndex() < this.history.length - 1;
- };
- NavigationController.prototype.canGoToIndex = function (index) {
- return index >= 0 && index < this.history.length;
- };
- NavigationController.prototype.canGoToOffset = function (offset) {
- return this.canGoToIndex(this.currentIndex + offset);
- };
- NavigationController.prototype.clearHistory = function () {
- this.history = [];
- this.currentIndex = -1;
- this.pendingIndex = -1;
- this.inPageIndex = -1;
- };
- NavigationController.prototype.goBack = function () {
- 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], {});
- }
- };
- NavigationController.prototype.goForward = function () {
- 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], {});
- }
- };
- NavigationController.prototype.goToIndex = function (index) {
- if (!this.canGoToIndex(index)) {
- return;
- }
- this.pendingIndex = index;
- return this.webContents._loadURL(this.history[this.pendingIndex], {});
- };
- NavigationController.prototype.goToOffset = function (offset) {
- 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);
- }
- };
- NavigationController.prototype.getActiveIndex = function () {
- if (this.pendingIndex === -1) {
- return this.currentIndex;
- } else {
- return this.pendingIndex;
- }
- };
- NavigationController.prototype.length = function () {
- return this.history.length;
- };
- return NavigationController;
- })();
- module.exports = NavigationController;
|