summaryrefslogtreecommitdiffstats
path: root/extensions/renderer
diff options
context:
space:
mode:
authorlfg <lfg@chromium.org>2014-09-11 18:54:25 -0700
committerCommit bot <commit-bot@chromium.org>2014-09-12 01:57:08 +0000
commita71ea760fdfc9ca27ce29e160fb393c961555465 (patch)
tree349cc1193729e9ccb8f9e044b2e548fed08f0615 /extensions/renderer
parent81d302348beed094f26e2cd8b647266eac8ef534 (diff)
downloadchromium_src-a71ea760fdfc9ca27ce29e160fb393c961555465.zip
chromium_src-a71ea760fdfc9ca27ce29e160fb393c961555465.tar.gz
chromium_src-a71ea760fdfc9ca27ce29e160fb393c961555465.tar.bz2
Moving web_view.js to extensions.
BUG=352293 Review URL: https://codereview.chromium.org/564913003 Cr-Commit-Position: refs/heads/master@{#294519}
Diffstat (limited to 'extensions/renderer')
-rw-r--r--extensions/renderer/dispatcher.cc7
-rw-r--r--extensions/renderer/resources/extensions_renderer_resources.grd4
-rw-r--r--extensions/renderer/resources/web_view.js1041
-rw-r--r--extensions/renderer/resources/web_view_deny.js38
-rw-r--r--extensions/renderer/resources/web_view_events.js621
-rw-r--r--extensions/renderer/resources/web_view_experimental.js31
6 files changed, 1742 insertions, 0 deletions
diff --git a/extensions/renderer/dispatcher.cc b/extensions/renderer/dispatcher.cc
index 10b26c5..163218c 100644
--- a/extensions/renderer/dispatcher.cc
+++ b/extensions/renderer/dispatcher.cc
@@ -519,8 +519,15 @@ std::vector<std::pair<std::string, int> > Dispatcher::GetJsResources() {
IDR_UNCAUGHT_EXCEPTION_HANDLER_JS));
resources.push_back(std::make_pair("unload_event", IDR_UNLOAD_EVENT_JS));
resources.push_back(std::make_pair("utils", IDR_UTILS_JS));
+ // Note: webView not webview so that this doesn't interfere with the
+ // chrome.webview API bindings.
+ resources.push_back(std::make_pair("webView", IDR_WEB_VIEW_JS));
+ resources.push_back(std::make_pair("webViewEvents", IDR_WEB_VIEW_EVENTS_JS));
+ resources.push_back(
+ std::make_pair("webViewExperimental", IDR_WEB_VIEW_EXPERIMENTAL_JS));
resources.push_back(std::make_pair("webViewInternal",
IDR_WEB_VIEW_INTERNAL_CUSTOM_BINDINGS_JS));
+ resources.push_back(std::make_pair("denyWebView", IDR_WEB_VIEW_DENY_JS));
resources.push_back(
std::make_pair(mojo::kBufferModuleName, IDR_MOJO_BUFFER_JS));
resources.push_back(
diff --git a/extensions/renderer/resources/extensions_renderer_resources.grd b/extensions/renderer/resources/extensions_renderer_resources.grd
index 488443c..3f5d610 100644
--- a/extensions/renderer/resources/extensions_renderer_resources.grd
+++ b/extensions/renderer/resources/extensions_renderer_resources.grd
@@ -31,7 +31,11 @@
<include name="IDR_UNCAUGHT_EXCEPTION_HANDLER_JS" file="uncaught_exception_handler.js" type="BINDATA" />
<include name="IDR_UNLOAD_EVENT_JS" file="unload_event.js" type="BINDATA" />
<include name="IDR_UTILS_JS" file="utils.js" type="BINDATA" />
+ <include name="IDR_WEB_VIEW_DENY_JS" file="web_view_deny.js" type="BINDATA" />
+ <include name="IDR_WEB_VIEW_EVENTS_JS" file="web_view_events.js" type="BINDATA" />
+ <include name="IDR_WEB_VIEW_EXPERIMENTAL_JS" file="web_view_experimental.js" type="BINDATA" />
<include name="IDR_WEB_VIEW_INTERNAL_CUSTOM_BINDINGS_JS" file="web_view_internal.js" type="BINDATA" />
+ <include name="IDR_WEB_VIEW_JS" file="web_view.js" type="BINDATA" />
<!-- Custom bindings for APIs. -->
<include name="IDR_APP_RUNTIME_CUSTOM_BINDINGS_JS" file="app_runtime_custom_bindings.js" type="BINDATA" />
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;
diff --git a/extensions/renderer/resources/web_view_deny.js b/extensions/renderer/resources/web_view_deny.js
new file mode 100644
index 0000000..a3e70f4
--- /dev/null
+++ b/extensions/renderer/resources/web_view_deny.js
@@ -0,0 +1,38 @@
+// 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.
+
+var DocumentNatives = requireNative('document_natives');
+
+// Output error message to console when using the <webview> tag with no
+// permission.
+var errorMessage = "You do not have permission to use the webview element." +
+ " Be sure to declare the 'webview' permission in your manifest file.";
+
+// Registers <webview> custom element.
+function registerWebViewElement() {
+ var proto = Object.create(HTMLElement.prototype);
+
+ proto.createdCallback = function() {
+ window.console.error(errorMessage);
+ };
+
+ 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;
+
+ registerWebViewElement();
+ window.removeEventListener(event.type, listener, useCapture);
+}, useCapture);
diff --git a/extensions/renderer/resources/web_view_events.js b/extensions/renderer/resources/web_view_events.js
new file mode 100644
index 0000000..32b500c
--- /dev/null
+++ b/extensions/renderer/resources/web_view_events.js
@@ -0,0 +1,621 @@
+// Copyright 2014 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.
+
+// Event management for WebViewInternal.
+
+var DeclarativeWebRequestSchema =
+ requireNative('schema_registry').GetSchema('declarativeWebRequest');
+var EventBindings = require('event_bindings');
+var IdGenerator = requireNative('id_generator');
+var MessagingNatives = requireNative('messaging_natives');
+var WebRequestEvent = require('webRequestInternal').WebRequestEvent;
+var WebRequestSchema =
+ requireNative('schema_registry').GetSchema('webRequest');
+var WebView = require('webViewInternal').WebView;
+
+var CreateEvent = function(name) {
+ var eventOpts = {supportsListeners: true, supportsFilters: true};
+ return new EventBindings.Event(name, undefined, eventOpts);
+};
+
+var FrameNameChangedEvent = CreateEvent('webViewInternal.onFrameNameChanged');
+var PluginDestroyedEvent = CreateEvent('webViewInternal.onPluginDestroyed');
+var WebRequestMessageEvent = CreateEvent('webViewInternal.onMessage');
+
+// WEB_VIEW_EVENTS is a map of stable <webview> DOM event names to their
+// associated extension event descriptor objects.
+// An event listener will be attached to the extension event |evt| specified in
+// the descriptor.
+// |fields| specifies the public-facing fields in the DOM event that are
+// accessible to <webview> developers.
+// |customHandler| allows a handler function to be called each time an extension
+// event is caught by its event listener. The DOM event should be dispatched
+// within this handler function. With no handler function, the DOM event
+// will be dispatched by default each time the extension event is caught.
+// |cancelable| (default: false) specifies whether the event's default
+// behavior can be canceled. If the default action associated with the event
+// is prevented, then its dispatch function will return false in its event
+// handler. The event must have a custom handler for this to be meaningful.
+var WEB_VIEW_EVENTS = {
+ 'close': {
+ evt: CreateEvent('webViewInternal.onClose'),
+ fields: []
+ },
+ 'consolemessage': {
+ evt: CreateEvent('webViewInternal.onConsoleMessage'),
+ fields: ['level', 'message', 'line', 'sourceId']
+ },
+ 'contentload': {
+ evt: CreateEvent('webViewInternal.onContentLoad'),
+ fields: []
+ },
+ 'dialog': {
+ cancelable: true,
+ customHandler: function(handler, event, webViewEvent) {
+ handler.handleDialogEvent(event, webViewEvent);
+ },
+ evt: CreateEvent('webViewInternal.onDialog'),
+ fields: ['defaultPromptText', 'messageText', 'messageType', 'url']
+ },
+ 'exit': {
+ evt: CreateEvent('webViewInternal.onExit'),
+ fields: ['processId', 'reason']
+ },
+ 'findupdate': {
+ evt: CreateEvent('webViewInternal.onFindReply'),
+ fields: [
+ 'searchText',
+ 'numberOfMatches',
+ 'activeMatchOrdinal',
+ 'selectionRect',
+ 'canceled',
+ 'finalUpdate'
+ ]
+ },
+ 'loadabort': {
+ cancelable: true,
+ customHandler: function(handler, event, webViewEvent) {
+ handler.handleLoadAbortEvent(event, webViewEvent);
+ },
+ evt: CreateEvent('webViewInternal.onLoadAbort'),
+ fields: ['url', 'isTopLevel', 'reason']
+ },
+ 'loadcommit': {
+ customHandler: function(handler, event, webViewEvent) {
+ handler.handleLoadCommitEvent(event, webViewEvent);
+ },
+ evt: CreateEvent('webViewInternal.onLoadCommit'),
+ fields: ['url', 'isTopLevel']
+ },
+ 'loadprogress': {
+ evt: CreateEvent('webViewInternal.onLoadProgress'),
+ fields: ['url', 'progress']
+ },
+ 'loadredirect': {
+ evt: CreateEvent('webViewInternal.onLoadRedirect'),
+ fields: ['isTopLevel', 'oldUrl', 'newUrl']
+ },
+ 'loadstart': {
+ evt: CreateEvent('webViewInternal.onLoadStart'),
+ fields: ['url', 'isTopLevel']
+ },
+ 'loadstop': {
+ evt: CreateEvent('webViewInternal.onLoadStop'),
+ fields: []
+ },
+ 'newwindow': {
+ cancelable: true,
+ customHandler: function(handler, event, webViewEvent) {
+ handler.handleNewWindowEvent(event, webViewEvent);
+ },
+ evt: CreateEvent('webViewInternal.onNewWindow'),
+ fields: [
+ 'initialHeight',
+ 'initialWidth',
+ 'targetUrl',
+ 'windowOpenDisposition',
+ 'name'
+ ]
+ },
+ 'permissionrequest': {
+ cancelable: true,
+ customHandler: function(handler, event, webViewEvent) {
+ handler.handlePermissionEvent(event, webViewEvent);
+ },
+ evt: CreateEvent('webViewInternal.onPermissionRequest'),
+ fields: [
+ 'identifier',
+ 'lastUnlockedBySelf',
+ 'name',
+ 'permission',
+ 'requestMethod',
+ 'url',
+ 'userGesture'
+ ]
+ },
+ 'responsive': {
+ evt: CreateEvent('webViewInternal.onResponsive'),
+ fields: ['processId']
+ },
+ 'sizechanged': {
+ evt: CreateEvent('webViewInternal.onSizeChanged'),
+ customHandler: function(handler, event, webViewEvent) {
+ handler.handleSizeChangedEvent(event, webViewEvent);
+ },
+ fields: ['oldHeight', 'oldWidth', 'newHeight', 'newWidth']
+ },
+ 'unresponsive': {
+ evt: CreateEvent('webViewInternal.onUnresponsive'),
+ fields: ['processId']
+ },
+ 'zoomchange': {
+ evt: CreateEvent('webViewInternal.onZoomChange'),
+ fields: ['oldZoomFactor', 'newZoomFactor']
+ }
+};
+
+function DeclarativeWebRequestEvent(opt_eventName,
+ opt_argSchemas,
+ opt_eventOptions,
+ opt_webViewInstanceId) {
+ var subEventName = opt_eventName + '/' + IdGenerator.GetNextId();
+ EventBindings.Event.call(this, subEventName, opt_argSchemas, opt_eventOptions,
+ opt_webViewInstanceId);
+
+ // TODO(lazyboy): When do we dispose this listener?
+ WebRequestMessageEvent.addListener(function() {
+ // Re-dispatch to subEvent's listeners.
+ $Function.apply(this.dispatch, this, $Array.slice(arguments));
+ }.bind(this), {instanceId: opt_webViewInstanceId || 0});
+}
+
+DeclarativeWebRequestEvent.prototype = {
+ __proto__: EventBindings.Event.prototype
+};
+
+// Constructor.
+function WebViewEvents(webViewInternal, viewInstanceId) {
+ this.webViewInternal = webViewInternal;
+ this.viewInstanceId = viewInstanceId;
+ this.setup();
+}
+
+// Sets up events.
+WebViewEvents.prototype.setup = function() {
+ this.setupFrameNameChangedEvent();
+ this.setupPluginDestroyedEvent();
+ this.setupWebRequestEvents();
+ this.webViewInternal.setupExperimentalContextMenus();
+
+ var events = this.getEvents();
+ for (var eventName in events) {
+ this.setupEvent(eventName, events[eventName]);
+ }
+};
+
+WebViewEvents.prototype.setupFrameNameChangedEvent = function() {
+ FrameNameChangedEvent.addListener(function(e) {
+ this.webViewInternal.onFrameNameChanged(e.name);
+ }.bind(this), {instanceId: this.viewInstanceId});
+};
+
+WebViewEvents.prototype.setupPluginDestroyedEvent = function() {
+ PluginDestroyedEvent.addListener(function(e) {
+ this.webViewInternal.onPluginDestroyed();
+ }.bind(this), {instanceId: this.viewInstanceId});
+};
+
+WebViewEvents.prototype.setupWebRequestEvents = function() {
+ var request = {};
+ var createWebRequestEvent = function(webRequestEvent) {
+ return function() {
+ if (!this[webRequestEvent.name]) {
+ this[webRequestEvent.name] =
+ new WebRequestEvent(
+ 'webViewInternal.' + webRequestEvent.name,
+ webRequestEvent.parameters,
+ webRequestEvent.extraParameters, webRequestEvent.options,
+ this.viewInstanceId);
+ }
+ return this[webRequestEvent.name];
+ }.bind(this);
+ }.bind(this);
+
+ var createDeclarativeWebRequestEvent = function(webRequestEvent) {
+ return function() {
+ if (!this[webRequestEvent.name]) {
+ // The onMessage event gets a special event type because we want
+ // the listener to fire only for messages targeted for this particular
+ // <webview>.
+ var EventClass = webRequestEvent.name === 'onMessage' ?
+ DeclarativeWebRequestEvent : EventBindings.Event;
+ this[webRequestEvent.name] =
+ new EventClass(
+ 'webViewInternal.' + webRequestEvent.name,
+ webRequestEvent.parameters,
+ webRequestEvent.options,
+ this.viewInstanceId);
+ }
+ return this[webRequestEvent.name];
+ }.bind(this);
+ }.bind(this);
+
+ for (var i = 0; i < DeclarativeWebRequestSchema.events.length; ++i) {
+ var eventSchema = DeclarativeWebRequestSchema.events[i];
+ var webRequestEvent = createDeclarativeWebRequestEvent(eventSchema);
+ Object.defineProperty(
+ request,
+ eventSchema.name,
+ {
+ get: webRequestEvent,
+ enumerable: true
+ }
+ );
+ }
+
+ // Populate the WebRequest events from the API definition.
+ for (var i = 0; i < WebRequestSchema.events.length; ++i) {
+ var webRequestEvent = createWebRequestEvent(WebRequestSchema.events[i]);
+ Object.defineProperty(
+ request,
+ WebRequestSchema.events[i].name,
+ {
+ get: webRequestEvent,
+ enumerable: true
+ }
+ );
+ }
+
+ this.webViewInternal.setRequestPropertyOnWebViewNode(request);
+};
+
+WebViewEvents.prototype.getEvents = function() {
+ var experimentalEvents = this.webViewInternal.maybeGetExperimentalEvents();
+ for (var eventName in experimentalEvents) {
+ WEB_VIEW_EVENTS[eventName] = experimentalEvents[eventName];
+ }
+ var chromeEvents = this.webViewInternal.maybeGetChromeWebViewEvents();
+ for (var eventName in chromeEvents) {
+ WEB_VIEW_EVENTS[eventName] = chromeEvents[eventName];
+ }
+ return WEB_VIEW_EVENTS;
+};
+
+WebViewEvents.prototype.setupEvent = function(name, info) {
+ info.evt.addListener(function(e) {
+ var details = {bubbles:true};
+ if (info.cancelable) {
+ details.cancelable = true;
+ }
+ var webViewEvent = new Event(name, details);
+ $Array.forEach(info.fields, function(field) {
+ if (e[field] !== undefined) {
+ webViewEvent[field] = e[field];
+ }
+ }.bind(this));
+ if (info.customHandler) {
+ info.customHandler(this, e, webViewEvent);
+ return;
+ }
+ this.webViewInternal.dispatchEvent(webViewEvent);
+ }.bind(this), {instanceId: this.viewInstanceId});
+
+ this.webViewInternal.setupEventProperty(name);
+};
+
+
+WebViewEvents.prototype.handleDialogEvent = function(event, webViewEvent) {
+ var showWarningMessage = function(dialogType) {
+ var VOWELS = ['a', 'e', 'i', 'o', 'u'];
+ var WARNING_MSG_DIALOG_BLOCKED = '<webview>: %1 %2 dialog was blocked.';
+ var article = (VOWELS.indexOf(dialogType.charAt(0)) >= 0) ? 'An' : 'A';
+ var output = WARNING_MSG_DIALOG_BLOCKED.replace('%1', article);
+ output = output.replace('%2', dialogType);
+ window.console.warn(output);
+ };
+
+ var requestId = event.requestId;
+ var actionTaken = false;
+
+ var validateCall = function() {
+ var ERROR_MSG_DIALOG_ACTION_ALREADY_TAKEN = '<webview>: ' +
+ 'An action has already been taken for this "dialog" event.';
+
+ if (actionTaken) {
+ throw new Error(ERROR_MSG_DIALOG_ACTION_ALREADY_TAKEN);
+ }
+ actionTaken = true;
+ };
+
+ var getGuestInstanceId = function() {
+ return this.webViewInternal.getGuestInstanceId();
+ }.bind(this);
+
+ var dialog = {
+ ok: function(user_input) {
+ validateCall();
+ user_input = user_input || '';
+ WebView.setPermission(getGuestInstanceId(), requestId, 'allow',
+ user_input);
+ },
+ cancel: function() {
+ validateCall();
+ WebView.setPermission(getGuestInstanceId(), requestId, 'deny');
+ }
+ };
+ webViewEvent.dialog = dialog;
+
+ var defaultPrevented = !this.webViewInternal.dispatchEvent(webViewEvent);
+ if (actionTaken) {
+ return;
+ }
+
+ if (defaultPrevented) {
+ // Tell the JavaScript garbage collector to track lifetime of |dialog| and
+ // call back when the dialog object has been collected.
+ MessagingNatives.BindToGC(dialog, function() {
+ // Avoid showing a warning message if the decision has already been made.
+ if (actionTaken) {
+ return;
+ }
+ WebView.setPermission(
+ getGuestInstanceId(), requestId, 'default', '', function(allowed) {
+ if (allowed) {
+ return;
+ }
+ showWarningMessage(event.messageType);
+ });
+ });
+ } else {
+ actionTaken = true;
+ // The default action is equivalent to canceling the dialog.
+ WebView.setPermission(
+ getGuestInstanceId(), requestId, 'default', '', function(allowed) {
+ if (allowed) {
+ return;
+ }
+ showWarningMessage(event.messageType);
+ });
+ }
+};
+
+WebViewEvents.prototype.handleLoadAbortEvent = function(event, webViewEvent) {
+ var showWarningMessage = function(reason) {
+ var WARNING_MSG_LOAD_ABORTED = '<webview>: ' +
+ 'The load has aborted with reason "%1".';
+ window.console.warn(WARNING_MSG_LOAD_ABORTED.replace('%1', reason));
+ };
+ if (this.webViewInternal.dispatchEvent(webViewEvent)) {
+ showWarningMessage(event.reason);
+ }
+};
+
+WebViewEvents.prototype.handleLoadCommitEvent = function(event, webViewEvent) {
+ this.webViewInternal.onLoadCommit(event.currentEntryIndex, event.entryCount,
+ event.processId, event.url,
+ event.isTopLevel);
+ this.webViewInternal.dispatchEvent(webViewEvent);
+};
+
+WebViewEvents.prototype.handleNewWindowEvent = function(event, webViewEvent) {
+ var ERROR_MSG_NEWWINDOW_ACTION_ALREADY_TAKEN = '<webview>: ' +
+ 'An action has already been taken for this "newwindow" event.';
+
+ var ERROR_MSG_NEWWINDOW_UNABLE_TO_ATTACH = '<webview>: ' +
+ 'Unable to attach the new window to the provided webViewInternal.';
+
+ var ERROR_MSG_WEBVIEW_EXPECTED = '<webview> element expected.';
+
+ var showWarningMessage = function() {
+ var WARNING_MSG_NEWWINDOW_BLOCKED = '<webview>: A new window was blocked.';
+ window.console.warn(WARNING_MSG_NEWWINDOW_BLOCKED);
+ };
+
+ var requestId = event.requestId;
+ var actionTaken = false;
+ var getGuestInstanceId = function() {
+ return this.webViewInternal.getGuestInstanceId();
+ }.bind(this);
+
+ var validateCall = function () {
+ if (actionTaken) {
+ throw new Error(ERROR_MSG_NEWWINDOW_ACTION_ALREADY_TAKEN);
+ }
+ actionTaken = true;
+ };
+
+ var windowObj = {
+ attach: function(webview) {
+ validateCall();
+ if (!webview || !webview.tagName || webview.tagName != 'WEBVIEW')
+ throw new Error(ERROR_MSG_WEBVIEW_EXPECTED);
+ // Attach happens asynchronously to give the tagWatcher an opportunity
+ // to pick up the new webview before attach operates on it, if it hasn't
+ // been attached to the DOM already.
+ // Note: Any subsequent errors cannot be exceptions because they happen
+ // asynchronously.
+ setTimeout(function() {
+ var webViewInternal = privates(webview).internal;
+ // Update the partition.
+ if (event.storagePartitionId) {
+ webViewInternal.onAttach(event.storagePartitionId);
+ }
+
+ var attached = webViewInternal.attachWindow(event.windowId, true);
+
+ if (!attached) {
+ window.console.error(ERROR_MSG_NEWWINDOW_UNABLE_TO_ATTACH);
+ }
+
+ var guestInstanceId = getGuestInstanceId();
+ if (!guestInstanceId) {
+ // If the opener is already gone, then we won't have its
+ // guestInstanceId.
+ return;
+ }
+
+ // If the object being passed into attach is not a valid <webview>
+ // then we will fail and it will be treated as if the new window
+ // was rejected. The permission API plumbing is used here to clean
+ // up the state created for the new window if attaching fails.
+ WebView.setPermission(
+ guestInstanceId, requestId, attached ? 'allow' : 'deny');
+ }, 0);
+ },
+ discard: function() {
+ validateCall();
+ var guestInstanceId = getGuestInstanceId();
+ if (!guestInstanceId) {
+ // If the opener is already gone, then we won't have its
+ // guestInstanceId.
+ return;
+ }
+ WebView.setPermission(guestInstanceId, requestId, 'deny');
+ }
+ };
+ webViewEvent.window = windowObj;
+
+ var defaultPrevented = !this.webViewInternal.dispatchEvent(webViewEvent);
+ if (actionTaken) {
+ return;
+ }
+
+ if (defaultPrevented) {
+ // Make browser plugin track lifetime of |windowObj|.
+ MessagingNatives.BindToGC(windowObj, function() {
+ // Avoid showing a warning message if the decision has already been made.
+ if (actionTaken) {
+ return;
+ }
+
+ var guestInstanceId = getGuestInstanceId();
+ if (!guestInstanceId) {
+ // If the opener is already gone, then we won't have its
+ // guestInstanceId.
+ return;
+ }
+
+ WebView.setPermission(
+ guestInstanceId, requestId, 'default', '', function(allowed) {
+ if (allowed) {
+ return;
+ }
+ showWarningMessage();
+ });
+ });
+ } else {
+ actionTaken = true;
+ // The default action is to discard the window.
+ WebView.setPermission(
+ getGuestInstanceId(), requestId, 'default', '', function(allowed) {
+ if (allowed) {
+ return;
+ }
+ showWarningMessage();
+ });
+ }
+};
+
+WebViewEvents.prototype.getPermissionTypes = function() {
+ var permissions =
+ ['media',
+ 'geolocation',
+ 'pointerLock',
+ 'download',
+ 'loadplugin',
+ 'filesystem'];
+ return permissions.concat(
+ this.webViewInternal.maybeGetExperimentalPermissions());
+};
+
+WebViewEvents.prototype.handlePermissionEvent =
+ function(event, webViewEvent) {
+ var ERROR_MSG_PERMISSION_ALREADY_DECIDED = '<webview>: ' +
+ 'Permission has already been decided for this "permissionrequest" event.';
+
+ var showWarningMessage = function(permission) {
+ var WARNING_MSG_PERMISSION_DENIED = '<webview>: ' +
+ 'The permission request for "%1" has been denied.';
+ window.console.warn(
+ WARNING_MSG_PERMISSION_DENIED.replace('%1', permission));
+ };
+
+ var requestId = event.requestId;
+ var getGuestInstanceId = function() {
+ return this.webViewInternal.getGuestInstanceId();
+ }.bind(this);
+
+ if (this.getPermissionTypes().indexOf(event.permission) < 0) {
+ // The permission type is not allowed. Trigger the default response.
+ WebView.setPermission(
+ getGuestInstanceId(), requestId, 'default', '', function(allowed) {
+ if (allowed) {
+ return;
+ }
+ showWarningMessage(event.permission);
+ });
+ return;
+ }
+
+ var decisionMade = false;
+ var validateCall = function() {
+ if (decisionMade) {
+ throw new Error(ERROR_MSG_PERMISSION_ALREADY_DECIDED);
+ }
+ decisionMade = true;
+ };
+
+ // Construct the event.request object.
+ var request = {
+ allow: function() {
+ validateCall();
+ WebView.setPermission(getGuestInstanceId(), requestId, 'allow');
+ },
+ deny: function() {
+ validateCall();
+ WebView.setPermission(getGuestInstanceId(), requestId, 'deny');
+ }
+ };
+ webViewEvent.request = request;
+
+ var defaultPrevented = !this.webViewInternal.dispatchEvent(webViewEvent);
+ if (decisionMade) {
+ return;
+ }
+
+ if (defaultPrevented) {
+ // Make browser plugin track lifetime of |request|.
+ MessagingNatives.BindToGC(request, function() {
+ // Avoid showing a warning message if the decision has already been made.
+ if (decisionMade) {
+ return;
+ }
+ WebView.setPermission(
+ getGuestInstanceId(), requestId, 'default', '', function(allowed) {
+ if (allowed) {
+ return;
+ }
+ showWarningMessage(event.permission);
+ });
+ });
+ } else {
+ decisionMade = true;
+ WebView.setPermission(
+ getGuestInstanceId(), requestId, 'default', '',
+ function(allowed) {
+ if (allowed) {
+ return;
+ }
+ showWarningMessage(event.permission);
+ });
+ }
+};
+
+WebViewEvents.prototype.handleSizeChangedEvent = function(
+ event, webViewEvent) {
+ this.webViewInternal.onSizeChanged(webViewEvent);
+};
+
+exports.WebViewEvents = WebViewEvents;
+exports.CreateEvent = CreateEvent;
diff --git a/extensions/renderer/resources/web_view_experimental.js b/extensions/renderer/resources/web_view_experimental.js
new file mode 100644
index 0000000..c1246ae
--- /dev/null
+++ b/extensions/renderer/resources/web_view_experimental.js
@@ -0,0 +1,31 @@
+// Copyright 2014 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 experimental API for <webview>.
+// See web_view.js for details.
+//
+// <webview> Experimental API is only available on canary and dev channels of
+// Chrome.
+
+var WebViewInternal = require('webView').WebViewInternal;
+
+WebViewInternal.prototype.maybeGetExperimentalEvents = function() {
+ return {};
+};
+
+/** @private */
+WebViewInternal.prototype.maybeGetExperimentalPermissions = function() {
+ return [];
+};
+
+/** @private */
+WebViewInternal.prototype.captureVisibleRegion = function(spec, callback) {
+ WebView.captureVisibleRegion(this.guestInstanceId, spec, callback);
+};
+
+WebViewInternal.maybeRegisterExperimentalAPIs = function(proto) {
+ proto.captureVisibleRegion = function(spec, callback) {
+ privates(this).internal.captureVisibleRegion(spec, callback);
+ };
+};