2016-02-01 19:46:02 +00:00
'use strict' ;
2016-01-13 04:46:13 +00:00
2016-01-14 22:20:06 +00:00
const deprecate = require ( 'electron' ) . deprecate ;
const webFrame = require ( 'electron' ) . webFrame ;
const remote = require ( 'electron' ) . remote ;
const ipcRenderer = require ( 'electron' ) . ipcRenderer ;
2016-01-12 02:40:23 +00:00
2016-01-14 22:20:06 +00:00
const v8Util = process . atomBinding ( 'v8_util' ) ;
const guestViewInternal = require ( './guest-view-internal' ) ;
const webViewConstants = require ( './web-view-constants' ) ;
2016-01-12 02:40:23 +00:00
2016-01-14 22:20:06 +00:00
var hasProp = { } . hasOwnProperty ;
var slice = [ ] . slice ;
2016-01-12 02:40:23 +00:00
2016-01-14 18:35:29 +00:00
// ID generator.
2016-01-14 22:20:06 +00:00
var nextId = 0 ;
2016-01-12 02:40:23 +00:00
2016-01-14 22:20:06 +00:00
var getNextId = function ( ) {
2016-01-12 02:40:23 +00:00
return ++ nextId ;
} ;
2016-01-14 18:35:29 +00:00
// Represents the internal state of the WebView node.
2016-01-14 22:20:06 +00:00
var WebViewImpl = ( function ( ) {
2016-01-12 02:40:23 +00:00
function WebViewImpl ( webviewNode ) {
var shadowRoot ;
this . webviewNode = webviewNode ;
v8Util . setHiddenValue ( this . webviewNode , 'internal' , this ) ;
this . attached = false ;
this . elementAttached = false ;
this . beforeFirstNavigation = true ;
2016-01-14 18:35:29 +00:00
// on* Event handlers.
2016-01-12 02:40:23 +00:00
this . on = { } ;
this . browserPluginNode = this . createBrowserPluginNode ( ) ;
shadowRoot = this . webviewNode . createShadowRoot ( ) ;
2016-03-03 22:22:20 +00:00
shadowRoot . innerHTML = '<style>:host { display: flex; }</style>' ;
2016-01-12 02:40:23 +00:00
this . setupWebViewAttributes ( ) ;
this . setupFocusPropagation ( ) ;
this . viewInstanceId = getNextId ( ) ;
shadowRoot . appendChild ( this . browserPluginNode ) ;
2016-01-13 04:46:13 +00:00
// Subscribe to host's zoom level changes.
2016-01-13 06:58:16 +00:00
this . onZoomLevelChanged = ( zoomLevel ) => {
2016-01-13 04:46:13 +00:00
this . webviewNode . setZoomLevel ( zoomLevel ) ;
2016-01-19 22:53:59 +00:00
} ;
2016-01-13 06:58:16 +00:00
webFrame . on ( 'zoom-level-changed' , this . onZoomLevelChanged ) ;
2016-01-12 02:40:23 +00:00
}
WebViewImpl . prototype . createBrowserPluginNode = function ( ) {
2016-01-14 19:10:12 +00:00
// We create BrowserPlugin as a custom element in order to observe changes
// to attributes synchronously.
2016-01-12 02:40:23 +00:00
var browserPluginNode ;
browserPluginNode = new WebViewImpl . BrowserPlugin ( ) ;
v8Util . setHiddenValue ( browserPluginNode , 'internal' , this ) ;
return browserPluginNode ;
} ;
2016-01-14 18:35:29 +00:00
// Resets some state upon reattaching <webview> element to the DOM.
2016-01-12 02:40:23 +00:00
WebViewImpl . prototype . reset = function ( ) {
2016-01-13 06:58:16 +00:00
// Unlisten the zoom-level-changed event.
webFrame . removeListener ( 'zoom-level-changed' , this . onZoomLevelChanged ) ;
2016-01-12 02:40:23 +00:00
2016-01-14 19:10:12 +00:00
// 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.
2016-01-12 02:40:23 +00:00
if ( this . guestInstanceId ) {
guestViewInternal . destroyGuest ( this . guestInstanceId ) ;
this . webContents = null ;
this . guestInstanceId = void 0 ;
this . beforeFirstNavigation = true ;
this . attributes [ webViewConstants . ATTRIBUTE _PARTITION ] . validPartitionId = true ;
}
return this . internalInstanceId = 0 ;
} ;
2016-01-14 18:35:29 +00:00
// Sets the <webview>.request property.
2016-01-12 02:40:23 +00:00
WebViewImpl . prototype . setRequestPropertyOnWebViewNode = function ( request ) {
return Object . defineProperty ( this . webviewNode , 'request' , {
value : request ,
enumerable : true
} ) ;
} ;
WebViewImpl . prototype . setupFocusPropagation = function ( ) {
if ( ! this . webviewNode . hasAttribute ( 'tabIndex' ) ) {
2016-01-14 19:10:12 +00:00
// <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.
2016-01-12 02:40:23 +00:00
this . webviewNode . setAttribute ( 'tabIndex' , - 1 ) ;
}
this . webviewNode . addEventListener ( 'focus' , ( function ( _this ) {
2016-01-19 22:49:40 +00:00
return function ( ) {
2016-01-14 18:35:29 +00:00
// Focus the BrowserPlugin when the <webview> takes focus.
2016-01-12 02:40:23 +00:00
return _this . browserPluginNode . focus ( ) ;
} ;
} ) ( this ) ) ;
return this . webviewNode . addEventListener ( 'blur' , ( function ( _this ) {
2016-01-19 22:49:40 +00:00
return function ( ) {
2016-01-14 18:35:29 +00:00
// Blur the BrowserPlugin when the <webview> loses focus.
2016-01-12 02:40:23 +00:00
return _this . browserPluginNode . blur ( ) ;
} ;
} ) ( this ) ) ;
} ;
2016-01-14 19:10:12 +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.
2016-01-12 02:40:23 +00:00
WebViewImpl . prototype . handleWebviewAttributeMutation = function ( attributeName , oldValue , newValue ) {
if ( ! this . attributes [ attributeName ] || this . attributes [ attributeName ] . ignoreMutation ) {
return ;
}
2016-01-14 18:35:29 +00:00
// Let the changed attribute handle its own mutation;
2016-01-12 02:40:23 +00:00
return this . attributes [ attributeName ] . handleMutation ( oldValue , newValue ) ;
} ;
WebViewImpl . prototype . handleBrowserPluginAttributeMutation = function ( attributeName , oldValue , newValue ) {
if ( attributeName === webViewConstants . ATTRIBUTE _INTERNALINSTANCEID && ! oldValue && ! ! newValue ) {
this . browserPluginNode . removeAttribute ( webViewConstants . ATTRIBUTE _INTERNALINSTANCEID ) ;
this . internalInstanceId = parseInt ( newValue ) ;
2016-01-14 18:35:29 +00:00
// Track when the element resizes using the element resize callback.
2016-01-12 02:40:23 +00:00
webFrame . registerElementResizeCallback ( this . internalInstanceId , this . onElementResize . bind ( this ) ) ;
if ( ! this . guestInstanceId ) {
return ;
}
return guestViewInternal . attachGuest ( this . internalInstanceId , this . guestInstanceId , this . buildParams ( ) ) ;
}
} ;
WebViewImpl . prototype . onSizeChanged = function ( webViewEvent ) {
2016-01-19 22:49:40 +00:00
var maxHeight , maxWidth , minHeight , minWidth , newHeight , newWidth , node , width ;
2016-01-12 02:40:23 +00:00
newWidth = webViewEvent . newWidth ;
newHeight = webViewEvent . newHeight ;
node = this . webviewNode ;
width = node . offsetWidth ;
2016-01-14 18:35:29 +00:00
// Check the current bounds to make sure we do not resize <webview>
// outside of current constraints.
2016-01-12 02:40:23 +00:00
maxWidth = this . attributes [ webViewConstants . ATTRIBUTE _MAXWIDTH ] . getValue ( ) | width ;
maxHeight = this . attributes [ webViewConstants . ATTRIBUTE _MAXHEIGHT ] . getValue ( ) | width ;
minWidth = this . attributes [ webViewConstants . ATTRIBUTE _MINWIDTH ] . getValue ( ) | width ;
minHeight = this . attributes [ webViewConstants . ATTRIBUTE _MINHEIGHT ] . getValue ( ) | width ;
minWidth = Math . min ( minWidth , maxWidth ) ;
minHeight = Math . min ( minHeight , maxHeight ) ;
if ( ! this . attributes [ webViewConstants . ATTRIBUTE _AUTOSIZE ] . getValue ( ) || ( newWidth >= minWidth && newWidth <= maxWidth && newHeight >= minHeight && newHeight <= maxHeight ) ) {
node . style . width = newWidth + 'px' ;
node . style . height = newHeight + 'px' ;
2016-01-14 19:10:12 +00:00
// Only fire the DOM event if the size of the <webview> has actually
// changed.
2016-01-12 02:40:23 +00:00
return this . dispatchEvent ( webViewEvent ) ;
}
} ;
WebViewImpl . prototype . onElementResize = function ( newSize ) {
2016-01-14 18:35:29 +00:00
// Dispatch the 'resize' event.
2016-01-12 02:40:23 +00:00
var resizeEvent ;
resizeEvent = new Event ( 'resize' , {
bubbles : true
} ) ;
resizeEvent . newWidth = newSize . width ;
resizeEvent . newHeight = newSize . height ;
this . dispatchEvent ( resizeEvent ) ;
if ( this . guestInstanceId ) {
return guestViewInternal . setSize ( this . guestInstanceId , {
normal : newSize
} ) ;
}
} ;
WebViewImpl . prototype . createGuest = function ( ) {
return guestViewInternal . createGuest ( this . buildParams ( ) , ( function ( _this ) {
return function ( event , guestInstanceId ) {
return _this . attachWindow ( guestInstanceId ) ;
} ;
} ) ( this ) ) ;
} ;
WebViewImpl . prototype . dispatchEvent = function ( webViewEvent ) {
return this . webviewNode . dispatchEvent ( webViewEvent ) ;
} ;
2016-01-14 18:35:29 +00:00
// Adds an 'on<event>' property on the webview, which can be used to set/unset
// an event handler.
2016-01-12 02:40:23 +00:00
WebViewImpl . prototype . setupEventProperty = function ( eventName ) {
var propertyName ;
propertyName = 'on' + eventName . toLowerCase ( ) ;
return Object . defineProperty ( this . webviewNode , propertyName , {
get : ( function ( _this ) {
return function ( ) {
return _this . on [ propertyName ] ;
} ;
} ) ( this ) ,
set : ( function ( _this ) {
return function ( value ) {
if ( _this . on [ propertyName ] ) {
_this . webviewNode . removeEventListener ( eventName , _this . on [ propertyName ] ) ;
}
_this . on [ propertyName ] = value ;
if ( value ) {
return _this . webviewNode . addEventListener ( eventName , value ) ;
}
} ;
} ) ( this ) ,
enumerable : true
} ) ;
} ;
2016-01-14 18:35:29 +00:00
// Updates state upon loadcommit.
2016-01-12 02:40:23 +00:00
WebViewImpl . prototype . onLoadCommit = function ( webViewEvent ) {
var newValue , oldValue ;
oldValue = this . webviewNode . getAttribute ( webViewConstants . ATTRIBUTE _SRC ) ;
newValue = webViewEvent . url ;
if ( webViewEvent . isMainFrame && ( oldValue !== newValue ) ) {
2016-01-14 19:10:12 +00:00
// Touching the src attribute triggers a navigation. To avoid
// triggering a page reload on every guest-initiated navigation,
// we do not handle this mutation.
2016-01-12 02:40:23 +00:00
return this . attributes [ webViewConstants . ATTRIBUTE _SRC ] . setValueIgnoreMutation ( newValue ) ;
}
} ;
WebViewImpl . prototype . onAttach = function ( storagePartitionId ) {
return this . attributes [ webViewConstants . ATTRIBUTE _PARTITION ] . setValue ( storagePartitionId ) ;
} ;
WebViewImpl . prototype . buildParams = function ( ) {
var attribute , attributeName , css , elementRect , params , ref1 ;
params = {
instanceId : this . viewInstanceId ,
userAgentOverride : this . userAgentOverride
} ;
ref1 = this . attributes ;
for ( attributeName in ref1 ) {
if ( ! hasProp . call ( ref1 , attributeName ) ) continue ;
attribute = ref1 [ attributeName ] ;
params [ attributeName ] = attribute . getValue ( ) ;
}
2016-01-14 19:10:12 +00:00
// 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.
2016-01-12 02:40:23 +00:00
css = window . getComputedStyle ( this . webviewNode , null ) ;
elementRect = this . webviewNode . getBoundingClientRect ( ) ;
params . elementWidth = parseInt ( elementRect . width ) || parseInt ( css . getPropertyValue ( 'width' ) ) ;
params . elementHeight = parseInt ( elementRect . height ) || parseInt ( css . getPropertyValue ( 'height' ) ) ;
return params ;
} ;
WebViewImpl . prototype . attachWindow = function ( guestInstanceId ) {
this . guestInstanceId = guestInstanceId ;
this . webContents = remote . getGuestWebContents ( this . guestInstanceId ) ;
if ( ! this . internalInstanceId ) {
return true ;
}
return guestViewInternal . attachGuest ( this . internalInstanceId , this . guestInstanceId , this . buildParams ( ) ) ;
} ;
return WebViewImpl ;
} ) ( ) ;
2016-01-14 18:35:29 +00:00
// Registers browser plugin <object> custom element.
2016-01-14 22:20:06 +00:00
var registerBrowserPluginElement = function ( ) {
2016-01-12 02:40:23 +00:00
var proto ;
proto = Object . create ( HTMLObjectElement . prototype ) ;
proto . createdCallback = function ( ) {
this . setAttribute ( 'type' , 'application/browser-plugin' ) ;
this . setAttribute ( 'id' , 'browser-plugin-' + getNextId ( ) ) ;
2016-01-14 18:35:29 +00:00
// The <object> node fills in the <webview> container.
2016-03-03 22:22:20 +00:00
return this . style . flex = '1 1 auto' ;
2016-01-12 02:40:23 +00:00
} ;
proto . attributeChangedCallback = function ( name , oldValue , newValue ) {
var internal ;
internal = v8Util . getHiddenValue ( this , 'internal' ) ;
if ( ! internal ) {
return ;
}
return internal . handleBrowserPluginAttributeMutation ( name , oldValue , newValue ) ;
} ;
proto . attachedCallback = function ( ) {
2016-01-14 18:35:29 +00:00
// Load the plugin immediately.
2016-01-19 22:49:40 +00:00
return this . nonExistentAttribute ;
2016-01-12 02:40:23 +00:00
} ;
WebViewImpl . BrowserPlugin = webFrame . registerEmbedderCustomElement ( 'browserplugin' , {
"extends" : 'object' ,
prototype : proto
} ) ;
delete proto . createdCallback ;
delete proto . attachedCallback ;
delete proto . detachedCallback ;
return delete proto . attributeChangedCallback ;
} ;
2016-01-14 18:35:29 +00:00
// Registers <webview> custom element.
2016-01-14 22:20:06 +00:00
var registerWebViewElement = function ( ) {
2016-02-25 18:05:01 +00:00
var createBlockHandler , createNonBlockHandler , i , j , len , len1 , m , methods , nonblockMethods , proto ;
2016-01-12 02:40:23 +00:00
proto = Object . create ( HTMLObjectElement . prototype ) ;
proto . createdCallback = function ( ) {
return new WebViewImpl ( this ) ;
} ;
proto . attributeChangedCallback = function ( name , oldValue , newValue ) {
var internal ;
internal = v8Util . getHiddenValue ( this , 'internal' ) ;
if ( ! internal ) {
return ;
}
return internal . handleWebviewAttributeMutation ( name , oldValue , newValue ) ;
} ;
proto . detachedCallback = function ( ) {
var internal ;
internal = v8Util . getHiddenValue ( this , 'internal' ) ;
if ( ! internal ) {
return ;
}
guestViewInternal . deregisterEvents ( internal . viewInstanceId ) ;
internal . elementAttached = false ;
return internal . reset ( ) ;
} ;
proto . attachedCallback = function ( ) {
var internal ;
internal = v8Util . getHiddenValue ( this , 'internal' ) ;
if ( ! internal ) {
return ;
}
if ( ! internal . elementAttached ) {
guestViewInternal . registerEvents ( internal , internal . viewInstanceId ) ;
internal . elementAttached = true ;
return internal . attributes [ webViewConstants . ATTRIBUTE _SRC ] . parse ( ) ;
}
} ;
2016-01-14 18:35:29 +00:00
// Public-facing API methods.
2016-01-14 19:10:12 +00:00
methods = [
'getURL' ,
2016-01-21 21:31:35 +00:00
'loadURL' ,
2016-01-14 19:10:12 +00:00
'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' ,
2016-02-17 08:52:19 +00:00
'printToPDF' ,
2016-01-14 19:10:12 +00:00
] ;
2016-01-13 03:55:49 +00:00
nonblockMethods = [
'insertCSS' ,
2016-02-25 18:05:01 +00:00
'insertText' ,
2016-01-13 03:55:49 +00:00
'send' ,
2016-02-22 14:00:21 +00:00
'sendInputEvent' ,
2016-01-13 04:46:13 +00:00
'setZoomFactor' ,
'setZoomLevel' ,
2016-02-22 14:00:21 +00:00
'setZoomLevelLimits' ,
2016-01-13 03:55:49 +00:00
] ;
2016-01-12 02:40:23 +00:00
2016-01-14 18:35:29 +00:00
// Forward proto.foo* method calls to WebViewImpl.foo*.
2016-01-12 02:40:23 +00:00
createBlockHandler = function ( m ) {
return function ( ) {
2016-02-05 18:55:32 +00:00
var args = 1 <= arguments . length ? slice . call ( arguments , 0 ) : [ ] ;
var internal = v8Util . getHiddenValue ( this , 'internal' ) ;
if ( internal . webContents ) {
return internal . webContents [ m ] . apply ( internal . webContents , args ) ;
} else {
2016-02-11 21:32:34 +00:00
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. ` ) ;
2016-02-05 18:55:32 +00:00
}
2016-01-12 02:40:23 +00:00
} ;
} ;
for ( i = 0 , len = methods . length ; i < len ; i ++ ) {
m = methods [ i ] ;
proto [ m ] = createBlockHandler ( m ) ;
}
createNonBlockHandler = function ( m ) {
return function ( ) {
var args , internal ;
args = 1 <= arguments . length ? slice . call ( arguments , 0 ) : [ ] ;
internal = v8Util . getHiddenValue ( this , 'internal' ) ;
2016-02-25 18:05:01 +00:00
return ipcRenderer . send . apply ( ipcRenderer , [ 'ATOM_BROWSER_ASYNC_CALL_TO_GUEST_VIEW' , null , internal . guestInstanceId , m ] . concat ( slice . call ( args ) ) ) ;
2016-01-12 02:40:23 +00:00
} ;
} ;
for ( j = 0 , len1 = nonblockMethods . length ; j < len1 ; j ++ ) {
m = nonblockMethods [ j ] ;
proto [ m ] = createNonBlockHandler ( m ) ;
}
2016-02-25 18:05:01 +00:00
proto . executeJavaScript = function ( code , hasUserGesture , callback ) {
var internal = v8Util . getHiddenValue ( this , 'internal' ) ;
if ( typeof hasUserGesture === "function" ) {
callback = hasUserGesture ;
hasUserGesture = false ;
}
let requestId = getNextId ( ) ;
ipcRenderer . send ( 'ATOM_BROWSER_ASYNC_CALL_TO_GUEST_VIEW' , requestId , internal . guestInstanceId , "executeJavaScript" , code , hasUserGesture ) ;
ipcRenderer . once ( ` ATOM_RENDERER_ASYNC_CALL_TO_GUEST_VIEW_RESPONSE_ ${ requestId } ` , function ( event , result ) {
if ( callback )
callback ( result ) ;
} ) ;
} ;
2016-02-17 17:03:27 +00:00
2016-02-17 08:52:19 +00:00
// WebContents associated with this webview.
proto . getWebContents = function ( ) {
var internal = v8Util . getHiddenValue ( this , 'internal' ) ;
return internal . webContents ;
} ;
2016-01-14 18:35:29 +00:00
// Deprecated.
2016-01-12 02:40:23 +00:00
deprecate . rename ( proto , 'getUrl' , 'getURL' ) ;
window . WebView = webFrame . registerEmbedderCustomElement ( 'webview' , {
prototype : proto
} ) ;
2016-01-14 19:10:12 +00:00
// Delete the callbacks so developers cannot call them and produce unexpected
// behavior.
2016-01-12 02:40:23 +00:00
delete proto . createdCallback ;
delete proto . attachedCallback ;
delete proto . detachedCallback ;
return delete proto . attributeChangedCallback ;
} ;
2016-01-14 22:20:06 +00:00
var useCapture = true ;
2016-01-12 02:40:23 +00:00
2016-01-14 22:20:06 +00:00
var listener = function ( event ) {
2016-01-12 02:40:23 +00:00
if ( document . readyState === 'loading' ) {
return ;
}
registerBrowserPluginElement ( ) ;
registerWebViewElement ( ) ;
return window . removeEventListener ( event . type , listener , useCapture ) ;
} ;
window . addEventListener ( 'readystatechange' , listener , true ) ;
module . exports = WebViewImpl ;