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">
8 * The type of the extension data object. The definition is based on
9 * chrome/browser/ui/webui/extensions/extension_basic_info.cc
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,
21 * enableExtensionInfoDialog: boolean,
22 * enable_show_button: boolean,
24 * enabledIncognito: boolean,
25 * errorCollectionEnabled: (boolean|undefined),
26 * hasPopupAction: boolean,
27 * homepageProvided: boolean,
28 * homepageUrl: 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,
40 * locationText: string,
41 * managedInstall: boolean,
42 * manifestErrors: (Array<RuntimeError>|undefined),
44 * offlineEnabled: boolean,
45 * optionsOpenInTab: boolean,
46 * optionsPageHref: string,
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,
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)}}
69 ///////////////////////////////////////////////////////////////////////////////
73 * Provides an implementation for a single column grid.
75 * @extends {cr.ui.FocusRow}
77 function ExtensionFocusRow() {}
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.
84 ExtensionFocusRow.decorate = function(focusRow, boundary) {
85 focusRow.__proto__ = ExtensionFocusRow.prototype;
86 focusRow.decorate(boundary);
89 ExtensionFocusRow.prototype = {
90 __proto__: cr.ui.FocusRow.prototype,
93 getEquivalentElement: function(element) {
94 if (this.focusableElements.indexOf(element) > -1)
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);
118 // Return the first focusable element if no equivalent type is found.
119 return equivalent || this.focusableElements[0];
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);
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|.
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)
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
157 * @param {string} eventType The type of event to listen to.
158 * @param {function(Event)} handler The function that should be called
162 setupColumn: function(query, columnType, eventType, handler) {
163 var element = this.querySelector(query);
164 element.addEventListener(eventType, handler);
165 element.setAttribute('column-type', columnType);
169 * @param {Element} element
173 canAddElement_: function(element) {
174 if (!element || element.disabled)
177 var developerMode = $('extension-settings').classList.contains('dev-mode');
178 if (this.isDeveloperOption_(element) && !developerMode)
181 for (var el = element; el; el = el.parentElement) {
190 * Returns true if the element should only be shown in developer mode.
191 * @param {Element} element
195 isDeveloperOption_: function(element) {
196 return /^dev-/.test(element.getAttribute('column-type'));
200 cr.define('extensions', function() {
204 * Creates a new list of extensions.
205 * @param {Object=} opt_propertyBag Optional properties.
207 * @extends {HTMLDivElement}
209 var ExtensionList = cr.ui.define('div');
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.
217 var extensionReloadedTimestamp = {};
219 ExtensionList.prototype = {
220 __proto__: HTMLDivElement.prototype,
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.
229 optionsShown_: false,
231 /** @private {!cr.ui.FocusGrid} */
232 focusGrid_: new cr.ui.FocusGrid(),
235 * Indicates whether an uninstall dialog is being shown to prevent multiple
236 * dialogs from being displayed.
240 uninstallIsShowing_: false,
243 * Necessary to only show the butterbar once.
246 butterbarShown_: false,
248 decorate: function() {
249 this.showExtensionNodes_();
252 getIdQueryParam_: function() {
253 return parseQueryParams(document.location)['id'];
256 getOptionsQueryParam_: function() {
257 return parseQueryParams(document.location)['options'];
261 * Creates or updates all extension items from scratch.
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.
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);
278 this.updateNode_(extension, node);
280 this.createNode_(extension, nextExt ? $(nextExt.id) : null);
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) {
287 if (seenIds.indexOf(node.id) < 0) {
288 node.parentElement.removeChild(node);
289 // Unregister the removed node from events.
290 assertInstanceof(node, ExtensionFocusRow).destroy();
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);
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();
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.
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);
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.
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]);
352 // The 'allow in incognito' checkbox.
353 row.setupColumn('.incognito-control input', 'incognito', 'change',
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;
361 butterBar.hidden = true;
363 chrome.developerPrivate.allowIncognito(extension.id, checked);
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)]);
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',
380 chrome.send('extensionSettingsAllowOnAllUrls',
381 [extension.id, String(e.target.checked)]);
384 // The 'allow file:// access' checkbox.
385 row.setupColumn('.file-access-control input', 'localUrls', 'click',
387 chrome.developerPrivate.allowFileAccess(extension.id, e.target.checked);
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]);
400 row.setupColumn('.options-button', 'options', 'click', function(e) {
401 this.showEmbeddedExtensionOptions_(extension.id, false);
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]);
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();
420 // The 'Launch' link.
421 row.setupColumn('.launch-link', 'launch', 'click', function(e) {
422 chrome.send('extensionSettingsLaunch', [extension.id]);
425 // The 'Reload' terminated link.
426 row.setupColumn('.terminated-reload-link', 'terminatedReload', 'click',
428 chrome.send('extensionSettingsReload', [extension.id]);
431 // The 'Repair' corrupted link.
432 row.setupColumn('.corrupted-repair-button', 'repair', 'click',
434 chrome.send('extensionSettingsRepair', [extension.id]);
437 // The 'Enabled' checkbox.
438 row.setupColumn('.enable-checkbox input', 'enabled', 'change',
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.
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_)
463 this.uninstallIsShowing_ = true;
464 chrome.management.uninstall(extension.id,
465 {showConfirmDialog: true},
467 // TODO(devlin): What should we do if the uninstall fails?
468 this.uninstallIsShowing_ = false;
470 if (trash.classList.contains('mouse-clicked'))
473 if (chrome.runtime.lastError) {
474 // The uninstall failed (e.g. a cancel). Allow the trash to close.
475 trash.classList.remove('open');
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.
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',
489 chrome.send('extensionSettingsShowPath', [String(extension.id)]);
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);
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.
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',
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');
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] + ')';
541 item.style.backgroundImage = 'url(' + extension.icon + ')';
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,
558 var incognito = item.querySelector('input');
559 incognito.disabled = !extension.incognitoCanBeEnabled;
560 incognito.checked = extension.enabledIncognito;
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,
573 item.querySelector('input').checked = extension.errorCollectionEnabled;
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;
584 // The 'allow file:// access' checkbox.
585 this.updateVisibility_(row, '.file-access-control',
586 isActive && extension.wantsFileAccess,
588 item.querySelector('input').checked = extension.allowFileAccess;
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,
603 item.href = extension.homepageUrl;
604 item.textContent = loadTimeData.getString(
605 extension.homepageProvided ? 'extensionSettingsVisitWebsite' :
606 'extensionSettingsVisitWebStore');
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;
638 // Button for extensions controlled by policy.
639 var controlNode = row.querySelector('.enable-controls');
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',
654 } else if (!needsIndicator && indicator) {
655 controlNode.removeChild(indicator);
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,
667 item.querySelector('a:first-of-type').textContent =
668 ' ' + extension.prettifiedPath;
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
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);
710 this.updateVisibility_(row, '.active-views', extension.views.length > 0,
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 +
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),
741 link.addEventListener('click', link.clickHandler);
743 if (i < extension.views.length - 1) {
744 link = link.cloneNode(true);
745 item.appendChild(link);
749 var allLinks = item.querySelectorAll('a');
750 for (var i = 0; i < allLinks.length; ++i) {
751 allLinks[i].setAttribute('column-type', 'dev-activeViews' + i);
755 // The extension warnings (describing runtime issues).
756 this.updateVisibility_(row, '.extension-warnings', !!extension.warnings,
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;
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.
770 this.updateErrors_(row.querySelector('.manifest-errors'),
771 'dev-manifestErrors', extension.manifestErrors);
772 this.updateErrors_(row.querySelector('.runtime-errors'),
773 'dev-runtimeErrors', extension.runtimeErrors);
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);
789 if (location.hash.substr(1) == extension.id) {
790 // Scroll beneath the fixed header so that the extension is not
792 var topScroll = row.offsetTop - $('page-header').offsetHeight;
793 var pad = parseInt(window.getComputedStyle(row, null).marginTop, 10);
795 topScroll -= pad / 2;
796 setScrollTopForDocument(document, topScroll);
799 row.updateFocusableElements();
800 this.focusGrid_.addRow(row);
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
810 setText_: function(node, query, textContent) {
811 node.querySelector(query).textContent = textContent;
815 * Updates an element's visibility and calls |shownCallback| if it is
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
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);
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
837 * @param {Array<RuntimeError>|undefined} errors The errors to be displayed.
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 = '';
850 new extensions.ExtensionErrorList(assertInstanceof(errors, Array));
852 panel.appendChild(errorList);
854 var list = errorList.getErrorListElement();
856 list.setAttribute('column-type', columnType + 'list');
858 var button = errorList.getToggleElement();
860 button.setAttribute('column-type', columnType + 'button');
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
867 * @param {boolean} scroll Whether the page should scroll to the extension
870 showEmbeddedExtensionOptions_: function(extensionId, scroll) {
871 if (this.optionsShown_)
874 // Get the extension from the given id.
875 var extension = this.data_.extensions.filter(function(extension) {
876 return extension.enabled && extension.id == extensionId;
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();
897 overlay.setExtensionAndShowOverlay(extensionId, extension.name,
898 extension.icon, shownCallback);
899 this.optionsShown_ = true;
902 $('overlay').addEventListener('cancelOverlay', function f() {
903 self.optionsShown_ = false;
904 $('overlay').removeEventListener('cancelOverlay', f);
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();
914 ExtensionList: ExtensionList