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
5var DocumentNatives = requireNative('document_natives');
6var ExtensionOptionsEvents =
7    require('extensionOptionsEvents').ExtensionOptionsEvents;
8var GuestViewInternal =
9    require('binding').Binding.create('guestViewInternal').generate();
10var IdGenerator = requireNative('id_generator');
11var utils = require('utils');
12var guestViewInternalNatives = requireNative('guest_view_internal');
13
14// Mapping of the autosize attribute names to default values
15var AUTO_SIZE_ATTRIBUTES = {
16  'autosize': 'on',
17  'maxheight': window.innerHeight,
18  'maxwidth': window.innerWidth,
19  'minheight': 32,
20  'minwidth': 32
21};
22
23function 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
51ExtensionOptionsInternal.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
65ExtensionOptionsInternal.prototype.createBrowserPluginNode = function() {
66  var browserPluginNode = new ExtensionOptionsInternal.BrowserPlugin();
67  privates(browserPluginNode).internal = this;
68  return browserPluginNode;
69};
70
71ExtensionOptionsInternal.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
92ExtensionOptionsInternal.prototype.dispatchEvent =
93    function(extensionOptionsEvent) {
94  return this.extensionoptionsNode.dispatchEvent(extensionOptionsEvent);
95};
96
97ExtensionOptionsInternal.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
142ExtensionOptionsInternal.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
153ExtensionOptionsInternal.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
167ExtensionOptionsInternal.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
175ExtensionOptionsInternal.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.
203ExtensionOptionsInternal.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
222ExtensionOptionsInternal.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
257ExtensionOptionsInternal.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 */
277ExtensionOptionsInternal.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 */
287ExtensionOptionsInternal.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
296function 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
327function 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
368var useCapture = true;
369window.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);
377