blob: b995ea998b95d6cb291a14af7b87ddc0fa6ef8c2 [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.
'use strict';
var DocumentNatives = requireNative('document_natives');
var EventBindings = require('event_bindings');
var MessagingNatives = requireNative('messaging_natives');
var WebRequestEvent = require('webRequestInternal').WebRequestEvent;
var WebRequestSchema =
requireNative('schema_registry').GetSchema('webRequest');
var WebView = require('binding').Binding.create('webview').generate();
var WebViewNatives = requireNative('webview_natives');
// This secret enables hiding <webview> private members from the outside scope.
// Outside of this file, |secret| is inaccessible. The only way to access the
// <webview> element's internal members is via the |secret|. Since it's only
// accessible by code here (and in web_view_experimental), only <webview>'s
// API can access it and not external developers.
var secret = {};
var WEB_VIEW_ATTRIBUTE_MAXHEIGHT = 'maxheight';
var WEB_VIEW_ATTRIBUTE_MAXWIDTH = 'maxwidth';
var WEB_VIEW_ATTRIBUTE_MINHEIGHT = 'minheight';
var WEB_VIEW_ATTRIBUTE_MINWIDTH = 'minwidth';
/** @type {Array.<string>} */
var WEB_VIEW_ATTRIBUTES = [
'name',
'partition',
'autosize',
WEB_VIEW_ATTRIBUTE_MINHEIGHT,
WEB_VIEW_ATTRIBUTE_MINWIDTH,
WEB_VIEW_ATTRIBUTE_MAXHEIGHT,
WEB_VIEW_ATTRIBUTE_MAXWIDTH
];
var CreateEvent = function(name) {
var eventOpts = {supportsListeners: true, supportsFilters: true};
return new EventBindings.Event(name, undefined, eventOpts);
};
var WEB_VIEW_EVENTS = {
'close': {
evt: CreateEvent('webview.onClose'),
fields: []
},
'consolemessage': {
evt: CreateEvent('webview.onConsoleMessage'),
fields: ['level', 'message', 'line', 'sourceId']
},
'contentload': {
evt: CreateEvent('webview.onContentLoad'),
fields: []
},
'exit': {
evt: CreateEvent('webview.onExit'),
fields: ['processId', 'reason']
},
'loadabort': {
evt: CreateEvent('webview.onLoadAbort'),
fields: ['url', 'isTopLevel', 'reason']
},
'loadcommit': {
customHandler: function(webViewInternal, event, webViewEvent) {
webViewInternal.handleLoadCommitEvent_(event, webViewEvent);
},
evt: CreateEvent('webview.onLoadCommit'),
fields: ['url', 'isTopLevel']
},
'loadprogress': {
evt: CreateEvent('webview.onLoadProgress'),
fields: ['url', 'progress']
},
'loadredirect': {
evt: CreateEvent('webview.onLoadRedirect'),
fields: ['isTopLevel', 'oldUrl', 'newUrl']
},
'loadstart': {
evt: CreateEvent('webview.onLoadStart'),
fields: ['url', 'isTopLevel']
},
'loadstop': {
evt: CreateEvent('webview.onLoadStop'),
fields: []
},
'newwindow': {
cancelable: true,
customHandler: function(webViewInternal, event, webViewEvent) {
webViewInternal.handleNewWindowEvent_(event, webViewEvent);
},
evt: CreateEvent('webview.onNewWindow'),
fields: [
'initialHeight',
'initialWidth',
'targetUrl',
'windowOpenDisposition',
'name'
]
},
'permissionrequest': {
cancelable: true,
customHandler: function(webViewInternal, event, webViewEvent) {
webViewInternal.handlePermissionEvent_(event, webViewEvent);
},
evt: CreateEvent('webview.onPermissionRequest'),
fields: [
'lastUnlockedBySelf',
'permission',
'requestMethod',
'url',
'userGesture'
]
},
'responsive': {
evt: CreateEvent('webview.onResponsive'),
fields: ['processId']
},
'sizechanged': {
evt: CreateEvent('webview.onSizeChanged'),
customHandler: function(webViewInternal, event, webViewEvent) {
webViewInternal.handleSizeChangedEvent_(event, webViewEvent);
},
fields: ['oldHeight', 'oldWidth', 'newHeight', 'newWidth']
},
'unresponsive': {
evt: CreateEvent('webview.onUnresponsive'),
fields: ['processId']
}
};
// Implemented when the experimental API is available.
WebViewInternal.maybeRegisterExperimentalAPIs = function(proto) {}
/**
* @constructor
*/
function WebViewInternal(webviewNode) {
this.webviewNode_ = webviewNode;
this.browserPluginNode_ = this.createBrowserPluginNode_();
var shadowRoot = this.webviewNode_.webkitCreateShadowRoot();
shadowRoot.appendChild(this.browserPluginNode_);
this.setupWebviewNodeAttributes_();
this.setupFocusPropagation_();
this.setupWebviewNodeProperties_();
this.setupWebviewNodeEvents_();
}
/**
* @private
*/
WebViewInternal.prototype.createBrowserPluginNode_ = function() {
// We create BrowserPlugin as a custom element in order to observe changes
// to attributes synchronously.
var browserPluginNode = new WebViewInternal.BrowserPlugin();
Object.defineProperty(browserPluginNode, 'internal_', {
enumerable: false,
writable: false,
value: function(key) {
if (key !== secret) {
return null;
}
return this;
}.bind(this)
});
var ALL_ATTRIBUTES = WEB_VIEW_ATTRIBUTES.concat(['src']);
$Array.forEach(ALL_ATTRIBUTES, function(attributeName) {
// Only copy attributes that have been assigned values, rather than copying
// a series of undefined attributes to BrowserPlugin.
if (this.webviewNode_.hasAttribute(attributeName)) {
browserPluginNode.setAttribute(
attributeName, this.webviewNode_.getAttribute(attributeName));
} else if (this.webviewNode_[attributeName]){
// Reading property using has/getAttribute does not work on
// document.DOMContentLoaded event (but works on
// window.DOMContentLoaded event).
// So copy from property if copying from attribute fails.
browserPluginNode.setAttribute(
attributeName, this.webviewNode_[attributeName]);
}
}, this);
return browserPluginNode;
};
/**
* @private
*/
WebViewInternal.prototype.setupFocusPropagation_ = function() {
if (!this.webviewNode_.hasAttribute('tabIndex')) {
// <webview> needs a tabIndex in order to respond to keyboard focus.
// TODO(fsamuel): This introduces unexpected tab ordering. We need to find
// a way to take keyboard focus without messing with tab ordering.
// See http://crbug.com/231664.
this.webviewNode_.setAttribute('tabIndex', 0);
}
var self = this;
this.webviewNode_.addEventListener('focus', function(e) {
// Focus the BrowserPlugin when the <webview> takes focus.
self.browserPluginNode_.focus();
});
this.webviewNode_.addEventListener('blur', function(e) {
// Blur the BrowserPlugin when the <webview> loses focus.
self.browserPluginNode_.blur();
});
};
/**
* @private
*/
WebViewInternal.prototype.canGoBack_ = function() {
return this.entryCount_ > 1 && this.currentEntryIndex_ > 0;
};
/**
* @private
*/
WebViewInternal.prototype.canGoForward_ = function() {
return this.currentEntryIndex_ >= 0 &&
this.currentEntryIndex_ < (this.entryCount_ - 1);
};
/**
* @private
*/
WebViewInternal.prototype.getProcessId_ = function() {
return this.processId_;
};
/**
* @private
*/
WebViewInternal.prototype.go_ = function(relativeIndex) {
if (!this.instanceId_) {
return;
}
WebView.go(this.instanceId_, relativeIndex);
};
/**
* @private
*/
WebViewInternal.prototype.reload_ = function() {
if (!this.instanceId_) {
return;
}
WebView.reload(this.instanceId_);
};
/**
* @private
*/
WebViewInternal.prototype.stop_ = function() {
if (!this.instanceId_) {
return;
}
WebView.stop(this.instanceId_);
};
/**
* @private
*/
WebViewInternal.prototype.terminate_ = function() {
if (!this.instanceId_) {
return;
}
WebView.terminate(this.instanceId_);
};
/**
* @private
*/
WebViewInternal.prototype.validateExecuteCodeCall_ = function() {
var ERROR_MSG_CANNOT_INJECT_SCRIPT = '<webview>: ' +
'Script cannot be injected into content until the page has loaded.';
if (!this.instanceId_) {
throw new Error(ERROR_MSG_CANNOT_INJECT_SCRIPT);
}
};
/**
* @private
*/
WebViewInternal.prototype.executeScript_ = function(var_args) {
this.validateExecuteCodeCall_();
var args = $Array.concat([this.instanceId_], $Array.slice(arguments));
$Function.apply(WebView.executeScript, null, args);
};
/**
* @private
*/
WebViewInternal.prototype.insertCSS_ = function(var_args) {
this.validateExecuteCodeCall_();
var args = $Array.concat([this.instanceId_], $Array.slice(arguments));
$Function.apply(WebView.insertCSS, null, args);
};
/**
* @private
*/
WebViewInternal.prototype.setupWebviewNodeProperties_ = function() {
var ERROR_MSG_CONTENTWINDOW_NOT_AVAILABLE = '<webview>: ' +
'contentWindow is not available at this time. It will become available ' +
'when the page has finished loading.';
var self = this;
var browserPluginNode = this.browserPluginNode_;
// Expose getters and setters for the attributes.
$Array.forEach(WEB_VIEW_ATTRIBUTES, function(attributeName) {
Object.defineProperty(this.webviewNode_, attributeName, {
get: function() {
if (browserPluginNode.hasOwnProperty(attributeName)) {
return browserPluginNode[attributeName];
} else {
return browserPluginNode.getAttribute(attributeName);
}
},
set: function(value) {
if (browserPluginNode.hasOwnProperty(attributeName)) {
// Give the BrowserPlugin first stab at the attribute so that it can
// throw an exception if there is a problem. This attribute will then
// be propagated back to the <webview>.
browserPluginNode[attributeName] = value;
} else {
browserPluginNode.setAttribute(attributeName, value);
}
},
enumerable: true
});
}, this);
// <webview> src does not quite behave the same as BrowserPlugin src, and so
// we don't simply keep the two in sync.
this.src_ = this.webviewNode_.getAttribute('src');
Object.defineProperty(this.webviewNode_, 'src', {
get: function() {
return self.src_;
},
set: function(value) {
self.webviewNode_.setAttribute('src', value);
},
// No setter.
enumerable: true
});
// We cannot use {writable: true} property descriptor because we want a
// dynamic getter value.
Object.defineProperty(this.webviewNode_, 'contentWindow', {
get: function() {
if (browserPluginNode.contentWindow)
return browserPluginNode.contentWindow;
console.error(ERROR_MSG_CONTENTWINDOW_NOT_AVAILABLE);
},
// No setter.
enumerable: true
});
};
/**
* @private
*/
WebViewInternal.prototype.setupWebviewNodeAttributes_ = function() {
Object.defineProperty(this.webviewNode_, 'internal_', {
enumerable: false,
writable: false,
value: function(key) {
if (key !== secret) {
return null;
}
return this;
}.bind(this)
});
this.setupWebViewSrcAttributeMutationObserver_();
};
/**
* @private
*/
WebViewInternal.prototype.setupWebViewSrcAttributeMutationObserver_ =
function() {
// The purpose of this mutation observer is to catch assignment to the src
// attribute without any changes to its value. This is useful in the case
// where the webview guest has crashed and navigating to the same address
// spawns off a new process.
var self = this;
this.srcObserver_ = new MutationObserver(function(mutations) {
$Array.forEach(mutations, function(mutation) {
var oldValue = mutation.oldValue;
var newValue = self.webviewNode_.getAttribute(mutation.attributeName);
if (oldValue != newValue) {
return;
}
self.handleWebviewAttributeMutation_(
mutation.attributeName, oldValue, newValue);
});
});
var params = {
attributes: true,
attributeOldValue: true,
attributeFilter: ['src']
};
this.srcObserver_.observe(this.webviewNode_, params);
};
/**
* @private
*/
WebViewInternal.prototype.handleWebviewAttributeMutation_ =
function(name, oldValue, newValue) {
// 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.
if (name == 'src') {
// We treat null attribute (attribute removed) and the empty string as
// one case.
oldValue = oldValue || '';
newValue = newValue || '';
// Once we have navigated, we don't allow clearing the src attribute.
// Once <webview> enters a navigated state, it cannot be return back to a
// placeholder state.
if (newValue == '' && oldValue != '') {
// src attribute changes normally initiate a navigation. We suppress
// the next src attribute handler call to avoid reloading the page
// on every guest-initiated navigation.
this.ignoreNextSrcAttributeChange_ = true;
this.webviewNode_.setAttribute('src', oldValue);
return;
}
this.src_ = newValue;
if (this.ignoreNextSrcAttributeChange_) {
// Don't allow the src mutation observer to see this change.
this.srcObserver_.takeRecords();
this.ignoreNextSrcAttributeChange_ = false;
return;
}
}
if (this.browserPluginNode_.hasOwnProperty(name)) {
this.browserPluginNode_[name] = newValue;
} else {
this.browserPluginNode_.setAttribute(name, newValue);
}
};
/**
* @private
*/
WebViewInternal.prototype.handleBrowserPluginAttributeMutation_ =
function(name, newValue) {
// This observer monitors mutations to attributes of the BrowserPlugin and
// updates the <webview> attributes accordingly.
// |newValue| is null if the attribute |name| has been removed.
if (newValue != null) {
// 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.
if (newValue != this.webviewNode_.getAttribute(name)) {
this.webviewNode_.setAttribute(name, newValue);
}
} else {
// If an attribute is removed from the BrowserPlugin, then remove it
// from the <webview> as well.
this.webviewNode_.removeAttribute(name);
}
};
/**
* @private
*/
WebViewInternal.prototype.getEvents_ = function() {
var experimentalEvents = this.maybeGetExperimentalEvents_();
for (var eventName in experimentalEvents) {
WEB_VIEW_EVENTS[eventName] = experimentalEvents[eventName];
}
return WEB_VIEW_EVENTS;
};
WebViewInternal.prototype.handleSizeChangedEvent_ =
function(event, webViewEvent) {
var node = this.webviewNode_;
var width = node.offsetWidth;
var height = node.offsetHeight;
// Check the current bounds to make sure we do not resize <webview>
// outside of current constraints.
var maxWidth;
if (node.hasAttribute(WEB_VIEW_ATTRIBUTE_MAXWIDTH) &&
node[WEB_VIEW_ATTRIBUTE_MAXWIDTH]) {
maxWidth = node[WEB_VIEW_ATTRIBUTE_MAXWIDTH];
} else {
maxWidth = width;
}
var minWidth;
if (node.hasAttribute(WEB_VIEW_ATTRIBUTE_MINWIDTH) &&
node[WEB_VIEW_ATTRIBUTE_MINWIDTH]) {
minWidth = node[WEB_VIEW_ATTRIBUTE_MINWIDTH];
} else {
minWidth = width;
}
if (minWidth > maxWidth) {
minWidth = maxWidth;
}
var maxHeight;
if (node.hasAttribute(WEB_VIEW_ATTRIBUTE_MAXHEIGHT) &&
node[WEB_VIEW_ATTRIBUTE_MAXHEIGHT]) {
maxHeight = node[WEB_VIEW_ATTRIBUTE_MAXHEIGHT];
} else {
maxHeight = height;
}
var minHeight;
if (node.hasAttribute(WEB_VIEW_ATTRIBUTE_MINHEIGHT) &&
node[WEB_VIEW_ATTRIBUTE_MINHEIGHT]) {
minHeight = node[WEB_VIEW_ATTRIBUTE_MINHEIGHT];
} else {
minHeight = height;
}
if (minHeight > maxHeight) {
minHeight = maxHeight;
}
if (webViewEvent.newWidth >= minWidth &&
webViewEvent.newWidth <= maxWidth &&
webViewEvent.newHeight >= minHeight &&
webViewEvent.newHeight <= maxHeight) {
node.style.width = webViewEvent.newWidth + 'px';
node.style.height = webViewEvent.newHeight + 'px';
}
node.dispatchEvent(webViewEvent);
};
/**
* @private
*/
WebViewInternal.prototype.setupWebviewNodeEvents_ = function() {
var self = this;
this.viewInstanceId_ = WebViewNatives.GetNextInstanceID();
var onInstanceIdAllocated = function(e) {
var detail = e.detail ? JSON.parse(e.detail) : {};
self.instanceId_ = detail.windowId;
var params = {
'api': 'webview',
'instanceId': self.viewInstanceId_
};
self.browserPluginNode_['-internal-attach'](params);
var events = self.getEvents_();
for (var eventName in events) {
self.setupEvent_(eventName, events[eventName]);
}
};
this.browserPluginNode_.addEventListener('-internal-instanceid-allocated',
onInstanceIdAllocated);
this.setupWebRequestEvents_();
};
/**
* @private
*/
WebViewInternal.prototype.setupEvent_ = function(eventName, eventInfo) {
var self = this;
var webviewNode = this.webviewNode_;
eventInfo.evt.addListener(function(event) {
var details = {bubbles:true};
if (eventInfo.cancelable)
details.cancelable = true;
var webViewEvent = new Event(eventName, details);
$Array.forEach(eventInfo.fields, function(field) {
if (event[field] !== undefined) {
webViewEvent[field] = event[field];
}
});
if (eventInfo.customHandler) {
eventInfo.customHandler(self, event, webViewEvent);
return;
}
webviewNode.dispatchEvent(webViewEvent);
}, {instanceId: self.instanceId_});
};
/**
* @private
*/
WebViewInternal.prototype.getPermissionTypes_ = function() {
return ['media', 'geolocation', 'pointerLock', 'download'];
};
/**
* @private
*/
WebViewInternal.prototype.handleLoadCommitEvent_ =
function(event, webViewEvent) {
this.currentEntryIndex_ = event.currentEntryIndex;
this.entryCount_ = event.entryCount;
this.processId_ = event.processId;
var oldValue = this.webviewNode_.getAttribute('src');
var newValue = event.url;
if (event.isTopLevel && (oldValue != newValue)) {
// Touching the src attribute triggers a navigation. To avoid
// triggering a page reload on every guest-initiated navigation,
// we use the flag ignoreNextSrcAttributeChange_ here.
this.ignoreNextSrcAttributeChange_ = true;
this.webviewNode_.setAttribute('src', newValue);
}
this.webviewNode_.dispatchEvent(webViewEvent);
}
/**
* @private
*/
WebViewInternal.prototype.handleNewWindowEvent_ =
function(event, webViewEvent) {
var ERROR_MSG_NEWWINDOW_ACTION_ALREADY_TAKEN = '<webview>: ' +
'An action has already been taken for this "newwindow" event.';
var ERROR_MSG_NEWWINDOW_UNABLE_TO_ATTACH = '<webview>: ' +
'Unable to attach the new window to the provided webview.';
var ERROR_MSG_WEBVIEW_EXPECTED = '<webview> element expected.';
var showWarningMessage = function() {
var WARNING_MSG_NEWWINDOW_BLOCKED = '<webview>: A new window was blocked.';
console.warn(WARNING_MSG_NEWWINDOW_BLOCKED);
};
var self = this;
var browserPluginNode = this.browserPluginNode_;
var webviewNode = this.webviewNode_;
var requestId = event.requestId;
var actionTaken = false;
var validateCall = function () {
if (actionTaken) {
throw new Error(ERROR_MSG_NEWWINDOW_ACTION_ALREADY_TAKEN);
}
actionTaken = true;
};
var window = {
attach: function(webview) {
validateCall();
if (!webview)
throw new Error(ERROR_MSG_WEBVIEW_EXPECTED);
// Attach happens asynchronously to give the tagWatcher an opportunity
// to pick up the new webview before attach operates on it, if it hasn't
// been attached to the DOM already.
// Note: Any subsequent errors cannot be exceptions because they happen
// asynchronously.
setTimeout(function() {
var attached =
browserPluginNode['-internal-attachWindowTo'](webview,
event.windowId);
if (!attached) {
console.error(ERROR_MSG_NEWWINDOW_UNABLE_TO_ATTACH);
}
// If the object being passed into attach is not a valid <webview>
// then we will fail and it will be treated as if the new window
// was rejected. The permission API plumbing is used here to clean
// up the state created for the new window if attaching fails.
WebView.setPermission(self.instanceId_, requestId, attached, '');
}, 0);
},
discard: function() {
validateCall();
WebView.setPermission(self.instanceId_, requestId, false, '');
}
};
webViewEvent.window = window;
var defaultPrevented = !webviewNode.dispatchEvent(webViewEvent);
if (actionTaken) {
return;
}
if (defaultPrevented) {
// Make browser plugin track lifetime of |window|.
MessagingNatives.BindToGC(window, function() {
// Avoid showing a warning message if the decision has already been made.
if (actionTaken) {
return;
}
WebView.setPermission(self.instanceId_, requestId, false, '');
showWarningMessage();
});
} else {
actionTaken = true;
// The default action is to discard the window.
WebView.setPermission(self.instanceId_, requestId, false, '');
showWarningMessage();
}
};
WebViewInternal.prototype.handlePermissionEvent_ =
function(event, webViewEvent) {
var ERROR_MSG_PERMISSION_ALREADY_DECIDED = '<webview>: ' +
'Permission has already been decided for this "permissionrequest" event.';
var showWarningMessage = function(permission) {
var WARNING_MSG_PERMISSION_DENIED = '<webview>: ' +
'The permission request for "%1" has been denied.';
console.warn(WARNING_MSG_PERMISSION_DENIED.replace('%1', permission));
};
var PERMISSION_TYPES = this.getPermissionTypes_();
var self = this;
var browserPluginNode = this.browserPluginNode_;
var webviewNode = this.webviewNode_;
var requestId = event.requestId;
var decisionMade = false;
var validateCall = function() {
if (decisionMade) {
throw new Error(ERROR_MSG_PERMISSION_ALREADY_DECIDED);
}
decisionMade = true;
};
// Construct the event.request object.
var request = {
allow: function() {
validateCall();
WebView.setPermission(self.instanceId_, requestId, true, '');
},
deny: function() {
validateCall();
WebView.setPermission(self.instanceId_, requestId, false, '');
}
};
webViewEvent.request = request;
var defaultPrevented = !webviewNode.dispatchEvent(webViewEvent);
if (decisionMade) {
return;
}
if (defaultPrevented) {
// Make browser plugin track lifetime of |request|.
MessagingNatives.BindToGC(request, function() {
// Avoid showing a warning message if the decision has already been made.
if (decisionMade) {
return;
}
WebView.setPermission(self.instanceId_, requestId, false, '');
showWarningMessage(event.permission);
});
} else {
decisionMade = true;
WebView.setPermission(self.instanceId_, requestId, false, '');
showWarningMessage(event.permission);
}
};
/**
* @private
*/
WebViewInternal.prototype.setupWebRequestEvents_ = function() {
var self = this;
var request = {};
var createWebRequestEvent = function(webRequestEvent) {
return function() {
if (!self[webRequestEvent.name + '_']) {
self[webRequestEvent.name + '_'] =
new WebRequestEvent(
'webview.' + webRequestEvent.name,
webRequestEvent.parameters,
webRequestEvent.extraParameters, null,
self.viewInstanceId_);
}
return self[webRequestEvent.name + '_'];
};
};
// Populate the WebRequest events from the API definition.
for (var i = 0; i < WebRequestSchema.events.length; ++i) {
var webRequestEvent = createWebRequestEvent(WebRequestSchema.events[i]);
Object.defineProperty(
request,
WebRequestSchema.events[i].name,
{
get: webRequestEvent,
enumerable: true
}
);
this.maybeAttachWebRequestEventToWebview_(WebRequestSchema.events[i].name,
webRequestEvent);
}
Object.defineProperty(
this.webviewNode_,
'request',
{
value: request,
enumerable: true,
writable: false
}
);
};
// Registers browser plugin <object> custom element.
function registerBrowserPluginElement() {
var proto = Object.create(HTMLObjectElement.prototype);
proto.createdCallback = function() {
this.setAttribute('type', 'application/browser-plugin');
// The <object> node fills in the <webview> container.
this.style.width = '100%';
this.style.height = '100%';
};
proto.attributeChangedCallback = function(name, oldValue, newValue) {
if (!this.internal_) {
return;
}
var internal = this.internal_(secret);
internal.handleBrowserPluginAttributeMutation_(name, newValue);
};
WebViewInternal.BrowserPlugin =
DocumentNatives.RegisterElement('browser-plugin', {extends: 'object',
prototype: proto});
delete proto.createdCallback;
delete proto.enteredDocumentCallback;
delete proto.leftDocumentCallback;
delete proto.attributeChangedCallback;
}
// Registers <webview> custom element.
function registerWebViewElement() {
var proto = Object.create(HTMLElement.prototype);
proto.createdCallback = function() {
new WebViewInternal(this);
};
proto.attributeChangedCallback = function(name, oldValue, newValue) {
var internal = this.internal_(secret);
internal.handleWebviewAttributeMutation_(name, oldValue, newValue);
};
proto.back = function() {
this.go(-1);
};
proto.forward = function() {
this.go(1);
};
proto.canGoBack = function() {
return this.internal_(secret).canGoBack_();
};
proto.canGoForward = function() {
return this.internal_(secret).canGoForward_();
};
proto.getProcessId = function() {
return this.internal_(secret).getProcessId_();
};
proto.go = function(relativeIndex) {
this.internal_(secret).go_(relativeIndex);
};
proto.reload = function() {
this.internal_(secret).reload_();
};
proto.stop = function() {
this.internal_(secret).stop_();
};
proto.terminate = function() {
this.internal_(secret).terminate_();
};
proto.executeScript = function(var_args) {
var internal = this.internal_(secret);
$Function.apply(internal.executeScript_, internal, arguments);
};
proto.insertCSS = function(var_args) {
var internal = this.internal_(secret);
$Function.apply(internal.insertCSS_, internal, arguments);
};
WebViewInternal.maybeRegisterExperimentalAPIs(proto, secret);
window.WebView =
DocumentNatives.RegisterElement('webview', {prototype: proto});
// Delete the callbacks so developers cannot call them and produce unexpected
// behavior.
delete proto.createdCallback;
delete proto.enteredDocumentCallback;
delete proto.leftDocumentCallback;
delete proto.attributeChangedCallback;
}
var useCapture = true;
window.addEventListener('readystatechange', function listener(event) {
if (document.readyState == 'loading')
return;
registerBrowserPluginElement();
registerWebViewElement();
window.removeEventListener(event.type, listener, useCapture);
}, useCapture);
/**
* Implemented when the experimental API is available.
* @private
*/
WebViewInternal.prototype.maybeGetExperimentalEvents_ = function() {};
/**
* Implemented when the experimental API is available.
* @private
*/
WebViewInternal.prototype.maybeAttachWebRequestEventToWebview_ = function() {};
exports.WebView = WebView;
exports.WebViewInternal = WebViewInternal;
exports.CreateEvent = CreateEvent;