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