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');
13// TODO(lazyboy): Rename this to WebViewInternal and call WebViewInternal
14// something else.
15var WebView = require('webViewInternal').WebView;
16var WebViewEvents = require('webViewEvents').WebViewEvents;
17var guestViewInternalNatives = requireNative('guest_view_internal');
18
19var WEB_VIEW_ATTRIBUTE_AUTOSIZE = 'autosize';
20var WEB_VIEW_ATTRIBUTE_MAXHEIGHT = 'maxheight';
21var WEB_VIEW_ATTRIBUTE_MAXWIDTH = 'maxwidth';
22var WEB_VIEW_ATTRIBUTE_MINHEIGHT = 'minheight';
23var WEB_VIEW_ATTRIBUTE_MINWIDTH = 'minwidth';
24var 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
32var WEB_VIEW_ATTRIBUTE_ALLOWTRANSPARENCY = "allowtransparency";
33var WEB_VIEW_ATTRIBUTE_PARTITION = 'partition';
34
35var ERROR_MSG_ALREADY_NAVIGATED =
36    'The object has already navigated, so its partition cannot be changed.';
37var ERROR_MSG_INVALID_PARTITION_ATTRIBUTE = 'Invalid partition attribute.';
38
39/** @class representing state of storage partition. */
40function Partition() {
41  this.validPartitionId = true;
42  this.persistStorage = false;
43  this.storagePartitionId = '';
44};
45
46Partition.prototype.toAttribute = function() {
47  if (!this.validPartitionId) {
48    return '';
49  }
50  return (this.persistStorage ? 'persist:' : '') + this.storagePartitionId;
51};
52
53Partition.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.
81WebViewInternal.maybeRegisterExperimentalAPIs = function(proto) {}
82
83/**
84 * @constructor
85 */
86function 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 */
122WebViewInternal.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
130WebViewInternal.prototype.getGuestInstanceId = function() {
131  return this.guestInstanceId;
132};
133
134/**
135 * Resets some state upon reattaching <webview> element to the DOM.
136 */
137WebViewInternal.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.
155WebViewInternal.prototype.setRequestPropertyOnWebViewNode = function(request) {
156  Object.defineProperty(
157      this.webviewNode,
158      'request',
159      {
160        value: request,
161        enumerable: true
162      }
163  );
164};
165
166WebViewInternal.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 */
188WebViewInternal.prototype.back = function() {
189  return this.go(-1);
190};
191
192/**
193 * @private
194 */
195WebViewInternal.prototype.forward = function() {
196  return this.go(1);
197};
198
199/**
200 * @private
201 */
202WebViewInternal.prototype.canGoBack = function() {
203  return this.entryCount > 1 && this.currentEntryIndex > 0;
204};
205
206/**
207 * @private
208 */
209WebViewInternal.prototype.canGoForward = function() {
210  return this.currentEntryIndex >= 0 &&
211      this.currentEntryIndex < (this.entryCount - 1);
212};
213
214/**
215 * @private
216 */
217WebViewInternal.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 */
228WebViewInternal.prototype.getProcessId = function() {
229  return this.processId;
230};
231
232/**
233 * @private
234 */
235WebViewInternal.prototype.go = function(relativeIndex) {
236  if (!this.guestInstanceId) {
237    return;
238  }
239  WebView.go(this.guestInstanceId, relativeIndex);
240};
241
242/**
243 * @private
244 */
245WebViewInternal.prototype.print = function() {
246  this.executeScript({code: 'window.print();'});
247};
248
249/**
250 * @private
251 */
252WebViewInternal.prototype.reload = function() {
253  if (!this.guestInstanceId) {
254    return;
255  }
256  WebView.reload(this.guestInstanceId);
257};
258
259/**
260 * @private
261 */
262WebViewInternal.prototype.stop = function() {
263  if (!this.guestInstanceId) {
264    return;
265  }
266  WebView.stop(this.guestInstanceId);
267};
268
269/**
270 * @private
271 */
272WebViewInternal.prototype.terminate = function() {
273  if (!this.guestInstanceId) {
274    return;
275  }
276  WebView.terminate(this.guestInstanceId);
277};
278
279/**
280 * @private
281 */
282WebViewInternal.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 */
293WebViewInternal.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 */
307WebViewInternal.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
318WebViewInternal.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 */
336WebViewInternal.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 */
408WebViewInternal.prototype.setupWebviewNodeAttributes = function() {
409  this.setupWebViewSrcAttributeMutationObserver();
410};
411
412/**
413 * @private
414 */
415WebViewInternal.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 */
443WebViewInternal.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 */
539WebViewInternal.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
570WebViewInternal.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.
632WebViewInternal.prototype.isPluginInRenderTree = function() {
633  return !!this.internalInstanceId && this.internalInstanceId != 0;
634};
635
636WebViewInternal.prototype.hasNavigated = function() {
637  return !this.beforeFirstNavigation;
638};
639
640/** @return {boolean} */
641WebViewInternal.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} */
670WebViewInternal.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
677WebViewInternal.prototype.hasGuestInstanceID = function() {
678  return this.guestInstanceId != undefined;
679};
680
681WebViewInternal.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
697WebViewInternal.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
706WebViewInternal.prototype.onPluginDestroyed = function() {
707  this.reset();
708};
709
710WebViewInternal.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 */
718WebViewInternal.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.
736WebViewInternal.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
754WebViewInternal.prototype.onAttach = function(storagePartitionId) {
755  this.webviewNode.setAttribute('partition', storagePartitionId);
756  this.partition.fromAttribute(storagePartitionId, this.hasNavigated());
757};
758
759
760/** @private */
761WebViewInternal.prototype.getUserAgent = function() {
762  return this.userAgentOverride || navigator.userAgent;
763};
764
765/** @private */
766WebViewInternal.prototype.isUserAgentOverridden = function() {
767  return !!this.userAgentOverride &&
768      this.userAgentOverride != navigator.userAgent;
769};
770
771/** @private */
772WebViewInternal.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 */
783WebViewInternal.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 */
791WebViewInternal.prototype.stopFinding = function(action) {
792  if (!this.guestInstanceId) {
793    return;
794  }
795  WebView.stopFinding(this.guestInstanceId, action);
796};
797
798/** @private */
799WebViewInternal.prototype.setZoom = function(zoomFactor, callback) {
800  if (!this.guestInstanceId) {
801    return;
802  }
803  WebView.setZoom(this.guestInstanceId, zoomFactor, callback);
804};
805
806WebViewInternal.prototype.getZoom = function(callback) {
807  if (!this.guestInstanceId) {
808    return;
809  }
810  WebView.getZoom(this.guestInstanceId, callback);
811};
812
813WebViewInternal.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
833WebViewInternal.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.
854function 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.
889function 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
971var useCapture = true;
972window.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 */
985WebViewInternal.prototype.maybeGetChromeWebViewEvents = function() {};
986
987/**
988 * Implemented when the ChromeWebView API is available.
989 * @private
990 */
991WebViewInternal.prototype.maybeSetupChromeWebViewEvents = function() {};
992
993/**
994 * Implemented when the experimental API is available.
995 * @private
996 */
997WebViewInternal.prototype.maybeGetExperimentalEvents = function() {};
998
999/**
1000 * Implemented when the experimental API is available.
1001 * @private
1002 */
1003WebViewInternal.prototype.maybeGetExperimentalPermissions = function() {
1004  return [];
1005};
1006
1007/**
1008 * Implemented when the experimental API is available.
1009 * @private
1010 */
1011WebViewInternal.prototype.setupExperimentalContextMenus = function() {
1012};
1013
1014exports.WebView = WebView;
1015exports.WebViewInternal = WebViewInternal;
1016