236 lines
		
	
	
	
		
			8 KiB
			
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			236 lines
		
	
	
	
		
			8 KiB
			
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
'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-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) {
 | 
						|
            // the webcontents has started another unrelated navigation in the
 | 
						|
            // main frame (probably from the app calling `loadURL` again); reject
 | 
						|
            // the promise
 | 
						|
            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
 |