ozone: evdev: Sync caps lock LED state to evdev
[chromium-blink-merge.git] / chrome / browser / resources / extensions / extension_list.js
blob16b11322a5281b1d57869ed58818efc5323d9662
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  * The type of the extension data object. The definition is based on
9  * chrome/browser/ui/webui/extensions/extension_basic_info.cc
10  * and
11  * chrome/browser/ui/webui/extensions/extension_settings_handler.cc
12  *     ExtensionSettingsHandler::CreateExtensionDetailValue()
13  * @typedef {{allow_reload: boolean,
14  *            allowAllUrls: boolean,
15  *            allowFileAccess: boolean,
16  *            blacklistText: string,
17  *            corruptInstall: boolean,
18  *            dependentExtensions: Array,
19  *            description: string,
20  *            detailsUrl: string,
21  *            enableExtensionInfoDialog: boolean,
22  *            enable_show_button: boolean,
23  *            enabled: boolean,
24  *            enabledIncognito: boolean,
25  *            errorCollectionEnabled: (boolean|undefined),
26  *            hasPopupAction: boolean,
27  *            homepageProvided: boolean,
28  *            homepageUrl: string,
29  *            icon: string,
30  *            id: string,
31  *            incognitoCanBeEnabled: boolean,
32  *            installedByCustodian: boolean,
33  *            installWarnings: (Array|undefined),
34  *            is_hosted_app: boolean,
35  *            is_platform_app: boolean,
36  *            isFromStore: boolean,
37  *            isUnpacked: boolean,
38  *            kioskEnabled: boolean,
39  *            kioskOnly: boolean,
40  *            locationText: string,
41  *            managedInstall: boolean,
42  *            manifestErrors: (Array<RuntimeError>|undefined),
43  *            name: string,
44  *            offlineEnabled: boolean,
45  *            optionsOpenInTab: boolean,
46  *            optionsPageHref: string,
47  *            optionsUrl: string,
48  *            order: number,
49  *            packagedApp: boolean,
50  *            path: (string|undefined),
51  *            policyText: (string|undefined),
52  *            prettifiedPath: (string|undefined),
53  *            recommendedInstall: boolean,
54  *            runtimeErrors: (Array<RuntimeError>|undefined),
55  *            showAllUrls: boolean,
56  *            suspiciousInstall: boolean,
57  *            terminated: boolean,
58  *            updateRequiredByPolicy: boolean,
59  *            version: string,
60  *            views: Array<{renderViewId: number, renderProcessId: number,
61  *                path: string, incognito: boolean,
62  *                generatedBackgroundPage: boolean}>,
63  *            wantsErrorCollection: boolean,
64  *            wantsFileAccess: boolean,
65  *            warnings: (Array|undefined)}}
66  */
67 var ExtensionData;
69 ///////////////////////////////////////////////////////////////////////////////
70 // ExtensionFocusRow:
72 /**
73  * Provides an implementation for a single column grid.
74  * @constructor
75  * @extends {cr.ui.FocusRow}
76  */
77 function ExtensionFocusRow() {}
79 /**
80  * Decorates |focusRow| so that it can be treated as a ExtensionFocusRow.
81  * @param {Element} focusRow The element that has all the columns.
82  * @param {Node} boundary Focus events are ignored outside of this node.
83  */
84 ExtensionFocusRow.decorate = function(focusRow, boundary) {
85   focusRow.__proto__ = ExtensionFocusRow.prototype;
86   focusRow.decorate(boundary);
89 ExtensionFocusRow.prototype = {
90   __proto__: cr.ui.FocusRow.prototype,
92   /** @override */
93   getEquivalentElement: function(element) {
94     if (this.focusableElements.indexOf(element) > -1)
95       return element;
97     // All elements default to another element with the same type.
98     var columnType = element.getAttribute('column-type');
99     var equivalent = this.querySelector('[column-type=' + columnType + ']');
101     if (!equivalent || !this.canAddElement_(equivalent)) {
102       var actionLinks = ['options', 'website', 'launch', 'localReload'];
103       var optionalControls = ['showButton', 'incognito', 'dev-collectErrors',
104                               'allUrls', 'localUrls'];
105       var removeStyleButtons = ['trash', 'enterprise'];
106       var enableControls = ['terminatedReload', 'repair', 'enabled'];
108       if (actionLinks.indexOf(columnType) > -1)
109         equivalent = this.getFirstFocusableByType_(actionLinks);
110       else if (optionalControls.indexOf(columnType) > -1)
111         equivalent = this.getFirstFocusableByType_(optionalControls);
112       else if (removeStyleButtons.indexOf(columnType) > -1)
113         equivalent = this.getFirstFocusableByType_(removeStyleButtons);
114       else if (enableControls.indexOf(columnType) > -1)
115         equivalent = this.getFirstFocusableByType_(enableControls);
116     }
118     // Return the first focusable element if no equivalent type is found.
119     return equivalent || this.focusableElements[0];
120   },
122   /** Updates the list of focusable elements. */
123   updateFocusableElements: function() {
124     this.focusableElements.length = 0;
126     var focusableCandidates = this.querySelectorAll('[column-type]');
127     for (var i = 0; i < focusableCandidates.length; ++i) {
128       var element = focusableCandidates[i];
129       if (this.canAddElement_(element))
130         this.addFocusableElement(element);
131     }
132   },
134   /**
135    * Get the first focusable element that matches a list of types.
136    * @param {Array<string>} types An array of types to match from.
137    * @return {?Element} Return the first element that matches a type in |types|.
138    * @private
139    */
140   getFirstFocusableByType_: function(types) {
141     for (var i = 0; i < this.focusableElements.length; ++i) {
142       var element = this.focusableElements[i];
143       if (types.indexOf(element.getAttribute('column-type')) > -1)
144         return element;
145     }
146     return null;
147   },
149   /**
150    * Setup a typical column in the ExtensionFocusRow. A column can be any
151    * element and should have an action when clicked/toggled. This function
152    * adds a listener and a handler for an event. Also adds the "column-type"
153    * attribute to make the element focusable in |updateFocusableElements|.
154    * @param {string} query A query to select the element to set up.
155    * @param {string} columnType A tag used to identify the column when
156    *     changing focus.
157    * @param {string} eventType The type of event to listen to.
158    * @param {function(Event)} handler The function that should be called
159    *     by the event.
160    * @private
161    */
162   setupColumn: function(query, columnType, eventType, handler) {
163     var element = this.querySelector(query);
164     element.addEventListener(eventType, handler);
165     element.setAttribute('column-type', columnType);
166   },
168   /**
169    * @param {Element} element
170    * @return {boolean}
171    * @private
172    */
173   canAddElement_: function(element) {
174     if (!element || element.disabled)
175       return false;
177     var developerMode = $('extension-settings').classList.contains('dev-mode');
178     if (this.isDeveloperOption_(element) && !developerMode)
179       return false;
181     for (var el = element; el; el = el.parentElement) {
182       if (el.hidden)
183         return false;
184     }
186     return true;
187   },
189   /**
190    * Returns true if the element should only be shown in developer mode.
191    * @param {Element} element
192    * @return {boolean}
193    * @private
194    */
195   isDeveloperOption_: function(element) {
196     return /^dev-/.test(element.getAttribute('column-type'));
197   },
200 cr.define('extensions', function() {
201   'use strict';
203   /**
204    * Creates a new list of extensions.
205    * @param {Object=} opt_propertyBag Optional properties.
206    * @constructor
207    * @extends {HTMLDivElement}
208    */
209   var ExtensionList = cr.ui.define('div');
211   /**
212    * @type {Object<string, number>} A map from extension id to last reloaded
213    *     timestamp. The timestamp is recorded when the user click the 'Reload'
214    *     link. It is used to refresh the icon of an unpacked extension.
215    *     This persists between calls to decorate.
216    */
217   var extensionReloadedTimestamp = {};
219   ExtensionList.prototype = {
220     __proto__: HTMLDivElement.prototype,
222     /**
223      * Indicates whether an embedded options page that was navigated to through
224      * the '?options=' URL query has been shown to the user. This is necessary
225      * to prevent showExtensionNodes_ from opening the options more than once.
226      * @type {boolean}
227      * @private
228      */
229     optionsShown_: false,
231     /** @private {!cr.ui.FocusGrid} */
232     focusGrid_: new cr.ui.FocusGrid(),
234     /**
235      * Indicates whether an uninstall dialog is being shown to prevent multiple
236      * dialogs from being displayed.
237      * @type {boolean}
238      * @private
239      */
240     uninstallIsShowing_: false,
242     /**
243      * Necessary to only show the butterbar once.
244      * @private {boolean}
245      */
246     butterbarShown_: false,
248     decorate: function() {
249       this.showExtensionNodes_();
250     },
252     getIdQueryParam_: function() {
253       return parseQueryParams(document.location)['id'];
254     },
256     getOptionsQueryParam_: function() {
257       return parseQueryParams(document.location)['options'];
258     },
260     /**
261      * Creates or updates all extension items from scratch.
262      * @private
263      */
264     showExtensionNodes_: function() {
265       // Remove the rows from |focusGrid_| without destroying them.
266       this.focusGrid_.rows.length = 0;
268       // Any node that is not updated will be removed.
269       var seenIds = [];
271       // Iterate over the extension data and add each item to the list.
272       this.data_.extensions.forEach(function(extension, i) {
273         var nextExt = this.data_.extensions[i + 1];
274         var node = $(extension.id);
275         seenIds.push(extension.id);
277         if (node)
278           this.updateNode_(extension, node);
279         else
280           this.createNode_(extension, nextExt ? $(nextExt.id) : null);
281       }, this);
283       // Remove extensions that are no longer installed.
284       var nodes = document.querySelectorAll('.extension-list-item-wrapper[id]');
285       for (var i = 0; i < nodes.length; ++i) {
286         var node = nodes[i];
287         if (seenIds.indexOf(node.id) < 0) {
288           node.parentElement.removeChild(node);
289           // Unregister the removed node from events.
290           assertInstanceof(node, ExtensionFocusRow).destroy();
291         }
292       }
294       var idToHighlight = this.getIdQueryParam_();
295       if (idToHighlight && $(idToHighlight))
296         this.scrollToNode_(idToHighlight);
298       var idToOpenOptions = this.getOptionsQueryParam_();
299       if (idToOpenOptions && $(idToOpenOptions))
300         this.showEmbeddedExtensionOptions_(idToOpenOptions, true);
302       var noExtensions = this.data_.extensions.length == 0;
303       this.classList.toggle('empty-extension-list', noExtensions);
304     },
306     /** Updates each row's focusable elements without rebuilding the grid. */
307     updateFocusableElements: function() {
308       var rows = document.querySelectorAll('.extension-list-item-wrapper[id]');
309       for (var i = 0; i < rows.length; ++i) {
310         assertInstanceof(rows[i], ExtensionFocusRow).updateFocusableElements();
311       }
312     },
314     /**
315      * Scrolls the page down to the extension node with the given id.
316      * @param {string} extensionId The id of the extension to scroll to.
317      * @private
318      */
319     scrollToNode_: function(extensionId) {
320       // Scroll offset should be calculated slightly higher than the actual
321       // offset of the element being scrolled to, so that it ends up not all
322       // the way at the top. That way it is clear that there are more elements
323       // above the element being scrolled to.
324       var scrollFudge = 1.2;
325       var scrollTop = $(extensionId).offsetTop - scrollFudge *
326           $(extensionId).clientHeight;
327       setScrollTopForDocument(document, scrollTop);
328     },
330     /**
331      * Synthesizes and initializes an HTML element for the extension metadata
332      * given in |extension|.
333      * @param {!ExtensionData} extension A dictionary of extension metadata.
334      * @param {?Element} nextNode |node| should be inserted before |nextNode|.
335      *     |node| will be appended to the end if |nextNode| is null.
336      * @private
337      */
338     createNode_: function(extension, nextNode) {
339       var template = $('template-collection').querySelector(
340           '.extension-list-item-wrapper');
341       var node = template.cloneNode(true);
342       ExtensionFocusRow.decorate(node, $('extension-settings-list'));
344       var row = assertInstanceof(node, ExtensionFocusRow);
345       row.id = extension.id;
347       // The 'Show Browser Action' button.
348       row.setupColumn('.show-button', 'showButton', 'click', function(e) {
349         chrome.send('extensionSettingsShowButton', [extension.id]);
350       });
352       // The 'allow in incognito' checkbox.
353       row.setupColumn('.incognito-control input', 'incognito', 'change',
354                       function(e) {
355         var butterBar = row.querySelector('.butter-bar');
356         var checked = e.target.checked;
357         if (!this.butterbarShown_) {
358           butterBar.hidden = !checked || extension.is_hosted_app;
359           this.butterbarShown_ = !butterBar.hidden;
360         } else {
361           butterBar.hidden = true;
362         }
363         chrome.developerPrivate.allowIncognito(extension.id, checked);
364       }.bind(this));
366       // The 'collect errors' checkbox. This should only be visible if the
367       // error console is enabled - we can detect this by the existence of the
368       // |errorCollectionEnabled| property.
369       row.setupColumn('.error-collection-control input', 'dev-collectErrors',
370                       'change', function(e) {
371         chrome.send('extensionSettingsEnableErrorCollection',
372                     [extension.id, String(e.target.checked)]);
373       });
375       // The 'allow on all urls' checkbox. This should only be visible if
376       // active script restrictions are enabled. If they are not enabled, no
377       // extensions should want all urls.
378       row.setupColumn('.all-urls-control input', 'allUrls', 'click',
379                       function(e) {
380         chrome.send('extensionSettingsAllowOnAllUrls',
381                     [extension.id, String(e.target.checked)]);
382       });
384       // The 'allow file:// access' checkbox.
385       row.setupColumn('.file-access-control input', 'localUrls', 'click',
386                       function(e) {
387         chrome.developerPrivate.allowFileAccess(extension.id, e.target.checked);
388       });
390       // The 'Options' button or link, depending on its behaviour.
391       // Set an href to get the correct mouse-over appearance (link,
392       // footer) - but the actual link opening is done through chrome.send
393       // with a preventDefault().
394       row.querySelector('.options-link').href = extension.optionsPageHref;
395       row.setupColumn('.options-link', 'options', 'click', function(e) {
396         chrome.send('extensionSettingsOptions', [extension.id]);
397         e.preventDefault();
398       });
400       row.setupColumn('.options-button', 'options', 'click', function(e) {
401         this.showEmbeddedExtensionOptions_(extension.id, false);
402         e.preventDefault();
403       }.bind(this));
405       // The 'View in Web Store/View Web Site' link.
406       row.querySelector('.site-link').setAttribute('column-type', 'website');
408       // The 'Permissions' link.
409       row.setupColumn('.permissions-link', 'details', 'click', function(e) {
410         chrome.send('extensionSettingsPermissions', [extension.id]);
411         e.preventDefault();
412       });
414       // The 'Reload' link.
415       row.setupColumn('.reload-link', 'localReload', 'click', function(e) {
416         chrome.developerPrivate.reload(extension.id, {failQuietly: true});
417         extensionReloadedTimestamp[extension.id] = Date.now();
418       });
420       // The 'Launch' link.
421       row.setupColumn('.launch-link', 'launch', 'click', function(e) {
422         chrome.send('extensionSettingsLaunch', [extension.id]);
423       });
425       // The 'Reload' terminated link.
426       row.setupColumn('.terminated-reload-link', 'terminatedReload', 'click',
427                       function(e) {
428         chrome.send('extensionSettingsReload', [extension.id]);
429       });
431       // The 'Repair' corrupted link.
432       row.setupColumn('.corrupted-repair-button', 'repair', 'click',
433                       function(e) {
434         chrome.send('extensionSettingsRepair', [extension.id]);
435       });
437       // The 'Enabled' checkbox.
438       row.setupColumn('.enable-checkbox input', 'enabled', 'change',
439                       function(e) {
440         var checked = e.target.checked;
441         // TODO(devlin): What should we do if this fails?
442         chrome.management.setEnabled(extension.id, checked);
444         // This may seem counter-intuitive (to not set/clear the checkmark)
445         // but this page will be updated asynchronously if the extension
446         // becomes enabled/disabled. It also might not become enabled or
447         // disabled, because the user might e.g. get prompted when enabling
448         // and choose not to.
449         e.preventDefault();
450       });
452       // 'Remove' button.
453       var trashTemplate = $('template-collection').querySelector('.trash');
454       var trash = trashTemplate.cloneNode(true);
455       trash.title = loadTimeData.getString('extensionUninstall');
456       trash.hidden = extension.managedInstall;
457       trash.setAttribute('column-type', 'trash');
458       trash.addEventListener('click', function(e) {
459         trash.classList.add('open');
460         trash.classList.toggle('mouse-clicked', e.detail > 0);
461         if (this.uninstallIsShowing_)
462           return;
463         this.uninstallIsShowing_ = true;
464         chrome.management.uninstall(extension.id,
465                                     {showConfirmDialog: true},
466                                     function() {
467           // TODO(devlin): What should we do if the uninstall fails?
468           this.uninstallIsShowing_ = false;
470           if (trash.classList.contains('mouse-clicked'))
471             trash.blur();
473           if (chrome.runtime.lastError) {
474             // The uninstall failed (e.g. a cancel). Allow the trash to close.
475             trash.classList.remove('open');
476           } else {
477             // Leave the trash open if the uninstall succeded. Otherwise it can
478             // half-close right before it's removed when the DOM is modified.
479           }
480         }.bind(this));
481       }.bind(this));
482       row.querySelector('.enable-controls').appendChild(trash);
484       // Developer mode ////////////////////////////////////////////////////////
486       // The path, if provided by unpacked extension.
487       row.setupColumn('.load-path a:first-of-type', 'dev-loadPath', 'click',
488                       function(e) {
489         chrome.send('extensionSettingsShowPath', [String(extension.id)]);
490         e.preventDefault();
491       });
493       // Maintain the order that nodes should be in when creating as well as
494       // when adding only one new row.
495       this.insertBefore(row, nextNode);
496       this.updateNode_(extension, row);
497     },
499     /**
500      * Updates an HTML element for the extension metadata given in |extension|.
501      * @param {!ExtensionData} extension A dictionary of extension metadata.
502      * @param {!ExtensionFocusRow} row The node that is being updated.
503      * @private
504      */
505     updateNode_: function(extension, row) {
506       var isActive = extension.enabled && !extension.terminated;
508       row.classList.toggle('inactive-extension', !isActive);
510       // Hack to keep the closure compiler happy about |remove|.
511       // TODO(hcarmona): Remove this hack when the closure compiler is updated.
512       var node = /** @type {Element} */ (row);
513       node.classList.remove('policy-controlled', 'may-not-modify',
514                             'may-not-remove');
515       var classes = [];
516       if (extension.managedInstall) {
517         classes.push('policy-controlled', 'may-not-modify');
518       } else if (extension.dependentExtensions.length > 0) {
519         classes.push('may-not-remove', 'may-not-modify');
520       } else if (extension.recommendedInstall) {
521         classes.push('may-not-remove');
522       } else if (extension.suspiciousInstall ||
523                  extension.corruptInstall ||
524                  extension.updateRequiredByPolicy) {
525         classes.push('may-not-modify');
526       }
527       row.classList.add.apply(row.classList, classes);
529       row.classList.toggle('extension-highlight',
530                            row.id == this.getIdQueryParam_());
532       var item = row.querySelector('.extension-list-item');
533       // Prevent the image cache of extension icon by using the reloaded
534       // timestamp as a query string. The timestamp is recorded when the user
535       // clicks the 'Reload' link. http://crbug.com/159302.
536       if (extensionReloadedTimestamp[extension.id]) {
537         item.style.backgroundImage =
538             'url(' + extension.icon + '?' +
539             extensionReloadedTimestamp[extension.id] + ')';
540       } else {
541         item.style.backgroundImage = 'url(' + extension.icon + ')';
542       }
544       this.setText_(row, '.extension-title', extension.name);
545       this.setText_(row, '.extension-version', extension.version);
546       this.setText_(row, '.location-text', extension.locationText);
547       this.setText_(row, '.blacklist-text', extension.blacklistText);
548       this.setText_(row, '.extension-description', extension.description);
550       // The 'Show Browser Action' button.
551       this.updateVisibility_(row, '.show-button',
552                              isActive && extension.enable_show_button);
554       // The 'allow in incognito' checkbox.
555       this.updateVisibility_(row, '.incognito-control',
556                              isActive && this.data_.incognitoAvailable,
557                              function(item) {
558         var incognito = item.querySelector('input');
559         incognito.disabled = !extension.incognitoCanBeEnabled;
560         incognito.checked = extension.enabledIncognito;
561       });
563       // Hide butterBar if incognito is not enabled for the extension.
564       var butterBar = row.querySelector('.butter-bar');
565       butterBar.hidden = butterBar.hidden || !extension.enabledIncognito;
567       // The 'collect errors' checkbox. This should only be visible if the
568       // error console is enabled - we can detect this by the existence of the
569       // |errorCollectionEnabled| property.
570       this.updateVisibility_(row, '.error-collection-control',
571                              isActive && extension.wantsErrorCollection,
572                              function(item) {
573         item.querySelector('input').checked = extension.errorCollectionEnabled;
574       });
576       // The 'allow on all urls' checkbox. This should only be visible if
577       // active script restrictions are enabled. If they are not enabled, no
578       // extensions should want all urls.
579       this.updateVisibility_(row, '.all-urls-control',
580                              isActive && extension.showAllUrls, function(item) {
581         item.querySelector('input').checked = extension.allowAllUrls;
582       });
584       // The 'allow file:// access' checkbox.
585       this.updateVisibility_(row, '.file-access-control',
586                              isActive && extension.wantsFileAccess,
587                              function(item) {
588         item.querySelector('input').checked = extension.allowFileAccess;
589       });
591       // The 'Options' button or link, depending on its behaviour.
592       var optionsEnabled = extension.enabled && !!extension.optionsUrl;
593       this.updateVisibility_(row, '.options-link', optionsEnabled &&
594                              extension.optionsOpenInTab);
595       this.updateVisibility_(row, '.options-button', optionsEnabled &&
596                              !extension.optionsOpenInTab);
598       // The 'View in Web Store/View Web Site' link.
599       var siteLinkEnabled = !!extension.homepageUrl &&
600                             !extension.enableExtensionInfoDialog;
601       this.updateVisibility_(row, '.site-link', siteLinkEnabled,
602                              function(item) {
603         item.href = extension.homepageUrl;
604         item.textContent = loadTimeData.getString(
605             extension.homepageProvided ? 'extensionSettingsVisitWebsite' :
606                                          'extensionSettingsVisitWebStore');
607       });
609       // The 'Reload' link.
610       this.updateVisibility_(row, '.reload-link', extension.allow_reload);
612       // The 'Launch' link.
613       this.updateVisibility_(row, '.launch-link', extension.allow_reload &&
614                              extension.is_platform_app);
616       // The 'Reload' terminated link.
617       var isTerminated = extension.terminated;
618       this.updateVisibility_(row, '.terminated-reload-link', isTerminated);
620       // The 'Repair' corrupted link.
621       var canRepair = !isTerminated && extension.corruptInstall &&
622                       extension.isFromStore;
623       this.updateVisibility_(row, '.corrupted-repair-button', canRepair);
625       // The 'Enabled' checkbox.
626       var isOK = !isTerminated && !canRepair;
627       this.updateVisibility_(row, '.enable-checkbox', isOK, function(item) {
628         var enableCheckboxDisabled = extension.managedInstall ||
629                                      extension.suspiciousInstall ||
630                                      extension.corruptInstall ||
631                                      extension.updateRequiredByPolicy ||
632                                      extension.installedByCustodian ||
633                                      extension.dependentExtensions.length > 0;
634         item.querySelector('input').disabled = enableCheckboxDisabled;
635         item.querySelector('input').checked = extension.enabled;
636       });
638       // Button for extensions controlled by policy.
639       var controlNode = row.querySelector('.enable-controls');
640       var indicator =
641           controlNode.querySelector('.controlled-extension-indicator');
642       var needsIndicator = isOK && extension.managedInstall;
644       if (needsIndicator && !indicator) {
645         indicator = new cr.ui.ControlledIndicator();
646         indicator.classList.add('controlled-extension-indicator');
647         indicator.setAttribute('controlled-by', 'policy');
648         var textPolicy = extension.policyText || '';
649         indicator.setAttribute('textpolicy', textPolicy);
650         indicator.image.setAttribute('aria-label', textPolicy);
651         controlNode.appendChild(indicator);
652         indicator.querySelector('div').setAttribute('column-type',
653                                                     'enterprise');
654       } else if (!needsIndicator && indicator) {
655         controlNode.removeChild(indicator);
656       }
658       // Developer mode ////////////////////////////////////////////////////////
660       // First we have the id.
661       var idLabel = row.querySelector('.extension-id');
662       idLabel.textContent = ' ' + extension.id;
664       // Then the path, if provided by unpacked extension.
665       this.updateVisibility_(row, '.load-path', extension.isUnpacked,
666                              function(item) {
667         item.querySelector('a:first-of-type').textContent =
668             ' ' + extension.prettifiedPath;
669       });
671       // Then the 'managed, cannot uninstall/disable' message.
672       // We would like to hide managed installed message since this
673       // extension is disabled.
674       var isRequired = extension.managedInstall || extension.recommendedInstall;
675       this.updateVisibility_(row, '.managed-message', isRequired &&
676                              !extension.updateRequiredByPolicy);
678       // Then the 'This isn't from the webstore, looks suspicious' message.
679       this.updateVisibility_(row, '.suspicious-install-message', !isRequired &&
680                              extension.suspiciousInstall);
682       // Then the 'This is a corrupt extension' message.
683       this.updateVisibility_(row, '.corrupt-install-message', !isRequired &&
684                              extension.corruptInstall);
686       // Then the 'An update required by enterprise policy' message. Note that
687       // a force-installed extension might be disabled due to being outdated
688       // as well.
689       this.updateVisibility_(row, '.update-required-message',
690                              extension.updateRequiredByPolicy);
692       // The 'following extensions depend on this extension' list.
693       var hasDependents = extension.dependentExtensions.length > 0;
694       row.classList.toggle('developer-extras', hasDependents);
695       this.updateVisibility_(row, '.dependent-extensions-message',
696                              hasDependents, function(item) {
697         var dependentList = item.querySelector('ul');
698         dependentList.textContent = '';
699         var dependentTemplate = $('template-collection').querySelector(
700             '.dependent-list-item');
701         extension.dependentExtensions.forEach(function(elem) {
702           var depNode = dependentTemplate.cloneNode(true);
703           depNode.querySelector('.dep-extension-title').textContent = elem.name;
704           depNode.querySelector('.dep-extension-id').textContent = elem.id;
705           dependentList.appendChild(depNode);
706         });
707       });
709       // The active views.
710       this.updateVisibility_(row, '.active-views', extension.views.length > 0,
711                              function(item) {
712         var link = item.querySelector('a');
714         // Link needs to be an only child before the list is updated.
715         while (link.nextElementSibling)
716           item.removeChild(link.nextElementSibling);
718         // Link needs to be cleaned up if it was used before.
719         link.textContent = '';
720         if (link.clickHandler)
721           link.removeEventListener('click', link.clickHandler);
723         extension.views.forEach(function(view, i) {
724           var displayName = view.generatedBackgroundPage ?
725               loadTimeData.getString('backgroundPage') : view.path;
726           var label = displayName +
727               (view.incognito ?
728                   ' ' + loadTimeData.getString('viewIncognito') : '') +
729               (view.renderProcessId == -1 ?
730                   ' ' + loadTimeData.getString('viewInactive') : '');
731           link.textContent = label;
732           link.clickHandler = function(e) {
733             // TODO(estade): remove conversion to string?
734             chrome.send('extensionSettingsInspect', [
735               String(extension.id),
736               String(view.renderProcessId),
737               String(view.renderViewId),
738               view.incognito
739             ]);
740           };
741           link.addEventListener('click', link.clickHandler);
743           if (i < extension.views.length - 1) {
744             link = link.cloneNode(true);
745             item.appendChild(link);
746           }
747         });
749         var allLinks = item.querySelectorAll('a');
750         for (var i = 0; i < allLinks.length; ++i) {
751           allLinks[i].setAttribute('column-type', 'dev-activeViews' + i);
752         }
753       });
755       // The extension warnings (describing runtime issues).
756       this.updateVisibility_(row, '.extension-warnings', !!extension.warnings,
757                              function(item) {
758         var warningList = item.querySelector('ul');
759         warningList.textContent = '';
760         extension.warnings.forEach(function(warning) {
761           var li = document.createElement('li');
762           warningList.appendChild(li).innerText = warning;
763         });
764       });
766       // If the ErrorConsole is enabled, we should have manifest and/or runtime
767       // errors. Otherwise, we may have install warnings. We should not have
768       // both ErrorConsole errors and install warnings.
769       // Errors.
770       this.updateErrors_(row.querySelector('.manifest-errors'),
771                          'dev-manifestErrors', extension.manifestErrors);
772       this.updateErrors_(row.querySelector('.runtime-errors'),
773                          'dev-runtimeErrors', extension.runtimeErrors);
775       // Install warnings.
776       this.updateVisibility_(row, '.install-warnings',
777                              !!extension.installWarnings, function(item) {
778         var installWarningList = item.querySelector('ul');
779         installWarningList.textContent = '';
780         if (extension.installWarnings) {
781           extension.installWarnings.forEach(function(warning) {
782             var li = document.createElement('li');
783             li.innerText = warning.message;
784             installWarningList.appendChild(li);
785           });
786         }
787       });
789       if (location.hash.substr(1) == extension.id) {
790         // Scroll beneath the fixed header so that the extension is not
791         // obscured.
792         var topScroll = row.offsetTop - $('page-header').offsetHeight;
793         var pad = parseInt(window.getComputedStyle(row, null).marginTop, 10);
794         if (!isNaN(pad))
795           topScroll -= pad / 2;
796         setScrollTopForDocument(document, topScroll);
797       }
799       row.updateFocusableElements();
800       this.focusGrid_.addRow(row);
801     },
803     /**
804      * Updates an element's textContent.
805      * @param {Element} node Ancestor of the element specified by |query|.
806      * @param {string} query A query to select an element in |node|.
807      * @param {string} textContent
808      * @private
809      */
810     setText_: function(node, query, textContent) {
811       node.querySelector(query).textContent = textContent;
812     },
814     /**
815      * Updates an element's visibility and calls |shownCallback| if it is
816      * visible.
817      * @param {Element} node Ancestor of the element specified by |query|.
818      * @param {string} query A query to select an element in |node|.
819      * @param {boolean} visible Whether the element should be visible or not.
820      * @param {function(Element)=} opt_shownCallback Callback if the element is
821      *     visible. The element passed in will be the element specified by
822      *     |query|.
823      * @private
824      */
825     updateVisibility_: function(node, query, visible, opt_shownCallback) {
826       var item = assert(node.querySelector(query));
827       item.hidden = !visible;
828       if (visible && opt_shownCallback)
829         opt_shownCallback(item);
830     },
832     /**
833      * Updates an element to show a list of errors.
834      * @param {Element} panel An element to hold the errors.
835      * @param {string} columnType A tag used to identify the column when
836      *     changing focus.
837      * @param {Array<RuntimeError>|undefined} errors The errors to be displayed.
838      * @private
839      */
840     updateErrors_: function(panel, columnType, errors) {
841       // TODO(hcarmona): Look into updating the ExtensionErrorList rather than
842       // rebuilding it every time.
843       panel.hidden = !errors || errors.length == 0;
844       panel.textContent = '';
846       if (panel.hidden)
847         return;
849       var errorList =
850           new extensions.ExtensionErrorList(assertInstanceof(errors, Array));
852       panel.appendChild(errorList);
854       var list = errorList.getErrorListElement();
855       if (list)
856         list.setAttribute('column-type', columnType + 'list');
858       var button = errorList.getToggleElement();
859       if (button)
860         button.setAttribute('column-type', columnType + 'button');
861     },
863     /**
864      * Opens the extension options overlay for the extension with the given id.
865      * @param {string} extensionId The id of extension whose options page should
866      *     be displayed.
867      * @param {boolean} scroll Whether the page should scroll to the extension
868      * @private
869      */
870     showEmbeddedExtensionOptions_: function(extensionId, scroll) {
871       if (this.optionsShown_)
872         return;
874       // Get the extension from the given id.
875       var extension = this.data_.extensions.filter(function(extension) {
876         return extension.enabled && extension.id == extensionId;
877       })[0];
879       if (!extension)
880         return;
882       if (scroll)
883         this.scrollToNode_(extensionId);
885       // Add the options query string. Corner case: the 'options' query string
886       // will clobber the 'id' query string if the options link is clicked when
887       // 'id' is in the URL, or if both query strings are in the URL.
888       uber.replaceState({}, '?options=' + extensionId);
890       var overlay = extensions.ExtensionOptionsOverlay.getInstance();
891       var shownCallback = function() {
892         // This overlay doesn't get focused automatically as <extensionoptions>
893         // is created after the overlay is shown.
894         if (cr.ui.FocusOutlineManager.forDocument(document).visible)
895           overlay.setInitialFocus();
896       };
897       overlay.setExtensionAndShowOverlay(extensionId, extension.name,
898                                          extension.icon, shownCallback);
899       this.optionsShown_ = true;
901       var self = this;
902       $('overlay').addEventListener('cancelOverlay', function f() {
903         self.optionsShown_ = false;
904         $('overlay').removeEventListener('cancelOverlay', f);
905       });
907       // TODO(dbeam): why do we need to focus <extensionoptions> before and
908       // after its showing animation? Makes very little sense to me.
909       overlay.setInitialFocus();
910     },
911   };
913   return {
914     ExtensionList: ExtensionList
915   };