web-view.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450
  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. bubbles: true
  127. })
  128. // Using client size values, because when a webview is transformed `newSize`
  129. // is incorrect
  130. newSize.width = this.webviewNode.clientWidth
  131. newSize.height = this.webviewNode.clientHeight
  132. resizeEvent.newWidth = newSize.width
  133. resizeEvent.newHeight = newSize.height
  134. this.dispatchEvent(resizeEvent)
  135. if (this.guestInstanceId &&
  136. !this.attributes[webViewConstants.ATTRIBUTE_DISABLEGUESTRESIZE].getValue()) {
  137. guestViewInternal.setSize(this.guestInstanceId, {
  138. normal: newSize
  139. })
  140. }
  141. }
  142. createGuest () {
  143. return guestViewInternal.createGuest(this.buildParams(), (event, guestInstanceId) => {
  144. this.attachGuestInstance(guestInstanceId)
  145. })
  146. }
  147. dispatchEvent (webViewEvent) {
  148. this.webviewNode.dispatchEvent(webViewEvent)
  149. }
  150. // Adds an 'on<event>' property on the webview, which can be used to set/unset
  151. // an event handler.
  152. setupEventProperty (eventName) {
  153. const propertyName = `on${eventName.toLowerCase()}`
  154. return Object.defineProperty(this.webviewNode, propertyName, {
  155. get: () => {
  156. return this.on[propertyName]
  157. },
  158. set: (value) => {
  159. if (this.on[propertyName]) {
  160. this.webviewNode.removeEventListener(eventName, this.on[propertyName])
  161. }
  162. this.on[propertyName] = value
  163. if (value) {
  164. return this.webviewNode.addEventListener(eventName, value)
  165. }
  166. },
  167. enumerable: true
  168. })
  169. }
  170. // Updates state upon loadcommit.
  171. onLoadCommit (webViewEvent) {
  172. const oldValue = this.webviewNode.getAttribute(webViewConstants.ATTRIBUTE_SRC)
  173. const newValue = webViewEvent.url
  174. if (webViewEvent.isMainFrame && (oldValue !== newValue)) {
  175. // Touching the src attribute triggers a navigation. To avoid
  176. // triggering a page reload on every guest-initiated navigation,
  177. // we do not handle this mutation.
  178. this.attributes[webViewConstants.ATTRIBUTE_SRC].setValueIgnoreMutation(newValue)
  179. }
  180. }
  181. onAttach (storagePartitionId) {
  182. return this.attributes[webViewConstants.ATTRIBUTE_PARTITION].setValue(storagePartitionId)
  183. }
  184. buildParams () {
  185. const params = {
  186. instanceId: this.viewInstanceId,
  187. userAgentOverride: this.userAgentOverride
  188. }
  189. for (const attributeName in this.attributes) {
  190. if (hasProp.call(this.attributes, attributeName)) {
  191. params[attributeName] = this.attributes[attributeName].getValue()
  192. }
  193. }
  194. // When the WebView is not participating in layout (display:none)
  195. // then getBoundingClientRect() would report a width and height of 0.
  196. // However, in the case where the WebView has a fixed size we can
  197. // use that value to initially size the guest so as to avoid a relayout of
  198. // the on display:block.
  199. const css = window.getComputedStyle(this.webviewNode, null)
  200. const elementRect = this.webviewNode.getBoundingClientRect()
  201. params.elementWidth = parseInt(elementRect.width) || parseInt(css.getPropertyValue('width'))
  202. params.elementHeight = parseInt(elementRect.height) || parseInt(css.getPropertyValue('height'))
  203. return params
  204. }
  205. attachGuestInstance (guestInstanceId) {
  206. this.guestInstanceId = guestInstanceId
  207. this.attributes[webViewConstants.ATTRIBUTE_GUESTINSTANCE].setValueIgnoreMutation(guestInstanceId)
  208. this.webContents = remote.getGuestWebContents(this.guestInstanceId)
  209. if (!this.internalInstanceId) {
  210. return true
  211. }
  212. return guestViewInternal.attachGuest(this.internalInstanceId, this.guestInstanceId, this.buildParams())
  213. }
  214. }
  215. // Registers browser plugin <object> custom element.
  216. const registerBrowserPluginElement = function () {
  217. const proto = Object.create(HTMLObjectElement.prototype)
  218. proto.createdCallback = function () {
  219. this.setAttribute('type', 'application/browser-plugin')
  220. this.setAttribute('id', `browser-plugin-${getNextId()}`)
  221. // The <object> node fills in the <webview> container.
  222. this.style.flex = '1 1 auto'
  223. }
  224. proto.attributeChangedCallback = function (name, oldValue, newValue) {
  225. const internal = v8Util.getHiddenValue(this, 'internal')
  226. if (internal) {
  227. internal.handleBrowserPluginAttributeMutation(name, oldValue, newValue)
  228. }
  229. }
  230. proto.attachedCallback = function () {
  231. // Load the plugin immediately.
  232. return this.nonExistentAttribute
  233. }
  234. WebViewImpl.BrowserPlugin = webFrame.registerEmbedderCustomElement('browserplugin', {
  235. 'extends': 'object',
  236. prototype: proto
  237. })
  238. delete proto.createdCallback
  239. delete proto.attachedCallback
  240. delete proto.detachedCallback
  241. delete proto.attributeChangedCallback
  242. }
  243. // Registers <webview> custom element.
  244. const registerWebViewElement = function () {
  245. const proto = Object.create(HTMLObjectElement.prototype)
  246. proto.createdCallback = function () {
  247. return new WebViewImpl(this)
  248. }
  249. proto.attributeChangedCallback = function (name, oldValue, newValue) {
  250. const internal = v8Util.getHiddenValue(this, 'internal')
  251. if (internal) {
  252. internal.handleWebviewAttributeMutation(name, oldValue, newValue)
  253. }
  254. }
  255. proto.detachedCallback = function () {
  256. const internal = v8Util.getHiddenValue(this, 'internal')
  257. if (!internal) {
  258. return
  259. }
  260. guestViewInternal.deregisterEvents(internal.viewInstanceId)
  261. internal.elementAttached = false
  262. this.internalInstanceId = 0
  263. internal.reset()
  264. }
  265. proto.attachedCallback = function () {
  266. const internal = v8Util.getHiddenValue(this, 'internal')
  267. if (!internal) {
  268. return
  269. }
  270. if (!internal.elementAttached) {
  271. guestViewInternal.registerEvents(internal, internal.viewInstanceId)
  272. internal.elementAttached = true
  273. const instance = internal.attributes[webViewConstants.ATTRIBUTE_GUESTINSTANCE].getValue()
  274. if (instance) {
  275. internal.attachGuestInstance(instance)
  276. } else {
  277. internal.attributes[webViewConstants.ATTRIBUTE_SRC].parse()
  278. }
  279. }
  280. }
  281. // Public-facing API methods.
  282. const methods = [
  283. 'getURL',
  284. 'loadURL',
  285. 'getTitle',
  286. 'isLoading',
  287. 'isLoadingMainFrame',
  288. 'isWaitingForResponse',
  289. 'stop',
  290. 'reload',
  291. 'reloadIgnoringCache',
  292. 'canGoBack',
  293. 'canGoForward',
  294. 'canGoToOffset',
  295. 'clearHistory',
  296. 'goBack',
  297. 'goForward',
  298. 'goToIndex',
  299. 'goToOffset',
  300. 'isCrashed',
  301. 'setUserAgent',
  302. 'getUserAgent',
  303. 'openDevTools',
  304. 'closeDevTools',
  305. 'isDevToolsOpened',
  306. 'isDevToolsFocused',
  307. 'inspectElement',
  308. 'setAudioMuted',
  309. 'isAudioMuted',
  310. 'undo',
  311. 'redo',
  312. 'cut',
  313. 'copy',
  314. 'paste',
  315. 'pasteAndMatchStyle',
  316. 'delete',
  317. 'selectAll',
  318. 'unselect',
  319. 'replace',
  320. 'replaceMisspelling',
  321. 'findInPage',
  322. 'stopFindInPage',
  323. 'getId',
  324. 'downloadURL',
  325. 'inspectServiceWorker',
  326. 'print',
  327. 'printToPDF',
  328. 'showDefinitionForSelection',
  329. 'capturePage',
  330. 'setZoomFactor',
  331. 'setZoomLevel',
  332. 'getZoomLevel',
  333. 'getZoomFactor'
  334. ]
  335. const nonblockMethods = [
  336. 'insertCSS',
  337. 'insertText',
  338. 'send',
  339. 'sendInputEvent',
  340. 'setLayoutZoomLevelLimits',
  341. 'setVisualZoomLevelLimits'
  342. ]
  343. // Forward proto.foo* method calls to WebViewImpl.foo*.
  344. const createBlockHandler = function (m) {
  345. return function (...args) {
  346. const internal = v8Util.getHiddenValue(this, 'internal')
  347. if (internal.webContents) {
  348. return internal.webContents[m](...args)
  349. } else {
  350. 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.`)
  351. }
  352. }
  353. }
  354. for (const method of methods) {
  355. proto[method] = createBlockHandler(method)
  356. }
  357. const createNonBlockHandler = function (m) {
  358. return function (...args) {
  359. const internal = v8Util.getHiddenValue(this, 'internal')
  360. ipcRenderer.send('ELECTRON_BROWSER_ASYNC_CALL_TO_GUEST_VIEW', null, internal.guestInstanceId, m, ...args)
  361. }
  362. }
  363. for (const method of nonblockMethods) {
  364. proto[method] = createNonBlockHandler(method)
  365. }
  366. proto.executeJavaScript = function (code, hasUserGesture, callback) {
  367. const internal = v8Util.getHiddenValue(this, 'internal')
  368. if (typeof hasUserGesture === 'function') {
  369. callback = hasUserGesture
  370. hasUserGesture = false
  371. }
  372. const requestId = getNextId()
  373. ipcRenderer.send('ELECTRON_BROWSER_ASYNC_CALL_TO_GUEST_VIEW', requestId, internal.guestInstanceId, 'executeJavaScript', code, hasUserGesture)
  374. ipcRenderer.once(`ELECTRON_RENDERER_ASYNC_CALL_TO_GUEST_VIEW_RESPONSE_${requestId}`, function (event, result) {
  375. if (callback) callback(result)
  376. })
  377. }
  378. // WebContents associated with this webview.
  379. proto.getWebContents = function () {
  380. return v8Util.getHiddenValue(this, 'internal').webContents
  381. }
  382. window.WebView = webFrame.registerEmbedderCustomElement('webview', {
  383. prototype: proto
  384. })
  385. // Delete the callbacks so developers cannot call them and produce unexpected
  386. // behavior.
  387. delete proto.createdCallback
  388. delete proto.attachedCallback
  389. delete proto.detachedCallback
  390. delete proto.attributeChangedCallback
  391. }
  392. const useCapture = true
  393. const listener = function (event) {
  394. if (document.readyState === 'loading') {
  395. return
  396. }
  397. registerBrowserPluginElement()
  398. registerWebViewElement()
  399. window.removeEventListener(event.type, listener, useCapture)
  400. }
  401. window.addEventListener('readystatechange', listener, true)
  402. module.exports = WebViewImpl