{deprecate, webFrame, remote, ipcRenderer} = require 'electron' v8Util = process.atomBinding 'v8_util' guestViewInternal = require './guest-view-internal' webViewConstants = require './web-view-constants' # ID generator. nextId = 0 getNextId = -> ++nextId # Represents the internal state of the WebView node. class WebViewImpl constructor: (@webviewNode) -> v8Util.setHiddenValue @webviewNode, 'internal', this @attached = false @elementAttached = false @beforeFirstNavigation = true # on* Event handlers. @on = {} @browserPluginNode = @createBrowserPluginNode() shadowRoot = @webviewNode.createShadowRoot() @setupWebViewAttributes() @setupFocusPropagation() @viewInstanceId = getNextId() shadowRoot.appendChild @browserPluginNode createBrowserPluginNode: -> # We create BrowserPlugin as a custom element in order to observe changes # to attributes synchronously. browserPluginNode = new WebViewImpl.BrowserPlugin() v8Util.setHiddenValue browserPluginNode, 'internal', this browserPluginNode # Resets some state upon reattaching element to the DOM. reset: -> # If guestInstanceId is defined then the has navigated and has # already picked up a partition ID. Thus, we need to reset the initialization # state. However, it may be the case that beforeFirstNavigation is false BUT # guestInstanceId has yet to be initialized. This means that we have not # heard back from createGuest yet. We will not reset the flag in this case so # that we don't end up allocating a second guest. if @guestInstanceId guestViewInternal.destroyGuest @guestInstanceId @webContents = null @guestInstanceId = undefined @beforeFirstNavigation = true @attributes[webViewConstants.ATTRIBUTE_PARTITION].validPartitionId = true @internalInstanceId = 0 # Sets the .request property. setRequestPropertyOnWebViewNode: (request) -> Object.defineProperty @webviewNode, 'request', value: request, enumerable: true setupFocusPropagation: -> unless @webviewNode.hasAttribute 'tabIndex' # needs a tabIndex in order to be focusable. # TODO(fsamuel): It would be nice to avoid exposing a tabIndex attribute # to allow to be focusable. # See http://crbug.com/231664. @webviewNode.setAttribute 'tabIndex', -1 @webviewNode.addEventListener 'focus', (e) => # Focus the BrowserPlugin when the takes focus. @browserPluginNode.focus() @webviewNode.addEventListener 'blur', (e) => # Blur the BrowserPlugin when the loses focus. @browserPluginNode.blur() # This observer monitors mutations to attributes of the and # updates the BrowserPlugin properties accordingly. In turn, updating # a BrowserPlugin property will update the corresponding BrowserPlugin # attribute, if necessary. See BrowserPlugin::UpdateDOMAttribute for more # details. handleWebviewAttributeMutation: (attributeName, oldValue, newValue) -> if not @attributes[attributeName] or @attributes[attributeName].ignoreMutation return # Let the changed attribute handle its own mutation; @attributes[attributeName].handleMutation oldValue, newValue handleBrowserPluginAttributeMutation: (attributeName, oldValue, newValue) -> if attributeName is webViewConstants.ATTRIBUTE_INTERNALINSTANCEID and !oldValue and !!newValue @browserPluginNode.removeAttribute webViewConstants.ATTRIBUTE_INTERNALINSTANCEID @internalInstanceId = parseInt newValue # Track when the element resizes using the element resize callback. webFrame.registerElementResizeCallback @internalInstanceId, @onElementResize.bind(this) return unless @guestInstanceId guestViewInternal.attachGuest @internalInstanceId, @guestInstanceId, @buildParams() onSizeChanged: (webViewEvent) -> newWidth = webViewEvent.newWidth newHeight = webViewEvent.newHeight node = @webviewNode width = node.offsetWidth height = node.offsetHeight # Check the current bounds to make sure we do not resize # outside of current constraints. maxWidth = @attributes[webViewConstants.ATTRIBUTE_MAXWIDTH].getValue() | width maxHeight = @attributes[webViewConstants.ATTRIBUTE_MAXHEIGHT].getValue() | width minWidth = @attributes[webViewConstants.ATTRIBUTE_MINWIDTH].getValue() | width minHeight = @attributes[webViewConstants.ATTRIBUTE_MINHEIGHT].getValue() | width minWidth = Math.min minWidth, maxWidth minHeight = Math.min minHeight, maxHeight if not @attributes[webViewConstants.ATTRIBUTE_AUTOSIZE].getValue() or (newWidth >= minWidth and newWidth <= maxWidth and newHeight >= minHeight and newHeight <= maxHeight) node.style.width = newWidth + 'px' node.style.height = newHeight + 'px' # Only fire the DOM event if the size of the has actually # changed. @dispatchEvent webViewEvent onElementResize: (newSize) -> # Dispatch the 'resize' event. resizeEvent = new Event('resize', bubbles: true) resizeEvent.newWidth = newSize.width resizeEvent.newHeight = newSize.height @dispatchEvent resizeEvent if @guestInstanceId guestViewInternal.setSize @guestInstanceId, normal: newSize createGuest: -> guestViewInternal.createGuest @buildParams(), (event, guestInstanceId) => @attachWindow guestInstanceId dispatchEvent: (webViewEvent) -> @webviewNode.dispatchEvent webViewEvent # Adds an 'on' property on the webview, which can be used to set/unset # an event handler. setupEventProperty: (eventName) -> propertyName = 'on' + eventName.toLowerCase() Object.defineProperty @webviewNode, propertyName, get: => @on[propertyName] set: (value) => if @on[propertyName] @webviewNode.removeEventListener eventName, @on[propertyName] @on[propertyName] = value if value @webviewNode.addEventListener eventName, value enumerable: true # Updates state upon loadcommit. onLoadCommit: (webViewEvent) -> oldValue = @webviewNode.getAttribute webViewConstants.ATTRIBUTE_SRC newValue = webViewEvent.url if webViewEvent.isMainFrame and (oldValue != newValue) # Touching the src attribute triggers a navigation. To avoid # triggering a page reload on every guest-initiated navigation, # we do not handle this mutation @attributes[webViewConstants.ATTRIBUTE_SRC].setValueIgnoreMutation newValue onAttach: (storagePartitionId) -> @attributes[webViewConstants.ATTRIBUTE_PARTITION].setValue storagePartitionId buildParams: -> params = instanceId: @viewInstanceId userAgentOverride: @userAgentOverride for own attributeName, attribute of @attributes params[attributeName] = attribute.getValue() # When the WebView is not participating in layout (display:none) # then getBoundingClientRect() would report a width and height of 0. # However, in the case where the WebView has a fixed size we can # use that value to initially size the guest so as to avoid a relayout of # the on display:block. css = window.getComputedStyle @webviewNode, null elementRect = @webviewNode.getBoundingClientRect() params.elementWidth = parseInt(elementRect.width) || parseInt(css.getPropertyValue('width')) params.elementHeight = parseInt(elementRect.height) || parseInt(css.getPropertyValue('height')) params attachWindow: (guestInstanceId) -> @guestInstanceId = guestInstanceId @webContents = remote.getGuestWebContents @guestInstanceId return true unless @internalInstanceId guestViewInternal.attachGuest @internalInstanceId, @guestInstanceId, @buildParams() # Registers browser plugin custom element. registerBrowserPluginElement = -> proto = Object.create HTMLObjectElement.prototype proto.createdCallback = -> @setAttribute 'type', 'application/browser-plugin' @setAttribute 'id', 'browser-plugin-' + getNextId() # The node fills in the container. @style.display = 'block' @style.width = '100%' @style.height = '100%' proto.attributeChangedCallback = (name, oldValue, newValue) -> internal = v8Util.getHiddenValue this, 'internal' return unless internal internal.handleBrowserPluginAttributeMutation name, oldValue, newValue proto.attachedCallback = -> # Load the plugin immediately. unused = this.nonExistentAttribute WebViewImpl.BrowserPlugin = webFrame.registerEmbedderCustomElement 'browserplugin', extends: 'object', prototype: proto delete proto.createdCallback delete proto.attachedCallback delete proto.detachedCallback delete proto.attributeChangedCallback # Registers custom element. registerWebViewElement = -> proto = Object.create HTMLObjectElement.prototype proto.createdCallback = -> new WebViewImpl(this) proto.attributeChangedCallback = (name, oldValue, newValue) -> internal = v8Util.getHiddenValue this, 'internal' return unless internal internal.handleWebviewAttributeMutation name, oldValue, newValue proto.detachedCallback = -> internal = v8Util.getHiddenValue this, 'internal' return unless internal guestViewInternal.deregisterEvents internal.viewInstanceId internal.elementAttached = false internal.reset() proto.attachedCallback = -> internal = v8Util.getHiddenValue this, 'internal' return unless internal unless internal.elementAttached guestViewInternal.registerEvents internal, internal.viewInstanceId internal.elementAttached = true internal.attributes[webViewConstants.ATTRIBUTE_SRC].parse() # Public-facing API methods. methods = [ 'getURL' 'getTitle' 'isLoading' 'isWaitingForResponse' 'stop' 'reload' 'reloadIgnoringCache' 'canGoBack' 'canGoForward' 'canGoToOffset' 'clearHistory' 'goBack' 'goForward' 'goToIndex' 'goToOffset' 'isCrashed' 'setUserAgent' 'getUserAgent' 'openDevTools' 'closeDevTools' 'isDevToolsOpened' 'inspectElement' 'setAudioMuted' 'isAudioMuted' 'undo' 'redo' 'cut' 'copy' 'paste' 'pasteAndMatchStyle' 'delete' 'selectAll' 'unselect' 'replace' 'replaceMisspelling' 'findInPage' 'stopFindInPage' 'getId' 'downloadURL' 'inspectServiceWorker' 'print' 'printToPDF' ] nonblockMethods = [ 'send', 'sendInputEvent', 'executeJavaScript', 'insertCSS' ] # Forward proto.foo* method calls to WebViewImpl.foo*. createBlockHandler = (m) -> (args...) -> internal = v8Util.getHiddenValue this, 'internal' internal.webContents[m] args... proto[m] = createBlockHandler m for m in methods createNonBlockHandler = (m) -> (args...) -> internal = v8Util.getHiddenValue this, 'internal' ipcRenderer.send('ATOM_BROWSER_ASYNC_CALL_TO_GUEST_VIEW', internal.guestInstanceId, m, args...) proto[m] = createNonBlockHandler m for m in nonblockMethods # Deprecated. deprecate.rename proto, 'getUrl', 'getURL' window.WebView = webFrame.registerEmbedderCustomElement 'webview', prototype: proto # Delete the callbacks so developers cannot call them and produce unexpected # behavior. delete proto.createdCallback delete proto.attachedCallback delete proto.detachedCallback delete proto.attributeChangedCallback useCapture = true listener = (event) -> return if document.readyState == 'loading' registerBrowserPluginElement() registerWebViewElement() window.removeEventListener event.type, listener, useCapture window.addEventListener 'readystatechange', listener, true module.exports = WebViewImpl