extension_options.js revision 5f1c94371a64b3196d4be9466099bb892df9b88e
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');
12
13// Mapping of the autosize attribute names to default values
14var AUTO_SIZE_ATTRIBUTES = {
15  'autosize': 'on',
16  'maxheight': 600,
17  'maxwidth': 800,
18  'minheight': 32,
19  'minwidth': 80
20};
21
22function ExtensionOptionsInternal(extensionoptionsNode) {
23  privates(extensionoptionsNode).internal = this;
24  this.extensionoptionsNode = extensionoptionsNode;
25  this.viewInstanceId = IdGenerator.GetNextId();
26
27  // on* Event handlers.
28  this.eventHandlers = {};
29  new ExtensionOptionsEvents(this, this.viewInstanceId);
30
31  this.setupNodeProperties();
32
33  if (this.parseExtensionAttribute())
34    this.init();
35};
36
37ExtensionOptionsInternal.prototype.attachWindow = function(instanceId) {
38  this.instanceId = instanceId;
39  var params = {
40    'autosize': this.autosize,
41    'instanceId': this.viewInstanceId,
42    'maxheight': parseInt(this.maxheight || 0),
43    'maxwidth': parseInt(this.maxwidth || 0),
44    'minheight': parseInt(this.minheight || 0),
45    'minwidth': parseInt(this.minwidth || 0)
46  }
47  return this.browserPluginNode['-internal-attach'](instanceId, params);
48};
49
50ExtensionOptionsInternal.prototype.createBrowserPluginNode = function() {
51  var browserPluginNode = new ExtensionOptionsInternal.BrowserPlugin();
52  privates(browserPluginNode).internal = this;
53  return browserPluginNode;
54};
55
56ExtensionOptionsInternal.prototype.createGuest = function() {
57  var params = {
58    'extensionId': this.extensionId,
59  };
60  GuestViewInternal.createGuest(
61      'extensionoptions',
62      params,
63      function(instanceId) {
64        if (instanceId == 0) {
65          this.initCalled = false;
66        } else {
67          this.attachWindow(instanceId);
68          GuestViewInternal.setAutoSize(this.instanceId, {
69            'enableAutoSize':
70                this.extensionoptionsNode.hasAttribute('autosize'),
71            'min': {
72            'width': parseInt(this.minwidth || 0),
73            'height': parseInt(this.minheight || 0)
74          },
75            'max': {
76              'width': parseInt(this.maxwidth || 0),
77              'height': parseInt(this.maxheight || 0)
78            }
79          });
80        }
81      }.bind(this));
82};
83
84ExtensionOptionsInternal.prototype.dispatchEvent =
85    function(extensionOptionsEvent) {
86  return this.extensionoptionsNode.dispatchEvent(extensionOptionsEvent);
87};
88
89ExtensionOptionsInternal.prototype.handleExtensionOptionsAttributeMutation =
90    function(name, oldValue, newValue) {
91  // We treat null attribute (attribute removed) and the empty string as
92  // one case.
93  oldValue = oldValue || '';
94  newValue = newValue || '';
95
96  if (oldValue === newValue)
97    return;
98
99  if (name == 'extension') {
100    this.extensionId = newValue;
101    // Create new guest view if one hasn't been created for this element.
102    if (!this.instanceId && this.parseExtensionAttribute())
103      this.init();
104    // TODO(ericzeng): Implement navigation to another guest view if we want
105    // that functionality.
106  } else if (AUTO_SIZE_ATTRIBUTES.hasOwnProperty(name) > -1) {
107    this[name] = newValue;
108    this.resetSizeConstraintsIfInvalid();
109
110    if (!this.instanceId)
111      return;
112
113    GuestViewInternal.setAutoSize(this.instanceId, {
114      'enableAutoSize': this.extensionoptionsNode.hasAttribute('autosize'),
115      'min': {
116        'width': parseInt(this.minwidth || 0),
117        'height': parseInt(this.minheight || 0)
118      },
119      'max': {
120        'width': parseInt(this.maxwidth || 0),
121        'height': parseInt(this.maxheight || 0)
122      }
123    });
124  }
125};
126
127ExtensionOptionsInternal.prototype.init = function() {
128  if (this.initCalled)
129    return;
130
131  this.initCalled = true;
132  this.browserPluginNode = this.createBrowserPluginNode();
133  var shadowRoot = this.extensionoptionsNode.createShadowRoot();
134  shadowRoot.appendChild(this.browserPluginNode);
135  this.createGuest();
136};
137
138ExtensionOptionsInternal.prototype.onSizeChanged = function(width, height) {
139  this.browserPluginNode.style.width = width + 'px';
140  this.browserPluginNode.style.height = height + 'px';
141};
142
143ExtensionOptionsInternal.prototype.parseExtensionAttribute = function() {
144  if (this.extensionoptionsNode.hasAttribute('extension')) {
145    var extensionId = this.extensionoptionsNode.getAttribute('extension');
146    // Only allow extensions to embed their own options page (if it has one).
147    if (chrome.runtime.id == extensionId &&
148        chrome.runtime.getManifest().hasOwnProperty('options_page')) {
149      this.extensionId  = extensionId;
150      return true;
151    }
152  }
153  return false;
154};
155
156// Adds an 'on<event>' property on the view, which can be used to set/unset
157// an event handler.
158ExtensionOptionsInternal.prototype.setupEventProperty = function(eventName) {
159  var propertyName = 'on' + eventName.toLowerCase();
160  var self = this;
161  var extensionoptionsNode = this.extensionoptionsNode;
162  Object.defineProperty(extensionoptionsNode, propertyName, {
163    get: function() {
164      return self.eventHandlers[propertyName];
165    },
166    set: function(value) {
167      if (self.eventHandlers[propertyName])
168        extensionoptionsNode.removeEventListener(
169            eventName, self.eventHandlers[propertyName]);
170      self.eventHandlers[propertyName] = value;
171      if (value)
172        extensionoptionsNode.addEventListener(eventName, value);
173    },
174    enumerable: true
175  });
176};
177
178ExtensionOptionsInternal.prototype.setupNodeProperties = function() {
179  utils.forEach(AUTO_SIZE_ATTRIBUTES, function(attributeName) {
180    // Get the size constraints from the <extensionoptions> tag, or use the
181    // defaults if not specified
182    if (this.extensionoptionsNode.hasAttribute(attributeName)) {
183      this[attributeName] =
184          this.extensionoptionsNode.getAttribute(attributeName);
185    } else {
186      this[attributeName] = AUTO_SIZE_ATTRIBUTES[attributeName];
187    }
188
189    Object.defineProperty(this.extensionoptionsNode, attributeName, {
190      get: function() {
191        return this[attributeName];
192      }.bind(this),
193      set: function(value) {
194        this.extensionoptionsNode.setAttribute(attributeName, value);
195      }.bind(this),
196      enumerable: true
197    });
198  }, this);
199
200  this.resetSizeConstraintsIfInvalid();
201
202  Object.defineProperty(this.extensionoptionsNode, 'extension', {
203    get: function() {
204      return this.extensionId;
205    }.bind(this),
206    set: function(value) {
207      this.extensionoptionsNode.setAttribute('extension', value);
208    }.bind(this),
209    enumerable: true
210  });
211};
212
213ExtensionOptionsInternal.prototype.resetSizeConstraintsIfInvalid = function () {
214  if (this.minheight > this.maxheight || this.minheight < 0) {
215    this.minheight = AUTO_SIZE_ATTRIBUTES.minheight;
216    this.maxheight = AUTO_SIZE_ATTRIBUTES.maxheight;
217  }
218  if (this.minwidth > this.maxwidth || this.minwidth < 0) {
219    this.minwidth = AUTO_SIZE_ATTRIBUTES.minwidth;
220    this.maxwidth = AUTO_SIZE_ATTRIBUTES.maxwidth;
221  }
222}
223
224function registerBrowserPluginElement() {
225  var proto = Object.create(HTMLObjectElement.prototype);
226
227  proto.createdCallback = function() {
228    this.setAttribute('type', 'application/browser-plugin');
229    this.style.width = '100%';
230    this.style.height = '100%';
231  };
232
233  proto.attachedCallback = function() {
234    // Load the plugin immediately.
235    var unused = this.nonExistentAttribute;
236  };
237
238  ExtensionOptionsInternal.BrowserPlugin =
239      DocumentNatives.RegisterElement('extensionoptionsplugin',
240                                       {extends: 'object', prototype: proto});
241  delete proto.createdCallback;
242  delete proto.attachedCallback;
243  delete proto.detachedCallback;
244  delete proto.attributeChangedCallback;
245}
246
247function registerExtensionOptionsElement() {
248  var proto = Object.create(HTMLElement.prototype);
249
250  proto.createdCallback = function() {
251    new ExtensionOptionsInternal(this);
252  };
253
254  proto.attributeChangedCallback = function(name, oldValue, newValue) {
255    var internal = privates(this).internal;
256    if (!internal)
257      return;
258    internal.handleExtensionOptionsAttributeMutation(name, oldValue, newValue);
259  };
260
261  window.ExtensionOptions =
262      DocumentNatives.RegisterElement('extensionoptions', {prototype: proto});
263
264  // Delete the callbacks so developers cannot call them and produce unexpected
265  // behavior.
266  delete proto.createdCallback;
267  delete proto.attachedCallback;
268  delete proto.detachedCallback;
269  delete proto.attributeChangedCallback;
270}
271
272var useCapture = true;
273window.addEventListener('readystatechange', function listener(event) {
274  if (document.readyState == 'loading')
275    return;
276
277  registerBrowserPluginElement();
278  registerExtensionOptionsElement();
279  window.removeEventListener(event.type, listener, useCapture);
280}, useCapture);
281