1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 <include src="extension_error.js">
7 cr.define('options', function() {
11 * Creates a new list of extensions.
12 * @param {Object=} opt_propertyBag Optional properties.
14 * @extends {cr.ui.div}
16 var ExtensionsList = cr.ui.define('div');
19 * @type {Object.<string, boolean>} A map from extension id to a boolean
20 * indicating whether the incognito warning is showing. This persists
21 * between calls to decorate.
23 var butterBarVisibility = {};
26 * @type {Object.<string, string>} A map from extension id to last reloaded
27 * timestamp. The timestamp is recorded when the user click the 'Reload'
28 * link. It is used to refresh the icon of an unpacked extension.
29 * This persists between calls to decorate.
31 var extensionReloadedTimestamp = {};
33 ExtensionsList.prototype = {
34 __proto__: HTMLDivElement.prototype,
37 decorate: function() {
38 this.textContent = '';
40 this.showExtensionNodes_();
43 getIdQueryParam_: function() {
44 return parseQueryParams(document.location)['id'];
48 * Creates all extension items from scratch.
51 showExtensionNodes_: function() {
52 // Iterate over the extension data and add each item to the list.
53 this.data_.extensions.forEach(this.createNode_, this);
55 var idToHighlight = this.getIdQueryParam_();
56 if (idToHighlight && $(idToHighlight)) {
57 // Scroll offset should be calculated slightly higher than the actual
58 // offset of the element being scrolled to, so that it ends up not all
59 // the way at the top. That way it is clear that there are more elements
60 // above the element being scrolled to.
61 var scrollFudge = 1.2;
62 var scrollTop = $(idToHighlight).offsetTop - scrollFudge *
63 $(idToHighlight).clientHeight;
64 setScrollTopForDocument(document, scrollTop);
67 if (this.data_.extensions.length == 0)
68 this.classList.add('empty-extension-list');
70 this.classList.remove('empty-extension-list');
74 * Synthesizes and initializes an HTML element for the extension metadata
75 * given in |extension|.
76 * @param {Object} extension A dictionary of extension metadata.
79 createNode_: function(extension) {
80 var template = $('template-collection').querySelector(
81 '.extension-list-item-wrapper');
82 var node = template.cloneNode(true);
83 node.id = extension.id;
85 if (!extension.enabled || extension.terminated)
86 node.classList.add('inactive-extension');
88 if (extension.managedInstall ||
89 extension.dependentExtensions.length > 0) {
90 node.classList.add('may-not-modify');
91 node.classList.add('may-not-remove');
92 } else if (extension.suspiciousInstall || extension.corruptInstall) {
93 node.classList.add('may-not-modify');
96 var idToHighlight = this.getIdQueryParam_();
97 if (node.id == idToHighlight)
98 node.classList.add('extension-highlight');
100 var item = node.querySelector('.extension-list-item');
101 // Prevent the image cache of extension icon by using the reloaded
102 // timestamp as a query string. The timestamp is recorded when the user
103 // clicks the 'Reload' link. http://crbug.com/159302.
104 if (extensionReloadedTimestamp[extension.id]) {
105 item.style.backgroundImage =
106 'url(' + extension.icon + '?' +
107 extensionReloadedTimestamp[extension.id] + ')';
109 item.style.backgroundImage = 'url(' + extension.icon + ')';
112 var title = node.querySelector('.extension-title');
113 title.textContent = extension.name;
115 var version = node.querySelector('.extension-version');
116 version.textContent = extension.version;
118 var locationText = node.querySelector('.location-text');
119 locationText.textContent = extension.locationText;
121 var blacklistText = node.querySelector('.blacklist-text');
122 blacklistText.textContent = extension.blacklistText;
124 var description = node.querySelector('.extension-description span');
125 description.textContent = extension.description;
127 // The 'Show Browser Action' button.
128 if (extension.enable_show_button) {
129 var showButton = node.querySelector('.show-button');
130 showButton.addEventListener('click', function(e) {
131 chrome.send('extensionSettingsShowButton', [extension.id]);
133 showButton.hidden = false;
136 // The 'allow in incognito' checkbox.
137 node.querySelector('.incognito-control').hidden =
138 !this.data_.incognitoAvailable;
139 var incognito = node.querySelector('.incognito-control input');
140 incognito.disabled = !extension.incognitoCanBeEnabled;
141 incognito.checked = extension.enabledIncognito;
142 if (!incognito.disabled) {
143 incognito.addEventListener('change', function(e) {
144 var checked = e.target.checked;
145 butterBarVisibility[extension.id] = checked;
146 butterBar.hidden = !checked || extension.is_hosted_app;
147 chrome.send('extensionSettingsEnableIncognito',
148 [extension.id, String(checked)]);
151 var butterBar = node.querySelector('.butter-bar');
152 butterBar.hidden = !butterBarVisibility[extension.id];
154 // The 'collect errors' checkbox. This should only be visible if the
155 // error console is enabled - we can detect this by the existence of the
156 // |errorCollectionEnabled| property.
157 if (extension.wantsErrorCollection) {
158 node.querySelector('.error-collection-control').hidden = false;
159 var errorCollection =
160 node.querySelector('.error-collection-control input');
161 errorCollection.checked = extension.errorCollectionEnabled;
162 errorCollection.addEventListener('change', function(e) {
163 chrome.send('extensionSettingsEnableErrorCollection',
164 [extension.id, String(e.target.checked)]);
168 // The 'allow on all urls' checkbox. This should only be visible if
169 // active script restrictions are enabled. If they are not enabled, no
170 // extensions should want all urls.
171 if (extension.wantsAllUrls) {
172 var allUrls = node.querySelector('.all-urls-control');
173 allUrls.addEventListener('click', function(e) {
174 chrome.send('extensionSettingsAllowOnAllUrls',
175 [extension.id, String(e.target.checked)]);
177 allUrls.querySelector('input').checked = extension.allowAllUrls;
178 allUrls.hidden = false;
181 // The 'allow file:// access' checkbox.
182 if (extension.wantsFileAccess) {
183 var fileAccess = node.querySelector('.file-access-control');
184 fileAccess.addEventListener('click', function(e) {
185 chrome.send('extensionSettingsAllowFileAccess',
186 [extension.id, String(e.target.checked)]);
188 fileAccess.querySelector('input').checked = extension.allowFileAccess;
189 fileAccess.hidden = false;
192 // The 'Options' link.
193 if (extension.enabled && extension.optionsUrl) {
194 var options = node.querySelector('.options-link');
195 options.addEventListener('click', function(e) {
196 chrome.send('extensionSettingsOptions', [extension.id]);
199 options.hidden = false;
202 // The 'Permissions' link.
203 var permissions = node.querySelector('.permissions-link');
204 permissions.addEventListener('click', function(e) {
205 chrome.send('extensionSettingsPermissions', [extension.id]);
209 // The 'View in Web Store/View Web Site' link.
210 if (extension.homepageUrl) {
211 var siteLink = node.querySelector('.site-link');
212 siteLink.href = extension.homepageUrl;
213 siteLink.textContent = loadTimeData.getString(
214 extension.homepageProvided ? 'extensionSettingsVisitWebsite' :
215 'extensionSettingsVisitWebStore');
216 siteLink.hidden = false;
219 if (extension.allow_reload) {
220 // The 'Reload' link.
221 var reload = node.querySelector('.reload-link');
222 reload.addEventListener('click', function(e) {
223 chrome.send('extensionSettingsReload', [extension.id]);
224 extensionReloadedTimestamp[extension.id] = Date.now();
226 reload.hidden = false;
228 if (extension.is_platform_app) {
229 // The 'Launch' link.
230 var launch = node.querySelector('.launch-link');
231 launch.addEventListener('click', function(e) {
232 chrome.send('extensionSettingsLaunch', [extension.id]);
234 launch.hidden = false;
238 if (!extension.terminated) {
239 // The 'Enabled' checkbox.
240 var enable = node.querySelector('.enable-checkbox');
241 enable.hidden = false;
242 var enableCheckboxDisabled = extension.managedInstall ||
243 extension.suspiciousInstall ||
244 extension.corruptInstall ||
245 extension.dependentExtensions.length > 0;
246 enable.querySelector('input').disabled = enableCheckboxDisabled;
248 if (!enableCheckboxDisabled) {
249 enable.addEventListener('click', function(e) {
250 // When e.target is the label instead of the checkbox, it doesn't
251 // have the checked property and the state of the checkbox is
253 var checked = e.target.checked;
254 if (checked == undefined)
255 checked = !e.currentTarget.querySelector('input').checked;
256 chrome.send('extensionSettingsEnable',
257 [extension.id, checked ? 'true' : 'false']);
259 // This may seem counter-intuitive (to not set/clear the checkmark)
260 // but this page will be updated asynchronously if the extension
261 // becomes enabled/disabled. It also might not become enabled or
262 // disabled, because the user might e.g. get prompted when enabling
263 // and choose not to.
268 enable.querySelector('input').checked = extension.enabled;
270 var terminatedReload = node.querySelector('.terminated-reload-link');
271 terminatedReload.hidden = false;
272 terminatedReload.addEventListener('click', function(e) {
273 chrome.send('extensionSettingsReload', [extension.id]);
278 var trashTemplate = $('template-collection').querySelector('.trash');
279 var trash = trashTemplate.cloneNode(true);
280 trash.title = loadTimeData.getString('extensionUninstall');
281 trash.addEventListener('click', function(e) {
282 butterBarVisibility[extension.id] = false;
283 chrome.send('extensionSettingsUninstall', [extension.id]);
285 node.querySelector('.enable-controls').appendChild(trash);
287 // Developer mode ////////////////////////////////////////////////////////
289 // First we have the id.
290 var idLabel = node.querySelector('.extension-id');
291 idLabel.textContent = ' ' + extension.id;
293 // Then the path, if provided by unpacked extension.
294 if (extension.isUnpacked) {
295 var loadPath = node.querySelector('.load-path');
296 loadPath.hidden = false;
297 var pathLink = loadPath.querySelector('a:nth-of-type(1)');
298 pathLink.textContent = ' ' + extension.prettifiedPath;
299 pathLink.addEventListener('click', function(e) {
300 chrome.send('extensionSettingsShowPath', [String(extension.id)]);
305 // Then the 'managed, cannot uninstall/disable' message.
306 if (extension.managedInstall) {
307 node.querySelector('.managed-message').hidden = false;
309 if (extension.suspiciousInstall) {
310 // Then the 'This isn't from the webstore, looks suspicious' message.
311 node.querySelector('.suspicious-install-message').hidden = false;
313 if (extension.corruptInstall) {
314 // Then the 'This is a corrupt extension' message.
315 node.querySelector('.corrupt-install-message').hidden = false;
319 if (extension.dependentExtensions.length > 0) {
320 var dependentMessage =
321 node.querySelector('.dependent-extensions-message');
322 dependentMessage.hidden = false;
323 var dependentList = dependentMessage.querySelector('ul');
324 var dependentTemplate = $('template-collection').querySelector(
325 '.dependent-list-item');
326 extension.dependentExtensions.forEach(function(elem) {
327 var depNode = dependentTemplate.cloneNode(true);
328 depNode.querySelector('.dep-extension-title').textContent = elem.name;
329 depNode.querySelector('.dep-extension-id').textContent = elem.id;
330 dependentList.appendChild(depNode);
334 // Then active views.
335 if (extension.views.length > 0) {
336 var activeViews = node.querySelector('.active-views');
337 activeViews.hidden = false;
338 var link = activeViews.querySelector('a');
340 extension.views.forEach(function(view, i) {
341 var displayName = view.generatedBackgroundPage ?
342 loadTimeData.getString('backgroundPage') : view.path;
343 var label = displayName +
345 ' ' + loadTimeData.getString('viewIncognito') : '') +
346 (view.renderProcessId == -1 ?
347 ' ' + loadTimeData.getString('viewInactive') : '');
348 link.textContent = label;
349 link.addEventListener('click', function(e) {
350 // TODO(estade): remove conversion to string?
351 chrome.send('extensionSettingsInspect', [
352 String(extension.id),
353 String(view.renderProcessId),
354 String(view.renderViewId),
359 if (i < extension.views.length - 1) {
360 link = link.cloneNode(true);
361 activeViews.appendChild(link);
366 // The extension warnings (describing runtime issues).
367 if (extension.warnings) {
368 var panel = node.querySelector('.extension-warnings');
369 panel.hidden = false;
370 var list = panel.querySelector('ul');
371 extension.warnings.forEach(function(warning) {
372 list.appendChild(document.createElement('li')).innerText = warning;
376 // If the ErrorConsole is enabled, we should have manifest and/or runtime
377 // errors. Otherwise, we may have install warnings. We should not have
378 // both ErrorConsole errors and install warnings.
379 if (extension.manifestErrors) {
380 var panel = node.querySelector('.manifest-errors');
381 panel.hidden = false;
382 panel.appendChild(new extensions.ExtensionErrorList(
383 extension.manifestErrors));
385 if (extension.runtimeErrors) {
386 var panel = node.querySelector('.runtime-errors');
387 panel.hidden = false;
388 panel.appendChild(new extensions.ExtensionErrorList(
389 extension.runtimeErrors));
391 if (extension.installWarnings) {
392 var panel = node.querySelector('.install-warnings');
393 panel.hidden = false;
394 var list = panel.querySelector('ul');
395 extension.installWarnings.forEach(function(warning) {
396 var li = document.createElement('li');
397 li.innerText = warning.message;
398 list.appendChild(li);
402 this.appendChild(node);
403 if (location.hash.substr(1) == extension.id) {
404 // Scroll beneath the fixed header so that the extension is not
406 var topScroll = node.offsetTop - $('page-header').offsetHeight;
407 var pad = parseInt(getComputedStyle(node, null).marginTop, 10);
409 topScroll -= pad / 2;
410 setScrollTopForDocument(document, topScroll);
416 ExtensionsList: ExtensionsList