Upstream version 11.40.271.0
[platform/framework/web/crosswalk.git] / src / extensions / renderer / resources / guest_view / 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 guestViewInternalNatives = requireNative('guest_view_internal');
13 var IdGenerator = requireNative('id_generator');
14 var WebViewConstants = require('webViewConstants').WebViewConstants;
15 var WebViewEvents = require('webViewEvents').WebViewEvents;
16 var WebViewInternal = require('webViewInternal').WebViewInternal;
17
18 // Represents the internal state of the WebView node.
19 function WebView(webviewNode) {
20   privates(webviewNode).internal = this;
21   this.webviewNode = webviewNode;
22   this.attached = false;
23   this.pendingGuestCreation = false;
24   this.elementAttached = false;
25
26   this.beforeFirstNavigation = true;
27   this.contentWindow = null;
28
29   // on* Event handlers.
30   this.on = {};
31
32   this.browserPluginNode = this.createBrowserPluginNode();
33   var shadowRoot = this.webviewNode.createShadowRoot();
34   this.setupWebViewAttributes();
35   this.setupFocusPropagation();
36   this.setupWebviewNodeProperties();
37
38   this.viewInstanceId = IdGenerator.GetNextId();
39
40   new WebViewEvents(this, this.viewInstanceId);
41
42   shadowRoot.appendChild(this.browserPluginNode);
43 }
44
45 WebView.prototype.createBrowserPluginNode = function() {
46   // We create BrowserPlugin as a custom element in order to observe changes
47   // to attributes synchronously.
48   var browserPluginNode = new WebView.BrowserPlugin();
49   privates(browserPluginNode).internal = this;
50   return browserPluginNode;
51 };
52
53 WebView.prototype.getGuestInstanceId = function() {
54   return this.guestInstanceId;
55 };
56
57 // Resets some state upon reattaching <webview> element to the DOM.
58 WebView.prototype.reset = function() {
59   // If guestInstanceId is defined then the <webview> has navigated and has
60   // already picked up a partition ID. Thus, we need to reset the initialization
61   // state. However, it may be the case that beforeFirstNavigation is false BUT
62   // guestInstanceId has yet to be initialized. This means that we have not
63   // heard back from createGuest yet. We will not reset the flag in this case so
64   // that we don't end up allocating a second guest.
65   if (this.guestInstanceId) {
66     GuestViewInternal.destroyGuest(this.guestInstanceId);
67     this.guestInstanceId = undefined;
68     this.beforeFirstNavigation = true;
69     this.attributes[WebViewConstants.ATTRIBUTE_PARTITION].validPartitionId =
70         true;
71     this.contentWindow = null;
72   }
73   this.internalInstanceId = 0;
74 };
75
76 // Sets the <webview>.request property.
77 WebView.prototype.setRequestPropertyOnWebViewNode = function(request) {
78   Object.defineProperty(
79       this.webviewNode,
80       'request',
81       {
82         value: request,
83         enumerable: true
84       }
85   );
86 };
87
88 WebView.prototype.setupFocusPropagation = function() {
89   if (!this.webviewNode.hasAttribute('tabIndex')) {
90     // <webview> needs a tabIndex in order to be focusable.
91     // TODO(fsamuel): It would be nice to avoid exposing a tabIndex attribute
92     // to allow <webview> to be focusable.
93     // See http://crbug.com/231664.
94     this.webviewNode.setAttribute('tabIndex', -1);
95   }
96   this.webviewNode.addEventListener('focus', function(e) {
97     // Focus the BrowserPlugin when the <webview> takes focus.
98     this.browserPluginNode.focus();
99   }.bind(this));
100   this.webviewNode.addEventListener('blur', function(e) {
101     // Blur the BrowserPlugin when the <webview> loses focus.
102     this.browserPluginNode.blur();
103   }.bind(this));
104 };
105
106 // Validation helper function for executeScript() and insertCSS().
107 WebView.prototype.validateExecuteCodeCall  = function() {
108   if (!this.guestInstanceId) {
109     throw new Error(WebViewConstants.ERROR_MSG_CANNOT_INJECT_SCRIPT);
110   }
111 };
112
113 WebView.prototype.setupWebviewNodeProperties = function() {
114   // We cannot use {writable: true} property descriptor because we want a
115   // dynamic getter value.
116   Object.defineProperty(this.webviewNode, 'contentWindow', {
117     get: function() {
118       if (this.contentWindow) {
119         return this.contentWindow;
120       }
121       window.console.error(
122           WebViewConstants.ERROR_MSG_CONTENTWINDOW_NOT_AVAILABLE);
123     }.bind(this),
124     // No setter.
125     enumerable: true
126   });
127 };
128
129 // This observer monitors mutations to attributes of the <webview> and
130 // updates the BrowserPlugin properties accordingly. In turn, updating
131 // a BrowserPlugin property will update the corresponding BrowserPlugin
132 // attribute, if necessary. See BrowserPlugin::UpdateDOMAttribute for more
133 // details.
134 WebView.prototype.handleWebviewAttributeMutation = function(
135     attributeName, oldValue, newValue) {
136   if (!this.attributes[attributeName] ||
137       this.attributes[attributeName].ignoreMutation) {
138     return;
139   }
140
141   // Let the changed attribute handle its own mutation;
142   this.attributes[attributeName].handleMutation(oldValue, newValue);
143 };
144
145 WebView.prototype.handleBrowserPluginAttributeMutation =
146     function(attributeName, oldValue, newValue) {
147   if (attributeName == WebViewConstants.ATTRIBUTE_INTERNALINSTANCEID &&
148       !oldValue && !!newValue) {
149     this.browserPluginNode.removeAttribute(
150         WebViewConstants.ATTRIBUTE_INTERNALINSTANCEID);
151     this.internalInstanceId = parseInt(newValue);
152
153     if (!this.guestInstanceId) {
154       return;
155     }
156     guestViewInternalNatives.AttachGuest(
157         this.internalInstanceId,
158         this.guestInstanceId,
159         this.buildAttachParams(),
160         function(w) {
161           this.contentWindow = w;
162         }.bind(this)
163     );
164   }
165 };
166
167 WebView.prototype.onSizeChanged = function(webViewEvent) {
168   var newWidth = webViewEvent.newWidth;
169   var newHeight = webViewEvent.newHeight;
170
171   var node = this.webviewNode;
172
173   var width = node.offsetWidth;
174   var height = node.offsetHeight;
175
176   // Check the current bounds to make sure we do not resize <webview>
177   // outside of current constraints.
178   var maxWidth;
179   if (node.hasAttribute(WebViewConstants.ATTRIBUTE_MAXWIDTH) &&
180       node[WebViewConstants.ATTRIBUTE_MAXWIDTH]) {
181     maxWidth = node[WebViewConstants.ATTRIBUTE_MAXWIDTH];
182   } else {
183     maxWidth = width;
184   }
185
186   var minWidth;
187   if (node.hasAttribute(WebViewConstants.ATTRIBUTE_MINWIDTH) &&
188       node[WebViewConstants.ATTRIBUTE_MINWIDTH]) {
189     minWidth = node[WebViewConstants.ATTRIBUTE_MINWIDTH];
190   } else {
191     minWidth = width;
192   }
193   if (minWidth > maxWidth) {
194     minWidth = maxWidth;
195   }
196
197   var maxHeight;
198   if (node.hasAttribute(WebViewConstants.ATTRIBUTE_MAXHEIGHT) &&
199       node[WebViewConstants.ATTRIBUTE_MAXHEIGHT]) {
200     maxHeight = node[WebViewConstants.ATTRIBUTE_MAXHEIGHT];
201   } else {
202     maxHeight = height;
203   }
204
205   var minHeight;
206   if (node.hasAttribute(WebViewConstants.ATTRIBUTE_MINHEIGHT) &&
207       node[WebViewConstants.ATTRIBUTE_MINHEIGHT]) {
208     minHeight = node[WebViewConstants.ATTRIBUTE_MINHEIGHT];
209   } else {
210     minHeight = height;
211   }
212   if (minHeight > maxHeight) {
213     minHeight = maxHeight;
214   }
215
216   if (!this.attributes[WebViewConstants.ATTRIBUTE_AUTOSIZE].getValue() ||
217       (newWidth >= minWidth &&
218        newWidth <= maxWidth &&
219        newHeight >= minHeight &&
220        newHeight <= maxHeight)) {
221     node.style.width = newWidth + 'px';
222     node.style.height = newHeight + 'px';
223     // Only fire the DOM event if the size of the <webview> has actually
224     // changed.
225     this.dispatchEvent(webViewEvent);
226   }
227 };
228
229 // Returns if <object> is in the render tree.
230 WebView.prototype.isPluginInRenderTree = function() {
231   return !!this.internalInstanceId && this.internalInstanceId != 0;
232 };
233
234 WebView.prototype.hasNavigated = function() {
235   return !this.beforeFirstNavigation;
236 };
237
238 WebView.prototype.parseSrcAttribute = function() {
239   if (!this.attributes[WebViewConstants.ATTRIBUTE_PARTITION].validPartitionId ||
240       !this.attributes[WebViewConstants.ATTRIBUTE_SRC].getValue()) {
241     return;
242   }
243
244   if (this.guestInstanceId == undefined) {
245     if (this.beforeFirstNavigation) {
246       this.beforeFirstNavigation = false;
247       this.createGuest();
248     }
249     return;
250   }
251
252   // Navigate to |this.src|.
253   WebViewInternal.navigate(
254       this.guestInstanceId,
255       this.attributes[WebViewConstants.ATTRIBUTE_SRC].getValue());
256 };
257
258 WebView.prototype.parseAttributes = function() {
259   if (!this.elementAttached) {
260     return;
261   }
262   this.parseSrcAttribute();
263 };
264
265 WebView.prototype.createGuest = function() {
266   if (this.pendingGuestCreation) {
267     return;
268   }
269   var params = {
270     'storagePartitionId': this.attributes[
271         WebViewConstants.ATTRIBUTE_PARTITION].getValue()
272   };
273   GuestViewInternal.createGuest(
274       'webview',
275       params,
276       function(guestInstanceId) {
277         this.pendingGuestCreation = false;
278         if (!this.elementAttached) {
279           GuestViewInternal.destroyGuest(guestInstanceId);
280           return;
281         }
282         this.attachWindow(guestInstanceId);
283       }.bind(this)
284   );
285   this.pendingGuestCreation = true;
286 };
287
288 WebView.prototype.onFrameNameChanged = function(name) {
289   name = name || '';
290   if (name === '') {
291     this.webviewNode.removeAttribute(WebViewConstants.ATTRIBUTE_NAME);
292   } else {
293     this.attributes[WebViewConstants.ATTRIBUTE_NAME].setValue(name);
294   }
295 };
296
297 WebView.prototype.dispatchEvent = function(webViewEvent) {
298   return this.webviewNode.dispatchEvent(webViewEvent);
299 };
300
301 // Adds an 'on<event>' property on the webview, which can be used to set/unset
302 // an event handler.
303 WebView.prototype.setupEventProperty = function(eventName) {
304   var propertyName = 'on' + eventName.toLowerCase();
305   Object.defineProperty(this.webviewNode, propertyName, {
306     get: function() {
307       return this.on[propertyName];
308     }.bind(this),
309     set: function(value) {
310       if (this.on[propertyName])
311         this.webviewNode.removeEventListener(eventName, this.on[propertyName]);
312       this.on[propertyName] = value;
313       if (value)
314         this.webviewNode.addEventListener(eventName, value);
315     }.bind(this),
316     enumerable: true
317   });
318 };
319
320 // Updates state upon loadcommit.
321 WebView.prototype.onLoadCommit = function(
322     baseUrlForDataUrl, currentEntryIndex, entryCount,
323     processId, url, isTopLevel) {
324   this.baseUrlForDataUrl = baseUrlForDataUrl;
325   this.currentEntryIndex = currentEntryIndex;
326   this.entryCount = entryCount;
327   this.processId = processId;
328   var oldValue = this.attributes[WebViewConstants.ATTRIBUTE_SRC].getValue();
329   var newValue = url;
330   if (isTopLevel && (oldValue != newValue)) {
331     // Touching the src attribute triggers a navigation. To avoid
332     // triggering a page reload on every guest-initiated navigation,
333     // we do not handle this mutation.
334     this.attributes[WebViewConstants.ATTRIBUTE_SRC].setValueIgnoreMutation(
335         newValue);
336   }
337 };
338
339 WebView.prototype.onAttach = function(storagePartitionId) {
340   this.attributes[WebViewConstants.ATTRIBUTE_PARTITION].setValue(
341       storagePartitionId);
342 };
343
344 WebView.prototype.buildAttachParams = function() {
345   var params = {
346     'instanceId': this.viewInstanceId,
347     'userAgentOverride': this.userAgentOverride
348   };
349   for (var i in this.attributes) {
350     params[i] = this.attributes[i].getValue();
351   }
352   return params;
353 };
354
355 WebView.prototype.attachWindow = function(guestInstanceId) {
356   this.guestInstanceId = guestInstanceId;
357   var params = this.buildAttachParams();
358
359   if (!this.isPluginInRenderTree()) {
360     return true;
361   }
362
363   return guestViewInternalNatives.AttachGuest(
364       this.internalInstanceId,
365       this.guestInstanceId,
366       params, function(w) {
367         this.contentWindow = w;
368       }.bind(this)
369   );
370 };
371
372 // -----------------------------------------------------------------------------
373 // Public-facing API methods.
374
375
376 // Navigates to the previous history entry.
377 WebView.prototype.back = function(callback) {
378   return this.go(-1, callback);
379 };
380
381 // Returns whether there is a previous history entry to navigate to.
382 WebView.prototype.canGoBack = function() {
383   return this.entryCount > 1 && this.currentEntryIndex > 0;
384 };
385
386 // Returns whether there is a subsequent history entry to navigate to.
387 WebView.prototype.canGoForward = function() {
388   return this.currentEntryIndex >= 0 &&
389       this.currentEntryIndex < (this.entryCount - 1);
390 };
391
392 // Clears browsing data for the WebView partition.
393 WebView.prototype.clearData = function() {
394   if (!this.guestInstanceId) {
395     return;
396   }
397   var args = $Array.concat([this.guestInstanceId], $Array.slice(arguments));
398   $Function.apply(WebViewInternal.clearData, null, args);
399 };
400
401 // Injects JavaScript code into the guest page.
402 WebView.prototype.executeScript = function(var_args) {
403   this.validateExecuteCodeCall();
404   var webviewSrc = this.attributes[WebViewConstants.ATTRIBUTE_SRC].getValue();
405   if (this.baseUrlForDataUrl != '') {
406     webviewSrc = this.baseUrlForDataUrl;
407   }
408   var args = $Array.concat([this.guestInstanceId, webviewSrc],
409                            $Array.slice(arguments));
410   $Function.apply(WebViewInternal.executeScript, null, args);
411 };
412
413 // Initiates a find-in-page request.
414 WebView.prototype.find = function(search_text, options, callback) {
415   if (!this.guestInstanceId) {
416     return;
417   }
418   WebViewInternal.find(this.guestInstanceId, search_text, options, callback);
419 };
420
421 // Navigates to the subsequent history entry.
422 WebView.prototype.forward = function(callback) {
423   return this.go(1, callback);
424 };
425
426 // Returns Chrome's internal process ID for the guest web page's current
427 // process.
428 WebView.prototype.getProcessId = function() {
429   return this.processId;
430 };
431
432 // Returns the user agent string used by the webview for guest page requests.
433 WebView.prototype.getUserAgent = function() {
434   return this.userAgentOverride || navigator.userAgent;
435 };
436
437 // Gets the current zoom factor.
438 WebView.prototype.getZoom = function(callback) {
439   if (!this.guestInstanceId) {
440     return;
441   }
442   WebViewInternal.getZoom(this.guestInstanceId, callback);
443 };
444
445 // Navigates to a history entry using a history index relative to the current
446 // navigation.
447 WebView.prototype.go = function(relativeIndex, callback) {
448   if (!this.guestInstanceId) {
449     return;
450   }
451   WebViewInternal.go(this.guestInstanceId, relativeIndex, callback);
452 };
453
454 // Injects CSS into the guest page.
455 WebView.prototype.insertCSS = function(var_args) {
456   this.validateExecuteCodeCall();
457   var webviewSrc = this.attributes[WebViewConstants.ATTRIBUTE_SRC].getValue();
458   if (this.baseUrlForDataUrl != '') {
459     webviewSrc = this.baseUrlForDataUrl;
460   }
461   var args = $Array.concat([this.guestInstanceId, webviewSrc],
462                            $Array.slice(arguments));
463   $Function.apply(WebViewInternal.insertCSS, null, args);
464 };
465
466 // Indicates whether or not the webview's user agent string has been overridden.
467 WebView.prototype.isUserAgentOverridden = function() {
468   return !!this.userAgentOverride &&
469       this.userAgentOverride != navigator.userAgent;
470 };
471
472 // Prints the contents of the webview.
473 WebView.prototype.print = function() {
474   this.executeScript({code: 'window.print();'});
475 };
476
477 // Reloads the current top-level page.
478 WebView.prototype.reload = function() {
479   if (!this.guestInstanceId) {
480     return;
481   }
482   WebViewInternal.reload(this.guestInstanceId);
483 };
484
485 // Override the user agent string used by the webview for guest page requests.
486 WebView.prototype.setUserAgentOverride = function(userAgentOverride) {
487   this.userAgentOverride = userAgentOverride;
488   if (!this.guestInstanceId) {
489     // If we are not attached yet, then we will pick up the user agent on
490     // attachment.
491     return;
492   }
493   WebViewInternal.overrideUserAgent(this.guestInstanceId, userAgentOverride);
494 };
495
496 // Changes the zoom factor of the page.
497 WebView.prototype.setZoom = function(zoomFactor, callback) {
498   if (!this.guestInstanceId) {
499     return;
500   }
501   WebViewInternal.setZoom(this.guestInstanceId, zoomFactor, callback);
502 };
503
504 // Stops loading the current navigation if one is in progress.
505 WebView.prototype.stop = function() {
506   if (!this.guestInstanceId) {
507     return;
508   }
509   WebViewInternal.stop(this.guestInstanceId);
510 };
511
512 // Ends the current find session.
513 WebView.prototype.stopFinding = function(action) {
514   if (!this.guestInstanceId) {
515     return;
516   }
517   WebViewInternal.stopFinding(this.guestInstanceId, action);
518 };
519
520 // Forcibly kills the guest web page's renderer process.
521 WebView.prototype.terminate = function() {
522   if (!this.guestInstanceId) {
523     return;
524   }
525   WebViewInternal.terminate(this.guestInstanceId);
526 };
527
528 // -----------------------------------------------------------------------------
529
530 // Registers browser plugin <object> custom element.
531 function registerBrowserPluginElement() {
532   var proto = Object.create(HTMLObjectElement.prototype);
533
534   proto.createdCallback = function() {
535     this.setAttribute('type', 'application/browser-plugin');
536     this.setAttribute('id', 'browser-plugin-' + IdGenerator.GetNextId());
537     // The <object> node fills in the <webview> container.
538     this.style.width = '100%';
539     this.style.height = '100%';
540   };
541
542   proto.attributeChangedCallback = function(name, oldValue, newValue) {
543     var internal = privates(this).internal;
544     if (!internal) {
545       return;
546     }
547     internal.handleBrowserPluginAttributeMutation(name, oldValue, newValue);
548   };
549
550   proto.attachedCallback = function() {
551     // Load the plugin immediately.
552     var unused = this.nonExistentAttribute;
553   };
554
555   WebView.BrowserPlugin =
556       DocumentNatives.RegisterElement('browserplugin', {extends: 'object',
557                                                         prototype: proto});
558
559   delete proto.createdCallback;
560   delete proto.attachedCallback;
561   delete proto.detachedCallback;
562   delete proto.attributeChangedCallback;
563 }
564
565 // Registers <webview> custom element.
566 function registerWebViewElement() {
567   var proto = Object.create(HTMLElement.prototype);
568
569   proto.createdCallback = function() {
570     new WebView(this);
571   };
572
573   proto.attributeChangedCallback = function(name, oldValue, newValue) {
574     var internal = privates(this).internal;
575     if (!internal) {
576       return;
577     }
578     internal.handleWebviewAttributeMutation(name, oldValue, newValue);
579   };
580
581   proto.detachedCallback = function() {
582     var internal = privates(this).internal;
583     if (!internal) {
584       return;
585     }
586     internal.elementAttached = false;
587     internal.reset();
588   };
589
590   proto.attachedCallback = function() {
591     var internal = privates(this).internal;
592     if (!internal) {
593       return;
594     }
595     if (!internal.elementAttached) {
596       internal.elementAttached = true;
597       internal.parseAttributes();
598     }
599   };
600
601   // Public-facing API methods.
602   var methods = [
603     'back',
604     'canGoBack',
605     'canGoForward',
606     'clearData',
607     'executeScript',
608     'find',
609     'forward',
610     'getProcessId',
611     'getUserAgent',
612     'getZoom',
613     'go',
614     'insertCSS',
615     'isUserAgentOverridden',
616     'print',
617     'reload',
618     'setUserAgentOverride',
619     'setZoom',
620     'stop',
621     'stopFinding',
622     'terminate'
623   ];
624
625   // Add the experimental API methods, if available.
626   var experimentalMethods =
627       WebView.maybeGetExperimentalAPIs();
628   methods = $Array.concat(methods, experimentalMethods);
629
630   // Forward proto.foo* method calls to WebView.foo*.
631   var createHandler = function(m) {
632     return function(var_args) {
633       var internal = privates(this).internal;
634       return $Function.apply(internal[m], internal, arguments);
635     };
636   };
637   for (var i = 0; methods[i]; ++i) {
638     proto[methods[i]] = createHandler(methods[i]);
639   }
640
641   window.WebView =
642       DocumentNatives.RegisterElement('webview', {prototype: proto});
643
644   // Delete the callbacks so developers cannot call them and produce unexpected
645   // behavior.
646   delete proto.createdCallback;
647   delete proto.attachedCallback;
648   delete proto.detachedCallback;
649   delete proto.attributeChangedCallback;
650 }
651
652 var useCapture = true;
653 window.addEventListener('readystatechange', function listener(event) {
654   if (document.readyState == 'loading')
655     return;
656
657   registerBrowserPluginElement();
658   registerWebViewElement();
659   window.removeEventListener(event.type, listener, useCapture);
660 }, useCapture);
661
662 // Implemented when the ChromeWebView API is available.
663 WebView.prototype.maybeGetChromeWebViewEvents = function() {};
664
665 // Implemented when the experimental WebView API is available.
666 WebView.maybeGetExperimentalAPIs = function() {};
667 WebView.prototype.maybeGetExperimentalEvents = function() {};
668 WebView.prototype.setupExperimentalContextMenus = function() {};
669
670 // Exports.
671 exports.WebView = WebView;
672 exports.WebViewInternal = WebViewInternal;