blob: 43c6dc4624b2a5acb8ddedc7a314296176eab96f [file] [log] [blame]
// 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.
// Shim that simulates a <webview> tag via Mutation Observers.
//
// The actual tag is implemented via the browser plugin. The internals of this
// are hidden via Shadow DOM.
var watchForTag = require("tagWatcher").watchForTag;
var WEB_VIEW_ATTRIBUTES = ['name', 'src', 'partition', 'autosize', 'minheight',
'minwidth', 'maxheight', 'maxwidth'];
// All exposed api methods for <webview>, these are forwarded to the browser
// plugin.
var WEB_VIEW_API_METHODS = [
'back',
'canGoBack',
'canGoForward',
'forward',
'getProcessId',
'go',
'reload',
'stop',
'terminate'
];
var WEB_VIEW_EVENTS = {
'exit' : ['processId', 'reason'],
'loadabort' : ['url', 'isTopLevel', 'reason'],
'loadcommit' : ['url', 'isTopLevel'],
'loadredirect' : ['oldUrl', 'newUrl', 'isTopLevel'],
'loadstart' : ['url', 'isTopLevel'],
'loadstop' : [],
'sizechanged': ['oldHeight', 'oldWidth', 'newHeight', 'newWidth'],
};
window.addEventListener('DOMContentLoaded', function() {
watchForTag('WEBVIEW', function(addedNode) { new WebView(addedNode); });
});
/**
* @constructor
*/
function WebView(node) {
this.node_ = node;
var shadowRoot = node.webkitCreateShadowRoot();
this.objectNode_ = document.createElement('object');
this.objectNode_.type = 'application/browser-plugin';
// The <object> node fills in the <webview> container.
this.objectNode_.style.width = '100%';
this.objectNode_.style.height = '100%';
WEB_VIEW_ATTRIBUTES.forEach(function(attributeName) {
// Only copy attributes that have been assigned values, rather than copying
// a series of undefined attributes to BrowserPlugin.
if (this.node_.hasAttribute(attributeName)) {
this.objectNode_.setAttribute(
attributeName, this.node_.getAttribute(attributeName));
}
}, this);
shadowRoot.appendChild(this.objectNode_);
// this.objectNode_[apiMethod] are not necessarily defined immediately after
// the shadow object is appended to the shadow root.
var self = this;
WEB_VIEW_API_METHODS.forEach(function(apiMethod) {
node[apiMethod] = function(var_args) {
return self.objectNode_[apiMethod].apply(self.objectNode_, arguments);
};
}, this);
node['executeScript'] = function(var_args) {
var args = [self.objectNode_.getProcessId(),
self.objectNode_.getRouteId()].concat(
Array.prototype.slice.call(arguments));
chrome.webview.executeScript.apply(null, args);
}
// Map attribute modifications on the <webview> tag to property changes in
// the underlying <object> node.
var handleMutation = this.handleMutation_.bind(this);
var observer = new WebKitMutationObserver(function(mutations) {
mutations.forEach(handleMutation);
});
observer.observe(
this.node_,
{attributes: true, attributeFilter: WEB_VIEW_ATTRIBUTES});
var handleObjectMutation = this.handleObjectMutation_.bind(this);
var objectObserver = new WebKitMutationObserver(function(mutations) {
mutations.forEach(handleObjectMutation);
});
objectObserver.observe(
this.objectNode_,
{attributes: true, attributeFilter: WEB_VIEW_ATTRIBUTES});
var objectNode = this.objectNode_;
// Expose getters and setters for the attributes.
WEB_VIEW_ATTRIBUTES.forEach(function(attributeName) {
Object.defineProperty(this.node_, attributeName, {
get: function() {
return objectNode[attributeName];
},
set: function(value) {
objectNode[attributeName] = value;
},
enumerable: true
});
}, this);
// We cannot use {writable: true} property descriptor because we want dynamic
// getter value.
Object.defineProperty(this.node_, 'contentWindow', {
get: function() {
// TODO(fsamuel): This is a workaround to enable
// contentWindow.postMessage until http://crbug.com/152006 is fixed.
if (objectNode.contentWindow)
return objectNode.contentWindow.self;
console.error('contentWindow is not available at this time. ' +
'It will become available when the page has finished loading.');
},
// No setter.
enumerable: true
});
for (var eventName in WEB_VIEW_EVENTS) {
this.setupEvent_(eventName, WEB_VIEW_EVENTS[eventName]);
}
this.maybeSetupPermissionEvent_();
}
/**
* @private
*/
WebView.prototype.handleMutation_ = function(mutation) {
// 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.
this.objectNode_[mutation.attributeName] =
this.node_.getAttribute(mutation.attributeName);
};
/**
* @private
*/
WebView.prototype.handleObjectMutation_ = function(mutation) {
// This observer monitors mutations to attributes of the BrowserPlugin and
// updates the <webview> attributes accordingly.
if (!this.objectNode_.hasAttribute(mutation.attributeName)) {
// If an attribute is removed from the BrowserPlugin, then remove it
// from the <webview> as well.
this.node_.removeAttribute(mutation.attributeName);
} else {
// 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.
var oldValue = this.node_.getAttribute(mutation.attributeName);
var newValue = this.objectNode_.getAttribute(mutation.attributeName);
if (newValue != oldValue) {
this.node_.setAttribute(mutation.attributeName, newValue);
}
}
};
/**
* @private
*/
WebView.prototype.setupEvent_ = function(eventname, attribs) {
var node = this.node_;
this.objectNode_.addEventListener('-internal-' + eventname, function(e) {
var evt = new Event(eventname, { bubbles: true });
var detail = e.detail ? JSON.parse(e.detail) : {};
attribs.forEach(function(attribName) {
evt[attribName] = detail[attribName];
});
node.dispatchEvent(evt);
});
};
/**
* Implemented when experimental permission is available.
* @private
*/
WebView.prototype.maybeSetupPermissionEvent_ = function() {};
exports.WebView = WebView;