diff options
Diffstat (limited to 'extensions/renderer/resources/web_view.js')
-rw-r--r-- | extensions/renderer/resources/web_view.js | 1041 |
1 files changed, 1041 insertions, 0 deletions
diff --git a/extensions/renderer/resources/web_view.js b/extensions/renderer/resources/web_view.js new file mode 100644 index 0000000..610c65f --- /dev/null +++ b/extensions/renderer/resources/web_view.js @@ -0,0 +1,1041 @@ +// 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'); +// TODO(lazyboy): Rename this to WebViewInternal and call WebViewInternal +// something else. +var WebView = require('webViewInternal').WebView; +var WebViewEvents = require('webViewEvents').WebViewEvents; +var guestViewInternalNatives = requireNative('guest_view_internal'); + +var WEB_VIEW_ATTRIBUTE_AUTOSIZE = 'autosize'; +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 AUTO_SIZE_ATTRIBUTES = [ + WEB_VIEW_ATTRIBUTE_AUTOSIZE, + WEB_VIEW_ATTRIBUTE_MAXHEIGHT, + WEB_VIEW_ATTRIBUTE_MAXWIDTH, + WEB_VIEW_ATTRIBUTE_MINHEIGHT, + WEB_VIEW_ATTRIBUTE_MINWIDTH +]; + +var WEB_VIEW_ATTRIBUTE_PARTITION = 'partition'; + +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', +]; + +/** @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.elementAttached = 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(); + this.partition = new Partition(); + + this.setupWebviewNodeAttributes(); + this.setupFocusPropagation(); + this.setupWebviewNodeProperties(); + + this.viewInstanceId = IdGenerator.GetNextId(); + + new WebViewEvents(this, this.viewInstanceId); + + shadowRoot.appendChild(this.browserPluginNode); +} + +/** + * @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.getGuestInstanceId = function() { + return this.guestInstanceId; +}; + +/** + * Resets some state upon reattaching <webview> element to the DOM. + */ +WebViewInternal.prototype.reset = function() { + // 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 (this.guestInstanceId) { + this.guestInstanceId = undefined; + this.beforeFirstNavigation = true; + this.validPartitionId = true; + this.partition.validPartitionId = true; + } + this.internalInstanceId = 0; +}; + +// 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.guestInstanceId) { + return; + } + var args = $Array.concat([this.guestInstanceId], $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.guestInstanceId) { + return; + } + WebView.go(this.guestInstanceId, relativeIndex); +}; + +/** + * @private + */ +WebViewInternal.prototype.print = function() { + this.executeScript({code: 'window.print();'}); +}; + +/** + * @private + */ +WebViewInternal.prototype.reload = function() { + if (!this.guestInstanceId) { + return; + } + WebView.reload(this.guestInstanceId); +}; + +/** + * @private + */ +WebViewInternal.prototype.stop = function() { + if (!this.guestInstanceId) { + return; + } + WebView.stop(this.guestInstanceId); +}; + +/** + * @private + */ +WebViewInternal.prototype.terminate = function() { + if (!this.guestInstanceId) { + return; + } + WebView.terminate(this.guestInstanceId); +}; + +/** + * @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.guestInstanceId) { + throw new Error(ERROR_MSG_CANNOT_INJECT_SCRIPT); + } +}; + +/** + * @private + */ +WebViewInternal.prototype.executeScript = function(var_args) { + this.validateExecuteCodeCall(); + var args = $Array.concat([this.guestInstanceId, 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.guestInstanceId, this.src], + $Array.slice(arguments)); + $Function.apply(WebView.insertCSS, null, args); +}; + +WebViewInternal.prototype.setupAutoSizeProperties = function() { + var self = this; + $Array.forEach(AUTO_SIZE_ATTRIBUTES, function(attributeName) { + this[attributeName] = this.webviewNode.getAttribute(attributeName); + Object.defineProperty(this.webviewNode, attributeName, { + get: function() { + return self[attributeName]; + }, + set: function(value) { + self.webviewNode.setAttribute(attributeName, value); + }, + enumerable: true + }); + }, this); +}; + +/** + * @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.'; + + this.setupAutoSizeProperties(); + 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. + this.srcAndPartitionObserver = new MutationObserver(function(mutations) { + $Array.forEach(mutations, function(mutation) { + var oldValue = mutation.oldValue; + var newValue = this.webviewNode.getAttribute(mutation.attributeName); + if (oldValue != newValue) { + return; + } + this.handleWebviewAttributeMutation( + mutation.attributeName, oldValue, newValue); + }.bind(this)); + }.bind(this)); + 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 (AUTO_SIZE_ATTRIBUTES.indexOf(name) > -1) { + this[name] = newValue; + if (!this.guestInstanceId) { + return; + } + // Convert autosize attribute to boolean. + var autosize = this.webviewNode.hasAttribute(WEB_VIEW_ATTRIBUTE_AUTOSIZE); + GuestViewInternal.setAutoSize(this.guestInstanceId, { + 'enableAutoSize': autosize, + 'min': { + 'width': parseInt(this.minwidth || 0), + 'height': parseInt(this.minheight || 0) + }, + 'max': { + 'width': parseInt(this.maxwidth || 0), + 'height': parseInt(this.maxheight || 0) + } + }); + return; + } else 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.guestInstanceId) { + return; + } + WebView.setName(this.guestInstanceId, 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 == 'internalinstanceid' && !oldValue && !!newValue) { + this.browserPluginNode.removeAttribute('internalinstanceid'); + this.internalInstanceId = parseInt(newValue); + + if (!this.deferredAttachState) { + this.parseAttributes(); + return; + } + + if (!!this.guestInstanceId && this.guestInstanceId != 0) { + window.setTimeout(function() { + var isNewWindow = this.deferredAttachState ? + this.deferredAttachState.isNewWindow : false; + var params = this.buildAttachParams(isNewWindow); + guestViewInternalNatives.AttachGuest( + this.internalInstanceId, + this.guestInstanceId, + params); + }.bind(this), 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(webViewEvent) { + var newWidth = webViewEvent.newWidth; + var newHeight = webViewEvent.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 (!this.webviewNode.hasAttribute(WEB_VIEW_ATTRIBUTE_AUTOSIZE) || + (newWidth >= minWidth && + newWidth <= maxWidth && + newHeight >= minHeight && + 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. + this.dispatchEvent(webViewEvent); + } +}; + +// Returns if <object> is in the render tree. +WebViewInternal.prototype.isPluginInRenderTree = function() { + return !!this.internalInstanceId && this.internalInstanceId != 0; +}; + +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.elementAttached) { + return true; + } + + if (!this.hasGuestInstanceID()) { + if (this.beforeFirstNavigation) { + this.beforeFirstNavigation = false; + this.allocateInstanceId(); + } + return true; + } + + // Navigate to this.src. + WebView.navigate(this.guestInstanceId, 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.guestInstanceId != undefined; +}; + +WebViewInternal.prototype.allocateInstanceId = function() { + var storagePartitionId = + this.webviewNode.getAttribute(WEB_VIEW_ATTRIBUTE_PARTITION) || + this.webviewNode[WEB_VIEW_ATTRIBUTE_PARTITION]; + var params = { + 'storagePartitionId': storagePartitionId, + }; + var self = this; + GuestViewInternal.createGuest( + 'webview', + params, + function(guestInstanceId) { + // TODO(lazyboy): Make sure this.autoNavigate_ stuff correctly updated + // |self.src| at this point. + self.attachWindow(guestInstanceId, 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.onPluginDestroyed = function() { + this.reset(); +}; + +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(); + Object.defineProperty(this.webviewNode, propertyName, { + get: function() { + return this.on[propertyName]; + }.bind(this), + set: function(value) { + if (this.on[propertyName]) + this.webviewNode.removeEventListener(eventName, self.on[propertyName]); + this.on[propertyName] = value; + if (value) + this.webviewNode.addEventListener(eventName, value); + }.bind(this), + 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.guestInstanceId) { + // If we are not attached yet, then we will pick up the user agent on + // attachment. + return; + } + WebView.overrideUserAgent(this.guestInstanceId, userAgentOverride); +}; + +/** @private */ +WebViewInternal.prototype.find = function(search_text, options, callback) { + if (!this.guestInstanceId) { + return; + } + WebView.find(this.guestInstanceId, search_text, options, callback); +}; + +/** @private */ +WebViewInternal.prototype.stopFinding = function(action) { + if (!this.guestInstanceId) { + return; + } + WebView.stopFinding(this.guestInstanceId, action); +}; + +/** @private */ +WebViewInternal.prototype.setZoom = function(zoomFactor, callback) { + if (!this.guestInstanceId) { + return; + } + WebView.setZoom(this.guestInstanceId, zoomFactor, callback); +}; + +WebViewInternal.prototype.getZoom = function(callback) { + if (!this.guestInstanceId) { + return; + } + WebView.getZoom(this.guestInstanceId, callback); +}; + +WebViewInternal.prototype.buildAttachParams = function(isNewWindow) { + var params = { + 'autosize': this.webviewNode.hasAttribute(WEB_VIEW_ATTRIBUTE_AUTOSIZE), + 'instanceId': this.viewInstanceId, + 'maxheight': parseInt(this.maxheight || 0), + 'maxwidth': parseInt(this.maxwidth || 0), + 'minheight': parseInt(this.minheight || 0), + 'minwidth': parseInt(this.minwidth || 0), + '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(guestInstanceId, + isNewWindow) { + this.guestInstanceId = guestInstanceId; + var params = this.buildAttachParams(isNewWindow); + + if (!this.isPluginInRenderTree()) { + this.deferredAttachState = {isNewWindow: isNewWindow}; + return true; + } + + this.deferredAttachState = null; + return guestViewInternalNatives.AttachGuest( + this.internalInstanceId, + this.guestInstanceId, + params); +}; + +// Registers browser plugin <object> custom element. +function registerBrowserPluginElement() { + var proto = Object.create(HTMLObjectElement.prototype); + + proto.createdCallback = function() { + this.setAttribute('type', 'application/browser-plugin'); + this.setAttribute('id', 'browser-plugin-' + IdGenerator.GetNextId()); + // 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('browserplugin', {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.attributeChangedCallback = function(name, oldValue, newValue) { + var internal = privates(this).internal; + if (!internal) { + return; + } + internal.handleWebviewAttributeMutation(name, oldValue, newValue); + }; + + proto.detachedCallback = function() { + var internal = privates(this).internal; + if (!internal) { + return; + } + internal.elementAttached = false; + internal.reset(); + }; + + proto.attachedCallback = function() { + var internal = privates(this).internal; + if (!internal) { + return; + } + if (!internal.elementAttached) { + internal.elementAttached = true; + internal.parseAttributes(); + } + }; + + var methods = [ + 'back', + 'find', + 'forward', + 'canGoBack', + 'canGoForward', + 'clearData', + 'getProcessId', + 'getZoom', + 'go', + 'print', + 'reload', + 'setZoom', + 'stop', + 'stopFinding', + '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 ChromeWebView API is available. + * @private + */ +WebViewInternal.prototype.maybeGetChromeWebViewEvents = function() {}; + +/** + * 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 []; +}; + +/** + * Implemented when the experimental API is available. + * @private + */ +WebViewInternal.prototype.setupExperimentalContextMenus = function() { +}; + +exports.WebView = WebView; +exports.WebViewInternal = WebViewInternal; |