web-view.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  1. 'use strict'
  2. const { webFrame } = require('electron')
  3. const v8Util = process.atomBinding('v8_util')
  4. const ipcRenderer = require('@electron/internal/renderer/ipc-renderer-internal')
  5. const guestViewInternal = require('@electron/internal/renderer/web-view/guest-view-internal')
  6. const webViewConstants = require('@electron/internal/renderer/web-view/web-view-constants')
  7. const errorUtils = require('@electron/internal/common/error-utils')
  8. const {
  9. syncMethods,
  10. asyncCallbackMethods,
  11. asyncPromiseMethods
  12. } = require('@electron/internal/common/web-view-methods')
  13. // ID generator.
  14. let nextId = 0
  15. const getNextId = function () {
  16. return ++nextId
  17. }
  18. // Represents the internal state of the WebView node.
  19. class WebViewImpl {
  20. constructor (webviewNode) {
  21. this.webviewNode = webviewNode
  22. v8Util.setHiddenValue(this.webviewNode, 'internal', this)
  23. this.elementAttached = false
  24. this.beforeFirstNavigation = true
  25. this.hasFocus = false
  26. // on* Event handlers.
  27. this.on = {}
  28. // Create internal iframe element.
  29. this.internalElement = this.createInternalElement()
  30. const shadowRoot = this.webviewNode.attachShadow({ mode: 'open' })
  31. shadowRoot.innerHTML = '<!DOCTYPE html><style type="text/css">:host { display: flex; }</style>'
  32. this.setupWebViewAttributes()
  33. this.viewInstanceId = getNextId()
  34. shadowRoot.appendChild(this.internalElement)
  35. // Provide access to contentWindow.
  36. Object.defineProperty(this.webviewNode, 'contentWindow', {
  37. get: () => {
  38. return this.internalElement.contentWindow
  39. },
  40. enumerable: true
  41. })
  42. }
  43. createInternalElement () {
  44. const iframeElement = document.createElement('iframe')
  45. iframeElement.style.flex = '1 1 auto'
  46. iframeElement.style.width = '100%'
  47. iframeElement.style.border = '0'
  48. v8Util.setHiddenValue(iframeElement, 'internal', this)
  49. return iframeElement
  50. }
  51. // Resets some state upon reattaching <webview> element to the DOM.
  52. reset () {
  53. // If guestInstanceId is defined then the <webview> has navigated and has
  54. // already picked up a partition ID. Thus, we need to reset the initialization
  55. // state. However, it may be the case that beforeFirstNavigation is false BUT
  56. // guestInstanceId has yet to be initialized. This means that we have not
  57. // heard back from createGuest yet. We will not reset the flag in this case so
  58. // that we don't end up allocating a second guest.
  59. if (this.guestInstanceId) {
  60. guestViewInternal.destroyGuest(this.guestInstanceId)
  61. this.guestInstanceId = void 0
  62. }
  63. this.beforeFirstNavigation = true
  64. this.attributes[webViewConstants.ATTRIBUTE_PARTITION].validPartitionId = true
  65. // Since attachment swaps a local frame for a remote frame, we need our
  66. // internal iframe element to be local again before we can reattach.
  67. const newFrame = this.createInternalElement()
  68. const oldFrame = this.internalElement
  69. this.internalElement = newFrame
  70. oldFrame.parentNode.replaceChild(newFrame, oldFrame)
  71. }
  72. // Sets the <webview>.request property.
  73. setRequestPropertyOnWebViewNode (request) {
  74. Object.defineProperty(this.webviewNode, 'request', {
  75. value: request,
  76. enumerable: true
  77. })
  78. }
  79. // This observer monitors mutations to attributes of the <webview> and
  80. // updates the BrowserPlugin properties accordingly. In turn, updating
  81. // a BrowserPlugin property will update the corresponding BrowserPlugin
  82. // attribute, if necessary. See BrowserPlugin::UpdateDOMAttribute for more
  83. // details.
  84. handleWebviewAttributeMutation (attributeName, oldValue, newValue) {
  85. if (!this.attributes[attributeName] || this.attributes[attributeName].ignoreMutation) {
  86. return
  87. }
  88. // Let the changed attribute handle its own mutation
  89. this.attributes[attributeName].handleMutation(oldValue, newValue)
  90. }
  91. onElementResize () {
  92. const resizeEvent = new Event('resize')
  93. resizeEvent.newWidth = this.webviewNode.clientWidth
  94. resizeEvent.newHeight = this.webviewNode.clientHeight
  95. this.dispatchEvent(resizeEvent)
  96. }
  97. createGuest () {
  98. return guestViewInternal.createGuest(this.buildParams(), (event, guestInstanceId) => {
  99. this.attachGuestInstance(guestInstanceId)
  100. })
  101. }
  102. createGuestSync () {
  103. this.beforeFirstNavigation = false
  104. this.attachGuestInstance(guestViewInternal.createGuestSync(this.buildParams()))
  105. }
  106. dispatchEvent (webViewEvent) {
  107. this.webviewNode.dispatchEvent(webViewEvent)
  108. }
  109. // Adds an 'on<event>' property on the webview, which can be used to set/unset
  110. // an event handler.
  111. setupEventProperty (eventName) {
  112. const propertyName = `on${eventName.toLowerCase()}`
  113. return Object.defineProperty(this.webviewNode, propertyName, {
  114. get: () => {
  115. return this.on[propertyName]
  116. },
  117. set: (value) => {
  118. if (this.on[propertyName]) {
  119. this.webviewNode.removeEventListener(eventName, this.on[propertyName])
  120. }
  121. this.on[propertyName] = value
  122. if (value) {
  123. return this.webviewNode.addEventListener(eventName, value)
  124. }
  125. },
  126. enumerable: true
  127. })
  128. }
  129. // Updates state upon loadcommit.
  130. onLoadCommit (webViewEvent) {
  131. const oldValue = this.webviewNode.getAttribute(webViewConstants.ATTRIBUTE_SRC)
  132. const newValue = webViewEvent.url
  133. if (webViewEvent.isMainFrame && (oldValue !== newValue)) {
  134. // Touching the src attribute triggers a navigation. To avoid
  135. // triggering a page reload on every guest-initiated navigation,
  136. // we do not handle this mutation.
  137. this.attributes[webViewConstants.ATTRIBUTE_SRC].setValueIgnoreMutation(newValue)
  138. }
  139. }
  140. // Emits focus/blur events.
  141. onFocusChange () {
  142. const hasFocus = document.activeElement === this.webviewNode
  143. if (hasFocus !== this.hasFocus) {
  144. this.hasFocus = hasFocus
  145. this.dispatchEvent(new Event(hasFocus ? 'focus' : 'blur'))
  146. }
  147. }
  148. onAttach (storagePartitionId) {
  149. return this.attributes[webViewConstants.ATTRIBUTE_PARTITION].setValue(storagePartitionId)
  150. }
  151. buildParams () {
  152. const params = {
  153. instanceId: this.viewInstanceId,
  154. userAgentOverride: this.userAgentOverride
  155. }
  156. for (const attributeName in this.attributes) {
  157. if (this.attributes.hasOwnProperty(attributeName)) {
  158. params[attributeName] = this.attributes[attributeName].getValue()
  159. }
  160. }
  161. return params
  162. }
  163. attachGuestInstance (guestInstanceId) {
  164. if (!this.elementAttached) {
  165. // The element could be detached before we got response from browser.
  166. return
  167. }
  168. this.internalInstanceId = getNextId()
  169. this.guestInstanceId = guestInstanceId
  170. guestViewInternal.attachGuest(this.internalInstanceId, this.guestInstanceId, this.buildParams(), this.internalElement.contentWindow)
  171. // ResizeObserver is a browser global not recognized by "standard".
  172. /* globals ResizeObserver */
  173. // TODO(zcbenz): Should we deprecate the "resize" event? Wait, it is not
  174. // even documented.
  175. this.resizeObserver = new ResizeObserver(this.onElementResize.bind(this)).observe(this.internalElement)
  176. }
  177. }
  178. // Registers <webview> custom element.
  179. const registerWebViewElement = (window) => {
  180. const proto = Object.create(window.HTMLObjectElement.prototype)
  181. proto.createdCallback = function () {
  182. return new WebViewImpl(this)
  183. }
  184. proto.attributeChangedCallback = function (name, oldValue, newValue) {
  185. const internal = v8Util.getHiddenValue(this, 'internal')
  186. if (internal) {
  187. internal.handleWebviewAttributeMutation(name, oldValue, newValue)
  188. }
  189. }
  190. proto.detachedCallback = function () {
  191. const internal = v8Util.getHiddenValue(this, 'internal')
  192. if (!internal) {
  193. return
  194. }
  195. guestViewInternal.deregisterEvents(internal.viewInstanceId)
  196. internal.elementAttached = false
  197. this.internalInstanceId = 0
  198. internal.reset()
  199. }
  200. proto.attachedCallback = function () {
  201. const internal = v8Util.getHiddenValue(this, 'internal')
  202. if (!internal) {
  203. return
  204. }
  205. if (!internal.elementAttached) {
  206. guestViewInternal.registerEvents(internal, internal.viewInstanceId)
  207. internal.elementAttached = true
  208. internal.attributes[webViewConstants.ATTRIBUTE_SRC].parse()
  209. }
  210. }
  211. const getGuestInstanceId = function (self) {
  212. const internal = v8Util.getHiddenValue(self, 'internal')
  213. if (!internal.guestInstanceId) {
  214. throw new Error('The WebView must be attached to the DOM and the dom-ready event emitted before this method can be called.')
  215. }
  216. return internal.guestInstanceId
  217. }
  218. // Forward proto.foo* method calls to WebViewImpl.foo*.
  219. const createBlockHandler = function (method) {
  220. return function (...args) {
  221. const [error, result] = ipcRenderer.sendSync('ELECTRON_GUEST_VIEW_MANAGER_SYNC_CALL', getGuestInstanceId(this), method, args)
  222. if (error) {
  223. throw errorUtils.deserialize(error)
  224. } else {
  225. return result
  226. }
  227. }
  228. }
  229. for (const method of syncMethods) {
  230. proto[method] = createBlockHandler(method)
  231. }
  232. const createNonBlockHandler = function (method) {
  233. return function (...args) {
  234. const callback = (typeof args[args.length - 1] === 'function') ? args.pop() : null
  235. const requestId = getNextId()
  236. ipcRenderer.once(`ELECTRON_GUEST_VIEW_MANAGER_ASYNC_CALL_RESPONSE_${requestId}`, function (event, error, result) {
  237. if (error == null) {
  238. if (callback) callback(result)
  239. } else {
  240. throw errorUtils.deserialize(error)
  241. }
  242. })
  243. ipcRenderer.send('ELECTRON_GUEST_VIEW_MANAGER_ASYNC_CALL', requestId, getGuestInstanceId(this), method, args, callback != null)
  244. }
  245. }
  246. for (const method of asyncCallbackMethods) {
  247. proto[method] = createNonBlockHandler(method)
  248. }
  249. const createPromiseHandler = function (method) {
  250. return function (...args) {
  251. return new Promise((resolve, reject) => {
  252. const callback = (typeof args[args.length - 1] === 'function') ? args.pop() : null
  253. const requestId = getNextId()
  254. ipcRenderer.once(`ELECTRON_GUEST_VIEW_MANAGER_ASYNC_CALL_RESPONSE_${requestId}`, function (event, error, result) {
  255. if (error == null) {
  256. if (callback) {
  257. callback(result)
  258. }
  259. resolve(result)
  260. } else {
  261. reject(errorUtils.deserialize(error))
  262. }
  263. })
  264. ipcRenderer.send('ELECTRON_GUEST_VIEW_MANAGER_ASYNC_CALL', requestId, getGuestInstanceId(this), method, args, callback != null)
  265. })
  266. }
  267. }
  268. for (const method of asyncPromiseMethods) {
  269. proto[method] = createPromiseHandler(method)
  270. }
  271. // WebContents associated with this webview.
  272. proto.getWebContents = function () {
  273. const { getRemoteForUsage } = require('@electron/internal/renderer/remote')
  274. const remote = getRemoteForUsage('getWebContents()')
  275. const internal = v8Util.getHiddenValue(this, 'internal')
  276. if (!internal.guestInstanceId) {
  277. internal.createGuestSync()
  278. }
  279. return remote.getGuestWebContents(internal.guestInstanceId)
  280. }
  281. // Focusing the webview should move page focus to the underlying iframe.
  282. proto.focus = function () {
  283. this.contentWindow.focus()
  284. }
  285. window.WebView = webFrame.registerEmbedderCustomElement(window, 'webview', {
  286. prototype: proto
  287. })
  288. // Delete the callbacks so developers cannot call them and produce unexpected
  289. // behavior.
  290. delete proto.createdCallback
  291. delete proto.attachedCallback
  292. delete proto.detachedCallback
  293. delete proto.attributeChangedCallback
  294. }
  295. const setupWebView = (window) => {
  296. require('@electron/internal/renderer/web-view/web-view-attributes')
  297. const useCapture = true
  298. window.addEventListener('readystatechange', function listener (event) {
  299. if (document.readyState === 'loading') {
  300. return
  301. }
  302. registerWebViewElement(window)
  303. window.removeEventListener(event.type, listener, useCapture)
  304. }, useCapture)
  305. }
  306. module.exports = { setupWebView, WebViewImpl }