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