| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| // This module implements Webview (<webview>) as a custom element that wraps a |
| // BrowserPlugin object element. The object element is hidden within |
| // the shadow DOM of the Webview element. |
| |
| var DocumentNatives = requireNative('document_natives'); |
| var GuestViewInternal = |
| require('binding').Binding.create('guestViewInternal').generate(); |
| var IdGenerator = requireNative('id_generator'); |
| var WebView = require('webview').WebView; |
| var WebViewEvents = require('webViewEvents').WebViewEvents; |
| |
| var WEB_VIEW_ATTRIBUTE_MAXHEIGHT = 'maxheight'; |
| var WEB_VIEW_ATTRIBUTE_MAXWIDTH = 'maxwidth'; |
| var WEB_VIEW_ATTRIBUTE_MINHEIGHT = 'minheight'; |
| var WEB_VIEW_ATTRIBUTE_MINWIDTH = 'minwidth'; |
| var WEB_VIEW_ATTRIBUTE_PARTITION = 'partition'; |
| |
| var PLUGIN_METHOD_ATTACH = '-internal-attach'; |
| |
| var ERROR_MSG_ALREADY_NAVIGATED = |
| 'The object has already navigated, so its partition cannot be changed.'; |
| var ERROR_MSG_INVALID_PARTITION_ATTRIBUTE = 'Invalid partition attribute.'; |
| |
| /** @type {Array.<string>} */ |
| var WEB_VIEW_ATTRIBUTES = [ |
| 'allowtransparency', |
| 'autosize', |
| WEB_VIEW_ATTRIBUTE_MINHEIGHT, |
| WEB_VIEW_ATTRIBUTE_MINWIDTH, |
| WEB_VIEW_ATTRIBUTE_MAXHEIGHT, |
| WEB_VIEW_ATTRIBUTE_MAXWIDTH |
| ]; |
| |
| /** @class representing state of storage partition. */ |
| function Partition() { |
| this.validPartitionId = true; |
| this.persistStorage = false; |
| this.storagePartitionId = ''; |
| }; |
| |
| Partition.prototype.toAttribute = function() { |
| if (!this.validPartitionId) { |
| return ''; |
| } |
| return (this.persistStorage ? 'persist:' : '') + this.storagePartitionId; |
| }; |
| |
| Partition.prototype.fromAttribute = function(value, hasNavigated) { |
| var result = {}; |
| if (hasNavigated) { |
| result.error = ERROR_MSG_ALREADY_NAVIGATED; |
| return result; |
| } |
| if (!value) { |
| value = ''; |
| } |
| |
| var LEN = 'persist:'.length; |
| if (value.substr(0, LEN) == 'persist:') { |
| value = value.substr(LEN); |
| if (!value) { |
| this.validPartitionId = false; |
| result.error = ERROR_MSG_INVALID_PARTITION_ATTRIBUTE; |
| return result; |
| } |
| this.persistStorage = true; |
| } else { |
| this.persistStorage = false; |
| } |
| |
| this.storagePartitionId = value; |
| return result; |
| }; |
| |
| // Implemented when the experimental API is available. |
| WebViewInternal.maybeRegisterExperimentalAPIs = function(proto) {} |
| |
| /** |
| * @constructor |
| */ |
| function WebViewInternal(webviewNode) { |
| privates(webviewNode).internal = this; |
| this.webviewNode = webviewNode; |
| this.attached = false; |
| |
| this.beforeFirstNavigation = true; |
| this.validPartitionId = true; |
| // Used to save some state upon deferred attachment. |
| // If <object> bindings is not available, we defer attachment. |
| // This state contains whether or not the attachment request was for |
| // newwindow. |
| this.deferredAttachState = null; |
| |
| // on* Event handlers. |
| this.on = {}; |
| |
| this.browserPluginNode = this.createBrowserPluginNode(); |
| var shadowRoot = this.webviewNode.createShadowRoot(); |
| shadowRoot.appendChild(this.browserPluginNode); |
| |
| this.setupWebviewNodeAttributes(); |
| this.setupFocusPropagation(); |
| this.setupWebviewNodeProperties(); |
| |
| this.viewInstanceId = IdGenerator.GetNextId(); |
| |
| this.partition = new Partition(); |
| this.parseAttributes(); |
| |
| new WebViewEvents(this, this.viewInstanceId); |
| } |
| |
| /** |
| * @private |
| */ |
| WebViewInternal.prototype.createBrowserPluginNode = function() { |
| // We create BrowserPlugin as a custom element in order to observe changes |
| // to attributes synchronously. |
| var browserPluginNode = new WebViewInternal.BrowserPlugin(); |
| privates(browserPluginNode).internal = this; |
| |
| $Array.forEach(WEB_VIEW_ATTRIBUTES, function(attributeName) { |
| // Only copy attributes that have been assigned values, rather than copying |
| // a series of undefined attributes to BrowserPlugin. |
| if (this.webviewNode.hasAttribute(attributeName)) { |
| browserPluginNode.setAttribute( |
| attributeName, this.webviewNode.getAttribute(attributeName)); |
| } else if (this.webviewNode[attributeName]){ |
| // Reading property using has/getAttribute does not work on |
| // document.DOMContentLoaded event (but works on |
| // window.DOMContentLoaded event). |
| // So copy from property if copying from attribute fails. |
| browserPluginNode.setAttribute( |
| attributeName, this.webviewNode[attributeName]); |
| } |
| }, this); |
| |
| return browserPluginNode; |
| }; |
| |
| WebViewInternal.prototype.getInstanceId = function() { |
| return this.instanceId; |
| }; |
| |
| /** |
| * Resets some state upon reattaching <webview> element to the DOM. |
| */ |
| WebViewInternal.prototype.resetUponReattachment = function() { |
| this.instanceId = undefined; |
| this.beforeFirstNavigation = true; |
| this.validPartitionId = true; |
| this.partition.validPartitionId = true; |
| }; |
| |
| // Sets <webview>.request property. |
| WebViewInternal.prototype.setRequestPropertyOnWebViewNode = function(request) { |
| Object.defineProperty( |
| this.webviewNode, |
| 'request', |
| { |
| value: request, |
| enumerable: true |
| } |
| ); |
| }; |
| |
| WebViewInternal.prototype.setupFocusPropagation = function() { |
| if (!this.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. |
| this.webviewNode.setAttribute('tabIndex', -1); |
| } |
| var self = this; |
| this.webviewNode.addEventListener('focus', function(e) { |
| // Focus the BrowserPlugin when the <webview> takes focus. |
| self.browserPluginNode.focus(); |
| }); |
| this.webviewNode.addEventListener('blur', function(e) { |
| // Blur the BrowserPlugin when the <webview> loses focus. |
| self.browserPluginNode.blur(); |
| }); |
| }; |
| |
| /** |
| * @private |
| */ |
| WebViewInternal.prototype.back = function() { |
| return this.go(-1); |
| }; |
| |
| /** |
| * @private |
| */ |
| WebViewInternal.prototype.forward = function() { |
| return this.go(1); |
| }; |
| |
| /** |
| * @private |
| */ |
| WebViewInternal.prototype.canGoBack = function() { |
| return this.entryCount > 1 && this.currentEntryIndex > 0; |
| }; |
| |
| /** |
| * @private |
| */ |
| WebViewInternal.prototype.canGoForward = function() { |
| return this.currentEntryIndex >= 0 && |
| this.currentEntryIndex < (this.entryCount - 1); |
| }; |
| |
| /** |
| * @private |
| */ |
| WebViewInternal.prototype.clearData = function() { |
| if (!this.instanceId) { |
| return; |
| } |
| var args = $Array.concat([this.instanceId], $Array.slice(arguments)); |
| $Function.apply(WebView.clearData, null, args); |
| }; |
| |
| /** |
| * @private |
| */ |
| WebViewInternal.prototype.getProcessId = function() { |
| return this.processId; |
| }; |
| |
| /** |
| * @private |
| */ |
| WebViewInternal.prototype.go = function(relativeIndex) { |
| if (!this.instanceId) { |
| return; |
| } |
| WebView.go(this.instanceId, relativeIndex); |
| }; |
| |
| /** |
| * @private |
| */ |
| WebViewInternal.prototype.reload = function() { |
| if (!this.instanceId) { |
| return; |
| } |
| WebView.reload(this.instanceId); |
| }; |
| |
| /** |
| * @private |
| */ |
| WebViewInternal.prototype.stop = function() { |
| if (!this.instanceId) { |
| return; |
| } |
| WebView.stop(this.instanceId); |
| }; |
| |
| /** |
| * @private |
| */ |
| WebViewInternal.prototype.terminate = function() { |
| if (!this.instanceId) { |
| return; |
| } |
| WebView.terminate(this.instanceId); |
| }; |
| |
| /** |
| * @private |
| */ |
| WebViewInternal.prototype.validateExecuteCodeCall = function() { |
| var ERROR_MSG_CANNOT_INJECT_SCRIPT = '<webview>: ' + |
| 'Script cannot be injected into content until the page has loaded.'; |
| if (!this.instanceId) { |
| throw new Error(ERROR_MSG_CANNOT_INJECT_SCRIPT); |
| } |
| }; |
| |
| /** |
| * @private |
| */ |
| WebViewInternal.prototype.executeScript = function(var_args) { |
| this.validateExecuteCodeCall(); |
| var args = $Array.concat([this.instanceId, this.src], |
| $Array.slice(arguments)); |
| $Function.apply(WebView.executeScript, null, args); |
| }; |
| |
| /** |
| * @private |
| */ |
| WebViewInternal.prototype.insertCSS = function(var_args) { |
| this.validateExecuteCodeCall(); |
| var args = $Array.concat([this.instanceId, this.src], |
| $Array.slice(arguments)); |
| $Function.apply(WebView.insertCSS, null, args); |
| }; |
| |
| /** |
| * @private |
| */ |
| WebViewInternal.prototype.setupWebviewNodeProperties = function() { |
| var ERROR_MSG_CONTENTWINDOW_NOT_AVAILABLE = '<webview>: ' + |
| 'contentWindow is not available at this time. It will become available ' + |
| 'when the page has finished loading.'; |
| |
| var self = this; |
| var browserPluginNode = this.browserPluginNode; |
| // Expose getters and setters for the attributes. |
| $Array.forEach(WEB_VIEW_ATTRIBUTES, function(attributeName) { |
| Object.defineProperty(this.webviewNode, attributeName, { |
| get: function() { |
| if (browserPluginNode.hasOwnProperty(attributeName)) { |
| return browserPluginNode[attributeName]; |
| } else { |
| return browserPluginNode.getAttribute(attributeName); |
| } |
| }, |
| set: function(value) { |
| if (browserPluginNode.hasOwnProperty(attributeName)) { |
| // Give the BrowserPlugin first stab at the attribute so that it can |
| // throw an exception if there is a problem. This attribute will then |
| // be propagated back to the <webview>. |
| browserPluginNode[attributeName] = value; |
| } else { |
| browserPluginNode.setAttribute(attributeName, value); |
| } |
| }, |
| enumerable: true |
| }); |
| }, this); |
| |
| // <webview> src does not quite behave the same as BrowserPlugin src, and so |
| // we don't simply keep the two in sync. |
| this.src = this.webviewNode.getAttribute('src'); |
| Object.defineProperty(this.webviewNode, 'src', { |
| get: function() { |
| return self.src; |
| }, |
| set: function(value) { |
| self.webviewNode.setAttribute('src', value); |
| }, |
| // No setter. |
| enumerable: true |
| }); |
| |
| Object.defineProperty(this.webviewNode, 'name', { |
| get: function() { |
| return self.name; |
| }, |
| set: function(value) { |
| self.webviewNode.setAttribute('name', value); |
| }, |
| enumerable: true |
| }); |
| |
| Object.defineProperty(this.webviewNode, 'partition', { |
| get: function() { |
| return self.partition.toAttribute(); |
| }, |
| set: function(value) { |
| var result = self.partition.fromAttribute(value, self.hasNavigated()); |
| if (result.error) { |
| throw result.error; |
| } |
| self.webviewNode.setAttribute('partition', value); |
| }, |
| enumerable: true |
| }); |
| |
| // We cannot use {writable: true} property descriptor because we want a |
| // dynamic getter value. |
| Object.defineProperty(this.webviewNode, 'contentWindow', { |
| get: function() { |
| if (browserPluginNode.contentWindow) |
| return browserPluginNode.contentWindow; |
| window.console.error(ERROR_MSG_CONTENTWINDOW_NOT_AVAILABLE); |
| }, |
| // No setter. |
| enumerable: true |
| }); |
| }; |
| |
| /** |
| * @private |
| */ |
| WebViewInternal.prototype.setupWebviewNodeAttributes = function() { |
| this.setupWebViewSrcAttributeMutationObserver(); |
| }; |
| |
| /** |
| * @private |
| */ |
| WebViewInternal.prototype.setupWebViewSrcAttributeMutationObserver = |
| function() { |
| // The purpose of this mutation observer is to catch assignment to the src |
| // attribute without any changes to its value. This is useful in the case |
| // where the webview guest has crashed and navigating to the same address |
| // spawns off a new process. |
| var self = this; |
| this.srcAndPartitionObserver = new MutationObserver(function(mutations) { |
| $Array.forEach(mutations, function(mutation) { |
| var oldValue = mutation.oldValue; |
| var newValue = self.webviewNode.getAttribute(mutation.attributeName); |
| if (oldValue != newValue) { |
| return; |
| } |
| self.handleWebviewAttributeMutation( |
| mutation.attributeName, oldValue, newValue); |
| }); |
| }); |
| var params = { |
| attributes: true, |
| attributeOldValue: true, |
| attributeFilter: ['src', 'partition'] |
| }; |
| this.srcAndPartitionObserver.observe(this.webviewNode, params); |
| }; |
| |
| /** |
| * @private |
| */ |
| WebViewInternal.prototype.handleWebviewAttributeMutation = |
| function(name, oldValue, newValue) { |
| // 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. |
| if (name == 'name') { |
| // We treat null attribute (attribute removed) and the empty string as |
| // one case. |
| oldValue = oldValue || ''; |
| newValue = newValue || ''; |
| |
| if (oldValue === newValue) { |
| return; |
| } |
| this.name = newValue; |
| if (!this.instanceId) { |
| return; |
| } |
| WebView.setName(this.instanceId, newValue); |
| return; |
| } else if (name == 'src') { |
| // We treat null attribute (attribute removed) and the empty string as |
| // one case. |
| oldValue = oldValue || ''; |
| newValue = newValue || ''; |
| // Once we have navigated, we don't allow clearing the src attribute. |
| // Once <webview> enters a navigated state, it cannot be return back to a |
| // placeholder state. |
| if (newValue == '' && oldValue != '') { |
| // src attribute changes normally initiate a navigation. We suppress |
| // the next src attribute handler call to avoid reloading the page |
| // on every guest-initiated navigation. |
| this.ignoreNextSrcAttributeChange = true; |
| this.webviewNode.setAttribute('src', oldValue); |
| return; |
| } |
| this.src = newValue; |
| if (this.ignoreNextSrcAttributeChange) { |
| // Don't allow the src mutation observer to see this change. |
| this.srcAndPartitionObserver.takeRecords(); |
| this.ignoreNextSrcAttributeChange = false; |
| return; |
| } |
| var result = {}; |
| this.parseSrcAttribute(result); |
| |
| if (result.error) { |
| throw result.error; |
| } |
| } else if (name == 'partition') { |
| // Note that throwing error here won't synchronously propagate. |
| this.partition.fromAttribute(newValue, this.hasNavigated()); |
| } |
| |
| // No <webview> -> <object> mutation propagation for these attributes. |
| if (name == 'src' || name == 'partition') { |
| return; |
| } |
| |
| if (this.browserPluginNode.hasOwnProperty(name)) { |
| this.browserPluginNode[name] = newValue; |
| } else { |
| this.browserPluginNode.setAttribute(name, newValue); |
| } |
| }; |
| |
| /** |
| * @private |
| */ |
| WebViewInternal.prototype.handleBrowserPluginAttributeMutation = |
| function(name, oldValue, newValue) { |
| if (name == 'internalbindings' && !oldValue && newValue) { |
| this.browserPluginNode.removeAttribute('internalbindings'); |
| |
| if (this.deferredAttachState) { |
| var self = this; |
| // A setTimeout is necessary for the binding to be initialized properly. |
| window.setTimeout(function() { |
| if (self.hasBindings()) { |
| var params = self.buildAttachParams( |
| self.deferredAttachState.isNewWindow); |
| self.browserPluginNode[PLUGIN_METHOD_ATTACH](self.instanceId, params); |
| self.deferredAttachState = null; |
| } |
| }, 0); |
| } |
| return; |
| } |
| |
| // This observer monitors mutations to attributes of the BrowserPlugin and |
| // updates the <webview> attributes accordingly. |
| // |newValue| is null if the attribute |name| has been removed. |
| if (newValue != null) { |
| // Update the <webview> attribute to match the BrowserPlugin attribute. |
| // Note: Calling setAttribute on <webview> will trigger its mutation |
| // observer which will then propagate that attribute to BrowserPlugin. In |
| // cases where we permit assigning a BrowserPlugin attribute the same value |
| // again (such as navigation when crashed), this could end up in an infinite |
| // loop. Thus, we avoid this loop by only updating the <webview> attribute |
| // if the BrowserPlugin attributes differs from it. |
| if (newValue != this.webviewNode.getAttribute(name)) { |
| this.webviewNode.setAttribute(name, newValue); |
| } |
| } else { |
| // If an attribute is removed from the BrowserPlugin, then remove it |
| // from the <webview> as well. |
| this.webviewNode.removeAttribute(name); |
| } |
| }; |
| |
| WebViewInternal.prototype.onSizeChanged = function(newWidth, newHeight) { |
| var node = this.webviewNode; |
| |
| var width = node.offsetWidth; |
| var height = node.offsetHeight; |
| |
| // Check the current bounds to make sure we do not resize <webview> |
| // outside of current constraints. |
| var maxWidth; |
| if (node.hasAttribute(WEB_VIEW_ATTRIBUTE_MAXWIDTH) && |
| node[WEB_VIEW_ATTRIBUTE_MAXWIDTH]) { |
| maxWidth = node[WEB_VIEW_ATTRIBUTE_MAXWIDTH]; |
| } else { |
| maxWidth = width; |
| } |
| |
| var minWidth; |
| if (node.hasAttribute(WEB_VIEW_ATTRIBUTE_MINWIDTH) && |
| node[WEB_VIEW_ATTRIBUTE_MINWIDTH]) { |
| minWidth = node[WEB_VIEW_ATTRIBUTE_MINWIDTH]; |
| } else { |
| minWidth = width; |
| } |
| if (minWidth > maxWidth) { |
| minWidth = maxWidth; |
| } |
| |
| var maxHeight; |
| if (node.hasAttribute(WEB_VIEW_ATTRIBUTE_MAXHEIGHT) && |
| node[WEB_VIEW_ATTRIBUTE_MAXHEIGHT]) { |
| maxHeight = node[WEB_VIEW_ATTRIBUTE_MAXHEIGHT]; |
| } else { |
| maxHeight = height; |
| } |
| var minHeight; |
| if (node.hasAttribute(WEB_VIEW_ATTRIBUTE_MINHEIGHT) && |
| node[WEB_VIEW_ATTRIBUTE_MINHEIGHT]) { |
| minHeight = node[WEB_VIEW_ATTRIBUTE_MINHEIGHT]; |
| } else { |
| minHeight = height; |
| } |
| if (minHeight > maxHeight) { |
| minHeight = maxHeight; |
| } |
| |
| if (newWidth >= minWidth && |
| newWidth <= maxWidth && |
| newHeight >= minHeight && |
| newHeight <= maxHeight) { |
| node.style.width = newWidth + 'px'; |
| node.style.height = newHeight + 'px'; |
| } |
| }; |
| |
| // Returns true if Browser Plugin bindings is available. |
| // Bindings are unavailable if <object> is not in the render tree. |
| WebViewInternal.prototype.hasBindings = function() { |
| return 'function' == typeof this.browserPluginNode[PLUGIN_METHOD_ATTACH]; |
| }; |
| |
| WebViewInternal.prototype.hasNavigated = function() { |
| return !this.beforeFirstNavigation; |
| }; |
| |
| /** @return {boolean} */ |
| WebViewInternal.prototype.parseSrcAttribute = function(result) { |
| if (!this.partition.validPartitionId) { |
| result.error = ERROR_MSG_INVALID_PARTITION_ATTRIBUTE; |
| return false; |
| } |
| this.src = this.webviewNode.getAttribute('src'); |
| |
| if (!this.src) { |
| return true; |
| } |
| |
| if (!this.hasGuestInstanceID()) { |
| if (this.beforeFirstNavigation) { |
| this.beforeFirstNavigation = false; |
| this.allocateInstanceId(); |
| } |
| return true; |
| } |
| |
| // Navigate to this.src. |
| WebView.navigate(this.instanceId, this.src); |
| return true; |
| }; |
| |
| /** @return {boolean} */ |
| WebViewInternal.prototype.parseAttributes = function() { |
| var hasNavigated = this.hasNavigated(); |
| var attributeValue = this.webviewNode.getAttribute('partition'); |
| var result = this.partition.fromAttribute(attributeValue, hasNavigated); |
| return this.parseSrcAttribute(result); |
| }; |
| |
| WebViewInternal.prototype.hasGuestInstanceID = function() { |
| return this.instanceId != undefined; |
| }; |
| |
| WebViewInternal.prototype.allocateInstanceId = function() { |
| // Parse .src and .partition. |
| var self = this; |
| GuestViewInternal.allocateInstanceId( |
| function(instanceId) { |
| // TODO(lazyboy): Make sure this.autoNavigate_ stuff correctly updated |
| // |self.src| at this point. |
| self.attachWindow(instanceId, false); |
| }); |
| }; |
| |
| WebViewInternal.prototype.onFrameNameChanged = function(name) { |
| this.name = name || ''; |
| if (this.name === '') { |
| this.webviewNode.removeAttribute('name'); |
| } else { |
| this.webviewNode.setAttribute('name', this.name); |
| } |
| }; |
| |
| WebViewInternal.prototype.dispatchEvent = function(webViewEvent) { |
| return this.webviewNode.dispatchEvent(webViewEvent); |
| }; |
| |
| /** |
| * Adds an 'on<event>' property on the webview, which can be used to set/unset |
| * an event handler. |
| */ |
| WebViewInternal.prototype.setupEventProperty = function(eventName) { |
| var propertyName = 'on' + eventName.toLowerCase(); |
| var self = this; |
| var webviewNode = this.webviewNode; |
| Object.defineProperty(webviewNode, propertyName, { |
| get: function() { |
| return self.on[propertyName]; |
| }, |
| set: function(value) { |
| if (self.on[propertyName]) |
| webviewNode.removeEventListener(eventName, self.on[propertyName]); |
| self.on[propertyName] = value; |
| if (value) |
| webviewNode.addEventListener(eventName, value); |
| }, |
| enumerable: true |
| }); |
| }; |
| |
| // Updates state upon loadcommit. |
| WebViewInternal.prototype.onLoadCommit = function( |
| currentEntryIndex, entryCount, processId, url, isTopLevel) { |
| this.currentEntryIndex = currentEntryIndex; |
| this.entryCount = entryCount; |
| this.processId = processId; |
| var oldValue = this.webviewNode.getAttribute('src'); |
| var newValue = url; |
| if (isTopLevel && (oldValue != newValue)) { |
| // Touching the src attribute triggers a navigation. To avoid |
| // triggering a page reload on every guest-initiated navigation, |
| // we use the flag ignoreNextSrcAttributeChange here. |
| this.ignoreNextSrcAttributeChange = true; |
| this.webviewNode.setAttribute('src', newValue); |
| } |
| }; |
| |
| WebViewInternal.prototype.onAttach = function(storagePartitionId) { |
| this.webviewNode.setAttribute('partition', storagePartitionId); |
| this.partition.fromAttribute(storagePartitionId, this.hasNavigated()); |
| }; |
| |
| |
| /** @private */ |
| WebViewInternal.prototype.getUserAgent = function() { |
| return this.userAgentOverride || navigator.userAgent; |
| }; |
| |
| /** @private */ |
| WebViewInternal.prototype.isUserAgentOverridden = function() { |
| return !!this.userAgentOverride && |
| this.userAgentOverride != navigator.userAgent; |
| }; |
| |
| /** @private */ |
| WebViewInternal.prototype.setUserAgentOverride = function(userAgentOverride) { |
| this.userAgentOverride = userAgentOverride; |
| if (!this.instanceId) { |
| // If we are not attached yet, then we will pick up the user agent on |
| // attachment. |
| return; |
| } |
| WebView.overrideUserAgent(this.instanceId, userAgentOverride); |
| }; |
| |
| WebViewInternal.prototype.buildAttachParams = function(isNewWindow) { |
| var params = { |
| 'api': 'webview', |
| 'instanceId': this.viewInstanceId, |
| 'name': this.name, |
| // We don't need to navigate new window from here. |
| 'src': isNewWindow ? undefined : this.src, |
| // If we have a partition from the opener, that will also be already |
| // set via this.onAttach(). |
| 'storagePartitionId': this.partition.toAttribute(), |
| 'userAgentOverride': this.userAgentOverride |
| }; |
| return params; |
| }; |
| |
| WebViewInternal.prototype.attachWindow = function(instanceId, isNewWindow) { |
| this.instanceId = instanceId; |
| var params = this.buildAttachParams(isNewWindow); |
| |
| if (!this.hasBindings()) { |
| // No bindings means that the plugin isn't there (display: none), we defer |
| // attachWindow in this case. |
| this.deferredAttachState = {isNewWindow: isNewWindow}; |
| return false; |
| } |
| |
| this.deferredAttachState = null; |
| return this.browserPluginNode[PLUGIN_METHOD_ATTACH](this.instanceId, params); |
| }; |
| |
| // Registers browser plugin <object> custom element. |
| function registerBrowserPluginElement() { |
| var proto = Object.create(HTMLObjectElement.prototype); |
| |
| proto.createdCallback = function() { |
| this.setAttribute('type', 'application/browser-plugin'); |
| // The <object> node fills in the <webview> container. |
| this.style.width = '100%'; |
| this.style.height = '100%'; |
| }; |
| |
| proto.attributeChangedCallback = function(name, oldValue, newValue) { |
| var internal = privates(this).internal; |
| if (!internal) { |
| return; |
| } |
| internal.handleBrowserPluginAttributeMutation(name, oldValue, newValue); |
| }; |
| |
| proto.attachedCallback = function() { |
| // Load the plugin immediately. |
| var unused = this.nonExistentAttribute; |
| }; |
| |
| WebViewInternal.BrowserPlugin = |
| DocumentNatives.RegisterElement('browser-plugin', {extends: 'object', |
| prototype: proto}); |
| |
| delete proto.createdCallback; |
| delete proto.attachedCallback; |
| delete proto.detachedCallback; |
| delete proto.attributeChangedCallback; |
| } |
| |
| // Registers <webview> custom element. |
| function registerWebViewElement() { |
| var proto = Object.create(HTMLElement.prototype); |
| |
| proto.createdCallback = function() { |
| new WebViewInternal(this); |
| }; |
| |
| proto.customElementDetached = false; |
| |
| proto.attributeChangedCallback = function(name, oldValue, newValue) { |
| var internal = privates(this).internal; |
| if (!internal) { |
| return; |
| } |
| internal.handleWebviewAttributeMutation(name, oldValue, newValue); |
| }; |
| |
| proto.detachedCallback = function() { |
| this.customElementDetached = true; |
| }; |
| |
| proto.attachedCallback = function() { |
| if (this.customElementDetached) { |
| var webViewInternal = privates(this).internal; |
| webViewInternal.resetUponReattachment(); |
| webViewInternal.allocateInstanceId(); |
| } |
| this.customElementDetached = false; |
| }; |
| |
| var methods = [ |
| 'back', |
| 'forward', |
| 'canGoBack', |
| 'canGoForward', |
| 'clearData', |
| 'getProcessId', |
| 'go', |
| 'reload', |
| 'stop', |
| 'terminate', |
| 'executeScript', |
| 'insertCSS', |
| 'getUserAgent', |
| 'isUserAgentOverridden', |
| 'setUserAgentOverride' |
| ]; |
| |
| // Forward proto.foo* method calls to WebViewInternal.foo*. |
| for (var i = 0; methods[i]; ++i) { |
| var createHandler = function(m) { |
| return function(var_args) { |
| var internal = privates(this).internal; |
| return $Function.apply(internal[m], internal, arguments); |
| }; |
| }; |
| proto[methods[i]] = createHandler(methods[i]); |
| } |
| |
| WebViewInternal.maybeRegisterExperimentalAPIs(proto); |
| |
| window.WebView = |
| DocumentNatives.RegisterElement('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; |
| } |
| |
| var useCapture = true; |
| window.addEventListener('readystatechange', function listener(event) { |
| if (document.readyState == 'loading') |
| return; |
| |
| registerBrowserPluginElement(); |
| registerWebViewElement(); |
| window.removeEventListener(event.type, listener, useCapture); |
| }, useCapture); |
| |
| /** |
| * Implemented when the experimental API is available. |
| * @private |
| */ |
| WebViewInternal.prototype.maybeGetExperimentalEvents = function() {}; |
| |
| /** |
| * Implemented when the experimental API is available. |
| * @private |
| */ |
| WebViewInternal.prototype.maybeGetExperimentalPermissions = function() { |
| return []; |
| }; |
| |
| /** |
| * Calls to show contextmenu right away instead of dispatching a 'contextmenu' |
| * event. |
| * This will be overridden in web_view_experimental.js to implement contextmenu |
| * API. |
| */ |
| WebViewInternal.prototype.maybeHandleContextMenu = function(e, webViewEvent) { |
| var requestId = e.requestId; |
| // Setting |params| = undefined will show the context menu unmodified, hence |
| // the 'contextmenu' API is disabled for stable channel. |
| var params = undefined; |
| WebView.showContextMenu(this.instanceId, requestId, params); |
| }; |
| |
| /** |
| * Implemented when the experimental API is available. |
| * @private |
| */ |
| WebViewInternal.prototype.setupExperimentalContextMenus = function() {}; |
| |
| exports.WebView = WebView; |
| exports.WebViewInternal = WebViewInternal; |