web-contents.js 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  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. duplex: 0,
  71. copies: 1,
  72. collate: true,
  73. shouldPrintBackgrounds: false,
  74. shouldPrintSelectionOnly: false
  75. }
  76. // JavaScript implementations of WebContents.
  77. const binding = process.atomBinding('web_contents')
  78. const {WebContents} = binding
  79. Object.setPrototypeOf(NavigationController.prototype, EventEmitter.prototype)
  80. Object.setPrototypeOf(WebContents.prototype, NavigationController.prototype)
  81. // WebContents::send(channel, args..)
  82. // WebContents::sendToAll(channel, args..)
  83. WebContents.prototype.send = function (channel, ...args) {
  84. if (channel == null) throw new Error('Missing required channel argument')
  85. return this._send(false, channel, args)
  86. }
  87. WebContents.prototype.sendToAll = function (channel, ...args) {
  88. if (channel == null) throw new Error('Missing required channel argument')
  89. return this._send(true, channel, args)
  90. }
  91. // Following methods are mapped to webFrame.
  92. const webFrameMethods = [
  93. 'insertCSS',
  94. 'insertText',
  95. 'setLayoutZoomLevelLimits',
  96. 'setVisualZoomLevelLimits',
  97. 'setZoomFactor',
  98. 'setZoomLevel',
  99. // TODO(kevinsawicki): Remove in 2.0, deprecate before then with warnings
  100. 'setZoomLevelLimits'
  101. ]
  102. const webFrameMethodsWithResult = [
  103. 'getZoomFactor',
  104. 'getZoomLevel'
  105. ]
  106. const asyncWebFrameMethods = function (requestId, method, callback, ...args) {
  107. return new Promise((resolve, reject) => {
  108. this.send('ELECTRON_INTERNAL_RENDERER_ASYNC_WEB_FRAME_METHOD', requestId, method, args)
  109. ipcMain.once(`ELECTRON_INTERNAL_BROWSER_ASYNC_WEB_FRAME_RESPONSE_${requestId}`, function (event, error, result) {
  110. if (error == null) {
  111. if (callback != null) callback(result)
  112. resolve(result)
  113. } else {
  114. reject(error)
  115. }
  116. })
  117. })
  118. }
  119. const syncWebFrameMethods = function (requestId, method, callback, ...args) {
  120. this.send('ELECTRON_INTERNAL_RENDERER_SYNC_WEB_FRAME_METHOD', requestId, method, args)
  121. ipcMain.once(`ELECTRON_INTERNAL_BROWSER_SYNC_WEB_FRAME_RESPONSE_${requestId}`, function (event, result) {
  122. if (callback) callback(result)
  123. })
  124. }
  125. for (const method of webFrameMethods) {
  126. WebContents.prototype[method] = function (...args) {
  127. this.send('ELECTRON_INTERNAL_RENDERER_WEB_FRAME_METHOD', method, args)
  128. }
  129. }
  130. for (const method of webFrameMethodsWithResult) {
  131. WebContents.prototype[method] = function (...args) {
  132. const callback = args[args.length - 1]
  133. const actualArgs = args.slice(0, args.length - 2)
  134. syncWebFrameMethods.call(this, getNextId(), method, callback, ...actualArgs)
  135. }
  136. }
  137. // Make sure WebContents::executeJavaScript would run the code only when the
  138. // WebContents has been loaded.
  139. WebContents.prototype.executeJavaScript = function (code, hasUserGesture, callback) {
  140. const requestId = getNextId()
  141. if (typeof hasUserGesture === 'function') {
  142. callback = hasUserGesture
  143. hasUserGesture = false
  144. }
  145. if (this.getURL() && !this.isLoadingMainFrame()) {
  146. return asyncWebFrameMethods.call(this, requestId, 'executeJavaScript', callback, code, hasUserGesture)
  147. } else {
  148. return new Promise((resolve, reject) => {
  149. this.once('did-finish-load', () => {
  150. asyncWebFrameMethods.call(this, requestId, 'executeJavaScript', callback, code, hasUserGesture).then(resolve).catch(reject)
  151. })
  152. })
  153. }
  154. }
  155. // Translate the options of printToPDF.
  156. WebContents.prototype.printToPDF = function (options, callback) {
  157. const printingSetting = Object.assign({}, defaultPrintingSetting)
  158. if (options.landscape) {
  159. printingSetting.landscape = options.landscape
  160. }
  161. if (options.marginsType) {
  162. printingSetting.marginsType = options.marginsType
  163. }
  164. if (options.printSelectionOnly) {
  165. printingSetting.shouldPrintSelectionOnly = options.printSelectionOnly
  166. }
  167. if (options.printBackground) {
  168. printingSetting.shouldPrintBackgrounds = options.printBackground
  169. }
  170. if (options.pageSize) {
  171. const pageSize = options.pageSize
  172. if (typeof pageSize === 'object') {
  173. if (!pageSize.height || !pageSize.width) {
  174. return callback(new Error('Must define height and width for pageSize'))
  175. }
  176. // Dimensions in Microns
  177. // 1 meter = 10^6 microns
  178. printingSetting.mediaSize = {
  179. name: 'CUSTOM',
  180. custom_display_name: 'Custom',
  181. height_microns: pageSize.height,
  182. width_microns: pageSize.width
  183. }
  184. } else if (PDFPageSizes[pageSize]) {
  185. printingSetting.mediaSize = PDFPageSizes[pageSize]
  186. } else {
  187. return callback(new Error(`Does not support pageSize with ${pageSize}`))
  188. }
  189. } else {
  190. printingSetting.mediaSize = PDFPageSizes['A4']
  191. }
  192. this._printToPDF(printingSetting, callback)
  193. }
  194. // Add JavaScript wrappers for WebContents class.
  195. WebContents.prototype._init = function () {
  196. // The navigation controller.
  197. NavigationController.call(this, this)
  198. // Every remote callback from renderer process would add a listenter to the
  199. // render-view-deleted event, so ignore the listenters warning.
  200. this.setMaxListeners(0)
  201. // Dispatch IPC messages to the ipc module.
  202. this.on('ipc-message', function (event, [channel, ...args]) {
  203. ipcMain.emit(channel, event, ...args)
  204. })
  205. this.on('ipc-message-sync', function (event, [channel, ...args]) {
  206. Object.defineProperty(event, 'returnValue', {
  207. set: function (value) {
  208. return event.sendReply(JSON.stringify(value))
  209. },
  210. get: function () {}
  211. })
  212. ipcMain.emit(channel, event, ...args)
  213. })
  214. // Handle context menu action request from pepper plugin.
  215. this.on('pepper-context-menu', function (event, params) {
  216. // Access Menu via electron.Menu to prevent circular require
  217. const menu = electron.Menu.buildFromTemplate(params.menu)
  218. menu.popup(params.x, params.y)
  219. })
  220. // The devtools requests the webContents to reload.
  221. this.on('devtools-reload-page', function () {
  222. this.reload()
  223. })
  224. // Delays the page-title-updated event to next tick.
  225. this.on('-page-title-updated', function (...args) {
  226. setImmediate(() => {
  227. this.emit('page-title-updated', ...args)
  228. })
  229. })
  230. app.emit('web-contents-created', {}, this)
  231. }
  232. // JavaScript wrapper of Debugger.
  233. const {Debugger} = process.atomBinding('debugger')
  234. Object.setPrototypeOf(Debugger.prototype, EventEmitter.prototype)
  235. // Public APIs.
  236. module.exports = {
  237. create (options = {}) {
  238. return binding.create(options)
  239. },
  240. fromId (id) {
  241. return binding.fromId(id)
  242. },
  243. getFocusedWebContents () {
  244. let focused = null
  245. for (let contents of binding.getAllWebContents()) {
  246. if (!contents.isFocused()) continue
  247. if (focused == null) focused = contents
  248. // Return webview web contents which may be embedded inside another
  249. // web contents that is also reporting as focused
  250. if (contents.getType() === 'webview') return contents
  251. }
  252. return focused
  253. },
  254. getAllWebContents () {
  255. return binding.getAllWebContents()
  256. }
  257. }