web-contents.js 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. 'use strict'
  2. const {EventEmitter} = require('events')
  3. const electron = require('electron')
  4. const {app, ipcMain, session, NavigationController} = electron
  5. // session is not used here, the purpose is to make sure session is initalized
  6. // before the webContents module.
  7. session
  8. let nextId = 0
  9. const getNextId = function () {
  10. return ++nextId
  11. }
  12. // Stock page sizes
  13. const PDFPageSizes = {
  14. A5: {
  15. custom_display_name: 'A5',
  16. height_microns: 210000,
  17. name: 'ISO_A5',
  18. width_microns: 148000
  19. },
  20. A4: {
  21. custom_display_name: 'A4',
  22. height_microns: 297000,
  23. name: 'ISO_A4',
  24. is_default: 'true',
  25. width_microns: 210000
  26. },
  27. A3: {
  28. custom_display_name: 'A3',
  29. height_microns: 420000,
  30. name: 'ISO_A3',
  31. width_microns: 297000
  32. },
  33. Legal: {
  34. custom_display_name: 'Legal',
  35. height_microns: 355600,
  36. name: 'NA_LEGAL',
  37. width_microns: 215900
  38. },
  39. Letter: {
  40. custom_display_name: 'Letter',
  41. height_microns: 279400,
  42. name: 'NA_LETTER',
  43. width_microns: 215900
  44. },
  45. Tabloid: {
  46. height_microns: 431800,
  47. name: 'NA_LEDGER',
  48. width_microns: 279400,
  49. custom_display_name: 'Tabloid'
  50. }
  51. }
  52. // Default printing setting
  53. const defaultPrintingSetting = {
  54. pageRage: [],
  55. mediaSize: {},
  56. landscape: false,
  57. color: 2,
  58. headerFooterEnabled: false,
  59. marginsType: 0,
  60. isFirstRequest: false,
  61. requestID: getNextId(),
  62. previewModifiable: true,
  63. printToPDF: true,
  64. printWithCloudPrint: false,
  65. printWithPrivet: false,
  66. printWithExtension: false,
  67. deviceName: 'Save as PDF',
  68. generateDraftData: true,
  69. fitToPageEnabled: false,
  70. scaleFactor: 1,
  71. rasterizePDF: false,
  72. duplex: 0,
  73. copies: 1,
  74. collate: true,
  75. shouldPrintBackgrounds: false,
  76. shouldPrintSelectionOnly: false
  77. }
  78. // JavaScript implementations of WebContents.
  79. const binding = process.atomBinding('web_contents')
  80. const {WebContents} = binding
  81. Object.setPrototypeOf(NavigationController.prototype, EventEmitter.prototype)
  82. Object.setPrototypeOf(WebContents.prototype, NavigationController.prototype)
  83. // WebContents::send(channel, args..)
  84. // WebContents::sendToAll(channel, args..)
  85. WebContents.prototype.send = function (channel, ...args) {
  86. if (channel == null) throw new Error('Missing required channel argument')
  87. return this._send(false, channel, args)
  88. }
  89. WebContents.prototype.sendToAll = function (channel, ...args) {
  90. if (channel == null) throw new Error('Missing required channel argument')
  91. return this._send(true, channel, args)
  92. }
  93. // Following methods are mapped to webFrame.
  94. const webFrameMethods = [
  95. 'insertCSS',
  96. 'insertText',
  97. 'setLayoutZoomLevelLimits',
  98. 'setVisualZoomLevelLimits',
  99. // TODO(kevinsawicki): Remove in 2.0, deprecate before then with warnings
  100. 'setZoomLevelLimits'
  101. ]
  102. const webFrameMethodsWithResult = []
  103. const asyncWebFrameMethods = function (requestId, method, callback, ...args) {
  104. return new Promise((resolve, reject) => {
  105. this.send('ELECTRON_INTERNAL_RENDERER_ASYNC_WEB_FRAME_METHOD', requestId, method, args)
  106. ipcMain.once(`ELECTRON_INTERNAL_BROWSER_ASYNC_WEB_FRAME_RESPONSE_${requestId}`, function (event, error, result) {
  107. if (error == null) {
  108. if (typeof callback === 'function') callback(result)
  109. resolve(result)
  110. } else {
  111. reject(error)
  112. }
  113. })
  114. })
  115. }
  116. const syncWebFrameMethods = function (requestId, method, callback, ...args) {
  117. this.send('ELECTRON_INTERNAL_RENDERER_SYNC_WEB_FRAME_METHOD', requestId, method, args)
  118. ipcMain.once(`ELECTRON_INTERNAL_BROWSER_SYNC_WEB_FRAME_RESPONSE_${requestId}`, function (event, result) {
  119. if (callback) callback(result)
  120. })
  121. }
  122. for (const method of webFrameMethods) {
  123. WebContents.prototype[method] = function (...args) {
  124. this.send('ELECTRON_INTERNAL_RENDERER_WEB_FRAME_METHOD', method, args)
  125. }
  126. }
  127. for (const method of webFrameMethodsWithResult) {
  128. WebContents.prototype[method] = function (...args) {
  129. const callback = args[args.length - 1]
  130. const actualArgs = args.slice(0, args.length - 2)
  131. syncWebFrameMethods.call(this, getNextId(), method, callback, ...actualArgs)
  132. }
  133. }
  134. // Make sure WebContents::executeJavaScript would run the code only when the
  135. // WebContents has been loaded.
  136. WebContents.prototype.executeJavaScript = function (code, hasUserGesture, callback) {
  137. const requestId = getNextId()
  138. if (typeof hasUserGesture === 'function') {
  139. // Shift.
  140. callback = hasUserGesture
  141. hasUserGesture = null
  142. }
  143. if (hasUserGesture == null) {
  144. hasUserGesture = false
  145. }
  146. if (this.getURL() && !this.isLoadingMainFrame()) {
  147. return asyncWebFrameMethods.call(this, requestId, 'executeJavaScript', callback, code, hasUserGesture)
  148. } else {
  149. return new Promise((resolve, reject) => {
  150. this.once('did-finish-load', () => {
  151. asyncWebFrameMethods.call(this, requestId, 'executeJavaScript', callback, code, hasUserGesture).then(resolve).catch(reject)
  152. })
  153. })
  154. }
  155. }
  156. // Translate the options of printToPDF.
  157. WebContents.prototype.printToPDF = function (options, callback) {
  158. const printingSetting = Object.assign({}, defaultPrintingSetting)
  159. if (options.landscape) {
  160. printingSetting.landscape = options.landscape
  161. }
  162. if (options.marginsType) {
  163. printingSetting.marginsType = options.marginsType
  164. }
  165. if (options.printSelectionOnly) {
  166. printingSetting.shouldPrintSelectionOnly = options.printSelectionOnly
  167. }
  168. if (options.printBackground) {
  169. printingSetting.shouldPrintBackgrounds = options.printBackground
  170. }
  171. if (options.pageSize) {
  172. const pageSize = options.pageSize
  173. if (typeof pageSize === 'object') {
  174. if (!pageSize.height || !pageSize.width) {
  175. return callback(new Error('Must define height and width for pageSize'))
  176. }
  177. // Dimensions in Microns
  178. // 1 meter = 10^6 microns
  179. printingSetting.mediaSize = {
  180. name: 'CUSTOM',
  181. custom_display_name: 'Custom',
  182. height_microns: pageSize.height,
  183. width_microns: pageSize.width
  184. }
  185. } else if (PDFPageSizes[pageSize]) {
  186. printingSetting.mediaSize = PDFPageSizes[pageSize]
  187. } else {
  188. return callback(new Error(`Does not support pageSize with ${pageSize}`))
  189. }
  190. } else {
  191. printingSetting.mediaSize = PDFPageSizes['A4']
  192. }
  193. this._printToPDF(printingSetting, callback)
  194. }
  195. WebContents.prototype.getZoomLevel = function (callback) {
  196. if (typeof callback !== 'function') {
  197. throw new Error('Must pass function as an argument')
  198. }
  199. process.nextTick(() => {
  200. const zoomLevel = this._getZoomLevel()
  201. callback(zoomLevel)
  202. })
  203. }
  204. WebContents.prototype.getZoomFactor = function (callback) {
  205. if (typeof callback !== 'function') {
  206. throw new Error('Must pass function as an argument')
  207. }
  208. process.nextTick(() => {
  209. const zoomFactor = this._getZoomFactor()
  210. callback(zoomFactor)
  211. })
  212. }
  213. // Add JavaScript wrappers for WebContents class.
  214. WebContents.prototype._init = function () {
  215. // The navigation controller.
  216. NavigationController.call(this, this)
  217. // Every remote callback from renderer process would add a listenter to the
  218. // render-view-deleted event, so ignore the listenters warning.
  219. this.setMaxListeners(0)
  220. // Dispatch IPC messages to the ipc module.
  221. this.on('ipc-message', function (event, [channel, ...args]) {
  222. ipcMain.emit(channel, event, ...args)
  223. })
  224. this.on('ipc-message-sync', function (event, [channel, ...args]) {
  225. Object.defineProperty(event, 'returnValue', {
  226. set: function (value) {
  227. return event.sendReply(JSON.stringify(value))
  228. },
  229. get: function () {}
  230. })
  231. ipcMain.emit(channel, event, ...args)
  232. })
  233. // Handle context menu action request from pepper plugin.
  234. this.on('pepper-context-menu', function (event, params) {
  235. // Access Menu via electron.Menu to prevent circular require
  236. const menu = electron.Menu.buildFromTemplate(params.menu)
  237. menu.popup(event.sender.getOwnerBrowserWindow(), params.x, params.y)
  238. })
  239. // The devtools requests the webContents to reload.
  240. this.on('devtools-reload-page', function () {
  241. this.reload()
  242. })
  243. app.emit('web-contents-created', {}, this)
  244. }
  245. // JavaScript wrapper of Debugger.
  246. const {Debugger} = process.atomBinding('debugger')
  247. Object.setPrototypeOf(Debugger.prototype, EventEmitter.prototype)
  248. // Public APIs.
  249. module.exports = {
  250. create (options = {}) {
  251. return binding.create(options)
  252. },
  253. fromId (id) {
  254. return binding.fromId(id)
  255. },
  256. getFocusedWebContents () {
  257. let focused = null
  258. for (let contents of binding.getAllWebContents()) {
  259. if (!contents.isFocused()) continue
  260. if (focused == null) focused = contents
  261. // Return webview web contents which may be embedded inside another
  262. // web contents that is also reporting as focused
  263. if (contents.getType() === 'webview') return contents
  264. }
  265. return focused
  266. },
  267. getAllWebContents () {
  268. return binding.getAllWebContents()
  269. }
  270. }