web-view.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455
  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. const hasProp = {}.hasOwnProperty
  7. // ID generator.
  8. let nextId = 0
  9. const getNextId = function () {
  10. return ++nextId
  11. }
  12. // Represents the internal state of the WebView node.
  13. class WebViewImpl {
  14. constructor (webviewNode) {
  15. this.webviewNode = webviewNode
  16. v8Util.setHiddenValue(this.webviewNode, 'internal', this)
  17. this.attached = false
  18. this.elementAttached = false
  19. this.beforeFirstNavigation = true
  20. // on* Event handlers.
  21. this.on = {}
  22. this.browserPluginNode = this.createBrowserPluginNode()
  23. const shadowRoot = this.webviewNode.attachShadow({mode: 'open'})
  24. shadowRoot.innerHTML = '<!DOCTYPE html><style type="text/css">:host { display: flex; }</style>'
  25. this.setupWebViewAttributes()
  26. this.setupFocusPropagation()
  27. this.viewInstanceId = getNextId()
  28. shadowRoot.appendChild(this.browserPluginNode)
  29. }
  30. createBrowserPluginNode () {
  31. // We create BrowserPlugin as a custom element in order to observe changes
  32. // to attributes synchronously.
  33. const browserPluginNode = new WebViewImpl.BrowserPlugin()
  34. v8Util.setHiddenValue(browserPluginNode, 'internal', this)
  35. return browserPluginNode
  36. }
  37. // Resets some state upon reattaching <webview> element to the DOM.
  38. reset () {
  39. // If guestInstanceId is defined then the <webview> has navigated and has
  40. // already picked up a partition ID. Thus, we need to reset the initialization
  41. // state. However, it may be the case that beforeFirstNavigation is false BUT
  42. // guestInstanceId has yet to be initialized. This means that we have not
  43. // heard back from createGuest yet. We will not reset the flag in this case so
  44. // that we don't end up allocating a second guest.
  45. if (this.guestInstanceId) {
  46. guestViewInternal.destroyGuest(this.guestInstanceId)
  47. this.guestInstanceId = void 0
  48. }
  49. this.webContents = null
  50. this.beforeFirstNavigation = true
  51. this.attributes[webViewConstants.ATTRIBUTE_PARTITION].validPartitionId = true
  52. // Set guestinstance last since this can trigger the attachedCallback to fire
  53. // when moving the webview using element.replaceChild
  54. this.attributes[webViewConstants.ATTRIBUTE_GUESTINSTANCE].setValueIgnoreMutation(undefined)
  55. }
  56. // Sets the <webview>.request property.
  57. setRequestPropertyOnWebViewNode (request) {
  58. Object.defineProperty(this.webviewNode, 'request', {
  59. value: request,
  60. enumerable: true
  61. })
  62. }
  63. setupFocusPropagation () {
  64. if (!this.webviewNode.hasAttribute('tabIndex')) {
  65. // <webview> needs a tabIndex in order to be focusable.
  66. // TODO(fsamuel): It would be nice to avoid exposing a tabIndex attribute
  67. // to allow <webview> to be focusable.
  68. // See http://crbug.com/231664.
  69. this.webviewNode.setAttribute('tabIndex', -1)
  70. }
  71. // Focus the BrowserPlugin when the <webview> takes focus.
  72. this.webviewNode.addEventListener('focus', () => {
  73. this.browserPluginNode.focus()
  74. })
  75. // Blur the BrowserPlugin when the <webview> loses focus.
  76. this.webviewNode.addEventListener('blur', () => {
  77. this.browserPluginNode.blur()
  78. })
  79. }
  80. // This observer monitors mutations to attributes of the <webview> and
  81. // updates the BrowserPlugin properties accordingly. In turn, updating
  82. // a BrowserPlugin property will update the corresponding BrowserPlugin
  83. // attribute, if necessary. See BrowserPlugin::UpdateDOMAttribute for more
  84. // details.
  85. handleWebviewAttributeMutation (attributeName, oldValue, newValue) {
  86. if (!this.attributes[attributeName] || this.attributes[attributeName].ignoreMutation) {
  87. return
  88. }
  89. // Let the changed attribute handle its own mutation
  90. this.attributes[attributeName].handleMutation(oldValue, newValue)
  91. }
  92. handleBrowserPluginAttributeMutation (attributeName, oldValue, newValue) {
  93. if (attributeName === webViewConstants.ATTRIBUTE_INTERNALINSTANCEID && !oldValue && !!newValue) {
  94. this.browserPluginNode.removeAttribute(webViewConstants.ATTRIBUTE_INTERNALINSTANCEID)
  95. this.internalInstanceId = parseInt(newValue)
  96. // Track when the element resizes using the element resize callback.
  97. webFrame.registerElementResizeCallback(this.internalInstanceId, this.onElementResize.bind(this))
  98. if (this.guestInstanceId) {
  99. guestViewInternal.attachGuest(this.internalInstanceId, this.guestInstanceId, this.buildParams())
  100. }
  101. }
  102. }
  103. onSizeChanged (webViewEvent) {
  104. const {newHeight, newWidth} = webViewEvent
  105. const node = this.webviewNode
  106. const width = node.offsetWidth
  107. // Check the current bounds to make sure we do not resize <webview>
  108. // outside of current constraints.
  109. const maxWidth = this.attributes[webViewConstants.ATTRIBUTE_MAXWIDTH].getValue() | width
  110. const maxHeight = this.attributes[webViewConstants.ATTRIBUTE_MAXHEIGHT].getValue() | width
  111. let minWidth = this.attributes[webViewConstants.ATTRIBUTE_MINWIDTH].getValue() | width
  112. let minHeight = this.attributes[webViewConstants.ATTRIBUTE_MINHEIGHT].getValue() | width
  113. minWidth = Math.min(minWidth, maxWidth)
  114. minHeight = Math.min(minHeight, maxHeight)
  115. if (!this.attributes[webViewConstants.ATTRIBUTE_AUTOSIZE].getValue() || (newWidth >= minWidth && newWidth <= maxWidth && newHeight >= minHeight && newHeight <= maxHeight)) {
  116. node.style.width = `${newWidth}px`
  117. node.style.height = `${newHeight}px`
  118. // Only fire the DOM event if the size of the <webview> has actually
  119. // changed.
  120. this.dispatchEvent(webViewEvent)
  121. }
  122. }
  123. onElementResize (newSize) {
  124. // Dispatch the 'resize' event.
  125. const resizeEvent = new Event('resize')
  126. // Using client size values, because when a webview is transformed `newSize`
  127. // is incorrect
  128. newSize.width = this.webviewNode.clientWidth
  129. newSize.height = this.webviewNode.clientHeight
  130. resizeEvent.newWidth = newSize.width
  131. resizeEvent.newHeight = newSize.height
  132. this.dispatchEvent(resizeEvent)
  133. if (this.guestInstanceId &&
  134. !this.attributes[webViewConstants.ATTRIBUTE_DISABLEGUESTRESIZE].getValue()) {
  135. guestViewInternal.setSize(this.guestInstanceId, {
  136. normal: newSize
  137. })
  138. }
  139. }
  140. createGuest () {
  141. return guestViewInternal.createGuest(this.buildParams(), (event, guestInstanceId) => {
  142. this.attachGuestInstance(guestInstanceId)
  143. })
  144. }
  145. createGuestSync () {
  146. this.attachGuestInstance(guestViewInternal.createGuestSync(this.buildParams()))
  147. }
  148. dispatchEvent (webViewEvent) {
  149. this.webviewNode.dispatchEvent(webViewEvent)
  150. }
  151. // Adds an 'on<event>' property on the webview, which can be used to set/unset
  152. // an event handler.
  153. setupEventProperty (eventName) {
  154. const propertyName = `on${eventName.toLowerCase()}`
  155. return Object.defineProperty(this.webviewNode, propertyName, {
  156. get: () => {
  157. return this.on[propertyName]
  158. },
  159. set: (value) => {
  160. if (this.on[propertyName]) {
  161. this.webviewNode.removeEventListener(eventName, this.on[propertyName])
  162. }
  163. this.on[propertyName] = value
  164. if (value) {
  165. return this.webviewNode.addEventListener(eventName, value)
  166. }
  167. },
  168. enumerable: true
  169. })
  170. }
  171. // Updates state upon loadcommit.
  172. onLoadCommit (webViewEvent) {
  173. const oldValue = this.webviewNode.getAttribute(webViewConstants.ATTRIBUTE_SRC)
  174. const newValue = webViewEvent.url
  175. if (webViewEvent.isMainFrame && (oldValue !== newValue)) {
  176. // Touching the src attribute triggers a navigation. To avoid
  177. // triggering a page reload on every guest-initiated navigation,
  178. // we do not handle this mutation.
  179. this.attributes[webViewConstants.ATTRIBUTE_SRC].setValueIgnoreMutation(newValue)
  180. }
  181. }
  182. onAttach (storagePartitionId) {
  183. return this.attributes[webViewConstants.ATTRIBUTE_PARTITION].setValue(storagePartitionId)
  184. }
  185. buildParams () {
  186. const params = {
  187. instanceId: this.viewInstanceId,
  188. userAgentOverride: this.userAgentOverride
  189. }
  190. for (const attributeName in this.attributes) {
  191. if (hasProp.call(this.attributes, attributeName)) {
  192. params[attributeName] = this.attributes[attributeName].getValue()
  193. }
  194. }
  195. // When the WebView is not participating in layout (display:none)
  196. // then getBoundingClientRect() would report a width and height of 0.
  197. // However, in the case where the WebView has a fixed size we can
  198. // use that value to initially size the guest so as to avoid a relayout of
  199. // the on display:block.
  200. const css = window.getComputedStyle(this.webviewNode, null)
  201. const elementRect = this.webviewNode.getBoundingClientRect()
  202. params.elementWidth = parseInt(elementRect.width) || parseInt(css.getPropertyValue('width'))
  203. params.elementHeight = parseInt(elementRect.height) || parseInt(css.getPropertyValue('height'))
  204. return params
  205. }
  206. attachGuestInstance (guestInstanceId) {
  207. this.guestInstanceId = guestInstanceId
  208. this.attributes[webViewConstants.ATTRIBUTE_GUESTINSTANCE].setValueIgnoreMutation(guestInstanceId)
  209. this.webContents = remote.getGuestWebContents(this.guestInstanceId)
  210. if (!this.internalInstanceId) {
  211. return true
  212. }
  213. return guestViewInternal.attachGuest(this.internalInstanceId, this.guestInstanceId, this.buildParams())
  214. }
  215. }
  216. // Registers browser plugin <object> custom element.
  217. const registerBrowserPluginElement = function () {
  218. const proto = Object.create(HTMLObjectElement.prototype)
  219. proto.createdCallback = function () {
  220. this.setAttribute('type', 'application/browser-plugin')
  221. this.setAttribute('id', `browser-plugin-${getNextId()}`)
  222. // The <object> node fills in the <webview> container.
  223. this.style.flex = '1 1 auto'
  224. }
  225. proto.attributeChangedCallback = function (name, oldValue, newValue) {
  226. const internal = v8Util.getHiddenValue(this, 'internal')
  227. if (internal) {
  228. internal.handleBrowserPluginAttributeMutation(name, oldValue, newValue)
  229. }
  230. }
  231. proto.attachedCallback = function () {
  232. // Load the plugin immediately.
  233. return this.nonExistentAttribute
  234. }
  235. WebViewImpl.BrowserPlugin = webFrame.registerEmbedderCustomElement('browserplugin', {
  236. 'extends': 'object',
  237. prototype: proto
  238. })
  239. delete proto.createdCallback
  240. delete proto.attachedCallback
  241. delete proto.detachedCallback
  242. delete proto.attributeChangedCallback
  243. }
  244. // Registers <webview> custom element.
  245. const registerWebViewElement = function () {
  246. const proto = Object.create(HTMLObjectElement.prototype)
  247. proto.createdCallback = function () {
  248. return new WebViewImpl(this)
  249. }
  250. proto.attributeChangedCallback = function (name, oldValue, newValue) {
  251. const internal = v8Util.getHiddenValue(this, 'internal')
  252. if (internal) {
  253. internal.handleWebviewAttributeMutation(name, oldValue, newValue)
  254. }
  255. }
  256. proto.detachedCallback = function () {
  257. const internal = v8Util.getHiddenValue(this, 'internal')
  258. if (!internal) {
  259. return
  260. }
  261. guestViewInternal.deregisterEvents(internal.viewInstanceId)
  262. internal.elementAttached = false
  263. this.internalInstanceId = 0
  264. internal.reset()
  265. }
  266. proto.attachedCallback = function () {
  267. const internal = v8Util.getHiddenValue(this, 'internal')
  268. if (!internal) {
  269. return
  270. }
  271. if (!internal.elementAttached) {
  272. guestViewInternal.registerEvents(internal, internal.viewInstanceId)
  273. internal.elementAttached = true
  274. const instance = internal.attributes[webViewConstants.ATTRIBUTE_GUESTINSTANCE].getValue()
  275. if (instance) {
  276. internal.attachGuestInstance(instance)
  277. } else {
  278. internal.attributes[webViewConstants.ATTRIBUTE_SRC].parse()
  279. }
  280. }
  281. }
  282. // Public-facing API methods.
  283. const methods = [
  284. 'getURL',
  285. 'loadURL',
  286. 'getTitle',
  287. 'isLoading',
  288. 'isLoadingMainFrame',
  289. 'isWaitingForResponse',
  290. 'stop',
  291. 'reload',
  292. 'reloadIgnoringCache',
  293. 'canGoBack',
  294. 'canGoForward',
  295. 'canGoToOffset',
  296. 'clearHistory',
  297. 'goBack',
  298. 'goForward',
  299. 'goToIndex',
  300. 'goToOffset',
  301. 'isCrashed',
  302. 'setUserAgent',
  303. 'getUserAgent',
  304. 'openDevTools',
  305. 'closeDevTools',
  306. 'isDevToolsOpened',
  307. 'isDevToolsFocused',
  308. 'inspectElement',
  309. 'setAudioMuted',
  310. 'isAudioMuted',
  311. 'undo',
  312. 'redo',
  313. 'cut',
  314. 'copy',
  315. 'paste',
  316. 'pasteAndMatchStyle',
  317. 'delete',
  318. 'selectAll',
  319. 'unselect',
  320. 'replace',
  321. 'replaceMisspelling',
  322. 'findInPage',
  323. 'stopFindInPage',
  324. 'getId',
  325. 'downloadURL',
  326. 'inspectServiceWorker',
  327. 'print',
  328. 'printToPDF',
  329. 'showDefinitionForSelection',
  330. 'capturePage',
  331. 'setZoomFactor',
  332. 'setZoomLevel',
  333. 'getZoomLevel',
  334. 'getZoomFactor'
  335. ]
  336. const nonblockMethods = [
  337. 'insertCSS',
  338. 'insertText',
  339. 'send',
  340. 'sendInputEvent',
  341. 'setLayoutZoomLevelLimits',
  342. 'setVisualZoomLevelLimits'
  343. ]
  344. // Forward proto.foo* method calls to WebViewImpl.foo*.
  345. const createBlockHandler = function (m) {
  346. return function (...args) {
  347. const internal = v8Util.getHiddenValue(this, 'internal')
  348. if (internal.webContents) {
  349. return internal.webContents[m](...args)
  350. } else {
  351. 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.`)
  352. }
  353. }
  354. }
  355. for (const method of methods) {
  356. proto[method] = createBlockHandler(method)
  357. }
  358. const createNonBlockHandler = function (m) {
  359. return function (...args) {
  360. const internal = v8Util.getHiddenValue(this, 'internal')
  361. ipcRenderer.send('ELECTRON_BROWSER_ASYNC_CALL_TO_GUEST_VIEW', null, internal.guestInstanceId, m, ...args)
  362. }
  363. }
  364. for (const method of nonblockMethods) {
  365. proto[method] = createNonBlockHandler(method)
  366. }
  367. proto.executeJavaScript = function (code, hasUserGesture, callback) {
  368. const internal = v8Util.getHiddenValue(this, 'internal')
  369. if (typeof hasUserGesture === 'function') {
  370. callback = hasUserGesture
  371. hasUserGesture = false
  372. }
  373. const requestId = getNextId()
  374. ipcRenderer.send('ELECTRON_BROWSER_ASYNC_CALL_TO_GUEST_VIEW', requestId, internal.guestInstanceId, 'executeJavaScript', code, hasUserGesture)
  375. ipcRenderer.once(`ELECTRON_RENDERER_ASYNC_CALL_TO_GUEST_VIEW_RESPONSE_${requestId}`, function (event, result) {
  376. if (callback) callback(result)
  377. })
  378. }
  379. // WebContents associated with this webview.
  380. proto.getWebContents = function () {
  381. const internal = v8Util.getHiddenValue(this, 'internal')
  382. if (!internal.webContents) {
  383. internal.createGuestSync()
  384. }
  385. return internal.webContents
  386. }
  387. window.WebView = webFrame.registerEmbedderCustomElement('webview', {
  388. prototype: proto
  389. })
  390. // Delete the callbacks so developers cannot call them and produce unexpected
  391. // behavior.
  392. delete proto.createdCallback
  393. delete proto.attachedCallback
  394. delete proto.detachedCallback
  395. delete proto.attributeChangedCallback
  396. }
  397. const useCapture = true
  398. const listener = function (event) {
  399. if (document.readyState === 'loading') {
  400. return
  401. }
  402. registerBrowserPluginElement()
  403. registerWebViewElement()
  404. window.removeEventListener(event.type, listener, useCapture)
  405. }
  406. window.addEventListener('readystatechange', listener, true)
  407. module.exports = WebViewImpl