Revert of Add button to add new FSP services to Files app. (patchset #8 id:140001...
[chromium-blink-merge.git] / chrome / browser / resources / extensions / extension_list.js
blob15832f38b4e0277a4b4cb6a14875f192bf169eaf
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.
5 <include src="extension_error.js">
7 ///////////////////////////////////////////////////////////////////////////////
8 // ExtensionFocusRow:
10 /**
11  * Provides an implementation for a single column grid.
12  * @constructor
13  * @extends {cr.ui.FocusRow}
14  */
15 function ExtensionFocusRow() {}
17 /**
18  * Decorates |focusRow| so that it can be treated as a ExtensionFocusRow.
19  * @param {Element} focusRow The element that has all the columns.
20  * @param {Node} boundary Focus events are ignored outside of this node.
21  */
22 ExtensionFocusRow.decorate = function(focusRow, boundary) {
23   focusRow.__proto__ = ExtensionFocusRow.prototype;
24   focusRow.decorate(boundary);
27 ExtensionFocusRow.prototype = {
28   __proto__: cr.ui.FocusRow.prototype,
30   /** @override */
31   getEquivalentElement: function(element) {
32     if (this.focusableElements.indexOf(element) > -1)
33       return element;
35     // All elements default to another element with the same type.
36     var columnType = element.getAttribute('column-type');
37     var equivalent = this.querySelector('[column-type=' + columnType + ']');
39     if (!equivalent || !this.canAddElement_(equivalent)) {
40       var actionLinks = ['options', 'website', 'launch', 'localReload'];
41       var optionalControls = ['showButton', 'incognito', 'dev-collectErrors',
42                               'allUrls', 'localUrls'];
43       var removeStyleButtons = ['trash', 'enterprise'];
44       var enableControls = ['terminatedReload', 'repair', 'enabled'];
46       if (actionLinks.indexOf(columnType) > -1)
47         equivalent = this.getFirstFocusableByType_(actionLinks);
48       else if (optionalControls.indexOf(columnType) > -1)
49         equivalent = this.getFirstFocusableByType_(optionalControls);
50       else if (removeStyleButtons.indexOf(columnType) > -1)
51         equivalent = this.getFirstFocusableByType_(removeStyleButtons);
52       else if (enableControls.indexOf(columnType) > -1)
53         equivalent = this.getFirstFocusableByType_(enableControls);
54     }
56     // Return the first focusable element if no equivalent type is found.
57     return equivalent || this.focusableElements[0];
58   },
60   /** Updates the list of focusable elements. */
61   updateFocusableElements: function() {
62     this.focusableElements.length = 0;
64     var focusableCandidates = this.querySelectorAll('[column-type]');
65     for (var i = 0; i < focusableCandidates.length; ++i) {
66       var element = focusableCandidates[i];
67       if (this.canAddElement_(element))
68         this.addFocusableElement(element);
69     }
70   },
72   /**
73    * Get the first focusable element that matches a list of types.
74    * @param {Array<string>} types An array of types to match from.
75    * @return {?Element} Return the first element that matches a type in |types|.
76    * @private
77    */
78   getFirstFocusableByType_: function(types) {
79     for (var i = 0; i < this.focusableElements.length; ++i) {
80       var element = this.focusableElements[i];
81       if (types.indexOf(element.getAttribute('column-type')) > -1)
82         return element;
83     }
84     return null;
85   },
87   /**
88    * Setup a typical column in the ExtensionFocusRow. A column can be any
89    * element and should have an action when clicked/toggled. This function
90    * adds a listener and a handler for an event. Also adds the "column-type"
91    * attribute to make the element focusable in |updateFocusableElements|.
92    * @param {string} query A query to select the element to set up.
93    * @param {string} columnType A tag used to identify the column when
94    *     changing focus.
95    * @param {string} eventType The type of event to listen to.
96    * @param {function(Event)} handler The function that should be called
97    *     by the event.
98    * @private
99    */
100   setupColumn: function(query, columnType, eventType, handler) {
101     var element = this.querySelector(query);
102     element.addEventListener(eventType, handler);
103     element.setAttribute('column-type', columnType);
104   },
106   /**
107    * @param {Element} element
108    * @return {boolean}
109    * @private
110    */
111   canAddElement_: function(element) {
112     if (!element || element.disabled)
113       return false;
115     var developerMode = $('extension-settings').classList.contains('dev-mode');
116     if (this.isDeveloperOption_(element) && !developerMode)
117       return false;
119     for (var el = element; el; el = el.parentElement) {
120       if (el.hidden)
121         return false;
122     }
124     return true;
125   },
127   /**
128    * Returns true if the element should only be shown in developer mode.
129    * @param {Element} element
130    * @return {boolean}
131    * @private
132    */
133   isDeveloperOption_: function(element) {
134     return /^dev-/.test(element.getAttribute('column-type'));
135   },
138 cr.define('extensions', function() {
139   'use strict';
141   /**
142    * Creates a new list of extensions.
143    * @constructor
144    * @extends {HTMLDivElement}
145    */
146   function ExtensionList() {
147     var div = document.createElement('div');
148     div.__proto__ = ExtensionList.prototype;
149     /** @private {!Array<ExtensionInfo>} */
150     div.extensions_ = [];
151     return div;
152   }
154   /**
155    * @type {Object<string, number>} A map from extension id to last reloaded
156    *     timestamp. The timestamp is recorded when the user click the 'Reload'
157    *     link. It is used to refresh the icon of an unpacked extension.
158    *     This persists between calls to decorate.
159    */
160   var extensionReloadedTimestamp = {};
162   ExtensionList.prototype = {
163     __proto__: HTMLDivElement.prototype,
165     /**
166      * Indicates whether an embedded options page that was navigated to through
167      * the '?options=' URL query has been shown to the user. This is necessary
168      * to prevent showExtensionNodes_ from opening the options more than once.
169      * @type {boolean}
170      * @private
171      */
172     optionsShown_: false,
174     /** @private {!cr.ui.FocusGrid} */
175     focusGrid_: new cr.ui.FocusGrid(),
177     /**
178      * Indicates whether an uninstall dialog is being shown to prevent multiple
179      * dialogs from being displayed.
180      * @private {boolean}
181      */
182     uninstallIsShowing_: false,
184     /**
185      * Indicates whether a permissions prompt is showing.
186      * @private {boolean}
187      */
188     permissionsPromptIsShowing_: false,
190     /**
191      * Necessary to only show the butterbar once.
192      * @private {boolean}
193      */
194     butterbarShown_: false,
196     /**
197      * Whether or not incognito mode is available.
198      * @private {boolean}
199      */
200     incognitoAvailable_: false,
202     /**
203      * Whether or not the app info dialog is enabled.
204      * @private {boolean}
205      */
206     enableAppInfoDialog_: false,
208     /**
209      * Updates the extensions on the page.
210      * @param {boolean} incognitoAvailable Whether or not incognito is allowed.
211      * @param {boolean} enableAppInfoDialog Whether or not the app info dialog
212      *     is enabled.
213      * @return {Promise} A promise that is resolved once the extensions data is
214      *     fully updated.
215      */
216     updateExtensionsData: function(incognitoAvailable, enableAppInfoDialog) {
217       // If we start to need more information about the extension configuration,
218       // consider passing in the full object from the ExtensionSettings.
219       this.incognitoAvailable_ = incognitoAvailable;
220       this.enableAppInfoDialog_ = enableAppInfoDialog;
221       return new Promise(function(resolve, reject) {
222         chrome.developerPrivate.getExtensionsInfo(
223             {includeDisabled: true, includeTerminated: true},
224             function(extensions) {
225           // Sort in order of unpacked vs. packed, followed by name, followed by
226           // id.
227           extensions.sort(function(a, b) {
228             function compare(x, y) {
229               return x < y ? -1 : (x > y ? 1 : 0);
230             }
231             function compareLocation(x, y) {
232               return x.location == chrome.developerPrivate.Location.UNPACKED ?
233                   -1 : (x.location == y.location ? 0 : 1);
234             }
235             return compareLocation(a, b) ||
236                    compare(a.name.toLowerCase(), b.name.toLowerCase()) ||
237                    compare(a.id, b.id);
238           });
239           this.extensions_ = extensions;
240           this.showExtensionNodes_();
241           resolve();
242         }.bind(this));
243       }.bind(this));
244     },
246     /** @return {number} The number of extensions being displayed. */
247     getNumExtensions: function() {
248       return this.extensions_.length;
249     },
251     getIdQueryParam_: function() {
252       return parseQueryParams(document.location)['id'];
253     },
255     getOptionsQueryParam_: function() {
256       return parseQueryParams(document.location)['options'];
257     },
259     /**
260      * Creates or updates all extension items from scratch.
261      * @private
262      */
263     showExtensionNodes_: function() {
264       // Remove the rows from |focusGrid_| without destroying them.
265       this.focusGrid_.rows.length = 0;
267       // Any node that is not updated will be removed.
268       var seenIds = [];
270       // Iterate over the extension data and add each item to the list.
271       this.extensions_.forEach(function(extension, i) {
272         var nextExt = this.extensions_[i + 1];
273         var node = $(extension.id);
274         seenIds.push(extension.id);
276         if (node)
277           this.updateNode_(extension, node);
278         else
279           this.createNode_(extension, nextExt ? $(nextExt.id) : null);
280       }, this);
282       // Remove extensions that are no longer installed.
283       var nodes = document.querySelectorAll('.extension-list-item-wrapper[id]');
284       for (var i = 0; i < nodes.length; ++i) {
285         var node = nodes[i];
286         if (seenIds.indexOf(node.id) < 0) {
287           if (node.contains(document.activeElement)) {
288             var focusableNode = nodes[i + 1] || nodes[i - 1];
289             if (focusableNode) {
290               focusableNode.getEquivalentElement(
291                   document.activeElement).focus();
292             }
293           }
295           node.parentElement.removeChild(node);
296           // Unregister the removed node from events.
297           assertInstanceof(node, ExtensionFocusRow).destroy();
298         }
299       }
301       var idToHighlight = this.getIdQueryParam_();
302       if (idToHighlight && $(idToHighlight))
303         this.scrollToNode_(idToHighlight);
305       var idToOpenOptions = this.getOptionsQueryParam_();
306       if (idToOpenOptions && $(idToOpenOptions))
307         this.showEmbeddedExtensionOptions_(idToOpenOptions, true);
308     },
310     /** Updates each row's focusable elements without rebuilding the grid. */
311     updateFocusableElements: function() {
312       var rows = document.querySelectorAll('.extension-list-item-wrapper[id]');
313       for (var i = 0; i < rows.length; ++i) {
314         assertInstanceof(rows[i], ExtensionFocusRow).updateFocusableElements();
315       }
316     },
318     /**
319      * Scrolls the page down to the extension node with the given id.
320      * @param {string} extensionId The id of the extension to scroll to.
321      * @private
322      */
323     scrollToNode_: function(extensionId) {
324       // Scroll offset should be calculated slightly higher than the actual
325       // offset of the element being scrolled to, so that it ends up not all
326       // the way at the top. That way it is clear that there are more elements
327       // above the element being scrolled to.
328       var scrollFudge = 1.2;
329       var scrollTop = $(extensionId).offsetTop - scrollFudge *
330           $(extensionId).clientHeight;
331       setScrollTopForDocument(document, scrollTop);
332     },
334     /**
335      * Synthesizes and initializes an HTML element for the extension metadata
336      * given in |extension|.
337      * @param {!ExtensionInfo} extension A dictionary of extension metadata.
338      * @param {?Element} nextNode |node| should be inserted before |nextNode|.
339      *     |node| will be appended to the end if |nextNode| is null.
340      * @private
341      */
342     createNode_: function(extension, nextNode) {
343       var template = $('template-collection').querySelector(
344           '.extension-list-item-wrapper');
345       var node = template.cloneNode(true);
346       ExtensionFocusRow.decorate(node, $('extension-settings-list'));
348       var row = assertInstanceof(node, ExtensionFocusRow);
349       row.id = extension.id;
351       // The 'Show Browser Action' button.
352       row.setupColumn('.show-button', 'showButton', 'click', function(e) {
353         chrome.developerPrivate.updateExtensionConfiguration({
354           extensionId: extension.id,
355           showActionButton: true
356         });
357       });
359       // The 'allow in incognito' checkbox.
360       row.setupColumn('.incognito-control input', 'incognito', 'change',
361                       function(e) {
362         var butterBar = row.querySelector('.butter-bar');
363         var checked = e.target.checked;
364         if (!this.butterbarShown_) {
365           butterBar.hidden = !checked ||
366               extension.type ==
367                   chrome.developerPrivate.ExtensionType.HOSTED_APP;
368           this.butterbarShown_ = !butterBar.hidden;
369         } else {
370           butterBar.hidden = true;
371         }
372         chrome.developerPrivate.updateExtensionConfiguration({
373           extensionId: extension.id,
374           incognitoAccess: e.target.checked
375         });
376       }.bind(this));
378       // The 'collect errors' checkbox. This should only be visible if the
379       // error console is enabled - we can detect this by the existence of the
380       // |errorCollectionEnabled| property.
381       row.setupColumn('.error-collection-control input', 'dev-collectErrors',
382                       'change', function(e) {
383         chrome.developerPrivate.updateExtensionConfiguration({
384           extensionId: extension.id,
385           errorCollection: e.target.checked
386         });
387       });
389       // The 'allow on all urls' checkbox. This should only be visible if
390       // active script restrictions are enabled. If they are not enabled, no
391       // extensions should want all urls.
392       row.setupColumn('.all-urls-control input', 'allUrls', 'click',
393                       function(e) {
394         chrome.developerPrivate.updateExtensionConfiguration({
395           extensionId: extension.id,
396           runOnAllUrls: e.target.checked
397         });
398       });
400       // The 'allow file:// access' checkbox.
401       row.setupColumn('.file-access-control input', 'localUrls', 'click',
402                       function(e) {
403         chrome.developerPrivate.updateExtensionConfiguration({
404           extensionId: extension.id,
405           fileAccess: e.target.checked
406         });
407       });
409       // The 'Options' button or link, depending on its behaviour.
410       // Set an href to get the correct mouse-over appearance (link,
411       // footer) - but the actual link opening is done through chrome.send
412       // with a preventDefault().
413       row.querySelector('.options-link').href =
414           extension.optionsPage ? extension.optionsPage.url : '';
415       row.setupColumn('.options-link', 'options', 'click', function(e) {
416         chrome.send('extensionSettingsOptions', [extension.id]);
417         e.preventDefault();
418       });
420       row.setupColumn('.options-button', 'options', 'click', function(e) {
421         this.showEmbeddedExtensionOptions_(extension.id, false);
422         e.preventDefault();
423       }.bind(this));
425       // The 'View in Web Store/View Web Site' link.
426       row.querySelector('.site-link').setAttribute('column-type', 'website');
428       // The 'Permissions' link.
429       row.setupColumn('.permissions-link', 'details', 'click', function(e) {
430         if (!this.permissionsPromptIsShowing_) {
431           chrome.developerPrivate.showPermissionsDialog(extension.id,
432                                                         function() {
433             this.permissionsPromptIsShowing_ = false;
434           }.bind(this));
435           this.permissionsPromptIsShowing_ = true;
436         }
437         e.preventDefault();
438       });
440       // The 'Reload' link.
441       row.setupColumn('.reload-link', 'localReload', 'click', function(e) {
442         chrome.developerPrivate.reload(extension.id, {failQuietly: true});
443         extensionReloadedTimestamp[extension.id] = Date.now();
444       });
446       // The 'Launch' link.
447       row.setupColumn('.launch-link', 'launch', 'click', function(e) {
448         chrome.send('extensionSettingsLaunch', [extension.id]);
449       });
451       // The 'Reload' terminated link.
452       row.setupColumn('.terminated-reload-link', 'terminatedReload', 'click',
453                       function(e) {
454         chrome.developerPrivate.reload(extension.id, {failQuietly: true});
455       });
457       // The 'Repair' corrupted link.
458       row.setupColumn('.corrupted-repair-button', 'repair', 'click',
459                       function(e) {
460         chrome.send('extensionSettingsRepair', [extension.id]);
461       });
463       // The 'Enabled' checkbox.
464       row.setupColumn('.enable-checkbox input', 'enabled', 'change',
465                       function(e) {
466         var checked = e.target.checked;
467         // TODO(devlin): What should we do if this fails?
468         chrome.management.setEnabled(extension.id, checked);
470         // This may seem counter-intuitive (to not set/clear the checkmark)
471         // but this page will be updated asynchronously if the extension
472         // becomes enabled/disabled. It also might not become enabled or
473         // disabled, because the user might e.g. get prompted when enabling
474         // and choose not to.
475         e.preventDefault();
476       });
478       // 'Remove' button.
479       var trashTemplate = $('template-collection').querySelector('.trash');
480       var trash = trashTemplate.cloneNode(true);
481       trash.title = loadTimeData.getString('extensionUninstall');
482       trash.hidden = !extension.userMayModify;
483       trash.setAttribute('column-type', 'trash');
484       trash.addEventListener('click', function(e) {
485         trash.classList.add('open');
486         trash.classList.toggle('mouse-clicked', e.detail > 0);
487         if (this.uninstallIsShowing_)
488           return;
489         this.uninstallIsShowing_ = true;
490         chrome.management.uninstall(extension.id,
491                                     {showConfirmDialog: true},
492                                     function() {
493           // TODO(devlin): What should we do if the uninstall fails?
494           this.uninstallIsShowing_ = false;
496           if (trash.classList.contains('mouse-clicked'))
497             trash.blur();
499           if (chrome.runtime.lastError) {
500             // The uninstall failed (e.g. a cancel). Allow the trash to close.
501             trash.classList.remove('open');
502           } else {
503             // Leave the trash open if the uninstall succeded. Otherwise it can
504             // half-close right before it's removed when the DOM is modified.
505           }
506         }.bind(this));
507       }.bind(this));
508       row.querySelector('.enable-controls').appendChild(trash);
510       // Developer mode ////////////////////////////////////////////////////////
512       // The path, if provided by unpacked extension.
513       row.setupColumn('.load-path a:first-of-type', 'dev-loadPath', 'click',
514                       function(e) {
515         chrome.send('extensionSettingsShowPath', [String(extension.id)]);
516         e.preventDefault();
517       });
519       // Maintain the order that nodes should be in when creating as well as
520       // when adding only one new row.
521       this.insertBefore(row, nextNode);
522       this.updateNode_(extension, row);
523     },
525     /**
526      * Updates an HTML element for the extension metadata given in |extension|.
527      * @param {!ExtensionInfo} extension A dictionary of extension metadata.
528      * @param {!ExtensionFocusRow} row The node that is being updated.
529      * @private
530      */
531     updateNode_: function(extension, row) {
532       var isActive =
533           extension.state == chrome.developerPrivate.ExtensionState.ENABLED;
534       row.classList.toggle('inactive-extension', !isActive);
536       // Hack to keep the closure compiler happy about |remove|.
537       // TODO(hcarmona): Remove this hack when the closure compiler is updated.
538       var node = /** @type {Element} */ (row);
539       node.classList.remove('policy-controlled', 'may-not-modify',
540                             'may-not-remove');
541       var classes = [];
542       if (!extension.userMayModify) {
543         classes.push('policy-controlled', 'may-not-modify');
544       } else if (extension.dependentExtensions.length > 0) {
545         classes.push('may-not-remove', 'may-not-modify');
546       } else if (extension.mustRemainInstalled) {
547         classes.push('may-not-remove');
548       } else if (extension.disableReasons.suspiciousInstall ||
549                  extension.disableReasons.corruptInstall ||
550                  extension.disableReasons.updateRequired) {
551         classes.push('may-not-modify');
552       }
553       row.classList.add.apply(row.classList, classes);
555       row.classList.toggle('extension-highlight',
556                            row.id == this.getIdQueryParam_());
558       var item = row.querySelector('.extension-list-item');
559       // Prevent the image cache of extension icon by using the reloaded
560       // timestamp as a query string. The timestamp is recorded when the user
561       // clicks the 'Reload' link. http://crbug.com/159302.
562       if (extensionReloadedTimestamp[extension.id]) {
563         item.style.backgroundImage =
564             'url(' + extension.iconUrl + '?' +
565             extensionReloadedTimestamp[extension.id] + ')';
566       } else {
567         item.style.backgroundImage = 'url(' + extension.iconUrl + ')';
568       }
570       this.setText_(row, '.extension-title', extension.name);
571       this.setText_(row, '.extension-version', extension.version);
572       this.setText_(row, '.location-text', extension.locationText || '');
573       this.setText_(row, '.blacklist-text', extension.blacklistText || '');
574       this.setText_(row, '.extension-description', extension.description);
576       // The 'Show Browser Action' button.
577       this.updateVisibility_(row, '.show-button',
578                              isActive && extension.actionButtonHidden);
580       // The 'allow in incognito' checkbox.
581       this.updateVisibility_(row, '.incognito-control',
582                              isActive && this.incognitoAvailable_,
583                              function(item) {
584         var incognito = item.querySelector('input');
585         incognito.disabled = !extension.incognitoAccess.isEnabled;
586         incognito.checked = extension.incognitoAccess.isActive;
587       });
589       // Hide butterBar if incognito is not enabled for the extension.
590       var butterBar = row.querySelector('.butter-bar');
591       butterBar.hidden =
592           butterBar.hidden || !extension.incognitoAccess.isEnabled;
594       // The 'collect errors' checkbox. This should only be visible if the
595       // error console is enabled - we can detect this by the existence of the
596       // |errorCollectionEnabled| property.
597       this.updateVisibility_(
598           row, '.error-collection-control',
599           isActive && extension.errorCollection.isEnabled,
600           function(item) {
601         item.querySelector('input').checked =
602             extension.errorCollection.isActive;
603       });
605       // The 'allow on all urls' checkbox. This should only be visible if
606       // active script restrictions are enabled. If they are not enabled, no
607       // extensions should want all urls.
608       this.updateVisibility_(
609           row, '.all-urls-control',
610           isActive && extension.runOnAllUrls.isEnabled,
611           function(item) {
612         item.querySelector('input').checked = extension.runOnAllUrls.isActive;
613       });
615       // The 'allow file:// access' checkbox.
616       this.updateVisibility_(row, '.file-access-control',
617                              isActive && extension.fileAccess.isEnabled,
618                              function(item) {
619         item.querySelector('input').checked = extension.fileAccess.isActive;
620       });
622       // The 'Options' button or link, depending on its behaviour.
623       var optionsEnabled = isActive && !!extension.optionsPage;
624       this.updateVisibility_(row, '.options-link', optionsEnabled &&
625                              extension.optionsPage.openInTab);
626       this.updateVisibility_(row, '.options-button', optionsEnabled &&
627                              !extension.optionsPage.openInTab);
629       // The 'View in Web Store/View Web Site' link.
630       var siteLinkEnabled = !!extension.homepageUrl &&
631                             !this.enableAppInfoDialog_;
632       this.updateVisibility_(row, '.site-link', siteLinkEnabled,
633                              function(item) {
634         item.href = extension.homepageUrl;
635         item.textContent = loadTimeData.getString(
636             extension.homepageProvided ? 'extensionSettingsVisitWebsite' :
637                                          'extensionSettingsVisitWebStore');
638       });
640       var isUnpacked =
641           extension.location == chrome.developerPrivate.Location.UNPACKED;
642       // The 'Reload' link.
643       this.updateVisibility_(row, '.reload-link', isUnpacked);
645       // The 'Launch' link.
646       this.updateVisibility_(
647           row, '.launch-link',
648           isUnpacked && extension.type ==
649                             chrome.developerPrivate.ExtensionType.PLATFORM_APP);
651       // The 'Reload' terminated link.
652       var isTerminated =
653           extension.state == chrome.developerPrivate.ExtensionState.TERMINATED;
654       this.updateVisibility_(row, '.terminated-reload-link', isTerminated);
656       // The 'Repair' corrupted link.
657       var canRepair = !isTerminated &&
658                       extension.disableReasons.corruptInstall &&
659                       extension.location ==
660                           chrome.developerPrivate.Location.FROM_STORE;
661       this.updateVisibility_(row, '.corrupted-repair-button', canRepair);
663       // The 'Enabled' checkbox.
664       var isOK = !isTerminated && !canRepair;
665       this.updateVisibility_(row, '.enable-checkbox', isOK, function(item) {
666         var enableCheckboxDisabled =
667             !extension.userMayModify ||
668             extension.disableReasons.suspiciousInstall ||
669             extension.disableReasons.corruptInstall ||
670             extension.disableReasons.updateRequired ||
671             extension.installedByCustodian ||
672             extension.dependentExtensions.length > 0;
673         item.querySelector('input').disabled = enableCheckboxDisabled;
674         item.querySelector('input').checked = isActive;
675       });
677       // Button for extensions controlled by policy.
678       var controlNode = row.querySelector('.enable-controls');
679       var indicator =
680           controlNode.querySelector('.controlled-extension-indicator');
681       var needsIndicator = isOK &&
682                            !extension.userMayModify &&
683                            extension.policyText;
684       // TODO(treib): If userMayModify is false, but policyText is empty, that
685       // indicates this extension is controlled by something else than
686       // enterprise policy (such as the profile being supervised). For now, just
687       // don't show the indicator in this case. We should really handle this
688       // better though (ie use a different text and icon).
690       if (needsIndicator && !indicator) {
691         indicator = new cr.ui.ControlledIndicator();
692         indicator.classList.add('controlled-extension-indicator');
693         indicator.setAttribute('controlled-by', 'policy');
694         var textPolicy = extension.policyText || '';
695         indicator.setAttribute('textpolicy', textPolicy);
696         indicator.image.setAttribute('aria-label', textPolicy);
697         controlNode.appendChild(indicator);
698         indicator.querySelector('div').setAttribute('column-type',
699                                                     'enterprise');
700       } else if (!needsIndicator && indicator) {
701         controlNode.removeChild(indicator);
702       }
704       // Developer mode ////////////////////////////////////////////////////////
706       // First we have the id.
707       var idLabel = row.querySelector('.extension-id');
708       idLabel.textContent = ' ' + extension.id;
710       // Then the path, if provided by unpacked extension.
711       this.updateVisibility_(row, '.load-path', isUnpacked,
712                              function(item) {
713         item.querySelector('a:first-of-type').textContent =
714             ' ' + extension.prettifiedPath;
715       });
717       // Then the 'managed, cannot uninstall/disable' message.
718       // We would like to hide managed installed message since this
719       // extension is disabled.
720       var isRequired =
721           !extension.userMayModify || extension.mustRemainInstalled;
722       this.updateVisibility_(row, '.managed-message', isRequired &&
723                              !extension.disableReasons.updateRequired);
725       // Then the 'This isn't from the webstore, looks suspicious' message.
726       this.updateVisibility_(row, '.suspicious-install-message', !isRequired &&
727                              extension.disableReasons.suspiciousInstall);
729       // Then the 'This is a corrupt extension' message.
730       this.updateVisibility_(row, '.corrupt-install-message', !isRequired &&
731                              extension.disableReasons.corruptInstall);
733       // Then the 'An update required by enterprise policy' message. Note that
734       // a force-installed extension might be disabled due to being outdated
735       // as well.
736       this.updateVisibility_(row, '.update-required-message',
737                              extension.disableReasons.updateRequired);
739       // The 'following extensions depend on this extension' list.
740       var hasDependents = extension.dependentExtensions.length > 0;
741       row.classList.toggle('developer-extras', hasDependents);
742       this.updateVisibility_(row, '.dependent-extensions-message',
743                              hasDependents, function(item) {
744         var dependentList = item.querySelector('ul');
745         dependentList.textContent = '';
746         var dependentTemplate = $('template-collection').querySelector(
747             '.dependent-list-item');
748         extension.dependentExtensions.forEach(function(dependentId) {
749           var dependentExtension = null;
750           for (var i = 0; i < this.extensions_.length; ++i) {
751             if (this.extensions_[i].id == dependentId) {
752               dependentExtension = this.extensions_[i];
753               break;
754             }
755           }
756           if (!dependentExtension)
757             return;
759           var depNode = dependentTemplate.cloneNode(true);
760           depNode.querySelector('.dep-extension-title').textContent =
761               dependentExtension.name;
762           depNode.querySelector('.dep-extension-id').textContent =
763               dependentExtension.id;
764           dependentList.appendChild(depNode);
765         }, this);
766       }.bind(this));
768       // The active views.
769       this.updateVisibility_(row, '.active-views', extension.views.length > 0,
770                              function(item) {
771         var link = item.querySelector('a');
773         // Link needs to be an only child before the list is updated.
774         while (link.nextElementSibling)
775           item.removeChild(link.nextElementSibling);
777         // Link needs to be cleaned up if it was used before.
778         link.textContent = '';
779         if (link.clickHandler)
780           link.removeEventListener('click', link.clickHandler);
782         extension.views.forEach(function(view, i) {
783           if (view.type == chrome.developerPrivate.ViewType.EXTENSION_DIALOG ||
784               view.type == chrome.developerPrivate.ViewType.EXTENSION_POPUP) {
785             return;
786           }
787           var displayName;
788           if (view.url.indexOf('chrome-extension://') == 0) {
789             var pathOffset = 'chrome-extension://'.length + 32 + 1;
790             displayName = view.url.substring(pathOffset);
791             if (displayName == '_generated_background_page.html')
792               displayName = loadTimeData.getString('backgroundPage');
793           } else {
794             displayName = view.url;
795           }
796           var label = displayName +
797               (view.incognito ?
798                   ' ' + loadTimeData.getString('viewIncognito') : '') +
799               (view.renderProcessId == -1 ?
800                   ' ' + loadTimeData.getString('viewInactive') : '');
801           link.textContent = label;
802           link.clickHandler = function(e) {
803             chrome.developerPrivate.openDevTools({
804               extensionId: extension.id,
805               renderProcessId: view.renderProcessId,
806               renderViewId: view.renderViewId,
807               incognito: view.incognito
808             });
809           };
810           link.addEventListener('click', link.clickHandler);
812           if (i < extension.views.length - 1) {
813             link = link.cloneNode(true);
814             item.appendChild(link);
815           }
816         });
818         var allLinks = item.querySelectorAll('a');
819         for (var i = 0; i < allLinks.length; ++i) {
820           allLinks[i].setAttribute('column-type', 'dev-activeViews' + i);
821         }
822       });
824       // The extension warnings (describing runtime issues).
825       this.updateVisibility_(row, '.extension-warnings',
826                              extension.runtimeWarnings.length > 0,
827                              function(item) {
828         var warningList = item.querySelector('ul');
829         warningList.textContent = '';
830         extension.runtimeWarnings.forEach(function(warning) {
831           var li = document.createElement('li');
832           warningList.appendChild(li).innerText = warning;
833         });
834       });
836       // If the ErrorConsole is enabled, we should have manifest and/or runtime
837       // errors. Otherwise, we may have install warnings. We should not have
838       // both ErrorConsole errors and install warnings.
839       // Errors.
840       this.updateErrors_(row.querySelector('.manifest-errors'),
841                          'dev-manifestErrors', extension.manifestErrors);
842       this.updateErrors_(row.querySelector('.runtime-errors'),
843                          'dev-runtimeErrors', extension.runtimeErrors);
845       // Install warnings.
846       this.updateVisibility_(row, '.install-warnings',
847                              extension.installWarnings.length > 0,
848                              function(item) {
849         var installWarningList = item.querySelector('ul');
850         installWarningList.textContent = '';
851         if (extension.installWarnings) {
852           extension.installWarnings.forEach(function(warning) {
853             var li = document.createElement('li');
854             li.innerText = warning;
855             installWarningList.appendChild(li);
856           });
857         }
858       });
860       if (location.hash.substr(1) == extension.id) {
861         // Scroll beneath the fixed header so that the extension is not
862         // obscured.
863         var topScroll = row.offsetTop - $('page-header').offsetHeight;
864         var pad = parseInt(window.getComputedStyle(row, null).marginTop, 10);
865         if (!isNaN(pad))
866           topScroll -= pad / 2;
867         setScrollTopForDocument(document, topScroll);
868       }
870       row.updateFocusableElements();
871       this.focusGrid_.addRow(row);
872     },
874     /**
875      * Updates an element's textContent.
876      * @param {Element} node Ancestor of the element specified by |query|.
877      * @param {string} query A query to select an element in |node|.
878      * @param {string} textContent
879      * @private
880      */
881     setText_: function(node, query, textContent) {
882       node.querySelector(query).textContent = textContent;
883     },
885     /**
886      * Updates an element's visibility and calls |shownCallback| if it is
887      * visible.
888      * @param {Element} node Ancestor of the element specified by |query|.
889      * @param {string} query A query to select an element in |node|.
890      * @param {boolean} visible Whether the element should be visible or not.
891      * @param {function(Element)=} opt_shownCallback Callback if the element is
892      *     visible. The element passed in will be the element specified by
893      *     |query|.
894      * @private
895      */
896     updateVisibility_: function(node, query, visible, opt_shownCallback) {
897       var item = assert(node.querySelector(query));
898       item.hidden = !visible;
899       if (visible && opt_shownCallback)
900         opt_shownCallback(item);
901     },
903     /**
904      * Updates an element to show a list of errors.
905      * @param {Element} panel An element to hold the errors.
906      * @param {string} columnType A tag used to identify the column when
907      *     changing focus.
908      * @param {Array<RuntimeError|ManifestError>|undefined} errors The errors
909      *     to be displayed.
910      * @private
911      */
912     updateErrors_: function(panel, columnType, errors) {
913       // TODO(hcarmona): Look into updating the ExtensionErrorList rather than
914       // rebuilding it every time.
915       panel.hidden = !errors || errors.length == 0;
916       panel.textContent = '';
918       if (panel.hidden)
919         return;
921       var errorList =
922           new extensions.ExtensionErrorList(assertInstanceof(errors, Array));
924       panel.appendChild(errorList);
926       var list = errorList.getErrorListElement();
927       if (list)
928         list.setAttribute('column-type', columnType + 'list');
930       var button = errorList.getToggleElement();
931       if (button)
932         button.setAttribute('column-type', columnType + 'button');
933     },
935     /**
936      * Opens the extension options overlay for the extension with the given id.
937      * @param {string} extensionId The id of extension whose options page should
938      *     be displayed.
939      * @param {boolean} scroll Whether the page should scroll to the extension
940      * @private
941      */
942     showEmbeddedExtensionOptions_: function(extensionId, scroll) {
943       if (this.optionsShown_)
944         return;
946       // Get the extension from the given id.
947       var extension = this.extensions_.filter(function(extension) {
948         return extension.state ==
949                    chrome.developerPrivate.ExtensionState.ENABLED &&
950                extension.id == extensionId;
951       })[0];
953       if (!extension)
954         return;
956       if (scroll)
957         this.scrollToNode_(extensionId);
959       // Add the options query string. Corner case: the 'options' query string
960       // will clobber the 'id' query string if the options link is clicked when
961       // 'id' is in the URL, or if both query strings are in the URL.
962       uber.replaceState({}, '?options=' + extensionId);
964       var overlay = extensions.ExtensionOptionsOverlay.getInstance();
965       var shownCallback = function() {
966         // This overlay doesn't get focused automatically as <extensionoptions>
967         // is created after the overlay is shown.
968         if (cr.ui.FocusOutlineManager.forDocument(document).visible)
969           overlay.setInitialFocus();
970       };
971       overlay.setExtensionAndShowOverlay(extensionId, extension.name,
972                                          extension.iconUrl, shownCallback);
973       this.optionsShown_ = true;
975       var self = this;
976       $('overlay').addEventListener('cancelOverlay', function f() {
977         self.optionsShown_ = false;
978         $('overlay').removeEventListener('cancelOverlay', f);
979       });
981       // TODO(dbeam): why do we need to focus <extensionoptions> before and
982       // after its showing animation? Makes very little sense to me.
983       overlay.setInitialFocus();
984     },
985   };
987   return {
988     ExtensionList: ExtensionList
989   };