Home | History | Annotate | Download | only in extensions
      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);
    377