web_view.js revision 90dce4d38c5ff5333bea97d859d4e484e27edf0c
1// Copyright (c) 2012 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5// Shim that simulates a <webview> tag via Mutation Observers.
6//
7// The actual tag is implemented via the browser plugin. The internals of this
8// are hidden via Shadow DOM.
9
10var chrome = requireNative('chrome').GetChrome();
11var forEach = require('utils').forEach;
12var watchForTag = require('tagWatcher').watchForTag;
13
14var WEB_VIEW_ATTRIBUTES = ['name', 'src', 'partition', 'autosize', 'minheight',
15    'minwidth', 'maxheight', 'maxwidth'];
16
17// All exposed api methods for <webview>, these are forwarded to the browser
18// plugin.
19var WEB_VIEW_API_METHODS = [
20  'back',
21  'canGoBack',
22  'canGoForward',
23  'forward',
24  'getProcessId',
25  'go',
26  'reload',
27  'stop',
28  'terminate'
29];
30
31var WEB_VIEW_EVENTS = {
32  'close': [],
33  'consolemessage': ['level', 'message', 'line', 'sourceId'],
34  'contentload' : [],
35  'exit' : ['processId', 'reason'],
36  'loadabort' : ['url', 'isTopLevel', 'reason'],
37  'loadcommit' : ['url', 'isTopLevel'],
38  'loadredirect' : ['oldUrl', 'newUrl', 'isTopLevel'],
39  'loadstart' : ['url', 'isTopLevel'],
40  'loadstop' : [],
41  'sizechanged': ['oldHeight', 'oldWidth', 'newHeight', 'newWidth'],
42};
43
44window.addEventListener('DOMContentLoaded', function() {
45  watchForTag('WEBVIEW', function(addedNode) { new WebView(addedNode); });
46});
47
48/**
49 * @constructor
50 */
51function WebView(node) {
52  this.node_ = node;
53  var shadowRoot = node.webkitCreateShadowRoot();
54
55  this.objectNode_ = document.createElement('object');
56  this.objectNode_.type = 'application/browser-plugin';
57  // The <object> node fills in the <webview> container.
58  this.objectNode_.style.width = '100%';
59  this.objectNode_.style.height = '100%';
60  forEach(WEB_VIEW_ATTRIBUTES, function(i, attributeName) {
61    // Only copy attributes that have been assigned values, rather than copying
62    // a series of undefined attributes to BrowserPlugin.
63    if (this.node_.hasAttribute(attributeName)) {
64      this.objectNode_.setAttribute(
65          attributeName, this.node_.getAttribute(attributeName));
66    }
67  }, this);
68
69  if (!this.node_.hasAttribute('tabIndex')) {
70    // <webview> needs a tabIndex in order to respond to keyboard focus.
71    // TODO(fsamuel): This introduces unexpected tab ordering. We need to find
72    // a way to take keyboard focus without messing with tab ordering.
73    // See http://crbug.com/231664.
74    this.node_.setAttribute('tabIndex', 0);
75  }
76  var self = this;
77  this.node_.addEventListener('focus', function(e) {
78    // Focus the BrowserPlugin when the <webview> takes focus.
79    self.objectNode_.focus();
80  });
81  this.node_.addEventListener('blur', function(e) {
82    // Blur the BrowserPlugin when the <webview> loses focus.
83    self.objectNode_.blur();
84  });
85
86  shadowRoot.appendChild(this.objectNode_);
87
88  // this.objectNode_[apiMethod] are not necessarily defined immediately after
89  // the shadow object is appended to the shadow root.
90  forEach(WEB_VIEW_API_METHODS, function(i, apiMethod) {
91    node[apiMethod] = function(var_args) {
92      return self.objectNode_[apiMethod].apply(self.objectNode_, arguments);
93    };
94  }, this);
95
96  // Map attribute modifications on the <webview> tag to property changes in
97  // the underlying <object> node.
98  var handleMutation = function(i, mutation) {
99    this.handleMutation_(mutation);
100  }.bind(this);
101  var observer = new WebKitMutationObserver(function(mutations) {
102    forEach(mutations, handleMutation);
103  });
104  observer.observe(
105      this.node_,
106      {attributes: true, attributeFilter: WEB_VIEW_ATTRIBUTES});
107
108  var handleObjectMutation = function(i, mutation) {
109    this.handleObjectMutation_(mutation);
110  }.bind(this);
111  var objectObserver = new WebKitMutationObserver(function(mutations) {
112    forEach(mutations, handleObjectMutation);
113  });
114  objectObserver.observe(
115      this.objectNode_,
116      {attributes: true, attributeFilter: WEB_VIEW_ATTRIBUTES});
117
118  var objectNode = this.objectNode_;
119  // Expose getters and setters for the attributes.
120  forEach(WEB_VIEW_ATTRIBUTES, function(i, attributeName) {
121    Object.defineProperty(this.node_, attributeName, {
122      get: function() {
123        return objectNode[attributeName];
124      },
125      set: function(value) {
126        objectNode[attributeName] = value;
127      },
128      enumerable: true
129    });
130  }, this);
131
132
133  // We cannot use {writable: true} property descriptor because we want dynamic
134  // getter value.
135  Object.defineProperty(this.node_, 'contentWindow', {
136    get: function() {
137      // TODO(fsamuel): This is a workaround to enable
138      // contentWindow.postMessage until http://crbug.com/152006 is fixed.
139      if (objectNode.contentWindow)
140        return objectNode.contentWindow.self;
141      console.error('contentWindow is not available at this time. ' +
142          'It will become available when the page has finished loading.');
143    },
144    // No setter.
145    enumerable: true
146  });
147
148  for (var eventName in WEB_VIEW_EVENTS) {
149    this.setupEvent_(eventName, WEB_VIEW_EVENTS[eventName]);
150  }
151  this.maybeSetupNewWindowEvent_();
152  this.maybeSetupPermissionEvent_();
153  this.maybeSetupExecuteCodeAPI_();
154  this.maybeSetupWebRequestEvents_();
155}
156
157/**
158 * @private
159 */
160WebView.prototype.handleMutation_ = function(mutation) {
161  // This observer monitors mutations to attributes of the <webview> and
162  // updates the BrowserPlugin properties accordingly. In turn, updating
163  // a BrowserPlugin property will update the corresponding BrowserPlugin
164  // attribute, if necessary. See BrowserPlugin::UpdateDOMAttribute for more
165  // details.
166  this.objectNode_[mutation.attributeName] =
167      this.node_.getAttribute(mutation.attributeName);
168};
169
170/**
171 * @private
172 */
173WebView.prototype.handleObjectMutation_ = function(mutation) {
174  // This observer monitors mutations to attributes of the BrowserPlugin and
175  // updates the <webview> attributes accordingly.
176  if (!this.objectNode_.hasAttribute(mutation.attributeName)) {
177    // If an attribute is removed from the BrowserPlugin, then remove it
178    // from the <webview> as well.
179    this.node_.removeAttribute(mutation.attributeName);
180  } else {
181    // Update the <webview> attribute to match the BrowserPlugin attribute.
182    // Note: Calling setAttribute on <webview> will trigger its mutation
183    // observer which will then propagate that attribute to BrowserPlugin. In
184    // cases where we permit assigning a BrowserPlugin attribute the same value
185    // again (such as navigation when crashed), this could end up in an infinite
186    // loop. Thus, we avoid this loop by only updating the <webview> attribute
187    // if the BrowserPlugin attributes differs from it.
188    var oldValue = this.node_.getAttribute(mutation.attributeName);
189    var newValue = this.objectNode_.getAttribute(mutation.attributeName);
190    if (newValue != oldValue) {
191      this.node_.setAttribute(mutation.attributeName, newValue);
192    }
193  }
194};
195
196/**
197 * @private
198 */
199WebView.prototype.setupEvent_ = function(eventname, attribs) {
200  var node = this.node_;
201  this.objectNode_.addEventListener('-internal-' + eventname, function(e) {
202    var evt = new Event(eventname, { bubbles: true });
203    var detail = e.detail ? JSON.parse(e.detail) : {};
204    forEach(attribs, function(i, attribName) {
205      evt[attribName] = detail[attribName];
206    });
207    node.dispatchEvent(evt);
208  });
209};
210
211/**
212 * Implemented when the experimental API is available.
213 * @private
214 */
215WebView.prototype.maybeSetupNewWindowEvent_ = function() {};
216
217/**
218 * Implemented when experimental permission is available.
219 * @private
220 */
221WebView.prototype.maybeSetupPermissionEvent_ = function() {};
222
223/**
224 * Implemented when experimental permission is available.
225 * @private
226 */
227WebView.prototype.maybeSetupExecuteScript_ = function() {};
228
229/**
230 * Implemented when experimental permission is available.
231 * @private
232 */
233WebView.prototype.maybeSetupWebRequestEvents_ = function() {};
234
235exports.WebView = WebView;
236