Cast: Stop logging kVideoFrameSentToEncoder and rename a couple events.
[chromium-blink-merge.git] / chrome / renderer / resources / extensions / web_view.js
blob7267212b79e275f32430d08b9e9aab659da22b05
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.
5 // This module implements Webview (<webview>) as a custom element that wraps a
6 // BrowserPlugin object element. The object element is hidden within
7 // the shadow DOM of the Webview element.
9 var DocumentNatives = requireNative('document_natives');
10 var EventBindings = require('event_bindings');
11 var IdGenerator = requireNative('id_generator');
12 var MessagingNatives = requireNative('messaging_natives');
13 var WebRequestEvent = require('webRequestInternal').WebRequestEvent;
14 var WebRequestSchema =
15 requireNative('schema_registry').GetSchema('webRequest');
16 var DeclarativeWebRequestSchema =
17 requireNative('schema_registry').GetSchema('declarativeWebRequest');
18 var WebView = require('webview').WebView;
20 var WEB_VIEW_ATTRIBUTE_MAXHEIGHT = 'maxheight';
21 var WEB_VIEW_ATTRIBUTE_MAXWIDTH = 'maxwidth';
22 var WEB_VIEW_ATTRIBUTE_MINHEIGHT = 'minheight';
23 var WEB_VIEW_ATTRIBUTE_MINWIDTH = 'minwidth';
25 /** @type {Array.<string>} */
26 var WEB_VIEW_ATTRIBUTES = [
27 'allowtransparency',
28 'autosize',
29 'name',
30 'partition',
31 WEB_VIEW_ATTRIBUTE_MINHEIGHT,
32 WEB_VIEW_ATTRIBUTE_MINWIDTH,
33 WEB_VIEW_ATTRIBUTE_MAXHEIGHT,
34 WEB_VIEW_ATTRIBUTE_MAXWIDTH
37 var CreateEvent = function(name) {
38 var eventOpts = {supportsListeners: true, supportsFilters: true};
39 return new EventBindings.Event(name, undefined, eventOpts);
42 // WEB_VIEW_EVENTS is a map of stable <webview> DOM event names to their
43 // associated extension event descriptor objects.
44 // An event listener will be attached to the extension event |evt| specified in
45 // the descriptor.
46 // |fields| specifies the public-facing fields in the DOM event that are
47 // accessible to <webview> developers.
48 // |customHandler| allows a handler function to be called each time an extension
49 // event is caught by its event listener. The DOM event should be dispatched
50 // within this handler function. With no handler function, the DOM event
51 // will be dispatched by default each time the extension event is caught.
52 // |cancelable| (default: false) specifies whether the event's default
53 // behavior can be canceled. If the default action associated with the event
54 // is prevented, then its dispatch function will return false in its event
55 // handler. The event must have a custom handler for this to be meaningful.
56 var WEB_VIEW_EVENTS = {
57 'close': {
58 evt: CreateEvent('webview.onClose'),
59 fields: []
61 'consolemessage': {
62 evt: CreateEvent('webview.onConsoleMessage'),
63 fields: ['level', 'message', 'line', 'sourceId']
65 'contentload': {
66 evt: CreateEvent('webview.onContentLoad'),
67 fields: []
69 'dialog': {
70 cancelable: true,
71 customHandler: function(webViewInternal, event, webViewEvent) {
72 webViewInternal.handleDialogEvent(event, webViewEvent);
74 evt: CreateEvent('webview.onDialog'),
75 fields: ['defaultPromptText', 'messageText', 'messageType', 'url']
77 'exit': {
78 evt: CreateEvent('webview.onExit'),
79 fields: ['processId', 'reason']
81 'loadabort': {
82 cancelable: true,
83 customHandler: function(webViewInternal, event, webViewEvent) {
84 webViewInternal.handleLoadAbortEvent(event, webViewEvent);
86 evt: CreateEvent('webview.onLoadAbort'),
87 fields: ['url', 'isTopLevel', 'reason']
89 'loadcommit': {
90 customHandler: function(webViewInternal, event, webViewEvent) {
91 webViewInternal.handleLoadCommitEvent(event, webViewEvent);
93 evt: CreateEvent('webview.onLoadCommit'),
94 fields: ['url', 'isTopLevel']
96 'loadprogress': {
97 evt: CreateEvent('webview.onLoadProgress'),
98 fields: ['url', 'progress']
100 'loadredirect': {
101 evt: CreateEvent('webview.onLoadRedirect'),
102 fields: ['isTopLevel', 'oldUrl', 'newUrl']
104 'loadstart': {
105 evt: CreateEvent('webview.onLoadStart'),
106 fields: ['url', 'isTopLevel']
108 'loadstop': {
109 evt: CreateEvent('webview.onLoadStop'),
110 fields: []
112 'newwindow': {
113 cancelable: true,
114 customHandler: function(webViewInternal, event, webViewEvent) {
115 webViewInternal.handleNewWindowEvent(event, webViewEvent);
117 evt: CreateEvent('webview.onNewWindow'),
118 fields: [
119 'initialHeight',
120 'initialWidth',
121 'targetUrl',
122 'windowOpenDisposition',
123 'name'
126 'permissionrequest': {
127 cancelable: true,
128 customHandler: function(webViewInternal, event, webViewEvent) {
129 webViewInternal.handlePermissionEvent(event, webViewEvent);
131 evt: CreateEvent('webview.onPermissionRequest'),
132 fields: [
133 'identifier',
134 'lastUnlockedBySelf',
135 'name',
136 'permission',
137 'requestMethod',
138 'url',
139 'userGesture'
142 'responsive': {
143 evt: CreateEvent('webview.onResponsive'),
144 fields: ['processId']
146 'sizechanged': {
147 evt: CreateEvent('webview.onSizeChanged'),
148 customHandler: function(webViewInternal, event, webViewEvent) {
149 webViewInternal.handleSizeChangedEvent(event, webViewEvent);
151 fields: ['oldHeight', 'oldWidth', 'newHeight', 'newWidth']
153 'unresponsive': {
154 evt: CreateEvent('webview.onUnresponsive'),
155 fields: ['processId']
159 // Implemented when the experimental API is available.
160 WebViewInternal.maybeRegisterExperimentalAPIs = function(proto) {}
163 * @constructor
165 function WebViewInternal(webviewNode) {
166 privates(webviewNode).internal = this;
167 this.webviewNode = webviewNode;
168 this.attached = false;
169 this.browserPluginNode = this.createBrowserPluginNode();
170 var shadowRoot = this.webviewNode.createShadowRoot();
171 shadowRoot.appendChild(this.browserPluginNode);
173 this.setupWebviewNodeAttributes();
174 this.setupFocusPropagation();
175 this.setupWebviewNodeProperties();
176 this.setupWebviewNodeEvents();
180 * @private
182 WebViewInternal.prototype.createBrowserPluginNode = function() {
183 // We create BrowserPlugin as a custom element in order to observe changes
184 // to attributes synchronously.
185 var browserPluginNode = new WebViewInternal.BrowserPlugin();
186 privates(browserPluginNode).internal = this;
188 var ALL_ATTRIBUTES = WEB_VIEW_ATTRIBUTES.concat(['src']);
189 $Array.forEach(ALL_ATTRIBUTES, function(attributeName) {
190 // Only copy attributes that have been assigned values, rather than copying
191 // a series of undefined attributes to BrowserPlugin.
192 if (this.webviewNode.hasAttribute(attributeName)) {
193 browserPluginNode.setAttribute(
194 attributeName, this.webviewNode.getAttribute(attributeName));
195 } else if (this.webviewNode[attributeName]){
196 // Reading property using has/getAttribute does not work on
197 // document.DOMContentLoaded event (but works on
198 // window.DOMContentLoaded event).
199 // So copy from property if copying from attribute fails.
200 browserPluginNode.setAttribute(
201 attributeName, this.webviewNode[attributeName]);
203 }, this);
205 return browserPluginNode;
209 * @private
211 WebViewInternal.prototype.setupFocusPropagation = function() {
212 if (!this.webviewNode.hasAttribute('tabIndex')) {
213 // <webview> needs a tabIndex in order to be focusable.
214 // TODO(fsamuel): It would be nice to avoid exposing a tabIndex attribute
215 // to allow <webview> to be focusable.
216 // See http://crbug.com/231664.
217 this.webviewNode.setAttribute('tabIndex', -1);
219 var self = this;
220 this.webviewNode.addEventListener('focus', function(e) {
221 // Focus the BrowserPlugin when the <webview> takes focus.
222 self.browserPluginNode.focus();
224 this.webviewNode.addEventListener('blur', function(e) {
225 // Blur the BrowserPlugin when the <webview> loses focus.
226 self.browserPluginNode.blur();
231 * @private
233 WebViewInternal.prototype.canGoBack = function() {
234 return this.entryCount > 1 && this.currentEntryIndex > 0;
238 * @private
240 WebViewInternal.prototype.canGoForward = function() {
241 return this.currentEntryIndex >= 0 &&
242 this.currentEntryIndex < (this.entryCount - 1);
246 * @private
248 WebViewInternal.prototype.clearData = function() {
249 if (!this.instanceId) {
250 return;
252 var args = $Array.concat([this.instanceId], $Array.slice(arguments));
253 $Function.apply(WebView.clearData, null, args);
257 * @private
259 WebViewInternal.prototype.getProcessId = function() {
260 return this.processId;
264 * @private
266 WebViewInternal.prototype.go = function(relativeIndex) {
267 if (!this.instanceId) {
268 return;
270 WebView.go(this.instanceId, relativeIndex);
274 * @private
276 WebViewInternal.prototype.reload = function() {
277 if (!this.instanceId) {
278 return;
280 WebView.reload(this.instanceId);
284 * @private
286 WebViewInternal.prototype.stop = function() {
287 if (!this.instanceId) {
288 return;
290 WebView.stop(this.instanceId);
294 * @private
296 WebViewInternal.prototype.terminate = function() {
297 if (!this.instanceId) {
298 return;
300 WebView.terminate(this.instanceId);
304 * @private
306 WebViewInternal.prototype.validateExecuteCodeCall = function() {
307 var ERROR_MSG_CANNOT_INJECT_SCRIPT = '<webview>: ' +
308 'Script cannot be injected into content until the page has loaded.';
309 if (!this.instanceId) {
310 throw new Error(ERROR_MSG_CANNOT_INJECT_SCRIPT);
315 * @private
317 WebViewInternal.prototype.executeScript = function(var_args) {
318 this.validateExecuteCodeCall();
319 var args = $Array.concat([this.instanceId, this.src],
320 $Array.slice(arguments));
321 $Function.apply(WebView.executeScript, null, args);
325 * @private
327 WebViewInternal.prototype.insertCSS = function(var_args) {
328 this.validateExecuteCodeCall();
329 var args = $Array.concat([this.instanceId, this.src],
330 $Array.slice(arguments));
331 $Function.apply(WebView.insertCSS, null, args);
335 * @private
337 WebViewInternal.prototype.setupWebviewNodeProperties = function() {
338 var ERROR_MSG_CONTENTWINDOW_NOT_AVAILABLE = '<webview>: ' +
339 'contentWindow is not available at this time. It will become available ' +
340 'when the page has finished loading.';
342 var self = this;
343 var browserPluginNode = this.browserPluginNode;
344 // Expose getters and setters for the attributes.
345 $Array.forEach(WEB_VIEW_ATTRIBUTES, function(attributeName) {
346 Object.defineProperty(this.webviewNode, attributeName, {
347 get: function() {
348 if (browserPluginNode.hasOwnProperty(attributeName)) {
349 return browserPluginNode[attributeName];
350 } else {
351 return browserPluginNode.getAttribute(attributeName);
354 set: function(value) {
355 if (browserPluginNode.hasOwnProperty(attributeName)) {
356 // Give the BrowserPlugin first stab at the attribute so that it can
357 // throw an exception if there is a problem. This attribute will then
358 // be propagated back to the <webview>.
359 browserPluginNode[attributeName] = value;
360 } else {
361 browserPluginNode.setAttribute(attributeName, value);
364 enumerable: true
366 }, this);
368 // <webview> src does not quite behave the same as BrowserPlugin src, and so
369 // we don't simply keep the two in sync.
370 this.src = this.webviewNode.getAttribute('src');
371 Object.defineProperty(this.webviewNode, 'src', {
372 get: function() {
373 return self.src;
375 set: function(value) {
376 self.webviewNode.setAttribute('src', value);
378 // No setter.
379 enumerable: true
382 // We cannot use {writable: true} property descriptor because we want a
383 // dynamic getter value.
384 Object.defineProperty(this.webviewNode, 'contentWindow', {
385 get: function() {
386 if (browserPluginNode.contentWindow)
387 return browserPluginNode.contentWindow;
388 window.console.error(ERROR_MSG_CONTENTWINDOW_NOT_AVAILABLE);
390 // No setter.
391 enumerable: true
396 * @private
398 WebViewInternal.prototype.setupWebviewNodeAttributes = function() {
399 this.setupWebViewSrcAttributeMutationObserver();
403 * @private
405 WebViewInternal.prototype.setupWebViewSrcAttributeMutationObserver =
406 function() {
407 // The purpose of this mutation observer is to catch assignment to the src
408 // attribute without any changes to its value. This is useful in the case
409 // where the webview guest has crashed and navigating to the same address
410 // spawns off a new process.
411 var self = this;
412 this.srcObserver = new MutationObserver(function(mutations) {
413 $Array.forEach(mutations, function(mutation) {
414 var oldValue = mutation.oldValue;
415 var newValue = self.webviewNode.getAttribute(mutation.attributeName);
416 if (oldValue != newValue) {
417 return;
419 self.handleWebviewAttributeMutation(
420 mutation.attributeName, oldValue, newValue);
423 var params = {
424 attributes: true,
425 attributeOldValue: true,
426 attributeFilter: ['src']
428 this.srcObserver.observe(this.webviewNode, params);
432 * @private
434 WebViewInternal.prototype.handleWebviewAttributeMutation =
435 function(name, oldValue, newValue) {
436 // This observer monitors mutations to attributes of the <webview> and
437 // updates the BrowserPlugin properties accordingly. In turn, updating
438 // a BrowserPlugin property will update the corresponding BrowserPlugin
439 // attribute, if necessary. See BrowserPlugin::UpdateDOMAttribute for more
440 // details.
441 if (name == 'src') {
442 // We treat null attribute (attribute removed) and the empty string as
443 // one case.
444 oldValue = oldValue || '';
445 newValue = newValue || '';
446 // Once we have navigated, we don't allow clearing the src attribute.
447 // Once <webview> enters a navigated state, it cannot be return back to a
448 // placeholder state.
449 if (newValue == '' && oldValue != '') {
450 // src attribute changes normally initiate a navigation. We suppress
451 // the next src attribute handler call to avoid reloading the page
452 // on every guest-initiated navigation.
453 this.ignoreNextSrcAttributeChange = true;
454 this.webviewNode.setAttribute('src', oldValue);
455 return;
457 this.src = newValue;
458 if (this.ignoreNextSrcAttributeChange) {
459 // Don't allow the src mutation observer to see this change.
460 this.srcObserver.takeRecords();
461 this.ignoreNextSrcAttributeChange = false;
462 return;
465 if (this.browserPluginNode.hasOwnProperty(name)) {
466 this.browserPluginNode[name] = newValue;
467 } else {
468 this.browserPluginNode.setAttribute(name, newValue);
473 * @private
475 WebViewInternal.prototype.handleBrowserPluginAttributeMutation =
476 function(name, newValue) {
477 // This observer monitors mutations to attributes of the BrowserPlugin and
478 // updates the <webview> attributes accordingly.
479 // |newValue| is null if the attribute |name| has been removed.
480 if (newValue != null) {
481 // Update the <webview> attribute to match the BrowserPlugin attribute.
482 // Note: Calling setAttribute on <webview> will trigger its mutation
483 // observer which will then propagate that attribute to BrowserPlugin. In
484 // cases where we permit assigning a BrowserPlugin attribute the same value
485 // again (such as navigation when crashed), this could end up in an infinite
486 // loop. Thus, we avoid this loop by only updating the <webview> attribute
487 // if the BrowserPlugin attributes differs from it.
488 if (newValue != this.webviewNode.getAttribute(name)) {
489 this.webviewNode.setAttribute(name, newValue);
491 } else {
492 // If an attribute is removed from the BrowserPlugin, then remove it
493 // from the <webview> as well.
494 this.webviewNode.removeAttribute(name);
499 * @private
501 WebViewInternal.prototype.getEvents = function() {
502 var experimentalEvents = this.maybeGetExperimentalEvents();
503 for (var eventName in experimentalEvents) {
504 WEB_VIEW_EVENTS[eventName] = experimentalEvents[eventName];
506 return WEB_VIEW_EVENTS;
509 WebViewInternal.prototype.handleSizeChangedEvent =
510 function(event, webViewEvent) {
511 var node = this.webviewNode;
513 var width = node.offsetWidth;
514 var height = node.offsetHeight;
516 // Check the current bounds to make sure we do not resize <webview>
517 // outside of current constraints.
518 var maxWidth;
519 if (node.hasAttribute(WEB_VIEW_ATTRIBUTE_MAXWIDTH) &&
520 node[WEB_VIEW_ATTRIBUTE_MAXWIDTH]) {
521 maxWidth = node[WEB_VIEW_ATTRIBUTE_MAXWIDTH];
522 } else {
523 maxWidth = width;
526 var minWidth;
527 if (node.hasAttribute(WEB_VIEW_ATTRIBUTE_MINWIDTH) &&
528 node[WEB_VIEW_ATTRIBUTE_MINWIDTH]) {
529 minWidth = node[WEB_VIEW_ATTRIBUTE_MINWIDTH];
530 } else {
531 minWidth = width;
533 if (minWidth > maxWidth) {
534 minWidth = maxWidth;
537 var maxHeight;
538 if (node.hasAttribute(WEB_VIEW_ATTRIBUTE_MAXHEIGHT) &&
539 node[WEB_VIEW_ATTRIBUTE_MAXHEIGHT]) {
540 maxHeight = node[WEB_VIEW_ATTRIBUTE_MAXHEIGHT];
541 } else {
542 maxHeight = height;
544 var minHeight;
545 if (node.hasAttribute(WEB_VIEW_ATTRIBUTE_MINHEIGHT) &&
546 node[WEB_VIEW_ATTRIBUTE_MINHEIGHT]) {
547 minHeight = node[WEB_VIEW_ATTRIBUTE_MINHEIGHT];
548 } else {
549 minHeight = height;
551 if (minHeight > maxHeight) {
552 minHeight = maxHeight;
555 if (webViewEvent.newWidth >= minWidth &&
556 webViewEvent.newWidth <= maxWidth &&
557 webViewEvent.newHeight >= minHeight &&
558 webViewEvent.newHeight <= maxHeight) {
559 node.style.width = webViewEvent.newWidth + 'px';
560 node.style.height = webViewEvent.newHeight + 'px';
562 node.dispatchEvent(webViewEvent);
566 * @private
568 WebViewInternal.prototype.setupWebviewNodeEvents = function() {
569 var self = this;
570 this.viewInstanceId = IdGenerator.GetNextId();
571 var onInstanceIdAllocated = function(e) {
572 var detail = e.detail ? JSON.parse(e.detail) : {};
573 self.attachWindowAndSetUpEvents(detail.windowId);
575 this.browserPluginNode.addEventListener('-internal-instanceid-allocated',
576 onInstanceIdAllocated);
577 this.setupWebRequestEvents();
578 this.setupExperimentalContextMenus_();
580 this.on = {};
581 var events = self.getEvents();
582 for (var eventName in events) {
583 this.setupEventProperty(eventName);
588 * @private
590 WebViewInternal.prototype.setupEvent = function(eventName, eventInfo) {
591 var self = this;
592 var webviewNode = this.webviewNode;
593 eventInfo.evt.addListener(function(event) {
594 var details = {bubbles:true};
595 if (eventInfo.cancelable)
596 details.cancelable = true;
597 var webViewEvent = new Event(eventName, details);
598 $Array.forEach(eventInfo.fields, function(field) {
599 if (event[field] !== undefined) {
600 webViewEvent[field] = event[field];
603 if (eventInfo.customHandler) {
604 eventInfo.customHandler(self, event, webViewEvent);
605 return;
607 webviewNode.dispatchEvent(webViewEvent);
608 }, {instanceId: self.instanceId});
612 * Adds an 'on<event>' property on the webview, which can be used to set/unset
613 * an event handler.
614 * @private
616 WebViewInternal.prototype.setupEventProperty = function(eventName) {
617 var propertyName = 'on' + eventName.toLowerCase();
618 var self = this;
619 var webviewNode = this.webviewNode;
620 Object.defineProperty(webviewNode, propertyName, {
621 get: function() {
622 return self.on[propertyName];
624 set: function(value) {
625 if (self.on[propertyName])
626 webviewNode.removeEventListener(eventName, self.on[propertyName]);
627 self.on[propertyName] = value;
628 if (value)
629 webviewNode.addEventListener(eventName, value);
631 enumerable: true
636 * @private
638 WebViewInternal.prototype.getPermissionTypes = function() {
639 var permissions =
640 ['media', 'geolocation', 'pointerLock', 'download', 'loadplugin'];
641 return permissions.concat(this.maybeGetExperimentalPermissions());
645 * @private
647 WebViewInternal.prototype.handleDialogEvent =
648 function(event, webViewEvent) {
649 var showWarningMessage = function(dialogType) {
650 var VOWELS = ['a', 'e', 'i', 'o', 'u'];
651 var WARNING_MSG_DIALOG_BLOCKED = '<webview>: %1 %2 dialog was blocked.';
652 var article = (VOWELS.indexOf(dialogType.charAt(0)) >= 0) ? 'An' : 'A';
653 var output = WARNING_MSG_DIALOG_BLOCKED.replace('%1', article);
654 output = output.replace('%2', dialogType);
655 window.console.warn(output);
658 var self = this;
659 var browserPluginNode = this.browserPluginNode;
660 var webviewNode = this.webviewNode;
662 var requestId = event.requestId;
663 var actionTaken = false;
665 var validateCall = function() {
666 var ERROR_MSG_DIALOG_ACTION_ALREADY_TAKEN = '<webview>: ' +
667 'An action has already been taken for this "dialog" event.';
669 if (actionTaken) {
670 throw new Error(ERROR_MSG_DIALOG_ACTION_ALREADY_TAKEN);
672 actionTaken = true;
675 var dialog = {
676 ok: function(user_input) {
677 validateCall();
678 user_input = user_input || '';
679 WebView.setPermission(self.instanceId, requestId, 'allow', user_input);
681 cancel: function() {
682 validateCall();
683 WebView.setPermission(self.instanceId, requestId, 'deny');
686 webViewEvent.dialog = dialog;
688 var defaultPrevented = !webviewNode.dispatchEvent(webViewEvent);
689 if (actionTaken) {
690 return;
693 if (defaultPrevented) {
694 // Tell the JavaScript garbage collector to track lifetime of |dialog| and
695 // call back when the dialog object has been collected.
696 MessagingNatives.BindToGC(dialog, function() {
697 // Avoid showing a warning message if the decision has already been made.
698 if (actionTaken) {
699 return;
701 WebView.setPermission(
702 self.instanceId, requestId, 'default', '', function(allowed) {
703 if (allowed) {
704 return;
706 showWarningMessage(event.messageType);
709 } else {
710 actionTaken = true;
711 // The default action is equivalent to canceling the dialog.
712 WebView.setPermission(
713 self.instanceId, requestId, 'default', '', function(allowed) {
714 if (allowed) {
715 return;
717 showWarningMessage(event.messageType);
723 * @private
725 WebViewInternal.prototype.handleLoadAbortEvent =
726 function(event, webViewEvent) {
727 var showWarningMessage = function(reason) {
728 var WARNING_MSG_LOAD_ABORTED = '<webview>: ' +
729 'The load has aborted with reason "%1".';
730 window.console.warn(WARNING_MSG_LOAD_ABORTED.replace('%1', reason));
732 if (this.webviewNode.dispatchEvent(webViewEvent)) {
733 showWarningMessage(event.reason);
738 * @private
740 WebViewInternal.prototype.handleLoadCommitEvent =
741 function(event, webViewEvent) {
742 this.currentEntryIndex = event.currentEntryIndex;
743 this.entryCount = event.entryCount;
744 this.processId = event.processId;
745 var oldValue = this.webviewNode.getAttribute('src');
746 var newValue = event.url;
747 if (event.isTopLevel && (oldValue != newValue)) {
748 // Touching the src attribute triggers a navigation. To avoid
749 // triggering a page reload on every guest-initiated navigation,
750 // we use the flag ignoreNextSrcAttributeChange here.
751 this.ignoreNextSrcAttributeChange = true;
752 this.webviewNode.setAttribute('src', newValue);
754 this.webviewNode.dispatchEvent(webViewEvent);
758 * @private
760 WebViewInternal.prototype.handleNewWindowEvent =
761 function(event, webViewEvent) {
762 var ERROR_MSG_NEWWINDOW_ACTION_ALREADY_TAKEN = '<webview>: ' +
763 'An action has already been taken for this "newwindow" event.';
765 var ERROR_MSG_NEWWINDOW_UNABLE_TO_ATTACH = '<webview>: ' +
766 'Unable to attach the new window to the provided webview.';
768 var ERROR_MSG_WEBVIEW_EXPECTED = '<webview> element expected.';
770 var showWarningMessage = function() {
771 var WARNING_MSG_NEWWINDOW_BLOCKED = '<webview>: A new window was blocked.';
772 window.console.warn(WARNING_MSG_NEWWINDOW_BLOCKED);
775 var self = this;
776 var browserPluginNode = this.browserPluginNode;
777 var webviewNode = this.webviewNode;
779 var requestId = event.requestId;
780 var actionTaken = false;
782 var validateCall = function () {
783 if (actionTaken) {
784 throw new Error(ERROR_MSG_NEWWINDOW_ACTION_ALREADY_TAKEN);
786 actionTaken = true;
789 var windowObj = {
790 attach: function(webview) {
791 validateCall();
792 if (!webview || !webview.tagName || webview.tagName != 'WEBVIEW')
793 throw new Error(ERROR_MSG_WEBVIEW_EXPECTED);
794 // Attach happens asynchronously to give the tagWatcher an opportunity
795 // to pick up the new webview before attach operates on it, if it hasn't
796 // been attached to the DOM already.
797 // Note: Any subsequent errors cannot be exceptions because they happen
798 // asynchronously.
799 setTimeout(function() {
800 var webViewInternal = privates(webview).internal;
801 var attached =
802 webViewInternal.attachWindowAndSetUpEvents(event.windowId);
804 if (!attached) {
805 window.console.error(ERROR_MSG_NEWWINDOW_UNABLE_TO_ATTACH);
807 // If the object being passed into attach is not a valid <webview>
808 // then we will fail and it will be treated as if the new window
809 // was rejected. The permission API plumbing is used here to clean
810 // up the state created for the new window if attaching fails.
811 WebView.setPermission(
812 self.instanceId, requestId, attached ? 'allow' : 'deny');
813 }, 0);
815 discard: function() {
816 validateCall();
817 WebView.setPermission(self.instanceId, requestId, 'deny');
820 webViewEvent.window = windowObj;
822 var defaultPrevented = !webviewNode.dispatchEvent(webViewEvent);
823 if (actionTaken) {
824 return;
827 if (defaultPrevented) {
828 // Make browser plugin track lifetime of |windowObj|.
829 MessagingNatives.BindToGC(windowObj, function() {
830 // Avoid showing a warning message if the decision has already been made.
831 if (actionTaken) {
832 return;
834 WebView.setPermission(
835 self.instanceId, requestId, 'default', '', function(allowed) {
836 if (allowed) {
837 return;
839 showWarningMessage();
842 } else {
843 actionTaken = true;
844 // The default action is to discard the window.
845 WebView.setPermission(
846 self.instanceId, requestId, 'default', '', function(allowed) {
847 if (allowed) {
848 return;
850 showWarningMessage();
855 WebViewInternal.prototype.handlePermissionEvent =
856 function(event, webViewEvent) {
857 var ERROR_MSG_PERMISSION_ALREADY_DECIDED = '<webview>: ' +
858 'Permission has already been decided for this "permissionrequest" event.';
860 var showWarningMessage = function(permission) {
861 var WARNING_MSG_PERMISSION_DENIED = '<webview>: ' +
862 'The permission request for "%1" has been denied.';
863 window.console.warn(
864 WARNING_MSG_PERMISSION_DENIED.replace('%1', permission));
867 var requestId = event.requestId;
868 var self = this;
870 if (this.getPermissionTypes().indexOf(event.permission) < 0) {
871 // The permission type is not allowed. Trigger the default response.
872 WebView.setPermission(
873 self.instanceId, requestId, 'default', '', function(allowed) {
874 if (allowed) {
875 return;
877 showWarningMessage(event.permission);
879 return;
882 var browserPluginNode = this.browserPluginNode;
883 var webviewNode = this.webviewNode;
885 var decisionMade = false;
887 var validateCall = function() {
888 if (decisionMade) {
889 throw new Error(ERROR_MSG_PERMISSION_ALREADY_DECIDED);
891 decisionMade = true;
894 // Construct the event.request object.
895 var request = {
896 allow: function() {
897 validateCall();
898 WebView.setPermission(self.instanceId, requestId, 'allow');
900 deny: function() {
901 validateCall();
902 WebView.setPermission(self.instanceId, requestId, 'deny');
905 webViewEvent.request = request;
907 var defaultPrevented = !webviewNode.dispatchEvent(webViewEvent);
908 if (decisionMade) {
909 return;
912 if (defaultPrevented) {
913 // Make browser plugin track lifetime of |request|.
914 MessagingNatives.BindToGC(request, function() {
915 // Avoid showing a warning message if the decision has already been made.
916 if (decisionMade) {
917 return;
919 WebView.setPermission(
920 self.instanceId, requestId, 'default', '', function(allowed) {
921 if (allowed) {
922 return;
924 showWarningMessage(event.permission);
927 } else {
928 decisionMade = true;
929 WebView.setPermission(
930 self.instanceId, requestId, 'default', '', function(allowed) {
931 if (allowed) {
932 return;
934 showWarningMessage(event.permission);
940 * @private
942 WebViewInternal.prototype.setupWebRequestEvents = function() {
943 var self = this;
944 var request = {};
945 var createWebRequestEvent = function(webRequestEvent) {
946 return function() {
947 if (!self[webRequestEvent.name]) {
948 self[webRequestEvent.name] =
949 new WebRequestEvent(
950 'webview.' + webRequestEvent.name,
951 webRequestEvent.parameters,
952 webRequestEvent.extraParameters, webRequestEvent.options,
953 self.viewInstanceId);
955 return self[webRequestEvent.name];
959 for (var i = 0; i < DeclarativeWebRequestSchema.events.length; ++i) {
960 var eventSchema = DeclarativeWebRequestSchema.events[i];
961 var webRequestEvent = createWebRequestEvent(eventSchema);
962 this.maybeAttachWebRequestEventToObject(request,
963 eventSchema.name,
964 webRequestEvent);
967 // Populate the WebRequest events from the API definition.
968 for (var i = 0; i < WebRequestSchema.events.length; ++i) {
969 var webRequestEvent = createWebRequestEvent(WebRequestSchema.events[i]);
970 Object.defineProperty(
971 request,
972 WebRequestSchema.events[i].name,
974 get: webRequestEvent,
975 enumerable: true
978 this.maybeAttachWebRequestEventToObject(this.webviewNode,
979 WebRequestSchema.events[i].name,
980 webRequestEvent);
982 Object.defineProperty(
983 this.webviewNode,
984 'request',
986 value: request,
987 enumerable: true
992 /** @private */
993 WebViewInternal.prototype.getUserAgent = function() {
994 return this.userAgentOverride || navigator.userAgent;
997 /** @private */
998 WebViewInternal.prototype.isUserAgentOverridden = function() {
999 return !!this.userAgentOverride &&
1000 this.userAgentOverride != navigator.userAgent;
1003 /** @private */
1004 WebViewInternal.prototype.setUserAgentOverride = function(userAgentOverride) {
1005 this.userAgentOverride = userAgentOverride;
1006 if (!this.instanceId) {
1007 // If we are not attached yet, then we will pick up the user agent on
1008 // attachment.
1009 return;
1011 WebView.overrideUserAgent(this.instanceId, userAgentOverride);
1014 /** @private */
1015 WebViewInternal.prototype.attachWindowAndSetUpEvents = function(instanceId) {
1016 this.instanceId = instanceId;
1017 var params = {
1018 'api': 'webview',
1019 'instanceId': this.viewInstanceId
1021 if (this.userAgentOverride) {
1022 params['userAgentOverride'] = this.userAgentOverride;
1024 this.browserPluginNode['-internal-attach'](this.instanceId, params);
1026 var events = this.getEvents();
1027 for (var eventName in events) {
1028 this.setupEvent(eventName, events[eventName]);
1030 return true;
1033 // Registers browser plugin <object> custom element.
1034 function registerBrowserPluginElement() {
1035 var proto = Object.create(HTMLObjectElement.prototype);
1037 proto.createdCallback = function() {
1038 this.setAttribute('type', 'application/browser-plugin');
1039 // The <object> node fills in the <webview> container.
1040 this.style.width = '100%';
1041 this.style.height = '100%';
1044 proto.attributeChangedCallback = function(name, oldValue, newValue) {
1045 var internal = privates(this).internal;
1046 if (!internal) {
1047 return;
1049 internal.handleBrowserPluginAttributeMutation(name, newValue);
1052 proto.attachedCallback = function() {
1053 // Load the plugin immediately.
1054 var unused = this.nonExistentAttribute;
1057 WebViewInternal.BrowserPlugin =
1058 DocumentNatives.RegisterElement('browser-plugin', {extends: 'object',
1059 prototype: proto});
1061 delete proto.createdCallback;
1062 delete proto.attachedCallback;
1063 delete proto.detachedCallback;
1064 delete proto.attributeChangedCallback;
1067 // Registers <webview> custom element.
1068 function registerWebViewElement() {
1069 var proto = Object.create(HTMLElement.prototype);
1071 proto.createdCallback = function() {
1072 new WebViewInternal(this);
1075 proto.attributeChangedCallback = function(name, oldValue, newValue) {
1076 var internal = privates(this).internal;
1077 if (!internal) {
1078 return;
1080 internal.handleWebviewAttributeMutation(name, oldValue, newValue);
1083 proto.back = function() {
1084 this.go(-1);
1087 proto.forward = function() {
1088 this.go(1);
1091 proto.canGoBack = function() {
1092 return privates(this).internal.canGoBack();
1095 proto.canGoForward = function() {
1096 return privates(this).internal.canGoForward();
1099 proto.clearData = function() {
1100 var internal = privates(this).internal;
1101 $Function.apply(internal.clearData, internal, arguments);
1104 proto.getProcessId = function() {
1105 return privates(this).internal.getProcessId();
1108 proto.go = function(relativeIndex) {
1109 privates(this).internal.go(relativeIndex);
1112 proto.reload = function() {
1113 privates(this).internal.reload();
1116 proto.stop = function() {
1117 privates(this).internal.stop();
1120 proto.terminate = function() {
1121 privates(this).internal.terminate();
1124 proto.executeScript = function(var_args) {
1125 var internal = privates(this).internal;
1126 $Function.apply(internal.executeScript, internal, arguments);
1129 proto.insertCSS = function(var_args) {
1130 var internal = privates(this).internal;
1131 $Function.apply(internal.insertCSS, internal, arguments);
1134 proto.getUserAgent = function() {
1135 return privates(this).internal.getUserAgent();
1138 proto.isUserAgentOverridden = function() {
1139 return privates(this).internal.isUserAgentOverridden();
1142 proto.setUserAgentOverride = function(userAgentOverride) {
1143 privates(this).internal.setUserAgentOverride(userAgentOverride);
1145 WebViewInternal.maybeRegisterExperimentalAPIs(proto);
1147 window.WebView =
1148 DocumentNatives.RegisterElement('webview', {prototype: proto});
1150 // Delete the callbacks so developers cannot call them and produce unexpected
1151 // behavior.
1152 delete proto.createdCallback;
1153 delete proto.attachedCallback;
1154 delete proto.detachedCallback;
1155 delete proto.attributeChangedCallback;
1158 var useCapture = true;
1159 window.addEventListener('readystatechange', function listener(event) {
1160 if (document.readyState == 'loading')
1161 return;
1163 registerBrowserPluginElement();
1164 registerWebViewElement();
1165 window.removeEventListener(event.type, listener, useCapture);
1166 }, useCapture);
1169 * Implemented when the experimental API is available.
1170 * @private
1172 WebViewInternal.prototype.maybeGetExperimentalEvents = function() {};
1175 * Implemented when the experimental API is available.
1176 * @private
1178 WebViewInternal.prototype.maybeAttachWebRequestEventToObject = function() {};
1181 * Implemented when the experimental API is available.
1182 * @private
1184 WebViewInternal.prototype.maybeGetExperimentalPermissions = function() {
1185 return [];
1189 * Implemented when the experimental API is available.
1190 * @private
1192 WebViewInternal.prototype.setupExperimentalContextMenus_ = function() {};
1194 exports.WebView = WebView;
1195 exports.WebViewInternal = WebViewInternal;
1196 exports.CreateEvent = CreateEvent;