2016-03-25 19:57:17 +00:00
'use strict'
2016-01-13 04:46:13 +00:00
2018-09-13 16:10:51 +00:00
const { ipcRenderer , remote , webFrame } = require ( 'electron' )
2016-01-12 02:40:23 +00:00
2016-03-25 19:57:17 +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
2018-07-10 08:15:40 +00:00
// An unique ID that can represent current context.
2018-07-19 04:29:47 +00:00
const contextId = v8Util . getHiddenValue ( global , 'contextId' )
2018-07-10 08:15:40 +00:00
2016-01-14 18:35:29 +00:00
// ID generator.
2016-11-03 17:39:40 +00:00
let nextId = 0
2016-01-12 02:40:23 +00:00
2016-11-03 17:39:40 +00:00
const getNextId = function ( ) {
2016-03-25 19:57:17 +00:00
return ++ nextId
}
2016-01-12 02:40:23 +00:00
2016-01-14 18:35:29 +00:00
// Represents the internal state of the WebView node.
2016-11-03 17:55:12 +00:00
class WebViewImpl {
constructor ( webviewNode ) {
2016-03-25 19:57:17 +00:00
this . webviewNode = webviewNode
v8Util . setHiddenValue ( this . webviewNode , 'internal' , this )
this . elementAttached = false
this . beforeFirstNavigation = true
2018-08-28 18:35:44 +00:00
this . hasFocus = false
2016-01-12 02:40:23 +00:00
2016-01-14 18:35:29 +00:00
// on* Event handlers.
2016-03-25 19:57:17 +00:00
this . on = { }
2018-08-23 01:45:43 +00:00
2018-08-28 18:35:44 +00:00
// Create internal iframe element.
2018-08-16 22:57:40 +00:00
this . internalElement = this . createInternalElement ( )
2018-09-13 16:10:51 +00:00
const shadowRoot = this . webviewNode . attachShadow ( { mode : 'open' } )
2016-09-22 21:47:20 +00:00
shadowRoot . innerHTML = '<!DOCTYPE html><style type="text/css">:host { display: flex; }</style>'
2016-03-25 19:57:17 +00:00
this . setupWebViewAttributes ( )
this . viewInstanceId = getNextId ( )
2018-08-16 22:57:40 +00:00
shadowRoot . appendChild ( this . internalElement )
2018-08-28 18:35:44 +00:00
// Provide access to contentWindow.
Object . defineProperty ( this . webviewNode , 'contentWindow' , {
get : ( ) => {
return this . internalElement . contentWindow
} ,
enumerable : true
} )
2016-01-12 02:40:23 +00:00
}
2018-08-16 22:57:40 +00:00
createInternalElement ( ) {
const iframeElement = document . createElement ( 'iframe' )
2018-08-31 20:53:13 +00:00
iframeElement . style . flex = '1 1 auto'
2018-08-16 22:57:40 +00:00
iframeElement . style . border = '0'
v8Util . setHiddenValue ( iframeElement , 'internal' , this )
return iframeElement
2016-03-25 19:57:17 +00:00
}
2016-01-12 02:40:23 +00:00
2016-01-14 18:35:29 +00:00
// Resets some state upon reattaching <webview> element to the DOM.
2016-11-03 17:55:12 +00:00
reset ( ) {
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 ) {
2018-09-07 06:41:48 +00:00
guestViewInternal . destroyGuest ( this . guestInstanceId )
2016-03-25 19:57:17 +00:00
this . guestInstanceId = void 0
2016-01-12 02:40:23 +00:00
}
2016-09-08 17:01:01 +00:00
this . webContents = null
this . beforeFirstNavigation = true
this . attributes [ webViewConstants . ATTRIBUTE _PARTITION ] . validPartitionId = true
2016-11-03 00:28:37 +00:00
2018-08-16 22:57:40 +00:00
// Since attachment swaps a local frame for a remote frame, we need our
// internal iframe element to be local again before we can reattach.
const newFrame = this . createInternalElement ( )
const oldFrame = this . internalElement
this . internalElement = newFrame
oldFrame . parentNode . replaceChild ( newFrame , oldFrame )
2016-03-25 19:57:17 +00:00
}
2016-01-12 02:40:23 +00:00
2016-01-14 18:35:29 +00:00
// Sets the <webview>.request property.
2016-11-03 17:55:12 +00:00
setRequestPropertyOnWebViewNode ( request ) {
2016-11-03 17:19:52 +00:00
Object . defineProperty ( this . webviewNode , 'request' , {
2016-01-12 02:40:23 +00:00
value : request ,
enumerable : true
2016-03-25 19:57:17 +00:00
} )
}
2016-01-12 02:40:23 +00:00
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-11-03 17:55:12 +00:00
handleWebviewAttributeMutation ( attributeName , oldValue , newValue ) {
2016-01-12 02:40:23 +00:00
if ( ! this . attributes [ attributeName ] || this . attributes [ attributeName ] . ignoreMutation ) {
2016-03-25 19:57:17 +00:00
return
2016-01-12 02:40:23 +00:00
}
2016-03-25 19:57:17 +00:00
// Let the changed attribute handle its own mutation
2016-11-03 17:19:52 +00:00
this . attributes [ attributeName ] . handleMutation ( oldValue , newValue )
2016-03-25 19:57:17 +00:00
}
2016-01-12 02:40:23 +00:00
2018-08-16 22:57:40 +00:00
onElementResize ( ) {
2018-08-23 06:42:35 +00:00
const resizeEvent = new Event ( 'resize' )
2018-08-16 22:57:40 +00:00
resizeEvent . newWidth = this . webviewNode . clientWidth
resizeEvent . newHeight = this . webviewNode . clientHeight
2016-03-25 19:57:17 +00:00
this . dispatchEvent ( resizeEvent )
}
2016-01-12 02:40:23 +00:00
2016-11-03 17:55:12 +00:00
createGuest ( ) {
2016-03-10 19:54:17 +00:00
return guestViewInternal . createGuest ( this . buildParams ( ) , ( event , guestInstanceId ) => {
2016-09-08 17:01:01 +00:00
this . attachGuestInstance ( guestInstanceId )
2016-03-25 19:57:17 +00:00
} )
}
2016-01-12 02:40:23 +00:00
2018-07-21 02:11:28 +00:00
createGuestSync ( ) {
2018-08-16 22:57:40 +00:00
this . beforeFirstNavigation = false
2018-07-21 02:11:28 +00:00
this . attachGuestInstance ( guestViewInternal . createGuestSync ( this . buildParams ( ) ) )
}
2016-11-03 17:55:12 +00:00
dispatchEvent ( webViewEvent ) {
2016-11-03 17:19:52 +00:00
this . webviewNode . dispatchEvent ( webViewEvent )
2016-03-25 19:57:17 +00:00
}
2016-01-12 02:40:23 +00:00
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-11-03 17:55:12 +00:00
setupEventProperty ( eventName ) {
2016-11-03 17:39:40 +00:00
const propertyName = ` on ${ eventName . toLowerCase ( ) } `
2016-01-12 02:40:23 +00:00
return Object . defineProperty ( this . webviewNode , propertyName , {
2016-03-10 19:54:17 +00:00
get : ( ) => {
2016-03-25 19:57:17 +00:00
return this . on [ propertyName ]
2016-03-10 19:54:17 +00:00
} ,
set : ( value ) => {
if ( this . on [ propertyName ] ) {
2016-03-25 19:57:17 +00:00
this . webviewNode . removeEventListener ( eventName , this . on [ propertyName ] )
2016-03-10 19:54:17 +00:00
}
2016-03-25 19:57:17 +00:00
this . on [ propertyName ] = value
2016-03-10 19:54:17 +00:00
if ( value ) {
2016-03-25 19:57:17 +00:00
return this . webviewNode . addEventListener ( eventName , value )
2016-03-10 19:54:17 +00:00
}
} ,
2016-01-12 02:40:23 +00:00
enumerable : true
2016-03-25 19:57:17 +00:00
} )
}
2016-01-12 02:40:23 +00:00
2016-01-14 18:35:29 +00:00
// Updates state upon loadcommit.
2016-11-03 17:55:12 +00:00
onLoadCommit ( webViewEvent ) {
2016-11-03 17:39:40 +00:00
const oldValue = this . webviewNode . getAttribute ( webViewConstants . ATTRIBUTE _SRC )
const newValue = webViewEvent . url
2016-01-12 02:40:23 +00:00
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-11-03 17:39:40 +00:00
this . attributes [ webViewConstants . ATTRIBUTE _SRC ] . setValueIgnoreMutation ( newValue )
2016-01-12 02:40:23 +00:00
}
2016-03-25 19:57:17 +00:00
}
2016-01-12 02:40:23 +00:00
2018-08-28 18:35:44 +00:00
// Emits focus/blur events.
onFocusChange ( ) {
const hasFocus = document . activeElement === this . webviewNode
if ( hasFocus !== this . hasFocus ) {
this . hasFocus = hasFocus
this . dispatchEvent ( new Event ( hasFocus ? 'focus' : 'blur' ) )
}
}
2016-11-03 17:55:12 +00:00
onAttach ( storagePartitionId ) {
2016-03-25 19:57:17 +00:00
return this . attributes [ webViewConstants . ATTRIBUTE _PARTITION ] . setValue ( storagePartitionId )
}
2016-01-12 02:40:23 +00:00
2016-11-03 17:55:12 +00:00
buildParams ( ) {
2016-11-03 17:39:40 +00:00
const params = {
2016-01-12 02:40:23 +00:00
instanceId : this . viewInstanceId ,
2017-01-30 17:06:50 +00:00
userAgentOverride : this . userAgentOverride
2016-03-25 19:57:17 +00:00
}
2016-11-03 17:39:40 +00:00
for ( const attributeName in this . attributes ) {
2018-08-16 22:57:40 +00:00
if ( this . attributes . hasOwnProperty ( attributeName ) ) {
2016-11-03 17:39:40 +00:00
params [ attributeName ] = this . attributes [ attributeName ] . getValue ( )
}
2016-01-12 02:40:23 +00:00
}
2016-03-25 19:57:17 +00:00
return params
}
2016-11-03 17:55:12 +00:00
attachGuestInstance ( guestInstanceId ) {
2018-08-16 22:57:40 +00:00
if ( ! this . elementAttached ) {
// The element could be detached before we got response from browser.
return
}
this . internalInstanceId = getNextId ( )
2016-03-25 19:57:17 +00:00
this . guestInstanceId = guestInstanceId
this . webContents = remote . getGuestWebContents ( this . guestInstanceId )
2018-08-16 22:57:40 +00:00
guestViewInternal . attachGuest ( this . internalInstanceId , this . guestInstanceId , this . buildParams ( ) , this . internalElement . contentWindow )
// ResizeObserver is a browser global not recognized by "standard".
/* globals ResizeObserver */
// TODO(zcbenz): Should we deprecate the "resize" event? Wait, it is not
// even documented.
this . resizeObserver = new ResizeObserver ( this . onElementResize . bind ( this ) ) . observe ( this . internalElement )
2016-03-25 19:57:17 +00:00
}
}
2016-01-12 02:40:23 +00:00
2016-01-14 18:35:29 +00:00
// Registers <webview> custom element.
2017-02-22 22:15:59 +00:00
const registerWebViewElement = function ( ) {
2016-11-03 17:39:40 +00:00
const proto = Object . create ( HTMLObjectElement . prototype )
2016-03-25 19:57:17 +00:00
proto . createdCallback = function ( ) {
return new WebViewImpl ( this )
}
proto . attributeChangedCallback = function ( name , oldValue , newValue ) {
2017-02-22 22:15:59 +00:00
const internal = v8Util . getHiddenValue ( this , 'internal' )
2016-11-03 17:19:52 +00:00
if ( internal ) {
internal . handleWebviewAttributeMutation ( name , oldValue , newValue )
2016-01-12 02:40:23 +00:00
}
2016-03-25 19:57:17 +00:00
}
proto . detachedCallback = function ( ) {
2017-02-22 22:15:59 +00:00
const internal = v8Util . getHiddenValue ( this , 'internal' )
2016-01-12 02:40:23 +00:00
if ( ! internal ) {
2016-03-25 19:57:17 +00:00
return
2016-01-12 02:40:23 +00:00
}
2016-03-25 19:57:17 +00:00
guestViewInternal . deregisterEvents ( internal . viewInstanceId )
internal . elementAttached = false
2016-09-08 17:01:01 +00:00
this . internalInstanceId = 0
2016-11-03 00:28:37 +00:00
internal . reset ( )
2016-03-25 19:57:17 +00:00
}
proto . attachedCallback = function ( ) {
2017-02-22 22:15:59 +00:00
const internal = v8Util . getHiddenValue ( this , 'internal' )
2016-01-12 02:40:23 +00:00
if ( ! internal ) {
2016-03-25 19:57:17 +00:00
return
2016-01-12 02:40:23 +00:00
}
if ( ! internal . elementAttached ) {
2016-03-25 19:57:17 +00:00
guestViewInternal . registerEvents ( internal , internal . viewInstanceId )
internal . elementAttached = true
2018-08-16 22:57:40 +00:00
internal . attributes [ webViewConstants . ATTRIBUTE _SRC ] . parse ( )
2016-01-12 02:40:23 +00:00
}
2016-03-25 19:57:17 +00:00
}
2016-01-12 02:40:23 +00:00
2016-01-14 18:35:29 +00:00
// Public-facing API methods.
2016-11-03 17:39:40 +00:00
const methods = [
2016-01-14 19:10:12 +00:00
'getURL' ,
2016-01-21 21:31:35 +00:00
'loadURL' ,
2016-01-14 19:10:12 +00:00
'getTitle' ,
'isLoading' ,
2016-04-18 17:37:08 +00:00
'isLoadingMainFrame' ,
2016-01-14 19:10:12 +00:00
'isWaitingForResponse' ,
'stop' ,
'reload' ,
'reloadIgnoringCache' ,
'canGoBack' ,
'canGoForward' ,
'canGoToOffset' ,
'clearHistory' ,
'goBack' ,
'goForward' ,
'goToIndex' ,
'goToOffset' ,
'isCrashed' ,
'setUserAgent' ,
'getUserAgent' ,
'openDevTools' ,
'closeDevTools' ,
'isDevToolsOpened' ,
'isDevToolsFocused' ,
'inspectElement' ,
'setAudioMuted' ,
'isAudioMuted' ,
2018-07-12 11:35:11 +00:00
'isCurrentlyAudible' ,
2016-01-14 19:10:12 +00:00
'undo' ,
'redo' ,
'cut' ,
'copy' ,
'paste' ,
'pasteAndMatchStyle' ,
'delete' ,
'selectAll' ,
'unselect' ,
'replace' ,
'replaceMisspelling' ,
'findInPage' ,
'stopFindInPage' ,
'downloadURL' ,
'inspectServiceWorker' ,
'print' ,
2016-06-02 17:12:38 +00:00
'printToPDF' ,
2016-07-05 22:43:57 +00:00
'showDefinitionForSelection' ,
2017-02-17 15:48:30 +00:00
'capturePage' ,
'setZoomFactor' ,
'setZoomLevel' ,
'getZoomLevel' ,
'getZoomFactor'
2016-03-25 19:57:17 +00:00
]
2016-11-03 17:39:40 +00:00
const nonblockMethods = [
2016-01-13 03:55:49 +00:00
'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-11-21 19:59:27 +00:00
'setLayoutZoomLevelLimits' ,
2018-02-20 13:57:48 +00:00
'setVisualZoomLevelLimits'
2016-03-25 19:57:17 +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-11-03 17:39:40 +00:00
const createBlockHandler = function ( m ) {
2016-03-25 19:57:17 +00:00
return function ( ... args ) {
const internal = v8Util . getHiddenValue ( this , 'internal' )
2016-02-05 18:55:32 +00:00
if ( internal . webContents ) {
2016-11-03 18:37:18 +00:00
return internal . webContents [ m ] ( ... args )
2016-02-05 18:55:32 +00:00
} else {
2016-03-25 19:57:17 +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-03-25 19:57:17 +00:00
}
}
2016-11-03 18:09:53 +00:00
for ( const method of methods ) {
2016-11-03 17:39:40 +00:00
proto [ method ] = createBlockHandler ( method )
2016-03-25 19:57:17 +00:00
}
2016-11-03 18:09:53 +00:00
2016-11-03 17:39:40 +00:00
const createNonBlockHandler = function ( m ) {
2016-03-25 19:57:17 +00:00
return function ( ... args ) {
const internal = v8Util . getHiddenValue ( this , 'internal' )
2018-07-10 08:15:40 +00:00
ipcRenderer . send ( 'ELECTRON_BROWSER_ASYNC_CALL_TO_GUEST_VIEW' , contextId , null , internal . guestInstanceId , m , ... args )
2016-03-25 19:57:17 +00:00
}
2016-01-12 02:40:23 +00:00
}
2016-11-03 18:09:53 +00:00
for ( const method of nonblockMethods ) {
2016-11-03 17:39:40 +00:00
proto [ method ] = createNonBlockHandler ( method )
2016-01-12 02:40:23 +00:00
}
2016-03-25 19:57:17 +00:00
proto . executeJavaScript = function ( code , hasUserGesture , callback ) {
2016-11-03 17:39:40 +00:00
const internal = v8Util . getHiddenValue ( this , 'internal' )
2016-03-25 19:57:17 +00:00
if ( typeof hasUserGesture === 'function' ) {
callback = hasUserGesture
hasUserGesture = false
2016-02-25 18:05:01 +00:00
}
2016-11-03 17:39:40 +00:00
const requestId = getNextId ( )
2018-07-10 08:15:40 +00:00
ipcRenderer . send ( 'ELECTRON_BROWSER_ASYNC_CALL_TO_GUEST_VIEW' , contextId , requestId , internal . guestInstanceId , 'executeJavaScript' , code , hasUserGesture )
2016-04-06 23:21:26 +00:00
ipcRenderer . once ( ` ELECTRON_RENDERER_ASYNC_CALL_TO_GUEST_VIEW_RESPONSE_ ${ requestId } ` , function ( event , result ) {
2016-03-29 00:35:49 +00:00
if ( callback ) callback ( result )
2016-03-25 19:57:17 +00:00
} )
}
2016-02-17 17:03:27 +00:00
2016-02-17 08:52:19 +00:00
// WebContents associated with this webview.
2016-03-25 19:57:17 +00:00
proto . getWebContents = function ( ) {
2018-07-21 02:11:28 +00:00
const internal = v8Util . getHiddenValue ( this , 'internal' )
if ( ! internal . webContents ) {
internal . createGuestSync ( )
}
return internal . webContents
2016-03-25 19:57:17 +00:00
}
2016-02-17 08:52:19 +00:00
2018-09-03 02:41:54 +00:00
// Focusing the webview should move page focus to the underlying iframe.
proto . focus = function ( ) {
this . contentWindow . focus ( )
}
2016-01-12 02:40:23 +00:00
window . WebView = webFrame . registerEmbedderCustomElement ( 'webview' , {
prototype : proto
2016-03-25 19:57:17 +00:00
} )
2016-01-12 02:40:23 +00:00
2016-01-14 19:10:12 +00:00
// Delete the callbacks so developers cannot call them and produce unexpected
// behavior.
2016-03-25 19:57:17 +00:00
delete proto . createdCallback
delete proto . attachedCallback
delete proto . detachedCallback
2016-11-03 17:19:52 +00:00
delete proto . attributeChangedCallback
2016-03-25 19:57:17 +00:00
}
2016-01-12 02:40:23 +00:00
2016-11-03 17:39:40 +00:00
const useCapture = true
2016-01-12 02:40:23 +00:00
2016-11-03 17:39:40 +00:00
const listener = function ( event ) {
2016-01-12 02:40:23 +00:00
if ( document . readyState === 'loading' ) {
2016-03-25 19:57:17 +00:00
return
2016-01-12 02:40:23 +00:00
}
2016-03-25 19:57:17 +00:00
registerWebViewElement ( )
2016-11-03 17:19:52 +00:00
window . removeEventListener ( event . type , listener , useCapture )
2016-03-25 19:57:17 +00:00
}
2016-01-12 02:40:23 +00:00
2016-03-25 19:57:17 +00:00
window . addEventListener ( 'readystatechange' , listener , true )
2016-01-12 02:40:23 +00:00
2016-03-25 19:57:17 +00:00
module . exports = WebViewImpl