Upstream version 10.39.225.0
[platform/framework/web/crosswalk.git] / src / chrome / renderer / resources / extensions / extension_options.js
1 // Copyright 2014 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 var DocumentNatives = requireNative('document_natives');
6 var ExtensionOptionsEvents =
7     require('extensionOptionsEvents').ExtensionOptionsEvents;
8 var GuestViewInternal =
9     require('binding').Binding.create('guestViewInternal').generate();
10 var IdGenerator = requireNative('id_generator');
11 var utils = require('utils');
12 var guestViewInternalNatives = requireNative('guest_view_internal');
13
14 // Mapping of the autosize attribute names to default values
15 var AUTO_SIZE_ATTRIBUTES = {
16   'autosize': 'on',
17   'maxheight': window.innerHeight,
18   'maxwidth': window.innerWidth,
19   'minheight': 32,
20   'minwidth': 32
21 };
22
23 function ExtensionOptionsInternal(extensionoptionsNode) {
24   privates(extensionoptionsNode).internal = this;
25   this.extensionoptionsNode = extensionoptionsNode;
26   this.viewInstanceId = IdGenerator.GetNextId();
27
28   this.autosizeDeferred = false;
29
30   // on* Event handlers.
31   this.eventHandlers = {};
32
33   // setupEventProperty is normally called in extension_options_events.js to
34   // register events, but the createfailed event is registered here because
35   // the event is fired from here instead of through
36   // extension_options_events.js.
37   this.setupEventProperty('createfailed');
38   new ExtensionOptionsEvents(this, this.viewInstanceId);
39
40   this.setupNodeProperties();
41
42   this.parseExtensionAttribute();
43
44   // Once the browser plugin has been created, the guest view will be created
45   // and attached. See handleBrowserPluginAttributeMutation().
46   this.browserPluginNode = this.createBrowserPluginNode();
47   var shadowRoot = this.extensionoptionsNode.createShadowRoot();
48   shadowRoot.appendChild(this.browserPluginNode);
49 };
50
51 ExtensionOptionsInternal.prototype.attachWindow = function() {
52   return guestViewInternalNatives.AttachGuest(
53       this.internalInstanceId,
54       this.guestInstanceId,
55       {
56         'autosize': this.extensionoptionsNode.hasAttribute('autosize'),
57         'instanceId': this.viewInstanceId,
58         'maxheight': parseInt(this.maxheight || 0),
59         'maxwidth': parseInt(this.maxwidth || 0),
60         'minheight': parseInt(this.minheight || 0),
61         'minwidth': parseInt(this.minwidth || 0)
62       });
63 };
64
65 ExtensionOptionsInternal.prototype.createBrowserPluginNode = function() {
66   var browserPluginNode = new ExtensionOptionsInternal.BrowserPlugin();
67   privates(browserPluginNode).internal = this;
68   return browserPluginNode;
69 };
70
71 ExtensionOptionsInternal.prototype.createGuest = function() {
72   var params = {
73     'extensionId': this.extensionId,
74   };
75   GuestViewInternal.createGuest(
76       'extensionoptions',
77       params,
78       function(guestInstanceId) {
79         if (guestInstanceId == 0) {
80           // Fire a createfailed event here rather than in ExtensionOptionsGuest
81           // because the guest will not be created, and cannot fire an event.
82           this.initCalled = false;
83           var createFailedEvent = new Event('createfailed', { bubbles: true });
84           this.dispatchEvent(createFailedEvent);
85         } else {
86           this.guestInstanceId = guestInstanceId;
87           this.attachWindow();
88         }
89       }.bind(this));
90 };
91
92 ExtensionOptionsInternal.prototype.dispatchEvent =
93     function(extensionOptionsEvent) {
94   return this.extensionoptionsNode.dispatchEvent(extensionOptionsEvent);
95 };
96
97 ExtensionOptionsInternal.prototype.handleExtensionOptionsAttributeMutation =
98     function(name, oldValue, newValue) {
99   // We treat null attribute (attribute removed) and the empty string as
100   // one case.
101   oldValue = oldValue || '';
102   newValue = newValue || '';
103
104   if (oldValue === newValue)
105     return;
106
107   if (name == 'extension' && !oldValue && newValue) {
108     this.extensionId = newValue;
109     // If the browser plugin is not ready then don't create the guest until
110     // it is ready (in handleBrowserPluginAttributeMutation).
111     if (!this.internalInstanceId)
112       return;
113
114     // If a guest view does not exist then create one.
115     if (!this.guestInstanceId) {
116       this.createGuest();
117       return;
118     }
119     // TODO(ericzeng): Implement navigation to another guest view if we want
120     // that functionality.
121   } else if (AUTO_SIZE_ATTRIBUTES.hasOwnProperty(name) > -1) {
122     this[name] = newValue;
123     this.resetSizeConstraintsIfInvalid();
124
125     if (!this.guestInstanceId)
126       return;
127
128     GuestViewInternal.setAutoSize(this.guestInstanceId, {
129       'enableAutoSize': this.extensionoptionsNode.hasAttribute('autosize'),
130       'min': {
131         'width': parseInt(this.minwidth || 0),
132         'height': parseInt(this.minheight || 0)
133       },
134       'max': {
135         'width': parseInt(this.maxwidth || 0),
136         'height': parseInt(this.maxheight || 0)
137       }
138     });
139   }
140 };
141
142 ExtensionOptionsInternal.prototype.handleBrowserPluginAttributeMutation =
143     function(name, oldValue, newValue) {
144   if (name == 'internalinstanceid' && !oldValue && !!newValue) {
145     this.internalInstanceId = parseInt(newValue);
146     this.browserPluginNode.removeAttribute('internalinstanceid');
147     if (this.extensionId)
148       this.createGuest();
149
150   }
151 };
152
153 ExtensionOptionsInternal.prototype.onSizeChanged =
154     function(newWidth, newHeight, oldWidth, oldHeight) {
155   if (this.autosizeDeferred) {
156     this.deferredAutoSizeState = {
157       newWidth: newWidth,
158       newHeight: newHeight,
159       oldWidth: oldWidth,
160       oldHeight: oldHeight
161     };
162   } else {
163     this.resize(newWidth, newHeight, oldWidth, oldHeight);
164   }
165 };
166
167 ExtensionOptionsInternal.prototype.parseExtensionAttribute = function() {
168   if (this.extensionoptionsNode.hasAttribute('extension')) {
169     this.extensionId = this.extensionoptionsNode.getAttribute('extension');
170     return true;
171   }
172   return false;
173 };
174
175 ExtensionOptionsInternal.prototype.resize =
176     function(newWidth, newHeight, oldWidth, oldHeight) {
177   this.browserPluginNode.style.width = newWidth + 'px';
178   this.browserPluginNode.style.height = newHeight + 'px';
179
180   // Do not allow the options page's dimensions to shrink so that the options
181   // page has a consistent UI. If the new size is larger than the minimum,
182   // make that the new minimum size.
183   if (newWidth > this.minwidth)
184     this.minwidth = newWidth;
185   if (newHeight > this.minheight)
186     this.minheight = newHeight;
187
188   GuestViewInternal.setAutoSize(this.guestInstanceId, {
189     'enableAutoSize': this.extensionoptionsNode.hasAttribute('autosize'),
190     'min': {
191       'width': parseInt(this.minwidth || 0),
192       'height': parseInt(this.minheight || 0)
193     },
194     'max': {
195       'width': parseInt(this.maxwidth || 0),
196       'height': parseInt(this.maxheight || 0)
197     }
198   });
199 };
200
201 // Adds an 'on<event>' property on the view, which can be used to set/unset
202 // an event handler.
203 ExtensionOptionsInternal.prototype.setupEventProperty = function(eventName) {
204   var propertyName = 'on' + eventName.toLowerCase();
205   var extensionoptionsNode = this.extensionoptionsNode;
206   Object.defineProperty(extensionoptionsNode, propertyName, {
207     get: function() {
208       return this.eventHandlers[propertyName];
209     }.bind(this),
210     set: function(value) {
211       if (this.eventHandlers[propertyName])
212         extensionoptionsNode.removeEventListener(
213             eventName, this.eventHandlers[propertyName]);
214       this.eventHandlers[propertyName] = value;
215       if (value)
216         extensionoptionsNode.addEventListener(eventName, value);
217     }.bind(this),
218     enumerable: true
219   });
220 };
221
222 ExtensionOptionsInternal.prototype.setupNodeProperties = function() {
223   utils.forEach(AUTO_SIZE_ATTRIBUTES, function(attributeName) {
224     // Get the size constraints from the <extensionoptions> tag, or use the
225     // defaults if not specified
226     if (this.extensionoptionsNode.hasAttribute(attributeName)) {
227       this[attributeName] =
228           this.extensionoptionsNode.getAttribute(attributeName);
229     } else {
230       this[attributeName] = AUTO_SIZE_ATTRIBUTES[attributeName];
231     }
232
233     Object.defineProperty(this.extensionoptionsNode, attributeName, {
234       get: function() {
235         return this[attributeName];
236       }.bind(this),
237       set: function(value) {
238         this.extensionoptionsNode.setAttribute(attributeName, value);
239       }.bind(this),
240       enumerable: true
241     });
242   }, this);
243
244   this.resetSizeConstraintsIfInvalid();
245
246   Object.defineProperty(this.extensionoptionsNode, 'extension', {
247     get: function() {
248       return this.extensionId;
249     }.bind(this),
250     set: function(value) {
251       this.extensionoptionsNode.setAttribute('extension', value);
252     }.bind(this),
253     enumerable: true
254   });
255 };
256
257 ExtensionOptionsInternal.prototype.resetSizeConstraintsIfInvalid = function () {
258   if (this.minheight > this.maxheight || this.minheight < 0) {
259     this.minheight = AUTO_SIZE_ATTRIBUTES.minheight;
260     this.maxheight = AUTO_SIZE_ATTRIBUTES.maxheight;
261   }
262   if (this.minwidth > this.maxwidth || this.minwidth < 0) {
263     this.minwidth = AUTO_SIZE_ATTRIBUTES.minwidth;
264     this.maxwidth = AUTO_SIZE_ATTRIBUTES.maxwidth;
265   }
266 };
267
268 /**
269  * Toggles whether the element should automatically resize to its preferred
270  * size. If set to true, when the element receives new autosize dimensions,
271  * it passes them to the embedder in a sizechanged event, but does not resize
272  * itself to those dimensions until the embedder calls resumeDeferredAutoSize.
273  * This allows the embedder to defer the resizing until it is ready.
274  * When set to false, the element resizes whenever it receives new autosize
275  * dimensions.
276  */
277 ExtensionOptionsInternal.prototype.setDeferAutoSize = function(value) {
278   if (!value)
279     resumeDeferredAutoSize();
280   this.autosizeDeferred = value;
281 };
282
283 /**
284  * Allows the element to resize to most recent set of autosize dimensions if
285  * autosizing is being deferred.
286  */
287 ExtensionOptionsInternal.prototype.resumeDeferredAutoSize = function() {
288   if (this.autosizeDeferred) {
289     this.resize(this.deferredAutoSizeState.newWidth,
290                 this.deferredAutoSizeState.newHeight,
291                 this.deferredAutoSizeState.oldWidth,
292                 this.deferredAutoSizeState.oldHeight);
293   }
294 };
295
296 function registerBrowserPluginElement() {
297   var proto = Object.create(HTMLObjectElement.prototype);
298
299   proto.createdCallback = function() {
300     this.setAttribute('type', 'application/browser-plugin');
301     this.style.width = '100%';
302     this.style.height = '100%';
303   };
304
305   proto.attributeChangedCallback = function(name, oldValue, newValue) {
306     var internal = privates(this).internal;
307     if (!internal) {
308       return;
309     }
310     internal.handleBrowserPluginAttributeMutation(name, oldValue, newValue);
311   };
312
313   proto.attachedCallback = function() {
314     // Load the plugin immediately.
315     var unused = this.nonExistentAttribute;
316   };
317
318   ExtensionOptionsInternal.BrowserPlugin =
319       DocumentNatives.RegisterElement('extensionoptionsplugin',
320                                       {extends: 'object', prototype: proto});
321   delete proto.createdCallback;
322   delete proto.attachedCallback;
323   delete proto.detachedCallback;
324   delete proto.attributeChangedCallback;
325 }
326
327 function registerExtensionOptionsElement() {
328   var proto = Object.create(HTMLElement.prototype);
329
330   proto.createdCallback = function() {
331     new ExtensionOptionsInternal(this);
332   };
333
334   proto.attributeChangedCallback = function(name, oldValue, newValue) {
335     var internal = privates(this).internal;
336     if (!internal)
337       return;
338     internal.handleExtensionOptionsAttributeMutation(name, oldValue, newValue);
339   };
340
341   var methods = [
342     'setDeferAutoSize',
343     'resumeDeferredAutoSize'
344   ];
345
346   // Forward proto.foo* method calls to ExtensionOptionsInternal.foo*.
347   for (var i = 0; methods[i]; ++i) {
348     var createHandler = function(m) {
349       return function(var_args) {
350         var internal = privates(this).internal;
351         return $Function.apply(internal[m], internal, arguments);
352       };
353     };
354     proto[methods[i]] = createHandler(methods[i]);
355   }
356
357   window.ExtensionOptions =
358       DocumentNatives.RegisterElement('extensionoptions', {prototype: proto});
359
360   // Delete the callbacks so developers cannot call them and produce unexpected
361   // behavior.
362   delete proto.createdCallback;
363   delete proto.attachedCallback;
364   delete proto.detachedCallback;
365   delete proto.attributeChangedCallback;
366 }
367
368 var useCapture = true;
369 window.addEventListener('readystatechange', function listener(event) {
370   if (document.readyState == 'loading')
371     return;
372
373   registerBrowserPluginElement();
374   registerExtensionOptionsElement();
375   window.removeEventListener(event.type, listener, useCapture);
376 }, useCapture);