web-view.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400
  1. 'use strict'
  2. const {ipcRenderer, remote, webFrame} = require('electron')
  3. const v8Util = process.atomBinding('v8_util')
  4. const guestViewInternal = require('./guest-view-internal')
  5. const webViewConstants = require('./web-view-constants')
  6. // An unique ID that can represent current context.
  7. const contextId = v8Util.getHiddenValue(global, 'contextId')
  8. // ID generator.
  9. let nextId = 0
  10. const getNextId = function () {
  11. return ++nextId
  12. }
  13. // A list of removed attributes from 3.0.
  14. const removedAttributes = [
  15. 'autoresize',
  16. 'disableguestresize',
  17. 'guestinstance'
  18. ]
  19. // Represents the internal state of the WebView node.
  20. class WebViewImpl {
  21. constructor (webviewNode) {
  22. this.webviewNode = webviewNode
  23. v8Util.setHiddenValue(this.webviewNode, 'internal', this)
  24. this.elementAttached = false
  25. this.beforeFirstNavigation = true
  26. this.hasFocus = false
  27. // Check for removed attributes.
  28. for (const attributeName of removedAttributes) {
  29. if (this.webviewNode.hasAttribute(attributeName)) {
  30. this.reportRemovedAttribute(attributeName)
  31. }
  32. }
  33. // on* Event handlers.
  34. this.on = {}
  35. // Create internal iframe element.
  36. this.internalElement = this.createInternalElement()
  37. const shadowRoot = this.webviewNode.attachShadow({mode: 'open'})
  38. shadowRoot.innerHTML = '<!DOCTYPE html><style type="text/css">:host { display: flex; }</style>'
  39. this.setupWebViewAttributes()
  40. this.viewInstanceId = getNextId()
  41. shadowRoot.appendChild(this.internalElement)
  42. // Provide access to contentWindow.
  43. Object.defineProperty(this.webviewNode, 'contentWindow', {
  44. get: () => {
  45. return this.internalElement.contentWindow
  46. },
  47. enumerable: true
  48. })
  49. }
  50. createInternalElement () {
  51. const iframeElement = document.createElement('iframe')
  52. iframeElement.style.flex = '1 1 auto'
  53. iframeElement.style.width = '100%'
  54. iframeElement.style.border = '0'
  55. v8Util.setHiddenValue(iframeElement, 'internal', this)
  56. return iframeElement
  57. }
  58. // Resets some state upon reattaching <webview> element to the DOM.
  59. reset () {
  60. // If guestInstanceId is defined then the <webview> has navigated and has
  61. // already picked up a partition ID. Thus, we need to reset the initialization
  62. // state. However, it may be the case that beforeFirstNavigation is false BUT
  63. // guestInstanceId has yet to be initialized. This means that we have not
  64. // heard back from createGuest yet. We will not reset the flag in this case so
  65. // that we don't end up allocating a second guest.
  66. if (this.guestInstanceId) {
  67. guestViewInternal.destroyGuest(this.guestInstanceId)
  68. this.guestInstanceId = void 0
  69. }
  70. this.webContents = null
  71. this.beforeFirstNavigation = true
  72. this.attributes[webViewConstants.ATTRIBUTE_PARTITION].validPartitionId = true
  73. // Since attachment swaps a local frame for a remote frame, we need our
  74. // internal iframe element to be local again before we can reattach.
  75. const newFrame = this.createInternalElement()
  76. const oldFrame = this.internalElement
  77. this.internalElement = newFrame
  78. oldFrame.parentNode.replaceChild(newFrame, oldFrame)
  79. }
  80. // Sets the <webview>.request property.
  81. setRequestPropertyOnWebViewNode (request) {
  82. Object.defineProperty(this.webviewNode, 'request', {
  83. value: request,
  84. enumerable: true
  85. })
  86. }
  87. // This observer monitors mutations to attributes of the <webview> and
  88. // updates the BrowserPlugin properties accordingly. In turn, updating
  89. // a BrowserPlugin property will update the corresponding BrowserPlugin
  90. // attribute, if necessary. See BrowserPlugin::UpdateDOMAttribute for more
  91. // details.
  92. handleWebviewAttributeMutation (attributeName, oldValue, newValue) {
  93. if (removedAttributes.includes(attributeName)) {
  94. this.reportRemovedAttribute(attributeName)
  95. return
  96. }
  97. if (!this.attributes[attributeName] || this.attributes[attributeName].ignoreMutation) {
  98. return
  99. }
  100. // Let the changed attribute handle its own mutation
  101. this.attributes[attributeName].handleMutation(oldValue, newValue)
  102. }
  103. onElementResize () {
  104. const resizeEvent = new Event('resize', { bubbles: true })
  105. resizeEvent.newWidth = this.webviewNode.clientWidth
  106. resizeEvent.newHeight = this.webviewNode.clientHeight
  107. this.dispatchEvent(resizeEvent)
  108. }
  109. createGuest () {
  110. return guestViewInternal.createGuest(this.buildParams(), (event, guestInstanceId) => {
  111. this.attachGuestInstance(guestInstanceId)
  112. })
  113. }
  114. createGuestSync () {
  115. this.beforeFirstNavigation = false
  116. this.attachGuestInstance(guestViewInternal.createGuestSync(this.buildParams()))
  117. }
  118. dispatchEvent (webViewEvent) {
  119. this.webviewNode.dispatchEvent(webViewEvent)
  120. }
  121. // Adds an 'on<event>' property on the webview, which can be used to set/unset
  122. // an event handler.
  123. setupEventProperty (eventName) {
  124. const propertyName = `on${eventName.toLowerCase()}`
  125. return Object.defineProperty(this.webviewNode, propertyName, {
  126. get: () => {
  127. return this.on[propertyName]
  128. },
  129. set: (value) => {
  130. if (this.on[propertyName]) {
  131. this.webviewNode.removeEventListener(eventName, this.on[propertyName])
  132. }
  133. this.on[propertyName] = value
  134. if (value) {
  135. return this.webviewNode.addEventListener(eventName, value)
  136. }
  137. },
  138. enumerable: true
  139. })
  140. }
  141. // Updates state upon loadcommit.
  142. onLoadCommit (webViewEvent) {
  143. const oldValue = this.webviewNode.getAttribute(webViewConstants.ATTRIBUTE_SRC)
  144. const newValue = webViewEvent.url
  145. if (webViewEvent.isMainFrame && (oldValue !== newValue)) {
  146. // Touching the src attribute triggers a navigation. To avoid
  147. // triggering a page reload on every guest-initiated navigation,
  148. // we do not handle this mutation.
  149. this.attributes[webViewConstants.ATTRIBUTE_SRC].setValueIgnoreMutation(newValue)
  150. }
  151. }
  152. // Emits focus/blur events.
  153. onFocusChange () {
  154. const hasFocus = document.activeElement === this.webviewNode
  155. if (hasFocus !== this.hasFocus) {
  156. this.hasFocus = hasFocus
  157. this.dispatchEvent(new Event(hasFocus ? 'focus' : 'blur'))
  158. }
  159. }
  160. onAttach (storagePartitionId) {
  161. return this.attributes[webViewConstants.ATTRIBUTE_PARTITION].setValue(storagePartitionId)
  162. }
  163. buildParams () {
  164. const params = {
  165. instanceId: this.viewInstanceId,
  166. userAgentOverride: this.userAgentOverride
  167. }
  168. for (const attributeName in this.attributes) {
  169. if (this.attributes.hasOwnProperty(attributeName)) {
  170. params[attributeName] = this.attributes[attributeName].getValue()
  171. }
  172. }
  173. return params
  174. }
  175. attachGuestInstance (guestInstanceId) {
  176. if (!this.elementAttached) {
  177. // The element could be detached before we got response from browser.
  178. return
  179. }
  180. this.internalInstanceId = getNextId()
  181. this.guestInstanceId = guestInstanceId
  182. this.webContents = remote.getGuestWebContents(this.guestInstanceId)
  183. guestViewInternal.attachGuest(this.internalInstanceId, this.guestInstanceId, this.buildParams(), this.internalElement.contentWindow)
  184. // ResizeObserver is a browser global not recognized by "standard".
  185. /* globals ResizeObserver */
  186. // TODO(zcbenz): Should we deprecate the "resize" event? Wait, it is not
  187. // even documented.
  188. this.resizeObserver = new ResizeObserver(this.onElementResize.bind(this)).observe(this.internalElement)
  189. }
  190. // TODO(zcbenz): Remove the warning in 4.0.
  191. reportRemovedAttribute (attributeName) {
  192. console.error(`The "${attributeName}" attribute has been removed from the <webview> tag,`,
  193. 'see https://github.com/electron/electron/issues/14120 for more.')
  194. }
  195. }
  196. // Registers <webview> custom element.
  197. const registerWebViewElement = function () {
  198. const proto = Object.create(HTMLObjectElement.prototype)
  199. proto.createdCallback = function () {
  200. return new WebViewImpl(this)
  201. }
  202. proto.attributeChangedCallback = function (name, oldValue, newValue) {
  203. const internal = v8Util.getHiddenValue(this, 'internal')
  204. if (internal) {
  205. internal.handleWebviewAttributeMutation(name, oldValue, newValue)
  206. }
  207. }
  208. proto.detachedCallback = function () {
  209. const internal = v8Util.getHiddenValue(this, 'internal')
  210. if (!internal) {
  211. return
  212. }
  213. guestViewInternal.deregisterEvents(internal.viewInstanceId)
  214. internal.elementAttached = false
  215. this.internalInstanceId = 0
  216. internal.reset()
  217. }
  218. proto.attachedCallback = function () {
  219. const internal = v8Util.getHiddenValue(this, 'internal')
  220. if (!internal) {
  221. return
  222. }
  223. if (!internal.elementAttached) {
  224. guestViewInternal.registerEvents(internal, internal.viewInstanceId)
  225. internal.elementAttached = true
  226. internal.attributes[webViewConstants.ATTRIBUTE_SRC].parse()
  227. }
  228. }
  229. // Public-facing API methods.
  230. const methods = [
  231. 'getURL',
  232. 'loadURL',
  233. 'getTitle',
  234. 'isLoading',
  235. 'isLoadingMainFrame',
  236. 'isWaitingForResponse',
  237. 'stop',
  238. 'reload',
  239. 'reloadIgnoringCache',
  240. 'canGoBack',
  241. 'canGoForward',
  242. 'canGoToOffset',
  243. 'clearHistory',
  244. 'goBack',
  245. 'goForward',
  246. 'goToIndex',
  247. 'goToOffset',
  248. 'isCrashed',
  249. 'setUserAgent',
  250. 'getUserAgent',
  251. 'openDevTools',
  252. 'closeDevTools',
  253. 'isDevToolsOpened',
  254. 'isDevToolsFocused',
  255. 'inspectElement',
  256. 'setAudioMuted',
  257. 'isAudioMuted',
  258. 'undo',
  259. 'redo',
  260. 'cut',
  261. 'copy',
  262. 'paste',
  263. 'pasteAndMatchStyle',
  264. 'delete',
  265. 'selectAll',
  266. 'unselect',
  267. 'replace',
  268. 'replaceMisspelling',
  269. 'findInPage',
  270. 'stopFindInPage',
  271. 'getId',
  272. 'downloadURL',
  273. 'inspectServiceWorker',
  274. 'print',
  275. 'printToPDF',
  276. 'showDefinitionForSelection',
  277. 'capturePage',
  278. 'setZoomFactor',
  279. 'setZoomLevel',
  280. 'getZoomLevel',
  281. 'getZoomFactor'
  282. ]
  283. const nonblockMethods = [
  284. 'insertCSS',
  285. 'insertText',
  286. 'send',
  287. 'sendInputEvent',
  288. 'setLayoutZoomLevelLimits',
  289. 'setVisualZoomLevelLimits'
  290. ]
  291. // Forward proto.foo* method calls to WebViewImpl.foo*.
  292. const createBlockHandler = function (m) {
  293. return function (...args) {
  294. const internal = v8Util.getHiddenValue(this, 'internal')
  295. if (internal.webContents) {
  296. return internal.webContents[m](...args)
  297. } else {
  298. throw new Error(`Cannot call ${m} because the webContents is unavailable. The WebView must be attached to the DOM and the dom-ready event emitted before this method can be called.`)
  299. }
  300. }
  301. }
  302. for (const method of methods) {
  303. proto[method] = createBlockHandler(method)
  304. }
  305. const createNonBlockHandler = function (m) {
  306. return function (...args) {
  307. const internal = v8Util.getHiddenValue(this, 'internal')
  308. ipcRenderer.send('ELECTRON_BROWSER_ASYNC_CALL_TO_GUEST_VIEW', contextId, null, internal.guestInstanceId, m, ...args)
  309. }
  310. }
  311. for (const method of nonblockMethods) {
  312. proto[method] = createNonBlockHandler(method)
  313. }
  314. proto.executeJavaScript = function (code, hasUserGesture, callback) {
  315. const internal = v8Util.getHiddenValue(this, 'internal')
  316. if (typeof hasUserGesture === 'function') {
  317. callback = hasUserGesture
  318. hasUserGesture = false
  319. }
  320. const requestId = getNextId()
  321. ipcRenderer.send('ELECTRON_BROWSER_ASYNC_CALL_TO_GUEST_VIEW', contextId, requestId, internal.guestInstanceId, 'executeJavaScript', code, hasUserGesture)
  322. ipcRenderer.once(`ELECTRON_RENDERER_ASYNC_CALL_TO_GUEST_VIEW_RESPONSE_${requestId}`, function (event, result) {
  323. if (callback) callback(result)
  324. })
  325. }
  326. // WebContents associated with this webview.
  327. proto.getWebContents = function () {
  328. const internal = v8Util.getHiddenValue(this, 'internal')
  329. if (!internal.webContents) {
  330. internal.createGuestSync()
  331. }
  332. return internal.webContents
  333. }
  334. // Focusing the webview should move page focus to the underlying iframe.
  335. proto.focus = function () {
  336. this.contentWindow.focus()
  337. }
  338. window.WebView = webFrame.registerEmbedderCustomElement('webview', {
  339. prototype: proto
  340. })
  341. // Delete the callbacks so developers cannot call them and produce unexpected
  342. // behavior.
  343. delete proto.createdCallback
  344. delete proto.attachedCallback
  345. delete proto.detachedCallback
  346. delete proto.attributeChangedCallback
  347. }
  348. const useCapture = true
  349. const listener = function (event) {
  350. if (document.readyState === 'loading') {
  351. return
  352. }
  353. registerWebViewElement()
  354. window.removeEventListener(event.type, listener, useCapture)
  355. }
  356. window.addEventListener('readystatechange', listener, true)
  357. module.exports = WebViewImpl