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 cr.define('extensions', function() {
11 * @param {string} name The name of the template to clone.
12 * @return {!Element} The freshly cloned template.
14 function cloneTemplate(name) {
15 var node = $('templates').querySelector('.' + name).cloneNode(true);
16 return assertInstanceof(node, Element);
20 * @extends {HTMLElement}
23 function ExtensionWrapper() {
24 var wrapper = cloneTemplate('extension-list-item-wrapper');
25 wrapper.__proto__ = ExtensionWrapper.prototype;
30 ExtensionWrapper.prototype = {
31 __proto__: HTMLElement.prototype,
33 initialize: function() {
34 var boundary = $('extension-settings-list');
35 /** @private {!extensions.FocusRow} */
36 this.focusRow_ = new extensions.FocusRow(this, boundary);
39 /** @return {!cr.ui.FocusRow} */
40 getFocusRow: function() {
41 return this.focusRow_;
45 * Add an item to the focus row and listen for |eventType| events.
46 * @param {string} focusType A tag used to identify equivalent elements when
47 * changing focus between rows.
48 * @param {string} query A query to select the element to set up.
49 * @param {string=} opt_eventType The type of event to listen to.
50 * @param {function(Event)=} opt_handler The function that should be called
54 setupColumn: function(focusType, query, opt_eventType, opt_handler) {
55 assert(this.focusRow_.addItem(focusType, query));
58 this.querySelector(query).addEventListener(opt_eventType, opt_handler);
63 var ExtensionCommandsOverlay = extensions.ExtensionCommandsOverlay;
66 * Compares two extensions for the order they should appear in the list.
67 * @param {ExtensionInfo} a The first extension.
68 * @param {ExtensionInfo} b The second extension.
69 * returns {number} -1 if A comes before B, 1 if A comes after B, 0 if equal.
71 function compareExtensions(a, b) {
72 function compare(x, y) {
73 return x < y ? -1 : (x > y ? 1 : 0);
75 function compareLocation(x, y) {
76 if (x.location == y.location)
78 if (x.location == chrome.developerPrivate.Location.UNPACKED)
80 if (y.location == chrome.developerPrivate.Location.UNPACKED)
84 return compareLocation(a, b) ||
85 compare(a.name.toLowerCase(), b.name.toLowerCase()) ||
90 function ExtensionListDelegate() {}
92 ExtensionListDelegate.prototype = {
94 * Called when the number of extensions in the list has changed.
96 onExtensionCountChanged: assertNotReached,
100 * Creates a new list of extensions.
101 * @param {extensions.ExtensionListDelegate} delegate
103 * @extends {HTMLDivElement}
105 function ExtensionList(delegate) {
106 var div = document.createElement('div');
107 div.__proto__ = ExtensionList.prototype;
108 div.initialize(delegate);
112 ExtensionList.prototype = {
113 __proto__: HTMLDivElement.prototype,
116 * Indicates whether an embedded options page that was navigated to through
117 * the '?options=' URL query has been shown to the user. This is necessary
118 * to prevent showExtensionNodes_ from opening the options more than once.
122 optionsShown_: false,
124 /** @private {!cr.ui.FocusGrid} */
125 focusGrid_: new cr.ui.FocusGrid(),
128 * Indicates whether an uninstall dialog is being shown to prevent multiple
129 * dialogs from being displayed.
132 uninstallIsShowing_: false,
135 * Indicates whether a permissions prompt is showing.
138 permissionsPromptIsShowing_: false,
141 * Necessary to only show the butterbar once.
144 butterbarShown_: false,
147 * Whether or not any initial navigation (like scrolling to an extension,
148 * or opening an options page) has occurred.
151 didInitialNavigation_: false,
154 * Whether or not incognito mode is available.
157 incognitoAvailable_: false,
160 * Whether or not the app info dialog is enabled.
163 enableAppInfoDialog_: false,
166 * Initializes the list.
167 * @param {!extensions.ExtensionListDelegate} delegate
169 initialize: function(delegate) {
170 /** @private {!Array<ExtensionInfo>} */
171 this.extensions_ = [];
173 /** @private {!extensions.ExtensionListDelegate} */
174 this.delegate_ = delegate;
176 this.resetLoadFinished();
178 chrome.developerPrivate.onItemStateChanged.addListener(
179 function(eventData) {
180 var EventType = chrome.developerPrivate.EventType;
181 switch (eventData.event_type) {
182 case EventType.VIEW_REGISTERED:
183 case EventType.VIEW_UNREGISTERED:
184 case EventType.INSTALLED:
185 case EventType.LOADED:
186 case EventType.UNLOADED:
187 case EventType.ERROR_ADDED:
188 case EventType.ERRORS_REMOVED:
189 case EventType.PREFS_CHANGED:
190 if (eventData.extensionInfo) {
191 this.updateOrCreateWrapper_(eventData.extensionInfo);
192 this.focusGrid_.ensureRowActive();
195 case EventType.UNINSTALLED:
196 var index = this.getIndexOfExtension_(eventData.item_id);
197 this.extensions_.splice(index, 1);
198 this.removeWrapper_(getRequiredElement(eventData.item_id));
204 if (eventData.event_type == EventType.UNLOADED)
205 this.hideEmbeddedExtensionOptions_(eventData.item_id);
207 if (eventData.event_type == EventType.INSTALLED ||
208 eventData.event_type == EventType.UNINSTALLED) {
209 this.delegate_.onExtensionCountChanged();
212 if (eventData.event_type == EventType.LOADED ||
213 eventData.event_type == EventType.UNLOADED ||
214 eventData.event_type == EventType.PREFS_CHANGED ||
215 eventData.event_type == EventType.UNINSTALLED) {
216 // We update the commands overlay whenever an extension is added or
217 // removed (other updates wouldn't affect command-ly things). We
218 // need both UNLOADED and UNINSTALLED since the UNLOADED event results
219 // in an extension losing active keybindings, and UNINSTALLED can
220 // result in the "Keyboard shortcuts" link being removed.
221 ExtensionCommandsOverlay.updateExtensionsData(this.extensions_);
227 * Resets the |loadFinished| promise so that it can be used again; this
228 * is useful if the page updates and tests need to wait for it to finish.
230 resetLoadFinished: function() {
232 * |loadFinished| should be used for testing purposes and will be
233 * fulfilled when this list has finished loading the first time.
236 this.loadFinished = new Promise(function(resolve, reject) {
237 /** @private {function(?)} */
238 this.resolveLoadFinished_ = resolve;
243 * Updates the extensions on the page.
244 * @param {boolean} incognitoAvailable Whether or not incognito is allowed.
245 * @param {boolean} enableAppInfoDialog Whether or not the app info dialog
247 * @return {Promise} A promise that is resolved once the extensions data is
250 updateExtensionsData: function(incognitoAvailable, enableAppInfoDialog) {
251 // If we start to need more information about the extension configuration,
252 // consider passing in the full object from the ExtensionSettings.
253 this.incognitoAvailable_ = incognitoAvailable;
254 this.enableAppInfoDialog_ = enableAppInfoDialog;
255 /** @private {Promise} */
256 this.extensionsUpdated_ = new Promise(function(resolve, reject) {
257 chrome.developerPrivate.getExtensionsInfo(
258 {includeDisabled: true, includeTerminated: true},
259 function(extensions) {
260 // Sort in order of unpacked vs. packed, followed by name, followed by
262 extensions.sort(compareExtensions);
263 this.extensions_ = extensions;
264 this.showExtensionNodes_();
266 // We keep the commands overlay's extension info in sync, so that we
267 // don't duplicate the same querying logic there.
268 ExtensionCommandsOverlay.updateExtensionsData(this.extensions_);
272 // |resolve| is async so it's necessary to use |then| here in order to
273 // do work after other |then|s have finished. This is important so
274 // elements are visible when these updates happen.
275 this.extensionsUpdated_.then(function() {
276 this.onUpdateFinished_();
277 this.resolveLoadFinished_();
281 return this.extensionsUpdated_;
285 * Updates elements that need to be visible in order to update properly.
288 onUpdateFinished_: function() {
289 // Cannot focus or highlight a extension if there are none, and we should
290 // only scroll to a particular extension or open the options page once.
291 if (this.extensions_.length == 0 || this.didInitialNavigation_)
294 this.didInitialNavigation_ = true;
295 assert(!this.hidden);
296 assert(!this.parentElement.hidden);
298 var idToHighlight = this.getIdQueryParam_();
300 var wrapper = $(idToHighlight);
302 this.scrollToWrapper_(idToHighlight);
304 var focusRow = wrapper.getFocusRow();
305 (focusRow.getFirstFocusable('enabled') ||
306 focusRow.getFirstFocusable('remove-enterprise') ||
307 focusRow.getFirstFocusable('website') ||
308 focusRow.getFirstFocusable('details')).focus();
312 var idToOpenOptions = this.getOptionsQueryParam_();
313 if (idToOpenOptions && $(idToOpenOptions))
314 this.showEmbeddedExtensionOptions_(idToOpenOptions, true);
317 /** @return {number} The number of extensions being displayed. */
318 getNumExtensions: function() {
319 return this.extensions_.length;
323 * @param {string} id The id of the extension.
324 * @return {number} The index of the extension with the given id.
327 getIndexOfExtension_: function(id) {
328 for (var i = 0; i < this.extensions_.length; ++i) {
329 if (this.extensions_[i].id == id)
335 getIdQueryParam_: function() {
336 return parseQueryParams(document.location)['id'];
339 getOptionsQueryParam_: function() {
340 return parseQueryParams(document.location)['options'];
344 * Creates or updates all extension items from scratch.
347 showExtensionNodes_: function() {
348 // Any node that is not updated will be removed.
351 // Iterate over the extension data and add each item to the list.
352 this.extensions_.forEach(function(extension) {
353 seenIds.push(extension.id);
354 this.updateOrCreateWrapper_(extension);
356 this.focusGrid_.ensureRowActive();
358 // Remove extensions that are no longer installed.
359 var wrappers = document.querySelectorAll(
360 '.extension-list-item-wrapper[id]');
361 Array.prototype.forEach.call(wrappers, function(wrapper) {
362 if (seenIds.indexOf(wrapper.id) < 0)
363 this.removeWrapper_(wrapper);
368 * Removes the wrapper from the DOM and updates the focused element if
370 * @param {!Element} wrapper
373 removeWrapper_: function(wrapper) {
374 // If focus is in the wrapper about to be removed, move it first. This
375 // happens when clicking the trash can to remove an extension.
376 if (wrapper.contains(document.activeElement)) {
377 var wrappers = document.querySelectorAll(
378 '.extension-list-item-wrapper[id]');
379 var index = Array.prototype.indexOf.call(wrappers, wrapper);
381 var focusableWrapper = wrappers[index + 1] || wrappers[index - 1];
382 if (focusableWrapper) {
383 var newFocusRow = focusableWrapper.getFocusRow();
384 newFocusRow.getEquivalentElement(document.activeElement).focus();
388 var focusRow = wrapper.getFocusRow();
389 this.focusGrid_.removeRow(focusRow);
390 this.focusGrid_.ensureRowActive();
393 wrapper.parentNode.removeChild(wrapper);
397 * Scrolls the page down to the extension node with the given id.
398 * @param {string} extensionId The id of the extension to scroll to.
401 scrollToWrapper_: function(extensionId) {
402 // Scroll offset should be calculated slightly higher than the actual
403 // offset of the element being scrolled to, so that it ends up not all
404 // the way at the top. That way it is clear that there are more elements
405 // above the element being scrolled to.
406 var wrapper = $(extensionId);
407 var scrollFudge = 1.2;
408 var scrollTop = wrapper.offsetTop - scrollFudge * wrapper.clientHeight;
409 setScrollTopForDocument(document, scrollTop);
413 * Synthesizes and initializes an HTML element for the extension metadata
414 * given in |extension|.
415 * @param {!ExtensionInfo} extension A dictionary of extension metadata.
416 * @param {?Element} nextWrapper The newly created wrapper will be inserted
417 * before |nextWrapper| if non-null (else it will be appended to the
421 createWrapper_: function(extension, nextWrapper) {
422 var wrapper = new ExtensionWrapper;
423 wrapper.id = extension.id;
425 // The 'Permissions' link.
426 wrapper.setupColumn('details', '.permissions-link', 'click', function(e) {
427 if (!this.permissionsPromptIsShowing_) {
428 chrome.developerPrivate.showPermissionsDialog(extension.id,
430 this.permissionsPromptIsShowing_ = false;
432 this.permissionsPromptIsShowing_ = true;
437 wrapper.setupColumn('options', '.options-button', 'click', function(e) {
438 this.showEmbeddedExtensionOptions_(extension.id, false);
442 // The 'Options' button or link, depending on its behaviour.
443 // Set an href to get the correct mouse-over appearance (link,
444 // footer) - but the actual link opening is done through developerPrivate
445 // API with a preventDefault().
446 wrapper.querySelector('.options-link').href =
447 extension.optionsPage ? extension.optionsPage.url : '';
448 wrapper.setupColumn('options', '.options-link', 'click', function(e) {
449 chrome.developerPrivate.showOptions(extension.id);
453 // The 'View in Web Store/View Web Site' link.
454 wrapper.setupColumn('website', '.site-link');
456 // The 'Launch' link.
457 wrapper.setupColumn('launch', '.launch-link', 'click', function(e) {
458 chrome.management.launchApp(extension.id);
461 // The 'Reload' link.
462 wrapper.setupColumn('localReload', '.reload-link', 'click', function(e) {
463 chrome.developerPrivate.reload(extension.id, {failQuietly: true});
466 wrapper.setupColumn('errors', '.errors-link', 'click', function(e) {
467 var extensionId = extension.id;
468 assert(this.extensions_.length > 0);
469 var newEx = this.extensions_.filter(function(e) {
470 return e.state == chrome.developerPrivate.ExtensionState.ENABLED &&
473 var errors = newEx.manifestErrors.concat(newEx.runtimeErrors);
474 extensions.ExtensionErrorOverlay.getInstance().setErrorsAndShowOverlay(
475 errors, extensionId, newEx.name);
478 wrapper.setupColumn('suspiciousLearnMore',
479 '.suspicious-install-message .learn-more-link');
481 // The path, if provided by unpacked extension.
482 wrapper.setupColumn('loadPath', '.load-path a:first-of-type', 'click',
484 chrome.developerPrivate.showPath(extension.id);
488 // The 'Show Browser Action' button.
489 wrapper.setupColumn('showButton', '.show-button', 'click', function(e) {
490 chrome.developerPrivate.updateExtensionConfiguration({
491 extensionId: extension.id,
492 showActionButton: true
496 // The 'allow in incognito' checkbox.
497 wrapper.setupColumn('incognito', '.incognito-control input', 'change',
499 var butterBar = wrapper.querySelector('.butter-bar');
500 var checked = e.target.checked;
501 if (!this.butterbarShown_) {
502 butterBar.hidden = !checked ||
504 chrome.developerPrivate.ExtensionType.HOSTED_APP;
505 this.butterbarShown_ = !butterBar.hidden;
507 butterBar.hidden = true;
509 chrome.developerPrivate.updateExtensionConfiguration({
510 extensionId: extension.id,
511 incognitoAccess: e.target.checked
515 // The 'collect errors' checkbox. This should only be visible if the
516 // error console is enabled - we can detect this by the existence of the
517 // |errorCollectionEnabled| property.
518 wrapper.setupColumn('collectErrors', '.error-collection-control input',
519 'change', function(e) {
520 chrome.developerPrivate.updateExtensionConfiguration({
521 extensionId: extension.id,
522 errorCollection: e.target.checked
526 // The 'allow on all urls' checkbox. This should only be visible if
527 // active script restrictions are enabled. If they are not enabled, no
528 // extensions should want all urls.
529 wrapper.setupColumn('allUrls', '.all-urls-control input', 'click',
531 chrome.developerPrivate.updateExtensionConfiguration({
532 extensionId: extension.id,
533 runOnAllUrls: e.target.checked
537 // The 'allow file:// access' checkbox.
538 wrapper.setupColumn('localUrls', '.file-access-control input', 'click',
540 chrome.developerPrivate.updateExtensionConfiguration({
541 extensionId: extension.id,
542 fileAccess: e.target.checked
546 // The 'Reload' terminated link.
547 wrapper.setupColumn('terminatedReload', '.terminated-reload-link',
548 'click', function(e) {
549 chrome.developerPrivate.reload(extension.id, {failQuietly: true});
552 // The 'Repair' corrupted link.
553 wrapper.setupColumn('repair', '.corrupted-repair-button', 'click',
555 chrome.developerPrivate.repairExtension(extension.id);
558 // The 'Enabled' checkbox.
559 wrapper.setupColumn('enabled', '.enable-checkbox input', 'change',
561 var checked = e.target.checked;
562 // TODO(devlin): What should we do if this fails?
563 chrome.management.setEnabled(extension.id, checked);
565 // This may seem counter-intuitive (to not set/clear the checkmark)
566 // but this page will be updated asynchronously if the extension
567 // becomes enabled/disabled. It also might not become enabled or
568 // disabled, because the user might e.g. get prompted when enabling
569 // and choose not to.
574 var trash = cloneTemplate('trash');
575 trash.title = loadTimeData.getString('extensionUninstall');
577 wrapper.querySelector('.enable-controls').appendChild(trash);
579 wrapper.setupColumn('remove-enterprise', '.trash', 'click', function(e) {
580 trash.classList.add('open');
581 trash.classList.toggle('mouse-clicked', e.detail > 0);
582 if (this.uninstallIsShowing_)
584 this.uninstallIsShowing_ = true;
585 chrome.management.uninstall(extension.id,
586 {showConfirmDialog: true},
588 // TODO(devlin): What should we do if the uninstall fails?
589 this.uninstallIsShowing_ = false;
591 if (trash.classList.contains('mouse-clicked'))
594 if (chrome.runtime.lastError) {
595 // The uninstall failed (e.g. a cancel). Allow the trash to close.
596 trash.classList.remove('open');
598 // Leave the trash open if the uninstall succeded. Otherwise it can
599 // half-close right before it's removed when the DOM is modified.
604 // Maintain the order that nodes should be in when creating as well as
605 // when adding only one new wrapper.
606 this.insertBefore(wrapper, nextWrapper);
607 this.updateWrapper_(extension, wrapper);
609 var nextRow = this.focusGrid_.getRowForRoot(nextWrapper); // May be null.
610 this.focusGrid_.addRowBefore(wrapper.getFocusRow(), nextRow);
614 * Updates an HTML element for the extension metadata given in |extension|.
615 * @param {!ExtensionInfo} extension A dictionary of extension metadata.
616 * @param {!Element} wrapper The extension wrapper element to update.
619 updateWrapper_: function(extension, wrapper) {
621 extension.state == chrome.developerPrivate.ExtensionState.ENABLED;
622 wrapper.classList.toggle('inactive-extension', !isActive);
623 wrapper.classList.remove('controlled', 'may-not-remove');
625 if (extension.controlledInfo) {
626 wrapper.classList.add('controlled');
627 } else if (!extension.userMayModify ||
628 extension.mustRemainInstalled ||
629 extension.dependentExtensions.length > 0) {
630 wrapper.classList.add('may-not-remove');
633 var item = wrapper.querySelector('.extension-list-item');
634 item.style.backgroundImage = 'url(' + extension.iconUrl + ')';
636 this.setText_(wrapper, '.extension-title', extension.name);
637 this.setText_(wrapper, '.extension-version', extension.version);
638 this.setText_(wrapper, '.location-text', extension.locationText || '');
639 this.setText_(wrapper, '.blacklist-text', extension.blacklistText || '');
640 this.setText_(wrapper, '.extension-description', extension.description);
642 // The 'Show Browser Action' button.
643 this.updateVisibility_(wrapper, '.show-button',
644 isActive && extension.actionButtonHidden);
646 // The 'allow in incognito' checkbox.
647 this.updateVisibility_(wrapper, '.incognito-control',
648 isActive && this.incognitoAvailable_,
650 var incognito = item.querySelector('input');
651 incognito.disabled = !extension.incognitoAccess.isEnabled;
652 incognito.checked = extension.incognitoAccess.isActive;
655 // Hide butterBar if incognito is not enabled for the extension.
656 var butterBar = wrapper.querySelector('.butter-bar');
658 butterBar.hidden || !extension.incognitoAccess.isEnabled;
660 // The 'collect errors' checkbox. This should only be visible if the
661 // error console is enabled - we can detect this by the existence of the
662 // |errorCollectionEnabled| property.
663 this.updateVisibility_(
664 wrapper, '.error-collection-control',
665 isActive && extension.errorCollection.isEnabled,
667 item.querySelector('input').checked =
668 extension.errorCollection.isActive;
671 // The 'allow on all urls' checkbox. This should only be visible if
672 // active script restrictions are enabled. If they are not enabled, no
673 // extensions should want all urls.
674 this.updateVisibility_(
675 wrapper, '.all-urls-control',
676 isActive && extension.runOnAllUrls.isEnabled,
678 item.querySelector('input').checked = extension.runOnAllUrls.isActive;
681 // The 'allow file:// access' checkbox.
682 this.updateVisibility_(wrapper, '.file-access-control',
683 isActive && extension.fileAccess.isEnabled,
685 item.querySelector('input').checked = extension.fileAccess.isActive;
688 // The 'Options' button or link, depending on its behaviour.
689 var optionsEnabled = isActive && !!extension.optionsPage;
690 this.updateVisibility_(wrapper, '.options-link', optionsEnabled &&
691 extension.optionsPage.openInTab);
692 this.updateVisibility_(wrapper, '.options-button', optionsEnabled &&
693 !extension.optionsPage.openInTab);
695 // The 'View in Web Store/View Web Site' link.
696 var siteLinkEnabled = !!extension.homePage.url &&
697 !this.enableAppInfoDialog_;
698 this.updateVisibility_(wrapper, '.site-link', siteLinkEnabled,
700 item.href = extension.homePage.url;
701 item.textContent = loadTimeData.getString(
702 extension.homePage.specified ? 'extensionSettingsVisitWebsite' :
703 'extensionSettingsVisitWebStore');
707 extension.location == chrome.developerPrivate.Location.UNPACKED;
708 // The 'Reload' link.
709 this.updateVisibility_(wrapper, '.reload-link', isUnpacked);
711 // The 'Launch' link.
712 this.updateVisibility_(
713 wrapper, '.launch-link',
714 isUnpacked && extension.type ==
715 chrome.developerPrivate.ExtensionType.PLATFORM_APP);
717 // The 'Errors' link.
718 var hasErrors = extension.runtimeErrors.length > 0 ||
719 extension.manifestErrors.length > 0;
720 this.updateVisibility_(wrapper, '.errors-link', hasErrors,
722 var Level = chrome.developerPrivate.ErrorLevel;
725 map[Level.LOG] = {weight: 0, name: 'extension-error-info-icon'};
726 map[Level.WARN] = {weight: 1, name: 'extension-error-warning-icon'};
727 map[Level.ERROR] = {weight: 2, name: 'extension-error-fatal-icon'};
729 // Find the highest severity of all the errors; manifest errors all have
730 // a 'warning' level severity.
731 var highestSeverity = extension.runtimeErrors.reduce(
732 function(prev, error) {
733 return map[error.severity].weight > map[prev].weight ?
734 error.severity : prev;
735 }, extension.manifestErrors.length ? Level.WARN : Level.LOG);
737 // Adjust the class on the icon.
738 var icon = item.querySelector('.extension-error-icon');
739 // TODO(hcarmona): Populate alt text with a proper description since
740 // this icon conveys the severity of the error. (info, warning, fatal).
742 icon.className = 'extension-error-icon'; // Remove other classes.
743 icon.classList.add(map[highestSeverity].name);
746 // The 'Reload' terminated link.
748 extension.state == chrome.developerPrivate.ExtensionState.TERMINATED;
749 this.updateVisibility_(wrapper, '.terminated-reload-link', isTerminated);
751 // The 'Repair' corrupted link.
752 var canRepair = !isTerminated &&
753 extension.disableReasons.corruptInstall &&
754 extension.location ==
755 chrome.developerPrivate.Location.FROM_STORE;
756 this.updateVisibility_(wrapper, '.corrupted-repair-button', canRepair);
758 // The 'Enabled' checkbox.
759 var isOK = !isTerminated && !canRepair;
760 this.updateVisibility_(wrapper, '.enable-checkbox', isOK, function(item) {
761 var enableCheckboxDisabled =
762 !extension.userMayModify ||
763 extension.disableReasons.suspiciousInstall ||
764 extension.disableReasons.corruptInstall ||
765 extension.disableReasons.updateRequired ||
766 extension.dependentExtensions.length > 0;
767 item.querySelector('input').disabled = enableCheckboxDisabled;
768 item.querySelector('input').checked = isActive;
771 // Indicator for extensions controlled by policy.
772 var controlNode = wrapper.querySelector('.enable-controls');
774 controlNode.querySelector('.controlled-extension-indicator');
775 var needsIndicator = isOK && extension.controlledInfo;
777 if (needsIndicator && !indicator) {
778 indicator = new cr.ui.ControlledIndicator();
779 indicator.classList.add('controlled-extension-indicator');
780 var ControllerType = chrome.developerPrivate.ControllerType;
781 var controlledByStr = '';
782 switch (extension.controlledInfo.type) {
783 case ControllerType.POLICY:
784 controlledByStr = 'policy';
786 case ControllerType.CHILD_CUSTODIAN:
787 controlledByStr = 'child-custodian';
789 case ControllerType.SUPERVISED_USER_CUSTODIAN:
790 controlledByStr = 'supervised-user-custodian';
793 indicator.setAttribute('controlled-by', controlledByStr);
794 var text = extension.controlledInfo.text;
795 indicator.setAttribute('text' + controlledByStr, text);
796 indicator.image.setAttribute('aria-label', text);
797 controlNode.appendChild(indicator);
798 wrapper.setupColumn('remove-enterprise', '[controlled-by] div');
799 } else if (!needsIndicator && indicator) {
800 controlNode.removeChild(indicator);
803 // Developer mode ////////////////////////////////////////////////////////
805 // First we have the id.
806 var idLabel = wrapper.querySelector('.extension-id');
807 idLabel.textContent = ' ' + extension.id;
809 // Then the path, if provided by unpacked extension.
810 this.updateVisibility_(wrapper, '.load-path', isUnpacked,
812 item.querySelector('a:first-of-type').textContent =
813 ' ' + extension.prettifiedPath;
816 // Then the 'managed, cannot uninstall/disable' message.
817 // We would like to hide managed installed message since this
818 // extension is disabled.
820 !extension.userMayModify || extension.mustRemainInstalled;
821 this.updateVisibility_(wrapper, '.managed-message', isRequired &&
822 !extension.disableReasons.updateRequired);
824 // Then the 'This isn't from the webstore, looks suspicious' message.
825 var isSuspicious = extension.disableReasons.suspiciousInstall;
826 this.updateVisibility_(wrapper, '.suspicious-install-message',
827 !isRequired && isSuspicious);
829 // Then the 'This is a corrupt extension' message.
830 this.updateVisibility_(wrapper, '.corrupt-install-message', !isRequired &&
831 extension.disableReasons.corruptInstall);
833 // Then the 'An update required by enterprise policy' message. Note that
834 // a force-installed extension might be disabled due to being outdated
836 this.updateVisibility_(wrapper, '.update-required-message',
837 extension.disableReasons.updateRequired);
839 // The 'following extensions depend on this extension' list.
840 var hasDependents = extension.dependentExtensions.length > 0;
841 wrapper.classList.toggle('developer-extras', hasDependents);
842 this.updateVisibility_(wrapper, '.dependent-extensions-message',
843 hasDependents, function(item) {
844 var dependentList = item.querySelector('ul');
845 dependentList.textContent = '';
846 extension.dependentExtensions.forEach(function(dependentId) {
847 var dependentExtension = null;
848 for (var i = 0; i < this.extensions_.length; ++i) {
849 if (this.extensions_[i].id == dependentId) {
850 dependentExtension = this.extensions_[i];
854 if (!dependentExtension)
857 var depNode = cloneTemplate('dependent-list-item');
858 depNode.querySelector('.dep-extension-title').textContent =
859 dependentExtension.name;
860 depNode.querySelector('.dep-extension-id').textContent =
861 dependentExtension.id;
862 dependentList.appendChild(depNode);
867 this.updateVisibility_(wrapper, '.active-views',
868 extension.views.length > 0, function(item) {
869 var link = item.querySelector('a');
871 // Link needs to be an only child before the list is updated.
872 while (link.nextElementSibling)
873 item.removeChild(link.nextElementSibling);
875 // Link needs to be cleaned up if it was used before.
876 link.textContent = '';
877 if (link.clickHandler)
878 link.removeEventListener('click', link.clickHandler);
880 extension.views.forEach(function(view, i) {
881 if (view.type == chrome.developerPrivate.ViewType.EXTENSION_DIALOG ||
882 view.type == chrome.developerPrivate.ViewType.EXTENSION_POPUP) {
886 if (view.url.indexOf('chrome-extension://') == 0) {
887 var pathOffset = 'chrome-extension://'.length + 32 + 1;
888 displayName = view.url.substring(pathOffset);
889 if (displayName == '_generated_background_page.html')
890 displayName = loadTimeData.getString('backgroundPage');
892 displayName = view.url;
894 var label = displayName +
896 ' ' + loadTimeData.getString('viewIncognito') : '') +
897 (view.renderProcessId == -1 ?
898 ' ' + loadTimeData.getString('viewInactive') : '');
899 link.textContent = label;
900 link.clickHandler = function(e) {
901 chrome.developerPrivate.openDevTools({
902 extensionId: extension.id,
903 renderProcessId: view.renderProcessId,
904 renderViewId: view.renderViewId,
905 incognito: view.incognito
908 link.addEventListener('click', link.clickHandler);
910 if (i < extension.views.length - 1) {
911 link = link.cloneNode(true);
912 item.appendChild(link);
915 wrapper.setupColumn('activeView', '.active-views a:last-of-type');
919 // The extension warnings (describing runtime issues).
920 this.updateVisibility_(wrapper, '.extension-warnings',
921 extension.runtimeWarnings.length > 0,
923 var warningList = item.querySelector('ul');
924 warningList.textContent = '';
925 extension.runtimeWarnings.forEach(function(warning) {
926 var li = document.createElement('li');
927 warningList.appendChild(li).innerText = warning;
932 this.updateVisibility_(wrapper, '.install-warnings',
933 extension.installWarnings.length > 0,
935 var installWarningList = item.querySelector('ul');
936 installWarningList.textContent = '';
937 if (extension.installWarnings) {
938 extension.installWarnings.forEach(function(warning) {
939 var li = document.createElement('li');
940 li.innerText = warning;
941 installWarningList.appendChild(li);
946 if (location.hash.substr(1) == extension.id) {
947 // Scroll beneath the fixed header so that the extension is not
949 var topScroll = wrapper.offsetTop - $('page-header').offsetHeight;
950 var pad = parseInt(window.getComputedStyle(wrapper).marginTop, 10);
952 topScroll -= pad / 2;
953 setScrollTopForDocument(document, topScroll);
958 * Updates an element's textContent.
959 * @param {Node} node Ancestor of the element specified by |query|.
960 * @param {string} query A query to select an element in |node|.
961 * @param {string} textContent
964 setText_: function(node, query, textContent) {
965 node.querySelector(query).textContent = textContent;
969 * Updates an element's visibility and calls |shownCallback| if it is
971 * @param {Node} node Ancestor of the element specified by |query|.
972 * @param {string} query A query to select an element in |node|.
973 * @param {boolean} visible Whether the element should be visible or not.
974 * @param {function(Element)=} opt_shownCallback Callback if the element is
975 * visible. The element passed in will be the element specified by
979 updateVisibility_: function(node, query, visible, opt_shownCallback) {
980 var element = assertInstanceof(node.querySelector(query), Element);
981 element.hidden = !visible;
982 if (visible && opt_shownCallback)
983 opt_shownCallback(element);
987 * Opens the extension options overlay for the extension with the given id.
988 * @param {string} extensionId The id of extension whose options page should
990 * @param {boolean} scroll Whether the page should scroll to the extension
993 showEmbeddedExtensionOptions_: function(extensionId, scroll) {
994 if (this.optionsShown_)
997 // Get the extension from the given id.
998 var extension = this.extensions_.filter(function(extension) {
999 return extension.state ==
1000 chrome.developerPrivate.ExtensionState.ENABLED &&
1001 extension.id == extensionId;
1008 this.scrollToWrapper_(extensionId);
1010 // Add the options query string. Corner case: the 'options' query string
1011 // will clobber the 'id' query string if the options link is clicked when
1012 // 'id' is in the URL, or if both query strings are in the URL.
1013 uber.replaceState({}, '?options=' + extensionId);
1015 var overlay = extensions.ExtensionOptionsOverlay.getInstance();
1016 var shownCallback = function() {
1017 // This overlay doesn't get focused automatically as <extensionoptions>
1018 // is created after the overlay is shown.
1019 if (cr.ui.FocusOutlineManager.forDocument(document).visible)
1020 overlay.setInitialFocus();
1022 overlay.setExtensionAndShow(extensionId, extension.name,
1023 extension.iconUrl, shownCallback);
1024 this.optionsShown_ = true;
1027 $('overlay').addEventListener('cancelOverlay', function f() {
1028 self.optionsShown_ = false;
1029 $('overlay').removeEventListener('cancelOverlay', f);
1031 // Remove the options query string.
1032 uber.replaceState({}, '');
1035 // TODO(dbeam): why do we need to focus <extensionoptions> before and
1036 // after its showing animation? Makes very little sense to me.
1037 overlay.setInitialFocus();
1041 * Hides the extension options overlay for the extension with id
1042 * |extensionId|. If there is an overlay showing for a different extension,
1044 * @param {string} extensionId ID of the extension to hide.
1047 hideEmbeddedExtensionOptions_: function(extensionId) {
1048 if (!this.optionsShown_)
1051 var overlay = extensions.ExtensionOptionsOverlay.getInstance();
1052 if (overlay.getExtensionId() == extensionId)
1057 * Updates or creates a wrapper for |extension|.
1058 * @param {!ExtensionInfo} extension The information about the extension to
1062 updateOrCreateWrapper_: function(extension) {
1063 var currIndex = this.getIndexOfExtension_(extension.id);
1064 if (currIndex != -1) {
1065 // If there is a current version of the extension, update it with the
1067 this.extensions_[currIndex] = extension;
1069 // If the extension isn't found, push it back and sort. Technically, we
1070 // could optimize by inserting it at the right location, but since this
1071 // only happens on extension install, it's not worth it.
1072 this.extensions_.push(extension);
1073 this.extensions_.sort(compareExtensions);
1076 var wrapper = $(extension.id);
1078 this.updateWrapper_(extension, wrapper);
1080 var nextExt = this.extensions_[this.extensions_.indexOf(extension) + 1];
1081 this.createWrapper_(extension, nextExt ? $(nextExt.id) : null);
1087 ExtensionList: ExtensionList,
1088 ExtensionListDelegate: ExtensionListDelegate