2014-10-22 14:55:13 +00:00
|
|
|
v8Util = process.atomBinding 'v8_util'
|
2014-10-23 09:54:52 +00:00
|
|
|
guestViewInternal = require './guest-view-internal'
|
2014-12-08 21:36:30 +00:00
|
|
|
webViewConstants = require './web-view-constants'
|
2014-10-24 10:24:12 +00:00
|
|
|
webFrame = require 'web-frame'
|
2014-10-24 11:57:29 +00:00
|
|
|
remote = require 'remote'
|
2014-10-22 14:55:13 +00:00
|
|
|
|
2014-12-08 21:36:30 +00:00
|
|
|
# ID generator.
|
|
|
|
nextId = 0
|
|
|
|
getNextId = -> ++nextId
|
2014-10-22 14:55:13 +00:00
|
|
|
|
|
|
|
# Represents the internal state of the WebView node.
|
|
|
|
class WebView
|
|
|
|
constructor: (@webviewNode) ->
|
2014-10-22 15:37:27 +00:00
|
|
|
v8Util.setHiddenValue @webviewNode, 'internal', this
|
2014-10-22 14:55:13 +00:00
|
|
|
@attached = false
|
|
|
|
@pendingGuestCreation = false
|
|
|
|
@elementAttached = false
|
|
|
|
|
|
|
|
@beforeFirstNavigation = true
|
|
|
|
@contentWindow = null
|
|
|
|
|
|
|
|
# on* Event handlers.
|
|
|
|
@on = {}
|
|
|
|
|
|
|
|
@browserPluginNode = @createBrowserPluginNode()
|
|
|
|
shadowRoot = @webviewNode.createShadowRoot()
|
2014-12-09 00:14:12 +00:00
|
|
|
@setupWebViewAttributes()
|
2014-10-22 14:55:13 +00:00
|
|
|
@setupFocusPropagation()
|
|
|
|
@setupWebviewNodeProperties()
|
|
|
|
|
|
|
|
@viewInstanceId = getNextId()
|
2014-12-08 16:05:34 +00:00
|
|
|
|
2014-10-25 01:05:50 +00:00
|
|
|
guestViewInternal.registerEvents this, @viewInstanceId
|
2014-10-22 14:55:13 +00:00
|
|
|
|
|
|
|
shadowRoot.appendChild @browserPluginNode
|
|
|
|
|
|
|
|
createBrowserPluginNode: ->
|
|
|
|
# We create BrowserPlugin as a custom element in order to observe changes
|
|
|
|
# to attributes synchronously.
|
|
|
|
browserPluginNode = new WebView.BrowserPlugin()
|
|
|
|
v8Util.setHiddenValue browserPluginNode, 'internal', this
|
|
|
|
browserPluginNode
|
|
|
|
|
|
|
|
getGuestInstanceId: ->
|
|
|
|
@guestInstanceId
|
|
|
|
|
|
|
|
# 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
|
2014-12-08 16:05:34 +00:00
|
|
|
# FIXME
|
2014-10-23 09:54:52 +00:00
|
|
|
guestViewInternal.destroyGuest @guestInstanceId
|
2014-10-22 14:55:13 +00:00
|
|
|
@guestInstanceId = undefined
|
|
|
|
@beforeFirstNavigation = true
|
2014-12-09 00:14:12 +00:00
|
|
|
@attributes[webViewConstants.ATTRIBUTE_PARTITION].validPartitionId = true
|
2014-10-22 14:55:13 +00:00
|
|
|
@contentWindow = null
|
|
|
|
@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()
|
|
|
|
|
|
|
|
# Validation helper function for executeScript() and insertCSS().
|
|
|
|
validateExecuteCodeCall: ->
|
2014-12-08 21:36:30 +00:00
|
|
|
throw new Error(webViewConstants.ERROR_MSG_CANNOT_INJECT_SCRIPT) unless @guestInstanceId
|
2014-10-22 14:55:13 +00:00
|
|
|
|
|
|
|
setupWebviewNodeProperties: ->
|
|
|
|
# We cannot use {writable: true} property descriptor because we want a
|
|
|
|
# dynamic getter value.
|
|
|
|
Object.defineProperty @webviewNode, 'contentWindow',
|
|
|
|
get: =>
|
|
|
|
return @contentWindow if @contentWindow?
|
2014-12-08 21:36:30 +00:00
|
|
|
window.console.error webViewConstants.ERROR_MSG_CONTENTWINDOW_NOT_AVAILABLE
|
2014-10-22 14:55:13 +00:00
|
|
|
# No setter.
|
|
|
|
enumerable: true
|
2014-11-06 19:29:41 +00:00
|
|
|
|
2014-10-22 14:55:13 +00:00
|
|
|
# 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.
|
2014-12-09 00:56:14 +00:00
|
|
|
handleWebviewAttributeMutation: (attributeName, oldValue, newValue) ->
|
2014-12-09 05:06:23 +00:00
|
|
|
if not @attributes[attributeName] or @attributes[attributeName].ignoreMutation
|
2014-12-09 00:56:14 +00:00
|
|
|
return
|
|
|
|
|
2014-12-09 05:06:23 +00:00
|
|
|
# Let the changed attribute handle its own mutation;
|
|
|
|
@attributes[attributeName].handleMutation oldValue, newValue
|
2014-10-22 14:55:13 +00:00
|
|
|
|
2014-12-09 00:56:14 +00:00
|
|
|
handleBrowserPluginAttributeMutation: (attributeName, oldValue, newValue) ->
|
|
|
|
if attributeName is webViewConstants.ATTRIBUTE_INTERNALINSTANCEID and !oldValue and !!newValue
|
2014-12-09 00:14:12 +00:00
|
|
|
@browserPluginNode.removeAttribute webViewConstants.ATTRIBUTE_INTERNALINSTANCEID
|
2014-12-08 16:05:34 +00:00
|
|
|
@internalInstanceId = parseInt newValue
|
2014-10-22 14:55:13 +00:00
|
|
|
|
2014-12-09 01:15:50 +00:00
|
|
|
return unless @guestInstanceId
|
|
|
|
|
|
|
|
guestViewInternal.attachGuest @internalInstanceId, @guestInstanceId, params, (w) => @contentWindow = w
|
2014-10-22 14:55:13 +00:00
|
|
|
|
|
|
|
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.
|
2014-12-08 21:36:30 +00:00
|
|
|
if node.hasAttribute(webViewConstants.ATTRIBUTE_MAXWIDTH) and
|
|
|
|
node[webViewConstants.ATTRIBUTE_MAXWIDTH]
|
|
|
|
maxWidth = node[webViewConstants.ATTRIBUTE_MAXWIDTH]
|
2014-10-22 14:55:13 +00:00
|
|
|
else
|
|
|
|
maxWidth = width
|
|
|
|
|
2014-12-08 21:36:30 +00:00
|
|
|
if node.hasAttribute(webViewConstants.ATTRIBUTE_MINWIDTH) and
|
|
|
|
node[webViewConstants.ATTRIBUTE_MINWIDTH]
|
|
|
|
minWidth = node[webViewConstants.ATTRIBUTE_MINWIDTH]
|
2014-10-22 14:55:13 +00:00
|
|
|
else
|
|
|
|
minWidth = width
|
|
|
|
minWidth = maxWidth if minWidth > maxWidth
|
|
|
|
|
2014-12-08 21:36:30 +00:00
|
|
|
if node.hasAttribute(webViewConstants.ATTRIBUTE_MAXHEIGHT) and
|
|
|
|
node[webViewConstants.ATTRIBUTE_MAXHEIGHT]
|
|
|
|
maxHeight = node[webViewConstants.ATTRIBUTE_MAXHEIGHT]
|
2014-10-22 14:55:13 +00:00
|
|
|
else
|
|
|
|
maxHeight = height
|
|
|
|
|
2014-12-08 21:36:30 +00:00
|
|
|
if node.hasAttribute(webViewConstants.ATTRIBUTE_MINHEIGHT) and
|
|
|
|
node[webViewConstants.ATTRIBUTE_MINHEIGHT]
|
|
|
|
minHeight = node[webViewConstants.ATTRIBUTE_MINHEIGHT]
|
2014-10-22 14:55:13 +00:00
|
|
|
else
|
|
|
|
minHeight = height
|
|
|
|
minHeight = maxHeight if minHeight > maxHeight
|
|
|
|
|
2014-12-09 00:56:14 +00:00
|
|
|
if not @attributes[webViewConstants.ATTRIBUTE_AUTOSIZE].getValue() or
|
2014-10-22 14:55:13 +00:00
|
|
|
(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
|
|
|
|
|
|
|
|
# Returns if <object> is in the render tree.
|
|
|
|
isPluginInRenderTree: ->
|
2014-12-08 16:05:34 +00:00
|
|
|
!!@internalInstanceId && @internalInstanceId != 0
|
2014-10-22 14:55:13 +00:00
|
|
|
|
|
|
|
hasNavigated: ->
|
|
|
|
not @beforeFirstNavigation
|
|
|
|
|
2014-12-09 00:56:14 +00:00
|
|
|
parseSrcAttribute: ->
|
|
|
|
if not @attributes[webViewConstants.ATTRIBUTE_PARTITION].validPartitionId or
|
|
|
|
not @attributes[webViewConstants.ATTRIBUTE_SRC].getValue()
|
2014-10-22 14:55:13 +00:00
|
|
|
return
|
|
|
|
|
|
|
|
unless @guestInstanceId?
|
|
|
|
if @beforeFirstNavigation
|
|
|
|
@beforeFirstNavigation = false
|
|
|
|
@createGuest()
|
|
|
|
return
|
|
|
|
|
|
|
|
# Navigate to |this.src|.
|
2014-12-09 00:14:12 +00:00
|
|
|
httpreferrer = @attributes[webViewConstants.ATTRIBUTE_HTTPREFERRER].getValue()
|
|
|
|
urlOptions = if httpreferrer then {httpreferrer} else {}
|
|
|
|
remote.getGuestWebContents(@guestInstanceId).loadUrl @attributes[webViewConstants.ATTRIBUTE_SRC].getValue(), urlOptions
|
2014-10-22 14:55:13 +00:00
|
|
|
|
|
|
|
parseAttributes: ->
|
|
|
|
return unless @elementAttached
|
|
|
|
hasNavigated = @hasNavigated()
|
2014-12-09 00:56:14 +00:00
|
|
|
@parseSrcAttribute()
|
2014-10-22 14:55:13 +00:00
|
|
|
|
|
|
|
createGuest: ->
|
2014-10-23 09:54:52 +00:00
|
|
|
return if @pendingGuestCreation
|
2014-10-25 12:52:42 +00:00
|
|
|
params =
|
2014-12-09 00:56:14 +00:00
|
|
|
storagePartitionId: @attributes[webViewConstants.ATTRIBUTE_PARTITION].getValue()
|
2014-12-08 21:36:30 +00:00
|
|
|
nodeIntegration: @webviewNode.hasAttribute webViewConstants.ATTRIBUTE_NODEINTEGRATION
|
|
|
|
plugins: @webviewNode.hasAttribute webViewConstants.ATTRIBUTE_PLUGINS
|
|
|
|
if @webviewNode.hasAttribute webViewConstants.ATTRIBUTE_PRELOAD
|
|
|
|
preload = @webviewNode.getAttribute webViewConstants.ATTRIBUTE_PRELOAD
|
2014-11-06 06:35:32 +00:00
|
|
|
# Get the full path.
|
|
|
|
a = document.createElement 'a'
|
|
|
|
a.href = preload
|
|
|
|
params.preload = a.href
|
|
|
|
# Only support file: or asar: protocol.
|
|
|
|
protocol = params.preload.substr 0, 5
|
|
|
|
unless protocol in ['file:', 'asar:']
|
|
|
|
delete params.preload
|
2014-12-08 21:36:30 +00:00
|
|
|
console.error webViewConstants.ERROR_MSG_INVALID_PRELOAD_ATTRIBUTE
|
2014-10-23 09:54:52 +00:00
|
|
|
guestViewInternal.createGuest 'webview', params, (guestInstanceId) =>
|
|
|
|
@pendingGuestCreation = false
|
|
|
|
unless @elementAttached
|
|
|
|
guestViewInternal.destroyGuest guestInstanceId
|
|
|
|
return
|
2014-12-09 01:15:50 +00:00
|
|
|
@attachWindow guestInstanceId
|
2014-10-22 14:55:13 +00:00
|
|
|
@pendingGuestCreation = true
|
|
|
|
|
2014-10-25 07:48:59 +00:00
|
|
|
dispatchEvent: (webViewEvent) ->
|
2014-10-22 14:55:13 +00:00
|
|
|
@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: (@baseUrlForDataUrl, @currentEntryIndex, @entryCount, @processId, url, isTopLevel) ->
|
2014-12-09 00:14:12 +00:00
|
|
|
oldValue = @webviewNode.getAttribute webViewConstants.ATTRIBUTE_SRC
|
2014-10-22 14:55:13 +00:00
|
|
|
newValue = url
|
|
|
|
if isTopLevel and (oldValue != newValue)
|
|
|
|
# Touching the src attribute triggers a navigation. To avoid
|
|
|
|
# triggering a page reload on every guest-initiated navigation,
|
2014-12-09 05:06:23 +00:00
|
|
|
# we do not handle this mutation
|
|
|
|
@attributes[webViewConstants.ATTRIBUTE_SRC].setValueIgnoreMutation newValue
|
2014-10-22 14:55:13 +00:00
|
|
|
|
|
|
|
onAttach: (storagePartitionId) ->
|
2014-12-09 00:14:12 +00:00
|
|
|
@attributes[webViewConstants.ATTRIBUTE_PARTITION].setValue storagePartitionId
|
2014-10-22 14:55:13 +00:00
|
|
|
|
2014-12-09 01:15:50 +00:00
|
|
|
buildAttachParams: ->
|
2014-12-09 05:06:23 +00:00
|
|
|
params =
|
|
|
|
instanceId: @viewInstanceId
|
|
|
|
userAgentOverride: @userAgentOverride
|
|
|
|
for attributeName, attribute of @attributes
|
|
|
|
params[attributeName] = attribute.getValue()
|
|
|
|
params
|
2014-10-22 14:55:13 +00:00
|
|
|
|
2014-12-09 01:15:50 +00:00
|
|
|
attachWindow: (guestInstanceId) ->
|
2014-10-22 14:55:13 +00:00
|
|
|
@guestInstanceId = guestInstanceId
|
2014-12-09 01:15:50 +00:00
|
|
|
params = @buildAttachParams()
|
2014-10-22 14:55:13 +00:00
|
|
|
|
|
|
|
unless @isPluginInRenderTree()
|
|
|
|
return true
|
|
|
|
|
2014-12-08 16:05:34 +00:00
|
|
|
guestViewInternal.attachGuest @internalInstanceId, @guestInstanceId, params, (w) => @contentWindow = w
|
2014-10-22 14:55:13 +00:00
|
|
|
|
|
|
|
# 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.width = '100%'
|
|
|
|
@style.height = '100%'
|
|
|
|
|
|
|
|
proto.attributeChangedCallback = (name, oldValue, newValue) ->
|
2014-10-22 15:37:27 +00:00
|
|
|
internal = v8Util.getHiddenValue this, 'internal'
|
2014-10-22 14:55:13 +00:00
|
|
|
return unless internal
|
|
|
|
internal.handleBrowserPluginAttributeMutation name, oldValue, newValue
|
|
|
|
|
|
|
|
proto.attachedCallback = ->
|
|
|
|
# Load the plugin immediately.
|
|
|
|
unused = this.nonExistentAttribute
|
|
|
|
|
2014-10-24 10:24:12 +00:00
|
|
|
WebView.BrowserPlugin = webFrame.registerEmbedderCustomElement 'browserplugin',
|
2014-10-22 14:55:13 +00:00
|
|
|
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 WebView(this)
|
|
|
|
|
|
|
|
proto.attributeChangedCallback = (name, oldValue, newValue) ->
|
2014-10-22 15:37:27 +00:00
|
|
|
internal = v8Util.getHiddenValue this, 'internal'
|
2014-10-22 14:55:13 +00:00
|
|
|
return unless internal
|
|
|
|
internal.handleWebviewAttributeMutation name, oldValue, newValue
|
|
|
|
|
|
|
|
proto.detachedCallback = ->
|
2014-10-22 15:37:27 +00:00
|
|
|
internal = v8Util.getHiddenValue this, 'internal'
|
2014-10-22 14:55:13 +00:00
|
|
|
return unless internal
|
|
|
|
internal.elementAttached = false
|
|
|
|
internal.reset()
|
|
|
|
|
|
|
|
proto.attachedCallback = ->
|
2014-10-22 15:37:27 +00:00
|
|
|
internal = v8Util.getHiddenValue this, 'internal'
|
2014-10-22 14:55:13 +00:00
|
|
|
return unless internal
|
|
|
|
unless internal.elementAttached
|
|
|
|
internal.elementAttached = true
|
|
|
|
internal.parseAttributes()
|
|
|
|
|
2014-10-24 12:49:51 +00:00
|
|
|
# Public-facing API methods.
|
|
|
|
methods = [
|
|
|
|
"getUrl"
|
|
|
|
"getTitle"
|
|
|
|
"isLoading"
|
|
|
|
"isWaitingForResponse"
|
|
|
|
"stop"
|
|
|
|
"reload"
|
2014-11-23 05:26:09 +00:00
|
|
|
"reloadIgnoringCache"
|
2014-10-24 12:49:51 +00:00
|
|
|
"canGoBack"
|
|
|
|
"canGoForward"
|
|
|
|
"canGoToOffset"
|
|
|
|
"goBack"
|
|
|
|
"goForward"
|
|
|
|
"goToIndex"
|
|
|
|
"goToOffset"
|
|
|
|
"isCrashed"
|
2014-10-24 13:04:50 +00:00
|
|
|
"setUserAgent"
|
2014-10-24 12:49:51 +00:00
|
|
|
"executeJavaScript"
|
2014-11-14 08:34:14 +00:00
|
|
|
"insertCSS"
|
|
|
|
"openDevTools"
|
|
|
|
"closeDevTools"
|
|
|
|
"isDevToolsOpened"
|
2014-10-24 12:49:51 +00:00
|
|
|
"send"
|
2014-11-14 08:34:14 +00:00
|
|
|
"getId"
|
2014-10-24 12:49:51 +00:00
|
|
|
]
|
|
|
|
|
|
|
|
# Forward proto.foo* method calls to WebView.foo*.
|
|
|
|
createHandler = (m) ->
|
|
|
|
(args...) ->
|
|
|
|
internal = v8Util.getHiddenValue this, 'internal'
|
|
|
|
remote.getGuestWebContents(internal.guestInstanceId)[m] args...
|
|
|
|
proto[m] = createHandler m for m in methods
|
|
|
|
|
2014-10-24 10:44:15 +00:00
|
|
|
window.WebView = webFrame.registerEmbedderCustomElement 'webview',
|
2014-10-22 14:55:13 +00:00
|
|
|
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
|
2014-12-09 00:14:12 +00:00
|
|
|
|
|
|
|
module.exports = WebView
|