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 GuestViewInternal =
11 require('binding').Binding.create('guestViewInternal').generate();
12 var guestViewInternalNatives = requireNative('guest_view_internal');
13 var IdGenerator = requireNative('id_generator');
14 var WebViewConstants = require('webViewConstants').WebViewConstants;
15 var WebViewEvents = require('webViewEvents').WebViewEvents;
16 var WebViewInternal = require('webViewInternal').WebViewInternal;
18 // Represents the internal state of the WebView node.
19 function WebView(webviewNode) {
20 privates(webviewNode).internal = this;
21 this.webviewNode = webviewNode;
22 this.attached = false;
23 this.pendingGuestCreation = false;
24 this.elementAttached = false;
26 this.beforeFirstNavigation = true;
27 this.contentWindow = null;
29 // on* Event handlers.
32 this.browserPluginNode = this.createBrowserPluginNode();
33 var shadowRoot = this.webviewNode.createShadowRoot();
34 this.setupWebViewAttributes();
35 this.setupFocusPropagation();
36 this.setupWebviewNodeProperties();
38 this.viewInstanceId = IdGenerator.GetNextId();
40 new WebViewEvents(this, this.viewInstanceId);
42 shadowRoot.appendChild(this.browserPluginNode);
45 WebView.prototype.createBrowserPluginNode = function() {
46 // We create BrowserPlugin as a custom element in order to observe changes
47 // to attributes synchronously.
48 var browserPluginNode = new WebView.BrowserPlugin();
49 privates(browserPluginNode).internal = this;
50 return browserPluginNode;
53 WebView.prototype.getGuestInstanceId = function() {
54 return this.guestInstanceId;
57 // Resets some state upon reattaching <webview> element to the DOM.
58 WebView.prototype.reset = function() {
59 // If guestInstanceId is defined then the <webview> has navigated and has
60 // already picked up a partition ID. Thus, we need to reset the initialization
61 // state. However, it may be the case that beforeFirstNavigation is false BUT
62 // guestInstanceId has yet to be initialized. This means that we have not
63 // heard back from createGuest yet. We will not reset the flag in this case so
64 // that we don't end up allocating a second guest.
65 if (this.guestInstanceId) {
66 GuestViewInternal.destroyGuest(this.guestInstanceId);
67 this.guestInstanceId = undefined;
68 this.beforeFirstNavigation = true;
69 this.attributes[WebViewConstants.ATTRIBUTE_PARTITION].validPartitionId =
71 this.contentWindow = null;
73 this.internalInstanceId = 0;
76 // Sets the <webview>.request property.
77 WebView.prototype.setRequestPropertyOnWebViewNode = function(request) {
78 Object.defineProperty(
88 WebView.prototype.setupFocusPropagation = function() {
89 if (!this.webviewNode.hasAttribute('tabIndex')) {
90 // <webview> needs a tabIndex in order to be focusable.
91 // TODO(fsamuel): It would be nice to avoid exposing a tabIndex attribute
92 // to allow <webview> to be focusable.
93 // See http://crbug.com/231664.
94 this.webviewNode.setAttribute('tabIndex', -1);
96 this.webviewNode.addEventListener('focus', function(e) {
97 // Focus the BrowserPlugin when the <webview> takes focus.
98 this.browserPluginNode.focus();
100 this.webviewNode.addEventListener('blur', function(e) {
101 // Blur the BrowserPlugin when the <webview> loses focus.
102 this.browserPluginNode.blur();
106 // Validation helper function for executeScript() and insertCSS().
107 WebView.prototype.validateExecuteCodeCall = function() {
108 if (!this.guestInstanceId) {
109 throw new Error(WebViewConstants.ERROR_MSG_CANNOT_INJECT_SCRIPT);
113 WebView.prototype.setupWebviewNodeProperties = function() {
114 // We cannot use {writable: true} property descriptor because we want a
115 // dynamic getter value.
116 Object.defineProperty(this.webviewNode, 'contentWindow', {
118 if (this.contentWindow) {
119 return this.contentWindow;
121 window.console.error(
122 WebViewConstants.ERROR_MSG_CONTENTWINDOW_NOT_AVAILABLE);
129 // This observer monitors mutations to attributes of the <webview> and
130 // updates the BrowserPlugin properties accordingly. In turn, updating
131 // a BrowserPlugin property will update the corresponding BrowserPlugin
132 // attribute, if necessary. See BrowserPlugin::UpdateDOMAttribute for more
134 WebView.prototype.handleWebviewAttributeMutation = function(
135 attributeName, oldValue, newValue) {
136 if (!this.attributes[attributeName] ||
137 this.attributes[attributeName].ignoreMutation) {
141 // Let the changed attribute handle its own mutation;
142 this.attributes[attributeName].handleMutation(oldValue, newValue);
145 WebView.prototype.handleBrowserPluginAttributeMutation =
146 function(attributeName, oldValue, newValue) {
147 if (attributeName == WebViewConstants.ATTRIBUTE_INTERNALINSTANCEID &&
148 !oldValue && !!newValue) {
149 this.browserPluginNode.removeAttribute(
150 WebViewConstants.ATTRIBUTE_INTERNALINSTANCEID);
151 this.internalInstanceId = parseInt(newValue);
153 if (!this.guestInstanceId) {
156 guestViewInternalNatives.AttachGuest(
157 this.internalInstanceId,
158 this.guestInstanceId,
159 this.buildAttachParams(),
161 this.contentWindow = w;
167 WebView.prototype.onSizeChanged = function(webViewEvent) {
168 var newWidth = webViewEvent.newWidth;
169 var newHeight = webViewEvent.newHeight;
171 var node = this.webviewNode;
173 var width = node.offsetWidth;
174 var height = node.offsetHeight;
176 // Check the current bounds to make sure we do not resize <webview>
177 // outside of current constraints.
179 if (node.hasAttribute(WebViewConstants.ATTRIBUTE_MAXWIDTH) &&
180 node[WebViewConstants.ATTRIBUTE_MAXWIDTH]) {
181 maxWidth = node[WebViewConstants.ATTRIBUTE_MAXWIDTH];
187 if (node.hasAttribute(WebViewConstants.ATTRIBUTE_MINWIDTH) &&
188 node[WebViewConstants.ATTRIBUTE_MINWIDTH]) {
189 minWidth = node[WebViewConstants.ATTRIBUTE_MINWIDTH];
193 if (minWidth > maxWidth) {
198 if (node.hasAttribute(WebViewConstants.ATTRIBUTE_MAXHEIGHT) &&
199 node[WebViewConstants.ATTRIBUTE_MAXHEIGHT]) {
200 maxHeight = node[WebViewConstants.ATTRIBUTE_MAXHEIGHT];
206 if (node.hasAttribute(WebViewConstants.ATTRIBUTE_MINHEIGHT) &&
207 node[WebViewConstants.ATTRIBUTE_MINHEIGHT]) {
208 minHeight = node[WebViewConstants.ATTRIBUTE_MINHEIGHT];
212 if (minHeight > maxHeight) {
213 minHeight = maxHeight;
216 if (!this.attributes[WebViewConstants.ATTRIBUTE_AUTOSIZE].getValue() ||
217 (newWidth >= minWidth &&
218 newWidth <= maxWidth &&
219 newHeight >= minHeight &&
220 newHeight <= maxHeight)) {
221 node.style.width = newWidth + 'px';
222 node.style.height = newHeight + 'px';
223 // Only fire the DOM event if the size of the <webview> has actually
225 this.dispatchEvent(webViewEvent);
229 // Returns if <object> is in the render tree.
230 WebView.prototype.isPluginInRenderTree = function() {
231 return !!this.internalInstanceId && this.internalInstanceId != 0;
234 WebView.prototype.hasNavigated = function() {
235 return !this.beforeFirstNavigation;
238 WebView.prototype.parseSrcAttribute = function() {
239 if (!this.attributes[WebViewConstants.ATTRIBUTE_PARTITION].validPartitionId ||
240 !this.attributes[WebViewConstants.ATTRIBUTE_SRC].getValue()) {
244 if (this.guestInstanceId == undefined) {
245 if (this.beforeFirstNavigation) {
246 this.beforeFirstNavigation = false;
252 // Navigate to |this.src|.
253 WebViewInternal.navigate(
254 this.guestInstanceId,
255 this.attributes[WebViewConstants.ATTRIBUTE_SRC].getValue());
258 WebView.prototype.parseAttributes = function() {
259 if (!this.elementAttached) {
262 this.parseSrcAttribute();
265 WebView.prototype.createGuest = function() {
266 if (this.pendingGuestCreation) {
270 'storagePartitionId': this.attributes[
271 WebViewConstants.ATTRIBUTE_PARTITION].getValue()
273 GuestViewInternal.createGuest(
276 function(guestInstanceId) {
277 this.pendingGuestCreation = false;
278 if (!this.elementAttached) {
279 GuestViewInternal.destroyGuest(guestInstanceId);
282 this.attachWindow(guestInstanceId);
285 this.pendingGuestCreation = true;
288 WebView.prototype.onFrameNameChanged = function(name) {
291 this.webviewNode.removeAttribute(WebViewConstants.ATTRIBUTE_NAME);
293 this.attributes[WebViewConstants.ATTRIBUTE_NAME].setValue(name);
297 WebView.prototype.dispatchEvent = function(webViewEvent) {
298 return this.webviewNode.dispatchEvent(webViewEvent);
301 // Adds an 'on<event>' property on the webview, which can be used to set/unset
303 WebView.prototype.setupEventProperty = function(eventName) {
304 var propertyName = 'on' + eventName.toLowerCase();
305 Object.defineProperty(this.webviewNode, propertyName, {
307 return this.on[propertyName];
309 set: function(value) {
310 if (this.on[propertyName])
311 this.webviewNode.removeEventListener(eventName, this.on[propertyName]);
312 this.on[propertyName] = value;
314 this.webviewNode.addEventListener(eventName, value);
320 // Updates state upon loadcommit.
321 WebView.prototype.onLoadCommit = function(
322 baseUrlForDataUrl, currentEntryIndex, entryCount,
323 processId, url, isTopLevel) {
324 this.baseUrlForDataUrl = baseUrlForDataUrl;
325 this.currentEntryIndex = currentEntryIndex;
326 this.entryCount = entryCount;
327 this.processId = processId;
328 var oldValue = this.attributes[WebViewConstants.ATTRIBUTE_SRC].getValue();
330 if (isTopLevel && (oldValue != newValue)) {
331 // Touching the src attribute triggers a navigation. To avoid
332 // triggering a page reload on every guest-initiated navigation,
333 // we do not handle this mutation.
334 this.attributes[WebViewConstants.ATTRIBUTE_SRC].setValueIgnoreMutation(
339 WebView.prototype.onAttach = function(storagePartitionId) {
340 this.attributes[WebViewConstants.ATTRIBUTE_PARTITION].setValue(
344 WebView.prototype.buildAttachParams = function() {
346 'instanceId': this.viewInstanceId,
347 'userAgentOverride': this.userAgentOverride
349 for (var i in this.attributes) {
350 params[i] = this.attributes[i].getValue();
355 WebView.prototype.attachWindow = function(guestInstanceId) {
356 this.guestInstanceId = guestInstanceId;
357 var params = this.buildAttachParams();
359 if (!this.isPluginInRenderTree()) {
363 return guestViewInternalNatives.AttachGuest(
364 this.internalInstanceId,
365 this.guestInstanceId,
366 params, function(w) {
367 this.contentWindow = w;
372 // -----------------------------------------------------------------------------
373 // Public-facing API methods.
376 // Navigates to the previous history entry.
377 WebView.prototype.back = function(callback) {
378 return this.go(-1, callback);
381 // Returns whether there is a previous history entry to navigate to.
382 WebView.prototype.canGoBack = function() {
383 return this.entryCount > 1 && this.currentEntryIndex > 0;
386 // Returns whether there is a subsequent history entry to navigate to.
387 WebView.prototype.canGoForward = function() {
388 return this.currentEntryIndex >= 0 &&
389 this.currentEntryIndex < (this.entryCount - 1);
392 // Clears browsing data for the WebView partition.
393 WebView.prototype.clearData = function() {
394 if (!this.guestInstanceId) {
397 var args = $Array.concat([this.guestInstanceId], $Array.slice(arguments));
398 $Function.apply(WebViewInternal.clearData, null, args);
401 // Injects JavaScript code into the guest page.
402 WebView.prototype.executeScript = function(var_args) {
403 this.validateExecuteCodeCall();
404 var webviewSrc = this.attributes[WebViewConstants.ATTRIBUTE_SRC].getValue();
405 if (this.baseUrlForDataUrl != '') {
406 webviewSrc = this.baseUrlForDataUrl;
408 var args = $Array.concat([this.guestInstanceId, webviewSrc],
409 $Array.slice(arguments));
410 $Function.apply(WebViewInternal.executeScript, null, args);
413 // Initiates a find-in-page request.
414 WebView.prototype.find = function(search_text, options, callback) {
415 if (!this.guestInstanceId) {
418 WebViewInternal.find(this.guestInstanceId, search_text, options, callback);
421 // Navigates to the subsequent history entry.
422 WebView.prototype.forward = function(callback) {
423 return this.go(1, callback);
426 // Returns Chrome's internal process ID for the guest web page's current
428 WebView.prototype.getProcessId = function() {
429 return this.processId;
432 // Returns the user agent string used by the webview for guest page requests.
433 WebView.prototype.getUserAgent = function() {
434 return this.userAgentOverride || navigator.userAgent;
437 // Gets the current zoom factor.
438 WebView.prototype.getZoom = function(callback) {
439 if (!this.guestInstanceId) {
442 WebViewInternal.getZoom(this.guestInstanceId, callback);
445 // Navigates to a history entry using a history index relative to the current
447 WebView.prototype.go = function(relativeIndex, callback) {
448 if (!this.guestInstanceId) {
451 WebViewInternal.go(this.guestInstanceId, relativeIndex, callback);
454 // Injects CSS into the guest page.
455 WebView.prototype.insertCSS = function(var_args) {
456 this.validateExecuteCodeCall();
457 var webviewSrc = this.attributes[WebViewConstants.ATTRIBUTE_SRC].getValue();
458 if (this.baseUrlForDataUrl != '') {
459 webviewSrc = this.baseUrlForDataUrl;
461 var args = $Array.concat([this.guestInstanceId, webviewSrc],
462 $Array.slice(arguments));
463 $Function.apply(WebViewInternal.insertCSS, null, args);
466 // Indicates whether or not the webview's user agent string has been overridden.
467 WebView.prototype.isUserAgentOverridden = function() {
468 return !!this.userAgentOverride &&
469 this.userAgentOverride != navigator.userAgent;
472 // Prints the contents of the webview.
473 WebView.prototype.print = function() {
474 this.executeScript({code: 'window.print();'});
477 // Reloads the current top-level page.
478 WebView.prototype.reload = function() {
479 if (!this.guestInstanceId) {
482 WebViewInternal.reload(this.guestInstanceId);
485 // Override the user agent string used by the webview for guest page requests.
486 WebView.prototype.setUserAgentOverride = function(userAgentOverride) {
487 this.userAgentOverride = userAgentOverride;
488 if (!this.guestInstanceId) {
489 // If we are not attached yet, then we will pick up the user agent on
493 WebViewInternal.overrideUserAgent(this.guestInstanceId, userAgentOverride);
496 // Changes the zoom factor of the page.
497 WebView.prototype.setZoom = function(zoomFactor, callback) {
498 if (!this.guestInstanceId) {
501 WebViewInternal.setZoom(this.guestInstanceId, zoomFactor, callback);
504 // Stops loading the current navigation if one is in progress.
505 WebView.prototype.stop = function() {
506 if (!this.guestInstanceId) {
509 WebViewInternal.stop(this.guestInstanceId);
512 // Ends the current find session.
513 WebView.prototype.stopFinding = function(action) {
514 if (!this.guestInstanceId) {
517 WebViewInternal.stopFinding(this.guestInstanceId, action);
520 // Forcibly kills the guest web page's renderer process.
521 WebView.prototype.terminate = function() {
522 if (!this.guestInstanceId) {
525 WebViewInternal.terminate(this.guestInstanceId);
528 // -----------------------------------------------------------------------------
530 // Registers browser plugin <object> custom element.
531 function registerBrowserPluginElement() {
532 var proto = Object.create(HTMLObjectElement.prototype);
534 proto.createdCallback = function() {
535 this.setAttribute('type', 'application/browser-plugin');
536 this.setAttribute('id', 'browser-plugin-' + IdGenerator.GetNextId());
537 // The <object> node fills in the <webview> container.
538 this.style.width = '100%';
539 this.style.height = '100%';
542 proto.attributeChangedCallback = function(name, oldValue, newValue) {
543 var internal = privates(this).internal;
547 internal.handleBrowserPluginAttributeMutation(name, oldValue, newValue);
550 proto.attachedCallback = function() {
551 // Load the plugin immediately.
552 var unused = this.nonExistentAttribute;
555 WebView.BrowserPlugin =
556 DocumentNatives.RegisterElement('browserplugin', {extends: 'object',
559 delete proto.createdCallback;
560 delete proto.attachedCallback;
561 delete proto.detachedCallback;
562 delete proto.attributeChangedCallback;
565 // Registers <webview> custom element.
566 function registerWebViewElement() {
567 var proto = Object.create(HTMLElement.prototype);
569 proto.createdCallback = function() {
573 proto.attributeChangedCallback = function(name, oldValue, newValue) {
574 var internal = privates(this).internal;
578 internal.handleWebviewAttributeMutation(name, oldValue, newValue);
581 proto.detachedCallback = function() {
582 var internal = privates(this).internal;
586 internal.elementAttached = false;
590 proto.attachedCallback = function() {
591 var internal = privates(this).internal;
595 if (!internal.elementAttached) {
596 internal.elementAttached = true;
597 internal.parseAttributes();
601 // Public-facing API methods.
615 'isUserAgentOverridden',
618 'setUserAgentOverride',
625 // Add the experimental API methods, if available.
626 var experimentalMethods =
627 WebView.maybeGetExperimentalAPIs();
628 methods = $Array.concat(methods, experimentalMethods);
630 // Forward proto.foo* method calls to WebView.foo*.
631 var createHandler = function(m) {
632 return function(var_args) {
633 var internal = privates(this).internal;
634 return $Function.apply(internal[m], internal, arguments);
637 for (var i = 0; methods[i]; ++i) {
638 proto[methods[i]] = createHandler(methods[i]);
642 DocumentNatives.RegisterElement('webview', {prototype: proto});
644 // Delete the callbacks so developers cannot call them and produce unexpected
646 delete proto.createdCallback;
647 delete proto.attachedCallback;
648 delete proto.detachedCallback;
649 delete proto.attributeChangedCallback;
652 var useCapture = true;
653 window.addEventListener('readystatechange', function listener(event) {
654 if (document.readyState == 'loading')
657 registerBrowserPluginElement();
658 registerWebViewElement();
659 window.removeEventListener(event.type, listener, useCapture);
662 // Implemented when the ChromeWebView API is available.
663 WebView.prototype.maybeGetChromeWebViewEvents = function() {};
665 // Implemented when the experimental WebView API is available.
666 WebView.maybeGetExperimentalAPIs = function() {};
667 WebView.prototype.maybeGetExperimentalEvents = function() {};
668 WebView.prototype.setupExperimentalContextMenus = function() {};
671 exports.WebView = WebView;
672 exports.WebViewInternal = WebViewInternal;