342 lines
12 KiB
CoffeeScript
342 lines
12 KiB
CoffeeScript
{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 <webview> element to the DOM.
|
|
reset: ->
|
|
# If guestInstanceId is defined then the <webview> 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 <webview>.request property.
|
|
setRequestPropertyOnWebViewNode: (request) ->
|
|
Object.defineProperty @webviewNode, 'request', value: request, enumerable: true
|
|
|
|
setupFocusPropagation: ->
|
|
unless @webviewNode.hasAttribute 'tabIndex'
|
|
# <webview> needs a tabIndex in order to be focusable.
|
|
# TODO(fsamuel): It would be nice to avoid exposing a tabIndex attribute
|
|
# to allow <webview> to be focusable.
|
|
# See http://crbug.com/231664.
|
|
@webviewNode.setAttribute 'tabIndex', -1
|
|
@webviewNode.addEventListener 'focus', (e) =>
|
|
# Focus the BrowserPlugin when the <webview> takes focus.
|
|
@browserPluginNode.focus()
|
|
@webviewNode.addEventListener 'blur', (e) =>
|
|
# Blur the BrowserPlugin when the <webview> loses focus.
|
|
@browserPluginNode.blur()
|
|
|
|
# This observer monitors mutations to attributes of the <webview> 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 <webview>
|
|
# 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 <webview> 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<event>' 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 <object> custom element.
|
|
registerBrowserPluginElement = ->
|
|
proto = Object.create HTMLObjectElement.prototype
|
|
|
|
proto.createdCallback = ->
|
|
@setAttribute 'type', 'application/browser-plugin'
|
|
@setAttribute 'id', 'browser-plugin-' + getNextId()
|
|
# The <object> node fills in the <webview> 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 <webview> 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'
|
|
'isDevToolsFocused'
|
|
'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
|