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