[sql] Remove _HAS_EXCEPTIONS=0 from build info.
[chromium-blink-merge.git] / chrome / browser / resources / extensions / extension_list.js
blobd8cc1e03a9a04052b96fb971a4215e525ab9eb39
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 <include src="extension_error.js">
7 ///////////////////////////////////////////////////////////////////////////////
8 // ExtensionFocusRow:
10 /**
11 * Provides an implementation for a single column grid.
12 * @constructor
13 * @extends {cr.ui.FocusRow}
15 function ExtensionFocusRow() {}
17 /**
18 * Decorates |focusRow| so that it can be treated as a ExtensionFocusRow.
19 * @param {Element} focusRow The element that has all the columns.
20 * @param {Node} boundary Focus events are ignored outside of this node.
22 ExtensionFocusRow.decorate = function(focusRow, boundary) {
23 focusRow.__proto__ = ExtensionFocusRow.prototype;
24 focusRow.decorate(boundary);
27 ExtensionFocusRow.prototype = {
28 __proto__: cr.ui.FocusRow.prototype,
30 /** @override */
31 getEquivalentElement: function(element) {
32 if (this.focusableElements.indexOf(element) > -1)
33 return element;
35 // All elements default to another element with the same type.
36 var columnType = element.getAttribute('column-type');
37 var equivalent = this.querySelector('[column-type=' + columnType + ']');
39 if (!equivalent || !this.canAddElement_(equivalent)) {
40 var actionLinks = ['options', 'website', 'launch', 'localReload'];
41 var optionalControls = ['showButton', 'incognito', 'dev-collectErrors',
42 'allUrls', 'localUrls'];
43 var removeStyleButtons = ['trash', 'enterprise'];
44 var enableControls = ['terminatedReload', 'repair', 'enabled'];
46 if (actionLinks.indexOf(columnType) > -1)
47 equivalent = this.getFirstFocusableByType_(actionLinks);
48 else if (optionalControls.indexOf(columnType) > -1)
49 equivalent = this.getFirstFocusableByType_(optionalControls);
50 else if (removeStyleButtons.indexOf(columnType) > -1)
51 equivalent = this.getFirstFocusableByType_(removeStyleButtons);
52 else if (enableControls.indexOf(columnType) > -1)
53 equivalent = this.getFirstFocusableByType_(enableControls);
56 // Return the first focusable element if no equivalent type is found.
57 return equivalent || this.focusableElements[0];
60 /** @override */
61 makeActive: function(active) {
62 cr.ui.FocusRow.prototype.makeActive.call(this, active);
64 // Only highlight if the row has focus.
65 this.classList.toggle('extension-highlight',
66 active && this.contains(document.activeElement));
69 /** Updates the list of focusable elements. */
70 updateFocusableElements: function() {
71 this.focusableElements.length = 0;
73 var focusableCandidates = this.querySelectorAll('[column-type]');
74 for (var i = 0; i < focusableCandidates.length; ++i) {
75 var element = focusableCandidates[i];
76 if (this.canAddElement_(element))
77 this.addFocusableElement(element);
81 /**
82 * Get the first focusable element that matches a list of types.
83 * @param {Array<string>} types An array of types to match from.
84 * @return {?Element} Return the first element that matches a type in |types|.
85 * @private
87 getFirstFocusableByType_: function(types) {
88 for (var i = 0; i < this.focusableElements.length; ++i) {
89 var element = this.focusableElements[i];
90 if (types.indexOf(element.getAttribute('column-type')) > -1)
91 return element;
93 return null;
96 /**
97 * Setup a typical column in the ExtensionFocusRow. A column can be any
98 * element and should have an action when clicked/toggled. This function
99 * adds a listener and a handler for an event. Also adds the "column-type"
100 * attribute to make the element focusable in |updateFocusableElements|.
101 * @param {string} query A query to select the element to set up.
102 * @param {string} columnType A tag used to identify the column when
103 * changing focus.
104 * @param {string} eventType The type of event to listen to.
105 * @param {function(Event)} handler The function that should be called
106 * by the event.
107 * @private
109 setupColumn: function(query, columnType, eventType, handler) {
110 var element = this.querySelector(query);
111 element.addEventListener(eventType, handler);
112 element.setAttribute('column-type', columnType);
116 * @param {Element} element
117 * @return {boolean}
118 * @private
120 canAddElement_: function(element) {
121 if (!element || element.disabled)
122 return false;
124 var developerMode = $('extension-settings').classList.contains('dev-mode');
125 if (this.isDeveloperOption_(element) && !developerMode)
126 return false;
128 for (var el = element; el; el = el.parentElement) {
129 if (el.hidden)
130 return false;
133 return true;
137 * Returns true if the element should only be shown in developer mode.
138 * @param {Element} element
139 * @return {boolean}
140 * @private
142 isDeveloperOption_: function(element) {
143 return /^dev-/.test(element.getAttribute('column-type'));
147 cr.define('extensions', function() {
148 'use strict';
150 var ExtensionCommandsOverlay = extensions.ExtensionCommandsOverlay;
153 * Compares two extensions for the order they should appear in the list.
154 * @param {ExtensionInfo} a The first extension.
155 * @param {ExtensionInfo} b The second extension.
156 * returns {number} -1 if A comes before B, 1 if A comes after B, 0 if equal.
158 function compareExtensions(a, b) {
159 function compare(x, y) {
160 return x < y ? -1 : (x > y ? 1 : 0);
162 function compareLocation(x, y) {
163 if (x.location == y.location)
164 return 0;
165 if (x.location == chrome.developerPrivate.Location.UNPACKED)
166 return -1;
167 if (y.location == chrome.developerPrivate.Location.UNPACKED)
168 return 1;
169 return 0;
171 return compareLocation(a, b) ||
172 compare(a.name.toLowerCase(), b.name.toLowerCase()) ||
173 compare(a.id, b.id);
176 /** @interface */
177 function ExtensionListDelegate() {}
179 ExtensionListDelegate.prototype = {
181 * Called when the number of extensions in the list has changed.
183 onExtensionCountChanged: assertNotReached,
187 * Creates a new list of extensions.
188 * @param {extensions.ExtensionListDelegate} delegate
189 * @constructor
190 * @extends {HTMLDivElement}
192 function ExtensionList(delegate) {
193 var div = document.createElement('div');
194 div.__proto__ = ExtensionList.prototype;
195 div.initialize(delegate);
196 return div;
199 ExtensionList.prototype = {
200 __proto__: HTMLDivElement.prototype,
203 * Indicates whether an embedded options page that was navigated to through
204 * the '?options=' URL query has been shown to the user. This is necessary
205 * to prevent showExtensionNodes_ from opening the options more than once.
206 * @type {boolean}
207 * @private
209 optionsShown_: false,
211 /** @private {!cr.ui.FocusGrid} */
212 focusGrid_: new cr.ui.FocusGrid(),
215 * Indicates whether an uninstall dialog is being shown to prevent multiple
216 * dialogs from being displayed.
217 * @private {boolean}
219 uninstallIsShowing_: false,
222 * Indicates whether a permissions prompt is showing.
223 * @private {boolean}
225 permissionsPromptIsShowing_: false,
228 * Necessary to only show the butterbar once.
229 * @private {boolean}
231 butterbarShown_: false,
234 * Whether or not incognito mode is available.
235 * @private {boolean}
237 incognitoAvailable_: false,
240 * Whether or not the app info dialog is enabled.
241 * @private {boolean}
243 enableAppInfoDialog_: false,
246 * Initializes the list.
247 * @param {!extensions.ExtensionListDelegate} delegate
249 initialize: function(delegate) {
250 /** @private {!Array<ExtensionInfo>} */
251 this.extensions_ = [];
253 /** @private {!extensions.ExtensionListDelegate} */
254 this.delegate_ = delegate;
257 * |loadFinished| should be used for testing purposes and will be
258 * fulfilled when this list has finished loading the first time.
259 * @type {Promise}
260 * */
261 this.loadFinished = new Promise(function(resolve, reject) {
262 /** @private {function(?)} */
263 this.resolveLoadFinished_ = resolve;
264 }.bind(this));
266 chrome.developerPrivate.onItemStateChanged.addListener(
267 function(eventData) {
268 var EventType = chrome.developerPrivate.EventType;
269 switch (eventData.event_type) {
270 case EventType.VIEW_REGISTERED:
271 case EventType.VIEW_UNREGISTERED:
272 case EventType.INSTALLED:
273 case EventType.LOADED:
274 case EventType.UNLOADED:
275 case EventType.ERROR_ADDED:
276 case EventType.ERRORS_REMOVED:
277 case EventType.PREFS_CHANGED:
278 if (eventData.extensionInfo) {
279 this.updateExtension_(eventData.extensionInfo);
280 this.focusGrid_.ensureRowActive();
282 break;
283 case EventType.UNINSTALLED:
284 var index = this.getIndexOfExtension_(eventData.item_id);
285 this.extensions_.splice(index, 1);
286 this.removeNode_(getRequiredElement(eventData.item_id));
287 break;
288 default:
289 assertNotReached();
292 if (eventData.event_type == EventType.UNLOADED)
293 this.hideEmbeddedExtensionOptions_(eventData.item_id);
295 if (eventData.event_type == EventType.INSTALLED ||
296 eventData.event_type == EventType.UNINSTALLED) {
297 this.delegate_.onExtensionCountChanged();
300 if (eventData.event_type == EventType.LOADED ||
301 eventData.event_type == EventType.UNLOADED ||
302 eventData.event_type == EventType.PREFS_CHANGED ||
303 eventData.event_type == EventType.UNINSTALLED) {
304 // We update the commands overlay whenever an extension is added or
305 // removed (other updates wouldn't affect command-ly things). We
306 // need both UNLOADED and UNINSTALLED since the UNLOADED event results
307 // in an extension losing active keybindings, and UNINSTALLED can
308 // result in the "Keyboard shortcuts" link being removed.
309 ExtensionCommandsOverlay.updateExtensionsData(this.extensions_);
311 }.bind(this));
315 * Updates the extensions on the page.
316 * @param {boolean} incognitoAvailable Whether or not incognito is allowed.
317 * @param {boolean} enableAppInfoDialog Whether or not the app info dialog
318 * is enabled.
319 * @return {Promise} A promise that is resolved once the extensions data is
320 * fully updated.
322 updateExtensionsData: function(incognitoAvailable, enableAppInfoDialog) {
323 // If we start to need more information about the extension configuration,
324 // consider passing in the full object from the ExtensionSettings.
325 this.incognitoAvailable_ = incognitoAvailable;
326 this.enableAppInfoDialog_ = enableAppInfoDialog;
327 /** @private {Promise} */
328 this.extensionsUpdated_ = new Promise(function(resolve, reject) {
329 chrome.developerPrivate.getExtensionsInfo(
330 {includeDisabled: true, includeTerminated: true},
331 function(extensions) {
332 // Sort in order of unpacked vs. packed, followed by name, followed by
333 // id.
334 extensions.sort(compareExtensions);
335 this.extensions_ = extensions;
336 this.showExtensionNodes_();
338 // We keep the commands overlay's extension info in sync, so that we
339 // don't duplicate the same querying logic there.
340 ExtensionCommandsOverlay.updateExtensionsData(this.extensions_);
342 resolve();
344 // |resolve| is async so it's necessary to use |then| here in order to
345 // do work after other |then|s have finished. This is important so
346 // elements are visible when these updates happen.
347 this.extensionsUpdated_.then(function() {
348 this.onUpdateFinished_();
349 this.resolveLoadFinished_();
350 }.bind(this));
351 }.bind(this));
352 }.bind(this));
353 return this.extensionsUpdated_;
357 * Updates elements that need to be visible in order to update properly.
358 * @private
360 onUpdateFinished_: function() {
361 // Cannot focus or highlight a extension if there are none.
362 if (this.extensions_.length == 0)
363 return;
365 assert(!this.hidden);
366 assert(!this.parentElement.hidden);
368 this.updateFocusableElements();
370 var idToHighlight = this.getIdQueryParam_();
371 if (idToHighlight && $(idToHighlight)) {
372 this.scrollToNode_(idToHighlight);
373 this.setInitialFocus_(idToHighlight);
376 var idToOpenOptions = this.getOptionsQueryParam_();
377 if (idToOpenOptions && $(idToOpenOptions))
378 this.showEmbeddedExtensionOptions_(idToOpenOptions, true);
381 /** @return {number} The number of extensions being displayed. */
382 getNumExtensions: function() {
383 return this.extensions_.length;
387 * @param {string} id The id of the extension.
388 * @return {number} The index of the extension with the given id.
389 * @private
391 getIndexOfExtension_: function(id) {
392 for (var i = 0; i < this.extensions_.length; ++i) {
393 if (this.extensions_[i].id == id)
394 return i;
396 return -1;
399 getIdQueryParam_: function() {
400 return parseQueryParams(document.location)['id'];
403 getOptionsQueryParam_: function() {
404 return parseQueryParams(document.location)['options'];
408 * Creates or updates all extension items from scratch.
409 * @private
411 showExtensionNodes_: function() {
412 // Any node that is not updated will be removed.
413 var seenIds = [];
415 // Iterate over the extension data and add each item to the list.
416 this.extensions_.forEach(function(extension) {
417 seenIds.push(extension.id);
418 this.updateExtension_(extension);
419 }, this);
420 this.focusGrid_.ensureRowActive();
422 // Remove extensions that are no longer installed.
423 var nodes = document.querySelectorAll('.extension-list-item-wrapper[id]');
424 Array.prototype.forEach.call(nodes, function(node) {
425 if (seenIds.indexOf(node.id) < 0)
426 this.removeNode_(node);
427 }, this);
430 /** Updates each row's focusable elements without rebuilding the grid. */
431 updateFocusableElements: function() {
432 var rows = document.querySelectorAll('.extension-list-item-wrapper[id]');
433 for (var i = 0; i < rows.length; ++i) {
434 assertInstanceof(rows[i], ExtensionFocusRow).updateFocusableElements();
439 * Removes the node from the DOM, and updates the focused element if needed.
440 * @param {!HTMLElement} node
441 * @private
443 removeNode_: function(node) {
444 if (node.contains(document.activeElement)) {
445 var nodes =
446 document.querySelectorAll('.extension-list-item-wrapper[id]');
447 var index = Array.prototype.indexOf.call(nodes, node);
448 assert(index != -1);
449 var focusableNode = nodes[index + 1] || nodes[index - 1];
450 if (focusableNode)
451 focusableNode.getEquivalentElement(document.activeElement).focus();
453 node.parentNode.removeChild(node);
454 this.focusGrid_.removeRow(assertInstanceof(node, ExtensionFocusRow));
456 // Unregister the removed node from events.
457 assertInstanceof(node, ExtensionFocusRow).destroy();
459 this.focusGrid_.ensureRowActive();
463 * Scrolls the page down to the extension node with the given id.
464 * @param {string} extensionId The id of the extension to scroll to.
465 * @private
467 scrollToNode_: function(extensionId) {
468 // Scroll offset should be calculated slightly higher than the actual
469 // offset of the element being scrolled to, so that it ends up not all
470 // the way at the top. That way it is clear that there are more elements
471 // above the element being scrolled to.
472 var scrollFudge = 1.2;
473 var scrollTop = $(extensionId).offsetTop - scrollFudge *
474 $(extensionId).clientHeight;
475 setScrollTopForDocument(document, scrollTop);
479 * @param {string} extensionId The id of the extension that should have
480 * initial focus
481 * @private
483 setInitialFocus_: function(extensionId) {
484 var focusRow = assertInstanceof($(extensionId), ExtensionFocusRow);
485 var columnTypePriority = ['enabled', 'enterprise', 'website', 'details'];
486 var elementToFocus = null;
487 var elementPriority = columnTypePriority.length;
489 for (var i = 0; i < focusRow.focusableElements.length; ++i) {
490 var element = focusRow.focusableElements[i];
491 var priority =
492 columnTypePriority.indexOf(element.getAttribute('column-type'));
493 if (priority > -1 && priority < elementPriority) {
494 elementToFocus = element;
495 elementPriority = priority;
499 focusRow.getEquivalentElement(elementToFocus).focus();
503 * Synthesizes and initializes an HTML element for the extension metadata
504 * given in |extension|.
505 * @param {!ExtensionInfo} extension A dictionary of extension metadata.
506 * @param {?Element} nextNode |node| should be inserted before |nextNode|.
507 * |node| will be appended to the end if |nextNode| is null.
508 * @private
510 createNode_: function(extension, nextNode) {
511 var template = $('template-collection').querySelector(
512 '.extension-list-item-wrapper');
513 var node = template.cloneNode(true);
514 ExtensionFocusRow.decorate(node, $('extension-settings-list'));
516 var row = assertInstanceof(node, ExtensionFocusRow);
517 row.id = extension.id;
519 // The 'Show Browser Action' button.
520 row.setupColumn('.show-button', 'showButton', 'click', function(e) {
521 chrome.developerPrivate.updateExtensionConfiguration({
522 extensionId: extension.id,
523 showActionButton: true
527 // The 'allow in incognito' checkbox.
528 row.setupColumn('.incognito-control input', 'incognito', 'change',
529 function(e) {
530 var butterBar = row.querySelector('.butter-bar');
531 var checked = e.target.checked;
532 if (!this.butterbarShown_) {
533 butterBar.hidden = !checked ||
534 extension.type ==
535 chrome.developerPrivate.ExtensionType.HOSTED_APP;
536 this.butterbarShown_ = !butterBar.hidden;
537 } else {
538 butterBar.hidden = true;
540 chrome.developerPrivate.updateExtensionConfiguration({
541 extensionId: extension.id,
542 incognitoAccess: e.target.checked
544 }.bind(this));
546 // The 'collect errors' checkbox. This should only be visible if the
547 // error console is enabled - we can detect this by the existence of the
548 // |errorCollectionEnabled| property.
549 row.setupColumn('.error-collection-control input', 'dev-collectErrors',
550 'change', function(e) {
551 chrome.developerPrivate.updateExtensionConfiguration({
552 extensionId: extension.id,
553 errorCollection: e.target.checked
557 // The 'allow on all urls' checkbox. This should only be visible if
558 // active script restrictions are enabled. If they are not enabled, no
559 // extensions should want all urls.
560 row.setupColumn('.all-urls-control input', 'allUrls', 'click',
561 function(e) {
562 chrome.developerPrivate.updateExtensionConfiguration({
563 extensionId: extension.id,
564 runOnAllUrls: e.target.checked
568 // The 'allow file:// access' checkbox.
569 row.setupColumn('.file-access-control input', 'localUrls', 'click',
570 function(e) {
571 chrome.developerPrivate.updateExtensionConfiguration({
572 extensionId: extension.id,
573 fileAccess: e.target.checked
577 // The 'Options' button or link, depending on its behaviour.
578 // Set an href to get the correct mouse-over appearance (link,
579 // footer) - but the actual link opening is done through developerPrivate
580 // API with a preventDefault().
581 row.querySelector('.options-link').href =
582 extension.optionsPage ? extension.optionsPage.url : '';
583 row.setupColumn('.options-link', 'options', 'click', function(e) {
584 chrome.developerPrivate.showOptions(extension.id);
585 e.preventDefault();
588 row.setupColumn('.options-button', 'options', 'click', function(e) {
589 this.showEmbeddedExtensionOptions_(extension.id, false);
590 e.preventDefault();
591 }.bind(this));
593 // The 'View in Web Store/View Web Site' link.
594 row.querySelector('.site-link').setAttribute('column-type', 'website');
596 // The 'Permissions' link.
597 row.setupColumn('.permissions-link', 'details', 'click', function(e) {
598 if (!this.permissionsPromptIsShowing_) {
599 chrome.developerPrivate.showPermissionsDialog(extension.id,
600 function() {
601 this.permissionsPromptIsShowing_ = false;
602 }.bind(this));
603 this.permissionsPromptIsShowing_ = true;
605 e.preventDefault();
608 // The 'Reload' link.
609 row.setupColumn('.reload-link', 'localReload', 'click', function(e) {
610 chrome.developerPrivate.reload(extension.id, {failQuietly: true});
613 // The 'Launch' link.
614 row.setupColumn('.launch-link', 'launch', 'click', function(e) {
615 chrome.management.launchApp(extension.id);
618 row.setupColumn('.errors-link', 'errors', 'click', function(e) {
619 var extensionId = extension.id;
620 assert(this.extensions_.length > 0);
621 var newEx = this.extensions_.filter(function(e) {
622 return e.state == chrome.developerPrivate.ExtensionState.ENABLED &&
623 e.id == extensionId;
624 })[0];
625 var errors = newEx.manifestErrors.concat(newEx.runtimeErrors);
626 extensions.ExtensionErrorOverlay.getInstance().setErrorsAndShowOverlay(
627 errors, extensionId, newEx.name);
628 }.bind(this));
630 // The 'Reload' terminated link.
631 row.setupColumn('.terminated-reload-link', 'terminatedReload', 'click',
632 function(e) {
633 chrome.developerPrivate.reload(extension.id, {failQuietly: true});
636 // The 'Repair' corrupted link.
637 row.setupColumn('.corrupted-repair-button', 'repair', 'click',
638 function(e) {
639 chrome.developerPrivate.repairExtension(extension.id);
642 // The 'Enabled' checkbox.
643 row.setupColumn('.enable-checkbox input', 'enabled', 'change',
644 function(e) {
645 var checked = e.target.checked;
646 // TODO(devlin): What should we do if this fails?
647 chrome.management.setEnabled(extension.id, checked);
649 // This may seem counter-intuitive (to not set/clear the checkmark)
650 // but this page will be updated asynchronously if the extension
651 // becomes enabled/disabled. It also might not become enabled or
652 // disabled, because the user might e.g. get prompted when enabling
653 // and choose not to.
654 e.preventDefault();
657 // 'Remove' button.
658 var trashTemplate = $('template-collection').querySelector('.trash');
659 var trash = trashTemplate.cloneNode(true);
660 trash.title = loadTimeData.getString('extensionUninstall');
661 trash.setAttribute('column-type', 'trash');
662 trash.addEventListener('click', function(e) {
663 trash.classList.add('open');
664 trash.classList.toggle('mouse-clicked', e.detail > 0);
665 if (this.uninstallIsShowing_)
666 return;
667 this.uninstallIsShowing_ = true;
668 chrome.management.uninstall(extension.id,
669 {showConfirmDialog: true},
670 function() {
671 // TODO(devlin): What should we do if the uninstall fails?
672 this.uninstallIsShowing_ = false;
674 if (trash.classList.contains('mouse-clicked'))
675 trash.blur();
677 if (chrome.runtime.lastError) {
678 // The uninstall failed (e.g. a cancel). Allow the trash to close.
679 trash.classList.remove('open');
680 } else {
681 // Leave the trash open if the uninstall succeded. Otherwise it can
682 // half-close right before it's removed when the DOM is modified.
684 }.bind(this));
685 }.bind(this));
686 row.querySelector('.enable-controls').appendChild(trash);
688 // Developer mode ////////////////////////////////////////////////////////
690 // The path, if provided by unpacked extension.
691 row.setupColumn('.load-path a:first-of-type', 'dev-loadPath', 'click',
692 function(e) {
693 chrome.developerPrivate.showPath(extension.id);
694 e.preventDefault();
697 // Maintain the order that nodes should be in when creating as well as
698 // when adding only one new row.
699 this.insertBefore(row, nextNode);
700 this.updateNode_(extension, row);
702 var nextRow = null;
703 if (nextNode)
704 nextRow = assertInstanceof(nextNode, ExtensionFocusRow);
706 this.focusGrid_.addRowBefore(row, nextRow);
710 * Updates an HTML element for the extension metadata given in |extension|.
711 * @param {!ExtensionInfo} extension A dictionary of extension metadata.
712 * @param {!ExtensionFocusRow} row The node that is being updated.
713 * @private
715 updateNode_: function(extension, row) {
716 var isActive =
717 extension.state == chrome.developerPrivate.ExtensionState.ENABLED;
718 row.classList.toggle('inactive-extension', !isActive);
720 // Hack to keep the closure compiler happy about |remove|.
721 // TODO(hcarmona): Remove this hack when the closure compiler is updated.
722 var node = /** @type {Element} */ (row);
723 node.classList.remove('controlled', 'may-not-remove');
724 var classes = [];
725 if (extension.controlledInfo) {
726 classes.push('controlled');
727 } else if (!extension.userMayModify ||
728 extension.mustRemainInstalled ||
729 extension.dependentExtensions.length > 0) {
730 classes.push('may-not-remove');
732 row.classList.add.apply(row.classList, classes);
734 var item = row.querySelector('.extension-list-item');
735 item.style.backgroundImage = 'url(' + extension.iconUrl + ')';
737 this.setText_(row, '.extension-title', extension.name);
738 this.setText_(row, '.extension-version', extension.version);
739 this.setText_(row, '.location-text', extension.locationText || '');
740 this.setText_(row, '.blacklist-text', extension.blacklistText || '');
741 this.setText_(row, '.extension-description', extension.description);
743 // The 'Show Browser Action' button.
744 this.updateVisibility_(row, '.show-button',
745 isActive && extension.actionButtonHidden);
747 // The 'allow in incognito' checkbox.
748 this.updateVisibility_(row, '.incognito-control',
749 isActive && this.incognitoAvailable_,
750 function(item) {
751 var incognito = item.querySelector('input');
752 incognito.disabled = !extension.incognitoAccess.isEnabled;
753 incognito.checked = extension.incognitoAccess.isActive;
756 // Hide butterBar if incognito is not enabled for the extension.
757 var butterBar = row.querySelector('.butter-bar');
758 butterBar.hidden =
759 butterBar.hidden || !extension.incognitoAccess.isEnabled;
761 // The 'collect errors' checkbox. This should only be visible if the
762 // error console is enabled - we can detect this by the existence of the
763 // |errorCollectionEnabled| property.
764 this.updateVisibility_(
765 row, '.error-collection-control',
766 isActive && extension.errorCollection.isEnabled,
767 function(item) {
768 item.querySelector('input').checked =
769 extension.errorCollection.isActive;
772 // The 'allow on all urls' checkbox. This should only be visible if
773 // active script restrictions are enabled. If they are not enabled, no
774 // extensions should want all urls.
775 this.updateVisibility_(
776 row, '.all-urls-control',
777 isActive && extension.runOnAllUrls.isEnabled,
778 function(item) {
779 item.querySelector('input').checked = extension.runOnAllUrls.isActive;
782 // The 'allow file:// access' checkbox.
783 this.updateVisibility_(row, '.file-access-control',
784 isActive && extension.fileAccess.isEnabled,
785 function(item) {
786 item.querySelector('input').checked = extension.fileAccess.isActive;
789 // The 'Options' button or link, depending on its behaviour.
790 var optionsEnabled = isActive && !!extension.optionsPage;
791 this.updateVisibility_(row, '.options-link', optionsEnabled &&
792 extension.optionsPage.openInTab);
793 this.updateVisibility_(row, '.options-button', optionsEnabled &&
794 !extension.optionsPage.openInTab);
796 // The 'View in Web Store/View Web Site' link.
797 var siteLinkEnabled = !!extension.homePage.url &&
798 !this.enableAppInfoDialog_;
799 this.updateVisibility_(row, '.site-link', siteLinkEnabled,
800 function(item) {
801 item.href = extension.homePage.url;
802 item.textContent = loadTimeData.getString(
803 extension.homePage.specified ? 'extensionSettingsVisitWebsite' :
804 'extensionSettingsVisitWebStore');
807 var isUnpacked =
808 extension.location == chrome.developerPrivate.Location.UNPACKED;
809 // The 'Reload' link.
810 this.updateVisibility_(row, '.reload-link', isUnpacked);
812 // The 'Launch' link.
813 this.updateVisibility_(
814 row, '.launch-link',
815 isUnpacked && extension.type ==
816 chrome.developerPrivate.ExtensionType.PLATFORM_APP);
818 // The 'Errors' link.
819 var hasErrors = extension.runtimeErrors.length > 0 ||
820 extension.manifestErrors.length > 0;
821 this.updateVisibility_(row, '.errors-link', hasErrors, function(item) {
822 var Level = chrome.developerPrivate.ErrorLevel;
824 var map = {};
825 map[Level.LOG] = {weight: 0, name: 'extension-error-info-icon'};
826 map[Level.WARN] = {weight: 1, name: 'extension-error-warning-icon'};
827 map[Level.ERROR] = {weight: 2, name: 'extension-error-fatal-icon'};
829 // Find the highest severity of all the errors; manifest errors all have
830 // a 'warning' level severity.
831 var highestSeverity = extension.runtimeErrors.reduce(
832 function(prev, error) {
833 return map[error.severity].weight > map[prev].weight ?
834 error.severity : prev;
835 }, extension.manifestErrors.length ? Level.WARN : Level.LOG);
837 // Adjust the class on the icon.
838 var icon = item.querySelector('.extension-error-icon');
839 // TODO(hcarmona): Populate alt text with a proper description since
840 // this icon conveys the severity of the error. (info, warning, fatal).
841 icon.alt = '';
842 icon.className = 'extension-error-icon'; // Remove other classes.
843 icon.classList.add(map[highestSeverity].name);
846 // The 'Reload' terminated link.
847 var isTerminated =
848 extension.state == chrome.developerPrivate.ExtensionState.TERMINATED;
849 this.updateVisibility_(row, '.terminated-reload-link', isTerminated);
851 // The 'Repair' corrupted link.
852 var canRepair = !isTerminated &&
853 extension.disableReasons.corruptInstall &&
854 extension.location ==
855 chrome.developerPrivate.Location.FROM_STORE;
856 this.updateVisibility_(row, '.corrupted-repair-button', canRepair);
858 // The 'Enabled' checkbox.
859 var isOK = !isTerminated && !canRepair;
860 this.updateVisibility_(row, '.enable-checkbox', isOK, function(item) {
861 var enableCheckboxDisabled =
862 !extension.userMayModify ||
863 extension.disableReasons.suspiciousInstall ||
864 extension.disableReasons.corruptInstall ||
865 extension.disableReasons.updateRequired ||
866 extension.dependentExtensions.length > 0;
867 item.querySelector('input').disabled = enableCheckboxDisabled;
868 item.querySelector('input').checked = isActive;
871 // Indicator for extensions controlled by policy.
872 var controlNode = row.querySelector('.enable-controls');
873 var indicator =
874 controlNode.querySelector('.controlled-extension-indicator');
875 var needsIndicator = isOK && extension.controlledInfo;
877 if (needsIndicator && !indicator) {
878 indicator = new cr.ui.ControlledIndicator();
879 indicator.classList.add('controlled-extension-indicator');
880 var ControllerType = chrome.developerPrivate.ControllerType;
881 var controlledByStr = '';
882 switch (extension.controlledInfo.type) {
883 case ControllerType.POLICY:
884 controlledByStr = 'policy';
885 break;
886 case ControllerType.CHILD_CUSTODIAN:
887 controlledByStr = 'child-custodian';
888 break;
889 case ControllerType.SUPERVISED_USER_CUSTODIAN:
890 controlledByStr = 'supervised-user-custodian';
891 break;
893 indicator.setAttribute('controlled-by', controlledByStr);
894 var text = extension.controlledInfo.text;
895 indicator.setAttribute('text' + controlledByStr, text);
896 indicator.image.setAttribute('aria-label', text);
897 controlNode.appendChild(indicator);
898 indicator.querySelector('div').setAttribute('column-type',
899 'enterprise');
900 } else if (!needsIndicator && indicator) {
901 controlNode.removeChild(indicator);
904 // Developer mode ////////////////////////////////////////////////////////
906 // First we have the id.
907 var idLabel = row.querySelector('.extension-id');
908 idLabel.textContent = ' ' + extension.id;
910 // Then the path, if provided by unpacked extension.
911 this.updateVisibility_(row, '.load-path', isUnpacked,
912 function(item) {
913 item.querySelector('a:first-of-type').textContent =
914 ' ' + extension.prettifiedPath;
917 // Then the 'managed, cannot uninstall/disable' message.
918 // We would like to hide managed installed message since this
919 // extension is disabled.
920 var isRequired =
921 !extension.userMayModify || extension.mustRemainInstalled;
922 this.updateVisibility_(row, '.managed-message', isRequired &&
923 !extension.disableReasons.updateRequired);
925 // Then the 'This isn't from the webstore, looks suspicious' message.
926 this.updateVisibility_(row, '.suspicious-install-message', !isRequired &&
927 extension.disableReasons.suspiciousInstall);
929 // Then the 'This is a corrupt extension' message.
930 this.updateVisibility_(row, '.corrupt-install-message', !isRequired &&
931 extension.disableReasons.corruptInstall);
933 // Then the 'An update required by enterprise policy' message. Note that
934 // a force-installed extension might be disabled due to being outdated
935 // as well.
936 this.updateVisibility_(row, '.update-required-message',
937 extension.disableReasons.updateRequired);
939 // The 'following extensions depend on this extension' list.
940 var hasDependents = extension.dependentExtensions.length > 0;
941 row.classList.toggle('developer-extras', hasDependents);
942 this.updateVisibility_(row, '.dependent-extensions-message',
943 hasDependents, function(item) {
944 var dependentList = item.querySelector('ul');
945 dependentList.textContent = '';
946 var dependentTemplate = $('template-collection').querySelector(
947 '.dependent-list-item');
948 extension.dependentExtensions.forEach(function(dependentId) {
949 var dependentExtension = null;
950 for (var i = 0; i < this.extensions_.length; ++i) {
951 if (this.extensions_[i].id == dependentId) {
952 dependentExtension = this.extensions_[i];
953 break;
956 if (!dependentExtension)
957 return;
959 var depNode = dependentTemplate.cloneNode(true);
960 depNode.querySelector('.dep-extension-title').textContent =
961 dependentExtension.name;
962 depNode.querySelector('.dep-extension-id').textContent =
963 dependentExtension.id;
964 dependentList.appendChild(depNode);
965 }, this);
966 }.bind(this));
968 // The active views.
969 this.updateVisibility_(row, '.active-views', extension.views.length > 0,
970 function(item) {
971 var link = item.querySelector('a');
973 // Link needs to be an only child before the list is updated.
974 while (link.nextElementSibling)
975 item.removeChild(link.nextElementSibling);
977 // Link needs to be cleaned up if it was used before.
978 link.textContent = '';
979 if (link.clickHandler)
980 link.removeEventListener('click', link.clickHandler);
982 extension.views.forEach(function(view, i) {
983 if (view.type == chrome.developerPrivate.ViewType.EXTENSION_DIALOG ||
984 view.type == chrome.developerPrivate.ViewType.EXTENSION_POPUP) {
985 return;
987 var displayName;
988 if (view.url.indexOf('chrome-extension://') == 0) {
989 var pathOffset = 'chrome-extension://'.length + 32 + 1;
990 displayName = view.url.substring(pathOffset);
991 if (displayName == '_generated_background_page.html')
992 displayName = loadTimeData.getString('backgroundPage');
993 } else {
994 displayName = view.url;
996 var label = displayName +
997 (view.incognito ?
998 ' ' + loadTimeData.getString('viewIncognito') : '') +
999 (view.renderProcessId == -1 ?
1000 ' ' + loadTimeData.getString('viewInactive') : '');
1001 link.textContent = label;
1002 link.clickHandler = function(e) {
1003 chrome.developerPrivate.openDevTools({
1004 extensionId: extension.id,
1005 renderProcessId: view.renderProcessId,
1006 renderViewId: view.renderViewId,
1007 incognito: view.incognito
1010 link.addEventListener('click', link.clickHandler);
1012 if (i < extension.views.length - 1) {
1013 link = link.cloneNode(true);
1014 item.appendChild(link);
1018 var allLinks = item.querySelectorAll('a');
1019 for (var i = 0; i < allLinks.length; ++i) {
1020 allLinks[i].setAttribute('column-type', 'dev-activeViews' + i);
1024 // The extension warnings (describing runtime issues).
1025 this.updateVisibility_(row, '.extension-warnings',
1026 extension.runtimeWarnings.length > 0,
1027 function(item) {
1028 var warningList = item.querySelector('ul');
1029 warningList.textContent = '';
1030 extension.runtimeWarnings.forEach(function(warning) {
1031 var li = document.createElement('li');
1032 warningList.appendChild(li).innerText = warning;
1036 // Install warnings.
1037 this.updateVisibility_(row, '.install-warnings',
1038 extension.installWarnings.length > 0,
1039 function(item) {
1040 var installWarningList = item.querySelector('ul');
1041 installWarningList.textContent = '';
1042 if (extension.installWarnings) {
1043 extension.installWarnings.forEach(function(warning) {
1044 var li = document.createElement('li');
1045 li.innerText = warning;
1046 installWarningList.appendChild(li);
1051 if (location.hash.substr(1) == extension.id) {
1052 // Scroll beneath the fixed header so that the extension is not
1053 // obscured.
1054 var topScroll = row.offsetTop - $('page-header').offsetHeight;
1055 var pad = parseInt(window.getComputedStyle(row, null).marginTop, 10);
1056 if (!isNaN(pad))
1057 topScroll -= pad / 2;
1058 setScrollTopForDocument(document, topScroll);
1061 row.updateFocusableElements();
1065 * Updates an element's textContent.
1066 * @param {Element} node Ancestor of the element specified by |query|.
1067 * @param {string} query A query to select an element in |node|.
1068 * @param {string} textContent
1069 * @private
1071 setText_: function(node, query, textContent) {
1072 node.querySelector(query).textContent = textContent;
1076 * Updates an element's visibility and calls |shownCallback| if it is
1077 * visible.
1078 * @param {Element} node Ancestor of the element specified by |query|.
1079 * @param {string} query A query to select an element in |node|.
1080 * @param {boolean} visible Whether the element should be visible or not.
1081 * @param {function(Element)=} opt_shownCallback Callback if the element is
1082 * visible. The element passed in will be the element specified by
1083 * |query|.
1084 * @private
1086 updateVisibility_: function(node, query, visible, opt_shownCallback) {
1087 var item = assert(node.querySelector(query));
1088 item.hidden = !visible;
1089 if (visible && opt_shownCallback)
1090 opt_shownCallback(item);
1094 * Opens the extension options overlay for the extension with the given id.
1095 * @param {string} extensionId The id of extension whose options page should
1096 * be displayed.
1097 * @param {boolean} scroll Whether the page should scroll to the extension
1098 * @private
1100 showEmbeddedExtensionOptions_: function(extensionId, scroll) {
1101 if (this.optionsShown_)
1102 return;
1104 // Get the extension from the given id.
1105 var extension = this.extensions_.filter(function(extension) {
1106 return extension.state ==
1107 chrome.developerPrivate.ExtensionState.ENABLED &&
1108 extension.id == extensionId;
1109 })[0];
1111 if (!extension)
1112 return;
1114 if (scroll)
1115 this.scrollToNode_(extensionId);
1117 // Add the options query string. Corner case: the 'options' query string
1118 // will clobber the 'id' query string if the options link is clicked when
1119 // 'id' is in the URL, or if both query strings are in the URL.
1120 uber.replaceState({}, '?options=' + extensionId);
1122 var overlay = extensions.ExtensionOptionsOverlay.getInstance();
1123 var shownCallback = function() {
1124 // This overlay doesn't get focused automatically as <extensionoptions>
1125 // is created after the overlay is shown.
1126 if (cr.ui.FocusOutlineManager.forDocument(document).visible)
1127 overlay.setInitialFocus();
1129 overlay.setExtensionAndShow(extensionId, extension.name,
1130 extension.iconUrl, shownCallback);
1131 this.optionsShown_ = true;
1133 var self = this;
1134 $('overlay').addEventListener('cancelOverlay', function f() {
1135 self.optionsShown_ = false;
1136 $('overlay').removeEventListener('cancelOverlay', f);
1138 // Remove the options query string.
1139 uber.replaceState({}, '');
1142 // TODO(dbeam): why do we need to focus <extensionoptions> before and
1143 // after its showing animation? Makes very little sense to me.
1144 overlay.setInitialFocus();
1148 * Hides the extension options overlay for the extension with id
1149 * |extensionId|. If there is an overlay showing for a different extension,
1150 * nothing happens.
1151 * @param {string} extensionId ID of the extension to hide.
1152 * @private
1154 hideEmbeddedExtensionOptions_: function(extensionId) {
1155 if (!this.optionsShown_)
1156 return;
1158 var overlay = extensions.ExtensionOptionsOverlay.getInstance();
1159 if (overlay.getExtensionId() == extensionId)
1160 overlay.close();
1164 * Updates the node for the extension.
1165 * @param {!ExtensionInfo} extension The information about the extension to
1166 * update.
1167 * @private
1169 updateExtension_: function(extension) {
1170 var currIndex = this.getIndexOfExtension_(extension.id);
1171 if (currIndex != -1) {
1172 // If there is a current version of the extension, update it with the
1173 // new version.
1174 this.extensions_[currIndex] = extension;
1175 } else {
1176 // If the extension isn't found, push it back and sort. Technically, we
1177 // could optimize by inserting it at the right location, but since this
1178 // only happens on extension install, it's not worth it.
1179 this.extensions_.push(extension);
1180 this.extensions_.sort(compareExtensions);
1183 var node = /** @type {ExtensionFocusRow} */ ($(extension.id));
1184 if (node) {
1185 this.updateNode_(extension, node);
1186 } else {
1187 var nextExt = this.extensions_[this.extensions_.indexOf(extension) + 1];
1188 this.createNode_(extension, nextExt ? $(nextExt.id) : null);
1193 return {
1194 ExtensionList: ExtensionList,
1195 ExtensionListDelegate: ExtensionListDelegate