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];
60 /** Updates the list of focusable elements. */
61 updateFocusableElements: function() {
62 this.focusableElements
.length
= 0;
64 var focusableCandidates
= this.querySelectorAll('[column-type]');
65 for (var i
= 0; i
< focusableCandidates
.length
; ++i
) {
66 var element
= focusableCandidates
[i
];
67 if (this.canAddElement_(element
))
68 this.addFocusableElement(element
);
73 * Get the first focusable element that matches a list of types.
74 * @param {Array<string>} types An array of types to match from.
75 * @return {?Element} Return the first element that matches a type in |types|.
78 getFirstFocusableByType_: function(types
) {
79 for (var i
= 0; i
< this.focusableElements
.length
; ++i
) {
80 var element
= this.focusableElements
[i
];
81 if (types
.indexOf(element
.getAttribute('column-type')) > -1)
88 * Setup a typical column in the ExtensionFocusRow. A column can be any
89 * element and should have an action when clicked/toggled. This function
90 * adds a listener and a handler for an event. Also adds the "column-type"
91 * attribute to make the element focusable in |updateFocusableElements|.
92 * @param {string} query A query to select the element to set up.
93 * @param {string} columnType A tag used to identify the column when
95 * @param {string} eventType The type of event to listen to.
96 * @param {function(Event)} handler The function that should be called
100 setupColumn: function(query
, columnType
, eventType
, handler
) {
101 var element
= this.querySelector(query
);
102 element
.addEventListener(eventType
, handler
);
103 element
.setAttribute('column-type', columnType
);
107 * @param {Element} element
111 canAddElement_: function(element
) {
112 if (!element
|| element
.disabled
)
115 var developerMode
= $('extension-settings').classList
.contains('dev-mode');
116 if (this.isDeveloperOption_(element
) && !developerMode
)
119 for (var el
= element
; el
; el
= el
.parentElement
) {
128 * Returns true if the element should only be shown in developer mode.
129 * @param {Element} element
133 isDeveloperOption_: function(element
) {
134 return /^dev-/.test(element
.getAttribute('column-type'));
138 cr
.define('extensions', function() {
142 * Creates a new list of extensions.
144 * @extends {HTMLDivElement}
146 function ExtensionList() {
147 var div
= document
.createElement('div');
148 div
.__proto__
= ExtensionList
.prototype;
149 /** @private {!Array<ExtensionInfo>} */
150 div
.extensions_
= [];
155 * @type {Object<string, number>} A map from extension id to last reloaded
156 * timestamp. The timestamp is recorded when the user click the 'Reload'
157 * link. It is used to refresh the icon of an unpacked extension.
158 * This persists between calls to decorate.
160 var extensionReloadedTimestamp
= {};
162 ExtensionList
.prototype = {
163 __proto__
: HTMLDivElement
.prototype,
166 * Indicates whether an embedded options page that was navigated to through
167 * the '?options=' URL query has been shown to the user. This is necessary
168 * to prevent showExtensionNodes_ from opening the options more than once.
172 optionsShown_
: false,
174 /** @private {!cr.ui.FocusGrid} */
175 focusGrid_
: new cr
.ui
.FocusGrid(),
178 * Indicates whether an uninstall dialog is being shown to prevent multiple
179 * dialogs from being displayed.
182 uninstallIsShowing_
: false,
185 * Indicates whether a permissions prompt is showing.
188 permissionsPromptIsShowing_
: false,
191 * Necessary to only show the butterbar once.
194 butterbarShown_
: false,
197 * Whether or not incognito mode is available.
200 incognitoAvailable_
: false,
203 * Whether or not the app info dialog is enabled.
206 enableAppInfoDialog_
: false,
209 * Updates the extensions on the page.
210 * @param {boolean} incognitoAvailable Whether or not incognito is allowed.
211 * @param {boolean} enableAppInfoDialog Whether or not the app info dialog
213 * @return {Promise} A promise that is resolved once the extensions data is
216 updateExtensionsData: function(incognitoAvailable
, enableAppInfoDialog
) {
217 // If we start to need more information about the extension configuration,
218 // consider passing in the full object from the ExtensionSettings.
219 this.incognitoAvailable_
= incognitoAvailable
;
220 this.enableAppInfoDialog_
= enableAppInfoDialog
;
221 /** @private {Promise} */
222 this.extensionsUpdated_
= new Promise(function(resolve
, reject
) {
223 chrome
.developerPrivate
.getExtensionsInfo(
224 {includeDisabled
: true, includeTerminated
: true},
225 function(extensions
) {
226 // Sort in order of unpacked vs. packed, followed by name, followed by
228 extensions
.sort(function(a
, b
) {
229 function compare(x
, y
) {
230 return x
< y
? -1 : (x
> y
? 1 : 0);
232 function compareLocation(x
, y
) {
233 return x
.location
== chrome
.developerPrivate
.Location
.UNPACKED
?
234 -1 : (x
.location
== y
.location
? 0 : 1);
236 return compareLocation(a
, b
) ||
237 compare(a
.name
.toLowerCase(), b
.name
.toLowerCase()) ||
240 this.extensions_
= extensions
;
241 this.showExtensionNodes_();
245 return this.extensionsUpdated_
;
248 /** @return {number} The number of extensions being displayed. */
249 getNumExtensions: function() {
250 return this.extensions_
.length
;
253 getIdQueryParam_: function() {
254 return parseQueryParams(document
.location
)['id'];
257 getOptionsQueryParam_: function() {
258 return parseQueryParams(document
.location
)['options'];
262 * Creates or updates all extension items from scratch.
265 showExtensionNodes_: function() {
266 // Remove the rows from |focusGrid_| without destroying them.
267 this.focusGrid_
.rows
.length
= 0;
269 // Any node that is not updated will be removed.
272 // Iterate over the extension data and add each item to the list.
273 this.extensions_
.forEach(function(extension
, i
) {
274 var nextExt
= this.extensions_
[i
+ 1];
275 var node
= $(extension
.id
);
276 seenIds
.push(extension
.id
);
279 this.updateNode_(extension
, node
);
281 this.createNode_(extension
, nextExt
? $(nextExt
.id
) : null);
284 // Remove extensions that are no longer installed.
285 var nodes
= document
.querySelectorAll('.extension-list-item-wrapper[id]');
286 for (var i
= 0; i
< nodes
.length
; ++i
) {
288 if (seenIds
.indexOf(node
.id
) < 0) {
289 if (node
.contains(document
.activeElement
)) {
290 var focusableNode
= nodes
[i
+ 1] || nodes
[i
- 1];
292 focusableNode
.getEquivalentElement(
293 document
.activeElement
).focus();
297 node
.parentElement
.removeChild(node
);
298 // Unregister the removed node from events.
299 assertInstanceof(node
, ExtensionFocusRow
).destroy();
303 var idToHighlight
= this.getIdQueryParam_();
304 if (idToHighlight
&& $(idToHighlight
))
305 this.scrollToNode_(idToHighlight
);
307 var idToOpenOptions
= this.getOptionsQueryParam_();
308 if (idToOpenOptions
&& $(idToOpenOptions
))
309 this.showEmbeddedExtensionOptions_(idToOpenOptions
, true);
312 /** Updates each row's focusable elements without rebuilding the grid. */
313 updateFocusableElements: function() {
314 var rows
= document
.querySelectorAll('.extension-list-item-wrapper[id]');
315 for (var i
= 0; i
< rows
.length
; ++i
) {
316 assertInstanceof(rows
[i
], ExtensionFocusRow
).updateFocusableElements();
321 * Scrolls the page down to the extension node with the given id.
322 * @param {string} extensionId The id of the extension to scroll to.
325 scrollToNode_: function(extensionId
) {
326 // Scroll offset should be calculated slightly higher than the actual
327 // offset of the element being scrolled to, so that it ends up not all
328 // the way at the top. That way it is clear that there are more elements
329 // above the element being scrolled to.
330 var scrollFudge
= 1.2;
331 var scrollTop
= $(extensionId
).offsetTop
- scrollFudge
*
332 $(extensionId
).clientHeight
;
333 setScrollTopForDocument(document
, scrollTop
);
337 * Synthesizes and initializes an HTML element for the extension metadata
338 * given in |extension|.
339 * @param {!ExtensionInfo} extension A dictionary of extension metadata.
340 * @param {?Element} nextNode |node| should be inserted before |nextNode|.
341 * |node| will be appended to the end if |nextNode| is null.
344 createNode_: function(extension
, nextNode
) {
345 var template
= $('template-collection').querySelector(
346 '.extension-list-item-wrapper');
347 var node
= template
.cloneNode(true);
348 ExtensionFocusRow
.decorate(node
, $('extension-settings-list'));
350 var row
= assertInstanceof(node
, ExtensionFocusRow
);
351 row
.id
= extension
.id
;
353 // The 'Show Browser Action' button.
354 row
.setupColumn('.show-button', 'showButton', 'click', function(e
) {
355 chrome
.developerPrivate
.updateExtensionConfiguration({
356 extensionId
: extension
.id
,
357 showActionButton
: true
361 // The 'allow in incognito' checkbox.
362 row
.setupColumn('.incognito-control input', 'incognito', 'change',
364 var butterBar
= row
.querySelector('.butter-bar');
365 var checked
= e
.target
.checked
;
366 if (!this.butterbarShown_
) {
367 butterBar
.hidden
= !checked
||
369 chrome
.developerPrivate
.ExtensionType
.HOSTED_APP
;
370 this.butterbarShown_
= !butterBar
.hidden
;
372 butterBar
.hidden
= true;
374 chrome
.developerPrivate
.updateExtensionConfiguration({
375 extensionId
: extension
.id
,
376 incognitoAccess
: e
.target
.checked
380 // The 'collect errors' checkbox. This should only be visible if the
381 // error console is enabled - we can detect this by the existence of the
382 // |errorCollectionEnabled| property.
383 row
.setupColumn('.error-collection-control input', 'dev-collectErrors',
384 'change', function(e
) {
385 chrome
.developerPrivate
.updateExtensionConfiguration({
386 extensionId
: extension
.id
,
387 errorCollection
: e
.target
.checked
391 // The 'allow on all urls' checkbox. This should only be visible if
392 // active script restrictions are enabled. If they are not enabled, no
393 // extensions should want all urls.
394 row
.setupColumn('.all-urls-control input', 'allUrls', 'click',
396 chrome
.developerPrivate
.updateExtensionConfiguration({
397 extensionId
: extension
.id
,
398 runOnAllUrls
: e
.target
.checked
402 // The 'allow file:// access' checkbox.
403 row
.setupColumn('.file-access-control input', 'localUrls', 'click',
405 chrome
.developerPrivate
.updateExtensionConfiguration({
406 extensionId
: extension
.id
,
407 fileAccess
: e
.target
.checked
411 // The 'Options' button or link, depending on its behaviour.
412 // Set an href to get the correct mouse-over appearance (link,
413 // footer) - but the actual link opening is done through chrome.send
414 // with a preventDefault().
415 row
.querySelector('.options-link').href
=
416 extension
.optionsPage
? extension
.optionsPage
.url
: '';
417 row
.setupColumn('.options-link', 'options', 'click', function(e
) {
418 chrome
.send('extensionSettingsOptions', [extension
.id
]);
422 row
.setupColumn('.options-button', 'options', 'click', function(e
) {
423 this.showEmbeddedExtensionOptions_(extension
.id
, false);
427 // The 'View in Web Store/View Web Site' link.
428 row
.querySelector('.site-link').setAttribute('column-type', 'website');
430 // The 'Permissions' link.
431 row
.setupColumn('.permissions-link', 'details', 'click', function(e
) {
432 if (!this.permissionsPromptIsShowing_
) {
433 chrome
.developerPrivate
.showPermissionsDialog(extension
.id
,
435 this.permissionsPromptIsShowing_
= false;
437 this.permissionsPromptIsShowing_
= true;
442 // The 'Reload' link.
443 row
.setupColumn('.reload-link', 'localReload', 'click', function(e
) {
444 chrome
.developerPrivate
.reload(extension
.id
, {failQuietly
: true});
445 extensionReloadedTimestamp
[extension
.id
] = Date
.now();
448 // The 'Launch' link.
449 row
.setupColumn('.launch-link', 'launch', 'click', function(e
) {
450 chrome
.send('extensionSettingsLaunch', [extension
.id
]);
453 // The 'Reload' terminated link.
454 row
.setupColumn('.terminated-reload-link', 'terminatedReload', 'click',
456 chrome
.developerPrivate
.reload(extension
.id
, {failQuietly
: true});
459 // The 'Repair' corrupted link.
460 row
.setupColumn('.corrupted-repair-button', 'repair', 'click',
462 chrome
.send('extensionSettingsRepair', [extension
.id
]);
465 // The 'Enabled' checkbox.
466 row
.setupColumn('.enable-checkbox input', 'enabled', 'change',
468 var checked
= e
.target
.checked
;
469 // TODO(devlin): What should we do if this fails?
470 chrome
.management
.setEnabled(extension
.id
, checked
);
472 // This may seem counter-intuitive (to not set/clear the checkmark)
473 // but this page will be updated asynchronously if the extension
474 // becomes enabled/disabled. It also might not become enabled or
475 // disabled, because the user might e.g. get prompted when enabling
476 // and choose not to.
481 var trashTemplate
= $('template-collection').querySelector('.trash');
482 var trash
= trashTemplate
.cloneNode(true);
483 trash
.title
= loadTimeData
.getString('extensionUninstall');
484 trash
.hidden
= !extension
.userMayModify
;
485 trash
.setAttribute('column-type', 'trash');
486 trash
.addEventListener('click', function(e
) {
487 trash
.classList
.add('open');
488 trash
.classList
.toggle('mouse-clicked', e
.detail
> 0);
489 if (this.uninstallIsShowing_
)
491 this.uninstallIsShowing_
= true;
492 chrome
.management
.uninstall(extension
.id
,
493 {showConfirmDialog
: true},
495 // TODO(devlin): What should we do if the uninstall fails?
496 this.uninstallIsShowing_
= false;
498 if (trash
.classList
.contains('mouse-clicked'))
501 if (chrome
.runtime
.lastError
) {
502 // The uninstall failed (e.g. a cancel). Allow the trash to close.
503 trash
.classList
.remove('open');
505 // Leave the trash open if the uninstall succeded. Otherwise it can
506 // half-close right before it's removed when the DOM is modified.
510 row
.querySelector('.enable-controls').appendChild(trash
);
512 // Developer mode ////////////////////////////////////////////////////////
514 // The path, if provided by unpacked extension.
515 row
.setupColumn('.load-path a:first-of-type', 'dev-loadPath', 'click',
517 chrome
.send('extensionSettingsShowPath', [String(extension
.id
)]);
521 // Maintain the order that nodes should be in when creating as well as
522 // when adding only one new row.
523 this.insertBefore(row
, nextNode
);
524 this.updateNode_(extension
, row
);
528 * Updates an HTML element for the extension metadata given in |extension|.
529 * @param {!ExtensionInfo} extension A dictionary of extension metadata.
530 * @param {!ExtensionFocusRow} row The node that is being updated.
533 updateNode_: function(extension
, row
) {
535 extension
.state
== chrome
.developerPrivate
.ExtensionState
.ENABLED
;
536 row
.classList
.toggle('inactive-extension', !isActive
);
538 // Hack to keep the closure compiler happy about |remove|.
539 // TODO(hcarmona): Remove this hack when the closure compiler is updated.
540 var node
= /** @type {Element} */ (row
);
541 node
.classList
.remove('policy-controlled', 'may-not-modify',
544 if (!extension
.userMayModify
) {
545 classes
.push('policy-controlled', 'may-not-modify');
546 } else if (extension
.dependentExtensions
.length
> 0) {
547 classes
.push('may-not-remove', 'may-not-modify');
548 } else if (extension
.mustRemainInstalled
) {
549 classes
.push('may-not-remove');
550 } else if (extension
.disableReasons
.suspiciousInstall
||
551 extension
.disableReasons
.corruptInstall
||
552 extension
.disableReasons
.updateRequired
) {
553 classes
.push('may-not-modify');
555 row
.classList
.add
.apply(row
.classList
, classes
);
557 row
.classList
.toggle('extension-highlight',
558 row
.id
== this.getIdQueryParam_());
560 var item
= row
.querySelector('.extension-list-item');
561 // Prevent the image cache of extension icon by using the reloaded
562 // timestamp as a query string. The timestamp is recorded when the user
563 // clicks the 'Reload' link. http://crbug.com/159302.
564 if (extensionReloadedTimestamp
[extension
.id
]) {
565 item
.style
.backgroundImage
=
566 'url(' + extension
.iconUrl
+ '?' +
567 extensionReloadedTimestamp
[extension
.id
] + ')';
569 item
.style
.backgroundImage
= 'url(' + extension
.iconUrl
+ ')';
572 this.setText_(row
, '.extension-title', extension
.name
);
573 this.setText_(row
, '.extension-version', extension
.version
);
574 this.setText_(row
, '.location-text', extension
.locationText
|| '');
575 this.setText_(row
, '.blacklist-text', extension
.blacklistText
|| '');
576 this.setText_(row
, '.extension-description', extension
.description
);
578 // The 'Show Browser Action' button.
579 this.updateVisibility_(row
, '.show-button',
580 isActive
&& extension
.actionButtonHidden
);
582 // The 'allow in incognito' checkbox.
583 this.updateVisibility_(row
, '.incognito-control',
584 isActive
&& this.incognitoAvailable_
,
586 var incognito
= item
.querySelector('input');
587 incognito
.disabled
= !extension
.incognitoAccess
.isEnabled
;
588 incognito
.checked
= extension
.incognitoAccess
.isActive
;
591 // Hide butterBar if incognito is not enabled for the extension.
592 var butterBar
= row
.querySelector('.butter-bar');
594 butterBar
.hidden
|| !extension
.incognitoAccess
.isEnabled
;
596 // The 'collect errors' checkbox. This should only be visible if the
597 // error console is enabled - we can detect this by the existence of the
598 // |errorCollectionEnabled| property.
599 this.updateVisibility_(
600 row
, '.error-collection-control',
601 isActive
&& extension
.errorCollection
.isEnabled
,
603 item
.querySelector('input').checked
=
604 extension
.errorCollection
.isActive
;
607 // The 'allow on all urls' checkbox. This should only be visible if
608 // active script restrictions are enabled. If they are not enabled, no
609 // extensions should want all urls.
610 this.updateVisibility_(
611 row
, '.all-urls-control',
612 isActive
&& extension
.runOnAllUrls
.isEnabled
,
614 item
.querySelector('input').checked
= extension
.runOnAllUrls
.isActive
;
617 // The 'allow file:// access' checkbox.
618 this.updateVisibility_(row
, '.file-access-control',
619 isActive
&& extension
.fileAccess
.isEnabled
,
621 item
.querySelector('input').checked
= extension
.fileAccess
.isActive
;
624 // The 'Options' button or link, depending on its behaviour.
625 var optionsEnabled
= isActive
&& !!extension
.optionsPage
;
626 this.updateVisibility_(row
, '.options-link', optionsEnabled
&&
627 extension
.optionsPage
.openInTab
);
628 this.updateVisibility_(row
, '.options-button', optionsEnabled
&&
629 !extension
.optionsPage
.openInTab
);
631 // The 'View in Web Store/View Web Site' link.
632 var siteLinkEnabled
= !!extension
.homepageUrl
&&
633 !this.enableAppInfoDialog_
;
634 this.updateVisibility_(row
, '.site-link', siteLinkEnabled
,
636 item
.href
= extension
.homepageUrl
;
637 item
.textContent
= loadTimeData
.getString(
638 extension
.homepageProvided
? 'extensionSettingsVisitWebsite' :
639 'extensionSettingsVisitWebStore');
643 extension
.location
== chrome
.developerPrivate
.Location
.UNPACKED
;
644 // The 'Reload' link.
645 this.updateVisibility_(row
, '.reload-link', isUnpacked
);
647 // The 'Launch' link.
648 this.updateVisibility_(
650 isUnpacked
&& extension
.type
==
651 chrome
.developerPrivate
.ExtensionType
.PLATFORM_APP
);
653 // The 'Reload' terminated link.
655 extension
.state
== chrome
.developerPrivate
.ExtensionState
.TERMINATED
;
656 this.updateVisibility_(row
, '.terminated-reload-link', isTerminated
);
658 // The 'Repair' corrupted link.
659 var canRepair
= !isTerminated
&&
660 extension
.disableReasons
.corruptInstall
&&
661 extension
.location
==
662 chrome
.developerPrivate
.Location
.FROM_STORE
;
663 this.updateVisibility_(row
, '.corrupted-repair-button', canRepair
);
665 // The 'Enabled' checkbox.
666 var isOK
= !isTerminated
&& !canRepair
;
667 this.updateVisibility_(row
, '.enable-checkbox', isOK
, function(item
) {
668 var enableCheckboxDisabled
=
669 !extension
.userMayModify
||
670 extension
.disableReasons
.suspiciousInstall
||
671 extension
.disableReasons
.corruptInstall
||
672 extension
.disableReasons
.updateRequired
||
673 extension
.installedByCustodian
||
674 extension
.dependentExtensions
.length
> 0;
675 item
.querySelector('input').disabled
= enableCheckboxDisabled
;
676 item
.querySelector('input').checked
= isActive
;
679 // Button for extensions controlled by policy.
680 var controlNode
= row
.querySelector('.enable-controls');
682 controlNode
.querySelector('.controlled-extension-indicator');
683 var needsIndicator
= isOK
&&
684 !extension
.userMayModify
&&
685 extension
.policyText
;
686 // TODO(treib): If userMayModify is false, but policyText is empty, that
687 // indicates this extension is controlled by something else than
688 // enterprise policy (such as the profile being supervised). For now, just
689 // don't show the indicator in this case. We should really handle this
690 // better though (ie use a different text and icon).
692 if (needsIndicator
&& !indicator
) {
693 indicator
= new cr
.ui
.ControlledIndicator();
694 indicator
.classList
.add('controlled-extension-indicator');
695 indicator
.setAttribute('controlled-by', 'policy');
696 var textPolicy
= extension
.policyText
|| '';
697 indicator
.setAttribute('textpolicy', textPolicy
);
698 indicator
.image
.setAttribute('aria-label', textPolicy
);
699 controlNode
.appendChild(indicator
);
700 indicator
.querySelector('div').setAttribute('column-type',
702 } else if (!needsIndicator
&& indicator
) {
703 controlNode
.removeChild(indicator
);
706 // Developer mode ////////////////////////////////////////////////////////
708 // First we have the id.
709 var idLabel
= row
.querySelector('.extension-id');
710 idLabel
.textContent
= ' ' + extension
.id
;
712 // Then the path, if provided by unpacked extension.
713 this.updateVisibility_(row
, '.load-path', isUnpacked
,
715 item
.querySelector('a:first-of-type').textContent
=
716 ' ' + extension
.prettifiedPath
;
719 // Then the 'managed, cannot uninstall/disable' message.
720 // We would like to hide managed installed message since this
721 // extension is disabled.
723 !extension
.userMayModify
|| extension
.mustRemainInstalled
;
724 this.updateVisibility_(row
, '.managed-message', isRequired
&&
725 !extension
.disableReasons
.updateRequired
);
727 // Then the 'This isn't from the webstore, looks suspicious' message.
728 this.updateVisibility_(row
, '.suspicious-install-message', !isRequired
&&
729 extension
.disableReasons
.suspiciousInstall
);
731 // Then the 'This is a corrupt extension' message.
732 this.updateVisibility_(row
, '.corrupt-install-message', !isRequired
&&
733 extension
.disableReasons
.corruptInstall
);
735 // Then the 'An update required by enterprise policy' message. Note that
736 // a force-installed extension might be disabled due to being outdated
738 this.updateVisibility_(row
, '.update-required-message',
739 extension
.disableReasons
.updateRequired
);
741 // The 'following extensions depend on this extension' list.
742 var hasDependents
= extension
.dependentExtensions
.length
> 0;
743 row
.classList
.toggle('developer-extras', hasDependents
);
744 this.updateVisibility_(row
, '.dependent-extensions-message',
745 hasDependents
, function(item
) {
746 var dependentList
= item
.querySelector('ul');
747 dependentList
.textContent
= '';
748 var dependentTemplate
= $('template-collection').querySelector(
749 '.dependent-list-item');
750 extension
.dependentExtensions
.forEach(function(dependentId
) {
751 var dependentExtension
= null;
752 for (var i
= 0; i
< this.extensions_
.length
; ++i
) {
753 if (this.extensions_
[i
].id
== dependentId
) {
754 dependentExtension
= this.extensions_
[i
];
758 if (!dependentExtension
)
761 var depNode
= dependentTemplate
.cloneNode(true);
762 depNode
.querySelector('.dep-extension-title').textContent
=
763 dependentExtension
.name
;
764 depNode
.querySelector('.dep-extension-id').textContent
=
765 dependentExtension
.id
;
766 dependentList
.appendChild(depNode
);
771 this.updateVisibility_(row
, '.active-views', extension
.views
.length
> 0,
773 var link
= item
.querySelector('a');
775 // Link needs to be an only child before the list is updated.
776 while (link
.nextElementSibling
)
777 item
.removeChild(link
.nextElementSibling
);
779 // Link needs to be cleaned up if it was used before.
780 link
.textContent
= '';
781 if (link
.clickHandler
)
782 link
.removeEventListener('click', link
.clickHandler
);
784 extension
.views
.forEach(function(view
, i
) {
785 if (view
.type
== chrome
.developerPrivate
.ViewType
.EXTENSION_DIALOG
||
786 view
.type
== chrome
.developerPrivate
.ViewType
.EXTENSION_POPUP
) {
790 if (view
.url
.indexOf('chrome-extension://') == 0) {
791 var pathOffset
= 'chrome-extension://'.length
+ 32 + 1;
792 displayName
= view
.url
.substring(pathOffset
);
793 if (displayName
== '_generated_background_page.html')
794 displayName
= loadTimeData
.getString('backgroundPage');
796 displayName
= view
.url
;
798 var label
= displayName
+
800 ' ' + loadTimeData
.getString('viewIncognito') : '') +
801 (view
.renderProcessId
== -1 ?
802 ' ' + loadTimeData
.getString('viewInactive') : '');
803 link
.textContent
= label
;
804 link
.clickHandler = function(e
) {
805 chrome
.developerPrivate
.openDevTools({
806 extensionId
: extension
.id
,
807 renderProcessId
: view
.renderProcessId
,
808 renderViewId
: view
.renderViewId
,
809 incognito
: view
.incognito
812 link
.addEventListener('click', link
.clickHandler
);
814 if (i
< extension
.views
.length
- 1) {
815 link
= link
.cloneNode(true);
816 item
.appendChild(link
);
820 var allLinks
= item
.querySelectorAll('a');
821 for (var i
= 0; i
< allLinks
.length
; ++i
) {
822 allLinks
[i
].setAttribute('column-type', 'dev-activeViews' + i
);
826 // The extension warnings (describing runtime issues).
827 this.updateVisibility_(row
, '.extension-warnings',
828 extension
.runtimeWarnings
.length
> 0,
830 var warningList
= item
.querySelector('ul');
831 warningList
.textContent
= '';
832 extension
.runtimeWarnings
.forEach(function(warning
) {
833 var li
= document
.createElement('li');
834 warningList
.appendChild(li
).innerText
= warning
;
838 // If the ErrorConsole is enabled, we should have manifest and/or runtime
839 // errors. Otherwise, we may have install warnings. We should not have
840 // both ErrorConsole errors and install warnings.
842 this.updateErrors_(row
.querySelector('.manifest-errors'),
843 'dev-manifestErrors', extension
.manifestErrors
);
844 this.updateErrors_(row
.querySelector('.runtime-errors'),
845 'dev-runtimeErrors', extension
.runtimeErrors
);
848 this.updateVisibility_(row
, '.install-warnings',
849 extension
.installWarnings
.length
> 0,
851 var installWarningList
= item
.querySelector('ul');
852 installWarningList
.textContent
= '';
853 if (extension
.installWarnings
) {
854 extension
.installWarnings
.forEach(function(warning
) {
855 var li
= document
.createElement('li');
856 li
.innerText
= warning
;
857 installWarningList
.appendChild(li
);
862 if (location
.hash
.substr(1) == extension
.id
) {
863 // Scroll beneath the fixed header so that the extension is not
865 var topScroll
= row
.offsetTop
- $('page-header').offsetHeight
;
866 var pad
= parseInt(window
.getComputedStyle(row
, null).marginTop
, 10);
868 topScroll
-= pad
/ 2;
869 setScrollTopForDocument(document
, topScroll
);
872 row
.updateFocusableElements();
873 this.focusGrid_
.addRow(row
);
877 * Updates an element's textContent.
878 * @param {Element} node Ancestor of the element specified by |query|.
879 * @param {string} query A query to select an element in |node|.
880 * @param {string} textContent
883 setText_: function(node
, query
, textContent
) {
884 node
.querySelector(query
).textContent
= textContent
;
888 * Updates an element's visibility and calls |shownCallback| if it is
890 * @param {Element} node Ancestor of the element specified by |query|.
891 * @param {string} query A query to select an element in |node|.
892 * @param {boolean} visible Whether the element should be visible or not.
893 * @param {function(Element)=} opt_shownCallback Callback if the element is
894 * visible. The element passed in will be the element specified by
898 updateVisibility_: function(node
, query
, visible
, opt_shownCallback
) {
899 var item
= assert(node
.querySelector(query
));
900 item
.hidden
= !visible
;
901 if (visible
&& opt_shownCallback
)
902 opt_shownCallback(item
);
906 * Updates an element to show a list of errors.
907 * @param {Element} panel An element to hold the errors.
908 * @param {string} columnType A tag used to identify the column when
910 * @param {Array<RuntimeError|ManifestError>|undefined} errors The errors
914 updateErrors_: function(panel
, columnType
, errors
) {
915 // TODO(hcarmona): Look into updating the ExtensionErrorList rather than
916 // rebuilding it every time.
917 panel
.hidden
= !errors
|| errors
.length
== 0;
918 panel
.textContent
= '';
924 new extensions
.ExtensionErrorList(assertInstanceof(errors
, Array
));
926 panel
.appendChild(errorList
);
928 var list
= errorList
.getErrorListElement();
930 list
.setAttribute('column-type', columnType
+ 'list');
932 var button
= errorList
.getToggleElement();
934 button
.setAttribute('column-type', columnType
+ 'button');
938 * Opens the extension options overlay for the extension with the given id.
939 * @param {string} extensionId The id of extension whose options page should
941 * @param {boolean} scroll Whether the page should scroll to the extension
944 showEmbeddedExtensionOptions_: function(extensionId
, scroll
) {
945 if (this.optionsShown_
)
948 // Get the extension from the given id.
949 var extension
= this.extensions_
.filter(function(extension
) {
950 return extension
.state
==
951 chrome
.developerPrivate
.ExtensionState
.ENABLED
&&
952 extension
.id
== extensionId
;
959 this.scrollToNode_(extensionId
);
961 // Add the options query string. Corner case: the 'options' query string
962 // will clobber the 'id' query string if the options link is clicked when
963 // 'id' is in the URL, or if both query strings are in the URL.
964 uber
.replaceState({}, '?options=' + extensionId
);
966 var overlay
= extensions
.ExtensionOptionsOverlay
.getInstance();
967 var shownCallback = function() {
968 // This overlay doesn't get focused automatically as <extensionoptions>
969 // is created after the overlay is shown.
970 if (cr
.ui
.FocusOutlineManager
.forDocument(document
).visible
)
971 overlay
.setInitialFocus();
973 overlay
.setExtensionAndShowOverlay(extensionId
, extension
.name
,
974 extension
.iconUrl
, shownCallback
);
975 this.optionsShown_
= true;
978 $('overlay').addEventListener('cancelOverlay', function f() {
979 self
.optionsShown_
= false;
980 $('overlay').removeEventListener('cancelOverlay', f
);
983 // TODO(dbeam): why do we need to focus <extensionoptions> before and
984 // after its showing animation? Makes very little sense to me.
985 overlay
.setInitialFocus();
990 ExtensionList
: ExtensionList