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