1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 <include src
="extension_error.js">
7 ///////////////////////////////////////////////////////////////////////////////
11 * Provides an implementation for a single column grid.
13 * @extends {cr.ui.FocusRow}
15 function ExtensionFocusRow() {}
18 * Decorates |focusRow| so that it can be treated as a ExtensionFocusRow.
19 * @param {Element} focusRow The element that has all the columns.
20 * @param {Node} boundary Focus events are ignored outside of this node.
22 ExtensionFocusRow
.decorate = function(focusRow
, boundary
) {
23 focusRow
.__proto__
= ExtensionFocusRow
.prototype;
24 focusRow
.decorate(boundary
);
27 ExtensionFocusRow
.prototype = {
28 __proto__
: cr
.ui
.FocusRow
.prototype,
31 getEquivalentElement: function(element
) {
32 if (this.focusableElements
.indexOf(element
) > -1)
35 // All elements default to another element with the same type.
36 var columnType
= element
.getAttribute('column-type');
37 var equivalent
= this.querySelector('[column-type=' + columnType
+ ']');
39 if (!equivalent
|| !this.canAddElement_(equivalent
)) {
40 var actionLinks
= ['options', 'website', 'launch', 'localReload'];
41 var optionalControls
= ['showButton', 'incognito', 'dev-collectErrors',
42 'allUrls', 'localUrls'];
43 var removeStyleButtons
= ['trash', 'enterprise'];
44 var enableControls
= ['terminatedReload', 'repair', 'enabled'];
46 if (actionLinks
.indexOf(columnType
) > -1)
47 equivalent
= this.getFirstFocusableByType_(actionLinks
);
48 else if (optionalControls
.indexOf(columnType
) > -1)
49 equivalent
= this.getFirstFocusableByType_(optionalControls
);
50 else if (removeStyleButtons
.indexOf(columnType
) > -1)
51 equivalent
= this.getFirstFocusableByType_(removeStyleButtons
);
52 else if (enableControls
.indexOf(columnType
) > -1)
53 equivalent
= this.getFirstFocusableByType_(enableControls
);
56 // Return the first focusable element if no equivalent type is found.
57 return equivalent
|| this.focusableElements
[0];
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
);
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|.
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)
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
104 * @param {string} eventType The type of event to listen to.
105 * @param {function(Event)} handler The function that should be called
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
120 canAddElement_: function(element
) {
121 if (!element
|| element
.disabled
)
124 var developerMode
= $('extension-settings').classList
.contains('dev-mode');
125 if (this.isDeveloperOption_(element
) && !developerMode
)
128 for (var el
= element
; el
; el
= el
.parentElement
) {
137 * Returns true if the element should only be shown in developer mode.
138 * @param {Element} element
142 isDeveloperOption_: function(element
) {
143 return /^dev-/.test(element
.getAttribute('column-type'));
147 cr
.define('extensions', function() {
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
)
165 if (x
.location
== chrome
.developerPrivate
.Location
.UNPACKED
)
167 if (y
.location
== chrome
.developerPrivate
.Location
.UNPACKED
)
171 return compareLocation(a
, b
) ||
172 compare(a
.name
.toLowerCase(), b
.name
.toLowerCase()) ||
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
190 * @extends {HTMLDivElement}
192 function ExtensionList(delegate
) {
193 var div
= document
.createElement('div');
194 div
.__proto__
= ExtensionList
.prototype;
195 div
.initialize(delegate
);
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.
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.
219 uninstallIsShowing_
: false,
222 * Indicates whether a permissions prompt is showing.
225 permissionsPromptIsShowing_
: false,
228 * Necessary to only show the butterbar once.
231 butterbarShown_
: false,
234 * Whether or not incognito mode is available.
237 incognitoAvailable_
: false,
240 * Whether or not the app info dialog is enabled.
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.
261 this.loadFinished
= new Promise(function(resolve
, reject
) {
262 /** @private {function(?)} */
263 this.resolveLoadFinished_
= resolve
;
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();
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
));
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_
);
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
319 * @return {Promise} A promise that is resolved once the extensions data is
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
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_
);
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_();
353 return this.extensionsUpdated_
;
357 * Updates elements that need to be visible in order to update properly.
360 onUpdateFinished_: function() {
361 // Cannot focus or highlight a extension if there are none.
362 if (this.extensions_
.length
== 0)
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.
391 getIndexOfExtension_: function(id
) {
392 for (var i
= 0; i
< this.extensions_
.length
; ++i
) {
393 if (this.extensions_
[i
].id
== id
)
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.
411 showExtensionNodes_: function() {
412 // Any node that is not updated will be removed.
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
);
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
);
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
443 removeNode_: function(node
) {
444 if (node
.contains(document
.activeElement
)) {
446 document
.querySelectorAll('.extension-list-item-wrapper[id]');
447 var index
= Array
.prototype.indexOf
.call(nodes
, node
);
449 var focusableNode
= nodes
[index
+ 1] || nodes
[index
- 1];
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.
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
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
];
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.
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',
530 var butterBar
= row
.querySelector('.butter-bar');
531 var checked
= e
.target
.checked
;
532 if (!this.butterbarShown_
) {
533 butterBar
.hidden
= !checked
||
535 chrome
.developerPrivate
.ExtensionType
.HOSTED_APP
;
536 this.butterbarShown_
= !butterBar
.hidden
;
538 butterBar
.hidden
= true;
540 chrome
.developerPrivate
.updateExtensionConfiguration({
541 extensionId
: extension
.id
,
542 incognitoAccess
: e
.target
.checked
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',
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',
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
);
588 row
.setupColumn('.options-button', 'options', 'click', function(e
) {
589 this.showEmbeddedExtensionOptions_(extension
.id
, false);
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
,
601 this.permissionsPromptIsShowing_
= false;
603 this.permissionsPromptIsShowing_
= true;
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
&&
625 var errors
= newEx
.manifestErrors
.concat(newEx
.runtimeErrors
);
626 extensions
.ExtensionErrorOverlay
.getInstance().setErrorsAndShowOverlay(
627 errors
, extensionId
, newEx
.name
);
630 // The 'Reload' terminated link.
631 row
.setupColumn('.terminated-reload-link', 'terminatedReload', 'click',
633 chrome
.developerPrivate
.reload(extension
.id
, {failQuietly
: true});
636 // The 'Repair' corrupted link.
637 row
.setupColumn('.corrupted-repair-button', 'repair', 'click',
639 chrome
.developerPrivate
.repairExtension(extension
.id
);
642 // The 'Enabled' checkbox.
643 row
.setupColumn('.enable-checkbox input', 'enabled', 'change',
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.
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_
)
667 this.uninstallIsShowing_
= true;
668 chrome
.management
.uninstall(extension
.id
,
669 {showConfirmDialog
: true},
671 // TODO(devlin): What should we do if the uninstall fails?
672 this.uninstallIsShowing_
= false;
674 if (trash
.classList
.contains('mouse-clicked'))
677 if (chrome
.runtime
.lastError
) {
678 // The uninstall failed (e.g. a cancel). Allow the trash to close.
679 trash
.classList
.remove('open');
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.
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',
693 chrome
.developerPrivate
.showPath(extension
.id
);
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
);
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.
715 updateNode_: function(extension
, row
) {
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');
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_
,
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');
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
,
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
,
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
,
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
,
801 item
.href
= extension
.homePage
.url
;
802 item
.textContent
= loadTimeData
.getString(
803 extension
.homePage
.specified
? 'extensionSettingsVisitWebsite' :
804 'extensionSettingsVisitWebStore');
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_(
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
;
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).
842 icon
.className
= 'extension-error-icon'; // Remove other classes.
843 icon
.classList
.add(map
[highestSeverity
].name
);
846 // The 'Reload' terminated link.
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');
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';
886 case ControllerType
.CHILD_CUSTODIAN
:
887 controlledByStr
= 'child-custodian';
889 case ControllerType
.SUPERVISED_USER_CUSTODIAN
:
890 controlledByStr
= 'supervised-user-custodian';
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',
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
,
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.
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
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
];
956 if (!dependentExtension
)
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
);
969 this.updateVisibility_(row
, '.active-views', extension
.views
.length
> 0,
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
) {
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');
994 displayName
= view
.url
;
996 var label
= displayName
+
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,
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,
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
1054 var topScroll
= row
.offsetTop
- $('page-header').offsetHeight
;
1055 var pad
= parseInt(window
.getComputedStyle(row
, null).marginTop
, 10);
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
1071 setText_: function(node
, query
, textContent
) {
1072 node
.querySelector(query
).textContent
= textContent
;
1076 * Updates an element's visibility and calls |shownCallback| if it is
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
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
1097 * @param {boolean} scroll Whether the page should scroll to the extension
1100 showEmbeddedExtensionOptions_: function(extensionId
, scroll
) {
1101 if (this.optionsShown_
)
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
;
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;
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,
1151 * @param {string} extensionId ID of the extension to hide.
1154 hideEmbeddedExtensionOptions_: function(extensionId
) {
1155 if (!this.optionsShown_
)
1158 var overlay
= extensions
.ExtensionOptionsOverlay
.getInstance();
1159 if (overlay
.getExtensionId() == extensionId
)
1164 * Updates the node for the extension.
1165 * @param {!ExtensionInfo} extension The information about the extension to
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
1174 this.extensions_
[currIndex
] = extension
;
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
));
1185 this.updateNode_(extension
, node
);
1187 var nextExt
= this.extensions_
[this.extensions_
.indexOf(extension
) + 1];
1188 this.createNode_(extension
, nextExt
? $(nextExt
.id
) : null);
1194 ExtensionList
: ExtensionList
,
1195 ExtensionListDelegate
: ExtensionListDelegate