Give names to all utility processes.
[chromium-blink-merge.git] / chrome / browser / resources / extensions / extension_list.js
blobc05e07f01f8c8d00bec4574b3da35b35d4d8d833
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}
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.
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);
56 // Return the first focusable element if no equivalent type is found.
57 return equivalent || this.focusableElements[0];
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);
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
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;
84 return null;
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
100 setupColumn: function(query, columnType, eventType, handler) {
101 var element = this.querySelector(query);
102 element.addEventListener(eventType, handler);
103 element.setAttribute('column-type', columnType);
107 * @param {Element} element
108 * @return {boolean}
109 * @private
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;
124 return true;
128 * Returns true if the element should only be shown in developer mode.
129 * @param {Element} element
130 * @return {boolean}
131 * @private
133 isDeveloperOption_: function(element) {
134 return /^dev-/.test(element.getAttribute('column-type'));
138 cr.define('extensions', function() {
139 'use strict';
142 * Creates a new list of extensions.
143 * @constructor
144 * @extends {HTMLDivElement}
146 function ExtensionList() {
147 var div = document.createElement('div');
148 div.__proto__ = ExtensionList.prototype;
149 /** @private {!Array<ExtensionInfo>} */
150 div.extensions_ = [];
151 return div;
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.
160 var extensionReloadedTimestamp = {};
162 ExtensionList.prototype = {
163 __proto__: HTMLDivElement.prototype,
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
172 optionsShown_: false,
174 /** @private {!cr.ui.FocusGrid} */
175 focusGrid_: new cr.ui.FocusGrid(),
178 * Indicates whether an uninstall dialog is being shown to prevent multiple
179 * dialogs from being displayed.
180 * @private {boolean}
182 uninstallIsShowing_: false,
185 * Indicates whether a permissions prompt is showing.
186 * @private {boolean}
188 permissionsPromptIsShowing_: false,
191 * Necessary to only show the butterbar once.
192 * @private {boolean}
194 butterbarShown_: false,
197 * Whether or not incognito mode is available.
198 * @private {boolean}
200 incognitoAvailable_: false,
203 * Whether or not the app info dialog is enabled.
204 * @private {boolean}
206 enableAppInfoDialog_: false,
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.
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 /** @private {Promise} */
222 this.extensionsUpdated_ = new Promise(function(resolve, reject) {
223 chrome.developerPrivate.getExtensionsInfo(
224 {includeDisabled: true, includeTerminated: true},
225 function(extensions) {
226 // Sort in order of unpacked vs. packed, followed by name, followed by
227 // id.
228 extensions.sort(function(a, b) {
229 function compare(x, y) {
230 return x < y ? -1 : (x > y ? 1 : 0);
232 function compareLocation(x, y) {
233 return x.location == chrome.developerPrivate.Location.UNPACKED ?
234 -1 : (x.location == y.location ? 0 : 1);
236 return compareLocation(a, b) ||
237 compare(a.name.toLowerCase(), b.name.toLowerCase()) ||
238 compare(a.id, b.id);
240 this.extensions_ = extensions;
241 this.showExtensionNodes_();
242 resolve();
243 }.bind(this));
244 }.bind(this));
245 return this.extensionsUpdated_;
248 /** @return {number} The number of extensions being displayed. */
249 getNumExtensions: function() {
250 return this.extensions_.length;
253 getIdQueryParam_: function() {
254 return parseQueryParams(document.location)['id'];
257 getOptionsQueryParam_: function() {
258 return parseQueryParams(document.location)['options'];
262 * Creates or updates all extension items from scratch.
263 * @private
265 showExtensionNodes_: function() {
266 // Remove the rows from |focusGrid_| without destroying them.
267 this.focusGrid_.rows.length = 0;
269 // Any node that is not updated will be removed.
270 var seenIds = [];
272 // Iterate over the extension data and add each item to the list.
273 this.extensions_.forEach(function(extension, i) {
274 var nextExt = this.extensions_[i + 1];
275 var node = $(extension.id);
276 seenIds.push(extension.id);
278 if (node)
279 this.updateNode_(extension, node);
280 else
281 this.createNode_(extension, nextExt ? $(nextExt.id) : null);
282 }, this);
284 // Remove extensions that are no longer installed.
285 var nodes = document.querySelectorAll('.extension-list-item-wrapper[id]');
286 for (var i = 0; i < nodes.length; ++i) {
287 var node = nodes[i];
288 if (seenIds.indexOf(node.id) < 0) {
289 if (node.contains(document.activeElement)) {
290 var focusableNode = nodes[i + 1] || nodes[i - 1];
291 if (focusableNode) {
292 focusableNode.getEquivalentElement(
293 document.activeElement).focus();
297 node.parentElement.removeChild(node);
298 // Unregister the removed node from events.
299 assertInstanceof(node, ExtensionFocusRow).destroy();
303 var idToHighlight = this.getIdQueryParam_();
304 if (idToHighlight && $(idToHighlight))
305 this.scrollToNode_(idToHighlight);
307 var idToOpenOptions = this.getOptionsQueryParam_();
308 if (idToOpenOptions && $(idToOpenOptions))
309 this.showEmbeddedExtensionOptions_(idToOpenOptions, true);
312 /** Updates each row's focusable elements without rebuilding the grid. */
313 updateFocusableElements: function() {
314 var rows = document.querySelectorAll('.extension-list-item-wrapper[id]');
315 for (var i = 0; i < rows.length; ++i) {
316 assertInstanceof(rows[i], ExtensionFocusRow).updateFocusableElements();
321 * Scrolls the page down to the extension node with the given id.
322 * @param {string} extensionId The id of the extension to scroll to.
323 * @private
325 scrollToNode_: function(extensionId) {
326 // Scroll offset should be calculated slightly higher than the actual
327 // offset of the element being scrolled to, so that it ends up not all
328 // the way at the top. That way it is clear that there are more elements
329 // above the element being scrolled to.
330 var scrollFudge = 1.2;
331 var scrollTop = $(extensionId).offsetTop - scrollFudge *
332 $(extensionId).clientHeight;
333 setScrollTopForDocument(document, scrollTop);
337 * Synthesizes and initializes an HTML element for the extension metadata
338 * given in |extension|.
339 * @param {!ExtensionInfo} extension A dictionary of extension metadata.
340 * @param {?Element} nextNode |node| should be inserted before |nextNode|.
341 * |node| will be appended to the end if |nextNode| is null.
342 * @private
344 createNode_: function(extension, nextNode) {
345 var template = $('template-collection').querySelector(
346 '.extension-list-item-wrapper');
347 var node = template.cloneNode(true);
348 ExtensionFocusRow.decorate(node, $('extension-settings-list'));
350 var row = assertInstanceof(node, ExtensionFocusRow);
351 row.id = extension.id;
353 // The 'Show Browser Action' button.
354 row.setupColumn('.show-button', 'showButton', 'click', function(e) {
355 chrome.developerPrivate.updateExtensionConfiguration({
356 extensionId: extension.id,
357 showActionButton: true
361 // The 'allow in incognito' checkbox.
362 row.setupColumn('.incognito-control input', 'incognito', 'change',
363 function(e) {
364 var butterBar = row.querySelector('.butter-bar');
365 var checked = e.target.checked;
366 if (!this.butterbarShown_) {
367 butterBar.hidden = !checked ||
368 extension.type ==
369 chrome.developerPrivate.ExtensionType.HOSTED_APP;
370 this.butterbarShown_ = !butterBar.hidden;
371 } else {
372 butterBar.hidden = true;
374 chrome.developerPrivate.updateExtensionConfiguration({
375 extensionId: extension.id,
376 incognitoAccess: e.target.checked
378 }.bind(this));
380 // The 'collect errors' checkbox. This should only be visible if the
381 // error console is enabled - we can detect this by the existence of the
382 // |errorCollectionEnabled| property.
383 row.setupColumn('.error-collection-control input', 'dev-collectErrors',
384 'change', function(e) {
385 chrome.developerPrivate.updateExtensionConfiguration({
386 extensionId: extension.id,
387 errorCollection: e.target.checked
391 // The 'allow on all urls' checkbox. This should only be visible if
392 // active script restrictions are enabled. If they are not enabled, no
393 // extensions should want all urls.
394 row.setupColumn('.all-urls-control input', 'allUrls', 'click',
395 function(e) {
396 chrome.developerPrivate.updateExtensionConfiguration({
397 extensionId: extension.id,
398 runOnAllUrls: e.target.checked
402 // The 'allow file:// access' checkbox.
403 row.setupColumn('.file-access-control input', 'localUrls', 'click',
404 function(e) {
405 chrome.developerPrivate.updateExtensionConfiguration({
406 extensionId: extension.id,
407 fileAccess: e.target.checked
411 // The 'Options' button or link, depending on its behaviour.
412 // Set an href to get the correct mouse-over appearance (link,
413 // footer) - but the actual link opening is done through chrome.send
414 // with a preventDefault().
415 row.querySelector('.options-link').href =
416 extension.optionsPage ? extension.optionsPage.url : '';
417 row.setupColumn('.options-link', 'options', 'click', function(e) {
418 chrome.send('extensionSettingsOptions', [extension.id]);
419 e.preventDefault();
422 row.setupColumn('.options-button', 'options', 'click', function(e) {
423 this.showEmbeddedExtensionOptions_(extension.id, false);
424 e.preventDefault();
425 }.bind(this));
427 // The 'View in Web Store/View Web Site' link.
428 row.querySelector('.site-link').setAttribute('column-type', 'website');
430 // The 'Permissions' link.
431 row.setupColumn('.permissions-link', 'details', 'click', function(e) {
432 if (!this.permissionsPromptIsShowing_) {
433 chrome.developerPrivate.showPermissionsDialog(extension.id,
434 function() {
435 this.permissionsPromptIsShowing_ = false;
436 }.bind(this));
437 this.permissionsPromptIsShowing_ = true;
439 e.preventDefault();
442 // The 'Reload' link.
443 row.setupColumn('.reload-link', 'localReload', 'click', function(e) {
444 chrome.developerPrivate.reload(extension.id, {failQuietly: true});
445 extensionReloadedTimestamp[extension.id] = Date.now();
448 // The 'Launch' link.
449 row.setupColumn('.launch-link', 'launch', 'click', function(e) {
450 chrome.send('extensionSettingsLaunch', [extension.id]);
453 // The 'Reload' terminated link.
454 row.setupColumn('.terminated-reload-link', 'terminatedReload', 'click',
455 function(e) {
456 chrome.developerPrivate.reload(extension.id, {failQuietly: true});
459 // The 'Repair' corrupted link.
460 row.setupColumn('.corrupted-repair-button', 'repair', 'click',
461 function(e) {
462 chrome.send('extensionSettingsRepair', [extension.id]);
465 // The 'Enabled' checkbox.
466 row.setupColumn('.enable-checkbox input', 'enabled', 'change',
467 function(e) {
468 var checked = e.target.checked;
469 // TODO(devlin): What should we do if this fails?
470 chrome.management.setEnabled(extension.id, checked);
472 // This may seem counter-intuitive (to not set/clear the checkmark)
473 // but this page will be updated asynchronously if the extension
474 // becomes enabled/disabled. It also might not become enabled or
475 // disabled, because the user might e.g. get prompted when enabling
476 // and choose not to.
477 e.preventDefault();
480 // 'Remove' button.
481 var trashTemplate = $('template-collection').querySelector('.trash');
482 var trash = trashTemplate.cloneNode(true);
483 trash.title = loadTimeData.getString('extensionUninstall');
484 trash.hidden = !extension.userMayModify;
485 trash.setAttribute('column-type', 'trash');
486 trash.addEventListener('click', function(e) {
487 trash.classList.add('open');
488 trash.classList.toggle('mouse-clicked', e.detail > 0);
489 if (this.uninstallIsShowing_)
490 return;
491 this.uninstallIsShowing_ = true;
492 chrome.management.uninstall(extension.id,
493 {showConfirmDialog: true},
494 function() {
495 // TODO(devlin): What should we do if the uninstall fails?
496 this.uninstallIsShowing_ = false;
498 if (trash.classList.contains('mouse-clicked'))
499 trash.blur();
501 if (chrome.runtime.lastError) {
502 // The uninstall failed (e.g. a cancel). Allow the trash to close.
503 trash.classList.remove('open');
504 } else {
505 // Leave the trash open if the uninstall succeded. Otherwise it can
506 // half-close right before it's removed when the DOM is modified.
508 }.bind(this));
509 }.bind(this));
510 row.querySelector('.enable-controls').appendChild(trash);
512 // Developer mode ////////////////////////////////////////////////////////
514 // The path, if provided by unpacked extension.
515 row.setupColumn('.load-path a:first-of-type', 'dev-loadPath', 'click',
516 function(e) {
517 chrome.send('extensionSettingsShowPath', [String(extension.id)]);
518 e.preventDefault();
521 // Maintain the order that nodes should be in when creating as well as
522 // when adding only one new row.
523 this.insertBefore(row, nextNode);
524 this.updateNode_(extension, row);
528 * Updates an HTML element for the extension metadata given in |extension|.
529 * @param {!ExtensionInfo} extension A dictionary of extension metadata.
530 * @param {!ExtensionFocusRow} row The node that is being updated.
531 * @private
533 updateNode_: function(extension, row) {
534 var isActive =
535 extension.state == chrome.developerPrivate.ExtensionState.ENABLED;
536 row.classList.toggle('inactive-extension', !isActive);
538 // Hack to keep the closure compiler happy about |remove|.
539 // TODO(hcarmona): Remove this hack when the closure compiler is updated.
540 var node = /** @type {Element} */ (row);
541 node.classList.remove('policy-controlled', 'may-not-modify',
542 'may-not-remove');
543 var classes = [];
544 if (!extension.userMayModify) {
545 classes.push('policy-controlled', 'may-not-modify');
546 } else if (extension.dependentExtensions.length > 0) {
547 classes.push('may-not-remove', 'may-not-modify');
548 } else if (extension.mustRemainInstalled) {
549 classes.push('may-not-remove');
550 } else if (extension.disableReasons.suspiciousInstall ||
551 extension.disableReasons.corruptInstall ||
552 extension.disableReasons.updateRequired) {
553 classes.push('may-not-modify');
555 row.classList.add.apply(row.classList, classes);
557 row.classList.toggle('extension-highlight',
558 row.id == this.getIdQueryParam_());
560 var item = row.querySelector('.extension-list-item');
561 // Prevent the image cache of extension icon by using the reloaded
562 // timestamp as a query string. The timestamp is recorded when the user
563 // clicks the 'Reload' link. http://crbug.com/159302.
564 if (extensionReloadedTimestamp[extension.id]) {
565 item.style.backgroundImage =
566 'url(' + extension.iconUrl + '?' +
567 extensionReloadedTimestamp[extension.id] + ')';
568 } else {
569 item.style.backgroundImage = 'url(' + extension.iconUrl + ')';
572 this.setText_(row, '.extension-title', extension.name);
573 this.setText_(row, '.extension-version', extension.version);
574 this.setText_(row, '.location-text', extension.locationText || '');
575 this.setText_(row, '.blacklist-text', extension.blacklistText || '');
576 this.setText_(row, '.extension-description', extension.description);
578 // The 'Show Browser Action' button.
579 this.updateVisibility_(row, '.show-button',
580 isActive && extension.actionButtonHidden);
582 // The 'allow in incognito' checkbox.
583 this.updateVisibility_(row, '.incognito-control',
584 isActive && this.incognitoAvailable_,
585 function(item) {
586 var incognito = item.querySelector('input');
587 incognito.disabled = !extension.incognitoAccess.isEnabled;
588 incognito.checked = extension.incognitoAccess.isActive;
591 // Hide butterBar if incognito is not enabled for the extension.
592 var butterBar = row.querySelector('.butter-bar');
593 butterBar.hidden =
594 butterBar.hidden || !extension.incognitoAccess.isEnabled;
596 // The 'collect errors' checkbox. This should only be visible if the
597 // error console is enabled - we can detect this by the existence of the
598 // |errorCollectionEnabled| property.
599 this.updateVisibility_(
600 row, '.error-collection-control',
601 isActive && extension.errorCollection.isEnabled,
602 function(item) {
603 item.querySelector('input').checked =
604 extension.errorCollection.isActive;
607 // The 'allow on all urls' checkbox. This should only be visible if
608 // active script restrictions are enabled. If they are not enabled, no
609 // extensions should want all urls.
610 this.updateVisibility_(
611 row, '.all-urls-control',
612 isActive && extension.runOnAllUrls.isEnabled,
613 function(item) {
614 item.querySelector('input').checked = extension.runOnAllUrls.isActive;
617 // The 'allow file:// access' checkbox.
618 this.updateVisibility_(row, '.file-access-control',
619 isActive && extension.fileAccess.isEnabled,
620 function(item) {
621 item.querySelector('input').checked = extension.fileAccess.isActive;
624 // The 'Options' button or link, depending on its behaviour.
625 var optionsEnabled = isActive && !!extension.optionsPage;
626 this.updateVisibility_(row, '.options-link', optionsEnabled &&
627 extension.optionsPage.openInTab);
628 this.updateVisibility_(row, '.options-button', optionsEnabled &&
629 !extension.optionsPage.openInTab);
631 // The 'View in Web Store/View Web Site' link.
632 var siteLinkEnabled = !!extension.homepageUrl &&
633 !this.enableAppInfoDialog_;
634 this.updateVisibility_(row, '.site-link', siteLinkEnabled,
635 function(item) {
636 item.href = extension.homepageUrl;
637 item.textContent = loadTimeData.getString(
638 extension.homepageProvided ? 'extensionSettingsVisitWebsite' :
639 'extensionSettingsVisitWebStore');
642 var isUnpacked =
643 extension.location == chrome.developerPrivate.Location.UNPACKED;
644 // The 'Reload' link.
645 this.updateVisibility_(row, '.reload-link', isUnpacked);
647 // The 'Launch' link.
648 this.updateVisibility_(
649 row, '.launch-link',
650 isUnpacked && extension.type ==
651 chrome.developerPrivate.ExtensionType.PLATFORM_APP);
653 // The 'Reload' terminated link.
654 var isTerminated =
655 extension.state == chrome.developerPrivate.ExtensionState.TERMINATED;
656 this.updateVisibility_(row, '.terminated-reload-link', isTerminated);
658 // The 'Repair' corrupted link.
659 var canRepair = !isTerminated &&
660 extension.disableReasons.corruptInstall &&
661 extension.location ==
662 chrome.developerPrivate.Location.FROM_STORE;
663 this.updateVisibility_(row, '.corrupted-repair-button', canRepair);
665 // The 'Enabled' checkbox.
666 var isOK = !isTerminated && !canRepair;
667 this.updateVisibility_(row, '.enable-checkbox', isOK, function(item) {
668 var enableCheckboxDisabled =
669 !extension.userMayModify ||
670 extension.disableReasons.suspiciousInstall ||
671 extension.disableReasons.corruptInstall ||
672 extension.disableReasons.updateRequired ||
673 extension.installedByCustodian ||
674 extension.dependentExtensions.length > 0;
675 item.querySelector('input').disabled = enableCheckboxDisabled;
676 item.querySelector('input').checked = isActive;
679 // Button for extensions controlled by policy.
680 var controlNode = row.querySelector('.enable-controls');
681 var indicator =
682 controlNode.querySelector('.controlled-extension-indicator');
683 var needsIndicator = isOK &&
684 !extension.userMayModify &&
685 extension.policyText;
686 // TODO(treib): If userMayModify is false, but policyText is empty, that
687 // indicates this extension is controlled by something else than
688 // enterprise policy (such as the profile being supervised). For now, just
689 // don't show the indicator in this case. We should really handle this
690 // better though (ie use a different text and icon).
692 if (needsIndicator && !indicator) {
693 indicator = new cr.ui.ControlledIndicator();
694 indicator.classList.add('controlled-extension-indicator');
695 indicator.setAttribute('controlled-by', 'policy');
696 var textPolicy = extension.policyText || '';
697 indicator.setAttribute('textpolicy', textPolicy);
698 indicator.image.setAttribute('aria-label', textPolicy);
699 controlNode.appendChild(indicator);
700 indicator.querySelector('div').setAttribute('column-type',
701 'enterprise');
702 } else if (!needsIndicator && indicator) {
703 controlNode.removeChild(indicator);
706 // Developer mode ////////////////////////////////////////////////////////
708 // First we have the id.
709 var idLabel = row.querySelector('.extension-id');
710 idLabel.textContent = ' ' + extension.id;
712 // Then the path, if provided by unpacked extension.
713 this.updateVisibility_(row, '.load-path', isUnpacked,
714 function(item) {
715 item.querySelector('a:first-of-type').textContent =
716 ' ' + extension.prettifiedPath;
719 // Then the 'managed, cannot uninstall/disable' message.
720 // We would like to hide managed installed message since this
721 // extension is disabled.
722 var isRequired =
723 !extension.userMayModify || extension.mustRemainInstalled;
724 this.updateVisibility_(row, '.managed-message', isRequired &&
725 !extension.disableReasons.updateRequired);
727 // Then the 'This isn't from the webstore, looks suspicious' message.
728 this.updateVisibility_(row, '.suspicious-install-message', !isRequired &&
729 extension.disableReasons.suspiciousInstall);
731 // Then the 'This is a corrupt extension' message.
732 this.updateVisibility_(row, '.corrupt-install-message', !isRequired &&
733 extension.disableReasons.corruptInstall);
735 // Then the 'An update required by enterprise policy' message. Note that
736 // a force-installed extension might be disabled due to being outdated
737 // as well.
738 this.updateVisibility_(row, '.update-required-message',
739 extension.disableReasons.updateRequired);
741 // The 'following extensions depend on this extension' list.
742 var hasDependents = extension.dependentExtensions.length > 0;
743 row.classList.toggle('developer-extras', hasDependents);
744 this.updateVisibility_(row, '.dependent-extensions-message',
745 hasDependents, function(item) {
746 var dependentList = item.querySelector('ul');
747 dependentList.textContent = '';
748 var dependentTemplate = $('template-collection').querySelector(
749 '.dependent-list-item');
750 extension.dependentExtensions.forEach(function(dependentId) {
751 var dependentExtension = null;
752 for (var i = 0; i < this.extensions_.length; ++i) {
753 if (this.extensions_[i].id == dependentId) {
754 dependentExtension = this.extensions_[i];
755 break;
758 if (!dependentExtension)
759 return;
761 var depNode = dependentTemplate.cloneNode(true);
762 depNode.querySelector('.dep-extension-title').textContent =
763 dependentExtension.name;
764 depNode.querySelector('.dep-extension-id').textContent =
765 dependentExtension.id;
766 dependentList.appendChild(depNode);
767 }, this);
768 }.bind(this));
770 // The active views.
771 this.updateVisibility_(row, '.active-views', extension.views.length > 0,
772 function(item) {
773 var link = item.querySelector('a');
775 // Link needs to be an only child before the list is updated.
776 while (link.nextElementSibling)
777 item.removeChild(link.nextElementSibling);
779 // Link needs to be cleaned up if it was used before.
780 link.textContent = '';
781 if (link.clickHandler)
782 link.removeEventListener('click', link.clickHandler);
784 extension.views.forEach(function(view, i) {
785 if (view.type == chrome.developerPrivate.ViewType.EXTENSION_DIALOG ||
786 view.type == chrome.developerPrivate.ViewType.EXTENSION_POPUP) {
787 return;
789 var displayName;
790 if (view.url.indexOf('chrome-extension://') == 0) {
791 var pathOffset = 'chrome-extension://'.length + 32 + 1;
792 displayName = view.url.substring(pathOffset);
793 if (displayName == '_generated_background_page.html')
794 displayName = loadTimeData.getString('backgroundPage');
795 } else {
796 displayName = view.url;
798 var label = displayName +
799 (view.incognito ?
800 ' ' + loadTimeData.getString('viewIncognito') : '') +
801 (view.renderProcessId == -1 ?
802 ' ' + loadTimeData.getString('viewInactive') : '');
803 link.textContent = label;
804 link.clickHandler = function(e) {
805 chrome.developerPrivate.openDevTools({
806 extensionId: extension.id,
807 renderProcessId: view.renderProcessId,
808 renderViewId: view.renderViewId,
809 incognito: view.incognito
812 link.addEventListener('click', link.clickHandler);
814 if (i < extension.views.length - 1) {
815 link = link.cloneNode(true);
816 item.appendChild(link);
820 var allLinks = item.querySelectorAll('a');
821 for (var i = 0; i < allLinks.length; ++i) {
822 allLinks[i].setAttribute('column-type', 'dev-activeViews' + i);
826 // The extension warnings (describing runtime issues).
827 this.updateVisibility_(row, '.extension-warnings',
828 extension.runtimeWarnings.length > 0,
829 function(item) {
830 var warningList = item.querySelector('ul');
831 warningList.textContent = '';
832 extension.runtimeWarnings.forEach(function(warning) {
833 var li = document.createElement('li');
834 warningList.appendChild(li).innerText = warning;
838 // If the ErrorConsole is enabled, we should have manifest and/or runtime
839 // errors. Otherwise, we may have install warnings. We should not have
840 // both ErrorConsole errors and install warnings.
841 // Errors.
842 this.updateErrors_(row.querySelector('.manifest-errors'),
843 'dev-manifestErrors', extension.manifestErrors);
844 this.updateErrors_(row.querySelector('.runtime-errors'),
845 'dev-runtimeErrors', extension.runtimeErrors);
847 // Install warnings.
848 this.updateVisibility_(row, '.install-warnings',
849 extension.installWarnings.length > 0,
850 function(item) {
851 var installWarningList = item.querySelector('ul');
852 installWarningList.textContent = '';
853 if (extension.installWarnings) {
854 extension.installWarnings.forEach(function(warning) {
855 var li = document.createElement('li');
856 li.innerText = warning;
857 installWarningList.appendChild(li);
862 if (location.hash.substr(1) == extension.id) {
863 // Scroll beneath the fixed header so that the extension is not
864 // obscured.
865 var topScroll = row.offsetTop - $('page-header').offsetHeight;
866 var pad = parseInt(window.getComputedStyle(row, null).marginTop, 10);
867 if (!isNaN(pad))
868 topScroll -= pad / 2;
869 setScrollTopForDocument(document, topScroll);
872 row.updateFocusableElements();
873 this.focusGrid_.addRow(row);
877 * Updates an element's textContent.
878 * @param {Element} node Ancestor of the element specified by |query|.
879 * @param {string} query A query to select an element in |node|.
880 * @param {string} textContent
881 * @private
883 setText_: function(node, query, textContent) {
884 node.querySelector(query).textContent = textContent;
888 * Updates an element's visibility and calls |shownCallback| if it is
889 * visible.
890 * @param {Element} node Ancestor of the element specified by |query|.
891 * @param {string} query A query to select an element in |node|.
892 * @param {boolean} visible Whether the element should be visible or not.
893 * @param {function(Element)=} opt_shownCallback Callback if the element is
894 * visible. The element passed in will be the element specified by
895 * |query|.
896 * @private
898 updateVisibility_: function(node, query, visible, opt_shownCallback) {
899 var item = assert(node.querySelector(query));
900 item.hidden = !visible;
901 if (visible && opt_shownCallback)
902 opt_shownCallback(item);
906 * Updates an element to show a list of errors.
907 * @param {Element} panel An element to hold the errors.
908 * @param {string} columnType A tag used to identify the column when
909 * changing focus.
910 * @param {Array<RuntimeError|ManifestError>|undefined} errors The errors
911 * to be displayed.
912 * @private
914 updateErrors_: function(panel, columnType, errors) {
915 // TODO(hcarmona): Look into updating the ExtensionErrorList rather than
916 // rebuilding it every time.
917 panel.hidden = !errors || errors.length == 0;
918 panel.textContent = '';
920 if (panel.hidden)
921 return;
923 var errorList =
924 new extensions.ExtensionErrorList(assertInstanceof(errors, Array));
926 panel.appendChild(errorList);
928 var list = errorList.getErrorListElement();
929 if (list)
930 list.setAttribute('column-type', columnType + 'list');
932 var button = errorList.getToggleElement();
933 if (button)
934 button.setAttribute('column-type', columnType + 'button');
938 * Opens the extension options overlay for the extension with the given id.
939 * @param {string} extensionId The id of extension whose options page should
940 * be displayed.
941 * @param {boolean} scroll Whether the page should scroll to the extension
942 * @private
944 showEmbeddedExtensionOptions_: function(extensionId, scroll) {
945 if (this.optionsShown_)
946 return;
948 // Get the extension from the given id.
949 var extension = this.extensions_.filter(function(extension) {
950 return extension.state ==
951 chrome.developerPrivate.ExtensionState.ENABLED &&
952 extension.id == extensionId;
953 })[0];
955 if (!extension)
956 return;
958 if (scroll)
959 this.scrollToNode_(extensionId);
961 // Add the options query string. Corner case: the 'options' query string
962 // will clobber the 'id' query string if the options link is clicked when
963 // 'id' is in the URL, or if both query strings are in the URL.
964 uber.replaceState({}, '?options=' + extensionId);
966 var overlay = extensions.ExtensionOptionsOverlay.getInstance();
967 var shownCallback = function() {
968 // This overlay doesn't get focused automatically as <extensionoptions>
969 // is created after the overlay is shown.
970 if (cr.ui.FocusOutlineManager.forDocument(document).visible)
971 overlay.setInitialFocus();
973 overlay.setExtensionAndShowOverlay(extensionId, extension.name,
974 extension.iconUrl, shownCallback);
975 this.optionsShown_ = true;
977 var self = this;
978 $('overlay').addEventListener('cancelOverlay', function f() {
979 self.optionsShown_ = false;
980 $('overlay').removeEventListener('cancelOverlay', f);
983 // TODO(dbeam): why do we need to focus <extensionoptions> before and
984 // after its showing animation? Makes very little sense to me.
985 overlay.setInitialFocus();
989 return {
990 ExtensionList: ExtensionList