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 ///////////////////////////////////////////////////////////////////////////////
11 * Provides an implementation for a single column grid.
13 * @extends {cr.ui.FocusRow}
15 function ExtensionFocusRow() {}
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,
31 getEquivalentElement: function(element) {
32 if (this.focusableElements.indexOf(element) > -1)
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);
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|.
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)
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
95 * @param {string} eventType The type of event to listen to.
96 * @param {function(Event)} handler The function that should be called
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
111 canAddElement_: function(element) {
112 if (!element || element.disabled)
115 var developerMode = $('extension-settings').classList.contains('dev-mode');
116 if (this.isDeveloperOption_(element) && !developerMode)
119 for (var el = element; el; el = el.parentElement) {
128 * Returns true if the element should only be shown in developer mode.
129 * @param {Element} element
133 isDeveloperOption_: function(element) {
134 return /^dev-/.test(element.getAttribute('column-type'));
138 cr.define('extensions', function() {
142 * Creates a new list of extensions.
144 * @extends {HTMLDivElement}
146 function ExtensionList() {
147 var div = document.createElement('div');
148 div.__proto__ = ExtensionList.prototype;
149 /** @private {!Array<ExtensionInfo>} */
150 div.extensions_ = [];
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.
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.
182 uninstallIsShowing_: false,
185 * Indicates whether a permissions prompt is showing.
188 permissionsPromptIsShowing_: false,
191 * Necessary to only show the butterbar once.
194 butterbarShown_: false,
197 * Whether or not incognito mode is available.
200 incognitoAvailable_: false,
203 * Whether or not the app info dialog is enabled.
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
213 * @return {Promise} A promise that is resolved once the extensions data is
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
227 extensions.sort(function(a, b) {
228 function compare(x, y) {
229 return x < y ? -1 : (x > y ? 1 : 0);
231 function compareLocation(x, y) {
232 return x.location == chrome.developerPrivate.Location.UNPACKED ?
233 -1 : (x.location == y.location ? 0 : 1);
235 return compareLocation(a, b) ||
236 compare(a.name.toLowerCase(), b.name.toLowerCase()) ||
239 this.extensions_ = extensions;
240 this.showExtensionNodes_();
246 /** @return {number} The number of extensions being displayed. */
247 getNumExtensions: function() {
248 return this.extensions_.length;
251 getIdQueryParam_: function() {
252 return parseQueryParams(document.location)['id'];
255 getOptionsQueryParam_: function() {
256 return parseQueryParams(document.location)['options'];
260 * Creates or updates all extension items from scratch.
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.
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);
277 this.updateNode_(extension, node);
279 this.createNode_(extension, nextExt ? $(nextExt.id) : null);
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) {
286 if (seenIds.indexOf(node.id) < 0) {
287 if (node.contains(document.activeElement)) {
288 var focusableNode = nodes[i + 1] || nodes[i - 1];
290 focusableNode.getEquivalentElement(
291 document.activeElement).focus();
295 node.parentElement.removeChild(node);
296 // Unregister the removed node from events.
297 assertInstanceof(node, ExtensionFocusRow).destroy();
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);
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();
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.
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);
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.
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
359 // The 'allow in incognito' checkbox.
360 row.setupColumn('.incognito-control input', 'incognito', 'change',
362 var butterBar = row.querySelector('.butter-bar');
363 var checked = e.target.checked;
364 if (!this.butterbarShown_) {
365 butterBar.hidden = !checked ||
367 chrome.developerPrivate.ExtensionType.HOSTED_APP;
368 this.butterbarShown_ = !butterBar.hidden;
370 butterBar.hidden = true;
372 chrome.developerPrivate.updateExtensionConfiguration({
373 extensionId: extension.id,
374 incognitoAccess: e.target.checked
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
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',
394 chrome.developerPrivate.updateExtensionConfiguration({
395 extensionId: extension.id,
396 runOnAllUrls: e.target.checked
400 // The 'allow file:// access' checkbox.
401 row.setupColumn('.file-access-control input', 'localUrls', 'click',
403 chrome.developerPrivate.updateExtensionConfiguration({
404 extensionId: extension.id,
405 fileAccess: e.target.checked
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]);
420 row.setupColumn('.options-button', 'options', 'click', function(e) {
421 this.showEmbeddedExtensionOptions_(extension.id, false);
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,
433 this.permissionsPromptIsShowing_ = false;
435 this.permissionsPromptIsShowing_ = true;
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();
446 // The 'Launch' link.
447 row.setupColumn('.launch-link', 'launch', 'click', function(e) {
448 chrome.send('extensionSettingsLaunch', [extension.id]);
451 // The 'Reload' terminated link.
452 row.setupColumn('.terminated-reload-link', 'terminatedReload', 'click',
454 chrome.developerPrivate.reload(extension.id, {failQuietly: true});
457 // The 'Repair' corrupted link.
458 row.setupColumn('.corrupted-repair-button', 'repair', 'click',
460 chrome.send('extensionSettingsRepair', [extension.id]);
463 // The 'Enabled' checkbox.
464 row.setupColumn('.enable-checkbox input', 'enabled', 'change',
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.
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_)
489 this.uninstallIsShowing_ = true;
490 chrome.management.uninstall(extension.id,
491 {showConfirmDialog: true},
493 // TODO(devlin): What should we do if the uninstall fails?
494 this.uninstallIsShowing_ = false;
496 if (trash.classList.contains('mouse-clicked'))
499 if (chrome.runtime.lastError) {
500 // The uninstall failed (e.g. a cancel). Allow the trash to close.
501 trash.classList.remove('open');
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.
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',
515 chrome.send('extensionSettingsShowPath', [String(extension.id)]);
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);
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.
531 updateNode_: function(extension, row) {
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',
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');
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] + ')';
567 item.style.backgroundImage = 'url(' + extension.iconUrl + ')';
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_,
584 var incognito = item.querySelector('input');
585 incognito.disabled = !extension.incognitoAccess.isEnabled;
586 incognito.checked = extension.incognitoAccess.isActive;
589 // Hide butterBar if incognito is not enabled for the extension.
590 var butterBar = row.querySelector('.butter-bar');
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,
601 item.querySelector('input').checked =
602 extension.errorCollection.isActive;
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,
612 item.querySelector('input').checked = extension.runOnAllUrls.isActive;
615 // The 'allow file:// access' checkbox.
616 this.updateVisibility_(row, '.file-access-control',
617 isActive && extension.fileAccess.isEnabled,
619 item.querySelector('input').checked = extension.fileAccess.isActive;
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,
634 item.href = extension.homepageUrl;
635 item.textContent = loadTimeData.getString(
636 extension.homepageProvided ? 'extensionSettingsVisitWebsite' :
637 'extensionSettingsVisitWebStore');
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_(
648 isUnpacked && extension.type ==
649 chrome.developerPrivate.ExtensionType.PLATFORM_APP);
651 // The 'Reload' terminated link.
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;
677 // Button for extensions controlled by policy.
678 var controlNode = row.querySelector('.enable-controls');
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',
700 } else if (!needsIndicator && indicator) {
701 controlNode.removeChild(indicator);
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,
713 item.querySelector('a:first-of-type').textContent =
714 ' ' + extension.prettifiedPath;
717 // Then the 'managed, cannot uninstall/disable' message.
718 // We would like to hide managed installed message since this
719 // extension is disabled.
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
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];
756 if (!dependentExtension)
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);
769 this.updateVisibility_(row, '.active-views', extension.views.length > 0,
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) {
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');
794 displayName = view.url;
796 var label = displayName +
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
810 link.addEventListener('click', link.clickHandler);
812 if (i < extension.views.length - 1) {
813 link = link.cloneNode(true);
814 item.appendChild(link);
818 var allLinks = item.querySelectorAll('a');
819 for (var i = 0; i < allLinks.length; ++i) {
820 allLinks[i].setAttribute('column-type', 'dev-activeViews' + i);
824 // The extension warnings (describing runtime issues).
825 this.updateVisibility_(row, '.extension-warnings',
826 extension.runtimeWarnings.length > 0,
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;
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.
840 this.updateErrors_(row.querySelector('.manifest-errors'),
841 'dev-manifestErrors', extension.manifestErrors);
842 this.updateErrors_(row.querySelector('.runtime-errors'),
843 'dev-runtimeErrors', extension.runtimeErrors);
846 this.updateVisibility_(row, '.install-warnings',
847 extension.installWarnings.length > 0,
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);
860 if (location.hash.substr(1) == extension.id) {
861 // Scroll beneath the fixed header so that the extension is not
863 var topScroll = row.offsetTop - $('page-header').offsetHeight;
864 var pad = parseInt(window.getComputedStyle(row, null).marginTop, 10);
866 topScroll -= pad / 2;
867 setScrollTopForDocument(document, topScroll);
870 row.updateFocusableElements();
871 this.focusGrid_.addRow(row);
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
881 setText_: function(node, query, textContent) {
882 node.querySelector(query).textContent = textContent;
886 * Updates an element's visibility and calls |shownCallback| if it is
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
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);
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
908 * @param {Array<RuntimeError|ManifestError>|undefined} errors The errors
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 = '';
922 new extensions.ExtensionErrorList(assertInstanceof(errors, Array));
924 panel.appendChild(errorList);
926 var list = errorList.getErrorListElement();
928 list.setAttribute('column-type', columnType + 'list');
930 var button = errorList.getToggleElement();
932 button.setAttribute('column-type', columnType + 'button');
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
939 * @param {boolean} scroll Whether the page should scroll to the extension
942 showEmbeddedExtensionOptions_: function(extensionId, scroll) {
943 if (this.optionsShown_)
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;
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();
971 overlay.setExtensionAndShowOverlay(extensionId, extension.name,
972 extension.iconUrl, shownCallback);
973 this.optionsShown_ = true;
976 $('overlay').addEventListener('cancelOverlay', function f() {
977 self.optionsShown_ = false;
978 $('overlay').removeEventListener('cancelOverlay', f);
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();
988 ExtensionList: ExtensionList