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 IdGenerator = requireNative('id_generator');
13 var WebView = require('webview').WebView;
14 var WebViewEvents = require('webViewEvents').WebViewEvents;
16 var WEB_VIEW_ATTRIBUTE_MAXHEIGHT = 'maxheight';
17 var WEB_VIEW_ATTRIBUTE_MAXWIDTH = 'maxwidth';
18 var WEB_VIEW_ATTRIBUTE_MINHEIGHT = 'minheight';
19 var WEB_VIEW_ATTRIBUTE_MINWIDTH = 'minwidth';
20 var WEB_VIEW_ATTRIBUTE_PARTITION = 'partition';
22 var ERROR_MSG_ALREADY_NAVIGATED =
23 'The object has already navigated, so its partition cannot be changed.';
24 var ERROR_MSG_INVALID_PARTITION_ATTRIBUTE = 'Invalid partition attribute.';
26 /** @type {Array.<string>} */
27 var WEB_VIEW_ATTRIBUTES = [
30 WEB_VIEW_ATTRIBUTE_MINHEIGHT,
31 WEB_VIEW_ATTRIBUTE_MINWIDTH,
32 WEB_VIEW_ATTRIBUTE_MAXHEIGHT,
33 WEB_VIEW_ATTRIBUTE_MAXWIDTH
36 /** @class representing state of storage partition. */
37 function Partition() {
38 this.validPartitionId = true;
39 this.persistStorage = false;
40 this.storagePartitionId = '';
43 Partition.prototype.toAttribute = function() {
44 if (!this.validPartitionId) {
47 return (this.persistStorage ? 'persist:' : '') + this.storagePartitionId;
50 Partition.prototype.fromAttribute = function(value, hasNavigated) {
53 result.error = ERROR_MSG_ALREADY_NAVIGATED;
60 var LEN = 'persist:'.length;
61 if (value.substr(0, LEN) == 'persist:') {
62 value = value.substr(LEN);
64 this.validPartitionId = false;
65 result.error = ERROR_MSG_INVALID_PARTITION_ATTRIBUTE;
68 this.persistStorage = true;
70 this.persistStorage = false;
73 this.storagePartitionId = value;
77 // Implemented when the experimental API is available.
78 WebViewInternal.maybeRegisterExperimentalAPIs = function(proto) {}
83 function WebViewInternal(webviewNode) {
84 privates(webviewNode).internal = this;
85 this.webviewNode = webviewNode;
86 this.attached = false;
88 this.beforeFirstNavigation = true;
89 this.validPartitionId = true;
91 // on* Event handlers.
94 this.browserPluginNode = this.createBrowserPluginNode();
95 var shadowRoot = this.webviewNode.createShadowRoot();
96 shadowRoot.appendChild(this.browserPluginNode);
98 this.setupWebviewNodeAttributes();
99 this.setupFocusPropagation();
100 this.setupWebviewNodeProperties();
102 this.viewInstanceId = IdGenerator.GetNextId();
104 this.partition = new Partition();
105 this.parseAttributes();
107 new WebViewEvents(this, this.viewInstanceId);
113 WebViewInternal.prototype.createBrowserPluginNode = function() {
114 // We create BrowserPlugin as a custom element in order to observe changes
115 // to attributes synchronously.
116 var browserPluginNode = new WebViewInternal.BrowserPlugin();
117 privates(browserPluginNode).internal = this;
119 $Array.forEach(WEB_VIEW_ATTRIBUTES, function(attributeName) {
120 // Only copy attributes that have been assigned values, rather than copying
121 // a series of undefined attributes to BrowserPlugin.
122 if (this.webviewNode.hasAttribute(attributeName)) {
123 browserPluginNode.setAttribute(
124 attributeName, this.webviewNode.getAttribute(attributeName));
125 } else if (this.webviewNode[attributeName]){
126 // Reading property using has/getAttribute does not work on
127 // document.DOMContentLoaded event (but works on
128 // window.DOMContentLoaded event).
129 // So copy from property if copying from attribute fails.
130 browserPluginNode.setAttribute(
131 attributeName, this.webviewNode[attributeName]);
135 return browserPluginNode;
138 WebViewInternal.prototype.getInstanceId = function() {
139 return this.instanceId;
143 * Resets some state upon reattaching <webview> element to the DOM.
145 WebViewInternal.prototype.resetUponReattachment = function() {
146 this.instanceId = undefined;
147 this.beforeFirstNavigation = true;
148 this.validPartitionId = true;
149 this.partition.validPartitionId = true;
152 // Sets <webview>.request property.
153 WebViewInternal.prototype.setRequestPropertyOnWebViewNode = function(request) {
154 Object.defineProperty(
164 WebViewInternal.prototype.setupFocusPropagation = function() {
165 if (!this.webviewNode.hasAttribute('tabIndex')) {
166 // <webview> needs a tabIndex in order to be focusable.
167 // TODO(fsamuel): It would be nice to avoid exposing a tabIndex attribute
168 // to allow <webview> to be focusable.
169 // See http://crbug.com/231664.
170 this.webviewNode.setAttribute('tabIndex', -1);
173 this.webviewNode.addEventListener('focus', function(e) {
174 // Focus the BrowserPlugin when the <webview> takes focus.
175 self.browserPluginNode.focus();
177 this.webviewNode.addEventListener('blur', function(e) {
178 // Blur the BrowserPlugin when the <webview> loses focus.
179 self.browserPluginNode.blur();
186 WebViewInternal.prototype.back = function() {
193 WebViewInternal.prototype.forward = function() {
200 WebViewInternal.prototype.canGoBack = function() {
201 return this.entryCount > 1 && this.currentEntryIndex > 0;
207 WebViewInternal.prototype.canGoForward = function() {
208 return this.currentEntryIndex >= 0 &&
209 this.currentEntryIndex < (this.entryCount - 1);
215 WebViewInternal.prototype.clearData = function() {
216 if (!this.instanceId) {
219 var args = $Array.concat([this.instanceId], $Array.slice(arguments));
220 $Function.apply(WebView.clearData, null, args);
226 WebViewInternal.prototype.getProcessId = function() {
227 return this.processId;
233 WebViewInternal.prototype.go = function(relativeIndex) {
234 if (!this.instanceId) {
237 WebView.go(this.instanceId, relativeIndex);
243 WebViewInternal.prototype.reload = function() {
244 if (!this.instanceId) {
247 WebView.reload(this.instanceId);
253 WebViewInternal.prototype.stop = function() {
254 if (!this.instanceId) {
257 WebView.stop(this.instanceId);
263 WebViewInternal.prototype.terminate = function() {
264 if (!this.instanceId) {
267 WebView.terminate(this.instanceId);
273 WebViewInternal.prototype.validateExecuteCodeCall = function() {
274 var ERROR_MSG_CANNOT_INJECT_SCRIPT = '<webview>: ' +
275 'Script cannot be injected into content until the page has loaded.';
276 if (!this.instanceId) {
277 throw new Error(ERROR_MSG_CANNOT_INJECT_SCRIPT);
284 WebViewInternal.prototype.executeScript = function(var_args) {
285 this.validateExecuteCodeCall();
286 var args = $Array.concat([this.instanceId, this.src],
287 $Array.slice(arguments));
288 $Function.apply(WebView.executeScript, null, args);
294 WebViewInternal.prototype.insertCSS = function(var_args) {
295 this.validateExecuteCodeCall();
296 var args = $Array.concat([this.instanceId, this.src],
297 $Array.slice(arguments));
298 $Function.apply(WebView.insertCSS, null, args);
304 WebViewInternal.prototype.setupWebviewNodeProperties = function() {
305 var ERROR_MSG_CONTENTWINDOW_NOT_AVAILABLE = '<webview>: ' +
306 'contentWindow is not available at this time. It will become available ' +
307 'when the page has finished loading.';
310 var browserPluginNode = this.browserPluginNode;
311 // Expose getters and setters for the attributes.
312 $Array.forEach(WEB_VIEW_ATTRIBUTES, function(attributeName) {
313 Object.defineProperty(this.webviewNode, attributeName, {
315 if (browserPluginNode.hasOwnProperty(attributeName)) {
316 return browserPluginNode[attributeName];
318 return browserPluginNode.getAttribute(attributeName);
321 set: function(value) {
322 if (browserPluginNode.hasOwnProperty(attributeName)) {
323 // Give the BrowserPlugin first stab at the attribute so that it can
324 // throw an exception if there is a problem. This attribute will then
325 // be propagated back to the <webview>.
326 browserPluginNode[attributeName] = value;
328 browserPluginNode.setAttribute(attributeName, value);
335 // <webview> src does not quite behave the same as BrowserPlugin src, and so
336 // we don't simply keep the two in sync.
337 this.src = this.webviewNode.getAttribute('src');
338 Object.defineProperty(this.webviewNode, 'src', {
342 set: function(value) {
343 self.webviewNode.setAttribute('src', value);
349 Object.defineProperty(this.webviewNode, 'name', {
353 set: function(value) {
354 self.webviewNode.setAttribute('name', value);
359 Object.defineProperty(this.webviewNode, 'partition', {
361 return self.partition.toAttribute();
363 set: function(value) {
364 var result = self.partition.fromAttribute(value, self.hasNavigated());
368 self.webviewNode.setAttribute('partition', value);
373 // We cannot use {writable: true} property descriptor because we want a
374 // dynamic getter value.
375 Object.defineProperty(this.webviewNode, 'contentWindow', {
377 if (browserPluginNode.contentWindow)
378 return browserPluginNode.contentWindow;
379 window.console.error(ERROR_MSG_CONTENTWINDOW_NOT_AVAILABLE);
389 WebViewInternal.prototype.setupWebviewNodeAttributes = function() {
390 this.setupWebViewSrcAttributeMutationObserver();
396 WebViewInternal.prototype.setupWebViewSrcAttributeMutationObserver =
398 // The purpose of this mutation observer is to catch assignment to the src
399 // attribute without any changes to its value. This is useful in the case
400 // where the webview guest has crashed and navigating to the same address
401 // spawns off a new process.
403 this.srcAndPartitionObserver = new MutationObserver(function(mutations) {
404 $Array.forEach(mutations, function(mutation) {
405 var oldValue = mutation.oldValue;
406 var newValue = self.webviewNode.getAttribute(mutation.attributeName);
407 if (oldValue != newValue) {
410 self.handleWebviewAttributeMutation(
411 mutation.attributeName, oldValue, newValue);
416 attributeOldValue: true,
417 attributeFilter: ['src', 'partition']
419 this.srcAndPartitionObserver.observe(this.webviewNode, params);
425 WebViewInternal.prototype.handleWebviewAttributeMutation =
426 function(name, oldValue, newValue) {
427 // This observer monitors mutations to attributes of the <webview> and
428 // updates the BrowserPlugin properties accordingly. In turn, updating
429 // a BrowserPlugin property will update the corresponding BrowserPlugin
430 // attribute, if necessary. See BrowserPlugin::UpdateDOMAttribute for more
432 if (name == 'name') {
433 // We treat null attribute (attribute removed) and the empty string as
435 oldValue = oldValue || '';
436 newValue = newValue || '';
438 if (oldValue === newValue) {
441 this.name = newValue;
442 if (!this.instanceId) {
445 WebView.setName(this.instanceId, newValue);
447 } else if (name == 'src') {
448 // We treat null attribute (attribute removed) and the empty string as
450 oldValue = oldValue || '';
451 newValue = newValue || '';
452 // Once we have navigated, we don't allow clearing the src attribute.
453 // Once <webview> enters a navigated state, it cannot be return back to a
454 // placeholder state.
455 if (newValue == '' && oldValue != '') {
456 // src attribute changes normally initiate a navigation. We suppress
457 // the next src attribute handler call to avoid reloading the page
458 // on every guest-initiated navigation.
459 this.ignoreNextSrcAttributeChange = true;
460 this.webviewNode.setAttribute('src', oldValue);
464 if (this.ignoreNextSrcAttributeChange) {
465 // Don't allow the src mutation observer to see this change.
466 this.srcAndPartitionObserver.takeRecords();
467 this.ignoreNextSrcAttributeChange = false;
471 this.parseSrcAttribute(result);
476 } else if (name == 'partition') {
477 // Note that throwing error here won't synchronously propagate.
478 this.partition.fromAttribute(newValue, this.hasNavigated());
481 // No <webview> -> <object> mutation propagation for these attributes.
482 if (name == 'src' || name == 'partition') {
486 if (this.browserPluginNode.hasOwnProperty(name)) {
487 this.browserPluginNode[name] = newValue;
489 this.browserPluginNode.setAttribute(name, newValue);
496 WebViewInternal.prototype.handleBrowserPluginAttributeMutation =
497 function(name, newValue) {
498 // This observer monitors mutations to attributes of the BrowserPlugin and
499 // updates the <webview> attributes accordingly.
500 // |newValue| is null if the attribute |name| has been removed.
501 if (newValue != null) {
502 // Update the <webview> attribute to match the BrowserPlugin attribute.
503 // Note: Calling setAttribute on <webview> will trigger its mutation
504 // observer which will then propagate that attribute to BrowserPlugin. In
505 // cases where we permit assigning a BrowserPlugin attribute the same value
506 // again (such as navigation when crashed), this could end up in an infinite
507 // loop. Thus, we avoid this loop by only updating the <webview> attribute
508 // if the BrowserPlugin attributes differs from it.
509 if (newValue != this.webviewNode.getAttribute(name)) {
510 this.webviewNode.setAttribute(name, newValue);
513 // If an attribute is removed from the BrowserPlugin, then remove it
514 // from the <webview> as well.
515 this.webviewNode.removeAttribute(name);
519 WebViewInternal.prototype.onSizeChanged = function(newWidth, newHeight) {
520 var node = this.webviewNode;
522 var width = node.offsetWidth;
523 var height = node.offsetHeight;
525 // Check the current bounds to make sure we do not resize <webview>
526 // outside of current constraints.
528 if (node.hasAttribute(WEB_VIEW_ATTRIBUTE_MAXWIDTH) &&
529 node[WEB_VIEW_ATTRIBUTE_MAXWIDTH]) {
530 maxWidth = node[WEB_VIEW_ATTRIBUTE_MAXWIDTH];
536 if (node.hasAttribute(WEB_VIEW_ATTRIBUTE_MINWIDTH) &&
537 node[WEB_VIEW_ATTRIBUTE_MINWIDTH]) {
538 minWidth = node[WEB_VIEW_ATTRIBUTE_MINWIDTH];
542 if (minWidth > maxWidth) {
547 if (node.hasAttribute(WEB_VIEW_ATTRIBUTE_MAXHEIGHT) &&
548 node[WEB_VIEW_ATTRIBUTE_MAXHEIGHT]) {
549 maxHeight = node[WEB_VIEW_ATTRIBUTE_MAXHEIGHT];
554 if (node.hasAttribute(WEB_VIEW_ATTRIBUTE_MINHEIGHT) &&
555 node[WEB_VIEW_ATTRIBUTE_MINHEIGHT]) {
556 minHeight = node[WEB_VIEW_ATTRIBUTE_MINHEIGHT];
560 if (minHeight > maxHeight) {
561 minHeight = maxHeight;
564 if (newWidth >= minWidth &&
565 newWidth <= maxWidth &&
566 newHeight >= minHeight &&
567 newHeight <= maxHeight) {
568 node.style.width = newWidth + 'px';
569 node.style.height = newHeight + 'px';
573 WebViewInternal.prototype.hasNavigated = function() {
574 return !this.beforeFirstNavigation;
577 /** @return {boolean} */
578 WebViewInternal.prototype.parseSrcAttribute = function(result) {
579 if (!this.partition.validPartitionId) {
580 result.error = ERROR_MSG_INVALID_PARTITION_ATTRIBUTE;
583 this.src = this.webviewNode.getAttribute('src');
589 if (!this.hasGuestInstanceID()) {
590 if (this.beforeFirstNavigation) {
591 this.beforeFirstNavigation = false;
592 this.allocateInstanceId();
597 // Navigate to this.src.
598 WebView.navigate(this.instanceId, this.src);
602 /** @return {boolean} */
603 WebViewInternal.prototype.parseAttributes = function() {
604 var hasNavigated = this.hasNavigated();
605 var attributeValue = this.webviewNode.getAttribute('partition');
606 var result = this.partition.fromAttribute(attributeValue, hasNavigated);
607 return this.parseSrcAttribute(result);
610 WebViewInternal.prototype.hasGuestInstanceID = function() {
611 return this.instanceId != undefined;
614 WebViewInternal.prototype.allocateInstanceId = function() {
615 // Parse .src and .partition.
617 GuestViewInternal.allocateInstanceId(
618 function(instanceId) {
619 self.instanceId = instanceId;
620 // TODO(lazyboy): Make sure this.autoNavigate_ stuff correctly updated
621 // |self.src| at this point.
622 self.attachWindowAndSetUpEvents(self.instanceId, self.src);
626 WebViewInternal.prototype.onFrameNameChanged = function(name) {
627 this.name = name || '';
628 if (this.name === '') {
629 this.webviewNode.removeAttribute('name');
631 this.webviewNode.setAttribute('name', this.name);
635 WebViewInternal.prototype.dispatchEvent = function(webViewEvent) {
636 return this.webviewNode.dispatchEvent(webViewEvent);
640 * Adds an 'on<event>' property on the webview, which can be used to set/unset
643 WebViewInternal.prototype.setupEventProperty = function(eventName) {
644 var propertyName = 'on' + eventName.toLowerCase();
646 var webviewNode = this.webviewNode;
647 Object.defineProperty(webviewNode, propertyName, {
649 return self.on[propertyName];
651 set: function(value) {
652 if (self.on[propertyName])
653 webviewNode.removeEventListener(eventName, self.on[propertyName]);
654 self.on[propertyName] = value;
656 webviewNode.addEventListener(eventName, value);
662 // Updates state upon loadcommit.
663 WebViewInternal.prototype.onLoadCommit = function(
664 currentEntryIndex, entryCount, processId, url, isTopLevel) {
665 this.currentEntryIndex = currentEntryIndex;
666 this.entryCount = entryCount;
667 this.processId = processId;
668 var oldValue = this.webviewNode.getAttribute('src');
670 if (isTopLevel && (oldValue != newValue)) {
671 // Touching the src attribute triggers a navigation. To avoid
672 // triggering a page reload on every guest-initiated navigation,
673 // we use the flag ignoreNextSrcAttributeChange here.
674 this.ignoreNextSrcAttributeChange = true;
675 this.webviewNode.setAttribute('src', newValue);
679 WebViewInternal.prototype.onAttach = function(storagePartitionId) {
680 this.webviewNode.setAttribute('partition', storagePartitionId);
681 this.partition.fromAttribute(storagePartitionId, this.hasNavigated());
686 WebViewInternal.prototype.getUserAgent = function() {
687 return this.userAgentOverride || navigator.userAgent;
691 WebViewInternal.prototype.isUserAgentOverridden = function() {
692 return !!this.userAgentOverride &&
693 this.userAgentOverride != navigator.userAgent;
697 WebViewInternal.prototype.setUserAgentOverride = function(userAgentOverride) {
698 this.userAgentOverride = userAgentOverride;
699 if (!this.instanceId) {
700 // If we are not attached yet, then we will pick up the user agent on
704 WebView.overrideUserAgent(this.instanceId, userAgentOverride);
708 WebViewInternal.prototype.attachWindowAndSetUpEvents = function(
709 instanceId, opt_src, opt_partitionId) {
710 this.instanceId = instanceId;
711 // If we have a partition from the opener, use that instead.
712 var storagePartitionId =
714 this.webviewNode.getAttribute(WEB_VIEW_ATTRIBUTE_PARTITION) ||
715 this.webviewNode[WEB_VIEW_ATTRIBUTE_PARTITION];
718 'instanceId': this.viewInstanceId,
721 'storagePartitionId': storagePartitionId,
722 'userAgentOverride': this.userAgentOverride
725 return this.browserPluginNode['-internal-attach'](this.instanceId, params);
728 // Registers browser plugin <object> custom element.
729 function registerBrowserPluginElement() {
730 var proto = Object.create(HTMLObjectElement.prototype);
732 proto.createdCallback = function() {
733 this.setAttribute('type', 'application/browser-plugin');
734 // The <object> node fills in the <webview> container.
735 this.style.width = '100%';
736 this.style.height = '100%';
739 proto.attributeChangedCallback = function(name, oldValue, newValue) {
740 var internal = privates(this).internal;
744 internal.handleBrowserPluginAttributeMutation(name, newValue);
747 proto.attachedCallback = function() {
748 // Load the plugin immediately.
749 var unused = this.nonExistentAttribute;
752 WebViewInternal.BrowserPlugin =
753 DocumentNatives.RegisterElement('browser-plugin', {extends: 'object',
756 delete proto.createdCallback;
757 delete proto.attachedCallback;
758 delete proto.detachedCallback;
759 delete proto.attributeChangedCallback;
762 // Registers <webview> custom element.
763 function registerWebViewElement() {
764 var proto = Object.create(HTMLElement.prototype);
766 proto.createdCallback = function() {
767 new WebViewInternal(this);
770 proto.customElementDetached = false;
772 proto.attributeChangedCallback = function(name, oldValue, newValue) {
773 var internal = privates(this).internal;
777 internal.handleWebviewAttributeMutation(name, oldValue, newValue);
780 proto.detachedCallback = function() {
781 this.customElementDetached = true;
784 proto.attachedCallback = function() {
785 if (this.customElementDetached) {
786 var webViewInternal = privates(this).internal;
787 webViewInternal.resetUponReattachment();
788 webViewInternal.allocateInstanceId();
790 this.customElementDetached = false;
807 'isUserAgentOverridden',
808 'setUserAgentOverride'
811 // Forward proto.foo* method calls to WebViewInternal.foo*.
812 for (var i = 0; methods[i]; ++i) {
813 var createHandler = function(m) {
814 return function(var_args) {
815 var internal = privates(this).internal;
816 return $Function.apply(internal[m], internal, arguments);
819 proto[methods[i]] = createHandler(methods[i]);
822 WebViewInternal.maybeRegisterExperimentalAPIs(proto);
825 DocumentNatives.RegisterElement('webview', {prototype: proto});
827 // Delete the callbacks so developers cannot call them and produce unexpected
829 delete proto.createdCallback;
830 delete proto.attachedCallback;
831 delete proto.detachedCallback;
832 delete proto.attributeChangedCallback;
835 var useCapture = true;
836 window.addEventListener('readystatechange', function listener(event) {
837 if (document.readyState == 'loading')
840 registerBrowserPluginElement();
841 registerWebViewElement();
842 window.removeEventListener(event.type, listener, useCapture);
846 * Implemented when the experimental API is available.
849 WebViewInternal.prototype.maybeGetExperimentalEvents = function() {};
852 * Implemented when the experimental API is available.
855 WebViewInternal.prototype.maybeGetExperimentalPermissions = function() {
860 * Calls to show contextmenu right away instead of dispatching a 'contextmenu'
862 * This will be overridden in web_view_experimental.js to implement contextmenu
865 WebViewInternal.prototype.maybeHandleContextMenu = function(e, webViewEvent) {
866 var requestId = e.requestId;
867 // Setting |params| = undefined will show the context menu unmodified, hence
868 // the 'contextmenu' API is disabled for stable channel.
869 var params = undefined;
870 WebView.showContextMenu(this.instanceId, requestId, params);
874 * Implemented when the experimental API is available.
877 WebViewInternal.prototype.setupExperimentalContextMenus = function() {};
879 exports.WebView = WebView;
880 exports.WebViewInternal = WebViewInternal;