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