242 lines
8.5 KiB
JavaScript
242 lines
8.5 KiB
JavaScript
'use strict'
|
|
|
|
const ipcMainUtils = require('@electron/internal/browser/ipc-main-internal-utils')
|
|
|
|
// The history operation in renderer is redirected to browser.
|
|
ipcMainUtils.handle('ELECTRON_NAVIGATION_CONTROLLER_GO_BACK', function (event) {
|
|
return event.sender.goBack()
|
|
})
|
|
|
|
ipcMainUtils.handle('ELECTRON_NAVIGATION_CONTROLLER_GO_FORWARD', function (event) {
|
|
return event.sender.goForward()
|
|
})
|
|
|
|
ipcMainUtils.handle('ELECTRON_NAVIGATION_CONTROLLER_GO_TO_OFFSET', function (event, offset) {
|
|
return event.sender.goToOffset(offset)
|
|
})
|
|
|
|
ipcMainUtils.handle('ELECTRON_NAVIGATION_CONTROLLER_LENGTH', function (event) {
|
|
return 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-commited', (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'
|
|
})
|
|
}
|
|
|
|
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
|