Home | History | Annotate | Download | only in extensions
      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 cr.define('options', function() {
      6   'use strict';
      7 
      8   /**
      9    * A lookup helper function to find the first node that has an id (starting
     10    * at |node| and going up the parent chain).
     11    * @param {Element} node The node to start looking at.
     12    */
     13   function findIdNode(node) {
     14     while (node && !node.id) {
     15       node = node.parentNode;
     16     }
     17     return node;
     18   }
     19 
     20   /**
     21    * Creates a new list of extensions.
     22    * @param {Object=} opt_propertyBag Optional properties.
     23    * @constructor
     24    * @extends {cr.ui.div}
     25    */
     26   var ExtensionsList = cr.ui.define('div');
     27 
     28   /**
     29    * @type {Object.<string, boolean>} A map from extension id to a boolean
     30    *     indicating whether the incognito warning is showing. This persists
     31    *     between calls to decorate.
     32    */
     33   var butterBarVisibility = {};
     34 
     35   /**
     36    * @type {Object.<string, string>} A map from extension id to last reloaded
     37    *     timestamp. The timestamp is recorded when the user click the 'Reload'
     38    *     link. It is used to refresh the icon of an unpacked extension.
     39    *     This persists between calls to decorate.
     40    */
     41   var extensionReloadedTimestamp = {};
     42 
     43   ExtensionsList.prototype = {
     44     __proto__: HTMLDivElement.prototype,
     45 
     46     /** @override */
     47     decorate: function() {
     48       this.textContent = '';
     49 
     50       this.showExtensionNodes_();
     51     },
     52 
     53     getIdQueryParam_: function() {
     54       return parseQueryParams(document.location)['id'];
     55     },
     56 
     57     /**
     58      * Creates all extension items from scratch.
     59      * @private
     60      */
     61     showExtensionNodes_: function() {
     62       // Iterate over the extension data and add each item to the list.
     63       this.data_.extensions.forEach(this.createNode_, this);
     64 
     65       var idToHighlight = this.getIdQueryParam_();
     66       if (idToHighlight && $(idToHighlight)) {
     67         // Scroll offset should be calculated slightly higher than the actual
     68         // offset of the element being scrolled to, so that it ends up not all
     69         // the way at the top. That way it is clear that there are more elements
     70         // above the element being scrolled to.
     71         var scrollFudge = 1.2;
     72         var offset = $(idToHighlight).offsetTop -
     73                      (scrollFudge * $(idToHighlight).clientHeight);
     74         var wrapper = this.parentNode;
     75         var list = wrapper.parentNode;
     76         list.scrollTop = offset;
     77       }
     78 
     79       if (this.data_.extensions.length == 0)
     80         this.classList.add('empty-extension-list');
     81       else
     82         this.classList.remove('empty-extension-list');
     83     },
     84 
     85     /**
     86      * Synthesizes and initializes an HTML element for the extension metadata
     87      * given in |extension|.
     88      * @param {Object} extension A dictionary of extension metadata.
     89      * @private
     90      */
     91     createNode_: function(extension) {
     92       var template = $('template-collection').querySelector(
     93           '.extension-list-item-wrapper');
     94       var node = template.cloneNode(true);
     95       node.id = extension.id;
     96 
     97       if (!extension.enabled || extension.terminated)
     98         node.classList.add('inactive-extension');
     99 
    100       if (!extension.userModifiable)
    101         node.classList.add('may-not-disable');
    102 
    103       var idToHighlight = this.getIdQueryParam_();
    104       if (node.id == idToHighlight)
    105         node.classList.add('extension-highlight');
    106 
    107       var item = node.querySelector('.extension-list-item');
    108       // Prevent the image cache of extension icon by using the reloaded
    109       // timestamp as a query string. The timestamp is recorded when the user
    110       // clicks the 'Reload' link. http://crbug.com/159302.
    111       if (extensionReloadedTimestamp[extension.id]) {
    112         item.style.backgroundImage =
    113             'url(' + extension.icon + '?' +
    114             extensionReloadedTimestamp[extension.id] + ')';
    115       } else {
    116         item.style.backgroundImage = 'url(' + extension.icon + ')';
    117       }
    118 
    119       var title = node.querySelector('.extension-title');
    120       title.textContent = extension.name;
    121 
    122       var version = node.querySelector('.extension-version');
    123       version.textContent = extension.version;
    124 
    125       var locationText = node.querySelector('.location-text');
    126       locationText.textContent = extension.locationText;
    127 
    128       var description = node.querySelector('.extension-description span');
    129       description.textContent = extension.description;
    130 
    131       // The 'Show Browser Action' button.
    132       if (extension.enable_show_button) {
    133         var showButton = node.querySelector('.show-button');
    134         showButton.addEventListener('click', function(e) {
    135           chrome.send('extensionSettingsShowButton', [extension.id]);
    136         });
    137         showButton.hidden = false;
    138       }
    139 
    140       // The 'allow in incognito' checkbox.
    141       var incognito = node.querySelector('.incognito-control input');
    142       incognito.disabled = !extension.incognitoCanBeToggled;
    143       incognito.checked = extension.enabledIncognito;
    144       if (!incognito.disabled) {
    145         incognito.addEventListener('change', function(e) {
    146           var checked = e.target.checked;
    147           butterBarVisibility[extension.id] = checked;
    148           butterBar.hidden = !checked || extension.is_hosted_app;
    149           chrome.send('extensionSettingsEnableIncognito',
    150                       [extension.id, String(checked)]);
    151         });
    152       }
    153       var butterBar = node.querySelector('.butter-bar');
    154       butterBar.hidden = !butterBarVisibility[extension.id];
    155 
    156       // The 'allow file:// access' checkbox.
    157       if (extension.wantsFileAccess) {
    158         var fileAccess = node.querySelector('.file-access-control');
    159         fileAccess.addEventListener('click', function(e) {
    160           chrome.send('extensionSettingsAllowFileAccess',
    161                       [extension.id, String(e.target.checked)]);
    162         });
    163         fileAccess.querySelector('input').checked = extension.allowFileAccess;
    164         fileAccess.hidden = false;
    165       }
    166 
    167       // The 'Options' link.
    168       if (extension.enabled && extension.optionsUrl) {
    169         var options = node.querySelector('.options-link');
    170         options.addEventListener('click', function(e) {
    171           chrome.send('extensionSettingsOptions', [extension.id]);
    172           e.preventDefault();
    173         });
    174         options.hidden = false;
    175       }
    176 
    177       // The 'Permissions' link.
    178       var permissions = node.querySelector('.permissions-link');
    179       permissions.addEventListener('click', function(e) {
    180         chrome.send('extensionSettingsPermissions', [extension.id]);
    181         e.preventDefault();
    182       });
    183 
    184       // The 'View in Web Store/View Web Site' link.
    185       if (extension.homepageUrl) {
    186         var siteLink = node.querySelector('.site-link');
    187         siteLink.href = extension.homepageUrl;
    188         siteLink.textContent = loadTimeData.getString(
    189                 extension.homepageProvided ? 'extensionSettingsVisitWebsite' :
    190                                              'extensionSettingsVisitWebStore');
    191         siteLink.hidden = false;
    192       }
    193 
    194       if (extension.allow_reload) {
    195         // The 'Reload' link.
    196         var reload = node.querySelector('.reload-link');
    197         reload.addEventListener('click', function(e) {
    198           chrome.send('extensionSettingsReload', [extension.id]);
    199           extensionReloadedTimestamp[extension.id] = Date.now();
    200         });
    201         reload.hidden = false;
    202 
    203         if (extension.is_platform_app) {
    204           // The 'Launch' link.
    205           var launch = node.querySelector('.launch-link');
    206           launch.addEventListener('click', function(e) {
    207             chrome.send('extensionSettingsLaunch', [extension.id]);
    208           });
    209           launch.hidden = false;
    210 
    211           // The 'Restart' link.
    212           var restart = node.querySelector('.restart-link');
    213           restart.addEventListener('click', function(e) {
    214             chrome.send('extensionSettingsRestart', [extension.id]);
    215           });
    216           restart.hidden = false;
    217         }
    218       }
    219 
    220       if (!extension.terminated) {
    221         // The 'Enabled' checkbox.
    222         var enable = node.querySelector('.enable-checkbox');
    223         enable.hidden = false;
    224         enable.querySelector('input').disabled = !extension.userModifiable;
    225 
    226         if (extension.userModifiable) {
    227           enable.addEventListener('click', function(e) {
    228             // When e.target is the label instead of the checkbox, it doesn't
    229             // have the checked property and the state of the checkbox is
    230             // left unchanged.
    231             var checked = e.target.checked;
    232             if (checked == undefined)
    233               checked = !e.currentTarget.querySelector('input').checked;
    234             chrome.send('extensionSettingsEnable',
    235                         [extension.id, checked ? 'true' : 'false']);
    236 
    237             // This may seem counter-intuitive (to not set/clear the checkmark)
    238             // but this page will be updated asynchronously if the extension
    239             // becomes enabled/disabled. It also might not become enabled or
    240             // disabled, because the user might e.g. get prompted when enabling
    241             // and choose not to.
    242             e.preventDefault();
    243           });
    244         }
    245 
    246         enable.querySelector('input').checked = extension.enabled;
    247       } else {
    248         var terminatedReload = node.querySelector('.terminated-reload-link');
    249         terminatedReload.hidden = false;
    250         terminatedReload.addEventListener('click', function(e) {
    251           chrome.send('extensionSettingsReload', [extension.id]);
    252         });
    253       }
    254 
    255       // 'Remove' button.
    256       var trashTemplate = $('template-collection').querySelector('.trash');
    257       var trash = trashTemplate.cloneNode(true);
    258       trash.title = loadTimeData.getString('extensionUninstall');
    259       trash.addEventListener('click', function(e) {
    260         butterBarVisibility[extension.id] = false;
    261         chrome.send('extensionSettingsUninstall', [extension.id]);
    262       });
    263       node.querySelector('.enable-controls').appendChild(trash);
    264 
    265       // Developer mode ////////////////////////////////////////////////////////
    266 
    267       // First we have the id.
    268       var idLabel = node.querySelector('.extension-id');
    269       idLabel.textContent = ' ' + extension.id;
    270 
    271       // Then the path, if provided by unpacked extension.
    272       if (extension.isUnpacked) {
    273         var loadPath = node.querySelector('.load-path');
    274         loadPath.hidden = false;
    275         loadPath.querySelector('span:nth-of-type(2)').textContent =
    276             ' ' + extension.path;
    277       }
    278 
    279       // Then the 'managed, cannot uninstall/disable' message.
    280       if (!extension.userModifiable)
    281         node.querySelector('.managed-message').hidden = false;
    282 
    283       // Then active views.
    284       if (extension.views.length > 0) {
    285         var activeViews = node.querySelector('.active-views');
    286         activeViews.hidden = false;
    287         var link = activeViews.querySelector('a');
    288 
    289         extension.views.forEach(function(view, i) {
    290           var label = view.path +
    291               (view.incognito ?
    292                   ' ' + loadTimeData.getString('viewIncognito') : '') +
    293               (view.renderProcessId == -1 ?
    294                   ' ' + loadTimeData.getString('viewInactive') : '');
    295           link.textContent = label;
    296           link.addEventListener('click', function(e) {
    297             // TODO(estade): remove conversion to string?
    298             chrome.send('extensionSettingsInspect', [
    299               String(extension.id),
    300               String(view.renderProcessId),
    301               String(view.renderViewId),
    302               view.incognito
    303             ]);
    304           });
    305 
    306           if (i < extension.views.length - 1) {
    307             link = link.cloneNode(true);
    308             activeViews.appendChild(link);
    309           }
    310         });
    311       }
    312 
    313       // The extension warnings (describing runtime issues).
    314       if (extension.warnings) {
    315         var panel = node.querySelector('.extension-warnings');
    316         panel.hidden = false;
    317         var list = panel.querySelector('ul');
    318         extension.warnings.forEach(function(warning) {
    319           list.appendChild(document.createElement('li')).innerText = warning;
    320         });
    321       }
    322 
    323       // The install warnings.
    324       if (extension.installWarnings) {
    325         var panel = node.querySelector('.install-warnings');
    326         panel.hidden = false;
    327         var list = panel.querySelector('ul');
    328         extension.installWarnings.forEach(function(warning) {
    329           var li = document.createElement('li');
    330           li[warning.isHTML ? 'innerHTML' : 'innerText'] = warning.message;
    331           list.appendChild(li);
    332         });
    333       }
    334 
    335       this.appendChild(node);
    336       if (location.hash.substr(1) == extension.id) {
    337         // Scroll beneath the fixed header so that the extension is not
    338         // obscured.
    339         var topScroll = node.offsetTop - $('page-header').offsetHeight;
    340         var pad = parseInt(getComputedStyle(node, null).marginTop, 10);
    341         if (!isNaN(pad))
    342           topScroll -= pad / 2;
    343         document.body.scrollTop = topScroll;
    344       }
    345     }
    346   };
    347 
    348   return {
    349     ExtensionsList: ExtensionsList
    350   };
    351 });
    352