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