Home | History | Annotate | Download | only in resources
      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.
      4 
      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.
      8 
      9 var DocumentNatives = requireNative('document_natives');
     10 var GuestViewInternal =
     11     require('binding').Binding.create('guestViewInternal').generate();
     12 var IdGenerator = requireNative('id_generator');
     13 // TODO(lazyboy): Rename this to WebViewInternal and call WebViewInternal
     14 // something else.
     15 var WebView = require('webViewInternal').WebView;
     16 var WebViewEvents = require('webViewEvents').WebViewEvents;
     17 var guestViewInternalNatives = requireNative('guest_view_internal');
     18 
     19 var WEB_VIEW_ATTRIBUTE_AUTOSIZE = 'autosize';
     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';
     24 var AUTO_SIZE_ATTRIBUTES = [
     25   WEB_VIEW_ATTRIBUTE_AUTOSIZE,
     26   WEB_VIEW_ATTRIBUTE_MAXHEIGHT,
     27   WEB_VIEW_ATTRIBUTE_MAXWIDTH,
     28   WEB_VIEW_ATTRIBUTE_MINHEIGHT,
     29   WEB_VIEW_ATTRIBUTE_MINWIDTH
     30 ];
     31 
     32 var WEB_VIEW_ATTRIBUTE_ALLOWTRANSPARENCY = "allowtransparency";
     33 var WEB_VIEW_ATTRIBUTE_PARTITION = 'partition';
     34 
     35 var ERROR_MSG_ALREADY_NAVIGATED =
     36     'The object has already navigated, so its partition cannot be changed.';
     37 var ERROR_MSG_INVALID_PARTITION_ATTRIBUTE = 'Invalid partition attribute.';
     38 
     39 /** @class representing state of storage partition. */
     40 function Partition() {
     41   this.validPartitionId = true;
     42   this.persistStorage = false;
     43   this.storagePartitionId = '';
     44 };
     45 
     46 Partition.prototype.toAttribute = function() {
     47   if (!this.validPartitionId) {
     48     return '';
     49   }
     50   return (this.persistStorage ? 'persist:' : '') + this.storagePartitionId;
     51 };
     52 
     53 Partition.prototype.fromAttribute = function(value, hasNavigated) {
     54   var result = {};
     55   if (hasNavigated) {
     56     result.error = ERROR_MSG_ALREADY_NAVIGATED;
     57     return result;
     58   }
     59   if (!value) {
     60     value = '';
     61   }
     62 
     63   var LEN = 'persist:'.length;
     64   if (value.substr(0, LEN) == 'persist:') {
     65     value = value.substr(LEN);
     66     if (!value) {
     67       this.validPartitionId = false;
     68       result.error = ERROR_MSG_INVALID_PARTITION_ATTRIBUTE;
     69       return result;
     70     }
     71     this.persistStorage = true;
     72   } else {
     73     this.persistStorage = false;
     74   }
     75 
     76   this.storagePartitionId = value;
     77   return result;
     78 };
     79 
     80 // Implemented when the experimental API is available.
     81 WebViewInternal.maybeRegisterExperimentalAPIs = function(proto) {}
     82 
     83 /**
     84  * @constructor
     85  */
     86 function WebViewInternal(webviewNode) {
     87   privates(webviewNode).internal = this;
     88   this.webviewNode = webviewNode;
     89   this.attached = false;
     90   this.elementAttached = false;
     91 
     92   this.beforeFirstNavigation = true;
     93   this.contentWindow = null;
     94   this.validPartitionId = true;
     95   // Used to save some state upon deferred attachment.
     96   // If <object> bindings is not available, we defer attachment.
     97   // This state contains whether or not the attachment request was for
     98   // newwindow.
     99   this.deferredAttachState = null;
    100 
    101   // on* Event handlers.
    102   this.on = {};
    103 
    104   this.browserPluginNode = this.createBrowserPluginNode();
    105   var shadowRoot = this.webviewNode.createShadowRoot();
    106   this.partition = new Partition();
    107 
    108   this.setupWebviewNodeAttributes();
    109   this.setupFocusPropagation();
    110   this.setupWebviewNodeProperties();
    111 
    112   this.viewInstanceId = IdGenerator.GetNextId();
    113 
    114   new WebViewEvents(this, this.viewInstanceId);
    115 
    116   shadowRoot.appendChild(this.browserPluginNode);
    117 }
    118 
    119 /**
    120  * @private
    121  */
    122 WebViewInternal.prototype.createBrowserPluginNode = function() {
    123   // We create BrowserPlugin as a custom element in order to observe changes
    124   // to attributes synchronously.
    125   var browserPluginNode = new WebViewInternal.BrowserPlugin();
    126   privates(browserPluginNode).internal = this;
    127   return browserPluginNode;
    128 };
    129 
    130 WebViewInternal.prototype.getGuestInstanceId = function() {
    131   return this.guestInstanceId;
    132 };
    133 
    134 /**
    135  * Resets some state upon reattaching <webview> element to the DOM.
    136  */
    137 WebViewInternal.prototype.reset = function() {
    138   // If guestInstanceId is defined then the <webview> has navigated and has
    139   // already picked up a partition ID. Thus, we need to reset the initialization
    140   // state. However, it may be the case that beforeFirstNavigation is false BUT
    141   // guestInstanceId has yet to be initialized. This means that we have not
    142   // heard back from createGuest yet. We will not reset the flag in this case so
    143   // that we don't end up allocating a second guest.
    144   if (this.guestInstanceId) {
    145     this.guestInstanceId = undefined;
    146     this.beforeFirstNavigation = true;
    147     this.validPartitionId = true;
    148     this.partition.validPartitionId = true;
    149     this.contentWindow = null;
    150   }
    151   this.internalInstanceId = 0;
    152 };
    153 
    154 // Sets <webview>.request property.
    155 WebViewInternal.prototype.setRequestPropertyOnWebViewNode = function(request) {
    156   Object.defineProperty(
    157       this.webviewNode,
    158       'request',
    159       {
    160         value: request,
    161         enumerable: true
    162       }
    163   );
    164 };
    165 
    166 WebViewInternal.prototype.setupFocusPropagation = function() {
    167   if (!this.webviewNode.hasAttribute('tabIndex')) {
    168     // <webview> needs a tabIndex in order to be focusable.
    169     // TODO(fsamuel): It would be nice to avoid exposing a tabIndex attribute
    170     // to allow <webview> to be focusable.
    171     // See http://crbug.com/231664.
    172     this.webviewNode.setAttribute('tabIndex', -1);
    173   }
    174   var self = this;
    175   this.webviewNode.addEventListener('focus', function(e) {
    176     // Focus the BrowserPlugin when the <webview> takes focus.
    177     this.browserPluginNode.focus();
    178   }.bind(this));
    179   this.webviewNode.addEventListener('blur', function(e) {
    180     // Blur the BrowserPlugin when the <webview> loses focus.
    181     this.browserPluginNode.blur();
    182   }.bind(this));
    183 };
    184 
    185 /**
    186  * @private
    187  */
    188 WebViewInternal.prototype.back = function() {
    189   return this.go(-1);
    190 };
    191 
    192 /**
    193  * @private
    194  */
    195 WebViewInternal.prototype.forward = function() {
    196   return this.go(1);
    197 };
    198 
    199 /**
    200  * @private
    201  */
    202 WebViewInternal.prototype.canGoBack = function() {
    203   return this.entryCount > 1 && this.currentEntryIndex > 0;
    204 };
    205 
    206 /**
    207  * @private
    208  */
    209 WebViewInternal.prototype.canGoForward = function() {
    210   return this.currentEntryIndex >= 0 &&
    211       this.currentEntryIndex < (this.entryCount - 1);
    212 };
    213 
    214 /**
    215  * @private
    216  */
    217 WebViewInternal.prototype.clearData = function() {
    218   if (!this.guestInstanceId) {
    219     return;
    220   }
    221   var args = $Array.concat([this.guestInstanceId], $Array.slice(arguments));
    222   $Function.apply(WebView.clearData, null, args);
    223 };
    224 
    225 /**
    226  * @private
    227  */
    228 WebViewInternal.prototype.getProcessId = function() {
    229   return this.processId;
    230 };
    231 
    232 /**
    233  * @private
    234  */
    235 WebViewInternal.prototype.go = function(relativeIndex) {
    236   if (!this.guestInstanceId) {
    237     return;
    238   }
    239   WebView.go(this.guestInstanceId, relativeIndex);
    240 };
    241 
    242 /**
    243  * @private
    244  */
    245 WebViewInternal.prototype.print = function() {
    246   this.executeScript({code: 'window.print();'});
    247 };
    248 
    249 /**
    250  * @private
    251  */
    252 WebViewInternal.prototype.reload = function() {
    253   if (!this.guestInstanceId) {
    254     return;
    255   }
    256   WebView.reload(this.guestInstanceId);
    257 };
    258 
    259 /**
    260  * @private
    261  */
    262 WebViewInternal.prototype.stop = function() {
    263   if (!this.guestInstanceId) {
    264     return;
    265   }
    266   WebView.stop(this.guestInstanceId);
    267 };
    268 
    269 /**
    270  * @private
    271  */
    272 WebViewInternal.prototype.terminate = function() {
    273   if (!this.guestInstanceId) {
    274     return;
    275   }
    276   WebView.terminate(this.guestInstanceId);
    277 };
    278 
    279 /**
    280  * @private
    281  */
    282 WebViewInternal.prototype.validateExecuteCodeCall  = function() {
    283   var ERROR_MSG_CANNOT_INJECT_SCRIPT = '<webview>: ' +
    284       'Script cannot be injected into content until the page has loaded.';
    285   if (!this.guestInstanceId) {
    286     throw new Error(ERROR_MSG_CANNOT_INJECT_SCRIPT);
    287   }
    288 };
    289 
    290 /**
    291  * @private
    292  */
    293 WebViewInternal.prototype.executeScript = function(var_args) {
    294   this.validateExecuteCodeCall();
    295   var webview_src = this.src;
    296   if (this.baseUrlForDataUrl != '') {
    297     webview_src = this.baseUrlForDataUrl;
    298   }
    299   var args = $Array.concat([this.guestInstanceId, webview_src],
    300                            $Array.slice(arguments));
    301   $Function.apply(WebView.executeScript, null, args);
    302 };
    303 
    304 /**
    305  * @private
    306  */
    307 WebViewInternal.prototype.insertCSS = function(var_args) {
    308   this.validateExecuteCodeCall();
    309   var webview_src = this.src;
    310   if (this.baseUrlForDataUrl != '') {
    311     webview_src = this.baseUrlForDataUrl;
    312   }
    313   var args = $Array.concat([this.guestInstanceId, webview_src],
    314                            $Array.slice(arguments));
    315   $Function.apply(WebView.insertCSS, null, args);
    316 };
    317 
    318 WebViewInternal.prototype.setupAutoSizeProperties = function() {
    319   $Array.forEach(AUTO_SIZE_ATTRIBUTES, function(attributeName) {
    320     this[attributeName] = this.webviewNode.getAttribute(attributeName);
    321     Object.defineProperty(this.webviewNode, attributeName, {
    322       get: function() {
    323         return this[attributeName];
    324       }.bind(this),
    325       set: function(value) {
    326         this.webviewNode.setAttribute(attributeName, value);
    327       }.bind(this),
    328       enumerable: true
    329     });
    330   }.bind(this), this);
    331 };
    332 
    333 /**
    334  * @private
    335  */
    336 WebViewInternal.prototype.setupWebviewNodeProperties = function() {
    337   var ERROR_MSG_CONTENTWINDOW_NOT_AVAILABLE = '<webview>: ' +
    338     'contentWindow is not available at this time. It will become available ' +
    339         'when the page has finished loading.';
    340 
    341   this.setupAutoSizeProperties();
    342 
    343   Object.defineProperty(this.webviewNode,
    344                         WEB_VIEW_ATTRIBUTE_ALLOWTRANSPARENCY, {
    345     get: function() {
    346       return this.allowtransparency;
    347     }.bind(this),
    348     set: function(value) {
    349       this.webviewNode.setAttribute(WEB_VIEW_ATTRIBUTE_ALLOWTRANSPARENCY,
    350                                     value);
    351     }.bind(this),
    352     enumerable: true
    353   });
    354 
    355   // We cannot use {writable: true} property descriptor because we want a
    356   // dynamic getter value.
    357   Object.defineProperty(this.webviewNode, 'contentWindow', {
    358     get: function() {
    359       if (this.contentWindow) {
    360         return this.contentWindow;
    361       }
    362       window.console.error(ERROR_MSG_CONTENTWINDOW_NOT_AVAILABLE);
    363     }.bind(this),
    364     // No setter.
    365     enumerable: true
    366   });
    367 
    368   Object.defineProperty(this.webviewNode, 'name', {
    369     get: function() {
    370       return this.name;
    371     }.bind(this),
    372     set: function(value) {
    373       this.webviewNode.setAttribute('name', value);
    374     }.bind(this),
    375     enumerable: true
    376   });
    377 
    378   Object.defineProperty(this.webviewNode, 'partition', {
    379     get: function() {
    380       return this.partition.toAttribute();
    381     }.bind(this),
    382     set: function(value) {
    383       var result = this.partition.fromAttribute(value, this.hasNavigated());
    384       if (result.error) {
    385         throw result.error;
    386       }
    387       this.webviewNode.setAttribute('partition', value);
    388     }.bind(this),
    389     enumerable: true
    390   });
    391 
    392   this.src = this.webviewNode.getAttribute('src');
    393   Object.defineProperty(this.webviewNode, 'src', {
    394     get: function() {
    395       return this.src;
    396     }.bind(this),
    397     set: function(value) {
    398       this.webviewNode.setAttribute('src', value);
    399     }.bind(this),
    400     // No setter.
    401     enumerable: true
    402   });
    403 };
    404 
    405 /**
    406  * @private
    407  */
    408 WebViewInternal.prototype.setupWebviewNodeAttributes = function() {
    409   this.setupWebViewSrcAttributeMutationObserver();
    410 };
    411 
    412 /**
    413  * @private
    414  */
    415 WebViewInternal.prototype.setupWebViewSrcAttributeMutationObserver =
    416     function() {
    417   // The purpose of this mutation observer is to catch assignment to the src
    418   // attribute without any changes to its value. This is useful in the case
    419   // where the webview guest has crashed and navigating to the same address
    420   // spawns off a new process.
    421   this.srcAndPartitionObserver = new MutationObserver(function(mutations) {
    422     $Array.forEach(mutations, function(mutation) {
    423       var oldValue = mutation.oldValue;
    424       var newValue = this.webviewNode.getAttribute(mutation.attributeName);
    425       if (oldValue != newValue) {
    426         return;
    427       }
    428       this.handleWebviewAttributeMutation(
    429           mutation.attributeName, oldValue, newValue);
    430     }.bind(this));
    431   }.bind(this));
    432   var params = {
    433     attributes: true,
    434     attributeOldValue: true,
    435     attributeFilter: ['src', 'partition']
    436   };
    437   this.srcAndPartitionObserver.observe(this.webviewNode, params);
    438 };
    439 
    440 /**
    441  * @private
    442  */
    443 WebViewInternal.prototype.handleWebviewAttributeMutation =
    444       function(name, oldValue, newValue) {
    445   // This observer monitors mutations to attributes of the <webview> and
    446   // updates the BrowserPlugin properties accordingly. In turn, updating
    447   // a BrowserPlugin property will update the corresponding BrowserPlugin
    448   // attribute, if necessary. See BrowserPlugin::UpdateDOMAttribute for more
    449   // details.
    450   if (AUTO_SIZE_ATTRIBUTES.indexOf(name) > -1) {
    451     this[name] = newValue;
    452     if (!this.guestInstanceId) {
    453       return;
    454     }
    455     // Convert autosize attribute to boolean.
    456     var autosize = this.webviewNode.hasAttribute(WEB_VIEW_ATTRIBUTE_AUTOSIZE);
    457     GuestViewInternal.setAutoSize(this.guestInstanceId, {
    458       'enableAutoSize': autosize,
    459       'min': {
    460         'width': parseInt(this.minwidth || 0),
    461         'height': parseInt(this.minheight || 0)
    462       },
    463       'max': {
    464         'width': parseInt(this.maxwidth || 0),
    465         'height': parseInt(this.maxheight || 0)
    466       }
    467     });
    468     return;
    469   } else if (name == WEB_VIEW_ATTRIBUTE_ALLOWTRANSPARENCY) {
    470     // We treat null attribute (attribute removed) and the empty string as
    471     // one case.
    472     oldValue = oldValue || '';
    473     newValue = newValue || '';
    474 
    475     if (oldValue === newValue) {
    476       return;
    477     }
    478     this.allowtransparency = newValue != '';
    479 
    480     if (!this.guestInstanceId) {
    481       return;
    482     }
    483 
    484     WebView.setAllowTransparency(this.guestInstanceId, this.allowtransparency);
    485     return;
    486   } else if (name == 'name') {
    487     // We treat null attribute (attribute removed) and the empty string as
    488     // one case.
    489     oldValue = oldValue || '';
    490     newValue = newValue || '';
    491 
    492     if (oldValue === newValue) {
    493       return;
    494     }
    495     this.name = newValue;
    496     if (!this.guestInstanceId) {
    497       return;
    498     }
    499     WebView.setName(this.guestInstanceId, newValue);
    500     return;
    501   } else if (name == 'src') {
    502     // We treat null attribute (attribute removed) and the empty string as
    503     // one case.
    504     oldValue = oldValue || '';
    505     newValue = newValue || '';
    506     // Once we have navigated, we don't allow clearing the src attribute.
    507     // Once <webview> enters a navigated state, it cannot be return back to a
    508     // placeholder state.
    509     if (newValue == '' && oldValue != '') {
    510       // src attribute changes normally initiate a navigation. We suppress
    511       // the next src attribute handler call to avoid reloading the page
    512       // on every guest-initiated navigation.
    513       this.ignoreNextSrcAttributeChange = true;
    514       this.webviewNode.setAttribute('src', oldValue);
    515       return;
    516     }
    517     this.src = newValue;
    518     if (this.ignoreNextSrcAttributeChange) {
    519       // Don't allow the src mutation observer to see this change.
    520       this.srcAndPartitionObserver.takeRecords();
    521       this.ignoreNextSrcAttributeChange = false;
    522       return;
    523     }
    524     var result = {};
    525     this.parseSrcAttribute(result);
    526 
    527     if (result.error) {
    528       throw result.error;
    529     }
    530   } else if (name == 'partition') {
    531     // Note that throwing error here won't synchronously propagate.
    532     this.partition.fromAttribute(newValue, this.hasNavigated());
    533   }
    534 };
    535 
    536 /**
    537  * @private
    538  */
    539 WebViewInternal.prototype.handleBrowserPluginAttributeMutation =
    540     function(name, oldValue, newValue) {
    541   if (name == 'internalinstanceid' && !oldValue && !!newValue) {
    542     this.browserPluginNode.removeAttribute('internalinstanceid');
    543     this.internalInstanceId = parseInt(newValue);
    544 
    545     if (!this.deferredAttachState) {
    546       this.parseAttributes();
    547       return;
    548     }
    549 
    550     if (!!this.guestInstanceId && this.guestInstanceId != 0) {
    551       window.setTimeout(function() {
    552         var isNewWindow = this.deferredAttachState ?
    553             this.deferredAttachState.isNewWindow : false;
    554         var params = this.buildAttachParams(isNewWindow);
    555         guestViewInternalNatives.AttachGuest(
    556             this.internalInstanceId,
    557             this.guestInstanceId,
    558             params,
    559             function(w) {
    560               this.contentWindow = w;
    561             }.bind(this)
    562         );
    563       }.bind(this), 0);
    564     }
    565 
    566     return;
    567   }
    568 };
    569 
    570 WebViewInternal.prototype.onSizeChanged = function(webViewEvent) {
    571   var newWidth = webViewEvent.newWidth;
    572   var newHeight = webViewEvent.newHeight;
    573 
    574   var node = this.webviewNode;
    575 
    576   var width = node.offsetWidth;
    577   var height = node.offsetHeight;
    578 
    579   // Check the current bounds to make sure we do not resize <webview>
    580   // outside of current constraints.
    581   var maxWidth;
    582   if (node.hasAttribute(WEB_VIEW_ATTRIBUTE_MAXWIDTH) &&
    583       node[WEB_VIEW_ATTRIBUTE_MAXWIDTH]) {
    584     maxWidth = node[WEB_VIEW_ATTRIBUTE_MAXWIDTH];
    585   } else {
    586     maxWidth = width;
    587   }
    588 
    589   var minWidth;
    590   if (node.hasAttribute(WEB_VIEW_ATTRIBUTE_MINWIDTH) &&
    591       node[WEB_VIEW_ATTRIBUTE_MINWIDTH]) {
    592     minWidth = node[WEB_VIEW_ATTRIBUTE_MINWIDTH];
    593   } else {
    594     minWidth = width;
    595   }
    596   if (minWidth > maxWidth) {
    597     minWidth = maxWidth;
    598   }
    599 
    600   var maxHeight;
    601   if (node.hasAttribute(WEB_VIEW_ATTRIBUTE_MAXHEIGHT) &&
    602       node[WEB_VIEW_ATTRIBUTE_MAXHEIGHT]) {
    603     maxHeight = node[WEB_VIEW_ATTRIBUTE_MAXHEIGHT];
    604   } else {
    605     maxHeight = height;
    606   }
    607   var minHeight;
    608   if (node.hasAttribute(WEB_VIEW_ATTRIBUTE_MINHEIGHT) &&
    609       node[WEB_VIEW_ATTRIBUTE_MINHEIGHT]) {
    610     minHeight = node[WEB_VIEW_ATTRIBUTE_MINHEIGHT];
    611   } else {
    612     minHeight = height;
    613   }
    614   if (minHeight > maxHeight) {
    615     minHeight = maxHeight;
    616   }
    617 
    618   if (!this.webviewNode.hasAttribute(WEB_VIEW_ATTRIBUTE_AUTOSIZE) ||
    619       (newWidth >= minWidth &&
    620        newWidth <= maxWidth &&
    621        newHeight >= minHeight &&
    622        newHeight <= maxHeight)) {
    623     node.style.width = newWidth + 'px';
    624     node.style.height = newHeight + 'px';
    625     // Only fire the DOM event if the size of the <webview> has actually
    626     // changed.
    627     this.dispatchEvent(webViewEvent);
    628   }
    629 };
    630 
    631 // Returns if <object> is in the render tree.
    632 WebViewInternal.prototype.isPluginInRenderTree = function() {
    633   return !!this.internalInstanceId && this.internalInstanceId != 0;
    634 };
    635 
    636 WebViewInternal.prototype.hasNavigated = function() {
    637   return !this.beforeFirstNavigation;
    638 };
    639 
    640 /** @return {boolean} */
    641 WebViewInternal.prototype.parseSrcAttribute = function(result) {
    642   if (!this.partition.validPartitionId) {
    643     result.error = ERROR_MSG_INVALID_PARTITION_ATTRIBUTE;
    644     return false;
    645   }
    646   this.src = this.webviewNode.getAttribute('src');
    647 
    648   if (!this.src) {
    649     return true;
    650   }
    651 
    652   if (!this.elementAttached) {
    653     return true;
    654   }
    655 
    656   if (!this.hasGuestInstanceID()) {
    657     if (this.beforeFirstNavigation) {
    658       this.beforeFirstNavigation = false;
    659       this.allocateInstanceId();
    660     }
    661     return true;
    662   }
    663 
    664   // Navigate to this.src.
    665   WebView.navigate(this.guestInstanceId, this.src);
    666   return true;
    667 };
    668 
    669 /** @return {boolean} */
    670 WebViewInternal.prototype.parseAttributes = function() {
    671   var hasNavigated = this.hasNavigated();
    672   var attributeValue = this.webviewNode.getAttribute('partition');
    673   var result = this.partition.fromAttribute(attributeValue, hasNavigated);
    674   return this.parseSrcAttribute(result);
    675 };
    676 
    677 WebViewInternal.prototype.hasGuestInstanceID = function() {
    678   return this.guestInstanceId != undefined;
    679 };
    680 
    681 WebViewInternal.prototype.allocateInstanceId = function() {
    682   var storagePartitionId =
    683       this.webviewNode.getAttribute(WEB_VIEW_ATTRIBUTE_PARTITION) ||
    684       this.webviewNode[WEB_VIEW_ATTRIBUTE_PARTITION];
    685   var params = {
    686     'storagePartitionId': storagePartitionId,
    687   };
    688   GuestViewInternal.createGuest(
    689       'webview',
    690       params,
    691       function(guestInstanceId) {
    692         this.attachWindow(guestInstanceId, false);
    693       }.bind(this)
    694   );
    695 };
    696 
    697 WebViewInternal.prototype.onFrameNameChanged = function(name) {
    698   this.name = name || '';
    699   if (this.name === '') {
    700     this.webviewNode.removeAttribute('name');
    701   } else {
    702     this.webviewNode.setAttribute('name', this.name);
    703   }
    704 };
    705 
    706 WebViewInternal.prototype.onPluginDestroyed = function() {
    707   this.reset();
    708 };
    709 
    710 WebViewInternal.prototype.dispatchEvent = function(webViewEvent) {
    711   return this.webviewNode.dispatchEvent(webViewEvent);
    712 };
    713 
    714 /**
    715  * Adds an 'on<event>' property on the webview, which can be used to set/unset
    716  * an event handler.
    717  */
    718 WebViewInternal.prototype.setupEventProperty = function(eventName) {
    719   var propertyName = 'on' + eventName.toLowerCase();
    720   Object.defineProperty(this.webviewNode, propertyName, {
    721     get: function() {
    722       return this.on[propertyName];
    723     }.bind(this),
    724     set: function(value) {
    725       if (this.on[propertyName])
    726         this.webviewNode.removeEventListener(eventName, this.on[propertyName]);
    727       this.on[propertyName] = value;
    728       if (value)
    729         this.webviewNode.addEventListener(eventName, value);
    730     }.bind(this),
    731     enumerable: true
    732   });
    733 };
    734 
    735 // Updates state upon loadcommit.
    736 WebViewInternal.prototype.onLoadCommit = function(
    737     baseUrlForDataUrl, currentEntryIndex, entryCount,
    738     processId, url, isTopLevel) {
    739   this.baseUrlForDataUrl = baseUrlForDataUrl;
    740   this.currentEntryIndex = currentEntryIndex;
    741   this.entryCount = entryCount;
    742   this.processId = processId;
    743   var oldValue = this.webviewNode.getAttribute('src');
    744   var newValue = url;
    745   if (isTopLevel && (oldValue != newValue)) {
    746     // Touching the src attribute triggers a navigation. To avoid
    747     // triggering a page reload on every guest-initiated navigation,
    748     // we use the flag ignoreNextSrcAttributeChange here.
    749     this.ignoreNextSrcAttributeChange = true;
    750     this.webviewNode.setAttribute('src', newValue);
    751   }
    752 };
    753 
    754 WebViewInternal.prototype.onAttach = function(storagePartitionId) {
    755   this.webviewNode.setAttribute('partition', storagePartitionId);
    756   this.partition.fromAttribute(storagePartitionId, this.hasNavigated());
    757 };
    758 
    759 
    760 /** @private */
    761 WebViewInternal.prototype.getUserAgent = function() {
    762   return this.userAgentOverride || navigator.userAgent;
    763 };
    764 
    765 /** @private */
    766 WebViewInternal.prototype.isUserAgentOverridden = function() {
    767   return !!this.userAgentOverride &&
    768       this.userAgentOverride != navigator.userAgent;
    769 };
    770 
    771 /** @private */
    772 WebViewInternal.prototype.setUserAgentOverride = function(userAgentOverride) {
    773   this.userAgentOverride = userAgentOverride;
    774   if (!this.guestInstanceId) {
    775     // If we are not attached yet, then we will pick up the user agent on
    776     // attachment.
    777     return;
    778   }
    779   WebView.overrideUserAgent(this.guestInstanceId, userAgentOverride);
    780 };
    781 
    782 /** @private */
    783 WebViewInternal.prototype.find = function(search_text, options, callback) {
    784   if (!this.guestInstanceId) {
    785     return;
    786   }
    787   WebView.find(this.guestInstanceId, search_text, options, callback);
    788 };
    789 
    790 /** @private */
    791 WebViewInternal.prototype.stopFinding = function(action) {
    792   if (!this.guestInstanceId) {
    793     return;
    794   }
    795   WebView.stopFinding(this.guestInstanceId, action);
    796 };
    797 
    798 /** @private */
    799 WebViewInternal.prototype.setZoom = function(zoomFactor, callback) {
    800   if (!this.guestInstanceId) {
    801     return;
    802   }
    803   WebView.setZoom(this.guestInstanceId, zoomFactor, callback);
    804 };
    805 
    806 WebViewInternal.prototype.getZoom = function(callback) {
    807   if (!this.guestInstanceId) {
    808     return;
    809   }
    810   WebView.getZoom(this.guestInstanceId, callback);
    811 };
    812 
    813 WebViewInternal.prototype.buildAttachParams = function(isNewWindow) {
    814   var params = {
    815     'allowtransparency': this.allowtransparency || false,
    816     'autosize': this.webviewNode.hasAttribute(WEB_VIEW_ATTRIBUTE_AUTOSIZE),
    817     'instanceId': this.viewInstanceId,
    818     'maxheight': parseInt(this.maxheight || 0),
    819     'maxwidth': parseInt(this.maxwidth || 0),
    820     'minheight': parseInt(this.minheight || 0),
    821     'minwidth': parseInt(this.minwidth || 0),
    822     'name': this.name,
    823     // We don't need to navigate new window from here.
    824     'src': isNewWindow ? undefined : this.src,
    825     // If we have a partition from the opener, that will also be already
    826     // set via this.onAttach().
    827     'storagePartitionId': this.partition.toAttribute(),
    828     'userAgentOverride': this.userAgentOverride
    829   };
    830   return params;
    831 };
    832 
    833 WebViewInternal.prototype.attachWindow = function(guestInstanceId,
    834                                                   isNewWindow) {
    835   this.guestInstanceId = guestInstanceId;
    836   var params = this.buildAttachParams(isNewWindow);
    837 
    838   if (!this.isPluginInRenderTree()) {
    839     this.deferredAttachState = {isNewWindow: isNewWindow};
    840     return true;
    841   }
    842 
    843   this.deferredAttachState = null;
    844   return guestViewInternalNatives.AttachGuest(
    845       this.internalInstanceId,
    846       this.guestInstanceId,
    847       params, function(w) {
    848         this.contentWindow = w;
    849       }.bind(this)
    850   );
    851 };
    852 
    853 // Registers browser plugin <object> custom element.
    854 function registerBrowserPluginElement() {
    855   var proto = Object.create(HTMLObjectElement.prototype);
    856 
    857   proto.createdCallback = function() {
    858     this.setAttribute('type', 'application/browser-plugin');
    859     this.setAttribute('id', 'browser-plugin-' + IdGenerator.GetNextId());
    860     // The <object> node fills in the <webview> container.
    861     this.style.width = '100%';
    862     this.style.height = '100%';
    863   };
    864 
    865   proto.attributeChangedCallback = function(name, oldValue, newValue) {
    866     var internal = privates(this).internal;
    867     if (!internal) {
    868       return;
    869     }
    870     internal.handleBrowserPluginAttributeMutation(name, oldValue, newValue);
    871   };
    872 
    873   proto.attachedCallback = function() {
    874     // Load the plugin immediately.
    875     var unused = this.nonExistentAttribute;
    876   };
    877 
    878   WebViewInternal.BrowserPlugin =
    879       DocumentNatives.RegisterElement('browserplugin', {extends: 'object',
    880                                                         prototype: proto});
    881 
    882   delete proto.createdCallback;
    883   delete proto.attachedCallback;
    884   delete proto.detachedCallback;
    885   delete proto.attributeChangedCallback;
    886 }
    887 
    888 // Registers <webview> custom element.
    889 function registerWebViewElement() {
    890   var proto = Object.create(HTMLElement.prototype);
    891 
    892   proto.createdCallback = function() {
    893     new WebViewInternal(this);
    894   };
    895 
    896   proto.attributeChangedCallback = function(name, oldValue, newValue) {
    897     var internal = privates(this).internal;
    898     if (!internal) {
    899       return;
    900     }
    901     internal.handleWebviewAttributeMutation(name, oldValue, newValue);
    902   };
    903 
    904   proto.detachedCallback = function() {
    905     var internal = privates(this).internal;
    906     if (!internal) {
    907       return;
    908     }
    909     internal.elementAttached = false;
    910     internal.reset();
    911   };
    912 
    913   proto.attachedCallback = function() {
    914     var internal = privates(this).internal;
    915     if (!internal) {
    916       return;
    917     }
    918     if (!internal.elementAttached) {
    919       internal.elementAttached = true;
    920       internal.parseAttributes();
    921     }
    922   };
    923 
    924   var methods = [
    925     'back',
    926     'find',
    927     'forward',
    928     'canGoBack',
    929     'canGoForward',
    930     'clearData',
    931     'getProcessId',
    932     'getZoom',
    933     'go',
    934     'print',
    935     'reload',
    936     'setZoom',
    937     'stop',
    938     'stopFinding',
    939     'terminate',
    940     'executeScript',
    941     'insertCSS',
    942     'getUserAgent',
    943     'isUserAgentOverridden',
    944     'setUserAgentOverride'
    945   ];
    946 
    947   // Forward proto.foo* method calls to WebViewInternal.foo*.
    948   for (var i = 0; methods[i]; ++i) {
    949     var createHandler = function(m) {
    950       return function(var_args) {
    951         var internal = privates(this).internal;
    952         return $Function.apply(internal[m], internal, arguments);
    953       };
    954     };
    955     proto[methods[i]] = createHandler(methods[i]);
    956   }
    957 
    958   WebViewInternal.maybeRegisterExperimentalAPIs(proto);
    959 
    960   window.WebView =
    961       DocumentNatives.RegisterElement('webview', {prototype: proto});
    962 
    963   // Delete the callbacks so developers cannot call them and produce unexpected
    964   // behavior.
    965   delete proto.createdCallback;
    966   delete proto.attachedCallback;
    967   delete proto.detachedCallback;
    968   delete proto.attributeChangedCallback;
    969 }
    970 
    971 var useCapture = true;
    972 window.addEventListener('readystatechange', function listener(event) {
    973   if (document.readyState == 'loading')
    974     return;
    975 
    976   registerBrowserPluginElement();
    977   registerWebViewElement();
    978   window.removeEventListener(event.type, listener, useCapture);
    979 }, useCapture);
    980 
    981 /**
    982  * Implemented when the ChromeWebView API is available.
    983  * @private
    984  */
    985 WebViewInternal.prototype.maybeGetChromeWebViewEvents = function() {};
    986 
    987 /**
    988  * Implemented when the ChromeWebView API is available.
    989  * @private
    990  */
    991 WebViewInternal.prototype.maybeSetupChromeWebViewEvents = function() {};
    992 
    993 /**
    994  * Implemented when the experimental API is available.
    995  * @private
    996  */
    997 WebViewInternal.prototype.maybeGetExperimentalEvents = function() {};
    998 
    999 /**
   1000  * Implemented when the experimental API is available.
   1001  * @private
   1002  */
   1003 WebViewInternal.prototype.maybeGetExperimentalPermissions = function() {
   1004   return [];
   1005 };
   1006 
   1007 /**
   1008  * Implemented when the experimental API is available.
   1009  * @private
   1010  */
   1011 WebViewInternal.prototype.setupExperimentalContextMenus = function() {
   1012 };
   1013 
   1014 exports.WebView = WebView;
   1015 exports.WebViewInternal = WebViewInternal;
   1016