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;
471 row.querySelector('.enable-controls').appendChild(trash);
473 // Developer mode ////////////////////////////////////////////////////////
475 // The path, if provided by unpacked extension.
476 row.setupColumn('.load-path a:first-of-type', 'dev-loadPath', 'click',
478 chrome.send('extensionSettingsShowPath', [String(extension.id)]);
482 // Maintain the order that nodes should be in when creating as well as
483 // when adding only one new row.
484 this.insertBefore(row, nextNode);
485 this.updateNode_(extension, row);
489 * Updates an HTML element for the extension metadata given in |extension|.
490 * @param {!ExtensionData} extension A dictionary of extension metadata.
491 * @param {!ExtensionFocusRow} row The node that is being updated.
494 updateNode_: function(extension, row) {
495 var isActive = extension.enabled && !extension.terminated;
497 row.classList.toggle('inactive-extension', !isActive);
499 // Hack to keep the closure compiler happy about |remove|.
500 // TODO(hcarmona): Remove this hack when the closure compiler is updated.
501 var node = /** @type {Element} */ (row);
502 node.classList.remove('policy-controlled', 'may-not-modify',
505 if (extension.managedInstall) {
506 classes.push('policy-controlled', 'may-not-modify');
507 } else if (extension.dependentExtensions.length > 0) {
508 classes.push('may-not-remove', 'may-not-modify');
509 } else if (extension.recommendedInstall) {
510 classes.push('may-not-remove');
511 } else if (extension.suspiciousInstall ||
512 extension.corruptInstall ||
513 extension.updateRequiredByPolicy) {
514 classes.push('may-not-modify');
516 row.classList.add.apply(row.classList, classes);
518 row.classList.toggle('extension-highlight',
519 row.id == this.getIdQueryParam_());
521 var item = row.querySelector('.extension-list-item');
522 // Prevent the image cache of extension icon by using the reloaded
523 // timestamp as a query string. The timestamp is recorded when the user
524 // clicks the 'Reload' link. http://crbug.com/159302.
525 if (extensionReloadedTimestamp[extension.id]) {
526 item.style.backgroundImage =
527 'url(' + extension.icon + '?' +
528 extensionReloadedTimestamp[extension.id] + ')';
530 item.style.backgroundImage = 'url(' + extension.icon + ')';
533 this.setText_(row, '.extension-title', extension.name);
534 this.setText_(row, '.extension-version', extension.version);
535 this.setText_(row, '.location-text', extension.locationText);
536 this.setText_(row, '.blacklist-text', extension.blacklistText);
537 this.setText_(row, '.extension-description', extension.description);
539 // The 'Show Browser Action' button.
540 this.updateVisibility_(row, '.show-button',
541 isActive && extension.enable_show_button);
543 // The 'allow in incognito' checkbox.
544 this.updateVisibility_(row, '.incognito-control',
545 isActive && this.data_.incognitoAvailable,
547 var incognito = item.querySelector('input');
548 incognito.disabled = !extension.incognitoCanBeEnabled;
549 incognito.checked = extension.enabledIncognito;
552 // Hide butterBar if incognito is not enabled for the extension.
553 var butterBar = row.querySelector('.butter-bar');
554 butterBar.hidden = butterBar.hidden || !extension.enabledIncognito;
556 // The 'collect errors' checkbox. This should only be visible if the
557 // error console is enabled - we can detect this by the existence of the
558 // |errorCollectionEnabled| property.
559 this.updateVisibility_(row, '.error-collection-control',
560 isActive && extension.wantsErrorCollection,
562 item.querySelector('input').checked = extension.errorCollectionEnabled;
565 // The 'allow on all urls' checkbox. This should only be visible if
566 // active script restrictions are enabled. If they are not enabled, no
567 // extensions should want all urls.
568 this.updateVisibility_(row, '.all-urls-control',
569 isActive && extension.showAllUrls, function(item) {
570 item.querySelector('input').checked = extension.allowAllUrls;
573 // The 'allow file:// access' checkbox.
574 this.updateVisibility_(row, '.file-access-control',
575 isActive && extension.wantsFileAccess,
577 item.querySelector('input').checked = extension.allowFileAccess;
580 // The 'Options' button or link, depending on its behaviour.
581 var optionsEnabled = extension.enabled && !!extension.optionsUrl;
582 this.updateVisibility_(row, '.options-link', optionsEnabled &&
583 extension.optionsOpenInTab);
584 this.updateVisibility_(row, '.options-button', optionsEnabled &&
585 !extension.optionsOpenInTab);
587 // The 'View in Web Store/View Web Site' link.
588 var siteLinkEnabled = !!extension.homepageUrl &&
589 !extension.enableExtensionInfoDialog;
590 this.updateVisibility_(row, '.site-link', siteLinkEnabled,
592 item.href = extension.homepageUrl;
593 item.textContent = loadTimeData.getString(
594 extension.homepageProvided ? 'extensionSettingsVisitWebsite' :
595 'extensionSettingsVisitWebStore');
598 // The 'Reload' link.
599 this.updateVisibility_(row, '.reload-link', extension.allow_reload);
601 // The 'Launch' link.
602 this.updateVisibility_(row, '.launch-link', extension.allow_reload &&
603 extension.is_platform_app);
605 // The 'Reload' terminated link.
606 var isTerminated = extension.terminated;
607 this.updateVisibility_(row, '.terminated-reload-link', isTerminated);
609 // The 'Repair' corrupted link.
610 var canRepair = !isTerminated && extension.corruptInstall &&
611 extension.isFromStore;
612 this.updateVisibility_(row, '.corrupted-repair-button', canRepair);
614 // The 'Enabled' checkbox.
615 var isOK = !isTerminated && !canRepair;
616 this.updateVisibility_(row, '.enable-checkbox', isOK, function(item) {
617 var enableCheckboxDisabled = extension.managedInstall ||
618 extension.suspiciousInstall ||
619 extension.corruptInstall ||
620 extension.updateRequiredByPolicy ||
621 extension.installedByCustodian ||
622 extension.dependentExtensions.length > 0;
623 item.querySelector('input').disabled = enableCheckboxDisabled;
624 item.querySelector('input').checked = extension.enabled;
627 // Button for extensions controlled by policy.
628 var controlNode = row.querySelector('.enable-controls');
630 controlNode.querySelector('.controlled-extension-indicator');
631 var needsIndicator = isOK && extension.managedInstall;
633 if (needsIndicator && !indicator) {
634 indicator = new cr.ui.ControlledIndicator();
635 indicator.classList.add('controlled-extension-indicator');
636 indicator.setAttribute('controlled-by', 'policy');
637 var textPolicy = extension.policyText || '';
638 indicator.setAttribute('textpolicy', textPolicy);
639 indicator.image.setAttribute('aria-label', textPolicy);
640 controlNode.appendChild(indicator);
641 indicator.querySelector('div').setAttribute('column-type',
643 } else if (!needsIndicator && indicator) {
644 controlNode.removeChild(indicator);
647 // Developer mode ////////////////////////////////////////////////////////
649 // First we have the id.
650 var idLabel = row.querySelector('.extension-id');
651 idLabel.textContent = ' ' + extension.id;
653 // Then the path, if provided by unpacked extension.
654 this.updateVisibility_(row, '.load-path', extension.isUnpacked,
656 item.querySelector('a:first-of-type').textContent =
657 ' ' + extension.prettifiedPath;
660 // Then the 'managed, cannot uninstall/disable' message.
661 // We would like to hide managed installed message since this
662 // extension is disabled.
663 var isRequired = extension.managedInstall || extension.recommendedInstall;
664 this.updateVisibility_(row, '.managed-message', isRequired &&
665 !extension.updateRequiredByPolicy);
667 // Then the 'This isn't from the webstore, looks suspicious' message.
668 this.updateVisibility_(row, '.suspicious-install-message', !isRequired &&
669 extension.suspiciousInstall);
671 // Then the 'This is a corrupt extension' message.
672 this.updateVisibility_(row, '.corrupt-install-message', !isRequired &&
673 extension.corruptInstall);
675 // Then the 'An update required by enterprise policy' message. Note that
676 // a force-installed extension might be disabled due to being outdated
678 this.updateVisibility_(row, '.update-required-message',
679 extension.updateRequiredByPolicy);
681 // The 'following extensions depend on this extension' list.
682 var hasDependents = extension.dependentExtensions.length > 0;
683 row.classList.toggle('developer-extras', hasDependents);
684 this.updateVisibility_(row, '.dependent-extensions-message',
685 hasDependents, function(item) {
686 var dependentList = item.querySelector('ul');
687 dependentList.textContent = '';
688 var dependentTemplate = $('template-collection').querySelector(
689 '.dependent-list-item');
690 extension.dependentExtensions.forEach(function(elem) {
691 var depNode = dependentTemplate.cloneNode(true);
692 depNode.querySelector('.dep-extension-title').textContent = elem.name;
693 depNode.querySelector('.dep-extension-id').textContent = elem.id;
694 dependentList.appendChild(depNode);
699 this.updateVisibility_(row, '.active-views', extension.views.length > 0,
701 var link = item.querySelector('a');
703 // Link needs to be an only child before the list is updated.
704 while (link.nextElementSibling)
705 item.removeChild(link.nextElementSibling);
707 // Link needs to be cleaned up if it was used before.
708 link.textContent = '';
709 if (link.clickHandler)
710 link.removeEventListener('click', link.clickHandler);
712 extension.views.forEach(function(view, i) {
713 var displayName = view.generatedBackgroundPage ?
714 loadTimeData.getString('backgroundPage') : view.path;
715 var label = displayName +
717 ' ' + loadTimeData.getString('viewIncognito') : '') +
718 (view.renderProcessId == -1 ?
719 ' ' + loadTimeData.getString('viewInactive') : '');
720 link.textContent = label;
721 link.clickHandler = function(e) {
722 // TODO(estade): remove conversion to string?
723 chrome.send('extensionSettingsInspect', [
724 String(extension.id),
725 String(view.renderProcessId),
726 String(view.renderViewId),
730 link.addEventListener('click', link.clickHandler);
732 if (i < extension.views.length - 1) {
733 link = link.cloneNode(true);
734 item.appendChild(link);
738 var allLinks = item.querySelectorAll('a');
739 for (var i = 0; i < allLinks.length; ++i) {
740 allLinks[i].setAttribute('column-type', 'dev-activeViews' + i);
744 // The extension warnings (describing runtime issues).
745 this.updateVisibility_(row, '.extension-warnings', !!extension.warnings,
747 var warningList = item.querySelector('ul');
748 warningList.textContent = '';
749 extension.warnings.forEach(function(warning) {
750 var li = document.createElement('li');
751 warningList.appendChild(li).innerText = warning;
755 // If the ErrorConsole is enabled, we should have manifest and/or runtime
756 // errors. Otherwise, we may have install warnings. We should not have
757 // both ErrorConsole errors and install warnings.
759 this.updateErrors_(row.querySelector('.manifest-errors'),
760 'dev-manifestErrors', extension.manifestErrors);
761 this.updateErrors_(row.querySelector('.runtime-errors'),
762 'dev-runtimeErrors', extension.runtimeErrors);
765 this.updateVisibility_(row, '.install-warnings',
766 !!extension.installWarnings, function(item) {
767 var installWarningList = item.querySelector('ul');
768 installWarningList.textContent = '';
769 if (extension.installWarnings) {
770 extension.installWarnings.forEach(function(warning) {
771 var li = document.createElement('li');
772 li.innerText = warning.message;
773 installWarningList.appendChild(li);
778 if (location.hash.substr(1) == extension.id) {
779 // Scroll beneath the fixed header so that the extension is not
781 var topScroll = row.offsetTop - $('page-header').offsetHeight;
782 var pad = parseInt(window.getComputedStyle(row, null).marginTop, 10);
784 topScroll -= pad / 2;
785 setScrollTopForDocument(document, topScroll);
788 row.updateFocusableElements();
789 this.focusGrid_.addRow(row);
793 * Updates an element's textContent.
794 * @param {Element} node Ancestor of the element specified by |query|.
795 * @param {string} query A query to select an element in |node|.
796 * @param {string} textContent
799 setText_: function(node, query, textContent) {
800 node.querySelector(query).textContent = textContent;
804 * Updates an element's visibility and calls |shownCallback| if it is
806 * @param {Element} node Ancestor of the element specified by |query|.
807 * @param {string} query A query to select an element in |node|.
808 * @param {boolean} visible Whether the element should be visible or not.
809 * @param {function(Element)=} opt_shownCallback Callback if the element is
810 * visible. The element passed in will be the element specified by
814 updateVisibility_: function(node, query, visible, opt_shownCallback) {
815 var item = assert(node.querySelector(query));
816 item.hidden = !visible;
817 if (visible && opt_shownCallback)
818 opt_shownCallback(item);
822 * Updates an element to show a list of errors.
823 * @param {Element} panel An element to hold the errors.
824 * @param {string} columnType A tag used to identify the column when
826 * @param {Array<RuntimeError>|undefined} errors The errors to be displayed.
829 updateErrors_: function(panel, columnType, errors) {
830 // TODO(hcarmona): Look into updating the ExtensionErrorList rather than
831 // rebuilding it every time.
832 panel.hidden = !errors || errors.length == 0;
833 panel.textContent = '';
839 new extensions.ExtensionErrorList(assertInstanceof(errors, Array));
841 panel.appendChild(errorList);
843 var list = errorList.getErrorListElement();
845 list.setAttribute('column-type', columnType + 'list');
847 var button = errorList.getToggleElement();
849 button.setAttribute('column-type', columnType + 'button');
853 * Opens the extension options overlay for the extension with the given id.
854 * @param {string} extensionId The id of extension whose options page should
856 * @param {boolean} scroll Whether the page should scroll to the extension
859 showEmbeddedExtensionOptions_: function(extensionId, scroll) {
860 if (this.optionsShown_)
863 // Get the extension from the given id.
864 var extension = this.data_.extensions.filter(function(extension) {
865 return extension.enabled && extension.id == extensionId;
872 this.scrollToNode_(extensionId);
874 // Add the options query string. Corner case: the 'options' query string
875 // will clobber the 'id' query string if the options link is clicked when
876 // 'id' is in the URL, or if both query strings are in the URL.
877 uber.replaceState({}, '?options=' + extensionId);
879 var overlay = extensions.ExtensionOptionsOverlay.getInstance();
880 var shownCallback = function() {
881 // This overlay doesn't get focused automatically as <extensionoptions>
882 // is created after the overlay is shown.
883 if (cr.ui.FocusOutlineManager.forDocument(document).visible)
884 overlay.setInitialFocus();
886 overlay.setExtensionAndShowOverlay(extensionId, extension.name,
887 extension.icon, shownCallback);
888 this.optionsShown_ = true;
891 $('overlay').addEventListener('cancelOverlay', function f() {
892 self.optionsShown_ = false;
893 $('overlay').removeEventListener('cancelOverlay', f);
896 // TODO(dbeam): why do we need to focus <extensionoptions> before and
897 // after its showing animation? Makes very little sense to me.
898 overlay.setInitialFocus();
902 ExtensionList.uninstallCancel = function() {
903 var trash = document.querySelector('.trash.open');
904 if (trash.classList.contains('mouse-clicked'))
906 trash.classList.remove('open');
910 ExtensionList: ExtensionList